Complex classes like BBCParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use BBCParser, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 20 | class BBCParser |
||
| 21 | { |
||
| 22 | const MAX_PERMUTE_ITERATIONS = 5040; |
||
| 23 | |||
| 24 | protected $message; |
||
| 25 | protected $bbc; |
||
| 26 | protected $bbc_codes; |
||
| 27 | protected $item_codes; |
||
| 28 | protected $tags; |
||
| 29 | protected $pos; |
||
| 30 | protected $pos1; |
||
| 31 | protected $pos2; |
||
| 32 | protected $pos3; |
||
| 33 | protected $last_pos; |
||
| 34 | protected $do_smileys = true; |
||
| 35 | protected $open_tags = array(); |
||
| 36 | // This is the actual tag that's open |
||
| 37 | protected $inside_tag; |
||
| 38 | |||
| 39 | protected $autolinker; |
||
| 40 | protected $possible_html; |
||
| 41 | protected $html_parser; |
||
| 42 | |||
| 43 | protected $can_cache = true; |
||
| 44 | protected $num_footnotes = 0; |
||
| 45 | protected $smiley_marker = "\r"; |
||
| 46 | |||
| 47 | /** |
||
| 48 | * BBCParser constructor. |
||
| 49 | * |
||
| 50 | * @param \BBC\Codes $bbc |
||
| 51 | * @param \BBC\Autolink|null $autolinker |
||
| 52 | * @param \BBC\HtmlParser|null $html_parser |
||
| 53 | */ |
||
| 54 | 1 | public function __construct(Codes $bbc, Autolink $autolinker = null, HtmlParser $html_parser = null) |
|
| 66 | |||
| 67 | /** |
||
| 68 | * Reset the parser's properties for a new message |
||
| 69 | */ |
||
| 70 | public function resetParser() |
||
| 71 | { |
||
| 72 | $this->pos = -1; |
||
| 73 | $this->pos1 = null; |
||
| 74 | $this->pos2 = null; |
||
| 75 | $this->last_pos = null; |
||
| 76 | $this->open_tags = array(); |
||
| 77 | $this->inside_tag = null; |
||
| 78 | $this->lastAutoPos = 0; |
||
|
|
|||
| 79 | $this->can_cache = true; |
||
| 80 | $this->num_footnotes = 0; |
||
| 81 | } |
||
| 82 | |||
| 83 | /** |
||
| 84 | * Parse the BBC in a string/message |
||
| 85 | * |
||
| 86 | * @param string $message |
||
| 87 | * |
||
| 88 | * @return string |
||
| 89 | */ |
||
| 90 | public function parse($message) |
||
| 91 | { |
||
| 92 | call_integration_hook('integrate_pre_bbc_parser', array(&$message, $this->bbc)); |
||
| 93 | |||
| 94 | $this->message = $message; |
||
| 95 | |||
| 96 | // Don't waste cycles |
||
| 97 | if ($this->message === '') |
||
| 98 | { |
||
| 99 | return ''; |
||
| 100 | } |
||
| 101 | |||
| 102 | // Clean up any cut/paste issues we may have |
||
| 103 | $this->message = sanitizeMSCutPaste($this->message); |
||
| 104 | |||
| 105 | // @todo remove from here and make the caller figure it out |
||
| 106 | if (!$this->parsingEnabled()) |
||
| 107 | { |
||
| 108 | return $this->message; |
||
| 109 | } |
||
| 110 | |||
| 111 | $this->resetParser(); |
||
| 112 | |||
| 113 | // @todo change this to <br> (it will break tests) |
||
| 114 | $this->message = str_replace("\n", '<br />', $this->message); |
||
| 115 | |||
| 116 | // Check if the message might have a link or email to save a bunch of parsing in autolink() |
||
| 117 | $this->autolinker->setPossibleAutolink($this->message); |
||
| 118 | |||
| 119 | $this->possible_html = !empty($GLOBALS['modSettings']['enablePostHTML']) && strpos($message, '<') !== false; |
||
| 120 | |||
| 121 | // Don't load the HTML Parser unless we have to |
||
| 122 | if ($this->possible_html && $this->html_parser === null) |
||
| 123 | { |
||
| 124 | $this->loadHtmlParser(); |
||
| 125 | } |
||
| 126 | |||
| 127 | // This handles pretty much all of the parsing. It is a separate method so it is easier to override and profile. |
||
| 128 | $this->parse_loop(); |
||
| 129 | |||
| 130 | // Close any remaining tags. |
||
| 131 | while ($tag = $this->closeOpenedTag()) |
||
| 132 | { |
||
| 133 | $this->message .= $this->noSmileys($tag[Codes::ATTR_AFTER]); |
||
| 134 | } |
||
| 135 | |||
| 136 | if (isset($this->message[0]) && $this->message[0] === ' ') |
||
| 137 | { |
||
| 138 | $this->message = substr_replace($this->message, ' ', 0, 1); |
||
| 139 | //$this->message = ' ' . substr($this->message, 1); |
||
| 140 | } |
||
| 141 | |||
| 142 | // Cleanup whitespace. |
||
| 143 | $this->message = str_replace(array(' ', '<br /> ', ' '), array(' ', '<br /> ', "\n"), $this->message); |
||
| 144 | |||
| 145 | // Finish footnotes if we have any. |
||
| 146 | if ($this->num_footnotes > 0) |
||
| 147 | { |
||
| 148 | $this->handleFootnotes(); |
||
| 149 | } |
||
| 150 | |||
| 151 | // Allow addons access to what the parser created |
||
| 152 | $message = $this->message; |
||
| 153 | call_integration_hook('integrate_post_bbc_parser', array(&$message)); |
||
| 154 | $this->message = $message; |
||
| 155 | |||
| 156 | return $this->message; |
||
| 157 | } |
||
| 158 | |||
| 159 | protected function parse_loop() |
||
| 160 | { |
||
| 161 | while ($this->pos !== false) |
||
| 162 | { |
||
| 163 | $this->last_pos = isset($this->last_pos) ? max($this->pos, $this->last_pos) : $this->pos; |
||
| 164 | $this->pos = strpos($this->message, '[', $this->pos + 1); |
||
| 165 | |||
| 166 | // Failsafe. |
||
| 167 | if ($this->pos === false || $this->last_pos > $this->pos) |
||
| 168 | { |
||
| 169 | $this->pos = strlen($this->message) + 1; |
||
| 170 | } |
||
| 171 | |||
| 172 | // Can't have a one letter smiley, URL, or email! (sorry.) |
||
| 173 | if ($this->last_pos < $this->pos - 1) |
||
| 174 | { |
||
| 175 | $this->betweenTags(); |
||
| 176 | } |
||
| 177 | |||
| 178 | // Are we there yet? Are we there yet? |
||
| 179 | if ($this->pos >= strlen($this->message) - 1) |
||
| 180 | { |
||
| 181 | return; |
||
| 182 | } |
||
| 183 | |||
| 184 | $next_char = strtolower($this->message[$this->pos + 1]); |
||
| 185 | |||
| 186 | // Possibly a closer? |
||
| 187 | if ($next_char === '/') |
||
| 188 | { |
||
| 189 | if ($this->hasOpenTags()) |
||
| 190 | { |
||
| 191 | $this->handleOpenTags(); |
||
| 192 | } |
||
| 193 | |||
| 194 | // We don't allow / to be used for anything but the closing character, so this can't be a tag |
||
| 195 | continue; |
||
| 196 | } |
||
| 197 | |||
| 198 | // No tags for this character, so just keep going (fastest possible course.) |
||
| 199 | if (!isset($this->bbc_codes[$next_char])) |
||
| 200 | { |
||
| 201 | continue; |
||
| 202 | } |
||
| 203 | |||
| 204 | $this->inside_tag = !$this->hasOpenTags() ? null : $this->getLastOpenedTag(); |
||
| 205 | |||
| 206 | if ($this->isItemCode($next_char) && isset($this->message[$this->pos + 2]) && $this->message[$this->pos + 2] === ']' && !$this->bbc->isDisabled('list') && !$this->bbc->isDisabled('li')) |
||
| 207 | { |
||
| 208 | // Itemcodes cannot be 0 and must be preceeded by a semi-colon, space, tab, new line, or greater than sign |
||
| 209 | if (!($this->message[$this->pos + 1] === '0' && !in_array($this->message[$this->pos - 1], array(';', ' ', "\t", "\n", '>')))) |
||
| 210 | { |
||
| 211 | // Item codes are complicated buggers... they are implicit [li]s and can make [list]s! |
||
| 212 | $this->handleItemCode(); |
||
| 213 | } |
||
| 214 | |||
| 215 | // No matter what, we have to continue here. |
||
| 216 | continue; |
||
| 217 | } |
||
| 218 | else |
||
| 219 | { |
||
| 220 | $tag = $this->findTag($this->bbc_codes[$next_char]); |
||
| 221 | } |
||
| 222 | |||
| 223 | // Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode. |
||
| 224 | if ($tag === null && $this->inside_tag !== null && !empty($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN])) |
||
| 225 | { |
||
| 226 | $this->closeOpenedTag(); |
||
| 227 | $tmp = $this->noSmileys($this->inside_tag[Codes::ATTR_AFTER]); |
||
| 228 | $this->message = substr_replace($this->message, $tmp, $this->pos, 0); |
||
| 229 | $this->pos += strlen($tmp) - 1; |
||
| 230 | } |
||
| 231 | |||
| 232 | // No tag? Keep looking, then. Silly people using brackets without actual tags. |
||
| 233 | if ($tag === null) |
||
| 234 | { |
||
| 235 | continue; |
||
| 236 | } |
||
| 237 | |||
| 238 | // Propagate the list to the child (so wrapping the disallowed tag won't work either.) |
||
| 239 | if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN])) |
||
| 240 | { |
||
| 241 | $tag[Codes::ATTR_DISALLOW_CHILDREN] = isset($tag[Codes::ATTR_DISALLOW_CHILDREN]) ? $tag[Codes::ATTR_DISALLOW_CHILDREN] + $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN] : $this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]; |
||
| 242 | } |
||
| 243 | |||
| 244 | // Is this tag disabled? |
||
| 245 | if ($this->bbc->isDisabled($tag[Codes::ATTR_TAG])) |
||
| 246 | { |
||
| 247 | $this->handleDisabled($tag); |
||
| 248 | } |
||
| 249 | |||
| 250 | // The only special case is 'html', which doesn't need to close things. |
||
| 251 | if ($tag[Codes::ATTR_BLOCK_LEVEL] && $tag[Codes::ATTR_TAG] !== 'html' && !$this->inside_tag[Codes::ATTR_BLOCK_LEVEL]) |
||
| 252 | { |
||
| 253 | $this->closeNonBlockLevel(); |
||
| 254 | } |
||
| 255 | |||
| 256 | // This is the part where we actually handle the tags. I know, crazy how long it took. |
||
| 257 | if ($this->handleTag($tag)) |
||
| 258 | { |
||
| 259 | continue; |
||
| 260 | } |
||
| 261 | |||
| 262 | // If this is block level, eat any breaks after it. |
||
| 263 | if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos + 1]) && substr_compare($this->message, '<br />', $this->pos + 1, 6) === 0) |
||
| 264 | { |
||
| 265 | $this->message = substr_replace($this->message, '', $this->pos + 1, 6); |
||
| 266 | } |
||
| 267 | |||
| 268 | // Are we trimming outside this tag? |
||
| 269 | if (!empty($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_OUTSIDE) |
||
| 270 | { |
||
| 271 | $this->trimWhiteSpace($this->message, $this->pos + 1); |
||
| 272 | } |
||
| 273 | } |
||
| 274 | } |
||
| 275 | |||
| 276 | protected function handleOpenTags() |
||
| 277 | { |
||
| 278 | // Next closing bracket after the first character |
||
| 279 | $this->pos2 = strpos($this->message, ']', $this->pos + 1); |
||
| 280 | |||
| 281 | // Playing games? string = [/] |
||
| 282 | if ($this->pos2 === $this->pos + 2) |
||
| 283 | { |
||
| 284 | return; |
||
| 285 | } |
||
| 286 | |||
| 287 | // Get everything between [/ and ] |
||
| 288 | $look_for = strtolower(substr($this->message, $this->pos + 2, $this->pos2 - $this->pos - 2)); |
||
| 289 | $to_close = array(); |
||
| 290 | $block_level = null; |
||
| 291 | |||
| 292 | do |
||
| 293 | { |
||
| 294 | // Get the last opened tag |
||
| 295 | $tag = $this->closeOpenedTag(); |
||
| 296 | |||
| 297 | // No open tags |
||
| 298 | if (!$tag) |
||
| 299 | { |
||
| 300 | break; |
||
| 301 | } |
||
| 302 | |||
| 303 | if ($tag[Codes::ATTR_BLOCK_LEVEL]) |
||
| 304 | { |
||
| 305 | // Only find out if we need to. |
||
| 306 | if ($block_level === false) |
||
| 307 | { |
||
| 308 | $this->addOpenTag($tag); |
||
| 309 | break; |
||
| 310 | } |
||
| 311 | |||
| 312 | // The idea is, if we are LOOKING for a block level tag, we can close them on the way. |
||
| 313 | if (isset($look_for[1]) && isset($this->bbc_codes[$look_for[0]])) |
||
| 314 | { |
||
| 315 | foreach ($this->bbc_codes[$look_for[0]] as $temp) |
||
| 316 | { |
||
| 317 | if ($temp[Codes::ATTR_TAG] === $look_for) |
||
| 318 | { |
||
| 319 | $block_level = $temp[Codes::ATTR_BLOCK_LEVEL]; |
||
| 320 | break; |
||
| 321 | } |
||
| 322 | } |
||
| 323 | } |
||
| 324 | |||
| 325 | if ($block_level !== true) |
||
| 326 | { |
||
| 327 | $block_level = false; |
||
| 328 | $this->addOpenTag($tag); |
||
| 329 | break; |
||
| 330 | } |
||
| 331 | } |
||
| 332 | |||
| 333 | $to_close[] = $tag; |
||
| 334 | } while ($tag[Codes::ATTR_TAG] !== $look_for); |
||
| 335 | |||
| 336 | // Did we just eat through everything and not find it? |
||
| 337 | if (!$this->hasOpenTags() && (empty($tag) || $tag[Codes::ATTR_TAG] !== $look_for)) |
||
| 338 | { |
||
| 339 | $this->open_tags = $to_close; |
||
| 340 | return; |
||
| 341 | } |
||
| 342 | elseif (!empty($to_close) && $tag[Codes::ATTR_TAG] !== $look_for) |
||
| 343 | { |
||
| 344 | if ($block_level === null && isset($look_for[0], $this->bbc_codes[$look_for[0]])) |
||
| 345 | { |
||
| 346 | foreach ($this->bbc_codes[$look_for[0]] as $temp) |
||
| 347 | { |
||
| 348 | if ($temp[Codes::ATTR_TAG] === $look_for) |
||
| 349 | { |
||
| 350 | $block_level = !empty($temp[Codes::ATTR_BLOCK_LEVEL]); |
||
| 351 | break; |
||
| 352 | } |
||
| 353 | } |
||
| 354 | } |
||
| 355 | |||
| 356 | // We're not looking for a block level tag (or maybe even a tag that exists...) |
||
| 357 | if (!$block_level) |
||
| 358 | { |
||
| 359 | foreach ($to_close as $tag) |
||
| 360 | { |
||
| 361 | $this->addOpenTag($tag); |
||
| 362 | } |
||
| 363 | |||
| 364 | return; |
||
| 365 | } |
||
| 366 | } |
||
| 367 | |||
| 368 | foreach ($to_close as $tag) |
||
| 369 | { |
||
| 370 | $tmp = $this->noSmileys($tag[Codes::ATTR_AFTER]); |
||
| 371 | $this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos); |
||
| 372 | $this->pos += strlen($tmp); |
||
| 373 | $this->pos2 = $this->pos - 1; |
||
| 374 | |||
| 375 | // See the comment at the end of the big loop - just eating whitespace ;). |
||
| 376 | if ($tag[Codes::ATTR_BLOCK_LEVEL] && isset($this->message[$this->pos]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0) |
||
| 377 | { |
||
| 378 | $this->message = substr_replace($this->message, '', $this->pos, 6); |
||
| 379 | } |
||
| 380 | |||
| 381 | // Trim inside whitespace |
||
| 382 | if (!empty($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_INSIDE) |
||
| 383 | { |
||
| 384 | $this->trimWhiteSpace($this->message, $this->pos + 1); |
||
| 385 | } |
||
| 386 | } |
||
| 387 | |||
| 388 | if (!empty($to_close)) |
||
| 389 | { |
||
| 390 | $this->pos--; |
||
| 391 | } |
||
| 392 | } |
||
| 393 | |||
| 394 | /** |
||
| 395 | * Turn smiley parsing on/off |
||
| 396 | * @param bool $toggle |
||
| 397 | * @return \BBC\Parser |
||
| 398 | */ |
||
| 399 | public function doSmileys($toggle) |
||
| 404 | |||
| 405 | /** |
||
| 406 | * Check if parsing is enabled |
||
| 407 | * |
||
| 408 | * @return bool |
||
| 409 | */ |
||
| 410 | public function parsingEnabled() |
||
| 411 | { |
||
| 412 | return !empty($GLOBALS['modSettings']['enableBBC']); |
||
| 413 | } |
||
| 414 | |||
| 415 | public function loadHtmlParser() |
||
| 421 | |||
| 422 | /** |
||
| 423 | * Parse the HTML in a string |
||
| 424 | * |
||
| 425 | * @param string &$data |
||
| 426 | */ |
||
| 427 | protected function parseHTML(&$data) |
||
| 431 | |||
| 432 | /** |
||
| 433 | * Parse URIs and email addresses in a string to url and email BBC tags to be parsed by the BBC parser |
||
| 434 | * |
||
| 435 | * @param string &$data |
||
| 436 | */ |
||
| 437 | protected function autoLink(&$data) |
||
| 438 | { |
||
| 439 | if ($data === '' || $data === $this->smiley_marker || !$this->autolinker->hasPossible()) |
||
| 440 | { |
||
| 441 | return; |
||
| 442 | } |
||
| 443 | |||
| 444 | // Are we inside tags that should be auto linked? |
||
| 445 | if ($this->hasOpenTags()) |
||
| 446 | { |
||
| 447 | foreach ($this->getOpenedTags() as $open_tag) |
||
| 448 | { |
||
| 449 | if (!$open_tag[Codes::ATTR_AUTOLINK]) |
||
| 450 | { |
||
| 451 | return; |
||
| 452 | } |
||
| 453 | } |
||
| 454 | } |
||
| 455 | |||
| 456 | $this->autolinker->parse($data); |
||
| 457 | } |
||
| 458 | |||
| 459 | /** |
||
| 460 | * Load the autolink regular expression to be used in autoLink() |
||
| 461 | */ |
||
| 462 | 1 | protected function loadAutolink() |
|
| 469 | |||
| 470 | /** |
||
| 471 | * Find if the current character is the start of a tag and get it |
||
| 472 | * |
||
| 473 | * @param array $possible_codes |
||
| 474 | * |
||
| 475 | * @return null|array the tag that was found or null if no tag found |
||
| 476 | */ |
||
| 477 | protected function findTag(array $possible_codes) |
||
| 478 | { |
||
| 479 | $tag = null; |
||
| 480 | $last_check = null; |
||
| 481 | |||
| 482 | foreach ($possible_codes as $possible) |
||
| 483 | { |
||
| 484 | // Skip tags that didn't match the next X characters |
||
| 485 | if ($possible[Codes::ATTR_TAG] === $last_check) |
||
| 486 | { |
||
| 487 | continue; |
||
| 488 | } |
||
| 489 | |||
| 490 | // The character after the possible tag or nothing |
||
| 491 | $next_c = isset($this->message[$this->pos + 1 + $possible[Codes::ATTR_LENGTH]]) ? $this->message[$this->pos + 1 + $possible[Codes::ATTR_LENGTH]] : ''; |
||
| 492 | |||
| 493 | // This only happens if the tag is the last character of the string |
||
| 494 | if ($next_c === '') |
||
| 495 | { |
||
| 496 | break; |
||
| 497 | } |
||
| 498 | |||
| 499 | // The next character must be one of these or it's not a tag |
||
| 500 | if ($next_c !== ' ' && $next_c !== ']' && $next_c !== '=' && $next_c !== '/') |
||
| 501 | { |
||
| 502 | $last_check = $possible[Codes::ATTR_TAG]; |
||
| 503 | continue; |
||
| 504 | } |
||
| 505 | |||
| 506 | // Not a match? |
||
| 507 | if (substr_compare($this->message, $possible[Codes::ATTR_TAG], $this->pos + 1, $possible[Codes::ATTR_LENGTH], true) !== 0) |
||
| 508 | { |
||
| 509 | $last_check = $possible[Codes::ATTR_TAG]; |
||
| 510 | continue; |
||
| 511 | } |
||
| 512 | |||
| 513 | $tag = $this->checkCodeAttributes($next_c, $possible, $tag); |
||
| 514 | if ($tag === null) |
||
| 515 | { |
||
| 516 | continue; |
||
| 517 | } |
||
| 518 | |||
| 519 | // Quotes can have alternate styling, we do this php-side due to all the permutations of quotes. |
||
| 520 | if ($tag[Codes::ATTR_TAG] === 'quote') |
||
| 521 | { |
||
| 522 | $this->alternateQuoteStyle($tag); |
||
| 523 | } |
||
| 524 | |||
| 525 | break; |
||
| 526 | } |
||
| 527 | |||
| 528 | // If there is a code that says you can't cache, the message can't be cached |
||
| 529 | if ($tag !== null && $this->can_cache !== false) |
||
| 530 | { |
||
| 531 | $this->can_cache = empty($tag[Codes::ATTR_NO_CACHE]); |
||
| 532 | } |
||
| 533 | |||
| 534 | if ($tag[Codes::ATTR_TAG] === 'footnote') |
||
| 535 | { |
||
| 536 | $this->num_footnotes++; |
||
| 537 | } |
||
| 538 | |||
| 539 | return $tag; |
||
| 540 | } |
||
| 541 | |||
| 542 | /** |
||
| 543 | * @param array $tag |
||
| 544 | */ |
||
| 545 | protected function alternateQuoteStyle(array &$tag) |
||
| 569 | |||
| 570 | /** |
||
| 571 | * @param $next_c |
||
| 572 | * @param array $possible |
||
| 573 | * @return array|void |
||
| 574 | */ |
||
| 575 | protected function checkCodeAttributes($next_c, array $possible) |
||
| 576 | { |
||
| 577 | // Do we want parameters? |
||
| 578 | if (!empty($possible[Codes::ATTR_PARAM])) |
||
| 579 | { |
||
| 580 | if ($next_c !== ' ') |
||
| 581 | { |
||
| 582 | return; |
||
| 583 | } |
||
| 584 | } |
||
| 585 | // parsed_content demands an immediate ] without parameters! |
||
| 586 | elseif ($possible[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_CONTENT) |
||
| 587 | { |
||
| 588 | if ($next_c !== ']') |
||
| 589 | { |
||
| 590 | return; |
||
| 591 | } |
||
| 592 | } |
||
| 593 | else |
||
| 594 | { |
||
| 595 | // Do we need an equal sign? |
||
| 596 | if ($next_c !== '=' && in_array($possible[Codes::ATTR_TYPE], array(Codes::TYPE_UNPARSED_EQUALS, Codes::TYPE_UNPARSED_COMMAS, Codes::TYPE_UNPARSED_COMMAS_CONTENT, Codes::TYPE_UNPARSED_EQUALS_CONTENT, Codes::TYPE_PARSED_EQUALS))) |
||
| 597 | { |
||
| 598 | return; |
||
| 599 | } |
||
| 600 | |||
| 601 | if ($next_c !== ']') |
||
| 602 | { |
||
| 603 | // An immediate ]? |
||
| 604 | if ($possible[Codes::ATTR_TYPE] === Codes::TYPE_UNPARSED_CONTENT) |
||
| 605 | { |
||
| 606 | return; |
||
| 607 | } |
||
| 608 | // Maybe we just want a /... |
||
| 609 | elseif ($possible[Codes::ATTR_TYPE] === Codes::TYPE_CLOSED && substr_compare($this->message, '/]', $this->pos + 1 + $possible[Codes::ATTR_LENGTH], 2) !== 0 && substr_compare($this->message, ' /]', $this->pos + 1 + $possible[Codes::ATTR_LENGTH], 3) !== 0) |
||
| 610 | { |
||
| 611 | return; |
||
| 612 | } |
||
| 613 | } |
||
| 614 | } |
||
| 615 | |||
| 616 | |||
| 617 | // Check allowed tree? |
||
| 618 | if (isset($possible[Codes::ATTR_REQUIRE_PARENTS]) && ($this->inside_tag === null || !isset($possible[Codes::ATTR_REQUIRE_PARENTS][$this->inside_tag[Codes::ATTR_TAG]]))) |
||
| 619 | { |
||
| 620 | return; |
||
| 621 | } |
||
| 622 | |||
| 623 | if ($this->inside_tag !== null) |
||
| 624 | { |
||
| 625 | if (isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN]) && !isset($this->inside_tag[Codes::ATTR_REQUIRE_CHILDREN][$possible[Codes::ATTR_TAG]])) |
||
| 626 | { |
||
| 627 | return; |
||
| 628 | } |
||
| 629 | |||
| 630 | // If this is in the list of disallowed child tags, don't parse it. |
||
| 631 | if (isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN]) && isset($this->inside_tag[Codes::ATTR_DISALLOW_CHILDREN][$possible[Codes::ATTR_TAG]])) |
||
| 632 | { |
||
| 633 | return; |
||
| 634 | } |
||
| 635 | |||
| 636 | // Not allowed in this parent, replace the tags or show it like regular text |
||
| 637 | if (isset($possible[Codes::ATTR_DISALLOW_PARENTS]) && isset($possible[Codes::ATTR_DISALLOW_PARENTS][$this->inside_tag[Codes::ATTR_TAG]])) |
||
| 638 | { |
||
| 639 | if (!isset($possible[Codes::ATTR_DISALLOW_BEFORE], $possible[Codes::ATTR_DISALLOW_AFTER])) |
||
| 640 | { |
||
| 641 | return; |
||
| 642 | } |
||
| 643 | |||
| 644 | $possible[Codes::ATTR_BEFORE] = isset($possible[Codes::ATTR_DISALLOW_BEFORE]) ? $possible[Codes::ATTR_DISALLOW_BEFORE] : $possible[Codes::ATTR_BEFORE]; |
||
| 645 | $possible[Codes::ATTR_AFTER] = isset($possible[Codes::ATTR_DISALLOW_AFTER]) ? $possible[Codes::ATTR_DISALLOW_AFTER] : $possible[Codes::ATTR_AFTER]; |
||
| 646 | } |
||
| 647 | } |
||
| 648 | |||
| 649 | if (isset($possible[Codes::ATTR_TEST]) && $this->handleTest($possible)) |
||
| 650 | { |
||
| 651 | return; |
||
| 652 | } |
||
| 653 | |||
| 654 | // +1 for [, then the length of the tag, then a space |
||
| 655 | $this->pos1 = $this->pos + 1 + $possible[Codes::ATTR_LENGTH] + 1; |
||
| 656 | |||
| 657 | // This is long, but it makes things much easier and cleaner. |
||
| 658 | if (!empty($possible[Codes::ATTR_PARAM])) |
||
| 659 | { |
||
| 660 | $match = $this->matchParameters($possible, $matches); |
||
| 661 | |||
| 662 | // Didn't match our parameter list, try the next possible. |
||
| 663 | if (!$match) |
||
| 664 | { |
||
| 665 | return; |
||
| 666 | } |
||
| 667 | |||
| 668 | return $this->setupTagParameters($possible, $matches); |
||
| 669 | } |
||
| 670 | |||
| 671 | return $possible; |
||
| 672 | } |
||
| 673 | |||
| 674 | protected function handleTest(array $possible) |
||
| 675 | { |
||
| 676 | return preg_match('~^' . $possible[Codes::ATTR_TEST] . '~', substr($this->message, $this->pos + 2 + $possible[Codes::ATTR_LENGTH], strpos($this->message, ']', $this->pos) - ($this->pos + 2 + $possible[Codes::ATTR_LENGTH]))) === 0; |
||
| 677 | } |
||
| 678 | |||
| 679 | protected function handleItemCode() |
||
| 753 | |||
| 754 | /** |
||
| 755 | * Handle codes that are of the parsed context type |
||
| 756 | * @param array $tag |
||
| 757 | * |
||
| 758 | * @return bool |
||
| 759 | */ |
||
| 760 | protected function handleTypeParsedContext(array $tag) |
||
| 761 | { |
||
| 762 | // @todo Check for end tag first, so people can say "I like that [i] tag"? |
||
| 763 | $this->addOpenTag($tag); |
||
| 764 | $tmp = $this->noSmileys($tag[Codes::ATTR_BEFORE]); |
||
| 765 | $this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos1 - $this->pos); |
||
| 766 | $this->pos += strlen($tmp) - 1; |
||
| 767 | |||
| 768 | return false; |
||
| 769 | } |
||
| 770 | |||
| 771 | /** |
||
| 772 | * Handle codes that are of the unparsed context type |
||
| 773 | * @param array $tag |
||
| 774 | * |
||
| 775 | * @return bool |
||
| 776 | */ |
||
| 777 | protected function handleTypeUnparsedContext(array $tag) |
||
| 778 | { |
||
| 779 | // Find the next closer |
||
| 780 | $this->pos2 = stripos($this->message, '[/' . $tag[Codes::ATTR_TAG] . ']', $this->pos1); |
||
| 781 | |||
| 782 | // No closer |
||
| 783 | if ($this->pos2 === false) |
||
| 784 | { |
||
| 785 | return true; |
||
| 786 | } |
||
| 787 | |||
| 788 | $data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1); |
||
| 789 | |||
| 790 | if (!empty($tag[Codes::ATTR_BLOCK_LEVEL]) && isset($data[0]) && substr_compare($data, '<br />', 0, 6) === 0) |
||
| 791 | { |
||
| 792 | $data = substr($data, 6); |
||
| 793 | } |
||
| 794 | |||
| 795 | if (isset($tag[Codes::ATTR_VALIDATE])) |
||
| 796 | { |
||
| 797 | //$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled()); |
||
| 798 | $this->filterData($tag, $data); |
||
| 799 | } |
||
| 800 | |||
| 801 | $code = strtr($tag[Codes::ATTR_CONTENT], array('$1' => $data)); |
||
| 802 | $tmp = $this->noSmileys($code); |
||
| 803 | $this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 3 + $tag[Codes::ATTR_LENGTH] - $this->pos); |
||
| 804 | $this->pos += strlen($tmp) - 1; |
||
| 805 | $this->last_pos = $this->pos + 1; |
||
| 806 | |||
| 807 | return false; |
||
| 808 | } |
||
| 809 | |||
| 810 | /** |
||
| 811 | * Handle codes that are of the unparsed equals context type |
||
| 812 | * @param array $tag |
||
| 813 | * |
||
| 814 | * @return bool |
||
| 815 | */ |
||
| 816 | protected function handleUnparsedEqualsContext(array $tag) |
||
| 873 | |||
| 874 | /** |
||
| 875 | * Handle codes that are of the closed type |
||
| 876 | * @param array $tag |
||
| 877 | * |
||
| 878 | * @return bool |
||
| 879 | */ |
||
| 880 | protected function handleTypeClosed(array $tag) |
||
| 881 | { |
||
| 882 | $this->pos2 = strpos($this->message, ']', $this->pos); |
||
| 883 | $tmp = $this->noSmileys($tag[Codes::ATTR_CONTENT]); |
||
| 884 | $this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + 1 - $this->pos); |
||
| 885 | $this->pos += strlen($tmp) - 1; |
||
| 886 | |||
| 887 | return false; |
||
| 888 | } |
||
| 889 | |||
| 890 | /** |
||
| 891 | * Handle codes that are of the unparsed commas context type |
||
| 892 | * @param array $tag |
||
| 893 | * |
||
| 894 | * @return bool |
||
| 895 | */ |
||
| 896 | protected function handleUnparsedCommasContext(array $tag) |
||
| 932 | |||
| 933 | /** |
||
| 934 | * Handle codes that are of the unparsed commas type |
||
| 935 | * @param array $tag |
||
| 936 | * |
||
| 937 | * @return bool |
||
| 938 | */ |
||
| 939 | protected function handleUnparsedCommas(array $tag) |
||
| 976 | |||
| 977 | /** |
||
| 978 | * Handle codes that are of the equals type |
||
| 979 | * @param array $tag |
||
| 980 | * |
||
| 981 | * @return bool |
||
| 982 | */ |
||
| 983 | protected function handleEquals(array $tag) |
||
| 984 | { |
||
| 985 | // The value may be quoted for some tags - check. |
||
| 986 | if (isset($tag[Codes::ATTR_QUOTED])) |
||
| 987 | { |
||
| 988 | $quoted = substr_compare($this->message, '"', $this->pos1, 6) === 0; |
||
| 989 | if ($tag[Codes::ATTR_QUOTED] !== Codes::OPTIONAL && !$quoted) |
||
| 990 | { |
||
| 991 | return true; |
||
| 992 | } |
||
| 993 | |||
| 994 | if ($quoted) |
||
| 995 | { |
||
| 996 | $this->pos1 += 6; |
||
| 997 | } |
||
| 998 | } |
||
| 999 | else |
||
| 1000 | { |
||
| 1001 | $quoted = false; |
||
| 1002 | } |
||
| 1003 | |||
| 1004 | $this->pos2 = strpos($this->message, $quoted === false ? ']' : '"]', $this->pos1); |
||
| 1005 | if ($this->pos2 === false) |
||
| 1006 | { |
||
| 1007 | return true; |
||
| 1008 | } |
||
| 1009 | |||
| 1010 | $data = substr($this->message, $this->pos1, $this->pos2 - $this->pos1); |
||
| 1011 | |||
| 1012 | // Validation for my parking, please! |
||
| 1013 | if (isset($tag[Codes::ATTR_VALIDATE])) |
||
| 1014 | { |
||
| 1015 | //$tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled()); |
||
| 1016 | $this->filterData($tag, $data); |
||
| 1017 | } |
||
| 1018 | |||
| 1019 | // For parsed content, we must recurse to avoid security problems. |
||
| 1020 | if ($tag[Codes::ATTR_TYPE] === Codes::TYPE_PARSED_EQUALS) |
||
| 1021 | { |
||
| 1022 | $this->recursiveParser($data, $tag); |
||
| 1023 | } |
||
| 1024 | |||
| 1025 | $tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], array('$1' => $data)); |
||
| 1026 | |||
| 1027 | $this->addOpenTag($tag); |
||
| 1028 | |||
| 1029 | $code = strtr($tag[Codes::ATTR_BEFORE], array('$1' => $data)); |
||
| 1030 | $tmp = $this->noSmileys($code); |
||
| 1031 | $this->message = substr_replace($this->message, $tmp, $this->pos, $this->pos2 + ($quoted === false ? 1 : 7) - $this->pos); |
||
| 1032 | $this->pos += strlen($tmp) - 1; |
||
| 1033 | |||
| 1034 | return false; |
||
| 1035 | } |
||
| 1036 | |||
| 1037 | /** |
||
| 1038 | * Handles a tag by its type. Offloads the actual handling to handle*() method |
||
| 1039 | * @param array $tag |
||
| 1040 | * |
||
| 1041 | * @return bool true if there was something wrong and the parser should advance |
||
| 1042 | */ |
||
| 1043 | protected function handleTag(array $tag) |
||
| 1044 | { |
||
| 1045 | switch ($tag[Codes::ATTR_TYPE]) |
||
| 1046 | { |
||
| 1047 | case Codes::TYPE_PARSED_CONTENT: |
||
| 1048 | return $this->handleTypeParsedContext($tag); |
||
| 1049 | |||
| 1050 | // Don't parse the content, just skip it. |
||
| 1051 | case Codes::TYPE_UNPARSED_CONTENT: |
||
| 1052 | return $this->handleTypeUnparsedContext($tag); |
||
| 1053 | |||
| 1054 | // Don't parse the content, just skip it. |
||
| 1055 | case Codes::TYPE_UNPARSED_EQUALS_CONTENT: |
||
| 1056 | return $this->handleUnparsedEqualsContext($tag); |
||
| 1057 | |||
| 1058 | // A closed tag, with no content or value. |
||
| 1059 | case Codes::TYPE_CLOSED: |
||
| 1060 | return $this->handleTypeClosed($tag); |
||
| 1061 | |||
| 1062 | // This one is sorta ugly... :/ |
||
| 1063 | case Codes::TYPE_UNPARSED_COMMAS_CONTENT: |
||
| 1064 | return $this->handleUnparsedCommasContext($tag); |
||
| 1065 | |||
| 1066 | // This has parsed content, and a csv value which is unparsed. |
||
| 1067 | case Codes::TYPE_UNPARSED_COMMAS: |
||
| 1068 | return $this->handleUnparsedCommas($tag); |
||
| 1069 | |||
| 1070 | // A tag set to a value, parsed or not. |
||
| 1071 | case Codes::TYPE_PARSED_EQUALS: |
||
| 1072 | case Codes::TYPE_UNPARSED_EQUALS: |
||
| 1073 | return $this->handleEquals($tag); |
||
| 1074 | } |
||
| 1075 | |||
| 1076 | return false; |
||
| 1077 | } |
||
| 1078 | |||
| 1079 | // @todo I don't know what else to call this. It's the area that isn't a tag. |
||
| 1080 | protected function betweenTags() |
||
| 1081 | { |
||
| 1082 | // Make sure the $this->last_pos is not negative. |
||
| 1083 | $this->last_pos = max($this->last_pos, 0); |
||
| 1084 | |||
| 1085 | // Pick a block of data to do some raw fixing on. |
||
| 1086 | $data = substr($this->message, $this->last_pos, $this->pos - $this->last_pos); |
||
| 1087 | |||
| 1088 | // This happens when the pos is > last_pos and there is a trailing \n from one of the tags having "AFTER" |
||
| 1089 | // In micro-optimization tests, using substr() here doesn't prove to be slower. This is much easier to read so leave it. |
||
| 1090 | if ($data === $this->smiley_marker) |
||
| 1091 | { |
||
| 1092 | return; |
||
| 1093 | } |
||
| 1094 | |||
| 1095 | // Take care of some HTML! |
||
| 1096 | if ($this->possible_html && strpos($data, '<') !== false) |
||
| 1097 | { |
||
| 1098 | // @todo new \Parser\BBC\HTML; |
||
| 1099 | $this->parseHTML($data); |
||
| 1100 | } |
||
| 1101 | |||
| 1102 | if (!empty($GLOBALS['modSettings']['autoLinkUrls'])) |
||
| 1103 | { |
||
| 1104 | $this->autoLink($data); |
||
| 1105 | } |
||
| 1106 | |||
| 1107 | // This cannot be moved earlier. It breaks tests |
||
| 1108 | $data = str_replace("\t", ' ', $data); |
||
| 1109 | |||
| 1110 | // If it wasn't changed, no copying or other boring stuff has to happen! |
||
| 1111 | if (substr_compare($this->message, $data, $this->last_pos, $this->pos - $this->last_pos)) |
||
| 1112 | { |
||
| 1113 | $this->message = substr_replace($this->message, $data, $this->last_pos, $this->pos - $this->last_pos); |
||
| 1114 | |||
| 1115 | // Since we changed it, look again in case we added or removed a tag. But we don't want to skip any. |
||
| 1116 | $old_pos = strlen($data) + $this->last_pos; |
||
| 1117 | $this->pos = strpos($this->message, '[', $this->last_pos); |
||
| 1118 | $this->pos = $this->pos === false ? $old_pos : min($this->pos, $old_pos); |
||
| 1119 | } |
||
| 1120 | } |
||
| 1121 | |||
| 1122 | protected function handleFootnotes() |
||
| 1123 | { |
||
| 1124 | global $fn_num, $fn_content, $fn_count; |
||
| 1125 | static $fn_total; |
||
| 1126 | |||
| 1127 | // @todo temporary until we have nesting |
||
| 1128 | $this->message = str_replace(array('[footnote]', '[/footnote]'), '', $this->message); |
||
| 1129 | |||
| 1130 | $fn_num = 0; |
||
| 1131 | $fn_content = array(); |
||
| 1132 | $fn_count = isset($fn_total) ? $fn_total : 0; |
||
| 1133 | |||
| 1134 | // Replace our footnote text with a [1] link, save the text for use at the end of the message |
||
| 1135 | $this->message = preg_replace_callback('~(%fn%(.*?)%fn%)~is', array($this, 'footnoteCallback'), $this->message); |
||
| 1136 | $fn_total += $fn_num; |
||
| 1137 | |||
| 1138 | // If we have footnotes, add them in at the end of the message |
||
| 1139 | if (!empty($fn_num)) |
||
| 1140 | { |
||
| 1141 | $this->message .= '<div class="bbc_footnotes">' . implode('', $fn_content) . '</div>'; |
||
| 1142 | } |
||
| 1143 | } |
||
| 1144 | |||
| 1145 | /** |
||
| 1146 | * @param array $matches |
||
| 1147 | * @return string |
||
| 1148 | */ |
||
| 1149 | protected function footnoteCallback(array $matches) |
||
| 1150 | { |
||
| 1151 | global $fn_num, $fn_content, $fn_count; |
||
| 1152 | |||
| 1153 | $fn_num++; |
||
| 1154 | $fn_content[] = '<div class="target" id="fn' . $fn_num . '_' . $fn_count . '"><sup>' . $fn_num . ' </sup>' . $matches[2] . '<a class="footnote_return" href="#ref' . $fn_num . '_' . $fn_count . '">↵</a></div>'; |
||
| 1155 | |||
| 1156 | return '<a class="target" href="#fn' . $fn_num . '_' . $fn_count . '" id="ref' . $fn_num . '_' . $fn_count . '">[' . $fn_num . ']</a>'; |
||
| 1157 | } |
||
| 1158 | |||
| 1159 | /** |
||
| 1160 | * Parse a tag that is disabled |
||
| 1161 | * @param array $tag |
||
| 1162 | */ |
||
| 1163 | protected function handleDisabled(array &$tag) |
||
| 1181 | |||
| 1182 | /** |
||
| 1183 | * @param array &$possible |
||
| 1184 | * @param array &$matches |
||
| 1185 | * @return bool |
||
| 1186 | */ |
||
| 1187 | protected function matchParameters(array &$possible, &$matches) |
||
| 1188 | { |
||
| 1189 | if (!isset($possible['regex_cache'])) |
||
| 1190 | { |
||
| 1191 | $possible['regex_cache'] = array(); |
||
| 1192 | foreach ($possible[Codes::ATTR_PARAM] as $p => $info) |
||
| 1193 | { |
||
| 1194 | $quote = empty($info[Codes::PARAM_ATTR_QUOTED]) ? '' : '"'; |
||
| 1195 | |||
| 1196 | $possible['regex_cache'][] = '(\s+' . $p . '=' . $quote . (isset($info[Codes::PARAM_ATTR_MATCH]) ? $info[Codes::PARAM_ATTR_MATCH] : '(.+?)') . $quote . ')' . (empty($info[Codes::PARAM_ATTR_OPTIONAL]) ? '' : '?'); |
||
| 1197 | } |
||
| 1198 | $possible['regex_size'] = count($possible['regex_cache']) - 1; |
||
| 1199 | $possible['regex_keys'] = range(0, $possible['regex_size']); |
||
| 1200 | } |
||
| 1201 | |||
| 1202 | // Okay, this may look ugly and it is, but it's not going to happen much and it is the best way |
||
| 1203 | // of allowing any order of parameters but still parsing them right. |
||
| 1204 | $message_stub = substr($this->message, $this->pos1 - 1); |
||
| 1205 | |||
| 1206 | // If an addon adds many parameters we can exceed max_execution time, lets prevent that |
||
| 1207 | // 5040 = 7, 40,320 = 8, (N!) etc |
||
| 1208 | $max_iterations = self::MAX_PERMUTE_ITERATIONS; |
||
| 1209 | |||
| 1210 | // Use the same range to start each time. Most BBC is in the order that it should be in when it starts. |
||
| 1211 | $keys = $possible['regex_keys']; |
||
| 1212 | |||
| 1213 | // Step, one by one, through all possible permutations of the parameters until we have a match |
||
| 1214 | do |
||
| 1215 | { |
||
| 1216 | $match_preg = '~^'; |
||
| 1217 | foreach ($keys as $key) |
||
| 1218 | { |
||
| 1219 | $match_preg .= $possible['regex_cache'][$key]; |
||
| 1220 | } |
||
| 1221 | $match_preg .= '\]~i'; |
||
| 1222 | |||
| 1223 | // Check if this combination of parameters matches the user input |
||
| 1224 | $match = preg_match($match_preg, $message_stub, $matches) !== 0; |
||
| 1225 | } while (!$match && --$max_iterations && ($keys = pc_next_permutation($keys, $possible['regex_size']))); |
||
| 1226 | |||
| 1227 | return $match; |
||
| 1228 | } |
||
| 1229 | |||
| 1230 | /** |
||
| 1231 | * Recursively call the parser with a new Codes object |
||
| 1232 | * This allows to parse BBC in parameters like [quote author="[url]www.quotes.com[/url]"]Something famous.[/quote] |
||
| 1233 | * |
||
| 1234 | * @param string &$data |
||
| 1235 | * @param array $tag |
||
| 1236 | */ |
||
| 1237 | protected function recursiveParser(&$data, array $tag) |
||
| 1255 | |||
| 1256 | /** |
||
| 1257 | * @return array |
||
| 1258 | */ |
||
| 1259 | public function getBBC() |
||
| 1263 | |||
| 1264 | /** |
||
| 1265 | * Enable the parsing of smileys |
||
| 1266 | * @param bool|true $enable |
||
| 1267 | * |
||
| 1268 | * @return $this |
||
| 1269 | */ |
||
| 1270 | public function enableSmileys($enable = true) |
||
| 1275 | |||
| 1276 | /** |
||
| 1277 | * Open a tag |
||
| 1278 | * @param array $tag |
||
| 1279 | */ |
||
| 1280 | protected function addOpenTag(array $tag) |
||
| 1281 | { |
||
| 1282 | $this->open_tags[] = $tag; |
||
| 1283 | } |
||
| 1284 | |||
| 1285 | /** |
||
| 1286 | * @param string|false $tag = false False closes the last open tag. Anything else finds that tag LIFO |
||
| 1287 | * |
||
| 1288 | * @return mixed |
||
| 1289 | */ |
||
| 1290 | protected function closeOpenedTag($tag = false) |
||
| 1291 | { |
||
| 1292 | if ($tag === false) |
||
| 1293 | { |
||
| 1294 | return array_pop($this->open_tags); |
||
| 1295 | } |
||
| 1296 | elseif (isset($this->open_tags[$tag])) |
||
| 1297 | { |
||
| 1298 | $return = $this->open_tags[$tag]; |
||
| 1299 | unset($this->open_tags[$tag]); |
||
| 1300 | return $return; |
||
| 1301 | } |
||
| 1302 | } |
||
| 1303 | |||
| 1304 | /** |
||
| 1305 | * Check if there are any tags that are open |
||
| 1306 | * @return bool |
||
| 1307 | */ |
||
| 1308 | protected function hasOpenTags() |
||
| 1309 | { |
||
| 1310 | return !empty($this->open_tags); |
||
| 1311 | } |
||
| 1312 | |||
| 1313 | /** |
||
| 1314 | * Get the last opened tag |
||
| 1315 | * @return array |
||
| 1316 | */ |
||
| 1317 | protected function getLastOpenedTag() |
||
| 1318 | { |
||
| 1319 | return end($this->open_tags); |
||
| 1320 | } |
||
| 1321 | |||
| 1322 | /** |
||
| 1323 | * Get the currently opened tags |
||
| 1324 | * @param bool|false $tags_only True if you want just the tag or false for the whole code |
||
| 1325 | * |
||
| 1326 | * @return array |
||
| 1327 | */ |
||
| 1328 | protected function getOpenedTags($tags_only = false) |
||
| 1329 | { |
||
| 1330 | if (!$tags_only) |
||
| 1331 | { |
||
| 1332 | return $this->open_tags; |
||
| 1333 | } |
||
| 1334 | |||
| 1335 | $tags = array(); |
||
| 1336 | foreach ($this->open_tags as $tag) |
||
| 1337 | { |
||
| 1338 | $tags[] = $tag[Codes::ATTR_TAG]; |
||
| 1339 | } |
||
| 1340 | return $tags; |
||
| 1341 | } |
||
| 1342 | |||
| 1343 | /** |
||
| 1344 | * @param string &$message |
||
| 1345 | * @param null|int $offset = null |
||
| 1346 | */ |
||
| 1347 | protected function trimWhiteSpace(&$message, $offset = null) |
||
| 1348 | { |
||
| 1349 | if (preg_match('~(<br />| |\s)*~', $this->message, $matches, null, $offset) !== 0 && isset($matches[0]) && $matches[0] !== '') |
||
| 1350 | { |
||
| 1351 | $this->message = substr_replace($this->message, '', $this->pos, strlen($matches[0])); |
||
| 1352 | } |
||
| 1353 | } |
||
| 1354 | |||
| 1355 | /** |
||
| 1356 | * @param array $possible |
||
| 1357 | * @param array $matches |
||
| 1358 | * |
||
| 1359 | * @return array |
||
| 1360 | */ |
||
| 1361 | protected function setupTagParameters(array $possible, array $matches) |
||
| 1362 | { |
||
| 1363 | $params = array(); |
||
| 1364 | for ($i = 1, $n = count($matches); $i < $n; $i += 2) |
||
| 1365 | { |
||
| 1366 | $key = strtok(ltrim($matches[$i]), '='); |
||
| 1367 | |||
| 1368 | if (isset($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE])) |
||
| 1369 | { |
||
| 1370 | $params['{' . $key . '}'] = strtr($possible[Codes::ATTR_PARAM][$key][Codes::PARAM_ATTR_VALUE], array('$1' => $matches[$i + 1])); |
||
| 1371 | } |
||
| 1372 | elseif (isset($possible[Codes::ATTR_PARAM][$key][Codes::ATTR_VALIDATE])) |
||
| 1373 | { |
||
| 1374 | $params['{' . $key . '}'] = $possible[Codes::ATTR_PARAM][$key][Codes::ATTR_VALIDATE]($matches[$i + 1]); |
||
| 1375 | } |
||
| 1376 | else |
||
| 1377 | { |
||
| 1378 | $params['{' . $key . '}'] = $matches[$i + 1]; |
||
| 1379 | } |
||
| 1380 | |||
| 1381 | // Just to make sure: replace any $ or { so they can't interpolate wrongly. |
||
| 1382 | $params['{' . $key . '}'] = str_replace(array('$', '{'), array('$', '{'), $params['{' . $key . '}']); |
||
| 1383 | } |
||
| 1384 | |||
| 1385 | foreach ($possible[Codes::ATTR_PARAM] as $p => $info) |
||
| 1386 | { |
||
| 1387 | if (!isset($params['{' . $p . '}'])) |
||
| 1388 | { |
||
| 1389 | $params['{' . $p . '}'] = ''; |
||
| 1390 | } |
||
| 1391 | } |
||
| 1392 | |||
| 1393 | // We found our tag |
||
| 1394 | $tag = $possible; |
||
| 1395 | |||
| 1396 | // Put the parameters into the string. |
||
| 1397 | if (isset($tag[Codes::ATTR_BEFORE])) |
||
| 1398 | { |
||
| 1399 | $tag[Codes::ATTR_BEFORE] = strtr($tag[Codes::ATTR_BEFORE], $params); |
||
| 1400 | } |
||
| 1401 | if (isset($tag[Codes::ATTR_AFTER])) |
||
| 1402 | { |
||
| 1403 | $tag[Codes::ATTR_AFTER] = strtr($tag[Codes::ATTR_AFTER], $params); |
||
| 1404 | } |
||
| 1405 | if (isset($tag[Codes::ATTR_CONTENT])) |
||
| 1406 | { |
||
| 1407 | $tag[Codes::ATTR_CONTENT] = strtr($tag[Codes::ATTR_CONTENT], $params); |
||
| 1408 | } |
||
| 1409 | |||
| 1410 | $this->pos1 += strlen($matches[0]) - 1; |
||
| 1411 | |||
| 1412 | return $tag; |
||
| 1413 | } |
||
| 1414 | |||
| 1415 | /** |
||
| 1416 | * Check if a tag (not a code) is open |
||
| 1417 | * @param string $tag |
||
| 1418 | * |
||
| 1419 | * @return bool |
||
| 1420 | */ |
||
| 1421 | protected function isOpen($tag) |
||
| 1433 | |||
| 1434 | /** |
||
| 1435 | * Check if a character is an item code |
||
| 1436 | * @param string $char |
||
| 1437 | * |
||
| 1438 | * @return bool |
||
| 1439 | */ |
||
| 1440 | protected function isItemCode($char) |
||
| 1441 | { |
||
| 1442 | return isset($this->item_codes[$char]); |
||
| 1443 | } |
||
| 1444 | |||
| 1445 | /** |
||
| 1446 | * Close any open codes that aren't block level. |
||
| 1447 | * Used before opening a code that *is* block level |
||
| 1448 | */ |
||
| 1449 | protected function closeNonBlockLevel() |
||
| 1450 | { |
||
| 1451 | $n = count($this->open_tags) - 1; |
||
| 1452 | while (empty($this->open_tags[$n][Codes::ATTR_BLOCK_LEVEL]) && $n >= 0) |
||
| 1453 | { |
||
| 1454 | $n--; |
||
| 1455 | } |
||
| 1456 | |||
| 1457 | // Close all the non block level tags so this tag isn't surrounded by them. |
||
| 1458 | for ($i = count($this->open_tags) - 1; $i > $n; $i--) |
||
| 1459 | { |
||
| 1460 | $tmp = $this->noSmileys($this->open_tags[$i][Codes::ATTR_AFTER]); |
||
| 1461 | $this->message = substr_replace($this->message, $tmp, $this->pos, 0); |
||
| 1462 | $ot_strlen = strlen($tmp); |
||
| 1463 | $this->pos += $ot_strlen; |
||
| 1464 | $this->pos1 += $ot_strlen; |
||
| 1465 | |||
| 1466 | // Trim or eat trailing stuff... see comment at the end of the big loop. |
||
| 1467 | if (!empty($this->open_tags[$i][Codes::ATTR_BLOCK_LEVEL]) && substr_compare($this->message, '<br />', $this->pos, 6) === 0) |
||
| 1468 | { |
||
| 1469 | $this->message = substr_replace($this->message, '', $this->pos, 6); |
||
| 1470 | } |
||
| 1471 | |||
| 1472 | if (isset($tag[Codes::ATTR_TRIM]) && $tag[Codes::ATTR_TRIM] !== Codes::TRIM_INSIDE) |
||
| 1473 | { |
||
| 1474 | $this->trimWhiteSpace($this->message, $this->pos); |
||
| 1475 | } |
||
| 1476 | |||
| 1477 | $this->closeOpenedTag(); |
||
| 1478 | } |
||
| 1479 | } |
||
| 1480 | |||
| 1481 | /** |
||
| 1482 | * Add markers around a string to denote that smileys should not be parsed |
||
| 1483 | * |
||
| 1484 | * @param string $string |
||
| 1485 | * |
||
| 1486 | * @return string |
||
| 1487 | */ |
||
| 1488 | protected function noSmileys($string) |
||
| 1489 | { |
||
| 1490 | return $this->smiley_marker . $string . $this->smiley_marker; |
||
| 1491 | } |
||
| 1492 | |||
| 1493 | public function canCache() |
||
| 1497 | |||
| 1498 | // This is just so I can profile it. |
||
| 1499 | protected function filterData(array $tag, &$data) |
||
| 1500 | { |
||
| 1501 | $tag[Codes::ATTR_VALIDATE]($tag, $data, $this->bbc->getDisabled()); |
||
| 1502 | } |
||
| 1503 | } |
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: