use denise::{Rect, Role, Size, Theme, theme};
use denise_ui::Side;
use kdl::{KdlDocument, KdlEntry, KdlEntryFormat, KdlNode, KdlNodeFormat, KdlValue};
use crate::error::{At, Error, Reason};
pub const VERSION: u64 = 1;
pub const MAX_DEPTH: usize = 64;
pub const MAX_SOURCE: usize = 1 << 22;
pub const MAX_COMMENTED_DEPTH: usize = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FormKind {
Screen,
Window,
Dialog,
Drawer,
Shelf,
Fragment,
}
impl FormKind {
pub const NAMES: &'static [&'static str] =
&["screen", "window", "dialog", "drawer", "shelf", "fragment"];
pub const fn what(self) -> &'static str {
match self {
Self::Screen => "A panel's whole surface: the root of a `Ui`.",
Self::Window => "A desktop window, with a title bar and a size somebody can drag.",
Self::Dialog => "A modal: a pushed scene on a panel, a modal window on a desktop.",
Self::Drawer => "A panel that slides in from an edge, over a dimmed screen.",
Self::Shelf => "A bar that slides in from an edge, with nothing dimmed behind it.",
Self::Fragment => "A subtree with no root of its own, for reuse inside other forms.",
}
}
pub const fn default_side(self) -> Side {
match self {
Self::Shelf => Side::Below,
_ => Side::Before,
}
}
fn from_name(name: &str) -> Option<Self> {
Some(match name {
"screen" => FormKind::Screen,
"window" => FormKind::Window,
"dialog" => FormKind::Dialog,
"drawer" => FormKind::Drawer,
"shelf" => FormKind::Shelf,
"fragment" => FormKind::Fragment,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Scaling {
#[default]
None,
Proportional,
Stretch,
}
impl Scaling {
pub const NAMES: &'static [&'static str] = &["none", "proportional", "stretch"];
pub const fn what(self) -> &'static str {
match self {
Self::None => "Never scaled: drawn at its design size, in the middle.",
Self::Proportional => "Scaled to fit by one factor, with a margin on the long axis.",
Self::Stretch => "Scaled per axis to fill the surface, distorting if it must.",
}
}
fn from_name(name: &str) -> Option<Self> {
Some(match name {
"none" => Self::None,
"proportional" => Self::Proportional,
"stretch" => Self::Stretch,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Placement {
pub x: f32,
pub y: f32,
pub rect: Rect,
}
impl Placement {
#[must_use]
pub fn uniform(self) -> f32 {
if self.x < self.y { self.x } else { self.y }
}
}
pub const THEMES: &[&str] = &["dark", "light", "high-contrast"];
#[derive(Clone, Debug, PartialEq)]
pub enum Literal {
Text(String),
Name(String),
Flag(bool),
Int(i64),
Float(f64),
Verbatim(String),
}
impl Literal {
pub fn text(text: impl Into<String>) -> Self {
Literal::Text(text.into())
}
pub fn name(name: impl Into<String>) -> Self {
Literal::Name(name.into())
}
fn parts(&self) -> Result<(KdlValue, String), Error> {
Ok(match self {
Literal::Text(text) => (KdlValue::String(text.clone()), quoted(text)),
Literal::Name(name) => {
let value = KdlValue::String(name.clone());
let repr = value.to_string();
(value, repr)
}
Literal::Flag(flag) => {
let value = KdlValue::Bool(*flag);
let repr = value.to_string();
(value, repr)
}
Literal::Int(number) => {
let value = KdlValue::Integer(i128::from(*number));
let repr = value.to_string();
(value, repr)
}
Literal::Float(number) => {
let value = KdlValue::Float(*number);
let repr = value.to_string();
(value, repr)
}
Literal::Verbatim(text) => (one_value(text)?, text.clone()),
})
}
fn class(&self) -> Result<Class, Error> {
Ok(match self {
Literal::Text(_) | Literal::Name(_) => Class::Text,
Literal::Flag(_) => Class::Flag,
Literal::Int(_) | Literal::Float(_) => Class::Number,
Literal::Verbatim(text) => Class::of(&one_value(text)?),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Class {
Text,
Number,
Flag,
Nothing,
}
impl Class {
fn of(value: &KdlValue) -> Self {
match value {
KdlValue::String(_) => Class::Text,
KdlValue::Integer(_) | KdlValue::Float(_) => Class::Number,
KdlValue::Bool(_) => Class::Flag,
KdlValue::Null => Class::Nothing,
}
}
const fn noun(self) -> &'static str {
match self {
Class::Text => "a string",
Class::Number => "a number",
Class::Flag => "true or false",
Class::Nothing => "nothing",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Edit {
Property {
path: Vec<usize>,
name: String,
value: Option<Literal>,
},
Insert {
parent: Vec<usize>,
index: usize,
text: String,
},
Argument {
path: Vec<usize>,
value: Literal,
},
Move {
from: Vec<usize>,
to: Vec<usize>,
index: usize,
},
Remove {
path: Vec<usize>,
},
Many(Vec<Edit>),
Replace {
path: Vec<usize>,
text: String,
},
}
impl Edit {
pub fn number(path: &[usize], name: &str, value: Option<i64>) -> Self {
Edit::property(path, name, value.map(Literal::Int))
}
pub fn property(path: &[usize], name: &str, value: Option<Literal>) -> Self {
Edit::Property {
path: path.to_vec(),
name: name.to_string(),
value,
}
}
pub fn argument(path: &[usize], text: impl Into<String>) -> Self {
Edit::Argument {
path: path.to_vec(),
value: Literal::Text(text.into()),
}
}
pub fn move_to(from: &[usize], to: &[usize], index: usize) -> Self {
Edit::Move {
from: from.to_vec(),
to: to.to_vec(),
index,
}
}
pub fn remove(path: &[usize]) -> Self {
Edit::Remove {
path: path.to_vec(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Written {
pub path: Vec<usize>,
pub kind: String,
pub name: Option<String>,
pub argument: Option<String>,
pub line: String,
}
#[derive(Clone, Debug)]
pub struct Form {
source: String,
doc: KdlDocument,
}
impl Form {
pub fn parse(source: &str) -> Result<Self, Error> {
if source.len() > MAX_SOURCE {
return Err(Error::new(
At::START,
Reason::TooLarge { limit: MAX_SOURCE },
));
}
if let Some(refusal) = unparseable(source) {
return Err(refusal.error(source));
}
let doc: KdlDocument = source.parse().map_err(|error: kdl::KdlError| {
let first = error.diagnostics.first();
let at = first.map_or(At::START, |d| At::of(source, d.span.offset()));
let message = first.map_or_else(
|| String::from("this is not a KDL document"),
|d| {
d.message
.clone()
.unwrap_or_else(|| d.help.clone().unwrap_or_default())
},
);
let message = if message.is_empty() {
String::from("this is not a KDL document")
} else {
message
};
Error::new(at, Reason::Syntax(message))
})?;
let mut doc = doc;
restore_after_close(&mut doc, source);
let reproduced = doc.to_string();
if reproduced != source {
let at = source
.bytes()
.zip(reproduced.bytes())
.position(|(a, b)| a != b)
.unwrap_or_else(|| source.len().min(reproduced.len()));
return Err(Error::new(At::of(source, at), Reason::NotPreserved));
}
let form = Self {
source: source.to_string(),
doc,
};
form.check_shape()?;
Ok(form)
}
fn check_shape(&self) -> Result<(), Error> {
let nodes = self.doc.nodes();
let root = match nodes {
[only] if only.name().value() == "form" => only,
[] => {
return Err(Error::new(
At::START,
Reason::NotAForm {
found: String::from("nothing"),
},
));
}
[first, ..] => {
let found = if first.name().value() == "form" {
nodes[1].name().value().to_string()
} else {
first.name().value().to_string()
};
let offender = if first.name().value() == "form" {
&nodes[1]
} else {
first
};
return Err(Error::new(
self.at(offender.span().offset()),
Reason::NotAForm { found },
));
}
};
let version = root
.get("version")
.and_then(KdlValue::as_integer)
.and_then(|v| u64::try_from(v).ok())
.ok_or_else(|| Error::new(self.at_node(root), Reason::Version))?;
if version > VERSION {
return Err(Error::new(
self.at_node(root),
Reason::FromTheFuture {
wanted: version,
understood: VERSION,
},
));
}
for axis in ["width", "height"] {
if root.get(axis).and_then(KdlValue::as_integer).is_none() {
return Err(Error::new(
self.at_node(root),
Reason::Missing {
kind: String::from("form"),
name: if axis == "width" { "width" } else { "height" },
},
));
}
}
let kind = self
.named(root, "kind", FormKind::NAMES, FormKind::from_name)?
.unwrap_or(FormKind::Screen);
self.named(root, "theme", THEMES, |n| THEMES.contains(&n).then_some(()))?;
for entry in root.entries() {
let Some(name) = entry.name() else {
continue;
};
let name = name.value();
if name == "version" || crate::build::form_property(kind, name).is_some() {
continue;
}
let accepted: Vec<&'static str> = crate::build::FORM_PROPERTIES
.iter()
.chain(crate::build::kind_properties(kind))
.map(|property| property.name)
.collect();
return Err(Error::new(
self.at(entry.span().offset()),
Reason::UnknownFormProperty {
kind: FormKind::NAMES[kind as usize],
found: name.to_string(),
accepted,
},
));
}
if matches!(kind, FormKind::Drawer | FormKind::Shelf)
&& root.get("extent").and_then(KdlValue::as_integer).is_none()
{
return Err(Error::new(
self.at_node(root),
Reason::Missing {
kind: String::from(FormKind::NAMES[kind as usize]),
name: "extent",
},
));
}
self.named(root, "side", denise_ui::widgets::SIDES, |n| {
denise_ui::widgets::describe::side_from_name(n)
})?;
if let Some(background) = root.get("background") {
let name = background.as_string().ok_or_else(|| {
Error::new(
self.at_node(root),
Reason::WrongType {
kind: "form",
name: String::from("background"),
wanted: "one of the listed names",
},
)
})?;
if denise_ui::widgets::describe::role_from_name(name).is_none() {
return Err(Error::new(
self.at_node(root),
Reason::NotAName {
name: String::from("colour role"),
found: name.to_string(),
accepted: denise_ui::widgets::ROLES,
},
));
}
}
Ok(())
}
fn named<T>(
&self,
node: &KdlNode,
property: &'static str,
accepted: &'static [&'static str],
parse: impl Fn(&str) -> Option<T>,
) -> Result<Option<T>, Error> {
let Some(value) = node.get(property) else {
return Ok(None);
};
let name = value.as_string().ok_or_else(|| {
Error::new(
self.at_node(node),
Reason::WrongType {
kind: "form",
name: property.to_string(),
wanted: "one of the listed names",
},
)
})?;
parse(name)
.ok_or_else(|| {
Error::new(
self.at_node(node),
Reason::NotAName {
name: property.to_string(),
found: name.to_string(),
accepted,
},
)
})
.map(Some)
}
pub(crate) fn at(&self, offset: usize) -> At {
At::of(&self.source, offset)
}
pub(crate) fn at_node(&self, node: &KdlNode) -> At {
self.at(node.span().offset())
}
pub(crate) fn root(&self) -> &KdlNode {
self.doc
.nodes()
.first()
.expect("checked at parse: exactly one `form` node")
}
pub fn title(&self) -> &str {
self.root()
.entries()
.iter()
.find(|e| e.name().is_none())
.and_then(|e| e.value().as_string())
.unwrap_or_default()
}
pub fn name(&self) -> Option<&str> {
self.root().get("name").and_then(KdlValue::as_string)
}
pub fn version(&self) -> u64 {
self.root()
.get("version")
.and_then(KdlValue::as_integer)
.and_then(|v| u64::try_from(v).ok())
.expect("checked at parse")
}
pub fn kind(&self) -> FormKind {
self.root()
.get("kind")
.and_then(KdlValue::as_string)
.and_then(FormKind::from_name)
.unwrap_or(FormKind::Screen)
}
pub fn scaling(&self) -> Scaling {
self.root()
.get("scaling")
.and_then(KdlValue::as_string)
.and_then(Scaling::from_name)
.unwrap_or_default()
}
pub fn fit(&self, surface: Size) -> Placement {
let design = self.size();
if design.width == 0 || design.height == 0 {
return Placement {
x: 1.0,
y: 1.0,
rect: Rect::ZERO,
};
}
let full = (
surface.width as f32 / design.width as f32,
surface.height as f32 / design.height as f32,
);
let (x, y) = match self.scaling() {
Scaling::None => (1.0, 1.0),
Scaling::Proportional => {
let both = if full.0 < full.1 { full.0 } else { full.1 };
(both, both)
}
Scaling::Stretch => full,
};
let scaled = Rect::from_size(design).scaled_by(x, y);
Placement {
x,
y,
rect: Rect::new(
(surface.width as i32 - scaled.width) / 2,
(surface.height as i32 - scaled.height) / 2,
scaled.width,
scaled.height,
),
}
}
pub fn resizable(&self) -> bool {
self.root()
.get("resizable")
.and_then(KdlValue::as_bool)
.unwrap_or(true)
}
pub fn min_size(&self) -> Option<Size> {
let axis = |name: &str| {
self.root()
.get(name)
.and_then(KdlValue::as_integer)
.and_then(|v| u32::try_from(v).ok())
};
match (axis("min-width"), axis("min-height")) {
(None, None) => None,
(width, height) => Some(Size::new(width.unwrap_or(0), height.unwrap_or(0))),
}
}
pub fn dim(&self) -> u8 {
self.root()
.get("dim")
.and_then(KdlValue::as_integer)
.and_then(|value| u8::try_from(value).ok())
.unwrap_or(160)
}
pub fn side(&self) -> Side {
self.root()
.get("side")
.and_then(KdlValue::as_string)
.and_then(denise_ui::widgets::describe::side_from_name)
.unwrap_or_else(|| self.kind().default_side())
}
pub fn extent(&self) -> i32 {
self.root()
.get("extent")
.and_then(KdlValue::as_integer)
.and_then(|value| i32::try_from(value).ok())
.unwrap_or(0)
}
pub fn size(&self) -> Size {
let axis = |name: &str| {
self.root()
.get(name)
.and_then(KdlValue::as_integer)
.and_then(|v| u32::try_from(v).ok())
.unwrap_or(0)
};
Size::new(axis("width"), axis("height"))
}
pub fn theme(&self) -> Theme {
match self.theme_name() {
"light" => theme::LIGHT,
"high-contrast" => theme::HIGH_CONTRAST,
_ => theme::DARK,
}
}
pub fn theme_name(&self) -> &str {
self.root()
.get("theme")
.and_then(KdlValue::as_string)
.unwrap_or("dark")
}
pub fn background(&self) -> Role {
self.root()
.get("background")
.and_then(KdlValue::as_string)
.and_then(denise_ui::widgets::describe::role_from_name)
.unwrap_or(Role::Base100)
}
pub fn text(&self) -> String {
self.doc.to_string()
}
fn at_mut(&mut self, path: &[usize]) -> Option<&mut KdlNode> {
let Some((&first, rest)) = path.split_first() else {
return self.doc.nodes_mut().first_mut();
};
let mut node = self
.doc
.nodes_mut()
.first_mut()?
.children_mut()
.as_mut()?
.nodes_mut()
.get_mut(first)?;
for &index in rest {
node = node.children_mut().as_mut()?.nodes_mut().get_mut(index)?;
}
Some(node)
}
pub fn set_number(&mut self, path: &[usize], name: &str, value: i64) -> bool {
match self.at_mut(path) {
Some(node) => set_literal(node, name, &Literal::Int(value)).is_ok(),
None => false,
}
}
fn node_at(&self, path: &[usize]) -> Option<&KdlNode> {
let mut node = self.doc.nodes().first()?;
for &index in path {
node = node.children()?.nodes().get(index)?;
}
Some(node)
}
pub fn property(&self, path: &[usize], name: &str) -> Option<String> {
Some(spell(self.node_at(path)?.get(name)?))
}
pub fn items(&self, path: &[usize], kind: &str) -> Vec<String> {
let Some(node) = self.node_at(path) else {
return Vec::new();
};
self.holder(node, kind)
.map(|(block, _)| {
block
.nodes()
.iter()
.filter(|child| child.name().value() == kind)
.map(|child| {
child
.entries()
.iter()
.find(|entry| entry.name().is_none())
.map_or_else(String::new, |entry| spell(entry.value()))
})
.collect()
})
.unwrap_or_default()
}
pub fn child_count(&self, path: &[usize]) -> usize {
self.node_at(path)
.and_then(KdlNode::children)
.map_or(0, |block| block.nodes().len())
}
pub fn item_path(&self, path: &[usize], kind: &str, nth: usize) -> Option<Vec<usize>> {
let node = self.node_at(path)?;
let (block, design) = self.holder(node, kind)?;
let at = block
.nodes()
.iter()
.enumerate()
.filter(|(_, child)| child.name().value() == kind)
.map(|(index, _)| index)
.nth(nth)?;
let mut full = path.to_vec();
if let Some(index) = design {
full.push(index);
}
full.push(at);
Some(full)
}
pub fn collection_parent(&self, path: &[usize], kind: &str) -> Option<Vec<usize>> {
let node = self.node_at(path)?;
let (_, design) = self.holder(node, kind)?;
let mut full = path.to_vec();
if let Some(index) = design {
full.push(index);
}
Some(full)
}
fn holder<'n>(
&self,
node: &'n KdlNode,
kind: &str,
) -> Option<(&'n KdlDocument, Option<usize>)> {
let children = node.children()?;
if !crate::build::is_placeholder(node.name().value(), kind) {
return Some((children, None));
}
let at = children
.nodes()
.iter()
.position(|child| child.name().value() == crate::build::DESIGN)?;
Some((children.nodes()[at].children()?, Some(at)))
}
pub fn node_text(&self, path: &[usize]) -> Option<String> {
let node = self.node_at(path)?;
let own = indent_of(node);
Some(reindent(&node.to_string(), &own, "", false))
}
pub fn written(&self) -> Vec<Written> {
let mut out = Vec::new();
let Some(root) = self.doc.nodes().first() else {
return out;
};
gather(root, &mut Vec::new(), &mut out);
out
}
pub fn argument(&self, path: &[usize]) -> Option<String> {
let node = self.node_at(path)?;
let entry = node.entries().iter().find(|e| e.name().is_none())?;
Some(spell(entry.value()))
}
pub fn clear_property(&mut self, path: &[usize], name: &str) -> bool {
let Some(node) = self.at_mut(path) else {
return false;
};
let before = node.entries().len();
node.retain(|entry| entry.name().map(|key| key.value()) != Some(name));
node.entries().len() != before
}
pub fn remove_at(&mut self, path: &[usize]) -> bool {
let Some((&last, above)) = path.split_last() else {
return false;
};
let Some(parent) = self.children_of_mut(above) else {
return false;
};
if last >= parent.len() {
return false;
}
parent.remove(last);
true
}
pub fn apply(&mut self, edit: Edit) -> Result<Edit, Error> {
match edit {
Edit::Property { path, name, value } => {
let node = self.at_mut(&path).ok_or_else(|| {
Error::new(At::START, Reason::NoSuchNode { path: path.clone() })
})?;
let Some(literal) = value else {
let text = node.to_string();
let key = name.clone();
node.retain(|entry| entry.name().map(|k| k.value()) != Some(&key));
return Ok(Edit::Replace { path, text });
};
let before = node
.entry(name.as_str())
.map(|entry| Literal::Verbatim(repr_of(entry)));
if let Some(was) = node.get(name.as_str()) {
let (holds, given) = (Class::of(was), literal.class()?);
if holds != given && holds != Class::Nothing {
return Err(Error::new(
At::START,
Reason::WrongKind {
name,
holds: holds.noun(),
given: given.noun(),
},
));
}
}
set_literal(node, name.as_str(), &literal)?;
Ok(Edit::Property {
path,
name,
value: before,
})
}
Edit::Insert {
parent,
index,
text,
} => {
let mut node = one_node(&text)?;
let holder = self.node_at(&parent).ok_or_else(|| {
Error::new(
At::START,
Reason::NoSuchNode {
path: parent.clone(),
},
)
})?;
let empty = holder.children().is_none();
let was = empty.then(|| holder.to_string());
let indent = indent_of(holder);
let step = self.indent_step();
let bare = node.format().is_none_or(|format| format.leading.is_empty());
let children = self.block_mut(&parent).ok_or_else(|| {
Error::new(
At::START,
Reason::NoSuchNode {
path: parent.clone(),
},
)
})?;
let index = index.min(children.nodes().len());
if bare {
let laid = reindent(
&node.to_string(),
"",
&format!("{indent}{step}"),
index == 0,
);
node = one_node(&laid)?;
let mut format = node.format().cloned().unwrap_or_default();
format.terminator = String::from("\n");
node.set_format(format);
}
let opens = node
.format()
.is_some_and(|format| format.leading.starts_with('\n'));
let displaced = index == 0 && !children.nodes().is_empty();
children.nodes_mut().insert(index, node);
if displaced
&& opens
&& let Some(after) = children.nodes_mut().get_mut(1)
{
let mut format = after.format().cloned().unwrap_or_default();
if let Some(rest) = format.leading.strip_prefix('\n') {
format.leading = rest.to_string();
after.set_format(format);
}
}
if empty {
let mut format = children.format().cloned().unwrap_or_default();
format.trailing = indent.clone();
children.set_format(format);
}
if empty && let Some(holder) = self.at_mut(&parent) {
let mut format = holder.format().cloned().unwrap_or_default();
format.before_children = String::from(" ");
holder.set_format(format);
}
match was {
Some(text) => Ok(Edit::Replace { path: parent, text }),
None => {
let mut path = parent;
path.push(index);
Ok(Edit::Remove { path })
}
}
}
Edit::Argument { path, value } => {
let node = self.at_mut(&path).ok_or_else(|| {
Error::new(At::START, Reason::NoSuchNode { path: path.clone() })
})?;
let Some(entry) = node.entries_mut().iter_mut().find(|e| e.name().is_none()) else {
return Err(Error::new(At::START, Reason::NoArgument));
};
let was = Literal::Verbatim(repr_of(entry));
let (new, repr) = value.parts()?;
entry.set_value(new);
match entry.format_mut() {
Some(format) => format.value_repr = repr,
None => entry.set_format(KdlEntryFormat {
value_repr: repr,
leading: String::from(" "),
..KdlEntryFormat::default()
}),
}
Ok(Edit::Argument { path, value: was })
}
Edit::Move { from, to, index } => {
if from.is_empty() || to.starts_with(&from) {
return Err(Error::new(At::START, Reason::IntoItself { path: from }));
}
let node = self.node_at(&from).ok_or_else(|| {
Error::new(At::START, Reason::NoSuchNode { path: from.clone() })
})?;
let text = node.to_string();
let old = indent_of(node);
let landing = after_removing(&to, &from).ok_or_else(|| {
Error::new(At::START, Reason::IntoItself { path: from.clone() })
})?;
let step = self.indent_step();
let new = match self.node_at(&to) {
Some(parent) if !to.is_empty() => indent_of(parent) + &step,
Some(_) => step,
None => {
return Err(Error::new(At::START, Reason::NoSuchNode { path: to }));
}
};
let held = self
.node_at(&to)
.and_then(KdlNode::children)
.map_or(0, |block| block.nodes().len());
let leaving = from.len() == to.len() + 1 && from.starts_with(&to);
let index = index.min(held - usize::from(leaving && held > 0));
let had = text.len() - text.trim_start_matches('\n').len();
let blanks = had.saturating_sub(usize::from(from.last() == Some(&0)));
let text = reindent(&text, &old, &new, index == 0);
let text = if blanks == 0 {
text
} else {
let mut with = "\n".repeat(blanks);
with.push_str(&text);
with
};
self.apply(Edit::Many(vec![
Edit::Remove { path: from },
Edit::Insert {
parent: landing,
index,
text,
},
]))
}
Edit::Remove { path } => {
let (&last, above) = path.split_last().ok_or_else(|| {
Error::new(At::START, Reason::NoSuchNode { path: Vec::new() })
})?;
let above = above.to_vec();
let children = self.children_of_mut(&above).ok_or_else(|| {
Error::new(
At::START,
Reason::NoSuchNode {
path: above.clone(),
},
)
})?;
if last >= children.len() {
return Err(Error::new(At::START, Reason::NoSuchNode { path }));
}
let opener = last == 0 && children.len() > 1;
let emptied = children.len() == 1;
let was = (opener || emptied).then(|| {
self.node_at(&above)
.map_or_else(String::new, |node| node.to_string())
});
let children = self.children_of_mut(&above).expect("just found");
let text = children.remove(last).to_string();
if opener && let Some(first) = children.first_mut() {
let mut format = first.format().cloned().unwrap_or_default();
format.leading.insert(0, '\n');
first.set_format(format);
}
if emptied {
self.drop_block(&above);
}
match was {
Some(text) => Ok(Edit::Replace { path: above, text }),
None => Ok(Edit::Insert {
parent: above,
index: last,
text,
}),
}
}
Edit::Many(edits) => {
let mut inverses: Vec<Edit> = Vec::with_capacity(edits.len());
for edit in edits {
match self.apply(edit) {
Ok(inverse) => inverses.push(inverse),
Err(error) => {
while let Some(inverse) = inverses.pop() {
let _ = self.apply(inverse);
}
return Err(error);
}
}
}
inverses.reverse();
Ok(Edit::Many(inverses))
}
Edit::Replace { path, text } => {
let node = one_node(&text)?;
let Some((&last, above)) = path.split_last() else {
let root = self
.doc
.nodes_mut()
.first_mut()
.ok_or_else(|| Error::new(At::START, Reason::NoSuchNode { path }))?;
let was = root.to_string();
*root = node;
return Ok(Edit::Replace {
path: Vec::new(),
text: was,
});
};
let above = above.to_vec();
let children = self.children_of_mut(&above).ok_or_else(|| {
Error::new(
At::START,
Reason::NoSuchNode {
path: above.clone(),
},
)
})?;
if last >= children.len() {
return Err(Error::new(At::START, Reason::NoSuchNode { path }));
}
let was = children[last].to_string();
children[last] = node;
Ok(Edit::Replace { path, text: was })
}
}
}
fn indent_step(&self) -> String {
self.doc
.nodes()
.first()
.and_then(KdlNode::children)
.and_then(|block| block.nodes().first())
.map(indent_of)
.filter(|indent| !indent.is_empty())
.unwrap_or_else(|| String::from(" "))
}
fn drop_block(&mut self, path: &[usize]) {
let holder = if path.is_empty() {
self.doc.nodes_mut().first_mut()
} else {
self.at_mut(path)
};
if let Some(node) = holder {
*node.children_mut() = None;
}
}
fn block_mut(&mut self, path: &[usize]) -> Option<&mut KdlDocument> {
if path.is_empty() {
return Some(self.doc.nodes_mut().first_mut()?.ensure_children());
}
Some(self.at_mut(path)?.ensure_children())
}
fn children_of_mut(&mut self, path: &[usize]) -> Option<&mut Vec<KdlNode>> {
if path.is_empty() {
return Some(
self.doc
.nodes_mut()
.first_mut()?
.children_mut()
.as_mut()?
.nodes_mut(),
);
}
Some(self.at_mut(path)?.children_mut().as_mut()?.nodes_mut())
}
}
pub fn tidy(source: &str) -> Result<String, Error> {
let form = Form::parse(source)?;
Ok(laid_out(source, &form.indent_step()))
}
fn laid_out(source: &str, step: &str) -> String {
let b = source.as_bytes();
let mut depth_at_line = vec![0usize];
let mut protected = vec![false];
let mut depth = 0usize;
let mut i = 0usize;
while i < b.len() {
if let Some(past) = not_structure(b, i) {
for _ in b[i..past.min(b.len())]
.iter()
.filter(|byte| **byte == b'\n')
{
depth_at_line.push(depth);
protected.push(true);
}
i = past.max(i + 1);
continue;
}
match b[i] {
b'{' => depth += 1,
b'}' => depth = depth.saturating_sub(1),
b'\n' => {
depth_at_line.push(depth);
protected.push(false);
}
_ => {}
}
i += 1;
}
let mut out = String::with_capacity(source.len());
for (number, line) in source.split_inclusive('\n').enumerate() {
let (body, ending) = match line.strip_suffix('\n') {
Some(body) => (body, "\n"),
None => (line, ""),
};
if protected.get(number).copied().unwrap_or(false) {
out.push_str(line);
continue;
}
let trimmed = body.trim();
if trimmed.is_empty() {
out.push_str(ending);
continue;
}
let depth = depth_at_line.get(number).copied().unwrap_or(0);
let depth = if trimmed.starts_with('}') {
depth.saturating_sub(1)
} else {
depth
};
for _ in 0..depth {
out.push_str(step);
}
out.push_str(body.trim_start());
while out.ends_with(' ') || out.ends_with('\t') {
out.pop();
}
out.push_str(ending);
}
out
}
fn not_structure(b: &[u8], at: usize) -> Option<usize> {
match *b.get(at)? {
b'/' if b.get(at + 1) == Some(&b'/') => {
let mut i = at;
while i < b.len() && b[i] != b'\n' {
i += 1;
}
Some(i)
}
b'/' if b.get(at + 1) == Some(&b'*') => {
let mut open = 1usize;
let mut i = at + 2;
while i < b.len() && open > 0 {
if b[i] == b'/' && b.get(i + 1) == Some(&b'*') {
open += 1;
i += 2;
} else if b[i] == b'*' && b.get(i + 1) == Some(&b'/') {
open -= 1;
i += 2;
} else {
i += 1;
}
}
Some(i)
}
b'#' => {
let mut hashes = 0usize;
let mut i = at;
while b.get(i) == Some(&b'#') {
hashes += 1;
i += 1;
}
if b.get(i) != Some(&b'"') {
return Some(i);
}
let mut scan = i + 1;
while scan < b.len() {
if b[scan] == b'"' {
let mut past = scan + 1;
let mut seen = 0usize;
while seen < hashes && b.get(past) == Some(&b'#') {
past += 1;
seen += 1;
}
if seen == hashes {
return Some(past);
}
}
scan += 1;
}
Some(i)
}
b'"' if b[at..].starts_with(b"\"\"\"") && opens_a_line(&b[at + 3..]) => {
let mut i = at + 3;
while i + 3 <= b.len() {
if b[i..].starts_with(b"\"\"\"") && closes_a_line(&b[at + 3..i]) {
return Some(i + 3);
}
i += 1;
}
Some(at + 1)
}
b'"' => {
let mut i = at + 1;
while i < b.len() && b[i] != b'"' {
i += if b[i] == b'\\' { 2 } else { 1 };
}
Some(if i < b.len() { i + 1 } else { at + 1 })
}
_ => None,
}
}
fn opens_a_line(rest: &[u8]) -> bool {
rest.iter()
.position(|byte| !matches!(byte, b' ' | b'\t' | b'\r'))
.is_none_or(|at| rest[at] == b'\n')
}
fn closes_a_line(before: &[u8]) -> bool {
before
.iter()
.rposition(|byte| !matches!(byte, b' ' | b'\t' | b'\r'))
.is_some_and(|at| before[at] == b'\n')
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Unparseable {
TooDeep(usize),
Unbalanced { at: usize, open: bool },
CommentedTooDeep(usize),
}
impl Unparseable {
fn error(self, source: &str) -> Error {
match self {
Self::TooDeep(at) => {
Error::new(At::of(source, at), Reason::TooDeep { limit: MAX_DEPTH })
}
Self::Unbalanced { at, open } => {
Error::new(At::of(source, at), Reason::Unbalanced { open })
}
Self::CommentedTooDeep(at) => Error::new(
At::of(source, at),
Reason::CommentedTooDeep {
limit: MAX_COMMENTED_DEPTH,
},
),
}
}
}
fn unparseable(source: &str) -> Option<Unparseable> {
let b = source.as_bytes();
let mut i = 0;
let mut depth = 0usize;
let mut blocks: Vec<bool> = Vec::new();
let mut opened: Vec<usize> = Vec::new();
let mut commented = 0usize;
let mut next_commented = false;
while i < b.len() {
let before = i;
if let Some(past) = not_structure(b, i) {
i = past;
} else {
match b[i] {
b'/' if b.get(i + 1) == Some(&b'-') => {
next_commented = true;
i += 2;
}
b'{' => {
depth += 1;
if depth > MAX_DEPTH {
return Some(Unparseable::TooDeep(i));
}
blocks.push(next_commented);
opened.push(i);
if next_commented {
commented += 1;
if commented > MAX_COMMENTED_DEPTH {
return Some(Unparseable::CommentedTooDeep(i));
}
}
next_commented = false;
i += 1;
}
b'}' => {
let Some(was_commented) = blocks.pop() else {
return Some(Unparseable::Unbalanced { at: i, open: false });
};
opened.pop();
depth -= 1;
if was_commented {
commented -= 1;
}
i += 1;
}
_ => i += 1,
}
}
let skipped = &b[before..i.min(b.len())];
if next_commented && skipped.iter().any(|byte| *byte == b'\n' || *byte == b';') {
next_commented = false;
}
}
opened.last().map(|at| Unparseable::Unbalanced {
at: *at,
open: true,
})
}
fn one_node(text: &str) -> Result<KdlNode, Error> {
let doc: KdlDocument = text.parse().map_err(|error: kdl::KdlError| {
let message = error
.diagnostics
.first()
.and_then(|d| d.message.clone())
.unwrap_or_else(|| String::from("this is not a node"));
Error::new(At::START, Reason::Syntax(message))
})?;
match doc.nodes() {
[only] => Ok(only.clone()),
other => Err(Error::new(
At::START,
Reason::Syntax(format!("this must be one node; it is {}", other.len())),
)),
}
}
pub fn fragment(text: &str, taken: &mut Vec<String>) -> Result<Vec<String>, Error> {
if text.len() > MAX_SOURCE {
return Err(Error::new(
At::START,
Reason::TooLarge { limit: MAX_SOURCE },
));
}
if let Some(refusal) = unparseable(text) {
return Err(refusal.error(text));
}
let mut doc: KdlDocument = text.parse().map_err(|error: kdl::KdlError| {
let first = error.diagnostics.first();
let at = first.map_or(At::START, |d| At::of(text, d.span.offset()));
let message = first
.and_then(|d| d.message.clone())
.unwrap_or_else(|| String::from("this is not form source"));
Error::new(at, Reason::Syntax(message))
})?;
for node in doc.nodes_mut() {
rename_apart(node, taken)?;
}
Ok(doc
.nodes()
.iter()
.map(|node| reindent(&node.to_string(), &indent_of(node), "", false))
.collect())
}
fn rename_apart(node: &mut KdlNode, taken: &mut Vec<String>) -> Result<(), Error> {
if let Some(entry) = node.entry("name") {
let was = spell(entry.value());
let now = unused(&was, taken);
if now != was {
set_literal(node, "name", &Literal::Name(now.clone()))?;
}
taken.push(now);
}
if let Some(block) = node.children_mut() {
for child in block.nodes_mut() {
rename_apart(child, taken)?;
}
}
Ok(())
}
fn unused(name: &str, taken: &[String]) -> String {
if !taken.iter().any(|held| held == name) {
return String::from(name);
}
let stem = name.trim_end_matches(|c: char| c.is_ascii_digit());
let stem = if stem.is_empty() { name } else { stem };
(2usize..)
.map(|number| format!("{stem}{number}"))
.find(|candidate| !taken.iter().any(|held| held == candidate))
.unwrap_or_else(|| String::from(name))
}
pub fn after_removing(path: &[usize], removed: &[usize]) -> Option<Vec<usize>> {
if path.starts_with(removed) {
return None;
}
let (index, ancestors) = removed.split_last()?;
let mut out = path.to_vec();
if out.len() > ancestors.len() && out.starts_with(ancestors) && out[ancestors.len()] > *index {
out[ancestors.len()] -= 1;
}
Some(out)
}
fn reindent(text: &str, old: &str, new: &str, first: bool) -> String {
let mut out = String::with_capacity(text.len() + 8);
for (index, line) in text
.trim_start_matches('\n')
.split_inclusive('\n')
.enumerate()
{
if old.is_empty() {
if !line.trim().is_empty() {
out.push_str(new);
}
out.push_str(line);
continue;
}
match line.strip_prefix(old) {
Some(rest) => {
out.push_str(new);
out.push_str(rest);
}
None if index == 0 => {
out.push_str(new);
out.push_str(line.trim_start());
}
None => out.push_str(line),
}
}
if first {
out.insert(0, '\n');
}
out
}
fn gather(node: &KdlNode, path: &mut Vec<usize>, out: &mut Vec<Written>) {
let mut line = String::from(node.name().value());
for entry in node.entries() {
line.push(' ');
line.push_str(&shown(entry));
}
out.push(Written {
path: path.clone(),
kind: node.name().value().to_string(),
name: node.get("name").map(spell),
argument: node
.entries()
.iter()
.find(|entry| entry.name().is_none())
.map(|entry| spell(entry.value())),
line,
});
let Some(children) = node.children() else {
return;
};
for (index, child) in children.nodes().iter().enumerate() {
path.push(index);
gather(child, path, out);
path.pop();
}
}
fn shown(entry: &KdlEntry) -> String {
let value = match entry.value().as_string() {
Some(text) => quoted(text),
None => entry.value().to_string(),
};
match entry.name() {
Some(name) => format!("{}={value}", name.value()),
None => value,
}
}
fn indent_of(node: &KdlNode) -> String {
let leading = node.format().map_or("", |format| format.leading.as_str());
let line = leading.rsplit('\n').next().unwrap_or("");
line.chars().filter(|c| c.is_whitespace()).collect()
}
fn one_value(text: &str) -> Result<KdlValue, Error> {
let entry = KdlEntry::parse(text).map_err(|error: kdl::KdlError| {
let message = error
.diagnostics
.first()
.and_then(|d| d.message.clone())
.unwrap_or_else(|| String::from("this is not a value"));
Error::new(At::START, Reason::Syntax(message))
})?;
if entry.name().is_some() {
return Err(Error::new(
At::START,
Reason::Syntax(String::from("this must be a value, not a property")),
));
}
Ok(entry.value().clone())
}
fn repr_of(entry: &KdlEntry) -> String {
entry.format().map_or_else(
|| entry.value().to_string(),
|format| format.value_repr.clone(),
)
}
fn spell(value: &KdlValue) -> String {
match value.as_string() {
Some(text) => text.to_string(),
None => value.to_string(),
}
}
fn quoted(text: &str) -> String {
let mut out = String::with_capacity(text.len() + 2);
out.push('"');
for character in text.chars() {
match character {
'\\' | '"' => {
out.push('\\');
out.push(character);
}
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0C}' => out.push_str("\\f"),
other => out.push(other),
}
}
out.push('"');
out
}
fn set_literal(node: &mut KdlNode, name: &str, literal: &Literal) -> Result<(), Error> {
let (value, repr) = literal.parts()?;
if let Some(entry) = node.entry_mut(name) {
entry.set_value(value);
match entry.format_mut() {
Some(format) => format.value_repr = repr,
None => entry.set_format(KdlEntryFormat {
value_repr: repr,
leading: String::from(" "),
..KdlEntryFormat::default()
}),
}
return Ok(());
}
let mut entry = KdlEntry::new_prop(name, value);
entry.set_format(KdlEntryFormat {
value_repr: repr,
leading: String::from(" "),
..KdlEntryFormat::default()
});
node.push(entry);
Ok(())
}
fn restore_after_close(doc: &mut KdlDocument, source: &str) {
for node in doc.nodes_mut() {
restore_subtree(node, source);
}
let trailing = doc.format().map_or(0, |format| format.trailing.len());
let Some(limit) = source.len().checked_sub(trailing) else {
return;
};
terminate_block(doc, limit, source);
}
fn restore_subtree(node: &mut KdlNode, source: &str) {
let Some(block) = node.children_mut() else {
return;
};
if block.nodes().is_empty() {
return;
}
for child in block.nodes_mut() {
restore_subtree(child, source);
}
let trailing = block
.format()
.map_or_else(String::new, |format| format.trailing.clone());
let last = block.nodes().last().expect("the block is not empty");
let Some(limit) = end_of_nodes(source, content_end(last), &trailing) else {
return;
};
terminate_block(block, limit, source);
}
fn terminate_block(block: &mut KdlDocument, limit: usize, source: &str) {
let bounds: Vec<usize> = (0..block.nodes().len())
.map(|i| block.nodes().get(i + 1).map_or(limit, owned_start))
.collect();
for (node, bound) in block.nodes_mut().iter_mut().zip(bounds) {
let from = content_end(node);
let Some(terminator) = source.get(from..bound) else {
continue;
};
let format = node.format().cloned().unwrap_or_default();
if terminator != format.terminator {
node.set_format(KdlNodeFormat {
terminator: terminator.to_string(),
..format
});
}
}
}
fn owned_start(node: &KdlNode) -> usize {
let leading = node.format().map_or(0, |format| format.leading.len());
node.span().offset().saturating_sub(leading)
}
fn content_end(node: &KdlNode) -> usize {
let format = node.format().cloned().unwrap_or_default();
let rendered = node.to_string().len();
let inner = rendered.saturating_sub(format.leading.len() + format.terminator.len());
node.span().offset() + inner
}
fn end_of_nodes(source: &str, from: usize, trailing: &str) -> Option<usize> {
let mut at = from;
loop {
let rest = source.get(at..)?;
if rest
.strip_prefix(trailing)
.is_some_and(|past| past.starts_with('}'))
{
return Some(at);
}
at += trivia_width(rest)?;
}
}
fn trivia_width(rest: &str) -> Option<usize> {
let first = rest.chars().next()?;
if first.is_whitespace() {
return Some(first.len_utf8());
}
if let Some(body) = rest.strip_prefix("//") {
return Some(2 + body.find('\n').map_or(body.len(), |end| end + 1));
}
if !rest.starts_with("/*") {
return None;
}
let bytes = rest.as_bytes();
let mut depth = 0usize;
let mut at = 0usize;
while at + 1 < bytes.len() {
match &bytes[at..at + 2] {
b"/*" => {
depth += 1;
at += 2;
}
b"*/" => {
depth -= 1;
at += 2;
if depth == 0 {
return Some(at);
}
}
_ => at += 1,
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn reproduced(source: &str) -> String {
let mut doc: KdlDocument = source.parse().expect("the shape under test parses");
restore_after_close(&mut doc, source);
doc.to_string()
}
#[test]
fn a_brace_keeps_what_follows_it_to_the_end_of_the_line() {
for source in [
"a {\n b 1\n} \nc 3\n",
"a {\n b 1\n} // x\nc 3\n",
"a {\n b 1\n} /* p\nq */\nc 3\n",
"a {\n b 1\n} // x\n",
"a {\n b 1\n} // x",
"a {\n b 1\n} // x\n\nc 3\n",
"o {\n a {\n b 1\n } // x\n}\n",
"o {\n a {\n b 1\n } /* p\nq */\n}\n",
"a {\n b {\n c 1\n } // x\n}\n",
"a {\n b {\n c 1\n } /-d 2\n}\n",
"a {\n b 1\n} // closes }\nc 3\n",
"a {\n b 1\n} /* p /* q */ r */\nc 3\n",
"a {} // x\nc 3\n",
] {
assert_eq!(reproduced(source), source, "in {source:?}");
}
}
#[test]
fn tidying_changes_only_the_whitespace_at_the_ends_of_a_line() {
for name in corpus() {
let source = std::fs::read_to_string(&name).expect("readable");
let tidied = tidy(&source).unwrap_or_else(|e| panic!("{name}: {e}"));
let before: Vec<&str> = source.lines().map(str::trim).collect();
let after: Vec<&str> = tidied.lines().map(str::trim).collect();
assert_eq!(before, after, "in {name}");
}
}
#[test]
fn tidying_a_tidy_file_is_a_no_op() {
for name in corpus() {
let source = std::fs::read_to_string(&name).expect("readable");
let once = tidy(&source).expect("tidies");
let twice = tidy(&once).expect("tidies again");
assert_eq!(once, twice, "in {name}");
let form = Form::parse(&once).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(form.text(), once, "in {name}");
}
}
fn corpus() -> Vec<String> {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
let mut found = Vec::new();
for dir in [
format!("{root}/forms"),
format!("{root}/denise-forms/tests/awkward"),
] {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "dform") {
found.push(path.to_string_lossy().into_owned());
}
}
}
assert!(found.len() > 6, "the corpus went missing: {found:?}");
found.sort();
found
}
#[test]
fn tidying_keeps_what_a_person_put_there_and_fixes_the_indent() {
let ragged = "\
// a note about the form
form \"F\" version=1 kind=screen width=20 height=20 {
label \"hi\" x=0 y=0 w=5 h=5 // a note about the label
panel name=p x=0 y=6 w=5 h=5 {
label \"in\" x=0 y=0 w=5 h=5
}
}
";
let out = tidy(ragged).expect("tidies");
assert!(out.contains("// a note about the form"), "{out}");
assert!(out.contains("// a note about the label"), "{out}");
assert!(out.contains("label \"hi\""), "{out}");
assert!(out.contains("\"hi\" x=0"), "{out}");
assert!(out.contains("h=5 // a note about the label\n\n"), "{out}");
assert!(out.contains("\n label \"hi\""), "{out}");
assert!(out.contains("\n label \"in\""), "{out}");
assert!(out.contains("\n }\n}\n"), "{out}");
}
#[test]
fn tidying_leaves_the_inside_of_a_multi_line_string_alone() {
let source = "\
form \"F\" version=1 kind=screen width=20 height=20 {
label \"one\" x=0 y=0 w=5 h=5
label \"\"\"
indented on purpose
and so is this
\"\"\" x=0 y=6 w=5 h=5
}
";
let out = tidy(source).expect("tidies");
assert!(out.contains("\n indented on purpose\n"), "{out}");
assert!(out.contains("\n and so is this\n"), "{out}");
assert!(out.contains("\n label \"\"\"\n"), "{out}");
}
#[test]
fn tidying_uses_the_indent_the_file_already_uses() {
let two = "\
form \"F\" version=1 kind=screen width=20 height=20 {
panel name=p x=0 y=0 w=5 h=5 {
label \"in\" x=0 y=0 w=5 h=5
}
}
";
let out = tidy(two).expect("tidies");
assert!(out.contains("\n panel"), "{out}");
assert!(out.contains("\n label"), "{out}");
}
#[test]
fn a_file_that_does_not_parse_is_not_rewritten() {
assert!(tidy("form \"F\" version=1 {").is_err());
assert!(tidy("not a form at all").is_err());
assert!(tidy(&("a /-{ ".repeat(8) + &"}".repeat(8))).is_err());
}
#[test]
fn a_type_annotation_kdl_cannot_write_back_is_refused_rather_than_mangled() {
for source in ["(Z) h", "( Z )h", "(Z) h\n", "(Z)h { (Y) i }\n"] {
let doc: KdlDocument = source.parse().expect("kdl reads it");
let mut repaired = doc;
restore_after_close(&mut repaired, source);
assert_ne!(
repaired.to_string(),
source,
"kdl now keeps {source:?} -- the refusal below can go"
);
}
let error = Form::parse("(Z) h").expect_err("cannot be reproduced");
assert!(matches!(error.reason, Reason::NotPreserved), "{error}");
let kept = "(Z)h\n";
let doc: KdlDocument = kept.parse().expect("kdl reads it");
assert_eq!(doc.to_string(), kept);
}
#[test]
fn a_file_kdl_keeps_intact_is_left_exactly_as_it_was() {
for source in [
"a {\n b 1\n}\nc 3\n",
"a 1; b 2\n",
"a { b 1 }; c 2\n",
"a \\\n 1\nc 3\n",
"a {\n b 1\n /-c 2\n}\n",
"a {\n}\nc 3\n",
"// a leading comment\na 1\n",
"a 1\n// and a trailing one\n",
"\n\na 1\n\n\nb 2\n",
"a \"a string with } and // in it\"\n",
] {
assert_eq!(reproduced(source), source, "in {source:?}");
}
}
#[test]
fn nesting_within_the_limit_is_allowed() {
let source = "a ".to_string() + &"{ b ".repeat(MAX_DEPTH) + &"}".repeat(MAX_DEPTH);
assert_eq!(unparseable(&source), None);
}
#[test]
fn one_level_past_the_limit_is_caught_before_the_parser_sees_it() {
let deep = MAX_DEPTH + 1;
let source = "a ".to_string() + &"{ b ".repeat(deep) + &"}".repeat(deep);
assert!(matches!(
unparseable(&source),
Some(Unparseable::TooDeep(_))
));
}
#[test]
fn commented_out_blocks_are_allowed_until_they_nest() {
for levels in 0..=MAX_COMMENTED_DEPTH {
let source = "a /-{ ".repeat(levels) + &"}".repeat(levels);
assert_eq!(unparseable(&source), None, "at {levels} levels");
}
let side_by_side = "a /-{ }\n".repeat(32);
assert_eq!(unparseable(&side_by_side), None);
let plain = "/- label \"x\" y=1\n".repeat(32);
assert_eq!(unparseable(&plain), None);
let separated = "/- a\nb {\n}\n".repeat(16);
assert_eq!(unparseable(&separated), None);
assert_eq!(unparseable(&"/- a; b {\n}\n".repeat(16)), None);
let quoted = "/- a \"{{{{\"\n".repeat(16);
assert_eq!(unparseable("ed), None);
}
#[test]
fn commented_out_blocks_nested_past_the_limit_never_reach_the_parser() {
let deep = MAX_COMMENTED_DEPTH + 1;
let source = "a /-{ ".repeat(deep) + &"}".repeat(deep);
assert!(matches!(
unparseable(&source),
Some(Unparseable::CommentedTooDeep(_))
));
assert!(matches!(
unparseable(&"a /- {\n".repeat(deep)),
Some(Unparseable::CommentedTooDeep(_))
));
assert!(matches!(
unparseable(&"/- a b c {\n d 1\n".repeat(deep)),
Some(Unparseable::CommentedTooDeep(_))
));
}
#[test]
fn a_triple_quote_is_only_a_string_when_it_opens_a_line() {
let hidden = String::from("a x=\"\"\" y\n") + &"b {\n".repeat(MAX_DEPTH + 1);
assert!(
matches!(unparseable(&hidden), Some(Unparseable::TooDeep(_))),
"a `\"\"\"` that opens no line must hide nothing"
);
let closed_wrong = String::from("a x=\"\"\"\nhi\"\"\"\n") + &"b {\n".repeat(MAX_DEPTH + 1);
assert!(
matches!(unparseable(&closed_wrong), Some(Unparseable::TooDeep(_))),
"a `\"\"\"` that closes no line must hide nothing"
);
let real = "a x=\"\"\"\n{{{{{{{{\n\"\"\"\nb 1\n";
assert_eq!(unparseable(real), None);
let padded = "a x=\"\"\" \n{{{{{{{{\n \"\"\"\nb 1\n";
assert_eq!(unparseable(padded), None);
let after = "a x=\"\"\"\n{{{{{{{{\n\"\"\" y=2\nb 1\n";
assert_eq!(unparseable(after), None);
}
#[test]
fn a_brace_with_no_partner_is_refused_before_the_parser_looks_for_one() {
assert!(matches!(
unparseable("a {\n b 1\n"),
Some(Unparseable::Unbalanced { open: true, .. })
));
assert!(matches!(
unparseable("a 1\n}\n"),
Some(Unparseable::Unbalanced { open: false, .. })
));
let Some(Unparseable::Unbalanced { at, open: true }) = unparseable("a {\n b {\n }\n")
else {
panic!("the outer brace is never closed")
};
assert_eq!(at, 2, "the outer `{{`, not the inner one");
for source in [
"a { }",
"a {\n}\n",
"a { b { c { } } }",
"a 1",
"",
"// nothing\n",
] {
assert_eq!(unparseable(source), None, "in {source:?}");
}
}
#[test]
fn a_brace_inside_a_string_is_not_structure() {
for source in [
r#"a "{{{{{{{{{{{{{{{{" b"#,
r##"a #"{{{{{{{{{{{{{{{{"# b"##,
"a \"\"\"\n{{{{{{{{{{{{{{{{\n\"\"\" b",
"a // {{{{{{{{{{{{{{{{{{{{{{{{{{{{\n b",
"a /* {{{{{{{{{{{{{{{{{{{{{{{{{{ */ b",
] {
assert_eq!(unparseable(source), None, "in {source}");
}
}
#[test]
fn an_unterminated_string_does_not_loop_forever() {
assert_eq!(unparseable("a \"unterminated"), None);
assert_eq!(unparseable("a #\"unterminated"), None);
assert_eq!(unparseable("a /* unterminated"), None);
assert_eq!(unparseable("/- a \"unterminated"), None);
assert_eq!(unparseable("/- a #\"unterminated"), None);
assert_eq!(unparseable("/- a /* unterminated"), None);
assert_eq!(unparseable("/- \"x\\"), None);
assert_eq!(unparseable("/- a \"\"\"unterminated"), None);
}
#[test]
fn a_quote_that_never_closes_hides_nothing_behind_it() {
for opener in ["\"", "###############\"", "\"\"\"", "#\""] {
let hidden = format!("a {opener}\n") + &"b /-{ ".repeat(8);
assert!(
matches!(unparseable(&hidden), Some(Unparseable::CommentedTooDeep(_))),
"behind {opener:?}"
);
let deep = format!("a {opener}\n") + &"{ b ".repeat(MAX_DEPTH + 1);
assert!(
matches!(unparseable(&deep), Some(Unparseable::TooDeep(_))),
"behind {opener:?}"
);
}
let closed = String::from("a \"{ { { { \"\n") + &"b /-{ ".repeat(8);
assert!(matches!(
unparseable(&closed),
Some(Unparseable::CommentedTooDeep(_))
));
assert_eq!(unparseable("a \"{ { { { { { { { \" b"), None);
}
}