[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 3 /**#@+ 4 * A parser extension that adds two tags, <ref> and <references> for adding 5 * citations to pages 6 * 7 * @ingroup Extensions 8 * 9 * @link http://www.mediawiki.org/wiki/Extension:Cite/Cite.php Documentation 10 * @link http://www.w3.org/TR/html4/struct/text.html#edef-CITE <cite> definition in HTML 11 * @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite <cite> definition in XHTML 2.0 12 * 13 * @bug 4579 14 * 15 * @author Ævar Arnfjörð Bjarmason <[email protected]> 16 * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason 17 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later 18 */ 19 20 class Cite { 21 /**#@+ 22 * @access private 23 */ 24 25 /** 26 * Datastructure representing <ref> input, in the format of: 27 * <code> 28 * array( 29 * 'user supplied' => array( 30 * 'text' => 'user supplied reference & key', 31 * 'count' => 1, // occurs twice 32 * 'number' => 1, // The first reference, we want 33 * // all occourances of it to 34 * // use the same number 35 * ), 36 * 0 => 'Anonymous reference', 37 * 1 => 'Another anonymous reference', 38 * 'some key' => array( 39 * 'text' => 'this one occurs once' 40 * 'count' => 0, 41 * 'number' => 4 42 * ), 43 * 3 => 'more stuff' 44 * ); 45 * </code> 46 * 47 * This works because: 48 * * PHP's datastructures are guaranteed to be returned in the 49 * order that things are inserted into them (unless you mess 50 * with that) 51 * * User supplied keys can't be integers, therefore avoiding 52 * conflict with anonymous keys 53 * 54 * @var array 55 **/ 56 public $mRefs = array(); 57 58 /** 59 * Count for user displayed output (ref[1], ref[2], ...) 60 * 61 * @var int 62 */ 63 public $mOutCnt = 0; 64 public $mGroupCnt = array(); 65 66 /** 67 * Counter to track the total number of (useful) calls to either the 68 * ref or references tag hook 69 */ 70 public $mCallCnt = 0; 71 72 /** 73 * The backlinks, in order, to pass as $3 to 74 * 'cite_references_link_many_format', defined in 75 * 'cite_references_link_many_format_backlink_labels 76 * 77 * @var array 78 */ 79 public $mBacklinkLabels; 80 81 /** 82 * The links to use per group, in order. 83 * 84 * @var array 85 */ 86 public $mLinkLabels = array(); 87 88 /** 89 * @var Parser 90 */ 91 public $mParser; 92 93 /** 94 * True when the ParserAfterParse hook has been called. 95 * Used to avoid doing anything in ParserBeforeTidy. 96 * 97 * @var boolean 98 */ 99 public $mHaveAfterParse = false; 100 101 /** 102 * True when a <ref> tag is being processed. 103 * Used to avoid infinite recursion 104 * 105 * @var boolean 106 */ 107 public $mInCite = false; 108 109 /** 110 * True when a <references> tag is being processed. 111 * Used to detect the use of <references> to define refs 112 * 113 * @var boolean 114 */ 115 public $mInReferences = false; 116 117 /** 118 * Error stack used when defining refs in <references> 119 * 120 * @var array 121 */ 122 public $mReferencesErrors = array(); 123 124 /** 125 * Group used when in <references> block 126 * 127 * @var string 128 */ 129 public $mReferencesGroup = ''; 130 131 /** 132 * <ref> call stack 133 * Used to cleanup out of sequence ref calls created by #tag 134 * See description of function rollbackRef. 135 * 136 * @var array 137 */ 138 public $mRefCallStack = array(); 139 140 /** 141 * Did we install us into $wgHooks yet? 142 * @var Boolean 143 */ 144 static protected $hooksInstalled = false; 145 146 /**#@+ @access private */ 147 148 /** 149 * Callback function for <ref> 150 * 151 * @param $str string Input 152 * @param $argv array Arguments 153 * @param $parser Parser 154 * @param $frame PPFrame 155 * 156 * @return string 157 */ 158 function ref( $str, $argv, $parser, $frame ) { 159 if ( $this->mInCite ) { 160 return htmlspecialchars( "<ref>$str</ref>" ); 161 } else { 162 $this->mCallCnt++; 163 $this->mInCite = true; 164 $ret = $this->guardedRef( $str, $argv, $parser ); 165 $this->mInCite = false; 166 $parserOutput = $parser->getOutput(); 167 $parserOutput->addModules( 'ext.cite' ); 168 $parserOutput->addModuleStyles( 'ext.rtlcite' ); 169 if ( is_callable( array( $frame, 'setVolatile' ) ) ) { 170 $frame->setVolatile(); 171 } 172 return $ret; 173 } 174 } 175 176 /** 177 * @param $str string Input 178 * @param $argv array Arguments 179 * @param $parser Parser 180 * @param $default_group string 181 * @return string 182 */ 183 function guardedRef( $str, $argv, $parser, $default_group = CITE_DEFAULT_GROUP ) { 184 $this->mParser = $parser; 185 186 # The key here is the "name" attribute. 187 list( $key, $group, $follow ) = $this->refArg( $argv ); 188 189 # Split these into groups. 190 if ( $group === null ) { 191 if ( $this->mInReferences ) { 192 $group = $this->mReferencesGroup; 193 } else { 194 $group = $default_group; 195 } 196 } 197 198 # This section deals with constructions of the form 199 # 200 # <references> 201 # <ref name="foo"> BAR </ref> 202 # </references> 203 # 204 if ( $this->mInReferences ) { 205 if ( $group != $this->mReferencesGroup ) { 206 # <ref> and <references> have conflicting group attributes. 207 $this->mReferencesErrors[] = 208 $this->error( 'cite_error_references_group_mismatch', htmlspecialchars( $group ) ); 209 } elseif ( $str !== '' ) { 210 if ( !isset( $this->mRefs[$group] ) ) { 211 # Called with group attribute not defined in text. 212 $this->mReferencesErrors[] = 213 $this->error( 'cite_error_references_missing_group', htmlspecialchars( $group ) ); 214 } elseif ( $key === null || $key === '' ) { 215 # <ref> calls inside <references> must be named 216 $this->mReferencesErrors[] = 217 $this->error( 'cite_error_references_no_key' ); 218 } elseif ( !isset( $this->mRefs[$group][$key] ) ) { 219 # Called with name attribute not defined in text. 220 $this->mReferencesErrors[] = 221 $this->error( 'cite_error_references_missing_key', $key ); 222 } else { 223 # Assign the text to corresponding ref 224 $this->mRefs[$group][$key]['text'] = $str; 225 } 226 } else { 227 # <ref> called in <references> has no content. 228 $this->mReferencesErrors[] = 229 $this->error( 'cite_error_empty_references_define', $key ); 230 } 231 return ''; 232 } 233 234 if ( $str === '' ) { 235 # <ref ...></ref>. This construct is invalid if 236 # it's a contentful ref, but OK if it's a named duplicate and should 237 # be equivalent <ref ... />, for compatability with #tag. 238 if ( $key == false ) { 239 $this->mRefCallStack[] = false; 240 return $this->error( 'cite_error_ref_no_input' ); 241 } else { 242 $str = null; 243 } 244 } 245 246 if ( $key === false ) { 247 # TODO: Comment this case; what does this condition mean? 248 $this->mRefCallStack[] = false; 249 return $this->error( 'cite_error_ref_too_many_keys' ); 250 } 251 252 if ( $str === null && $key === null ) { 253 # Something like <ref />; this makes no sense. 254 $this->mRefCallStack[] = false; 255 return $this->error( 'cite_error_ref_no_key' ); 256 } 257 258 if ( preg_match( '/^[0-9]+$/', $key ) || preg_match( '/^[0-9]+$/', $follow ) ) { 259 # Numeric names mess up the resulting id's, potentially produ- 260 # cing duplicate id's in the XHTML. The Right Thing To Do 261 # would be to mangle them, but it's not really high-priority 262 # (and would produce weird id's anyway). 263 264 $this->mRefCallStack[] = false; 265 return $this->error( 'cite_error_ref_numeric_key' ); 266 } 267 268 if ( preg_match( 269 '/<ref\b[^<]*?>/', 270 preg_replace( '#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#', '', $str ) 271 ) ) { 272 # (bug 6199) This most likely implies that someone left off the 273 # closing </ref> tag, which will cause the entire article to be 274 # eaten up until the next <ref>. So we bail out early instead. 275 # The fancy regex above first tries chopping out anything that 276 # looks like a comment or SGML tag, which is a crude way to avoid 277 # false alarms for <nowiki>, <pre>, etc. 278 # 279 # Possible improvement: print the warning, followed by the contents 280 # of the <ref> tag. This way no part of the article will be eaten 281 # even temporarily. 282 283 $this->mRefCallStack[] = false; 284 return $this->error( 'cite_error_included_ref' ); 285 } 286 287 if ( is_string( $key ) || is_string( $str ) ) { 288 # We don't care about the content: if the key exists, the ref 289 # is presumptively valid. Either it stores a new ref, or re- 290 # fers to an existing one. If it refers to a nonexistent ref, 291 # we'll figure that out later. Likewise it's definitely valid 292 # if there's any content, regardless of key. 293 294 return $this->stack( $str, $key, $group, $follow, $argv ); 295 } 296 297 # Not clear how we could get here, but something is probably 298 # wrong with the types. Let's fail fast. 299 throw new MWException( 'Invalid $str and/or $key: ' . serialize( array( $str, $key ) ) ); 300 } 301 302 /** 303 * Parse the arguments to the <ref> tag 304 * 305 * "name" : Key of the reference. 306 * "group" : Group to which it belongs. Needs to be passed to <references /> too. 307 * "follow" : If the current reference is the continuation of another, key of that reference. 308 * 309 * 310 * @param $argv array The argument vector 311 * @return mixed false on invalid input, a string on valid 312 * input and null on no input 313 */ 314 function refArg( $argv ) { 315 global $wgAllowCiteGroups; 316 $cnt = count( $argv ); 317 $group = null; 318 $key = null; 319 $follow = null; 320 321 if ( $cnt > 2 ) { 322 // There should only be one key or follow parameter, and one group parameter 323 // FIXME : this looks inconsistent, it should probably return a tuple 324 return false; 325 } elseif ( $cnt >= 1 ) { 326 if ( isset( $argv['name'] ) && isset( $argv['follow'] ) ) { 327 return array( false, false, false ); 328 } 329 if ( isset( $argv['name'] ) ) { 330 // Key given. 331 $key = Sanitizer::escapeId( $argv['name'], 'noninitial' ); 332 unset( $argv['name'] ); 333 --$cnt; 334 } 335 if ( isset( $argv['follow'] ) ) { 336 // Follow given. 337 $follow = Sanitizer::escapeId( $argv['follow'], 'noninitial' ); 338 unset( $argv['follow'] ); 339 --$cnt; 340 } 341 if ( isset( $argv['group'] ) ) { 342 if ( !$wgAllowCiteGroups ) { 343 // remove when groups are fully tested. 344 return array( false ); 345 } 346 // Group given. 347 $group = $argv['group']; 348 unset( $argv['group'] ); 349 --$cnt; 350 } 351 352 if ( $cnt == 0 ) { 353 return array ( $key, $group, $follow ); 354 } else { 355 // Invalid key 356 return array( false, false, false ); 357 } 358 } else { 359 // No key 360 return array( null, $group, false ); 361 } 362 } 363 364 /** 365 * Populate $this->mRefs based on input and arguments to <ref> 366 * 367 * @param $str string Input from the <ref> tag 368 * @param $key mixed Argument to the <ref> tag as returned by $this->refArg() 369 * @param $group 370 * @param $follow 371 * @param $call 372 * 373 * @return string 374 */ 375 function stack( $str, $key = null, $group, $follow, $call ) { 376 if ( !isset( $this->mRefs[$group] ) ) { 377 $this->mRefs[$group] = array(); 378 } 379 if ( !isset( $this->mGroupCnt[$group] ) ) { 380 $this->mGroupCnt[$group] = 0; 381 } 382 383 if ( $follow != null ) { 384 if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) { 385 // add text to the note that is being followed 386 $this->mRefs[$group][$follow]['text'] = $this->mRefs[$group][$follow]['text'] . ' ' . $str; 387 } else { 388 // insert part of note at the beginning of the group 389 for ( $k = 0 ; $k < count( $this->mRefs[$group] ) ; $k++ ) { 390 if ( $this->mRefs[$group][$k]['follow'] == null ) { 391 break; 392 } 393 } 394 array_splice( $this->mRefs[$group], $k, 0, 395 array( array( 'count' => - 1, 396 'text' => $str, 397 'key' => ++$this->mOutCnt , 398 'follow' => $follow ) ) ); 399 array_splice( $this->mRefCallStack, $k, 0, 400 array( array( 'new', $call, $str, $key, $group, $this->mOutCnt ) ) ); 401 } 402 // return an empty string : this is not a reference 403 return ''; 404 } 405 if ( $key === null ) { 406 // No key 407 // $this->mRefs[$group][] = $str; 408 $this->mRefs[$group][] = array( 'count' => - 1, 'text' => $str, 'key' => ++$this->mOutCnt ); 409 $this->mRefCallStack[] = array( 'new', $call, $str, $key, $group, $this->mOutCnt ); 410 411 return $this->linkRef( $group, $this->mOutCnt ); 412 } elseif ( is_string( $key ) ) { 413 // Valid key 414 if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) { 415 // First occurrence 416 $this->mRefs[$group][$key] = array( 417 'text' => $str, 418 'count' => 0, 419 'key' => ++$this->mOutCnt, 420 'number' => ++$this->mGroupCnt[$group] 421 ); 422 $this->mRefCallStack[] = array( 'new', $call, $str, $key, $group, $this->mOutCnt ); 423 424 return 425 $this->linkRef( 426 $group, 427 $key, 428 $this->mRefs[$group][$key]['key'] . "-" . $this->mRefs[$group][$key]['count'], 429 $this->mRefs[$group][$key]['number'], 430 "-" . $this->mRefs[$group][$key]['key'] 431 ); 432 } else { 433 // We've been here before 434 if ( $this->mRefs[$group][$key]['text'] === null && $str !== '' ) { 435 // If no text found before, use this text 436 $this->mRefs[$group][$key]['text'] = $str; 437 $this->mRefCallStack[] = array( 'assign', $call, $str, $key, $group, 438 $this->mRefs[$group][$key]['key'] ); 439 } else { 440 $this->mRefCallStack[] = array( 'increment', $call, $str, $key, $group, 441 $this->mRefs[$group][$key]['key'] ); 442 } 443 return 444 $this->linkRef( 445 $group, 446 $key, 447 $this->mRefs[$group][$key]['key'] . "-" . ++$this->mRefs[$group][$key]['count'], 448 $this->mRefs[$group][$key]['number'], 449 "-" . $this->mRefs[$group][$key]['key'] 450 ); 451 } 452 } else { 453 throw new MWException( 'Invalid stack key: ' . serialize( $key ) ); 454 } 455 } 456 457 /** 458 * Partially undoes the effect of calls to stack() 459 * 460 * Called by guardedReferences() 461 * 462 * The option to define <ref> within <references> makes the 463 * behavior of <ref> context dependent. This is normally fine 464 * but certain operations (especially #tag) lead to out-of-order 465 * parser evaluation with the <ref> tags being processed before 466 * their containing <reference> element is read. This leads to 467 * stack corruption that this function works to fix. 468 * 469 * This function is not a total rollback since some internal 470 * counters remain incremented. Doing so prevents accidentally 471 * corrupting certain links. 472 * 473 * @param $type 474 * @param $key 475 * @param $group 476 * @param $index 477 */ 478 function rollbackRef( $type, $key, $group, $index ) { 479 if ( !isset( $this->mRefs[$group] ) ) { 480 return; 481 } 482 483 if ( $key === null ) { 484 foreach ( $this->mRefs[$group] as $k => $v ) { 485 if ( $this->mRefs[$group][$k]['key'] === $index ) { 486 $key = $k; 487 break; 488 } 489 } 490 } 491 492 # Sanity checks that specified element exists. 493 if ( $key === null ) { 494 return; 495 } 496 if ( !isset( $this->mRefs[$group][$key] ) ) { 497 return; 498 } 499 if ( $this->mRefs[$group][$key]['key'] != $index ) { 500 return; 501 } 502 503 switch ( $type ) { 504 case 'new': 505 # Rollback the addition of new elements to the stack. 506 unset( $this->mRefs[$group][$key] ); 507 if ( count( $this->mRefs[$group] ) == 0 ) { 508 unset( $this->mRefs[$group] ); 509 unset( $this->mGroupCnt[$group] ); 510 } 511 break; 512 case 'assign': 513 # Rollback assignment of text to pre-existing elements. 514 $this->mRefs[$group][$key]['text'] = null; 515 # continue without break 516 case 'increment': 517 # Rollback increase in named ref occurrences. 518 $this->mRefs[$group][$key]['count']--; 519 break; 520 } 521 } 522 523 /** 524 * Callback function for <references> 525 * 526 * @param $str string Input 527 * @param $argv array Arguments 528 * @param $parser Parser 529 * @param $frame PPFrame 530 * 531 * @return string 532 */ 533 function references( $str, $argv, $parser, $frame ) { 534 if ( $this->mInCite || $this->mInReferences ) { 535 if ( is_null( $str ) ) { 536 return htmlspecialchars( "<references/>" ); 537 } else { 538 return htmlspecialchars( "<references>$str</references>" ); 539 } 540 } else { 541 $this->mCallCnt++; 542 $this->mInReferences = true; 543 $ret = $this->guardedReferences( $str, $argv, $parser ); 544 $this->mInReferences = false; 545 if ( is_callable( array( $frame, 'setVolatile' ) ) ) { 546 $frame->setVolatile(); 547 } 548 return $ret; 549 } 550 } 551 552 /** 553 * @param $str string 554 * @param $argv array 555 * @param $parser Parser 556 * @param $group string 557 * @return string 558 */ 559 function guardedReferences( $str, $argv, $parser, $group = CITE_DEFAULT_GROUP ) { 560 global $wgAllowCiteGroups; 561 562 $this->mParser = $parser; 563 564 if ( isset( $argv['group'] ) && $wgAllowCiteGroups ) { 565 $group = $argv['group']; 566 unset ( $argv['group'] ); 567 } 568 569 if ( strval( $str ) !== '' ) { 570 $this->mReferencesGroup = $group; 571 572 # Detect whether we were sent already rendered <ref>s 573 # Mostly a side effect of using #tag to call references 574 $count = substr_count( $str, $parser->uniqPrefix() . "-ref-" ); 575 for ( $i = 1; $i <= $count; $i++ ) { 576 if ( count( $this->mRefCallStack ) < 1 ) { 577 break; 578 } 579 580 # The following assumes that the parsed <ref>s sent within 581 # the <references> block were the most recent calls to 582 # <ref>. This assumption is true for all known use cases, 583 # but not strictly enforced by the parser. It is possible 584 # that some unusual combination of #tag, <references> and 585 # conditional parser functions could be created that would 586 # lead to malformed references here. 587 $call = array_pop( $this->mRefCallStack ); 588 if ( $call !== false ) { 589 list( $type, $ref_argv, $ref_str, 590 $ref_key, $ref_group, $ref_index ) = $call; 591 592 # Undo effects of calling <ref> while unaware of containing <references> 593 $this->rollbackRef( $type, $ref_key, $ref_group, $ref_index ); 594 595 # Rerun <ref> call now that mInReferences is set. 596 $this->guardedRef( $ref_str, $ref_argv, $parser ); 597 } 598 } 599 600 # Parse $str to process any unparsed <ref> tags. 601 $parser->recursiveTagParse( $str ); 602 603 # Reset call stack 604 $this->mRefCallStack = array(); 605 } 606 607 if ( count( $argv ) && $wgAllowCiteGroups ) { 608 return $this->error( 'cite_error_references_invalid_parameters_group' ); 609 } elseif ( count( $argv ) ) { 610 return $this->error( 'cite_error_references_invalid_parameters' ); 611 } else { 612 $s = $this->referencesFormat( $group ); 613 if ( $parser->getOptions()->getIsSectionPreview() ) { 614 return $s; 615 } 616 617 # Append errors generated while processing <references> 618 if ( count( $this->mReferencesErrors ) > 0 ) { 619 $s .= "\n" . implode( "<br />\n", $this->mReferencesErrors ); 620 $this->mReferencesErrors = array(); 621 } 622 return $s; 623 } 624 } 625 626 /** 627 * Make output to be returned from the references() function 628 * 629 * @param $group 630 * 631 * @return string XHTML ready for output 632 */ 633 function referencesFormat( $group ) { 634 if ( ( count( $this->mRefs ) == 0 ) || ( empty( $this->mRefs[$group] ) ) ) { 635 return ''; 636 } 637 638 wfProfileIn( __METHOD__ ); 639 wfProfileIn( __METHOD__ . '-entries' ); 640 $ent = array(); 641 foreach ( $this->mRefs[$group] as $k => $v ) { 642 $ent[] = $this->referencesFormatEntry( $k, $v ); 643 } 644 645 $prefix = wfMessage( 'cite_references_prefix' )->inContentLanguage()->plain(); 646 $suffix = wfMessage( 'cite_references_suffix' )->inContentLanguage()->plain(); 647 $content = implode( "\n", $ent ); 648 649 // Prepare the parser input. We add new lines between the pieces to avoid a confused tidy (bug 13073) 650 $parserInput = $prefix . "\n" . $content . "\n" . $suffix; 651 652 // Let's try to cache it. 653 global $wgMemc; 654 $cacheKey = wfMemcKey( 'citeref', md5( $parserInput ), $this->mParser->Title()->getArticleID() ); 655 656 wfProfileOut( __METHOD__ . '-entries' ); 657 658 global $wgCiteCacheReferences; 659 $data = false; 660 if ( $wgCiteCacheReferences ) { 661 wfProfileIn( __METHOD__ . '-cache-get' ); 662 $data = $wgMemc->get( $cacheKey ); 663 wfProfileOut( __METHOD__ . '-cache-get' ); 664 } 665 666 if ( !$data || !$this->mParser->isValidHalfParsedText( $data ) ) { 667 wfProfileIn( __METHOD__ . '-parse' ); 668 669 // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar 670 $ret = rtrim( $this->mParser->recursiveTagParse( $parserInput ), "\n" ); 671 672 if ( $wgCiteCacheReferences ) { 673 $serData = $this->mParser->serializeHalfParsedText( $ret ); 674 $wgMemc->set( $cacheKey, $serData, 86400 ); 675 } 676 677 wfProfileOut( __METHOD__ . '-parse' ); 678 } else { 679 $ret = $this->mParser->unserializeHalfParsedText( $data ); 680 } 681 682 wfProfileOut( __METHOD__ ); 683 684 // done, clean up so we can reuse the group 685 unset( $this->mRefs[$group] ); 686 unset( $this->mGroupCnt[$group] ); 687 688 return $ret; 689 } 690 691 /** 692 * Format a single entry for the referencesFormat() function 693 * 694 * @param string $key The key of the reference 695 * @param mixed $val The value of the reference, string for anonymous 696 * references, array for user-suppplied 697 * @return string Wikitext 698 */ 699 function referencesFormatEntry( $key, $val ) { 700 // Anonymous reference 701 if ( !is_array( $val ) ) { 702 return wfMessage( 703 'cite_references_link_one', 704 $this->referencesKey( $key ), 705 $this->refKey( $key ), 706 $this->referenceText( $key, $val ) 707 )->inContentLanguage()->plain(); 708 } 709 $text = $this->referenceText( $key, $val['text'] ); 710 if ( isset( $val['follow'] ) ) { 711 return wfMessage( 712 'cite_references_no_link', 713 $this->referencesKey( $val['follow'] ), 714 $text 715 )->inContentLanguage()->plain(); 716 } elseif ( $val['text'] == '' ) { 717 return wfMessage( 718 'cite_references_link_one', 719 $this->referencesKey( $key ), 720 $this->refKey( $key, $val['count'] ), 721 $text 722 )->inContentLanguage()->plain(); 723 } 724 725 if ( $val['count'] < 0 ) { 726 return wfMessage( 727 'cite_references_link_one', 728 $this->referencesKey( $val['key'] ), 729 # $this->refKey( $val['key'], $val['count'] ), 730 $this->refKey( $val['key'] ), 731 $text 732 )->inContentLanguage()->plain(); 733 // Standalone named reference, I want to format this like an 734 // anonymous reference because displaying "1. 1.1 Ref text" is 735 // overkill and users frequently use named references when they 736 // don't need them for convenience 737 } elseif ( $val['count'] === 0 ) { 738 return wfMessage( 739 'cite_references_link_one', 740 $this->referencesKey( $key . "-" . $val['key'] ), 741 # $this->refKey( $key, $val['count'] ), 742 $this->refKey( $key, $val['key'] . "-" . $val['count'] ), 743 $text 744 )->inContentLanguage()->plain(); 745 // Named references with >1 occurrences 746 } else { 747 $links = array(); 748 // for group handling, we have an extra key here. 749 for ( $i = 0; $i <= $val['count']; ++$i ) { 750 $links[] = wfMessage( 751 'cite_references_link_many_format', 752 $this->refKey( $key, $val['key'] . "-$i" ), 753 $this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ), 754 $this->referencesFormatEntryAlternateBacklinkLabel( $i ) 755 )->inContentLanguage()->plain(); 756 } 757 758 $list = $this->listToText( $links ); 759 760 return wfMessage( 'cite_references_link_many', 761 $this->referencesKey( $key . "-" . $val['key'] ), 762 $list, 763 $text 764 )->inContentLanguage()->plain(); 765 } 766 } 767 768 /** 769 * Returns formatted reference text 770 * @param String $key 771 * @param String $text 772 * @return String 773 */ 774 function referenceText( $key, $text ) { 775 if ( $text == '' ) { 776 return $this->error( 'cite_error_references_no_text', $key, 'noparse' ); 777 } 778 return '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\n"; 779 } 780 781 /** 782 * Generate a numeric backlink given a base number and an 783 * offset, e.g. $base = 1, $offset = 2; = 1.2 784 * Since bug #5525, it correctly does 1.9 -> 1.10 as well as 1.099 -> 1.100 785 * 786 * @static 787 * 788 * @param int $base The base 789 * @param int $offset The offset 790 * @param int $max Maximum value expected. 791 * @return string 792 */ 793 function referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) { 794 global $wgContLang; 795 $scope = strlen( $max ); 796 $ret = $wgContLang->formatNum( 797 sprintf( "%s.%0{$scope}s", $base, $offset ) 798 ); 799 return $ret; 800 } 801 802 /** 803 * Generate a custom format backlink given an offset, e.g. 804 * $offset = 2; = c if $this->mBacklinkLabels = array( 'a', 805 * 'b', 'c', ...). Return an error if the offset > the # of 806 * array items 807 * 808 * @param int $offset The offset 809 * 810 * @return string 811 */ 812 function referencesFormatEntryAlternateBacklinkLabel( $offset ) { 813 if ( !isset( $this->mBacklinkLabels ) ) { 814 $this->genBacklinkLabels(); 815 } 816 if ( isset( $this->mBacklinkLabels[$offset] ) ) { 817 return $this->mBacklinkLabels[$offset]; 818 } else { 819 // Feed me! 820 return $this->error( 'cite_error_references_no_backlink_label', null, 'noparse' ); 821 } 822 } 823 824 /** 825 * Generate a custom format link for a group given an offset, e.g. 826 * the second <ref group="foo"> is b if $this->mLinkLabels["foo"] = 827 * array( 'a', 'b', 'c', ...). 828 * Return an error if the offset > the # of array items 829 * 830 * @param int $offset The offset 831 * @param string $group The group name 832 * @param string $label The text to use if there's no message for them. 833 * 834 * @return string 835 */ 836 function getLinkLabel( $offset, $group, $label ) { 837 $message = "cite_link_label_group-$group"; 838 if ( !isset( $this->mLinkLabels[$group] ) ) { 839 $this->genLinkLabels( $group, $message ); 840 } 841 if ( $this->mLinkLabels[$group] === false ) { 842 // Use normal representation, ie. "$group 1", "$group 2"... 843 return $label; 844 } 845 846 if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) { 847 return $this->mLinkLabels[$group][$offset - 1]; 848 } else { 849 // Feed me! 850 return $this->error( 'cite_error_no_link_label_group', array( $group, $message ), 'noparse' ); 851 } 852 } 853 854 /** 855 * Return an id for use in wikitext output based on a key and 856 * optionally the number of it, used in <references>, not <ref> 857 * (since otherwise it would link to itself) 858 * 859 * @static 860 * 861 * @param string $key The key 862 * @param int $num The number of the key 863 * @return string A key for use in wikitext 864 */ 865 function refKey( $key, $num = null ) { 866 $prefix = wfMessage( 'cite_reference_link_prefix' )->inContentLanguage()->text(); 867 $suffix = wfMessage( 'cite_reference_link_suffix' )->inContentLanguage()->text(); 868 if ( isset( $num ) ) { 869 $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num ) 870 ->inContentLanguage()->plain(); 871 } 872 873 return "$prefix$key$suffix"; 874 } 875 876 /** 877 * Return an id for use in wikitext output based on a key and 878 * optionally the number of it, used in <ref>, not <references> 879 * (since otherwise it would link to itself) 880 * 881 * @static 882 * 883 * @param string $key The key 884 * @param int $num The number of the key 885 * @return string A key for use in wikitext 886 */ 887 function referencesKey( $key, $num = null ) { 888 $prefix = wfMessage( 'cite_references_link_prefix' )->inContentLanguage()->text(); 889 $suffix = wfMessage( 'cite_references_link_suffix' )->inContentLanguage()->text(); 890 if ( isset( $num ) ) { 891 $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num ) 892 ->inContentLanguage()->plain(); 893 } 894 895 return "$prefix$key$suffix"; 896 } 897 898 /** 899 * Generate a link (<sup ...) for the <ref> element from a key 900 * and return XHTML ready for output 901 * 902 * @param $group 903 * @param $key string The key for the link 904 * @param $count int The index of the key, used for distinguishing 905 * multiple occurrences of the same key 906 * @param $label int The label to use for the link, I want to 907 * use the same label for all occourances of 908 * the same named reference. 909 * @param $subkey string 910 * 911 * @return string 912 */ 913 function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) { 914 global $wgContLang; 915 $label = is_null( $label ) ? ++$this->mGroupCnt[$group] : $label; 916 917 return 918 $this->mParser->recursiveTagParse( 919 wfMessage( 920 'cite_reference_link', 921 $this->refKey( $key, $count ), 922 $this->referencesKey( $key . $subkey ), 923 $this->getLinkLabel( $label, $group, 924 ( ( $group == CITE_DEFAULT_GROUP ) ? '' : "$group " ) . $wgContLang->formatNum( $label ) ) 925 )->inContentLanguage()->plain() 926 ); 927 } 928 929 /** 930 * This does approximately the same thing as 931 * Language::listToText() but due to this being used for a 932 * slightly different purpose (people might not want , as the 933 * first separator and not 'and' as the second, and this has to 934 * use messages from the content language) I'm rolling my own. 935 * 936 * @static 937 * 938 * @param array $arr The array to format 939 * @return string 940 */ 941 function listToText( $arr ) { 942 $cnt = count( $arr ); 943 944 $sep = wfMessage( 'cite_references_link_many_sep' )->inContentLanguage()->plain(); 945 $and = wfMessage( 'cite_references_link_many_and' )->inContentLanguage()->plain(); 946 947 if ( $cnt == 1 ) { 948 // Enforce always returning a string 949 return (string)$arr[0]; 950 } else { 951 $t = array_slice( $arr, 0, $cnt - 1 ); 952 return implode( $sep, $t ) . $and . $arr[$cnt - 1]; 953 } 954 } 955 956 /** 957 * Generate the labels to pass to the 958 * 'cite_references_link_many_format' message, the format is an 959 * arbitrary number of tokens separated by [\t\n ] 960 */ 961 function genBacklinkLabels() { 962 wfProfileIn( __METHOD__ ); 963 $text = wfMessage( 'cite_references_link_many_format_backlink_labels' ) 964 ->inContentLanguage()->plain(); 965 $this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text ); 966 wfProfileOut( __METHOD__ ); 967 } 968 969 /** 970 * Generate the labels to pass to the 971 * 'cite_reference_link' message instead of numbers, the format is an 972 * arbitrary number of tokens separated by [\t\n ] 973 * 974 * @param $group 975 * @param $message 976 */ 977 function genLinkLabels( $group, $message ) { 978 wfProfileIn( __METHOD__ ); 979 $text = false; 980 $msg = wfMessage( $message )->inContentLanguage(); 981 if ( $msg->exists() ) { 982 $text = $msg->plain(); 983 } 984 $this->mLinkLabels[$group] = ( $text == '' ) ? false : preg_split( '#[\n\t ]#', $text ); 985 wfProfileOut( __METHOD__ ); 986 } 987 988 /** 989 * Gets run when Parser::clearState() gets run, since we don't 990 * want the counts to transcend pages and other instances 991 * 992 * @param $parser Parser 993 * 994 * @return bool 995 */ 996 function clearState( &$parser ) { 997 if ( $parser->extCite !== $this ) { 998 return $parser->extCite->clearState( $parser ); 999 } 1000 1001 # Don't clear state when we're in the middle of parsing 1002 # a <ref> tag 1003 if ( $this->mInCite || $this->mInReferences ) { 1004 return true; 1005 } 1006 1007 $this->mGroupCnt = array(); 1008 $this->mOutCnt = 0; 1009 $this->mCallCnt = 0; 1010 $this->mRefs = array(); 1011 $this->mReferencesErrors = array(); 1012 $this->mRefCallStack = array(); 1013 1014 return true; 1015 } 1016 1017 /** 1018 * Gets run when the parser is cloned. 1019 * 1020 * @param $parser Parser 1021 * 1022 * @return bool 1023 */ 1024 function cloneState( $parser ) { 1025 if ( $parser->extCite !== $this ) { 1026 return $parser->extCite->cloneState( $parser ); 1027 } 1028 1029 $parser->extCite = clone $this; 1030 $parser->setHook( 'ref' , array( $parser->extCite, 'ref' ) ); 1031 $parser->setHook( 'references' , array( $parser->extCite, 'references' ) ); 1032 1033 // Clear the state, making sure it will actually work. 1034 $parser->extCite->mInCite = false; 1035 $parser->extCite->mInReferences = false; 1036 $parser->extCite->clearState( $parser ); 1037 1038 return true; 1039 } 1040 1041 /** 1042 * Called at the end of page processing to append an error if refs were 1043 * used without a references tag. 1044 * 1045 * @param $afterParse bool true if called from the ParserAfterParse hook 1046 * @param $parser Parser 1047 * @param $text string 1048 * 1049 * @return bool 1050 */ 1051 function checkRefsNoReferences( $afterParse, &$parser, &$text ) { 1052 if ( $parser->extCite !== $this ) { 1053 return $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text ); 1054 } 1055 1056 if ( $afterParse ) { 1057 $this->mHaveAfterParse = true; 1058 } elseif ( $this->mHaveAfterParse ) { 1059 return true; 1060 } 1061 1062 if ( $parser->getOptions()->getIsSectionPreview() ) { 1063 return true; 1064 } 1065 1066 foreach ( $this->mRefs as $group => $refs ) { 1067 if ( count( $refs ) == 0 ) { 1068 continue; 1069 } 1070 if ( $group == CITE_DEFAULT_GROUP ) { 1071 $text .= $this->referencesFormat( $group, '', '' ); 1072 } else { 1073 $text .= "\n<br />" . $this->error( 'cite_error_group_refs_without_references', htmlspecialchars( $group ) ); 1074 } 1075 } 1076 return true; 1077 } 1078 1079 /** 1080 * Hook for the InlineEditor extension. If any ref or reference reference tag is in the text, the entire 1081 * page should be reparsed, so we return false in that case. 1082 * 1083 * @param $output 1084 * 1085 * @return bool 1086 */ 1087 function checkAnyCalls( &$output ) { 1088 global $wgParser; 1089 /* InlineEditor always uses $wgParser */ 1090 return ( $wgParser->extCite->mCallCnt <= 0 ); 1091 } 1092 1093 /** 1094 * Initialize the parser hooks 1095 * 1096 * @param $parser Parser 1097 * 1098 * @return bool 1099 */ 1100 static function setHooks( $parser ) { 1101 global $wgHooks; 1102 1103 $parser->extCite = new self(); 1104 1105 if ( !Cite::$hooksInstalled ) { 1106 $wgHooks['ParserClearState'][] = array( $parser->extCite, 'clearState' ); 1107 $wgHooks['ParserCloned'][] = array( $parser->extCite, 'cloneState' ); 1108 $wgHooks['ParserAfterParse'][] = array( $parser->extCite, 'checkRefsNoReferences', true ); 1109 $wgHooks['ParserBeforeTidy'][] = array( $parser->extCite, 'checkRefsNoReferences', false ); 1110 $wgHooks['InlineEditorPartialAfterParse'][] = array( $parser->extCite, 'checkAnyCalls' ); 1111 Cite::$hooksInstalled = true; 1112 } 1113 $parser->setHook( 'ref' , array( $parser->extCite, 'ref' ) ); 1114 $parser->setHook( 'references' , array( $parser->extCite, 'references' ) ); 1115 1116 return true; 1117 } 1118 1119 /** 1120 * Return an error message based on an error ID 1121 * 1122 * @param string $key Message name for the error 1123 * @param string $param Parameter to pass to the message 1124 * @param string $parse Whether to parse the message ('parse') or not ('noparse') 1125 * @return string XHTML or wikitext ready for output 1126 */ 1127 function error( $key, $param = null, $parse = 'parse' ) { 1128 # We rely on the fact that PHP is okay with passing unused argu- 1129 # ments to functions. If $1 is not used in the message, wfMessage will 1130 # just ignore the extra parameter. 1131 $ret = '<strong class="error mw-ext-cite-error">' . 1132 wfMessage( 'cite_error', wfMessage( $key, $param )->inContentLanguage()->plain() )->inContentLanguage()->plain() . 1133 '</strong>'; 1134 if ( $parse == 'parse' ) { 1135 $ret = $this->mParser->recursiveTagParse( $ret ); 1136 } 1137 return $ret; 1138 } 1139 1140 /**#@-*/ 1141 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |