Par nodes 🧳

This commit is contained in:
Laurenz 2020-08-04 11:46:04 +02:00
parent dbfb3d2ced
commit ed4fdcb0ad
10 changed files with 224 additions and 207 deletions

BIN
fonts/SegoeUI-Emoji.ttf Normal file

Binary file not shown.

View File

@ -142,8 +142,6 @@ pub enum Command<'a> {
/// Start a new line. /// Start a new line.
BreakLine, BreakLine,
/// Start a new paragraph.
BreakParagraph,
/// Start a new page, which will be part of the finished layout even if it /// Start a new page, which will be part of the finished layout even if it
/// stays empty (since the page break is a _hard_ space break). /// stays empty (since the page break is a _hard_ space break).
BreakPage, BreakPage,

View File

@ -43,26 +43,43 @@ impl<'a> TreeLayouter<'a> {
} }
} }
async fn layout_tree(&mut self, tree: &SyntaxTree) {
for node in tree {
self.layout_node(node).await;
}
}
fn finish(self) -> Pass<MultiLayout> { fn finish(self) -> Pass<MultiLayout> {
Pass::new(self.layouter.finish(), self.feedback) Pass::new(self.layouter.finish(), self.feedback)
} }
fn layout_tree<'t>(&'t mut self, tree: &'t SyntaxTree) -> DynFuture<'t, ()> {
Box::pin(async move {
for node in tree {
self.layout_node(node).await;
}
})
}
async fn layout_node(&mut self, node: &Spanned<SyntaxNode>) { async fn layout_node(&mut self, node: &Spanned<SyntaxNode>) {
let decorate = |this: &mut TreeLayouter, deco| { let decorate = |this: &mut Self, deco| {
this.feedback.decorations.push(Spanned::new(deco, node.span)); this.feedback.decorations.push(Spanned::new(deco, node.span));
}; };
match &node.v { match &node.v {
SyntaxNode::Space => self.layout_space(), SyntaxNode::Spacing => {
SyntaxNode::Parbreak => self.layout_paragraph(), self.layouter.add_primary_spacing(
self.style.text.word_spacing(),
SpacingKind::WORD,
);
},
SyntaxNode::Linebreak => self.layouter.finish_line(), SyntaxNode::Linebreak => self.layouter.finish_line(),
SyntaxNode::ToggleItalic => {
self.style.text.italic = !self.style.text.italic;
decorate(self, Decoration::Italic);
}
SyntaxNode::ToggleBolder => {
self.style.text.bolder = !self.style.text.bolder;
decorate(self, Decoration::Bold);
}
SyntaxNode::Text(text) => { SyntaxNode::Text(text) => {
if self.style.text.italic { if self.style.text.italic {
decorate(self, Decoration::Italic); decorate(self, Decoration::Italic);
@ -75,16 +92,6 @@ impl<'a> TreeLayouter<'a> {
self.layout_text(text).await; self.layout_text(text).await;
} }
SyntaxNode::ToggleItalic => {
self.style.text.italic = !self.style.text.italic;
decorate(self, Decoration::Italic);
}
SyntaxNode::ToggleBolder => {
self.style.text.bolder = !self.style.text.bolder;
decorate(self, Decoration::Bold);
}
SyntaxNode::Raw(lines) => { SyntaxNode::Raw(lines) => {
// TODO: Make this more efficient. // TODO: Make this more efficient.
let fallback = self.style.text.fallback.clone(); let fallback = self.style.text.fallback.clone();
@ -109,14 +116,25 @@ impl<'a> TreeLayouter<'a> {
self.style.text.fallback = fallback; self.style.text.fallback = fallback;
} }
SyntaxNode::Par(par) => self.layout_par(par).await,
SyntaxNode::Dyn(dynamic) => { SyntaxNode::Dyn(dynamic) => {
self.layout_dyn(Spanned::new(dynamic.as_ref(), node.span)).await; self.layout_dyn(Spanned::new(dynamic.as_ref(), node.span)).await;
} }
} }
} }
async fn layout_par(&mut self, par: &SyntaxTree) {
self.layouter.add_secondary_spacing(
self.style.text.paragraph_spacing(),
SpacingKind::PARAGRAPH,
);
self.layout_tree(par).await;
}
async fn layout_dyn(&mut self, dynamic: Spanned<&dyn DynamicNode>) { async fn layout_dyn(&mut self, dynamic: Spanned<&dyn DynamicNode>) {
// Execute the tree's command-generating layout function. // Execute the dynamic node's command-generating layout function.
let layouted = dynamic.v.layout(LayoutContext { let layouted = dynamic.v.layout(LayoutContext {
style: &self.style, style: &self.style,
spaces: self.layouter.remaining(), spaces: self.layouter.remaining(),
@ -131,11 +149,21 @@ impl<'a> TreeLayouter<'a> {
} }
} }
fn execute_command<'r>( async fn layout_text(&mut self, text: &str) {
&'r mut self, self.layouter.add(
command: Command<'r>, layout_text(
tree_span: Span, text,
) -> DynFuture<'r, ()> { Box::pin(async move { TextContext {
loader: &self.ctx.loader,
style: &self.style.text,
dir: self.ctx.axes.primary,
align: self.ctx.align,
}
).await
);
}
async fn execute_command(&mut self, command: Command<'_>, span: Span) {
use Command::*; use Command::*;
match command { match command {
@ -149,13 +177,12 @@ impl<'a> TreeLayouter<'a> {
} }
BreakLine => self.layouter.finish_line(), BreakLine => self.layouter.finish_line(),
BreakParagraph => self.layout_paragraph(),
BreakPage => { BreakPage => {
if self.ctx.root { if self.ctx.root {
self.layouter.finish_space(true) self.layouter.finish_space(true)
} else { } else {
error!( error!(
@self.feedback, tree_span, @self.feedback, span,
"page break cannot only be issued from root context", "page break cannot only be issued from root context",
); );
} }
@ -183,7 +210,7 @@ impl<'a> TreeLayouter<'a> {
], true); ], true);
} else { } else {
error!( error!(
@self.feedback, tree_span, @self.feedback, span,
"page style cannot only be changed from root context", "page style cannot only be changed from root context",
); );
} }
@ -195,33 +222,5 @@ impl<'a> TreeLayouter<'a> {
self.ctx.axes = axes; self.ctx.axes = axes;
} }
} }
}) }
async fn layout_text(&mut self, text: &str) {
self.layouter.add(
layout_text(
text,
TextContext {
loader: &self.ctx.loader,
style: &self.style.text,
dir: self.ctx.axes.primary,
align: self.ctx.align,
}
).await
);
}
fn layout_space(&mut self) {
self.layouter.add_primary_spacing(
self.style.text.word_spacing(),
SpacingKind::WORD,
);
}
fn layout_paragraph(&mut self) {
self.layouter.add_secondary_spacing(
self.style.text.paragraph_spacing(),
SpacingKind::PARAGRAPH,
);
} }
} }

View File

@ -22,7 +22,6 @@ pub fn std() -> Scope {
std.add::<PageFunc>("page"); std.add::<PageFunc>("page");
std.add::<AlignFunc>("align"); std.add::<AlignFunc>("align");
std.add::<BoxFunc>("box"); std.add::<BoxFunc>("box");
std.add::<ParBreakFunc>("parbreak");
std.add::<PageBreakFunc>("pagebreak"); std.add::<PageBreakFunc>("pagebreak");
std.add_with_meta::<SpacingFunc>("h", Horizontal); std.add_with_meta::<SpacingFunc>("h", Horizontal);
std.add_with_meta::<SpacingFunc>("v", Vertical); std.add_with_meta::<SpacingFunc>("v", Vertical);

View File

@ -57,3 +57,12 @@ function! {
vec![SetPageStyle(style)] vec![SetPageStyle(style)]
} }
} }
function! {
/// `pagebreak`: Ends the current page.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct PageBreakFunc;
parse(default)
layout(self, ctx, f) { vec![BreakPage] }
}

View File

@ -2,26 +2,6 @@ use crate::layout::SpacingKind;
use crate::length::ScaleLength; use crate::length::ScaleLength;
use super::*; use super::*;
function! {
/// `parbreak`: Ends the current paragraph.
///
/// This has the same effect as two subsequent newlines.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct ParBreakFunc;
parse(default)
layout(self, ctx, f) { vec![BreakParagraph] }
}
function! {
/// `pagebreak`: Ends the current page.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct PageBreakFunc;
parse(default)
layout(self, ctx, f) { vec![BreakPage] }
}
function! { function! {
/// `h` and `v`: Add horizontal or vertical spacing. /// `h` and `v`: Add horizontal or vertical spacing.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]

View File

@ -75,8 +75,10 @@ impl Default for TextStyle {
"monospace" => ["source code pro", "noto sans mono"], "monospace" => ["source code pro", "noto sans mono"],
"math" => ["latin modern math", "serif"], "math" => ["latin modern math", "serif"],
}, },
base: ["source sans pro", "noto sans", base: [
"noto emoji", "latin modern math"], "source sans pro", "noto sans", "segoe ui emoji",
"noto emoji", "latin modern math",
],
}, },
variant: FontVariant { variant: FontVariant {
style: FontStyle::Normal, style: FontStyle::Normal,

View File

@ -96,6 +96,7 @@ pub struct ParseState {
/// function's body. /// function's body.
pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> { pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
let mut tree = SyntaxTree::new(); let mut tree = SyntaxTree::new();
let mut par = SyntaxTree::new();
let mut feedback = Feedback::new(); let mut feedback = Feedback::new();
for token in Tokens::new(src, offset, TokenMode::Body) { for token in Tokens::new(src, offset, TokenMode::Body) {
@ -103,10 +104,16 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
let node = match token.v { let node = match token.v {
// Starting from two newlines counts as a paragraph break, a single // Starting from two newlines counts as a paragraph break, a single
// newline does not. // newline does not.
Token::Space(newlines) => if newlines >= 2 { Token::Space(newlines) => if newlines < 2 {
SyntaxNode::Parbreak SyntaxNode::Spacing
} else { } else {
SyntaxNode::Space // End the current paragraph if it is not empty.
if let (Some(first), Some(last)) = (par.first(), par.last()) {
let span = Span::merge(first.span, last.span);
let node = SyntaxNode::Par(std::mem::take(&mut par));
tree.push(Spanned::new(node, span));
}
continue;
} }
Token::Function { header, body, terminated } => { Token::Function { header, body, terminated } => {
@ -136,6 +143,12 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
} }
}; };
par.push(Spanned::new(node, span));
}
if let (Some(first), Some(last)) = (par.first(), par.last()) {
let span = Span::merge(first.span, last.span);
let node = SyntaxNode::Par(par);
tree.push(Spanned::new(node, span)); tree.push(Spanned::new(node, span));
} }
@ -671,8 +684,7 @@ mod tests {
use Decoration::*; use Decoration::*;
use Expr::{Bool, Length as Len, Number as Num}; use Expr::{Bool, Length as Len, Number as Num};
use SyntaxNode::{ use SyntaxNode::{
Space as S, Parbreak, Linebreak, ToggleItalic as Italic, Spacing as S, Linebreak, ToggleItalic as Italic, ToggleBolder as Bold,
ToggleBolder as Bold,
}; };
/// Test whether the given string parses into /// Test whether the given string parses into
@ -714,10 +726,10 @@ mod tests {
}; };
} }
/// Shorthand for `p!("[val: ...]" => func!("val", ...))`. /// Shorthand for `p!("[val: ...]" => par![func!("val", ...)])`.
macro_rules! pval { macro_rules! pval {
($header:expr => $($tts:tt)*) => { ($header:expr => $($tts:tt)*) => {
p!(concat!("[val: ", $header, "]") => [func!("val": $($tts)*)]); p!(concat!("[val: ", $header, "]") => [par![func!("val": $($tts)*)]]);
} }
} }
@ -725,7 +737,6 @@ mod tests {
fn Str(text: &str) -> Expr { Expr::Str(text.to_string()) } fn Str(text: &str) -> Expr { Expr::Str(text.to_string()) }
fn Color(r: u8, g: u8, b: u8, a: u8) -> Expr { Expr::Color(RgbaColor::new(r, g, b, a)) } fn Color(r: u8, g: u8, b: u8, a: u8) -> Expr { Expr::Color(RgbaColor::new(r, g, b, a)) }
fn ColorStr(color: &str) -> Expr { Expr::Color(RgbaColor::from_str(color).expect("invalid test color")) } fn ColorStr(color: &str) -> Expr { Expr::Color(RgbaColor::from_str(color).expect("invalid test color")) }
fn ColorHealed() -> Expr { Expr::Color(RgbaColor::new_healed(0, 0, 0, 255)) }
fn Neg(e1: Expr) -> Expr { Expr::Neg(Box::new(Z(e1))) } fn Neg(e1: Expr) -> Expr { Expr::Neg(Box::new(Z(e1))) }
fn Add(e1: Expr, e2: Expr) -> Expr { Expr::Add(Box::new(Z(e1)), Box::new(Z(e2))) } fn Add(e1: Expr, e2: Expr) -> Expr { Expr::Add(Box::new(Z(e1)), Box::new(Z(e2))) }
fn Sub(e1: Expr, e2: Expr) -> Expr { Expr::Sub(Box::new(Z(e1)), Box::new(Z(e2))) } fn Sub(e1: Expr, e2: Expr) -> Expr { Expr::Sub(Box::new(Z(e1)), Box::new(Z(e2))) }
@ -764,6 +775,12 @@ mod tests {
}; };
} }
macro_rules! par {
($($tts:tt)*) => {
SyntaxNode::Par(span_vec![$($tts)*].0)
};
}
macro_rules! func { macro_rules! func {
($name:tt ($name:tt
$(: ($($pos:tt)*) $(, { $($key:tt => $value:expr),* })? )? $(: ($($pos:tt)*) $(, { $($key:tt => $value:expr),* })? )?
@ -837,32 +854,32 @@ mod tests {
} }
#[test] #[test]
fn parse_basic_SyntaxNodes() { fn parse_basic_nodes() {
// Basic SyntaxNodes. // Basic nodes.
p!("" => []); p!("" => []);
p!("hi" => [T("hi")]); p!("hi" => [par![T("hi")]]);
p!("*hi" => [Bold, T("hi")]); p!("*hi" => [par![Bold, T("hi")]]);
p!("hi_" => [T("hi"), Italic]); p!("hi_" => [par![T("hi"), Italic]]);
p!("hi you" => [T("hi"), S, T("you")]); p!("hi you" => [par![T("hi"), S, T("you")]]);
p!("hi// you\nw" => [T("hi"), S, T("w")]); p!("hi// you\nw" => [par![T("hi"), S, T("w")]]);
p!("\n\n\nhello" => [Parbreak, T("hello")]); p!("\n\n\nhello" => [par![T("hello")]]);
p!("first//\n//\nsecond" => [T("first"), S, S, T("second")]); p!("first//\n//\nsecond" => [par![T("first"), S, S, T("second")]]);
p!("first//\n \nsecond" => [T("first"), Parbreak, T("second")]); p!("first//\n \nsecond" => [par![T("first")], par![T("second")]]);
p!("first/*\n \n*/second" => [T("first"), T("second")]); p!("first/*\n \n*/second" => [par![T("first"), T("second")]]);
p!(r"a\ b" => [T("a"), Linebreak, S, T("b")]); p!(r"a\ b" => [par![T("a"), Linebreak, S, T("b")]]);
p!("💜\n\n 🌍" => [T("💜"), Parbreak, T("🌍")]); p!("💜\n\n 🌍" => [par![T("💜")], par![T("🌍")]]);
// Raw markup. // Raw markup.
p!("`py`" => [raw!["py"]]); p!("`py`" => [par![raw!["py"]]]);
p!("[val][`hi]`]" => [func!("val"; [raw!["hi]"]])]); p!("[val][`hi]`]" => [par![func!("val"; [par![raw!["hi]"]]])]]);
p!("`hi\nyou" => [raw!["hi", "you"]], [(1:3, 1:3, "expected backtick")]); p!("`hi\nyou" => [par![raw!["hi", "you"]]], [(1:3, 1:3, "expected backtick")]);
p!("`hi\\`du`" => [raw!["hi`du"]]); p!("`hi\\`du`" => [par![raw!["hi`du"]]]);
// Spanned SyntaxNodes. // Spanned SyntaxNodes.
p!("Hi" => [(0:0, 0:2, T("Hi"))]); p!("Hi" => [(0:0, 0:2, par![(0:0, 0:2, T("Hi"))])]);
p!("*Hi*" => [(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)]); p!("*Hi*" => [(0:0, 0:4, par![(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)])]);
p!("🌎\n*/[n]" => p!("🌎\n*/[n]" =>
[(0:0, 0:1, T("🌎")), (0:1, 1:0, S), (1:2, 1:5, func!((0:1, 0:2, "n")))], [(0:0, 1:5, par![(0:0, 0:1, T("🌎")), (0:1, 1:0, S), (1:2, 1:5, func!((0:1, 0:2, "n")))])],
[(1:0, 1:2, "unexpected end of block comment")], [(1:0, 1:2, "unexpected end of block comment")],
[(1:3, 1:4, ResolvedFunc)], [(1:3, 1:4, ResolvedFunc)],
); );
@ -871,52 +888,52 @@ mod tests {
#[test] #[test]
fn parse_function_names() { fn parse_function_names() {
// No closing bracket. // No closing bracket.
p!("[" => [func!("")], [ p!("[" => [par![func!("")]], [
(0:1, 0:1, "expected function name"), (0:1, 0:1, "expected function name"),
(0:1, 0:1, "expected closing bracket") (0:1, 0:1, "expected closing bracket")
]); ]);
// No name. // No name.
p!("[]" => [func!("")], [(0:1, 0:1, "expected function name")]); p!("[]" => [par![func!("")]], [(0:1, 0:1, "expected function name")]);
p!("[\"]" => [func!("")], [ p!("[\"]" => [par![func!("")]], [
(0:1, 0:3, "expected function name, found string"), (0:1, 0:3, "expected function name, found string"),
(0:3, 0:3, "expected closing bracket"), (0:3, 0:3, "expected closing bracket"),
]); ]);
// An unknown name. // An unknown name.
p!("[hi]" => p!("[hi]" =>
[func!("hi")], [par![func!("hi")]],
[(0:1, 0:3, "unknown function")], [(0:1, 0:3, "unknown function")],
[(0:1, 0:3, UnresolvedFunc)], [(0:1, 0:3, UnresolvedFunc)],
); );
// A valid name. // A valid name.
p!("[f]" => [func!("f")], [], [(0:1, 0:2, ResolvedFunc)]); p!("[f]" => [par![func!("f")]], [], [(0:1, 0:2, ResolvedFunc)]);
p!("[ f]" => [func!("f")], [], [(0:3, 0:4, ResolvedFunc)]); p!("[ f]" => [par![func!("f")]], [], [(0:3, 0:4, ResolvedFunc)]);
// An invalid token for a name. // An invalid token for a name.
p!("[12]" => [func!("")], [(0:1, 0:3, "expected function name, found number")], []); p!("[12]" => [par![func!("")]], [(0:1, 0:3, "expected function name, found number")], []);
p!("[🌎]" => [func!("")], [(0:1, 0:2, "expected function name, found invalid token")], []); p!("[🌎]" => [par![func!("")]], [(0:1, 0:2, "expected function name, found invalid token")], []);
p!("[ 🌎]" => [func!("")], [(0:3, 0:4, "expected function name, found invalid token")], []); p!("[ 🌎]" => [par![func!("")]], [(0:3, 0:4, "expected function name, found invalid token")], []);
} }
#[test] #[test]
fn parse_colon_starting_function_arguments() { fn parse_colon_starting_function_arguments() {
// Valid. // Valid.
p!("[val: true]" => p!("[val: true]" =>
[func!["val": (Bool(true))]], [], [par![func!["val": (Bool(true))]]], [],
[(0:1, 0:4, ResolvedFunc)], [(0:1, 0:4, ResolvedFunc)],
); );
// No colon before arg. // No colon before arg.
p!("[val\"s\"]" => [func!("val")], [(0:4, 0:4, "expected colon")]); p!("[val\"s\"]" => [par![func!("val")]], [(0:4, 0:4, "expected colon")]);
// No colon before valid, but wrong token. // No colon before valid, but wrong token.
p!("[val=]" => [func!("val")], [(0:4, 0:4, "expected colon")]); p!("[val=]" => [par![func!("val")]], [(0:4, 0:4, "expected colon")]);
// No colon before invalid tokens, which are ignored. // No colon before invalid tokens, which are ignored.
p!("[val/🌎:$]" => p!("[val/🌎:$]" =>
[func!("val")], [par![func!("val")]],
[(0:4, 0:4, "expected colon")], [(0:4, 0:4, "expected colon")],
[(0:1, 0:4, ResolvedFunc)], [(0:1, 0:4, ResolvedFunc)],
); );
@ -924,18 +941,18 @@ mod tests {
// String in invalid header without colon still parsed as string // String in invalid header without colon still parsed as string
// Note: No "expected quote" error because not even the string was // Note: No "expected quote" error because not even the string was
// expected. // expected.
p!("[val/\"]" => [func!("val")], [ p!("[val/\"]" => [par![func!("val")]], [
(0:4, 0:4, "expected colon"), (0:4, 0:4, "expected colon"),
(0:7, 0:7, "expected closing bracket"), (0:7, 0:7, "expected closing bracket"),
]); ]);
// Just colon without args. // Just colon without args.
p!("[val:]" => [func!("val")]); p!("[val:]" => [par![func!("val")]]);
p!("[val:/*12pt*/]" => [func!("val")]); p!("[val:/*12pt*/]" => [par![func!("val")]]);
// Whitespace / comments around colon. // Whitespace / comments around colon.
p!("[val\n:\ntrue]" => [func!("val": (Bool(true)))]); p!("[val\n:\ntrue]" => [par![func!("val": (Bool(true)))]]);
p!("[val/*:*/://\ntrue]" => [func!("val": (Bool(true)))]); p!("[val/*:*/://\ntrue]" => [par![func!("val": (Bool(true)))]]);
} }
#[test] #[test]
@ -963,24 +980,25 @@ mod tests {
pval!("12e-3cm/1pt" => (Div(Len(Length::cm(12e-3)), Len(Length::pt(1.0))))); pval!("12e-3cm/1pt" => (Div(Len(Length::cm(12e-3)), Len(Length::pt(1.0)))));
// Span of expression. // Span of expression.
p!("[val: 1 + 3]" => [(0:0, 0:12, func!((0:1, 0:4, "val"): ( p!("[val: 1 + 3]" => [(0:0, 0:12, par![(0:0, 0:12, func!(
(0:6, 0:11, Expr::Add( (0:1, 0:4, "val"): ((0:6, 0:11, Expr::Add(
Box::new(span_item!((0:6, 0:7, Num(1.0)))), Box::new(span_item!((0:6, 0:7, Num(1.0)))),
Box::new(span_item!((0:10, 0:11, Num(3.0)))), Box::new(span_item!((0:10, 0:11, Num(3.0)))),
)) )))
)))]); ))])]);
// Unclosed string. // Unclosed string.
p!("[val: \"hello]" => [func!("val": (Str("hello]")), {})], [ p!("[val: \"hello]" => [par![func!("val": (Str("hello]")), {})]], [
(0:13, 0:13, "expected quote"), (0:13, 0:13, "expected quote"),
(0:13, 0:13, "expected closing bracket"), (0:13, 0:13, "expected closing bracket"),
]); ]);
// Invalid, healed colors. // Invalid, healed colors.
p!("[val: #12345]" => [func!("val": (ColorHealed()))], [(0:6, 0:12, "invalid color")]); let healed = Expr::Color(RgbaColor::new_healed(0, 0, 0, 255));
p!("[val: #a5]" => [func!("val": (ColorHealed()))], [(0:6, 0:9, "invalid color")]); p!("[val: #12345]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:12, "invalid color")]);
p!("[val: #14b2ah]" => [func!("val": (ColorHealed()))], [(0:6, 0:13, "invalid color")]); p!("[val: #a5]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:9, "invalid color")]);
p!("[val: #f075ff011]" => [func!("val": (ColorHealed()))], [(0:6, 0:16, "invalid color")]); p!("[val: #14b2ah]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:13, "invalid color")]);
p!("[val: #f075ff011]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:16, "invalid color")]);
} }
#[test] #[test]
@ -999,15 +1017,17 @@ mod tests {
pval!("3/4*5" => (Mul(Div(Num(3.0), Num(4.0)), Num(5.0)))); pval!("3/4*5" => (Mul(Div(Num(3.0), Num(4.0)), Num(5.0))));
// Span of parenthesized expression contains parens. // Span of parenthesized expression contains parens.
p!("[val: (1)]" => [(0:0, 0:10, func!((0:1, 0:4, "val"): ((0:6, 0:9, Num(1.0)))))]); p!("[val: (1)]" => [(0:0, 0:10, par![
(0:0, 0:10, func!((0:1, 0:4, "val"): ((0:6, 0:9, Num(1.0)))))
])]);
// Invalid expressions. // Invalid expressions.
p!("[val: 4pt--]" => [func!("val": (Len(Length::pt(4.0))))], [ p!("[val: 4pt--]" => [par![func!("val": (Len(Length::pt(4.0))))]], [
(0:10, 0:11, "dangling minus"), (0:10, 0:11, "dangling minus"),
(0:6, 0:10, "missing right summand") (0:6, 0:10, "missing right summand")
]); ]);
p!("[val: 3mm+4pt*]" => p!("[val: 3mm+4pt*]" =>
[func!("val": (Add(Len(Length::mm(3.0)), Len(Length::pt(4.0)))))], [par![func!("val": (Add(Len(Length::mm(3.0)), Len(Length::pt(4.0)))))]],
[(0:10, 0:14, "missing right factor")], [(0:10, 0:14, "missing right factor")],
); );
} }
@ -1026,19 +1046,19 @@ mod tests {
// Invalid value. // Invalid value.
p!("[val: sound(\x07)]" => p!("[val: sound(\x07)]" =>
[func!("val": (named_tuple!("sound")), {})], [par![func!("val": (named_tuple!("sound")), {})]],
[(0:12, 0:13, "expected value, found invalid token")], [(0:12, 0:13, "expected value, found invalid token")],
); );
// Invalid tuple name. // Invalid tuple name.
p!("[val: 👠(\"abc\", 13e-5)]" => p!("[val: 👠(\"abc\", 13e-5)]" =>
[func!("val": (tuple!(Str("abc"), Num(13.0e-5))), {})], [par![func!("val": (tuple!(Str("abc"), Num(13.0e-5))), {})]],
[(0:6, 0:7, "expected argument, found invalid token")], [(0:6, 0:7, "expected argument, found invalid token")],
); );
// Unclosed tuple. // Unclosed tuple.
p!("[val: lang(中文]" => p!("[val: lang(中文]" =>
[func!("val": (named_tuple!("lang", Id("中文"))), {})], [par![func!("val": (named_tuple!("lang", Id("中文"))), {})]],
[(0:13, 0:13, "expected closing paren")], [(0:13, 0:13, "expected closing paren")],
); );
@ -1059,18 +1079,18 @@ mod tests {
// Invalid commas. // Invalid commas.
p!("[val: (,)]" => p!("[val: (,)]" =>
[func!("val": (tuple!()), {})], [par![func!("val": (tuple!()), {})]],
[(0:7, 0:8, "expected value, found comma")], [(0:7, 0:8, "expected value, found comma")],
); );
p!("[val: (true false)]" => p!("[val: (true false)]" =>
[func!("val": (tuple!(Bool(true), Bool(false))), {})], [par![func!("val": (tuple!(Bool(true), Bool(false))), {})]],
[(0:11, 0:11, "expected comma")], [(0:11, 0:11, "expected comma")],
); );
} }
#[test] #[test]
fn parse_objects() { fn parse_objects() {
let val = || func!("val": (object! {}), {}); let val = || par![func!("val": (object! {}), {})];
// Okay objects. // Okay objects.
pval!("{}" => (object! {})); pval!("{}" => (object! {}));
@ -1078,11 +1098,11 @@ mod tests {
// Unclosed object. // Unclosed object.
p!("[val: {hello: world]" => p!("[val: {hello: world]" =>
[func!("val": (object! { "hello" => Id("world") }), {})], [par![func!("val": (object! { "hello" => Id("world") }), {})]],
[(0:19, 0:19, "expected closing brace")], [(0:19, 0:19, "expected closing brace")],
); );
p!("[val: { a]" => p!("[val: { a]" =>
[func!("val": (object! {}), {})], [par![func!("val": (object! {}), {})]],
[(0:9, 0:9, "expected colon"), (0:9, 0:9, "expected closing brace")], [(0:9, 0:9, "expected colon"), (0:9, 0:9, "expected closing brace")],
); );
@ -1098,25 +1118,25 @@ mod tests {
(0:12, 0:17, "expected key, found bool"), (0:12, 0:17, "expected key, found bool"),
]); ]);
p!("[val: { a b:c }]" => p!("[val: { a b:c }]" =>
[func!("val": (object! { "b" => Id("c") }), {})], [par![func!("val": (object! { "b" => Id("c") }), {})]],
[(0:9, 0:9, "expected colon")], [(0:9, 0:9, "expected colon")],
); );
// Missing value. // Missing value.
p!("[val: { key: : }]" => [val()], [(0:13, 0:14, "expected value, found colon")]); p!("[val: { key: : }]" => [val()], [(0:13, 0:14, "expected value, found colon")]);
p!("[val: { key: , k: \"s\" }]" => p!("[val: { key: , k: \"s\" }]" =>
[func!("val": (object! { "k" => Str("s") }), {})], [par![func!("val": (object! { "k" => Str("s") }), {})]],
[(0:13, 0:14, "expected value, found comma")], [(0:13, 0:14, "expected value, found comma")],
); );
// Missing comma, invalid token. // Missing comma, invalid token.
p!("[val: left={ a: 2, b: false 🌎 }]" => p!("[val: left={ a: 2, b: false 🌎 }]" =>
[func!("val": (), { [par![func!("val": (), {
"left" => object! { "left" => object! {
"a" => Num(2.0), "a" => Num(2.0),
"b" => Bool(false), "b" => Bool(false),
} }
})], })]],
[(0:27, 0:27, "expected comma"), [(0:27, 0:27, "expected comma"),
(0:28, 0:29, "expected key, found invalid token")], (0:28, 0:29, "expected key, found invalid token")],
); );
@ -1140,19 +1160,19 @@ mod tests {
fn parse_one_keyword_argument() { fn parse_one_keyword_argument() {
// Correct // Correct
p!("[val: x=true]" => p!("[val: x=true]" =>
[func!("val": (), { "x" => Bool(true) })], [], [par![func!("val": (), { "x" => Bool(true) })]], [],
[(0:6, 0:7, ArgumentKey), (0:1, 0:4, ResolvedFunc)], [(0:6, 0:7, ArgumentKey), (0:1, 0:4, ResolvedFunc)],
); );
// Spacing around keyword arguments // Spacing around keyword arguments
p!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" => p!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" =>
[S, func!("val": (), { "hi" => Str("s\n") })], [], [par![S, func!("val": (), { "hi" => Str("s\n") })]], [],
[(2:1, 2:3, ArgumentKey), (1:2, 1:5, ResolvedFunc)], [(2:1, 2:3, ArgumentKey), (1:2, 1:5, ResolvedFunc)],
); );
// Missing value // Missing value
p!("[val: x=]" => p!("[val: x=]" =>
[func!("val")], [par![func!("val")]],
[(0:8, 0:8, "expected value")], [(0:8, 0:8, "expected value")],
[(0:6, 0:7, ArgumentKey), (0:1, 0:4, ResolvedFunc)], [(0:6, 0:7, ArgumentKey), (0:1, 0:4, ResolvedFunc)],
); );
@ -1161,7 +1181,8 @@ mod tests {
#[test] #[test]
fn parse_multiple_mixed_arguments() { fn parse_multiple_mixed_arguments() {
p!("[val: 12pt, key=value]" => p!("[val: 12pt, key=value]" =>
[func!("val": (Len(Length::pt(12.0))), { "key" => Id("value") })], [], [par![func!("val": (Len(Length::pt(12.0))), { "key" => Id("value") })]],
[],
[(0:12, 0:15, ArgumentKey), (0:1, 0:4, ResolvedFunc)], [(0:12, 0:15, ArgumentKey), (0:1, 0:4, ResolvedFunc)],
); );
pval!("a , x=\"b\" , c" => (Id("a"), Id("c")), { "x" => Str("b") }); pval!("a , x=\"b\" , c" => (Id("a"), Id("c")), { "x" => Str("b") });
@ -1169,15 +1190,15 @@ mod tests {
#[test] #[test]
fn parse_invalid_values() { fn parse_invalid_values() {
p!("[val: )]" => [func!("val")], [(0:6, 0:7, "expected argument, found closing paren")]); p!("[val: )]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found closing paren")]);
p!("[val: }]" => [func!("val")], [(0:6, 0:7, "expected argument, found closing brace")]); p!("[val: }]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found closing brace")]);
p!("[val: :]" => [func!("val")], [(0:6, 0:7, "expected argument, found colon")]); p!("[val: :]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found colon")]);
p!("[val: ,]" => [func!("val")], [(0:6, 0:7, "expected argument, found comma")]); p!("[val: ,]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found comma")]);
p!("[val: =]" => [func!("val")], [(0:6, 0:7, "expected argument, found equals sign")]); p!("[val: =]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found equals sign")]);
p!("[val: 🌎]" => [func!("val")], [(0:6, 0:7, "expected argument, found invalid token")]); p!("[val: 🌎]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found invalid token")]);
p!("[val: 12ept]" => [func!("val")], [(0:6, 0:11, "expected argument, found invalid token")]); p!("[val: 12ept]" => [par![func!("val")]], [(0:6, 0:11, "expected argument, found invalid token")]);
p!("[val: [hi]]" => p!("[val: [hi]]" =>
[func!("val")], [par![func!("val")]],
[(0:6, 0:10, "expected argument, found function")], [(0:6, 0:10, "expected argument, found function")],
[(0:1, 0:4, ResolvedFunc)], [(0:1, 0:4, ResolvedFunc)],
); );
@ -1187,7 +1208,7 @@ mod tests {
fn parse_invalid_key_value_pairs() { fn parse_invalid_key_value_pairs() {
// Invalid keys. // Invalid keys.
p!("[val: true=you]" => p!("[val: true=you]" =>
[func!("val": (Bool(true), Id("you")), {})], [par![func!("val": (Bool(true), Id("you")), {})]],
[(0:10, 0:10, "expected comma"), [(0:10, 0:10, "expected comma"),
(0:10, 0:11, "expected argument, found equals sign")], (0:10, 0:11, "expected argument, found equals sign")],
[(0:1, 0:4, ResolvedFunc)], [(0:1, 0:4, ResolvedFunc)],
@ -1195,21 +1216,22 @@ mod tests {
// Unexpected equals. // Unexpected equals.
p!("[box: z=y=4]" => p!("[box: z=y=4]" =>
[func!("box": (Num(4.0)), { "z" => Id("y") })], [par![func!("box": (Num(4.0)), { "z" => Id("y") })]],
[(0:9, 0:9, "expected comma"), [(0:9, 0:9, "expected comma"),
(0:9, 0:10, "expected argument, found equals sign")], (0:9, 0:10, "expected argument, found equals sign")],
); );
// Invalid colon after keyable positional argument. // Invalid colon after keyable positional argument.
p!("[val: key:12]" => p!("[val: key:12]" =>
[func!("val": (Id("key"), Num(12.0)), {})], [par![func!("val": (Id("key"), Num(12.0)), {})]],
[(0:9, 0:9, "expected comma"), [(0:9, 0:9, "expected comma"),
(0:9, 0:10, "expected argument, found colon")], (0:9, 0:10, "expected argument, found colon")],
[(0:1, 0:4, ResolvedFunc)], [(0:1, 0:4, ResolvedFunc)],
); );
// Invalid colon after unkeyable positional argument. // Invalid colon after unkeyable positional argument.
p!("[val: true:12]" => [func!("val": (Bool(true), Num(12.0)), {})], p!("[val: true:12]" =>
[par![func!("val": (Bool(true), Num(12.0)), {})]],
[(0:10, 0:10, "expected comma"), [(0:10, 0:10, "expected comma"),
(0:10, 0:11, "expected argument, found colon")], (0:10, 0:11, "expected argument, found colon")],
[(0:1, 0:4, ResolvedFunc)], [(0:1, 0:4, ResolvedFunc)],
@ -1220,33 +1242,33 @@ mod tests {
fn parse_invalid_commas() { fn parse_invalid_commas() {
// Missing commas. // Missing commas.
p!("[val: 1pt 1]" => p!("[val: 1pt 1]" =>
[func!("val": (Len(Length::pt(1.0)), Num(1.0)), {})], [par![func!("val": (Len(Length::pt(1.0)), Num(1.0)), {})]],
[(0:9, 0:9, "expected comma")], [(0:9, 0:9, "expected comma")],
); );
p!(r#"[val: _"s"]"# => p!(r#"[val: _"s"]"# =>
[func!("val": (Id("_"), Str("s")), {})], [par![func!("val": (Id("_"), Str("s")), {})]],
[(0:7, 0:7, "expected comma")], [(0:7, 0:7, "expected comma")],
); );
// Unexpected commas. // Unexpected commas.
p!("[val:,]" => [func!("val")], [(0:5, 0:6, "expected argument, found comma")]); p!("[val:,]" => [par![func!("val")]], [(0:5, 0:6, "expected argument, found comma")]);
p!("[val: key=,]" => [func!("val")], [(0:10, 0:11, "expected value, found comma")]); p!("[val: key=,]" => [par![func!("val")]], [(0:10, 0:11, "expected value, found comma")]);
p!("[val:, true]" => p!("[val:, true]" =>
[func!("val": (Bool(true)), {})], [par![func!("val": (Bool(true)), {})]],
[(0:5, 0:6, "expected argument, found comma")], [(0:5, 0:6, "expected argument, found comma")],
); );
} }
#[test] #[test]
fn parse_bodies() { fn parse_bodies() {
p!("[val][Hi]" => [func!("val"; [T("Hi")])]); p!("[val][Hi]" => [par![func!("val"; [par![T("Hi")]])]]);
p!("[val:*][*Hi*]" => p!("[val:*][*Hi*]" =>
[func!("val"; [Bold, T("Hi"), Bold])], [par![func!("val"; [par![Bold, T("Hi"), Bold]])]],
[(0:5, 0:6, "expected argument, found star")], [(0:5, 0:6, "expected argument, found star")],
); );
// Errors in bodies. // Errors in bodies.
p!(" [val][ */ ]" => p!(" [val][ */ ]" =>
[S, func!("val"; [S, S])], [par![S, func!("val"; [par![S, S]])]],
[(0:8, 0:10, "unexpected end of block comment")], [(0:8, 0:10, "unexpected end of block comment")],
); );
} }
@ -1255,40 +1277,47 @@ mod tests {
fn parse_spanned_functions() { fn parse_spanned_functions() {
// Space before function // Space before function
p!(" [val]" => p!(" [val]" =>
[(0:0, 0:1, S), (0:1, 0:6, func!((0:1, 0:4, "val")))], [], [(0:0, 0:6, par![(0:0, 0:1, S), (0:1, 0:6, func!((0:1, 0:4, "val")))])],
[],
[(0:2, 0:5, ResolvedFunc)], [(0:2, 0:5, ResolvedFunc)],
); );
// Newline before function // Newline before function
p!(" \n\r\n[val]" => p!("a \n\r\n[val]" =>
[(0:0, 2:0, Parbreak), (2:0, 2:5, func!((0:1, 0:4, "val")))], [], [
(0:0, 0:1, par![(0:0, 0:1, T("a"))]),
(2:0, 2:5, par![(2:0, 2:5, func!((0:1, 0:4, "val")))]),
],
[],
[(2:1, 2:4, ResolvedFunc)], [(2:1, 2:4, ResolvedFunc)],
); );
// Content before function // Content before function
p!("hello [val][world] 🌎" => p!("hello [val][world] 🌎" =>
[ [(0:0, 0:20, par![
(0:0, 0:5, T("hello")), (0:0, 0:5, T("hello")),
(0:5, 0:6, S), (0:5, 0:6, S),
(0:6, 0:18, func!((0:1, 0:4, "val"); [(0:6, 0:11, T("world"))])), (0:6, 0:18, func!((0:1, 0:4, "val"); [
(0:6, 0:11, par![(0:6, 0:11, T("world"))])
])),
(0:18, 0:19, S), (0:18, 0:19, S),
(0:19, 0:20, T("🌎")) (0:19, 0:20, T("🌎"))
], ])],
[], [],
[(0:7, 0:10, ResolvedFunc)], [(0:7, 0:10, ResolvedFunc)],
); );
// Nested function // Nested function
p!(" [val][\nbody[ box]\n ]" => p!(" [val][\nbody[ box]\n ]" =>
[ [(0:0, 2:2, par![
(0:0, 0:1, S), (0:0, 0:1, S),
(0:1, 2:2, func!((0:1, 0:4, "val"); [ (0:1, 2:2, func!((0:1, 0:4, "val"); [(0:6, 2:1, par![
(0:6, 1:0, S), (0:6, 1:0, S),
(1:0, 1:4, T("body")), (1:0, 1:4, T("body")),
(1:4, 1:10, func!((0:2, 0:5, "box"))), (1:4, 1:10, func!((0:2, 0:5, "box"))),
(1:10, 2:1, S), (1:10, 2:1, S),
])) ])]))
], ])],
[], [],
[(0:2, 0:5, ResolvedFunc), (1:6, 1:9, ResolvedFunc)], [(0:2, 0:5, ResolvedFunc), (1:6, 1:9, ResolvedFunc)],
); );

View File

@ -86,22 +86,23 @@ pub trait SpanlessEq<Rhs = Self> {
} }
impl SpanlessEq for SyntaxNode { impl SpanlessEq for SyntaxNode {
fn spanless_eq(&self, other: &SyntaxNode) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
fn downcast<'a>(func: &'a (dyn DynamicNode + 'static)) -> &'a DebugFn { fn downcast<'a>(func: &'a (dyn DynamicNode + 'static)) -> &'a DebugFn {
func.downcast::<DebugFn>().expect("not a debug fn") func.downcast::<DebugFn>().expect("not a debug fn")
} }
match (self, other) { match (self, other) {
(SyntaxNode::Dyn(a), SyntaxNode::Dyn(b)) => { (Self::Dyn(a), Self::Dyn(b)) => {
downcast(a.as_ref()).spanless_eq(downcast(b.as_ref())) downcast(a.as_ref()).spanless_eq(downcast(b.as_ref()))
} }
(Self::Par(a), Self::Par(b)) => a.spanless_eq(b),
(a, b) => a == b, (a, b) => a == b,
} }
} }
} }
impl SpanlessEq for DebugFn { impl SpanlessEq for DebugFn {
fn spanless_eq(&self, other: &DebugFn) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
self.header.spanless_eq(&other.header) self.header.spanless_eq(&other.header)
&& self.body.spanless_eq(&other.body) && self.body.spanless_eq(&other.body)
} }
@ -132,7 +133,7 @@ impl SpanlessEq for FuncArg {
} }
impl SpanlessEq for Expr { impl SpanlessEq for Expr {
fn spanless_eq(&self, other: &Expr) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
match (self, other) { match (self, other) {
(Expr::Tuple(a), Expr::Tuple(b)) => a.spanless_eq(b), (Expr::Tuple(a), Expr::Tuple(b)) => a.spanless_eq(b),
(Expr::NamedTuple(a), Expr::NamedTuple(b)) => a.spanless_eq(b), (Expr::NamedTuple(a), Expr::NamedTuple(b)) => a.spanless_eq(b),
@ -148,20 +149,20 @@ impl SpanlessEq for Expr {
} }
impl SpanlessEq for Tuple { impl SpanlessEq for Tuple {
fn spanless_eq(&self, other: &Tuple) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
self.0.spanless_eq(&other.0) self.0.spanless_eq(&other.0)
} }
} }
impl SpanlessEq for NamedTuple { impl SpanlessEq for NamedTuple {
fn spanless_eq(&self, other: &NamedTuple) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
self.name.v == other.name.v self.name.v == other.name.v
&& self.tuple.v.spanless_eq(&other.tuple.v) && self.tuple.v.spanless_eq(&other.tuple.v)
} }
} }
impl SpanlessEq for Object { impl SpanlessEq for Object {
fn spanless_eq(&self, other: &Object) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
self.0.spanless_eq(&other.0) self.0.spanless_eq(&other.0)
} }
} }
@ -173,20 +174,20 @@ impl SpanlessEq for Pair {
} }
impl<T: SpanlessEq> SpanlessEq for Vec<T> { impl<T: SpanlessEq> SpanlessEq for Vec<T> {
fn spanless_eq(&self, other: &Vec<T>) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
self.len() == other.len() self.len() == other.len()
&& self.iter().zip(other).all(|(x, y)| x.spanless_eq(&y)) && self.iter().zip(other).all(|(x, y)| x.spanless_eq(&y))
} }
} }
impl<T: SpanlessEq> SpanlessEq for Spanned<T> { impl<T: SpanlessEq> SpanlessEq for Spanned<T> {
fn spanless_eq(&self, other: &Spanned<T>) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
self.v.spanless_eq(&other.v) self.v.spanless_eq(&other.v)
} }
} }
impl<T: SpanlessEq> SpanlessEq for Box<T> { impl<T: SpanlessEq> SpanlessEq for Box<T> {
fn spanless_eq(&self, other: &Box<T>) -> bool { fn spanless_eq(&self, other: &Self) -> bool {
(&**self).spanless_eq(&**other) (&**self).spanless_eq(&**other)
} }
} }

View File

@ -14,20 +14,20 @@ pub type SyntaxTree = SpanVec<SyntaxNode>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum SyntaxNode { pub enum SyntaxNode {
/// Whitespace containing less than two newlines. /// Whitespace containing less than two newlines.
Space, Spacing,
/// Whitespace with more than two newlines.
Parbreak,
/// A forced line break. /// A forced line break.
Linebreak, Linebreak,
/// Plain text.
Text(String),
/// Lines of raw text.
Raw(Vec<String>),
/// Italics were enabled / disabled. /// Italics were enabled / disabled.
ToggleItalic, ToggleItalic,
/// Bolder was enabled / disabled. /// Bolder was enabled / disabled.
ToggleBolder, ToggleBolder,
/// A dynamic node, create through function invocations in source code. /// Plain text.
Text(String),
/// Lines of raw text.
Raw(Vec<String>),
/// A paragraph of child nodes.
Par(SyntaxTree),
/// A dynamic node, created through function invocations in source code.
Dyn(Box<dyn DynamicNode>), Dyn(Box<dyn DynamicNode>),
} }
@ -35,13 +35,13 @@ impl PartialEq for SyntaxNode {
fn eq(&self, other: &SyntaxNode) -> bool { fn eq(&self, other: &SyntaxNode) -> bool {
use SyntaxNode::*; use SyntaxNode::*;
match (self, other) { match (self, other) {
(Space, Space) => true, (Spacing, Spacing) => true,
(Parbreak, Parbreak) => true,
(Linebreak, Linebreak) => true, (Linebreak, Linebreak) => true,
(Text(a), Text(b)) => a == b,
(Raw(a), Raw(b)) => a == b,
(ToggleItalic, ToggleItalic) => true, (ToggleItalic, ToggleItalic) => true,
(ToggleBolder, ToggleBolder) => true, (ToggleBolder, ToggleBolder) => true,
(Text(a), Text(b)) => a == b,
(Raw(a), Raw(b)) => a == b,
(Par(a), Par(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b, (Dyn(a), Dyn(b)) => a == b,
_ => false, _ => false,
} }