Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserPipelineFactory
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 10
812
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 defaultOptions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 procNamesToProcs
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 makePipeline
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 getCacheKey
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 parse
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 selectiveDOMUpdate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getPipeline
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 returnPipeline
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html;
5
6use Wikimedia\Assert\Assert;
7use Wikimedia\Parsoid\Config\Env;
8use Wikimedia\Parsoid\Core\InternalException;
9use Wikimedia\Parsoid\Core\SelectiveUpdateData;
10use Wikimedia\Parsoid\DOM\Document;
11use Wikimedia\Parsoid\Utils\PHPUtils;
12use Wikimedia\Parsoid\Utils\Utils;
13use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\AddAnnotationIds;
14use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\AddLinkAttributes;
15use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\CleanUp;
16use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\DedupeStyles;
17use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\DisplaySpace;
18use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\HandleLinkNeighbours;
19use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\Headings;
20use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\LiFixups;
21use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\TableFixups;
22use Wikimedia\Parsoid\Wt2Html\DOM\Handlers\UnpackDOMFragments;
23use Wikimedia\Parsoid\Wt2Html\DOM\Processors\AddMediaInfo;
24use Wikimedia\Parsoid\Wt2Html\DOM\Processors\AddMetaData;
25use Wikimedia\Parsoid\Wt2Html\DOM\Processors\AddRedLinks;
26use Wikimedia\Parsoid\Wt2Html\DOM\Processors\ComputeDSR;
27use Wikimedia\Parsoid\Wt2Html\DOM\Processors\ConvertOffsets;
28use Wikimedia\Parsoid\Wt2Html\DOM\Processors\LangConverter;
29use Wikimedia\Parsoid\Wt2Html\DOM\Processors\Linter;
30use Wikimedia\Parsoid\Wt2Html\DOM\Processors\MarkFosteredContent;
31use Wikimedia\Parsoid\Wt2Html\DOM\Processors\MigrateTemplateMarkerMetas;
32use Wikimedia\Parsoid\Wt2Html\DOM\Processors\MigrateTrailingNLs;
33use Wikimedia\Parsoid\Wt2Html\DOM\Processors\Normalize;
34use Wikimedia\Parsoid\Wt2Html\DOM\Processors\ProcessEmbeddedDocs;
35use Wikimedia\Parsoid\Wt2Html\DOM\Processors\ProcessTreeBuilderFixups;
36use Wikimedia\Parsoid\Wt2Html\DOM\Processors\PWrap;
37use Wikimedia\Parsoid\Wt2Html\DOM\Processors\RunExtensionProcessors;
38use Wikimedia\Parsoid\Wt2Html\DOM\Processors\UpdateTemplateOutput;
39use Wikimedia\Parsoid\Wt2Html\DOM\Processors\WrapAnnotations;
40use Wikimedia\Parsoid\Wt2Html\DOM\Processors\WrapSections;
41use Wikimedia\Parsoid\Wt2Html\DOM\Processors\WrapTemplates;
42use Wikimedia\Parsoid\Wt2Html\TreeBuilder\TreeBuilderStage;
43use Wikimedia\Parsoid\Wt2Html\TT\AttributeExpander;
44use Wikimedia\Parsoid\Wt2Html\TT\BehaviorSwitchHandler;
45use Wikimedia\Parsoid\Wt2Html\TT\DOMFragmentBuilder;
46use Wikimedia\Parsoid\Wt2Html\TT\ExtensionHandler;
47use Wikimedia\Parsoid\Wt2Html\TT\ExternalLinkHandler;
48use Wikimedia\Parsoid\Wt2Html\TT\IncludeOnly;
49use Wikimedia\Parsoid\Wt2Html\TT\LanguageVariantHandler;
50use Wikimedia\Parsoid\Wt2Html\TT\ListHandler;
51use Wikimedia\Parsoid\Wt2Html\TT\NoInclude;
52use Wikimedia\Parsoid\Wt2Html\TT\OnlyInclude;
53use Wikimedia\Parsoid\Wt2Html\TT\ParagraphWrapper;
54use Wikimedia\Parsoid\Wt2Html\TT\PreHandler;
55use Wikimedia\Parsoid\Wt2Html\TT\QuoteTransformer;
56use Wikimedia\Parsoid\Wt2Html\TT\SanitizerHandler;
57use Wikimedia\Parsoid\Wt2Html\TT\TemplateHandler;
58use Wikimedia\Parsoid\Wt2Html\TT\TokenStreamPatcher;
59use Wikimedia\Parsoid\Wt2Html\TT\WikiLinkHandler;
60
61/**
62 * This class assembles parser pipelines from parser stages
63 */
64class ParserPipelineFactory {
65    private static $initialized = false;
66    private static $globalPipelineId = 0;
67
68    public static function init(): void {
69        if ( self::$initialized ) {
70            return;
71        }
72
73        self::$stages["FullParseDOMTransform"]["processors"] =
74            array_merge(
75                self::NESTED_PIPELINE_DOM_TRANSFORMS,
76                self::FULL_PARSE_GLOBAL_DOM_TRANSFORMS
77            );
78
79        self::$stages["SelectiveUpdateFragmentDOMTransform"]["processors"] =
80            array_merge(
81                self::NESTED_PIPELINE_DOM_TRANSFORMS,
82                self::SELECTIVE_UPDATE_FRAGMENT_GLOBAL_DOM_TRANSFORMS
83            );
84
85        self::$initialized = true;
86    }
87
88    private const DOM_PROCESSOR_CONFIG = [
89        'addmetadata' => AddMetaData::class,
90        'annwrap' => WrapAnnotations::class,
91        'convertoffsets' => ConvertOffsets::class,
92        'dsr' => ComputeDSR::class,
93        'embedded-docs' => ProcessEmbeddedDocs::class,
94        'extpp' => RunExtensionProcessors::class,
95        'fostered' => MarkFosteredContent::class,
96        'linter' => Linter::class,
97        'lang-converter' => LangConverter::class,
98        'media' => AddMediaInfo::class,
99        'migrate-metas' => MigrateTemplateMarkerMetas::class,
100        'migrate-nls' => MigrateTrailingNLs::class,
101        'normalize' => Normalize::class,
102        'process-fixups' => ProcessTreeBuilderFixups::class,
103        'pwrap' => PWrap::class,
104        'redlinks' => AddRedLinks::class,
105        'sections' => WrapSections::class, // Don't process HTML in embedded attributes
106        'tplwrap' => WrapTemplates::class,
107        'update-template' => UpdateTemplateOutput::class,
108        'ann-ids' => [
109            'name' => 'AddAnnotationIds',
110            'handlers' => [
111                [ 'nodeName' => 'meta', 'action' => [ AddAnnotationIds::class, 'handler' ] ]
112            ],
113            'withAnnotations' => true
114        ],
115        'linkneighbours+dom-unpack' => [
116            'name' => 'HandleLinkNeighbours,UnpackDOMFragments',
117            'handlers' => [
118                // Link prefixes and suffixes
119                [ 'nodeName' => 'a', 'action' => [ HandleLinkNeighbours::class, 'handler' ] ],
120                [ 'nodeName' => null, 'action' => [ UnpackDOMFragments::class, 'handler' ] ]
121            ]
122        ],
123        'fixups' => [
124            'name' => 'MigrateTrailingCategories,TableFixups',
125            'tplInfo' => true,
126            'handlers' => [
127                // 1. Move trailing categories in <li>s out of the list
128                [ 'nodeName' => 'li', 'action' => [ LiFixups::class, 'migrateTrailingSolTransparentLinks' ] ],
129                [ 'nodeName' => 'dt', 'action' => [ LiFixups::class, 'migrateTrailingSolTransparentLinks' ] ],
130                [ 'nodeName' => 'dd', 'action' => [ LiFixups::class, 'migrateTrailingSolTransparentLinks' ] ],
131                // 2. Fix up issues from templated table cells and table cell attributes
132                [ 'nodeName' => 'td', 'action' => [ TableFixups::class, 'handleTableCellTemplates' ] ],
133                [ 'nodeName' => 'th', 'action' => [ TableFixups::class, 'handleTableCellTemplates' ] ],
134            ]
135        ],
136        'fixups+dedupe-styles' => [
137            'name' => 'MigrateTrailingCategories,TableFixups,DedupeStyles',
138            'tplInfo' => true,
139            'handlers' => [
140                // 1. Move trailing categories in <li>s out of the list
141                [ 'nodeName' => 'li', 'action' => [ LiFixups::class, 'migrateTrailingSolTransparentLinks' ] ],
142                [ 'nodeName' => 'dt', 'action' => [ LiFixups::class, 'migrateTrailingSolTransparentLinks' ] ],
143                [ 'nodeName' => 'dd', 'action' => [ LiFixups::class, 'migrateTrailingSolTransparentLinks' ] ],
144                // 2. Fix up issues from templated table cells and table cell attributes
145                [ 'nodeName' => 'td', 'action' => [ TableFixups::class, 'handleTableCellTemplates' ] ],
146                [ 'nodeName' => 'th', 'action' => [ TableFixups::class, 'handleTableCellTemplates' ] ],
147                // 3. Deduplicate template styles
148                // (should run after dom-fragment expansion + after extension post-processors)
149                [ 'nodeName' => 'style', 'action' => [ DedupeStyles::class, 'dedupe' ] ]
150            ]
151        ],
152        // Strip marker metas -- removes left over marker metas (ex: metas
153        // nested in expanded tpl/extension output).
154        'strip-metas' => [
155            'name' => 'CleanUp-stripMarkerMetas',
156            'handlers' => [
157                [ 'nodeName' => 'meta', 'action' => [ CleanUp::class, 'stripMarkerMetas' ] ]
158            ]
159        ],
160        'displayspace+linkclasses' => [
161            'name' => 'DisplaySpace+AddLinkAttributes',
162            'handlers' => [
163                [ 'nodeName' => null, 'action' => [ DisplaySpace::class, 'leftHandler' ] ],
164                [ 'nodeName' => null, 'action' => [ DisplaySpace::class, 'rightHandler' ] ],
165                [ 'nodeName' => 'a', 'action' => [ AddLinkAttributes::class, 'handler' ] ]
166            ]
167        ],
168        'gen-anchors' => [
169            'name' => 'Headings-genAnchors',
170            'handlers' => [
171                [ 'nodeName' => null, 'action' => [ Headings::class, 'genAnchors' ] ],
172            ]
173        ],
174        'dedupe-heading-ids' => [
175            'name' => 'Headings-dedupeIds',
176            'handlers' => [
177                [ 'nodeName' => null, 'action' => [ Headings::class, 'dedupeHeadingIds' ] ]
178            ]
179        ],
180        'heading-ids' => [
181            'name' => 'Headings-genAnchors',
182            'handlers' => [
183                [ 'nodeName' => null, 'action' => [ Headings::class, 'genAnchors' ] ],
184                [ 'nodeName' => null, 'action' => [ Headings::class, 'dedupeHeadingIds' ] ]
185            ]
186        ],
187        'cleanup' => [
188            'name' => 'CleanUp-handleEmptyElts,CleanUp-cleanup',
189            'tplInfo' => true,
190            'handlers' => [
191                // Strip empty elements from template content
192                [ 'nodeName' => null, 'action' => [ CleanUp::class, 'handleEmptyElements' ] ],
193                // Additional cleanup
194                [ 'nodeName' => null, 'action' => [ CleanUp::class, 'finalCleanup' ] ]
195            ]
196        ],
197        'saveDP' => [
198            'name' => 'CleanUp-saveDataParsoid',
199            'tplInfo' => true,
200            'handlers' => [
201                // Save data.parsoid into data-parsoid html attribute.
202                // Make this its own thing so that any changes to the DOM
203                // don't affect other handlers that run alongside it.
204                [ 'nodeName' => null, 'action' => [ CleanUp::class, 'saveDataParsoid' ] ]
205            ]
206        ]
207    ];
208
209    // NOTES about ordering / inclusion:
210    //
211    // media:
212    //    This is run at all levels for now - gallery extension's "packed" mode
213    //    would otherwise need a post-processing pass to scale media after it
214    //    has been fetched. That introduces an ordering dependency that may
215    //    or may not complicate things.
216    // migrate-metas:
217    //    - Run this after 'pwrap' because it can add additional opportunities for
218    //      meta migration which we will miss if we run this before p-wrapping.
219    //    - We could potentially move this just before 'tplwrap' by seeing this
220    //      as a preprocessing pass for that. But, we will have to update the pass
221    //      to update DSR properties where required.
222    //    - In summary, this can at most be moved before 'media' or after
223    //      'migrate-nls' without needing any other changes.
224    // dsr, tplwrap:
225    //    DSR computation and template wrapping cannot be skipped for top-level content
226    //    even if they are part of nested level pipelines, because such content might be
227    //    embedded in attributes and they may need to be processed independently.
228    //
229    // Nested (non-top-level) pipelines can never include the following:
230    // - lang-converter, convertoffsets, dedupe-styles, cleanup, saveDP
231    //
232    // FIXME: Perhaps introduce a config flag in the processor config that
233    // verifies this property against a pipeline's 'toplevel' state.
234    public const NESTED_PIPELINE_DOM_TRANSFORMS = [
235        'fostered', 'process-fixups', 'normalize', 'pwrap',
236        'media', 'migrate-metas', 'migrate-nls', 'dsr', 'tplwrap',
237        'ann-ids', 'annwrap', 'linkneighbours+dom-unpack'
238    ];
239
240    // NOTES about ordering:
241    // lang-converter, redlinks:
242    //    Language conversion and redlink marking are done here
243    //    *before* we cleanup and save data-parsoid because they
244    //    are also used in pb2pb/html2html passes, and we want to
245    //    keep their input/output formats consistent.
246    public const FULL_PARSE_GLOBAL_DOM_TRANSFORMS = [
247        // FIXME: It should be documented in the spec that an extension's
248        // wtDOMProcess handler is run once on the top level document.
249        'extpp',
250        'fixups+dedupe-styles', 'linter', 'strip-metas',
251        'lang-converter', 'redlinks', 'displayspace+linkclasses',
252        // Benefits from running after determining which media are redlinks
253        'heading-ids',
254        'sections', 'convertoffsets', 'cleanup',
255        'embedded-docs',
256        'saveDP', 'addmetadata'
257    ];
258
259    // Skipping sections, addmetadata from the above pipeline
260    //
261    // FIXME: Skip extpp, linter, lang-converter, redlinks, heading-ids, convertoffsets, saveDP for now.
262    // This replicates behavior prior to this refactor.
263    public const FULL_PARSE_EMBEDDED_DOC_DOM_TRANSFORMS = [
264        'fixups+dedupe-styles', 'strip-metas',
265        'displayspace+linkclasses',
266        'cleanup',
267        // Need to run this recursively
268        'embedded-docs',
269        // FIXME This means the data-* from embedded HTML fragments won't end up
270        // in the pagebundle. But, if we try to call this on those fragments,
271        // we get multiple calls to store embedded docs. So, we may need to
272        // write a custom traverser if we want these embedded data* objects
273        // in the pagebundle (this is not a regression since they weren't part
274        // of the pagebundle all this while anyway.)
275        /* 'saveDP' */
276    ];
277
278    public const SELECTIVE_UPDATE_FRAGMENT_GLOBAL_DOM_TRANSFORMS = [
279        'extpp', // FIXME: this should be a different processor
280        'fixups', 'strip-metas', 'redlinks', 'displayspace+linkclasses',
281        'gen-anchors', 'convertoffsets', 'cleanup',
282        // FIXME: This will probably need some special-case code to first
283        // strip old metadata before adding fresh metadata.
284        'addmetadata'
285    ];
286
287    public const SELECTIVE_UPDATE_GLOBAL_DOM_TRANSFORMS = [
288        'update-template', 'linter', 'lang-converter', /* FIXME: Are lang converters idempotent? */
289        'heading-ids', 'sections', 'saveDP'
290    ];
291
292    private static $stages = [
293        "Tokenizer" => [
294            "class" => PegTokenizer::class,
295        ],
296        "TokenTransform2" => [
297            "class" => TokenTransformManager::class,
298            "transformers" => [
299                OnlyInclude::class,
300                IncludeOnly::class,
301                NoInclude::class,
302
303                TemplateHandler::class,
304                ExtensionHandler::class,
305
306                // Expand attributes after templates to avoid expanding unused branches.
307                // No expansion of quotes, paragraphs etc in attributes,
308                // as with the legacy parser - up to end of TokenTransform2.
309                AttributeExpander::class,
310
311                // now all attributes expanded to tokens or string
312                // more convenient after attribute expansion
313                WikiLinkHandler::class,
314                ExternalLinkHandler::class,
315                LanguageVariantHandler::class,
316
317                // This converts dom-fragment-token tokens all the way to DOM
318                // and wraps them in DOMFragment wrapper tokens which will then
319                // get unpacked into the DOM by a dom-fragment unpacker.
320                DOMFragmentBuilder::class
321            ],
322        ],
323        "TokenTransform3" => [
324            "class" => TokenTransformManager::class,
325            "transformers" => [
326                TokenStreamPatcher::class,
327                // add <pre>s
328                PreHandler::class,
329                QuoteTransformer::class,
330                // add before transforms that depend on behavior switches
331                // examples: toc generation, edit sections
332                BehaviorSwitchHandler::class,
333
334                ListHandler::class,
335                SanitizerHandler::class,
336                // Wrap tokens into paragraphs post-sanitization so that
337                // tags that converted to text by the sanitizer have a chance
338                // of getting wrapped into paragraphs.  The sanitizer does not
339                // require the existence of p-tags for its functioning.
340                ParagraphWrapper::class
341            ],
342        ],
343        // Build a tree out of the fully processed token stream
344        "TreeBuilder" => [
345            "class" => TreeBuilderStage::class,
346        ],
347        // DOM transformer for top-level documents.
348        // This performs a lot of post-processing of the DOM
349        // (Template wrapping, broken wikitext/html detection, etc.)
350        "FullParseDOMTransform" => [
351            "class" => DOMPostProcessor::class,
352            "processors" => null // Will be initialized in an init function()
353        ],
354        // DOM transformer for fragments of a top-level document
355        "NestedFragmentDOMTransform" => [
356            "class" => DOMPostProcessor::class,
357            "processors" => self::NESTED_PIPELINE_DOM_TRANSFORMS
358        ],
359        // DOM transformations to run on attribute-embedded docs of the top level doc
360        "FullParseEmbeddedDocsDOMTransform" => [
361            "class" => DOMPostProcessor::class,
362            "processors" => self::FULL_PARSE_EMBEDDED_DOC_DOM_TRANSFORMS
363        ],
364        // DOM transformer for fragments during selective updates.
365        // This may eventually become identical to NestedFrgmentDOMTransform,
366        // but at this time, it is unclear if that will materialize.
367        "SelectiveUpdateFragmentDOMTransform" => [
368            "class" => DOMPostProcessor::class,
369            "processors" => null // Will be initialized in an init function()
370        ],
371        // DOM transformer for the top-level page during selective updates.
372        "SelectiveUpdateDOMTransform" => [
373            // For use in the top-level of the selective-update pipeline
374            "class" => DOMPostProcessor::class,
375            "processors" => self::SELECTIVE_UPDATE_GLOBAL_DOM_TRANSFORMS
376        ]
377    ];
378
379    private static $pipelineRecipes = [
380        // This pipeline takes wikitext as input and emits a fully
381        // processed DOM as output. This is the pipeline used for
382        // all top-level documents.
383        "fullparse-wikitext-to-dom" => [
384            "alwaysToplevel" => true,
385            "outType" => "DOM",
386            "stages" => [
387                "Tokenizer", "TokenTransform2", "TokenTransform3", "TreeBuilder", "FullParseDOMTransform"
388            ]
389        ],
390
391        "fullparse-embedded-docs-dom-to-dom" => [
392            "alwaysToplevel" => true,
393            "outType" => "DOM",
394            "stages" => [ "FullParseEmbeddedDocsDOMTransform" ]
395        ],
396
397        // This pipeline takes a DOM and emits a fully processed DOM as output.
398        "selective-update-dom-to-dom" => [
399            "alwaysToplevel" => true,
400            "outType" => "DOM",
401            "stages" => [ "SelectiveUpdateDOMTransform" ]
402        ],
403
404        // This pipeline takes wikitext as input and emits a partially
405        // processed DOM as output. This is the pipeline used for processing
406        // page fragments to DOM in a selective page update context
407        // This is always toplevel because the wikitext being updated
408        // is found at the toplevel of the page.
409        "selective-update-fragment-wikitext-to-dom" => [
410            "alwaysToplevel" => true,
411            "outType" => "DOM",
412            "stages" => [
413                "Tokenizer", "TokenTransform2", "TokenTransform3", "TreeBuilder", "SelectiveUpdateFragmentDOMTransform"
414            ]
415        ],
416
417        // This pipeline takes wikitext as input and emits a fully
418        // processed DOM as output. This is the pipeline used for
419        // wikitext fragments of a top-level document that should be
420        // processed to a DOM fragment. This pipeline doesn't run all
421        // of the DOM transformations in the DOMTransform pipeline.
422        // We will like use a specialized DOMTransform stage here.
423        "wikitext-to-fragment" => [
424            // FIXME: This is known to be always *not* top-level
425            // We could use a different flag to lock these pipelines too.
426            "outType" => "DOM",
427            "stages" => [
428                "Tokenizer", "TokenTransform2", "TokenTransform3", "TreeBuilder", "NestedFragmentDOMTransform"
429            ]
430        ],
431
432        // This pipeline takes tokens from stage 2 and emits a DOM fragment
433        // as output - this runs the same DOM transforms as the 'wikitext-to-fragment'
434        // pipeline and will get a spcialized DOMTransform stage as above.
435        "expanded-tokens-to-fragment" => [
436            "outType" => "DOM",
437            "stages" => [ "TokenTransform3", "TreeBuilder", "NestedFragmentDOMTransform" ]
438        ],
439
440        // This pipeline takes wikitext as input and emits tokens that
441        // have had all templates, extensions, links, images processed
442        "wikitext-to-expanded-tokens" => [
443            "outType" => "Tokens",
444            "stages" => [ "Tokenizer", "TokenTransform2" ]
445        ],
446
447        // This pipeline takes tokens from the PEG tokenizer and emits
448        // tokens that have had all templates and extensions processed.
449        "peg-tokens-to-expanded-tokens" => [
450            "outType" => "Tokens",
451            "stages" => [ "TokenTransform2" ]
452        ]
453    ];
454
455    private static $supportedOptions = [
456        // If true, templates found in content will have its contents expanded
457        'expandTemplates',
458
459        // If true, indicates pipeline is processing the expanded content of a
460        // template or its arguments
461        'inTemplate',
462
463        // If true, indicates that we are in a <includeonly> context
464        // (in current usage, isInclude === inTemplate)
465        'isInclude',
466
467        // The extension tag that is being processed (Ex: ref, references)
468        // (in current usage, only used for native tag implementation)
469        'extTag',
470
471        // Extension-specific options
472        'extTagOpts',
473
474        // Content being parsed is used in an inline context
475        'inlineContext',
476
477        // Are we processing content of attributes?
478        // (in current usage, used for transcluded attr. keys/values)
479        'attrExpansion',
480    ];
481
482    private array $pipelineCache = [];
483
484    private Env $env;
485
486    public function __construct( Env $env ) {
487        $this->env = $env;
488    }
489
490    /**
491     * Default options processing
492     *
493     * @param array $options
494     * @return array
495     */
496    private function defaultOptions( array $options ): array {
497        // default: not in a template
498        $options['inTemplate'] ??= false;
499
500        // default: not an include context
501        $options['isInclude'] ??= false;
502
503        // default: wrap templates
504        $options['expandTemplates'] ??= true;
505
506        // Catch pipeline option typos
507        foreach ( $options as $k => $v ) {
508            Assert::invariant(
509                in_array( $k, self::$supportedOptions, true ),
510                'Invalid cacheKey option: ' . $k
511            );
512        }
513
514        return $options;
515    }
516
517    public static function procNamesToProcs( array $procNames ): array {
518        $processors = [];
519        foreach ( $procNames as $name ) {
520            $proc = self::DOM_PROCESSOR_CONFIG[$name];
521            if ( !is_array( $proc ) ) {
522                $proc = [
523                    'name' => Utils::stripNamespace( $proc ),
524                    'Processor' => $proc,
525                ];
526            }
527            $proc['shortcut'] = $name;
528            $processors[] = $proc;
529        }
530        return $processors;
531    }
532
533    /**
534     * Generic pipeline creation from the above recipes.
535     *
536     * @param string $type
537     * @param string $cacheKey
538     * @param array $options
539     * @return ParserPipeline
540     */
541    private function makePipeline(
542        string $type, string $cacheKey, array $options
543    ): ParserPipeline {
544        self::init();
545
546        if ( !isset( self::$pipelineRecipes[$type] ) ) {
547            throw new InternalException( 'Unsupported Pipeline: ' . $type );
548        }
549        $recipe = self::$pipelineRecipes[$type];
550        $pipeStages = [];
551        $prevStage = null;
552        $recipeStages = $recipe["stages"];
553
554        foreach ( $recipeStages as $stageId ) {
555            $stageData = self::$stages[$stageId];
556            // @phan-suppress-next-line PhanNonClassMethodCall,PhanTypeExpectedObjectOrClassName
557            $stage = new $stageData["class"]( $this->env, $options, $stageId, $prevStage );
558            if ( isset( $stageData["transformers"] ) ) {
559                foreach ( $stageData["transformers"] as $tName ) {
560                    $stage->addTransformer( new $tName( $stage, $options ) );
561                }
562            } elseif ( isset( $stageData["processors"] ) ) {
563                $stage->registerProcessors( self::procNamesToProcs( $stageData["processors"] ) );
564            }
565
566            $prevStage = $stage;
567            $pipeStages[] = $stage;
568        }
569
570        return new ParserPipeline(
571            $recipe['alwaysToplevel'] ?? false,
572            $type,
573            $recipe["outType"],
574            $cacheKey,
575            $pipeStages,
576            $this->env
577        );
578    }
579
580    private function getCacheKey( string $cacheKey, array $options ): string {
581        if ( empty( $options['isInclude'] ) ) {
582            $cacheKey .= '::noInclude';
583        }
584        if ( empty( $options['expandTemplates'] ) ) {
585            $cacheKey .= '::noExpand';
586        }
587        if ( !empty( $options['inlineContext'] ) ) {
588            $cacheKey .= '::inlineContext';
589        }
590        if ( !empty( $options['inTemplate'] ) ) {
591            $cacheKey .= '::inTemplate';
592        }
593        if ( !empty( $options['attrExpansion'] ) ) {
594            $cacheKey .= '::attrExpansion';
595        }
596        if ( isset( $options['extTag'] ) ) {
597            $cacheKey .= '::' . $options['extTag'];
598            // FIXME: This is not the best strategy. But, instead of
599            // premature complexity, let us see how extensions want to
600            // use this and then figure out what constraints are needed.
601            if ( isset( $options['extTagOpts'] ) ) {
602                $cacheKey .= '::' . PHPUtils::jsonEncode( $options['extTagOpts'] );
603            }
604        }
605        return $cacheKey;
606    }
607
608    public function parse( string $src ): Document {
609        $pipe = $this->getPipeline( 'fullparse-wikitext-to-dom' );
610        $pipe->init( [
611            'frame' => $this->env->topFrame,
612            'toFragment' => false,
613        ] );
614        // Top-level doc parsing always start in SOL state
615        return $pipe->parseChunkily( $src, [ 'sol' => true ] )->ownerDocument;
616    }
617
618    /**
619     * @param SelectiveUpdateData $selparData
620     * @param array $options Options for selective DOM update
621     * - mode: (string) One of "template", "section", "generic"
622     *         For now, defaults to 'template', if absent
623     */
624    public function selectiveDOMUpdate( SelectiveUpdateData $selparData, array $options = [] ): Document {
625        $pipe = $this->getPipeline( 'selective-update-dom-to-dom' );
626        $pipe->init( [
627            'frame' => $this->env->topFrame,
628            'toFragment' => false,
629        ] );
630        return $pipe->selectiveParse( $selparData, $options );
631    }
632
633    /**
634     * Get a pipeline of a given type.  Pipelines are cached as they are
635     * frequently created.
636     *
637     * @param string $type
638     * @param array $options These also determine the key under which the
639     *   pipeline is cached for reuse.
640     * @return ParserPipeline
641     */
642    public function getPipeline(
643        string $type, array $options = []
644    ): ParserPipeline {
645        $options = $this->defaultOptions( $options );
646        $cacheKey = $this->getCacheKey( $type, $options );
647
648        $this->pipelineCache[$cacheKey] ??= [];
649
650        if ( $this->pipelineCache[$cacheKey] ) {
651            $pipe = array_pop( $this->pipelineCache[$cacheKey] );
652        } else {
653            $pipe = $this->makePipeline( $type, $cacheKey, $options );
654        }
655
656        // Debugging aid: Assign unique id to the pipeline
657        $pipe->setPipelineId( self::$globalPipelineId++ );
658
659        return $pipe;
660    }
661
662    /**
663     * Callback called by a pipeline at the end of its processing. Returns the
664     * pipeline to the cache.
665     *
666     * @param ParserPipeline $pipe
667     */
668    public function returnPipeline( ParserPipeline $pipe ): void {
669        $cacheKey = $pipe->getCacheKey();
670        $this->pipelineCache[$cacheKey] ??= [];
671        if ( count( $this->pipelineCache[$cacheKey] ) < 100 ) {
672            $this->pipelineCache[$cacheKey][] = $pipe;
673        }
674    }
675}