diff --git a/native/folio_nif/src/convert.rs b/native/folio_nif/src/convert.rs index 20a4c40..e5bc119 100644 --- a/native/folio_nif/src/convert.rs +++ b/native/folio_nif/src/convert.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use ecow::{eco_format, EcoString}; use typst::engine::Engine; -use typst::foundations::{Bytes, Content, NativeElement, OneOrMultiple, Smart}; +use typst::foundations::{Bytes, Content, NativeElement, OneOrMultiple, Smart, Unlabellable}; use std::sync::Arc; use typst::layout::{ Abs, AlignElem, Alignment, Axes, BlockBody, BlockElem, Celled, ColbreakElem, @@ -232,6 +232,7 @@ pub fn build_content(engine: &mut Engine, nodes: &[ExContent]) -> Content { (ExContent::Parbreak(_), _) | (_, ExContent::Parbreak(_)) => false, (ExContent::Pagebreak(_), _) | (_, ExContent::Pagebreak(_)) => false, (ExContent::Colbreak(_), _) | (_, ExContent::Colbreak(_)) => false, + (_, ExContent::Label(_)) => false, // Between two paragraph-like blocks (p, n) if is_paragraph_like(p) && is_paragraph_like(n) => true, // After a paragraph-like block and before another block @@ -244,11 +245,23 @@ pub fn build_content(engine: &mut Engine, nodes: &[ExContent]) -> Content { seq.push(ParbreakElem::shared().clone()); } } - seq.push(convert_node(engine, node)); + push_or_attach_label(engine, &mut seq, node); } Content::sequence(seq) } +fn push_or_attach_label(engine: &mut Engine, seq: &mut Vec, node: &ExContent) { + if let ExContent::Label(label) = node { + if let Some(lbl) = typst::foundations::Label::new(PicoStr::intern(&label.name)) { + if let Some(elem) = seq.iter_mut().rev().find(|n| !n.can::()) { + *elem = std::mem::take(elem).labelled(lbl); + return; + } + } + } + seq.push(convert_node(engine, node)); +} + fn is_block(node: &ExContent) -> bool { match node { ExContent::Heading(_) | ExContent::Paragraph(_) | ExContent::List(_) @@ -275,7 +288,11 @@ fn is_paragraph_like(node: &ExContent) -> bool { } fn cc(engine: &mut Engine, nodes: &[ExContent]) -> Content { - Content::sequence(nodes.iter().map(|n| convert_node(engine, n)).collect::>()) + let mut seq: Vec = Vec::with_capacity(nodes.len()); + for n in nodes { + push_or_attach_label(engine, &mut seq, n); + } + Content::sequence(seq) } fn convert_node(engine: &mut Engine, node: &ExContent) -> Content { diff --git a/native/folio_nif/src/world.rs b/native/folio_nif/src/world.rs index bb6044a..2e72ba4 100644 --- a/native/folio_nif/src/world.rs +++ b/native/folio_nif/src/world.rs @@ -3,14 +3,14 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, LazyLock, Mutex}; -use typst::comemo::{Track, TrackedMut}; +use typst::comemo::{Constraint, Track, TrackedMut}; use typst::diag::{FileError, FileResult}; use typst::engine::{Engine, Route, Sink, Traced}; use typst::foundations::{ - Bytes, Content, Context, Datetime, Derived, Duration, NativeElement, Smart, StyleChain, Styles, - Target, TargetElem, + Bytes, Content, Context, Datetime, Derived, Duration, NativeElement, Output, Smart, + StyleChain, Styles, Target, TargetElem, }; -use typst::introspection::EmptyIntrospector; +use typst::introspection::{EmptyIntrospector, Introspector, MAX_ITERS}; use typst::layout::{Abs, Margin, Sides}; use typst::layout::PageElem; use typst::loading::{DataSource, LoadSource, Loaded}; @@ -147,31 +147,56 @@ impl FolioWorld { } fn layout(&self, content: &[ExContent]) -> Result { - let mut sink = Sink::new(); - let introspector = EmptyIntrospector; let traced = Traced::default(); - - let mut engine = Engine { - routines: &typst::ROUTINES, - world: Track::track(self), - introspector: typst::utils::Protected::new(introspector.track()), - traced: traced.track(), - sink: sink.track_mut(), - route: Route::root(), + let empty = EmptyIntrospector; + + let mut build_sink = Sink::new(); + let (body, user_styles) = { + let mut engine = Engine { + routines: &typst::ROUTINES, + world: Track::track(self), + introspector: typst::utils::Protected::new(empty.track()), + traced: traced.track(), + sink: build_sink.track_mut(), + route: Route::root(), + }; + let body = build_content(&mut engine, content); + let mut styles = typst::foundations::Styles::new(); + apply_styles(&mut styles, &self.styles, &mut engine); + (body, styles) }; - let body = build_content(&mut engine, content); - let lib = &GLOBAL.library; let base = StyleChain::new(&lib.styles); - let mut user_styles = typst::foundations::Styles::new(); - apply_styles(&mut user_styles, &self.styles, &mut engine); let target_style: Styles = TargetElem::target.set(Target::Paged).wrap().into(); let chained = base.chain(&target_style); let styles = chained.chain(&user_styles); - layout_document(&mut engine, &body, styles) - .map_err(|e| format!("Layout error: {:?}", e)) + let mut prev: Option = None; + for _ in 0..MAX_ITERS { + let constraint = Constraint::new(); + let introspector: &dyn Introspector = match &prev { + Some(doc) => Output::introspector(doc), + None => &empty, + }; + let mut iter_sink = Sink::new(); + let mut engine = Engine { + routines: &typst::ROUTINES, + world: Track::track(self), + introspector: typst::utils::Protected::new(introspector.track_with(&constraint)), + traced: traced.track(), + sink: iter_sink.track_mut(), + route: Route::root(), + }; + let doc = layout_document(&mut engine, &body, styles) + .map_err(|e| format!("Layout error: {:?}", e))?; + drop(engine); + if constraint.validate(Output::introspector(&doc)) { + return Ok(doc); + } + prev = Some(doc); + } + prev.ok_or_else(|| "Layout error: introspection did not converge".to_string()) } pub fn eval_math(engine: &mut Engine, math_str: &str, block: bool) -> Content { diff --git a/test/folio_test.exs b/test/folio_test.exs index 3a9a5ee..f7c4dfa 100644 --- a/test/folio_test.exs +++ b/test/folio_test.exs @@ -136,6 +136,20 @@ defmodule FolioTest do assert is_binary(svg) assert String.starts_with?(svg, "= 2 + # Body content should be the same, but page numbering should differ + assert page1 != page2 + end end describe "to_png/2" do