diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 1fa6aa216..25d0cd5d8 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -16,7 +16,7 @@ use typst_library::introspection::{ }; use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; use typst_library::model::{DocumentInfo, ParElem}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_library::World; use typst_syntax::Span; @@ -139,7 +139,9 @@ fn html_fragment_impl( let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::HtmlFragment, + // No need to know about the `FragmentKind` because we handle both + // uniformly. + RealizationKind::HtmlFragment(&mut FragmentKind::Block), &mut engine, &mut locator, &arenas, @@ -189,7 +191,8 @@ fn handle( }; output.push(element.into()); } else if let Some(elem) = child.to_packed::() { - let children = handle_list(engine, locator, elem.children.iter(&styles))?; + let children = + html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; output.push( HtmlElement::new(tag::p) .with_children(children) diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 76d7b7433..f2c7ebd1e 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -20,13 +20,15 @@ use typst_library::model::ParElem; use typst_library::routines::{Pair, Routines}; use typst_library::text::TextElem; use typst_library::World; +use typst_utils::SliceExt; -use super::{layout_multi_block, layout_single_block}; +use super::{layout_multi_block, layout_single_block, FlowMode}; use crate::modifiers::layout_and_modify; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. #[typst_macros::time] +#[allow(clippy::too_many_arguments)] pub fn collect<'a>( engine: &mut Engine, bump: &'a Bump, @@ -34,6 +36,7 @@ pub fn collect<'a>( locator: Locator<'a>, base: Size, expand: bool, + mode: FlowMode, ) -> SourceResult>> { Collector { engine, @@ -45,7 +48,7 @@ pub fn collect<'a>( output: Vec::with_capacity(children.len()), last_was_par: false, } - .run() + .run(mode) } /// State for collection. @@ -62,7 +65,15 @@ struct Collector<'a, 'x, 'y> { impl<'a> Collector<'a, '_, '_> { /// Perform the collection. - fn run(mut self) -> SourceResult>> { + fn run(self, mode: FlowMode) -> SourceResult>> { + match mode { + FlowMode::Root | FlowMode::Block => self.run_block(), + FlowMode::Inline => self.run_inline(), + } + } + + /// Perform collection for block-level children. + fn run_block(mut self) -> SourceResult>> { for &(child, styles) in self.children { if let Some(elem) = child.to_packed::() { self.output.push(Child::Tag(&elem.tag)); @@ -95,6 +106,43 @@ impl<'a> Collector<'a, '_, '_> { Ok(self.output) } + /// Perform collection for inline-level children. + fn run_inline(mut self) -> SourceResult>> { + // Extract leading and trailing tags. + let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::()); + let inner = &self.children[start..end]; + + // Compute the shared styles, ignoring tags. + let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default(); + + // Layout the lines. + let lines = crate::inline::layout_inline( + self.engine, + inner, + &mut self.locator, + styles, + self.base, + self.expand, + false, + false, + )? + .into_frames(); + + for (c, _) in &self.children[..start] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + self.lines(lines, styles); + + for (c, _) in &self.children[end..] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + Ok(self.output) + } + /// Collect vertical spacing into a relative or fractional child. fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { self.output.push(match elem.amount { @@ -110,24 +158,34 @@ impl<'a> Collector<'a, '_, '_> { elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { - let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); - let spacing = ParElem::spacing_in(styles); - let costs = TextElem::costs_in(styles); - - let lines = crate::layout_inline( + let lines = crate::inline::layout_par( + elem, self.engine, - &elem.children, self.locator.next(&elem.span()), styles, - self.last_was_par, self.base, self.expand, + self.last_was_par, )? .into_frames(); + let spacing = ParElem::spacing_in(styles); self.output.push(Child::Rel(spacing.into(), 4)); + self.lines(lines, styles); + + self.output.push(Child::Rel(spacing.into(), 4)); + self.last_was_par = true; + + Ok(()) + } + + /// Collect laid-out lines. + fn lines(&mut self, lines: Vec, styles: StyleChain<'a>) { + let align = AlignElem::alignment_in(styles).resolve(styles); + let leading = ParElem::leading_in(styles); + let costs = TextElem::costs_in(styles); + // Determine whether to prevent widow and orphans. let len = lines.len(); let prevent_orphans = @@ -166,11 +224,6 @@ impl<'a> Collector<'a, '_, '_> { self.output .push(Child::Line(self.boxed(LineChild { frame, align, need }))); } - - self.output.push(Child::Rel(spacing.into(), 4)); - self.last_was_par = true; - - Ok(()) } /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 3cf66f9e3..76af8f650 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -17,7 +17,9 @@ use typst_library::model::{ use typst_syntax::Span; use typst_utils::{NonZeroExt, Numeric}; -use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; +use super::{ + distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work, +}; /// Composes the contents of a single page/region. A region can have multiple /// columns/subregions. @@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { migratable: bool, ) -> FlowResult<()> { // Footnotes are only supported at the root level. - if !self.config.root { + if self.config.mode != FlowMode::Root { return Ok(()); } diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2f0ec39a9..2acbbcef3 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -25,7 +25,7 @@ use typst_library::layout::{ Regions, Rel, Size, }; use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::TextElem; use typst_library::World; use typst_utils::{NonZeroExt, Numeric}; @@ -140,9 +140,10 @@ fn layout_fragment_impl( engine.route.check_layout_depth().at(content.span())?; + let mut kind = FragmentKind::Block; let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::LayoutFragment, + RealizationKind::LayoutFragment(&mut kind), &mut engine, &mut locator, &arenas, @@ -158,25 +159,46 @@ fn layout_fragment_impl( regions, columns, column_gutter, - false, + kind.into(), ) } +/// The mode a flow can be laid out in. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum FlowMode { + /// A root flow with block-level elements. Like `FlowMode::Block`, but can + /// additionally host footnotes and line numbers. + Root, + /// A flow whose children are block-level elements. + Block, + /// A flow whose children are inline-level elements. + Inline, +} + +impl From for FlowMode { + fn from(value: FragmentKind) -> Self { + match value { + FragmentKind::Inline => Self::Inline, + FragmentKind::Block => Self::Block, + } + } +} + /// Lays out realized content into regions, potentially with columns. #[allow(clippy::too_many_arguments)] -pub(crate) fn layout_flow( +pub fn layout_flow<'a>( engine: &mut Engine, - children: &[Pair], - locator: &mut SplitLocator, - shared: StyleChain, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, mut regions: Regions, columns: NonZeroUsize, column_gutter: Rel, - root: bool, + mode: FlowMode, ) -> SourceResult { // Prepare configuration that is shared across the whole flow. let config = Config { - root, + mode, shared, columns: { let mut count = columns.get(); @@ -195,7 +217,7 @@ pub(crate) fn layout_flow( gap: FootnoteEntry::gap_in(shared), expand: regions.expand.x, }, - line_numbers: root.then(|| LineNumberConfig { + line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig { scope: ParLine::numbering_scope_in(shared), default_clearance: { let width = if PageElem::flipped_in(shared) { @@ -225,6 +247,7 @@ pub(crate) fn layout_flow( locator.next(&()), Size::new(config.columns.width, regions.full), regions.expand.x, + mode, )?; let mut work = Work::new(&children); @@ -318,7 +341,7 @@ impl<'a, 'b> Work<'a, 'b> { struct Config<'x> { /// Whether this is the root flow, which can host footnotes and line /// numbers. - root: bool, + mode: FlowMode, /// The styles shared by the whole flow. This is used for footnotes and line /// numbers. shared: StyleChain<'x>, diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs index 6dfbc9696..e21928d3c 100644 --- a/crates/typst-layout/src/inline/box.rs +++ b/crates/typst-layout/src/inline/box.rs @@ -11,7 +11,7 @@ use typst_utils::Numeric; use crate::flow::unbreakable_pod; use crate::shapes::{clip_rect, fill_and_stroke}; -/// Lay out a box as part of a paragraph. +/// Lay out a box as part of inline layout. #[typst_macros::time(name = "box", span = elem.span())] pub fn layout_box( elem: &Packed, diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 6023f5c63..cbc490ba1 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -1,10 +1,11 @@ -use typst_library::diag::bail; +use typst_library::diag::warning; use typst_library::foundations::{Packed, Resolve}; use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::layout::{ Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; +use typst_library::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, @@ -16,7 +17,7 @@ use super::*; use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify}; // The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. +// full text. const SPACING_REPLACE: &str = " "; // Space const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character @@ -27,7 +28,7 @@ const POP_EMBEDDING: &str = "\u{202C}"; const LTR_ISOLATE: &str = "\u{2066}"; const POP_ISOLATE: &str = "\u{2069}"; -/// A prepared item in a paragraph layout. +/// A prepared item in a inline layout. #[derive(Debug)] pub enum Item<'a> { /// A shaped text run with consistent style and direction. @@ -113,38 +114,44 @@ impl Segment<'_> { } } -/// Collects all text of the paragraph into one string and a collection of -/// segments that correspond to pieces of that string. This also performs -/// string-level preprocessing like case transformations. +/// Collects all text into one string and a collection of segments that +/// correspond to pieces of that string. This also performs string-level +/// preprocessing like case transformations. #[typst_macros::time] pub fn collect<'a>( - children: &'a StyleVec, + children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: &'a StyleChain<'a>, + styles: StyleChain<'a>, region: Size, consecutive: bool, + paragraph: bool, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); let mut quoter = SmartQuoter::new(); - let outer_dir = TextElem::dir_in(*styles); - let first_line_indent = ParElem::first_line_indent_in(*styles); - if !first_line_indent.is_zero() - && consecutive - && AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into() - { - collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false)); - collector.spans.push(1, Span::detached()); + let outer_dir = TextElem::dir_in(styles); + + if paragraph && consecutive { + let first_line_indent = ParElem::first_line_indent_in(styles); + if !first_line_indent.is_zero() + && AlignElem::alignment_in(styles).resolve(styles).x + == outer_dir.start().into() + { + collector.push_item(Item::Absolute(first_line_indent.resolve(styles), false)); + collector.spans.push(1, Span::detached()); + } } - let hang = ParElem::hanging_indent_in(*styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); - collector.spans.push(1, Span::detached()); + if paragraph { + let hang = ParElem::hanging_indent_in(styles); + if !hang.is_zero() { + collector.push_item(Item::Absolute(-hang, false)); + collector.spans.push(1, Span::detached()); + } } - for (child, styles) in children.iter(styles) { + for &(child, styles) in children { let prev_len = collector.full.len(); if child.is::() { @@ -234,7 +241,13 @@ pub fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.push_item(Item::Tag(&elem.tag)); } else { - bail!(child.span(), "unexpected paragraph child"); + // Non-paragraph inline layout should never trigger this since it + // only won't be triggered if we see any non-inline content. + engine.sink.warn(warning!( + child.span(), + "{} may not occur inside of a paragraph and was ignored", + child.func().name() + )); }; let len = collector.full.len() - prev_len; diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 57044f0ec..7ad287c45 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -14,7 +14,7 @@ pub fn finalize( expand: bool, locator: &mut SplitLocator<'_>, ) -> SourceResult { - // Determine the paragraph's width: Full width of the region if we should + // Determine the resulting width: Full width of the region if we should // expand or there's fractional spacing, fit-to-width otherwise. let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index fba4bef80..9f6973807 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -18,12 +18,12 @@ const EN_DASH: char = '–'; const EM_DASH: char = '—'; const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks. -/// A layouted line, consisting of a sequence of layouted paragraph items that -/// are mostly borrowed from the preparation phase. This type enables you to -/// measure the size of a line in a range before committing to building the -/// line's frame. +/// A layouted line, consisting of a sequence of layouted inline items that are +/// mostly borrowed from the preparation phase. This type enables you to measure +/// the size of a line in a range before committing to building the line's +/// frame. /// -/// At most two paragraph items must be created individually for this line: The +/// At most two inline items must be created individually for this line: The /// first and last one since they may be broken apart by the start or end of the /// line, respectively. But even those can partially reuse previous results when /// the break index is safe-to-break per rustybuzz. @@ -430,7 +430,7 @@ pub fn commit( let mut offset = Abs::zero(); // We always build the line from left to right. In an LTR paragraph, we must - // thus add the hanging indent to the offset. When the paragraph is RTL, the + // thus add the hanging indent to the offset. In an RTL paragraph, the // hanging indent arises naturally due to the line width. if p.dir == Dir::LTR { offset += p.hang; @@ -631,7 +631,7 @@ fn overhang(c: char) -> f64 { } } -/// A collection of owned or borrowed paragraph items. +/// A collection of owned or borrowed inline items. pub struct Items<'a>(Vec>); impl<'a> Items<'a> { diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 7b66fcdb4..87113c689 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::*; -/// The cost of a line or paragraph layout. +/// The cost of a line or inline layout. type Cost = f64; // Cost parameters. @@ -104,7 +104,7 @@ impl Breakpoint { } } -/// Breaks the paragraph into lines. +/// Breaks the text into lines. pub fn linebreak<'a>( engine: &Engine, p: &'a Preparation<'a>, @@ -181,13 +181,12 @@ fn linebreak_simple<'a>( /// lines with hyphens even more. /// /// To find the layout with the minimal total cost the algorithm uses dynamic -/// programming: For each possible breakpoint it determines the optimal -/// paragraph layout _up to that point_. It walks over all possible start points -/// for a line ending at that point and finds the one for which the cost of the -/// line plus the cost of the optimal paragraph up to the start point (already -/// computed and stored in dynamic programming table) is minimal. The final -/// result is simply the layout determined for the last breakpoint at the end of -/// text. +/// programming: For each possible breakpoint, it determines the optimal layout +/// _up to that point_. It walks over all possible start points for a line +/// ending at that point and finds the one for which the cost of the line plus +/// the cost of the optimal layout up to the start point (already computed and +/// stored in dynamic programming table) is minimal. The final result is simply +/// the layout determined for the last breakpoint at the end of text. #[typst_macros::time] fn linebreak_optimized<'a>( engine: &Engine, @@ -215,7 +214,7 @@ fn linebreak_optimized_bounded<'a>( metrics: &CostMetrics, upper_bound: Cost, ) -> Vec> { - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry<'a> { pred: usize, total: Cost, @@ -321,7 +320,7 @@ fn linebreak_optimized_bounded<'a>( // This should only happen if our bound was faulty. Which shouldn't happen! if table[idx].end != p.text.len() { #[cfg(debug_assertions)] - panic!("bounded paragraph layout is incomplete"); + panic!("bounded inline layout is incomplete"); #[cfg(not(debug_assertions))] return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY); @@ -342,7 +341,7 @@ fn linebreak_optimized_bounded<'a>( /// (which is costly) to determine costs, it determines approximate costs using /// cumulative arrays. /// -/// This results in a likely good paragraph layouts, for which we then compute +/// This results in a likely good inline layouts, for which we then compute /// the exact cost. This cost is an upper bound for proper optimized /// linebreaking. We can use it to heavily prune the search space. #[typst_macros::time] @@ -355,7 +354,7 @@ fn linebreak_optimized_approximate( // Determine the cumulative estimation metrics. let estimates = Estimates::compute(p); - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry { pred: usize, total: Cost, @@ -862,7 +861,7 @@ struct CostMetrics { } impl CostMetrics { - /// Compute shared metrics for paragraph optimization. + /// Compute shared metrics for inline layout optimization. fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index bedc54d63..83ca82bf2 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,11 +13,11 @@ pub use self::box_::layout_box; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{StyleChain, StyleVec}; -use typst_library::introspection::{Introspector, Locator, LocatorLink}; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; use typst_library::layout::{Fragment, Size}; use typst_library::model::ParElem; -use typst_library::routines::Routines; +use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::World; use self::collect::{collect, Item, Segment, SpanMapper}; @@ -34,18 +34,18 @@ use self::shaping::{ /// Range of a substring of text. type Range = std::ops::Range; -/// Layouts content inline. -pub fn layout_inline( +/// Layouts the paragraph. +pub fn layout_par( + elem: &Packed, engine: &mut Engine, - children: &StyleVec, locator: Locator, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + consecutive: bool, ) -> SourceResult { - layout_inline_impl( - children, + layout_par_impl( + elem, engine.routines, engine.world, engine.introspector, @@ -54,17 +54,17 @@ pub fn layout_inline( engine.route.track(), locator.track(), styles, - consecutive, region, expand, + consecutive, ) } -/// The internal, memoized implementation of `layout_inline`. +/// The internal, memoized implementation of `layout_par`. #[comemo::memoize] #[allow(clippy::too_many_arguments)] -fn layout_inline_impl( - children: &StyleVec, +fn layout_par_impl( + elem: &Packed, routines: &Routines, world: Tracked, introspector: Tracked, @@ -73,12 +73,12 @@ fn layout_inline_impl( route: Tracked, locator: Tracked, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + consecutive: bool, ) -> SourceResult { let link = LocatorLink::new(locator); - let locator = Locator::link(&link); + let mut locator = Locator::link(&link).split(); let mut engine = Engine { routines, world, @@ -88,18 +88,51 @@ fn layout_inline_impl( route: Route::extend(route), }; - let mut locator = locator.split(); + let arenas = Arenas::default(); + let children = (engine.routines.realize)( + RealizationKind::LayoutPar, + &mut engine, + &mut locator, + &arenas, + &elem.body, + styles, + )?; + layout_inline( + &mut engine, + &children, + &mut locator, + styles, + region, + expand, + true, + consecutive, + ) +} + +/// Lays out realized content with inline layout. +#[allow(clippy::too_many_arguments)] +pub fn layout_inline<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + styles: StyleChain<'a>, + region: Size, + expand: bool, + paragraph: bool, + consecutive: bool, +) -> SourceResult { // Collect all text into one string for BiDi analysis. let (text, segments, spans) = - collect(children, &mut engine, &mut locator, &styles, region, consecutive)?; + collect(children, engine, locator, styles, region, consecutive, paragraph)?; - // Perform BiDi analysis and then prepares paragraph layout. - let p = prepare(&mut engine, children, &text, segments, spans, styles)?; + // Perform BiDi analysis and performs some preparation steps before we + // proceed to line breaking. + let p = prepare(engine, children, &text, segments, spans, styles, paragraph)?; - // Break the paragraph into lines. - let lines = linebreak(&engine, &p, region.x - p.hang); + // Break the text into lines. + let lines = linebreak(engine, &p, region.x - p.hang); // Turn the selected lines into frames. - finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator) + finalize(engine, &p, &lines, styles, region, expand, locator) } diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 2dd79aecf..e26c9b147 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,23 +1,26 @@ use typst_library::foundations::{Resolve, Smart}; use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; use typst_library::model::Linebreaks; +use typst_library::routines::Pair; use typst_library::text::{Costs, Lang, TextElem}; +use typst_utils::SliceExt; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; -/// A paragraph representation in which children are already layouted and text -/// is already preshaped. +/// A representation in which children are already layouted and text is already +/// preshaped. /// /// In many cases, we can directly reuse these results when constructing a line. /// Only when a line break falls onto a text index that is not safe-to-break per /// rustybuzz, we have to reshape that portion. pub struct Preparation<'a> { - /// The paragraph's full text. + /// The full text. pub text: &'a str, - /// Bidirectional text embedding levels for the paragraph. + /// Bidirectional text embedding levels. /// - /// This is `None` if the paragraph is BiDi-uniform (all the base direction). + /// This is `None` if all text directions are uniform (all the base + /// direction). pub bidi: Option>, /// Text runs, spacing and layouted elements. pub items: Vec<(Range, Item<'a>)>, @@ -33,15 +36,15 @@ pub struct Preparation<'a> { pub dir: Dir, /// The text language if it's the same for all children. pub lang: Option, - /// The paragraph's resolved horizontal alignment. + /// The resolved horizontal alignment. pub align: FixedAlignment, - /// Whether to justify the paragraph. + /// Whether to justify text. pub justify: bool, - /// The paragraph's hanging indent. + /// Hanging indent to apply. pub hang: Abs, /// Whether to add spacing between CJK and Latin characters. pub cjk_latin_spacing: bool, - /// Whether font fallback is enabled for this paragraph. + /// Whether font fallback is enabled. pub fallback: bool, /// How to determine line breaks. pub linebreaks: Smart, @@ -71,17 +74,18 @@ impl<'a> Preparation<'a> { } } -/// Performs BiDi analysis and then prepares paragraph layout by building a +/// Performs BiDi analysis and then prepares further layout by building a /// representation on which we can do line breaking without layouting each and /// every line from scratch. #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &'a StyleVec, + children: &[Pair<'a>], text: &'a str, segments: Vec>, spans: SpanMapper, styles: StyleChain<'a>, + paragraph: bool, ) -> SourceResult> { let dir = TextElem::dir_in(styles); let default_level = match dir { @@ -125,19 +129,22 @@ pub fn prepare<'a>( add_cjk_latin_spacing(&mut items); } + // Only apply hanging indent to real paragraphs. + let hang = if paragraph { ParElem::hanging_indent_in(styles) } else { Abs::zero() }; + Ok(Preparation { text, bidi: is_bidi.then_some(bidi), items, indices, spans, - hyphenate: children.shared_get(styles, TextElem::hyphenate_in), + hyphenate: shared_get(children, styles, TextElem::hyphenate_in), costs: TextElem::costs_in(styles), dir, - lang: children.shared_get(styles, TextElem::lang_in), + lang: shared_get(children, styles, TextElem::lang_in), align: AlignElem::alignment_in(styles).resolve(styles).x, justify: ParElem::justify_in(styles), - hang: ParElem::hanging_indent_in(styles), + hang, cjk_latin_spacing, fallback: TextElem::fallback_in(styles), linebreaks: ParElem::linebreaks_in(styles), @@ -145,6 +152,19 @@ pub fn prepare<'a>( }) } +/// Get a style property, but only if it is the same for all of the children. +fn shared_get( + children: &[Pair], + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, +) -> Option { + let value = getter(styles); + children + .group_by_key(|&(_, s)| s) + .all(|(s, _)| getter(s) == value) + .then_some(value) +} + /// Add some spacing between Han characters and western characters. See /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// in Horizontal Written Mode diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 2ed95f14f..b688981ae 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -29,7 +29,7 @@ use crate::modifiers::{FrameModifiers, FrameModify}; /// frame. #[derive(Clone)] pub struct ShapedText<'a> { - /// The start of the text in the full paragraph. + /// The start of the text in the full text. pub base: usize, /// The text that was shaped. pub text: &'a str, @@ -66,9 +66,9 @@ pub struct ShapedGlyph { pub y_offset: Em, /// The adjustability of the glyph. pub adjustability: Adjustability, - /// The byte range of this glyph's cluster in the full paragraph. A cluster - /// is a sequence of one or multiple glyphs that cannot be separated and - /// must always be treated as a union. + /// The byte range of this glyph's cluster in the full inline layout. A + /// cluster is a sequence of one or multiple glyphs that cannot be separated + /// and must always be treated as a union. /// /// The range values of the glyphs in a [`ShapedText`] should not overlap /// with each other, and they should be monotonically increasing (for @@ -405,7 +405,7 @@ impl<'a> ShapedText<'a> { /// Reshape a range of the shaped text, reusing information from this /// shaping process if possible. /// - /// The text `range` is relative to the whole paragraph. + /// The text `range` is relative to the whole inline layout. pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> { let text = &self.text[text_range.start - self.base..text_range.end - self.base]; if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 56d7afe11..443e90d61 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -17,7 +17,6 @@ mod transforms; pub use self::flow::{layout_columns, layout_fragment, layout_frame}; pub use self::grid::{layout_grid, layout_table}; pub use self::image::layout_image; -pub use self::inline::{layout_box, layout_inline}; pub use self::lists::{layout_enum, layout_list}; pub use self::math::{layout_equation_block, layout_equation_inline}; pub use self::pad::layout_pad; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 63127474b..f8d910abf 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -6,7 +6,7 @@ use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::grid::resolve::{Cell, CellGrid}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; -use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; +use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem}; use typst_library::text::TextElem; use crate::grid::GridLayouter; @@ -22,8 +22,9 @@ pub fn layout_list( ) -> SourceResult { let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -41,11 +42,17 @@ pub fn layout_list( let mut locator = locator.split(); for item in &elem.children { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(ListElem::set_depth(Depth(1))), + body.styled(ListElem::set_depth(Depth(1))), locator.next(&item.body.span()), )); } @@ -78,8 +85,9 @@ pub fn layout_enum( let reversed = elem.reversed(styles); let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -124,11 +132,17 @@ pub fn layout_enum( let resolved = resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(EnumElem::set_parents(smallvec![number])), + body.styled(EnumElem::set_parents(smallvec![number])), locator.next(&item.body.span()), )); number = diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index 19176ee88..bf8235411 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -2,6 +2,7 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Abs, Axis, Rel}; use typst_library::math::{EquationElem, LrElem, MidElem}; +use typst_utils::SliceExt; use unicode_math_class::MathClass; use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; @@ -29,15 +30,7 @@ pub fn layout_lr( let mut fragments = ctx.layout_into_fragments(body, styles)?; // Ignore leading and trailing ignorant fragments. - let start_idx = fragments - .iter() - .position(|f| !f.is_ignorant()) - .unwrap_or(fragments.len()); - let end_idx = fragments - .iter() - .skip(start_idx) - .rposition(|f| !f.is_ignorant()) - .map_or(start_idx, |i| start_idx + i + 1); + let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant()); let inner_fragments = &mut fragments[start_idx..end_idx]; let axis = scaled!(ctx, styles, axis_height); diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 702816ee6..e5a3d94c9 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -202,8 +202,7 @@ pub fn layout_equation_block( let counter = Counter::of(EquationElem::elem()) .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .spanned(span); - let number = - (engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?; + let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?; static NUMBER_GUTTER: Em = Em::new(0.5); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); @@ -619,7 +618,7 @@ fn layout_box( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let frame = (ctx.engine.routines.layout_box)( + let frame = crate::inline::layout_box( elem, ctx.engine, ctx.locator.next(&elem.span()), @@ -692,7 +691,7 @@ fn layout_external( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { - (ctx.engine.routines.layout_frame)( + crate::layout_frame( ctx.engine, content, ctx.locator.next(&content.span()), diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 6b9703aa2..5897c3c0c 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -1,8 +1,8 @@ use std::f64::consts::SQRT_2; -use ecow::{eco_vec, EcoString}; +use ecow::EcoString; use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, StyleVec, SymbolElem}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -100,14 +100,15 @@ fn layout_inline_text( // because it will be placed somewhere probably not at the left margin // it will overflow. So emulate an `hbox` instead and allow the // paragraph to extend as far as needed. - let frame = (ctx.engine.routines.layout_inline)( + let frame = crate::inline::layout_inline( ctx.engine, - &StyleVec::wrap(eco_vec![elem]), - ctx.locator.next(&span), + &[(&elem, styles)], + &mut ctx.locator.next(&span).split(), styles, - false, Size::splat(Abs::inf()), false, + false, + false, )? .into_frame(); diff --git a/crates/typst-layout/src/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs index 0bbae9f4c..8eab18a62 100644 --- a/crates/typst-layout/src/pages/collect.rs +++ b/crates/typst-layout/src/pages/collect.rs @@ -23,7 +23,7 @@ pub enum Item<'a> { /// things like tags and weak pagebreaks. pub fn collect<'a>( mut children: &'a mut [Pair<'a>], - mut locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, mut initial: StyleChain<'a>, ) -> Vec> { // The collected page-level items. diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 27002a6c9..14dc0f3fb 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -83,7 +83,7 @@ fn layout_document_impl( styles, )?; - let pages = layout_pages(&mut engine, &mut children, locator, styles)?; + let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?; let introspector = Introspector::paged(&pages); Ok(PagedDocument { pages, info, introspector }) @@ -93,7 +93,7 @@ fn layout_document_impl( fn layout_pages<'a>( engine: &mut Engine, children: &'a mut [Pair<'a>], - locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, styles: StyleChain<'a>, ) -> SourceResult> { // Slice up the children into logical parts. diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs index 79ff5ab05..6d2d29da5 100644 --- a/crates/typst-layout/src/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -19,7 +19,7 @@ use typst_library::visualize::Paint; use typst_library::World; use typst_utils::Numeric; -use crate::flow::layout_flow; +use crate::flow::{layout_flow, FlowMode}; /// A mostly finished layout for one page. Needs only knowledge of its exact /// page number to be finalized into a `Page`. (Because the margins can depend @@ -181,7 +181,7 @@ fn layout_page_run_impl( Regions::repeat(area, area.map(Abs::is_finite)), PageElem::columns_in(styles), ColumnsElem::gutter_in(styles), - true, + FlowMode::Root, )?; // Layouts a single marginal. diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 37094dcd8..983803300 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -776,107 +776,6 @@ impl<'a> Iterator for Links<'a> { } } -/// A sequence of elements with associated styles. -#[derive(Clone, PartialEq, Hash)] -pub struct StyleVec { - /// The elements themselves. - elements: EcoVec, - /// A run-length encoded list of style lists. - /// - /// Each element is a (styles, count) pair. Any elements whose - /// style falls after the end of this list is considered to - /// have an empty style list. - styles: EcoVec<(Styles, usize)>, -} - -impl StyleVec { - /// Create a style vector from an unstyled vector content. - pub fn wrap(elements: EcoVec) -> Self { - Self { elements, styles: EcoVec::new() } - } - - /// Create a `StyleVec` from a list of content with style chains. - pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { - let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); - let depth = trunk.links().count(); - - let mut elements = EcoVec::with_capacity(buf.len()); - let mut styles = EcoVec::<(Styles, usize)>::new(); - let mut last: Option<(StyleChain<'a>, usize)> = None; - - for &(element, chain) in buf { - elements.push(element.clone()); - - if let Some((prev, run)) = &mut last { - if chain == *prev { - *run += 1; - } else { - styles.push((prev.suffix(depth), *run)); - last = Some((chain, 1)); - } - } else { - last = Some((chain, 1)); - } - } - - if let Some((last, run)) = last { - let skippable = styles.is_empty() && last == trunk; - if !skippable { - styles.push((last.suffix(depth), run)); - } - } - - (StyleVec { elements, styles }, trunk) - } - - /// Whether there are no elements. - pub fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - /// The number of elements. - pub fn len(&self) -> usize { - self.elements.len() - } - - /// Iterate over the contained content and style chains. - pub fn iter<'a>( - &'a self, - outer: &'a StyleChain<'_>, - ) -> impl Iterator)> { - static EMPTY: Styles = Styles::new(); - self.elements - .iter() - .zip( - self.styles - .iter() - .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) - .chain(std::iter::repeat(&EMPTY)), - ) - .map(|(element, local)| (element, outer.chain(local))) - } - - /// Get a style property, but only if it is the same for all children of the - /// style vector. - pub fn shared_get( - &self, - styles: StyleChain<'_>, - getter: fn(StyleChain) -> T, - ) -> Option { - let value = getter(styles); - self.styles - .iter() - .all(|(local, _)| getter(styles.chain(local)) == value) - .then_some(value) - } -} - -impl Debug for StyleVec { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - f.debug_list().entries(&self.elements).finish() - } -} - /// A property that is resolved with other properties from the style chain. pub trait Resolve { /// The type of the resolved output. diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index c8c74269b..725f177b7 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst-library/src/layout/container.rs @@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke}; /// An inline-level container that sizes content. /// /// All elements except inline math, text, and boxes are block-level and cannot -/// occur inside of a paragraph. The box function can be used to integrate such -/// elements into a paragraph. Boxes take the size of their contents by default -/// but can also be sized explicitly. +/// occur inside of a [paragraph]($par). The box function can be used to +/// integrate such elements into a paragraph. Boxes take the size of their +/// contents by default but can also be sized explicitly. /// /// # Example /// ```example @@ -184,6 +184,10 @@ pub enum InlineItem { /// Such a container can be used to separate content, size it, and give it a /// background or border. /// +/// Blocks are also the primary way to control whether text becomes part of a +/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph) +/// for more details. +/// /// # Examples /// With a block, you can give a background to content while still allowing it /// to break across multiple pages. diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index 1e346280a..32be216a4 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; /// A mathematical equation. /// -/// Can be displayed inline with text or as a separate block. +/// Can be displayed inline with text or as a separate block. An equation +/// becomes block-level through the presence of at least one space after the +/// opening dollar sign and one space before the closing dollar sign. /// /// # Example /// ```example diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 762a97fd9..a391e5804 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -17,7 +17,7 @@ use hayagriva::{ use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; use typst_syntax::{Span, Spanned}; -use typst_utils::{ManuallyHash, NonZeroExt, PicoStr}; +use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; @@ -29,7 +29,7 @@ use crate::foundations::{ use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, - Sizing, TrackSizings, VElem, + Sides, Sizing, TrackSizings, }; use crate::loading::{DataSource, Load}; use crate::model::{ @@ -206,19 +206,20 @@ impl Show for Packed { const COLUMN_GUTTER: Em = Em::new(0.65); const INDENT: Em = Em::new(1.5); + let span = self.span(); + let mut seq = vec![]; if let Some(title) = self.title(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) }) { seq.push( HeadingElem::new(title) .with_depth(NonZeroUsize::ONE) .pack() - .spanned(self.span()), + .spanned(span), ); } - let span = self.span(); let works = Works::generate(engine).at(span)?; let references = works .references @@ -226,10 +227,9 @@ impl Show for Packed { .ok_or("CSL style is not suitable for bibliographies") .at(span)?; - let row_gutter = ParElem::spacing_in(styles); - let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack(); - if references.iter().any(|(prefix, _)| prefix.is_some()) { + let row_gutter = ParElem::spacing_in(styles); + let mut cells = vec![]; for (prefix, reference) in references { cells.push(GridChild::Item(GridItem::Cell( @@ -246,23 +246,27 @@ impl Show for Packed { .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) .pack() - .spanned(self.span()), + .spanned(span), ); } else { - for (i, (_, reference)) in references.iter().enumerate() { - if i > 0 { - seq.push(row_gutter_elem.clone()); - } - seq.push(reference.clone()); + for (_, reference) in references { + let realized = reference.clone(); + let block = if works.hanging_indent { + let body = HElem::new((-INDENT).into()).pack() + realized; + let inset = Sides::default() + .with(TextElem::dir_in(styles).start(), Some(INDENT.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + + seq.push(block.pack().spanned(span)); } } - let mut content = Content::sequence(seq); - if works.hanging_indent { - content = content.styled(ParElem::set_hanging_indent(INDENT.into())); - } - - Ok(content) + Ok(Content::sequence(seq)) } } diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index 4dc834ab7..a4126e72c 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -11,7 +11,9 @@ use crate::foundations::{ }; use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; -use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; +use crate::model::{ + ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem, +}; /// A numbered list. /// @@ -226,6 +228,8 @@ impl EnumElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { let mut elem = HtmlElem::new(tag::ol); if self.reversed(styles) { @@ -239,7 +243,12 @@ impl Show for Packed { if let Some(nr) = item.number(styles) { li = li.with_attr(attr::value, eco_format!("{nr}")); } - li.with_body(Some(item.body.clone())).pack().spanned(item.span()) + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + li.with_body(Some(body)).pack().spanned(item.span()) })); return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); } @@ -249,7 +258,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index ce7460c9b..78a79a8e2 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -19,7 +19,9 @@ use crate::layout::{ AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem, PlacementScope, VAlignment, VElem, }; -use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; +use crate::model::{ + Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement, +}; use crate::text::{Lang, Region, TextElem}; use crate::visualize::ImageElem; @@ -328,6 +330,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "figure", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); let target = TargetElem::target_in(styles); let mut realized = self.body.clone(); @@ -341,24 +344,27 @@ impl Show for Packed { seq.push(first); if !target.is_html() { let v = VElem::new(self.gap(styles).into()).with_weak(true); - seq.push(v.pack().spanned(self.span())) + seq.push(v.pack().spanned(span)) } seq.push(second); realized = Content::sequence(seq) } + // Ensure that the body is considered a paragraph. + realized += ParbreakElem::shared().clone().spanned(span); + if target.is_html() { return Ok(HtmlElem::new(tag::figure) .with_body(Some(realized)) .pack() - .spanned(self.span())); + .spanned(span)); } // Wrap the contents in a block. realized = BlockElem::new() .with_body(Some(BlockBody::Content(realized))) .pack() - .spanned(self.span()); + .spanned(span); // Wrap in a float. if let Some(align) = self.placement(styles) { @@ -367,10 +373,10 @@ impl Show for Packed { .with_scope(self.scope(styles)) .with_float(true) .pack() - .spanned(self.span()); + .spanned(span); } else if self.scope(styles) == PlacementScope::Parent { bail!( - self.span(), + span, "parent-scoped placement is only available for floating figures"; hint: "you can enable floating placement with `figure(placement: auto, ..)`" ); @@ -604,14 +610,17 @@ impl Show for Packed { realized = supplement + numbers + self.get_separator(styles) + realized; } - if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::figcaption) + Ok(if TargetElem::target_in(styles).is_html() { + HtmlElem::new(tag::figcaption) .with_body(Some(realized)) .pack() - .spanned(self.span())); - } - - Ok(realized) + .spanned(self.span()) + } else { + BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()) + }) } } diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index f3b2a19eb..dfa3933bb 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -310,11 +310,9 @@ impl Show for Packed { impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { - let text_size = Em::new(0.85); - let leading = Em::new(0.5); let mut out = Styles::new(); - out.set(ParElem::set_leading(leading.into())); - out.set(TextElem::set_size(TextSize(text_size.into()))); + out.set(ParElem::set_leading(Em::new(0.5).into())); + out.set(TextElem::set_size(TextSize(Em::new(0.85).into()))); out } } diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index 1e369d541..d93ec9172 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -8,7 +8,7 @@ use crate::foundations::{ }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockElem, Em, Length, VElem}; -use crate::model::ParElem; +use crate::model::{ParElem, ParbreakElem}; use crate::text::TextElem; /// A bullet list. @@ -141,11 +141,18 @@ impl ListElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { return Ok(HtmlElem::new(tag::ul) .with_body(Some(Content::sequence(self.children.iter().map(|item| { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } HtmlElem::new(tag::li) - .with_body(Some(item.body.clone())) + .with_body(Some(body)) .pack() .spanned(item.span()) })))) @@ -158,7 +165,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 0db056e40..1214f2b0e 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -297,7 +297,6 @@ impl ShowSet for Packed { let mut out = Styles::new(); out.set(HeadingElem::set_outlined(false)); out.set(HeadingElem::set_numbering(None)); - out.set(ParElem::set_first_line_indent(Em::new(0.0).into())); out.set(ParElem::set_justify(false)); out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into()))); // Makes the outline itself available to its entries. Should be diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs index 8b82abdf7..0bdbe4ea6 100644 --- a/crates/typst-library/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -1,22 +1,78 @@ -use std::fmt::{self, Debug, Formatter}; - use typst_utils::singleton; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, - StyleVec, Unlabellable, + elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Smart, + Unlabellable, }; use crate::introspection::{Count, CounterUpdate, Locatable}; use crate::layout::{Em, HAlignment, Length, OuterHAlignment}; use crate::model::Numbering; -/// Arranges text, spacing and inline-level elements into a paragraph. +/// A logical subdivison of textual content. /// -/// Although this function is primarily used in set rules to affect paragraph -/// properties, it can also be used to explicitly render its argument onto a -/// paragraph of its own. +/// Typst automatically collects _inline-level_ elements into paragraphs. +/// Inline-level elements include [text], [horizontal spacing]($h), +/// [boxes]($box), and [inline equations]($math.equation). +/// +/// To separate paragraphs, use a blank line (or an explicit [`parbreak`]). +/// Paragraphs are also automatically interrupted by any block-level element +/// (like [`block`], [`place`], or anything that shows itself as one of these). +/// +/// The `par` element is primarily used in set rules to affect paragraph +/// properties, but it can also be used to explicitly display its argument as a +/// paragraph of its own. Then, the paragraph's body may not contain any +/// block-level content. +/// +/// # Boxes and blocks +/// As explained above, usually paragraphs only contain inline-level content. +/// However, you can integrate any kind of block-level content into a paragraph +/// by wrapping it in a [`box`]. +/// +/// Conversely, you can separate inline-level content from a paragraph by +/// wrapping it in a [`block`]. In this case, it will not become part of any +/// paragraph at all. Read the following section for an explanation of why that +/// matters and how it differs from just adding paragraph breaks around the +/// content. +/// +/// # What becomes a paragraph? +/// When you add inline-level content to your document, Typst will automatically +/// wrap it in paragraphs. However, a typical document also contains some text +/// that is not semantically part of a paragraph, for example in a heading or +/// caption. +/// +/// The rules for when Typst wraps inline-level content in a paragraph are as +/// follows: +/// +/// - All text at the root of a document is wrapped in paragraphs. +/// +/// - Text in a container (like a `block`) is only wrapped in a paragraph if the +/// container holds any block-level content. If all of the contents are +/// inline-level, no paragraph is created. +/// +/// In the laid-out document, it's not immediately visible whether text became +/// part of a paragraph. However, it is still important for various reasons: +/// +/// - Certain paragraph styling like `first-line-indent` will only apply to +/// proper paragraphs, not any text. Similarly, `par` show rules of course +/// only trigger on paragraphs. +/// +/// - A proper distinction between paragraphs and other text helps people who +/// rely on assistive technologies (such as screen readers) navigate and +/// understand the document properly. Currently, this only applies to HTML +/// export since Typst does not yet output accessible PDFs, but support for +/// this is planned for the near future. +/// +/// - HTML export will generate a `

` tag only for paragraphs. +/// +/// When creating custom reusable components, you can and should take charge +/// over whether Typst creates paragraphs. By wrapping text in a [`block`] +/// instead of just adding paragraph breaks around it, you can force the absence +/// of a paragraph. Conversely, by adding a [`parbreak`] after some content in a +/// container, you can force it to become a paragraph even if it's just one +/// word. This is, for example, what [non-`tight`]($list.tight) lists do to +/// force their items to become paragraphs. /// /// # Example /// ```example @@ -37,7 +93,7 @@ use crate::model::Numbering; /// let $a$ be the smallest of the /// three integers. Then, we ... /// ``` -#[elem(scope, title = "Paragraph", Debug, Construct)] +#[elem(scope, title = "Paragraph")] pub struct ParElem { /// The spacing between lines. /// @@ -53,7 +109,6 @@ pub struct ParElem { /// distribution of the top- and bottom-edge values affects the bounds of /// the first and last line. #[resolve] - #[ghost] #[default(Em::new(0.65).into())] pub leading: Length, @@ -68,7 +123,6 @@ pub struct ParElem { /// takes precedence over the paragraph spacing. Headings, for instance, /// reduce the spacing below them by default for a better look. #[resolve] - #[ghost] #[default(Em::new(1.2).into())] pub spacing: Length, @@ -81,7 +135,6 @@ pub struct ParElem { /// Note that the current [alignment]($align.alignment) still has an effect /// on the placement of the last line except if it ends with a /// [justified line break]($linebreak.justify). - #[ghost] #[default(false)] pub justify: bool, @@ -106,7 +159,6 @@ pub struct ParElem { /// challenging to break in a visually /// pleasing way. /// ``` - #[ghost] pub linebreaks: Smart, /// The indent the first line of a paragraph should have. @@ -118,23 +170,15 @@ pub struct ParElem { /// space between paragraphs or by indented first lines. Consider reducing /// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading) /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`). - #[ghost] pub first_line_indent: Length, - /// The indent all but the first line of a paragraph should have. - #[ghost] + /// The indent that all but the first line of a paragraph should have. #[resolve] pub hanging_indent: Length, /// The contents of the paragraph. - #[external] #[required] pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: StyleVec, } #[scope] @@ -143,28 +187,6 @@ impl ParElem { type ParLine; } -impl Construct for ParElem { - fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult { - // The paragraph constructor is special: It doesn't create a paragraph - // element. Instead, it just ensures that the passed content lives in a - // separate paragraph and styles it. - let styles = Self::set(engine, args)?; - let body = args.expect::("body")?; - Ok(Content::sequence([ - ParbreakElem::shared().clone(), - body.styled_with_map(styles), - ParbreakElem::shared().clone(), - ])) - } -} - -impl Debug for ParElem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Par ")?; - self.children.fmt(f) - } -} - /// How to determine line breaks in a paragraph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum Linebreaks { diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 79e9b4e36..919ab12c7 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -212,17 +212,24 @@ impl Show for Packed { .pack() .spanned(self.span()), }; - let attribution = - [TextElem::packed('—'), SpaceElem::shared().clone(), attribution]; + let attribution = Content::sequence([ + TextElem::packed('—'), + SpaceElem::shared().clone(), + attribution, + ]); - if !html { - // Use v(0.9em, weak: true) to bring the attribution closer - // to the quote. + if html { + realized += attribution; + } else { + // Bring the attribution a bit closer to the quote. let gap = Spacing::Rel(Em::new(0.9).into()); let v = VElem::new(gap).with_weak(true).pack(); realized += v; + realized += BlockElem::new() + .with_body(Some(BlockBody::Content(attribution))) + .pack() + .aligned(Alignment::END); } - realized += Content::sequence(attribution).aligned(Alignment::END); } if !html { diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index c91eeb17a..9a2ed6aad 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -8,7 +8,7 @@ use crate::foundations::{ }; use crate::html::{tag, HtmlElem}; use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::{ListItemLike, ListLike, ParElem}; +use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; use crate::text::TextElem; /// A list of terms and their descriptions. @@ -116,17 +116,25 @@ impl TermsElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let span = self.span(); + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { return Ok(HtmlElem::new(tag::dl) .with_body(Some(Content::sequence(self.children.iter().flat_map( |item| { + // Text in wide term lists shall always turn into paragraphs. + let mut description = item.description.clone(); + if !tight { + description += ParbreakElem::shared(); + } + [ HtmlElem::new(tag::dt) .with_body(Some(item.term.clone())) .pack() .spanned(item.term.span()), HtmlElem::new(tag::dd) - .with_body(Some(item.description.clone())) + .with_body(Some(description)) .pack() .spanned(item.description.span()), ] @@ -139,7 +147,7 @@ impl Show for Packed { let indent = self.indent(styles); let hanging_indent = self.hanging_indent(styles); let gutter = self.spacing(styles).unwrap_or_else(|| { - if self.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -157,6 +165,12 @@ impl Show for Packed { seq.push(child.term.clone().strong()); seq.push((*separator).clone()); seq.push(child.description.clone()); + + // Text in wide term lists shall always turn into paragraphs. + if !tight { + seq.push(ParbreakElem::shared().clone()); + } + children.push(StackChild::Block(Content::sequence(seq))); } @@ -168,7 +182,7 @@ impl Show for Packed { .spanned(span) .padded(padding); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()) .with_weak(true) diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index a11268604..b283052a4 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -10,8 +10,7 @@ use typst_utils::LazyHash; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, StyleVec, - Styles, Value, + Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::layout::{ @@ -104,26 +103,6 @@ routines! { region: Region, ) -> SourceResult - /// Lays out inline content. - fn layout_inline( - engine: &mut Engine, - children: &StyleVec, - locator: Locator, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult - - /// Lays out a [`BoxElem`]. - fn layout_box( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult - /// Lays out a [`ListElem`]. fn layout_list( elem: &Packed, @@ -348,17 +327,62 @@ pub enum RealizationKind<'a> { /// This the root realization for layout. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. LayoutDocument(&'a mut DocumentInfo), - /// A nested realization in a container (e.g. a `block`). - LayoutFragment, + /// A nested realization in a container (e.g. a `block`). Requires a mutable + /// reference to an enum that will be set to `FragmentKind::Inline` if the + /// fragment's content was fully inline. + LayoutFragment(&'a mut FragmentKind), + /// A nested realization in a paragraph (i.e. a `par`) + LayoutPar, /// This the root realization for HTML. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. HtmlDocument(&'a mut DocumentInfo), - /// A nested realization in a container (e.g. a `block`). - HtmlFragment, + /// A nested realization in a container (e.g. a `block`). Requires a mutable + /// reference to an enum that will be set to `FragmentKind::Inline` if the + /// fragment's content was fully inline. + HtmlFragment(&'a mut FragmentKind), /// A realization within math. Math, } +impl RealizationKind<'_> { + /// It this a realization for HTML export? + pub fn is_html(&self) -> bool { + matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_)) + } + + /// It this a realization for a container? + pub fn is_fragment(&self) -> bool { + matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_)) + } + + /// If this is a document-level realization, accesses the document info. + pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> { + match self { + Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info), + _ => None, + } + } + + /// If this is a container-level realization, accesses the fragment kind. + pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> { + match self { + Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind), + _ => None, + } + } +} + +/// The kind of fragment output that realization produced. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum FragmentKind { + /// The fragment's contents were fully inline, and as a result, the output + /// elements are too. + Inline, + /// The fragment contained non-inline content, so inline content was forced + /// into paragraphs, and as a result, the output elements are not inline. + Block, +} + /// Temporary storage arenas for lifetime extension during realization. /// /// Must be kept live while the content returned from realization is processed. diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index ff42c3e95..754e89aac 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -15,8 +15,8 @@ use typst_library::diag::{bail, At, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{ Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, - SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles, - SymbolElem, Synthesize, Transformation, + SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, + Synthesize, Transformation, }; use typst_library::html::{tag, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; @@ -28,7 +28,7 @@ use typst_library::model::{ CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike, ParElem, ParbreakElem, TermsElem, }; -use typst_library::routines::{Arenas, Pair, RealizationKind}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_syntax::Span; use typst_utils::{SliceExt, SmallBitSet}; @@ -48,17 +48,18 @@ pub fn realize<'a>( locator, arenas, rules: match kind { - RealizationKind::LayoutDocument(_) | RealizationKind::LayoutFragment => { - LAYOUT_RULES - } + RealizationKind::LayoutDocument(_) => LAYOUT_RULES, + RealizationKind::LayoutFragment(_) => LAYOUT_RULES, + RealizationKind::LayoutPar => LAYOUT_PAR_RULES, RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES, - RealizationKind::HtmlFragment => HTML_FRAGMENT_RULES, + RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES, RealizationKind::Math => MATH_RULES, }, sink: vec![], groupings: ArrayVec::new(), outside: matches!(kind, RealizationKind::LayoutDocument(_)), may_attach: false, + saw_parbreak: false, kind, }; @@ -98,6 +99,8 @@ struct State<'a, 'x, 'y, 'z> { outside: bool, /// Whether now following attach spacing can survive. may_attach: bool, + /// Whether we visited any paragraph breaks. + saw_parbreak: bool, } /// Defines a rule for how certain elements shall be grouped during realization. @@ -125,6 +128,10 @@ struct GroupingRule { struct Grouping<'a> { /// The position in `s.sink` where the group starts. start: usize, + /// Only applies to `PAR` grouping: Whether this paragraph group is + /// interrupted, but not yet finished because it may be ignored due to being + /// fully inline. + interrupted: bool, /// The rule used for this grouping. rule: &'a GroupingRule, } @@ -575,19 +582,21 @@ fn visit_styled<'a>( for style in local.iter() { let Some(elem) = style.element() else { continue }; if elem == DocumentElem::elem() { - match &mut s.kind { - RealizationKind::LayoutDocument(info) - | RealizationKind::HtmlDocument(info) => info.populate(&local), - _ => bail!( + if let Some(info) = s.kind.as_document_mut() { + info.populate(&local) + } else { + bail!( style.span(), "document set rules are not allowed inside of containers" - ), + ); } } else if elem == PageElem::elem() { - let RealizationKind::LayoutDocument(_) = s.kind else { - let span = style.span(); - bail!(span, "page configuration is not allowed inside of containers"); - }; + if !matches!(s.kind, RealizationKind::LayoutDocument(_)) { + bail!( + style.span(), + "page configuration is not allowed inside of containers" + ); + } // When there are page styles, we "break free" from our show rule cage. pagebreak = true; @@ -650,7 +659,9 @@ fn visit_grouping_rules<'a>( } // If the element can be added to the active grouping, do it. - if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) { + if !active.interrupted + && ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content)) + { s.sink.push((content, styles)); return Ok(true); } @@ -661,7 +672,7 @@ fn visit_grouping_rules<'a>( // Start a new grouping. if let Some(rule) = matching { let start = s.sink.len(); - s.groupings.push(Grouping { start, rule }); + s.groupings.push(Grouping { start, rule, interrupted: false }); s.sink.push((content, styles)); return Ok(true); } @@ -676,22 +687,24 @@ fn visit_filter_rules<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult { - if content.is::() - && !matches!(s.kind, RealizationKind::Math | RealizationKind::HtmlFragment) - { - // Outside of maths, spaces that were not collected by the paragraph - // grouper don't interest us. + if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) { + return Ok(false); + } + + if content.is::() { + // Outside of maths and paragraph realization, spaces that were not + // collected by the paragraph grouper don't interest us. return Ok(true); } else if content.is::() { // Paragraph breaks are only a boundary for paragraph grouping, we don't // need to store them. s.may_attach = false; + s.saw_parbreak = true; return Ok(true); } else if !s.may_attach && content.to_packed::().is_some_and(|elem| elem.attach(styles)) { - // Delete attach spacing collapses if not immediately following a - // paragraph. + // Attach spacing collapses if not immediately following a paragraph. return Ok(true); } @@ -703,7 +716,18 @@ fn visit_filter_rules<'a>( /// Finishes all grouping. fn finish(s: &mut State) -> SourceResult<()> { - finish_grouping_while(s, |s| !s.groupings.is_empty())?; + finish_grouping_while(s, |s| { + // If this is a fragment realization and all we've got is inline + // content, don't turn it into a paragraph. + if is_fully_inline(s) { + *s.kind.as_fragment_mut().unwrap() = FragmentKind::Inline; + s.groupings.pop(); + collapse_spaces(&mut s.sink, 0); + false + } else { + !s.groupings.is_empty() + } + })?; // In math, spaces are top-level. if let RealizationKind::Math = s.kind { @@ -722,6 +746,12 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } finish_grouping_while(s, |s| { s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem)) + && if is_fully_inline(s) { + s.groupings[0].interrupted = true; + false + } else { + true + } })?; last = Some(elem); } @@ -729,9 +759,9 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } /// Finishes groupings while `f` returns `true`. -fn finish_grouping_while(s: &mut State, f: F) -> SourceResult<()> +fn finish_grouping_while(s: &mut State, mut f: F) -> SourceResult<()> where - F: Fn(&State) -> bool, + F: FnMut(&mut State) -> bool, { // Finishing of a group may result in new content and new grouping. This // can, in theory, go on for a bit. To prevent it from becoming an infinite @@ -750,7 +780,7 @@ where /// Finishes the currently innermost grouping. fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> { // The grouping we are interrupting. - let Grouping { start, rule } = s.groupings.pop().unwrap(); + let Grouping { start, rule, .. } = s.groupings.pop().unwrap(); // Trim trailing non-trigger elements. let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind)); @@ -794,12 +824,16 @@ const MAX_GROUP_NESTING: usize = 3; /// Grouping rules used in layout realization. static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; +/// Grouping rules used in paragraph layout realization. +static LAYOUT_PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; + /// Grouping rules used in HTML root realization. static HTML_DOCUMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; /// Grouping rules used in HTML fragment realization. -static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; +static HTML_FRAGMENT_RULES: &[&GroupingRule] = + &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; /// Grouping rules used in math realization. static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; @@ -836,12 +870,10 @@ static PAR: GroupingRule = GroupingRule { || elem == SmartQuoteElem::elem() || elem == InlineElem::elem() || elem == BoxElem::elem() - || (matches!( - kind, - RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment - ) && content - .to_packed::() - .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) + || (kind.is_html() + && content + .to_packed::() + .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) }, inner: |content| content.elem() == SpaceElem::elem(), interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(), @@ -914,17 +946,31 @@ fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> { // transparently become part of it. // 2. There is no group at all. In this case, we create one. if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) { - s.groupings.push(Grouping { start, rule: &PAR }); + s.groupings.push(Grouping { start, rule: &PAR, interrupted: false }); } Ok(()) } /// Whether there is an active grouping, but it is not a `PAR` grouping. -fn in_non_par_grouping(s: &State) -> bool { - s.groupings - .last() - .is_some_and(|grouping| !std::ptr::eq(grouping.rule, &PAR)) +fn in_non_par_grouping(s: &mut State) -> bool { + s.groupings.last().is_some_and(|grouping| { + !std::ptr::eq(grouping.rule, &PAR) || grouping.interrupted + }) +} + +/// Whether there is exactly one active grouping, it is a `PAR` grouping, and it +/// spans the whole sink (with the exception of leading tags). +fn is_fully_inline(s: &State) -> bool { + s.kind.is_fragment() + && !s.saw_parbreak + && match s.groupings.as_slice() { + [grouping] => { + std::ptr::eq(grouping.rule, &PAR) + && s.sink[..grouping.start].iter().all(|(c, _)| c.is::()) + } + _ => false, + } } /// Builds the `ParElem` from inline-level elements. @@ -936,11 +982,11 @@ fn finish_par(mut grouped: Grouped) -> SourceResult<()> { // Collect the children. let elems = grouped.get(); let span = select_span(elems); - let (children, trunk) = StyleVec::create(elems); + let (body, trunk) = repack(elems); // Create and visit the paragraph. let s = grouped.end(); - let elem = ParElem::new(children).pack().spanned(span); + let elem = ParElem::new(body).pack().spanned(span); visit(s, s.store(elem), trunk) } @@ -1277,3 +1323,26 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) { fn select_span(children: &[Pair]) -> Span { Span::find(children.iter().map(|(c, _)| c.span())) } + +/// Turn realized content with styles back into owned content and a trunk style +/// chain. +fn repack<'a>(buf: &[Pair<'a>]) -> (Content, StyleChain<'a>) { + let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); + let depth = trunk.links().count(); + + let mut seq = Vec::with_capacity(buf.len()); + + for (chain, group) in buf.group_by_key(|&(_, s)| s) { + let iter = group.iter().map(|&(c, _)| c.clone()); + let suffix = chain.suffix(depth); + if suffix.is_empty() { + seq.extend(iter); + } else if let &[(element, _)] = group { + seq.push(element.clone().styled_with_map(suffix)); + } else { + seq.push(Content::sequence(iter).styled_with_map(suffix)); + } + } + + (Content::sequence(seq), trunk) +} diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index f3fe79d2c..b59fe2f73 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -128,6 +128,20 @@ pub trait SliceExt { where F: FnMut(&T) -> K, K: PartialEq; + + /// Computes two indices which split a slice into three parts. + /// + /// - A prefix which matches `f` + /// - An inner portion + /// - A suffix which matches `f` and does not overlap with the prefix + /// + /// If all elements match `f`, the prefix becomes `self` and the suffix + /// will be empty. + /// + /// Returns the indices at which the inner portion and the suffix start. + fn split_prefix_suffix(&self, f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool; } impl SliceExt for [T] { @@ -157,6 +171,19 @@ impl SliceExt for [T] { fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> { GroupByKey { slice: self, f } } + + fn split_prefix_suffix(&self, mut f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool, + { + let start = self.iter().position(|v| !f(v)).unwrap_or(self.len()); + let end = self + .iter() + .skip(start) + .rposition(|v| !f(v)) + .map_or(start, |i| start + i + 1); + (start, end) + } } /// This struct is created by [`SliceExt::group_by_key`]. diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 7d02aa426..580ba9e80 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -333,8 +333,6 @@ pub static ROUTINES: Routines = Routines { realize: typst_realize::realize, layout_fragment: typst_layout::layout_fragment, layout_frame: typst_layout::layout_frame, - layout_inline: typst_layout::layout_inline, - layout_box: typst_layout::layout_box, layout_list: typst_layout::layout_list, layout_enum: typst_layout::layout_enum, layout_grid: typst_layout::layout_grid, diff --git a/tests/ref/bibliography-grid-par.png b/tests/ref/bibliography-grid-par.png new file mode 100644 index 000000000..5befbcc54 Binary files /dev/null and b/tests/ref/bibliography-grid-par.png differ diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png new file mode 100644 index 000000000..98a3c4d04 Binary files /dev/null and b/tests/ref/bibliography-indent-par.png differ diff --git a/tests/ref/enum-par.png b/tests/ref/enum-par.png new file mode 100644 index 000000000..ca923a526 Binary files /dev/null and b/tests/ref/enum-par.png differ diff --git a/tests/ref/figure-par.png b/tests/ref/figure-par.png new file mode 100644 index 000000000..d70bbcb12 Binary files /dev/null and b/tests/ref/figure-par.png differ diff --git a/tests/ref/heading-par.png b/tests/ref/heading-par.png new file mode 100644 index 000000000..affc0df47 Binary files /dev/null and b/tests/ref/heading-par.png differ diff --git a/tests/ref/html/enum-par.html b/tests/ref/html/enum-par.html new file mode 100644 index 000000000..60d4592b7 --- /dev/null +++ b/tests/ref/html/enum-par.html @@ -0,0 +1,36 @@ + + + + + + + +

+
    +
  1. Hello
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +

    The

    +
  2. +
  3. +

    World

    +
  4. +
+
+ + diff --git a/tests/ref/html/list-par.html b/tests/ref/html/list-par.html new file mode 100644 index 000000000..7c747ff44 --- /dev/null +++ b/tests/ref/html/list-par.html @@ -0,0 +1,36 @@ + + + + + + + +
+
    +
  • Hello
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +

    The

    +
  • +
  • +

    World

    +
  • +
+
+ + diff --git a/tests/ref/html/par-semantic-html.html b/tests/ref/html/par-semantic-html.html new file mode 100644 index 000000000..09c7d2fd0 --- /dev/null +++ b/tests/ref/html/par-semantic-html.html @@ -0,0 +1,16 @@ + + + + + + + +

Heading is no paragraph

+

I'm a paragraph.

+
I'm not.
+
+

We are two.

+

So we are paragraphs.

+
+ + diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html index 753807db2..c12d2ae2d 100644 --- a/tests/ref/html/quote-attribution-link.html +++ b/tests/ref/html/quote-attribution-link.html @@ -5,7 +5,7 @@ -
Compose papers faster
+
Compose papers faster

typst.com

diff --git a/tests/ref/html/quote-plato.html b/tests/ref/html/quote-plato.html index f516adc29..039835082 100644 --- a/tests/ref/html/quote-plato.html +++ b/tests/ref/html/quote-plato.html @@ -5,9 +5,9 @@ -
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
+
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.

— Plato

-
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.
+
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.

— from the Henry Cary literal translation of 1897

diff --git a/tests/ref/html/terms-par.html b/tests/ref/html/terms-par.html new file mode 100644 index 000000000..78bc5df16 --- /dev/null +++ b/tests/ref/html/terms-par.html @@ -0,0 +1,42 @@ + + + + + + + +
+
+
Hello
+
A
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+

The

+
+
World
+
+

B

+
+
+
+ + diff --git a/tests/ref/issue-5503-enum-in-align.png b/tests/ref/issue-5503-enum-in-align.png new file mode 100644 index 000000000..4857e731b Binary files /dev/null and b/tests/ref/issue-5503-enum-in-align.png differ diff --git a/tests/ref/issue-5503-enum-interrupted-by-par-align.png b/tests/ref/issue-5503-enum-interrupted-by-par-align.png deleted file mode 100644 index 9cc942b4c..000000000 Binary files a/tests/ref/issue-5503-enum-interrupted-by-par-align.png and /dev/null differ diff --git a/tests/ref/issue-5503-list-interrupted-by-par-align.png b/tests/ref/issue-5503-list-in-align.png similarity index 100% rename from tests/ref/issue-5503-list-interrupted-by-par-align.png rename to tests/ref/issue-5503-list-in-align.png diff --git a/tests/ref/issue-5503-terms-interrupted-by-par-align.png b/tests/ref/issue-5503-terms-in-align.png similarity index 100% rename from tests/ref/issue-5503-terms-interrupted-by-par-align.png rename to tests/ref/issue-5503-terms-in-align.png diff --git a/tests/ref/list-par.png b/tests/ref/list-par.png new file mode 100644 index 000000000..5f0003d17 Binary files /dev/null and b/tests/ref/list-par.png differ diff --git a/tests/ref/math-par.png b/tests/ref/math-par.png new file mode 100644 index 000000000..30d64794c Binary files /dev/null and b/tests/ref/math-par.png differ diff --git a/tests/ref/outline-par.png b/tests/ref/outline-par.png new file mode 100644 index 000000000..04c63f62c Binary files /dev/null and b/tests/ref/outline-par.png differ diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png new file mode 100644 index 000000000..f4bd071f6 Binary files /dev/null and b/tests/ref/par-contains-block.png differ diff --git a/tests/ref/par-contains-parbreak.png b/tests/ref/par-contains-parbreak.png new file mode 100644 index 000000000..f4bd071f6 Binary files /dev/null and b/tests/ref/par-contains-parbreak.png differ diff --git a/tests/ref/par-hanging-indent-semantic.png b/tests/ref/par-hanging-indent-semantic.png new file mode 100644 index 000000000..e05795c7f Binary files /dev/null and b/tests/ref/par-hanging-indent-semantic.png differ diff --git a/tests/ref/par-semantic-align.png b/tests/ref/par-semantic-align.png new file mode 100644 index 000000000..eda496411 Binary files /dev/null and b/tests/ref/par-semantic-align.png differ diff --git a/tests/ref/par-semantic-tag.png b/tests/ref/par-semantic-tag.png new file mode 100644 index 000000000..2b26e5aee Binary files /dev/null and b/tests/ref/par-semantic-tag.png differ diff --git a/tests/ref/par-semantic.png b/tests/ref/par-semantic.png new file mode 100644 index 000000000..c2eca74d9 Binary files /dev/null and b/tests/ref/par-semantic.png differ diff --git a/tests/ref/par-show.png b/tests/ref/par-show.png new file mode 100644 index 000000000..1ceb26f71 Binary files /dev/null and b/tests/ref/par-show.png differ diff --git a/tests/ref/quote-par.png b/tests/ref/quote-par.png new file mode 100644 index 000000000..89d15ddf9 Binary files /dev/null and b/tests/ref/quote-par.png differ diff --git a/tests/ref/table-cell-par.png b/tests/ref/table-cell-par.png new file mode 100644 index 000000000..24a2a55f2 Binary files /dev/null and b/tests/ref/table-cell-par.png differ diff --git a/tests/ref/terms-par.png b/tests/ref/terms-par.png new file mode 100644 index 000000000..910b5e5da Binary files /dev/null and b/tests/ref/terms-par.png differ diff --git a/tests/suite/layout/table.typ b/tests/suite/layout/table.typ index f59d8b424..5c2b07492 100644 --- a/tests/suite/layout/table.typ +++ b/tests/suite/layout/table.typ @@ -310,6 +310,17 @@ ) } +--- table-cell-par --- +// Ensure that table cells aren't considered paragraphs by default. +#show par: highlight + +#table( + columns: 3, + [A], + block[B], + par[C], +) + --- grid-cell-in-table --- // Error: 8-19 cannot use `grid.cell` as a table cell // Hint: 8-19 use `table.cell` instead diff --git a/tests/suite/math/text.typ b/tests/suite/math/text.typ index 760910f4d..8c7611114 100644 --- a/tests/suite/math/text.typ +++ b/tests/suite/math/text.typ @@ -43,3 +43,8 @@ $sum_(k in NN)^prime 1/k^2$ // Test script-script in a fraction. $ 1/(x^A) $ #[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$] + +--- math-par --- +// Ensure that math does not produce paragraphs. +#show par: highlight +$ a + "bc" + #[c] + #box[d] + #block[e] $ diff --git a/tests/suite/model/bibliography.typ b/tests/suite/model/bibliography.typ index 20eb8acd9..6de44e240 100644 --- a/tests/suite/model/bibliography.typ +++ b/tests/suite/model/bibliography.typ @@ -53,6 +53,24 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read @Zee04 #bibliography("/assets/bib/works_too.bib", style: "mla") +--- bibliography-grid-par --- +// Ensure that a grid-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib") + +--- bibliography-indent-par --- +// Ensure that an indent-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib", style: "mla") + --- issue-4618-bibliography-set-heading-level --- // Test that the bibliography block's heading is set to 2 by the show rule, // and therefore should be rendered like a level-2 heading. Notably, this diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index 288392d45..7176b04e2 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -183,22 +183,44 @@ a + 0. #set enum(number-align: horizon) #set enum(number-align: bottom) +--- enum-par render html --- +// Check whether the contents of enum items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + + Hello + + World +] + +#block[ + + Hello // Paragraphs + + From + + World // No paragraph because it's a tight enum +] + +#block[ + + Hello // Paragraphs + + From + + The + + + World // Paragraph because it's a wide enum +] + --- issue-2530-enum-item-panic --- // Enum item (pre-emptive) #enum.item(none)[Hello] #enum.item(17)[Hello] ---- issue-5503-enum-interrupted-by-par-align --- -// `align` is block-level and should interrupt an enum -// but not a `par` +--- issue-5503-enum-in-align --- +// `align` is block-level and should interrupt an enum. + a + b -#par(leading: 5em)[+ par] +#align(right)[+ c] + d -#par[+ par] -+ f -#align(right)[+ align] -+ h --- issue-5719-enum-nested --- // Enums can be immediately nested. diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index 58ba2b2a4..37fb4ecda 100644 --- a/tests/suite/model/figure.typ +++ b/tests/suite/model/figure.typ @@ -180,6 +180,17 @@ We can clearly see that @fig-cylinder and caption: [Underlined], ) +--- figure-par --- +// Ensure that a figure body is considered a paragraph. +#show par: highlight + +#figure[Text] + +#figure( + [Text], + caption: [A caption] +) + --- figure-and-caption-show --- // Test creating custom figure and custom caption diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ index 4e529fdf6..4e04e5c56 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -128,6 +128,11 @@ Not in heading // Hint: 1:19-1:25 you can enable heading numbering with `#set heading(numbering: "1.")` Cannot be used as @intro +--- heading-par --- +// Ensure that heading text isn't considered a paragraph. +#show par: highlight += Heading + --- heading-html-basic html --- // level 1 => h2 // ... diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 96ddf3c18..9bed930bb 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -238,6 +238,33 @@ World #text(red)[- World] #text(green)[- What up?] +--- list-par render html --- +// Check whether the contents of list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +#block[ + // No paragraphs. + - Hello + - World +] + +#block[ + - Hello // Paragraphs + + From + - World // No paragraph because it's a tight list. +] + +#block[ + - Hello // Paragraphs either way + + From + + The + + - World // Paragraph because it's a wide list. +] + --- issue-2530-list-item-panic --- // List item (pre-emptive) #list.item[Hello] @@ -262,18 +289,11 @@ World part($ x $ + parbreak() + parbreak() + list[A]) } ---- issue-5503-list-interrupted-by-par-align --- -// `align` is block-level and should interrupt a list -// but not a `par` +--- issue-5503-list-in-align --- +// `align` is block-level and should interrupt a list. #show list: [List] - a - b -#par(leading: 5em)[- c] -- d -- e -#par[- f] -- g -- h #align(right)[- i] - j diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index a755151d6..49fd7d7cb 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -242,6 +242,15 @@ A #outline(target: metadata) #metadata("hello") +--- outline-par --- +// Ensure that an outline does not produce paragraphs. +#show par: highlight + +#outline() + += A += B += C --- issue-2048-outline-multiline --- // Without the word joiner between the dots and the page number, diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index 0c2b5cb54..84f2ec152 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -19,6 +19,105 @@ heaven Would through the airy region stream so bright That birds would sing and think it were not night. See, how she leans her cheek upon her hand! O, that I were a glove upon that hand, That I might touch that cheek! +--- par-semantic --- +#show par: highlight + +I'm a paragraph. + +#align(center, table( + columns: 3, + + // No paragraphs. + [A], + block[B], + block[C *D*], + + // Paragraphs. + par[E], + [ + + F + ], + [ + G + + ], + + // Paragraphs. + parbreak() + [H], + [I] + parbreak(), + parbreak() + [J] + parbreak(), + + // Paragraphs. + [K #v(10pt)], + [#v(10pt) L], + [#place[] M], + + // Paragraphs. + [ + N + + O + ], + [#par[P]#par[Q]], + // No paragraphs. + [#block[R]#block[S]], +)) + +--- par-semantic-html html --- += Heading is no paragraph + +I'm a paragraph. + +#html.elem("div")[I'm not.] + +#html.elem("div")[ + We are two. + + So we are paragraphs. +] + +--- par-semantic-tag --- +#show par: highlight +#block[ + #metadata(none) + A + #metadata(none) +] + +#block(width: 100%, metadata(none) + align(center)[A]) +#block(width: 100%, align(center)[A] + metadata(none)) + +--- par-semantic-align --- +#show par: highlight +#show bibliography: none +#set block(width: 100%, stroke: 1pt, inset: 5pt) + +#bibliography("/assets/bib/works.bib") + +#block[ + #set align(right) + Hello +] + +#block[ + #set align(right) + Hello + @netwok +] + +#block[ + Hello + #align(right)[World] + You +] + +#block[ + Hello + #align(right)[@netwok] + You +] + --- par-leading-and-spacing --- // Test changing leading and spacing. #set par(spacing: 1em, leading: 2pt) @@ -69,6 +168,12 @@ Why would anybody ever ... #set par(hanging-indent: 15pt, justify: true) #lorem(10) +--- par-hanging-indent-semantic --- +#set par(hanging-indent: 15pt) += I am not affected + +I am affected by hanging indent. + --- par-hanging-indent-manual-linebreak --- #set par(hanging-indent: 1em) Welcome \ here. Does this work well? @@ -83,6 +188,22 @@ Welcome \ here. Does this work well? // Ensure that trailing whitespace layouts as intended. #box(fill: aqua, " ") +--- par-contains-parbreak --- +#par[ + Hello + // Warning: 4-14 parbreak may not occur inside of a paragraph and was ignored + #parbreak() + World +] + +--- par-contains-block --- +#par[ + Hello + // Warning: 4-11 block may not occur inside of a paragraph and was ignored + #block[] + World +] + --- par-empty-metadata --- // Check that metadata still works in a zero length paragraph. #block(height: 0pt)[#""#metadata(false)] @@ -94,6 +215,26 @@ Welcome \ here. Does this work well? #set text(hyphenate: false) Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. +--- par-show --- +// This is only slightly cursed. +#let revoke = metadata("revoke") +#show par: it => { + if bibliography.title == revoke { return it } + set bibliography(title: revoke) + let p = counter("p") + par[#p.step() §#context p.display() #it.body] +} + += A + +B + +C #parbreak() D + +#block[E] + +#block[F #parbreak() G] + --- issue-4278-par-trim-before-equation --- #set par(justify: true) #lorem(6) aa $a = c + b$ diff --git a/tests/suite/model/quote.typ b/tests/suite/model/quote.typ index d0dcc55dd..51c4bba59 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -107,3 +107,14 @@ When you said that #quote[he surely meant that #quote[she intended to say #quote )[ Compose papers faster ] + +--- quote-par --- +// Ensure that an inline quote is part of a paragraph, but a block quote +// does not result in paragraphs. +#show par: highlight + +An inline #quote[quote.] + +#quote(block: true, attribution: [The Test Author])[ + A block-level quote. +] diff --git a/tests/suite/model/terms.typ b/tests/suite/model/terms.typ index 23ac6e513..103a8033e 100644 --- a/tests/suite/model/terms.typ +++ b/tests/suite/model/terms.typ @@ -59,6 +59,34 @@ Not in list // Error: 8 expected colon / Hello +--- terms-par render html --- +// Check whether the contents of term list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + / Hello: A + / World: B +] + +#block[ + / Hello: A // Paragraphs + + From + / World: B // No paragraphs because it's a tight term list. +] + +#block[ + / Hello: A // Paragraphs + + From + + The + + / World: B // Paragraph because it's a wide term list. +] + + --- issue-1050-terms-indent --- #set page(width: 110pt) #set par(first-line-indent: 0.5cm) @@ -76,18 +104,10 @@ Not in list // Term item (pre-emptive) #terms.item[Hello][World!] ---- issue-5503-terms-interrupted-by-par-align --- -// `align` is block-level and should interrupt a `terms` -// but not a `par` +--- issue-5503-terms-in-align --- +// `align` is block-level and should interrupt a `terms`. #show terms: [Terms] / a: a -/ b: b -#par(leading: 5em)[/ c: c] -/ d: d -/ e: e -#par[/ f: f] -/ g: g -/ h: h #align(right)[/ i: i] / j: j