Skip to content

Commit bb27bba

Browse files
committed
Parse the whole input as a list of tokens
This allows consuming component values and validating the grammar in a single pass, which allows interpreting bad tokens as parse errors even when nested, and parsing any type of value with parseGrammar(). This deviates from CSS Syntax procedures that consume a prelude, a declaration value, a standalone value, as a list of component values, but they are written to prioritize understandability over efficiency. There should be no observable difference in the output. Since the related changes do not allow for easy diffing, this commit also comes with a few unrelated but non-important fixes and refactorings.
1 parent 8902468 commit bb27bba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1787
-1487
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ JavaScript implementation of CSS.
66
## Usage
77

88
```js
9-
const { cssom, install, parser: { parseCSSGrammar, parseCSSStyleSheet } } = require('@cdoublev/css')
9+
const { cssom, install, parser } = require('@cdoublev/css')
1010

1111
/**
1212
* install() expects a window-like global object (default: globalThis) with
@@ -15,7 +15,7 @@ const { cssom, install, parser: { parseCSSGrammar, parseCSSStyleSheet } } = requ
1515
install(/*myGlobalObject*/)
1616

1717
// Parse a style sheet and get a (non-constructed) CSSStyleSheet
18-
const stylesheet = parseCSSStyleSheet('style {}')
18+
const stylesheet = parser.parseStyleSheet('style {}')
1919

2020
// Create a (constructed) CSSStyleSheet
2121
const styleSheet = new /*myGlobalObject.*/CSSStyleSheet()
@@ -25,7 +25,7 @@ const stylesheet = cssom.CSSStyleSheet.create(myGlobalObject, undefined, private
2525
const style = cssom.CSSStyleDeclaration.create(myGlobalObject, undefined, privateProperties)
2626

2727
// Parse a value according to a CSS grammar
28-
const value = parseCSSGrammar('(width < 30rem), (orientation: portrait)', '<media-query-list>')
28+
const value = parser.parseGrammar('(width < 30rem), (orientation: portrait)', '<media-query-list>')
2929
```
3030

3131
The `webidl2js` wrappers are intended to implement:

__tests__/match.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { install } from '../lib/index.js'
33
import matchMediaQueryList from '../lib/match/media.js'
44
import matchSupport from '../lib/match/support.js'
5-
import { parseCSSGrammar } from '../lib/parse/parser.js'
5+
import { parseGrammar } from '../lib/parse/parser.js'
66

77
install()
88

@@ -21,7 +21,7 @@ describe('media', () => {
2121
}
2222

2323
function match(query, window) {
24-
return matchMediaQueryList(parseCSSGrammar(query, '<media-query-list>'), window)
24+
return matchMediaQueryList(parseGrammar(query, '<media-query-list>'), window)
2525
}
2626

2727
test('types', () => {
@@ -302,7 +302,7 @@ describe('media', () => {
302302
describe('support', () => {
303303

304304
function match(query) {
305-
return matchSupport(parseCSSGrammar(`(${query})`, '<supports-condition>'))
305+
return matchSupport(parseGrammar(`(${query})`, '<supports-condition>'))
306306
}
307307

308308
test('declaration', () => {

__tests__/stream.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ describe('prev(size, offset = 0)', () => {
148148
})
149149
})
150150

151+
describe('peek(predicate)', () => {
152+
it('returns whether the next item satisfies the given predicate', () => {
153+
expect(stream.peek(char => char === '.')).toBeFalsy()
154+
expect(stream.peek(char => char === 'h')).toBeTruthy()
155+
stream.moveToEnd()
156+
expect(stream.peek(_ => true)).toBeFalsy()
157+
})
158+
})
159+
151160
describe('atEnd(offset = 0)', () => {
152161
it('returns false when some items have not been consumed', () => {
153162
expect(stream.atEnd()).toBeFalsy()

__tests__/style.js

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -381,30 +381,34 @@ describe('arbitrary substitution', () => {
381381
test('invalid', () => {
382382
const style = createStyleBlock()
383383
const invalid = [
384-
// Invalid component value
385-
['env(name) "\n'],
386-
['env(name) url(bad .url)'],
387-
['env(name) ]'],
388-
['env(name) )'],
389-
['env(name) }'],
390-
['env(name) ;'],
391-
['env(name) !'],
392-
['env(name) {}', 'color'],
393-
['{} env(name)', 'color'],
394-
['env(name) !important'],
395-
['!important env(name)'],
384+
// Top-level or nested <bad-*-token>, ), ], }
385+
'env(name) fn("\n)',
386+
'env(name) (url(bad .url))',
387+
'env(name) [)]',
388+
'env(name) (])',
389+
'env(name) (})',
390+
// Top-level ; and !
391+
'env(name) ;',
392+
'env(name) !',
393+
'env(name) !important',
394+
// Positioned {} block
395+
'env(name) {}',
396+
'{} env(name)',
396397
// Nested
397-
['fn(env())'],
398-
['{env()}'],
398+
'fn(env())',
399+
'{env()}',
399400
]
400-
invalid.forEach(([input, property = '--custom']) => {
401-
style.setProperty(property, input)
402-
expect(style.getPropertyValue(property)).toBe('')
401+
invalid.forEach(input => {
402+
style.color = input
403+
expect(style.color).toBe('')
403404
})
404405
})
405406
test('valid', () => {
406407
const style = createStyleBlock()
407408
const valid = [
409+
// Nested ; and !
410+
['env(name) (;)'],
411+
['env(name) (!)'],
408412
// Valid at parse time
409413
['--custom(--custom(!))'],
410414
['attr(name, attr())'],
@@ -481,16 +485,40 @@ describe('<whole-value>', () => {
481485
})
482486

483487
describe('--*', () => {
488+
test('invalid', () => {
489+
const style = createStyleBlock()
490+
const invalid = [
491+
// Top-level or nested <bad-*-token>, ), ], }
492+
'fn("\n)',
493+
'(url(bad .url))',
494+
'[)]',
495+
'(])',
496+
'(})',
497+
// Top-level ; and !
498+
';',
499+
'!important',
500+
]
501+
invalid.forEach(input => {
502+
style.setProperty('--custom', input)
503+
expect(style.getPropertyValue('--custom')).toBe('')
504+
})
505+
})
484506
test('valid', () => {
485507
const style = createStyleBlock()
486508
const valid = [
487-
// Whitespaces and comments
488-
['', ' '],
489-
[' /**/ ', ' '],
509+
// Nested ; and !
510+
['(;)'],
511+
['(!)'],
512+
// Positioned {}-block
513+
['positioned {} block'],
514+
// Serialize exactly as specified but without leading and trailing whitespaces
490515
[
491516
' /**/ Red , ( orange /**/ ) , green /**/ ! /**/ important',
492517
'Red , ( orange /**/ ) , green !important',
493518
],
519+
// Empty value
520+
['', ' '],
521+
[' /**/ ', ' '],
494522
// Substitution
495523
['var( --PROPerty, /**/ 1e0 /**/ ) ', 'var( --PROPerty, /**/ 1e0 /**/ )'],
496524
['initial initial'],
@@ -1032,6 +1060,35 @@ describe('shape-outside', () => {
10321060
expect(style.shapeOutside).toBe('inset(1px)')
10331061
})
10341062
})
1063+
describe('@font-face/src', () => {
1064+
test('invalid', () => {
1065+
const style = CSSFontFaceDescriptors.create(globalThis, undefined, { parentRule: fontFaceRule })
1066+
const invalid = [
1067+
'{]}, local("serif")',
1068+
'local("serif"), {]}',
1069+
]
1070+
invalid.forEach(input => {
1071+
style.src = input
1072+
expect(style.src).toBe('')
1073+
})
1074+
})
1075+
test('valid', () => {
1076+
const style = CSSFontFaceDescriptors.create(globalThis, undefined, { parentRule: fontFaceRule })
1077+
const valid = [
1078+
'local("serif"), "\n',
1079+
'local("serif"), url(bad .url)',
1080+
'local("serif"), )',
1081+
'local("serif"), ]',
1082+
'local("serif"), }',
1083+
'local("serif"), ;',
1084+
'local("serif"), !',
1085+
]
1086+
valid.forEach(input => {
1087+
style.src = input
1088+
expect(style.src).toBe('local("serif")')
1089+
})
1090+
})
1091+
})
10351092
describe('text-emphasis-position', () => {
10361093
test('valid', () => {
10371094
const style = createStyleBlock()

0 commit comments

Comments
 (0)