diff options
40 files changed, 626 insertions, 588 deletions
diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs index 41a6bc35..8b148c85 100644 --- a/library/src/compute/foundations.rs +++ b/library/src/compute/foundations.rs @@ -53,7 +53,7 @@ pub fn repr( /// Fail with an error. /// /// ## Example -/// The code below produces the error `panicked at: "this is wrong"`. +/// The code below produces the error `panicked with: "this is wrong"`. /// ```typ /// #panic("this is wrong") /// ``` @@ -63,14 +63,21 @@ pub fn repr( /// Returns: #[func] pub fn panic( - /// The value (or message) to panic with. - #[default] - payload: Option<Value>, + /// The values to panic with. + #[variadic] + values: Vec<Value>, ) -> Value { - match payload { - Some(v) => bail!(args.span, "panicked with: {}", v.repr()), - None => bail!(args.span, "panicked"), + let mut msg = EcoString::from("panicked"); + if !values.is_empty() { + msg.push_str(" with: "); + for (i, value) in values.iter().enumerate() { + if i > 0 { + msg.push_str(", "); + } + msg.push_str(&value.repr()); + } } + bail!(args.span, msg); } /// Ensure that a condition is fulfilled. diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs index cf6d08f2..2a3998bf 100644 --- a/library/src/layout/align.rs +++ b/library/src/layout/align.rs @@ -58,7 +58,7 @@ pub struct AlignNode { } impl Show for AlignNode { - fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { Ok(self .body() .styled(Self::set_alignment(self.alignment(styles).map(Some)))) diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index 7a063bce..2671b983 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -32,7 +32,7 @@ impl Layout for FlowNode { let outer = styles; let mut styles = outer; if let Some(node) = child.to::<StyledNode>() { - map = node.map(); + map = node.styles(); styles = outer.chain(&map); child = node.body(); } @@ -48,15 +48,15 @@ impl Layout for FlowNode { || child.is::<ImageNode>() { layouter.layout_single(vt, &child, styles)?; - } else if child.has::<dyn Layout>() { + } else if child.can::<dyn Layout>() { layouter.layout_multiple(vt, &child, styles)?; } else if child.is::<ColbreakNode>() { if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() { layouter.finish_region(); } - } else if let Some(span) = child.span() { - bail!(span, "unexpected flow child"); + } else { + bail!(child.span(), "unexpected flow child"); } } @@ -207,7 +207,7 @@ impl<'a> FlowLayouter<'a> { let aligns = if let Some(align) = block.to::<AlignNode>() { align.alignment(styles) } else if let Some(styled) = block.to::<StyledNode>() { - AlignNode::alignment_in(styles.chain(&styled.map())) + AlignNode::alignment_in(styles.chain(&styled.styles())) } else { AlignNode::alignment_in(styles) } diff --git a/library/src/layout/hide.rs b/library/src/layout/hide.rs index 62628445..e939e6c3 100644 --- a/library/src/layout/hide.rs +++ b/library/src/layout/hide.rs @@ -23,7 +23,7 @@ pub struct HideNode { } impl Show for HideNode { - fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { Ok(self.body().styled(MetaNode::set_data(vec![Meta::Hidden]))) } } diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index e846f6f0..dc373ff5 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -148,7 +148,7 @@ fn realize_root<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<(Content, StyleChain<'a>)> { - if content.has::<dyn LayoutRoot>() && !applicable(content, styles) { + if content.can::<dyn LayoutRoot>() && !applicable(content, styles) { return Ok((content.clone(), styles)); } @@ -166,7 +166,7 @@ fn realize_block<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<(Content, StyleChain<'a>)> { - if content.has::<dyn Layout>() + if content.can::<dyn Layout>() && !content.is::<RectNode>() && !content.is::<SquareNode>() && !content.is::<EllipseNode>() @@ -227,16 +227,20 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { mut content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<()> { - if content.has::<dyn LayoutMath>() && !content.is::<FormulaNode>() { + if content.can::<dyn LayoutMath>() && !content.is::<FormulaNode>() { content = self.scratch.content.alloc(FormulaNode::new(content.clone()).pack()); } // Prepare only if this is the first application for this node. - if let Some(node) = content.with::<dyn Prepare>() { + if content.can::<dyn Prepare>() { if !content.is_prepared() { - let prepared = - node.prepare(self.vt, content.clone().prepared(), styles)?; + let prepared = content + .clone() + .prepared() + .with::<dyn Prepare>() + .unwrap() + .prepare(self.vt, styles)?; let stored = self.scratch.content.alloc(prepared); return self.accept(stored, styles); } @@ -291,11 +295,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { } } - if let Some(span) = content.span() { - bail!(span, "not allowed here"); - } - - Ok(()) + bail!(content.span(), "not allowed here"); } fn styled( @@ -303,7 +303,7 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> { styled: &'a StyledNode, styles: StyleChain<'a>, ) -> SourceResult<()> { - let map = self.scratch.maps.alloc(styled.map()); + let map = self.scratch.maps.alloc(styled.styles()); let stored = self.scratch.styles.alloc(styles); let content = self.scratch.content.alloc(styled.body()); let styles = stored.chain(map); @@ -436,7 +436,7 @@ impl<'a> FlowBuilder<'a> { return true; } - if content.has::<dyn Layout>() || content.is::<ParNode>() { + if content.can::<dyn Layout>() || content.is::<ParNode>() { let is_tight_list = if let Some(node) = content.to::<ListNode>() { node.tight(styles) } else if let Some(node) = content.to::<EnumNode>() { diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index e5644a2e..8dd81d29 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -519,7 +519,7 @@ fn collect<'a>( let mut styles = *styles; if let Some(node) = child.to::<StyledNode>() { child = Box::leak(Box::new(node.body())); - styles = outer.chain(Box::leak(Box::new(node.map()))); + styles = outer.chain(Box::leak(Box::new(node.styles()))); } let segment = if child.is::<SpaceNode>() { @@ -570,10 +570,8 @@ fn collect<'a>( let frac = node.width(styles).is_fractional(); full.push(if frac { SPACING_REPLACE } else { NODE_REPLACE }); Segment::Box(node, frac) - } else if let Some(span) = child.span() { - bail!(span, "unexpected document child"); } else { - continue; + bail!(child.span(), "unexpected paragraph child"); }; if let Some(last) = full.chars().last() { @@ -730,7 +728,7 @@ fn shared_get<'a, T: PartialEq>( children .iter() .filter_map(|child| child.to::<StyledNode>()) - .all(|node| getter(styles.chain(&node.map())) == value) + .all(|node| getter(styles.chain(&node.styles())) == value) .then(|| value) } diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs index c21fa884..1dd81a60 100644 --- a/library/src/layout/stack.rs +++ b/library/src/layout/stack.rs @@ -201,7 +201,7 @@ impl<'a> StackLayouter<'a> { let aligns = if let Some(align) = block.to::<AlignNode>() { align.alignment(styles) } else if let Some(styled) = block.to::<StyledNode>() { - AlignNode::alignment_in(styles.chain(&styled.map())) + AlignNode::alignment_in(styles.chain(&styled.styles())) } else { AlignNode::alignment_in(styles) } diff --git a/library/src/lib.rs b/library/src/lib.rs index ad8a2ac4..94b3e0c4 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -29,65 +29,65 @@ fn global(math: Module, calc: Module) -> Module { let mut global = Scope::deduplicating(); // Text. - global.define("text", text::TextNode::func()); - global.define("linebreak", text::LinebreakNode::func()); - global.define("smartquote", text::SmartQuoteNode::func()); - global.define("strong", text::StrongNode::func()); - global.define("emph", text::EmphNode::func()); + global.define("text", text::TextNode::id()); + global.define("linebreak", text::LinebreakNode::id()); + global.define("smartquote", text::SmartQuoteNode::id()); + global.define("strong", text::StrongNode::id()); + global.define("emph", text::EmphNode::id()); global.define("lower", text::lower); global.define("upper", text::upper); global.define("smallcaps", text::smallcaps); - global.define("sub", text::SubNode::func()); - global.define("super", text::SuperNode::func()); - global.define("underline", text::UnderlineNode::func()); - global.define("strike", text::StrikeNode::func()); - global.define("overline", text::OverlineNode::func()); - global.define("raw", text::RawNode::func()); + global.define("sub", text::SubNode::id()); + global.define("super", text::SuperNode::id()); + global.define("underline", text::UnderlineNode::id()); + global.define("strike", text::StrikeNode::id()); + global.define("overline", text::OverlineNode::id()); + global.define("raw", text::RawNode::id()); global.define("lorem", text::lorem); // Math. global.define("math", math); // Layout. - global.define("page", layout::PageNode::func()); - global.define("pagebreak", layout::PagebreakNode::func()); - global.define("v", layout::VNode::func()); - global.define("par", layout::ParNode::func()); - global.define("parbreak", layout::ParbreakNode::func()); - global.define("h", layout::HNode::func()); - global.define("box", layout::BoxNode::func()); - global.define("block", layout::BlockNode::func()); - global.define("list", layout::ListNode::func()); - global.define("enum", layout::EnumNode::func()); - global.define("terms", layout::TermsNode::func()); - global.define("table", layout::TableNode::func()); - global.define("stack", layout::StackNode::func()); - global.define("grid", layout::GridNode::func()); - global.define("columns", layout::ColumnsNode::func()); - global.define("colbreak", layout::ColbreakNode::func()); - global.define("place", layout::PlaceNode::func()); - global.define("align", layout::AlignNode::func()); - global.define("pad", layout::PadNode::func()); - global.define("repeat", layout::RepeatNode::func()); - global.define("move", layout::MoveNode::func()); - global.define("scale", layout::ScaleNode::func()); - global.define("rotate", layout::RotateNode::func()); - global.define("hide", layout::HideNode::func()); + global.define("page", layout::PageNode::id()); + global.define("pagebreak", layout::PagebreakNode::id()); + global.define("v", layout::VNode::id()); + global.define("par", layout::ParNode::id()); + global.define("parbreak", layout::ParbreakNode::id()); + global.define("h", layout::HNode::id()); + global.define("box", layout::BoxNode::id()); + global.define("block", layout::BlockNode::id()); + global.define("list", layout::ListNode::id()); + global.define("enum", layout::EnumNode::id()); + global.define("terms", layout::TermsNode::id()); + global.define("table", layout::TableNode::id()); + global.define("stack", layout::StackNode::id()); + global.define("grid", layout::GridNode::id()); + global.define("columns", layout::ColumnsNode::id()); + global.define("colbreak", layout::ColbreakNode::id()); + global.define("place", layout::PlaceNode::id()); + global.define("align", layout::AlignNode::id()); + global.define("pad", layout::PadNode::id()); + global.define("repeat", layout::RepeatNode::id()); + global.define("move", layout::MoveNode::id()); + global.define("scale", layout::ScaleNode::id()); + global.define("rotate", layout::RotateNode::id()); + global.define("hide", layout::HideNode::id()); // Visualize. - global.define("image", visualize::ImageNode::func()); - global.define("line", visualize::LineNode::func()); - global.define("rect", visualize::RectNode::func()); - global.define("square", visualize::SquareNode::func()); - global.define("ellipse", visualize::EllipseNode::func()); - global.define("circle", visualize::CircleNode::func()); + global.define("image", visualize::ImageNode::id()); + global.define("line", visualize::LineNode::id()); + global.define("rect", visualize::RectNode::id()); + global.define("square", visualize::SquareNode::id()); + global.define("ellipse", visualize::EllipseNode::id()); + global.define("circle", visualize::CircleNode::id()); // Meta. - global.define("document", meta::DocumentNode::func()); - global.define("ref", meta::RefNode::func()); - global.define("link", meta::LinkNode::func()); - global.define("outline", meta::OutlineNode::func()); - global.define("heading", meta::HeadingNode::func()); + global.define("document", meta::DocumentNode::id()); + global.define("ref", meta::RefNode::id()); + global.define("link", meta::LinkNode::id()); + global.define("outline", meta::OutlineNode::id()); + global.define("heading", meta::HeadingNode::id()); global.define("numbering", meta::numbering); // Symbols. diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 8f89e028..c11bea79 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -47,52 +47,52 @@ use crate::text::{ /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); - math.define("formula", FormulaNode::func()); - math.define("text", TextNode::func()); + math.define("formula", FormulaNode::id()); + math.define("text", TextNode::id()); // Grouping. - math.define("lr", LrNode::func()); + math.define("lr", LrNode::id()); math.define("abs", abs); math.define("norm", norm); math.define("floor", floor); math.define("ceil", ceil); // Attachments and accents. - math.define("attach", AttachNode::func()); - math.define("scripts", ScriptsNode::func()); - math.define("limits", LimitsNode::func()); - math.define("accent", AccentNode::func()); - math.define("underline", UnderlineNode::func()); - math.define("overline", OverlineNode::func()); - math.define("underbrace", UnderbraceNode::func()); - math.define("overbrace", OverbraceNode::func()); - math.define("underbracket", UnderbracketNode::func()); - math.define("overbracket", OverbracketNode::func()); + math.define("attach", AttachNode::id()); + math.define("scripts", ScriptsNode::id()); + math.define("limits", LimitsNode::id()); + math.define("accent", AccentNode::id()); + math.define("underline", UnderlineNode::id()); + math.define("overline", OverlineNode::id()); + math.define("underbrace", UnderbraceNode::id()); + math.define("overbrace", OverbraceNode::id()); + math.define("underbracket", UnderbracketNode::id()); + math.define("overbracket", OverbracketNode::id()); // Fractions and matrix-likes. - math.define("frac", FracNode::func()); - math.define("binom", BinomNode::func()); - math.define("vec", VecNode::func()); - math.define("mat", MatNode::func()); - math.define("cases", CasesNode::func()); + math.define("frac", FracNode::id()); + math.define("binom", BinomNode::id()); + math.define("vec", VecNode::id()); + math.define("mat", MatNode::id()); + math.define("cases", CasesNode::id()); // Roots. - math.define("sqrt", SqrtNode::func()); - math.define("root", RootNode::func()); + math.define("sqrt", SqrtNode::id()); + math.define("root", RootNode::id()); // Styles. - math.define("upright", UprightNode::func()); - math.define("bold", BoldNode::func()); - math.define("italic", ItalicNode::func()); - math.define("serif", SerifNode::func()); - math.define("sans", SansNode::func()); - math.define("cal", CalNode::func()); - math.define("frak", FrakNode::func()); - math.define("mono", MonoNode::func()); - math.define("bb", BbNode::func()); + math.define("upright", UprightNode::id()); + math.define("bold", BoldNode::id()); + math.define("italic", ItalicNode::id()); + math.define("serif", SerifNode::id()); + math.define("sans", SansNode::id()); + math.define("cal", CalNode::id()); + math.define("frak", FrakNode::id()); + math.define("mono", MonoNode::id()); + math.define("bb", BbNode::id()); // Text operators. - math.define("op", OpNode::func()); + math.define("op", OpNode::id()); op::define(&mut math); // Spacings. @@ -144,7 +144,7 @@ pub struct FormulaNode { } impl Show for FormulaNode { - fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let mut realized = self.clone().pack().guarded(Guard::Base(NodeId::of::<Self>())); if self.block(styles) { realized = realized.aligned(Axes::with_x(Some(Align::Center.into()))) @@ -183,10 +183,7 @@ impl Layout for FormulaNode { Some(font) }) else { - if let Some(span) = self.span() { - bail!(span, "current font does not support math"); - } - return Ok(Fragment::frame(Frame::new(Size::zero()))) + bail!(self.span(), "current font does not support math"); }; let mut ctx = MathContext::new(vt, styles, regions, &font, block); @@ -228,7 +225,7 @@ impl LayoutMath for Content { } if let Some(styled) = self.to::<StyledNode>() { - let map = styled.map(); + let map = styled.styles(); if TextNode::font_in(ctx.styles().chain(&map)) != TextNode::font_in(ctx.styles()) { diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs index 8da3c731..ae29e7a8 100644 --- a/library/src/meta/document.rs +++ b/library/src/meta/document.rs @@ -39,7 +39,7 @@ impl LayoutRoot for DocumentNode { let outer = styles; let mut styles = outer; if let Some(node) = child.to::<StyledNode>() { - map = node.map(); + map = node.styles(); styles = outer.chain(&map); child = node.body(); } @@ -48,8 +48,8 @@ impl LayoutRoot for DocumentNode { let number = 1 + pages.len(); let fragment = page.layout(vt, number, styles)?; pages.extend(fragment); - } else if let Some(span) = child.span() { - bail!(span, "unexpected document child"); + } else { + bail!(child.span(), "unexpected document child"); } } diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index 3a8d811c..8677aa55 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -79,13 +79,8 @@ pub struct HeadingNode { } impl Prepare for HeadingNode { - fn prepare( - &self, - vt: &mut Vt, - mut this: Content, - styles: StyleChain, - ) -> SourceResult<Content> { - let my_id = vt.identify(&this); + fn prepare(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let my_id = vt.identify(self); let mut counter = HeadingCounter::new(); for (node_id, node) in vt.locate(Selector::node::<HeadingNode>()) { @@ -105,18 +100,18 @@ impl Prepare for HeadingNode { numbers = numbering.apply(vt.world(), counter.advance(self))?; } - this.push_field("outlined", Value::Bool(self.outlined(styles))); - this.push_field("numbers", numbers); - - let meta = Meta::Node(my_id, this.clone()); - Ok(this.styled(MetaNode::set_data(vec![meta]))) + let mut node = self.clone().pack(); + node.push_field("outlined", Value::Bool(self.outlined(styles))); + node.push_field("numbers", numbers); + let meta = Meta::Node(my_id, node.clone()); + Ok(node.styled(MetaNode::set_data(vec![meta]))) } } impl Show for HeadingNode { - fn show(&self, _: &mut Vt, this: &Content, _: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { let mut realized = self.body(); - let numbers = this.field("numbers").unwrap(); + let numbers = self.0.field("numbers").unwrap(); if *numbers != Value::None { realized = numbers.clone().display() + HNode::new(Em::new(0.3).into()).with_weak(true).pack() diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs index 572c55b4..4aba3697 100644 --- a/library/src/meta/link.rs +++ b/library/src/meta/link.rs @@ -78,7 +78,7 @@ impl LinkNode { } impl Show for LinkNode { - fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { Ok(self.body()) } } diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 9e5614aa..d66a573d 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -70,12 +70,7 @@ pub struct OutlineNode { } impl Prepare for OutlineNode { - fn prepare( - &self, - vt: &mut Vt, - mut this: Content, - _: StyleChain, - ) -> SourceResult<Content> { + fn prepare(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { let headings = vt .locate(Selector::node::<HeadingNode>()) .into_iter() @@ -84,18 +79,14 @@ impl Prepare for OutlineNode { .map(|node| Value::Content(node.clone())) .collect(); - this.push_field("headings", Value::Array(Array::from_vec(headings))); - Ok(this) + let mut node = self.clone().pack(); + node.push_field("headings", Value::Array(Array::from_vec(headings))); + Ok(node) } } impl Show for OutlineNode { - fn show( - &self, - vt: &mut Vt, - _: &Content, - styles: StyleChain, - ) -> SourceResult<Content> { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let mut seq = vec![ParbreakNode::new().pack()]; if let Some(title) = self.title(styles) { let title = title.clone().unwrap_or_else(|| { diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index 20354556..bfc31785 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -25,7 +25,7 @@ pub struct RefNode { } impl Show for RefNode { - fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { Ok(TextNode::packed(eco_format!("@{}", self.target()))) } } diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs index 4dadf45a..27d30286 100644 --- a/library/src/text/deco.rs +++ b/library/src/text/deco.rs @@ -66,7 +66,7 @@ pub struct UnderlineNode { } impl Show for UnderlineNode { - fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { Ok(self.body().styled(TextNode::set_deco(Decoration { line: DecoLine::Underline, stroke: self.stroke(styles).unwrap_or_default(), @@ -145,7 +145,7 @@ pub struct OverlineNode { } impl Show for OverlineNode { - fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { Ok(self.body().styled(TextNode::set_deco(Decoration { line: DecoLine::Overline, stroke: self.stroke(styles).unwrap_or_default(), @@ -209,7 +209,7 @@ pub struct StrikeNode { } impl Show for StrikeNode { - fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { Ok(self.body().styled(TextNode::set_deco(Decoration { line: DecoLine::Strikethrough, stroke: self.stroke(styles).unwrap_or_default(), diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs index 60521f12..029e15f4 100644 --- a/library/src/text/misc.rs +++ b/library/src/text/misc.rs @@ -99,7 +99,7 @@ pub struct StrongNode { } impl Show for StrongNode { - fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { Ok(self.body().styled(TextNode::set_delta(Delta(self.delta(styles))))) } } @@ -159,7 +159,7 @@ pub struct EmphNode { } impl Show for EmphNode { - fn show(&self, _: &mut Vt, _: &Content, _: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { Ok(self.body().styled(TextNode::set_emph(Toggle))) } } diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs index 3768e65e..72dd1782 100644 --- a/library/src/text/raw.rs +++ b/library/src/text/raw.rs @@ -104,19 +104,15 @@ pub struct RawNode { } impl Prepare for RawNode { - fn prepare( - &self, - _: &mut Vt, - mut this: Content, - styles: StyleChain, - ) -> SourceResult<Content> { - this.push_field("lang", self.lang(styles).clone()); - Ok(this) + fn prepare(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let mut node = self.clone().pack(); + node.push_field("lang", self.lang(styles).clone()); + Ok(node) } } impl Show for RawNode { - fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> { + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let text = self.text(); let lang = self.lang(styles).as_ref().map(|s| s.to_lowercase()); let foreground = THEME @@ -279,7 +275,6 @@ pub static THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme { item("support.macro", Some("#16718d"), None), item("meta.annotation", Some("#301414"), None), item("entity.other, meta.interpolation", Some("#8b41b1"), None), - item("invalid", Some("#ff0000"), None), ], }); diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs index acd46d4e..20cfaa49 100644 --- a/library/src/text/shift.rs +++ b/library/src/text/shift.rs @@ -47,12 +47,7 @@ pub struct SubNode { } impl Show for SubNode { - fn show( - &self, - vt: &mut Vt, - _: &Content, - styles: StyleChain, - ) -> SourceResult<Content> { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let body = self.body(); let mut transformed = None; if self.typographic(styles) { @@ -114,12 +109,7 @@ pub struct SuperNode { } impl Show for SuperNode { - fn show( - &self, - vt: &mut Vt, - _: &Content, - styles: StyleChain, - ) -> SourceResult<Content> { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { let body = self.body(); let mut transformed = None; if self.typographic(styles) { diff --git a/macros/src/node.rs b/macros/src/node.rs index 92faf7dd..f89ee8df 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -186,7 +186,7 @@ fn create(node: &Node) -> TokenStream { #(#field_style_methods)* /// The node's span. - pub fn span(&self) -> Option<::typst::syntax::Span> { + pub fn span(&self) -> ::typst::syntax::Span { self.0.span() } } @@ -228,7 +228,7 @@ fn create_field_method(field: &Field) -> TokenStream { quote! { #[doc = #docs] #vis fn #ident(&self) -> #output { - self.0.cast_required_field(#name) + self.0.field(#name).unwrap().clone().cast().unwrap() } } } else { @@ -319,19 +319,8 @@ fn create_node_impl(node: &Node) -> TokenStream { static META: ::typst::model::NodeMeta = ::typst::model::NodeMeta { name: #name, vtable: #vtable_func, - }; - ::typst::model::NodeId::from_meta(&META) - } - - fn pack(self) -> ::typst::model::Content { - self.0 - } - - fn func() -> ::typst::eval::NodeFunc { - ::typst::eval::NodeFunc { - id: Self::id(), - construct: <Self as ::typst::model::Construct>::construct, - set: <Self as ::typst::model::Set>::set, + construct: <#ident as ::typst::model::Construct>::construct, + set: <#ident as ::typst::model::Set>::set, info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { name: #name, display: #display, @@ -340,7 +329,12 @@ fn create_node_impl(node: &Node) -> TokenStream { returns: ::std::vec!["content"], category: #category, }), - } + }; + ::typst::model::NodeId(&META) + } + + fn pack(self) -> ::typst::model::Content { + self.0 } } } diff --git a/src/eval/args.rs b/src/eval/args.rs index 159e9a77..17bd9cb4 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -1,10 +1,11 @@ -use std::fmt::{self, Debug, Formatter, Write}; +use std::fmt::{self, Debug, Formatter}; -use ecow::EcoVec; +use ecow::{eco_format, EcoVec}; use super::{Array, Cast, Dict, Str, Value}; use crate::diag::{bail, At, SourceResult}; use crate::syntax::{Span, Spanned}; +use crate::util::pretty_array; /// Evaluated arguments to a function. #[derive(Clone, PartialEq, Hash)] @@ -171,14 +172,9 @@ impl Args { impl Debug for Args { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_char('(')?; - for (i, arg) in self.items.iter().enumerate() { - arg.fmt(f)?; - if i + 1 < self.items.len() { - f.write_str(", ")?; - } - } - f.write_char(')') + let pieces: Vec<_> = + self.items.iter().map(|arg| eco_format!("{arg:?}")).collect(); + f.write_str(&pretty_array(&pieces, false)) } } diff --git a/src/eval/array.rs b/src/eval/array.rs index 53bae06f..8da9b3d2 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -1,11 +1,12 @@ use std::cmp::Ordering; -use std::fmt::{self, Debug, Formatter, Write}; +use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, AddAssign}; use ecow::{eco_format, EcoString, EcoVec}; use super::{ops, Args, Func, Value, Vm}; use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::util::pretty_array; /// Create a new [`Array`] from values. #[macro_export] @@ -147,7 +148,6 @@ impl Array { return Ok(Some(item.clone())); } } - Ok(None) } @@ -262,6 +262,14 @@ impl Array { self.0.iter().cloned().rev().collect() } + /// Split all values in the array. + pub fn split(&self, at: Value) -> Array { + self.as_slice() + .split(|value| *value == at) + .map(|subslice| Value::Array(subslice.iter().cloned().collect())) + .collect() + } + /// Join all values in the array, optionally with separator and last /// separator (between the final two items). pub fn join(&self, sep: Option<Value>, mut last: Option<Value>) -> StrResult<Value> { @@ -332,31 +340,10 @@ impl Array { } } -/// The out of bounds access error message. -#[cold] -fn out_of_bounds(index: i64, len: i64) -> EcoString { - eco_format!("array index out of bounds (index: {}, len: {})", index, len) -} - -/// The error message when the array is empty. -#[cold] -fn array_is_empty() -> EcoString { - "array is empty".into() -} - impl Debug for Array { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_char('(')?; - for (i, value) in self.iter().enumerate() { - value.fmt(f)?; - if i + 1 < self.0.len() { - f.write_str(", ")?; - } - } - if self.len() == 1 { - f.write_char(',')?; - } - f.write_char(')') + let pieces: Vec<_> = self.iter().map(|value| eco_format!("{value:?}")).collect(); + f.write_str(&pretty_array(&pieces, self.len() == 1)) } } @@ -404,3 +391,15 @@ impl<'a> IntoIterator for &'a Array { self.iter() } } + +/// The error message when the array is empty. +#[cold] +fn array_is_empty() -> EcoString { + "array is empty".into() +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: i64) -> EcoString { + eco_format!("array index out of bounds (index: {}, len: {})", index, len) +} diff --git a/src/eval/dict.rs b/src/eval/dict.rs index 6c1934c9..ececce07 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -1,5 +1,5 @@ use std::collections::BTreeMap; -use std::fmt::{self, Debug, Formatter, Write}; +use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, AddAssign}; use std::sync::Arc; @@ -8,7 +8,7 @@ use ecow::{eco_format, EcoString}; use super::{array, Array, Str, Value}; use crate::diag::StrResult; use crate::syntax::is_ident; -use crate::util::ArcExt; +use crate::util::{pretty_array, ArcExt}; /// Create a new [`Dict`] from key-value pairs. #[macro_export] @@ -132,31 +132,24 @@ impl Dict { } } -/// The missing key access error message. -#[cold] -fn missing_key(key: &str) -> EcoString { - eco_format!("dictionary does not contain key {:?}", Str::from(key)) -} - impl Debug for Dict { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_char('(')?; if self.is_empty() { - f.write_char(':')?; + return f.write_str("(:)"); } - for (i, (key, value)) in self.iter().enumerate() { - if is_ident(key) { - f.write_str(key)?; - } else { - write!(f, "{key:?}")?; - } - f.write_str(": ")?; - value.fmt(f)?; - if i + 1 < self.0.len() { - f.write_str(", ")?; - } - } - f.write_char(')') + + let pieces: Vec<_> = self + .iter() + .map(|(key, value)| { + if is_ident(key) { + eco_format!("{key}: {value:?}") + } else { + eco_format!("{key:?}: {value:?}") + } + }) + .collect(); + + f.write_str(&pretty_array(&pieces, false)) } } @@ -207,3 +200,9 @@ impl<'a> IntoIterator for &'a Dict { self.iter() } } + +/// The missing key access error message. +#[cold] +fn missing_key(key: &str) -> EcoString { + eco_format!("dictionary does not contain key {:?}", Str::from(key)) +} diff --git a/src/eval/func.rs b/src/eval/func.rs index 79ae142c..8da5c6bc 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -13,7 +13,7 @@ use super::{ Vm, }; use crate::diag::{bail, SourceResult, StrResult}; -use crate::model::{Content, NodeId, Selector, StyleMap}; +use crate::model::{NodeId, Selector, StyleMap}; use crate::syntax::ast::{self, AstNode, Expr}; use crate::syntax::{SourceId, Span, SyntaxNode}; use crate::util::hash128; @@ -29,7 +29,7 @@ enum Repr { /// A native Rust function. Native(NativeFunc), /// A function for a node. - Node(NodeFunc), + Node(NodeId), /// A user-defined closure. Closure(Closure), /// A nested function with pre-applied arguments. @@ -156,13 +156,13 @@ impl Func { /// Create a selector for this function's node type. pub fn select(&self, fields: Option<Dict>) -> StrResult<Selector> { - match &**self.0 { - Repr::Node(node) => { - if node.id == item!(text_id) { + match **self.0 { + Repr::Node(id) => { + if id == item!(text_id) { Err("to select text, please use a string or regex instead")?; } - Ok(Selector::Node(node.id, fields)) + Ok(Selector::Node(id, fields)) } _ => Err("this function is not selectable")?, } @@ -172,8 +172,8 @@ impl Func { impl Debug for Func { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self.name() { - Some(name) => write!(f, "<function {name}>"), - None => f.write_str("<function>"), + Some(name) => write!(f, "{name}"), + None => f.write_str("(..) => .."), } } } @@ -190,6 +190,16 @@ impl From<Repr> for Func { } } +impl From<NodeId> for Func { + fn from(id: NodeId) -> Self { + Repr::Node(id).into() + } +} + +cast_to_value! { + v: NodeId => Value::Func(v.into()) +} + /// A native Rust function. pub struct NativeFunc { /// The function's implementation. @@ -223,36 +233,6 @@ where } } -/// A function defined by a native Rust node. -pub struct NodeFunc { - /// The node's id. - pub id: NodeId, - /// The node's constructor. - pub construct: fn(&Vm, &mut Args) -> SourceResult<Content>, - /// The node's set rule. - pub set: fn(&mut Args) -> SourceResult<StyleMap>, - /// Details about the function. - pub info: Lazy<FuncInfo>, -} - -impl Hash for NodeFunc { - fn hash<H: Hasher>(&self, state: &mut H) { - self.id.hash(state); - (self.construct as usize).hash(state); - (self.set as usize).hash(state); - } -} - -impl From<NodeFunc> for Func { - fn from(node: NodeFunc) -> Self { - Repr::Node(node).into() - } -} - -cast_to_value! { - v: NodeFunc => Value::Func(v.into()) -} - /// Details about a function. #[derive(Debug, Clone)] pub struct FuncInfo { diff --git a/src/eval/methods.rs b/src/eval/methods.rs index dcb1ca31..197a2f65 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -69,6 +69,13 @@ pub fn call( _ => return missing(), }, + Value::Content(content) => match method { + "func" => Value::Func(content.id().into()), + "has" => Value::Bool(content.has(&args.expect::<EcoString>("field")?)), + "at" => content.at(&args.expect::<EcoString>("field")?).at(span)?.clone(), + _ => return missing(), + }, + Value::Array(array) => match method { "len" => Value::Int(array.len()), "first" => array.first().at(span)?.clone(), @@ -96,6 +103,7 @@ pub fn call( "all" => Value::Bool(array.all(vm, args.expect("function")?)?), "flatten" => Value::Array(array.flatten()), "rev" => Value::Array(array.rev()), + "split" => Value::Array(array.split(args.expect("separator")?)), "join" => { let sep = args.eat()?; let last = args.named("last")?; @@ -107,7 +115,7 @@ pub fn call( Value::Dict(dict) => match method { "len" => Value::Int(dict.len()), - "at" => dict.at(&args.expect::<Str>("key")?).cloned().at(span)?, + "at" => dict.at(&args.expect::<Str>("key")?).at(span)?.clone(), "keys" => Value::Array(dict.keys()), "values" => Value::Array(dict.values()), "pairs" => Value::Array(dict.pairs()), @@ -237,6 +245,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { ("starts-with", true), ("trim", true), ], + "content" => &[("func", false), ("has", true), ("at", true)], "array" => &[ ("all", true), ("any", true), @@ -248,6 +257,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { ("flatten", false), ("fold", true), ("insert", true), + ("split", true), ("join", true), ("last", false), ("len", false), diff --git a/src/eval/mod.rs b/src/eval/mod.rs index fcfda263..145f961a 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -47,6 +47,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::{ bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, }; +use crate::model::Unlabellable; use crate::model::{Content, Label, Recipe, Selector, StyleMap, Transform}; use crate::syntax::ast::AstNode; use crate::syntax::{ @@ -357,12 +358,12 @@ fn eval_markup( } let tail = eval_markup(vm, exprs)?; - seq.push(tail.apply_recipe(vm.world, recipe)?) + seq.push(tail.styled_with_recipe(vm.world, recipe)?) } expr => match expr.eval(vm)? { Value::Label(label) => { if let Some(node) = - seq.iter_mut().rev().find(|node| node.labellable()) + seq.iter_mut().rev().find(|node| !node.can::<dyn Unlabellable>()) { *node = mem::take(node).labelled(label); } @@ -786,7 +787,7 @@ fn eval_code( } let tail = eval_code(vm, exprs)?.display(); - Value::Content(tail.apply_recipe(vm.world, recipe)?) + Value::Content(tail.styled_with_recipe(vm.world, recipe)?) } _ => expr.eval(vm)?, }; @@ -979,6 +980,10 @@ impl Eval for ast::FuncCall { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { let span = self.span(); + if vm.depth >= MAX_CALL_DEPTH { + bail!(span, "maximum function call depth exceeded"); + } + let callee = self.callee(); let in_math = in_math(&callee); let callee_span = callee.span(); @@ -1042,11 +1047,6 @@ impl Eval for ast::FuncCall { )); } - // Finally, just a normal function call! - if vm.depth >= MAX_CALL_DEPTH { - bail!(span, "maximum function call depth exceeded"); - } - let callee = callee.cast::<Func>().at(callee_span)?; let point = || Tracepoint::Call(callee.name().map(Into::into)); callee.call(vm, args).trace(vm.world, point, span) diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 52b9b06a..5efb68c3 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -340,6 +340,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { (Symbol(a), Symbol(b)) => a == b, (Str(a), Str(b)) => a == b, (Label(a), Label(b)) => a == b, + (Content(a), Content(b)) => a == b, (Array(a), Array(b)) => a == b, (Dict(a), Dict(b)) => a == b, (Func(a), Func(b)) => a == b, diff --git a/src/eval/value.rs b/src/eval/value.rs index 9b9bc314..61af36f5 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -120,10 +120,7 @@ impl Value { match self { Self::Symbol(symbol) => symbol.clone().modified(&field).map(Self::Symbol), Self::Dict(dict) => dict.at(&field).cloned(), - Self::Content(content) => content - .field(&field) - .cloned() - .ok_or_else(|| eco_format!("unknown field `{field}`")), + Self::Content(content) => content.at(&field).cloned(), Self::Module(module) => module.get(&field).cloned(), v => Err(eco_format!("cannot access fields on type {}", v.type_name())), } @@ -190,7 +187,7 @@ impl Debug for Value { Self::Symbol(v) => Debug::fmt(v, f), Self::Str(v) => Debug::fmt(v, f), Self::Label(v) => Debug::fmt(v, f), - Self::Content(_) => f.pad("[...]"), + Self::Content(v) => Debug::fmt(v, f), Self::Array(v) => Debug::fmt(v, f), Self::Dict(v) => Debug::fmt(v, f), Self::Func(v) => Debug::fmt(v, f), diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index ac0cb60b..4f281050 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -1,11 +1,15 @@ +use std::fmt::Write; + +use ecow::EcoString; + use if_chain::if_chain; -use unicode_segmentation::UnicodeSegmentation; use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; use crate::eval::{CastInfo, Tracer, Value}; use crate::geom::{round_2, Length, Numeric}; use crate::syntax::ast; use crate::syntax::{LinkedNode, Source, SyntaxKind}; +use crate::util::pretty_comma_list; use crate::World; /// Describe the item under the cursor. @@ -60,31 +64,27 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option<Tool return None; } - let mut tooltip = String::new(); - let mut iter = values.into_iter().enumerate(); - for (i, value) in (&mut iter).take(Tracer::MAX - 1) { - if i > 0 && !tooltip.is_empty() { - tooltip.push_str(", "); - } - let repr = value.repr(); - let repr = repr.as_str(); - let len = repr.len(); - if len <= 40 { - tooltip.push_str(repr); - } else { - let mut graphemes = repr.graphemes(true); - let r = graphemes.next_back().map_or(0, str::len); - let l = graphemes.take(40).map(str::len).sum(); - tooltip.push_str(&repr[..l]); - tooltip.push_str("..."); - tooltip.push_str(&repr[len - r..]); + let mut last = None; + let mut pieces: Vec<EcoString> = vec![]; + let mut iter = values.iter(); + for value in (&mut iter).take(Tracer::MAX - 1) { + if let Some((prev, count)) = &mut last { + if *prev == value { + *count += 1; + continue; + } else if *count > 1 { + write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); + } } + pieces.push(value.repr().into()); + last = Some((value, 1)); } if iter.next().is_some() { - tooltip.push_str(", ..."); + pieces.push("...".into()); } + let tooltip = pretty_comma_list(&pieces, false); (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip)) } diff --git a/src/model/content.rs b/src/model/content.rs index d845ce1e..b895553c 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -2,22 +2,24 @@ use std::any::TypeId; use std::fmt::{self, Debug, Formatter, Write}; use std::hash::{Hash, Hasher}; use std::iter::{self, Sum}; -use std::ops::{Add, AddAssign}; +use std::ops::{Add, AddAssign, Deref}; use comemo::Tracked; -use ecow::{EcoString, EcoVec}; +use ecow::{eco_format, EcoString, EcoVec}; +use once_cell::sync::Lazy; use super::{node, Guard, Recipe, Style, StyleMap}; use crate::diag::{SourceResult, StrResult}; -use crate::eval::{cast_from_value, Args, Cast, NodeFunc, Value, Vm}; +use crate::eval::{cast_from_value, Args, FuncInfo, Str, Value, Vm}; use crate::syntax::Span; +use crate::util::pretty_array; use crate::World; /// Composable representation of styled content. #[derive(Clone, Hash)] pub struct Content { id: NodeId, - span: Option<Span>, + span: Span, fields: EcoVec<(EcoString, Value)>, modifiers: EcoVec<Modifier>, } @@ -33,7 +35,7 @@ impl Content { pub fn new<T: Node>() -> Self { Self { id: T::id(), - span: None, + span: Span::detached(), fields: EcoVec::new(), modifiers: EcoVec::new(), } @@ -52,82 +54,67 @@ impl Content { } } - /// Attach a span to the content. - pub fn spanned(mut self, span: Span) -> Self { - if let Some(styled) = self.to::<StyledNode>() { - self = StyledNode::new(styled.map(), styled.body().spanned(span)).pack(); - } - self.span = Some(span); - self - } - - /// Attach a label to the content. - pub fn labelled(self, label: Label) -> Self { - self.with_field("label", label) + /// The id of the contained node. + pub fn id(&self) -> NodeId { + self.id } - /// Style this content with a style entry. - pub fn styled(self, style: impl Into<Style>) -> Self { - self.styled_with_map(style.into().into()) + /// Whether the contained node is of type `T`. + pub fn is<T>(&self) -> bool + where + T: Node + 'static, + { + self.id == NodeId::of::<T>() } - /// Style this content with a full style map. - pub fn styled_with_map(self, styles: StyleMap) -> Self { - if styles.is_empty() { - self - } else if let Some(styled) = self.to::<StyledNode>() { - let mut map = styled.map(); - map.apply(styles); - StyledNode::new(map, styled.body()).pack() - } else { - StyledNode::new(styles, self).pack() - } + /// Cast to `T` if the contained node is of type `T`. + pub fn to<T>(&self) -> Option<&T> + where + T: Node + 'static, + { + self.is::<T>().then(|| unsafe { std::mem::transmute(self) }) } - /// Style this content with a recipe, eagerly applying it if possible. - pub fn apply_recipe( - self, - world: Tracked<dyn World>, - recipe: Recipe, - ) -> SourceResult<Self> { - if recipe.selector.is_none() { - recipe.apply(world, self) - } else { - Ok(self.styled(Style::Recipe(recipe))) - } + /// Whether this content has the given capability. + pub fn can<C>(&self) -> bool + where + C: ?Sized + 'static, + { + (self.id.0.vtable)(TypeId::of::<C>()).is_some() } - /// Repeat this content `n` times. - pub fn repeat(&self, n: i64) -> StrResult<Self> { - let count = usize::try_from(n) - .map_err(|_| format!("cannot repeat this content {} times", n))?; - - Ok(Self::sequence(vec![self.clone(); count])) + /// Cast to a trait object if this content has the given capability. + pub fn with<C>(&self) -> Option<&C> + where + C: ?Sized + 'static, + { + let vtable = (self.id.0.vtable)(TypeId::of::<C>())?; + let data = self as *const Self as *const (); + Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) }) } -} -impl Content { - /// The id of the contained node. - pub fn id(&self) -> NodeId { - self.id + /// The node's span. + pub fn span(&self) -> Span { + self.span } - /// The node's human-readable name. - pub fn name(&self) -> &'static str { - self.id.name() + /// Attach a span to the content. + pub fn spanned(mut self, span: Span) -> Self { + self.span = span; + self } - /// The node's span. - pub fn span(&self) -> Option<Span> { - self.span + /// Access a field on the content. + pub fn field(&self, name: &str) -> Option<&Value> { + self.fields + .iter() + .find(|(field, _)| field == name) + .map(|(_, value)| value) } - /// The content's label. - pub fn label(&self) -> Option<&Label> { - match self.field("label")? { - Value::Label(label) => Some(label), - _ => None, - } + /// List all fields on the content. + pub fn fields(&self) -> &[(EcoString, Value)] { + &self.fields } /// Attach a field to the content. @@ -150,88 +137,88 @@ impl Content { } } - /// Access a field on the content. - pub fn field(&self, name: &str) -> Option<&Value> { - static NONE: Value = Value::None; - self.fields - .iter() - .find(|(field, _)| field == name) - .map(|(_, value)| value) - .or_else(|| (name == "label").then(|| &NONE)) + /// Whether the content has the specified field. + pub fn has(&self, field: &str) -> bool { + self.field(field).is_some() + } + + /// Borrow the value of the given field. + pub fn at(&self, field: &str) -> StrResult<&Value> { + self.field(field).ok_or_else(|| missing_field(field)) } - #[track_caller] - pub fn cast_required_field<T: Cast>(&self, name: &str) -> T { - match self.field(name) { - Some(value) => value.clone().cast().unwrap(), - None => field_is_missing(name), + /// The content's label. + pub fn label(&self) -> Option<&Label> { + match self.field("label")? { + Value::Label(label) => Some(label), + _ => None, } } - /// List all fields on the content. - pub fn fields(&self) -> &[(EcoString, Value)] { - &self.fields + /// Attach a label to the content. + pub fn labelled(self, label: Label) -> Self { + self.with_field("label", label) } - /// Whether the contained node is of type `T`. - pub fn is<T>(&self) -> bool - where - T: Node + 'static, - { - self.id == NodeId::of::<T>() + /// Style this content with a style entry. + pub fn styled(self, style: impl Into<Style>) -> Self { + self.styled_with_map(style.into().into()) } - /// Cast to `T` if the contained node is of type `T`. - pub fn to<T>(&self) -> Option<&T> - where - T: Node + 'static, - { - self.is::<T>().then(|| unsafe { std::mem::transmute(self) }) + /// Style this content with a full style map. + pub fn styled_with_map(self, styles: StyleMap) -> Self { + if styles.is_empty() { + self + } else if let Some(styled) = self.to::<StyledNode>() { + let mut map = styled.styles(); + map.apply(styles); + StyledNode::new(map, styled.body()).pack() + } else { + StyledNode::new(styles, self).pack() + } } - /// Whether this content has the given capability. - pub fn has<C>(&self) -> bool - where - C: ?Sized + 'static, - { - (self.id.0.vtable)(TypeId::of::<C>()).is_some() + /// Style this content with a recipe, eagerly applying it if possible. + pub fn styled_with_recipe( + self, + world: Tracked<dyn World>, + recipe: Recipe, + ) -> SourceResult<Self> { + if recipe.selector.is_none() { + recipe.apply(world, self) + } else { + Ok(self.styled(Style::Recipe(recipe))) + } } - /// Cast to a trait object if this content has the given capability. - pub fn with<C>(&self) -> Option<&C> - where - C: ?Sized + 'static, - { - let vtable = (self.id.0.vtable)(TypeId::of::<C>())?; - let data = self as *const Self as *const (); - Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) }) + /// Repeat this content `n` times. + pub fn repeat(&self, n: i64) -> StrResult<Self> { + let count = usize::try_from(n) + .map_err(|_| format!("cannot repeat this content {} times", n))?; + + Ok(Self::sequence(vec![self.clone(); count])) } +} +#[doc(hidden)] +impl Content { /// Disable a show rule recipe. - #[doc(hidden)] pub fn guarded(mut self, id: Guard) -> Self { self.modifiers.push(Modifier::Guard(id)); self } /// Mark this content as prepared. - #[doc(hidden)] pub fn prepared(mut self) -> Self { self.modifiers.push(Modifier::Prepared); self } /// Whether this node was prepared. - #[doc(hidden)] pub fn is_prepared(&self) -> bool { self.modifiers.contains(&Modifier::Prepared) } - /// Whether a label can be attached to the content. - pub(crate) fn labellable(&self) -> bool { - !self.has::<dyn Unlabellable>() - } - /// Whether no show rule was executed for this node so far. pub(super) fn is_pristine(&self) -> bool { !self @@ -257,32 +244,24 @@ impl Content { impl Debug for Content { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - struct Pad<'a>(&'a str); - impl Debug for Pad<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(self.0) - } + let name = self.id.name; + if let Some(text) = item!(text_str)(self) { + f.write_char('[')?; + f.write_str(&text)?; + f.write_char(']')?; + return Ok(()); + } else if name == "space" { + return f.write_str("[ ]"); } - if let Some(styled) = self.to::<StyledNode>() { - styled.map().fmt(f)?; - styled.body().fmt(f) - } else if let Some(seq) = self.to::<SequenceNode>() { - f.debug_list().entries(&seq.children()).finish() - } else if self.id.name() == "space" { - ' '.fmt(f) - } else if self.id.name() == "text" { - self.field("text").unwrap().fmt(f) - } else { - f.write_str(self.name())?; - if self.fields.is_empty() { - return Ok(()); - } - f.write_char(' ')?; - f.debug_map() - .entries(self.fields.iter().map(|(name, value)| (Pad(name), value))) - .finish() - } + let pieces: Vec<_> = self + .fields + .iter() + .map(|(name, value)| eco_format!("{name}: {value:?}")) + .collect(); + + f.write_str(name)?; + f.write_str(&pretty_array(&pieces, false)) } } @@ -292,6 +271,17 @@ impl Default for Content { } } +impl PartialEq for Content { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.fields.len() == other.fields.len() + && self + .fields + .iter() + .all(|(name, value)| other.field(name) == Some(value)) + } +} + impl Add for Content { type Output = Self; @@ -323,48 +313,6 @@ impl Sum for Content { } } -/// A node with applied styles. -/// -/// Display: Styled -/// Category: special -#[node] -pub struct StyledNode { - /// The styles. - #[required] - pub map: StyleMap, - - /// The styled content. - #[required] - pub body: Content, -} - -cast_from_value! { - StyleMap: "style map", -} - -/// A sequence of nodes. -/// -/// Combines other arbitrary content. So, when you write `[Hi] + [you]` in -/// Typst, the two text nodes are combined into a single sequence node. -/// -/// Display: Sequence -/// Category: special -#[node] -pub struct SequenceNode { - #[variadic] - pub children: Vec<Content>, -} - -/// A label for a node. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Label(pub EcoString); - -impl Debug for Label { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "<{}>", self.0) - } -} - /// A constructable, stylable content node. pub trait Node: Construct + Set + Sized + 'static { /// The node's ID. @@ -372,33 +320,22 @@ pub trait Node: Construct + Set + Sized + 'static { /// Pack a node into type-erased content. fn pack(self) -> Content; - - /// The node's function. - fn func() -> NodeFunc; } /// A unique identifier for a node. #[derive(Copy, Clone)] -pub struct NodeId(&'static NodeMeta); +pub struct NodeId(pub &'static NodeMeta); impl NodeId { + /// Get the id of a node. pub fn of<T: Node>() -> Self { T::id() } - - pub fn from_meta(meta: &'static NodeMeta) -> Self { - Self(meta) - } - - /// The name of the identified node. - pub fn name(self) -> &'static str { - self.0.name - } } impl Debug for NodeId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(self.name()) + f.pad(self.name) } } @@ -416,10 +353,26 @@ impl PartialEq for NodeId { } } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +impl Deref for NodeId { + type Target = NodeMeta; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +/// Static node for a node. pub struct NodeMeta { + /// The node's name. pub name: &'static str, + /// The node's vtable for caspability dispatch. pub vtable: fn(of: TypeId) -> Option<*const ()>, + /// The node's constructor. + pub construct: fn(&Vm, &mut Args) -> SourceResult<Content>, + /// The node's set rule. + pub set: fn(&mut Args) -> SourceResult<StyleMap>, + /// Details about the function. + pub info: Lazy<FuncInfo>, } /// A node's constructor function. @@ -440,8 +393,51 @@ pub trait Set { /// Indicates that a node cannot be labelled. pub trait Unlabellable {} +/// A label for a node. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Label(pub EcoString); + +impl Debug for Label { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "<{}>", self.0) + } +} + +/// A sequence of nodes. +/// +/// Combines other arbitrary content. So, when you write `[Hi] + [you]` in +/// Typst, the two text nodes are combined into a single sequence node. +/// +/// Display: Sequence +/// Category: special +#[node] +pub struct SequenceNode { + #[variadic] + pub children: Vec<Content>, +} + +/// A node with applied styles. +/// +/// Display: Styled +/// Category: special +#[node] +pub struct StyledNode { + /// The styles. + #[required] + pub styles: StyleMap, + + /// The styled content. + #[required] + pub body: Content, +} + +cast_from_value! { + StyleMap: "style map", +} + +/// The missing key access error message. #[cold] #[track_caller] -fn field_is_missing(name: &str) -> ! { - panic!("required field `{name}` is missing") +fn missing_field(key: &str) -> EcoString { + eco_format!("content does not contain field {:?}", Str::from(key)) } diff --git a/src/model/realize.rs b/src/model/realize.rs index 502774bb..c4c67a4f 100644 --- a/src/model/realize.rs +++ b/src/model/realize.rs @@ -3,11 +3,11 @@ use crate::diag::SourceResult; /// Whether the target is affected by show rules in the given style chain. pub fn applicable(target: &Content, styles: StyleChain) -> bool { - if target.has::<dyn Prepare>() && !target.is_prepared() { + if target.can::<dyn Prepare>() && !target.is_prepared() { return true; } - if target.has::<dyn Show>() && target.is_pristine() { + if target.can::<dyn Show>() && target.is_pristine() { return true; } @@ -51,7 +51,7 @@ pub fn realize( if let Some(showable) = target.with::<dyn Show>() { let guard = Guard::Base(target.id()); if realized.is_none() && !target.is_guarded(guard) { - realized = Some(showable.show(vt, target, styles)?); + realized = Some(showable.show(vt, styles)?); } } @@ -135,23 +135,13 @@ fn try_apply( /// Preparations before execution of any show rule. pub trait Prepare { /// Prepare the node for show rule application. - fn prepare( - &self, - vt: &mut Vt, - this: Content, - styles: StyleChain, - ) -> SourceResult<Content>; + fn prepare(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>; } /// The base recipe for a node. pub trait Show { /// Execute the base recipe for this node. - fn show( - &self, - vt: &mut Vt, - this: &Content, - styles: StyleChain, - ) -> SourceResult<Content>; + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>; } /// Post-process a node after it was realized. diff --git a/src/model/styles.rs b/src/model/styles.rs index 3239bb17..b803bc34 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -1,13 +1,14 @@ -use std::fmt::{self, Debug, Formatter}; +use std::fmt::{self, Debug, Formatter, Write}; use std::iter; use comemo::Tracked; -use ecow::EcoString; +use ecow::{eco_format, EcoString}; use super::{Content, Label, Node, NodeId}; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value}; use crate::syntax::Span; +use crate::util::pretty_array; use crate::World; /// A map of style properties. @@ -79,10 +80,13 @@ impl PartialEq for StyleMap { impl Debug for StyleMap { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - for entry in self.0.iter() { - writeln!(f, "{:?}", entry)?; + if let [style] = self.0.as_slice() { + return style.fmt(f); } - Ok(()) + + let pieces: Vec<_> = + self.0.iter().map(|value| eco_format!("{value:?}")).collect(); + f.write_str(&pretty_array(&pieces, false)) } } @@ -160,7 +164,7 @@ impl Property { impl Debug for Property { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "#set {}({}: {:?})", self.node.name(), self.name, self.value)?; + write!(f, "set {}({}: {:?})", self.node.name, self.name, self.value)?; Ok(()) } } @@ -203,12 +207,10 @@ impl Recipe { Transform::Func(func) => { let args = Args::new(self.span, [Value::Content(content.clone())]); let mut result = func.call_detached(world, args); - if let Some(span) = content.span() { - // For selector-less show rules, a tracepoint makes no sense. - if self.selector.is_some() { - let point = || Tracepoint::Show(content.name().into()); - result = result.trace(world, point, span); - } + // For selector-less show rules, a tracepoint makes no sense. + if self.selector.is_some() { + let point = || Tracepoint::Show(content.id().name.into()); + result = result.trace(world, point, content.span()); } Ok(result?.display()) } @@ -219,12 +221,18 @@ impl Recipe { impl Debug for Recipe { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "#show {:?}: {:?}", self.selector, self.transform) + f.write_str("show")?; + if let Some(selector) = &self.selector { + f.write_char(' ')?; + selector.fmt(f)?; + } + f.write_str(": ")?; + self.transform.fmt(f) } } /// A selector in a show rule. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Clone, PartialEq, Hash)] pub enum Selector { /// Matches a specific type of node. /// @@ -267,6 +275,23 @@ impl Selector { } } +impl Debug for Selector { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Node(node, dict) => { + f.write_str(node.name)?; + if let Some(dict) = dict { + f.write_str(".where")?; + dict.fmt(f)?; + } + Ok(()) + } + Self::Label(label) => label.fmt(f), + Self::Regex(regex) => regex.fmt(f), + } + } +} + cast_from_value! { Selector: "selector", text: EcoString => Self::text(&text), @@ -276,7 +301,7 @@ cast_from_value! { } /// A show rule transformation that can be applied to a match. -#[derive(Debug, Clone, Hash)] +#[derive(Clone, Hash)] pub enum Transform { /// Replacement content. Content(Content), @@ -286,6 +311,16 @@ pub enum Transform { Style(StyleMap), } +impl Debug for Transform { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Content(content) => content.fmt(f), + Self::Func(func) => func.fmt(f), + Self::Style(styles) => styles.fmt(f), + } + } +} + cast_from_value! { Transform, content: Content => Self::Content(content), @@ -434,9 +469,9 @@ impl<'a> StyleChain<'a> { .map(|property| property.value.clone()), ) .map(move |value| { - value.cast().unwrap_or_else(|err| { - panic!("{} (for {}.{})", err, node.name(), name) - }) + value + .cast() + .unwrap_or_else(|err| panic!("{} (for {}.{})", err, node.name, name)) }) } diff --git a/src/util/mod.rs b/src/util/mod.rs index 54b9fe27..a3fad8ca 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -6,12 +6,12 @@ mod buffer; pub use buffer::Buffer; -use std::any::TypeId; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; +use ecow::EcoString; use siphasher::sip128::{Hasher128, SipHasher}; /// Turn a closure into a struct implementing [`Debug`]. @@ -133,35 +133,63 @@ impl PathExt for Path { } } -/// An alternative type id that prints as something readable in debug mode. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct ReadableTypeId { - id: TypeId, - #[cfg(debug_assertions)] - name: &'static str, +/// Format something as a a comma-separated list that support horizontal +/// formatting but falls back to vertical formatting if the pieces are too long. +pub fn pretty_array(pieces: &[EcoString], trailing_comma: bool) -> String { + let list = pretty_comma_list(&pieces, trailing_comma); + let mut buf = String::new(); + buf.push('('); + if list.contains('\n') { + buf.push('\n'); + buf.push_str(&indent(&list, 2)); + buf.push('\n'); + } else { + buf.push_str(&list); + } + buf.push(')'); + buf } -impl ReadableTypeId { - /// The type id of the given type. - pub fn of<T: 'static>() -> Self { - Self { - id: TypeId::of::<T>(), - #[cfg(debug_assertions)] - name: std::any::type_name::<T>(), +/// Format something as a a comma-separated list that support horizontal +/// formatting but falls back to vertical formatting if the pieces are too long. +pub fn pretty_comma_list(pieces: &[EcoString], trailing_comma: bool) -> String { + const MAX_WIDTH: usize = 50; + + let mut buf = String::new(); + let len = pieces.iter().map(|s| s.len()).sum::<usize>() + + 2 * pieces.len().saturating_sub(1); + + if len <= MAX_WIDTH { + for (i, piece) in pieces.iter().enumerate() { + if i > 0 { + buf.push_str(", "); + } + buf.push_str(piece); + } + if trailing_comma { + buf.push(','); + } + } else { + for piece in pieces { + buf.push_str(piece.trim()); + buf.push_str(",\n"); } } + + buf } -impl Debug for ReadableTypeId { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - #[cfg(debug_assertions)] - if let Some(part) = self.name.split("::").last() { - f.pad(part)?; +/// Indent a string by two spaces. +pub fn indent(text: &str, amount: usize) -> String { + let mut buf = String::new(); + for (i, line) in text.lines().enumerate() { + if i > 0 { + buf.push('\n'); } - - #[cfg(not(debug_assertions))] - f.pad("ReadableTypeId")?; - - Ok(()) + for _ in 0..amount { + buf.push(' '); + } + buf.push_str(line); } + buf } diff --git a/tests/ref/compiler/content-field.png b/tests/ref/compiler/content-field.png Binary files differnew file mode 100644 index 00000000..8ad5566a --- /dev/null +++ b/tests/ref/compiler/content-field.png diff --git a/tests/ref/compiler/repr.png b/tests/ref/compiler/repr.png Binary files differindex fe0b0c55..e3bed1be 100644 --- a/tests/ref/compiler/repr.png +++ b/tests/ref/compiler/repr.png diff --git a/tests/typ/compiler/content-field.typ b/tests/typ/compiler/content-field.typ new file mode 100644 index 00000000..0eeee458 --- /dev/null +++ b/tests/typ/compiler/content-field.typ @@ -0,0 +1,41 @@ +// Integrated test for content fields. + +#let compute(formula, ..vars) = { + let vars = vars.named() + let f(node) = { + let func = node.func() + if func == text { + let text = node.text + if regex("^\d+$") in text { + int(text) + } else if text in vars { + int(vars.at(text)) + } else { + panic("unknown math variable: " + text) + } + } else if func == math.attach { + let value = f(node.base) + if node.has("top") { + value = calc.pow(value, f(node.top)) + } + value + } else if node.has("children") { + node + .children + .filter(v => v != [ ]) + .split[+] + .map(xs => xs.fold(1, (prod, v) => prod * f(v))) + .fold(0, (sum, v) => sum + v) + } + } + let result = f(formula.body) + [With ] + vars + .pairs() + .map(p => $#p.first() = #p.last()$) + .join(", ", last: " and ") + [ we have:] + $ formula = result $ +} + +#compute($x y + y^2$, x: 2, y: 3) diff --git a/tests/typ/compiler/field.typ b/tests/typ/compiler/field.typ index 27b31f7d..342ae75b 100644 --- a/tests/typ/compiler/field.typ +++ b/tests/typ/compiler/field.typ @@ -31,7 +31,7 @@ #false.ok --- -// Error: 29-32 unknown field `fun` +// Error: 29-32 content does not contain field "fun" #show heading: node => node.fun = A diff --git a/tests/typ/compiler/label.typ b/tests/typ/compiler/label.typ index d4ff15d7..58b0dee9 100644 --- a/tests/typ/compiler/label.typ +++ b/tests/typ/compiler/label.typ @@ -22,7 +22,7 @@ The end. --- // Test labelled text. #show "t": it => { - set text(blue) if it.label == <last> + set text(blue) if it.has("label") and it.label == <last> it } diff --git a/tests/typ/compiler/ops.typ b/tests/typ/compiler/ops.typ index baa2898a..a29003ed 100644 --- a/tests/typ/compiler/ops.typ +++ b/tests/typ/compiler/ops.typ @@ -152,11 +152,13 @@ #test(test == test, true) #test((() => {}) == (() => {}), false) -// Content cannot be compared. +// Content compares field by field. #let t = [a] -#test(t == t, false) -#test([] == [], false) -#test([a] == [a], false) +#test(t == t, true) +#test([] == [], true) +#test([a] == [a], true) +#test(grid[a] == grid[a], true) +#test(grid[a] == grid[b], false) --- // Test comparison operators. diff --git a/tests/typ/compiler/repr.typ b/tests/typ/compiler/repr.typ index ed6cd84a..cfe428eb 100644 --- a/tests/typ/compiler/repr.typ +++ b/tests/typ/compiler/repr.typ @@ -30,16 +30,13 @@ #rgb("f7a205") \ #(2pt + rgb("f7a205")) ---- // Strings and escaping. -#raw(repr("hi"), lang: "typc") \ +#raw(repr("hi"), lang: "typc") #repr("a\n[]\"\u{1F680}string") ---- // Content. -#raw(repr[*{"H" + "i"} there*]) +#raw(lang: "typc", repr[*Hey*]) ---- // Functions are invisible. Nothing #let f(x) = x diff --git a/tests/typ/compiler/show-node.typ b/tests/typ/compiler/show-node.typ index f87b1971..0c1c1f52 100644 --- a/tests/typ/compiler/show-node.typ +++ b/tests/typ/compiler/show-node.typ @@ -78,7 +78,7 @@ Another text. = Heading --- -// Error: 25-29 unknown field `page` +// Error: 25-29 content does not contain field "page" #show heading: it => it.page = Heading |
