205 if ( $this->mInCite ) {
206 return htmlspecialchars(
"<ref>$str</ref>" );
210 $this->mInCite =
true;
212 $ret = $this->
guardedRef( $str, $argv, $parser );
214 $this->mInCite =
false;
217 $parserOutput->addModules(
'ext.cite.ux-enhancements' );
218 $parserOutput->addModuleStyles(
'ext.cite.styles' );
224 $this->mBumpRefData =
true;
242 $this->mParser = $parser;
244 # The key here is the "name" attribute.
245 list( $key, $group, $follow, $dir ) = $this->
refArg( $argv );
247 if ( $dir ===
'' && $str !==
'' ) {
248 $str .= $this->
plainError(
'cite_error_ref_invalid_dir', $argv[
'dir'] );
250 # Split these into groups.
251 if ( $group ===
null ) {
252 if ( $this->mInReferences ) {
266 if ( $this->mInReferences ) {
267 $isSectionPreview = $parser->
getOptions()->getIsSectionPreview();
268 if ( $group != $this->mReferencesGroup ) {
269 # <ref> and <references> have conflicting group attributes.
270 $this->mReferencesErrors[] =
272 'cite_error_references_group_mismatch',
275 } elseif ( $str !==
'' ) {
276 if ( !$isSectionPreview && !isset( $this->mRefs[$group] ) ) {
277 # Called with group attribute not defined in text.
278 $this->mReferencesErrors[] =
280 'cite_error_references_missing_group',
283 } elseif ( $key ===
null || $key ===
'' ) {
284 # <ref> calls inside <references> must be named
285 $this->mReferencesErrors[] =
286 $this->
error(
'cite_error_references_no_key' );
287 } elseif ( !$isSectionPreview && !isset( $this->mRefs[$group][$key] ) ) {
288 # Called with name attribute not defined in text.
289 $this->mReferencesErrors[] =
293 isset( $this->mRefs[$group][$key][
'text'] ) &&
294 $str !== $this->mRefs[$group][$key][
'text']
298 $this->mRefs[$group][$key][
'text'] .=
' ' . $this->
plainError(
299 'cite_error_references_duplicate_key', $key
302 # Assign the text to corresponding ref
303 $this->mRefs[$group][$key][
'text'] = $str;
307 # <ref> called in <references> has no content.
308 $this->mReferencesErrors[] =
315 # <ref ...></ref>. This construct is invalid if
316 # it's a contentful ref, but OK if it's a named duplicate and should
317 # be equivalent <ref ... />, for compatability with #tag.
318 if ( is_string( $key ) && $key !==
'' ) {
321 $this->mRefCallStack[] =
false;
323 return $this->
error(
'cite_error_ref_no_input' );
327 if ( $key ===
false ) {
328 # TODO: Comment this case; what does this condition mean?
329 $this->mRefCallStack[] =
false;
330 return $this->
error(
'cite_error_ref_too_many_keys' );
333 if ( $str ===
null && $key ===
null ) {
334 # Something like <ref />; this makes no sense.
335 $this->mRefCallStack[] =
false;
336 return $this->
error(
'cite_error_ref_no_key' );
339 if ( is_string( $key ) && preg_match(
'/^\d+$/', $key ) ||
340 is_string( $follow ) && preg_match(
'/^\d+$/', $follow )
342 # Numeric names mess up the resulting id's, potentially produ-
343 # cing duplicate id's in the XHTML. The Right Thing To Do
344 # would be to mangle them, but it's not really high-priority
345 # (and would produce weird id's anyway).
347 $this->mRefCallStack[] =
false;
348 return $this->
error(
'cite_error_ref_numeric_key' );
353 preg_replace(
'#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#',
'', $str )
355 # (bug T8199) This most likely implies that someone left off the
356 # closing </ref> tag, which will cause the entire article to be
357 # eaten up until the next <ref>. So we bail out early instead.
358 # The fancy regex above first tries chopping out anything that
359 # looks like a comment or SGML tag, which is a crude way to avoid
360 # false alarms for <nowiki>, <pre>, etc.
362 # Possible improvement: print the warning, followed by the contents
363 # of the <ref> tag. This way no part of the article will be eaten
366 $this->mRefCallStack[] =
false;
367 return $this->
error(
'cite_error_included_ref' );
370 if ( is_string( $key ) || is_string( $str ) ) {
371 # We don't care about the content: if the key exists, the ref
372 # is presumptively valid. Either it stores a new ref, or re-
373 # fers to an existing one. If it refers to a nonexistent ref,
374 # we'll figure that out later. Likewise it's definitely valid
375 # if there's any content, regardless of key.
377 return $this->
stack( $str, $key, $group, $follow, $argv, $dir, $parser );
380 # Not clear how we could get here, but something is probably
381 # wrong with the types. Let's fail fast.
382 throw new Exception(
'Invalid $str and/or $key: ' .
serialize( [ $str, $key ] ) );
404 if ( isset( $argv[
'dir'] ) ) {
407 $isValidDir = in_array( strtolower( $argv[
'dir'] ), [
'ltr',
'rtl' ] );
412 unset( $argv[
'dir'] );
415 if ( $argv === [] ) {
417 return [
null,
null,
false, $dir ];
420 if ( isset( $argv[
'name'] ) && isset( $argv[
'follow'] ) ) {
421 return [
false,
false,
false, false ];
424 if ( isset( $argv[
'name'] ) ) {
426 $key = trim( $argv[
'name'] );
427 unset( $argv[
'name'] );
429 if ( isset( $argv[
'follow'] ) ) {
431 $follow = trim( $argv[
'follow'] );
432 unset( $argv[
'follow'] );
434 if ( isset( $argv[
'group'] ) ) {
436 $group = $argv[
'group'];
437 unset( $argv[
'group'] );
440 if ( $argv !== [] ) {
442 return [
false,
false,
false, false ];
445 return [ $key, $group, $follow, $dir ];
462 private function stack( $str, $key, $group, $follow, array $call, $dir,
Parser $parser ) {
463 if ( !isset( $this->mRefs[$group] ) ) {
464 $this->mRefs[$group] = [];
466 if ( !isset( $this->mGroupCnt[$group] ) ) {
467 $this->mGroupCnt[$group] = 0;
469 if ( $follow !=
null ) {
470 if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) {
472 $this->mRefs[$group][$follow][
'text'] .=
' ' . $str;
475 $groupsCount = count( $this->mRefs[$group] );
476 for ( $k = 0; $k < $groupsCount; $k++ ) {
477 if ( !isset( $this->mRefs[$group][$k][
'follow'] ) ) {
481 array_splice( $this->mRefs[$group], $k, 0, [ [
484 'key' => ++$this->mOutCnt,
488 array_splice( $this->mRefCallStack, $k, 0,
489 [ [
'new', $call, $str, $key, $group, $this->mOutCnt ] ] );
495 if ( $key ===
null ) {
499 $this->mRefs[$group][] = [
505 $this->mRefCallStack[] = [
'new', $call, $str, $key, $group,
$this->mOutCnt ];
507 return $this->
linkRef( $group, $this->mOutCnt );
509 if ( !is_string( $key ) ) {
510 throw new Exception(
'Invalid stack key: ' .
serialize( $key ) );
514 if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) {
516 $this->mRefs[$group][$key] = [
520 'number' => ++$this->mGroupCnt[$group],
523 $this->mRefCallStack[] = [
'new', $call, $str, $key, $group,
$this->mOutCnt ];
528 $this->mRefs[$group][$key][
'key'] .
"-" . $this->mRefs[$group][$key][
'count'],
529 $this->mRefs[$group][$key][
'number'],
530 "-" . $this->mRefs[$group][$key][
'key']
535 if ( $this->mRefs[$group][$key][
'text'] ===
null && $str !==
'' ) {
537 $this->mRefs[$group][$key][
'text'] = $str;
539 $this->mRefs[$group][$key][
'dir'] = $dir;
540 $this->mRefCallStack[] = [
'assign', $call, $str, $key, $group,
541 $this->mRefs[$group][$key][
'key'] ];
543 if ( $str !=
null && $str !==
''
545 && $parser->mStripState->unstripBoth( $str )
546 !== $parser->mStripState->unstripBoth( $this->mRefs[$group][$key][
'text'] )
550 $this->mRefs[$group][$key][
'text'] .=
' ' . $this->
plainError(
551 'cite_error_references_duplicate_key', $key
554 $this->mRefCallStack[] = [
'increment', $call, $str, $key, $group,
555 $this->mRefs[$group][$key][
'key'] ];
560 $this->mRefs[$group][$key][
'key'] .
"-" . ++$this->mRefs[$group][$key][
'count'],
561 $this->mRefs[$group][$key][
'number'],
562 "-" . $this->mRefs[$group][$key][
'key']
588 if ( !isset( $this->mRefs[$group] ) ) {
592 if ( $key ===
null ) {
593 foreach ( $this->mRefs[$group] as $k => $v ) {
594 if ( $this->mRefs[$group][$k][
'key'] === $index ) {
602 if ( $key ===
null ) {
605 if ( !isset( $this->mRefs[$group][$key] ) ) {
608 if ( $this->mRefs[$group][$key][
'key'] != $index ) {
614 # Rollback the addition of new elements to the stack.
615 unset( $this->mRefs[$group][$key] );
616 if ( $this->mRefs[$group] === [] ) {
617 unset( $this->mRefs[$group] );
618 unset( $this->mGroupCnt[$group] );
622 # Rollback assignment of text to pre-existing elements.
623 $this->mRefs[$group][$key][
'text'] =
null;
624 # continue without break
626 # Rollback increase in named ref occurrences.
627 $this->mRefs[$group][$key][
'count']--;
643 if ( $this->mInCite || $this->mInReferences ) {
644 if ( $str ===
null ) {
645 return htmlspecialchars(
"<references/>" );
647 return htmlspecialchars(
"<references>$str</references>" );
650 $this->mInReferences =
true;
652 $this->mInReferences =
false;
671 global $wgCiteResponsiveReferences;
673 $this->mParser = $parser;
675 if ( isset( $argv[
'group'] ) ) {
676 $group = $argv[
'group'];
677 unset( $argv[
'group'] );
682 if ( strval( $str ) !==
'' ) {
683 $this->mReferencesGroup = $group;
685 # Detect whether we were sent already rendered <ref>s.
686 # Mostly a side effect of using #tag to call references.
687 # The following assumes that the parsed <ref>s sent within
688 # the <references> block were the most recent calls to
689 # <ref>. This assumption is true for all known use cases,
690 # but not strictly enforced by the parser. It is possible
691 # that some unusual combination of #tag, <references> and
692 # conditional parser functions could be created that would
693 # lead to malformed references here.
697 # Undo effects of calling <ref> while unaware of containing <references>
698 for ( $i = 1; $i <= $count; $i++ ) {
699 if ( !$this->mRefCallStack ) {
703 $call = array_pop( $this->mRefCallStack );
704 $redoStack[] = $call;
705 if ( $call !==
false ) {
706 list(
$type, $ref_argv, $ref_str,
707 $ref_key, $ref_group, $ref_index ) = $call;
712 # Rerun <ref> call now that mInReferences is set.
713 for ( $i = count( $redoStack ) - 1; $i >= 0; $i-- ) {
714 $call = $redoStack[$i];
715 if ( $call !==
false ) {
716 list(
$type, $ref_argv, $ref_str,
717 $ref_key, $ref_group, $ref_index ) = $call;
718 $this->
guardedRef( $ref_str, $ref_argv, $parser );
722 # Parse $str to process any unparsed <ref> tags.
726 $this->mRefCallStack = [];
729 if ( isset( $argv[
'responsive'] ) ) {
730 $responsive = $argv[
'responsive'] !==
'0';
731 unset( $argv[
'responsive'] );
733 $responsive = $wgCiteResponsiveReferences;
738 return $this->
error(
'cite_error_references_invalid_parameters' );
743 # Append errors generated while processing <references>
744 if ( $this->mReferencesErrors ) {
745 $s .=
"\n" . implode(
"<br />\n", $this->mReferencesErrors );
746 $this->mReferencesErrors = [];
762 if ( !$this->mRefs || !isset( $this->mRefs[$group] ) ) {
767 foreach ( $this->mRefs[$group] as $k => $v ) {
774 "\n" . implode(
"\n", $ent ) .
"\n"
778 $ret = rtrim( $this->mParser->recursiveTagParse( $parserInput ),
"\n" );
783 $wrapClasses = [
'mw-references-wrap' ];
784 if ( count( $this->mRefs[$group] ) > 10 ) {
785 $wrapClasses[] =
'mw-references-columns';
790 if ( !$this->mParser->getOptions()->getIsPreview() ) {
796 unset( $this->mRefs[$group] );
797 unset( $this->mGroupCnt[$group] );
812 if ( !is_array( $val ) ) {
814 'cite_references_link_one',
816 self::getReferencesKey( $key )
823 )->inContentLanguage()->plain();
826 if ( isset( $val[
'follow'] ) ) {
828 'cite_references_no_link',
830 self::getReferencesKey( $val[
'follow'] )
833 )->inContentLanguage()->plain();
835 if ( !isset( $val[
'count'] ) ) {
837 return wfMessage(
'cite_references_link_many',
839 self::getReferencesKey( $key .
"-" . ( $val[
'key'] ??
'' ) )
843 )->inContentLanguage()->plain();
845 if ( $val[
'count'] < 0 ) {
847 'cite_references_link_one',
849 self::getReferencesKey( $val[
'key'] )
852 # $this->
refKey( $val[
'key'], $val[
'count'] )
853 $this->
refKey( $val[
'key'] )
857 )->inContentLanguage()->plain();
863 if ( $val[
'count'] === 0 ) {
865 'cite_references_link_one',
867 self::getReferencesKey( $key .
"-" . $val[
'key'] )
870 # $this->
refKey( $key, $val[
'count'] ),
871 $this->
refKey( $key, $val[
'key'] .
"-" . $val[
'count'] )
875 )->inContentLanguage()->plain();
880 for ( $i = 0; $i <= $val[
'count']; ++$i ) {
882 'cite_references_link_many_format',
884 $this->
refKey( $key, $val[
'key'] .
"-$i" )
888 )->inContentLanguage()->plain();
893 return wfMessage(
'cite_references_link_many',
895 self::getReferencesKey( $key .
"-" . $val[
'key'] )
900 )->inContentLanguage()->plain();
910 if ( $text ===
null || $text ===
'' ) {
911 if ( $this->mParser->getOptions()->getIsSectionPreview() ) {
912 return $this->
warning(
'cite_warning_sectionpreview_no_text', $key,
'noparse' );
914 return $this->
plainError(
'cite_error_references_no_text', $key );
916 return '<span class="reference-text">' . rtrim( $text,
"\n" ) .
"</span>\n";
930 $scope = strlen( $max );
931 $ret = MediaWikiServices::getInstance()->getContentLanguage()->formatNum(
932 sprintf(
"%s.%0{$scope}s",
$base, $offset )
948 if ( !isset( $this->mBacklinkLabels ) ) {
951 if ( isset( $this->mBacklinkLabels[$offset] ) ) {
952 return $this->mBacklinkLabels[$offset];
955 return $this->
plainError(
'cite_error_references_no_backlink_label',
null );
972 $message =
"cite_link_label_group-$group";
973 if ( !isset( $this->mLinkLabels[$group] ) ) {
976 if ( $this->mLinkLabels[$group] ===
false ) {
981 if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) {
982 return $this->mLinkLabels[$group][$offset - 1];
985 return $this->
plainError(
'cite_error_no_link_label_group', [ $group, $message ] );
998 private function refKey( $key, $num =
null ) {
999 $prefix =
wfMessage(
'cite_reference_link_prefix' )->inContentLanguage()->text();
1000 $suffix =
wfMessage(
'cite_reference_link_suffix' )->inContentLanguage()->text();
1001 if ( $num !==
null ) {
1002 $key =
wfMessage(
'cite_reference_link_key_with_num', $key, $num )
1003 ->inContentLanguage()->plain();
1006 return "$prefix$key$suffix";
1018 $prefix =
wfMessage(
'cite_references_link_prefix' )->inContentLanguage()->text();
1019 $suffix =
wfMessage(
'cite_references_link_suffix' )->inContentLanguage()->text();
1021 return "$prefix$key$suffix";
1040 private function linkRef( $group, $key, $count =
null, $label =
null, $subkey =
'' ) {
1041 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1043 if ( $label ===
null ) {
1044 $label = ++$this->mGroupCnt[$group];
1047 return $this->mParser->recursiveTagParse(
1049 'cite_reference_link',
1051 $this->
refKey( $key, $count )
1054 self::getReferencesKey( $key . $subkey )
1058 ( ( $group === self::DEFAULT_GROUP ) ?
'' :
"$group " ) . $contLang->formatNum( $label ) )
1060 )->inContentLanguage()->plain()
1072 $ret = preg_replace(
'/__+/',
'_', $ret );
1089 $cnt = count( $arr );
1092 return (
string)$arr[0];
1095 $sep =
wfMessage(
'cite_references_link_many_sep' )->inContentLanguage()->plain();
1096 $and =
wfMessage(
'cite_references_link_many_and' )->inContentLanguage()->plain();
1097 $t = array_slice( $arr, 0, $cnt - 1 );
1098 return implode( $sep,
$t ) . $and . $arr[$cnt - 1];
1107 $text =
wfMessage(
'cite_references_link_many_format_backlink_labels' )
1108 ->inContentLanguage()->plain();
1109 $this->mBacklinkLabels = preg_split(
'/\s+/', $text );
1122 $msg =
wfMessage( $message )->inContentLanguage();
1123 if ( $msg->exists() ) {
1124 $text = $msg->plain();
1126 $this->mLinkLabels[$group] = $text ? preg_split(
'/\s+/', $text ) :
false;
1136 if ( $parser->extCite !== $this ) {
1141 # Don't clear state when we're in the middle of parsing
1143 if ( $this->mInCite || $this->mInReferences ) {
1147 $this->mGroupCnt = [];
1149 $this->mCallCnt = 0;
1151 $this->mReferencesErrors = [];
1152 $this->mRefCallStack = [];
1161 if ( $parser->extCite !== $this ) {
1162 $parser->extCite->cloneState( $parser );
1166 $parser->extCite = clone $this;
1167 $parser->
setHook(
'ref', [ $parser->extCite,
'ref' ] );
1168 $parser->
setHook(
'references', [ $parser->extCite,
'references' ] );
1171 $parser->extCite->mInCite =
false;
1172 $parser->extCite->mInReferences =
false;
1189 global $wgCiteResponsiveReferences;
1190 if ( $parser->extCite ===
null ) {
1193 if ( $parser->extCite !== $this ) {
1194 $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text );
1198 if ( $afterParse ) {
1199 $this->mHaveAfterParse =
true;
1200 } elseif ( $this->mHaveAfterParse ) {
1204 if ( !$parser->getOptions()->getIsPreview() ) {
1206 if ( $this->mRefs && isset( $this->mRefs[self::DEFAULT_GROUP] ) ) {
1209 $isSectionPreview =
false;
1211 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
1215 foreach ( $this->mRefs as $group => $refs ) {
1219 if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
1220 $this->mInReferences =
true;
1222 $this->mInReferences =
false;
1226 'cite_error_group_refs_without_references',
1231 if ( $isSectionPreview &&
$s !==
'' ) {
1233 $text .=
"\n" .
'<div class="mw-ext-cite-cite_section_preview_references" >';
1234 $headerMsg =
wfMessage(
'cite_section_preview_references' );
1235 if ( !$headerMsg->isDisabled() ) {
1236 $text .=
'<h2 id="mw-ext-cite-cite_section_preview_references_header" >'
1237 . $headerMsg->escaped()
1240 $text .=
$s .
'</div>';
1254 global $wgCiteStoreReferencesData;
1255 if ( !$wgCiteStoreReferencesData ) {
1258 $savedRefs = $this->mParser->getOutput()->getExtensionData( self::EXT_DATA_KEY );
1259 if ( $savedRefs ===
null ) {
1266 if ( $this->mBumpRefData ) {
1270 $savedRefs[
'refs'][] = [];
1271 $this->mBumpRefData =
false;
1273 $n = count( $savedRefs[
'refs'] ) - 1;
1275 $savedRefs[
'refs'][$n][$group] = $this->mRefs[$group];
1277 $this->mParser->getOutput()->setExtensionData( self::EXT_DATA_KEY, $savedRefs );
1288 $parser->extCite =
new self();
1290 if ( !self::$hooksInstalled ) {
1291 $wgHooks[
'ParserClearState'][] = [ $parser->extCite,
'clearState' ];
1292 $wgHooks[
'ParserCloned'][] = [ $parser->extCite,
'cloneState' ];
1293 $wgHooks[
'ParserAfterParse'][] = [ $parser->extCite,
'checkRefsNoReferences',
true ];
1294 $wgHooks[
'ParserBeforeTidy'][] = [ $parser->extCite,
'checkRefsNoReferences', false ];
1295 self::$hooksInstalled =
true;
1297 $parser->
setHook(
'ref', [ $parser->extCite,
'ref' ] );
1298 $parser->
setHook(
'references', [ $parser->extCite,
'references' ] );
1308 private function error( $key, $param =
null ) {
1310 return $this->mParser->recursiveTagParse( $error );
1322 # For ease of debugging and because errors are rare, we
1323 # use the user language and split the parser cache.
1324 $lang = $this->mParser->getOptions()->getUserLangObj();
1325 $dir =
$lang->getDir();
1327 # We rely on the fact that PHP is okay with passing unused argu-
1328 # ments to functions. If $1 is not used in the message, wfMessage will
1329 # just ignore the extra parameter.
1334 ->inLanguage(
$lang )
1337 $this->mParser->addTrackingCategory(
'cite-tracking-category-cite-error' );
1342 'class' =>
'error mw-ext-cite-error',
1343 'lang' =>
$lang->getHtmlCode(),
1360 private function warning( $key, $param =
null, $parse =
'parse' ) {
1361 # For ease of debugging and because errors are rare, we
1362 # use the user language and split the parser cache.
1363 $lang = $this->mParser->getOptions()->getUserLangObj();
1364 $dir =
$lang->getDir();
1366 # We rely on the fact that PHP is okay with passing unused argu-
1367 # ments to functions. If $1 is not used in the message, wfMessage will
1368 # just ignore the extra parameter.
1373 ->inLanguage(
$lang )
1376 $key = preg_replace(
'/^cite_warning_/',
'', $key ) .
'';
1380 'class' =>
'warning mw-ext-cite-warning mw-ext-cite-warning-' .
1382 'lang' =>
$lang->getHtmlCode(),
1388 if ( $parse ===
'parse' ) {
1389 $ret = $this->mParser->recursiveTagParse( $ret );
1403 global $wgCiteStoreReferencesData;
1404 if ( !$wgCiteStoreReferencesData ) {
1407 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1408 $key =
$cache->makeKey( self::EXT_DATA_KEY,
$title->getArticleID() );
1409 return $cache->getWithSetCallback(
1411 self::CACHE_DURATION_ONFETCH,
1412 function ( $oldValue, &$ttl, array &$setOpts ) use (
$title ) {
1414 $setOpts += Database::getCacheSetOptions(
$dbr );
1418 'checkKeys' => [ $key ],
1436 $string =
'', $i = 1 ) {
1437 $id =
$title->getArticleID();
1438 $result =
$dbr->selectField(
1443 'pp_propname' =>
'references-' . $i
1447 if ( $result !==
false ) {
1449 $decodedString = gzdecode( $string );
1450 if ( $decodedString !==
false ) {
1451 $json = json_decode( $decodedString,
true );
1452 if ( json_last_error() === JSON_ERROR_NONE ) {
1457 wfDebug(
"Corrupted json detected when retrieving stored references for title id $id" );
1466 wfDebug(
"Failed to retrieve stored references for title id $id" );