Skip to main content

tabnas_css/
plugin.rs

1/* Copyright (c) 2025 Richard Rodger, MIT License */
2
3//! The plugin: installing the grammar on an engine, and the actions that
4//! build the AST from what it matches.
5//!
6//! `makeActions` in `ts/src/css.ts` is the canonical form. Its node
7//! constructors REBIND `r.node`, and its setters and pushers mutate the node
8//! object a rule inherited from its parent by reference. The engine here
9//! shares one node CELL between a rule and the rules it pushes or replaces
10//! into, so the constructors install a fresh cell rather than write into the
11//! one they were handed, which would overwrite the parent's node; the setters
12//! and pushers write through the shared cell, as the canonical ones do
13//! through the shared object.
14//!
15//! A node is kept in one of two stores, chosen per parse:
16//!
17//! - **The tree**, for a parse through the engine's own API
18//!   ([`make`](crate::make), [`plugin`](crate::plugin),
19//!   [`Css::tabnas`](crate::Css::tabnas)): each cell holds the node itself,
20//!   an engine object, and a child is moved into its parent's list when it is
21//!   complete. The engine returns the stylesheet, and with recovery on a
22//!   partial one, which is not always the canonical port's (see
23//!   `doc/concepts.md`). The engine's value drops, copies and prints by
24//!   recursion, one frame per level, so this form is bounded at
25//!   [`TREE_RULE_DEPTH`] open rules by a parse guard.
26//! - **The arena**, for [`Css::parse`](crate::Css::parse), which asks for it
27//!   with [`arena_meta`]: every node is one flat record in a
28//!   per-parse list in `ctx.u`, a cell holds the record's id, and a child list
29//!   holds ids. The engine never holds a nested value, so no depth reaches a
30//!   recursion; [`Value::from_arena`](crate::Value) builds the crate's own
31//!   tree from the list without one, and this form has no depth limit.
32
33use std::cell::RefCell;
34use std::rc::Rc;
35use std::sync::{Arc, OnceLock};
36
37use tabnas::{ActionError, Context, GrammarSetting, PluginError, Rule, Tabnas, Token, Value};
38
39use crate::grammar::{specs, RULES};
40use crate::lex::{
41    self, col_width, es_is_whitespace, es_trim, split_selectors, vendor_prefix, BRACE_SCAN,
42    OVERSHOOT,
43};
44use crate::Options;
45
46/// The most rules the tree form lets be open at once.
47///
48/// 768 frames: 191 nested style rules (each takes four: `decls`, `decl`,
49/// `sel`, `declbody`) or 256 nested `@media` blocks (three: `items`,
50/// `statement`, `rulesbody`). A tree at the limit survives being dropped,
51/// cloned, compared, printed and written as JSON on a 2 MiB thread, the
52/// smallest stack a spawned thread gets by default, in a debug build. A
53/// deeper document fails with `cancel`. [`Css::parse`](crate::Css::parse)
54/// has no such limit, and neither does the canonical port: this is a
55/// Rust-only refusal, registered in `test/divergent.tsv`.
56pub const TREE_RULE_DEPTH: usize = 768;
57
58/// The name the tree form's bound is installed under, as a parse guard.
59const DEPTH_GUARD: &str = "tabnas-css/depth";
60
61/// The key of the meta [`arena_meta`] builds, for whoever reads it.
62const ARENA: &str = "tabnas-css/arena";
63
64/// The arena: the `ctx.u` key holding the per-parse list of node records.
65const NODES: &str = "tabnas-css/nodes";
66
67/// The `ctx.u` key holding the scalar positions of the source's astral
68/// characters, built when the first position is recorded.
69const ASTRAL: &str = "tabnas-css/astral";
70
71/// The meta [`Css::parse`](crate::Css::parse) passes to ask for the arena
72/// store; a parse without it builds the tree.
73///
74/// Recognised by its ADDRESS, not by its contents: the engine's API lets a
75/// caller pass meta of its own, and a key a caller could spell would let it
76/// switch the store, and with it the tree form's bound, from outside. Every
77/// call hands out the one allocation made here, which a caller has no way
78/// to make, and the engine passes meta to the context as it was given.
79pub(crate) fn arena_meta() -> Value {
80    arena_marker().clone()
81}
82
83fn arena_marker() -> &'static Value {
84    static META: OnceLock<Value> = OnceLock::new();
85    META.get_or_init(|| {
86        let mut meta = object();
87        put(&mut meta, ARENA, Value::Bool(true));
88        meta
89    })
90}
91
92/// Whether this parse builds into the arena.
93fn arena(ctx: &Context) -> bool {
94    match (&ctx.meta, arena_marker()) {
95        (Value::Object(meta), Value::Object(ours)) => Arc::ptr_eq(meta, ours),
96        _ => false,
97    }
98}
99
100fn object() -> Value {
101    Value::object(Default::default())
102}
103
104fn list() -> Value {
105    Value::array(Vec::new())
106}
107
108fn text(s: &str) -> Value {
109    Value::String(s.to_string())
110}
111
112/// Set `key` where assigning a JavaScript object property puts it: in its
113/// place when it is already present, and otherwise last, unless it is an
114/// array index, which JavaScript enumerates first, in ascending order. An
115/// at-keyword is a key (`@0 x;` is `{"0": "x", "type": "0"}` in the
116/// canonical port), so the order reaches the JSON.
117fn put(target: &mut Value, key: &str, value: Value) {
118    let Some(fields) = target.as_object_mut() else {
119        return;
120    };
121    match array_index(key) {
122        Some(index) if !fields.contains_key(key) => {
123            let at = fields
124                .keys()
125                .take_while(|k| array_index(k).is_some_and(|other| other < index))
126                .count();
127            fields.shift_insert(at, key.to_string(), value);
128        }
129        _ => {
130            fields.insert(key.to_string(), value);
131        }
132    }
133}
134
135/// The array index `key` names, if it names one: the canonical decimal
136/// form of an integer below 2^32 - 1, as ECMAScript defines one.
137fn array_index(key: &str) -> Option<u32> {
138    let canonical = !key.is_empty()
139        && key.bytes().all(|b| b.is_ascii_digit())
140        && (key == "0" || !key.starts_with('0'));
141    if !canonical {
142        return None;
143    }
144    key.parse::<u32>().ok().filter(|&index| index < u32::MAX)
145}
146
147/// Append to the list at `field`.
148fn push_to(target: &mut Value, field: &str, item: Value) {
149    let Some(fields) = target.as_object_mut() else {
150        return;
151    };
152    match fields.get_mut(field) {
153        Some(Value::Array(items)) => Arc::make_mut(items).push(item),
154        _ => {
155            fields.insert(field.to_string(), Value::array(vec![item]));
156        }
157    }
158}
159
160/// The arena's list of records.
161fn nodes(ctx: &mut Context) -> &mut Vec<Value> {
162    let slot = ctx.u.entry(NODES.to_string()).or_insert_with(list);
163    if slot.as_array_mut().is_none() {
164        *slot = list();
165    }
166    slot.as_array_mut().expect("the arena was just made a list")
167}
168
169/// Make `record` this rule's node, in a FRESH cell. The cell a rule starts
170/// with is its parent's (a push or a replace shares it), and the canonical
171/// constructor's `r.node = …` rebinds the child's name without touching the
172/// parent's object; writing into the shared cell would overwrite the parent.
173fn install_node(rule: &mut Rule, ctx: &mut Context, record: Value) {
174    let node = if arena(ctx) {
175        let records = nodes(ctx);
176        let id = records.len();
177        records.push(record);
178        Value::Number(id as f64)
179    } else {
180        record
181    };
182    rule.node = Rc::new(RefCell::new(node));
183}
184
185/// Edit this rule's node where it lives: in the arena, or in the cell.
186fn with_node(rule: &Rule, ctx: &mut Context, edit: impl FnOnce(&mut Value)) {
187    if arena(ctx) {
188        let id = match *rule.node.borrow() {
189            Value::Number(id) => id as usize,
190            _ => return,
191        };
192        if let Some(record) = nodes(ctx).get_mut(id) {
193            edit(record);
194        }
195    } else {
196        edit(&mut rule.node.borrow_mut());
197    }
198}
199
200// ---------------------------------------------------------------------------
201// Positions.
202
203/// The column of scalar `pos`, in UTF-16 code units, from the engine's
204/// column `ci` there.
205///
206/// The engine counts a column in Unicode scalars since the last reset (a
207/// `\n` in any token, or the end of a run of line characters); the canonical
208/// port counts UTF-16 code units since the same points. The difference is the
209/// astral characters in between, looked up in a sorted list built once per
210/// parse, so the cost per position is a binary search, and nothing at all for
211/// a source with no astral character.
212fn col16(ctx: &mut Context, pos: usize, ci: usize) -> usize {
213    if !ctx.u.contains_key(ASTRAL) {
214        let wide: Vec<Value> = if ctx.source.bytes().any(|b| 0xF0 <= b) {
215            ctx.source
216                .chars()
217                .enumerate()
218                .filter(|(_, c)| 2 == c.len_utf16())
219                .map(|(i, _)| Value::Number(i as f64))
220                .collect()
221        } else {
222            Vec::new()
223        };
224        ctx.u.insert(ASTRAL.to_string(), Value::array(wide));
225    }
226    let Some(Value::Array(wide)) = ctx.u.get(ASTRAL) else {
227        return ci;
228    };
229    if wide.is_empty() {
230        return ci;
231    }
232    let at = |v: &Value| match v {
233        Value::Number(n) => *n as usize,
234        _ => 0,
235    };
236    // The end-of-source token's column carries the scanners' overshoot, so
237    // this window can start before the reset; the scalar there is a reset
238    // character or before the source, never astral.
239    let from = pos.saturating_sub(ci.saturating_sub(1));
240    let low = wide.partition_point(|v| at(v) < from);
241    let high = wide.partition_point(|v| at(v) < pos);
242    ci + (high - low)
243}
244
245fn point(line: usize, column: usize) -> Value {
246    let mut at = object();
247    put(&mut at, "line", Value::Number(line as f64));
248    put(&mut at, "column", Value::Number(column as f64));
249    at
250}
251
252/// A token's first character: `startPos` in the canonical port.
253fn start_pos(ctx: &mut Context, token: &Token) -> Value {
254    let column = col16(ctx, token.site.pos, token.site.ci);
255    point(token.site.ri, column)
256}
257
258/// Just after a token's last character: `endPos` in the canonical port,
259/// measured over the token's source text.
260fn end_pos(ctx: &mut Context, token: &Token) -> Value {
261    let src = token.src.as_str();
262    match src.rfind('\n') {
263        Some(last) => point(
264            token.site.ri + src.matches('\n').count(),
265            1 + col_width(&src[last + 1..]),
266        ),
267        None => {
268            let column = col16(ctx, token.site.pos, token.site.ci);
269            point(token.site.ri, column + col_width(src))
270        }
271    }
272}
273
274/// `withPos` in the canonical port: a `position` for a node built from the
275/// rule's first matched token, with its end when `end` is set. An end not
276/// recorded here is absent until an action records it.
277fn with_pos(rule: &Rule, ctx: &mut Context, position: bool, end: bool) -> Option<Value> {
278    if !position {
279        return None;
280    }
281    let token = rule.o0()?;
282    let mut at = object();
283    put(&mut at, "start", start_pos(ctx, token));
284    if end {
285        put(&mut at, "end", end_pos(ctx, token));
286    }
287    Some(at)
288}
289
290/// Record the end of this rule's node, if it has a position.
291fn set_end(rule: &Rule, ctx: &mut Context, end: Value) {
292    with_node(rule, ctx, |record| {
293        if let Some(at) = record.as_object_mut().and_then(|f| f.get_mut("position")) {
294            put(at, "end", end);
295        }
296    });
297}
298
299// ---------------------------------------------------------------------------
300// Node builders: `makeAtRules`, `makeAtDecls`, `makeKeyframes` and
301// `makeAtStmt` in the canonical port. Key order is JavaScript's: a key
302// assigned twice keeps its first place.
303
304fn token_text(token: &Token) -> String {
305    match &token.val {
306        Value::String(s) => s.clone(),
307        _ => String::new(),
308    }
309}
310
311fn use_text(token: &Token, key: &str) -> String {
312    match token.use_data().get(key) {
313        Some(Value::String(s)) => s.clone(),
314        _ => String::new(),
315    }
316}
317
318/// A block at-rule whose body is a list of rules (`@media`, `@supports`,
319/// `@document`, `@host`, and any other with a rules body).
320fn make_at_rules(token: &Token) -> Value {
321    let kw = token_text(token);
322    let prelude = use_text(token, "prelude");
323    let mut node = object();
324    match kw.as_str() {
325        "host" => put(&mut node, "type", text("host")),
326        _ if "document" == kw || kw.ends_with("-document") => {
327            // `vendor` is always present on a document node, the empty
328            // string when the at-keyword carries no prefix.
329            put(&mut node, "type", text("document"));
330            put(&mut node, "document", text(&prelude));
331            put(&mut node, "vendor", text(vendor_prefix(&kw).unwrap_or("")));
332        }
333        _ => {
334            put(&mut node, "type", text(&kw));
335            put(&mut node, &kw, text(&prelude));
336        }
337    }
338    put(&mut node, "rules", list());
339    node
340}
341
342/// A block at-rule whose body is declarations (`@font-face`, `@page`, and
343/// any other with a declarations body). A `@page` prelude is a selector
344/// group: `@page toc, index:blank`.
345fn make_at_decls(token: &Token) -> Value {
346    let kw = token_text(token);
347    let mut node = object();
348    put(&mut node, "type", text(&kw));
349    if "page" == kw {
350        let selectors = split_selectors(&use_text(token, "prelude"))
351            .into_iter()
352            .map(Value::String)
353            .collect();
354        put(&mut node, "selectors", Value::array(selectors));
355    }
356    put(&mut node, "declarations", list());
357    node
358}
359
360/// A `@keyframes` block, possibly vendor-prefixed.
361fn make_keyframes(token: &Token) -> Value {
362    let kw = token_text(token);
363    let mut node = object();
364    put(&mut node, "type", text("keyframes"));
365    put(&mut node, "name", text(&use_text(token, "prelude")));
366    if let Some(vendor) = vendor_prefix(&kw) {
367        put(&mut node, "vendor", text(vendor));
368    }
369    put(&mut node, "keyframes", list());
370    node
371}
372
373/// A statement at-rule (`@import`, `@charset`, `@namespace`, …): the
374/// at-keyword is the node type and the field carrying its params.
375///
376/// `@custom-media --name <query>` splits its params into a name and a media
377/// query, as `/^(--\S+)\s*([\s\S]*)$/` does in the canonical port: `--` and
378/// at least one non-space character, then the rest, trimmed.
379fn make_at_stmt(token: &Token) -> Value {
380    let kw = token_text(token);
381    let params = use_text(token, "params");
382    let mut node = object();
383    if "custom-media" == kw && params.starts_with("--") {
384        let name_end = params.find(es_is_whitespace).unwrap_or(params.len());
385        if 2 < name_end {
386            put(&mut node, "type", text("custom-media"));
387            put(&mut node, "name", text(&params[..name_end]));
388            put(&mut node, "media", text(es_trim(&params[name_end..])));
389            return node;
390        }
391    }
392    put(&mut node, "type", text(&kw));
393    put(&mut node, &kw, text(&params));
394    node
395}
396
397// ---------------------------------------------------------------------------
398// The actions.
399
400type Outcome = Result<(), ActionError>;
401
402/// The text of the rule's first matched token, which the engine keeps after
403/// a `b: 1` backtrack.
404fn o0_text(rule: &Rule) -> String {
405    rule.o0().map(token_text).unwrap_or_default()
406}
407
408/// Append the completed child's node to this rule's node's `field`.
409///
410/// A child that built no node of its own is skipped. Every rule these
411/// pushers follow (`statement`, `decl`, `keyframe`) builds its node in each
412/// open alternate, so such a child failed before its constructor ran, which
413/// only the engine's recovery lets a parse survive. It still holds this
414/// rule's cell, and the engine then leaves `child_node` undefined, where
415/// `child_value()` would answer with this rule's own node. The canonical
416/// port pushes that node into itself there (`undefined !== c` sees the
417/// parent), a cycle, which a value here cannot hold; pushing a copy would
418/// put a stylesheet inside its own `rules`.
419fn push_child(rule: &Rule, ctx: &mut Context, field: &str) {
420    if rule.child_node.is_undefined() {
421        return;
422    }
423    let child = rule.child_node.clone();
424    with_node(rule, ctx, |record| push_to(record, field, child));
425}
426
427/// Register every action the grammar names, capturing the options each
428/// needs. Registration replaces by name, so a second install with other
429/// options takes effect.
430fn register_actions(parser: &mut Tabnas, position: bool) {
431    parser.action_with_context("@cssSheet", move |rule, ctx| -> Outcome {
432        let mut node = object();
433        put(&mut node, "type", text("stylesheet"));
434        put(&mut node, "rules", list());
435        if position {
436            let mut at = object();
437            put(&mut at, "start", point(1, 1));
438            put(&mut node, "position", at);
439        }
440        install_node(rule, ctx, node);
441        Ok(())
442    });
443
444    // The constructors of nodes built from the rule's first token: a start
445    // position always, and an end only for the single-token nodes.
446    type Build = fn(&Rule) -> Value;
447    let simple: [(&str, Build, bool); 4] = [
448        (
449            "@cssRule",
450            |_| {
451                let mut node = object();
452                put(&mut node, "type", text("rule"));
453                put(&mut node, "selectors", list());
454                put(&mut node, "declarations", list());
455                node
456            },
457            false,
458        ),
459        (
460            "@cssDecl",
461            |rule| {
462                let mut node = object();
463                put(&mut node, "type", text("declaration"));
464                put(&mut node, "property", Value::String(o0_text(rule)));
465                put(&mut node, "value", text(""));
466                node
467            },
468            false,
469        ),
470        (
471            "@cssComment",
472            |rule| {
473                let mut node = object();
474                put(&mut node, "type", text("comment"));
475                put(&mut node, "comment", Value::String(o0_text(rule)));
476                node
477            },
478            true,
479        ),
480        (
481            "@cssKeyframe",
482            |_| {
483                let mut node = object();
484                put(&mut node, "type", text("keyframe"));
485                put(&mut node, "values", list());
486                put(&mut node, "declarations", list());
487                node
488            },
489            false,
490        ),
491    ];
492    for (name, build, end) in simple {
493        parser.action_with_context(name, move |rule, ctx| -> Outcome {
494            let mut node = build(rule);
495            if let Some(at) = with_pos(rule, ctx, position, end) {
496                put(&mut node, "position", at);
497            }
498            install_node(rule, ctx, node);
499            Ok(())
500        });
501    }
502
503    type AtBuild = fn(&Token) -> Value;
504    let at_rules: [(&str, AtBuild, bool); 4] = [
505        ("@cssAtRules", make_at_rules, false),
506        ("@cssAtDecls", make_at_decls, false),
507        ("@cssKeyframes", make_keyframes, false),
508        ("@cssAtStmt", make_at_stmt, true),
509    ];
510    for (name, build, end) in at_rules {
511        parser.action_with_context(name, move |rule, ctx| -> Outcome {
512            let mut node = rule.o0().map(build).unwrap_or_else(object);
513            if let Some(at) = with_pos(rule, ctx, position, end) {
514                put(&mut node, "position", at);
515            }
516            install_node(rule, ctx, node);
517            Ok(())
518        });
519    }
520
521    // Field setters: they change the node the rule inherited.
522    for (name, field) in [("@cssSelector", "selectors"), ("@cssKfValue", "values")] {
523        parser.action_with_context(name, move |rule, ctx| -> Outcome {
524            let value = Value::String(o0_text(rule));
525            with_node(rule, ctx, |record| push_to(record, field, value));
526            Ok(())
527        });
528    }
529    parser.action_with_context("@cssDeclVal", move |rule, ctx| -> Outcome {
530        let value = Value::String(o0_text(rule));
531        with_node(rule, ctx, |record| put(record, "value", value));
532        if position {
533            if let Some(token) = rule.o0() {
534                let end = end_pos(ctx, token);
535                set_end(rule, ctx, end);
536            }
537        }
538        Ok(())
539    });
540
541    // The closing brace, or the end of the source for the stylesheet: the
542    // end of the node the block belongs to. A close phase, so the matched
543    // token is the rule's first CLOSE token.
544    //
545    // At the stylesheet's end an arena parse also hands back the whole list,
546    // as the root rule's node, for `Value::from_arena` to assemble: nothing
547    // runs after this close but the engine taking that node as the result.
548    // It is done here rather than in a rule lifecycle action so that the
549    // installed rules are the canonical port's, alternate for alternate and
550    // with no hook it lacks. A tree parse's node is already the stylesheet.
551    parser.action_with_context("@cssEnd", move |rule, ctx| -> Outcome {
552        if position {
553            if let Some(token) = rule.c0() {
554                let end = end_pos(ctx, token);
555                set_end(rule, ctx, end);
556            }
557        }
558        if "stylesheet" == rule.name.as_str() && arena(ctx) {
559            let records = ctx.u.shift_remove(NODES).unwrap_or_else(list);
560            rule.node = Rc::new(RefCell::new(records));
561        }
562        Ok(())
563    });
564
565    // List pushers: a completed child node into its parent's list.
566    for (name, field) in [
567        ("@cssPushRule", "rules"),
568        ("@cssPushDecl", "declarations"),
569        ("@cssPushKf", "keyframes"),
570    ] {
571        parser.action_with_context(name, move |rule, ctx| -> Outcome {
572            push_child(rule, ctx, field);
573            Ok(())
574        });
575    }
576}
577
578/// Whether `action` is this grammar's `@cssSheet`: as installed, or as
579/// [`Tabnas::merge`] renames it, `@<tag>:cssSheet`.
580fn is_css_sheet(action: &str) -> bool {
581    "@cssSheet" == action || (action.starts_with('@') && action.ends_with(":cssSheet"))
582}
583
584/// Install the plugin on `parser`. See [`plugin`](crate::plugin).
585///
586/// Safe to run more than once on one instance, and it is: `use_plugin` runs
587/// it again when the same plugin is used again with other options, and
588/// [`Tabnas::derive`] runs it on every derived instance. A second run
589/// replaces the matcher and the actions (which capture the options) and
590/// installs the rules afresh; the lex subscriber, which captures nothing, is
591/// added only by the first, since subscribers are not named and would
592/// otherwise run twice.
593pub(crate) fn install(parser: &mut Tabnas, options: &Options) -> Result<(), PluginError> {
594    let specs = specs().map_err(PluginError)?;
595    // Installed before on THIS instance when its `stylesheet` rule runs this
596    // grammar's `@cssSheet`. A rule of that name from another grammar is not
597    // this one, and the lex subscriber must still be added. The rule table
598    // is rebuilt on `derive`, as the subscribers are, so the two agree there.
599    let first = !parser.rule_specs().iter().any(|spec| {
600        "stylesheet" == spec.name
601            && spec
602                .open
603                .iter()
604                .any(|alt| alt.a.iter().any(|action| is_css_sheet(action)))
605    });
606
607    // The matcher emits these by name (see `lex::BY_NAME`); registering
608    // them here gives them numbers before the rules that name them.
609    for name in ["#CC", "#ATR", "#ATD", "#ATK", "#ATS", "#GC"] {
610        parser.token(name);
611    }
612    let lower = options.lowercase_properties;
613    parser.imperative_lex_match_ref("@css-token", move |lexer, rule, ctx| {
614        lex::css_token(lexer, rule, ctx, lower)
615    });
616    register_actions(parser, options.position);
617
618    // The plugin's per-parse state starts empty on every parse, whatever
619    // the caller seeded `ctx.u` with.
620    parser.parse_prepare_ref("@css-prepare", |ctx| {
621        ctx.u.shift_remove(NODES);
622        ctx.u.shift_remove(ASTRAL);
623        ctx.u.shift_remove(OVERSHOOT);
624        ctx.u.shift_remove(BRACE_SCAN);
625    });
626    if first {
627        parser.subscribe_lex(lex::lex_subscriber);
628    }
629
630    // A name of the plugin's own. A later plugin that installs a guard
631    // named `depth`, as jsonic, json and the grammars layered on them do,
632    // replaces only that one, and this bound stays. jsonic's own `depth`
633    // guard is removed: it counts `map` and `list` rules, which the
634    // grammar's options exclude, so it could never refuse a css parse, and
635    // it ran on every step, about 1% of a flat stylesheet's parse. The
636    // count comes first because it is the cheap test.
637    parser.remove_parse_guard("depth");
638    parser.parse_guard(DEPTH_GUARD, |ctx| {
639        ctx.rule_stack.len() <= TREE_RULE_DEPTH || arena(ctx)
640    });
641
642    if !first {
643        for name in RULES {
644            parser.remove_rule(name);
645        }
646    }
647    parser
648        .grammar(&specs.options)
649        .map_err(|e| PluginError(e.0))?;
650    parser
651        .grammar_with_setting(&specs.rules, &GrammarSetting::groups("css"))
652        .map_err(|e| PluginError(e.0))?;
653    Ok(())
654}