Page MenuHomePhabricator

new-Cite-ext-design.patch

Authored By
dominic.mayers
Jan 28 2024, 8:33 PM
Size
40 KB
Referenced Files
None
Subscribers
None

new-Cite-ext-design.patch

diff --git a/src/Cite.php b/src/Cite.php
index 20d8f79f..d6761def 100644
--- a/src/Cite.php
+++ b/src/Cite.php
@@ -30,6 +30,7 @@ use MediaWiki\MediaWikiServices;
use MediaWiki\Parser\Sanitizer;
use Parser;
use StatusValue;
+use PPFrame;
/**
* @license GPL-2.0-or-later
@@ -54,10 +55,10 @@ class Cite {
private ErrorReporter $errorReporter;
/**
- * True when a <ref> tag is being processed.
- * Used to avoid infinite recursion
+ * The depth of recursion in the process of <ref> tags
+ * Used to separate in list tags from the others.
*/
- private bool $inRefTag = false;
+ private int $depthRef = 0;
/**
* @var null|string The current group name while parsing nested <ref> in <references>. Null when
@@ -71,10 +72,10 @@ class Cite {
private StatusValue $mReferencesErrors;
private ReferenceStack $referenceStack;
- public function __construct( Parser $parser ) {
+ public function __construct(Parser $parser) {
$this->isSectionPreview = $parser->getOptions()->getIsSectionPreview();
- $messageLocalizer = new ReferenceMessageLocalizer( $parser->getContentLanguage() );
- $this->errorReporter = new ErrorReporter( $messageLocalizer );
+ $messageLocalizer = new ReferenceMessageLocalizer($parser->getContentLanguage());
+ $this->errorReporter = new ErrorReporter($messageLocalizer);
$this->mReferencesErrors = StatusValue::newGood();
$this->referenceStack = new ReferenceStack();
$anchorFormatter = new AnchorFormatter();
@@ -99,14 +100,9 @@ class Cite {
*
* @return string|null Null in case a <ref> tag is not allowed in the current context
*/
- public function ref( Parser $parser, ?string $text, array $argv ): ?string {
- if ( $this->inRefTag ) {
- return null;
- }
+ public function ref(Parser $parser, ?string $text, PPFrame $frame, array $argv): ?string {
- $this->inRefTag = true;
- $ret = $this->guardedRef( $parser, $text, $argv );
- $this->inRefTag = false;
+ $ret = $this->guardedRef($parser, $text, $frame, $argv);
return $ret;
}
@@ -121,62 +117,71 @@ class Cite {
private function guardedRef(
Parser $parser,
?string $text,
+ PPFrame $frame,
array $argv
): string {
// Tag every page where Book Referencing has been used, whether or not the ref tag is valid.
// This code and the page property will be removed once the feature is stable. See T237531.
- if ( array_key_exists( self::BOOK_REF_ATTRIBUTE, $argv ) ) {
- $parser->getOutput()->setPageProperty( self::BOOK_REF_PROPERTY, '' );
+ // Todo: Check that this is still OK with the new code.
+ if (array_key_exists(self::BOOK_REF_ATTRIBUTE, $argv)) {
+ $parser->getOutput()->setPageProperty(self::BOOK_REF_PROPERTY, '');
}
$status = $this->parseArguments(
$argv,
- [ 'group', 'name', self::BOOK_REF_ATTRIBUTE, 'follow', 'dir' ]
+ ['group', 'name', self::BOOK_REF_ATTRIBUTE, 'follow', 'dir']
);
$arguments = $status->getValue();
// Use the default group, or the references group when inside one.
$arguments['group'] ??= $this->inReferencesGroup ?? self::DEFAULT_GROUP;
- $validator = new Validator(
- $this->referenceStack,
- $this->inReferencesGroup,
- $this->isSectionPreview,
- MediaWikiServices::getInstance()->getMainConfig()->get( 'CiteBookReferencing' )
- );
- // @phan-suppress-next-line PhanParamTooFewUnpack No good way to document it.
- $status->merge( $validator->validateRef( $text, ...array_values( $arguments ) ) );
+ // The parameters must be processed, because they might be template variables.
+ // Use the default group, or the references group when inside one.
+ $gr = & $arguments['group'];
+ if (isset($gr) && $gr) {
+ // To process group template variable
+ $gr = trim($parser->recursiveTagParse($gr, $frame), ' "');
+ }
+ unset($gr);
+
+ $nm = & $arguments['name'];
+ if (isset($nm) && $nm) {
+ // To process name template variable.
+ $nm = trim($parser->recursiveTagParse($nm, $frame), '"');
+ if (!$nm)
+ $nm = null;
+ }
+ unset($nm);
// Validation cares about the difference between null and empty, but from here on we don't
- if ( $text !== null && trim( $text ) === '' ) {
+ if ($text !== null && trim($text) === '') {
$text = null;
}
- if ( $this->inReferencesGroup !== null ) {
- if ( !$status->isGood() ) {
- // We know we are in the middle of a <references> tag and can't display errors in place
- $this->mReferencesErrors->merge( $status );
- } elseif ( $text !== null ) {
- // Validation made sure we always have group and name while in <references>
- $this->referenceStack->listDefinedRef( $arguments['group'], $arguments['name'], $text );
- }
- return '';
- }
+ $inList = ($this->inReferencesGroup !== null && $this->depthRef == 0);
+ $grKey = $this->referenceStack->register($arguments['group'], $arguments['name'], $inList);
- if ( !$status->isGood() ) {
- $this->referenceStack->pushInvalidRef();
+ // This is not only a shortcut : a value null is not accepted when a string is expected.
+ $processed_text = $text;
+ if ($text !== null) {
+ $this->depthRef++;
+ $processed_text = $parser->recursiveTagParse($text, $frame);
+ $this->depthRef--;
+ }
- // FIXME: If we ever have multiple errors, these must all be presented to the user,
- // so they know what to correct.
- // TODO: Make this nicer, see T238061
- return $this->errorReporter->firstError( $parser, $status );
+ // Because of template variables that might be empty, we repeat.
+ if ($processed_text !== null && trim($processed_text) === '') {
+ $processed_text = null;
}
- // @phan-suppress-next-line PhanParamTooFewUnpack No good way to document it.
- $ref = $this->referenceStack->pushRef(
- $parser->getStripState(), $text, $argv, ...array_values( $arguments ) );
- return $ref
- ? $this->footnoteMarkFormatter->linkRef( $parser, $arguments['group'], $ref )
- : '';
+ $ref = $this->referenceStack->setHalfParsedHtml($processed_text, $arguments['group'], $grKey);
+ if (isset($arguments['dir'])) {
+ $ref = $this->referenceStack->setDir($arguments['dir'], $arguments['group'], $grKey);
+ }
+ if (isset($arguments[self::BOOK_REF_ATTRIBUTE])) {
+ $ref = $this->referenceStack->setExtends($arguments[self::BOOK_REF_ATTRIBUTE], $arguments['group'], $grKey);
+ }
+ return $inList ? '' : $this->footnoteMarkFormatter->linkRef($parser, $arguments['group'], $ref);
}
/**
@@ -186,21 +191,19 @@ class Cite {
* @return StatusValue Either an error, or has a value with the dictionary of field names and
* parsed or default values. Missing attributes will be `null`.
*/
- private function parseArguments( array $argv, array $allowedAttributes ): StatusValue {
- $expected = count( $allowedAttributes );
- $allValues = array_merge( array_fill_keys( $allowedAttributes, null ), $argv );
- if ( isset( $allValues['dir'] ) ) {
+ private function parseArguments(array $argv, array $allowedAttributes): StatusValue {
+ $expected = count($allowedAttributes);
+ $allValues = array_merge(array_fill_keys($allowedAttributes, null), $argv);
+ if (isset($allValues['dir'])) {
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal False positive
- $allValues['dir'] = strtolower( $allValues['dir'] );
+ $allValues['dir'] = strtolower($allValues['dir']);
}
- $status = StatusValue::newGood( array_slice( $allValues, 0, $expected ) );
+ $status = StatusValue::newGood(array_slice($allValues, 0, $expected));
- if ( count( $allValues ) > $expected ) {
+ if (count($allValues) > $expected) {
// A <ref> must have a name (can be null), but <references> can't have one
- $status->fatal( in_array( 'name', $allowedAttributes, true )
- ? 'cite_error_ref_too_many_keys'
- : 'cite_error_references_invalid_parameters'
+ $status->fatal(in_array('name', $allowedAttributes, true) ? 'cite_error_ref_too_many_keys' : 'cite_error_references_invalid_parameters'
);
}
@@ -216,27 +219,31 @@ class Cite {
*
* @return string|null Null in case a <references> tag is not allowed in the current context
*/
- public function references( Parser $parser, ?string $text, array $argv ): ?string {
- if ( $this->inRefTag || $this->inReferencesGroup !== null ) {
- return null;
- }
+ public function references(Parser $parser, ?string $text, PPFrame $frame, array $argv): ?string {
- $status = $this->parseArguments( $argv, [ 'group', 'responsive' ] );
+ $status = $this->parseArguments($argv, ['group', 'responsive']);
$arguments = $status->getValue();
- $this->inReferencesGroup = $arguments['group'] ?? self::DEFAULT_GROUP;
+ $gr = & $arguments['group'];
+ if (isset($gr) && $gr) {
+ // To process group template variable
+ $gr = trim($parser->recursiveTagParse($gr, $frame), ' "');
+ }
+ unset($gr);
- $status->merge( $this->parseReferencesTagContent( $parser, $text ) );
- if ( !$status->isGood() ) {
- $ret = $this->errorReporter->firstError( $parser, $status );
- } else {
- $responsive = $arguments['responsive'];
- $ret = $this->formatReferences( $parser, $this->inReferencesGroup, $responsive );
- // Append errors collected while {@see parseReferencesTagContent} processed <ref> tags
- // in <references>
- $ret .= $this->formatReferencesErrors( $parser );
+ $rp = & $arguments['responsive'];
+ if (isset($rp) && $rp) {
+ // To process responsive template variable
+ $rp = trim($parser->recursiveTagParse($rp, $frame), ' "');
}
+ unset($rp);
+ $this->inReferencesGroup = $arguments['group'] ?? self::DEFAULT_GROUP;
+
+ $this->parseReferencesTagContent($parser, $text, $frame);
+
+ $responsive = $arguments['responsive'];
+ $ret = $this->formatReferences($parser, $this->inReferencesGroup, $responsive);
$this->inReferencesGroup = null;
return $ret;
@@ -248,45 +255,25 @@ class Cite {
*
* @return StatusValue
*/
- private function parseReferencesTagContent( Parser $parser, ?string $text ): StatusValue {
+ private function parseReferencesTagContent(Parser $parser, ?string $text, PPFrame $frame): StatusValue {
// Nothing to parse in an empty <references /> tag
- if ( $text === null || trim( $text ) === '' ) {
+ if ($text === null || trim($text) === '') {
return StatusValue::newGood();
}
-
- if ( preg_match( '{' . preg_quote( Parser::MARKER_PREFIX ) . '-(?i:references)-}', $text ) ) {
- return StatusValue::newFatal( 'cite_error_included_references' );
- }
-
- // Detect whether we were sent already rendered <ref>s. Mostly a side effect of using
- // {{#tag:references}}. The following assumes that the parsed <ref>s sent within the
- // <references> block were the most recent calls to <ref>. This assumption is true for
- // all known use cases, but not strictly enforced by the parser. It is possible that
- // some unusual combination of #tag, <references> and conditional parser functions could
- // be created that would lead to malformed references here.
- preg_match_all( '{' . preg_quote( Parser::MARKER_PREFIX ) . '-(?i:ref)-}', $text, $matches );
- $count = count( $matches[0] );
-
- // Undo effects of calling <ref> while unaware of being contained in <references>
- foreach ( $this->referenceStack->rollbackRefs( $count ) as $call ) {
- // Rerun <ref> call with the <references> context now being known
- $this->guardedRef( $parser, ...$call );
- }
-
// Parse the <references> content to process any unparsed <ref> tags, but drop the resulting
// HTML
- $parser->recursiveTagParse( $text );
+ $ptext = $parser->recursiveTagParse($text, $frame);
return StatusValue::newGood();
}
- private function formatReferencesErrors( Parser $parser ): string {
+ private function formatReferencesErrors(Parser $parser): string {
$html = '';
- foreach ( $this->mReferencesErrors->getErrors() as $error ) {
- if ( $html ) {
+ foreach ($this->mReferencesErrors->getErrors() as $error) {
+ if ($html) {
$html .= "<br />\n";
}
- $html .= $this->errorReporter->halfParsed( $parser, $error['message'], ...$error['params'] );
+ $html .= $this->errorReporter->halfParsed($parser, $error['message'], ...$error['params']);
}
$this->mReferencesErrors = StatusValue::newGood();
return $html ? "\n$html" : '';
@@ -307,10 +294,10 @@ class Cite {
global $wgCiteResponsiveReferences;
return $this->referencesFormatter->formatReferences(
- $parser,
- $this->referenceStack->popGroup( $group ),
- $responsive !== null ? $responsive !== '0' : $wgCiteResponsiveReferences,
- $this->isSectionPreview
+ $parser,
+ $this->referenceStack->popGroup($group),
+ $responsive !== null ? $responsive !== '0' : $wgCiteResponsiveReferences,
+ $this->isSectionPreview
);
}
@@ -327,36 +314,23 @@ class Cite {
*
* @return string HTML
*/
- public function checkRefsNoReferences( Parser $parser, bool $isSectionPreview ): string {
+ public function checkRefsNoReferences(Parser $parser, bool $isSectionPreview): string {
+ global $wgCiteResponsiveReferences;
+ $groups = $this->referenceStack->getGroups();
$s = '';
- foreach ( $this->referenceStack->getGroups() as $group ) {
- if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
- $s .= $this->formatReferences( $parser, $group );
- } else {
- $s .= '<br />' . $this->errorReporter->halfParsed(
- $parser,
- 'cite_error_group_refs_without_references',
- Sanitizer::safeEncodeAttribute( $group )
- );
+ foreach ($groups as $group) {
+ if (!$isSectionPreview) {
+ $remainingRefs = $this->referenceStack->getGroupRefs($group);
}
- }
- if ( $isSectionPreview && $s !== '' ) {
- $headerMsg = wfMessage( 'cite_section_preview_references' );
- if ( !$headerMsg->isDisabled() ) {
- $s = Html::element(
- 'h2',
- [ 'id' => 'mw-ext-cite-cite_section_preview_references_header' ],
- $headerMsg->text()
- ) . $s;
- }
- // provide a preview of references in its own section
- $s = Html::rawElement(
- 'div',
- [ 'class' => 'mw-ext-cite-cite_section_preview_references' ],
- $s
+ $formattedRefs = $this->referencesFormatter->formatReferences(
+ $parser,
+ $remainingRefs,
+ $wgCiteResponsiveReferences,
+ $this->isSectionPreview
);
+ $s .= $formattedRefs;
}
- return $s !== '' ? "\n" . $s : '';
+ return $s;
}
/**
@@ -364,7 +338,6 @@ class Cite {
* @return never
*/
public function __clone() {
- throw new LogicException( 'Create a new instance please' );
+ throw new LogicException('Create a new instance please');
}
-
}
diff --git a/src/Hooks/CiteParserTagHooks.php b/src/Hooks/CiteParserTagHooks.php
index 256d7c6e..759e557a 100644
--- a/src/Hooks/CiteParserTagHooks.php
+++ b/src/Hooks/CiteParserTagHooks.php
@@ -14,9 +14,9 @@ class CiteParserTagHooks {
/**
* Enables the two <ref> and <references> tags.
*/
- public static function register( Parser $parser ): void {
- $parser->setHook( 'ref', [ __CLASS__, 'ref' ] );
- $parser->setHook( 'references', [ __CLASS__, 'references' ] );
+ public static function register(Parser $parser): void {
+ $parser->setHook('ref', [__CLASS__, 'ref']);
+ $parser->setHook('references', [__CLASS__, 'references']);
}
/**
@@ -35,16 +35,16 @@ class CiteParserTagHooks {
Parser $parser,
PPFrame $frame
): string {
- $cite = self::citeForParser( $parser );
- $result = $cite->ref( $parser, $text, $argv );
+ $cite = self::citeForParser($parser);
+ $result = $cite->ref($parser, $text, $frame, $argv);
- if ( $result === null ) {
- return htmlspecialchars( "<ref>$text</ref>" );
+ if ($result === null) {
+ return htmlspecialchars("<ref>$text</ref>");
}
$parserOutput = $parser->getOutput();
- $parserOutput->addModules( [ 'ext.cite.ux-enhancements' ] );
- $parserOutput->addModuleStyles( [ 'ext.cite.styles' ] );
+ $parserOutput->addModules(['ext.cite.ux-enhancements']);
+ $parserOutput->addModuleStyles(['ext.cite.styles']);
$frame->setVolatile();
return $result;
@@ -66,13 +66,11 @@ class CiteParserTagHooks {
Parser $parser,
PPFrame $frame
): string {
- $cite = self::citeForParser( $parser );
- $result = $cite->references( $parser, $text, $argv );
+ $cite = self::citeForParser($parser);
+ $result = $cite->references($parser, $text, $frame, $argv);
- if ( $result === null ) {
- return htmlspecialchars( $text === null
- ? "<references/>"
- : "<references>$text</references>"
+ if ($result === null) {
+ return htmlspecialchars($text === null ? "<references/>" : "<references>$text</references>"
);
}
@@ -83,9 +81,8 @@ class CiteParserTagHooks {
/**
* Get or create Cite state for this parser.
*/
- private static function citeForParser( Parser $parser ): Cite {
- $parser->extCite ??= new Cite( $parser );
+ private static function citeForParser(Parser $parser): Cite {
+ $parser->extCite ??= new Cite($parser);
return $parser->extCite;
}
-
}
diff --git a/src/ReferenceStack.php b/src/ReferenceStack.php
index eb312567..ecbefc98 100644
--- a/src/ReferenceStack.php
+++ b/src/ReferenceStack.php
@@ -48,114 +48,105 @@ class ReferenceStack {
private const ACTION_NEW = 'new';
/**
- * Leave a mark in the stack which matches an invalid ref tag.
+ * Register a new ref
+ *
+ * @param string $group
+ * @param ?string $name
+ * @param bool $inList
+ *
+ * @return string|int The group key $grKey of the new ref in the group
*/
- public function pushInvalidRef(): void {
- $this->refCallStack[] = false;
+ public function register(string $group, ?string $name, bool $inList): string|int {
+ if (!isset($this->refs[$group]) || !isset($name) || !isset($this->refs[$group][$name])) {
+ // This is the case where we increment $this->refSequence and $this->groupRefSequence[$group].
+ $this->refs[$group] ??= [];
+ $this->groupRefSequence[$group] ??= 0;
+ $ref = new ReferenceStackItem();
+ $ref->count = $inList ? 0 : 1;
+ $ref->group = $group;
+ $ref->number = ++$this->groupRefSequence[$group];
+ $ref->key = ++$this->refSequence;
+ if (!$name) {
+ // This is an anonymous reference, which will be given a numeric index.
+ $this->refs[$group][] = $ref;
+ } else {
+ // Valid group key with first occurrence
+ $ref->name = $name;
+ $this->refs[$group][$name] = $ref;
+ }
+ end($this->refs[$group]);
+ $grKey = key($this->refs[$group]);
+ } else {
+ if (!$inList)
+ $this->refs[$group][$name]->count++;
+ $grKey = $name;
+ }
+ return $grKey;
}
/**
- * Populate $this->refs and $this->refCallStack based on input and arguments to <ref>
+ * Set the text for a ref that is identified by its group and key
*
- * @param StripState $stripState
* @param ?string $text Content from the <ref> tag
- * @param string[] $argv
* @param string $group
- * @param ?string $name
- * @param ?string $extends
- * @param ?string $follow Guaranteed to not be a numeric string
- * @param ?string $dir ref direction
+ * @param string|int $grKey
*
- * @return ?ReferenceStackItem ref structure, or null if no footnote marker should be rendered
+ * @return ReferenceStackItem
*/
- public function pushRef(
- StripState $stripState,
+ public function setHalfParsedHtml(
?string $text,
- array $argv,
string $group,
- ?string $name,
- ?string $extends,
- ?string $follow,
- ?string $dir
- ): ?ReferenceStackItem {
- $this->refs[$group] ??= [];
- $this->groupRefSequence[$group] ??= 0;
-
- $ref = new ReferenceStackItem();
- $ref->count = 1;
- $ref->dir = $dir;
- // TODO: Read from this group field or deprecate it.
- $ref->group = $group;
- $ref->name = $name;
- $ref->text = $text;
-
- if ( $follow ) {
- if ( !isset( $this->refs[$group][$follow] ) ) {
- // Mark an incomplete follow="…" as such. This is valid e.g. in the Page:… namespace
- // on Wikisource.
- $ref->follow = $follow;
- $ref->key = $this->nextRefSequence();
- $this->refs[$group][] = $ref;
- $this->refCallStack[] = [ self::ACTION_NEW, $ref->key, $group, $name, $text, $argv ];
- } elseif ( $text !== null ) {
- // We know the parent already, so just perform the follow="…" and bail out
- $this->resolveFollow( $group, $follow, $text );
- }
- // A follow="…" never gets its own footnote marker
- return null;
+ string|int $grKey,
+ ): ReferenceStackItem {
+ if ($this->refs[$group][$grKey]->text === null && $text !== null) {
+ // If no text was set before, use this text
+ $this->refs[$group][$grKey]->text = $text;
}
+ // The case elseif $this->refs[$group][$grKey]->text !== $text must be previously managed by validation
+ return $this->refs[$group][$grKey];
+ }
- if ( !$name ) {
- // This is an anonymous reference, which will be given a numeric index.
- $this->refs[$group][] = &$ref;
- $ref->key = $this->nextRefSequence();
- $action = self::ACTION_NEW;
- } elseif ( !isset( $this->refs[$group][$name] ) ) {
- // Valid key with first occurrence
- $this->refs[$group][$name] = &$ref;
- $ref->key = $this->nextRefSequence();
- $action = self::ACTION_NEW;
- } elseif ( $this->refs[$group][$name]->placeholder ) {
- // Populate a placeholder.
- $ref->extendsCount = $this->refs[$group][$name]->extendsCount;
- $ref->key = $this->nextRefSequence();
- $ref->number = $this->refs[$group][$name]->number;
- $this->refs[$group][$name] =& $ref;
- $action = self::ACTION_NEW_FROM_PLACEHOLDER;
- } else {
- // Change an existing entry.
- $ref = &$this->refs[$group][$name];
- $ref->count++;
-
- if ( $ref->dir && $dir && $ref->dir !== $dir ) {
- $ref->warnings[] = [ 'cite_error_ref_conflicting_dir', $name ];
- }
-
- if ( $ref->text === null && $text !== null ) {
- // If no text was set before, use this text
- $ref->text = $text;
- // Use the dir parameter only from the full definition of a named ref tag
- $ref->dir = $dir;
- $action = self::ACTION_ASSIGN;
- } else {
- if ( $text !== null
- // T205803 different strip markers might hide the same text
- && $stripState->unstripBoth( $text )
- !== $stripState->unstripBoth( $ref->text )
- ) {
- // two refs with same name and different text
- $ref->warnings[] = [ 'cite_error_references_duplicate_key', $name ];
- }
- $action = self::ACTION_INCREMENT;
- }
+ /**
+ * Set the direction for a ref that is identified by its group and key
+ *
+ * @param ?string $dir Direction from the <ref> tag
+ * @param string $group
+ * @param string|int $grKey
+ *
+ * @return ReferenceStackItem
+ */
+ public function setDir(
+ ?string $dir,
+ string $group,
+ string|int $grKey,
+ ): ReferenceStackItem {
+ if ($this->refs[$group][$grKey]->dir === null && $dir !== null && $ref->text === null && $text !== null) {
+ // If no dir was set before, use this dir, but only if there is a text.
+ $this->refs[$group][$grKey]->dir = $dir;
}
+ // The case $this->refs[$group][$grKey]->dir !== $dir must be previously managed by validation
+ return $this->refs[$group][$grKey];
+ }
- $ref->number ??= ++$this->groupRefSequence[$group];
-
+ /**
+ * Set the parent for a ref that is identified by its group and key
+ *
+ * @param ?string $extends parent from the <ref> tag
+ * @param string $group
+ * @param string|int $grKey
+ *
+ * @return ReferenceStackItem
+ */
+ public function setExtends(
+ ?string $extends,
+ string $group,
+ string|int $grKey,
+ ): ReferenceStackItem {
+ $ref = & $this->refs[$group][$grKey];
// Do not mess with a known parent a second time
- if ( $extends && !isset( $ref->extendsIndex ) ) {
- $parentRef =& $this->refs[$group][$extends];
- if ( !isset( $parentRef ) ) {
+ if ($extends && !isset($ref->extendsIndex)) {
+ $parentRef = & $this->refs[$group][$extends];
+ if (!isset($parentRef)) {
// Create a new placeholder and give it the current sequence number.
$parentRef = new ReferenceStackItem();
$parentRef->name = $extends;
@@ -169,38 +160,11 @@ class ReferenceStack {
$parentRef->extendsCount ??= 0;
$ref->extends = $extends;
$ref->extendsIndex = ++$parentRef->extendsCount;
- } elseif ( $extends && $ref->extends !== $extends ) {
- // TODO: Change the error message to talk about "conflicting content or parent"?
- $ref->warnings[] = [ 'cite_error_references_duplicate_key', $name ];
}
-
- $this->refCallStack[] = [ $action, $ref->key, $group, $name, $text, $argv ];
- return $ref;
- }
-
- /**
- * 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, as a list of [ $text, $argv ]
- * @phan-return array<array{0:?string,1:array}>
- */
- public function rollbackRefs( int $count ): array {
- $redoStack = [];
- while ( $count-- && $this->refCallStack ) {
- $call = array_pop( $this->refCallStack );
- if ( $call ) {
- // @phan-suppress-next-line PhanParamTooFewUnpack
- $redoStack[] = $this->rollbackRef( ...$call );
- }
- }
-
- // Drop unused rollbacks, this group is finished.
- $this->refCallStack = [];
-
- return array_reverse( $redoStack );
+ // The case $extends && $ref->extends !== $extends )
+ // must be managed in validation
+ // $ref->warnings[] = [ 'cite_error_references_duplicate_key', $name ];
+ return $this->refs[$group][$grKey];
}
/**
@@ -234,15 +198,15 @@ class ReferenceStack {
?string $text,
array $argv
): array {
- if ( !$this->hasGroup( $group ) ) {
- throw new LogicException( "Cannot roll back ref with unknown group \"$group\"." );
+ if (!$this->hasGroup($group)) {
+ throw new LogicException("Cannot roll back ref with unknown group \"$group\".");
}
$lookup = $name ?: null;
- if ( $lookup === null ) {
+ if ($lookup === null) {
// Find anonymous ref by key.
- foreach ( $this->refs[$group] as $k => $v ) {
- if ( $this->refs[$group][$k]->key === $key ) {
+ foreach ($this->refs[$group] as $k => $v) {
+ if ($this->refs[$group][$k]->key === $key) {
$lookup = $k;
break;
}
@@ -250,26 +214,26 @@ class ReferenceStack {
}
// Obsessive sanity checks that the specified element exists.
- if ( $lookup === null ) {
- throw new LogicException( "Cannot roll back unknown ref by key $key." );
- } elseif ( !isset( $this->refs[$group][$lookup] ) ) {
- throw new LogicException( "Cannot roll back missing named ref \"$lookup\"." );
- } elseif ( $this->refs[$group][$lookup]->key !== $key ) {
+ if ($lookup === null) {
+ throw new LogicException("Cannot roll back unknown ref by key $key.");
+ } elseif (!isset($this->refs[$group][$lookup])) {
+ throw new LogicException("Cannot roll back missing named ref \"$lookup\".");
+ } elseif ($this->refs[$group][$lookup]->key !== $key) {
throw new LogicException(
- "Cannot roll back corrupt named ref \"$lookup\" which should have had key $key." );
+ "Cannot roll back corrupt named ref \"$lookup\" which should have had key $key.");
}
- $ref =& $this->refs[$group][$lookup];
+ $ref = & $this->refs[$group][$lookup];
- switch ( $action ) {
+ switch ($action) {
case self::ACTION_NEW:
// Rollback the addition of new elements to the stack
- unset( $this->refs[$group][$lookup] );
- if ( !$this->refs[$group] ) {
- $this->popGroup( $group );
- } elseif ( isset( $this->groupRefSequence[$group] ) ) {
+ unset($this->refs[$group][$lookup]);
+ if (!$this->refs[$group]) {
+ $this->popGroup($group);
+ } elseif (isset($this->groupRefSequence[$group])) {
$this->groupRefSequence[$group]--;
}
- if ( $ref->extends ) {
+ if ($ref->extends) {
$this->refs[$group][$ref->extends]->extendsCount--;
}
break;
@@ -287,9 +251,9 @@ class ReferenceStack {
$ref->count--;
break;
default:
- throw new LogicException( "Unknown call stack action \"$action\"" );
+ throw new LogicException("Unknown call stack action \"$action\"");
}
- return [ $text, $argv ];
+ return [$text, $argv];
}
/**
@@ -299,18 +263,18 @@ class ReferenceStack {
*
* @return array<string|int,ReferenceStackItem> The references from the removed group
*/
- public function popGroup( string $group ): array {
- $refs = $this->getGroupRefs( $group );
- unset( $this->refs[$group] );
- unset( $this->groupRefSequence[$group] );
+ public function popGroup(string $group): array {
+ $refs = $this->getGroupRefs($group);
+ unset($this->refs[$group]);
+ unset($this->groupRefSequence[$group]);
return $refs;
}
/**
* Returns true if the group exists and contains references.
*/
- public function hasGroup( string $group ): bool {
- return isset( $this->refs[$group] ) && $this->refs[$group];
+ public function hasGroup(string $group): bool {
+ return isset($this->refs[$group]) && $this->refs[$group];
}
/**
@@ -320,8 +284,8 @@ class ReferenceStack {
*/
public function getGroups(): array {
$groups = [];
- foreach ( $this->refs as $group => $refs ) {
- if ( $refs ) {
+ foreach ($this->refs as $group => $refs) {
+ if ($refs) {
$groups[] = $group;
}
}
@@ -335,30 +299,29 @@ class ReferenceStack {
*
* @return array<string|int,ReferenceStackItem>
*/
- public function getGroupRefs( string $group ): array {
+ public function getGroupRefs(string $group): array {
return $this->refs[$group] ?? [];
}
- private function resolveFollow( string $group, string $follow, string $text ): void {
- $previousRef =& $this->refs[$group][$follow];
+ private function resolveFollow(string $group, string $follow, string $text): void {
+ $previousRef = & $this->refs[$group][$follow];
$previousRef->text ??= '';
$previousRef->text .= " $text";
}
- public function listDefinedRef( string $group, string $name, string $text ): void {
- $ref =& $this->refs[$group][$name];
+ public function listDefinedRef(string $group, string $name, string $text): void {
+ $ref = & $this->refs[$group][$name];
$ref ??= new ReferenceStackItem();
$ref->placeholder = false;
- if ( !isset( $ref->text ) ) {
+ if (!isset($ref->text)) {
$ref->text = $text;
- } elseif ( $ref->text !== $text ) {
+ } elseif ($ref->text !== $text) {
// two refs with same key and different content
- $ref->warnings[] = [ 'cite_error_references_duplicate_key', $name ];
+ $ref->warnings[] = ['cite_error_references_duplicate_key', $name];
}
}
private function nextRefSequence() {
return ++$this->refSequence;
}
-
}
diff --git a/src/ReferencesFormatter.php b/src/ReferencesFormatter.php
index 329a93c3..aeb7edfe 100644
--- a/src/ReferencesFormatter.php
+++ b/src/ReferencesFormatter.php
@@ -49,23 +49,24 @@ class ReferencesFormatter {
bool $responsive,
bool $isSectionPreview
): string {
- if ( !$groupRefs ) {
+ if (!$groupRefs) {
return '';
}
- $wikitext = $this->formatRefsList( $parser, $groupRefs, $isSectionPreview );
+ $wikitext = $this->formatRefsList($parser, $groupRefs, $isSectionPreview);
// Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar
- $html = rtrim( $parser->recursiveTagParse( $wikitext ), "\n" );
+ // $parser->recursiveTagParse( $wikitext );
+ $html = $wikitext;
- if ( $responsive ) {
- $wrapClasses = [ 'mw-references-wrap' ];
- if ( count( $groupRefs ) > 10 ) {
+ if ($responsive) {
+ $wrapClasses = ['mw-references-wrap'];
+ if (count($groupRefs) > 10) {
$wrapClasses[] = 'mw-references-columns';
}
// 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.
- return Html::rawElement( 'div', [ 'class' => $wrapClasses ], $html );
+ return Html::rawElement('div', ['class' => $wrapClasses], $html);
}
return $html;
@@ -83,45 +84,13 @@ class ReferencesFormatter {
array $groupRefs,
bool $isSectionPreview
): string {
- // After sorting the list, we can assume that references are in the same order as their
- // numbering. Subreferences will come immediately after their parent.
- uasort(
- $groupRefs,
- static function ( ReferenceStackItem $a, ReferenceStackItem $b ): int {
- $cmp = ( $a->number ?? 0 ) - ( $b->number ?? 0 );
- return $cmp ?: ( $a->extendsIndex ?? 0 ) - ( $b->extendsIndex ?? 0 );
- }
- );
-
// 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";
- /** @var string|bool $indented */
- $indented = false;
- foreach ( $groupRefs as $key => &$ref ) {
- // Make sure the parent is not a subreference.
- // FIXME: Move to a validation function.
- $extends =& $ref->extends;
- if ( isset( $extends ) && isset( $groupRefs[$extends]->extends ) ) {
- $ref->warnings[] = [ 'cite_error_ref_nested_extends',
- $extends, $groupRefs[$extends]->extends ];
- }
-
- if ( !$indented && isset( $extends ) ) {
- // The nested <ol> must be inside the parent's <li>
- if ( preg_match( '#</li>\s*$#D', $parserInput, $matches, PREG_OFFSET_CAPTURE ) ) {
- $parserInput = substr( $parserInput, 0, $matches[0][1] );
- }
- $parserInput .= Html::openElement( 'ol', [ 'class' => 'mw-extended-references' ] );
- $indented = $matches[0][0] ?? true;
- } elseif ( $indented && !isset( $extends ) ) {
- $parserInput .= $this->closeIndention( $indented );
- $indented = false;
- }
- $parserInput .= $this->formatListItem( $parser, $key, $ref, $isSectionPreview ) . "\n";
+ foreach ($groupRefs as $grKey => &$ref) {
+ $parserInput .= $this->formatListItem($parser, $grKey, $ref, $isSectionPreview) . "\n";
}
- $parserInput .= $this->closeIndention( $indented );
- return Html::rawElement( 'ol', [ 'class' => 'references' ], $parserInput );
+ return Html::rawElement('ol', ['class' => 'references'], $parserInput);
}
/**
@@ -129,12 +98,12 @@ class ReferencesFormatter {
*
* @return string
*/
- private function closeIndention( $closingLi ): string {
- if ( !$closingLi ) {
+ private function closeIndention($closingLi): string {
+ if (!$closingLi) {
return '';
}
- return Html::closeElement( 'ol' ) . ( is_string( $closingLi ) ? $closingLi : '' );
+ return Html::closeElement('ol') . ( is_string($closingLi) ? $closingLi : '' );
}
/**
@@ -146,67 +115,70 @@ class ReferencesFormatter {
* @return string Wikitext, wrapped in a single <li> element
*/
private function formatListItem(
- Parser $parser, $key, ReferenceStackItem $ref, bool $isSectionPreview
+ Parser $parser, $grKey, ReferenceStackItem $ref, bool $isSectionPreview
): string {
- $text = $this->referenceText( $parser, $key, $ref, $isSectionPreview );
- $error = '';
+ $text = $this->referenceText($parser, $grKey, $ref, $isSectionPreview);
$extraAttributes = '';
- if ( isset( $ref->dir ) ) {
+ // Todo: Check that the followings are OK with the new code.
+ if (isset($ref->dir)) {
// The following classes are generated here:
// * mw-cite-dir-ltr
// * mw-cite-dir-rtl
- $extraAttributes = Html::expandAttributes( [ 'class' => 'mw-cite-dir-' . $ref->dir ] );
+ $extraAttributes = Html::expandAttributes(['class' => 'mw-cite-dir-' . $ref->dir]);
}
// Special case for an incomplete follow="…". This is valid e.g. in the Page:… namespace on
// Wikisource. Note this returns a <p>, not an <li> as expected!
- if ( isset( $ref->follow ) ) {
- return '<p id="' . $this->anchorFormatter->jumpLinkTarget( $ref->follow ) . '">' . $text . '</p>';
+ if (isset($ref->follow)) {
+ return '<p id="' . $this->anchorFormatter->jumpLinkTarget($ref->follow) . '">' . $text . '</p>';
}
- if ( $ref->count === 1 ) {
- if ( !isset( $ref->name ) ) {
+ if ($ref->count === 1) {
+ if (!isset($ref->name)) {
$id = $ref->key;
- $backlinkId = $this->anchorFormatter->backLink( $ref->key );
+ $backlinkId = $this->anchorFormatter->backLink($ref->key);
} else {
- $id = $key . '-' . $ref->key;
+ $id = $ref->name . '-' . $ref->key;
// TODO: Use count without decrementing.
- $backlinkId = $this->anchorFormatter->backLink( $key, $ref->key . '-' . ( $ref->count - 1 ) );
+ $backlinkId = $this->anchorFormatter->backLink($ref->name, $ref->key . '-' . ( $ref->count - 1 ));
}
- return $this->messageLocalizer->msg(
- 'cite_references_link_one',
- $this->anchorFormatter->jumpLinkTarget( $id ),
- $backlinkId,
- $text . $error,
- $extraAttributes
- )->plain();
+ // recursiveTagParse() is needed, but it should only be applied to the wikitext, not $text,
+ // which is half-parsed html.
+ $s = bin2hex(openssl_random_pseudo_bytes(10));
+ return str_replace("---Marker-$s-for-text-in-ReferencesFormatter::formatListItem---", $text, $parser->recursiveTagParse($this->messageLocalizer->msg(
+ 'cite_references_link_one',
+ $this->anchorFormatter->jumpLinkTarget($id),
+ $backlinkId,
+ "---Marker-$s-for-text-in-ReferencesFormatter::formatListItem---",
+ $extraAttributes
+ )->plain()));
}
// Named references with >1 occurrences
$backlinks = [];
- for ( $i = 0; $i < $ref->count; $i++ ) {
- $backlinks[] = $this->messageLocalizer->msg(
- 'cite_references_link_many_format',
- $this->anchorFormatter->backLink( $key, $ref->key . '-' . $i ),
- $this->referencesFormatEntryNumericBacklinkLabel(
- $ref->number .
- ( isset( $ref->extendsIndex ) ? '.' . $ref->extendsIndex : '' ),
- $i,
- $ref->count
- ),
- $this->referencesFormatEntryAlternateBacklinkLabel( $parser, $i )
- )->plain();
+ for ($i = 0; $i < $ref->count; $i++) {
+ // recursiveTagParse() is needed, because it is not executed on the entire ref item.
+ $backlinks[] = $parser->recursiveTagParse($this->messageLocalizer->msg(
+ 'cite_references_link_many_format',
+ $this->anchorFormatter->backLink($grKey, $ref->key . '-' . $i),
+ $this->referencesFormatEntryNumericBacklinkLabel(
+ $ref->number,
+ $i,
+ $ref->count
+ ),
+ $this->referencesFormatEntryAlternateBacklinkLabel($parser, $i)
+ )->plain());
}
$linkTargetId = $ref->count > 0 ?
- $this->anchorFormatter->jumpLinkTarget( $key . '-' . $ref->key ) : '';
+ $this->anchorFormatter->jumpLinkTarget($grKey . '-' . $ref->key) : 'Missing ref in prior text';
return $this->messageLocalizer->msg(
- 'cite_references_link_many',
- $linkTargetId,
- $this->listToText( $backlinks ),
- $text . $error,
- $extraAttributes
- )->plain();
+ 'cite_references_link_many',
+ $linkTargetId,
+ $this->listToText($backlinks),
+ $text,
+ $extraAttributes
+ )->plain();
}
/**
@@ -218,24 +190,22 @@ class ReferencesFormatter {
* @return string
*/
private function referenceText(
- Parser $parser, $key, ReferenceStackItem $ref, bool $isSectionPreview
+ Parser $parser, $grKey, ReferenceStackItem $ref, bool $isSectionPreview
): string {
$text = $ref->text ?? null;
- if ( $text === null ) {
- return $this->errorReporter->plain( $parser,
- $isSectionPreview
- ? 'cite_warning_sectionpreview_no_text'
- : 'cite_error_references_no_text', $key );
+ if ($text === null) {
+ return $this->errorReporter->plain($parser,
+ $isSectionPreview ? 'cite_warning_sectionpreview_no_text' : 'cite_error_references_no_text', $grKey);
}
- foreach ( $ref->warnings as $warning ) {
+ foreach ($ref->warnings as $warning) {
// @phan-suppress-next-line PhanParamTooFewUnpack
- $text .= ' ' . $this->errorReporter->plain( $parser, ...$warning );
+ $text .= ' ' . $this->errorReporter->plain($parser, ...$warning);
// FIXME: We could use a StatusValue object to get rid of duplicates
break;
}
- return '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\n";
+ return '<span class="reference-text">' . rtrim($text, "\n") . "</span>\n";
}
/**
@@ -254,11 +224,11 @@ class ReferencesFormatter {
int $offset,
int $max
): string {
- return $this->messageLocalizer->localizeDigits( $base ) .
- $this->messageLocalizer->localizeSeparators( '.' ) .
+ return $this->messageLocalizer->localizeDigits($base) .
+ $this->messageLocalizer->localizeSeparators('.') .
$this->messageLocalizer->localizeDigits(
- str_pad( (string)$offset, strlen( (string)$max ), '0', STR_PAD_LEFT )
- );
+ str_pad((string) $offset, strlen((string) $max), '0', STR_PAD_LEFT)
+ );
}
/**
@@ -272,12 +242,11 @@ class ReferencesFormatter {
): string {
$this->backlinkLabels ??= preg_split(
'/\s+/',
- $this->messageLocalizer->msg( 'cite_references_link_many_format_backlink_labels' )
+ $this->messageLocalizer->msg('cite_references_link_many_format_backlink_labels')
->plain()
);
- return $this->backlinkLabels[$offset]
- ?? $this->errorReporter->plain( $parser, 'cite_error_references_no_backlink_label' );
+ return $this->backlinkLabels[$offset] ?? $this->errorReporter->plain($parser, 'cite_error_references_no_backlink_label');
}
/**
@@ -291,16 +260,15 @@ class ReferencesFormatter {
*
* @return string
*/
- private function listToText( array $arr ): string {
- $lastElement = array_pop( $arr );
+ private function listToText(array $arr): string {
+ $lastElement = array_pop($arr);
- if ( $arr === [] ) {
- return (string)$lastElement;
+ if ($arr === []) {
+ return (string) $lastElement;
}
- $sep = $this->messageLocalizer->msg( 'cite_references_link_many_sep' )->plain();
- $and = $this->messageLocalizer->msg( 'cite_references_link_many_and' )->plain();
- return implode( $sep, $arr ) . $and . $lastElement;
+ $sep = $this->messageLocalizer->msg('cite_references_link_many_sep')->plain();
+ $and = $this->messageLocalizer->msg('cite_references_link_many_and')->plain();
+ return implode($sep, $arr) . $and . $lastElement;
}
-
}

File Metadata

Mime Type
text/x-diff
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
14506221
Default Alt Text
new-Cite-ext-design.patch (40 KB)

Event Timeline