tabnas_css/value.rs
1/* Copyright (c) 2025 Richard Rodger, MIT License */
2
3//! The AST value model: [`Node`], an insertion-ordered map with a `type`
4//! discriminator, and [`Value`], what a node field can hold.
5//!
6//! The TypeScript port returns plain JavaScript objects and the Go port
7//! returns `map[string]any`. The engine's own value, [`tabnas::Value`], is
8//! what the plugin form returns too, but [`Css::parse`](crate::Css::parse)
9//! returns this one, for two reasons: every traversal of it is iterative, so
10//! a tree of any depth can be dropped, cloned, compared, printed and written
11//! as JSON, where the engine's recurses once per level; and it keeps
12//! JavaScript's `undefined` (below). Insertion order is preserved because the
13//! AST is usually read back as JSON and `{"type": …}` first reads better than
14//! a sorted map — the conformance runners compare key sets, not key order,
15//! so nothing depends on it.
16//!
17//! [`Value::Undefined`] is the one non-obvious variant. It is JavaScript's
18//! `undefined`: a key that EXISTS in insertion order but is absent from the
19//! serialised JSON. The canonical port writes `node.position = { start, end:
20//! undefined }` at construction and fills `end` in later, so a node whose end
21//! is never recorded (a declaration with an empty value, say) serialises as
22//! `{"start":…}` with no `end` at all. Emitting `null` there instead would be
23//! a different AST.
24
25use std::fmt;
26use std::fmt::Write as _;
27use std::sync::Arc;
28
29use tabnas::Value as Engine;
30
31/// A value held by an AST node field.
32///
33/// Every traversal of a value is ITERATIVE: dropping, cloning, comparing,
34/// writing it as JSON and formatting it for [`std::fmt::Debug`]. An AST is as
35/// deep as the CSS that produced it, so a derived implementation of any of
36/// them would recurse once per level and abort the process on nothing worse
37/// than untrusted input. None of them is derived, for that reason.
38pub enum Value {
39 /// A key that exists but serialises to nothing (JavaScript `undefined`).
40 Undefined,
41 /// JSON `null`.
42 Null,
43 /// JSON `true` / `false`.
44 Bool(bool),
45 /// A JSON number. Integral values serialise without a decimal point.
46 Num(f64),
47 /// A JSON string.
48 Str(String),
49 /// A JSON array.
50 List(Vec<Value>),
51 /// A nested AST node (a JSON object).
52 Node(Node),
53}
54
55impl Value {
56 /// The string held here, if this is a [`Value::Str`].
57 pub fn as_str(&self) -> Option<&str> {
58 match self {
59 Value::Str(s) => Some(s),
60 _ => None,
61 }
62 }
63
64 /// The node held here, if this is a [`Value::Node`].
65 pub fn as_node(&self) -> Option<&Node> {
66 match self {
67 Value::Node(n) => Some(n),
68 _ => None,
69 }
70 }
71
72 /// The list held here, if this is a [`Value::List`].
73 pub fn as_list(&self) -> Option<&[Value]> {
74 match self {
75 Value::List(l) => Some(l),
76 _ => None,
77 }
78 }
79
80 /// This value as JSON. `Undefined` renders as `null` at the top level and
81 /// inside an array — only an object KEY can be dropped entirely, which is
82 /// what JavaScript's `JSON.stringify` does too.
83 pub fn to_json(&self) -> String {
84 let mut out = String::new();
85 write_json(self, &mut out);
86 out
87 }
88}
89
90impl From<&str> for Value {
91 fn from(s: &str) -> Self {
92 Value::Str(s.to_string())
93 }
94}
95
96impl From<String> for Value {
97 fn from(s: String) -> Self {
98 Value::Str(s)
99 }
100}
101
102impl From<Node> for Value {
103 fn from(n: Node) -> Self {
104 Value::Node(n)
105 }
106}
107
108/// An AST node: an insertion-ordered string-keyed map.
109///
110/// Like [`Value`], every traversal of it is iterative rather than derived.
111#[derive(Default)]
112pub struct Node {
113 entries: Vec<(String, Value)>,
114}
115
116impl Node {
117 /// An empty node.
118 pub fn new() -> Self {
119 Node {
120 entries: Vec::new(),
121 }
122 }
123
124 /// The node's `type` discriminator, if it has one.
125 pub fn node_type(&self) -> Option<&str> {
126 self.get("type").and_then(Value::as_str)
127 }
128
129 /// The value at `key`, if the key is present.
130 pub fn get(&self, key: &str) -> Option<&Value> {
131 self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
132 }
133
134 /// A mutable reference to the value at `key`, if the key is present.
135 pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
136 self.entries
137 .iter_mut()
138 .find(|(k, _)| k == key)
139 .map(|(_, v)| v)
140 }
141
142 /// Set `key`, keeping its existing position when it is already present
143 /// (as assigning to a JavaScript object property does).
144 pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
145 let key = key.into();
146 let value = value.into();
147 match self.get_mut(&key) {
148 Some(slot) => *slot = value,
149 None => self.entries.push((key, value)),
150 }
151 }
152
153 /// Append to the list at `key`, creating the list if it is absent.
154 pub fn push_to(&mut self, key: &str, value: Value) {
155 match self.get_mut(key) {
156 Some(Value::List(list)) => list.push(value),
157 Some(slot) => *slot = Value::List(vec![value]),
158 None => self
159 .entries
160 .push((key.to_string(), Value::List(vec![value]))),
161 }
162 }
163
164 /// The entries, in insertion order.
165 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
166 self.entries.iter().map(|(k, v)| (k.as_str(), v))
167 }
168
169 /// How many keys the node holds, `Undefined` ones included.
170 pub fn len(&self) -> usize {
171 self.entries.len()
172 }
173
174 /// Whether the node holds no keys at all.
175 pub fn is_empty(&self) -> bool {
176 self.entries.is_empty()
177 }
178
179 /// This node as JSON. Keys holding [`Value::Undefined`] are omitted.
180 pub fn to_json(&self) -> String {
181 let mut out = String::new();
182 write_steps(vec![Emit::Node(self)], &mut out, Mode::Json);
183 out
184 }
185}
186
187/// Dismantle a node iteratively when it is dropped.
188///
189/// An AST is as deep as the CSS that produced it, and a derived drop would
190/// recurse once per level — so a stylesheet nested a few thousand rules deep
191/// would abort the process, on nothing worse than untrusted input, at the
192/// moment the tree went out of scope. The engine keeps its own rule stack
193/// for the same reason; this is the other half of that promise.
194impl Drop for Node {
195 fn drop(&mut self) {
196 let mut pending: Vec<Value> = self.entries.drain(..).map(|(_, v)| v).collect();
197 while let Some(value) = pending.pop() {
198 match value {
199 Value::List(items) => pending.extend(items),
200 // `node` is emptied here, so its own drop finds nothing left
201 // to do and does not recurse.
202 Value::Node(mut node) => {
203 pending.extend(node.entries.drain(..).map(|(_, v)| v));
204 }
205 _ => {}
206 }
207 }
208 }
209}
210
211/// What [`write_steps`] is producing.
212#[derive(Clone, Copy, PartialEq, Eq)]
213enum Mode {
214 /// JSON: an `Undefined` key is dropped, as `JSON.stringify` drops a key
215 /// whose value is `undefined`.
216 Json,
217 /// A debug view: an `Undefined` key is kept and shown, because the point
218 /// of the debug view is to show what is there.
219 Debug,
220}
221
222/// One step of the iterative writer.
223enum Emit<'a> {
224 /// Write this value (pushing more steps for a node or a list).
225 Value(&'a Value),
226 /// Write this node — the same work as `Value(Value::Node(..))`, for a
227 /// node that is not wrapped in a [`Value`].
228 Node(&'a Node),
229 /// Write this object key and the `:` after it.
230 Key(&'a str),
231 /// Write this punctuation verbatim.
232 Raw(&'static str),
233}
234
235/// Write a value as JSON, iteratively.
236///
237/// Iterative for the same reason [`Node`]'s drop is: recursing once per
238/// nesting level turns a deeply nested stylesheet into a stack overflow, and
239/// serialising the AST is the most ordinary thing a caller does with it.
240fn write_json(root: &Value, out: &mut String) {
241 write_steps(vec![Emit::Value(root)], out, Mode::Json)
242}
243
244fn write_steps(mut steps: Vec<Emit<'_>>, out: &mut String, mode: Mode) {
245 while let Some(step) = steps.pop() {
246 match step {
247 Emit::Raw(text) => out.push_str(text),
248 Emit::Key(key) => {
249 write_json_string(key, out);
250 out.push(':');
251 }
252 Emit::Value(Value::Node(node)) | Emit::Node(node) => {
253 out.push('{');
254 steps.push(Emit::Raw("}"));
255 let entries: Vec<(&str, &Value)> = node
256 .iter()
257 .filter(|(_, v)| Mode::Debug == mode || !matches!(v, Value::Undefined))
258 .collect();
259 // Pushed in reverse so they come back off the stack in order.
260 for (i, (key, value)) in entries.iter().enumerate().rev() {
261 steps.push(Emit::Value(value));
262 steps.push(Emit::Key(key));
263 if 0 < i {
264 steps.push(Emit::Raw(","));
265 }
266 }
267 }
268 Emit::Value(Value::List(items)) => {
269 out.push('[');
270 steps.push(Emit::Raw("]"));
271 for (i, item) in items.iter().enumerate().rev() {
272 steps.push(Emit::Value(item));
273 if 0 < i {
274 steps.push(Emit::Raw(","));
275 }
276 }
277 }
278 Emit::Value(Value::Undefined) => out.push_str(match mode {
279 Mode::Json => "null",
280 Mode::Debug => "undefined",
281 }),
282 Emit::Value(Value::Null) => out.push_str("null"),
283 Emit::Value(Value::Bool(b)) => out.push_str(if *b { "true" } else { "false" }),
284 Emit::Value(Value::Num(n)) => write_num(*n, out),
285 Emit::Value(Value::Str(s)) => write_json_string(s, out),
286 }
287 }
288}
289
290/// The debug view is the JSON, with `Undefined` keys shown rather than
291/// dropped.
292///
293/// Not derived, and not a struct-shaped `{:#?}` dump. A derived formatter
294/// recurses once per nesting level, so `format!("{ast:?}")` on a stylesheet a
295/// few thousand rules deep aborts the process — on an AST whose parse, drop
296/// and serialisation are all depth-safe, and in the one operation a caller
297/// reaches for while debugging. This shares the iterative writer instead.
298impl fmt::Debug for Value {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 let mut out = String::new();
301 write_steps(vec![Emit::Value(self)], &mut out, Mode::Debug);
302 f.write_str(&out)
303 }
304}
305
306impl fmt::Debug for Node {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 let mut out = String::new();
309 write_steps(vec![Emit::Node(self)], &mut out, Mode::Debug);
310 f.write_str(&out)
311 }
312}
313
314/// One step of the iterative deep clone.
315enum CloneStep<'a> {
316 /// Clone this value, pushing more steps for a node or a list.
317 Enter(&'a Value),
318 /// Take the last `n` cloned values off the output and make a node of
319 /// them, under this node's keys.
320 CloseNode(&'a Node),
321 /// Take the last `n` cloned values off the output and make a list.
322 CloseList(usize),
323}
324
325/// A deep clone, iteratively.
326///
327/// A derived `Clone` recurses once per nesting level. Cloning a parsed AST is
328/// an ordinary thing to do, so it is depth-safe here like everything else.
329fn clone_value(root: &Value) -> Value {
330 let mut steps = vec![CloneStep::Enter(root)];
331 let mut done: Vec<Value> = Vec::new();
332 while let Some(step) = steps.pop() {
333 match step {
334 // Children are pushed after the close step, so they pop first and
335 // land on `done` in order, before the close reads them back.
336 CloneStep::Enter(Value::Node(node)) => {
337 steps.push(CloneStep::CloseNode(node));
338 for (_, value) in node.entries.iter().rev() {
339 steps.push(CloneStep::Enter(value));
340 }
341 }
342 CloneStep::Enter(Value::List(items)) => {
343 steps.push(CloneStep::CloseList(items.len()));
344 for item in items.iter().rev() {
345 steps.push(CloneStep::Enter(item));
346 }
347 }
348 CloneStep::Enter(leaf) => done.push(match leaf {
349 Value::Undefined => Value::Undefined,
350 Value::Null => Value::Null,
351 Value::Bool(b) => Value::Bool(*b),
352 Value::Num(n) => Value::Num(*n),
353 Value::Str(s) => Value::Str(s.clone()),
354 // Handled by the arms above.
355 Value::List(_) | Value::Node(_) => unreachable!(),
356 }),
357 CloneStep::CloseNode(node) => {
358 let values = done.split_off(done.len() - node.entries.len());
359 let entries = node
360 .entries
361 .iter()
362 .map(|(k, _)| k.clone())
363 .zip(values)
364 .collect();
365 done.push(Value::Node(Node { entries }));
366 }
367 CloneStep::CloseList(n) => {
368 let items = done.split_off(done.len() - n);
369 done.push(Value::List(items));
370 }
371 }
372 }
373 done.pop().expect("the clone produced no value")
374}
375
376impl Clone for Value {
377 fn clone(&self) -> Value {
378 clone_value(self)
379 }
380}
381
382impl Clone for Node {
383 fn clone(&self) -> Node {
384 let mut out = Node::new();
385 // One level by hand, then the iterative walk for each field, so that
386 // cloning a node never wraps it in a temporary Value.
387 for (key, value) in &self.entries {
388 out.entries.push((key.clone(), clone_value(value)));
389 }
390 out
391 }
392}
393
394/// An equality walk, iteratively, for the same reason as the rest.
395fn values_eq(a: &Value, b: &Value) -> bool {
396 let mut pending: Vec<(&Value, &Value)> = vec![(a, b)];
397 while let Some((a, b)) = pending.pop() {
398 match (a, b) {
399 (Value::Undefined, Value::Undefined) | (Value::Null, Value::Null) => {}
400 (Value::Bool(a), Value::Bool(b)) if a == b => {}
401 (Value::Num(a), Value::Num(b)) if a == b => {}
402 (Value::Str(a), Value::Str(b)) if a == b => {}
403 (Value::List(a), Value::List(b)) if a.len() == b.len() => {
404 pending.extend(a.iter().zip(b.iter()));
405 }
406 (Value::Node(a), Value::Node(b)) if a.entries.len() == b.entries.len() => {
407 for ((ak, av), (bk, bv)) in a.entries.iter().zip(b.entries.iter()) {
408 if ak != bk {
409 return false;
410 }
411 pending.push((av, bv));
412 }
413 }
414 _ => return false,
415 }
416 }
417 true
418}
419
420impl PartialEq for Value {
421 fn eq(&self, other: &Value) -> bool {
422 values_eq(self, other)
423 }
424}
425
426impl PartialEq for Node {
427 fn eq(&self, other: &Node) -> bool {
428 self.entries.len() == other.entries.len()
429 && self
430 .entries
431 .iter()
432 .zip(other.entries.iter())
433 .all(|((ak, _), (bk, _))| ak == bk)
434 && self
435 .entries
436 .iter()
437 .zip(other.entries.iter())
438 .all(|((_, av), (_, bv))| values_eq(av, bv))
439 }
440}
441
442/// Write a JSON number. Integral values lose the `.0` Rust would print, so
443/// a line number reads as `2` and not `2.0`.
444fn write_num(n: f64, out: &mut String) {
445 if n.is_finite() && n == n.trunc() && n.abs() < 1e15 {
446 let _ = write!(out, "{}", n as i64);
447 } else if n.is_finite() {
448 let _ = write!(out, "{}", n);
449 } else {
450 // JSON has no infinity or NaN; JavaScript's JSON.stringify writes
451 // null for both.
452 out.push_str("null");
453 }
454}
455
456/// Write a JSON string literal, escaping exactly what `JSON.stringify` does.
457fn write_json_string(s: &str, out: &mut String) {
458 out.push('"');
459 for c in s.chars() {
460 match c {
461 '"' => out.push_str("\\\""),
462 '\\' => out.push_str("\\\\"),
463 '\n' => out.push_str("\\n"),
464 '\r' => out.push_str("\\r"),
465 '\t' => out.push_str("\\t"),
466 '\u{08}' => out.push_str("\\b"),
467 '\u{0c}' => out.push_str("\\f"),
468 c if (c as u32) < 0x20 => {
469 let _ = write!(out, "\\u{:04x}", c as u32);
470 }
471 c => out.push(c),
472 }
473 }
474 out.push('"');
475}
476
477impl Value {
478 /// An engine value as this crate's, field for field.
479 ///
480 /// RECURSIVE, so only for values whose depth this crate controls: the
481 /// grammar document read from the embedded text. A parse result goes
482 /// through [`Value::from_arena`] instead.
483 pub(crate) fn from_engine(value: &Engine) -> Value {
484 match value {
485 Engine::Array(items) => Value::List(items.iter().map(Value::from_engine).collect()),
486 Engine::Object(fields) => {
487 let mut node = Node::new();
488 for (key, field) in fields.iter() {
489 node.set(key.clone(), Value::from_engine(field));
490 }
491 Value::Node(node)
492 }
493 scalar => Value::from_scalar(scalar),
494 }
495 }
496
497 fn from_scalar(value: &Engine) -> Value {
498 match value {
499 Engine::String(s) => Value::Str(s.clone()),
500 Engine::Number(n) => Value::Num(*n),
501 Engine::Bool(b) => Value::Bool(*b),
502 Engine::Null => Value::Null,
503 _ => Value::Undefined,
504 }
505 }
506
507 /// A parse result from the plugin's node store as the tree it describes.
508 ///
509 /// [`Css::parse`](crate::Css::parse) runs the grammar with each node kept
510 /// as one flat record in a per-parse list, and a child list holding the
511 /// ids of the children (`rules`, `declarations`, `keyframes`), so the
512 /// engine never holds a nested value it would drop or copy by recursion.
513 /// Every child's constructor runs after its parent's, so a child's id is
514 /// always greater than its parent's: walking the records from the last to
515 /// the first finds each child already built, and moves it into place. No
516 /// step recurses, whatever the depth.
517 ///
518 /// A record's `position` has an `end` only once one was recorded; here an
519 /// absent `end` becomes [`Value::Undefined`], the key the canonical port
520 /// writes at construction and fills in later.
521 ///
522 /// The engine's `emptyResult` for `""` arrives as an object rather than a
523 /// list, and is converted as it is: an empty stylesheet with no position,
524 /// as in the canonical port.
525 pub(crate) fn from_arena(value: Engine) -> Value {
526 match value {
527 Engine::Array(records) => {
528 let records = Arc::try_unwrap(records).unwrap_or_else(|shared| (*shared).clone());
529 let mut built: Vec<Option<Value>> = Vec::with_capacity(records.len());
530 built.resize_with(records.len(), || None);
531 for (id, record) in records.into_iter().enumerate().rev() {
532 let mut node = Node::new();
533 if let Engine::Object(fields) = record {
534 for (key, field) in fields.iter() {
535 let field = match field {
536 Engine::Array(items) => Value::List(
537 items
538 .iter()
539 .map(|item| match item {
540 Engine::Number(child) => built
541 .get_mut(*child as usize)
542 .and_then(Option::take)
543 .unwrap_or(Value::Null),
544 other => Value::from_scalar(other),
545 })
546 .collect(),
547 ),
548 Engine::Object(_) if "position" == key => position(field),
549 other => Value::from_scalar(other),
550 };
551 node.set(key.clone(), field);
552 }
553 }
554 built[id] = Some(Value::Node(node));
555 }
556 built
557 .first_mut()
558 .and_then(Option::take)
559 .unwrap_or(Value::Undefined)
560 }
561 Engine::Object(fields) => {
562 let mut node = Node::new();
563 for (key, field) in fields.iter() {
564 let field = match field {
565 Engine::Array(items) => {
566 Value::List(items.iter().map(Value::from_scalar).collect())
567 }
568 other => Value::from_scalar(other),
569 };
570 node.set(key.clone(), field);
571 }
572 Value::Node(node)
573 }
574 other => Value::from_scalar(&other),
575 }
576 }
577}
578
579/// A record's `position`, `{start, end}`, with an unrecorded `end` as
580/// [`Value::Undefined`].
581fn position(value: &Engine) -> Value {
582 let mut out = Node::new();
583 let fields = match value {
584 Engine::Object(fields) => Some(fields),
585 _ => None,
586 };
587 for key in ["start", "end"] {
588 match fields.and_then(|fields| fields.get(key)) {
589 Some(Engine::Object(point)) => {
590 let mut at = Node::new();
591 for (name, n) in point.iter() {
592 at.set(name.clone(), Value::from_scalar(n));
593 }
594 out.set(key, Value::Node(at));
595 }
596 _ => out.set(key, Value::Undefined),
597 }
598 }
599 Value::Node(out)
600}