inkferro_core/dom/node.rs
1//! Node data structures mirroring ink's DOM types (dom.ts:8-93).
2
3/// The four element kinds from ink's `ElementNames` (dom.ts:18-23).
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Kind {
6 /// `ink-root`
7 Root,
8 /// `ink-box`
9 Box,
10 /// `ink-text`
11 Text,
12 /// `ink-virtual-text`
13 VirtualText,
14}
15
16// ─── Style types ─────────────────────────────────────────────────────────────
17
18/// A length/percentage/auto dimension (mirrors Yoga's percent vs. point APIs).
19///
20/// Used for width, height, min/max, flex-basis, and inset properties.
21/// Percentage is stored as 0–100 (ink JS convention); the taffy mapping
22/// divides by 100 to obtain 0.0–1.0.
23#[derive(Debug, Clone, PartialEq, Default)]
24pub enum Dim {
25 /// A fixed size in terminal cells.
26 Points(f32),
27 /// A percentage (0–100) of the parent's corresponding dimension.
28 Percent(f32),
29 /// Automatic sizing (yoga/taffy default).
30 #[default]
31 Auto,
32}
33
34/// A length or percentage (no auto) used for margin/padding/border/gap.
35///
36/// Mirrors yoga's numeric-only API for these properties.
37#[derive(Debug, Clone, PartialEq)]
38pub enum Lp {
39 /// A fixed size in terminal cells.
40 Points(f32),
41 /// A percentage (0–100) of the relevant parent dimension.
42 Percent(f32),
43}
44
45/// `flexDirection` (styles.ts:504–525).
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum FlexDir {
48 #[default]
49 Row,
50 Column,
51 RowReverse,
52 ColumnReverse,
53}
54
55/// `flexWrap` (styles.ts:527–540).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum FlexWrap {
58 #[default]
59 NoWrap,
60 Wrap,
61 WrapReverse,
62}
63
64/// `alignItems` / `alignSelf` (styles.ts:552–593).
65///
66/// Used for both `align_items` (`Option<AlignItems>` in taffy) and
67/// `align_self` (`Option<AlignSelf>` in taffy). Taffy has no explicit
68/// `Auto` variant for `AlignSelf`; `None` in the `Option` encodes auto.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum Align {
71 Stretch,
72 FlexStart,
73 Center,
74 FlexEnd,
75 Baseline,
76}
77
78/// `alignContent` / `justifyContent` (styles.ts:595–661).
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ContentAlign {
81 FlexStart,
82 Center,
83 FlexEnd,
84 SpaceBetween,
85 SpaceAround,
86 SpaceEvenly,
87 Stretch,
88}
89
90/// `display` (styles.ts:721–727).
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum Display {
93 #[default]
94 Flex,
95 None,
96}
97
98/// `borderStyle` — mirrors `keyof Boxes | BoxStyle` from cli-boxes (styles.ts:255 —
99/// type declaration; the `typeof borderStyle === 'string' ? cliBoxes[...] : borderStyle`
100/// ternary lives in render-border.ts:32-34).
101///
102/// Presence (`Some`) means border is active (layout width = 1 per edge); `None`
103/// means no border (width = 0). The renderer uses the variant to select the
104/// correct box-drawing characters.
105///
106/// * `Named` — one of cli-boxes's named styles ("single", "double", "round", …).
107/// * `Custom` — caller-supplied box-drawing characters matching `BoxStyle` from
108/// cli-boxes. Eight fields: four corners + four edges.
109#[derive(Debug, Clone, PartialEq)]
110pub enum BorderStyle {
111 /// A named style from cli-boxes (e.g. `"single"`, `"double"`, `"round"`).
112 Named(String),
113 /// A fully custom `BoxStyle` object supplied directly by the caller
114 /// (styles.ts:255 — type declaration; ternary in render-border.ts:32-34).
115 Custom {
116 top_left: String,
117 top: String,
118 top_right: String,
119 right: String,
120 bottom_right: String,
121 bottom: String,
122 bottom_left: String,
123 left: String,
124 },
125}
126
127/// `position` (styles.ts:415–442).
128///
129/// Yoga has three values: `absolute`, `relative`, and `static`.
130/// Taffy only has `Relative` and `Absolute` — `static` maps to `Relative`
131/// (divergence documented in `style_to_taffy`).
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum Position {
134 #[default]
135 Relative,
136 Absolute,
137 /// Yoga-only; taffy backend maps to `Relative`. The difference is visible
138 /// only when inset (top/right/bottom/left) is set on a static node: yoga
139 /// ignores inset for static (styles.ts:21), but this mapping honors it.
140 /// Accepted: ink's API documents static as ignoring offsets (styles.ts:21)
141 /// and Box never defaults to static.
142 Static,
143}
144
145/// `textWrap` (styles.ts:10-16) — controls text wrapping within an ink-text
146/// node. Lives on `Style` (not a node attribute) because `dom.ts:242` reads
147/// it as `node.style?.textWrap`; `wrap-text.ts:3` types it `Styles['textWrap']`.
148///
149/// Default (when `None`) is `Wrap`, matching ink's `node.style?.textWrap ?? 'wrap'`
150/// (dom.ts:242).
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum TextWrap {
153 /// `'wrap'` — soft word-wrap with hard=true, trim=false (wrap-text.ts:21-25).
154 #[default]
155 Wrap,
156 /// `'hard'` — character-level hard wrap, word_wrap=false (wrap-text.ts:27-32).
157 Hard,
158 /// `'truncate'` or `'truncate-end'` — truncate at end (wrap-text.ts:33-47).
159 TruncateEnd,
160 /// `'truncate-middle'` — truncate in the middle.
161 TruncateMiddle,
162 /// `'truncate-start'` — truncate at the start.
163 TruncateStart,
164}
165
166/// `overflow` per-axis (styles.ts; Box.tsx resolves the `overflow` shorthand
167/// to `overflowX`/`overflowY` before reaching the DOM — styles.ts:731–734).
168///
169/// Only `overflowX` and `overflowY` are stored here; the `overflow` shorthand
170/// is resolved JS-side in Box.tsx (Box.tsx:90-92) and never reaches Rust.
171///
172/// styles.ts defines overflow as `'visible' | 'hidden'` only (styles.ts:384/391/398).
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub enum Overflow {
175 #[default]
176 Visible,
177 Hidden,
178}
179
180/// The layout style for a DOM node.
181///
182/// Mirrors ink's `Styles` type (styles.ts) with every prop that ink's
183/// `apply*` functions write to yoga (styles.ts:415–777). Layout-inert
184/// props (text color, background, border colors) are carried as raw strings
185/// for the renderer (M1-5) without influencing the taffy layout pass.
186///
187/// ### Shorthand resolution
188/// * `overflow` shorthand: resolved JS-side in Box.tsx before `setStyle` —
189/// only `overflow_x`/`overflow_y` per-axis values reach Rust.
190/// * `margin`/`marginX`/`marginY`, `padding`/`paddingX`/`paddingY`, `gap`
191/// shorthands: NOT resolved JS-side (yoga handles via `EDGE_ALL` /
192/// `GUTTER_ALL`). Rust carries these shorthands; the taffy mapping
193/// collapses them with an `Option::or` cascade.
194#[derive(Debug, Clone, PartialEq, Default)]
195pub struct Style {
196 // ── Position (styles.ts:415–442) ──────────────────────────────────────
197 pub position: Option<Position>,
198 pub top: Option<Dim>,
199 pub right: Option<Dim>,
200 pub bottom: Option<Dim>,
201 pub left: Option<Dim>,
202
203 // ── Margin (styles.ts:444–472) ────────────────────────────────────────
204 /// Shorthand — applies to all four edges (yoga `EDGE_ALL`).
205 pub margin: Option<Lp>,
206 /// Horizontal shorthand — left + right (yoga `EDGE_HORIZONTAL`).
207 pub margin_x: Option<Lp>,
208 /// Vertical shorthand — top + bottom (yoga `EDGE_VERTICAL`).
209 pub margin_y: Option<Lp>,
210 pub margin_top: Option<Lp>,
211 pub margin_right: Option<Lp>,
212 pub margin_bottom: Option<Lp>,
213 pub margin_left: Option<Lp>,
214
215 // ── Padding (styles.ts:474–502) ───────────────────────────────────────
216 /// Shorthand — applies to all four edges.
217 pub padding: Option<Lp>,
218 /// Horizontal shorthand — left + right.
219 pub padding_x: Option<Lp>,
220 /// Vertical shorthand — top + bottom.
221 pub padding_y: Option<Lp>,
222 pub padding_top: Option<Lp>,
223 pub padding_right: Option<Lp>,
224 pub padding_bottom: Option<Lp>,
225 pub padding_left: Option<Lp>,
226
227 // ── Flex (styles.ts:504–661) ──────────────────────────────────────────
228 pub flex_direction: Option<FlexDir>,
229 pub flex_wrap: Option<FlexWrap>,
230 pub flex_grow: Option<f32>,
231 pub flex_shrink: Option<f32>,
232 /// `flexBasis` — yoga accepts number (points), percent string, or auto.
233 pub flex_basis: Option<Dim>,
234 pub align_items: Option<Align>,
235 /// `None` encodes `auto` (taffy has no explicit `AlignSelf::Auto` variant).
236 pub align_self: Option<Align>,
237 pub align_content: Option<ContentAlign>,
238 pub justify_content: Option<ContentAlign>,
239
240 // ── Dimensions (styles.ts:663–719) ────────────────────────────────────
241 pub width: Option<Dim>,
242 pub height: Option<Dim>,
243 pub min_width: Option<Dim>,
244 pub min_height: Option<Dim>,
245 pub max_width: Option<Dim>,
246 pub max_height: Option<Dim>,
247 pub aspect_ratio: Option<f32>,
248
249 // ── Display (styles.ts:721–727) ───────────────────────────────────────
250 pub display: Option<Display>,
251
252 // ── Border (styles.ts:729–763) ────────────────────────────────────────
253 /// `borderStyle` value (styles.ts:255, 745). `Some(_)` → border active,
254 /// layout width = 1 per enabled edge. `None` → no border (width = 0).
255 /// Renderer uses the variant to select box-drawing characters (M1-5).
256 pub border_style: Option<BorderStyle>,
257 /// `borderTop === false` disables the top border edge (styles.ts:748-749).
258 pub border_top: Option<bool>,
259 pub border_right: Option<bool>,
260 pub border_bottom: Option<bool>,
261 pub border_left: Option<bool>,
262
263 // ── Gap (styles.ts:765–777) ───────────────────────────────────────────
264 /// All-axes shorthand (yoga `GUTTER_ALL`).
265 pub gap: Option<f32>,
266 pub column_gap: Option<f32>,
267 pub row_gap: Option<f32>,
268
269 // ── Text wrap (styles.ts:10-16; dom.ts:242 reads `node.style?.textWrap`) ──
270 /// `textWrap` mode for `ink-text` nodes. `None` → default `Wrap`
271 /// (matches ink's `?? 'wrap'` fallback at dom.ts:242).
272 pub text_wrap: Option<TextWrap>,
273
274 // ── Overflow per-axis (Box.tsx:90-92 resolves the shorthand JS-side) ──
275 pub overflow_x: Option<Overflow>,
276 pub overflow_y: Option<Overflow>,
277
278 // ── Layout-inert visual props (carried for M1-5 renderer) ─────────────
279 pub background_color: Option<String>,
280 pub border_color: Option<String>,
281 pub border_top_color: Option<String>,
282 pub border_right_color: Option<String>,
283 pub border_bottom_color: Option<String>,
284 pub border_left_color: Option<String>,
285 pub border_background_color: Option<String>,
286 pub border_top_background_color: Option<String>,
287 pub border_right_background_color: Option<String>,
288 pub border_bottom_background_color: Option<String>,
289 pub border_left_background_color: Option<String>,
290
291 // ── Per-edge border dim flags (BOOLEAN) — ink reads these from
292 // `node.style.border{Edge}DimColor`; render-border.ts:54-64 resolves
293 // border{Edge}DimColor ?? borderDimColor and threads `dim` through stylePiece.
294 pub border_dim_color: Option<bool>,
295 pub border_top_dim_color: Option<bool>,
296 pub border_right_dim_color: Option<bool>,
297 pub border_bottom_dim_color: Option<bool>,
298 pub border_left_dim_color: Option<bool>,
299}
300
301impl Style {
302 /// Per-edge border widths `[top, right, bottom, left]` in cells.
303 ///
304 /// Single source for BOTH the taffy layout reservation and the
305 /// render clip inset — these two must never disagree.
306 ///
307 /// Returns 1 for each edge that is active (border_style is `Some` and
308 /// the edge flag is not explicitly `false`), 0 otherwise.
309 pub(crate) fn border_edges(&self) -> [u16; 4] {
310 let on = self.border_style.is_some();
311 let edge = |f: Option<bool>| if on && f != Some(false) { 1u16 } else { 0u16 };
312 [
313 edge(self.border_top),
314 edge(self.border_right),
315 edge(self.border_bottom),
316 edge(self.border_left),
317 ]
318 }
319}
320
321/// Inline text styling for an `ink-text` subtree (P5.1 SET_TEXT_STYLE). Mirrors
322/// ink's `<Text>` styling props (color, backgroundColor, bold, italic, underline,
323/// strikethrough, inverse, dimColor).
324///
325/// The render walk reads this via `resolve_transform` (render/walk.rs) to compose
326/// SGR natively instead of dispatching a per-line JS transform. Stored alongside
327/// `has_transform`; a node carries exactly one of the two. A styled→plain
328/// rerender clears it via `ClearTextStyle` (P6.2 CLEAR_TEXT_STYLE).
329#[derive(Debug, Default, Clone, PartialEq)]
330pub struct TextStyle {
331 pub color: Option<String>,
332 pub background_color: Option<String>,
333 pub bold: bool,
334 pub italic: bool,
335 pub underline: bool,
336 pub strikethrough: bool,
337 pub inverse: bool,
338 pub dim_color: bool,
339}
340
341// ─── Node ────────────────────────────────────────────────────────────────────
342
343/// Attribute value enum mirroring `DOMNodeAttribute` (dom.ts:93).
344///
345/// JS `DOMNodeAttribute = boolean | string | number`; the number variant
346/// uses `f64` to match JS numeric semantics.
347#[derive(Debug, Clone, PartialEq)]
348pub enum AttrValue {
349 Bool(bool),
350 Str(String),
351 Number(f64),
352}
353
354/// A single node in the arena.
355///
356/// Mirrors the union of `DOMElement` + `TextNode` from dom.ts (dom.ts:27-81).
357/// Fields common to both are always present; `text` is meaningful only on
358/// `Text` and `VirtualText` kinds (where ink stores it in a child `#text`
359/// node — folded here per task spec).
360#[derive(Debug, Clone)]
361pub struct Node {
362 pub kind: Kind,
363 pub parent: Option<u32>,
364 pub children: Vec<u32>,
365 /// Text content. Mirrors `TextNode.nodeValue` (dom.ts:79-81).
366 /// For `Text`/`VirtualText` nodes. No-op field on `Root`/`Box`.
367 pub text: Option<String>,
368 /// Layout and visual style for this node.
369 pub style: Style,
370 /// String/bool/number attributes — mirrors `DOMElement.attributes`
371 /// (dom.ts:29). Does NOT include `internal_transform` or
372 /// `internal_static`; those are separate flags per the reconciler
373 /// (reconciler.ts:231-245).
374 pub attributes: Vec<(String, AttrValue)>,
375 /// `internal_static` flag (reconciler.ts:237-244).
376 pub is_static: bool,
377 /// Set by `Hide`/`Unhide` ops. The actual yoga display effect is JS-side.
378 pub is_hidden: bool,
379 /// `internal_transform` presence flag (reconciler.ts:231-235).
380 /// The transform function itself stays JS-side per the FFI design.
381 pub has_transform: bool,
382 /// Inline text styling (P5.1 SET_TEXT_STYLE). Read by the render walk via
383 /// `resolve_transform` to compose SGR natively. `None` until a `SetTextStyle`
384 /// op writes it; reset to `None` by `ClearTextStyle` on a styled→plain
385 /// rerender (P6.2).
386 pub text_styling: Option<TextStyle>,
387}
388
389impl Node {
390 pub fn new(kind: Kind) -> Self {
391 Self {
392 kind,
393 parent: None,
394 children: Vec::new(),
395 text: None,
396 style: Style::default(),
397 attributes: Vec::new(),
398 is_static: false,
399 is_hidden: false,
400 has_transform: false,
401 text_styling: None,
402 }
403 }
404}