interscript_rs/item.rs
1//! The item model — mirrors `Interscript::Node::Item` as produced by
2//! `Interscript::Isc::NodeAdapter` from a parsed map.
3
4use crate::stdlib::Prim;
5
6/// A `sub`-rule item tree (`from`, `to`, constraint items).
7#[derive(Debug, Clone, PartialEq)]
8pub enum Item {
9 /// Literal string (Item::String).
10 Str(String),
11 /// The `none` keyword — empty string.
12 None,
13 /// Zero-width / class primitive (NodeAdapter: Alias(stdlib symbol)).
14 Prim(Prim),
15 /// upcase/downcase function item. Only usable as a `sub` target and
16 /// only as Upcase/Downcase (the Ruby adapter raises on the rest).
17 Func(FuncName),
18 /// Alias reference: `map` for `dep_alias.name` qualified refs.
19 Alias { map: Option<String>, name: String },
20 /// capture(...). `id` is the 1-based regex group number, assigned
21 /// by the builder in lookaround assembly order
22 /// (before, not_before, from, not_after, after).
23 CaptureGroup { id: usize, inner: Box<Item> },
24 /// ref(N) — backreference to the Nth capture group.
25 CaptureRef(usize),
26 /// maybe(...)
27 Maybe(Box<Item>),
28 /// some(...)
29 Some(Box<Item>),
30 /// any("lo".."hi") — inclusive codepoint range.
31 Range(char, char),
32 /// any(...) — char set / alternation list (entries may be nested
33 /// Items: ranges, sets, primitives, alias refs).
34 Set(Vec<Item>),
35 /// Concatenation (`+` / whitespace joins). Also Item::Group.
36 Concat(Vec<Item>),
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum FuncName {
41 Upcase,
42 Downcase,
43}
44
45impl Item {
46 /// `Interscript::Node::Item::Group`-style concatenation with
47 /// flattening and None-elimination (`Items::Concat.from_parts`).
48 pub fn concat_from_parts(parts: Vec<Item>) -> Item {
49 let mut flat: Vec<Item> = Vec::new();
50 for p in parts {
51 match p {
52 Item::Concat(inner) => flat.extend(inner),
53 Item::None => {}
54 other => flat.push(other),
55 }
56 }
57 match flat.len() {
58 0 => Item::None,
59 1 => flat.pop().unwrap(),
60 _ => Item::Concat(flat),
61 }
62 }
63
64 /// `first_string` collapse (Ruby `nth_string` without the fiber
65 /// selection): what this item yields as a single replacement
66 /// string. Any → first entry; Range → lo. Alias/Prim/CaptureRef
67 /// keep identity (resolved later, like build_item).
68 pub fn first_string(&self) -> Item {
69 match self {
70 Item::Set(entries) => entries
71 .first()
72 .map(|e| e.first_string())
73 .unwrap_or(Item::None),
74 Item::Range(lo, _) => Item::Str(lo.to_string()),
75 Item::Maybe(inner) | Item::Some(inner) => inner.first_string(),
76 Item::CaptureGroup { inner, .. } => inner.first_string(),
77 Item::Concat(parts) => Item::Concat(parts.iter().map(|p| p.first_string()).collect()),
78 other => other.clone(),
79 }
80 }
81
82 /// `max_length` for the deterministic parallel sort. Primitives
83 /// count as 1 (0 for none), mirroring `Node::Item::Alias#max_length`.
84 pub fn max_length(&self) -> usize {
85 match self {
86 Item::Str(s) => s.chars().count(),
87 Item::None => 0,
88 Item::Prim(_) | Item::Func(_) | Item::Alias { .. } | Item::CaptureRef(_) => 1,
89 Item::CaptureGroup { inner, .. }
90 | Item::Maybe(inner)
91 | Item::Some(inner) => inner.max_length(),
92 Item::Range(_, _) => 1,
93 Item::Set(entries) => entries.iter().map(|e| e.max_length()).max().unwrap_or(0),
94 Item::Concat(parts) => parts.iter().map(|p| p.max_length()).sum(),
95 }
96 }
97}