Seditio Source
Root |
./othercms/phpBB3/vendor/s9e/text-formatter/src/Configurator/Helpers/TemplateModifier.php
<?php

/**
* @package   s9e\TextFormatter
* @copyright Copyright (c) 2010-2021 The s9e authors
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Helpers;

use
DOMAttr;
use
DOMDocument;
use
DOMText;
use
DOMXPath;

abstract class
TemplateModifier
{
   
/**
    * XSL namespace
    */
   
const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';

   
/**
    * Replace parts of a template that match given regexp
    *
    * Treats attribute values as plain text. Replacements within XPath expression is unsupported.
    * The callback must return an array with two elements. The first must be either of 'expression',
    * 'literal' or 'passthrough', and the second element depends on the first.
    *
    *  - 'expression' indicates that the replacement must be treated as an XPath expression such as
    *    '@foo', which must be passed as the second element.
    *
    *  - 'literal' indicates a literal (plain text) replacement, passed as its second element.
    *
    *  - 'passthrough' indicates that the replacement should the tag's content. It works differently
    *    whether it is inside an attribute's value or a text node. Within an attribute's value, the
    *    replacement will be the text content of the tag. Within a text node, the replacement
    *    becomes an <xsl:apply-templates/> node. A second optional argument can be passed to be used
    *    as its @select node-set.
    *
    * @param  string   $template Original template
    * @param  string   $regexp   Regexp for matching parts that need replacement
    * @param  callable $fn       Callback used to get the replacement
    * @return string             Processed template
    */
   
public static function replaceTokens($template, $regexp, $fn)
    {
       
$dom   = TemplateLoader::load($template);
       
$xpath = new DOMXPath($dom);
        foreach (
$xpath->query('//@*') as $attribute)
        {
           
self::replaceTokensInAttribute($attribute, $regexp, $fn);
        }
        foreach (
$xpath->query('//text()') as $node)
        {
           
self::replaceTokensInText($node, $regexp, $fn);
        }

        return
TemplateLoader::save($dom);
    }

   
/**
    * Create a node that implements given replacement strategy
    *
    * @param  DOMDocument $dom
    * @param  array       $replacement
    * @return DOMNode
    */
   
protected static function createReplacementNode(DOMDocument $dom, array $replacement)
    {
        if (
$replacement[0] === 'expression')
        {
           
$newNode = $dom->createElementNS(self::XMLNS_XSL, 'xsl:value-of');
           
$newNode->setAttribute('select', $replacement[1]);
        }
        elseif (
$replacement[0] === 'passthrough')
        {
           
$newNode = $dom->createElementNS(self::XMLNS_XSL, 'xsl:apply-templates');
            if (isset(
$replacement[1]))
            {
               
$newNode->setAttribute('select', $replacement[1]);
            }
        }
        else
        {
           
$newNode = $dom->createTextNode($replacement[1]);
        }

        return
$newNode;
    }

   
/**
    * Replace parts of an attribute that match given regexp
    *
    * @param  DOMAttr  $attribute Attribute
    * @param  string   $regexp    Regexp for matching parts that need replacement
    * @param  callable $fn        Callback used to get the replacement
    * @return void
    */
   
protected static function replaceTokensInAttribute(DOMAttr $attribute, $regexp, $fn)
    {
       
$attrValue = preg_replace_callback(
           
$regexp,
            function (
$m) use ($fn, $attribute)
            {
               
$replacement = $fn($m, $attribute);
                if (
$replacement[0] === 'expression' || $replacement[0] === 'passthrough')
                {
                   
// Use the node's text content as the default expression
                   
$replacement[] = '.';

                    return
'{' . $replacement[1] . '}';
                }
                else
                {
                    return
$replacement[1];
                }
            },
           
$attribute->value
       
);
       
$attribute->value = htmlspecialchars($attrValue, ENT_COMPAT, 'UTF-8');
    }

   
/**
    * Replace parts of a text node that match given regexp
    *
    * @param  DOMText  $node   Text node
    * @param  string   $regexp Regexp for matching parts that need replacement
    * @param  callable $fn     Callback used to get the replacement
    * @return void
    */
   
protected static function replaceTokensInText(DOMText $node, $regexp, $fn)
    {
       
// Grab the node's parent so that we can rebuild the text with added variables right
        // before the node, using DOM's insertBefore(). Technically, it would make more sense
        // to create a document fragment, append nodes then replace the node with the fragment
        // but it leads to namespace redeclarations, which looks ugly
       
$parentNode = $node->parentNode;
       
$dom        = $node->ownerDocument;

       
preg_match_all($regexp, $node->textContent, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
       
$lastPos = 0;
        foreach (
$matches as $m)
        {
           
$pos = $m[0][1];

           
// Catch-up to current position
           
$text = substr($node->textContent, $lastPos, $pos - $lastPos);
           
$parentNode->insertBefore($dom->createTextNode($text), $node);
           
$lastPos = $pos + strlen($m[0][0]);

           
// Get the replacement for this token
           
$replacement = $fn(array_column($m, 0), $node);
           
$newNode     = self::createReplacementNode($dom, $replacement);
           
$parentNode->insertBefore($newNode, $node);
        }

       
// Append the rest of the text
       
$text = substr($node->textContent, $lastPos);
       
$parentNode->insertBefore($dom->createTextNode($text), $node);

       
// Now remove the old text node
       
$parentNode->removeChild($node);
    }
}