Skip to content

Commit 2427f88

Browse files
committed
Add support for *-progress()
This implementation is still rough, mainly because it is not yet clear whether *-progress() should be type checked and simplified as an operator node, like a math function. The distinction does not seem necessary.
1 parent 7623480 commit 2427f88

File tree

10 files changed

+217
-26
lines changed

10 files changed

+217
-26
lines changed

__tests__/style.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ describe('CSSFontFaceDescriptors', () => {
334334
['attr(name)'],
335335
['env(name, attr(name))'],
336336
['mix(50%, 1, 1)', 'mix(50%, 1%, 1%)'],
337-
['progress(1 from 1 to 1)', 'progress(1% from 1% to 1%)'],
337+
['progress(1 from 1 to 1)', 'calc(1% * progress(1 from 1 to 1))'],
338338
['random-item(--key, 1)', 'random-item(--key, 1%)'],
339339
['sibling-count()', 'calc(1% * sibling-count())'],
340340
['toggle(1, 1)', 'toggle(1%, 1%)'],
@@ -428,6 +428,8 @@ describe('CSSKeyframeProperties', () => {
428428
expect(style.fontWeight).toBe('attr(name)')
429429
style.fontWeight = 'mix(50%, 1, 1)'
430430
expect(style.fontWeight).toBe('mix(50%, 1, 1)')
431+
style.fontWeight = 'progress(1 from 1 to 1)'
432+
expect(style.fontWeight).toBe('progress(1 from 1 to 1)')
431433
style.fontWeight = 'random-item(--key, 1)'
432434
expect(style.fontWeight).toBe('random-item(--key, 1)')
433435
style.fontWeight = 'sibling-count()'
@@ -507,7 +509,7 @@ describe('CSSPageDescriptors', () => {
507509
// Substitution accepted in element-dependent context
508510
['attr(name)', 'attr(name)'],
509511
['mix(50%, 1, 1)', 'mix(50%, 1px, 1px)'],
510-
['progress(1 from 1 to 1)', 'progress(1px from 1px to 1px)'],
512+
['progress(1 from 1 to 1)', 'calc(1px * progress(1 from 1 to 1))'],
511513
['random-item(--key, 1)', 'random-item(--key, 1px)'],
512514
['sibling-count()', 'calc(1px * sibling-count())'],
513515
['toggle(1, 1)', 'toggle(1px, 1px)'],
@@ -591,6 +593,8 @@ describe('CSSPositionTryDescriptors', () => {
591593
expect(style.top).toBe('attr(name)')
592594
style.top = 'mix(50%, 1px, 1px)'
593595
expect(style.top).toBe('mix(50%, 1px, 1px)')
596+
style.top = 'calc(1px * progress(1 from 1 to 1))'
597+
expect(style.top).toBe('calc(1px * progress(1 from 1 to 1))')
594598
style.top = 'random-item(--key, 1px)'
595599
expect(style.top).toBe('random-item(--key, 1px)')
596600
style.top = 'calc(1px * sibling-count())'

__tests__/value.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,6 +2737,71 @@ describe('<random()>', () => {
27372737
})
27382738
})
27392739

2740+
describe('<progress()>', () => {
2741+
test('invalid', () => {
2742+
const invalid = [
2743+
// Inconsistent calculation types
2744+
['<number> | <length>', 'progress(1 from 1px to 1)'],
2745+
['<number> | <percentage>', 'progress(1 from 1% to 1)'],
2746+
// Result type mismatch
2747+
['<number> | <percentage>', 'progress(1 from (1% + 1px) / 1px to 1)'],
2748+
['<length>', 'calc(1px * progress(1% from 1px to 1px))'],
2749+
]
2750+
invalid.forEach(([definition, input]) => expect(parse(definition, input, false)).toBeNull())
2751+
})
2752+
test('valid', () => {
2753+
expect(parse('<number>', 'progress(1 * 1 from 1% / 1% to 1em / 1px)'))
2754+
.toBe('progress(1 from 1 to 1em / 1px)')
2755+
expect(parse('<length-percentage>', 'calc(1px * progress(1 * 1 from 1% / 1% to 1em / 1px))'))
2756+
.toBe('calc(1px * progress(1 from 1% / 1% to 1em / 1px))')
2757+
})
2758+
})
2759+
describe('<container-progress()>', () => {
2760+
test('invalid', () => {
2761+
const invalid = [
2762+
// Invalid feature
2763+
['<number>', 'container-progress(resolution from 1dpi to 1dpi)'],
2764+
['<number>', 'container-progress(orientation from 1 to 1)'],
2765+
['<number>', 'container-progress(width: 1px from 1px to 1px)'],
2766+
['<number>', 'container-progress(width < 1px from 1px to 1px)'],
2767+
// Invalid value
2768+
['<number>', 'container-progress(width from 1 to 1)'],
2769+
['<number>', 'container-progress(width from 1% to 1%)'],
2770+
['<length-percentage>', 'calc(1px * container-progress(width from 1% to 1px))'],
2771+
['<length>', 'calc(1px * container-progress(width from 1% + 1px to 1px))'],
2772+
]
2773+
invalid.forEach(([definition, input]) => expect(parse(definition, input, false)).toBeNull())
2774+
})
2775+
test('valid', () => {
2776+
expect(parse('<number>', 'container-progress(width from 0px + 1px to 1px * 1)'))
2777+
.toBe('container-progress(width from 1px to 1px)')
2778+
expect(parse('<number>', 'container-progress(aspect-ratio from -1 to 1)'))
2779+
.toBe('container-progress(aspect-ratio from -1 to 1)')
2780+
})
2781+
})
2782+
describe('<media-progress()>', () => {
2783+
test('invalid', () => {
2784+
const invalid = [
2785+
// Invalid feature
2786+
['<number>', 'media-progress(inline-size from 1px to 1px)'],
2787+
['<number>', 'media-progress(orientation from 1 to 1)'],
2788+
['<number>', 'media-progress(width: 1px from 1px to 1px)'],
2789+
['<number>', 'media-progress(width < 1px from 1px to 1px)'],
2790+
// Invalid value
2791+
['<number>', 'media-progress(width from 1 to 1)'],
2792+
['<number>', 'media-progress(width from 1% to 1%)'],
2793+
['<length-percentage>', 'calc(1px * media-progress(width from 1% to 1px))'],
2794+
['<length>', 'calc(1px * media-progress(width from 1% + 1px to 1px))'],
2795+
]
2796+
invalid.forEach(([definition, input]) => expect(parse(definition, input, false)).toBeNull())
2797+
})
2798+
test('valid', () => {
2799+
expect(parse('<number>', 'media-progress(width from 0px + 1px to 1px * 1)'))
2800+
.toBe('media-progress(width from 1px to 1px)')
2801+
expect(parse('<number>', 'media-progress(aspect-ratio from -1 to 1)'))
2802+
.toBe('media-progress(aspect-ratio from -1 to 1)')
2803+
})
2804+
})
27402805
describe('<sibling-count()>, <sibling-index()>', () => {
27412806
test('valid', () => {
27422807
expect(parse('<integer>', 'sibling-index()')).toBe('sibling-index()')

lib/parse/definition.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11

22
const { isDigit, isIdentifierCharacter } = require('./tokenize.js')
3-
const { MAX_TERMS: MAX_MATH_FUNCTION_TERMS } = require('./math-function.js')
43
const Stream = require('./stream.js')
54
const arbitrary = require('./arbitrary.js')
65
const blocks = require('../values/blocks.js')
@@ -12,7 +11,14 @@ const properties = require('../properties/definitions.js')
1211

1312
const closingBlockTokens = Object.values(blocks.associatedTokens)
1413

15-
// UAs must support at least 20 repetitions
14+
/**
15+
* @see {@link https://drafts.csswg.org/css-values-4/#calc-syntax}
16+
*/
17+
const MAX_CALCULATION_TERMS = 32
18+
19+
/**
20+
* @see {@link https://drafts.csswg.org/css-values-4/#component-multipliers}
21+
*/
1622
const MAX_REPETITIONS = 20
1723

1824
/**
@@ -21,10 +27,10 @@ const MAX_REPETITIONS = 20
2127
*/
2228
function getMaxRepetitions(production) {
2329
if (['<calc-product>', '<calc-sum>'].includes(production?.definition.name)) {
24-
return MAX_MATH_FUNCTION_TERMS - 1
30+
return MAX_CALCULATION_TERMS - 1
2531
}
2632
if (['<hypoth()>', '<min()>', '<max()>'].includes(production?.definition.name)) {
27-
return MAX_MATH_FUNCTION_TERMS
33+
return MAX_CALCULATION_TERMS
2834
}
2935
return MAX_REPETITIONS
3036
}

lib/parse/postprocess.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -811,15 +811,34 @@ function postParseLineNames(names, node) {
811811
: names
812812
}
813813

814+
/**
815+
* @param {object} feature
816+
* @param {object} node
817+
* @returns {SyntaxError|object}
818+
*
819+
* It aborts parsing when the media feature is a non-range media feature name
820+
* received in container-progress() or media-progress().
821+
*/
822+
function postParseMediaFeature(feature, { context }) {
823+
context = context.function?.definition.name.split('-progress')[0]
824+
if (context && descriptors[`@${context}`][feature.value].type !== 'range') {
825+
return error(node)
826+
}
827+
return feature
828+
}
829+
814830
/**
815831
* @param {object} name
816832
* @returns {object|null}
817833
* @see {@link https://drafts.csswg.org/mediaqueries-5/#typedef-mf-boolean}
818834
*
819835
* It aborts parsing the name when its value cannot be evaluated as a boolean.
820836
*/
821-
function postParseMediaFeatureBoolean(name, node) {
822-
const { type, value } = descriptors[node.context.rule.definition.name][name.value]
837+
function postParseMediaFeatureBoolean(name, { context }) {
838+
context = context.function?.definition.name.endsWith('-progress')
839+
? `@${context.function.definition.name.split('-progress')[0]}`
840+
: context.rule.definition.name
841+
const { type, value } = descriptors[context][name.value]
823842
if (type === 'range' || value === '<mq-boolean>') {
824843
return name
825844
}
@@ -840,19 +859,22 @@ function postParseMediaFeatureBoolean(name, node) {
840859
function postParseMediaFeatureName(name, { context, parent }) {
841860
const lowercase = name.value.toLowerCase()
842861
const unprefixed = lowercase.replace(/(min|max)-/, '')
862+
context = context.function?.definition.name.endsWith('-progress')
863+
? `@${context.function.definition.name.split('-progress')[0]}`
864+
: context.rule.definition.name
843865
const target = compatibility.descriptors[context]?.aliases.get(unprefixed)
844866
if (lowercase === unprefixed) {
845867
if (target) {
846868
return { ...name, value: target }
847869
}
848-
if (descriptors[context.rule.definition.name][lowercase]) {
870+
if (descriptors[context][lowercase]) {
849871
return { ...name, value: lowercase }
850872
}
851873
} else if (parent?.parent?.definition.name === '<mf-plain>'/* only for <mf-name> tests: */ || !parent) {
852874
if (target) {
853875
return { ...name, value: `${lowercase.includes('min-') ? 'min' : 'max'}-${target}` }
854876
}
855-
if (descriptors[context.rule.definition.name][unprefixed]) {
877+
if (descriptors[context][unprefixed]) {
856878
return { ...name, value: lowercase }
857879
}
858880
}
@@ -1673,6 +1695,7 @@ module.exports = {
16731695
'<layer-name>': postParseLayerName,
16741696
'<length>': postParseDimension,
16751697
'<line-names>': postParseLineNames,
1698+
'<media-feature>': postParseMediaFeature,
16761699
'<media-query-list>': postParseMediaQueryList,
16771700
'<media-type>': postParseMediaType,
16781701
'<mf-boolean>': postParseMediaFeatureBoolean,

lib/parse/preprocess.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11

2-
const { MAX_TERMS: MAX_MATH_FUNCTION_TERMS } = require('./math-function.js')
32
const { findContext, findParent, findSibling } = require('../utils/context.js')
43
const { isColon, isComma, isMinus, isOmitted, isPlus, isWhitespace } = require('../utils/value.js')
54
const { length, omitted } = require('../values/value.js')
65
const createError = require('../error.js')
76
const pseudos = require('../values/pseudos.js')
87
const properties = require('../properties/definitions.js')
98

9+
/**
10+
* @see {@link https://drafts.csswg.org/css-values-4/#calc-syntax}
11+
*/
12+
const MAX_CALCULATION_TERMS = 32
13+
1014
/**
1115
* @param {object} node
1216
* @returns {SyntaxError}
@@ -44,7 +48,7 @@ function preParseCalcOperator(node) {
4448
function preParseCalcValue(node) {
4549
const { context: { globals } } = node
4650
let count = globals.get('calc-terms')
47-
if (count++ === MAX_MATH_FUNCTION_TERMS) {
51+
if (count++ === MAX_CALCULATION_TERMS) {
4852
return error(node)
4953
}
5054
globals.set('calc-terms', count)
@@ -245,6 +249,21 @@ function preParseLength(node) {
245249
}
246250
}
247251

252+
/**
253+
* @param {object} node
254+
* @returns {null|undefined}
255+
* @see {@link https://drafts.csswg.org/css-values-5/#funcdef-container-progress}
256+
* @see {@link https://drafts.csswg.org/css-values-5/#funcdef-media-progress}
257+
*
258+
* It aborts parsing a plain or range media feature when the context is
259+
* container-progress() or media-progress().
260+
*/
261+
function preParseMediaFeature(node) {
262+
if (node.context.function?.definition.name.endsWith('-progress')) {
263+
return null
264+
}
265+
}
266+
248267
/**
249268
* @param {object} node
250269
* @returns {SyntaxError|null|undefined}
@@ -272,4 +291,6 @@ module.exports = {
272291
'<length-percentage>': preParseLength,
273292
'<length>': preParseLength,
274293
'<mix()>': preParseMix,
294+
'<mf-plain>': preParseMediaFeature,
295+
'<mf-range>': preParseMediaFeature,
275296
}

lib/parse/replace.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { getNumericFunctionType, matchNumericType } = require('./types.js')
44
const { isAmpersand, isOmitted } = require('../utils/value.js')
55
const createError = require('../error.js')
66
const { findSibling } = require('../utils/context.js')
7-
const { simplifyCalculationTree } = require('./math-function.js')
7+
const { simplifyCalculationTree } = require('./simplify.js')
88
const substitutions = require('../values/substitutions.js')
99

1010
/**
@@ -133,7 +133,7 @@ function replaceWithMathFunction(name, node, parser) {
133133
if (match instanceof SyntaxError) {
134134
return match
135135
}
136-
// Eg. do not match the type of `min(1)` when parsing `translateX(calc(1px * min(1)))`
136+
// Validate type and simplify calculations once from top to bottom
137137
if (topLevel) {
138138
const { name, range } = definition
139139
const replacedType = name === '<integer>' ? '<number>' : name
@@ -169,14 +169,38 @@ function replaceWithNestingSelector(node) {
169169
* @returns {SyntaxError|object|null}
170170
*/
171171
function replaceWithNumericFunction(name, node, parser) {
172-
const { context, definition: { name: type }, input } = node
172+
const { context, definition: { name: type, range }, input, parent } = node
173+
// <*-progress()> and <sibling-*()> require an element-dependent context
173174
if (!context.rule.definition.elemental) {
174175
return error(node)
175176
}
177+
// <*-progress()> and <sibling-*()> resolve to <number>
176178
if (type === '<integer>' || type === '<number>') {
177-
return parser.parseCSSValue(input, `<${name}()>`, { ...context, replaced: node }, 'lazy') ?? error(node)
179+
const match = parser.parseCSSValue(input, `<${name}()>`, { ...context, replaced: node }, 'lazy')
180+
if (match === null) {
181+
return error(node)
182+
}
183+
if (match instanceof SyntaxError) {
184+
return match
185+
}
186+
// Validate type and simplify calculations once from top to bottom
187+
if (!context.replaced && name.includes('progress')) {
188+
const replacedType = type === '<integer>' ? '<number>' : type
189+
const hasResolutionType = type !== '<percentage>'
190+
&& parent?.definition.type === '|'
191+
&& parent.definition.value.some(definition => definition.name === '<percentage>')
192+
const resolutionType = hasResolutionType ? type : null
193+
const numericType = getNumericFunctionType(match, resolutionType)
194+
if (matchNumericType(numericType, replacedType, resolutionType)) {
195+
const value = simplifyCalculationTree(match, resolutionType)
196+
const round = type === '<integer>'
197+
return { ...match, ...value, range, round }
198+
}
199+
return error(node)
200+
}
201+
return match
178202
}
179-
return error(node)
203+
return null
180204
}
181205

182206
module.exports = {

lib/parse/math-function.js renamed to lib/parse/simplify.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ const { isCalculation, isCombinable, isComma, isNumeric, isOmitted } = require('
77
const error = require('../error.js')
88
const substitutions = require('../values/substitutions.js')
99

10-
const MAX_TERMS = 32
11-
1210
/**
1311
* @param {object} root
1412
* @param {string} [resolutionType]
@@ -39,10 +37,10 @@ function simplifyCalculationTree(root, resolutionType) {
3937
return root
4038
}
4139
// Leaf: substitution resolved at computed value time
42-
if (substitutions.numeric.state.includes(name ?? value)) {
40+
if (substitutions.numeric.state.includes(value)) {
4341
return root
4442
}
45-
// Operator: calculation operator or math function
43+
// Operator: calculation operator or numeric substitution function
4644
let args
4745
if (Array.isArray(value)) {
4846
function simplifyArguments(components, component) {
@@ -178,12 +176,12 @@ function simplifyCalculationTree(root, resolutionType) {
178176
}
179177
return map(root, () => args)
180178
}
181-
// Operator: math function
179+
// Operator: numeric substitution function
182180
if (name === 'calc') {
183181
return args[0]
184182
}
185-
if (name === 'calc-mix' || name === 'random') {
186-
return map(root, () => args)
183+
if (name === 'calc-mix' || name === 'random' || substitutions.numeric.state.includes(name)) {
184+
return map(root, () => list(args))
187185
}
188186
if (name === 'clamp' && args.some(component => component.value === 'none')) {
189187
const [min, center, max] = args
@@ -317,6 +315,5 @@ function simplifyCalculationTree(root, resolutionType) {
317315
}
318316

319317
module.exports = {
320-
MAX_TERMS,
321318
simplifyCalculationTree,
322319
}

0 commit comments

Comments
 (0)