Skip to content

Commit d3f049c

Browse files
jenkins-botGerrit Code Review
authored andcommitted
Merge "Add Counter Styles 3 and Lists 3"
2 parents c75b022 + e9c191b commit d3f049c

File tree

8 files changed

+526
-37
lines changed

8 files changed

+526
-37
lines changed

HISTORY.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
## css-sanitizer x.x.x (not yet released)
44
* Add support for CSS Box Sizing Level 4 (as seen in draft from 2025-02-24)
5-
- values: stretch, fit-content, and contain;
6-
- properties: aspect-ratio, contain-intrinsic-* (size, width, height, block-size, inline-size), min-intrinsic-size;
5+
- values: stretch, fit-content, and contain;
6+
- properties: aspect-ratio, contain-intrinsic-* (size, width, height,
7+
block-size, inline-size), min-intrinsic-size;
78
* Update Color to Level 4 (2025-04-24)
89
* Update Values and Units to Level 4 (WD 2024-03-12)
910
* Update Display Level 3 to CR 2023-03-30
1011
* Add support for Ruby Level 1 (WD 2022-12-31)
1112
* Add support for Transforms Level 2 (WD 2021-11-09)
12-
* Update Overflow Level 3 to WD 2023-03-29 and add support for Overflow Level 4 (WD 2023-03-21)
13+
* Update Overflow Level 3 to WD 2023-03-29 and add support for Overflow Level 4
14+
(WD 2023-03-21)
15+
* Add support for Lists and Counters Level 3 (WD 2020-11-17) and Counter Styles
16+
Level 3 (WD 2021-07-27)
1317

1418
## css-sanitizer 5.5.0 (2025-01-27)
1519
* Ensure <-token and identifiers are always separated as a security

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,24 @@ The sanitizer recognizes the following CSS modules:
6868
* [Cascade Level 4, 2018-08-28](https://www.w3.org/TR/2018/CR-css-cascade-4-20180828)
6969
* [Color Level 4, 2025-04-24](https://www.w3.org/TR/2025/CRD-css-color-4-20250424)
7070
* [Compositing Level 1, 2015-01-13](https://www.w3.org/TR/2015/CR-compositing-1-20150113/)
71+
* [Counter Styles Level 3, 2021-07-27](https://www.w3.org/TR/2021/CR-css-counter-styles-3-20210727/)
7172
* [CSS Level 2, 2011-06-07](https://www.w3.org/TR/2011/REC-CSS2-20110607/)
7273
* [Display Level 3, 2023-03-30](https://www.w3.org/TR/2023/CR-css-display-3-20230330/)
7374
* [Filter Effects Level 1, 2018-12-18](https://www.w3.org/TR/2018/WD-filter-effects-1-20181218)
7475
* [Flexbox Level 1, 2018-11-19](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119)
7576
* [Fonts Level 3, 2018-09-20](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920)
7677
* [Grid Level 1, 2017-12-14](https://www.w3.org/TR/2017/CR-css-grid-1-20171214/)
7778
* [Images Level 3, 2019-10-10](https://www.w3.org/TR/2019/CR-css-images-3-20191010)
79+
* [Lists and Counters Level 3, 2020-11-17](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/)
80+
* [Logical Properties and Values Level 1, 2018-08-27](https://www.w3.org/TR/2018/WD-css-logical-1-20180827/)
7881
* [Masking Level 1, 2014-08-26](https://www.w3.org/TR/2014/CR-css-masking-1-20140826/)
7982
* [Multicol Level 1, 2019-10-15](https://www.w3.org/TR/2019/WD-css-multicol-1-20191015)
8083
* [Overflow Level 3, 2023-03-29](https://www.w3.org/TR/2023/WD-css-overflow-3-20230329/)
8184
* [Overflow Level 4, 2023-03-21](https://www.w3.org/TR/2023/WD-css-overflow-4-20230321/)
8285
* [Page Level 3, 2018-10-18](https://www.w3.org/TR/2018/WD-css-page-3-20181018)
8386
* [Position Level 3, 2016-05-17](https://www.w3.org/TR/2016/WD-css-position-3-20160517/)
87+
* [Ruby Level 1, 2022-12-31](https://www.w3.org/TR/2022/WD-css-ruby-1-20221231/)
88+
* [Selectors Level 4, 2019-02-25](https://www.w3.org/TR/2019/WD-css-pseudo-4-20190225/)
8489
* [Shapes Level 1, 2014-03-20](https://www.w3.org/TR/2014/CR-css-shapes-1-20140320/)
8590
* [Sizing Level 3, 2021-12-17](https://www.w3.org/TR/2021/WD-css-sizing-3-20211217/)
8691
* [Sizing Level 4, 2025-02-24](https://drafts.csswg.org/css-sizing-4/)
@@ -93,9 +98,6 @@ The sanitizer recognizes the following CSS modules:
9398
* [UI 3 Level 3, 2018-06-21](https://www.w3.org/TR/2018/REC-css-ui-3-20180621)
9499
* [UI 4 Level 4, 2020-01-02](https://www.w3.org/TR/2020/WD-css-ui-4-20200102)
95100
* [Writing Modes Level 4, 2019-07-30](https://www.w3.org/TR/2019/CR-css-writing-modes-4-20190730)
96-
* [Selectors Level 4, 2019-02-25](https://www.w3.org/TR/2019/WD-css-pseudo-4-20190225/)
97-
* [Logical Properties and Values Level 1, 2018-08-27](https://www.w3.org/TR/2018/WD-css-logical-1-20180827/)
98-
* [Ruby Level 1, 2022-12-31](https://www.w3.org/TR/2022/WD-css-ruby-1-20221231/)
99101

100102
And also,
101103
* The `touch-action` property from

src/Grammar/MatcherFactory.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,46 @@ public function cssSingleEasingFunction() {
11541154
] );
11551155
}
11561156

1157+
/**
1158+
* Matcher for <counter-style>
1159+
* @see https://www.w3.org/TR/2021/CR-css-counter-styles-3-20210727/#typedef-counter-style
1160+
* @return Matcher
1161+
*/
1162+
public function counterStyle() {
1163+
return $this->cache[__METHOD__] ??= new Alternative( [
1164+
$this->customIdent( [ 'none' ] ),
1165+
new FunctionMatcher(
1166+
'symbols',
1167+
// "If the system is alphabetic or numeric, there must be at least two
1168+
// <string>s or <image>s, or else the function is invalid."
1169+
// Implement that by modifying the grammar
1170+
new Alternative( [
1171+
new Juxtaposition( [
1172+
new KeywordMatcher( [ 'numeric', 'alphabetic' ] ),
1173+
Quantifier::count(
1174+
new Alternative( [
1175+
$this->string(),
1176+
$this->image()
1177+
] ),
1178+
2, INF
1179+
)
1180+
] ),
1181+
new Juxtaposition( [
1182+
Quantifier::optional( new KeywordMatcher( [
1183+
'cyclic', 'symbolic', 'fixed'
1184+
] ) ),
1185+
Quantifier::plus(
1186+
new Alternative( [
1187+
$this->string(),
1188+
$this->image()
1189+
] )
1190+
)
1191+
] )
1192+
] )
1193+
)
1194+
] );
1195+
}
1196+
11571197
/***************************************************************************/
11581198
// region CSS Selectors Level 3
11591199
/**
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace Wikimedia\CSS\Sanitizer;
4+
5+
use Wikimedia\CSS\Grammar\Alternative;
6+
use Wikimedia\CSS\Grammar\Juxtaposition;
7+
use Wikimedia\CSS\Grammar\KeywordMatcher;
8+
use Wikimedia\CSS\Grammar\Matcher;
9+
use Wikimedia\CSS\Grammar\MatcherFactory;
10+
use Wikimedia\CSS\Grammar\Quantifier;
11+
use Wikimedia\CSS\Grammar\UnorderedGroup;
12+
use Wikimedia\CSS\Objects\AtRule;
13+
use Wikimedia\CSS\Objects\CSSObject;
14+
use Wikimedia\CSS\Objects\Rule;
15+
use 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+
*/
21+
class 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+
}

src/Sanitizer/StylePropertySanitizer.php

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public function __construct( MatcherFactory $matcherFactory ) {
7979
$this->addKnownProperties( $this->cssSizing4( $matcherFactory ) );
8080
$this->addKnownProperties( $this->cssLogical1( $matcherFactory ) );
8181
$this->addKnownProperties( $this->cssRuby1( $matcherFactory ) );
82+
$this->addKnownProperties( $this->cssLists3( $matcherFactory ) );
8283
}
8384

8485
/**
@@ -138,25 +139,21 @@ protected function css2( MatcherFactory $matcherFactory ) {
138139
] );
139140

140141
// https://www.w3.org/TR/2011/REC-CSS2-20110607/generate.html
141-
$props['list-style-type'] = new KeywordMatcher( [
142-
'disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman',
143-
'lower-greek', 'lower-latin', 'upper-latin', 'armenian', 'georgian', 'lower-alpha',
144-
'upper-alpha', 'none'
145-
] );
146142
$props['content'] = new Alternative( [
147143
new KeywordMatcher( [ 'normal', 'none' ] ),
148144
Quantifier::plus( new Alternative( [
149145
$matcherFactory->string(),
150146
// Replaces <url> per https://www.w3.org/TR/css-images-3/#placement
151147
$matcherFactory->image(),
148+
// Updated by https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#counter-functions
152149
new FunctionMatcher( 'counter', new Juxtaposition( [
153150
$matcherFactory->ident(),
154-
Quantifier::optional( $props['list-style-type'] ),
151+
Quantifier::optional( $matcherFactory->counterStyle() ),
155152
], true ) ),
156153
new FunctionMatcher( 'counters', new Juxtaposition( [
157154
$matcherFactory->ident(),
158155
$matcherFactory->string(),
159-
Quantifier::optional( $props['list-style-type'] ),
156+
Quantifier::optional( $matcherFactory->counterStyle() ),
160157
], true ) ),
161158
new FunctionMatcher( 'attr', $matcherFactory->ident() ),
162159
new KeywordMatcher( [ 'open-quote', 'close-quote', 'no-open-quote', 'no-close-quote' ] ),
@@ -167,22 +164,6 @@ protected function css2( MatcherFactory $matcherFactory ) {
167164
$matcherFactory->string(), $matcherFactory->string()
168165
] ) ),
169166
] );
170-
$props['counter-reset'] = new Alternative( [
171-
$none,
172-
Quantifier::plus( new Juxtaposition( [
173-
$matcherFactory->ident(), Quantifier::optional( $matcherFactory->integer() )
174-
] ) ),
175-
] );
176-
$props['counter-increment'] = $props['counter-reset'];
177-
$props['list-style-image'] = new Alternative( [
178-
$none,
179-
// Replaces <url> per https://www.w3.org/TR/css-images-3/#placement
180-
$matcherFactory->image()
181-
] );
182-
$props['list-style-position'] = new KeywordMatcher( [ 'inside', 'outside' ] );
183-
$props['list-style'] = UnorderedGroup::someOf( [
184-
$props['list-style-type'], $props['list-style-position'], $props['list-style-image']
185-
] );
186167

187168
// https://www.w3.org/TR/2011/REC-CSS2-20110607/tables.html
188169
$props['caption-side'] = new KeywordMatcher( [ 'top', 'bottom' ] );
@@ -2118,4 +2099,52 @@ protected function cssRuby1( $matcherFactory ) {
21182099
'ruby-overhang' => new KeywordMatcher( [ 'auto', 'none' ] ),
21192100
];
21202101
}
2102+
2103+
/**
2104+
* CSS Lists and Counters Module Level 3
2105+
* @see https://www.w3.org/TR/2020/WD-css-lists-3-20201117/
2106+
*
2107+
* @param MatcherFactory $matcherFactory
2108+
* @return Matcher[]
2109+
*/
2110+
protected function cssLists3( MatcherFactory $matcherFactory ) {
2111+
// @codeCoverageIgnoreStart
2112+
if ( isset( $this->cache[__METHOD__] ) ) {
2113+
return $this->cache[__METHOD__];
2114+
}
2115+
// @codeCoverageIgnoreEnd
2116+
$none = new KeywordMatcher( 'none' );
2117+
$props = [];
2118+
2119+
$props['counter-increment'] = $props['counter-reset'] = $props['counter-set'] =
2120+
new Alternative( [
2121+
Quantifier::plus( new Juxtaposition( [
2122+
$matcherFactory->customIdent( [ 'none' ] ),
2123+
Quantifier::optional( $matcherFactory->integer() )
2124+
] ) ),
2125+
$none
2126+
] );
2127+
2128+
$props['list-style-image'] = new Alternative( [
2129+
$matcherFactory->image(),
2130+
$none
2131+
] );
2132+
2133+
$props['list-style-position'] = new KeywordMatcher( [ 'inside', 'outside' ] );
2134+
2135+
$props['list-style-type'] = new Alternative( [
2136+
$matcherFactory->counterStyle(),
2137+
$matcherFactory->string(),
2138+
$none
2139+
] );
2140+
2141+
$props['list-style'] = UnorderedGroup::someOf( [
2142+
$props['list-style-position'], $props['list-style-image'], $props['list-style-type']
2143+
] );
2144+
2145+
$props['marker-side'] = new KeywordMatcher( [ 'match-self', 'match-parent' ] );
2146+
2147+
$this->cache[__METHOD__] = $props;
2148+
return $props;
2149+
}
21212150
}

0 commit comments

Comments
 (0)