1use std::sync::OnceLock;
17
18use tabnas::GrammarSpec;
19
20use crate::value::Value;
21
22const GRAMMAR_TEXT: &str = r##"
24# CSS Grammar Definition (AST)
25# Parses CSS into a reworkcss-style abstract syntax tree: ordered, typed
26# nodes that preserve declaration order, duplicate properties, rule types
27# and comments.
28#
29# { type: 'stylesheet', rules: [ Node, ... ] }
30# rule { type:'rule', selectors:[string], declarations:[Node] }
31# declaration { type:'declaration', property:string, value:string }
32# comment { type:'comment', comment:string }
33# at-rules (media/supports/import/keyframes/font-face/...) — see below
34#
35# Example:
36# a { color: red; color: blue } /* note */
37# parses to:
38# { type:'stylesheet', rules: [
39# { type:'rule', selectors:['a'], declarations:[
40# { type:'declaration', property:'color', value:'red' },
41# { type:'declaration', property:'color', value:'blue' } ] },
42# { type:'comment', comment:' note ' } ] }
43#
44# The cssToken lex matcher emits: #TX (one selector or a property name),
45# #GC (a top-level selector-group comma), #VL (a declaration value),
46# #CC (a comment, at a statement position), and the at-rule tokens
47# #ATR/#ATD/#ATK/#ATS (carrying the keyword in val and the prelude in
48# use). Fixed "{" "}" ":" lex as #OB #CB #CL; ";" is remapped to #CA.
49#
50# Every alt is tagged with the 'css' group via { rule: { alt: { g: 'css' } } }.
51
52{
53 rule: {
54 # The top-level stylesheet node. "items" fills its rules[].
55 stylesheet: {
56 open: [
57 { a: '@cssSheet' p: items g: 'css,sheet' }
58 ]
59 close: [
60 { s: '#ZZ' a: '@cssEnd' g: 'css,sheet,end' }
61 ]
62 }
63
64 # A statement list (the stylesheet body or an at-rule's rules body). Reads
65 # rule / at-rule / comment nodes into the enclosing node's rules[]. Each
66 # iteration pushes one "statement" child, then @cssPushRule appends it.
67 items: {
68 open: [
69 { s: '#ZZ' b: 1 g: 'css,items,end' }
70 { s: '#CB' b: 1 g: 'css,items,endblock' }
71 { p: statement g: 'css,items' }
72 ]
73 close: [
74 { s: '#ZZ' b: 1 a: '@cssPushRule' g: 'css,items,end' }
75 { s: '#CB' b: 1 a: '@cssPushRule' g: 'css,items,endblock' }
76 { a: '@cssPushRule' r: items g: 'css,items,next' }
77 ]
78 }
79
80 # Builds ONE statement node (resetting from the inherited list node).
81 statement: {
82 open: [
83 # A comment node.
84 { s: '#CC' a: '@cssComment' g: 'css,comment' }
85 # A block at-rule whose body is rules (e.g. @media): push items.
86 { s: '#ATR' a: '@cssAtRules' p: rulesbody g: 'css,atrules' }
87 # A block at-rule whose body is declarations (e.g. @font-face).
88 { s: '#ATD' a: '@cssAtDecls' p: declbody g: 'css,atdecls' }
89 # @keyframes: a body of keyframe blocks.
90 { s: '#ATK' a: '@cssKeyframes' p: kfbody g: 'css,keyframes' }
91 # A statement at-rule (e.g. @import "x"): a leaf node.
92 { s: '#ATS' a: '@cssAtStmt' g: 'css,atstmt' }
93 # A style rule: selectors + declarations.
94 { s: '#TX' b: 1 a: '@cssRule' p: sel g: 'css,rule' }
95 ]
96 close: [
97 { b: 1 g: 'css,statement,end' }
98 ]
99 }
100
101 # Reads a selector group into the (rule/page) node's selectors[], then the
102 # declaration block. Each selector is one #TX; #GC separates a group.
103 sel: {
104 open: [
105 { s: '#TX #GC' a: '@cssSelector' r: sel g: 'css,sel,group' }
106 { s: '#TX #OB' b: 1 a: '@cssSelector' p: declbody g: 'css,sel,last' }
107 ]
108 close: [
109 { b: 1 g: 'css,sel,end' }
110 ]
111 }
112
113 # The "{ ... }" wrapper around a declaration list; fills the parent node's
114 # declarations[].
115 declbody: {
116 open: [
117 { s: '#OB #CB' b: 1 g: 'css,declbody,empty' }
118 { s: '#OB' p: decls g: 'css,declbody' }
119 ]
120 close: [
121 { s: '#CB' a: '@cssEnd' g: 'css,declbody,end' }
122 ]
123 }
124
125 # A declaration list: declaration / comment nodes, ";"-separated.
126 decls: {
127 open: [
128 { s: '#CB' b: 1 g: 'css,decls,empty' }
129 { p: decl g: 'css,decls' }
130 ]
131 close: [
132 { s: '#CA #CB' b: 1 a: '@cssPushDecl' g: 'css,decls,trailing' }
133 { s: '#CB' b: 1 a: '@cssPushDecl' g: 'css,decls,end' }
134 { s: '#CA' a: '@cssPushDecl' r: decls g: 'css,decls,next' }
135 { a: '@cssPushDecl' r: decls g: 'css,decls,comment' }
136 ]
137 }
138
139 # Builds ONE block member: a declaration, a comment, or — for CSS
140 # Nesting — a nested style rule or at-rule. Disambiguated by the token
141 # after the key: #TX #CL is a declaration; #TX followed by "{"/"," is a
142 # nested rule; #ATR/#ATD/#ATK/#ATS is a nested at-rule.
143 decl: {
144 open: [
145 { s: '#CC' a: '@cssComment' g: 'css,comment' }
146 { s: '#TX #CL' a: '@cssDecl' p: declval g: 'css,decl' }
147 # Nested at-rules (CSS Nesting).
148 { s: '#ATR' a: '@cssAtRules' p: rulesbody g: 'css,nest,atrules' }
149 { s: '#ATD' a: '@cssAtDecls' p: declbody g: 'css,nest,atdecls' }
150 { s: '#ATK' a: '@cssKeyframes' p: kfbody g: 'css,nest,keyframes' }
151 { s: '#ATS' a: '@cssAtStmt' g: 'css,nest,atstmt' }
152 # A nested style rule (selector first, no ":").
153 { s: '#TX' b: 1 a: '@cssRule' p: sel g: 'css,nest,rule' }
154 ]
155 close: [
156 { b: 1 g: 'css,decl,end' }
157 ]
158 }
159
160 # The value of a declaration (a single #VL run).
161 declval: {
162 open: [
163 { s: '#VL' a: '@cssDeclVal' g: 'css,declval' }
164 { b: 1 g: 'css,declval,empty' }
165 ]
166 close: [
167 { b: 1 g: 'css,declval,end' }
168 ]
169 }
170
171 # The "{ ... }" rules body of a block at-rule (@media/@supports/...).
172 rulesbody: {
173 open: [
174 { s: '#OB #CB' b: 1 g: 'css,rulesbody,empty' }
175 { s: '#OB' p: items g: 'css,rulesbody' }
176 ]
177 close: [
178 { s: '#CB' a: '@cssEnd' g: 'css,rulesbody,end' }
179 ]
180 }
181
182 # The "{ ... }" body of @keyframes: a list of keyframe blocks.
183 kfbody: {
184 open: [
185 { s: '#OB #CB' b: 1 g: 'css,kfbody,empty' }
186 { s: '#OB' p: kfitems g: 'css,kfbody' }
187 ]
188 close: [
189 { s: '#CB' a: '@cssEnd' g: 'css,kfbody,end' }
190 ]
191 }
192
193 # A list of keyframe blocks (and comments) -> the keyframes node's
194 # keyframes[].
195 kfitems: {
196 open: [
197 { s: '#CB' b: 1 g: 'css,kfitems,empty' }
198 { p: keyframe g: 'css,kfitems' }
199 ]
200 close: [
201 { s: '#CB' b: 1 a: '@cssPushKf' g: 'css,kfitems,end' }
202 { a: '@cssPushKf' r: kfitems g: 'css,kfitems,next' }
203 ]
204 }
205
206 # One keyframe block: values (0%, 50%, from, to) + declarations. Mirrors
207 # "statement"+"sel" but builds a 'keyframe' node with values[].
208 keyframe: {
209 open: [
210 { s: '#CC' a: '@cssComment' g: 'css,comment' }
211 { s: '#TX' b: 1 a: '@cssKeyframe' p: kfsel g: 'css,keyframe' }
212 ]
213 close: [
214 { b: 1 g: 'css,keyframe,end' }
215 ]
216 }
217
218 kfsel: {
219 open: [
220 { s: '#TX #GC' a: '@cssKfValue' r: kfsel g: 'css,kfsel,group' }
221 { s: '#TX #OB' b: 1 a: '@cssKfValue' p: declbody g: 'css,kfsel,last' }
222 ]
223 close: [
224 { b: 1 g: 'css,kfsel,end' }
225 ]
226 }
227 }
228}
229"##;
230pub fn grammar_text() -> &'static str {
235 GRAMMAR_TEXT
236}
237
238pub(crate) const OPTIONS_DOC: &str = r##"{"options": {
266 "rule": {"exclude": "jsonic,imp", "start": "stylesheet", "history": 1},
267 "fixed": {"token": {"#CA": ";", "#OS": null, "#CS": null}},
268 "tokenSet": {"KEY": ["#TX"]},
269 "string": {"chars": ""},
270 "number": {"lex": false},
271 "text": {"lex": false},
272 "value": {"lex": false},
273 "comment": {"lex": true, "def": {
274 "hash": {"lex": false},
275 "slash": {"lex": false},
276 "multi": {"line": false, "start": "/*", "end": "*/", "lex": true}}},
277 "lex": {
278 "emptyResult": {"type": "stylesheet", "rules": []},
279 "match": {"cssToken": {"order": 100000, "make": "@css-token"}}
280 },
281 "parse": {"prepare": {"css": "@css-prepare"}}
282}}"##;
283
284pub(crate) const RULES: [&str; 13] = [
289 "stylesheet",
290 "items",
291 "statement",
292 "sel",
293 "declbody",
294 "decls",
295 "decl",
296 "declval",
297 "rulesbody",
298 "kfbody",
299 "kfitems",
300 "keyframe",
301 "kfsel",
302];
303
304pub(crate) struct Specs {
307 pub(crate) options: GrammarSpec,
308 pub(crate) rules: GrammarSpec,
309}
310
311pub(crate) fn specs() -> Result<&'static Specs, String> {
319 static SPECS: OnceLock<Result<Specs, String>> = OnceLock::new();
320 SPECS
321 .get_or_init(|| {
322 let options = GrammarSpec::from_json(OPTIONS_DOC).map_err(|e| e.0)?;
323 let parsed = tabnas_jsonic::make()
324 .parse(GRAMMAR_TEXT)
325 .map_err(|e| format!("css-grammar.jsonic does not read as jsonic: {e}"))?;
326 let rules =
327 GrammarSpec::from_json(&Value::from_engine(&parsed).to_json()).map_err(|e| e.0)?;
328 Ok(Specs { options, rules })
329 })
330 .as_ref()
331 .map_err(Clone::clone)
332}