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`. Rust has no such universal value, so this module
8//! supplies one. Insertion order is preserved because the AST is usually read
9//! back as JSON and `{"type": …}` first reads better than a sorted map — the
10//! conformance runners compare key sets, not key order, so nothing depends on
11//! it.
12//!
13//! [`Value::Undefined`] is the one non-obvious variant. It is JavaScript's
14//! `undefined`: a key that EXISTS in insertion order but is absent from the
15//! serialised JSON. The canonical port writes `node.position = { start, end:
16//! undefined }` at construction and fills `end` in later, so a node whose end
17//! is never recorded (a declaration with an empty value, say) serialises as
18//! `{"start":…}` with no `end` at all. Emitting `null` there instead would be
19//! a different AST.
20
21use std::fmt;
22use std::fmt::Write as _;
23
24/// A value held by an AST node field.
25///
26/// Every traversal of a value is ITERATIVE: dropping, cloning, comparing,
27/// writing it as JSON and formatting it for [`std::fmt::Debug`]. An AST is as
28/// deep as the CSS that produced it, so a derived implementation of any of
29/// them would recurse once per level and abort the process on nothing worse
30/// than untrusted input. None of them is derived, for that reason.
31pub enum Value {
32 /// A key that exists but serialises to nothing (JavaScript `undefined`).
33 Undefined,
34 /// JSON `null`.
35 Null,
36 /// JSON `true` / `false`.
37 Bool(bool),
38 /// A JSON number. Integral values serialise without a decimal point.
39 Num(f64),
40 /// A JSON string.
41 Str(String),
42 /// A JSON array.
43 List(Vec<Value>),
44 /// A nested AST node (a JSON object).
45 Node(Node),
46}
47
48impl Value {
49 /// The string held here, if this is a [`Value::Str`].
50 pub fn as_str(&self) -> Option<&str> {
51 match self {
52 Value::Str(s) => Some(s),
53 _ => None,
54 }
55 }
56
57 /// The node held here, if this is a [`Value::Node`].
58 pub fn as_node(&self) -> Option<&Node> {
59 match self {
60 Value::Node(n) => Some(n),
61 _ => None,
62 }
63 }
64
65 /// The list held here, if this is a [`Value::List`].
66 pub fn as_list(&self) -> Option<&[Value]> {
67 match self {
68 Value::List(l) => Some(l),
69 _ => None,
70 }
71 }
72
73 /// This value as JSON. `Undefined` renders as `null` at the top level and
74 /// inside an array — only an object KEY can be dropped entirely, which is
75 /// what JavaScript's `JSON.stringify` does too.
76 pub fn to_json(&self) -> String {
77 let mut out = String::new();
78 write_json(self, &mut out);
79 out
80 }
81}
82
83impl From<&str> for Value {
84 fn from(s: &str) -> Self {
85 Value::Str(s.to_string())
86 }
87}
88
89impl From<String> for Value {
90 fn from(s: String) -> Self {
91 Value::Str(s)
92 }
93}
94
95impl From<Node> for Value {
96 fn from(n: Node) -> Self {
97 Value::Node(n)
98 }
99}
100
101/// An AST node: an insertion-ordered string-keyed map.
102///
103/// Like [`Value`], every traversal of it is iterative rather than derived.
104#[derive(Default)]
105pub struct Node {
106 entries: Vec<(String, Value)>,
107}
108
109impl Node {
110 /// An empty node.
111 pub fn new() -> Self {
112 Node {
113 entries: Vec::new(),
114 }
115 }
116
117 /// The node's `type` discriminator, if it has one.
118 pub fn node_type(&self) -> Option<&str> {
119 self.get("type").and_then(Value::as_str)
120 }
121
122 /// The value at `key`, if the key is present.
123 pub fn get(&self, key: &str) -> Option<&Value> {
124 self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
125 }
126
127 /// A mutable reference to the value at `key`, if the key is present.
128 pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
129 self.entries
130 .iter_mut()
131 .find(|(k, _)| k == key)
132 .map(|(_, v)| v)
133 }
134
135 /// Set `key`, keeping its existing position when it is already present
136 /// (as assigning to a JavaScript object property does).
137 pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
138 let key = key.into();
139 let value = value.into();
140 match self.get_mut(&key) {
141 Some(slot) => *slot = value,
142 None => self.entries.push((key, value)),
143 }
144 }
145
146 /// Append to the list at `key`, creating the list if it is absent.
147 pub fn push_to(&mut self, key: &str, value: Value) {
148 match self.get_mut(key) {
149 Some(Value::List(list)) => list.push(value),
150 Some(slot) => *slot = Value::List(vec![value]),
151 None => self
152 .entries
153 .push((key.to_string(), Value::List(vec![value]))),
154 }
155 }
156
157 /// The entries, in insertion order.
158 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
159 self.entries.iter().map(|(k, v)| (k.as_str(), v))
160 }
161
162 /// How many keys the node holds, `Undefined` ones included.
163 pub fn len(&self) -> usize {
164 self.entries.len()
165 }
166
167 /// Whether the node holds no keys at all.
168 pub fn is_empty(&self) -> bool {
169 self.entries.is_empty()
170 }
171
172 /// This node as JSON. Keys holding [`Value::Undefined`] are omitted.
173 pub fn to_json(&self) -> String {
174 let mut out = String::new();
175 write_steps(vec![Emit::Node(self)], &mut out, Mode::Json);
176 out
177 }
178}
179
180/// Dismantle a node iteratively when it is dropped.
181///
182/// An AST is as deep as the CSS that produced it, and a derived drop would
183/// recurse once per level — so a stylesheet nested a few thousand rules deep
184/// would abort the process, on nothing worse than untrusted input, at the
185/// moment the tree went out of scope. The rule machine keeps its own stack
186/// for the same reason; this is the other half of that promise.
187impl Drop for Node {
188 fn drop(&mut self) {
189 let mut pending: Vec<Value> = self.entries.drain(..).map(|(_, v)| v).collect();
190 while let Some(value) = pending.pop() {
191 match value {
192 Value::List(items) => pending.extend(items),
193 // `node` is emptied here, so its own drop finds nothing left
194 // to do and does not recurse.
195 Value::Node(mut node) => {
196 pending.extend(node.entries.drain(..).map(|(_, v)| v));
197 }
198 _ => {}
199 }
200 }
201 }
202}
203
204/// What [`write_steps`] is producing.
205#[derive(Clone, Copy, PartialEq, Eq)]
206enum Mode {
207 /// JSON: an `Undefined` key is dropped, as `JSON.stringify` drops a key
208 /// whose value is `undefined`.
209 Json,
210 /// A debug view: an `Undefined` key is kept and shown, because the point
211 /// of the debug view is to show what is there.
212 Debug,
213}
214
215/// One step of the iterative writer.
216enum Emit<'a> {
217 /// Write this value (pushing more steps for a node or a list).
218 Value(&'a Value),
219 /// Write this node — the same work as `Value(Value::Node(..))`, for a
220 /// node that is not wrapped in a [`Value`].
221 Node(&'a Node),
222 /// Write this object key and the `:` after it.
223 Key(&'a str),
224 /// Write this punctuation verbatim.
225 Raw(&'static str),
226}
227
228/// Write a value as JSON, iteratively.
229///
230/// Iterative for the same reason [`Node`]'s drop is: recursing once per
231/// nesting level turns a deeply nested stylesheet into a stack overflow, and
232/// serialising the AST is the most ordinary thing a caller does with it.
233fn write_json(root: &Value, out: &mut String) {
234 write_steps(vec![Emit::Value(root)], out, Mode::Json)
235}
236
237fn write_steps(mut steps: Vec<Emit<'_>>, out: &mut String, mode: Mode) {
238 while let Some(step) = steps.pop() {
239 match step {
240 Emit::Raw(text) => out.push_str(text),
241 Emit::Key(key) => {
242 write_json_string(key, out);
243 out.push(':');
244 }
245 Emit::Value(Value::Node(node)) | Emit::Node(node) => {
246 out.push('{');
247 steps.push(Emit::Raw("}"));
248 let entries: Vec<(&str, &Value)> = node
249 .iter()
250 .filter(|(_, v)| Mode::Debug == mode || !matches!(v, Value::Undefined))
251 .collect();
252 // Pushed in reverse so they come back off the stack in order.
253 for (i, (key, value)) in entries.iter().enumerate().rev() {
254 steps.push(Emit::Value(value));
255 steps.push(Emit::Key(key));
256 if 0 < i {
257 steps.push(Emit::Raw(","));
258 }
259 }
260 }
261 Emit::Value(Value::List(items)) => {
262 out.push('[');
263 steps.push(Emit::Raw("]"));
264 for (i, item) in items.iter().enumerate().rev() {
265 steps.push(Emit::Value(item));
266 if 0 < i {
267 steps.push(Emit::Raw(","));
268 }
269 }
270 }
271 Emit::Value(Value::Undefined) => out.push_str(match mode {
272 Mode::Json => "null",
273 Mode::Debug => "undefined",
274 }),
275 Emit::Value(Value::Null) => out.push_str("null"),
276 Emit::Value(Value::Bool(b)) => out.push_str(if *b { "true" } else { "false" }),
277 Emit::Value(Value::Num(n)) => write_num(*n, out),
278 Emit::Value(Value::Str(s)) => write_json_string(s, out),
279 }
280 }
281}
282
283/// The debug view is the JSON, with `Undefined` keys shown rather than
284/// dropped.
285///
286/// Not derived, and not a struct-shaped `{:#?}` dump. A derived formatter
287/// recurses once per nesting level, so `format!("{ast:?}")` on a stylesheet a
288/// few thousand rules deep aborts the process — on an AST whose parse, drop
289/// and serialisation are all depth-safe, and in the one operation a caller
290/// reaches for while debugging. This shares the iterative writer instead.
291impl fmt::Debug for Value {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 let mut out = String::new();
294 write_steps(vec![Emit::Value(self)], &mut out, Mode::Debug);
295 f.write_str(&out)
296 }
297}
298
299impl fmt::Debug for Node {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 let mut out = String::new();
302 write_steps(vec![Emit::Node(self)], &mut out, Mode::Debug);
303 f.write_str(&out)
304 }
305}
306
307/// One step of the iterative deep clone.
308enum CloneStep<'a> {
309 /// Clone this value, pushing more steps for a node or a list.
310 Enter(&'a Value),
311 /// Take the last `n` cloned values off the output and make a node of
312 /// them, under this node's keys.
313 CloseNode(&'a Node),
314 /// Take the last `n` cloned values off the output and make a list.
315 CloseList(usize),
316}
317
318/// A deep clone, iteratively.
319///
320/// A derived `Clone` recurses once per nesting level. Cloning a parsed AST is
321/// an ordinary thing to do, so it is depth-safe here like everything else.
322fn clone_value(root: &Value) -> Value {
323 let mut steps = vec![CloneStep::Enter(root)];
324 let mut done: Vec<Value> = Vec::new();
325 while let Some(step) = steps.pop() {
326 match step {
327 // Children are pushed after the close step, so they pop first and
328 // land on `done` in order, before the close reads them back.
329 CloneStep::Enter(Value::Node(node)) => {
330 steps.push(CloneStep::CloseNode(node));
331 for (_, value) in node.entries.iter().rev() {
332 steps.push(CloneStep::Enter(value));
333 }
334 }
335 CloneStep::Enter(Value::List(items)) => {
336 steps.push(CloneStep::CloseList(items.len()));
337 for item in items.iter().rev() {
338 steps.push(CloneStep::Enter(item));
339 }
340 }
341 CloneStep::Enter(leaf) => done.push(match leaf {
342 Value::Undefined => Value::Undefined,
343 Value::Null => Value::Null,
344 Value::Bool(b) => Value::Bool(*b),
345 Value::Num(n) => Value::Num(*n),
346 Value::Str(s) => Value::Str(s.clone()),
347 // Handled by the arms above.
348 Value::List(_) | Value::Node(_) => unreachable!(),
349 }),
350 CloneStep::CloseNode(node) => {
351 let values = done.split_off(done.len() - node.entries.len());
352 let entries = node
353 .entries
354 .iter()
355 .map(|(k, _)| k.clone())
356 .zip(values)
357 .collect();
358 done.push(Value::Node(Node { entries }));
359 }
360 CloneStep::CloseList(n) => {
361 let items = done.split_off(done.len() - n);
362 done.push(Value::List(items));
363 }
364 }
365 }
366 done.pop().expect("the clone produced no value")
367}
368
369impl Clone for Value {
370 fn clone(&self) -> Value {
371 clone_value(self)
372 }
373}
374
375impl Clone for Node {
376 fn clone(&self) -> Node {
377 let mut out = Node::new();
378 // One level by hand, then the iterative walk for each field, so that
379 // cloning a node never wraps it in a temporary Value.
380 for (key, value) in &self.entries {
381 out.entries.push((key.clone(), clone_value(value)));
382 }
383 out
384 }
385}
386
387/// An equality walk, iteratively, for the same reason as the rest.
388fn values_eq(a: &Value, b: &Value) -> bool {
389 let mut pending: Vec<(&Value, &Value)> = vec![(a, b)];
390 while let Some((a, b)) = pending.pop() {
391 match (a, b) {
392 (Value::Undefined, Value::Undefined) | (Value::Null, Value::Null) => {}
393 (Value::Bool(a), Value::Bool(b)) if a == b => {}
394 (Value::Num(a), Value::Num(b)) if a == b => {}
395 (Value::Str(a), Value::Str(b)) if a == b => {}
396 (Value::List(a), Value::List(b)) if a.len() == b.len() => {
397 pending.extend(a.iter().zip(b.iter()));
398 }
399 (Value::Node(a), Value::Node(b)) if a.entries.len() == b.entries.len() => {
400 for ((ak, av), (bk, bv)) in a.entries.iter().zip(b.entries.iter()) {
401 if ak != bk {
402 return false;
403 }
404 pending.push((av, bv));
405 }
406 }
407 _ => return false,
408 }
409 }
410 true
411}
412
413impl PartialEq for Value {
414 fn eq(&self, other: &Value) -> bool {
415 values_eq(self, other)
416 }
417}
418
419impl PartialEq for Node {
420 fn eq(&self, other: &Node) -> bool {
421 self.entries.len() == other.entries.len()
422 && self
423 .entries
424 .iter()
425 .zip(other.entries.iter())
426 .all(|((ak, _), (bk, _))| ak == bk)
427 && self
428 .entries
429 .iter()
430 .zip(other.entries.iter())
431 .all(|((_, av), (_, bv))| values_eq(av, bv))
432 }
433}
434
435/// Write a JSON number. Integral values lose the `.0` Rust would print, so
436/// a line number reads as `2` and not `2.0`.
437fn write_num(n: f64, out: &mut String) {
438 if n.is_finite() && n == n.trunc() && n.abs() < 1e15 {
439 let _ = write!(out, "{}", n as i64);
440 } else if n.is_finite() {
441 let _ = write!(out, "{}", n);
442 } else {
443 // JSON has no infinity or NaN; JavaScript's JSON.stringify writes
444 // null for both.
445 out.push_str("null");
446 }
447}
448
449/// Write a JSON string literal, escaping exactly what `JSON.stringify` does.
450fn write_json_string(s: &str, out: &mut String) {
451 out.push('"');
452 for c in s.chars() {
453 match c {
454 '"' => out.push_str("\\\""),
455 '\\' => out.push_str("\\\\"),
456 '\n' => out.push_str("\\n"),
457 '\r' => out.push_str("\\r"),
458 '\t' => out.push_str("\\t"),
459 '\u{08}' => out.push_str("\\b"),
460 '\u{0c}' => out.push_str("\\f"),
461 c if (c as u32) < 0x20 => {
462 let _ = write!(out, "\\u{:04x}", c as u32);
463 }
464 c => out.push(c),
465 }
466 }
467 out.push('"');
468}