Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 80
ParserPipelineFactory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
930
0.00% covered (danger)
0.00%
0 / 80
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 defaultOptions
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 11
 makePipeline
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 22
 getCacheKey
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 15
 parse
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 14
 getPipeline
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 returnPipeline
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Wt2Html;
use Wikimedia\Assert\Assert;
use Wikimedia\Parsoid\Config\Env;
use Wikimedia\Parsoid\Core\InternalException;
use Wikimedia\Parsoid\DOM\Document;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Wt2Html\TreeBuilder\TreeBuilderStage;
use Wikimedia\Parsoid\Wt2Html\TT\AttributeExpander;
use Wikimedia\Parsoid\Wt2Html\TT\BehaviorSwitchHandler;
use Wikimedia\Parsoid\Wt2Html\TT\DOMFragmentBuilder;
use Wikimedia\Parsoid\Wt2Html\TT\ExtensionHandler;
use Wikimedia\Parsoid\Wt2Html\TT\ExternalLinkHandler;
use Wikimedia\Parsoid\Wt2Html\TT\IncludeOnly;
use Wikimedia\Parsoid\Wt2Html\TT\LanguageVariantHandler;
use Wikimedia\Parsoid\Wt2Html\TT\ListHandler;
use Wikimedia\Parsoid\Wt2Html\TT\NoInclude;
use Wikimedia\Parsoid\Wt2Html\TT\OnlyInclude;
use Wikimedia\Parsoid\Wt2Html\TT\ParagraphWrapper;
use Wikimedia\Parsoid\Wt2Html\TT\PreHandler;
use Wikimedia\Parsoid\Wt2Html\TT\QuoteTransformer;
use Wikimedia\Parsoid\Wt2Html\TT\SanitizerHandler;
use Wikimedia\Parsoid\Wt2Html\TT\TemplateHandler;
use Wikimedia\Parsoid\Wt2Html\TT\TokenStreamPatcher;
use Wikimedia\Parsoid\Wt2Html\TT\WikiLinkHandler;
/**
 * This class assembles parser pipelines from parser stages
 */
class ParserPipelineFactory {
    private static $globalPipelineId = 0;
    private static $stages = [
        "Tokenizer" => [
            "class" => PegTokenizer::class,
        ],
        "TokenTransform2" => [
            "class" => TokenTransformManager::class,
            "transformers" => [
                OnlyInclude::class,
                IncludeOnly::class,
                NoInclude::class,
                TemplateHandler::class,
                ExtensionHandler::class,
                // Expand attributes after templates to avoid expanding unused branches
                // No expansion of quotes, paragraphs etc in attributes, as in
                // PHP parser- up to text/x-mediawiki/expanded only.
                AttributeExpander::class,
                // now all attributes expanded to tokens or string
                // more convenient after attribute expansion
                WikiLinkHandler::class,
                ExternalLinkHandler::class,
                LanguageVariantHandler::class,
                // This converts dom-fragment-token tokens all the way to DOM
                // and wraps them in DOMFragment wrapper tokens which will then
                // get unpacked into the DOM by a dom-fragment unpacker.
                DOMFragmentBuilder::class
            ],
        ],
        "TokenTransform3" => [
            "class" => TokenTransformManager::class,
            "transformers" => [
                TokenStreamPatcher::class,
                // add <pre>s
                PreHandler::class,
                QuoteTransformer::class,
                // add before transforms that depend on behavior switches
                // examples: toc generation, edit sections
                BehaviorSwitchHandler::class,
                ListHandler::class,
                SanitizerHandler::class,
                // Wrap tokens into paragraphs post-sanitization so that
                // tags that converted to text by the sanitizer have a chance
                // of getting wrapped into paragraphs.  The sanitizer does not
                // require the existence of p-tags for its functioning.
                ParagraphWrapper::class
            ],
        ],
        "TreeBuilder" => [
            // Build a tree out of the fully processed token stream
            "class" => TreeBuilderStage::class,
        ],
        "DOMPP" => [
            // Generic DOM transformer.
            // This performs a lot of post-processing of the DOM
            // (Template wrapping, broken wikitext/html detection, etc.)
            "class" => DOMPostProcessor::class,
            "processors" => [],
        ],
    ];
    private static $pipelineRecipes = [
        // This pipeline takes wikitext as input and emits a fully
        // processed DOM as output. This is the pipeline used for
        // all top-level documents.
        // Stages 1-5 of the pipeline
        "text/x-mediawiki/full" => [
            "outType" => "DOM",
            "stages" => [
                "Tokenizer", "TokenTransform2", "TokenTransform3", "TreeBuilder", "DOMPP"
            ]
        ],
        // This pipeline takes wikitext as input and emits tokens that
        // have had all templates, extensions, links, images processed
        // Stages 1-2 of the pipeline
        "text/x-mediawiki" => [
            "outType" => "Tokens",
            "stages" => [ "Tokenizer", "TokenTransform2" ]
        ],
        // This pipeline takes tokens from the PEG tokenizer and emits
        // tokens that have had all templates and extensions processed.
        // Stage 2 of the pipeline
        "tokens/x-mediawiki" => [
            "outType" => "Tokens",
            "stages" => [ "TokenTransform2" ]
        ],
        // This pipeline takes tokens from stage 2 and emits a fully
        // processed DOM as output.
        // Stages 3-5 of the pipeline
        "tokens/x-mediawiki/expanded" => [
            "outType" => "DOM",
            "stages" => [ "TokenTransform3", "TreeBuilder", "DOMPP" ]
        ],
    ];
    private static $supportedOptions = [
        // If true, templates found in content will have its contents expanded
        'expandTemplates',
        // If true, indicates pipeline is processing the expanded content of a
        // template or its arguments
        'inTemplate',
        // If true, indicates that we are in a <includeonly> context
        // (in current usage, isInclude === inTemplate)
        'isInclude',
        // The extension tag that is being processed (Ex: ref, references)
        // (in current usage, only used for native tag implementation)
        'extTag',
        // Extension-specific options
        'extTagOpts',
        // Content being parsed is used in an inline context
        'inlineContext',
        // Are we processing content of attributes?
        // (in current usage, used for transcluded attr. keys/values)
        'attrExpansion'
    ];
    /** @var array */
    private $pipelineCache;
    /** @var Env */
    private $env;
    /**
     * @param Env $env
     */
    public function __construct( Env $env ) {
        $this->pipelineCache = [];
        $this->env = $env;
    }
    /**
     * Default options processing
     *
     * @param array $options
     * @return array
     */
    private function defaultOptions( array $options ): array {
        // default: not in a template
        if ( !isset( $options['inTemplate'] ) ) {
            $options['inTemplate'] = false;
        }
        // default: not an include context
        if ( !isset( $options['isInclude'] ) ) {
            $options['isInclude'] = false;
        }
        // default: wrap templates
        if ( !isset( $options['expandTemplates'] ) ) {
            $options['expandTemplates'] = true;
        }
        // Catch pipeline option typos
        foreach ( $options as $k => $v ) {
            Assert::invariant(
                in_array( $k, self::$supportedOptions, true ),
                'Invalid cacheKey option: ' . $k
            );
        }
        return $options;
    }
    /**
     * Generic pipeline creation from the above recipes.
     *
     * @param string $type
     * @param string $cacheKey
     * @param array $options
     * @return ParserPipeline
     */
    private function makePipeline(
        string $type, string $cacheKey, array $options
    ): ParserPipeline {
        if ( !isset( self::$pipelineRecipes[$type] ) ) {
            throw new InternalException( 'Unsupported Pipeline: ' . $type );
        }
        $recipe = self::$pipelineRecipes[$type];
        $pipeStages = [];
        $prevStage = null;
        $recipeStages = $recipe["stages"];
        foreach ( $recipeStages as $stageId ) {
            $stageData = self::$stages[$stageId];
            $stage = new $stageData["class"]( $this->env, $options, $stageId, $prevStage );
            if ( isset( $stageData["transformers"] ) ) {
                foreach ( $stageData["transformers"] as $tName ) {
                    $stage->addTransformer( new $tName( $stage, $options ) );
                }
            } elseif ( isset( $stageData["processors"] ) ) {
                $stage->registerProcessors( $stageData["processors"] );
            }
            $prevStage = $stage;
            $pipeStages[] = $stage;
        }
        return new ParserPipeline(
            $type,
            $recipe["outType"],
            $cacheKey,
            $pipeStages,
            $this->env
        );
    }
    /**
     * @param string $cacheKey
     * @param array $options
     * @return string
     */
    private function getCacheKey( string $cacheKey, array $options ): string {
        if ( empty( $options['isInclude'] ) ) {
            $cacheKey .= '::noInclude';
        }
        if ( empty( $options['expandTemplates'] ) ) {
            $cacheKey .= '::noExpand';
        }
        if ( !empty( $options['inlineContext'] ) ) {
            $cacheKey .= '::inlineContext';
        }
        if ( !empty( $options['inTemplate'] ) ) {
            $cacheKey .= '::inTemplate';
        }
        if ( !empty( $options['attrExpansion'] ) ) {
            $cacheKey .= '::attrExpansion';
        }
        if ( isset( $options['extTag'] ) ) {
            $cacheKey .= '::' . $options['extTag'];
            // FIXME: This is not the best strategy. But, instead of
            // premature complexity, let us see how extensions want to
            // use this and then figure out what constraints are needed.
            if ( isset( $options['extTagOpts'] ) ) {
                $cacheKey .= '::' . PHPUtils::jsonEncode( $options['extTagOpts'] );
            }
        }
        return $cacheKey;
    }
    /**
     * @param string $src
     * @return Document
     */
    public function parse( string $src ): Document {
        $pipe = $this->getPipeline( 'text/x-mediawiki/full' );
        $pipe->init( [
            'toplevel' => true,
            'frame' => $this->env->topFrame,
        ] );
        // Disable the garbage collector in PHP 7.2 (T230861)
        if ( gc_enabled() && version_compare( PHP_VERSION, '7.3.0', '<' ) ) {
            $gcDisabled = true;
            gc_collect_cycles();
            gc_disable();
        } else {
            $gcDisabled = false;
        }
        $result = $pipe->parseChunkily( $src, [
            'atTopLevel' => true,
            // Top-level doc parsing always start in SOL state
            'sol' => true,
        ] );
        if ( $gcDisabled ) {
            gc_enable();
            // There's no point running gc_collect_cycles() here, since objects
            // are not marked for collection while the GC is disabled. The root
            // buffer will be empty.
        }
        return $result->ownerDocument;
    }
    /**
     * Get a pipeline of a given type.  Pipelines are cached as they are
     * frequently created.
     *
     * @param string $type
     * @param array $options These also determine the key under which the
     *   pipeline is cached for reuse.
     * @return ParserPipeline
     */
    public function getPipeline(
        string $type, array $options = []
    ): ParserPipeline {
        $options = $this->defaultOptions( $options );
        $cacheKey = $this->getCacheKey( $type, $options );
        if ( empty( $this->pipelineCache[$cacheKey] ) ) {
            $this->pipelineCache[$cacheKey] = [];
        }
        if ( count( $this->pipelineCache[$cacheKey] ) ) {
            $pipe = array_pop( $this->pipelineCache[$cacheKey] );
        } else {
            $pipe = $this->makePipeline( $type, $cacheKey, $options );
        }
        // Debugging aid: Assign unique id to the pipeline
        $pipe->setPipelineId( self::$globalPipelineId++ );
        return $pipe;
    }
    /**
     * Callback called by a pipeline at the end of its processing. Returns the
     * pipeline to the cache.
     *
     * @param ParserPipeline $pipe
     */
    public function returnPipeline( ParserPipeline $pipe ): void {
        $cacheKey = $pipe->getCacheKey();
        if ( empty( $this->pipelineCache[$cacheKey] ) ) {
            $this->pipelineCache[$cacheKey] = [];
        }
        if ( count( $this->pipelineCache[$cacheKey] ) < 100 ) {
            $this->pipelineCache[$cacheKey][] = $pipe;
        }
    }
}