2
if ( ! defined( 'MEDIAWIKI' ) )
5
* A parser extension that adds two tags, <ref> and <references> for adding
9
* @subpackage Extensions
11
* @link http://meta.wikimedia.org/wiki/Cite/Cite.php Documentation
12
* @link http://www.w3.org/TR/html4/struct/text.html#edef-CITE <cite> definition in HTML
13
* @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite <cite> definition in XHTML 2.0
17
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
18
* @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
19
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
22
$wgExtensionFunctions[] = 'wfCite';
23
$wgExtensionCredits['parserhook'][] = array(
25
'author' => 'Ævar Arnfjörð Bjarmason',
26
'description' => 'adds <nowiki><ref[ name=id]></nowiki> and <nowiki><references/></nowiki> tags, for citations',
27
'url' => 'http://meta.wikimedia.org/wiki/Cite/Cite.php'
31
* Error codes, first array = internal errors; second array = user errors
33
$wgCiteErrors = array(
35
'CITE_ERROR_STR_INVALID',
36
'CITE_ERROR_KEY_INVALID_1',
37
'CITE_ERROR_KEY_INVALID_2',
38
'CITE_ERROR_STACK_INVALID_INPUT'
41
'CITE_ERROR_REF_NUMERIC_KEY',
42
'CITE_ERROR_REF_NO_KEY',
43
'CITE_ERROR_REF_TOO_MANY_KEYS',
44
'CITE_ERROR_REF_NO_INPUT',
45
'CITE_ERROR_REFERENCES_INVALID_INPUT',
46
'CITE_ERROR_REFERENCES_INVALID_PARAMETERS',
47
'CITE_ERROR_REFERENCES_NO_BACKLINK_LABEL'
51
for ( $i = 0; $i < count( $wgCiteErrors['system'] ); ++$i )
52
// System errors are negative integers
53
define( $wgCiteErrors['system'][$i], -($i + 1) );
54
for ( $i = 0; $i < count( $wgCiteErrors['user'] ); ++$i )
55
// User errors are positive integers
56
define( $wgCiteErrors['user'][$i], $i + 1 );
59
global $wgMessageCache;
60
$wgMessageCache->addMessages(
67
'cite_croak' => 'Cite croaked; $1: $2',
69
'cite_error_' . CITE_ERROR_STR_INVALID => 'Internal error; invalid $str',
70
'cite_error_' . CITE_ERROR_KEY_INVALID_1 => 'Internal error; invalid key',
71
'cite_error_' . CITE_ERROR_KEY_INVALID_2 => 'Internal error; invalid key',
72
'cite_error_' . CITE_ERROR_STACK_INVALID_INPUT => 'Internal error; invalid stack key',
75
'cite_error' => 'Cite error $1; $2',
77
'cite_error_' . CITE_ERROR_REF_NUMERIC_KEY => 'Invalid call; expecting a non-integer key',
78
'cite_error_' . CITE_ERROR_REF_NO_KEY => 'Invalid call; no key specified',
79
'cite_error_' . CITE_ERROR_REF_TOO_MANY_KEYS => 'Invalid call; invalid keys, e.g. too many or wrong key specified',
80
'cite_error_' . CITE_ERROR_REF_NO_INPUT => 'Invalid call; no input specified',
81
'cite_error_' . CITE_ERROR_REFERENCES_INVALID_INPUT => 'Invalid input; expecting none',
82
'cite_error_' . CITE_ERROR_REFERENCES_INVALID_PARAMETERS => 'Invalid parameters; expecting none',
83
'cite_error_' . CITE_ERROR_REFERENCES_NO_BACKLINK_LABEL => "Ran out of custom backlink labels, define more in the \"''cite_references_link_many_format_backlink_labels''\" message",
88
'cite_reference_link_key_with_num' => '$1_$2',
89
// Ids produced by <ref>
90
'cite_reference_link_prefix' => '_ref-',
91
'cite_reference_link_suffix' => '',
92
// Ids produced by <references>
93
'cite_references_link_prefix' => '_note-',
94
'cite_references_link_suffix' => '',
96
'cite_reference_link' => '<sup id="$1" class="reference">[[#$2|<nowiki>[</nowiki>$3<nowiki>]</nowiki>]]</sup>',
97
'cite_references_link_one' => '<li id="$1">[[#$2|↑]] $3</li>',
98
'cite_references_link_many' => '<li id="$1">↑ $2 $3</li>',
99
'cite_references_link_many_format' => '[[#$1|<sup>$2</sup>]]',
100
// An item from this set is passed as $3 in the message above
101
'cite_references_link_many_format_backlink_labels' => 'a b c d e f g h i j k l m n o p q r s t u v w x y z',
102
'cite_references_link_many_sep' => "\xc2\xa0", //
103
'cite_references_link_many_and' => "\xc2\xa0", // &nbps;
105
// Although I could just use # instead of <li> above and nothing here that
106
// will break on input that contains linebreaks
107
'cite_references_prefix' => '<ol class="references">',
108
'cite_references_suffix' => '</ol>',
118
* Datastructure representing <ref> input, in the format of:
121
* 'user supplied' => array(
122
* 'text' => 'user supplied reference & key',
123
* 'count' => 1, // occurs twice
124
* 'number' => 1, // The first reference, we want
125
* // all occourances of it to
126
* // use the same number
128
* 0 => 'Anonymous reference',
129
* 1 => 'Another anonymous reference',
130
* 'some key' => array(
131
* 'text' => 'this one occurs once'
139
* This works because:
140
* * PHP's datastructures are guarenteed to be returned in the
141
* order that things are inserted into them (unless you mess
143
* * User supplied keys can't be integers, therefore avoiding
144
* conflict with anonymous keys
148
var $mRefs = array();
151
* Count for user displayed output (ref[1], ref[2], ...)
158
* Internal counter for anonymous references, seperate from
159
* $mOutCnt because anonymous references won't increment it,
160
* but will incremement $mOutCnt
167
* The backlinks, in order, to pass as $3 to
168
* 'cite_references_link_many_format', defined in
169
* 'cite_references_link_many_format_backlink_labels
173
var $mBacklinkLabels;
181
* True when a <ref> or <references> tag is being processed.
182
* Used to avoid infinite recursion
186
var $mInCite = false;
197
/**#@+ @access private */
200
* Callback function for <ref>
202
* @param string $str Input
203
* @param array $argv Arguments
206
function ref( $str, $argv, $parser ) {
207
if ( $this->mInCite ) {
208
return htmlspecialchars( "<ref>$str</ref>" );
210
$this->mInCite = true;
211
$ret = $this->guardedRef( $str, $argv, $parser );
212
$this->mInCite = false;
217
function guardedRef( $str, $argv, $parser ) {
218
$this->mParser = $parser;
219
$key = $this->refArg( $argv );
221
if ( $str !== null ) {
223
return $this->error( CITE_ERROR_REF_NO_INPUT );
224
if ( is_string( $key ) )
225
// I don't want keys in the form of /^[0-9]+$/ because they would
226
// conflict with the php datastructure I'm using, besides, why specify
227
// a manual key if it's just going to be any old integer?
228
if ( sprintf( '%d', $key ) === (string)$key )
229
return $this->error( CITE_ERROR_REF_NUMERIC_KEY );
231
return $this->stack( $str, $key );
232
else if ( $key === null )
233
return $this->stack( $str );
234
else if ( $key === false )
235
return $this->error( CITE_ERROR_REF_TOO_MANY_KEYS );
237
$this->croak( CITE_ERROR_KEY_INVALID_1, serialize( $key ) );
238
} else if ( $str === null ) {
239
if ( is_string( $key ) )
240
if ( sprintf( '%d', $key ) === (string)$key )
241
return $this->error( CITE_ERROR_REF_NUMERIC_KEY );
243
return $this->stack( $str, $key );
244
else if ( $key === false )
245
return $this->error( CITE_ERROR_REF_TOO_MANY_KEYS );
246
else if ( $key === null )
247
return $this->error( CITE_ERROR_REF_NO_KEY );
249
$this->croak( CITE_ERROR_KEY_INVALID_2, serialize( $key ) );
252
$this->croak( CITE_ERROR_STR_INVALID, serialize( $str ) );
256
* Parse the arguments to the <ref> tag
260
* @param array $argv The argument vector
261
* @return mixed false on invalid input, a string on valid
262
* input and null on no input
264
function refArg( $argv ) {
266
$cnt = count( $argv );
269
// There should only be one key
271
else if ( $cnt == 1 )
272
if ( isset( $argv['name'] ) )
274
return $this->validateName( array_shift( $argv ) );
284
* Since the key name is used in an XHTML id attribute, it must
285
* conform to the validity rules. The restriction to begin with
286
* a letter is lifted since references have their own prefix.
288
* @fixme merge this code with the various section name transformations
289
* @fixme double-check for complete validity
290
* @return string if valid, false if invalid
292
function validateName( $name ) {
293
if( preg_match( '/^[A-Za-z0-9:_.-]*$/i', $name ) ) {
296
// WARNING: CRAPPY CUT AND PASTE MAKES BABY JESUS CRY
297
$text = urlencode( str_replace( ' ', '_', $name ) );
298
$replacearray = array(
303
array_keys( $replacearray ),
304
array_values( $replacearray ),
310
* Populate $this->mRefs based on input and arguments to <ref>
312
* @param string $str Input from the <ref> tag
313
* @param mixed $key Argument to the <ref> tag as returned by $this->refArg()
316
function stack( $str, $key = null ) {
317
if ( $key === null ) {
319
$this->mRefs[] = $str;
320
return $this->linkRef( $this->mInCnt++ );
321
} else if ( is_string( $key ) )
323
if ( ! @is_array( $this->mRefs[$key] ) ) {
325
$this->mRefs[$key] = array(
328
'number' => ++$this->mOutCnt
333
$this->mRefs[$key]['count'],
334
$this->mRefs[$key]['number']
337
// We've been here before
341
++$this->mRefs[$key]['count'],
342
$this->mRefs[$key]['number']
345
$this->croak( CITE_ERROR_STACK_INVALID_INPUT, serialize( array( $key, $str ) ) );
349
* Callback function for <references>
351
* @param string $str Input
352
* @param array $argv Arguments
355
function references( $str, $argv, $parser ) {
356
if ( $this->mInCite ) {
357
if ( is_null( $str ) ) {
358
return htmlspecialchars( "<references/>" );
360
return htmlspecialchars( "<references>$str</references>" );
363
$this->mInCite = true;
364
$ret = $this->guardedReferences( $str, $argv, $parser );
365
$this->mInCite = false;
370
function guardedReferences( $str, $argv, $parser ) {
371
$this->mParser = $parser;
373
return $this->error( CITE_ERROR_REFERENCES_INVALID_INPUT );
374
else if ( count( $argv ) )
375
return $this->error( CITE_ERROR_REFERENCES_INVALID_PARAMETERS );
377
return $this->referencesFormat();
381
* Make output to be returned from the references() function
383
* @return string XHTML ready for output
385
function referencesFormat() {
388
foreach ( $this->mRefs as $k => $v )
389
$ent[] = $this->referencesFormatEntry( $k, $v );
391
$prefix = wfMsgForContentNoTrans( 'cite_references_prefix' );
392
$suffix = wfMsgForContentNoTrans( 'cite_references_suffix' );
393
$content = implode( "\n", $ent );
395
// Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar
396
return rtrim( $this->parse( $prefix . $content . $suffix ), "\n" );
400
* Format a single entry for the referencesFormat() function
402
* @param string $key The key of the reference
403
* @param mixed $val The value of the reference, string for anonymous
404
* references, array for user-suppplied
405
* @return string Wikitext
407
function referencesFormatEntry( $key, $val ) {
408
// Anonymous reference
409
if ( ! is_array( $val ) )
411
wfMsgForContentNoTrans(
412
'cite_references_link_one',
413
$this->referencesKey( $key ),
414
$this->refKey( $key ),
417
// Standalone named reference, I want to format this like an
418
// anonymous reference because displaying "1. 1.1 Ref text" is
419
// overkill and users frequently use named references when they
420
// don't need them for convenience
421
else if ( $val['count'] === 0 )
423
wfMsgForContentNoTrans(
424
'cite_references_link_one',
425
$this->referencesKey( $key ),
426
$this->refKey( $key, $val['count'] ),
429
// Named references with >1 occurrences
433
for ( $i = 0; $i <= $val['count']; ++$i ) {
434
$links[] = wfMsgForContentNoTrans(
435
'cite_references_link_many_format',
436
$this->refKey( $key, $i ),
437
$this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),
438
$this->referencesFormatEntryAlternateBacklinkLabel( $i )
442
$list = $this->listToText( $links );
445
wfMsgForContentNoTrans( 'cite_references_link_many',
446
$this->referencesKey( $key ),
454
* Generate a numeric backlink given a base number and an
455
* offset, e.g. $base = 1, $offset = 2; = 1.2
456
* Since bug #5525, it correctly does 1.9 -> 1.10 as well as 1.099 -> 1.100
460
* @param int $base The base
461
* @param int $offset The offset
462
* @param int $max Maximum value expected.
465
function referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) {
467
$scope = strlen( $max );
468
$ret = $wgContLang->formatNum(
469
sprintf("%s.%0{$scope}s", $base, $offset)
475
* Generate a custom format backlink given an offset, e.g.
476
* $offset = 2; = c if $this->mBacklinkLabels = array( 'a',
477
* 'b', 'c', ...). Return an error if the offset > the # of
480
* @param int $offset The offset
484
function referencesFormatEntryAlternateBacklinkLabel( $offset ) {
485
if ( !isset( $this->mBacklinkLabels ) ) {
486
$this->genBacklinkLabels();
488
if ( isset( $this->mBacklinkLabels[$offset] ) ) {
489
return $this->mBacklinkLabels[$offset];
492
return $this->error( CITE_ERROR_REFERENCES_NO_BACKLINK_LABEL );
497
* Return an id for use in wikitext output based on a key and
498
* optionally the # of it, used in <references>, not <ref>
499
* (since otherwise it would link to itself)
503
* @param string $key The key
504
* @param int $num The number of the key
505
* @return string A key for use in wikitext
507
function refKey( $key, $num = null ) {
508
$prefix = wfMsgForContent( 'cite_reference_link_prefix' );
509
$suffix = wfMsgForContent( 'cite_reference_link_suffix' );
511
$key = wfMsgForContentNoTrans( 'cite_reference_link_key_with_num', $key, $num );
513
return $prefix . $key . $suffix;
517
* Return an id for use in wikitext output based on a key and
518
* optionally the # of it, used in <ref>, not <references>
519
* (since otherwise it would link to itself)
523
* @param string $key The key
524
* @param int $num The number of the key
525
* @return string A key for use in wikitext
527
function referencesKey( $key, $num = null ) {
528
$prefix = wfMsgForContent( 'cite_references_link_prefix' );
529
$suffix = wfMsgForContent( 'cite_references_link_suffix' );
531
$key = wfMsgForContentNoTrans( 'cite_reference_link_key_with_num', $key, $num );
533
return $prefix . $key . $suffix;
537
* Generate a link (<sup ...) for the <ref> element from a key
538
* and return XHTML ready for output
540
* @param string $key The key for the link
541
* @param int $count The # of the key, used for distinguishing
542
* multiple occourances of the same key
543
* @param int $label The label to use for the link, I want to
544
* use the same label for all occourances of
545
* the same named reference.
548
function linkRef( $key, $count = null, $label = null ) {
553
wfMsgForContentNoTrans(
554
'cite_reference_link',
555
$this->refKey( $key, $count ),
556
$this->referencesKey( $key ),
557
$wgContLang->formatNum( is_null( $label ) ? ++$this->mOutCnt : $label )
563
* This does approximately the same thing as
564
* Langauge::listToText() but due to this being used for a
565
* slightly different purpose (people might not want , as the
566
* first seperator and not 'and' as the second, and this has to
567
* use messages from the content language) I'm rolling my own.
571
* @param array $arr The array to format
574
function listToText( $arr ) {
575
$cnt = count( $arr );
577
$sep = wfMsgForContentNoTrans( 'cite_references_link_many_sep' );
578
$and = wfMsgForContentNoTrans( 'cite_references_link_many_and' );
581
// Enforce always returning a string
582
return (string)$arr[0];
584
$t = array_slice( $arr, 0, $cnt - 1 );
585
return implode( $sep, $t ) . $and . $arr[$cnt - 1];
590
* Parse a given fragment and fix up Tidy's trail of blood on
593
* @param string $in The text to parse
594
* @return string The parsed text
596
function parse( $in ) {
597
$ret = $this->mParser->parse(
599
$this->mParser->mTitle,
600
$this->mParser->mOptions,
601
// Avoid whitespace buildup
603
// Important, otherwise $this->clearState()
604
// would get run every time <ref> or
605
// <references> is called, fucking the whole
609
$text = $ret->getText();
611
return $this->fixTidy( $text );
615
* Tidy treats all input as a block, it will e.g. wrap most
616
* input in <p> if it isn't already, fix that and return the fixed text
620
* @param string $text The text to fix
621
* @return string The fixed text
623
function fixTidy( $text ) {
629
$text = preg_replace( '~^<p>\s*~', '', $text );
630
$text = preg_replace( '~\s*</p>\s*~', '', $text );
631
$text = preg_replace( '~\n$~', '', $text );
638
* Generate the labels to pass to the
639
* 'cite_references_link_many_format' message, the format is an
640
* arbitary number of tokens seperated by [\t\n ]
642
function genBacklinkLabels() {
643
wfProfileIn( __METHOD__ );
644
$text = wfMsgForContentNoTrans( 'cite_references_link_many_format_backlink_labels' );
645
$this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text );
646
wfProfileOut( __METHOD__ );
650
* Gets run when Parser::clearState() gets run, since we don't
651
* want the counts to transcend pages and other instances
653
function clearState() {
654
$this->mOutCnt = $this->mInCnt = 0;
655
$this->mRefs = array();
661
* Initialize the parser hooks
663
function setHooks() {
664
global $wgParser, $wgHooks;
666
$wgParser->setHook( 'ref' , array( &$this, 'ref' ) );
667
$wgParser->setHook( 'references' , array( &$this, 'references' ) );
669
$wgHooks['ParserClearState'][] = array( &$this, 'clearState' );
673
* Return an error message based on an error ID
675
* @param int $id ID for the error
676
* @return string XHTML ready for output
678
function error( $id ) {
680
// User errors are positive
683
'<strong class="error">' .
684
wfMsgforContent( 'cite_error', $id, wfMsgForContent( "cite_error_$id" ) ) .
688
return wfMsgforContent( 'cite_error', $id );
692
* Die with a backtrace if something happens in the code which
695
* @param int $error ID for the error
696
* @param string $data Serialized error data
698
function croak( $error, $data ) {
699
wfDebugDieBacktrace( wfMsgForContent( 'cite_croak', $this->error( $error ), $data ) );