Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
38 / 40
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
RemexCompatFormatter
95.00% covered (success)
95.00%
38 / 40
50.00% covered (danger)
50.00%
2 / 4
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 startDocument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 characters
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 element
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
14
1<?php
2
3namespace MediaWiki\Tidy;
4
5use MediaWiki\Parser\Sanitizer;
6use Wikimedia\RemexHtml\HTMLData;
7use Wikimedia\RemexHtml\Serializer\HtmlFormatter;
8use Wikimedia\RemexHtml\Serializer\SerializerNode;
9
10/**
11 * @internal
12 *
13 * WATCH OUT! Unlike normal HtmlFormatter, this class requires the 'ignoreCharRefs' option
14 * in Tokenizer to be used. If that option is not used, it will produce wrong results (T354361).
15 */
16class RemexCompatFormatter extends HtmlFormatter {
17    private const MARKED_EMPTY_ELEMENTS = [
18        'li' => true,
19        'p' => true,
20        'tr' => true,
21    ];
22
23    /** @var ?callable */
24    private $textProcessor;
25
26    public function __construct( $options = [] ) {
27        parent::__construct( $options );
28        // Escape non-breaking space
29        $this->attributeEscapes["\u{00A0}"] = '&#160;';
30        $this->textEscapes["\u{00A0}"] = '&#160;';
31        // Escape U+0338 (T387130)
32        $this->textEscapes["\u{0338}"] = '&#x338;';
33        // Disable escaping of '&', because we expect to see entities, due to 'ignoreCharRefs'
34        unset( $this->attributeEscapes["&"] );
35        unset( $this->textEscapes["&"] );
36        $this->textProcessor = $options['textProcessor'] ?? null;
37    }
38
39    public function startDocument( $fragmentNamespace, $fragmentName ) {
40        return '';
41    }
42
43    /**
44     * WATCH OUT! Unlike normal HtmlFormatter, this class expects that the $text argument contains
45     * unexpanded character references (entities), as a result of using the 'ignoreCharRefs' option
46     * in Tokenizer. If that option is not used, this method will produce wrong results (T354361).
47     *
48     * @inheritDoc
49     */
50    public function characters( SerializerNode $parent, $text, $start, $length ) {
51        $text = parent::characters( $parent, $text, $start, $length );
52
53        if ( $parent->namespace !== HTMLData::NS_HTML
54            || !isset( $this->rawTextElements[$parent->name] )
55        ) {
56            if ( $this->textProcessor !== null ) {
57                $text = ( $this->textProcessor )( $text );
58            }
59        }
60
61        // Ensure a consistent representation for all entities
62        $text = Sanitizer::normalizeCharReferences( $text );
63        return $text;
64    }
65
66    public function element( SerializerNode $parent, SerializerNode $node, $contents ) {
67        $data = $node->snData;
68        if ( $data && $data->isPWrapper ) {
69            if ( $data->nonblankNodeCount ) {
70                return "<p>$contents</p>";
71            } else {
72                return $contents;
73            }
74        }
75
76        $name = $node->name;
77        $attrs = $node->attrs;
78        if ( isset( self::MARKED_EMPTY_ELEMENTS[$name] ) && $attrs->count() === 0
79            && strspn( $contents, "\t\n\f\r " ) === strlen( $contents )
80        ) {
81            return "<{$name} class=\"mw-empty-elt\">$contents</{$name}>";
82        }
83
84        $s = "<$name";
85        foreach ( $attrs->getValues() as $attrName => $attrValue ) {
86            $encValue = strtr( $attrValue, $this->attributeEscapes );
87            $encValue = Sanitizer::normalizeCharReferences( $encValue );
88            $s .= " $attrName=\"$encValue\"";
89        }
90        if ( $node->namespace === HTMLData::NS_HTML && isset( $this->voidElements[$name] ) ) {
91            $s .= ' />';
92            return $s;
93        }
94
95        $s .= '>';
96        if ( $node->namespace === HTMLData::NS_HTML
97            && isset( $contents[0] ) && $contents[0] === "\n"
98            && isset( $this->prefixLfElements[$name] )
99        ) {
100            $s .= "\n$contents</$name>";
101        } else {
102            $s .= "$contents</$name>";
103        }
104        return $s;
105    }
106}