1use std::collections::HashMap;
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"##;
230#[derive(Clone, Debug, Default)]
239pub struct Alt {
240 pub s: Vec<String>,
242 pub b: usize,
244 pub p: Option<String>,
246 pub r: Option<String>,
248 pub a: Vec<String>,
250 pub g: String,
252}
253
254#[derive(Clone, Debug, Default)]
256pub struct RuleDef {
257 pub open: Vec<Alt>,
259 pub close: Vec<Alt>,
261}
262
263#[derive(Clone, Debug, Default)]
266pub struct Grammar {
267 rules: HashMap<String, RuleDef>,
268}
269
270impl Grammar {
271 pub fn load() -> Grammar {
279 Grammar::parse(GRAMMAR_TEXT).expect("css: embedded grammar is malformed")
280 }
281
282 pub fn parse(text: &str) -> Result<Grammar, String> {
292 let root = Reader::new(text).document()?;
293 unknown(&root, &["rule"], "the grammar document")?;
294 let mut rules = HashMap::new();
295 if let Some(GVal::Map(rule_map)) = root.get("rule") {
296 for (name, def) in rule_map {
297 let GVal::Map(def) = def else { continue };
298 unknown(def, &["open", "close"], &format!("rule {name:?}"))?;
299 rules.insert(
300 name.clone(),
301 RuleDef {
302 open: build_alts(def.get("open"), name, "open")?,
303 close: build_alts(def.get("close"), name, "close")?,
304 },
305 );
306 }
307 }
308 Ok(Grammar { rules })
309 }
310
311 pub fn rule(&self, name: &str) -> Option<&RuleDef> {
313 self.rules.get(name)
314 }
315
316 pub fn len(&self) -> usize {
318 self.rules.len()
319 }
320
321 pub fn is_empty(&self) -> bool {
323 self.rules.is_empty()
324 }
325
326 pub fn names(&self) -> impl Iterator<Item = &str> {
328 self.rules.keys().map(String::as_str)
329 }
330}
331
332pub fn grammar_text() -> &'static str {
336 GRAMMAR_TEXT
337}
338
339const ALT_FIELDS: [&str; 6] = ["s", "b", "p", "r", "a", "g"];
342
343fn build_alts(def: Option<&GVal>, rule: &str, phase: &str) -> Result<Vec<Alt>, String> {
344 let Some(GVal::List(items)) = def else {
345 return Ok(Vec::new());
346 };
347 let mut alts = Vec::with_capacity(items.len());
348 for (i, item) in items.iter().enumerate() {
349 let GVal::Map(m) = item else {
350 alts.push(Alt::default());
351 continue;
352 };
353 unknown(m, &ALT_FIELDS, &format!("rule {rule:?} {phase} alt {i}"))?;
354 alts.push(Alt {
355 s: string_list(m.get("s")),
356 b: m.get("b").and_then(GVal::as_num).unwrap_or(0.0).max(0.0) as usize,
359 p: m.get("p").and_then(GVal::as_str).map(str::to_string),
360 r: m.get("r").and_then(GVal::as_str).map(str::to_string),
361 a: string_list(m.get("a")),
362 g: m.get("g")
363 .and_then(GVal::as_str)
364 .unwrap_or_default()
365 .to_string(),
366 });
367 }
368 Ok(alts)
369}
370
371fn unknown(map: &GMap, known: &[&str], where_: &str) -> Result<(), String> {
373 for (key, _) in map {
374 if !known.contains(&key.as_str()) {
375 return Err(format!(
376 "{where_}: unknown field {key:?}. This port implements {known:?}; \
377 a grammar field the rule machine does not run must fail the \
378 build rather than be read as absent."
379 ));
380 }
381 }
382 Ok(())
383}
384
385fn string_list(def: Option<&GVal>) -> Vec<String> {
391 match def {
392 Some(GVal::Str(s)) => s.split_whitespace().map(str::to_string).collect(),
393 Some(GVal::List(items)) => items
394 .iter()
395 .filter_map(GVal::as_str)
396 .map(str::to_string)
397 .collect(),
398 _ => Vec::new(),
399 }
400}
401
402#[derive(Clone, Debug, PartialEq)]
407enum GVal {
408 Str(String),
409 Num(f64),
410 Bool(bool),
411 Map(GMap),
412 List(Vec<GVal>),
413}
414
415impl GVal {
416 fn as_str(&self) -> Option<&str> {
417 match self {
418 GVal::Str(s) => Some(s),
419 _ => None,
420 }
421 }
422 fn as_num(&self) -> Option<f64> {
423 match self {
424 GVal::Num(n) => Some(*n),
425 _ => None,
426 }
427 }
428}
429
430#[derive(Clone, Debug, Default, PartialEq)]
432struct GMap(Vec<(String, GVal)>);
433
434impl GMap {
435 fn get(&self, key: &str) -> Option<&GVal> {
436 self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
437 }
438}
439
440impl<'a> IntoIterator for &'a GMap {
441 type Item = (&'a String, &'a GVal);
442 type IntoIter = std::iter::Map<
443 std::slice::Iter<'a, (String, GVal)>,
444 fn(&'a (String, GVal)) -> (&'a String, &'a GVal),
445 >;
446 fn into_iter(self) -> Self::IntoIter {
447 self.0.iter().map(|(k, v)| (k, v))
448 }
449}
450
451struct Reader<'a> {
452 src: &'a [u8],
453 i: usize,
454}
455
456impl<'a> Reader<'a> {
457 fn new(text: &'a str) -> Reader<'a> {
458 Reader {
459 src: text.as_bytes(),
460 i: 0,
461 }
462 }
463
464 fn document(mut self) -> Result<GMap, String> {
466 self.skip_space();
467 let v = self.value()?;
468 self.skip_space();
469 if self.i < self.src.len() {
470 return Err(format!("trailing content at byte {}", self.i));
471 }
472 match v {
473 GVal::Map(m) => Ok(m),
474 other => Err(format!("grammar is not a map: {other:?}")),
475 }
476 }
477
478 fn skip_space(&mut self) {
481 while self.i < self.src.len() {
482 match self.src[self.i] {
483 b' ' | b'\t' | b'\r' | b'\n' | b',' => self.i += 1,
484 b'#' => {
485 while self.i < self.src.len() && self.src[self.i] != b'\n' {
486 self.i += 1;
487 }
488 }
489 _ => return,
490 }
491 }
492 }
493
494 fn value(&mut self) -> Result<GVal, String> {
495 self.skip_space();
496 match self.src.get(self.i) {
497 None => Err("unexpected end of grammar".to_string()),
498 Some(b'{') => self.map(),
499 Some(b'[') => self.list(),
500 Some(b'\'') | Some(b'"') => Ok(GVal::Str(self.quoted()?)),
501 Some(_) => Ok(scalar(self.bare())),
502 }
503 }
504
505 fn map(&mut self) -> Result<GVal, String> {
506 self.i += 1; let mut out = GMap::default();
508 loop {
509 self.skip_space();
510 match self.src.get(self.i) {
511 None => return Err("unclosed '{' in grammar".to_string()),
512 Some(b'}') => {
513 self.i += 1;
514 return Ok(GVal::Map(out));
515 }
516 _ => {}
517 }
518 let key = match self.src[self.i] {
519 b'\'' | b'"' => self.quoted()?,
520 _ => self.bare(),
521 };
522 self.skip_space();
523 if self.src.get(self.i) != Some(&b':') {
524 return Err(format!("expected ':' after key '{key}' in grammar"));
525 }
526 self.i += 1;
527 let value = self.value()?;
528 out.0.push((key, value));
529 }
530 }
531
532 fn list(&mut self) -> Result<GVal, String> {
533 self.i += 1; let mut out = Vec::new();
535 loop {
536 self.skip_space();
537 match self.src.get(self.i) {
538 None => return Err("unclosed '[' in grammar".to_string()),
539 Some(b']') => {
540 self.i += 1;
541 return Ok(GVal::List(out));
542 }
543 _ => out.push(self.value()?),
544 }
545 }
546 }
547
548 fn quoted(&mut self) -> Result<String, String> {
550 let quote = self.src[self.i];
551 self.i += 1;
552 let mut out = String::new();
553 while self.i < self.src.len() {
554 let c = self.src[self.i];
555 if c == quote {
556 self.i += 1;
557 return Ok(out);
558 }
559 if c == b'\\' && self.i + 1 < self.src.len() {
560 self.i += 1;
561 }
562 let next = next_char_end(self.src, self.i);
563 out.push_str(&lossy(&self.src[self.i..next]));
564 self.i = next;
565 }
566 Err("unclosed string in grammar".to_string())
567 }
568
569 fn bare(&mut self) -> String {
571 let start = self.i;
572 while self.i < self.src.len() {
573 match self.src[self.i] {
574 b' ' | b'\t' | b'\r' | b'\n' | b',' | b':' | b'{' | b'}' | b'[' | b']' | b'#' => {
575 break
576 }
577 _ => self.i += 1,
578 }
579 }
580 lossy(&self.src[start..self.i])
581 }
582}
583
584fn scalar(token: String) -> GVal {
586 match token.as_str() {
587 "true" => return GVal::Bool(true),
588 "false" => return GVal::Bool(false),
589 _ => {}
590 }
591 match token.parse::<f64>() {
592 Ok(n) => GVal::Num(n),
593 Err(_) => GVal::Str(token),
594 }
595}
596
597fn next_char_end(src: &[u8], i: usize) -> usize {
599 let mut e = i + 1;
600 while e < src.len() && (src[e] & 0xC0) == 0x80 {
601 e += 1;
602 }
603 e
604}
605
606fn lossy(bytes: &[u8]) -> String {
607 String::from_utf8_lossy(bytes).into_owned()
608}