mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Cite
synced 2024-09-24 02:38:57 +00:00
Extract stack and state to a new class
Most of this state is used to manage interactions with other state, and encapsulation allows us to hide data structures and access behind self-explanatory function names. The interface is still much wider than I'd like, but it can be improved in future work. There is one small behavior change in here: in the `follows` edge case demonstrated by I3bdf26fd14, we prepend if the splice point cannot be used because it has a non-numeric key. I believe this was the original intention of the logic, and is how the numeric case behaves. I've verified that when array_splice throws a warning about non-numeric key, it fails to add anything to the original array, so the broken follows ref disappeared. Bug: T237241 Change-Id: I091a0b71ee9aa78e841c2e328018e886a7217715
This commit is contained in:
parent
10e4a4353d
commit
8453e3ecd7
346
src/Cite.php
346
src/Cite.php
|
@ -31,7 +31,6 @@ use Parser;
|
|||
use ParserOptions;
|
||||
use ParserOutput;
|
||||
use Sanitizer;
|
||||
use StripState;
|
||||
|
||||
class Cite {
|
||||
|
||||
|
@ -69,57 +68,6 @@ class Cite {
|
|||
*/
|
||||
public const BOOK_REF_PROPERTY = 'ref-extends';
|
||||
|
||||
/**
|
||||
* Datastructure representing <ref> input, in the format of:
|
||||
* <code>
|
||||
* [
|
||||
* 'user supplied' => [
|
||||
* 'text' => 'user supplied reference & key',
|
||||
* 'count' => 1, // occurs twice
|
||||
* 'number' => 1, // The first reference, we want
|
||||
* // all occourances of it to
|
||||
* // use the same number
|
||||
* ],
|
||||
* 0 => [
|
||||
* 'text' => 'Anonymous reference',
|
||||
* 'count' => -1,
|
||||
* ],
|
||||
* 1 => [
|
||||
* 'text' => 'Another anonymous reference',
|
||||
* 'count' => -1,
|
||||
* ],
|
||||
* 'some key' => [
|
||||
* 'text' => 'this one occurs once'
|
||||
* 'count' => 0,
|
||||
* 'number' => 4
|
||||
* ],
|
||||
* 3 => 'more stuff'
|
||||
* ];
|
||||
* </code>
|
||||
*
|
||||
* This works because:
|
||||
* * PHP's datastructures are guaranteed to be returned in the
|
||||
* order that things are inserted into them (unless you mess
|
||||
* with that)
|
||||
* * User supplied keys can't be integers, therefore avoiding
|
||||
* conflict with anonymous keys
|
||||
*
|
||||
* @var array[][]
|
||||
*/
|
||||
private $mRefs = [];
|
||||
|
||||
/**
|
||||
* Count for user displayed output (ref[1], ref[2], ...)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $mOutCnt = 0;
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $mGroupCnt = [];
|
||||
|
||||
/**
|
||||
* The backlinks, in order, to pass as $3 to
|
||||
* 'cite_references_link_many_format', defined in
|
||||
|
@ -176,13 +124,9 @@ class Cite {
|
|||
private $mReferencesErrors = [];
|
||||
|
||||
/**
|
||||
* <ref> call stack
|
||||
* Used to cleanup out of sequence ref calls created by #tag
|
||||
* See description of function rollbackRef.
|
||||
*
|
||||
* @var (array|false)[]
|
||||
* @var ReferenceStack $referenceStack
|
||||
*/
|
||||
private $mRefCallStack = [];
|
||||
private $referenceStack;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
|
@ -199,6 +143,7 @@ class Cite {
|
|||
$parser->getOptions()->getUserLangObj(),
|
||||
$parser
|
||||
);
|
||||
$this->referenceStack = new ReferenceStack( $this->errorReporter );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,7 +213,7 @@ class Cite {
|
|||
if ( is_string( $name ) && $name !== '' ) {
|
||||
$text = null;
|
||||
} else {
|
||||
$this->mRefCallStack[] = false;
|
||||
$this->referenceStack->pushInvalidRef();
|
||||
return $this->errorReporter->halfParsed( 'cite_error_ref_no_input' );
|
||||
}
|
||||
}
|
||||
|
@ -277,13 +222,13 @@ class Cite {
|
|||
# Invalid attribute in the tag like <ref no_valid_attr="foo" />
|
||||
# or name and follow attribute used both in one tag checked in
|
||||
# Cite::refArg that returns false for the name then.
|
||||
$this->mRefCallStack[] = false;
|
||||
$this->referenceStack->pushInvalidRef();
|
||||
return $this->errorReporter->halfParsed( 'cite_error_ref_too_many_keys' );
|
||||
}
|
||||
|
||||
if ( $text === null && $name === null ) {
|
||||
# Something like <ref />; this makes no sense.
|
||||
$this->mRefCallStack[] = false;
|
||||
$this->referenceStack->pushInvalidRef();
|
||||
return $this->errorReporter->halfParsed( 'cite_error_ref_no_key' );
|
||||
}
|
||||
|
||||
|
@ -293,7 +238,7 @@ class Cite {
|
|||
# would be to mangle them, but it's not really high-priority
|
||||
# (and would produce weird id's anyway).
|
||||
|
||||
$this->mRefCallStack[] = false;
|
||||
$this->referenceStack->pushInvalidRef();
|
||||
return $this->errorReporter->halfParsed( 'cite_error_ref_numeric_key' );
|
||||
}
|
||||
|
||||
|
@ -312,7 +257,7 @@ class Cite {
|
|||
# of the <ref> tag. This way no part of the article will be eaten
|
||||
# even temporarily.
|
||||
|
||||
$this->mRefCallStack[] = false;
|
||||
$this->referenceStack->pushInvalidRef();
|
||||
return $this->errorReporter->halfParsed( 'cite_error_included_ref' );
|
||||
}
|
||||
|
||||
|
@ -323,7 +268,14 @@ class Cite {
|
|||
# we'll figure that out later. Likewise it's definitely valid
|
||||
# if there's any content, regardless of name.
|
||||
|
||||
return $this->stack( $text, $name, $group, $follow, $argv, $dir, $parser->getStripState() );
|
||||
$result = $this->referenceStack->pushRef(
|
||||
$text, $name, $group, $follow, $argv, $dir, $parser->getStripState() );
|
||||
if ( $result === null ) {
|
||||
return '';
|
||||
} else {
|
||||
[ $key, $count, $label, $subkey ] = $result;
|
||||
return $this->linkRef( $group, $key, $count, $label, $subkey );
|
||||
}
|
||||
}
|
||||
|
||||
# Not clear how we could get here, but something is probably
|
||||
|
@ -360,29 +312,29 @@ class Cite {
|
|||
'cite_error_empty_references_define',
|
||||
Sanitizer::safeEncodeAttribute( $name )
|
||||
);
|
||||
} elseif ( !isset( $this->mRefs[$group] ) && !$isSectionPreview ) {
|
||||
} elseif ( !$this->referenceStack->hasGroup( $group ) && !$isSectionPreview ) {
|
||||
# Called with group attribute not defined in text.
|
||||
$this->mReferencesErrors[] = $this->errorReporter->halfParsed(
|
||||
'cite_error_references_missing_group',
|
||||
Sanitizer::safeEncodeAttribute( $group )
|
||||
);
|
||||
} elseif ( !isset( $this->mRefs[$group][$name] ) && !$isSectionPreview ) {
|
||||
} else {
|
||||
$groupRefs = $this->referenceStack->getGroupRefs( $group );
|
||||
if ( !isset( $groupRefs[$name] ) && !$isSectionPreview ) {
|
||||
# Called with name attribute not defined in text.
|
||||
$this->mReferencesErrors[] = $this->errorReporter->halfParsed(
|
||||
'cite_error_references_missing_key',
|
||||
Sanitizer::safeEncodeAttribute( $name )
|
||||
);
|
||||
} elseif ( isset( $this->mRefs[$group][$name]['text'] ) &&
|
||||
$this->mRefs[$group][$name]['text'] !== $text
|
||||
) {
|
||||
} elseif ( isset( $groupRefs[$name]['text'] ) && $groupRefs[$name]['text'] !== $text ) {
|
||||
// two refs with same key and different content
|
||||
// add error message to the original ref
|
||||
$this->mRefs[$group][$name]['text'] .= ' ' . $this->errorReporter->plain(
|
||||
'cite_error_references_duplicate_key', $name
|
||||
);
|
||||
$text = $groupRefs[$name]['text'] . ' ' .
|
||||
$this->errorReporter->plain( 'cite_error_references_duplicate_key', $name );
|
||||
$this->referenceStack->setRefText( $group, $name, $text );
|
||||
} else {
|
||||
# Assign the text to corresponding ref
|
||||
$this->mRefs[$group][$name]['text'] = $text;
|
||||
$this->referenceStack->setRefText( $group, $name, $text );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -453,177 +405,6 @@ class Cite {
|
|||
return [ $name, $group, $follow, $dir, $extends ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate $this->mRefs based on input and arguments to <ref>
|
||||
*
|
||||
* @param string|null $text Content from the <ref> tag
|
||||
* @param string|null $name Argument to the <ref> tag as returned by $this->refArg()
|
||||
* @param string $group
|
||||
* @param string|null $follow Guaranteed to not be a numeric string
|
||||
* @param string[] $argv
|
||||
* @param string $dir ref direction
|
||||
* @param StripState $stripState
|
||||
*
|
||||
* @throws Exception
|
||||
* @return string
|
||||
*/
|
||||
private function stack(
|
||||
$text, $name, $group, $follow, array $argv, $dir, StripState $stripState
|
||||
) {
|
||||
if ( !isset( $this->mRefs[$group] ) ) {
|
||||
$this->mRefs[$group] = [];
|
||||
}
|
||||
if ( !isset( $this->mGroupCnt[$group] ) ) {
|
||||
$this->mGroupCnt[$group] = 0;
|
||||
}
|
||||
|
||||
if ( $follow ) {
|
||||
// We know the parent note already, so just perform the "follow" and bail out
|
||||
if ( isset( $this->mRefs[$group][$follow] ) ) {
|
||||
$this->mRefs[$group][$follow]['text'] .= ' ' . $text;
|
||||
return '';
|
||||
}
|
||||
|
||||
// insert part of note at the beginning of the group
|
||||
$groupsCount = count( $this->mRefs[$group] );
|
||||
for ( $k = 0; $k < $groupsCount; $k++ ) {
|
||||
if ( !isset( $this->mRefs[$group][$k]['follow'] ) ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
array_splice( $this->mRefs[$group], $k, 0, [ [
|
||||
'count' => -1,
|
||||
'text' => $text,
|
||||
'key' => ++$this->mOutCnt,
|
||||
'follow' => $follow,
|
||||
'dir' => $dir,
|
||||
] ] );
|
||||
array_splice( $this->mRefCallStack, $k, 0,
|
||||
[ [ 'new', $argv, $text, $name, $group, $this->mOutCnt ] ] );
|
||||
// A "follow" never gets it's own footnote marker
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( $name === null ) {
|
||||
$this->mRefs[$group][] = [
|
||||
'count' => -1,
|
||||
'text' => $text,
|
||||
'key' => ++$this->mOutCnt,
|
||||
'dir' => $dir
|
||||
];
|
||||
$this->mRefCallStack[] = [ 'new', $argv, $text, $name, $group, $this->mOutCnt ];
|
||||
|
||||
return $this->linkRef( $group, $this->mOutCnt );
|
||||
}
|
||||
if ( !is_string( $name ) ) {
|
||||
throw new Exception( 'Invalid stack key: ' . serialize( $name ) );
|
||||
}
|
||||
|
||||
// Valid key with first occurrence
|
||||
if ( !isset( $this->mRefs[$group][$name] ) ) {
|
||||
$this->mRefs[$group][$name] = [
|
||||
'text' => $text,
|
||||
'count' => -1,
|
||||
'key' => ++$this->mOutCnt,
|
||||
'number' => ++$this->mGroupCnt[$group],
|
||||
'dir' => $dir
|
||||
];
|
||||
$action = 'new';
|
||||
} elseif ( $this->mRefs[$group][$name]['text'] === null && $text !== '' ) {
|
||||
// If no text was set before, use this text
|
||||
$this->mRefs[$group][$name]['text'] = $text;
|
||||
// Use the dir parameter only from the full definition of a named ref tag
|
||||
$this->mRefs[$group][$name]['dir'] = $dir;
|
||||
$action = 'assign';
|
||||
} else {
|
||||
if ( $text != null && $text !== ''
|
||||
// T205803 different strip markers might hide the same text
|
||||
&& $stripState->unstripBoth( $text )
|
||||
!== $stripState->unstripBoth( $this->mRefs[$group][$name]['text'] )
|
||||
) {
|
||||
// two refs with same name and different text
|
||||
// add error message to the original ref
|
||||
$this->mRefs[$group][$name]['text'] .= ' ' . $this->errorReporter->plain(
|
||||
'cite_error_references_duplicate_key', $name
|
||||
);
|
||||
}
|
||||
$action = 'increment';
|
||||
}
|
||||
$this->mRefCallStack[] = [ $action, $argv, $text, $name, $group,
|
||||
$this->mRefs[$group][$name]['key'] ];
|
||||
return $this->linkRef(
|
||||
$group,
|
||||
$name,
|
||||
$this->mRefs[$group][$name]['key'] . "-" . ++$this->mRefs[$group][$name]['count'],
|
||||
$this->mRefs[$group][$name]['number'],
|
||||
"-" . $this->mRefs[$group][$name]['key']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Partially undoes the effect of calls to stack()
|
||||
*
|
||||
* Called by guardedReferences()
|
||||
*
|
||||
* The option to define <ref> within <references> makes the
|
||||
* behavior of <ref> context dependent. This is normally fine
|
||||
* but certain operations (especially #tag) lead to out-of-order
|
||||
* parser evaluation with the <ref> tags being processed before
|
||||
* their containing <reference> element is read. This leads to
|
||||
* stack corruption that this function works to fix.
|
||||
*
|
||||
* This function is not a total rollback since some internal
|
||||
* counters remain incremented. Doing so prevents accidentally
|
||||
* corrupting certain links.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string|null $name The name attribute passed in the ref tag.
|
||||
* @param string $group
|
||||
* @param int $index Autoincrement counter for this ref.
|
||||
*/
|
||||
private function rollbackRef( $type, $name, $group, $index ) {
|
||||
if ( !isset( $this->mRefs[$group] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = $name;
|
||||
if ( $name === null ) {
|
||||
foreach ( $this->mRefs[$group] as $k => $v ) {
|
||||
if ( $this->mRefs[$group][$k]['key'] === $index ) {
|
||||
$key = $k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity checks that specified element exists.
|
||||
if ( $key === null ||
|
||||
!isset( $this->mRefs[$group][$key] ) ||
|
||||
$this->mRefs[$group][$key]['key'] !== $index
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( $type ) {
|
||||
case 'new':
|
||||
# Rollback the addition of new elements to the stack.
|
||||
unset( $this->mRefs[$group][$key] );
|
||||
if ( $this->mRefs[$group] === [] ) {
|
||||
unset( $this->mRefs[$group] );
|
||||
unset( $this->mGroupCnt[$group] );
|
||||
}
|
||||
break;
|
||||
case 'assign':
|
||||
# Rollback assignment of text to pre-existing elements.
|
||||
$this->mRefs[$group][$key]['text'] = null;
|
||||
# continue without break
|
||||
case 'increment':
|
||||
# Rollback increase in named ref occurrences.
|
||||
$this->mRefs[$group][$key]['count']--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for <references>
|
||||
*
|
||||
|
@ -676,38 +457,18 @@ class Cite {
|
|||
# conditional parser functions could be created that would
|
||||
# lead to malformed references here.
|
||||
$count = substr_count( $text, Parser::MARKER_PREFIX . "-ref-" );
|
||||
$redoStack = [];
|
||||
|
||||
# Undo effects of calling <ref> while unaware of containing <references>
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
if ( !$this->mRefCallStack ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$call = array_pop( $this->mRefCallStack );
|
||||
$redoStack[] = $call;
|
||||
if ( $call !== false ) {
|
||||
list( $type, $ref_argv, $ref_text,
|
||||
$ref_key, $ref_group, $ref_index ) = $call;
|
||||
$this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );
|
||||
}
|
||||
}
|
||||
$redoStack = $this->referenceStack->rollbackRefs( $count );
|
||||
|
||||
# Rerun <ref> call now that mInReferences is set.
|
||||
for ( $i = count( $redoStack ); $i--; ) {
|
||||
$call = $redoStack[$i];
|
||||
if ( $call !== false ) {
|
||||
list( $type, $ref_argv, $ref_text,
|
||||
$ref_key, $ref_group, $ref_index ) = $call;
|
||||
foreach ( $redoStack as $call ) {
|
||||
[ $ref_argv, $ref_text ] = $call;
|
||||
$this->guardedRef( $ref_text, $ref_argv, $parser );
|
||||
}
|
||||
}
|
||||
|
||||
# Parse $text to process any unparsed <ref> tags.
|
||||
$parser->recursiveTagParse( $text );
|
||||
|
||||
# Reset call stack
|
||||
$this->mRefCallStack = [];
|
||||
}
|
||||
|
||||
if ( isset( $argv['responsive'] ) ) {
|
||||
|
@ -743,14 +504,15 @@ class Cite {
|
|||
* @return string HTML ready for output
|
||||
*/
|
||||
private function referencesFormat( $group, $responsive ) {
|
||||
if ( !isset( $this->mRefs[$group] ) ) {
|
||||
if ( !$this->referenceStack->hasGroup( $group ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Add new lines between the list items (ref entries) to avoid confusing tidy (T15073).
|
||||
// Note: This builds a string of wikitext, not html.
|
||||
$parserInput = "\n";
|
||||
foreach ( $this->mRefs[$group] as $key => $value ) {
|
||||
$groupRefs = $this->referenceStack->getGroupRefs( $group );
|
||||
foreach ( $groupRefs as $key => $value ) {
|
||||
$parserInput .= $this->referencesFormatEntry( $key, $value ) . "\n";
|
||||
}
|
||||
$parserInput = Html::rawElement( 'ol', [ 'class' => [ 'references' ] ], $parserInput );
|
||||
|
@ -762,7 +524,7 @@ class Cite {
|
|||
// Use a DIV wrap because column-count on a list directly is broken in Chrome.
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=498730.
|
||||
$wrapClasses = [ 'mw-references-wrap' ];
|
||||
if ( count( $this->mRefs[$group] ) > 10 ) {
|
||||
if ( count( $groupRefs ) > 10 ) {
|
||||
$wrapClasses[] = 'mw-references-columns';
|
||||
}
|
||||
$ret = Html::rawElement( 'div', [ 'class' => $wrapClasses ], $ret );
|
||||
|
@ -774,8 +536,7 @@ class Cite {
|
|||
}
|
||||
|
||||
// done, clean up so we can reuse the group
|
||||
unset( $this->mRefs[$group] );
|
||||
unset( $this->mGroupCnt[$group] );
|
||||
$this->referenceStack->deleteGroup( $group );
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
@ -783,8 +544,8 @@ class Cite {
|
|||
/**
|
||||
* Format a single entry for the referencesFormat() function
|
||||
*
|
||||
* @param string|int $key The name or group index of the reference
|
||||
* @param array $val A single reference as documented at {@see $mRefs}
|
||||
* @param string|int $key The key of the reference
|
||||
* @param array $val A single reference as documented at {@see ReferenceStack::$refs}
|
||||
* @return string Wikitext, wrapped in a single <li> element
|
||||
*/
|
||||
private function referencesFormatEntry( $key, array $val ) {
|
||||
|
@ -982,20 +743,16 @@ class Cite {
|
|||
* @param string $key The key for the link
|
||||
* @param int|null $count The index of the key, used for distinguishing
|
||||
* multiple occurrences of the same key
|
||||
* @param int|null $label The label to use for the link, I want to
|
||||
* use the same label for all occourances of
|
||||
* @param int $label The label to use for the link, I want to
|
||||
* use the same label for all occurrences of
|
||||
* the same named reference.
|
||||
* @param string $subkey
|
||||
* @param string|null $subkey
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) {
|
||||
private function linkRef( $group, $key, $count, $label, $subkey ) {
|
||||
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
|
||||
|
||||
if ( $label === null ) {
|
||||
$label = ++$this->mGroupCnt[$group];
|
||||
}
|
||||
|
||||
return $this->mParser->recursiveTagParse(
|
||||
wfMessage(
|
||||
'cite_reference_link',
|
||||
|
@ -1091,12 +848,9 @@ class Cite {
|
|||
// Don't clear when we're in the middle of parsing a <ref> or <references> tag
|
||||
return;
|
||||
}
|
||||
|
||||
$this->mGroupCnt = [];
|
||||
$this->mOutCnt = 0;
|
||||
$this->mRefs = [];
|
||||
$this->mReferencesErrors = [];
|
||||
$this->mRefCallStack = [];
|
||||
if ( $this->referenceStack ) {
|
||||
$this->referenceStack->clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1128,7 +882,9 @@ class Cite {
|
|||
|
||||
if ( !$parserOptions->getIsPreview() ) {
|
||||
// save references data for later use by LinksUpdate hooks
|
||||
if ( $this->mRefs && isset( $this->mRefs[self::DEFAULT_GROUP] ) ) {
|
||||
if ( $this->referenceStack &&
|
||||
$this->referenceStack->hasGroup( self::DEFAULT_GROUP )
|
||||
) {
|
||||
$this->saveReferencesData( $parserOutput );
|
||||
}
|
||||
$isSectionPreview = false;
|
||||
|
@ -1137,10 +893,8 @@ class Cite {
|
|||
}
|
||||
|
||||
$s = '';
|
||||
foreach ( $this->mRefs as $group => $refs ) {
|
||||
if ( !$refs ) {
|
||||
continue;
|
||||
}
|
||||
if ( $this->referenceStack ) {
|
||||
foreach ( $this->referenceStack->getGroups() as $group ) {
|
||||
if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
|
||||
$this->inReferencesGroup = $group;
|
||||
$s .= $this->referencesFormat( $group, $wgCiteResponsiveReferences );
|
||||
|
@ -1153,6 +907,7 @@ class Cite {
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $isSectionPreview && $s !== '' ) {
|
||||
// provide a preview of references in its own section
|
||||
$text .= "\n" . '<div class="mw-ext-cite-cite_section_preview_references" >';
|
||||
|
@ -1171,7 +926,6 @@ class Cite {
|
|||
/**
|
||||
* Saves references in parser extension data
|
||||
* This is called by each <references/> tag, and by checkRefsNoReferences
|
||||
* Assumes $this->mRefs[$group] is set
|
||||
*
|
||||
* @param ParserOutput $parserOutput
|
||||
* @param string $group
|
||||
|
@ -1198,7 +952,7 @@ class Cite {
|
|||
}
|
||||
$n = count( $savedRefs['refs'] ) - 1;
|
||||
// save group
|
||||
$savedRefs['refs'][$n][$group] = $this->mRefs[$group];
|
||||
$savedRefs['refs'][$n][$group] = $this->referenceStack->getGroupRefs( $group );
|
||||
|
||||
$parserOutput->setExtensionData( self::EXT_DATA_KEY, $savedRefs );
|
||||
}
|
||||
|
|
366
src/ReferenceStack.php
Normal file
366
src/ReferenceStack.php
Normal file
|
@ -0,0 +1,366 @@
|
|||
<?php
|
||||
|
||||
namespace Cite;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use StripState;
|
||||
|
||||
/**
|
||||
* Encapsulates most of Cite state during parsing. This includes metadata about each ref tag,
|
||||
* and a rollback stack to correct confusion caused by lost context when `{{#tag` is used.
|
||||
*/
|
||||
class ReferenceStack {
|
||||
|
||||
/**
|
||||
* Datastructure representing <ref> input, in the format of:
|
||||
* <code>
|
||||
* [
|
||||
* 'user supplied' => [
|
||||
* 'text' => 'user supplied reference & key',
|
||||
* 'count' => 1, // occurs twice
|
||||
* 'number' => 1, // The first reference, we want
|
||||
* // all occourances of it to
|
||||
* // use the same number
|
||||
* ],
|
||||
* 0 => [
|
||||
* 'text' => 'Anonymous reference',
|
||||
* 'count' => -1,
|
||||
* ],
|
||||
* 1 => [
|
||||
* 'text' => 'Another anonymous reference',
|
||||
* 'count' => -1,
|
||||
* ],
|
||||
* 'some key' => [
|
||||
* 'text' => 'this one occurs once'
|
||||
* 'count' => 0,
|
||||
* 'number' => 4
|
||||
* ],
|
||||
* 3 => 'more stuff'
|
||||
* ];
|
||||
* </code>
|
||||
*
|
||||
* This works because:
|
||||
* * PHP's datastructures are guaranteed to be returned in the
|
||||
* order that things are inserted into them (unless you mess
|
||||
* with that)
|
||||
* * User supplied keys can't be integers, therefore avoiding
|
||||
* conflict with anonymous keys
|
||||
*
|
||||
* In this structure, 'key' will either be an autoincrementing integer.
|
||||
*
|
||||
* @var array[][]
|
||||
*/
|
||||
private $refs = [];
|
||||
|
||||
/**
|
||||
* Count for user displayed output (ref[1], ref[2], ...)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $refSequence = 0;
|
||||
|
||||
/**
|
||||
* Counter for the number of refs in each group.
|
||||
* @var int[]
|
||||
*/
|
||||
private $groupRefSequence = [];
|
||||
|
||||
/**
|
||||
* <ref> call stack
|
||||
* Used to cleanup out of sequence ref calls created by #tag
|
||||
* See description of function rollbackRef.
|
||||
*
|
||||
* @var (array|false)[]
|
||||
*/
|
||||
private $refCallStack = [];
|
||||
|
||||
/**
|
||||
* @deprecated We should be able to push this responsibility to calling code.
|
||||
* @var CiteErrorReporter $errorReporter
|
||||
*/
|
||||
private $errorReporter;
|
||||
|
||||
/**
|
||||
* @param CiteErrorReporter $errorReporter
|
||||
*/
|
||||
public function __construct( CiteErrorReporter $errorReporter ) {
|
||||
$this->errorReporter = $errorReporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave a mark in the stack which matches an invalid ref tag.
|
||||
*/
|
||||
public function pushInvalidRef() {
|
||||
$this->refCallStack[] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate $this->refs and $this->refCallStack based on input and arguments to <ref>
|
||||
*
|
||||
* @param string|null $text Content from the <ref> tag
|
||||
* @param string|null $name Argument to the <ref> tag as returned by $this->refArg()
|
||||
* @param string $group
|
||||
* @param string|null $follow Guaranteed to not be a numeric string
|
||||
* @param string[] $argv
|
||||
* @param string $dir ref direction
|
||||
* @param StripState $stripState
|
||||
*
|
||||
* @return array|null
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function pushRef(
|
||||
$text, $name, $group, $follow, array $argv, $dir, StripState $stripState
|
||||
) {
|
||||
if ( !isset( $this->refs[$group] ) ) {
|
||||
$this->refs[$group] = [];
|
||||
}
|
||||
if ( !isset( $this->groupRefSequence[$group] ) ) {
|
||||
$this->groupRefSequence[$group] = 0;
|
||||
}
|
||||
|
||||
if ( $follow ) {
|
||||
// We know the parent note already, so just perform the "follow" and bail out
|
||||
if ( isset( $this->refs[$group][$follow] ) ) {
|
||||
$this->refs[$group][$follow]['text'] .= ' ' . $text;
|
||||
return null;
|
||||
}
|
||||
|
||||
// insert broken follow at the end of any other broken follows.
|
||||
// FIXME: This relies on an undocumented feature of array_splice, and produces
|
||||
// invalid HTML output, inserting a <p> tag into an <ol>.
|
||||
$groupsCount = count( $this->refs[$group] );
|
||||
for ( $k = 0; $k < $groupsCount; $k++ ) {
|
||||
if ( !isset( $this->refs[$group][$k]['follow'] ) ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
array_splice( $this->refs[$group], $k, 0, [ [
|
||||
'count' => -1,
|
||||
'text' => $text,
|
||||
'key' => ++$this->refSequence,
|
||||
'follow' => $follow,
|
||||
'dir' => $dir,
|
||||
] ] );
|
||||
array_splice( $this->refCallStack, $k, 0,
|
||||
[ [ 'new', $argv, $text, $name, $group, $this->refSequence ] ] );
|
||||
|
||||
// A "follow" never gets its own footnote marker
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( $name === null ) {
|
||||
$this->refs[$group][] = [
|
||||
'count' => -1,
|
||||
'text' => $text,
|
||||
'key' => ++$this->refSequence,
|
||||
'dir' => $dir
|
||||
];
|
||||
$this->refCallStack[] = [ 'new', $argv, $text, $name, $group, $this->refSequence ];
|
||||
|
||||
return [ $this->refSequence, null, ++$this->groupRefSequence[$group], null ];
|
||||
}
|
||||
if ( !is_string( $name ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid stack key: ' . serialize( $name ) );
|
||||
}
|
||||
|
||||
// Valid key with first occurrence
|
||||
if ( !isset( $this->refs[$group][$name] ) ) {
|
||||
$this->refs[$group][$name] = [
|
||||
'text' => $text,
|
||||
'count' => -1,
|
||||
'key' => ++$this->refSequence,
|
||||
'number' => ++$this->groupRefSequence[$group],
|
||||
'dir' => $dir
|
||||
];
|
||||
$action = 'new';
|
||||
} elseif ( $this->refs[$group][$name]['text'] === null && $text !== '' ) {
|
||||
// If no text was set before, use this text
|
||||
$this->refs[$group][$name]['text'] = $text;
|
||||
// Use the dir parameter only from the full definition of a named ref tag
|
||||
$this->refs[$group][$name]['dir'] = $dir;
|
||||
$action = 'assign';
|
||||
} else {
|
||||
if ( $text != null && $text !== ''
|
||||
// T205803 different strip markers might hide the same text
|
||||
&& $stripState->unstripBoth( $text )
|
||||
!== $stripState->unstripBoth( $this->refs[$group][$name]['text'] )
|
||||
) {
|
||||
// two refs with same name and different text
|
||||
// add error message to the original ref
|
||||
$this->refs[$group][$name]['text'] .= ' ' . $this->errorReporter->plain(
|
||||
'cite_error_references_duplicate_key', $name
|
||||
);
|
||||
}
|
||||
$action = 'increment';
|
||||
}
|
||||
$this->refCallStack[] = [ $action, $argv, $text, $name, $group,
|
||||
$this->refs[$group][$name]['key'] ];
|
||||
return [
|
||||
$name,
|
||||
$this->refs[$group][$name]['key'] . "-" . ++$this->refs[$group][$name]['count'],
|
||||
$this->refs[$group][$name]['number'] ?? ++$this->groupRefSequence[$group],
|
||||
"-" . $this->refs[$group][$name]['key']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the changes made by the last $count ref tags. This is used when we discover that the
|
||||
* last few tags were actually inside of a references tag.
|
||||
*
|
||||
* @param int $count
|
||||
* @return array Refs to restore under the correct context. [ $argv, $text ]
|
||||
*/
|
||||
public function rollbackRefs( $count ) : array {
|
||||
$redoStack = [];
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
if ( !$this->refCallStack ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$call = array_pop( $this->refCallStack );
|
||||
if ( $call !== false ) {
|
||||
[ $action, $argv, $text, $name, $group, $index ] = $call;
|
||||
$this->rollbackRef( $action, $name, $group, $index );
|
||||
$redoStack[] = [ $argv, $text ];
|
||||
}
|
||||
}
|
||||
// Drop unused rollbacks. TODO: Warn if not fully consumed?
|
||||
$this->refCallStack = [];
|
||||
|
||||
return array_reverse( $redoStack );
|
||||
}
|
||||
|
||||
/**
|
||||
* Partially undoes the effect of calls to stack()
|
||||
*
|
||||
* Called by guardedReferences()
|
||||
*
|
||||
* The option to define <ref> within <references> makes the
|
||||
* behavior of <ref> context dependent. This is normally fine
|
||||
* but certain operations (especially #tag) lead to out-of-order
|
||||
* parser evaluation with the <ref> tags being processed before
|
||||
* their containing <reference> element is read. This leads to
|
||||
* stack corruption that this function works to fix.
|
||||
*
|
||||
* This function is not a total rollback since some internal
|
||||
* counters remain incremented. Doing so prevents accidentally
|
||||
* corrupting certain links.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string|null $name The name attribute passed in the ref tag.
|
||||
* @param string $group
|
||||
* @param int $index Autoincrement counter for this ref.
|
||||
*/
|
||||
private function rollbackRef( $type, $name, $group, $index ) {
|
||||
if ( !$this->hasGroup( $group ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = $name;
|
||||
if ( $name === null ) {
|
||||
foreach ( $this->refs[$group] as $k => $v ) {
|
||||
if ( $this->refs[$group][$k]['key'] === $index ) {
|
||||
$key = $k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity checks that specified element exists.
|
||||
if ( $key === null ||
|
||||
!isset( $this->refs[$group][$key] ) ||
|
||||
$this->refs[$group][$key]['key'] !== $index
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( $type ) {
|
||||
case 'new':
|
||||
# Rollback the addition of new elements to the stack.
|
||||
unset( $this->refs[$group][$key] );
|
||||
if ( $this->refs[$group] === [] ) {
|
||||
unset( $this->refs[$group] );
|
||||
unset( $this->groupRefSequence[$group] );
|
||||
}
|
||||
break;
|
||||
case 'assign':
|
||||
# Rollback assignment of text to pre-existing elements.
|
||||
$this->refs[$group][$key]['text'] = null;
|
||||
# continue without break
|
||||
case 'increment':
|
||||
# Rollback increase in named ref occurrences.
|
||||
$this->refs[$group][$key]['count']--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all state.
|
||||
*/
|
||||
public function clear() {
|
||||
$this->groupRefSequence = [];
|
||||
$this->refSequence = 0;
|
||||
$this->refs = [];
|
||||
$this->mReferencesErrors = [];
|
||||
$this->refCallStack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear state for a single group.
|
||||
*
|
||||
* @param string $group
|
||||
*/
|
||||
public function deleteGroup( $group ) {
|
||||
unset( $this->refs[$group] );
|
||||
unset( $this->groupRefSequence[$group] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retruns true if the group exists.
|
||||
*
|
||||
* @param string $group
|
||||
* @return bool
|
||||
*/
|
||||
public function hasGroup( string $group ) : bool {
|
||||
return $this->refs && isset( $this->refs[$group] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all groups with references.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGroups() : array {
|
||||
$groups = [];
|
||||
foreach ( $this->refs as $group => $refs ) {
|
||||
if ( $refs ) {
|
||||
$groups[] = $group;
|
||||
}
|
||||
}
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all references for a group.
|
||||
*
|
||||
* @param string $group
|
||||
* @return array[]
|
||||
*/
|
||||
public function getGroupRefs( $group ) : array {
|
||||
return $this->refs[$group];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to set reference text from external code. Ideally we can take over
|
||||
* responsibility for this logic.
|
||||
* @deprecated
|
||||
*
|
||||
* @param string $group
|
||||
* @param string $name
|
||||
* @param string $text
|
||||
*/
|
||||
public function setRefText( $group, $name, $text ) {
|
||||
$this->refs[$group][$name]['text'] = $text;
|
||||
}
|
||||
|
||||
}
|
|
@ -433,7 +433,7 @@ It's not possible to follow="…" a <ref> defined in the <references> section
|
|||
!! end
|
||||
|
||||
!! test
|
||||
A follow="…" after it's parent is not merged
|
||||
A follow="…" before its parent is not merged
|
||||
!! wikitext
|
||||
<ref follow="theName">theFollows</ref>
|
||||
<ref name="theName">theValue</ref>
|
||||
|
@ -446,6 +446,53 @@ A follow="…" after it's parent is not merged
|
|||
</ol></div>
|
||||
!! end
|
||||
|
||||
# This is a nasty edge case which was dropping the ref entirely.
|
||||
!! test
|
||||
"follow" after a named ref but before its parent
|
||||
!! wikitext
|
||||
<ref name="first">First</ref>
|
||||
<ref follow="third">Second</ref>
|
||||
<ref name="third">Third</ref>
|
||||
<hr />
|
||||
<references />
|
||||
!! html/php
|
||||
<p><sup id="cite_ref-first_1-0" class="reference"><a href="#cite_note-first-1">[1]</a></sup>
|
||||
</p><p><sup id="cite_ref-third_3-0" class="reference"><a href="#cite_note-third-3">[2]</a></sup>
|
||||
</p>
|
||||
<hr />
|
||||
<div class="mw-references-wrap"><ol class="references">
|
||||
<p id="cite_note-third"><span class="reference-text">Second</span>
|
||||
</p>
|
||||
<li id="cite_note-first-1"><span class="mw-cite-backlink"><a href="#cite_ref-first_1-0">↑</a></span> <span class="reference-text">First</span>
|
||||
</li>
|
||||
<li id="cite_note-third-3"><span class="mw-cite-backlink"><a href="#cite_ref-third_3-0">↑</a></span> <span class="reference-text">Third</span>
|
||||
</li>
|
||||
</ol></div>
|
||||
!! end
|
||||
|
||||
!! test
|
||||
"follow" after an anonymous ref but before its parent
|
||||
!! wikitext
|
||||
<ref>First</ref>
|
||||
<ref follow="third">Second</ref>
|
||||
<ref name="third">Third</ref>
|
||||
<hr />
|
||||
<references />
|
||||
!! html/php
|
||||
<p><sup id="cite_ref-1" class="reference"><a href="#cite_note-1">[1]</a></sup>
|
||||
</p><p><sup id="cite_ref-third_3-0" class="reference"><a href="#cite_note-third-3">[2]</a></sup>
|
||||
</p>
|
||||
<hr />
|
||||
<div class="mw-references-wrap"><ol class="references">
|
||||
<p id="cite_note-third"><span class="reference-text">Second</span>
|
||||
</p>
|
||||
<li id="cite_note-1"><span class="mw-cite-backlink"><a href="#cite_ref-1">↑</a></span> <span class="reference-text">First</span>
|
||||
</li>
|
||||
<li id="cite_note-third-3"><span class="mw-cite-backlink"><a href="#cite_ref-third_3-0">↑</a></span> <span class="reference-text">Third</span>
|
||||
</li>
|
||||
</ol></div>
|
||||
!! end
|
||||
|
||||
!! test
|
||||
Valid follow="…" after it's parent
|
||||
!! wikitext
|
||||
|
|
368
tests/phpunit/unit/ReferenceStackTest.php
Normal file
368
tests/phpunit/unit/ReferenceStackTest.php
Normal file
|
@ -0,0 +1,368 @@
|
|||
<?php
|
||||
|
||||
namespace Cite\Tests\Unit;
|
||||
|
||||
use Cite\CiteErrorReporter;
|
||||
use Cite\ReferenceStack;
|
||||
use InvalidArgumentException;
|
||||
use MediaWikiUnitTestCase;
|
||||
use StripState;
|
||||
use Wikimedia\TestingAccessWrapper;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Cite\ReferenceStack
|
||||
*/
|
||||
class ReferenceStackTest extends MediaWikiUnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::pushInvalidRef
|
||||
*/
|
||||
public function testPushInvalidRef() {
|
||||
$stack = $this->newStack();
|
||||
|
||||
$stack->pushInvalidRef();
|
||||
|
||||
$spy = TestingAccessWrapper::newFromObject( $stack );
|
||||
$this->assertSame( [ false ], $spy->refCallStack );
|
||||
}
|
||||
|
||||
// TODO: testRollbackRefs()
|
||||
// TODO: testGetGroupRefs()
|
||||
|
||||
/**
|
||||
* @covers ::pushRef
|
||||
*
|
||||
* @dataProvider providePushRef
|
||||
*/
|
||||
public function testPushRefs(
|
||||
array $refs,
|
||||
$expectedOutputs,
|
||||
array $finalRefs,
|
||||
array $finalCallStack
|
||||
) {
|
||||
$mockStripState = $this->createMock( StripState::class );
|
||||
$mockStripState->method( 'unstripBoth' )->willReturnArgument( 0 );
|
||||
$stack = $this->newStack();
|
||||
|
||||
for ( $i = 0; $i < count( $refs ); $i++ ) {
|
||||
[ $text, $name, $group, $follow, $argv, $dir ] = $refs[$i];
|
||||
if ( is_string( $expectedOutputs ) ) {
|
||||
$this->expectException( $expectedOutputs );
|
||||
}
|
||||
$result = $stack->pushRef(
|
||||
$text, $name, $group, $follow, $argv, $dir, $mockStripState );
|
||||
|
||||
$this->assertSame( $expectedOutputs[$i], $result );
|
||||
}
|
||||
|
||||
$spy = TestingAccessWrapper::newFromObject( $stack );
|
||||
$this->assertSame( $finalRefs, $spy->refs );
|
||||
$this->assertSame( $finalCallStack, $spy->refCallStack );
|
||||
}
|
||||
|
||||
public function providePushRef() {
|
||||
return [
|
||||
'Anonymous ref in default group' => [
|
||||
[
|
||||
[ null, null, '', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 1, null, 1, null ]
|
||||
],
|
||||
[
|
||||
'' => [
|
||||
[
|
||||
'count' => -1,
|
||||
'text' => null,
|
||||
'key' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], null, null, '', 1 ]
|
||||
]
|
||||
],
|
||||
'Anonymous ref in named group' => [
|
||||
[
|
||||
[ null, null, 'foo', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 1, null, 1, null ]
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
[
|
||||
'count' => -1,
|
||||
'text' => null,
|
||||
'key' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], null, null, 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
'Ref with text' => [
|
||||
[
|
||||
[ 'text', null, 'foo', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 1, null, 1, null ]
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
[
|
||||
'count' => -1,
|
||||
'text' => 'text',
|
||||
'key' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], 'text', null, 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
'Named ref with text' => [
|
||||
[
|
||||
[ 'text', 'name', 'foo', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 'name', '1-0', 1, '-1' ]
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
'name' => [
|
||||
'text' => 'text',
|
||||
'count' => 0,
|
||||
'key' => 1,
|
||||
'number' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], 'text', 'name', 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
'Follow after base' => [
|
||||
[
|
||||
[ 'text-a', 'a', 'foo', null, [], 'rtl' ],
|
||||
[ 'text-b', 'b', 'foo', 'a', [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 'a', '1-0', 1, '-1' ],
|
||||
null
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
'a' => [
|
||||
'text' => 'text-a text-b',
|
||||
'count' => 0,
|
||||
'key' => 1,
|
||||
'number' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], 'text-a', 'a', 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
'Follow with no base' => [
|
||||
[
|
||||
[ 'text', 'b', 'foo', 'a', [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
null
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
[
|
||||
'count' => -1,
|
||||
'text' => 'text',
|
||||
'key' => 1,
|
||||
'follow' => 'a',
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], 'text', 'b', 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
'Follow pointing to later ref' => [
|
||||
[
|
||||
[ 'text-a', 'a', 'foo', null, [], 'rtl' ],
|
||||
[ 'text-b', 'b', 'foo', 'c', [], 'rtl' ],
|
||||
[ 'text-c', 'c', 'foo', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 'a', '1-0', 1, '-1' ],
|
||||
null,
|
||||
[ 'c', '3-0', 2, '-3' ],
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
0 => [
|
||||
'count' => -1,
|
||||
'text' => 'text-b',
|
||||
'key' => 2,
|
||||
'follow' => 'c',
|
||||
'dir' => 'rtl',
|
||||
],
|
||||
'a' => [
|
||||
'text' => 'text-a',
|
||||
'count' => 0,
|
||||
'key' => 1,
|
||||
'number' => 1,
|
||||
'dir' => 'rtl',
|
||||
],
|
||||
'c' => [
|
||||
'text' => 'text-c',
|
||||
'count' => 0,
|
||||
'key' => 3,
|
||||
'number' => 2,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], 'text-b', 'b', 'foo', 2 ],
|
||||
[ 'new', [], 'text-a', 'a', 'foo', 1 ],
|
||||
[ 'new', [], 'text-c', 'c', 'foo', 3 ]
|
||||
]
|
||||
],
|
||||
'Repeated ref, text in first tag' => [
|
||||
[
|
||||
[ 'text', 'a', 'foo', null, [], 'rtl' ],
|
||||
[ null, 'a', 'foo', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 'a', '1-0', 1, '-1' ],
|
||||
[ 'a', '1-1', 1, '-1' ],
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
'a' => [
|
||||
'text' => 'text',
|
||||
'count' => 1,
|
||||
'key' => 1,
|
||||
'number' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], 'text', 'a', 'foo', 1 ],
|
||||
[ 'increment', [], null, 'a', 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
'Repeated ref, text in second tag' => [
|
||||
[
|
||||
[ null, 'a', 'foo', null, [], 'rtl' ],
|
||||
[ 'text', 'a', 'foo', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 'a', '1-0', 1, '-1' ],
|
||||
[ 'a', '1-1', 1, '-1' ],
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
'a' => [
|
||||
'text' => 'text',
|
||||
'count' => 1,
|
||||
'key' => 1,
|
||||
'number' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], null, 'a', 'foo', 1 ],
|
||||
[ 'assign', [], 'text', 'a', 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
'Repeated ref, mismatched text' => [
|
||||
[
|
||||
[ 'text-1', 'a', 'foo', null, [], 'rtl' ],
|
||||
[ 'text-2', 'a', 'foo', null, [], 'rtl' ]
|
||||
],
|
||||
[
|
||||
[ 'a', '1-0', 1, '-1' ],
|
||||
[ 'a', '1-1', 1, '-1' ],
|
||||
],
|
||||
[
|
||||
'foo' => [
|
||||
'a' => [
|
||||
'text' => 'text-1 cite_error_references_duplicate_key',
|
||||
'count' => 1,
|
||||
'key' => 1,
|
||||
'number' => 1,
|
||||
'dir' => 'rtl',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
[ 'new', [], 'text-1', 'a', 'foo', 1 ],
|
||||
[ 'increment', [], 'text-2', 'a', 'foo', 1 ]
|
||||
]
|
||||
],
|
||||
|
||||
// FIXME: Split this off into a separate test method
|
||||
'Illegal value for name' => [
|
||||
[
|
||||
[ null, 123, '', null, [], 'rtl' ]
|
||||
],
|
||||
InvalidArgumentException::class,
|
||||
[],
|
||||
[]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getGroups
|
||||
*/
|
||||
public function testGetGroups() {
|
||||
$stack = $this->newStack();
|
||||
$spy = TestingAccessWrapper::newFromObject( $stack );
|
||||
$spy->refs = [ 'havenot' => [], 'have' => [ [ 'ref etc' ] ] ];
|
||||
|
||||
$this->assertSame( [ 'have' ], $stack->getGroups() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::hasGroup
|
||||
*/
|
||||
public function testHasGroup() {
|
||||
$stack = $this->newStack();
|
||||
$spy = TestingAccessWrapper::newFromObject( $stack );
|
||||
$spy->refs = [ 'present' => [ [ 'ref etc' ] ] ];
|
||||
|
||||
$this->assertFalse( $stack->hasGroup( 'absent' ) );
|
||||
$this->assertTrue( $stack->hasGroup( 'present' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::setRefText
|
||||
*/
|
||||
public function testSetRefText() {
|
||||
$stack = $this->newStack();
|
||||
|
||||
$stack->setRefText( 'group', 'name', 'the-text' );
|
||||
|
||||
$spy = TestingAccessWrapper::newFromObject( $stack );
|
||||
$this->assertSame(
|
||||
[ 'group' => [ 'name' => [ 'text' => 'the-text' ] ] ], $spy->refs );
|
||||
}
|
||||
|
||||
private function newStack() {
|
||||
$errorReporter = $this->createMock( CiteErrorReporter::class );
|
||||
$errorReporter->method( 'plain' )->willReturnArgument( 0 );
|
||||
return new ReferenceStack( $errorReporter );
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue