Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
94.59% |
35 / 37 |
|
93.75% |
15 / 16 |
CRAP | |
0.00% |
0 / 1 |
| MagicWord | |
94.59% |
35 / 37 |
|
93.75% |
15 / 16 |
20.06 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| load | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getRegex | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRegexCase | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| getRegexStart | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRegexStartToEnd | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getBaseRegex | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
| match | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| matchStartToEnd | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| matchAndRemove | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| matchStartAndRemove | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| replace | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| getSynonym | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getSynonyms | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isCaseSensitive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Parser; |
| 8 | |
| 9 | use MediaWiki\Language\Language; |
| 10 | use MediaWiki\MediaWikiServices; |
| 11 | use UnexpectedValueException; |
| 12 | use Wikimedia\StringUtils\StringUtils; |
| 13 | |
| 14 | /** |
| 15 | * This class encapsulates "magic words" such as "#redirect", __NOTOC__, etc. |
| 16 | * |
| 17 | * See docs/magicword.md. |
| 18 | * |
| 19 | * @par Usage: |
| 20 | * @code |
| 21 | * if ( $magicWordFactory->get( 'redirect' )->match( $text ) ) { |
| 22 | * // some code |
| 23 | * } |
| 24 | * @endcode |
| 25 | * |
| 26 | * Please avoid reading the data out of one of these objects and then writing |
| 27 | * special case code. If possible, add another match()-like function here. |
| 28 | * |
| 29 | * To add magic words in an extension, use $magicWords in a file listed in |
| 30 | * $wgExtensionMessagesFiles[]. |
| 31 | * |
| 32 | * @par Example: |
| 33 | * @code |
| 34 | * $magicWords = []; |
| 35 | * |
| 36 | * $magicWords['en'] = [ |
| 37 | * 'magicwordkey' => [ 0, 'case_insensitive_magic_word' ], |
| 38 | * 'magicwordkey2' => [ 1, 'CASE_sensitive_magic_word2' ], |
| 39 | * ]; |
| 40 | * @endcode |
| 41 | * |
| 42 | * For magic words which name Parser double underscore names, add a |
| 43 | * GetDoubleUnderscoreIDs hook. Use string keys. |
| 44 | * |
| 45 | * For magic words which name Parser magic variables, add a GetMagicVariableIDs |
| 46 | * hook. Use string keys. |
| 47 | * |
| 48 | * @since 1.1 |
| 49 | * @ingroup Parser |
| 50 | */ |
| 51 | class MagicWord { |
| 52 | |
| 53 | /** @var string|null Potentially null for a short time before {@see load} is called */ |
| 54 | public $mId; |
| 55 | |
| 56 | /** @var string[] */ |
| 57 | public array $mSynonyms; |
| 58 | |
| 59 | /** @var bool */ |
| 60 | public $mCaseSensitive; |
| 61 | |
| 62 | private ?string $mBaseRegex = null; |
| 63 | |
| 64 | private Language $contLang; |
| 65 | |
| 66 | /** |
| 67 | * @internal Use {@see MagicWordFactory::get} instead |
| 68 | * @param string|null $id Preload internal name of the magic word |
| 69 | * @param string[]|string $syn Preload synonyms for the magic word |
| 70 | * @param bool $cs If magic word is case sensitive |
| 71 | * @param Language|null $contentLanguage |
| 72 | */ |
| 73 | public function __construct( $id = null, $syn = [], $cs = false, ?Language $contentLanguage = null ) { |
| 74 | $this->mId = $id; |
| 75 | $this->mSynonyms = (array)$syn; |
| 76 | $this->mCaseSensitive = $cs; |
| 77 | $this->contLang = $contentLanguage ?? MediaWikiServices::getInstance()->getContentLanguage(); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Load synonym data from {@see LocalisationCache}. |
| 82 | * |
| 83 | * @internal For use by {@see MagicWordFactory::get} only |
| 84 | * @since 1.1 |
| 85 | * @param string $id |
| 86 | */ |
| 87 | public function load( $id ): void { |
| 88 | $this->mId = $id; |
| 89 | $this->contLang->getMagic( $this ); |
| 90 | if ( !$this->mSynonyms ) { |
| 91 | throw new UnexpectedValueException( "Error: invalid magic word '$id'" ); |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Create a regex to match the magic word in wikitext |
| 97 | * |
| 98 | * @since 1.1 |
| 99 | * @return string |
| 100 | */ |
| 101 | public function getRegex(): string { |
| 102 | return '/' . $this->getBaseRegex() . '/' . $this->getRegexCase(); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Get the regexp case modifier ("iu" or empty string). |
| 107 | * |
| 108 | * This is for building custom regexes that include {@see getBaseRegex}. |
| 109 | * The other getter methods return complete expressions that include this already. |
| 110 | * |
| 111 | * @internal Exposed for {@see Parser::cleanSig} only |
| 112 | * @return string |
| 113 | */ |
| 114 | public function getRegexCase(): string { |
| 115 | return $this->mCaseSensitive ? '' : 'iu'; |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Create a regex to match the word at the start of a line in wikitext |
| 120 | * |
| 121 | * @since 1.1 |
| 122 | * @return string |
| 123 | */ |
| 124 | public function getRegexStart(): string { |
| 125 | return '/^(?:' . $this->getBaseRegex() . ')/' . $this->getRegexCase(); |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * Create a regex to match the word as the only thing on a line of wikitext |
| 130 | * |
| 131 | * @since 1.23 |
| 132 | * @return string |
| 133 | */ |
| 134 | public function getRegexStartToEnd(): string { |
| 135 | return '/^(?:' . $this->getBaseRegex() . ')$/' . $this->getRegexCase(); |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Get the middle of {@see getRegex}, without the surrounding slashes or modifiers |
| 140 | * |
| 141 | * @internal Exposed for {@see Parser::cleanSig} only |
| 142 | * @since 1.1 |
| 143 | * @return string |
| 144 | */ |
| 145 | public function getBaseRegex(): string { |
| 146 | if ( $this->mBaseRegex === null ) { |
| 147 | // Sort the synonyms by length, descending, so that the longest synonym |
| 148 | // matches in precedence to the shortest |
| 149 | $synonyms = $this->mSynonyms; |
| 150 | usort( $synonyms, static fn ( $a, $b ) => strlen( $b ) <=> strlen( $a ) ); |
| 151 | foreach ( $synonyms as &$synonym ) { |
| 152 | $synonym = preg_quote( $synonym, '/' ); |
| 153 | } |
| 154 | $this->mBaseRegex = implode( '|', $synonyms ); |
| 155 | } |
| 156 | return $this->mBaseRegex; |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Check if given wikitext contains the magic word |
| 161 | * |
| 162 | * @since 1.1 |
| 163 | * @param string $text |
| 164 | * @return bool |
| 165 | */ |
| 166 | public function match( $text ): bool { |
| 167 | return (bool)preg_match( $this->getRegex(), $text ?? '' ); |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * Check if given wikitext contains the word as the only thing on a line |
| 172 | * |
| 173 | * @param string $text |
| 174 | * @return bool |
| 175 | * @since 1.23 |
| 176 | */ |
| 177 | public function matchStartToEnd( $text ): bool { |
| 178 | return (bool)preg_match( $this->getRegexStartToEnd(), $text ?? '' ); |
| 179 | } |
| 180 | |
| 181 | /** |
| 182 | * Remove any matches of this magic word from a given text |
| 183 | * |
| 184 | * Returns true if the text contains one or more matches, and alters the |
| 185 | * input string to remove all instances of the magic word. |
| 186 | * |
| 187 | * @since 1.1 |
| 188 | * @param string &$text |
| 189 | * @return bool |
| 190 | */ |
| 191 | public function matchAndRemove( &$text ): bool { |
| 192 | $text = preg_replace( $this->getRegex(), '', $text ?? '', -1, $count ); |
| 193 | return (bool)$count; |
| 194 | } |
| 195 | |
| 196 | /** |
| 197 | * @param string &$text |
| 198 | * @return bool |
| 199 | */ |
| 200 | public function matchStartAndRemove( &$text ): bool { |
| 201 | $text = preg_replace( $this->getRegexStart(), '', $text ?? '', -1, $count ); |
| 202 | return (bool)$count; |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Replace any matches of this word with something else |
| 207 | * |
| 208 | * @since 1.1 |
| 209 | * @param string $replacement |
| 210 | * @param string $subject |
| 211 | * @param int $limit |
| 212 | * @return string |
| 213 | */ |
| 214 | public function replace( $replacement, $subject, $limit = -1 ) { |
| 215 | $res = preg_replace( |
| 216 | $this->getRegex(), |
| 217 | StringUtils::escapeRegexReplacement( $replacement ), |
| 218 | $subject ?? '', |
| 219 | $limit |
| 220 | ); |
| 221 | return $res; |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Get one of the synonyms |
| 226 | * |
| 227 | * This exists primarily for calling `getSynonym( 0 )`, which is how |
| 228 | * you can obtain the preferred name of a magic word according to the |
| 229 | * current wiki's content language. For example, when demonstrating or |
| 230 | * semi-automatically creating content that uses a given magic word. |
| 231 | * |
| 232 | * This works because {@see LocalisationCache} merges magic word data by |
| 233 | * appending fallback languages (i.e. "en") after to the language's |
| 234 | * own data, and each language's `Messages*.php` file lists the |
| 235 | * preferred/canonical form as the first value. |
| 236 | * |
| 237 | * Calling this with a number other than 0 is unsupported and may |
| 238 | * fail. |
| 239 | * |
| 240 | * @since 1.1 |
| 241 | * @param int $i |
| 242 | * @return string |
| 243 | */ |
| 244 | public function getSynonym( $i ) { |
| 245 | return $this->mSynonyms[$i]; |
| 246 | } |
| 247 | |
| 248 | /** |
| 249 | * Get full list of synonyms |
| 250 | * |
| 251 | * @since 1.7 |
| 252 | * @return string[] |
| 253 | */ |
| 254 | public function getSynonyms(): array { |
| 255 | return $this->mSynonyms; |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * @since 1.7 |
| 260 | * @return bool |
| 261 | */ |
| 262 | public function isCaseSensitive() { |
| 263 | return $this->mCaseSensitive; |
| 264 | } |
| 265 | |
| 266 | /** |
| 267 | * @return string |
| 268 | * @deprecated since 1.42 Internal method should not be used |
| 269 | */ |
| 270 | public function getId() { |
| 271 | wfDeprecated( __METHOD__, '1.42' ); |
| 272 | return $this->mId; |
| 273 | } |
| 274 | } |