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("ed), 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}