MediaWiki master
MagicWordArray.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Parser;
8
9use LogicException;
11
21
23 public $names = [];
24 private MagicWordFactory $factory;
25
27 private $hash;
28
30 private $baseRegex;
31
33 private $regex;
34
39 public function __construct( $names = [], ?MagicWordFactory $factory = null ) {
40 $this->names = $names;
41 $this->factory = $factory ?? MediaWikiServices::getInstance()->getMagicWordFactory();
42 }
43
49 public function add( $name ): void {
50 $this->names[] = $name;
51 $this->hash = $this->baseRegex = $this->regex = null;
52 }
53
59 public function getHash(): array {
60 if ( $this->hash === null ) {
61 $this->hash = [ 0 => [], 1 => [] ];
62 foreach ( $this->names as $name ) {
63 $magic = $this->factory->get( $name );
64 $case = intval( $magic->isCaseSensitive() );
65 foreach ( $magic->getSynonyms() as $syn ) {
66 if ( !$case ) {
67 $syn = $this->factory->getContentLanguage()->lc( $syn );
68 }
69 $this->hash[$case][$syn] = $name;
70 }
71 }
72 }
73 return $this->hash;
74 }
75
87 public function getBaseRegex( bool $capture = true, string $delimiter = '/' ): array {
88 if ( $capture && $delimiter === '/' && $this->baseRegex !== null ) {
89 return $this->baseRegex;
90 }
91 $regex = [ 0 => [], 1 => [] ];
92 foreach ( $this->names as $name ) {
93 $magic = $this->factory->get( $name );
94 $case = $magic->isCaseSensitive() ? 1 : 0;
95 $quotedSynonyms = [];
96 foreach ( $magic->getSynonyms() as $syn ) {
97 $quotedSynonyms[] = preg_quote( $syn, $delimiter );
98 }
99 $quotedSynonyms = implode( '|', $quotedSynonyms );
100 if ( $capture ) {
101 if ( substr_count( $quotedSynonyms, '\$1' ) > 1 ) {
102 // We know synonyms are exclusive to each other, and so are their parameter
103 // value placeholders that will become (.*?) subpatterns later.
104 $quotedSynonyms = "(?|$quotedSynonyms)";
105 }
106 // Group name must start with a non-digit in PCRE 8.34+
107 $quotedSynonyms = "(?P<_$name>$quotedSynonyms)";
108 }
109 $regex[$case][] = $quotedSynonyms;
110 }
111 '@phan-var array<int,string[]> $regex';
112 foreach ( $regex as $case => &$re ) {
113 $re = count( $re ) ? implode( '|', $re ) : '(?!)';
114 if ( !$case ) {
115 $re = "(?i:{$re})";
116 }
117 }
118 '@phan-var array<int,string> $regex';
119
120 if ( $capture && $delimiter === '/' ) {
121 $this->baseRegex = $regex;
122 }
123 return $regex;
124 }
125
131 private function getRegex(): array {
132 if ( $this->regex === null ) {
133 $this->regex = [];
134 $base = $this->getBaseRegex( true, '/' );
135 foreach ( $base as $case => $re ) {
136 $this->regex[$case] = "/$re/JS";
137 }
138 // As a performance optimization, turn on unicode mode only for
139 // case-insensitive matching.
140 $this->regex[0] .= 'u';
141 }
142 return $this->regex;
143 }
144
150 private function getRegexStart(): array {
151 $newRegex = [];
152 $base = $this->getBaseRegex( true, '/' );
153 foreach ( $base as $case => $re ) {
154 $newRegex[$case] = "/^(?:$re)/JS";
155 }
156 // As a performance optimization, turn on unicode mode only for
157 // case-insensitive matching.
158 $newRegex[0] .= 'u';
159 return $newRegex;
160 }
161
167 private function getVariableStartToEndRegex(): array {
168 $newRegex = [];
169 $base = $this->getBaseRegex( true, '/' );
170 foreach ( $base as $case => $re ) {
171 $newRegex[$case] = str_replace( '\$1', '(.*?)', "/^(?:$re)$/JS" );
172 }
173 // As a performance optimization, turn on unicode mode only for
174 // case-insensitive matching.
175 $newRegex[0] .= 'u';
176 return $newRegex;
177 }
178
183 public function getNames() {
184 return $this->names;
185 }
186
194 private function parseMatch( array $matches ): array {
195 $magicName = null;
196 foreach ( $matches as $key => $match ) {
197 if ( $magicName !== null ) {
198 // The structure we found at this point is [ …,
199 // 'a_magicWordName' => 'matchedSynonym',
200 // n => 'matchedSynonym (again)',
201 // n + 1 => 'parameterValue',
202 // … ]
203 return [ $magicName, $match, $matches[$key + 1] ?? false ];
204 }
205 // Skip the initial full match and any non-matching group
206 if ( $match !== '' && $key !== 0 ) {
207 if ( !str_starts_with( $key, '_' ) ) {
208 throw new LogicException( 'Unexpected group name' );
209 }
210 $magicName = substr( $key, 1 );
211 }
212 }
213 throw new LogicException( 'Unexpected $m array with no match' );
214 }
215
223 public function matchVariableStartToEnd( $text ): array {
224 $regexes = $this->getVariableStartToEndRegex();
225 foreach ( $regexes as $regex ) {
226 $m = [];
227 if ( preg_match( $regex, $text, $m ) ) {
228 [ $id, $alias, $param ] = $this->parseMatch( $m );
229 return [ $id, $param ];
230 }
231 }
232 return [ false, false ];
233 }
234
242 public function matchStartToEnd( $text ) {
243 $hash = $this->getHash();
244 if ( isset( $hash[1][$text] ) ) {
245 return $hash[1][$text];
246 }
247 $lc = $this->factory->getContentLanguage()->lc( $text );
248 return $hash[0][$lc] ?? false;
249 }
250
264 public function matchAndRemove( &$text, bool $returnAlias = false ): array {
265 $found = [];
266 $regexes = $this->getRegex();
267 $res = preg_replace_callback( $regexes, function ( $m ) use ( &$found, $returnAlias ) {
268 [ $name, $alias, $param ] = $this->parseMatch( $m );
269 $found[$name] = $returnAlias ? $alias : $param;
270 return '';
271 }, $text );
272 // T321234: Don't try to fix old revisions with broken UTF-8, just return $text as is
273 if ( $res !== null ) {
274 $text = $res;
275 }
276 return $found;
277 }
278
289 public function matchStartAndRemove( &$text ) {
290 $regexes = $this->getRegexStart();
291 foreach ( $regexes as $regex ) {
292 if ( preg_match( $regex, $text, $m ) ) {
293 [ $id, ] = $this->parseMatch( $m );
294 if ( strlen( $m[0] ) >= strlen( $text ) ) {
295 $text = '';
296 } else {
297 $text = substr( $text, strlen( $m[0] ) );
298 }
299 return $id;
300 }
301 }
302 return false;
303 }
304}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Class for handling an array of magic words.
matchVariableStartToEnd( $text)
Match some text, with parameter capture.
__construct( $names=[], ?MagicWordFactory $factory=null)
matchStartAndRemove(&$text)
Return the ID of the magic word at the start of $text, and remove the prefix from $text.
getBaseRegex(bool $capture=true, string $delimiter='/')
Get the base regex.
matchStartToEnd( $text)
Match some text, without parameter capture.
add( $name)
Add a magic word by name.
matchAndRemove(&$text, bool $returnAlias=false)
Return an associative array for all items that match.
getHash()
Get a 2-d hashtable for this array.
Store information about magic words, and create/cache MagicWord objects.