Skip to main content

denise_forms/
form.rs

1//! The file, parsed but not yet built.
2
3use denise::{Rect, Role, Size, Theme, theme};
4use denise_ui::Side;
5use kdl::{KdlDocument, KdlEntry, KdlEntryFormat, KdlNode, KdlNodeFormat, KdlValue};
6
7use crate::error::{At, Error, Reason};
8
9/// The schema version this crate reads.
10///
11/// A file may add properties within a major version — an engine older than the
12/// file reports the added one as unknown, which is the same message a typo gets
13/// and has the same fix. A file whose `version` is *higher* than this is refused
14/// by number rather than misread.
15pub const VERSION: u64 = 1;
16
17/// How deep a form may nest.
18///
19/// A form is a screen. Anything approaching this is a generated file or a
20/// malicious one.
21///
22/// This is checked **before the file reaches the parser**, and that is not
23/// belt-and-braces: `kdl` is a recursive-descent parser and overflows the stack
24/// on a few hundred nested nodes, which a `Result` cannot catch and a panic
25/// handler cannot either. A form arrives from a designer, a clipboard or a
26/// download, so bounding it is this crate's job and not its caller's.
27pub const MAX_DEPTH: usize = 64;
28
29/// How large a form file may be.
30///
31/// Generous for a screen — the reference form is under five kilobytes — and small
32/// enough that a panel with a few megabytes of headroom cannot be talked into
33/// exhausting them by something claiming to be a form.
34pub const MAX_SOURCE: usize = 1 << 22;
35
36/// How deeply a form may nest a **commented-out children block** — a `{ … }`
37/// belonging to a node a `/-` has commented out.
38///
39/// Its own limit because neither [`MAX_SOURCE`] nor [`MAX_DEPTH`] bounds it in
40/// any useful way. `kdl` takes time that **doubles with every level** of one of
41/// these inside another: twenty levels is about a hundred bytes and twenty
42/// seconds, thirty is six hours, and the sixty-four [`MAX_DEPTH`] would permit
43/// is longer than the universe has been running. Found by the fuzz target
44/// `parse_form`, which kept reporting three-kilobyte inputs that took a second
45/// and a half to *fail* on.
46///
47/// One level is kept, because commenting a widget and its children out is a
48/// real thing to do while editing. Two is refused, because a block commented
49/// out inside a block that is already commented out changes nothing about what
50/// the file means — the fix is deleting an inner `/-` that was doing no work —
51/// and every one of the slow inputs the fuzzer found is past this line while
52/// every form in this repository is nowhere near it.
53///
54/// This bounds the shapes that have been found, and it is not a bound on the
55/// parser: `kdl` 6.7.1 has more exponential corners than this one. The bound on
56/// the parser is a clock, and it belongs to the caller —
57/// [`Form::parse_within`](crate::Form::parse_within) is it. See
58/// `fuzz/README.md`.
59pub const MAX_COMMENTED_DEPTH: usize = 1;
60
61/// What a form is for.
62///
63/// The engine reports it and opens nothing: whether a dialog is a pushed scene or
64/// a modal window is the application's decision, and it differs by machine.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
66pub enum FormKind {
67    /// The root tree of a `Ui` — a panel's whole surface.
68    Screen,
69    /// A desktop window.
70    Window,
71    /// A modal: `Ui::push_scene` on a panel, a modal window on a desktop.
72    Dialog,
73    /// `Ui::push_drawer`.
74    Drawer,
75    /// `Ui::push_shelf`.
76    Shelf,
77    /// A subtree with no root of its own, for reuse inside other forms.
78    Fragment,
79}
80
81impl FormKind {
82    /// Every kind, in the spelling a form file uses.
83    pub const NAMES: &'static [&'static str] =
84        &["screen", "window", "dialog", "drawer", "shelf", "fragment"];
85
86    /// ```
87    /// # use denise_forms::FormKind;
88    /// assert!(FormKind::Dialog.what().contains("modal"));
89    /// // Every kind has one, and no two share it.
90    /// assert_ne!(FormKind::Drawer.what(), FormKind::Shelf.what());
91    /// ```
92    /// One line on what this kind is for.
93    ///
94    /// Here rather than in the designer because it is a fact about the format:
95    /// the CLI prints it, the designer offers it when a form is being made, and
96    /// neither of them should be keeping its own copy.
97    pub const fn what(self) -> &'static str {
98        match self {
99            Self::Screen => "A panel's whole surface: the root of a `Ui`.",
100            Self::Window => "A desktop window, with a title bar and a size somebody can drag.",
101            Self::Dialog => "A modal: a pushed scene on a panel, a modal window on a desktop.",
102            Self::Drawer => "A panel that slides in from an edge, over a dimmed screen.",
103            Self::Shelf => "A bar that slides in from an edge, with nothing dimmed behind it.",
104            Self::Fragment => "A subtree with no root of its own, for reuse inside other forms.",
105        }
106    }
107
108    /// ```
109    /// # use denise_forms::FormKind;
110    /// # use denise_ui::Side;
111    /// // A drawer is a side panel; a shelf is a bar.
112    /// assert_eq!(FormKind::Drawer.default_side(), Side::Before);
113    /// assert_eq!(FormKind::Shelf.default_side(), Side::Below);
114    /// ```
115    /// Which edge one of these comes in from when the file does not say.
116    pub const fn default_side(self) -> Side {
117        match self {
118            Self::Shelf => Side::Below,
119            _ => Side::Before,
120        }
121    }
122
123    fn from_name(name: &str) -> Option<Self> {
124        Some(match name {
125            "screen" => FormKind::Screen,
126            "window" => FormKind::Window,
127            "dialog" => FormKind::Dialog,
128            "drawer" => FormKind::Drawer,
129            "shelf" => FormKind::Shelf,
130            "fragment" => FormKind::Fragment,
131            _ => return None,
132        })
133    }
134}
135
136/// Whether a form may be drawn at a size other than the one it was designed at.
137///
138/// Scaling is not always right, and the form is the thing that knows. A dial
139/// designed against a 1:1 photographic background, a layout whose text must stay
140/// a legal minimum size, a panel whose touch targets are already at the smallest
141/// a gloved finger can hit — each of those is a form that should be drawn at its
142/// design size and centred, not stretched to fit. So it is declared rather than
143/// assumed, and the default is the one every form written before this property
144/// existed already had.
145///
146/// This is a **deployment** concern, applied once on the way in.
147/// [`anchors`](crate::NODE_PROPERTIES) are a *design* concern, resolved by the
148/// tree at every reflow. They are different tools and they compose: a form may
149/// use either, both or neither.
150///
151/// ```
152/// # use denise_forms::{Form, Scaling};
153/// let form = Form::parse(r#"form "F" version=1 width=100 height=100 { }"#)?;
154/// assert_eq!(form.scaling(), Scaling::None, "the default is what was always true");
155/// # Ok::<(), denise_forms::Error>(())
156/// ```
157#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
158pub enum Scaling {
159    /// Never scaled. Drawn at its design size, centred in whatever it is given.
160    #[default]
161    None,
162    /// One factor on both axes — `min(target.w / design.w, target.h / design.h)`
163    /// — so nothing distorts, and the leftover is a margin on one axis.
164    Proportional,
165    /// A factor per axis, filling the surface. Distorts, and is occasionally
166    /// exactly what a signage layout wants.
167    Stretch,
168}
169
170impl Scaling {
171    /// Every one, in the spelling a form file uses.
172    pub const NAMES: &'static [&'static str] = &["none", "proportional", "stretch"];
173
174    /// ```
175    /// # use denise_forms::Scaling;
176    /// assert!(Scaling::None.what().contains("design size"));
177    /// assert_ne!(Scaling::Proportional.what(), Scaling::Stretch.what());
178    /// ```
179    /// One line on what this one does.
180    ///
181    /// Here rather than in the designer for the same reason as
182    /// [`FormKind::what`]: it is a fact about the format, and two copies of a
183    /// sentence drift.
184    pub const fn what(self) -> &'static str {
185        match self {
186            Self::None => "Never scaled: drawn at its design size, in the middle.",
187            Self::Proportional => "Scaled to fit by one factor, with a margin on the long axis.",
188            Self::Stretch => "Scaled per axis to fill the surface, distorting if it must.",
189        }
190    }
191
192    fn from_name(name: &str) -> Option<Self> {
193        Some(match name {
194            "none" => Self::None,
195            "proportional" => Self::Proportional,
196            "stretch" => Self::Stretch,
197            _ => return None,
198        })
199    }
200}
201
202/// Where a form goes on a surface, and by how much it is multiplied to get there.
203///
204/// What [`Form::fit`] works out and [`Form::build_fitted`] then applies. Held as
205/// a value rather than done in one call because the application needs the parts
206/// separately: [`Placement::rect`] is where to put the panel the form is built into,
207/// and [`Placement::uniform`] is what the theme has to be scaled by at `Ui::new` —
208/// **which is not optional**, or the widgets are the old size inside the new
209/// rectangles.
210#[derive(Clone, Copy, Debug, PartialEq)]
211pub struct Placement {
212    /// The horizontal factor.
213    pub x: f32,
214    /// The vertical factor.
215    pub y: f32,
216    /// Where the form's own rectangle lands in the surface it was fitted to,
217    /// already scaled and already centred.
218    pub rect: Rect,
219}
220
221impl Placement {
222    /// The one factor that everything which is not a rectangle scales by: text
223    /// sizes, border widths, row heights, and the theme's own metrics.
224    ///
225    /// The **smaller** of the two, so that a stretched layout never grows text
226    /// taller than the axis that had least room to give. Equal to either of them
227    /// whenever the fit is uniform, which is every fit except
228    /// [`Scaling::Stretch`].
229    ///
230    /// ```
231    /// # use denise::Size;
232    /// # use denise_forms::Form;
233    /// let form = Form::parse(
234    ///     r#"form "F" version=1 width=100 height=100 scaling=stretch { }"#,
235    /// )?;
236    /// let fit = form.fit(Size::new(400, 200));
237    /// assert_eq!((fit.x, fit.y), (4.0, 2.0));
238    /// assert_eq!(fit.uniform(), 2.0, "text follows the tighter axis");
239    /// # Ok::<(), denise_forms::Error>(())
240    /// ```
241    #[must_use]
242    pub fn uniform(self) -> f32 {
243        if self.x < self.y { self.x } else { self.y }
244    }
245}
246
247/// The themes a form may name.
248pub const THEMES: &[&str] = &["dark", "light", "high-contrast"];
249
250/// A property's value, as a form file writes it.
251///
252/// Smaller than KDL's own set, because a `.dform` property is one of these and
253/// nothing else. The spelling is part of it: `role=primary` and
254/// `text="primary"` hold the same string and are not the same line, so
255/// [`Name`](Literal::Name) and [`Text`](Literal::Text) are separate.
256#[derive(Clone, Debug, PartialEq)]
257pub enum Literal {
258    /// A quoted string: `text="Save"`.
259    Text(String),
260    /// A bare name, which is how an enum is written: `role=primary`.
261    ///
262    /// Quoted anyway if the name is not something KDL would read back bare, so
263    /// this can never produce a file that stops parsing.
264    Name(String),
265    /// `#true` or `#false`.
266    Flag(bool),
267    /// A whole number.
268    Int(i64),
269    /// A real number.
270    Float(f64),
271    /// Exactly this text, character for character, as it stood in the file.
272    ///
273    /// What [`Form::apply`] hands back for a value it replaced, and the reason
274    /// undo is byte-exact rather than merely correct: `1_000`, `0x10`, `70.0`
275    /// and `#"a raw string"#` are all values a typed variant could carry, and
276    /// none of them would be written the same way twice.
277    ///
278    /// One built by hand is checked: it has to be the text of a single value.
279    Verbatim(String),
280}
281
282impl Literal {
283    /// The spelling is the difference, and the file keeps it: `role=primary`
284    /// and `text="primary"` hold the same string and are not the same line.
285    ///
286    /// ```
287    /// # use denise_forms::{Edit, Form, Literal};
288    /// let mut form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" x=0 y=0 w=9 h=9 }"#)?;
289    ///
290    /// form.apply(Edit::property(&[0], "text", Some(Literal::text("Hello"))))?;
291    /// form.apply(Edit::property(&[0], "role", Some(Literal::name("primary"))))?;
292    ///
293    /// assert!(form.text().contains(r#"text="Hello""#), "{}", form.text());
294    /// assert!(form.text().contains("role=primary"), "{}", form.text());
295    /// # Ok::<(), denise_forms::Error>(())
296    /// ```
297    /// A quoted string.
298    pub fn text(text: impl Into<String>) -> Self {
299        Literal::Text(text.into())
300    }
301
302    /// See [`Literal::text`].
303    /// A bare name.
304    pub fn name(name: impl Into<String>) -> Self {
305        Literal::Name(name.into())
306    }
307
308    /// The value this stands for, and the text to write for it.
309    fn parts(&self) -> Result<(KdlValue, String), Error> {
310        Ok(match self {
311            Literal::Text(text) => (KdlValue::String(text.clone()), quoted(text)),
312            // `KdlValue`'s own rendering writes a plain identifier bare and
313            // quotes anything else, which is this variant's rule exactly.
314            Literal::Name(name) => {
315                let value = KdlValue::String(name.clone());
316                let repr = value.to_string();
317                (value, repr)
318            }
319            Literal::Flag(flag) => {
320                let value = KdlValue::Bool(*flag);
321                let repr = value.to_string();
322                (value, repr)
323            }
324            Literal::Int(number) => {
325                let value = KdlValue::Integer(i128::from(*number));
326                let repr = value.to_string();
327                (value, repr)
328            }
329            Literal::Float(number) => {
330                let value = KdlValue::Float(*number);
331                let repr = value.to_string();
332                (value, repr)
333            }
334            Literal::Verbatim(text) => (one_value(text)?, text.clone()),
335        })
336    }
337
338    /// What sort of thing this is, for the rule that a number and a string do
339    /// not replace each other.
340    fn class(&self) -> Result<Class, Error> {
341        Ok(match self {
342            Literal::Text(_) | Literal::Name(_) => Class::Text,
343            Literal::Flag(_) => Class::Flag,
344            Literal::Int(_) | Literal::Float(_) => Class::Number,
345            Literal::Verbatim(text) => Class::of(&one_value(text)?),
346        })
347    }
348}
349
350/// The sorts of value a property may hold.
351///
352/// Coarser than [`Literal`] on purpose: `value=70` becoming `value=70.5` is an
353/// ordinary edit, and `placeholder="Ada"` becoming `placeholder=70` is a form
354/// that will not load. One kind of change is worth refusing and the other is
355/// not, and this is the line between them.
356#[derive(Clone, Copy, Debug, PartialEq, Eq)]
357enum Class {
358    Text,
359    Number,
360    Flag,
361    Nothing,
362}
363
364impl Class {
365    fn of(value: &KdlValue) -> Self {
366        match value {
367            KdlValue::String(_) => Class::Text,
368            KdlValue::Integer(_) | KdlValue::Float(_) => Class::Number,
369            KdlValue::Bool(_) => Class::Flag,
370            KdlValue::Null => Class::Nothing,
371        }
372    }
373
374    const fn noun(self) -> &'static str {
375        match self {
376            Class::Text => "a string",
377            Class::Number => "a number",
378            Class::Flag => "true or false",
379            Class::Nothing => "nothing",
380        }
381    }
382}
383
384/// One reversible change to a form.
385///
386/// Applied with [`Form::apply`], which hands back the edit that undoes it. See
387/// there for why an inverse is always knowable.
388#[derive(Clone, Debug, PartialEq)]
389pub enum Edit {
390    /// Set a property, or take it away with `None`.
391    Property {
392        /// The node.
393        path: Vec<usize>,
394        /// The property.
395        name: String,
396        /// What to set it to, or `None` to remove it — which is what returning a
397        /// property to its default means, since a default is not written.
398        value: Option<Literal>,
399    },
400    /// Put a node, written as form-file text, among a parent's children.
401    ///
402    /// The text is a whole node with its own formatting, which is what makes this
403    /// the inverse of a removal *and* what a paste from the clipboard is.
404    Insert {
405        /// The parent's path; empty for the form itself.
406        parent: Vec<usize>,
407        /// Where among its children.
408        index: usize,
409        /// The node.
410        text: String,
411    },
412    /// Set a node's positional argument — the `"Hello"` in `label "Hello"`.
413    ///
414    /// Only ever *replaces* one. A node written without an argument does not
415    /// grow one this way: an argument has to come before every property, and
416    /// there is no shape of edit that puts something at the front of a line
417    /// without rewriting the line. Setting the matching property is what an
418    /// editor does instead, and means the same thing to the engine.
419    Argument {
420        /// The node.
421        path: Vec<usize>,
422        /// What to put there.
423        value: Literal,
424    },
425    /// Take a node out and put it back under another parent.
426    ///
427    /// Reordering among siblings and reparenting are the same edit: both take a
428    /// node out and put it back somewhere, and doing it as one keeps it to one
429    /// step on an undo stack.
430    ///
431    /// A node that changes depth is **re-indented** — every line of it, so the
432    /// children come along — because a file whose nesting and whose indentation
433    /// disagree is a file somebody has to fix by hand. Moving it back re-indents
434    /// it back, so an undo is still byte-for-byte.
435    Move {
436        /// The node now.
437        from: Vec<usize>,
438        /// The parent it goes under; empty for the form itself.
439        to: Vec<usize>,
440        /// Where among that parent's children.
441        index: usize,
442    },
443    /// Take a node, and everything under it, out.
444    Remove {
445        /// The node.
446        path: Vec<usize>,
447    },
448    /// Several edits as one.
449    ///
450    /// Applied in order and undone in reverse, which is what makes a drag that
451    /// moved *and* resized a single step on an undo stack rather than four. If
452    /// any of them fails the ones already applied are put back, so a compound
453    /// edit either happens or does not.
454    Many(Vec<Edit>),
455    /// Swap a node for another, written as form-file text.
456    ///
457    /// The exact inverse of anything that cannot be undone by putting a value
458    /// back — taking a property *away* is the case: a property re-added by name
459    /// lands at the end of the line rather than where it was, so the values would
460    /// come back right and the line would not. Restoring the node's own text
461    /// restores its order too.
462    Replace {
463        /// The node.
464        path: Vec<usize>,
465        /// What to put there.
466        text: String,
467    },
468}
469
470impl Edit {
471    /// Sets or clears a whole-number property.
472    ///
473    /// The common one by a long way: every rectangle a drag writes is four of
474    /// these.
475    /// ```
476    /// # use denise_forms::{Edit, Form};
477    /// let mut form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" x=0 y=0 w=9 h=9 }"#)?;
478    ///
479    /// form.apply(Edit::number(&[0], "x", Some(24)))?;
480    /// assert_eq!(form.property(&[0], "x").as_deref(), Some("24"));
481    ///
482    /// // `None` takes it out of the file, which is what a default is.
483    /// form.apply(Edit::number(&[0], "x", None))?;
484    /// assert_eq!(form.property(&[0], "x"), None);
485    /// # Ok::<(), denise_forms::Error>(())
486    /// ```
487    pub fn number(path: &[usize], name: &str, value: Option<i64>) -> Self {
488        Edit::property(path, name, value.map(Literal::Int))
489    }
490
491    /// The path is child indices from the `form` node down, and **the empty path
492    /// is the form itself** — its size, its kind, its theme.
493    ///
494    /// ```
495    /// # use denise_forms::{Edit, Form, Literal};
496    /// let mut form = Form::parse(r#"form "F" version=1 width=320 height=240 { label "Hi" x=0 y=0 w=9 h=9 }"#)?;
497    ///
498    /// form.apply(Edit::property(&[], "width", Some(Literal::Int(640))))?;
499    /// assert_eq!(form.size(), denise::Size::new(640, 240));
500    /// # Ok::<(), denise_forms::Error>(())
501    /// ```
502    /// Sets or clears a property.
503    pub fn property(path: &[usize], name: &str, value: Option<Literal>) -> Self {
504        Edit::Property {
505            path: path.to_vec(),
506            name: name.to_string(),
507            value,
508        }
509    }
510
511    /// A `label "Heading"` keeps its text there rather than in a `text=`
512    /// property, and so does the form's own title.
513    ///
514    /// ```
515    /// # use denise_forms::{Edit, Form};
516    /// let mut form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" x=0 y=0 w=9 h=9 }"#)?;
517    ///
518    /// form.apply(Edit::argument(&[0], "Hello"))?;
519    /// assert_eq!(form.argument(&[0]).as_deref(), Some("Hello"));
520    ///
521    /// // The form's title is its argument too.
522    /// form.apply(Edit::argument(&[], "Greeting"))?;
523    /// assert_eq!(form.title(), "Greeting");
524    /// # Ok::<(), denise_forms::Error>(())
525    /// ```
526    /// Sets a node's positional argument to a string.
527    pub fn argument(path: &[usize], text: impl Into<String>) -> Self {
528        Edit::Argument {
529            path: path.to_vec(),
530            value: Literal::Text(text.into()),
531        }
532    }
533
534    /// `index` is the position **after** the node has been taken out, which is
535    /// the part that is easy to get wrong: removing `[1]` moves `[3]` to `[2]`.
536    ///
537    /// ```
538    /// # use denise_forms::{Edit, Form};
539    /// let mut form = Form::parse(
540    ///     "form \"F\" version=1 width=99 height=99 {\n    label \"a\" x=0 y=0 w=9 h=9\n    panel name=box x=0 y=9 w=9 h=9\n}\n",
541    /// )?;
542    ///
543    /// // The label into the panel, which grows the braces it did not have.
544    /// form.apply(Edit::move_to(&[0], &[1], 0))?;
545    /// assert!(form.text().contains("panel name=box x=0 y=9 w=9 h=9 {"), "{}", form.text());
546    /// # Ok::<(), denise_forms::Error>(())
547    /// ```
548    /// Moves a node under another parent, or to another place among its
549    /// siblings.
550    pub fn move_to(from: &[usize], to: &[usize], index: usize) -> Self {
551        Edit::Move {
552            from: from.to_vec(),
553            to: to.to_vec(),
554            index,
555        }
556    }
557
558    /// Its children go with it, and so does the comment written above it — the
559    /// node's leading trivia is part of the node, which is what makes undoing a
560    /// removal put the comment back.
561    ///
562    /// ```
563    /// # use denise_forms::{Edit, Form};
564    /// let source = "form \"F\" version=1 width=99 height=99 {\n    // why\n    label \"a\" x=0 y=0 w=9 h=9\n}\n";
565    /// let mut form = Form::parse(source)?;
566    ///
567    /// let undo = form.apply(Edit::remove(&[0]))?;
568    /// assert!(!form.text().contains("why"));
569    ///
570    /// form.apply(undo)?;
571    /// assert_eq!(form.text(), source, "the comment came back with the node");
572    /// # Ok::<(), denise_forms::Error>(())
573    /// ```
574    /// Removes a node.
575    pub fn remove(path: &[usize]) -> Self {
576        Edit::Remove {
577            path: path.to_vec(),
578        }
579    }
580}
581
582/// One node of a form file, as the file writes it.
583///
584/// [`Form::written`] is where these come from, and says why they are not
585/// [`Placed`](crate::Placed).
586#[derive(Clone, Debug, PartialEq, Eq)]
587pub struct Written {
588    /// Child indices from the `form` node down to this one. Empty for the form.
589    pub path: Vec<usize>,
590    /// The node's name in the file — `label`, `panel`, `form`. Whatever the file
591    /// says, which is not necessarily a widget this toolkit has.
592    pub kind: String,
593    /// What `name=` gave it, if it gave one.
594    pub name: Option<String>,
595    /// Its positional argument — the `"Hello"` in `label "Hello"` — if it has
596    /// one. The other half of saying which node this is, to somebody who has
597    /// the file open and never named it.
598    pub argument: Option<String>,
599    /// The node's own entries, spelled canonically: its kind, then every
600    /// argument and property in the order the file writes them, and nothing
601    /// else. No children, no comment above it, and none of the spacing between
602    /// any of it — so a file somebody realigned by hand does not read as though
603    /// every node in it changed. Every string is quoted, whether the file
604    /// bothered to or not, for the same reason.
605    pub line: String,
606}
607
608/// A parsed form file.
609///
610/// Holds the document rather than a value taken from it — comments, spacing and
611/// entry order included — because the designer edits this and saves it back, and
612/// a save that reformats what nobody touched is a save people learn not to make.
613#[derive(Clone, Debug)]
614pub struct Form {
615    source: String,
616    doc: KdlDocument,
617}
618
619impl Form {
620    /// Parses a form file.
621    ///
622    /// The source is kept, and what is kept is checked: the parsed document is
623    /// written back out and compared to the input, so a file that would not
624    /// reproduce is refused with [`Reason::NotPreserved`] rather than accepted
625    /// and corrupted on the first save.
626    ///
627    /// This is bounded in shape and **not in time**: [`MAX_SOURCE`],
628    /// [`MAX_DEPTH`], [`MAX_COMMENTED_DEPTH`] and a brace count refuse every
629    /// slow file anybody has found, and `kdl` has exponential corners nobody
630    /// has found yet. For a form this program did not write, use
631    /// [`Form::parse_within`], which is this with a clock.
632    ///
633    /// ```
634    /// # use denise_forms::{Form, FormKind};
635    /// let form = Form::parse(r#"
636    ///     form "Hello" version=1 kind=screen width=460 height=260
637    /// "#)?;
638    /// assert_eq!(form.title(), "Hello");
639    /// assert_eq!(form.kind(), FormKind::Screen);
640    /// assert_eq!(form.size(), denise::Size::new(460, 260));
641    /// # Ok::<(), denise_forms::Error>(())
642    /// ```
643    pub fn parse(source: &str) -> Result<Self, Error> {
644        // Before the parser, not after: see `MAX_DEPTH`.
645        if source.len() > MAX_SOURCE {
646            return Err(Error::new(
647                At::START,
648                Reason::TooLarge { limit: MAX_SOURCE },
649            ));
650        }
651        if let Some(refusal) = unparseable(source) {
652            return Err(refusal.error(source));
653        }
654
655        let doc: KdlDocument = source.parse().map_err(|error: kdl::KdlError| {
656            let first = error.diagnostics.first();
657            let at = first.map_or(At::START, |d| At::of(source, d.span.offset()));
658            let message = first.map_or_else(
659                || String::from("this is not a KDL document"),
660                |d| {
661                    d.message
662                        .clone()
663                        .unwrap_or_else(|| d.help.clone().unwrap_or_default())
664                },
665            );
666            let message = if message.is_empty() {
667                String::from("this is not a KDL document")
668            } else {
669                message
670            };
671            Error::new(at, Reason::Syntax(message))
672        })?;
673
674        let mut doc = doc;
675        restore_after_close(&mut doc, source);
676        // What was accepted must be reproducible, or the first save silently
677        // loses bytes. The repair above covers every way kdl is known to drop
678        // trivia; anything it does not cover is refused here, at the first
679        // byte that differs — and the fuzz target `parse_form` treats this
680        // error as a finding, so the next lossy shape becomes a repair rather
681        // than a refusal.
682        let reproduced = doc.to_string();
683        if reproduced != source {
684            let at = source
685                .bytes()
686                .zip(reproduced.bytes())
687                .position(|(a, b)| a != b)
688                .unwrap_or_else(|| source.len().min(reproduced.len()));
689            return Err(Error::new(At::of(source, at), Reason::NotPreserved));
690        }
691
692        let form = Self {
693            source: source.to_string(),
694            doc,
695        };
696        form.check_shape()?;
697        Ok(form)
698    }
699
700    /// Everything that must be true before a single widget is built.
701    ///
702    /// Separate from building so that a file can be checked without an
703    /// application, a theme or a display — which is what `denise-forms check`
704    /// is, and what a form's own unit test wants.
705    fn check_shape(&self) -> Result<(), Error> {
706        let nodes = self.doc.nodes();
707        let root = match nodes {
708            [only] if only.name().value() == "form" => only,
709            [] => {
710                return Err(Error::new(
711                    At::START,
712                    Reason::NotAForm {
713                        found: String::from("nothing"),
714                    },
715                ));
716            }
717            [first, ..] => {
718                let found = if first.name().value() == "form" {
719                    // A second top-level node: point at *it*, not at the form.
720                    nodes[1].name().value().to_string()
721                } else {
722                    first.name().value().to_string()
723                };
724                let offender = if first.name().value() == "form" {
725                    &nodes[1]
726                } else {
727                    first
728                };
729                return Err(Error::new(
730                    self.at(offender.span().offset()),
731                    Reason::NotAForm { found },
732                ));
733            }
734        };
735
736        let version = root
737            .get("version")
738            .and_then(KdlValue::as_integer)
739            .and_then(|v| u64::try_from(v).ok())
740            .ok_or_else(|| Error::new(self.at_node(root), Reason::Version))?;
741        if version > VERSION {
742            return Err(Error::new(
743                self.at_node(root),
744                Reason::FromTheFuture {
745                    wanted: version,
746                    understood: VERSION,
747                },
748            ));
749        }
750
751        for axis in ["width", "height"] {
752            if root.get(axis).and_then(KdlValue::as_integer).is_none() {
753                return Err(Error::new(
754                    self.at_node(root),
755                    Reason::Missing {
756                        kind: String::from("form"),
757                        name: if axis == "width" { "width" } else { "height" },
758                    },
759                ));
760            }
761        }
762
763        let kind = self
764            .named(root, "kind", FormKind::NAMES, FormKind::from_name)?
765            .unwrap_or(FormKind::Screen);
766        self.named(root, "theme", THEMES, |n| THEMES.contains(&n).then_some(()))?;
767
768        // A property the form node does not have is a mistake, and every widget
769        // node has been told so since the beginning — the form node was the one
770        // place a typo went quietly into the file and stayed there. What counts
771        // is the descriptor, and the descriptor knows which kind it is: a
772        // `resizable` on a screen is not an unused property, it is a window's
773        // property on something that is not a window, and the message says so.
774        for entry in root.entries() {
775            let Some(name) = entry.name() else {
776                continue;
777            };
778            let name = name.value();
779            if name == "version" || crate::build::form_property(kind, name).is_some() {
780                continue;
781            }
782            let accepted: Vec<&'static str> = crate::build::FORM_PROPERTIES
783                .iter()
784                .chain(crate::build::kind_properties(kind))
785                .map(|property| property.name)
786                .collect();
787            return Err(Error::new(
788                self.at(entry.span().offset()),
789                Reason::UnknownFormProperty {
790                    kind: FormKind::NAMES[kind as usize],
791                    found: name.to_string(),
792                    accepted,
793                },
794            ));
795        }
796
797        // What comes in from an edge has to say how far, or there is nothing for
798        // the engine to slide and nothing for a designer to draw. Its other axis
799        // is the surface it comes in over, which is what `width` and `height`
800        // already are.
801        if matches!(kind, FormKind::Drawer | FormKind::Shelf)
802            && root.get("extent").and_then(KdlValue::as_integer).is_none()
803        {
804            return Err(Error::new(
805                self.at_node(root),
806                Reason::Missing {
807                    kind: String::from(FormKind::NAMES[kind as usize]),
808                    name: "extent",
809                },
810            ));
811        }
812        self.named(root, "side", denise_ui::widgets::SIDES, |n| {
813            denise_ui::widgets::describe::side_from_name(n)
814        })?;
815        if let Some(background) = root.get("background") {
816            let name = background.as_string().ok_or_else(|| {
817                Error::new(
818                    self.at_node(root),
819                    Reason::WrongType {
820                        kind: "form",
821                        name: String::from("background"),
822                        wanted: "one of the listed names",
823                    },
824                )
825            })?;
826            if denise_ui::widgets::describe::role_from_name(name).is_none() {
827                return Err(Error::new(
828                    self.at_node(root),
829                    Reason::NotAName {
830                        name: String::from("colour role"),
831                        found: name.to_string(),
832                        accepted: denise_ui::widgets::ROLES,
833                    },
834                ));
835            }
836        }
837        Ok(())
838    }
839
840    /// Reads an optional property that must be one of a fixed set of names.
841    fn named<T>(
842        &self,
843        node: &KdlNode,
844        property: &'static str,
845        accepted: &'static [&'static str],
846        parse: impl Fn(&str) -> Option<T>,
847    ) -> Result<Option<T>, Error> {
848        let Some(value) = node.get(property) else {
849            return Ok(None);
850        };
851        let name = value.as_string().ok_or_else(|| {
852            Error::new(
853                self.at_node(node),
854                Reason::WrongType {
855                    kind: "form",
856                    name: property.to_string(),
857                    wanted: "one of the listed names",
858                },
859            )
860        })?;
861        parse(name)
862            .ok_or_else(|| {
863                Error::new(
864                    self.at_node(node),
865                    Reason::NotAName {
866                        name: property.to_string(),
867                        found: name.to_string(),
868                        accepted,
869                    },
870                )
871            })
872            .map(Some)
873    }
874
875    pub(crate) fn at(&self, offset: usize) -> At {
876        At::of(&self.source, offset)
877    }
878
879    pub(crate) fn at_node(&self, node: &KdlNode) -> At {
880        self.at(node.span().offset())
881    }
882
883    /// The `form` node itself.
884    pub(crate) fn root(&self) -> &KdlNode {
885        self.doc
886            .nodes()
887            .first()
888            .expect("checked at parse: exactly one `form` node")
889    }
890
891    /// What a form says about itself, and the defaults for what it does not say.
892    ///
893    /// ```
894    /// # use denise_forms::{Form, FormKind};
895    /// let form = Form::parse(
896    ///     r#"form "Preferences" name=prefs version=1 kind=window width=520 height=340 theme=light background=base-200"#,
897    /// )?;
898    ///
899    /// assert_eq!(form.title(), "Preferences");
900    /// assert_eq!(form.name(), Some("prefs"));
901    /// assert_eq!(form.version(), 1);
902    /// assert_eq!(form.kind(), FormKind::Window);
903    /// assert_eq!(form.size(), denise::Size::new(520, 340));
904    /// assert_eq!(form.theme_name(), "light");
905    /// assert_eq!(form.background(), denise::Role::Base200);
906    /// assert_eq!(form.theme(), denise::theme::LIGHT);
907    ///
908    /// // Nothing written is the default: a window may be resized, and says
909    /// // nothing about a smallest size.
910    /// assert!(form.resizable());
911    /// assert_eq!(form.min_size(), None);
912    /// # Ok::<(), denise_forms::Error>(())
913    /// ```
914    /// The form's title — a window's title bar, and the designer's name for it.
915    pub fn title(&self) -> &str {
916        self.root()
917            .entries()
918            .iter()
919            .find(|e| e.name().is_none())
920            .and_then(|e| e.value().as_string())
921            .unwrap_or_default()
922    }
923
924    /// See [`Form::title`] for what a form says about itself.
925    /// The form's identifier, if it was given one.
926    pub fn name(&self) -> Option<&str> {
927        self.root().get("name").and_then(KdlValue::as_string)
928    }
929
930    /// See [`Form::title`] for what a form says about itself.
931    /// The schema version the file declares.
932    pub fn version(&self) -> u64 {
933        self.root()
934            .get("version")
935            .and_then(KdlValue::as_integer)
936            .and_then(|v| u64::try_from(v).ok())
937            .expect("checked at parse")
938    }
939
940    /// See [`Form::title`] for what a form says about itself.
941    /// What this form is for. [`FormKind::Screen`] unless the file says otherwise.
942    pub fn kind(&self) -> FormKind {
943        self.root()
944            .get("kind")
945            .and_then(KdlValue::as_string)
946            .and_then(FormKind::from_name)
947            .unwrap_or(FormKind::Screen)
948    }
949
950    /// See [`Form::title`] for what a form says about itself.
951    /// Whether this form consents to being drawn at another size.
952    ///
953    /// [`Scaling::None`] unless the file says otherwise, because that is what
954    /// every form written before the property existed already did.
955    pub fn scaling(&self) -> Scaling {
956        self.root()
957            .get("scaling")
958            .and_then(KdlValue::as_string)
959            .and_then(Scaling::from_name)
960            .unwrap_or_default()
961    }
962
963    /// How this form occupies a surface of some other size.
964    ///
965    /// Reads [`Form::scaling`] and does the arithmetic, so that the policy lives
966    /// in the file and the multiplication lives here — rather than in every
967    /// application that loads a form.
968    ///
969    /// ```
970    /// # use denise::Size;
971    /// # use denise_forms::Form;
972    /// # use denise::Rect;
973    /// let source = |scaling: &str| {
974    ///     format!(r#"form "F" version=1 width=200 height=100 scaling={scaling} {{ }}"#)
975    /// };
976    ///
977    /// // The default: its own size, in the middle of the surface.
978    /// let fixed = Form::parse(&source("none"))?;
979    /// let fit = fixed.fit(Size::new(400, 400));
980    /// assert_eq!((fit.x, fit.y), (1.0, 1.0));
981    /// assert_eq!(fit.rect, Rect::new(100, 150, 200, 100));
982    ///
983    /// // Proportional: as big as fits, letterboxed on the axis with room left.
984    /// let fits = Form::parse(&source("proportional"))?;
985    /// let fit = fits.fit(Size::new(400, 400));
986    /// assert_eq!((fit.x, fit.y), (2.0, 2.0), "the tighter axis decides");
987    /// assert_eq!(fit.rect, Rect::new(0, 100, 400, 200));
988    ///
989    /// // Stretch: the whole surface, whatever that does to the shape.
990    /// let fills = Form::parse(&source("stretch"))?;
991    /// let fit = fills.fit(Size::new(400, 400));
992    /// assert_eq!((fit.x, fit.y), (2.0, 4.0));
993    /// assert_eq!(fit.rect, Rect::from_size(Size::new(400, 400)));
994    /// # Ok::<(), denise_forms::Error>(())
995    /// ```
996    pub fn fit(&self, surface: Size) -> Placement {
997        let design = self.size();
998        // A form of no size cannot be fitted to anything; it is drawn where it
999        // is and the caller finds out from the empty rectangle.
1000        if design.width == 0 || design.height == 0 {
1001            return Placement {
1002                x: 1.0,
1003                y: 1.0,
1004                rect: Rect::ZERO,
1005            };
1006        }
1007        let full = (
1008            surface.width as f32 / design.width as f32,
1009            surface.height as f32 / design.height as f32,
1010        );
1011        let (x, y) = match self.scaling() {
1012            Scaling::None => (1.0, 1.0),
1013            Scaling::Proportional => {
1014                let both = if full.0 < full.1 { full.0 } else { full.1 };
1015                (both, both)
1016            }
1017            Scaling::Stretch => full,
1018        };
1019        // Scaled by its own edges from the origin, then centred in what is left
1020        // — so the two halves of the margin differ by at most a pixel and the
1021        // form is never a pixel wider than the arithmetic says.
1022        let scaled = Rect::from_size(design).scaled_by(x, y);
1023        Placement {
1024            x,
1025            y,
1026            rect: Rect::new(
1027                (surface.width as i32 - scaled.width) / 2,
1028                (surface.height as i32 - scaled.height) / 2,
1029                scaled.width,
1030                scaled.height,
1031            ),
1032        }
1033    }
1034
1035    /// ```
1036    /// # use denise_forms::Form;
1037    /// let fixed = Form::parse(
1038    ///     r#"form "F" version=1 kind=window width=400 height=300 resizable=#false min-width=320 min-height=240"#,
1039    /// )?;
1040    /// assert!(!fixed.resizable());
1041    /// assert_eq!(fixed.min_size(), Some(denise::Size::new(320, 240)));
1042    /// # Ok::<(), denise_forms::Error>(())
1043    /// ```
1044    /// Whether a window form may be resized. `true` unless the file says not.
1045    ///
1046    /// Meaningless on any other kind, which is why the file is not allowed to
1047    /// say it on one.
1048    pub fn resizable(&self) -> bool {
1049        self.root()
1050            .get("resizable")
1051            .and_then(KdlValue::as_bool)
1052            .unwrap_or(true)
1053    }
1054
1055    /// See [`Form::resizable`].
1056    /// The smallest a window form may be made, if it says.
1057    pub fn min_size(&self) -> Option<Size> {
1058        let axis = |name: &str| {
1059            self.root()
1060                .get(name)
1061                .and_then(KdlValue::as_integer)
1062                .and_then(|v| u32::try_from(v).ok())
1063        };
1064        match (axis("min-width"), axis("min-height")) {
1065            (None, None) => None,
1066            (width, height) => Some(Size::new(width.unwrap_or(0), height.unwrap_or(0))),
1067        }
1068    }
1069
1070    /// ```
1071    /// # use denise_forms::Form;
1072    /// let asked = Form::parse(r#"form "F" version=1 kind=dialog width=380 height=170 dim=200"#)?;
1073    /// assert_eq!(asked.dim(), 200);
1074    ///
1075    /// let quiet = Form::parse(r#"form "F" version=1 kind=dialog width=380 height=170"#)?;
1076    /// assert_eq!(quiet.dim(), 160);
1077    /// # Ok::<(), denise_forms::Error>(())
1078    /// ```
1079    /// How dark the backdrop behind a dialog is, 0 to 255. `160` by default,
1080    /// which is what [`denise_ui::Ui::push_scene`] is usually given.
1081    pub fn dim(&self) -> u8 {
1082        self.root()
1083            .get("dim")
1084            .and_then(KdlValue::as_integer)
1085            .and_then(|value| u8::try_from(value).ok())
1086            .unwrap_or(160)
1087    }
1088
1089    /// `width` and `height` are the surface it comes in *over*; [`extent`] is
1090    /// how far it comes in, and across the other axis it covers the surface.
1091    ///
1092    /// ```
1093    /// # use denise_forms::Form;
1094    /// # use denise_ui::Side;
1095    /// let drawer = Form::parse(r#"form "F" version=1 kind=drawer width=1024 height=600 extent=320"#)?;
1096    /// assert_eq!(drawer.side(), Side::Before);
1097    /// assert_eq!(drawer.extent(), 320);
1098    ///
1099    /// // A shelf is a bar rather than a side panel, so it comes in from below.
1100    /// let shelf = Form::parse(r#"form "F" version=1 kind=shelf width=1024 height=600 extent=180"#)?;
1101    /// assert_eq!(shelf.side(), Side::Below);
1102    /// # Ok::<(), denise_forms::Error>(())
1103    /// ```
1104    ///
1105    /// [`extent`]: Form::extent
1106    /// Which edge a drawer or a shelf comes in from.
1107    ///
1108    /// The defaults differ by kind and deliberately: a drawer is a side panel
1109    /// and a shelf is a bar, so they come in from different edges when nobody
1110    /// says.
1111    pub fn side(&self) -> Side {
1112        self.root()
1113            .get("side")
1114            .and_then(KdlValue::as_string)
1115            .and_then(denise_ui::widgets::describe::side_from_name)
1116            .unwrap_or_else(|| self.kind().default_side())
1117    }
1118
1119    /// See [`Form::side`].
1120    /// How far a drawer or a shelf comes in, in logical pixels.
1121    ///
1122    /// Required on those two kinds, so this is what the file says or `0` on a
1123    /// kind that has no such thing.
1124    pub fn extent(&self) -> i32 {
1125        self.root()
1126            .get("extent")
1127            .and_then(KdlValue::as_integer)
1128            .and_then(|value| i32::try_from(value).ok())
1129            .unwrap_or(0)
1130    }
1131
1132    /// See [`Form::title`] for what a form says about itself.
1133    /// The size the form was designed at, in logical pixels.
1134    pub fn size(&self) -> Size {
1135        let axis = |name: &str| {
1136            self.root()
1137                .get(name)
1138                .and_then(KdlValue::as_integer)
1139                .and_then(|v| u32::try_from(v).ok())
1140                .unwrap_or(0)
1141        };
1142        Size::new(axis("width"), axis("height"))
1143    }
1144
1145    /// See [`Form::title`] for what a form says about itself.
1146    /// The theme the file names, or the dark one.
1147    pub fn theme(&self) -> Theme {
1148        match self.theme_name() {
1149            "light" => theme::LIGHT,
1150            "high-contrast" => theme::HIGH_CONTRAST,
1151            _ => theme::DARK,
1152        }
1153    }
1154
1155    /// See [`Form::title`] for what a form says about itself.
1156    /// The theme's name, as the file spells it.
1157    pub fn theme_name(&self) -> &str {
1158        self.root()
1159            .get("theme")
1160            .and_then(KdlValue::as_string)
1161            .unwrap_or("dark")
1162    }
1163
1164    /// See [`Form::title`] for what a form says about itself.
1165    /// The surface the form is drawn on.
1166    pub fn background(&self) -> Role {
1167        self.root()
1168            .get("background")
1169            .and_then(KdlValue::as_string)
1170            .and_then(denise_ui::widgets::describe::role_from_name)
1171            .unwrap_or(Role::Base100)
1172    }
1173
1174    /// The file as it now stands.
1175    ///
1176    /// Byte for byte what was parsed, until something edits it, and then byte for
1177    /// byte what was parsed **apart from what was edited**. `kdl` holds the
1178    /// document rather than a value taken from it, so comments, blank lines,
1179    /// column alignment and entry order all survive an edit to a property three
1180    /// nodes away. That is the round trip the designer stands on, and the reason
1181    /// this crate parses the way it does.
1182    /// ```
1183    /// # use denise_forms::{Edit, Form};
1184    /// // A comment, a blank line, and columns somebody lined up by hand.
1185    /// let source = "\
1186    /// // The panel everything sits on.
1187    /// form \"F\" version=1 width=320 height=240 {
1188    ///
1189    ///     label \"One\"   x=8  y=8  w=80 h=20
1190    ///     label \"Two\"   x=8  y=32 w=80 h=20
1191    /// }
1192    /// ";
1193    /// let mut form = Form::parse(source)?;
1194    /// assert_eq!(form.text(), source, "parsing changed nothing");
1195    ///
1196    /// // One number, one line: everything else is where it was, spacing and all.
1197    /// form.apply(Edit::number(&[1], "y", Some(40)))?;
1198    /// assert_eq!(
1199    ///     form.text(),
1200    ///     source.replace("x=8  y=32", "x=8  y=40"),
1201    /// );
1202    /// # Ok::<(), denise_forms::Error>(())
1203    /// ```
1204    pub fn text(&self) -> String {
1205        self.doc.to_string()
1206    }
1207
1208    // ------------------------------------------------------------- editing
1209
1210    /// The node at a child path, if there is one.
1211    fn at_mut(&mut self, path: &[usize]) -> Option<&mut KdlNode> {
1212        // The empty path is the `form` node, the same as it is for `node_at`.
1213        // That is what lets an edit reach the form's own properties — its size,
1214        // its kind, its theme — through the one door every other edit uses, and
1215        // so undo them the same way.
1216        let Some((&first, rest)) = path.split_first() else {
1217            return self.doc.nodes_mut().first_mut();
1218        };
1219        let mut node = self
1220            .doc
1221            .nodes_mut()
1222            .first_mut()?
1223            .children_mut()
1224            .as_mut()?
1225            .nodes_mut()
1226            .get_mut(first)?;
1227        for &index in rest {
1228            node = node.children_mut().as_mut()?.nodes_mut().get_mut(index)?;
1229        }
1230        Some(node)
1231    }
1232
1233    /// Sets a whole-number property on the node at `path`.
1234    ///
1235    /// Replaces the value **in place** when the property is already there, which
1236    /// is what keeps a move to a one-line diff: everything else on the line, and
1237    /// every line around it, is untouched. Appends when it is not.
1238    ///
1239    /// Returns `false` if there is no node at that path.
1240    ///
1241    /// ```
1242    /// # use denise_forms::Form;
1243    /// let mut form = Form::parse(
1244    ///     "form \"F\" version=1 width=99 height=99 {\n    \
1245    ///      label \"hi\" x=10 y=20 w=30 h=40  // where it sits\n}\n",
1246    /// )?;
1247    /// assert!(form.set_number(&[0], "x", 25));
1248    /// assert!(form.text().contains("x=25 y=20"));
1249    /// assert!(form.text().contains("// where it sits"), "the comment survived");
1250    /// # Ok::<(), denise_forms::Error>(())
1251    /// ```
1252    pub fn set_number(&mut self, path: &[usize], name: &str, value: i64) -> bool {
1253        match self.at_mut(path) {
1254            // Only a `Literal::Verbatim` can fail to be written, so this is
1255            // always `true` when the path names a node.
1256            Some(node) => set_literal(node, name, &Literal::Int(value)).is_ok(),
1257            None => false,
1258        }
1259    }
1260
1261    /// The node at `path`, or the form itself for an empty one.
1262    fn node_at(&self, path: &[usize]) -> Option<&KdlNode> {
1263        let mut node = self.doc.nodes().first()?;
1264        for &index in path {
1265            node = node.children()?.nodes().get(index)?;
1266        }
1267        Some(node)
1268    }
1269
1270    /// What the file writes for a node's property, or `None` when it does not
1271    /// write it at all.
1272    ///
1273    /// The *value*, not the spelling: a string comes back unquoted, because an
1274    /// inspector's field edits the string and not the quotes around it. What a
1275    /// `None` means is the whole of "this property is at its default" — the
1276    /// schema does not write a default, so nothing written is the default.
1277    /// ```
1278    /// # use denise_forms::Form;
1279    /// let form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" name=greeting x=8 y=8 w=80 h=20 }"#)?;
1280    ///
1281    /// assert_eq!(form.property(&[0], "x").as_deref(), Some("8"));
1282    /// // Unquoted, because a field edits the string and not the quotes.
1283    /// assert_eq!(form.property(&[0], "name").as_deref(), Some("greeting"));
1284    /// // Not written is the default.
1285    /// assert_eq!(form.property(&[0], "role"), None);
1286    /// # Ok::<(), denise_forms::Error>(())
1287    /// ```
1288    pub fn property(&self, path: &[usize], name: &str) -> Option<String> {
1289        Some(spell(self.node_at(path)?.get(name)?))
1290    }
1291
1292    /// The arguments of a node's children of one kind, in file order.
1293    ///
1294    /// What a **collection** holds: a `select`'s `option`s, a `tabs`'s `tab`s, a
1295    /// `table`'s `column`s. Each item is the child's own argument, which is how
1296    /// every collection in this format writes its text.
1297    ///
1298    /// Named by the child node rather than by a plural, because that is what the
1299    /// file says and what [`PropertyKind::List`](denise_ui::widgets::PropertyKind::List)
1300    /// names: a property called `option` *is* the `option` nodes under it.
1301    ///
1302    /// ```
1303    /// # use denise_forms::Form;
1304    /// let form = Form::parse(
1305    ///     "form \"F\" version=1 width=99 height=99 {\n    select name=job x=0 y=0 w=9 h=9 {\n        option \"Reader\"\n        option \"Author\"\n    }\n}\n",
1306    /// )?;
1307    ///
1308    /// assert_eq!(form.items(&[0], "option"), ["Reader", "Author"]);
1309    /// // A kind the node does not hold, and a node that is not there.
1310    /// assert!(form.items(&[0], "tab").is_empty());
1311    /// assert!(form.items(&[9], "option").is_empty());
1312    /// # Ok::<(), denise_forms::Error>(())
1313    /// ```
1314    pub fn items(&self, path: &[usize], kind: &str) -> Vec<String> {
1315        let Some(node) = self.node_at(path) else {
1316            return Vec::new();
1317        };
1318        self.holder(node, kind)
1319            .map(|(block, _)| {
1320                block
1321                    .nodes()
1322                    .iter()
1323                    .filter(|child| child.name().value() == kind)
1324                    .map(|child| {
1325                        child
1326                            .entries()
1327                            .iter()
1328                            .find(|entry| entry.name().is_none())
1329                            .map_or_else(String::new, |entry| spell(entry.value()))
1330                    })
1331                    .collect()
1332            })
1333            .unwrap_or_default()
1334    }
1335
1336    /// How many children a node has, of every kind.
1337    ///
1338    /// Where an appended child goes. Not the same as `items(path, kind).len()`:
1339    /// a `table` holds `column`s *and* `row`s, so the index among one kind is
1340    /// not the index among children — which is the index every edit takes.
1341    ///
1342    /// ```
1343    /// # use denise_forms::Form;
1344    /// let form = Form::parse(
1345    ///     "form \"F\" version=1 width=99 height=99 {\n    table name=t x=0 y=0 w=9 h=9 {\n        column \"A\"\n        column \"B\"\n        design {\n            row \"1\"\n            row \"2\"\n        }\n    }\n}\n",
1346    /// )?;
1347    ///
1348    /// // Two columns and the `design` block, which is a child like any other.
1349    /// assert_eq!(form.child_count(&[0]), 3);
1350    /// assert_eq!(form.items(&[0], "row").len(), 2);
1351    /// assert_eq!(form.child_count(&[9]), 0);
1352    /// # Ok::<(), denise_forms::Error>(())
1353    /// ```
1354    pub fn child_count(&self, path: &[usize]) -> usize {
1355        self.node_at(path)
1356            .and_then(KdlNode::children)
1357            .map_or(0, |block| block.nodes().len())
1358    }
1359
1360    /// Where a node's `n`th child of one kind sits, for an edit that means it.
1361    ///
1362    /// A collection's items are addressed like any other node — see
1363    /// [`Edit::Argument`], [`Edit::Insert`], [`Edit::Remove`] and
1364    /// [`Edit::Move`], all of which already reach them — but the index among
1365    /// *`option`s* is not the index among children when a node holds more than
1366    /// one kind. This translates.
1367    ///
1368    /// ```
1369    /// # use denise_forms::Form;
1370    /// let form = Form::parse(
1371    ///     "form \"F\" version=1 width=99 height=99 {\n    table name=t x=0 y=0 w=9 h=9 {\n        column \"A\"\n        column \"B\"\n        design {\n            row \"1\"\n            row \"2\"\n        }\n    }\n}\n",
1372    /// )?;
1373    ///
1374    /// // The second `row` is the second child of the table's third child,
1375    /// // because placeholder content lives in `design`.
1376    /// assert_eq!(form.item_path(&[0], "row", 1), Some(vec![0, 2, 1]));
1377    /// // Real content is addressed where it is written.
1378    /// assert_eq!(form.item_path(&[0], "column", 1), Some(vec![0, 1]));
1379    /// // Past the end, and a kind the node does not hold.
1380    /// assert_eq!(form.item_path(&[0], "row", 2), None);
1381    /// assert_eq!(form.item_path(&[0], "option", 0), None);
1382    /// # Ok::<(), denise_forms::Error>(())
1383    /// ```
1384    pub fn item_path(&self, path: &[usize], kind: &str, nth: usize) -> Option<Vec<usize>> {
1385        let node = self.node_at(path)?;
1386        let (block, design) = self.holder(node, kind)?;
1387        let at = block
1388            .nodes()
1389            .iter()
1390            .enumerate()
1391            .filter(|(_, child)| child.name().value() == kind)
1392            .map(|(index, _)| index)
1393            .nth(nth)?;
1394        let mut full = path.to_vec();
1395        // A placeholder sits one level further down, inside `design`, and the
1396        // path has to say so or an edit lands on the wrong node.
1397        if let Some(index) = design {
1398            full.push(index);
1399        }
1400        full.push(at);
1401        Some(full)
1402    }
1403
1404    /// The node an item of `kind` is written under, as a path.
1405    ///
1406    /// The node at `path` itself for real content — a `table`'s `column`s are
1407    /// its own children. Its `design { … }` block for placeholder content, which
1408    /// is where a `row` goes so that no build but a designer's loads it.
1409    ///
1410    /// `None` when the node has no `design` block yet, which is the caller's cue
1411    /// to write one: the first `row` a designer adds brings the block with it.
1412    ///
1413    /// ```
1414    /// # use denise_forms::Form;
1415    /// let form = Form::parse(r#"
1416    /// form "F" version=1 width=99 height=99 {
1417    ///     table name=t x=0 y=0 w=99 h=99 {
1418    ///         column "Name"
1419    ///     }
1420    /// }
1421    /// "#)?;
1422    /// // A column is written on the table.
1423    /// assert_eq!(form.collection_parent(&[0], "column"), Some(vec![0]));
1424    /// // A row would need a `design` block, and there is none.
1425    /// assert_eq!(form.collection_parent(&[0], "row"), None);
1426    /// # Ok::<(), denise_forms::Error>(())
1427    /// ```
1428    pub fn collection_parent(&self, path: &[usize], kind: &str) -> Option<Vec<usize>> {
1429        let node = self.node_at(path)?;
1430        let (_, design) = self.holder(node, kind)?;
1431        let mut full = path.to_vec();
1432        if let Some(index) = design {
1433            full.push(index);
1434        }
1435        Some(full)
1436    }
1437
1438    /// Where a collection of `kind` under `node` actually lives, and the child
1439    /// index of the `design` block if it is in one.
1440    ///
1441    /// Real content is written as children of the node itself; placeholder
1442    /// content is written inside `design { … }`, so that every build but a
1443    /// designer's skips it. See [`Form::build_with_design`].
1444    fn holder<'n>(
1445        &self,
1446        node: &'n KdlNode,
1447        kind: &str,
1448    ) -> Option<(&'n KdlDocument, Option<usize>)> {
1449        let children = node.children()?;
1450        if !crate::build::is_placeholder(node.name().value(), kind) {
1451            return Some((children, None));
1452        }
1453        let at = children
1454            .nodes()
1455            .iter()
1456            .position(|child| child.name().value() == crate::build::DESIGN)?;
1457        Some((children.nodes()[at].children()?, Some(at)))
1458    }
1459
1460    /// The source of one node, as it stands in the file, with its own
1461    /// indentation taken off.
1462    ///
1463    /// What copying a node puts on the clipboard: `.dform` source that reads as
1464    /// source. Its children come with it, and so does a comment written above
1465    /// it — the node's leading trivia is part of the node, which is the same
1466    /// reason an undone removal puts the comment back.
1467    /// ```
1468    /// # use denise_forms::Form;
1469    /// let form = Form::parse(
1470    ///     "form \"F\" version=1 width=99 height=99 {\n    panel name=box x=0 y=0 w=9 h=9 {\n        label \"in\" x=1 y=1 w=2 h=2\n    }\n}\n",
1471    /// )?;
1472    ///
1473    /// assert_eq!(
1474    ///     form.node_text(&[0]).as_deref(),
1475    ///     Some("panel name=box x=0 y=0 w=9 h=9 {\n    label \"in\" x=1 y=1 w=2 h=2\n}\n"),
1476    /// );
1477    /// assert_eq!(form.node_text(&[9]), None);
1478    /// # Ok::<(), denise_forms::Error>(())
1479    /// ```
1480    pub fn node_text(&self, path: &[usize]) -> Option<String> {
1481        let node = self.node_at(path)?;
1482        let own = indent_of(node);
1483        Some(reindent(&node.to_string(), &own, "", false))
1484    }
1485
1486    /// Every node in the file, depth first, the form node itself first.
1487    ///
1488    /// What can be known about a form **without building it**, which is what
1489    /// comparing two versions of the same file needs: [`Placed`](crate::Placed)
1490    /// is the same
1491    /// node after [`build`](Form::build) and carries a `NodeId` that only exists
1492    /// once there is a tree, so it cannot describe a file nobody has opened.
1493    ///
1494    /// ```
1495    /// # use denise_forms::Form;
1496    /// let form = Form::parse(
1497    ///     "form \"F\" version=1 width=99 height=99 {\n    panel name=box x=0 y=0 w=9 h=9 {\n        label \"in\" x=1 y=1 w=2 h=2\n    }\n}\n",
1498    /// )?;
1499    ///
1500    /// let written = form.written();
1501    /// let kinds: Vec<&str> = written.iter().map(|node| node.kind.as_str()).collect();
1502    /// assert_eq!(kinds, ["form", "panel", "label"]);
1503    /// assert_eq!(written[1].name.as_deref(), Some("box"));
1504    /// assert_eq!(written[2].argument.as_deref(), Some("in"));
1505    /// assert_eq!(written[2].path, vec![0, 0]);
1506    /// // The node itself, without the children indented under it.
1507    /// assert_eq!(written[1].line, "panel name=\"box\" x=0 y=0 w=9 h=9");
1508    /// # Ok::<(), denise_forms::Error>(())
1509    /// ```
1510    pub fn written(&self) -> Vec<Written> {
1511        let mut out = Vec::new();
1512        let Some(root) = self.doc.nodes().first() else {
1513            return out;
1514        };
1515        gather(root, &mut Vec::new(), &mut out);
1516        out
1517    }
1518
1519    /// ```
1520    /// # use denise_forms::Form;
1521    /// let form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hello" x=0 y=0 w=9 h=9 }"#)?;
1522    /// assert_eq!(form.argument(&[0]).as_deref(), Some("Hello"));
1523    /// // The form's own argument is its title.
1524    /// assert_eq!(form.argument(&[]).as_deref(), Some("F"));
1525    /// # Ok::<(), denise_forms::Error>(())
1526    /// ```
1527    /// A node's positional argument — the `"Hello"` in `label "Hello"`.
1528    pub fn argument(&self, path: &[usize]) -> Option<String> {
1529        let node = self.node_at(path)?;
1530        let entry = node.entries().iter().find(|e| e.name().is_none())?;
1531        Some(spell(entry.value()))
1532    }
1533
1534    /// Removes a property from the node at `path`.
1535    ///
1536    /// What a designer does when a property goes back to its default: the schema
1537    /// says a default is not written, so resetting one is deleting it rather than
1538    /// spelling it out.
1539    /// ```
1540    /// # use denise_forms::Form;
1541    /// let mut form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" x=0 y=0 w=9 h=9 role=primary }"#)?;
1542    ///
1543    /// assert!(form.clear_property(&[0], "role"));
1544    /// assert_eq!(form.property(&[0], "role"), None);
1545    /// // Nothing there to clear.
1546    /// assert!(!form.clear_property(&[0], "role"));
1547    /// # Ok::<(), denise_forms::Error>(())
1548    /// ```
1549    ///
1550    /// Use [`Form::apply`] with [`Edit::property`] instead where the change has
1551    /// to be undoable: this one hands back nothing to put it back with.
1552    pub fn clear_property(&mut self, path: &[usize], name: &str) -> bool {
1553        let Some(node) = self.at_mut(path) else {
1554            return false;
1555        };
1556        // Not `KdlNode::remove`, whose documentation says string keys remove
1557        // properties and which returns `None` and removes nothing — `entry("z")`
1558        // finds the property that `remove("z")` cannot. `retain` does what the
1559        // other was for, and leaves the rest of the line alone.
1560        let before = node.entries().len();
1561        node.retain(|entry| entry.name().map(|key| key.value()) != Some(name));
1562        node.entries().len() != before
1563    }
1564
1565    /// Removes the node at `path`, and everything under it.
1566    ///
1567    /// Returns `false` if there is no node there.
1568    /// ```
1569    /// # use denise_forms::Form;
1570    /// let mut form = Form::parse(r#"form "F" version=1 width=99 height=99 { label "Hi" x=0 y=0 w=9 h=9 }"#)?;
1571    ///
1572    /// assert!(form.remove_at(&[0]));
1573    /// assert!(!form.text().contains("label"));
1574    /// assert!(!form.remove_at(&[0]), "there is nothing there now");
1575    /// # Ok::<(), denise_forms::Error>(())
1576    /// ```
1577    ///
1578    /// Use [`Form::apply`] with [`Edit::remove`] instead where the change has to
1579    /// be undoable.
1580    pub fn remove_at(&mut self, path: &[usize]) -> bool {
1581        let Some((&last, above)) = path.split_last() else {
1582            return false;
1583        };
1584        let Some(parent) = self.children_of_mut(above) else {
1585            return false;
1586        };
1587        if last >= parent.len() {
1588            return false;
1589        }
1590        parent.remove(last);
1591        true
1592    }
1593
1594    /// Applies an edit, and hands back the edit that undoes it.
1595    ///
1596    /// The whole of undo, and the reason it is exact: because this crate holds
1597    /// the **document** rather than a value taken from it, the inverse of an edit
1598    /// is knowable at the moment it is made and is itself an ordinary edit. There
1599    /// is no snapshot of anything — a stack of these is a stack of small,
1600    /// reversible facts.
1601    ///
1602    /// ```
1603    /// # use denise_forms::{Edit, Form};
1604    /// let source = "form \"F\" version=1 width=9 height=9 {\n    \
1605    ///                label \"hi\" x=1 y=2 w=3 h=4  // a note\n}\n";
1606    /// let mut form = Form::parse(source)?;
1607    ///
1608    /// let undo = form.apply(Edit::number(&[0], "x", Some(40)))?;
1609    /// assert!(form.text().contains("x=40"));
1610    ///
1611    /// form.apply(undo)?;
1612    /// assert_eq!(form.text(), source, "byte for byte, comment and all");
1613    /// # Ok::<(), denise_forms::Error>(())
1614    /// ```
1615    ///
1616    /// # Errors
1617    ///
1618    /// When the path names no node, when the text of an insertion is not one
1619    /// node, or when a property being replaced holds something other than a
1620    /// whole number — which could not be put back, and so is refused rather than
1621    /// silently made irreversible.
1622    pub fn apply(&mut self, edit: Edit) -> Result<Edit, Error> {
1623        match edit {
1624            Edit::Property { path, name, value } => {
1625                let node = self.at_mut(&path).ok_or_else(|| {
1626                    Error::new(At::START, Reason::NoSuchNode { path: path.clone() })
1627                })?;
1628                let Some(literal) = value else {
1629                    // Taking a property away cannot be undone by putting it back
1630                    // by name: an added entry lands at the end of the line rather
1631                    // than in its place in it. The node's own text is what
1632                    // carries the order.
1633                    let text = node.to_string();
1634                    let key = name.clone();
1635                    node.retain(|entry| entry.name().map(|k| k.value()) != Some(&key));
1636                    return Ok(Edit::Replace { path, text });
1637                };
1638
1639                // What was there, spelled the way the file spelled it. Anything
1640                // else would put `1_000` back as `1000`, which is a correct undo
1641                // and not an exact one.
1642                let before = node
1643                    .entry(name.as_str())
1644                    .map(|entry| Literal::Verbatim(repr_of(entry)));
1645                if let Some(was) = node.get(name.as_str()) {
1646                    let (holds, given) = (Class::of(was), literal.class()?);
1647                    if holds != given && holds != Class::Nothing {
1648                        return Err(Error::new(
1649                            At::START,
1650                            Reason::WrongKind {
1651                                name,
1652                                holds: holds.noun(),
1653                                given: given.noun(),
1654                            },
1655                        ));
1656                    }
1657                }
1658
1659                set_literal(node, name.as_str(), &literal)?;
1660                Ok(Edit::Property {
1661                    path,
1662                    name,
1663                    value: before,
1664                })
1665            }
1666
1667            Edit::Insert {
1668                parent,
1669                index,
1670                text,
1671            } => {
1672                let mut node = one_node(&text)?;
1673                let holder = self.node_at(&parent).ok_or_else(|| {
1674                    Error::new(
1675                        At::START,
1676                        Reason::NoSuchNode {
1677                            path: parent.clone(),
1678                        },
1679                    )
1680                })?;
1681                // A parent written without a `{ }` gets one — which is every
1682                // panel a designer has just placed. Undoing that has to take the
1683                // block away with the node, or an undone drop would leave an
1684                // empty pair of braces behind; the parent's own text is what
1685                // carries both, so the inverse replaces the parent rather than
1686                // removing the child.
1687                let empty = holder.children().is_none();
1688                let was = empty.then(|| holder.to_string());
1689                let indent = indent_of(holder);
1690                let step = self.indent_step();
1691
1692                let bare = node.format().is_none_or(|format| format.leading.is_empty());
1693                let children = self.block_mut(&parent).ok_or_else(|| {
1694                    Error::new(
1695                        At::START,
1696                        Reason::NoSuchNode {
1697                            path: parent.clone(),
1698                        },
1699                    )
1700                })?;
1701                let index = index.min(children.nodes().len());
1702
1703                // Trivia the caller gave stands: a removal's inverse carries the
1704                // node's own indentation and the comment written above it, and
1705                // putting that back is the whole of an exact undo. Text with
1706                // none — a designer's freshly seeded node — is laid out here,
1707                // because this is what knows how deep it is going.
1708                //
1709                // Every node ends in a newline, and its indentation is its own;
1710                // only the first in a block also carries the newline that
1711                // follows the brace, because nothing before it did.
1712                if bare {
1713                    // And *all* of it, not only its first line: a pasted panel
1714                    // arrives with its children, and they are as deep as the
1715                    // panel is now.
1716                    let laid = reindent(
1717                        &node.to_string(),
1718                        "",
1719                        &format!("{indent}{step}"),
1720                        index == 0,
1721                    );
1722                    node = one_node(&laid)?;
1723                    let mut format = node.format().cloned().unwrap_or_default();
1724                    format.terminator = String::from("\n");
1725                    node.set_format(format);
1726                }
1727
1728                // Whether the arrival carries the newline that follows the
1729                // opening brace — which only the first node in a block does.
1730                let opens = node
1731                    .format()
1732                    .is_some_and(|format| format.leading.starts_with('\n'));
1733                let displaced = index == 0 && !children.nodes().is_empty();
1734                children.nodes_mut().insert(index, node);
1735                // The node that used to be first is not any more, and the line
1736                // it was holding open is being held by the new arrival. Leaving
1737                // it with its own newline would leave a blank line behind —
1738                // exactly the mirror of what `Remove` puts right when it takes
1739                // the first node out.
1740                if displaced
1741                    && opens
1742                    && let Some(after) = children.nodes_mut().get_mut(1)
1743                {
1744                    let mut format = after.format().cloned().unwrap_or_default();
1745                    if let Some(rest) = format.leading.strip_prefix('\n') {
1746                        format.leading = rest.to_string();
1747                        after.set_format(format);
1748                    }
1749                }
1750                if empty {
1751                    // The closing brace lines up under the node that opened it,
1752                    // and there is a space before the one that opens it.
1753                    let mut format = children.format().cloned().unwrap_or_default();
1754                    format.trailing = indent.clone();
1755                    children.set_format(format);
1756                }
1757                if empty && let Some(holder) = self.at_mut(&parent) {
1758                    let mut format = holder.format().cloned().unwrap_or_default();
1759                    format.before_children = String::from(" ");
1760                    holder.set_format(format);
1761                }
1762
1763                match was {
1764                    Some(text) => Ok(Edit::Replace { path: parent, text }),
1765                    None => {
1766                        let mut path = parent;
1767                        path.push(index);
1768                        Ok(Edit::Remove { path })
1769                    }
1770                }
1771            }
1772
1773            Edit::Argument { path, value } => {
1774                let node = self.at_mut(&path).ok_or_else(|| {
1775                    Error::new(At::START, Reason::NoSuchNode { path: path.clone() })
1776                })?;
1777                let Some(entry) = node.entries_mut().iter_mut().find(|e| e.name().is_none()) else {
1778                    return Err(Error::new(At::START, Reason::NoArgument));
1779                };
1780                let was = Literal::Verbatim(repr_of(entry));
1781                let (new, repr) = value.parts()?;
1782                entry.set_value(new);
1783                match entry.format_mut() {
1784                    Some(format) => format.value_repr = repr,
1785                    None => entry.set_format(KdlEntryFormat {
1786                        value_repr: repr,
1787                        leading: String::from(" "),
1788                        ..KdlEntryFormat::default()
1789                    }),
1790                }
1791                Ok(Edit::Argument { path, value: was })
1792            }
1793
1794            Edit::Move { from, to, index } => {
1795                // A node cannot go inside itself, and the form is the document
1796                // rather than a node in it.
1797                if from.is_empty() || to.starts_with(&from) {
1798                    return Err(Error::new(At::START, Reason::IntoItself { path: from }));
1799                }
1800                let node = self.node_at(&from).ok_or_else(|| {
1801                    Error::new(At::START, Reason::NoSuchNode { path: from.clone() })
1802                })?;
1803                let text = node.to_string();
1804                let old = indent_of(node);
1805
1806                // Where it is going, once taking it out has shifted whatever
1807                // came after it — the case that is easy to miss: removing `[1]`
1808                // moves `[2]` to `[1]`, so a move that named `[2]` as its
1809                // destination has to be told.
1810                let landing = after_removing(&to, &from).ok_or_else(|| {
1811                    Error::new(At::START, Reason::IntoItself { path: from.clone() })
1812                })?;
1813                let step = self.indent_step();
1814                let new = match self.node_at(&to) {
1815                    Some(parent) if !to.is_empty() => indent_of(parent) + &step,
1816                    Some(_) => step,
1817                    None => {
1818                        return Err(Error::new(At::START, Reason::NoSuchNode { path: to }));
1819                    }
1820                };
1821
1822                // How many children the landing will have once the node has
1823                // left it, which decides both where the index can reach and
1824                // whether it becomes the first — the one position that carries
1825                // the newline after the brace.
1826                let held = self
1827                    .node_at(&to)
1828                    .and_then(KdlNode::children)
1829                    .map_or(0, |block| block.nodes().len());
1830                let leaving = from.len() == to.len() + 1 && from.starts_with(&to);
1831                let index = index.min(held - usize::from(leaving && held > 0));
1832
1833                // The blank lines that stood above it, which travel with it.
1834                //
1835                // A node's leading newlines are **one structural** — the line
1836                // the brace opened, carried by whichever node is first in the
1837                // block — plus one for every blank line above it. Nothing in the
1838                // trivia says which is which; only the node's position does. So
1839                // the count is taken here, where the old position is still
1840                // known, and put back below against the new one.
1841                let had = text.len() - text.trim_start_matches('\n').len();
1842                let blanks = had.saturating_sub(usize::from(from.last() == Some(&0)));
1843
1844                let text = reindent(&text, &old, &new, index == 0);
1845                // `reindent` leaves exactly the structural newline the landing
1846                // calls for; the blank lines go back on top of it. Pure newlines,
1847                // so the order among them does not matter — what matters is how
1848                // many.
1849                let text = if blanks == 0 {
1850                    text
1851                } else {
1852                    let mut with = "\n".repeat(blanks);
1853                    with.push_str(&text);
1854                    with
1855                };
1856                // Two edits as one, so the inverse is the pair reversed: put the
1857                // node back where it came from, with the text it had there, and
1858                // take away any braces this had to make. `Many` already does all
1859                // of that, and doing it by hand would be doing it again.
1860                self.apply(Edit::Many(vec![
1861                    Edit::Remove { path: from },
1862                    Edit::Insert {
1863                        parent: landing,
1864                        index,
1865                        text,
1866                    },
1867                ]))
1868            }
1869
1870            Edit::Remove { path } => {
1871                let (&last, above) = path.split_last().ok_or_else(|| {
1872                    Error::new(At::START, Reason::NoSuchNode { path: Vec::new() })
1873                })?;
1874                let above = above.to_vec();
1875                let children = self.children_of_mut(&above).ok_or_else(|| {
1876                    Error::new(
1877                        At::START,
1878                        Reason::NoSuchNode {
1879                            path: above.clone(),
1880                        },
1881                    )
1882                })?;
1883                if last >= children.len() {
1884                    return Err(Error::new(At::START, Reason::NoSuchNode { path }));
1885                }
1886                // Taking the *first* node out of a block leaves the next one
1887                // holding the line the brace opened, and it was not written to
1888                // carry the newline that starts it. Putting that right changes a
1889                // line nobody asked about, so the inverse restores the parent's
1890                // whole text rather than reasoning about which newline went
1891                // where — the same answer this file gives every time an edit
1892                // cannot be undone by putting one value back.
1893                // Two shapes of removal cannot be undone by putting the node
1894                // back on its own, and both are answered the same way this file
1895                // answers every such case — with the parent's own text, which
1896                // carries the shape as well as the contents.
1897                //
1898                // Taking the *first* node out leaves the next one holding the
1899                // line the brace opened, and it was not written to carry the
1900                // newline that starts it. Taking the *last* one out leaves an
1901                // empty `{ }` that nobody typed.
1902                let opener = last == 0 && children.len() > 1;
1903                let emptied = children.len() == 1;
1904                let was = (opener || emptied).then(|| {
1905                    self.node_at(&above)
1906                        .map_or_else(String::new, |node| node.to_string())
1907                });
1908
1909                let children = self.children_of_mut(&above).expect("just found");
1910                // The node's own text carries its leading trivia — its
1911                // indentation and any comment written above it — so putting it
1912                // back puts all of that back too.
1913                let text = children.remove(last).to_string();
1914                if opener && let Some(first) = children.first_mut() {
1915                    // Unconditionally, and that is the whole of the other half
1916                    // of #151. The node that is now first never held the line
1917                    // the brace opened, so it always needs one — and a leading
1918                    // newline it *already* has is a blank line somebody wrote,
1919                    // not the structural one. Skipping the insert when it starts
1920                    // with `\n` looked like idempotence and was a blank line
1921                    // being quietly promoted into the structural newline, so a
1922                    // node moved away from the front took the blank line above
1923                    // its neighbour with it.
1924                    let mut format = first.format().cloned().unwrap_or_default();
1925                    format.leading.insert(0, '\n');
1926                    first.set_format(format);
1927                }
1928                if emptied {
1929                    self.drop_block(&above);
1930                }
1931                match was {
1932                    Some(text) => Ok(Edit::Replace { path: above, text }),
1933                    None => Ok(Edit::Insert {
1934                        parent: above,
1935                        index: last,
1936                        text,
1937                    }),
1938                }
1939            }
1940
1941            Edit::Many(edits) => {
1942                let mut inverses: Vec<Edit> = Vec::with_capacity(edits.len());
1943                for edit in edits {
1944                    match self.apply(edit) {
1945                        Ok(inverse) => inverses.push(inverse),
1946                        Err(error) => {
1947                            // Put back what was already done, newest first, so a
1948                            // refused compound leaves the document as it was.
1949                            while let Some(inverse) = inverses.pop() {
1950                                let _ = self.apply(inverse);
1951                            }
1952                            return Err(error);
1953                        }
1954                    }
1955                }
1956                inverses.reverse();
1957                Ok(Edit::Many(inverses))
1958            }
1959
1960            Edit::Replace { path, text } => {
1961                let node = one_node(&text)?;
1962                let Some((&last, above)) = path.split_last() else {
1963                    // The form itself, which is the whole document: an insertion
1964                    // into a form written without a `{ }` undoes to this.
1965                    let root = self
1966                        .doc
1967                        .nodes_mut()
1968                        .first_mut()
1969                        .ok_or_else(|| Error::new(At::START, Reason::NoSuchNode { path }))?;
1970                    let was = root.to_string();
1971                    *root = node;
1972                    return Ok(Edit::Replace {
1973                        path: Vec::new(),
1974                        text: was,
1975                    });
1976                };
1977                let above = above.to_vec();
1978                let children = self.children_of_mut(&above).ok_or_else(|| {
1979                    Error::new(
1980                        At::START,
1981                        Reason::NoSuchNode {
1982                            path: above.clone(),
1983                        },
1984                    )
1985                })?;
1986                if last >= children.len() {
1987                    return Err(Error::new(At::START, Reason::NoSuchNode { path }));
1988                }
1989                let was = children[last].to_string();
1990                children[last] = node;
1991                Ok(Edit::Replace { path, text: was })
1992            }
1993        }
1994    }
1995
1996    /// One step of indentation, as this file writes it.
1997    ///
1998    /// Read from the form's own first child rather than assumed to be four
1999    /// spaces, so a file written with two stays written with two.
2000    fn indent_step(&self) -> String {
2001        self.doc
2002            .nodes()
2003            .first()
2004            .and_then(KdlNode::children)
2005            .and_then(|block| block.nodes().first())
2006            .map(indent_of)
2007            .filter(|indent| !indent.is_empty())
2008            .unwrap_or_else(|| String::from("    "))
2009    }
2010
2011    /// Takes away the children block of the node at `path`.
2012    ///
2013    /// So that there is no such thing as an empty one: `panel name=card` and
2014    /// `panel name=card { }` mean the same to the engine, and only the first is
2015    /// what somebody would have written.
2016    fn drop_block(&mut self, path: &[usize]) {
2017        let holder = if path.is_empty() {
2018            self.doc.nodes_mut().first_mut()
2019        } else {
2020            self.at_mut(path)
2021        };
2022        if let Some(node) = holder {
2023            *node.children_mut() = None;
2024        }
2025    }
2026
2027    /// The children block of the node at `path`, made if it has none.
2028    fn block_mut(&mut self, path: &[usize]) -> Option<&mut KdlDocument> {
2029        if path.is_empty() {
2030            return Some(self.doc.nodes_mut().first_mut()?.ensure_children());
2031        }
2032        Some(self.at_mut(path)?.ensure_children())
2033    }
2034
2035    /// The children of the node at `path`, or of the form itself for an empty one.
2036    fn children_of_mut(&mut self, path: &[usize]) -> Option<&mut Vec<KdlNode>> {
2037        if path.is_empty() {
2038            return Some(
2039                self.doc
2040                    .nodes_mut()
2041                    .first_mut()?
2042                    .children_mut()
2043                    .as_mut()?
2044                    .nodes_mut(),
2045            );
2046        }
2047        Some(self.at_mut(path)?.children_mut().as_mut()?.nodes_mut())
2048    }
2049}
2050
2051/// Lays a form's indentation out again, and changes nothing else.
2052///
2053/// Not a canonical formatter. `kdl`'s own
2054/// ([`autoformat`](https://docs.rs/kdl/latest/kdl/struct.KdlDocument.html#method.autoformat))
2055/// is one, and it deletes a comment written at the end of a node's line
2056/// ([#119](https://github.com/bisand/denise/issues/119),
2057/// [kdl-org/kdl-rs#179](https://github.com/kdl-org/kdl-rs/issues/179)) — which
2058/// is not a thing to ship into a format whose first promise is that comments
2059/// survive. It also unquotes strings and drops blank lines.
2060///
2061/// So this does the one thing hand-editing actually breaks and nothing else:
2062/// **only the whitespace at the two ends of a line is ever touched.** Comments
2063/// keep their text and their position, strings keep their quoting, properties
2064/// keep their order, blank lines stay blank lines, and columns lined up by hand
2065/// inside a line stay lined up. What changes is the indent in front of each
2066/// line, to one step per level of nesting, and trailing whitespace, which goes.
2067///
2068/// The step is the file's own — whatever the first node inside `form` uses — so
2069/// a file written with two spaces stays a two-space file. Four spaces is the
2070/// fallback for a file that does not say.
2071///
2072/// Lines inside a multi-line string or a block comment are left exactly as they
2073/// are, because those are content rather than layout.
2074///
2075/// ```
2076/// # use denise_forms::tidy;
2077/// let ragged = "\
2078/// form \"F\" version=1 width=20 height=20 {
2079///     label \"one\" x=0 y=0 w=5 h=5   // kept, and still here
2080///         label \"two\" x=0 y=6 w=5 h=5
2081/// }
2082/// ";
2083/// let tidied = tidy(ragged)?;
2084/// assert_eq!(tidied, "\
2085/// form \"F\" version=1 width=20 height=20 {
2086///     label \"one\" x=0 y=0 w=5 h=5   // kept, and still here
2087///     label \"two\" x=0 y=6 w=5 h=5
2088/// }
2089/// ");
2090/// # Ok::<(), denise_forms::Error>(())
2091/// ```
2092///
2093/// Refuses a file it cannot parse, because a formatter that rewrites what it
2094/// does not understand is how a file gets lost.
2095pub fn tidy(source: &str) -> Result<String, Error> {
2096    let form = Form::parse(source)?;
2097    Ok(laid_out(source, &form.indent_step()))
2098}
2099
2100/// See [`tidy`]. Split out so the arithmetic can be tested on source that is
2101/// not a whole form.
2102fn laid_out(source: &str, step: &str) -> String {
2103    let b = source.as_bytes();
2104    // Which byte each line starts at, and the depth the scan had reached there.
2105    let mut depth_at_line = vec![0usize];
2106    // Lines whose own bytes are inside a string or a block comment, and so are
2107    // content rather than layout.
2108    let mut protected = vec![false];
2109    let mut depth = 0usize;
2110    let mut i = 0usize;
2111
2112    while i < b.len() {
2113        if let Some(past) = not_structure(b, i) {
2114            // A newline inside one of these starts a line nobody may reindent.
2115            for _ in b[i..past.min(b.len())]
2116                .iter()
2117                .filter(|byte| **byte == b'\n')
2118            {
2119                depth_at_line.push(depth);
2120                protected.push(true);
2121            }
2122            i = past.max(i + 1);
2123            continue;
2124        }
2125        match b[i] {
2126            b'{' => depth += 1,
2127            b'}' => depth = depth.saturating_sub(1),
2128            b'\n' => {
2129                depth_at_line.push(depth);
2130                protected.push(false);
2131            }
2132            _ => {}
2133        }
2134        i += 1;
2135    }
2136
2137    let mut out = String::with_capacity(source.len());
2138    for (number, line) in source.split_inclusive('\n').enumerate() {
2139        let (body, ending) = match line.strip_suffix('\n') {
2140            Some(body) => (body, "\n"),
2141            None => (line, ""),
2142        };
2143        if protected.get(number).copied().unwrap_or(false) {
2144            out.push_str(line);
2145            continue;
2146        }
2147        let trimmed = body.trim();
2148        if trimmed.is_empty() {
2149            // A blank line is a paragraph break, and carries no indentation.
2150            out.push_str(ending);
2151            continue;
2152        }
2153        // A line that starts by closing its block belongs one level out.
2154        let depth = depth_at_line.get(number).copied().unwrap_or(0);
2155        let depth = if trimmed.starts_with('}') {
2156            depth.saturating_sub(1)
2157        } else {
2158            depth
2159        };
2160        for _ in 0..depth {
2161            out.push_str(step);
2162        }
2163        out.push_str(body.trim_start());
2164        // Trailing whitespace is never anything, and after a closing brace it
2165        // is the shape that `restore_after_close` exists to keep -- so tidying
2166        // a file is also how somebody gets rid of it.
2167        while out.ends_with(' ') || out.ends_with('\t') {
2168            out.pop();
2169        }
2170        out.push_str(ending);
2171    }
2172    out
2173}
2174
2175/// If a comment or a string begins at `at`, the offset just past it.
2176///
2177/// **The one place that decides what is not structure.** A `{` inside a string
2178/// or a comment is text, and everything that walks form source without parsing
2179/// it — the limits in [`unparseable`], the indentation in [`tidy`] — has to
2180/// agree about which those are. They agree by both asking here; four separate
2181/// bugs came out of two scans disagreeing before this was one function.
2182///
2183/// It should never recognise a string that `kdl` would not, because every skip
2184/// is a stretch of bytes not counted as structure. So a string is skipped only
2185/// when it is certainly a string: it must close, a `"""` must open a line, and
2186/// its closing `"""` must lead one. Being wrong the other way — counting the
2187/// insides of something that turns out to be a string — cannot refuse a file
2188/// that parses, and that is the direction to be wrong in.
2189fn not_structure(b: &[u8], at: usize) -> Option<usize> {
2190    match *b.get(at)? {
2191        // A line comment, to the end of its line but not over the newline:
2192        // callers count that newline themselves.
2193        b'/' if b.get(at + 1) == Some(&b'/') => {
2194            let mut i = at;
2195            while i < b.len() && b[i] != b'\n' {
2196                i += 1;
2197            }
2198            Some(i)
2199        }
2200        // A block comment, which nests.
2201        b'/' if b.get(at + 1) == Some(&b'*') => {
2202            let mut open = 1usize;
2203            let mut i = at + 2;
2204            while i < b.len() && open > 0 {
2205                if b[i] == b'/' && b.get(i + 1) == Some(&b'*') {
2206                    open += 1;
2207                    i += 2;
2208                } else if b[i] == b'*' && b.get(i + 1) == Some(&b'/') {
2209                    open -= 1;
2210                    i += 2;
2211                } else {
2212                    i += 1;
2213                }
2214            }
2215            Some(i)
2216        }
2217        // A raw string, `#"..."#` with matching hashes. Bare `#true` and
2218        // friends fall through harmlessly: no quote follows the hashes.
2219        b'#' => {
2220            let mut hashes = 0usize;
2221            let mut i = at;
2222            while b.get(i) == Some(&b'#') {
2223                hashes += 1;
2224                i += 1;
2225            }
2226            if b.get(i) != Some(&b'"') {
2227                return Some(i);
2228            }
2229            let mut scan = i + 1;
2230            while scan < b.len() {
2231                if b[scan] == b'"' {
2232                    let mut past = scan + 1;
2233                    let mut seen = 0usize;
2234                    while seen < hashes && b.get(past) == Some(&b'#') {
2235                        past += 1;
2236                        seen += 1;
2237                    }
2238                    if seen == hashes {
2239                        return Some(past);
2240                    }
2241                }
2242                scan += 1;
2243            }
2244            // A run of hashes with no matching close is not a raw string, so
2245            // the rest of the file must not be swallowed on the strength of it.
2246            Some(i)
2247        }
2248        // A multi-line string, which KDL spells `"""` *and a newline*, and ends
2249        // at the first `"""` that a line's own whitespace leads up to.
2250        b'"' if b[at..].starts_with(b"\"\"\"") && opens_a_line(&b[at + 3..]) => {
2251            let mut i = at + 3;
2252            while i + 3 <= b.len() {
2253                if b[i..].starts_with(b"\"\"\"") && closes_a_line(&b[at + 3..i]) {
2254                    return Some(i + 3);
2255                }
2256                i += 1;
2257            }
2258            Some(at + 1)
2259        }
2260        b'"' => {
2261            let mut i = at + 1;
2262            while i < b.len() && b[i] != b'"' {
2263                // An escaped character, including an escaped quote.
2264                i += if b[i] == b'\\' { 2 } else { 1 };
2265            }
2266            // `i` can overshoot on a trailing backslash, which is one more way
2267            // for a quote not to close.
2268            Some(if i < b.len() { i + 1 } else { at + 1 })
2269        }
2270        _ => None,
2271    }
2272}
2273
2274/// Whether what follows an opening `"""` is the rest of its line and then a
2275/// newline, which is what makes it a multi-line string rather than an error.
2276fn opens_a_line(rest: &[u8]) -> bool {
2277    rest.iter()
2278        .position(|byte| !matches!(byte, b' ' | b'\t' | b'\r'))
2279        .is_none_or(|at| rest[at] == b'\n')
2280}
2281
2282/// Whether a closing `"""` has only a line's own indentation in front of it,
2283/// which is what KDL requires of one. Anything may follow it.
2284fn closes_a_line(before: &[u8]) -> bool {
2285    before
2286        .iter()
2287        .rposition(|byte| !matches!(byte, b' ' | b'\t' | b'\r'))
2288        .is_some_and(|at| before[at] == b'\n')
2289}
2290
2291/// What a scan of the source can refuse without parsing it.
2292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2293enum Unparseable {
2294    /// A `{` that opens one level past [`MAX_DEPTH`], and its offset.
2295    TooDeep(usize),
2296    /// A brace with no partner, and its offset. `true` for a `{` that is never
2297    /// closed, `false` for a `}` that closes nothing.
2298    Unbalanced { at: usize, open: bool },
2299    /// The `{` of a commented-out block that opens one level past
2300    /// [`MAX_COMMENTED_DEPTH`], and its offset. Commented out either way round:
2301    /// `/-{` on the block, or a `/-` on the node the block belongs to.
2302    CommentedTooDeep(usize),
2303}
2304
2305impl Unparseable {
2306    /// The error to refuse the file with.
2307    fn error(self, source: &str) -> Error {
2308        match self {
2309            Self::TooDeep(at) => {
2310                Error::new(At::of(source, at), Reason::TooDeep { limit: MAX_DEPTH })
2311            }
2312            Self::Unbalanced { at, open } => {
2313                Error::new(At::of(source, at), Reason::Unbalanced { open })
2314            }
2315            Self::CommentedTooDeep(at) => Error::new(
2316                At::of(source, at),
2317                Reason::CommentedTooDeep {
2318                    limit: MAX_COMMENTED_DEPTH,
2319                },
2320            ),
2321        }
2322    }
2323}
2324
2325/// The brace that puts the file past a limit the parser must not be asked to
2326/// reach, if there is one.
2327///
2328/// A scanner rather than a parse, because it has to run *before* the parser.
2329/// [`not_structure`] decides what is a comment or a string, and everything left
2330/// over is structure — braces cannot appear in a bare identifier.
2331///
2332/// Two things are counted, and both are about what the parser costs rather than
2333/// about what a form means. Plain nesting past [`MAX_DEPTH`] overflows `kdl`'s
2334/// recursive descent; commented-out blocks nested past [`MAX_COMMENTED_DEPTH`]
2335/// send it exponential. A third is not counted at all but falls out of the
2336/// same walk: braces that do not balance, which no parser can make sense of.
2337/// None of the three is a `Result` the parser could hand back — one is a stack
2338/// overflow, one never returns, and the third would cost a slow failure — so
2339/// all three are refused out here, where a byte scan is all it costs.
2340fn unparseable(source: &str) -> Option<Unparseable> {
2341    let b = source.as_bytes();
2342    let mut i = 0;
2343    let mut depth = 0usize;
2344    // Whether each open block was commented out, so that closing one puts the
2345    // count back, and how many of them are open right now.
2346    let mut blocks: Vec<bool> = Vec::new();
2347    // Where each open block's `{` is, for an error that points at the brace
2348    // rather than at the end of the file.
2349    let mut opened: Vec<usize> = Vec::new();
2350    let mut commented = 0usize;
2351    // Set by a `/-` and cleared at the end of the node it commented out, so a
2352    // `{` reached while it is set is that node's own children block.
2353    let mut next_commented = false;
2354
2355    while i < b.len() {
2356        let before = i;
2357        if let Some(past) = not_structure(b, i) {
2358            i = past;
2359        } else {
2360            match b[i] {
2361                // A slashdash. It comments out the node that follows it, and
2362                // when that node carries a children block with anything in it,
2363                // `kdl` pays for the block twice over at every level of nesting.
2364                b'/' if b.get(i + 1) == Some(&b'-') => {
2365                    next_commented = true;
2366                    i += 2;
2367                }
2368                b'{' => {
2369                    depth += 1;
2370                    if depth > MAX_DEPTH {
2371                        return Some(Unparseable::TooDeep(i));
2372                    }
2373                    blocks.push(next_commented);
2374                    opened.push(i);
2375                    if next_commented {
2376                        commented += 1;
2377                        if commented > MAX_COMMENTED_DEPTH {
2378                            return Some(Unparseable::CommentedTooDeep(i));
2379                        }
2380                    }
2381                    next_commented = false;
2382                    i += 1;
2383                }
2384                b'}' => {
2385                    // A `}` with nothing open cannot be parsed by anything, and
2386                    // saying so here costs one byte of lookback.
2387                    let Some(was_commented) = blocks.pop() else {
2388                        return Some(Unparseable::Unbalanced { at: i, open: false });
2389                    };
2390                    opened.pop();
2391                    depth -= 1;
2392                    if was_commented {
2393                        commented -= 1;
2394                    }
2395                    i += 1;
2396                }
2397                _ => i += 1,
2398            }
2399        }
2400        // A node ends at a newline or a `;`, and so does the reach of a
2401        // slashdash waiting for a block. Reading it off the bytes just skipped
2402        // covers the newline in the open and the one inside a block comment or
2403        // a multi-line string alike.
2404        //
2405        // `min` because an unterminated string leaves `i` past the end — the
2406        // arms above step over the closing quote whether or not it was there,
2407        // which the loop condition forgives and a slice would not.
2408        let skipped = &b[before..i.min(b.len())];
2409        if next_commented && skipped.iter().any(|byte| *byte == b'\n' || *byte == b';') {
2410            next_commented = false;
2411        }
2412    }
2413    // A `{` still open at the end of the file is the other half of the same
2414    // thing. `opened` holds where each one started, so the error points at the
2415    // brace that was never closed rather than at the end of the file.
2416    opened.last().map(|at| Unparseable::Unbalanced {
2417        at: *at,
2418        open: true,
2419    })
2420}
2421
2422/// Parses form-file text that must be exactly one node.
2423fn one_node(text: &str) -> Result<KdlNode, Error> {
2424    let doc: KdlDocument = text.parse().map_err(|error: kdl::KdlError| {
2425        let message = error
2426            .diagnostics
2427            .first()
2428            .and_then(|d| d.message.clone())
2429            .unwrap_or_else(|| String::from("this is not a node"));
2430        Error::new(At::START, Reason::Syntax(message))
2431    })?;
2432    match doc.nodes() {
2433        [only] => Ok(only.clone()),
2434        other => Err(Error::new(
2435            At::START,
2436            Reason::Syntax(format!("this must be one node; it is {}", other.len())),
2437        )),
2438    }
2439}
2440
2441/// The top-level nodes of a fragment of form source, each as its own text.
2442///
2443/// A *fragment* is what the nodes of a form look like without the `form` node
2444/// around them: what copying a selection produces, and what somebody who typed
2445/// a widget into a text editor is likely to have. Each node comes back with its
2446/// own indentation taken off, ready for [`Edit::Insert`] to lay it out wherever
2447/// it is going.
2448///
2449/// Every `name=` in it that `taken` already holds is given a number until it
2450/// does not collide, and every name it settles on is added to `taken`. So
2451/// pasting the same fragment twice gives two sets of names rather than two
2452/// clashes, and the caller passes in the names the document already uses.
2453///
2454/// This answers whether the text is *nodes*. Whether they are widgets this
2455/// engine knows, with properties it has, is what [`Form::build`] answers — the
2456/// schema lives with the builder and there is no second copy of it here, so a
2457/// caller that cares builds the fragment before pasting it.
2458///
2459/// ```
2460/// # use denise_forms::fragment;
2461/// let mut taken = vec![String::from("card")];
2462/// let nodes = fragment("panel name=card x=0 y=0 w=10 h=10", &mut taken)?;
2463/// assert_eq!(nodes, vec![String::from("panel name=card2 x=0 y=0 w=10 h=10")]);
2464/// assert_eq!(taken, vec![String::from("card"), String::from("card2")]);
2465/// # Ok::<(), denise_forms::Error>(())
2466/// ```
2467pub fn fragment(text: &str, taken: &mut Vec<String>) -> Result<Vec<String>, Error> {
2468    // The same two guards a whole file gets, and for the same reason: this text
2469    // came from the system clipboard, which is to say from anywhere.
2470    if text.len() > MAX_SOURCE {
2471        return Err(Error::new(
2472            At::START,
2473            Reason::TooLarge { limit: MAX_SOURCE },
2474        ));
2475    }
2476    if let Some(refusal) = unparseable(text) {
2477        return Err(refusal.error(text));
2478    }
2479
2480    let mut doc: KdlDocument = text.parse().map_err(|error: kdl::KdlError| {
2481        let first = error.diagnostics.first();
2482        let at = first.map_or(At::START, |d| At::of(text, d.span.offset()));
2483        let message = first
2484            .and_then(|d| d.message.clone())
2485            .unwrap_or_else(|| String::from("this is not form source"));
2486        Error::new(at, Reason::Syntax(message))
2487    })?;
2488
2489    for node in doc.nodes_mut() {
2490        rename_apart(node, taken)?;
2491    }
2492    Ok(doc
2493        .nodes()
2494        .iter()
2495        .map(|node| reindent(&node.to_string(), &indent_of(node), "", false))
2496        .collect())
2497}
2498
2499/// Gives every `name=` in a subtree one that `taken` does not already hold.
2500fn rename_apart(node: &mut KdlNode, taken: &mut Vec<String>) -> Result<(), Error> {
2501    if let Some(entry) = node.entry("name") {
2502        let was = spell(entry.value());
2503        let now = unused(&was, taken);
2504        if now != was {
2505            set_literal(node, "name", &Literal::Name(now.clone()))?;
2506        }
2507        taken.push(now);
2508    }
2509    if let Some(block) = node.children_mut() {
2510        for child in block.nodes_mut() {
2511            rename_apart(child, taken)?;
2512        }
2513    }
2514    Ok(())
2515}
2516
2517/// `name` if nobody has it, and `name2`, `name3`… until somebody does not.
2518///
2519/// A name that already ends in digits carries on from its stem, so pasting
2520/// `nav2` gives `nav3` rather than `nav22`.
2521fn unused(name: &str, taken: &[String]) -> String {
2522    if !taken.iter().any(|held| held == name) {
2523        return String::from(name);
2524    }
2525    let stem = name.trim_end_matches(|c: char| c.is_ascii_digit());
2526    let stem = if stem.is_empty() { name } else { stem };
2527    (2usize..)
2528        .map(|number| format!("{stem}{number}"))
2529        .find(|candidate| !taken.iter().any(|held| held == candidate))
2530        .unwrap_or_else(|| String::from(name))
2531}
2532
2533/// A path as it stands once `removed` has been taken out.
2534///
2535/// `None` when the path was inside what was removed. The interesting case is the
2536/// quiet one: taking node `[1]` out moves `[3]` to `[2]`, so anything holding a
2537/// path across a removal has to be told — a move that names a destination
2538/// *after* its source, and an editor holding a selection.
2539///
2540/// ```
2541/// # use denise_forms::after_removing;
2542/// // The node after the one taken out slides up.
2543/// assert_eq!(after_removing(&[3], &[1]), Some(vec![2]));
2544/// // One before it does not, and neither does one in another parent.
2545/// assert_eq!(after_removing(&[0], &[1]), Some(vec![0]));
2546/// assert_eq!(after_removing(&[5, 3], &[1]), Some(vec![4, 3]));
2547/// // And a path inside what was removed is nowhere at all.
2548/// assert_eq!(after_removing(&[1, 0], &[1]), None);
2549/// ```
2550pub fn after_removing(path: &[usize], removed: &[usize]) -> Option<Vec<usize>> {
2551    if path.starts_with(removed) {
2552        return None;
2553    }
2554    let (index, ancestors) = removed.split_last()?;
2555    let mut out = path.to_vec();
2556    if out.len() > ancestors.len() && out.starts_with(ancestors) && out[ancestors.len()] > *index {
2557        out[ancestors.len()] -= 1;
2558    }
2559    Some(out)
2560}
2561
2562/// A node's text, moved from one depth to another.
2563///
2564/// Every line that begins with the old indentation gets the new one instead, so
2565/// the node's children move with it and keep their shape relative to it. `first`
2566/// says whether it is becoming the first node in its block, which is the one
2567/// position that also carries the newline after the brace.
2568fn reindent(text: &str, old: &str, new: &str, first: bool) -> String {
2569    let mut out = String::with_capacity(text.len() + 8);
2570    for (index, line) in text
2571        .trim_start_matches('\n')
2572        .split_inclusive('\n')
2573        .enumerate()
2574    {
2575        if old.is_empty() {
2576            // Nothing to swap: indent what is there rather than every blank line.
2577            if !line.trim().is_empty() {
2578                out.push_str(new);
2579            }
2580            out.push_str(line);
2581            continue;
2582        }
2583        match line.strip_prefix(old) {
2584            Some(rest) => {
2585                out.push_str(new);
2586                out.push_str(rest);
2587            }
2588            // A line shallower than the node itself can only be part of its
2589            // leading trivia, and a blank line has nothing to indent.
2590            None if index == 0 => {
2591                out.push_str(new);
2592                out.push_str(line.trim_start());
2593            }
2594            None => out.push_str(line),
2595        }
2596    }
2597    // Where it is going decides this, not where it came from: only the first
2598    // node in a block carries the newline that follows the brace, and a node
2599    // that was first and is landing third would otherwise leave a blank line
2600    // behind it.
2601    if first {
2602        out.insert(0, '\n');
2603    }
2604    out
2605}
2606
2607/// The whitespace a node is written after, on its own line.
2608///
2609/// What an inserted child is indented one step past. Read from the file rather
2610/// than counted from the depth, so a form written with two spaces stays written
2611/// with two spaces.
2612/// Walks a node and everything under it into [`Written`]s, depth first.
2613fn gather(node: &KdlNode, path: &mut Vec<usize>, out: &mut Vec<Written>) {
2614    let mut line = String::from(node.name().value());
2615    for entry in node.entries() {
2616        line.push(' ');
2617        line.push_str(&shown(entry));
2618    }
2619    out.push(Written {
2620        path: path.clone(),
2621        kind: node.name().value().to_string(),
2622        name: node.get("name").map(spell),
2623        argument: node
2624            .entries()
2625            .iter()
2626            .find(|entry| entry.name().is_none())
2627            .map(|entry| spell(entry.value())),
2628        line,
2629    });
2630    let Some(children) = node.children() else {
2631        return;
2632    };
2633    for (index, child) in children.nodes().iter().enumerate() {
2634        path.push(index);
2635        gather(child, path, out);
2636        path.pop();
2637    }
2638}
2639
2640/// One entry as a form file would write it, with none of the file's own spacing.
2641fn shown(entry: &KdlEntry) -> String {
2642    let value = match entry.value().as_string() {
2643        Some(text) => quoted(text),
2644        None => entry.value().to_string(),
2645    };
2646    match entry.name() {
2647        Some(name) => format!("{}={value}", name.value()),
2648        None => value,
2649    }
2650}
2651
2652fn indent_of(node: &KdlNode) -> String {
2653    let leading = node.format().map_or("", |format| format.leading.as_str());
2654    let line = leading.rsplit('\n').next().unwrap_or("");
2655    line.chars().filter(|c| c.is_whitespace()).collect()
2656}
2657
2658/// Parses form-file text that must be exactly one value.
2659///
2660/// What checks a [`Literal::Verbatim`] before it is written: the text goes into
2661/// the file as it stands, so it has to be one value and not, say, `1 x=2`.
2662fn one_value(text: &str) -> Result<KdlValue, Error> {
2663    let entry = KdlEntry::parse(text).map_err(|error: kdl::KdlError| {
2664        let message = error
2665            .diagnostics
2666            .first()
2667            .and_then(|d| d.message.clone())
2668            .unwrap_or_else(|| String::from("this is not a value"));
2669        Error::new(At::START, Reason::Syntax(message))
2670    })?;
2671    if entry.name().is_some() {
2672        return Err(Error::new(
2673            At::START,
2674            Reason::Syntax(String::from("this must be a value, not a property")),
2675        ));
2676    }
2677    Ok(entry.value().clone())
2678}
2679
2680/// The text a property's value was written with.
2681///
2682/// An entry that came from a parse remembers it. One built in memory does not,
2683/// and renders canonically — which is then what it was written with, since
2684/// nobody has written it yet.
2685fn repr_of(entry: &KdlEntry) -> String {
2686    entry.format().map_or_else(
2687        || entry.value().to_string(),
2688        |format| format.value_repr.clone(),
2689    )
2690}
2691
2692/// A value as an inspector's field should show it.
2693///
2694/// A string is its own text with nothing around it; everything else is what the
2695/// file would write.
2696fn spell(value: &KdlValue) -> String {
2697    match value.as_string() {
2698        Some(text) => text.to_string(),
2699        None => value.to_string(),
2700    }
2701}
2702
2703/// A string as a form file would quote it.
2704///
2705/// [`KdlValue`]'s own rendering writes a plain identifier bare, which is right
2706/// for [`Literal::Name`] and wrong for [`Literal::Text`]: a label whose text
2707/// happens to be one word is still a string, and `text=Save` in a file whose
2708/// every other string is quoted reads as a mistake.
2709fn quoted(text: &str) -> String {
2710    let mut out = String::with_capacity(text.len() + 2);
2711    out.push('"');
2712    for character in text.chars() {
2713        match character {
2714            '\\' | '"' => {
2715                out.push('\\');
2716                out.push(character);
2717            }
2718            '\n' => out.push_str("\\n"),
2719            '\r' => out.push_str("\\r"),
2720            '\t' => out.push_str("\\t"),
2721            '\u{08}' => out.push_str("\\b"),
2722            '\u{0C}' => out.push_str("\\f"),
2723            other => out.push(other),
2724        }
2725    }
2726    out.push('"');
2727    out
2728}
2729
2730/// Sets a property, keeping everything about the line but the value.
2731///
2732/// Two things here are not what the obvious code would do, and both were found by
2733/// the test that asserts an edit undone is byte-for-byte what it was.
2734///
2735/// `KdlNode::insert` on a property that is already there **replaces the entry**,
2736/// and the replacement carries default spacing — so a line whose properties were
2737/// deliberately lined up in columns loses that the first time anything is
2738/// dragged. Reaching for the existing entry keeps its leading whitespace.
2739///
2740/// And `KdlEntry::set_value` alone is a **silent no-op** for anything that came
2741/// from a parse: the entry keeps the text it was written with, and renders that
2742/// rather than the value it now holds. The cached representation has to be set
2743/// too, which is why this is a function and not a line.
2744fn set_literal(node: &mut KdlNode, name: &str, literal: &Literal) -> Result<(), Error> {
2745    let (value, repr) = literal.parts()?;
2746    if let Some(entry) = node.entry_mut(name) {
2747        entry.set_value(value);
2748        match entry.format_mut() {
2749            Some(format) => format.value_repr = repr,
2750            // Built in memory rather than parsed, so there is no spacing to
2751            // keep and the whole format is this crate's to write.
2752            None => entry.set_format(KdlEntryFormat {
2753                value_repr: repr,
2754                leading: String::from(" "),
2755                ..KdlEntryFormat::default()
2756            }),
2757        }
2758        return Ok(());
2759    }
2760    // Nothing there to keep the shape of; appending is right.
2761    let mut entry = KdlEntry::new_prop(name, value);
2762    entry.set_format(KdlEntryFormat {
2763        value_repr: repr,
2764        leading: String::from(" "),
2765        ..KdlEntryFormat::default()
2766    });
2767    node.push(entry);
2768    Ok(())
2769}
2770
2771/// Puts back the bytes kdl eats after a closing brace.
2772///
2773/// `}  \n` parses and serialises back as `}` — the spaces *and* the newline
2774/// both gone, so the next node lands on the brace's line; `}  // a note` and
2775/// `} /* a note */` lose the comment the same way. All found by the fuzz
2776/// target `parse_form` within its first hour. A plain node's terminator keeps
2777/// its trivia, but whatever stands between a `}` and the next node is consumed
2778/// into a terminator that is then stored empty.
2779///
2780/// The bytes are recoverable because kdl still records where every node
2781/// *starts*, and its leading trivia with it. So the rule is a simple one, and
2782/// it is the same rule for a loss and for a file with nothing wrong: a node's
2783/// terminator is every byte between the end of what it renders as and the
2784/// beginning of what the next node owns. Applying it to a file kdl kept
2785/// intact reproduces the terminator kdl already stored.
2786///
2787/// Children first, because a repaired child grows its parent's rendering and
2788/// the end of that rendering is what the arithmetic measures from.
2789///
2790/// `Form::parse` verifies the whole document against the source afterwards, so
2791/// a shape this gets wrong is refused there rather than saved back corrupted.
2792fn restore_after_close(doc: &mut KdlDocument, source: &str) {
2793    for node in doc.nodes_mut() {
2794        restore_subtree(node, source);
2795    }
2796    // The document's own trailing trivia is kept as it is; the last node runs
2797    // up to where that begins.
2798    let trailing = doc.format().map_or(0, |format| format.trailing.len());
2799    let Some(limit) = source.len().checked_sub(trailing) else {
2800        return;
2801    };
2802    terminate_block(doc, limit, source);
2803}
2804
2805/// Repairs the children of `node`, and theirs, but not `node`'s own
2806/// terminator — that belongs to whoever owns the block `node` sits in.
2807///
2808/// See [`restore_after_close`].
2809fn restore_subtree(node: &mut KdlNode, source: &str) {
2810    let Some(block) = node.children_mut() else {
2811        return;
2812    };
2813    if block.nodes().is_empty() {
2814        // Nothing after `{` to bound, and an empty block keeps its own bytes;
2815        // anything lost after the `}` is this node's terminator, one level up.
2816        return;
2817    }
2818    for child in block.nodes_mut() {
2819        restore_subtree(child, source);
2820    }
2821    // Only now does the last child render in full, so only now is the end of
2822    // it the true byte offset that the walk to the closing brace starts from.
2823    let trailing = block
2824        .format()
2825        .map_or_else(String::new, |format| format.trailing.clone());
2826    let last = block.nodes().last().expect("the block is not empty");
2827    let Some(limit) = end_of_nodes(source, content_end(last), &trailing) else {
2828        return;
2829    };
2830    terminate_block(block, limit, source);
2831}
2832
2833/// Gives every node in one block the bytes that stand between it and the next.
2834fn terminate_block(block: &mut KdlDocument, limit: usize, source: &str) {
2835    let bounds: Vec<usize> = (0..block.nodes().len())
2836        .map(|i| block.nodes().get(i + 1).map_or(limit, owned_start))
2837        .collect();
2838    for (node, bound) in block.nodes_mut().iter_mut().zip(bounds) {
2839        let from = content_end(node);
2840        // A bound below the node's end, or one that lands inside a character,
2841        // means the arithmetic missed; leave the node alone and let the verify
2842        // in `Form::parse` refuse the file.
2843        let Some(terminator) = source.get(from..bound) else {
2844            continue;
2845        };
2846        let format = node.format().cloned().unwrap_or_default();
2847        if terminator != format.terminator {
2848            node.set_format(KdlNodeFormat {
2849                terminator: terminator.to_string(),
2850                ..format
2851            });
2852        }
2853    }
2854}
2855
2856/// The first byte of a node — its leading trivia, not its name.
2857fn owned_start(node: &KdlNode) -> usize {
2858    let leading = node.format().map_or(0, |format| format.leading.len());
2859    node.span().offset().saturating_sub(leading)
2860}
2861
2862/// The byte just past what a node renders as, which for a node with children
2863/// is the byte just past its `}`.
2864fn content_end(node: &KdlNode) -> usize {
2865    let format = node.format().cloned().unwrap_or_default();
2866    let rendered = node.to_string().len();
2867    let inner = rendered.saturating_sub(format.leading.len() + format.terminator.len());
2868    node.span().offset() + inner
2869}
2870
2871/// Where a block's nodes stop and the block's own trailing trivia begins.
2872///
2873/// Walking from `from` — the end of the block's last node — to the closing
2874/// brace crosses only trivia, and the block keeps the tail of it in `trailing`
2875/// already. So the answer is the first point at which `trailing` and then `}`
2876/// stand next in the source. Testing that before each step of the walk rather
2877/// than after it is what lets `trailing` start with whitespace of its own.
2878///
2879/// `None` when the walk meets something it does not recognise, which leaves
2880/// the block untouched and the file refused if kdl did lose bytes there.
2881fn end_of_nodes(source: &str, from: usize, trailing: &str) -> Option<usize> {
2882    let mut at = from;
2883    loop {
2884        let rest = source.get(at..)?;
2885        if rest
2886            .strip_prefix(trailing)
2887            .is_some_and(|past| past.starts_with('}'))
2888        {
2889            return Some(at);
2890        }
2891        at += trivia_width(rest)?;
2892    }
2893}
2894
2895/// The length of the one piece of trivia at the front of `rest`, or `None` if
2896/// what stands there is not trivia. A slashdash never reaches this: kdl keeps
2897/// commented-out nodes in the block's `trailing`, so the walk stops at the `/`
2898/// and the match against `trailing` has already succeeded there.
2899fn trivia_width(rest: &str) -> Option<usize> {
2900    let first = rest.chars().next()?;
2901    if first.is_whitespace() {
2902        return Some(first.len_utf8());
2903    }
2904    if let Some(body) = rest.strip_prefix("//") {
2905        return Some(2 + body.find('\n').map_or(body.len(), |end| end + 1));
2906    }
2907    if !rest.starts_with("/*") {
2908        return None;
2909    }
2910    // KDL's block comments nest, so this counts rather than searching for the
2911    // first `*/`. Every delimiter is ASCII, so the returned width lands on a
2912    // character boundary however the comment is spelled.
2913    let bytes = rest.as_bytes();
2914    let mut depth = 0usize;
2915    let mut at = 0usize;
2916    while at + 1 < bytes.len() {
2917        match &bytes[at..at + 2] {
2918            b"/*" => {
2919                depth += 1;
2920                at += 2;
2921            }
2922            b"*/" => {
2923                depth -= 1;
2924                at += 2;
2925                if depth == 0 {
2926                    return Some(at);
2927                }
2928            }
2929            _ => at += 1,
2930        }
2931    }
2932    None
2933}
2934
2935#[cfg(test)]
2936mod tests {
2937    use super::*;
2938
2939    /// Parses as kdl does, repairs, and hands back what would be saved.
2940    fn reproduced(source: &str) -> String {
2941        let mut doc: KdlDocument = source.parse().expect("the shape under test parses");
2942        restore_after_close(&mut doc, source);
2943        doc.to_string()
2944    }
2945
2946    #[test]
2947    fn a_brace_keeps_what_follows_it_to_the_end_of_the_line() {
2948        // Every one of these loses bytes in kdl itself. The first was found by
2949        // hand, the rest by the fuzz target `parse_form`.
2950        for source in [
2951            // Whitespace after a closing brace, then a sibling.
2952            "a {\n  b 1\n}  \nc 3\n",
2953            // A line comment in the same place.
2954            "a {\n  b 1\n}  // x\nc 3\n",
2955            // And a block comment, which may span lines of its own.
2956            "a {\n  b 1\n} /* p\nq */\nc 3\n",
2957            // The brace ends the file, with and without a final newline.
2958            "a {\n  b 1\n}  // x\n",
2959            "a {\n  b 1\n}  // x",
2960            // A blank line after the comment must survive as a blank line.
2961            "a {\n  b 1\n}  // x\n\nc 3\n",
2962            // The lossy brace is the last node of a block, so the walk to the
2963            // outer `}` is what finds the bound.
2964            "o {\n  a {\n    b 1\n  } // x\n}\n",
2965            "o {\n  a {\n    b 1\n  } /* p\nq */\n}\n",
2966            // The block's own trailing trivia stands between the two, both as
2967            // indentation and as a commented-out node.
2968            "a {\n  b {\n    c 1\n  }  // x\n}\n",
2969            "a {\n  b {\n    c 1\n  } /-d 2\n}\n",
2970            // A brace inside the comment is not the brace being looked for.
2971            "a {\n  b 1\n} // closes }\nc 3\n",
2972            // Nested block comments, which KDL counts rather than terminating
2973            // at the first `*/`.
2974            "a {\n  b 1\n} /* p /* q */ r */\nc 3\n",
2975            // An empty block, whose loss is the node's own terminator.
2976            "a {}  // x\nc 3\n",
2977        ] {
2978            assert_eq!(reproduced(source), source, "in {source:?}");
2979        }
2980    }
2981
2982    /// A shape the repair cannot reach, and the refusal that covers it.
2983    ///
2984    /// `kdl` records `before_ty_name`, `after_ty_name` and `after_ty` when it
2985    /// reads a node's type annotation and then writes none of them, so `(Z) h`
2986    /// comes back as `(Z)h`. Nothing this crate can set to a `KdlNodeFormat`
2987    /// changes that, which is what `Reason::NotPreserved` is *for*: the file is
2988    /// refused rather than accepted and corrupted on the first save. Found by
2989    /// the fuzz target `parse_form` in six bytes.
2990    ///
2991    /// If kdl ever writes those fields, this test fails and the refusal can go.
2992    /// The promise `tidy` makes: it moves lines, it does not edit them.
2993    ///
2994    /// Checked as a property rather than by example, because the whole point of
2995    /// the tool is that somebody can run it over a file they annotated without
2996    /// reading the diff. Every line of the output must be a line of the input
2997    /// with its ends trimmed, in the same order.
2998    #[test]
2999    fn tidying_changes_only_the_whitespace_at_the_ends_of_a_line() {
3000        for name in corpus() {
3001            let source = std::fs::read_to_string(&name).expect("readable");
3002            let tidied = tidy(&source).unwrap_or_else(|e| panic!("{name}: {e}"));
3003            let before: Vec<&str> = source.lines().map(str::trim).collect();
3004            let after: Vec<&str> = tidied.lines().map(str::trim).collect();
3005            assert_eq!(before, after, "in {name}");
3006        }
3007    }
3008
3009    /// And having laid a file out, laying it out again does nothing.
3010    #[test]
3011    fn tidying_a_tidy_file_is_a_no_op() {
3012        for name in corpus() {
3013            let source = std::fs::read_to_string(&name).expect("readable");
3014            let once = tidy(&source).expect("tidies");
3015            let twice = tidy(&once).expect("tidies again");
3016            assert_eq!(once, twice, "in {name}");
3017            // And what comes out is still the same form, byte-preserved by the
3018            // parser that has to read it back.
3019            let form = Form::parse(&once).unwrap_or_else(|e| panic!("{name}: {e}"));
3020            assert_eq!(form.text(), once, "in {name}");
3021        }
3022    }
3023
3024    /// Every `.dform` in the repository, laid out or awkward.
3025    fn corpus() -> Vec<String> {
3026        let root = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
3027        let mut found = Vec::new();
3028        for dir in [
3029            format!("{root}/forms"),
3030            format!("{root}/denise-forms/tests/awkward"),
3031        ] {
3032            let Ok(entries) = std::fs::read_dir(&dir) else {
3033                continue;
3034            };
3035            for entry in entries.flatten() {
3036                let path = entry.path();
3037                if path.extension().is_some_and(|e| e == "dform") {
3038                    found.push(path.to_string_lossy().into_owned());
3039                }
3040            }
3041        }
3042        assert!(found.len() > 6, "the corpus went missing: {found:?}");
3043        found.sort();
3044        found
3045    }
3046
3047    #[test]
3048    fn tidying_keeps_what_a_person_put_there_and_fixes_the_indent() {
3049        // A comment on the end of a line -- the thing kdl's own formatter
3050        // deletes, and the reason this exists at all.
3051        // The first node inside `form` sets the step -- four spaces here --
3052        // and everything after it has drifted.
3053        let ragged = "\
3054// a note about the form
3055form \"F\" version=1 kind=screen width=20 height=20 {
3056    label \"hi\"   x=0 y=0 w=5 h=5  // a note about the label
3057
3058  panel name=p x=0 y=6 w=5 h=5 {
3059label \"in\" x=0 y=0 w=5 h=5
3060        }
3061}
3062";
3063        let out = tidy(ragged).expect("tidies");
3064        assert!(out.contains("// a note about the form"), "{out}");
3065        assert!(out.contains("// a note about the label"), "{out}");
3066        // Quoting survives, which autoformat also takes away.
3067        assert!(out.contains("label \"hi\""), "{out}");
3068        // Columns lined up inside the line are the author's business.
3069        assert!(out.contains("\"hi\"   x=0"), "{out}");
3070        // The blank line is a paragraph break and stays one.
3071        assert!(out.contains("h=5  // a note about the label\n\n"), "{out}");
3072        // And the indent is one step per level, closing braces included.
3073        assert!(out.contains("\n    label \"hi\""), "{out}");
3074        assert!(out.contains("\n        label \"in\""), "{out}");
3075        assert!(out.contains("\n    }\n}\n"), "{out}");
3076    }
3077
3078    #[test]
3079    fn tidying_leaves_the_inside_of_a_multi_line_string_alone() {
3080        // The lines of a multi-line string are its value, not layout.
3081        let source = "\
3082form \"F\" version=1 kind=screen width=20 height=20 {
3083    label \"one\" x=0 y=0 w=5 h=5
3084        label \"\"\"
3085  indented on purpose
3086      and so is this
3087\"\"\" x=0 y=6 w=5 h=5
3088}
3089";
3090        let out = tidy(source).expect("tidies");
3091        assert!(out.contains("\n  indented on purpose\n"), "{out}");
3092        assert!(out.contains("\n      and so is this\n"), "{out}");
3093        // The line that opens it is still laid out.
3094        assert!(out.contains("\n    label \"\"\"\n"), "{out}");
3095    }
3096
3097    #[test]
3098    fn tidying_uses_the_indent_the_file_already_uses() {
3099        let two = "\
3100form \"F\" version=1 kind=screen width=20 height=20 {
3101  panel name=p x=0 y=0 w=5 h=5 {
3102          label \"in\" x=0 y=0 w=5 h=5
3103  }
3104}
3105";
3106        let out = tidy(two).expect("tidies");
3107        assert!(out.contains("\n  panel"), "{out}");
3108        assert!(out.contains("\n    label"), "{out}");
3109    }
3110
3111    #[test]
3112    fn a_file_that_does_not_parse_is_not_rewritten() {
3113        // Including the shapes the guards refuse: a formatter must not be the
3114        // way a file that cannot be read gets edited anyway.
3115        assert!(tidy("form \"F\" version=1 {").is_err());
3116        assert!(tidy("not a form at all").is_err());
3117        assert!(tidy(&("a /-{ ".repeat(8) + &"}".repeat(8))).is_err());
3118    }
3119
3120    #[test]
3121    fn a_type_annotation_kdl_cannot_write_back_is_refused_rather_than_mangled() {
3122        for source in ["(Z) h", "( Z )h", "(Z) h\n", "(Z)h { (Y) i }\n"] {
3123            let doc: KdlDocument = source.parse().expect("kdl reads it");
3124            let mut repaired = doc;
3125            restore_after_close(&mut repaired, source);
3126            assert_ne!(
3127                repaired.to_string(),
3128                source,
3129                "kdl now keeps {source:?} -- the refusal below can go"
3130            );
3131        }
3132        // And the door this crate actually puts in front of that.
3133        let error = Form::parse("(Z) h").expect_err("cannot be reproduced");
3134        assert!(matches!(error.reason, Reason::NotPreserved), "{error}");
3135        // The type annotation itself is fine when nothing is lost around it.
3136        let kept = "(Z)h\n";
3137        let doc: KdlDocument = kept.parse().expect("kdl reads it");
3138        assert_eq!(doc.to_string(), kept);
3139    }
3140
3141    #[test]
3142    fn a_file_kdl_keeps_intact_is_left_exactly_as_it_was() {
3143        // The repair runs over every file, so the shapes with nothing wrong
3144        // matter as much as the shapes with something wrong.
3145        for source in [
3146            "a {\n  b 1\n}\nc 3\n",
3147            "a 1; b 2\n",
3148            "a { b 1 }; c 2\n",
3149            "a \\\n  1\nc 3\n",
3150            "a {\n  b 1\n  /-c 2\n}\n",
3151            "a {\n}\nc 3\n",
3152            "// a leading comment\na 1\n",
3153            "a 1\n// and a trailing one\n",
3154            "\n\na 1\n\n\nb 2\n",
3155            "a \"a string with } and // in it\"\n",
3156        ] {
3157            assert_eq!(reproduced(source), source, "in {source:?}");
3158        }
3159    }
3160
3161    #[test]
3162    fn nesting_within_the_limit_is_allowed() {
3163        let source = "a ".to_string() + &"{ b ".repeat(MAX_DEPTH) + &"}".repeat(MAX_DEPTH);
3164        assert_eq!(unparseable(&source), None);
3165    }
3166
3167    #[test]
3168    fn one_level_past_the_limit_is_caught_before_the_parser_sees_it() {
3169        let deep = MAX_DEPTH + 1;
3170        let source = "a ".to_string() + &"{ b ".repeat(deep) + &"}".repeat(deep);
3171        assert!(matches!(
3172            unparseable(&source),
3173            Some(Unparseable::TooDeep(_))
3174        ));
3175    }
3176
3177    #[test]
3178    fn commented_out_blocks_are_allowed_until_they_nest() {
3179        // One is a person taking a widget and its children out for a minute.
3180        for levels in 0..=MAX_COMMENTED_DEPTH {
3181            let source = "a /-{ ".repeat(levels) + &"}".repeat(levels);
3182            assert_eq!(unparseable(&source), None, "at {levels} levels");
3183        }
3184        // Side by side is not nesting, however many there are: the cost is the
3185        // nesting, so the limit is on the nesting.
3186        let side_by_side = "a /-{ }\n".repeat(32);
3187        assert_eq!(unparseable(&side_by_side), None);
3188        // A slashdash on a node that carries no block is not counted at all,
3189        // which is the shape a person actually writes -- one widget taken out.
3190        let plain = "/- label \"x\" y=1\n".repeat(32);
3191        assert_eq!(unparseable(&plain), None);
3192        // Nor is a slashdash whose node ends before any block begins: the `{`
3193        // on the next line belongs to the node after it.
3194        let separated = "/- a\nb {\n}\n".repeat(16);
3195        assert_eq!(unparseable(&separated), None);
3196        assert_eq!(unparseable(&"/- a; b {\n}\n".repeat(16)), None);
3197        // And a `{` inside a string after a slashdash is not a block.
3198        let quoted = "/- a \"{{{{\"\n".repeat(16);
3199        assert_eq!(unparseable(&quoted), None);
3200    }
3201
3202    #[test]
3203    fn commented_out_blocks_nested_past_the_limit_never_reach_the_parser() {
3204        // Twenty of these is twenty seconds inside kdl, and the sixty-four
3205        // MAX_DEPTH would otherwise allow does not finish at all. The scan that
3206        // refuses them costs one pass over a hundred bytes.
3207        let deep = MAX_COMMENTED_DEPTH + 1;
3208        let source = "a /-{ ".repeat(deep) + &"}".repeat(deep);
3209        assert!(matches!(
3210            unparseable(&source),
3211            Some(Unparseable::CommentedTooDeep(_))
3212        ));
3213        // Unclosed, spaced out, and with the whole file around it, the same.
3214        assert!(matches!(
3215            unparseable(&"a /-  {\n".repeat(deep)),
3216            Some(Unparseable::CommentedTooDeep(_))
3217        ));
3218        // And the shape the fuzzer actually found: the slashdash is on the
3219        // node, the block is the node's own, and there is something in it.
3220        assert!(matches!(
3221            unparseable(&"/- a b c {\n  d 1\n".repeat(deep)),
3222            Some(Unparseable::CommentedTooDeep(_))
3223        ));
3224    }
3225
3226    #[test]
3227    fn a_triple_quote_is_only_a_string_when_it_opens_a_line() {
3228        // KDL spells a multi-line string `"""` and then a newline. kdl refuses
3229        // `""" x`, so a scan that read it as a string would skip whatever came
3230        // next -- which is how one fuzzed input hid a hundred and twenty braces.
3231        let hidden = String::from("a x=\"\"\" y\n") + &"b {\n".repeat(MAX_DEPTH + 1);
3232        assert!(
3233            matches!(unparseable(&hidden), Some(Unparseable::TooDeep(_))),
3234            "a `\"\"\"` that opens no line must hide nothing"
3235        );
3236        // Nor does a `\"\"\"` on the end of a line close one, so what follows
3237        // that line is structure and gets counted.
3238        let closed_wrong = String::from("a x=\"\"\"\nhi\"\"\"\n") + &"b {\n".repeat(MAX_DEPTH + 1);
3239        assert!(
3240            matches!(unparseable(&closed_wrong), Some(Unparseable::TooDeep(_))),
3241            "a `\"\"\"` that closes no line must hide nothing"
3242        );
3243        // The real thing still hides what is inside it, newline and all.
3244        let real = "a x=\"\"\"\n{{{{{{{{\n\"\"\"\nb 1\n";
3245        assert_eq!(unparseable(real), None);
3246        // Trailing spaces before the newline are still opening a line, and
3247        // indentation in front of the closer is still closing one.
3248        let padded = "a x=\"\"\"   \n{{{{{{{{\n   \"\"\"\nb 1\n";
3249        assert_eq!(unparseable(padded), None);
3250        // Something after the closing `\"\"\"` is allowed and changes nothing.
3251        let after = "a x=\"\"\"\n{{{{{{{{\n\"\"\" y=2\nb 1\n";
3252        assert_eq!(unparseable(after), None);
3253    }
3254
3255    #[test]
3256    fn a_brace_with_no_partner_is_refused_before_the_parser_looks_for_one() {
3257        // Neither of these can parse however long kdl spends deciding so, and
3258        // kdl can spend an unbounded amount of time on exactly this shape --
3259        // every slow input the fuzzer has found is wildly unbalanced.
3260        assert!(matches!(
3261            unparseable("a {\n  b 1\n"),
3262            Some(Unparseable::Unbalanced { open: true, .. })
3263        ));
3264        assert!(matches!(
3265            unparseable("a 1\n}\n"),
3266            Some(Unparseable::Unbalanced { open: false, .. })
3267        ));
3268        // The position is the brace itself, not the end of the file.
3269        let Some(Unparseable::Unbalanced { at, open: true }) = unparseable("a {\n  b {\n  }\n")
3270        else {
3271            panic!("the outer brace is never closed")
3272        };
3273        assert_eq!(at, 2, "the outer `{{`, not the inner one");
3274        // Balanced is balanced, however it is spelled.
3275        for source in [
3276            "a { }",
3277            "a {\n}\n",
3278            "a { b { c { } } }",
3279            "a 1",
3280            "",
3281            "// nothing\n",
3282        ] {
3283            assert_eq!(unparseable(source), None, "in {source:?}");
3284        }
3285    }
3286
3287    #[test]
3288    fn a_brace_inside_a_string_is_not_structure() {
3289        for source in [
3290            r#"a "{{{{{{{{{{{{{{{{" b"#,
3291            r##"a #"{{{{{{{{{{{{{{{{"# b"##,
3292            "a \"\"\"\n{{{{{{{{{{{{{{{{\n\"\"\" b",
3293            "a // {{{{{{{{{{{{{{{{{{{{{{{{{{{{\n b",
3294            "a /* {{{{{{{{{{{{{{{{{{{{{{{{{{ */ b",
3295        ] {
3296            assert_eq!(unparseable(source), None, "in {source}");
3297        }
3298    }
3299
3300    #[test]
3301    fn an_unterminated_string_does_not_loop_forever() {
3302        assert_eq!(unparseable("a \"unterminated"), None);
3303        assert_eq!(unparseable("a #\"unterminated"), None);
3304        assert_eq!(unparseable("a /* unterminated"), None);
3305        // Stepping over a closing quote that is not there leaves the scan past
3306        // the end of the source, which only a slashdash makes visible: it is
3307        // what asks for the bytes just read. Eighteen of them used to panic.
3308        assert_eq!(unparseable("/- a \"unterminated"), None);
3309        assert_eq!(unparseable("/- a #\"unterminated"), None);
3310        assert_eq!(unparseable("/- a /* unterminated"), None);
3311        assert_eq!(unparseable("/- \"x\\"), None);
3312        assert_eq!(unparseable("/- a \"\"\"unterminated"), None);
3313    }
3314
3315    #[test]
3316    fn a_quote_that_never_closes_hides_nothing_behind_it() {
3317        // Every one of these opens a string the file never closes, and every
3318        // one of them used to blind the scan to the whole rest of the file --
3319        // which is how a fuzzed input walked twenty-four slashdashes and
3320        // twenty-eight braces past both limits. kdl does not stop reading at an
3321        // unclosed quote either, so neither may this.
3322        for opener in ["\"", "###############\"", "\"\"\"", "#\""] {
3323            // (a bare `"""` closes nothing and opens no line, so it hides
3324            // nothing either way round)
3325            let hidden = format!("a {opener}\n") + &"b /-{ ".repeat(8);
3326            assert!(
3327                matches!(unparseable(&hidden), Some(Unparseable::CommentedTooDeep(_))),
3328                "behind {opener:?}"
3329            );
3330            let deep = format!("a {opener}\n") + &"{ b ".repeat(MAX_DEPTH + 1);
3331            assert!(
3332                matches!(unparseable(&deep), Some(Unparseable::TooDeep(_))),
3333                "behind {opener:?}"
3334            );
3335        }
3336        // A string that *does* close still hides what is inside it.
3337        let closed = String::from("a \"{ { { { \"\n") + &"b /-{ ".repeat(8);
3338        assert!(matches!(
3339            unparseable(&closed),
3340            Some(Unparseable::CommentedTooDeep(_))
3341        ));
3342        assert_eq!(unparseable("a \"{ { { { { { { { \" b"), None);
3343    }
3344}