diff --git a/src/exec/context.rs b/src/exec/context.rs index 333ad3bad..6101047e1 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -65,87 +65,66 @@ impl<'a> ExecContext<'a> { mem::replace(&mut self.stack, stack).build() } + /// Push any node into the active paragraph. + pub fn push(&mut self, node: impl Into) { + let align = self.state.aligns.cross; + self.stack.par.push(ParChild::Any(node.into(), align)); + } + + /// Push a word space into the active paragraph. + pub fn push_word_space(&mut self) { + let em = self.state.font.resolve_size(); + let amount = self.state.par.word_spacing.resolve(em); + self.stack.par.push_soft(ParChild::Spacing(amount)); + } + /// Push text into the active paragraph. /// /// The text is split into lines at newlines. pub fn push_text(&mut self, text: &str) { let mut scanner = Scanner::new(text); - let mut line = String::new(); - let push = |this: &mut Self, text| { - let props = this.state.font.resolve_props(); - let node = TextNode { text, props }; - let align = this.state.aligns.cross; - this.stack.par.folder.push(ParChild::Text(node, align)) - }; + let mut text = String::new(); while let Some(c) = scanner.eat_merging_crlf() { if is_newline(c) { - push(self, mem::take(&mut line)); - self.push_linebreak(); + self.stack.par.push_text(mem::take(&mut text), &self.state); + self.linebreak(); } else { - line.push(c); + text.push(c); } } - push(self, line); - } - - /// Push a word space. - pub fn push_word_space(&mut self) { - let em = self.state.font.resolve_size(); - let amount = self.state.par.word_spacing.resolve(em); - self.push_spacing(GenAxis::Cross, amount, 1); - } - - /// Apply a forced line break. - pub fn push_linebreak(&mut self) { - let em = self.state.font.resolve_size(); - let amount = self.state.par.leading.resolve(em); - self.push_spacing(GenAxis::Main, amount, 2); - } - - /// Apply a forced paragraph break. - pub fn push_parbreak(&mut self) { - let em = self.state.font.resolve_size(); - let amount = self.state.par.spacing.resolve(em); - self.push_spacing(GenAxis::Main, amount, 1); + self.stack.par.push_text(text, &self.state); } /// Push spacing into paragraph or stack depending on `axis`. - /// - /// The `softness` configures how the spacing interacts with surrounding - /// spacing. - pub fn push_spacing(&mut self, axis: GenAxis, amount: Length, softness: u8) { + pub fn push_spacing(&mut self, axis: GenAxis, amount: Length) { match axis { GenAxis::Main => { - let spacing = StackChild::Spacing(amount); - self.stack.finish_par(&self.state); - self.stack.folder.push_soft(spacing, softness); + self.stack.parbreak(&self.state); + self.stack.push_hard(StackChild::Spacing(amount)); } GenAxis::Cross => { - let spacing = ParChild::Spacing(amount); - self.stack.par.folder.push_soft(spacing, softness); + self.stack.par.push_hard(ParChild::Spacing(amount)); } } } - /// Push any node into the active paragraph. - pub fn push_into_par(&mut self, node: impl Into) { - let align = self.state.aligns.cross; - self.stack.par.folder.push(ParChild::Any(node.into(), align)); + /// Apply a forced line break. + pub fn linebreak(&mut self) { + self.stack.par.push_hard(ParChild::Linebreak); } - /// Push any node directly into the stack of paragraphs. - /// - /// This finishes the active paragraph and starts a new one. - pub fn push_into_stack(&mut self, node: impl Into) { - let aligns = self.state.aligns; - self.stack.finish_par(&self.state); - self.stack.folder.push(StackChild::Any(node.into(), aligns)); + /// Apply a forced paragraph break. + pub fn parbreak(&mut self) { + let em = self.state.font.resolve_size(); + let amount = self.state.par.spacing.resolve(em); + self.stack.parbreak(&self.state); + self.stack.push_soft(StackChild::Spacing(amount)); } - /// Finish the active page. - pub fn finish_page(&mut self, keep: bool, hard: bool, source: Span) { + /// Apply a forced page break. + pub fn pagebreak(&mut self, keep: bool, hard: bool, source: Span) { if let Some(builder) = &mut self.page { let page = mem::replace(builder, PageBuilder::new(&self.state, hard)); let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state)); @@ -158,7 +137,7 @@ impl<'a> ExecContext<'a> { /// Finish execution and return the created layout tree. pub fn finish(mut self) -> Pass { assert!(self.page.is_some()); - self.finish_page(true, false, Span::default()); + self.pagebreak(true, false, Span::default()); Pass::new(self.tree, self.diags) } } @@ -189,7 +168,8 @@ impl PageBuilder { struct StackBuilder { dirs: Gen, - folder: SoftFolder, + children: Vec, + last: Last, par: ParBuilder, } @@ -197,20 +177,36 @@ impl StackBuilder { fn new(state: &State) -> Self { Self { dirs: Gen::new(Dir::TTB, state.lang.dir), - folder: SoftFolder::new(), + children: vec![], + last: Last::None, par: ParBuilder::new(state), } } - fn finish_par(&mut self, state: &State) { + fn push_soft(&mut self, child: StackChild) { + self.last.soft(child); + } + + fn push_hard(&mut self, child: StackChild) { + self.last.hard(); + self.children.push(child); + } + + fn parbreak(&mut self, state: &State) { let par = mem::replace(&mut self.par, ParBuilder::new(state)); - self.folder.extend(par.build()); + if let Some(par) = par.build() { + self.children.extend(self.last.any()); + self.children.push(par); + } } fn build(self) -> StackNode { - let Self { dirs, mut folder, par } = self; - folder.extend(par.build()); - StackNode { dirs, children: folder.finish() } + let Self { dirs, mut children, par, mut last } = self; + if let Some(par) = par.build() { + children.extend(last.any()); + children.push(par); + } + StackNode { dirs, children } } } @@ -218,7 +214,8 @@ struct ParBuilder { aligns: Gen, dir: Dir, line_spacing: Length, - folder: SoftFolder, + children: Vec, + last: Last, } impl ParBuilder { @@ -228,13 +225,43 @@ impl ParBuilder { aligns: state.aligns, dir: state.lang.dir, line_spacing: state.par.leading.resolve(em), - folder: SoftFolder::new(), + children: vec![], + last: Last::None, } } + fn push(&mut self, child: ParChild) { + self.children.extend(self.last.any()); + self.children.push(child); + } + + fn push_text(&mut self, text: String, state: &State) { + self.children.extend(self.last.any()); + + let align = state.aligns.cross; + let props = state.font.resolve_props(); + + if let Some(ParChild::Text(prev, prev_align)) = self.children.last_mut() { + if *prev_align == align && prev.props == props { + prev.text.push_str(&text); + return; + } + } + + self.children.push(ParChild::Text(TextNode { text, props }, align)); + } + + fn push_soft(&mut self, child: ParChild) { + self.last.soft(child); + } + + fn push_hard(&mut self, child: ParChild) { + self.last.hard(); + self.children.push(child); + } + fn build(self) -> Option { - let Self { aligns, dir, line_spacing, folder } = self; - let children = folder.finish(); + let Self { aligns, dir, line_spacing, children, .. } = self; (!children.is_empty()).then(|| { let node = ParNode { dir, line_spacing, children }; StackChild::Any(node.into(), aligns) @@ -242,54 +269,28 @@ impl ParBuilder { } } -/// This is used to remove leading and trailing word/line/paragraph spacing -/// as well as collapse sequences of spacings into just one. -struct SoftFolder { - nodes: Vec, - last: Last, -} - +/// Finite state machine for spacing coalescing. enum Last { None, - Hard, - Soft(N, u8), + Any, + Soft(N), } -impl SoftFolder { - fn new() -> Self { - Self { nodes: vec![], last: Last::Hard } - } - - fn push(&mut self, node: N) { - let last = mem::replace(&mut self.last, Last::None); - if let Last::Soft(soft, _) = last { - self.nodes.push(soft); - } - self.nodes.push(node); - } - - fn push_soft(&mut self, node: N, softness: u8) { - if softness == 0 { - self.last = Last::Hard; - self.nodes.push(node); - } else { - match self.last { - Last::Hard => {} - Last::Soft(_, other) if softness >= other => {} - _ => self.last = Last::Soft(node, softness), - } +impl Last { + fn any(&mut self) -> Option { + match mem::replace(self, Self::Any) { + Self::Soft(soft) => Some(soft), + _ => None, } } - fn finish(self) -> Vec { - self.nodes - } -} - -impl Extend for SoftFolder { - fn extend>(&mut self, iter: T) { - for elem in iter { - self.push(elem); + fn soft(&mut self, soft: N) { + if let Self::Any = self { + *self = Self::Soft(soft); } } + + fn hard(&mut self) { + *self = Self::None; + } } diff --git a/src/layout/par.rs b/src/layout/par.rs index 02e27cbdc..e0b428216 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -23,6 +23,8 @@ pub enum ParChild { Text(TextNode, Align), /// Any child node and how to align it in its line. Any(AnyNode, Align), + /// A forced linebreak. + Linebreak, } /// A consecutive, styled run of text. @@ -55,6 +57,7 @@ impl Layout for ParNode { layouter.push_frame(frame, align); } } + ParChild::Linebreak => layouter.finish_line(), } } layouter.finish() diff --git a/src/library/align.rs b/src/library/align.rs index d5811bf4a..ccb25b346 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -52,7 +52,7 @@ pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { if let Some(vertical) = vertical { ctx.state.aligns.main = vertical.to_align(Dir::TTB); if ctx.state.aligns.main != snapshot.aligns.main { - ctx.push_linebreak(); + ctx.parbreak(); } } diff --git a/src/library/image.rs b/src/library/image.rs index 020f7d50d..ed7b2268a 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -23,7 +23,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let loaded = ctx.env.resources.load(&path.v, ImageResource::parse); if let Some((res, img)) = loaded { let dimensions = img.buf.dimensions(); - ctx.push_into_par(ImageNode { res, dimensions, width, height }); + ctx.push(ImageNode { res, dimensions, width, height }); } else { ctx.diag(error!(path.span, "failed to load image")); } diff --git a/src/library/lang.rs b/src/library/lang.rs index 79015c7d2..1248b707d 100644 --- a/src/library/lang.rs +++ b/src/library/lang.rs @@ -32,7 +32,7 @@ pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } } - ctx.push_parbreak(); + ctx.parbreak(); }) } diff --git a/src/library/markup.rs b/src/library/markup.rs index d7d750ea1..ac2356a9c 100644 --- a/src/library/markup.rs +++ b/src/library/markup.rs @@ -14,7 +14,7 @@ use crate::syntax::{HeadingNode, RawNode}; /// A template that inserts a line break. pub fn linebreak(_: &mut EvalContext, _: &mut FuncArgs) -> Value { Value::template(Node::LINEBREAK, move |ctx| { - ctx.push_linebreak(); + ctx.linebreak(); }) } @@ -24,7 +24,7 @@ pub fn linebreak(_: &mut EvalContext, _: &mut FuncArgs) -> Value { /// A template that inserts a paragraph break. pub fn parbreak(_: &mut EvalContext, _: &mut FuncArgs) -> Value { Value::template(Node::PARBREAK, move |ctx| { - ctx.push_parbreak(); + ctx.parbreak(); }) } @@ -118,7 +118,7 @@ pub fn heading(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { body.exec(ctx); ctx.state = snapshot; - ctx.push_parbreak(); + ctx.parbreak(); }) } @@ -155,7 +155,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { Value::template(Node::RAW, move |ctx| { if block { - ctx.push_parbreak(); + ctx.parbreak(); } let snapshot = ctx.state.clone(); @@ -164,7 +164,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { ctx.state = snapshot; if block { - ctx.push_parbreak(); + ctx.parbreak(); } }) } diff --git a/src/library/pad.rs b/src/library/pad.rs index d6b690070..7c422239d 100644 --- a/src/library/pad.rs +++ b/src/library/pad.rs @@ -32,6 +32,6 @@ pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { Value::template("pad", move |ctx| { let child = ctx.exec_group(&body).into(); - ctx.push_into_par(PadNode { padding, child }); + ctx.push(PadNode { padding, child }); }) } diff --git a/src/library/page.rs b/src/library/page.rs index fb3542edc..7b8557bef 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -83,13 +83,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { std::mem::swap(&mut page.size.width, &mut page.size.height); } - ctx.finish_page(false, true, span); + ctx.pagebreak(false, true, span); if let Some(body) = &body { // TODO: Restrict body to a single page? body.exec(ctx); ctx.state = snapshot; - ctx.finish_page(true, false, span); + ctx.pagebreak(true, false, span); } }) } @@ -101,6 +101,6 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn pagebreak(_: &mut EvalContext, args: &mut FuncArgs) -> Value { let span = args.span; Value::template("pagebreak", move |ctx| { - ctx.finish_page(true, true, span); + ctx.pagebreak(true, true, span); }) } diff --git a/src/library/par.rs b/src/library/par.rs index cf2549bf7..92fc20e8a 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -27,6 +27,6 @@ pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { ctx.state.par.word_spacing = word_spacing; } - ctx.push_parbreak(); + ctx.parbreak(); }) } diff --git a/src/library/shapes.rs b/src/library/shapes.rs index 6f9e66770..6c6a2f0b3 100644 --- a/src/library/shapes.rs +++ b/src/library/shapes.rs @@ -63,13 +63,13 @@ fn rect_impl( let node = FixedNode { width, height, aspect, child }; if let Some(color) = fill { - ctx.push_into_par(BackgroundNode { + ctx.push(BackgroundNode { shape: BackgroundShape::Rect, fill: Fill::Color(color), child: node.into(), }); } else { - ctx.push_into_par(node); + ctx.push(node); } }) } @@ -146,13 +146,13 @@ fn ellipse_impl( }; if let Some(color) = fill { - ctx.push_into_par(BackgroundNode { + ctx.push(BackgroundNode { shape: BackgroundShape::Ellipse, fill: Fill::Color(color), child: node.into(), }); } else { - ctx.push_into_par(node); + ctx.push(node); } }) } diff --git a/src/library/spacing.rs b/src/library/spacing.rs index 6a67a6535..36cd88fbd 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -32,7 +32,7 @@ fn spacing_impl( Value::template(name, move |ctx| { if let Some(linear) = spacing { let amount = linear.resolve(ctx.state.font.resolve_size()); - ctx.push_spacing(axis, amount, 0); + ctx.push_spacing(axis, amount); } }) } diff --git a/tests/ref/control/for.png b/tests/ref/control/for.png index 1c7dfd42e..bf6b535c6 100644 Binary files a/tests/ref/control/for.png and b/tests/ref/control/for.png differ diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png index a32229e7a..a7de742d2 100644 Binary files a/tests/ref/markup/heading.png and b/tests/ref/markup/heading.png differ diff --git a/tests/ref/markup/linebreak.png b/tests/ref/markup/linebreak.png index 512fa0f5e..32236e742 100644 Binary files a/tests/ref/markup/linebreak.png and b/tests/ref/markup/linebreak.png differ diff --git a/tests/ref/markup/nbsp.png b/tests/ref/markup/nbsp.png index cc920776a..8834bb2bc 100644 Binary files a/tests/ref/markup/nbsp.png and b/tests/ref/markup/nbsp.png differ diff --git a/tests/typ/control/for.typ b/tests/typ/control/for.typ index 5eaa6ae84..bca1af468 100644 --- a/tests/typ/control/for.typ +++ b/tests/typ/control/for.typ @@ -14,8 +14,8 @@ // Dictionary is not traversed in insertion order. // Should output `age: 1, name: Typst,`. -#for k, v in (name: "Typst", age: 2) [ - {k}: {v}, \ +#for k, v in (Name: "Typst", Age: 2) [ + {k}: {v}. ] // String. diff --git a/tests/typ/expand.typ b/tests/typ/expand.typ index 230447443..8b858a4ef 100644 --- a/tests/typ/expand.typ +++ b/tests/typ/expand.typ @@ -7,7 +7,7 @@ // Top-level paragraph fills page, boxed paragraph only when width is fixed. L #right[R] \ #rect(width: 50pt)[L #right[R]] \ -#rect[L #right[R]] \ +#rect[L #right[R]] // Pad inherits expansion behaviour. #pad[PL #right[PR]] \ diff --git a/tests/typ/expr/call.typ b/tests/typ/expr/call.typ index 0e941124c..dcf11806e 100644 --- a/tests/typ/expr/call.typ +++ b/tests/typ/expr/call.typ @@ -60,7 +60,7 @@ #args[a] \ #args(a) \ #args(a, [b]) \ -#args(a)[b] \ +#args(a)[b] // Template can be argument or body depending on whitespace. #if "template" == type[b] [Sure ] diff --git a/tests/typ/library/font.typ b/tests/typ/library/font.typ index 13f711250..397452e37 100644 --- a/tests/typ/library/font.typ +++ b/tests/typ/library/font.typ @@ -53,7 +53,7 @@ Emoji: 🐪, 🌋, 🏞 #font(sans-serif: "PT Sans") #font(sans-serif)[Sans-serif.] \ #font(monospace)[Monospace.] \ -#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.] \ +#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.] --- // Ref: false diff --git a/tests/typ/markup/linebreak.typ b/tests/typ/markup/linebreak.typ index a0a501b65..b1b83dcc8 100644 --- a/tests/typ/markup/linebreak.typ +++ b/tests/typ/markup/linebreak.typ @@ -10,17 +10,11 @@ Line \ Break // Directly before word does not work. No \Break ---- -// Leading line break. -\ Leading +\ Before -// Trailing before paragraph break. -Trailing 1 \ +Multiple \ \ \ -Trailing 2 - -// Trailing before end of document. -Trailing 3 \ +Times --- #let linebreak() = [ @@ -28,4 +22,4 @@ Trailing 3 \ #circle(radius: 2pt, fill: #000) \ ] -A \ B \ C \ +A \ B \ C diff --git a/tests/typ/markup/nbsp.typ b/tests/typ/markup/nbsp.typ index 5af6c84f9..372268eb5 100644 --- a/tests/typ/markup/nbsp.typ +++ b/tests/typ/markup/nbsp.typ @@ -1,5 +1,4 @@ // Test the non breaking space. --- -// Parsed correctly, but not actually doing anything at the moment. -The non-breaking~space does not work. +The non-breaking~space does work. diff --git a/tests/typ/repr.typ b/tests/typ/repr.typ index cf137745c..f24045107 100644 --- a/tests/typ/repr.typ +++ b/tests/typ/repr.typ @@ -9,7 +9,7 @@ {name} \ {ke-bab} \ -{α} \ +{α} // Error: 2-3 unknown variable {_} @@ -18,7 +18,7 @@ // Literal values. {none} (empty) \ {true} \ -{false} \ +{false} --- // Numerical values. @@ -33,16 +33,16 @@ {2.5rad} \ {45deg} \ // Not in monospace via repr. -#repr(45deg) \ +#repr(45deg) --- // Colors. -{#f7a20500} \ +{#f7a20500} --- // Strings and escaping. {"hi"} \ -{"a\n[]\"\u{1F680}string"} \ +{"a\n[]\"\u{1F680}string"} --- // Templates. @@ -54,4 +54,4 @@ {rect} \ {f} \ -{() => none} \ +{() => none} diff --git a/tests/typ/spacing.typ b/tests/typ/spacing.typ index ccaf084a2..bb86a59c8 100644 --- a/tests/typ/spacing.typ +++ b/tests/typ/spacing.typ @@ -7,7 +7,7 @@ A#let;B \ A#let x = 1;B #test(x, 1) \ A #let x = 2;B #test(x, 2) \ -A#let x = 3; B #test(x, 3) \ +A#let x = 3; B #test(x, 3) --- // Spacing around if-else. @@ -17,7 +17,7 @@ A#if true [B] C \ A #if true{"B"}C \ A #if true{"B"} C \ A#if false [] #else [B]C \ -A#if true [B] #else [] C \ +A#if true [B] #else [] C --- // Spacing around while loop. @@ -25,11 +25,11 @@ A#if true [B] #else [] C \ #let c = true; A#while c [{c = false}B]C \ #let c = true; A#while c [{c = false}B] C \ #let c = true; A #while c { c = false; "B" }C \ -#let c = true; A #while c { c = false; "B" } C \ +#let c = true; A #while c { c = false; "B" } C --- // Spacing around for loop. A#for _ in (none,) [B]C \ A#for _ in (none,) [B] C \ -A #for _ in (none,) {"B"}C \ +A #for _ in (none,) {"B"}C