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