Page MenuHomeWickedGov Phorge

Grammar.pegphp
No OneTemporary

Size
80 KB
Referenced Files
None
Subscribers
None

Grammar.pegphp

/**
* Combined Wiki (MediaWiki) and HTML tokenizer based on pegjs. Emits several
* chunks of tokens (one chunk per top-level block matched) and eventually an
* end event. Tokens map to HTML tags as far as possible, with custom tokens
* used where further processing on the token stream is needed.
*/
{
use Wikimedia\Assert\UnreachableException;
use Wikimedia\JsonCodec\JsonCodec;
use Wikimedia\Parsoid\Config\Env;
use Wikimedia\Parsoid\Config\SiteConfig;
use Wikimedia\Parsoid\Core\DomSourceRange;
use Wikimedia\Parsoid\NodeData\DataMw;
use Wikimedia\Parsoid\NodeData\DataParsoid;
use Wikimedia\Parsoid\NodeData\TempData;
use Wikimedia\Parsoid\Tokens\CommentTk;
use Wikimedia\Parsoid\Tokens\EOFTk;
use Wikimedia\Parsoid\Tokens\EndTagTk;
use Wikimedia\Parsoid\Tokens\KV;
use Wikimedia\Parsoid\Tokens\KVSourceRange;
use Wikimedia\Parsoid\Tokens\NlTk;
use Wikimedia\Parsoid\Tokens\SelfclosingTagTk;
use Wikimedia\Parsoid\Tokens\SourceRange;
use Wikimedia\Parsoid\Tokens\TagTk;
use Wikimedia\Parsoid\Tokens\Token;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Utils\TokenUtils;
use Wikimedia\Parsoid\Utils\Utils;
use Wikimedia\Parsoid\Utils\WTUtils;
use Wikimedia\Parsoid\Wikitext\Consts;
}
{
/** @var Env */
private $env;
/** @var SiteConfig */
private $siteConfig;
/** @var array */
private $pipelineOpts;
/** @var int */
private $pipelineOffset;
private $extTags;
private $startTime;
/** @var string */
private $reUrltextLookahead;
/** @var string */
private $urltextPlainSegment = '';
/** @var bool */
private $urltextFoundAutolink = false;
protected function initialize() {
$this->env = $this->options['env'];
$this->siteConfig = $this->env->getSiteConfig();
$tokenizer = $this->options['pegTokenizer'];
$this->pipelineOpts = $tokenizer->getOptions();
// FIXME: inTemplate option may not always be set in
// standalone tokenizers user by some pipelines handlers.
$this->pipelineOffset = $this->options['pipelineOffset'] ?? 0;
$this->extTags = $this->siteConfig->getExtensionTagNameMap();
// Non-greedy text_char sequence: stop at ampersand, double-underscore,
// magic link prefix or protocol
$this->reUrltextLookahead = '!(?:' .
'([^-\'<[{\n\r:;\]}|\!=&]*?)' .
'(?:__|$|[-\'<[{\n\r:;\]}|\!=&]|(RFC|PMID|ISBN|' .
'(?i)' . $this->siteConfig->getProtocolsRegex( true ) .
')))!A';
}
private $prevOffset = 0;
private $headingIndex = 0;
public function resetState() {
$this->prevOffset = 0;
$this->headingIndex = 0;
}
private function assert( $condition, $text ) {
if ( !$condition ) {
throw new \RuntimeException( "Grammar.pegphp assertion failure: $text" );
}
}
private function unreachable() {
throw new UnreachableException( "Grammar.pegphp: this should be unreachable" );
}
// Some shorthands for legibility
private function startOffset() {
return $this->savedPos;
}
private function endOffset() {
return $this->currPos;
}
private function tsrOffsets( $flag = 'default' ): SourceRange {
switch ( $flag ) {
case 'start':
return new SourceRange( $this->savedPos, $this->savedPos );
case 'end':
return new SourceRange( $this->currPos, $this->currPos );
default:
return new SourceRange( $this->savedPos, $this->currPos );
}
}
/*
* Emit a chunk of tokens to our consumers. Once this has been done, the
* current expression can return an empty list (true).
*/
private function emitChunk( $tokens ) {
// FIXME: We don't expect nulls here, but looks like
// hack from I1c695ab6cdd3655e98877c175ddbabdee9dc44b7
// introduces them. Work around it for now!
if ( !$tokens ) {
return [];
}
// Shift tsr of all tokens by the pipeline offset
TokenUtils::shiftTokenTSR( $tokens, $this->pipelineOffset );
$this->env->log( 'trace/peg', $this->options['pipelineId'] ?? '0', '----> ', $tokens );
$i = null;
$n = count( $tokens );
// Enforce parsing resource limits
for ( $i = 0; $i < $n; $i++ ) {
TokenizerUtils::enforceParserResourceLimits( $this->env, $tokens[ $i ] );
}
return $tokens;
}
/* ------------------------------------------------------------------------
* Extension tags should be parsed with higher priority than anything else.
*
* The trick we use is to strip out the content inside a matching tag-pair
* and not tokenize it. The content, if it needs to parsed (for example,
* for <ref>, <*include*> tags), is parsed in a fresh tokenizer context
* which means any error correction that needs to happen is restricted to
* the scope of the extension content and doesn't spill over to the higher
* level. Ex: <math><!--foo</math>.
*
* IGNORE: {{ this just balances the blocks in this comment for pegjs
*
* This trick also lets us prevent extension content (that don't accept WT)
* from being parsed as wikitext (Ex: <math>\frac{foo\frac{bar}}</math>)
* We don't want the "}}" being treated as a template closing tag and
* closing outer templates.
* --------------------------------------------------------------------- */
private function isXMLTag( string $name ): bool {
$lName = mb_strtolower( $name );
return isset( Consts::$HTML['HTML5Tags'][$lName] ) ||
isset( Consts::$HTML['OlderHTMLTags'][$lName] );
}
private function maybeAnnotationOrExtensionTag( Token $t, ?bool $end, array $attribs, SourceRange $tsr ) {
$tagName = mb_strtolower( $t->getName() );
$isAnnotationTag = $this->siteConfig->isAnnotationTag( $tagName );
if ( !$isAnnotationTag ) {
$pipepos = strpos( $tagName, '|' );
if ( $pipepos ) {
$strBeforePipe = substr( $tagName, 0, $pipepos );
$isAnnotationTag = $this->siteConfig->isAnnotationTag( $strBeforePipe );
if ( $isAnnotationTag ) {
$attribs = [ new KV( "name", substr( $tagName, $pipepos + 1, strlen( $tagName ) - $pipepos - 1 ) ) ];
$tagName = $strBeforePipe;
}
}
}
if ( $isAnnotationTag ) {
$metaAttrs = [ new KV( 'typeof', 'mw:Annotation/' . $tagName . ($end ? '/End' : '') ) ];
$datamw = null;
if ( count( $attribs ) > 0 ) {
$attrMap = [];
foreach ( $attribs as $attr ) {
// If the key or the value is not a string, we replace it by the thing that generated it and
// consider that wikitext as a raw string instead.
$k = is_string( $attr->k ) ? $attr->k : $attr->ksrc;
$v = is_string( $attr->v ) ? $attr->v : $attr->vsrc;
$attrMap[$k] = $v;
}
// Possible follow-up in T295168 for attribute sanitation
// T367616: 'attrs' should be renamed to 'extAttrs'
$datamw = new DataMw( [ 'attrs' => (object)$attrMap ] );
}
$dp = new DataParsoid();
$dp->tsr = $tsr;
$this->env->hasAnnotations = true;
// FIXME: Suppress annotation meta tokens from template pipelines
// since they may not have TSR values and won't get recognized as
// annotation ranges. Without TSR, they might end up stuck in
// fosterable positions and cause havoc on edits by breaking selser.
if ( empty( $this->pipelineOpts['inTemplate'] ) ) {
return [ new SelfclosingTagTk ( 'meta', $metaAttrs, $dp, $datamw ) ];
} else {
return [];
}
}
$isInstalledExt = isset( $this->extTags[$tagName] );
$isIncludeTag = WTUtils::isIncludeTag( $tagName );
// Extensions have higher precedence when they shadow html tags.
if ( !( $isInstalledExt || $isIncludeTag ) ) {
return $t;
}
$dp = $t->dataParsoid;
$skipPos = $this->currPos;
switch ( get_class( $t ) ) {
case EndTagTk::class:
if ( $isIncludeTag ) {
return $t;
}
// Similar to TagTk, we rely on the sanitizer to convert to text
// where necessary and emit tokens to ease the wikitext escaping
// code. However, extension tags that shadow html tags will see
// their unmatched end tags dropped while tree building, since
// the sanitizer will let them through.
return $t; // not text()
case SelfclosingTagTk::class:
$dp->src = $dp->tsr->substr( $this->input );
$dp->extTagOffsets = new DomSourceRange(
$dp->tsr->start, $dp->tsr->end,
$dp->tsr->length(), 0
);
if ( $isIncludeTag ) {
return $t;
}
break;
case TagTk::class:
$endTagRE = '~.*?(</' . preg_quote( $tagName, '~' ) . '\s*>)~iusA';
$tagContentFound = preg_match( $endTagRE, $this->input, $tagContent, 0, $dp->tsr->start );
if ( !$tagContentFound ) {
$dp->src = $dp->tsr->substr( $this->input );
$dp->extTagOffsets = new DomSourceRange(
$dp->tsr->start, $dp->tsr->end,
$dp->tsr->length(), 0
);
if ( $isIncludeTag ) {
return $t;
} else {
// This is undefined behaviour. The old parser currently
// returns text here (see core commit 674e8388cba),
// whereas this results in unclosed
// extension tags that shadow html tags falling back to
// their html equivalent. The sanitizer will take care
// of converting to text where necessary. We do this to
// simplify `hasWikitextTokens` when escaping wikitext,
// which wants these as tokens because it's otherwise
// lacking in context.
return $t; // not text()
}
}
$extSrc = $tagContent[0];
$extEndOffset = $dp->tsr->start + strlen( $extSrc );
$extEndTagWidth = strlen( $tagContent[1] );
if ( !empty( $this->pipelineOpts['inTemplate'] ) ) {
// Support nesting in extensions tags while tokenizing in templates
// to support the #tag parser function.
//
// It's necessary to permit this broadly in templates because
// there's no way to distinguish whether the nesting happened
// while expanding the #tag parser function, or just a general
// syntax errors. In other words,
//
// hi<ref>ho<ref>hi</ref>ho</ref>
//
// and
//
// hi{{#tag:ref|ho<ref>hi</ref>ho}}
//
// found in template are returned indistinguishably after a
// preprocessing request, though the old parser renders them
// differently. #tag in template is probably a common enough
// use case that we want to accept these false positives,
// though another approach could be to drop this code here, and
// invoke a native #tag handler and forgo those in templates.
//
// Expand `extSrc` as long as there is a <tagName> found in the
// extension source body.
$startTagRE = '~<' . preg_quote( $tagName, '~' ) . '(?:[^/>]|/(?!>))*>~i';
$s = substr( $extSrc, $dp->tsr->end - $dp->tsr->start );
$openTags = 0;
while ( true ) {
if ( preg_match_all( $startTagRE, $s, $matches ) ) {
$openTags += count( $matches[0] );
}
if ( !$openTags ) {
break;
}
if ( !preg_match( $endTagRE, $this->input, $tagContent, 0, $extEndOffset ) ) {
break;
}
$openTags -= 1;
$s = $tagContent[0];
$extEndOffset += strlen( $s );
$extEndTagWidth = strlen( $tagContent[1] );
$extSrc .= $s;
}
}
// Extension content source
$dp->src = $extSrc;
$dp->extTagOffsets = new DomSourceRange(
$dp->tsr->start, $extEndOffset,
$dp->tsr->length(), $extEndTagWidth
);
$skipPos = $dp->extTagOffsets->innerEnd();
// If the xml-tag is a known installed (not native) extension,
// skip the end-tag as well.
if ( $isInstalledExt ) {
$skipPos = $dp->extTagOffsets->end;
}
break;
default:
$this->unreachable();
}
$this->currPos = $skipPos;
if ( $isInstalledExt ) {
// update tsr->end to span the start and end tags.
$dp->tsr->end = $this->endOffset(); // was just modified above
return new SelfclosingTagTk( 'extension', [
new KV( 'typeof', 'mw:Extension' ),
new KV( 'name', $tagName ),
new KV( 'about', $this->env->newAboutId() ),
new KV( 'source', $dp->src ),
new KV( 'options', $t->attribs )
], $dp
);
} elseif ( $isIncludeTag ) {
// Parse ext-content, strip eof, and shift tsr
$extContent = $dp->extTagOffsets->stripTags( $dp->src );
$tokenizer = new PegTokenizer( $this->env );
$tokenizer->setSourceOffsets( new SourceRange( $dp->extTagOffsets->innerStart(), $dp->extTagOffsets->innerEnd() ) );
$extContentToks = $tokenizer->tokenizeSync( $extContent, [ 'sol' => true ] );
if ( $dp->extTagOffsets->closeWidth > 0 ) {
TokenUtils::stripEOFTkFromTokens( $extContentToks );
}
array_unshift( $extContentToks, $t );
return $extContentToks;
} else {
$this->unreachable();
}
}
}
/* ********************************************************
* "start" is the top-level start rule
*
* Other allowed start rules:
* - table_start_tag
* - url
* - row_syntax_table_args
* - table_attributes
* - generic_newline_attributes
* - tplarg_or_template_or_bust
* - extlink
* - list_item
*
* These start rules are listed in lib/wt2html/tokenizer.js
* and is used during PEG grammar compilation.
* ******************************************************** */
start "start" =
t:tlb* n:newlineToken* {
if ( count( $t ) ) {
$ret = TokenizerUtils::flattenIfArray( $t );
} else {
$ret = [];
}
if ( count( $n ) ) {
PHPUtils::pushArray($ret, $n);
}
$ret[] = new EOFTk();
return $ret;
}
generic_newline_attributes "generic_newline_attributes" = generic_newline_attribute*
table_attributes "table_attributes" =
(table_attribute / optionalSpaceToken b:broken_table_attribute_name_char { return $b; })*
/*
* This rule exists to support tokenizing the document in chunks.
* The parser's streaming interface will stop tokenization after each iteration
* of the starred subexpression, and yield to the node.js event-loop to
* schedule other pending event handlers.
*/
start_async =
(
(
& {
$this->startTime = null;
if ( $this->env->profiling() ) {
$profile = $this->env->getCurrentProfile();
$this->startTime = microtime( true );
}
return true;
}
t:tlb
& {
if ( $this->env->profiling() ) {
$profile = $this->env->getCurrentProfile();
$profile->bumpTimeUse(
'PEG', 1000 * ( microtime( true ) - $this->startTime ), 'PEG' );
}
return true;
}
) { return $t; }
/ newlineToken* &{
// "tlb" matches "block" matches "sol" matches "newlineToken"
// But, "tlb" is prefixed with a !eof clause, so, we should only
// get here on eof. So, safe to unconditionally terminate the
// generator loop here.
return false;
}
)*
/*
* A document (start rule) is a sequence of toplevelblocks. Tokens are
* emitted in chunks per toplevelblock to avoid buffering the full document.
*/
tlb "tlb" =
!eof b:block {
// Clear the tokenizer's backtracking cache after matching each
// toplevelblock. There won't be any backtracking as a document is just a
// sequence of toplevelblocks, so the cache for previous toplevelblocks
// will never be needed.
$end = $this->startOffset();
for ( ; $this->prevOffset < $end; $this->prevOffset++ ) {
unset( $this->cache[$this->prevOffset] );
}
$tokens = null;
if ( is_array( $b ) && count( $b ) ) {
$tokens = TokenizerUtils::flattenIfArray( $b );
} elseif ( is_string( $b ) ) {
$tokens = [ $b ];
}
// Emit tokens for this toplevelblock. This feeds a chunk to the parser pipeline.
return $this->emitChunk( $tokens );
}
/*
* Redirects can only occur as the first thing in a document. See
* WikitextContent::getRedirectTarget()
*/
redirect =
rw:redirect_word
sp:$space_or_newline*
c:$(":" space_or_newline*)?
wl:wikilink
& {
return count( $wl ) === 1 && $wl[0] instanceof Token;
}
{
$link = $wl[0];
if ( $sp ) {
$rw .= $sp;
}
if ( $c ) {
$rw .= $c;
}
// Build a redirect token
$dp = new DataParsoid;
$dp->src = $rw;
$dp->tsr = $this->tsrOffsets();
$dp->linkTk = $link;
$redirect = new SelfclosingTagTk( 'mw:redirect',
// Put 'href' into attributes so it gets template-expanded
[ $link->getAttributeKV( 'href' ) ],
$dp
);
return $redirect;
}
/* The 'redirect' magic word.
* The leading whitespace allowed is due to the PHP trim() function.
*/
redirect_word =
$(
[ \t\n\r\0\x0b]*
rw:$(!space_or_newline ![:\[] .)+
& {
return preg_match( $this->env->getSiteConfig()->getMagicWordMatcher( 'redirect' ), $rw );
}
)
/*
* The actual contents of each block.
*/
block =
// Redirect has to be the first alternative; otherwise it gets parsed as a <ol>
// In practice, WikiContent::getRedirectTargetAndText() in MediaWiki strips
// the redirect from the source so it never sees it.
&sof r:redirect cil:sol_transparent* bl:block_line? {
return array_merge( [ $r ], $cil, $bl ?: [] );
}
/ block_lines
// Inlineline includes generic tags; wrapped into paragraphs in token
// transform and DOM postprocessor
/ inlineline
/ s:sol !sof !inline_breaks { return $s; }
/*
* A block nested in other constructs. Avoid eating end delimiters for other
* constructs by checking against inline_breaks first.
*/
nested_block = !inline_breaks b:block { return $b; }
/*
* The same, but suitable for use inside a table construct.
* Doesn't match table_heading_tag, table_row_tag, table_data_tag,
* table_caption tag, or table_end_tag, although it does allow
* table_start_tag (for nested tables).
*/
nested_block_in_table =
// XXX: don't rely on a lame look-ahead like this; use syntax stops
// instead, so that multi-line th content followed by a line prefixed with
// a comment is also handled. Alternatively, implement a sol look-behind
// assertion accepting spaces and comments.
!(sol (space* sol)? space* (pipe / "!"))
// avoid recursion via nested_block_in_table, as that can lead to stack
// overflow in large tables
// See https://phabricator.wikimedia.org/T59670
b:nested_block<tableDataBlock> {
return $b;
}
/*
* Line-based block constructs.
*/
block_lines =
s:sol
// eat an empty line before the block
s2:(os:optionalSpaceToken so:sol { return array_merge( $os, $so ); })?
bl:block_line {
return array_merge( $s, $s2 ?: [], $bl );
}
// Horizontal rules
hr =
"----" d:$"-"*
// Check if a newline or content follows
lineContent:( &sol "" { return null; } / "" { return true; } ) {
$dataParsoid = new DataParsoid;
$dataParsoid->tsr = $this->tsrOffsets();
if ( $lineContent !== null ) {
$dataParsoid->lineContent = $lineContent;
}
if ( strlen( $d ) > 0 ) {
$dataParsoid->extra_dashes = strlen( $d );
}
return [new SelfclosingTagTk( 'hr', [], $dataParsoid )];
}
/*
* Block structures with start-of-line wiki syntax
*/
block_line =
heading
/ list_item
/ hr
/ st:optionalSpaceToken &[ <{}|!] tl:table_line {
return array_merge( $st, $tl );
}
br =
s:optionalSpaceToken &newline {
$dp = new DataParsoid;
$dp->tsr = $this->tsrOffsets();
return array_merge( $s, [
new SelfclosingTagTk( 'br', [], $dp )
] );
}
inline_breaks =
& [=|!{}:;\r\n[\]\-]
(
annOrExtTag: <annOrExtTag>
h: <h>
extlink: <extlink>
intemplate: <intemplate>
preproc: <preproc>
equal: <equal>
table: <table>
templateArg: <templateArg>
tableCellArg: <tableCellArg>
semicolon: <semicolon>
arrow: <arrow>
linkdesc: <linkdesc>
colon: <colon>
th: <th>
& {
return TokenizerUtils::inlineBreaks( $this->input, $this->endOffset(), [
'annOrExtTag' => $annOrExtTag,
'h' => $h,
'extlink' => $extlink,
'intemplate' => $intemplate,
'preproc' => $preproc,
'equal' => $equal,
'table' => $table,
'templateArg' => $templateArg,
'tableCellArg' => $tableCellArg,
'semicolon' => $semicolon,
'arrow' => $arrow,
'linkdesc' => $linkdesc,
'colon' => $colon,
'th' => $th
], $this->env );
}
)
inlineline =
c:(
urltext
/ !inline_breaks
r:(inline_element / !newline s:. { return $s; }) { return $r; }
)+ {
return TokenizerUtils::flattenStringlist( $c );
}
inline_xmlish_tag =
"<" tag:(xmlish_tag<annOrExtTag> / xmlish_tag<annOrExtTag=false> / tvar_old_syntax_closing_HACK )
{ return $tag; }
inline_element =
& '<' r:( inline_xmlish_tag / comment ) { return $r; }
/ & '{' r:tplarg_or_template { return $r; }
/ & "-{" r:lang_variant_or_tpl { return $r; }
// FIXME: The old parser's handleInternalLinks2 splits on [[, resulting
// in sequences with odd number of brackets parsing as text, and sequences
// with even number of brackets having its innermost pair parse as a
// wikilink. For now, we faithfully reproduce what's found there but
// wikitext, the language, shouldn't be defined by odd tokenizing behaviour
// in the old parser. Flagging this for a future cleanup.
/ $('[[' &'[')+
/ & '[' r:( wikilink / extlink ) { return $r; }
/ & "'" r:quote { return $r; }
/* Headings */
heading =
& "=" // guard, to make sure '='+ will match.
// XXX: Also check to end to avoid inline parsing?
r:(
s:$'='+ // moved in here to make s accessible to inner action
ce:(
(ill:inlineline<h>? { return $ill ?: []; })
$'='+
)?
& { return $ce || strlen( $s ) > 2; }
endTPos:("" { return $this->endOffset(); })
spc:( space / sol_transparent )*
&eolf
{
$c = null;
$e = null;
$level = null;
if ( $ce ) {
$c = $ce[0];
$e = $ce[1];
$level = min( strlen( $s ), strlen( $e ) );
} else {
// split up equal signs into two equal parts, with at least
// one character in the middle.
$level = (int)floor( ( strlen( $s ) - 1 ) / 2 );
$c = [ str_repeat( '=', strlen( $s ) - 2 * $level ) ];
$s = $e = str_repeat( '=', $level );
}
$level = min( 6, $level );
// convert surplus equals into text
if ( strlen( $s ) > $level ) {
$extras1 = substr( $s, 0, strlen( $s ) - $level );
if ( is_string( $c[0] ) ) {
$c[0] = $extras1 . $c[0];
} else {
array_unshift( $c, $extras1 );
}
}
if ( strlen( $e ) > $level ) {
$extras2 = substr( $e, 0, strlen( $e ) - $level );
$lastElem = PHPUtils::lastItem( $c );
if ( is_string( $lastElem ) ) {
$c[count( $c ) - 1] .= $extras2;
} else {
$c[] = $extras2;
}
}
$tagDP = new DataParsoid;
$tagDP->tsr = $this->tsrOffsets( 'start' );
$tagDP->tsr->end += $level;
// Match the old parser's behavior by
// (a) making headingIndex part of tokenizer state
// (don't reuse pipeline! see $this->resetState above)
// (b) assigning the index when ==*== is tokenized,
// even if we're inside a template argument
// or other context which won't end up putting the heading
// on the output page. T213468/T214538
$this->headingIndex++;
$tagDP->getTemp()->headingIndex = $this->headingIndex;
$res = [ new TagTk( 'h' . $level, [], $tagDP ) ];
PHPUtils::pushArray( $res, $c );
$endTagDP = new DataParsoid;
$endTagDP->tsr = new SourceRange( $endTPos - $level, $endTPos );
$res[] = new EndTagTk( 'h' . $level, [], $endTagDP );
$res[] = $spc;
return $res;
}
) { return $r; }
// Behavior switches. See:
// https://www.mediawiki.org/wiki/Help:Magic_words#Behavior_switches
behavior_switch =
bs:$('__' behavior_text '__') {
if ( $this->siteConfig->isBehaviorSwitch( $bs ) ) {
$dp = new DataParsoid;
$dp->tsr = $this->tsrOffsets();
$dp->src = $bs;
$dp->magicSrc = $bs;
return [
new SelfclosingTagTk( 'behavior-switch', [ new KV( 'word', $bs ) ], $dp )
];
} else {
return [ $bs ];
}
}
// Instead of defining a charset, the old parser's doDoubleUnderscore concats a
// regexp of all the language specific aliases of the behavior switches and
// then does a match and replace. Just be as permissive as possible and let the
// BehaviorSwitchPreprocessor back out of any overreach.
behavior_text = $( !'__' ( text_char / "-" ) )+
/**************************************************************
* External (bracketed and autolinked) links
**************************************************************/
autolink =
! <extlink>
// this must be a word boundary, so previous character must be non-word
! { return Utils::isUniWord(Utils::lastUniChar( $this->input, $this->endOffset() ) ); }
r:(
autourl
/ autoref
/ isbn
) { return $r; }
extlink "extlink" =
! <extlink> // extlink cannot be nested
r:(
"["
p0:( "" { return $this->endOffset(); })
flat:(
addr:(url_protocol ipv6urladdr / "")
target:(extlink_nonipv6url<extlink> / "")
{ return TokenizerUtils::flattenString( [ $addr, $target ] ); }
)
& {
// Protocol must be valid and there ought to be at least one
// post-protocol character. So strip last char off target
// before testing protocol.
if ( is_array( $flat ) ) {
// There are templates present, alas.
return count( $flat ) > 0;
}
return Utils::isProtocolValid( substr( $flat, 0, -1 ), $this->env );
}
p1:( "" { return $this->endOffset(); })
sp:$( space / unispace )*
p2:( "" { return $this->endOffset(); })
content:inlineline<extlink>?
p3:( "" { return $this->endOffset(); })
"]" {
$tsr1 = new SourceRange( $p0, $p1 );
$tsr2 = new SourceRange( $p2, $p3 );
$dp = new DataParsoid;
$dp->tsr = $this->tsrOffsets();
$dp->getTemp()->extLinkContentOffsets = $tsr2;
return [
new SelfclosingTagTk(
'extlink',
[
new KV( 'href', $flat, $tsr1->expandTsrV() ),
new KV( 'mw:content', $content ?? '', $tsr2->expandTsrV() ),
new KV( 'spaces', $sp )
],
$dp
)
]; }
) { return $r; }
RFC = &{ return $this->siteConfig->magicLinkEnabled("RFC"); } 'RFC' {
return 'RFC';
}
PMID = &{ return $this->siteConfig->magicLinkEnabled("PMID"); } 'PMID' {
return 'PMID';
}
autoref =
ref:(RFC / PMID) sp:space_or_nbsp+ identifier:$[0-9]+ end_of_word
{
$base_urls = [
'RFC' => 'https://tools.ietf.org/html/rfc%s',
'PMID' => '//www.ncbi.nlm.nih.gov/pubmed/%s?dopt=Abstract'
];
$tsr = $this->tsrOffsets();
$dp = new DataParsoid;
$dp->tsr = $tsr;
$dp->stx = 'magiclink';
$this->env->getDataAccess()->addTrackingCategory(
$this->env->getPageConfig(),
$this->env->getMetadata(),
'magiclink-tracking-' . strtolower($ref)
);
return [
new SelfclosingTagTk( 'extlink', [
new KV( 'href', sprintf( $base_urls[$ref], $identifier ) ),
new KV( 'mw:content', TokenizerUtils::flattenString( [ $ref, $sp, $identifier ] ), $tsr->expandTsrV() ),
new KV( 'typeof', 'mw:ExtLink/' . $ref )
],
$dp
)
];
}
isbn =
&{ return $this->siteConfig->magicLinkEnabled("ISBN"); }
'ISBN' sp:space_or_nbsp+ isbn:(
[0-9]
((space_or_nbsp_or_dash / "") [0-9])+
((space_or_nbsp_or_dash / "") [xX] / "")
)
isbncode:(
end_of_word
{
// Convert isbn token-and-entity array to stripped string.
$stripped = '';
foreach ( TokenizerUtils::flattenStringlist( $isbn ) as $part ) {
if ( is_string( $part ) ) {
$stripped .= $part;
}
}
return strtoupper( preg_replace( '/[^\dX]/i', '', $stripped ) );
}
)
&{
// ISBNs can only be 10 or 13 digits long (with a specific format)
return strlen( $isbncode ) === 10
|| ( strlen( $isbncode ) === 13 && preg_match( '/^97[89]/', $isbncode ) );
}
{
$tsr = $this->tsrOffsets();
$dp = new DataParsoid;
$dp->stx = 'magiclink';
$dp->tsr = $tsr;
$this->env->getDataAccess()->addTrackingCategory(
$this->env->getPageConfig(),
$this->env->getMetadata(),
'magiclink-tracking-isbn'
);
return [
new SelfclosingTagTk( 'extlink', [
new KV( 'href', 'Special:BookSources/' . $isbncode ),
new KV( 'mw:content', TokenizerUtils::flattenString( [ 'ISBN', $sp, $isbn ] ), $tsr->expandTsrV() ),
new KV( 'typeof', 'mw:WikiLink/ISBN' )
],
$dp
)
];
}
/* Default URL protocols in MediaWiki (see DefaultSettings). Normally
* these can be configured dynamically. */
url_protocol =
p:$( '//' / [A-Za-z] [-A-Za-z0-9+.]* ':' '//'? )
& { return Utils::isProtocolValid( $p, $this->env ); }
{ return $p; }
// no punctuation, and '{<' to trigger directives
no_punctuation_char = [^ \]\[\r\n"'<>\x00-\x20\x7f&\u00A0\u1680\u180E\u2000-\u200A\u202F\u205F\u3000{]
// this is the general url rule
// on the PHP side, the path part matches EXT_LINK_URL_CLASS
// which is '[^][<>"\\x00-\\x20\\x7F\p{Zs}]'
url =
proto:url_protocol
addr:(ipv6urladdr / "")
path:(
!inline_breaks c:(
no_punctuation_char
/ comment
/ tplarg_or_template
/ ['{]
/ ! ( "&" ( [lL][tT] / [gG][tT] ) ";" )
r:(
& "&" he:htmlentity { return $he; }
/ "&"
) { return $r; }
) { return $c; }
)*
// Must be at least one character after the protocol
& { return $addr !== '' || count( $path ) > 0; }
{
return TokenizerUtils::flattenString( array_merge( [ $proto, $addr ], $path ) );
}
// this is the somewhat-restricted rule used in autolinks
// See Parser::doMagicLinks and Parser.php::makeFreeExternalLink.
// The `path` portion matches EXT_LINK_URL_CLASS, as in the general
// url rule. As in PHP, we do some fancy fixup to yank out
// trailing punctuation, perhaps including parentheses.
autourl =
! '//' // protocol-relative autolinks not allowed (T32269)
r:(
proto:url_protocol
addr:(ipv6urladdr / "")
path:(
!inline_breaks c:(
no_punctuation_char
/ comment
/ tplarg_or_template
/ $("'" !"'") // single quotes are ok, double quotes are bad
/ "{"
/ ! ( rhe:raw_htmlentity &{ return $rhe === '<' || $rhe === '>' || $rhe === "\u{A0}"; } )
r:(
& "&" he:htmlentity { return $he; }
/ "&"
) { return $r; }
) { return $c; }
)*
{
// as in Parser.php::makeFreeExternalLink, we're going to
// yank trailing punctuation out of this match.
$url = TokenizerUtils::flattenStringlist( array_merge( [ $proto, $addr ], $path ) );
// only need to look at last element; HTML entities are strip-proof.
$last = PHPUtils::lastItem( $url );
$trim = 0;
if ( is_string( $last ) ) {
$strip = TokenizerUtils::getAutoUrlTerminatingChars( in_array( '(', $path, true ) );
$trim = strspn( strrev( $last ), $strip );
$url[ count( $url ) - 1 ] = substr( $last, 0, strlen( $last ) - $trim );
}
$url = TokenizerUtils::flattenStringlist( $url );
if ( count( $url ) === 1 && is_string( $url[0] ) && strlen( $url[0] ) <= strlen( $proto ) ) {
return null; // ensure we haven't stripped everything: T106945
}
$this->currPos -= $trim;
return $url;
}
)
&{ return $r !== null; }
{
$tsr = $this->tsrOffsets();
$dp = new DataParsoid;
$dp->tsr = $tsr;
$res = [ new SelfclosingTagTk( 'urllink', [ new KV( 'href', $r, $tsr->expandTsrV() ) ], $dp ) ];
return $res;
}
// This is extracted from EXT_LINK_ADDR in Parser.php: a simplified
// expression to match an IPv6 address. The IPv4 address and "at least
// one character of a host name" portions are punted to the `path`
// component of the `autourl` and `url` productions
ipv6urladdr =
$( "[" [0-9A-Fa-f:.]+ "]" )
/**************************************************************
* Templates, -arguments and wikilinks
**************************************************************/
/*
* Precedence: template arguments win over templates. See
* http://www.mediawiki.org/wiki/Preprocessor_ABNF#Ideal_precedence
* 4: {{{{·}}}} → {·{{{·}}}·}
* 5: {{{{{·}}}}} → {{·{{{·}}}·}}
* 6: {{{{{{·}}}}}} → {{{·{{{·}}}·}}}
* 7: {{{{{{{·}}}}}}} → {·{{{·{{{·}}}·}}}·}
* This is only if close has > 3 braces; otherwise we just match open
* and close as we find them.
*/
tplarg_or_template =
&'{{'
t:tplarg_or_template_guarded<intemplate=true> {
return $t;
}
tplarg_or_template_guarded =
&('{{' &('{{{'+ !'{') tplarg) a:(template/broken_template) { return $a; }
/ a:$('{' &('{{{'+ !'{'))? b:tplarg { return [ $a, $b ]; }
/ a:$('{' &('{{' !'{'))? b:template { return [ $a, $b ]; }
/ broken_template
tplarg_or_template_or_bust =
r:(tplarg_or_template / .)+ { return TokenizerUtils::flattenIfArray( $r ); }
template =
template_preproc<&preproc="}}">
// The old preprocessor maintains a single stack of "closing token we
// are currently looking for", with no backtracking. This means that
// once you see `[[ {{` you are looking only for `}}` -- if that template
// turns out to be broken you will never pop the `}}` and there is no way
// to close the `[[`. Since the PEG tokenizer in Parsoid uses backtracking
// and parses in a single pass (instead of PHP's split preprocessor/parser)
// we have to be a little more careful when we emulate this behavior.
// If we use a rule like:
// template = "{{" tplname tplargs* "}}"?
// Then we end up having to reinterpret `tplname tplargs*` as a tlb if it
// turns out we never find the `}}`, which involves a lot of tedious gluing
// tokens back together with fingers crossed we haven't discarded any
// significant newlines/whitespace/etc. An alternative would be a rule like:
// broken_template = "{{" tlb
// but again, `template` is used in many different contexts; `tlb` isn't
// necessarily the right one to recursively invoke. Instead we get the
// broken template off of the PEGjs production stack by returning immediately
// after `{{`, but we set the "preproc" reference parameter to false (the
// reference parameter feature having been introduced for this sole purpose)
// to indicate to the parent rule that we're "still in" the {{ context and
// shouldn't ever inlineBreak for any closing tokens above this one. For
// example:
// [[Foo{{Bar]]
// This will match as:
// wikilink->text,template->text --> FAILS looking for }}
// backtracks, popping "bracket_bracket" and "brace_brace" off preproc stack
// wikilink->text,broken_template,text --> FAILS looking for ]]
// backtracks, popping "bracket_bracket" and false off preproc stack
// broken_wikilink,text,broken_template,text --> OK
// with [false, false] left on the preproc stack
broken_template =
preproc:<&preproc>
t:"{{" {
$preproc = null;
return $t;
}
template_preproc =
"{{" leadWS:$( nl_comment_space* )
target:template_param_value
params:(
nl_comment_space* "|"
r:(
p0:("" { return $this->endOffset(); })
v:nl_comment_space*
p1:("" { return $this->endOffset(); })
&("|" / "}}")
{
// empty argument
$tsr0 = new SourceRange( $p0, $p1 );
return new KV( '', TokenizerUtils::flattenIfArray( $v ), $tsr0->expandTsrV() );
}
/ template_param
) { return $r; }
)*
trailWS:$( nl_comment_space* )
inline_breaks "}}"
{
// Insert target as first positional attribute, so that it can be
// generically expanded. The TemplateHandler then needs to shift it out
// again.
array_unshift( $params, new KV( TokenizerUtils::flattenIfArray( $target['tokens'] ), '', $target['srcOffsets']->expandTsrK() ) );
$dp = new DataParsoid;
$dp->tsr = $this->tsrOffsets();
$dp->src = $this->text();
$tmp = $dp->getTemp();
$tmp->leadWS = $leadWS;
$tmp->trailWS = $trailWS;
$obj = new SelfclosingTagTk( 'template', $params, $dp );
return $obj;
}
/ $('{{' space_or_newline* '}}')
tplarg =
tplarg_preproc<&preproc="}}">
tplarg_preproc =
"{{{"
p:("" { return $this->endOffset(); })
target:template_param_value?
params:(
nl_comment_space* "|"
r:(
p0:("" { return $this->endOffset(); })
v:nl_comment_space*
p1:("" { return $this->endOffset(); })
&("|" / "}}}")
{
// empty argument
return [ 'tokens' => $v, 'srcOffsets' => new SourceRange( $p0, $p1 ) ];
}
/ template_param_value
) { return $r; }
)*
nl_comment_space*
inline_breaks "}}}"
{
$kvs = [];
if ( $target === null ) {
$target = [ 'tokens' => '', 'srcOffsets' => new SourceRange( $p, $p ) ];
}
// Insert target as first positional attribute, so that it can be
// generically expanded. The TemplateHandler then needs to shift it out
// again.
$kvs[] = new KV( TokenizerUtils::flattenIfArray( $target['tokens'] ), '', $target['srcOffsets']->expandTsrK() );
foreach ( $params as $o ) {
$s = $o['srcOffsets'];
$kvs[] = new KV( '', TokenizerUtils::flattenIfArray( $o['tokens'] ), $s->expandTsrV() );
}
$dp = new DataParsoid;
$dp->tsr = $this->tsrOffsets();
$dp->src = $this->text();
$obj = new SelfclosingTagTk( 'templatearg', $kvs, $dp );
return $obj;
}
template_param =
name:template_param_name
val:(
kEndPos:("" { return $this->endOffset(); })
// no optionalSpaceToken here, it's eaten by template_param_name
"="
vStartPos:("" { return $this->endOffset(); })
optSp:optionalSpaceToken
tpv:template_param_value? {
return [
'kEndPos' => $kEndPos,
'vStartPos' => $vStartPos,
'value' => TokenizerUtils::flattenString( [ $optSp, $tpv['tokens'] ?? [] ] ),
];
}
)? {
if ( $val !== null ) {
if ( $val['value'] !== null ) {
$so = new KVSourceRange(
$this->startOffset(), $val['kEndPos'],
$val['vStartPos'], $this->endOffset()
);
return new KV(
$name,
TokenizerUtils::flattenIfArray( $val['value'] ),
$so
);
} else {
return new KV(
TokenizerUtils::flattenIfArray( $name ),
'',
$so
);
}
} else {
$so = new SourceRange( $this->startOffset(), $this->endOffset() );
return new KV(
'',
TokenizerUtils::flattenIfArray( $name ),
$so->expandTsrV()
);
}
}
// empty parameter
/ & [|}] {
$so = new SourceRange( $this->startOffset(), $this->endOffset() );
return new KV( '', '', $so->expandTsrV() );
}
template_param_name =
template_param_text<equal> / (&'=' { return ''; })
template_param_value =
tpt:template_param_text<equal=false>
{
return [ 'tokens' => $tpt, 'srcOffsets' => $this->tsrOffsets() ];
}
template_param_text =
il:(nested_block<table=false, extlink=false, templateArg=true, tableCellArg=false> / newlineToken)+
{
// il is guaranteed to be an array -- so, tu.flattenIfArray will
// always return an array
$r = TokenizerUtils::flattenIfArray( $il );
if ( count( $r ) === 1 && is_string( $r[0] ) ) {
$r = $r[0];
}
return $r;
}
//// Language converter block markup of language variants: -{ ... }-
// Note that "rightmost opening" precedence rule (see
// https://www.mediawiki.org/wiki/Preprocessor_ABNF ) means
// that neither -{{ nor -{{{ are parsed as a -{ token, although
// -{{{{ is (since {{{ has precedence over {{).
lang_variant_or_tpl =
&('-{' &('{{{'+ !'{') tplarg) a:lang_variant { return $a; }
/ a:$('-' &('{{{'+ !'{')) b:tplarg { return [ $a, $b ]; }
/ a:$('-' &('{{' '{{{'* !'{')) b:template { return [ $a, $b ]; }
/ &'-{' a:lang_variant { return $a; }
broken_lang_variant =
r:"-{"
preproc:<&preproc>
{
$preproc = null;
return $r;
}
lang_variant =
// FIXME: Maybe this should suppress "table" and "tableCellArg" like 'template_param_text' d too
lang_variant_preproc<&preproc="}-", extlink=false>
/ broken_lang_variant
lang_variant_preproc =
lv0:("-{" { return $this->startOffset(); })
f:(
&{ return $this->env->langConverterEnabled(); }
ff:opt_lang_variant_flags {
// if flags contains 'R', then don't treat ; or : specially inside.
if ( isset( $ff['flags'] ) ) {
$ff['raw'] = isset( $ff['flags']['R'] ) || isset( $ff['flags']['N'] );
} elseif ( isset( $ff['variants'] ) ) {
$ff['raw'] = true;
}
return $ff;
} /
&{ return !$this->env->langConverterEnabled(); }
"" {
// if language converter not enabled, don't try to parse inside.
return [ 'raw' => true ];
}
)
ts:(
&{ return $f['raw']; }
lv:lang_variant_text { return [ [ 'text' => $lv ] ]; }
/
&{ return !$f['raw']; }
lv:lang_variant_option_list { return $lv; }
)
inline_breaks
lv1:("}-" { return $this->endOffset(); })
{
if ( !$this->env->langConverterEnabled() ) {
return [ '-{', $ts[0]['text']['tokens'], '}-' ];
}
$lvsrc = substr( $this->input, $lv0, $lv1 - $lv0 );
$attribs = [];
foreach ( $ts as &$t ) {
// move token strings into KV attributes so that they are
// properly expanded by early stages of the token pipeline
foreach ( [ 'text', 'from', 'to' ] as $fld ) {
if ( !isset( $t[$fld] ) ) {
continue;
}
$name = 'mw:lv' . count( $attribs );
// Note that AttributeExpander will expect the tokens array to be
// flattened. We do that in lang_variant_text / lang_variant_nowiki
$attribs[] = new KV( $name, $t[$fld]['tokens'], $t[$fld]['srcOffsets']->expandTsrV() );
$t[$fld] = $name;
}
}
unset( $t );
$flags = isset( $f['flags'] ) ? array_keys( $f['flags'] ) : [];
sort( $flags );
$variants = isset( $f['variants'] ) ? array_keys( $f['variants'] ) : [];
sort( $variants );
$dp = new DataParsoid;
$dp->tsr = new SourceRange( $lv0, $lv1 );
$dp->src = $lvsrc;
$dp->flags = $flags;
$dp->variants = $variants;
$dp->original = $f['original'];
$dp->flagSp = $f['sp'];
$dp->texts = $ts;
return [
new SelfclosingTagTk(
'language-variant',
$attribs,
$dp
)
];
}
opt_lang_variant_flags =
f:( ff:lang_variant_flags "|" { return $ff; } )?
{
// Collect & separate flags and variants into a hashtable (by key) and ordered list
$flags = [];
$variants = [];
$flagList = [];
$flagSpace = [];
$variantList = [];
$variantSpace = [];
$useVariants = false;
if ( $f !== null ) {
// lang_variant_flags returns arrays in reverse order.
$spPtr = count( $f['sp'] ) - 1;
for ( $i = count( $f['flags'] ) - 1; $i >= 0; $i--) {
$item = $f['flags'][$i];
if ( isset( $item['flag'] ) ) {
$flagSpace[] = $f['sp'][$spPtr--];
$flags[$item['flag']] = true;
$flagList[] = $item['flag'];
$flagSpace[] = $f['sp'][$spPtr--];
}
if ( isset( $item['variant'] ) ) {
$variantSpace[] = $f['sp'][$spPtr--];
$variants[$item['variant']] = true;
$variantList[] = $item['variant'];
$variantSpace[] = $f['sp'][$spPtr--];
}
}
if ( $spPtr >= 0 ) {
// handle space after a trailing semicolon
$flagSpace[] = $f['sp'][$spPtr];
$variantSpace[] = $f['sp'][$spPtr];
}
}
// Parse flags (this logic is from core/languages/ConverterRule.php
// in the parseFlags() function)
if ( count( $flags ) === 0 && count( $variants ) === 0 ) {
$flags['$S'] = true;
} elseif ( isset( $flags['R'] ) ) {
$flags = [ 'R' => true ]; // remove other flags
} elseif ( isset( $flags['N'] ) ) {
$flags = [ 'N' => true ]; // remove other flags
} elseif ( isset( $flags['-'] ) ) {
$flags = [ '-' => true ]; // remove other flags
} elseif ( isset( $flags['T'] ) && count( $flags ) === 1 ) {
$flags['H'] = true;
} elseif ( isset( $flags['H'] ) ) {
// Replace A flag, and remove other flags except T and D
$nf = [ '$+' => true, 'H' => true ];
if ( isset( $flags['T'] ) ) { $nf['T'] = true; }
if ( isset( $flags['D'] ) ) { $nf['D'] = true; }
$flags = $nf;
} elseif ( count( $variants ) > 0 ) {
$useVariants = true;
} else {
if ( isset( $flags['A'] ) ) {
$flags['$+'] = true;
$flags['$S'] = true;
}
if ( isset( $flags['D'] ) ) {
unset( $flags['$S'] );
}
}
if ( $useVariants ) {
return [ 'variants' => $variants, 'original' => $variantList, 'sp' => $variantSpace ];
} else {
return [ 'flags' => $flags, 'original' => $flagList, 'sp' => $flagSpace ];
}
}
lang_variant_flags =
sp1:$(space_or_newline*) f:lang_variant_flag sp2:$(space_or_newline*)
more:( ";" lang_variant_flags? )?
{
$r = ( $more && $more[1] ) ? $more[1] : [ 'sp' => [], 'flags' => [] ];
// Note that sp and flags are in reverse order, since we're using
// right recursion and want to push instead of unshift.
$r['sp'][] = $sp2;
$r['sp'][] = $sp1;
$r['flags'][] = $f;
return $r;
}
/ sp:$(space_or_newline*) {
return [ 'sp' => [ $sp ], 'flags' => [] ];
}
lang_variant_flag =
f:[-+A-Z] { return [ 'flag' => $f ]; }
/ v:lang_variant_name { return [ 'variant' => $v ]; }
/ b:$(!space_or_newline !nowiki [^{}|;])+ { return [ 'bogus' => $b ]; /* bad flag */}
// language variant name, like zh, zh-cn, etc.
lang_variant_name =
$([a-z] [-a-zA-Z]+)
// Escaped otherwise-unrepresentable language names
// Primarily for supporting html2html round trips; PHP doesn't support
// using nowikis here (yet!)
/ nowiki_text
lang_variant_option_list =
o:lang_variant_option rest:( ";" oo:lang_variant_option { return $oo; })*
tr:( ";" $bogus_lang_variant_option )* // optional trailing crap
{
array_unshift( $rest, $o );
// if the last bogus option is just spaces, keep them; otherwise
// drop all this bogus stuff on the ground
if ( count($tr) > 0 ) {
$last = $tr[count($tr)-1];
if (preg_match('/^\s*$/Du', $last[1])) {
$rest[] = [ 'semi' => true, 'sp' => $last[1] ];
}
}
return $rest;
}
/ lvtext:lang_variant_text { return [ [ 'text' => $lvtext ] ]; }
bogus_lang_variant_option =
lang_variant_text?
lang_variant_option =
sp1:$(space_or_newline*) lang:lang_variant_name
sp2:$(space_or_newline*) ":"
sp3:$(space_or_newline*)
lvtext:(lang_variant_nowiki / lang_variant_text_no_semi)
{
return [
'twoway' => true,
'lang' => $lang,
'text' => $lvtext,
'sp' => [ $sp1, $sp2, $sp3 ]
];
}
/ sp1:$(space_or_newline*)
from:(lang_variant_nowiki / lang_variant_text_no_semi_or_arrow)
"=>"
sp2:$(space_or_newline*) lang:lang_variant_name
sp3:$(space_or_newline*) ":"
sp4:$(space_or_newline*)
to:(lang_variant_nowiki / lang_variant_text_no_semi)
{
return [
'oneway' => true,
'from' => $from,
'lang' => $lang,
'to' => $to,
'sp' => [ $sp1, $sp2, $sp3, $sp4 ]
];
}
// html2wt support: If a language name or conversion string can't be
// represented w/o breaking wikitext, just wrap it in a <nowiki>.
// PHP doesn't support this (yet), but Parsoid does.
lang_variant_nowiki =
n:nowiki_text
sp:$space_or_newline* {
$tsr = $this->tsrOffsets();
$tsr->end -= strlen( $sp );
return [
'tokens' => [ $n ],
'srcOffsets' => $tsr,
];
}
lang_variant_text =
tokens:(inlineline / "|" )*
{
return [
'tokens' => TokenizerUtils::flattenStringlist( $tokens ),
'srcOffsets' => $this->tsrOffsets(),
];
}
lang_variant_text_no_semi =
lang_variant_text<semicolon>
lang_variant_text_no_semi_or_arrow =
lang_variant_text_no_semi<arrow>
wikilink_content =
(
pipe
startPos:("" { return $this->endOffset(); })
lt:link_text? {
$tsr = new SourceRange( $startPos, $this->endOffset() );
$maybeContent = new KV( 'mw:maybeContent', $lt ?? [], $tsr->expandTsrV() );
$maybeContent->vsrc = substr( $this->input, $startPos, $this->endOffset() - $startPos );
return $maybeContent;
}
)*
wikilink =
wikilink_preproc<&preproc="]]">
/ broken_wikilink
// `broken-link` (see [[:mw:Preprocessor_ABNF]]), but careful because the
// second bracket could start an extlink. Set preproc to false as a reference
// parameter in the parent since we haven't seen a double-close bracket.
// (See full explanation above broken_template production.)
broken_wikilink =
&"[["
preproc:<&preproc>
&{ $preproc = null; return true; }
a:("[" (extlink / "[")) {
return $a;
}
wikilink_preproc =
"[["
spos:("" { return $this->endOffset(); })
target:wikilink_preprocessor_text?
tpos:("" { return $this->endOffset(); })
lcs:wikilink_content
inline_breaks "]]"
{
$pipeTrick = count( $lcs ) === 1 && count( $lcs[0]->v ) === 0;
$textTokens = [];
if ( $target === null || $pipeTrick ) {
$textTokens[] = '[[';
if ( $target ) {
$textTokens[] = $target;
}
foreach ( $lcs as $a ) {
// a is a mw:maybeContent attribute
$textTokens[] = '|';
if ( count( $a->v ) > 0 ) {
$textTokens[] = $a->v;
}
}
$textTokens[] = ']]';
return $textTokens;
}
$obj = new SelfclosingTagTk( 'wikilink' );
$tsr = new SourceRange( $spos, $tpos );
$hrefKV = new KV( 'href', $target, $tsr->expandTsrV() );
$hrefKV->vsrc = $tsr->substr( $this->input );
// XXX: Point to object with path, revision and input information
// obj.source = input;
$obj->attribs[] = $hrefKV;
$obj->attribs = array_merge( $obj->attribs, $lcs );
$dp = new DataParsoid;
$dp->tsr = $this->tsrOffsets();
$dp->src = $this->text();
$obj->dataParsoid = $dp;
return [ $obj ];
}
// Tables are allowed inside image captions.
// Suppress the equal flag temporarily in this rule to consume the '=' here.
link_text = link_text_parameterized<equal = false, linkdesc = true>
link_text_parameterized =
c:(
// This group is similar to "block_line" but "list_item"
// is omitted since `doBlockLevels` happens after
// `handleInternalLinks2`, where newlines are stripped.
(sol (heading / hr / full_table_in_link_caption))
/ urltext
/ (
!inline_breaks
r:( inline_element / '[' text_char+ ']' $(&(!']' / ']]')) / . ) { return $r; }
)
)+ {
return TokenizerUtils::flattenStringlist( $c );
}
/* Generic quote rule for italic and bold, further processed in a token
* stream transformation in doQuotes. Relies on NlTk tokens being emitted
* for each line of text to balance quotes per line.
*
* We are not using a simple pair rule here as we need to support mis-nested
* bolds/italics and MediaWiki's special heuristics for apostrophes, which are
* all not context free. */
quote =
quotes:$("''" "'"*) {
// sequences of four or more than five quotes are assumed to start
// with some number of plain-text apostrophes.
$plainticks = 0;
$result = [];
if ( strlen( $quotes ) === 4 ) {
$plainticks = 1;
} elseif ( strlen( $quotes ) > 5 ) {
$plainticks = strlen( $quotes ) - 5;
}
if ( $plainticks > 0 ) {
$result[] = substr( $quotes, 0, $plainticks );
}
// mw-quote token will be consumed in token transforms
$tsr = $this->tsrOffsets();
$tsr->start += $plainticks;
$dp = new DataParsoid;
$dp->tsr = $tsr;
$mwq = new SelfclosingTagTk( 'mw-quote',
[ new KV( 'value', substr( $quotes, $plainticks ) ) ],
$dp );
if ( strlen( $quotes ) > 2 ) {
$mwq->addAttribute( 'isSpace_1', $tsr->start > 0 && substr( $this->input, $tsr->start - 1, 1 ) === ' ');
$mwq->addAttribute( 'isSpace_2', $tsr->start > 1 && substr( $this->input, $tsr->start - 2, 1 ) === ' ');
}
$result[] = $mwq;
return $result;
}
/***********************************************************
* Xmlish tags
***********************************************************/
// FIXME: Temporary (?) hack to let us not horribly break on old tvar syntax
// In coordination with language team, get rid of this hack once all old uses
// are migrated to new syntax (T274881).
tvar_old_syntax_closing_HACK =
"/>"
& { return $this->env->hasAnnotations && $this->siteConfig->isAnnotationTag( 'tvar' ); }
{
$metaAttrs = [ new KV( 'typeof', 'mw:Annotation/tvar/End' ) ];
$dp = new DataParsoid();
$dp->tsr = $this->tsrOffsets();
$dp->tsr->start--; // For "<" matched at the start of xmlish_tag rule
if ( empty( $this->pipelineOpts['inTemplate'] ) ) {
return [ new SelfclosingTagTk ( 'meta', $metaAttrs, $dp ) ];
} else {
// suppress meta tags from pipeline output
return [];
}
}
annotation_tag =
annToken:extension_annotation_tag
&{
return ( $annToken instanceof Token && $annToken->getName() !== 'extension' );
}
{ return $annToken; }
extension_annotation_tag =
!<annOrExtTag>
"<" tag:(
extToken:xmlish_tag<annOrExtTag>
// Account for `maybeAnnotationOrExtensionTag` returning unmatched start / end tags
&{ return !$extToken || $extToken[0]->getName() === 'extension' ||
($extToken[0]->getName() === 'meta' && preg_match( WTUtils::ANNOTATION_META_TYPE_REGEXP, $extToken[0]->getAttributeV( 'typeof' ) ?? '' ) > 0); }
{ return !$extToken ? '' : $extToken[0]; }
/ tvar_old_syntax_closing_HACK ) { return $tag; }
nowiki =
& ("<" "/"? "nowiki"i )
extToken:extension_annotation_tag
{ return $extToken; }
// Used by lang_variant productions to protect special language names or
// conversion strings.
nowiki_text =
extToken:nowiki
{
$txt = Utils::extractExtBody( $extToken );
return Utils::decodeWtEntities( $txt );
}
/* Generic XML-like tags
*
* These also cover extensions (including Cite), which will hook into the
* token stream for further processing. The content of extension tags is
* parsed as regular inline, but the source positions of the tag are added
* to allow reconstructing the unparsed text from the input. */
// See http://www.w3.org/TR/html5/syntax.html#tag-open-state and the following
// paragraphs. Note that we don't enforce ascii alpha for the first character
// here because we need to be more permissive for extension tag names. That
// happens in xmlish_tag below.
tag_name = $[^\t\n\v />\0]+
// This rule is used in carefully crafted places of xmlish tag tokenizing with
// the inclusion of solidus to match where the spec would ignore those
// characters. In particular, it does not belong in between attribute name
// and value.
space_or_newline_or_solidus = space_or_newline / (s:"/" !">" { return $s; })
xmlish_tag =
& {
$this->assert(
$this->input[$this->currPos - 1] === '<',
'Failed to open xmlish_tag before entering.'
);
return true;
}
end:"/"?
name: tag_name
annOrExtTag: <annOrExtTag>
& {
if ( $annOrExtTag ) {
return WTUtils::isAnnOrExtTag( $this->env, $name );
} else {
// Only enforce ascii alpha first char for non-extension tags.
// See tag_name above for the details.
return preg_match( '/^[A-Za-z]/', $name ) && $this->isXMLTag( $name );
}
}
// By the time we get to `doTableStuff` in the old parser, we've already
// safely encoded element attributes. See 55313f4e in core.
attribs:generic_newline_attributes<table=false, tableCellArg=false>
space_or_newline_or_solidus* // No need to preserve this -- canonicalize on RT via dirty diff
selfclose:"/"?
space* // not preserved - canonicalized on RT via dirty diff
">"
{
$lcName = mb_strtolower( $name );
// Extension tags don't necessarily have the same semantics as html tags,
// so don't treat them as void elements.
$isVoidElt = Utils::isVoidElement( $lcName ) && !$annOrExtTag;
// Support </br>
if ( $lcName === 'br' && $end ) {
$end = null;
}
$tsr = $this->tsrOffsets();
$tsr->start--; // For "<" matched at the start of xmlish_tag rule
$res = TokenizerUtils::buildXMLTag( $name, $lcName, $attribs, $end, !!$selfclose || $isVoidElt, $tsr );
// change up data-attribs in one scenario
// void-elts that aren't self-closed ==> useful for accurate RT-ing
if ( !$selfclose && $isVoidElt ) {
unset( $res->dataParsoid->selfClose );
$res->dataParsoid->noClose = true;
}
$met = $this->maybeAnnotationOrExtensionTag( $res, $end, $attribs, $tsr );
return is_array( $met ) ? $met : [ $met ];
}
// A generic attribute that can span multiple lines.
generic_newline_attribute =
space_or_newline_or_solidus*
namePos0:("" { return $this->endOffset(); })
name:generic_attribute_name
namePos1:("" { return $this->endOffset(); })
vd:(space_or_newline* "=" v:generic_att_value? { return $v; })?
{
// NB: Keep in sync w/ table_attibute
$res = null;
// Encapsulate protected attributes.
if ( is_string( $name ) ) {
$name = TokenizerUtils::protectAttrs( $name );
}
$nameSO = new SourceRange( $namePos0, $namePos1 );
if ( $vd !== null ) {
$res = new KV( $name, $vd['value'], $nameSO->join( $vd['srcOffsets'] ) );
$res->vsrc = $vd['srcOffsets']->substr( $this->input );
} else {
$res = new KV( $name, '', $nameSO->expandTsrK() );
}
if ( is_array( $name ) ) {
$res->ksrc = $nameSO->substr( $this->input );
}
return $res;
}
// A single-line attribute.
table_attribute =
s:optionalSpaceToken
namePos0:("" { return $this->endOffset(); })
name:table_attribute_name
namePos1:("" { return $this->endOffset(); })
vd:(optionalSpaceToken "=" v:table_att_value? { return $v; })?
{
// NB: Keep in sync w/ generic_newline_attribute
$res = null;
// Encapsulate protected attributes.
if ( gettype( $name ) === 'string' ) {
$name = TokenizerUtils::protectAttrs( $name );
}
$nameSO = new SourceRange( $namePos0, $namePos1 );
if ( $vd !== null ) {
$res = new KV( $name, $vd['value'], $nameSO->join( $vd['srcOffsets'] ) );
$res->vsrc = $vd['srcOffsets']->substr( $this->input );
} else {
$res = new KV( $name, '', $nameSO->expandTsrK() );
}
if ( is_array( $name ) ) {
$res->ksrc = $nameSO->substr( $this->input );
}
return $res;
}
// The old parser's Sanitizer::removeHTMLtags explodes on < so that it can't
// be found anywhere in xmlish tags. This is a divergence from html5 tokenizing
// which happily permits it in attribute positions. Extension tags being the
// exception, since they're stripped beforehand.
less_than =
$(
&<annOrExtTag>
"<"
)
// The arrangement of chars is to emphasize the split between what's disallowed
// by html5 and what's necessary to give directive a chance.
// See: http://www.w3.org/TR/html5/syntax.html#attributes-0
generic_attribute_name =
q:$(["'=]?) // From #before-attribute-name-state, < is omitted for directive
r:(
$[^ \t\r\n\0/=><&{}\-!|]+
/ !inline_breaks
// \0/=> is the html5 attribute name set we do not want.
t:(
directive
/ less_than
/ $( !( space_or_newline / [\0/=><] ) . )
) { return $t; }
)*
& { return count( $r ) > 0 || $q !== ''; }
{
array_unshift( $r, $q );
return TokenizerUtils::flattenString( $r );
}
// Also accept these chars in a wikitext table or tr attribute name position.
// They are normally not matched by the table_attribute_name.
broken_table_attribute_name_char = c:[\0/=>] { return new KV( $c, '' ); }
// Same as generic_attribute_name, except for accepting tags and wikilinks.
// (That doesn't make sense (ie. match the old parser) in the generic case.)
// We also give a chance to break on \[ (see T2553).
table_attribute_name =
q:$(["'=]?) // From #before-attribute-name-state, < is omitted for directive
r:(
$[^ \t\r\n\0/=><&{}\-!|\[]+
/ !inline_breaks
// \0/=> is the html5 attribute name set we do not want.
t:(
$wikilink
/ directive
// Accept tags-inside-attributes as attribute names.
// The sanitizer will strip and shadow them for roundtripping.
// Example: <hiddentext>generated with.. </hiddentext>
/ x:inline_xmlish_tag ill:inlineline? { return array_merge( $x, $ill ?: [] ); }
/ $( !( space_or_newline / [\0/=>] ) . )
) { return $t; }
)*
& { return count( $r ) > 0 || $q !== ''; }
{
array_unshift( $r, $q );
return TokenizerUtils::flattenString( $r );
}
// Attribute value, quoted variants can span multiple lines.
// Missing end quote: accept /> look-ahead as heuristic.
// These need to be kept in sync with the attribute_preprocessor_text_*
generic_att_value =
s:$(space_or_newline* "'") t:attribute_preprocessor_text_single? q:$("'" / &('/'? '>')) {
return TokenizerUtils::getAttrVal( $t, $this->startOffset() + strlen( $s ), $this->endOffset() - strlen( $q ) );
}
/ s:$(space_or_newline* '"') t:attribute_preprocessor_text_double? q:$('"' / &('/'? '>')) {
return TokenizerUtils::getAttrVal( $t, $this->startOffset() + strlen( $s ), $this->endOffset() - strlen( $q ) );
}
/ s:$space_or_newline* t:attribute_preprocessor_text &(space_or_newline / eof / '/'? '>') {
return TokenizerUtils::getAttrVal( $t, $this->startOffset() + strlen( $s ), $this->endOffset() );
}
// Attribute value, restricted to a single line.
// Missing end quote: accept |, !!, \r, and \n look-ahead as heuristic.
// These need to be kept in sync with the table_attribute_preprocessor_text_*
table_att_value =
s:$(space* "'") t:table_attribute_preprocessor_text_single? q:$("'" / &('!!' / [|\r\n])) {
return TokenizerUtils::getAttrVal( $t, $this->startOffset() + strlen( $s ), $this->endOffset() - strlen( $q ) );
}
/ s:$(space* '"') t:table_attribute_preprocessor_text_double? q:$('"' / &('!!' / [|\r\n])) {
return TokenizerUtils::getAttrVal( $t, $this->startOffset() + strlen( $s ), $this->endOffset() - strlen( $q ) );
}
/ s:$space* t:table_attribute_preprocessor_text &(space_or_newline/ eof / '!!' / '|') {
return TokenizerUtils::getAttrVal( $t, $this->startOffset() + strlen( $s ), $this->endOffset() );
}
/*********************************************************
* Lists
*********************************************************/
list_item = dtdd / hacky_dl_uses / li
li =
bullets:list_char+
c:inlineline?
// The inline_break is to check if we've hit a template end delimiter.
&(eolf / inline_breaks)
{
// Leave bullets as an array -- list handler expects this
$tsr = $this->tsrOffsets( 'start' );
$tsr->end += count( $bullets );
$dp = new DataParsoid;
$dp->tsr = $tsr;
$li = new TagTk( 'listItem', [ new KV( 'bullets', $bullets, $tsr->expandTsrV() ) ], $dp );
return array_merge( [ $li ], $c ?: [] );
}
/*
* This rule is required to support wikitext of this form
* ::{|border="1"|foo|bar|baz|}
* where the leading colons are used to indent the entire table.
* This hack was added back in 2006 in commit
* a0746946312b0f1eda30a2c793f5f7052e8e5f3a based on a patch by Carl
* Fürstenberg.
*/
hacky_dl_uses =
bullets:":"+
tbl:(table_line (sol+ table_line)*)
line:inlineline?
&((space / comment)* eolf)
{
// Leave bullets as an array -- list handler expects this
$tsr = $this->tsrOffsets( 'start' );
$tsr->end += count( $bullets );
$dp = new DataParsoid;
$dp->tsr = $tsr;
$li = new TagTk( 'listItem', [ new KV( 'bullets', $bullets, $tsr->expandTsrV() ) ], $dp );
return TokenizerUtils::flattenIfArray( [ $li, $tbl, $line ?: [] ] );
}
dtdd_colon = c:inlineline_break_on_colon? cpos:(":" { return $this->endOffset(); })
{
return [ $c, $cpos ];
}
dtdd =
bullets:(!(";" !list_char) lc:list_char { return $lc; })*
";"
colons:dtdd_colon*
d:inlineline?
&eolf {
$bulletToks = [];
// Leave bullets as an array -- list handler expects this
// TSR: +1 for the leading ";"
$numBullets = count( $bullets ) + 1;
$tsr = $this->tsrOffsets( 'start' );
$tsr->end += $numBullets;
$li1Bullets = $bullets;
$li1Bullets[] = ';';
$dp = new DataParsoid;
$dp->tsr = $tsr;
$bulletToks[] = new TagTk( 'listItem', [ new KV( 'bullets', $li1Bullets, $tsr->expandTsrV() ) ], $dp );
foreach ( $colons as $colon) {
if ( $colon[0] ) { // can be null because of "?" in dtdd_colon
$bulletToks[] = $colon[0];
}
$cpos = $colon[1];
// TSR: -1 for the intermediate ":"
$li2Bullets = $bullets;
$li2Bullets[] = ':';
$tsr2 = new SourceRange( $cpos - 1, $cpos );
$dp2 = new DataParsoid;
$dp2->tsr = $tsr2;
$dp2->stx = 'row';
$bulletToks[] = new TagTk( 'listItem', [ new KV( 'bullets', $li2Bullets, $tsr2->expandTsrV() ) ], $dp2 );
}
if ( $d ) {
$bulletToks = array_merge( $bulletToks, $d );
}
return $bulletToks;
}
list_char = [*#:;]
inlineline_break_on_colon =
inlineline<colon>
/******************************************************************************
* Tables
* ------
* Table rules are geared to support independent parsing of fragments in
* templates (the common table start / row / table end use case). The tokens
* produced by these fragments then match up to a table while building the
* DOM tree. For similar reasons, table rows do not emit explicit end tag
* tokens.
*
* The separate table_line rule is faster than moving those rules
* directly to block_lines.
*
* Notes about the full_table_in_link_caption rule
* -----------------------------------------------------
* However, for link-tables, we have introduced a stricter parse wherein
* we require table-start and table-end tags to not come from a template.
* In addition, this new rule doesn't accept fosterable-content in
* the table unlike the more lax (sol table_line)+ rule.
*
* This is the best we can do at this time since we cannot distinguish
* between table rows and image options entirely in the tokenizer.
*
* Consider the following examples:
*
* Example 1:
*
* [[Image:Foo.jpg|left|30px|Example 1
* {{This-template-returns-a-table-start-tag}}
* |foo
* {{This-template-returns-a-table-end-tag}}
* ]]
*
* Example 2:
*
* [[Image:Foo.jpg|left|30px|Example 1
* {{1x|a}}
* |foo
* {{1x|b}}
* ]]
*
* So, we cannot know a priori (without preprocessing or fully expanding
* all templates) if "|foo" in the two examples is a table cell or an image
* option. This is a limitation of our tokenizer-based approach compared to
* the preprocessing-based approach of the old parser.
*
* Given this limitation, we are okay forcing a full-table context in
* link captions (if necessary, we can relax the fosterable-content requirement
* but that is broken wikitext anyway, so we can force that edge-case wikitext
* to get fixed by rejecting it).
******************************************************************************/
full_table_in_link_caption =
!inline_breaks
// Note that "linkdesc" is suppressed here to provide a nested parsing
// context in which to parse the table. Otherwise, we may break on
// on pipes in the `table_start_tag` and `table_row_tag` attributes.
// However, as a result, this can be more permissive than the old
// implementation (legacy parser?), but likelier to match the users intent.
// Suppress the recursion protection from tableDataBlock since we're trying
// to parse a full table and if the link is itself nested in a table this
// will always stop. Hopefully, this won't result in any overflows.
r: full_table_in_link_caption_parameterized<linkdesc=false, table, tableDataBlock=false> {
return $r;
}
full_table_in_link_caption_parameterized =
table_start_tag
// Accept multiple end tags since a nested table may have been
// opened in the table content line.
(
(sol+ (table_content_line / tplarg_or_template))*
sol+ table_end_tag
)+
// This rule assumes start-of-line position!
table_line =
(! inline_breaks / & '{{!}}' )
tl:(
table_start_tag
/ table_content_line<table>
/ table_end_tag
) {
return $tl;
}
table_content_line =
(space / comment)* (
table_heading_tags
/ table_row_tag
/ table_data_tags
/ table_caption_tag
)
table_start_tag "table_start_tag" =
sc:(space / comment)*
startPos:("" { return $this->endOffset(); })
b:"{" p:pipe
// ok to normalize away stray |} on rt (see T59360)
ta:(table_attributes<table=false> / &{ $this->unreachable(); })
tsEndPos:("" { return $this->endOffset(); })
s2:space*
{
$coms = TokenizerUtils::popComments( $ta );
if ( $coms ) {
$tsEndPos = $coms['commentStartPos'];
}
$dp = new DataParsoid;
$dp->tsr = new SourceRange( $startPos, $tsEndPos );
if ( $p !== '|' ) {
// Variation from default
$dp->startTagSrc = $b . $p;
}
return array_merge( $sc,
[ new TagTk( 'table', $ta, $dp ) ],
$coms ? $coms['buf'] : [],
$s2 );
}
// FIXME: Not sure if we want to support it, but this should allow columns.
table_caption_tag =
// avoid recursion via nested_block_in_table
! <tableDataBlock>
p:pipe "+"
args:row_syntax_table_args?
tagEndPos:("" { return $this->endOffset(); })
c:nested_block_in_table* {
$tsr = new SourceRange( $this->startOffset(), $tagEndPos );
return TokenizerUtils::buildTableTokens(
'caption', '|+', $args, $tsr, $this->endOffset(), $c, true );
}
table_row_tag =
// avoid recursion via nested_block_in_table
! <tableDataBlock>
p:pipe dashes:$"-"+
a:(table_attributes<table=false> / &{ $this->unreachable(); })
tagEndPos:("" { return $this->endOffset(); })
s2:space*
{
$coms = TokenizerUtils::popComments( $a );
if ( $coms ) {
$tagEndPos = $coms['commentStartPos'];
}
$da = new DataParsoid;
$da->tsr = new SourceRange( $this->startOffset(), $tagEndPos );
$da->startTagSrc = $p . $dashes;
// We rely on our tree builder to close the row as needed. This is
// needed to support building tables from fragment templates with
// individual cells or rows.
$trToken = new TagTk( 'tr', $a, $da );
return array_merge( [ $trToken ], $coms ? $coms['buf'] : [], $s2 );
}
table_heading_tags = table_heading_tags_parameterized<&th>
table_heading_tags_parameterized =
"!"
thTag:table_heading_tag
thTags:ths {
// Avoid modifying a cached result
$thTag[0] = clone $thTag[0];
$da = $thTag[0]->dataParsoid = clone $thTag[0]->dataParsoid;
$da->tsr = clone $da->tsr;
$da->tsr->start--; // include "!"
array_unshift( $thTags, $thTag );
return $thTags;
}
table_heading_tag =
arg:row_syntax_table_args?
tagEndPos:("" { return $this->endOffset(); })
c:(
th:<&th>
d:nested_block_in_table {
// Ignore newlines found in transclusions!
// This is not perfect (since {{..}} may not always tokenize to transclusions).
if ( $th !== false && strpos( preg_replace( "/{{[\s\S]+?}}/", "", $this->text() ), "\n" ) !== false ) {
// There's been a newline. Remove the break and continue
// tokenizing nested_block_in_tables.
$th = false;
}
return $d;
}
)* {
$tsr = new SourceRange( $this->startOffset(), $tagEndPos );
return TokenizerUtils::buildTableTokens( 'th', '!', $arg,
$tsr, $this->endOffset(), $c );
}
ths = (
pp:("!!" / pipe_pipe)
tht:table_heading_tag {
// Avoid modifying cached dataParsoid object
$tht[0] = clone $tht[0];
$da = $tht[0]->dataParsoid = clone $tht[0]->dataParsoid;
$da->tsr = clone $da->tsr;
$da->stx = 'row';
$da->setTempFlag( TempData::NON_MERGEABLE_TABLE_CELL );
$da->tsr->start -= strlen( $pp ); // include "!!" or "||"
if ( $pp !== '!!' || ( isset( $da->startTagSrc ) && $da->startTagSrc !== $pp ) ) {
// Variation from default
$da->startTagSrc = $pp . ( isset( $da->startTagSrc ) ? substr( $da->startTagSrc, 1 ) : '' );
}
return $tht;
}
)*
table_data_tags =
// avoid recursion via nested_block_in_table
! <tableDataBlock>
p:pipe
![+-] td:table_data_tag
tds:tds {
// Avoid modifying a cached result
$td[0] = clone $td[0];
$da = $td[0]->dataParsoid = clone $td[0]->dataParsoid;
$da->tsr = clone $da->tsr;
$da->tsr->start -= strlen( $p ); // include "|"
if ( $p !== '|' ) {
// Variation from default
$da->startTagSrc = $p;
}
return array_merge( $td, $tds );
}
table_data_tag =
! "}"
arg:row_syntax_table_args?
// use inline_breaks to break on tr etc
tagEndPos:("" { return $this->endOffset(); })
td:nested_block_in_table*
{
$tsr = new SourceRange( $this->startOffset(), $tagEndPos );
return TokenizerUtils::buildTableTokens( 'td', '|', $arg,
$tsr, $this->endOffset(), $td );
}
tds = (
pp:pipe_pipe
tdt:table_data_tag {
// Avoid modifying cached dataParsoid object
$tdt[0] = clone $tdt[0];
$da = $tdt[0]->dataParsoid = clone $tdt[0]->dataParsoid;
$da->tsr = clone $da->tsr;
$da->stx = 'row';
$da->setTempFlag( TempData::NON_MERGEABLE_TABLE_CELL );
$da->tsr->start -= strlen( $pp ); // include "||"
if ( $pp !== '||' || ( isset( $da->startTagSrc ) && $da->startTagSrc !== $pp ) ) {
// Variation from default
$da->startTagSrc = $pp . ( isset( $da->startTagSrc ) ? substr( $da->startTagSrc, 1 ) : '' );
}
return $tdt;
}
)*
table_end_tag =
sc:(space / comment)*
startPos:("" { return $this->endOffset(); })
p:pipe
b:"}"
{
$dp = new DataParsoid;
$dp->tsr = new SourceRange( $startPos, $this->endOffset() );
$tblEnd = new EndTagTk( 'table', [], $dp );
if ( $p !== '|' ) {
// p+"<brace-char>" is triggering some bug in pegJS
// I cannot even use that expression in the comment!
$tblEnd->dataParsoid->endTagSrc = $p . $b;
}
array_push( $sc, $tblEnd );
return $sc;
}
/**
* Table parameters separated from the content by a single pipe. Does *not*
* match if followed by double pipe (row-based syntax).
*/
row_syntax_table_args =
as:table_attributes<tableCellArg> s:optional_spaces p:pipe !pipe {
return [ $as, $s, $p ];
}
/*******************************************************************
* Text variants and other general rules
*******************************************************************/
/* All chars that cannot start syntactic structures in the middle of a line
* XXX: ] and other end delimiters should probably only be activated inside
* structures to avoid unnecessarily leaving the text rule on plain
* content.
*
* TODO: Much of this is should really be context-dependent (syntactic
* flags). The wikilink_preprocessor_text rule is an example where
* text_char is not quite right and had to be augmented. Try to minimize /
* clarify this carefully!
*
* This character class is inlined into $this->reUrltextLookahead. Changes
* here may also need to be reflected there.
*/
text_char = [^-'<[{\n\r:;\]}|!=]
/* Legend
* ' quotes (italic/bold)
* < start of xmlish_tag
* [ start of links
* { start of parser functions, transclusion and template args
* \n all sort of block-level markup at start of line
* \r ditto
* A-Za-z autolinks (http(s), nttp(s), mailto, ISBN, PMID, RFC)
*
* _ behavior switches (e.g., '__NOTOC__') (XXX: not URL related)
* ! and | table cell delimiters, might be better to specialize those
* = headings - also specialize those!
*
* The following chars are also included for now, but only apply in some
* contexts and should probably be enabled only in those:
* : separate definition in ; term : definition
* ] end of link
* } end of parser func/transclusion/template arg
* - start of lang_variant -{ ... }-
* ; separator in lang_variant
*/
urltext =
(
/* Very special performance hack:
* Look for a plain text sequence, and if found, pretend to match the
* empty string, but then advance currPos in the action and return the
* whole plain text segment as a single result.
*/
& {
if ( preg_match( $this->reUrltextLookahead, $this->input, $m, 0, $this->currPos ) ) {
$plain = $m[1];
$this->urltextPlainSegment = $plain;
$this->urltextFoundAutolink = ( $m[2] ?? '' ) !== '';
return (bool)strlen( $plain );
} else {
$this->urltextFoundAutolink = false;
return false;
}
}
'' {
$this->currPos += strlen( $this->urltextPlainSegment );
return $this->urltextPlainSegment;
}
/ & { return $this->urltextFoundAutolink; } al:autolink { return $al; }
/ & "&" he:htmlentity { return $he; }
/ & ('__') bs:behavior_switch { return $bs; }
/ text_char
)+
raw_htmlentity =
m:$("&" [#0-9a-zA-Zרלמرلم]+ ";") {
return Utils::decodeWtEntities( $m );
}
htmlentity =
cc:raw_htmlentity {
// if this is an invalid entity, don't tag it with 'mw:Entity'
// note that some entities (like &acE;) decode to 2 codepoints!
if ( mb_strlen( $cc ) > 2 /* decoded entity would be 1-2 codepoints */ ) {
return $cc;
}
$dpStart = new DataParsoid;
$dpStart->src = $this->text();
$dpStart->srcContent = $cc;
$dpStart->tsr = $this->tsrOffsets( 'start' );
$dpEnd = new DataParsoid;
$dpEnd->tsr = $this->tsrOffsets( 'end' );
return [
// If this changes, the nowiki extension's toDOM will need to follow suit
new TagTk( 'span', [ new KV( 'typeof', 'mw:Entity' ) ], $dpStart ),
$cc,
new EndTagTk( 'span', [], $dpEnd )
];
}
/**
* noinclude / includeonly / onlyinclude rules. These are normally
* handled by the xmlish_tag rule, except where generic tags are not
* allowed- for example in directives, which are allowed in various attribute
* names and -values.
*
* Example test case:
* {|
* |-<includeonly>
* foo
* </includeonly>
* |Hello
* |}
*/
include_limits =
& ("<" "/"? ( "includeonly"i / "noinclude"i / "onlyinclude"i ) )
"<" il:xmlish_tag<annOrExtTag>
sol_il: <sol_il>
& {
$il = $il[0];
$lname = mb_strtolower( $il->getName() );
if ( !WTUtils::isIncludeTag( $lname ) ) { return false; }
// Preserve SOL where necessary (for onlyinclude and noinclude)
// Note that this only works because we encounter <*include*> tags in
// the toplevel content and we rely on the php preprocessor to expand
// templates, so we shouldn't ever be tokenizing inInclude.
// Last line should be empty (except for comments)
if ( $lname !== 'includeonly' && $sol_il && $il instanceof TagTk ) {
$dp = $il->dataParsoid;
$inclContent = $dp->extTagOffsets->stripTags( $dp->src );
$nlpos = strrpos( $inclContent, "\n" );
$last = $nlpos === false ? $inclContent : substr( $inclContent, $nlpos + 1 );
if ( !preg_match( '/^(<!--([^-]|-(?!->))*-->)*$/D', $last ) ) {
return false;
}
}
return true;
}
{
return $il;
}
// 'Preprocessor' directive- higher-level things that can occur in otherwise
// plain-text content.
directive =
comment
/ extension_annotation_tag
/ tplarg_or_template
/ & "-{" v:lang_variant_or_tpl { return $v; }
/ & "&" e:htmlentity { return $e; }
/ include_limits
wikilink_preprocessor_text =
r:(
t:$[^<[{\n\r\t|!\]}{ &\-]+
// XXX gwicke: any more chars we need to allow here?
/ !inline_breaks wr:( directive / $( !"]]" ( text_char / [!<\-\}\]\n\r] ) ) )
{ return $wr; }
)+ {
return TokenizerUtils::flattenStringlist( $r );
}
// added special separator character class inline: separates url from
// description / text
extlink_nonipv6url =
// Prevent breaking on pipes when we're in a link description.
// See the test, 'Images with the "|" character in the comment'.
extlink_nonipv6url_parameterized<linkdesc=false>
extlink_nonipv6url_parameterized =
r:(
$[^<[{\n\r|!\]}\-\t&="' \u00A0\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]+
/ !inline_breaks s:( directive / [&|{\-!}=] ) { return $s; }
/ $(['] ![']) // single quotes are ok, double quotes are bad
)+ {
return TokenizerUtils::flattenString( $r );
}
// Attribute values with preprocessor support
// n.b. / is a permissible char in the three rules below.
// We only break on />, enforced by the negated expression.
// Hence, it isn't included in the stop set.
// The stop set is space_or_newline and > which matches generic_att_value.
attribute_preprocessor_text =
r:(
$[^{}&<\-|/ \t\n\r\x0c>]+
/ !inline_breaks
!'/>'
s:( directive / less_than / [{}&\-|/] ) { return $s; }
)+ {
return TokenizerUtils::flattenString( $r );
}
// The stop set is '> which matches generic_att_value.
attribute_preprocessor_text_single =
r:(
$[^{}&<\-|/'>]+
/ !inline_breaks
!'/>'
s:( directive / less_than / [{}&\-|/] ) { return $s; }
)* {
return TokenizerUtils::flattenString( $r );
}
// The stop set is "> which matches generic_att_value.
attribute_preprocessor_text_double =
r:(
$[^{}&<\-|/">]+
/ !inline_breaks
!'/>'
s:( directive / less_than / [{}&\-|/] ) { return $s; }
)* {
return TokenizerUtils::flattenString( $r );
}
// Variants with the entire attribute on a single line
// n.b. ! is a permissible char in the three rules below.
// We only break on !! in th, enforced by the inline break.
// Hence, it isn't included in the stop set.
// [ is also permissible but we give a chance to break
// for the [[ special case in the old parser's doTableStuff (See T2553).
// The stop set is space_or_newline and | which matches table_att_value.
table_attribute_preprocessor_text =
r:(
$[^{}&<\-!\[ \t\n\r\x0c|]+
/ !inline_breaks s:( directive / [{}&<\-!\[] ) { return $s; }
)+ {
return TokenizerUtils::flattenString( $r );
}
// The stop set is '\r\n| which matches table_att_value.
table_attribute_preprocessor_text_single =
r:(
$[^{}&<\-!\['\r\n|]+
/ !inline_breaks s:( directive / [{}&<\-!\[] ) { return $s; }
)* {
return TokenizerUtils::flattenString( $r );
}
// The stop set is "\r\n| which matches table_att_value.
table_attribute_preprocessor_text_double =
r:(
$[^{}&<\-!\["\r\n|]+
/ !inline_breaks s:( directive / [{}&<\-!\[] ) { return $s; }
)* {
return TokenizerUtils::flattenString( $r );
}
// Special-case support for those pipe templates
pipe = "|" / "{{!}}"
// SSS FIXME: what about |{{!}} and {{!}}|
pipe_pipe = "||" / "{{!}}{{!}}"
space = [ \t]
optional_spaces = $[ \t]*
// Start of file
sof = & { return $this->endOffset() === 0 && !$this->pipelineOffset; }
// End of file
eof = & { return $this->endOffset() === $this->inputLength; }
newline = '\n' / '\r\n'
newlineToken = newline { return [ new NlTk( $this->tsrOffsets() ) ]; }
eolf = newline / eof
// The old parser does a straight str.replace(/<!--((?!-->).)*-->/g, "")
// but, as always, things around here are a little more complicated.
//
// We accept the same comments, but because we emit them as HTML comments
// instead of deleting them, we have to encode the data to ensure that
// we always emit a valid HTML5 comment. See the encodeComment helper
// for further details.
comment =
'<!--' c:$(!"-->" .)* cEnd:$('-->' / eof) {
$data = WTUtils::encodeComment( $c );
$dp = new DataParsoid;
$dp->tsr = $this->tsrOffsets();
if ( $cEnd !== '-->' ) {
$dp->unclosedComment = true;
}
return [ new CommentTk( $data, $dp ) ];
}
nl_comment_space = newlineToken / space / comment
optionalSpaceToken =
s:optional_spaces {
if ( $s !== '' ) {
return [ $s ];
} else {
return [];
}
}
/* This rule corresponds to \s in the PHP preg_* functions,
* which is used frequently in the old parser. The inclusion of
* form feed (but not other whitespace, like vertical tab) is a quirk
* of Perl, which PHP inherited via the PCRE (Perl-Compatible Regular
* Expressions) library.
*/
space_or_newline = [ \t\n\r\x0c]
/* This rule corresponds to \b in the PHP preg_* functions,
* after a word character. That is, it's a zero-width lookahead that
* the next character is not a word character.
*/
end_of_word = eof / ![A-Za-z0-9_]
// Unicode "separator, space" category. It covers the \u0020 space as well
// as \u3000 IDEOGRAPHIC SPACE (see bug 19052). In PHP this is \p{Zs}.
// Keep this up-to-date with the characters tagged ;Zs; in
// http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
unispace = [ \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]
// Non-newline whitespace, including non-breaking spaces. Used for magic links.
space_or_nbsp =
space // includes \t
/ unispace
/ & "&" he:htmlentity &{ return is_array( $he ) && $he[ 1 ] === "\u{A0}"; }
{ return $he; }
// Used within ISBN magic links
space_or_nbsp_or_dash =
space_or_nbsp / "-"
// Elements that do not break beginning or end of line for blocks (headers for instance)
sol_transparent = comment / include_limits<sol_il> / annotation_tag / behavior_switch
sol = (empty_line_with_comments / sol_prefix) sol_transparent*
sol_prefix =
newlineToken
/ & {
// Use the sol flag only at the start of the input
// Flag should always be an actual boolean (not falsy or undefined)
$this->assert( is_bool( $this->options['sol'] ), 'sol should be boolean' );
return $this->endOffset() === 0 && $this->options['sol'];
} {
return [];
}
// This rule requires at least one comment to be matched
empty_line_with_comments =
sp:sol_prefix p:("" { return $this->endOffset(); }) c:(space* comment (space / comment)* newline)+ {
$dp = new DataParsoid;
$dp->tsr = new SourceRange( $p, $this->endOffset() );
$dp->tokens = TokenizerUtils::flattenIfArray( $c );
return [
$sp,
new SelfclosingTagTk( 'meta', [ new KV( 'typeof', 'mw:EmptyLine' ) ], $dp )
];
}

File Metadata

Mime Type
text/plain
Expires
Sat, Oct 3, 03:51 (3 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d1/70/6b67d93ca2dcd58ee80245e248be
Default Alt Text
Grammar.pegphp (80 KB)

Event Timeline