Page MenuHomeWickedGov Phorge

SerializerState.php
No OneTemporary

Size
28 KB
Referenced Files
None
Subscribers
None

SerializerState.php

<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Html2Wt;
use Composer\Semver\Semver;
use stdClass;
use Wikimedia\Assert\Assert;
use Wikimedia\Parsoid\Config\Env;
use Wikimedia\Parsoid\Core\DomSourceRange;
use Wikimedia\Parsoid\Core\SelectiveUpdateData;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Node;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
use Wikimedia\Parsoid\Html2Wt\ConstrainedText\ConstrainedText;
use Wikimedia\Parsoid\Tokens\SourceRange;
use Wikimedia\Parsoid\Utils\DiffDOMUtils;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Utils\Utils;
use Wikimedia\Parsoid\Utils\WTUtils;
/**
* State object for the wikitext serializers.
*/
class SerializerState {
/**
* Regexp for checking if what we have consumed wikimarkup that has special meaning at the
* beginning of the line, and is indeed at the beginning of the line (modulo comments and
* other ignored elements).
*
* @return string
*/
private function solWikitextRegexp(): string {
static $solWikitextRegexp = null;
if ( $solWikitextRegexp === null ) {
$sol = PHPUtils::reStrip(
$this->env->getSiteConfig()->solTransparentWikitextNoWsRegexp( true ),
'@'
);
$solWikitextRegexp = '@' .
'^(' . $sol . ')' .
'([\ \*#:;{\|!=].*)$' .
'@D';
}
return $solWikitextRegexp;
}
/**
* Regexp for checking whether we are at the start of the line (modulo comments and
* other ignored elements).
*
* @return string
*/
private function solRegexp(): string {
static $solRegexp = null;
if ( $solRegexp === null ) {
$sol = PHPUtils::reStrip(
$this->env->getSiteConfig()->solTransparentWikitextNoWsRegexp( true ),
'@'
);
$solRegexp = '@(^|\n)' . $sol . '$@D';
}
return $solRegexp;
}
/**
* Separator information:
* - constraints (array<array|int>|null): min/max number of newlines
* - src (string|null): collected separator text from DOM text/comment nodes
* - lastSourceNode (?Node): Seems to be bookkeeping to make sure we don't reuse
* original separators when `emitChunk` is called
* consecutively on the same node. However, it also
* differs from `state.prevNode` in that it only gets
* updated when a node calls `emitChunk` so that nodes
* serializing `justChildren` don't mix up `buildSep`.
* FIXME: could use a dedicated class
* @var stdClass
*/
public $sep;
/**
* Is the serializer at the start of a new wikitext line?
* @var bool
*/
public $onSOL = true;
/**
* True when wts kicks off, false after the first char has been output
* SSS FIXME: Can this be done away with in some way?
* @var bool
*/
public $atStartOfOutput = true;
/**
* Is the serializer currently handling link content (children of `<a>`)?
* @var bool
*/
public $inLink = false;
/**
* Is the serializer currently handling caption content?
* @var bool
*/
public $inCaption = false;
/**
* Is the serializer currently handling an indent-pre tag?
* @var bool
*/
public $inIndentPre = false;
/**
* Is the serializer currently handling a html-pre tag?
* @var bool
*/
public $inHTMLPre = false;
/**
* Is the serializer currently handling a tag that the PHP parser
* treats as a block tag?
* @var bool
*/
public $inPHPBlock = false;
/**
* Is the serializer being invoked recursively to serialize a
* template-generated attribute (via `WSP.getAttributeValue`'s
* template handling). If so, we should suppress some
* serialization escapes, like autolink protection, since
* these are not valid for attribute values.
* @var bool
*/
public $inAttribute = false;
/**
* Is the serializer currently processing a subtree that has been
* marked inserted compared to original content (ex: via VE / CX)?
*
* @var bool
*/
public $inInsertedContent;
/**
* Did we introduce nowikis for indent-pre protection?
* If yes, we might run a post-pass to strip useless ones.
* @var bool
*/
public $hasIndentPreNowikis = false;
/**
* Did we introduce nowikis to preserve quote semantics?
* If yes, we might run a post-pass to strip useless ones.
* @var bool
*/
public $hasQuoteNowikis = false;
/**
* Did we introduce `<nowiki />`s?
* If yes, we do a postpass to remove unnecessary trailing ones.
* @var bool
*/
public $hasSelfClosingNowikis = false;
/**
* Did we introduce nowikis around `=.*=` text?
* If yes, we do a postpass to remove unnecessary escapes.
* @var bool
*/
public $hasHeadingEscapes = false;
/**
* Records the nesting level of wikitext tables
* @var int
*/
public $wikiTableNesting = 0;
/**
* Stack of wikitext escaping handlers -- these handlers are responsible
* for smart escaping when the surrounding wikitext context is known.
* @var callable[] See {@link serializeChildren()}
*/
public $wteHandlerStack = [];
/**
* This array is used by the wikitext escaping algorithm -- represents
* a "single line" of output wikitext as represented by a block node in
* the DOM.
* - firstNode (?Node): first DOM node processed on this line
* - text (string): output so far from all nodes on the current line
* - chunks (ConstrainedText[]): list of chunks comprising the current line
* @var stdClass
* XXX: replace with output buffering per line
* FIXME: could use a dedicated class
*/
public $currLine;
/**
* Stack used to enforce single-line context
* @var SingleLineContext
*/
public $singleLineContext;
/**
* Text to be emitted at the start of file, for redirects
* @var string|null
*/
public $redirectText = null;
/** @var WikitextSerializer */
public $serializer;
/** @var ParsoidExtensionAPI */
public $extApi;
/** @var string The serialized output */
public $out = '';
/**
* Whether to use heuristics to determine if a list item, heading, table cell, etc.
* should have whitespace inserted after the "*#=|!" wikitext chars? This is normally
* true by default, but not so if HTML content version is older than 1.7.0.
* In practice, we are now at version 2.1, but Flow stores HTML, so till Flow migrates
* all its content over to a later version, we need a boolean flag.
* @var bool
*/
public $useWhitespaceHeuristics;
/**
* Are we in selective serialization mode?
* @see SelectiveSerializer
* @var bool
*/
public $selserMode;
private ?SelectiveUpdateData $selserData;
/**
* If in selser mode, while processing a node, do we know if
* its previous node has not been modified in an edit?
* @var bool
*/
public $prevNodeUnmodified;
/**
* If in selser mode, while processing a node, do we know if
* it has not been modified in an edit?
* @var bool
*/
public $currNodeUnmodified;
/**
* Should we run the wikitext escaping code on the wikitext chunk
* that will be emitted?
* @var bool
*/
public $needsEscaping = false;
/**
* Used as fast patch for special protected characters in WikitextEscapeHandlers and
* comes from LanguageVariantHandler
* @var string|null
*/
public $protect;
/** @var Separators */
public $separators;
/** @var Env */
private $env;
/** @var Element */
public $currNode;
/** @var Element */
private $prevNode;
/** @var array */
public $openAnnotations;
/**
* Log prefix to use in trace output
* @var string
*/
private $logPrefix = 'OUT:';
public $haveTrimmedWsDSR = false;
/**
* @param WikitextSerializer $serializer
* @param array $options List of options for serialization:
* - onSOL: (bool)
* - inPHPBlock: (bool)
* - inAttribute: (bool)
* - protect: (string)
* - selserData: (SelectiveUpdateData)
*/
public function __construct( WikitextSerializer $serializer, array $options = [] ) {
$this->env = $serializer->env;
$this->serializer = $serializer;
$this->extApi = new ParsoidExtensionAPI( $this->env, [ 'html2wt' => [ 'state' => $this ] ] );
$this->onSOL = $options['onSOL'] ?? $this->onSOL;
$this->inPHPBlock = $options['inPHPBlock'] ?? $this->inPHPBlock;
$this->inAttribute = $options['inAttribute'] ?? $this->inAttribute;
$this->protect = $options['protect'] ?? null;
$this->selserData = $options['selserData'] ?? null;
$this->resetCurrLine( null );
$this->singleLineContext = new SingleLineContext();
$this->resetSep();
$this->haveTrimmedWsDSR = Semver::satisfies( $this->env->getInputContentVersion(), '>=2.1.1' );
$this->separators = new Separators( $this->env, $this );
}
/**
* @note Porting note: this replaces direct access
* @return Env
*/
public function getEnv(): Env {
return $this->env;
}
/**
* Initialize a few boolean flags based on serialization mode.
* FIXME: Ideally, this should be private. Requires shuffing around
* where SerializerState is constructed so that $selserMode is known
* at the time of construction.
* @private for use by WikitextSerializer only
* @param bool $selserMode Are we running selective serialization?
*/
public function initMode( bool $selserMode ): void {
$this->useWhitespaceHeuristics =
Semver::satisfies( $this->env->getInputContentVersion(), '>=1.7.0' );
$this->selserMode = $selserMode;
}
/**
* Appends the separator source to the separator src buffer.
* Don't update $state->onSOL since this string hasn't been emitted yet.
* If content handlers change behavior based on whether this newline will
* be emitted or not, they should peek into this buffer (ex: see TDHandler
* and THHandler code).
*
* @param string $src
*/
public function appendSep( string $src ): void {
$this->sep->src = ( $this->sep->src ?? '' ) . $src;
}
/**
* Cycle the state after processing a node.
* @param Node $node
*/
public function updateSep( Node $node ): void {
$this->sep->lastSourceNode = $node;
}
private function resetSep() {
$this->sep = (object)[
'constraints' => null,
'src' => null,
'lastSourceNode' => null,
];
}
/**
* Reset the current line state.
* @param ?Node $node
*/
private function resetCurrLine( ?Node $node ): void {
$this->currLine = (object)[
'text' => '',
'chunks' => [],
'firstNode' => $node
];
}
/**
* Process and emit a line of ConstrainedText chunks, adjusting chunk boundaries as necessary.
* (Start of line and end of line are always safe for ConstrainedText chunks, so we don't need
* to buffer more than the last line.)
*/
private function flushLine(): void {
$this->out .= ConstrainedText::escapeLine( $this->currLine->chunks );
$this->currLine->chunks = [];
}
/**
* Extracts a subset of the page source bound by the supplied source range.
* @param SourceRange $sr
* @return string|null
*/
public function getOrigSrc( SourceRange $sr ): ?string {
Assert::invariant( $this->selserMode, 'SerializerState::$selserMode must be set' );
if (
$sr->start <= $sr->end &&
// FIXME: Having a $start greater than the source length is
// probably a canary for corruption. Maybe we should be throwing
// here instead. See T240053.
// But, see comment in UnpackDOMFragments where we very very rarely
// can deliberately set DSR to point outside page source.
$sr->start <= strlen( $this->selserData->revText )
) {
// XXX should use $frame->getSrcText() like WTUtils::getWTSource
return $sr->substr( $this->selserData->revText );
} else {
return null;
}
}
/**
* Check the validity of a DSR in the context of the page source.
*
* Returns false if Utils::isValidDSR() would return false, but also
* returns false if the DSR offsets would create a bad UTF-8 string
* (ie, the start offsets don't point to a valid UTF-8 start character).
* @param ?DomSourceRange $dsr DSR source range values
* @param bool $all Also check the widths of the container tag
* @return bool
*/
public function isValidDSR( ?DomSourceRange $dsr, bool $all = false ) {
if ( !Utils::isValidDSR( $dsr, $all ) ) {
return false;
}
if ( !( $dsr->start <= $dsr->end &&
$dsr->end <= strlen( $this->selserData->revText ) ) ) {
return false;
}
// check the UTF-8 ranges.
$src = $this->selserData->revText;
$check = static function ( $start, $end ) use ( $src ) {
if ( $start === $end ) {
// zero-length string is always ok
return true;
}
$firstChar = ord( $src[$start] );
if ( ( $firstChar & 0xC0 ) === 0x80 ) {
return false; // bad UTF-8 at start of string
}
$i = 0;
// This next loop won't pass $start because we've already
// asserted that the first character isn't 10xx xxxx
do {
$i--;
if ( $i <= -5 ) {
return false; // bad UTF-8 at end of string (>4 byte sequence)
}
$lastChar = ord( $src[$end + $i] );
} while ( ( $lastChar & 0xC0 ) === 0x80 );
if ( ( $lastChar & 0x80 ) === 0 ) {
return $i === -1;
} elseif ( ( $lastChar & 0xE0 ) === 0xC0 ) {
return $i === -2;
} elseif ( ( $lastChar & 0xF0 ) === 0xE0 ) {
return $i === -3;
} elseif ( ( $lastChar & 0xF8 ) === 0xF0 ) {
return $i === -4;
} else {
return false;
}
};
if ( !$all ) {
return $check( $dsr->start, $dsr->end );
}
// Check each inner ranges.
$openEnd = $dsr->start + $dsr->openWidth;
if ( $openEnd > $dsr->end ) {
return false;
}
if ( !$check( $dsr->start, $openEnd ) ) {
return false;
}
$closeStart = $dsr->end - $dsr->closeWidth;
if ( $dsr->start > $closeStart ) {
return false;
}
if ( !$check( $closeStart, $dsr->end ) ) {
return false;
}
if ( $openEnd > $closeStart ) {
return false;
}
if ( !$check( $openEnd, $closeStart ) ) {
return false;
}
return true;
}
/**
* Like it says on the tin.
* @param Node $node
*/
public function updateModificationFlags( Node $node ): void {
$this->prevNodeUnmodified = $this->currNodeUnmodified;
$this->currNodeUnmodified = false;
$this->prevNode = $node;
}
/**
* Separators put us in SOL state.
* @param string $sep
* @param Node $node
*/
private function sepIntroducedSOL( string $sep, Node $node ): void {
// Don't get tripped by newlines in comments! Be wary of nowikis added
// by makeSepIndentPreSafe on the last line.
$nonCommentSep = preg_replace( Utils::COMMENT_REGEXP, '', $sep );
if ( substr( $nonCommentSep, -1 ) === "\n" ) {
$this->onSOL = true;
}
if ( str_contains( $nonCommentSep, "\n" ) ) {
// process escapes in our full line
$this->flushLine();
$this->resetCurrLine( $node );
}
}
/**
* Accumulates chunks on the current line.
* @param ConstrainedText $chunk
* @param string $logPrefix
*/
private function pushToCurrLine( ConstrainedText $chunk, string $logPrefix ) {
// Emitting text that has not been escaped
$this->currLine->text .= $chunk->text;
$this->currLine->chunks[] = $chunk;
$this->serializer->trace( '--->', $logPrefix, static function () use ( $chunk ) {
return PHPUtils::jsonEncode( $chunk->text );
} );
}
/**
* Pushes the separator to the current line and resets the separator state.
* @param string $sep
* @param Node $node
* @param string $debugPrefix
*/
private function emitSep( string $sep, Node $node, string $debugPrefix ): void {
$sep = ConstrainedText::cast( $sep, $node );
// Replace newlines if we're in a single-line context
if ( $this->singleLineContext->enforced() ) {
$sep->text = preg_replace( '/\n/', ' ', $sep->text );
}
$this->pushToCurrLine( $sep, $debugPrefix );
$this->sepIntroducedSOL( $sep->text, $node );
// Reset separator state
$this->resetSep();
$this->updateSep( $node );
}
/**
* Determines if we can use the original separator for this node or if we
* need to build one based on its constraints, and then emits it.
*
* @param Node $node
*/
private function emitSepForNode( Node $node ): void {
/* When block nodes are deleted, the deletion affects whether unmodified
* newline separators between a pair of unmodified P tags can be reused.
*
* Example:
* ```
* Original WT : "<div>x</div>foo\nbar"
* Original HTML: "<div>x</div><p>foo</p>\n<p>bar</p>"
* Edited HTML : "<p>foo</p>\n<p>bar</p>"
* Annotated DOM: "<mw:DiffMarker is-block><p>foo</p>\n<p>bar</p>"
* Expected WT : "foo\n\nbar"
* ```
*
* Note the additional newline between "foo" and "bar" even though originally,
* there was just a single newline.
*
* So, even though the two P tags and the separator between them is
* unmodified, it is insufficient to rely on just that. We have to look at
* what has happened on the two wikitext lines onto which the two P tags
* will get serialized.
*
* Now, if you check the code for `nextToDeletedBlockNodeInWT`, that code is
* not really looking at ALL the nodes before/after the nodes that could
* serialize onto the wikitext lines. It is looking at the immediately
* adjacent nodes, i.e. it is not necessary to look if a block-tag was
* deleted 2 or 5 siblings away. If we had to actually examine all of those,
* nodes, this would get very complex, and it would be much simpler to just
* discard the original separators => potentially lots of dirty diffs.
*
* To understand why it is sufficient (for correctness) to examine just
* the immediately adjacent nodes, let us look at an additional example.
* ```
* Original WT : "a<div>b</div>c<div>d</div>e\nf"
* Original HTML: "<p>a</p><div>b</div><p>c</p><div>d</div><p>e</p>\n<p>f</p>"
* ```
* Note how `<block>` tags and `<p>` tags interleave in the HTML. This would be
* the case always no matter how much inline content showed up between the
* block tags in wikitext. If the b-`<div>` was deleted, we don't care
* about it, since we still have the d-`<div>` before the P tag that preserves
* the correctness of the single `"\n"` separator. If the d-`<div>` was deleted,
* we conservatively ignore the original separator and let normal P-P constraints
* take care of it. At worst, we might generate a dirty diff in this scenario. */
$origSepNeeded = ( $node !== $this->sep->lastSourceNode );
$origSepUsable = $origSepNeeded &&
(
// first-content-node of <body> ($this->prevNode)
(
DOMUtils::isBody( $this->prevNode ) &&
$node->parentNode === $this->prevNode
)
||
// unmodified sibling node of $this->prevNode
(
$this->prevNode && $this->prevNodeUnmodified &&
$node->parentNode === $this->prevNode->parentNode &&
!WTSUtils::nextToDeletedBlockNodeInWT( $this->prevNode, true )
)
) &&
$this->currNodeUnmodified && !WTSUtils::nextToDeletedBlockNodeInWT( $node, false );
$origSep = null;
if ( $origSepUsable ) {
if ( $this->prevNode instanceof Element && $node instanceof Element ) {
'@phan-var Element $node';/** @var Element $node */
if ( DOMUtils::isBody( $this->prevNode ) ) {
// <body> won't have DSR in body_only scenarios
$sr = new SourceRange( 0, 0 );
} else {
$sr = DOMDataUtils::getDataParsoid( $this->prevNode )->dsr;
}
$sr = $sr->to( DOMDataUtils::getDataParsoid( $node )->dsr );
$origSep = $this->getOrigSrc( $sr );
} elseif ( $this->sep->src && WTSUtils::isValidSep( $this->sep->src ) ) {
// We don't know where '$this->sep->src' comes from. So, reuse it
// only if it is a valid separator string.
$origSep = $this->sep->src;
}
}
if ( $origSep !== null ) {
$this->emitSep( $origSep, $node, 'ORIG-SEP:' );
} else {
$sep = $this->separators->buildSep( $node );
$this->emitSep( $sep ?? '', $node, 'SEP:' );
}
}
/**
* Recovers and emits any trimmed whitespace for $node
* @param Node $node
* @param bool $leading
* if true, trimmed leading whitespace is emitted
* if false, trimmed railing whitespace is emitted
* @return string|null
*/
public function recoverTrimmedWhitespace( Node $node, bool $leading ): ?string {
$sep = $this->separators->recoverTrimmedWhitespace( $node, $leading );
$this->serializer->trace( '--->', "TRIMMED-SEP:", static function () use ( $sep ) {
return PHPUtils::jsonEncode( $sep );
} );
return $sep;
}
/**
* Pushes the chunk to the current line.
* @param ConstrainedText|string $res
* @param Node $node
*/
public function emitChunk( $res, Node $node ): void {
$res = ConstrainedText::cast( $res, $node );
// Replace newlines if we're in a single-line context
if ( $this->singleLineContext->enforced() ) {
$res->text = str_replace( "\n", ' ', $res->text );
}
// Emit separator first
if ( $res->noSep ) {
/* skip separators for internal tokens from SelSer */
if ( $this->onSOL ) {
// process escapes in our full line
$this->flushLine();
$this->resetCurrLine( $node );
}
} else {
$this->emitSepForNode( $node );
}
$needsEscaping = $this->needsEscaping;
if ( $needsEscaping && $this->currNode instanceof Text ) {
$needsEscaping = !$this->inHTMLPre && ( $this->onSOL || !$this->currNodeUnmodified );
}
// Escape 'res' if necessary
if ( $needsEscaping ) {
$res = new ConstrainedText( [
'text' => $this->serializer->escapeWikitext( $this, $res->text, [
'node' => $node,
'isLastChild' => DiffDOMUtils::nextNonDeletedSibling( $node ) === null,
] ),
'prefix' => $res->prefix,
'suffix' => $res->suffix,
'node' => $res->node,
] );
$this->needsEscaping = false;
} else {
// If 'res' is coming from selser and the current node is a paragraph tag,
// check if 'res' might need some leading chars nowiki-escaped before being output.
// Because of block-tag p-wrapping behavior, sol-sensitive characters that used to
// be in non-sol positions, but yet wrapped in p-tags, could end up in sol-position
// if those block tags get deleted during edits.
//
// Ex: a<div>foo</div>*b
// -- wt2html --> <p>a</p><div>foo<div><p>*b</p>
// -- EDIT --> <p>a</p><p>*b</p>
// -- html2wt --> a\n\n<nowiki>*</nowiki>b
//
// In this scenario, the <p>a</p>, <p>*b</p>, and <p>#c</p>
// will be marked unmodified and will be processed below.
if ( $this->selserMode
&& $this->onSOL
&& $this->currNodeUnmodified
// 'node' came from original Parsoid HTML unmodified. So, if its content
// needs nowiki-escaping, we know that the reason it didn't parse into
// lists/headings/whatever is because it didn't occur at the start of the
// line => it had a block-tag in the original wikitext. So if the previous
// node was also unmodified (and since it also came from original Parsoid
// HTML), we can safely infer that it couldn't have been an inline node or
// a P-tag (if it were, the p-wrapping code would have swallowed that content
// into 'node'). So, it would have to be some sort of block tag => this.onSOL
// couldn't have been true (because we could have serialized 'node' on the
// same line as the block tag) => we can save some effort by eliminating
// scenarios where 'this.prevNodeUnmodified' is true.
&& !$this->prevNodeUnmodified
&& DOMCompat::nodeName( $node ) === 'p' && !WTUtils::isLiteralHTMLNode( $node )
) {
$pChild = DiffDOMUtils::firstNonSepChild( $node );
// If a text node, we have to make sure that the text doesn't
// get reparsed as non-text in the wt2html pipeline.
if ( $pChild instanceof Text ) {
$match = $res->matches( $this->solWikitextRegexp(), $this->env );
if ( $match && isset( $match[2] ) ) {
if ( preg_match( '/^([\*#:;]|{\||.*=$)/D', $match[2] )
// ! and | chars are harmless outside tables
|| ( strspn( $match[2], '|!' ) && $this->wikiTableNesting > 0 )
// indent-pres are suppressed inside <blockquote>
|| ( preg_match( '/^ \S/', $match[2] )
&& !DOMUtils::hasNameOrHasAncestorOfName( $node, 'blockquote' ) )
) {
$res = ConstrainedText::cast( ( $match[1] ?: '' )
. '<nowiki>' . substr( $match[2], 0, 1 ) . '</nowiki>'
. substr( $match[2], 1 ), $node );
}
}
}
}
}
// Output res
$this->pushToCurrLine( $res, $this->logPrefix );
// Update sol flag. Test for newlines followed by optional includeonly or comments
if ( !$res->matches( $this->solRegexp(), $this->env ) ) {
$this->onSOL = false;
}
// We've emit something so we're no longer at SOO.
$this->atStartOfOutput = false;
}
/**
* Serialize the children of a DOM node, sharing the global serializer state.
* Typically called by a DOM-based handler to continue handling its children.
* @param Element|DocumentFragment $node
* @param ?callable $wtEscaper ( $state, $text, $opts )
* PORT-FIXME document better; should this be done via WikitextEscapeHandlers somehow?
* @param ?Node $firstChild
*/
public function serializeChildren(
Node $node, ?callable $wtEscaper = null, ?Node $firstChild = null
): void {
// SSS FIXME: Unsure if this is the right thing always
if ( $wtEscaper ) {
$this->wteHandlerStack[] = $wtEscaper;
}
$child = $firstChild ?: $node->firstChild;
while ( $child !== null ) {
// We always get the next child to process
$child = $this->serializer->serializeNode( $child );
}
if ( $wtEscaper ) {
array_pop( $this->wteHandlerStack );
}
// If we serialized children explicitly,
// we were obviously processing a modified node.
$this->currNodeUnmodified = false;
}
/**
* Abstracts some steps taken in `serializeChildrenToString` and `serializeDOM`
*
* @param Element|DocumentFragment $node
* @param ?callable $wtEscaper See {@link serializeChildren()}
* @internal For use by WikitextSerializer only
*/
public function kickOffSerialize(
Node $node, ?callable $wtEscaper = null
): void {
$this->updateSep( $node );
$this->currNodeUnmodified = false;
$this->updateModificationFlags( $node );
$this->resetCurrLine( $node->firstChild );
$this->serializeChildren( $node, $wtEscaper );
// Emit child-parent seps.
$this->emitSepForNode( $node );
// We've reached EOF, flush the remaining buffered text.
$this->flushLine();
}
/**
* Serialize children to a string
*
* FIXME(arlorla): Shouldn't affect the separator state, but accidents have
* have been known to happen. T109793 suggests using its own wts / state.
*
* @param Element|DocumentFragment $node
* @param ?callable $wtEscaper See {@link serializeChildren()}
* @param string $inState
* @return string
*/
private function serializeChildrenToString(
Node $node, ?callable $wtEscaper, string $inState
): string {
$states = [ 'inLink', 'inCaption', 'inIndentPre', 'inHTMLPre', 'inPHPBlock', 'inAttribute' ];
Assert::parameter( in_array( $inState, $states, true ), '$inState', 'Must be one of: '
. implode( ', ', $states ) );
// FIXME: Make sure that the separators emitted here conform to the
// syntactic constraints of syntactic context.
$oldSep = $this->sep;
$oldSOL = $this->onSOL;
$oldOut = $this->out;
$oldStart = $this->atStartOfOutput;
$oldCurrLine = $this->currLine;
$oldLogPrefix = $this->logPrefix;
// Modification flags
$oldPrevNodeUnmodified = $this->prevNodeUnmodified;
$oldCurrNodeUnmodified = $this->currNodeUnmodified;
$oldPrevNode = $this->prevNode;
$this->out = '';
$this->logPrefix = 'OUT(C):';
$this->resetSep();
$this->onSOL = false;
$this->atStartOfOutput = false;
$this->$inState = true;
$this->singleLineContext->disable();
$this->kickOffSerialize( $node, $wtEscaper );
$this->singleLineContext->pop();
// restore the state
$bits = $this->out;
$this->out = $oldOut;
$this->$inState = false;
$this->sep = $oldSep;
$this->onSOL = $oldSOL;
$this->atStartOfOutput = $oldStart;
$this->currLine = $oldCurrLine;
$this->logPrefix = $oldLogPrefix;
// Modification flags
$this->prevNodeUnmodified = $oldPrevNodeUnmodified;
$this->currNodeUnmodified = $oldCurrNodeUnmodified;
$this->prevNode = $oldPrevNode;
return $bits;
}
/**
* Serialize children of a link to a string
* @param Element|DocumentFragment $node
* @param ?callable $wtEscaper See {@link serializeChildren()}
* @return string
*/
public function serializeLinkChildrenToString(
Node $node, ?callable $wtEscaper = null
): string {
return $this->serializeChildrenToString( $node, $wtEscaper, 'inLink' );
}
/**
* Serialize children of a caption to a string
* @param Element|DocumentFragment $node
* @param ?callable $wtEscaper See {@link serializeChildren()}
* @return string
*/
public function serializeCaptionChildrenToString(
Node $node, ?callable $wtEscaper = null
): string {
return $this->serializeChildrenToString( $node, $wtEscaper, 'inCaption' );
}
/**
* Serialize children of an indent-pre to a string
* @param Element|DocumentFragment $node
* @param ?callable $wtEscaper See {@link serializeChildren()}
* @return string
*/
public function serializeIndentPreChildrenToString(
Node $node, ?callable $wtEscaper = null
): string {
return $this->serializeChildrenToString( $node, $wtEscaper, 'inIndentPre' );
}
/**
* Take notes of the open annotation ranges and whether they have been extended.
* @param string $ann
* @param bool $extended
*/
public function openAnnotationRange( string $ann, bool $extended ) {
$this->openAnnotations[$ann] = $extended;
}
/**
* Removes the corresponding annotation range from the list of open ranges.
* @param string $ann
*/
public function closeAnnotationRange( string $ann ) {
unset( $this->openAnnotations[$ann] );
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 19 2026, 16:32 (4 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d4/26/7a4c6f724ba7ed1c8fe6b3ce4e6e
Default Alt Text
SerializerState.php (28 KB)

Event Timeline