Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
CounterStyleAtRuleSanitizer
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
3 / 3
9
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
1 / 1
1
 handlesRule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 doSanitize
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\CSS\Sanitizer;
5
6use Wikimedia\CSS\Grammar\Alternative;
7use Wikimedia\CSS\Grammar\Juxtaposition;
8use Wikimedia\CSS\Grammar\KeywordMatcher;
9use Wikimedia\CSS\Grammar\Matcher;
10use Wikimedia\CSS\Grammar\MatcherFactory;
11use Wikimedia\CSS\Grammar\Quantifier;
12use Wikimedia\CSS\Grammar\UnorderedGroup;
13use Wikimedia\CSS\Objects\AtRule;
14use Wikimedia\CSS\Objects\CSSObject;
15use Wikimedia\CSS\Objects\Rule;
16use Wikimedia\CSS\Util;
17
18/**
19 * Sanitizes a \@counter-style rule
20 * @see https://www.w3.org/TR/2021/CR-css-counter-styles-3-20210727/
21 */
22class CounterStyleAtRuleSanitizer extends RuleSanitizer {
23    /** @var Matcher */
24    protected $nameMatcher;
25
26    /** @var Sanitizer */
27    protected $propertySanitizer;
28
29    public function __construct( MatcherFactory $matcherFactory, array $options = [] ) {
30        $this->nameMatcher = $matcherFactory->customIdent( [ 'none' ] );
31
32        // Do not include <image> per at-risk note
33        $symbol = new Alternative( [
34            $matcherFactory->string(),
35            $matcherFactory->customIdent()
36        ] );
37
38        $integer = $matcherFactory->integer();
39        $counterStyleName = $matcherFactory->customIdent( [ 'none' ] );
40
41        $this->propertySanitizer = new PropertySanitizer( [
42            'additive-symbols' => Quantifier::hash(
43                UnorderedGroup::allOf( [
44                    $integer,
45                    $symbol,
46                ] )
47            ),
48            'fallback' => $counterStyleName,
49            'negative' => new Juxtaposition( [
50                $symbol,
51                Quantifier::optional( $symbol )
52            ] ),
53            'pad' => UnorderedGroup::allOf( [
54                $integer,
55                $symbol
56            ] ),
57            'prefix' => $symbol,
58            'range' => new Alternative( [
59                Quantifier::hash(
60                    Quantifier::count(
61                        new Alternative( [
62                            $integer,
63                            new KeywordMatcher( 'infinite' )
64                        ] ),
65                        2, 2
66                    )
67                ),
68                new KeywordMatcher( 'auto' )
69            ] ),
70            'speak-as' => new Alternative( [
71                new KeywordMatcher( [
72                    'auto', 'bullets', 'numbers', 'words', 'spell-out'
73                ] ),
74                $counterStyleName
75            ] ),
76            'suffix' => $symbol,
77            'symbols' => Quantifier::plus( $symbol ),
78            'system' => new Alternative( [
79                new KeywordMatcher( [
80                    'cyclic', 'numeric', 'alphabetic', 'symbolic', 'additive'
81                ] ),
82                new Juxtaposition( [
83                    new KeywordMatcher( 'fixed' ),
84                    Quantifier::optional( $integer )
85                ] ),
86                new Juxtaposition( [
87                    new KeywordMatcher( 'extends' ),
88                    $counterStyleName
89                ] )
90            ] )
91        ] );
92    }
93
94    /** @inheritDoc */
95    public function handlesRule( Rule $rule ) {
96        return $rule instanceof AtRule && !strcasecmp( $rule->getName(), 'counter-style' );
97    }
98
99    /** @inheritDoc */
100    protected function doSanitize( CSSObject $object ) {
101        if ( !$object instanceof AtRule || !$this->handlesRule( $object ) ) {
102            $this->sanitizationError( 'expected-at-rule', $object, [ 'counter-style' ] );
103            return null;
104        }
105
106        if ( $object->getBlock() === null ) {
107            $this->sanitizationError( 'at-rule-block-required', $object, [ 'counter-style' ] );
108            return null;
109        }
110
111        // Test the name
112        if ( !$this->nameMatcher->matchAgainst(
113            $object->getPrelude(), [ 'mark-significance' => true ]
114        ) ) {
115            $cv = Util::findFirstNonWhitespace( $object->getPrelude() );
116            if ( $cv ) {
117                $this->sanitizationError( 'invalid-counter-style-name', $cv );
118            } else {
119                $this->sanitizationError( 'missing-counter-style-name', $object );
120            }
121            return null;
122        }
123
124        // Test the declaration list
125        $ret = clone $object;
126        $this->fixPreludeWhitespace( $ret, false );
127        $this->sanitizeDeclarationBlock( $ret->getBlock(), $this->propertySanitizer );
128        // @phan-suppress-next-line PhanTypeMismatchReturn generics weakness
129        return $ret;
130    }
131}