summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2025-01-24 13:11:26 +0100
committerGitHub <noreply@github.com>2025-01-24 12:11:26 +0000
commit26e65bfef5b1da7f6c72e1409237cf03fb5d6069 (patch)
treedae6f71efead43736202dd6aea933b95b1bc7a14
parent467968af0788a3059e1bed47f9daee846f5b3904 (diff)
Semantic paragraphs (#5746)
-rw-r--r--crates/typst-html/src/lib.rs9
-rw-r--r--crates/typst-layout/src/flow/collect.rs85
-rw-r--r--crates/typst-layout/src/flow/compose.rs6
-rw-r--r--crates/typst-layout/src/flow/mod.rs45
-rw-r--r--crates/typst-layout/src/inline/box.rs2
-rw-r--r--crates/typst-layout/src/inline/collect.rs57
-rw-r--r--crates/typst-layout/src/inline/finalize.rs2
-rw-r--r--crates/typst-layout/src/inline/line.rs14
-rw-r--r--crates/typst-layout/src/inline/linebreak.rs27
-rw-r--r--crates/typst-layout/src/inline/mod.rs77
-rw-r--r--crates/typst-layout/src/inline/prepare.rs48
-rw-r--r--crates/typst-layout/src/inline/shaping.rs10
-rw-r--r--crates/typst-layout/src/lib.rs1
-rw-r--r--crates/typst-layout/src/lists.rs24
-rw-r--r--crates/typst-layout/src/math/lr.rs11
-rw-r--r--crates/typst-layout/src/math/mod.rs7
-rw-r--r--crates/typst-layout/src/math/text.rs13
-rw-r--r--crates/typst-layout/src/pages/collect.rs2
-rw-r--r--crates/typst-layout/src/pages/mod.rs4
-rw-r--r--crates/typst-layout/src/pages/run.rs4
-rw-r--r--crates/typst-library/src/foundations/styles.rs101
-rw-r--r--crates/typst-library/src/layout/container.rs10
-rw-r--r--crates/typst-library/src/math/equation.rs4
-rw-r--r--crates/typst-library/src/model/bibliography.rs44
-rw-r--r--crates/typst-library/src/model/enum.rs15
-rw-r--r--crates/typst-library/src/model/figure.rs33
-rw-r--r--crates/typst-library/src/model/footnote.rs6
-rw-r--r--crates/typst-library/src/model/list.rs13
-rw-r--r--crates/typst-library/src/model/outline.rs1
-rw-r--r--crates/typst-library/src/model/par.rs110
-rw-r--r--crates/typst-library/src/model/quote.rs19
-rw-r--r--crates/typst-library/src/model/terms.rs22
-rw-r--r--crates/typst-library/src/routines.rs76
-rw-r--r--crates/typst-realize/src/lib.rs155
-rw-r--r--crates/typst-utils/src/lib.rs27
-rw-r--r--crates/typst/src/lib.rs2
-rw-r--r--tests/ref/bibliography-grid-par.pngbin0 -> 8757 bytes
-rw-r--r--tests/ref/bibliography-indent-par.pngbin0 -> 9087 bytes
-rw-r--r--tests/ref/enum-par.pngbin0 -> 3521 bytes
-rw-r--r--tests/ref/figure-par.pngbin0 -> 1701 bytes
-rw-r--r--tests/ref/heading-par.pngbin0 -> 555 bytes
-rw-r--r--tests/ref/html/enum-par.html36
-rw-r--r--tests/ref/html/list-par.html36
-rw-r--r--tests/ref/html/par-semantic-html.html16
-rw-r--r--tests/ref/html/quote-attribution-link.html2
-rw-r--r--tests/ref/html/quote-plato.html4
-rw-r--r--tests/ref/html/terms-par.html42
-rw-r--r--tests/ref/issue-5503-enum-in-align.pngbin0 -> 421 bytes
-rw-r--r--tests/ref/issue-5503-enum-interrupted-by-par-align.pngbin1004 -> 0 bytes
-rw-r--r--tests/ref/issue-5503-list-in-align.png (renamed from tests/ref/issue-5503-list-interrupted-by-par-align.png)bin415 -> 415 bytes
-rw-r--r--tests/ref/issue-5503-terms-in-align.png (renamed from tests/ref/issue-5503-terms-interrupted-by-par-align.png)bin569 -> 569 bytes
-rw-r--r--tests/ref/list-par.pngbin0 -> 3319 bytes
-rw-r--r--tests/ref/math-par.pngbin0 -> 387 bytes
-rw-r--r--tests/ref/outline-par.pngbin0 -> 2911 bytes
-rw-r--r--tests/ref/par-contains-block.pngbin0 -> 426 bytes
-rw-r--r--tests/ref/par-contains-parbreak.pngbin0 -> 426 bytes
-rw-r--r--tests/ref/par-hanging-indent-semantic.pngbin0 -> 1594 bytes
-rw-r--r--tests/ref/par-semantic-align.pngbin0 -> 3082 bytes
-rw-r--r--tests/ref/par-semantic-tag.pngbin0 -> 278 bytes
-rw-r--r--tests/ref/par-semantic.pngbin0 -> 3485 bytes
-rw-r--r--tests/ref/par-show.pngbin0 -> 932 bytes
-rw-r--r--tests/ref/quote-par.pngbin0 -> 2792 bytes
-rw-r--r--tests/ref/table-cell-par.pngbin0 -> 645 bytes
-rw-r--r--tests/ref/terms-par.pngbin0 -> 3892 bytes
-rw-r--r--tests/suite/layout/table.typ11
-rw-r--r--tests/suite/math/text.typ5
-rw-r--r--tests/suite/model/bibliography.typ18
-rw-r--r--tests/suite/model/enum.typ38
-rw-r--r--tests/suite/model/figure.typ11
-rw-r--r--tests/suite/model/heading.typ5
-rw-r--r--tests/suite/model/list.typ38
-rw-r--r--tests/suite/model/outline.typ9
-rw-r--r--tests/suite/model/par.typ141
-rw-r--r--tests/suite/model/quote.typ11
-rw-r--r--tests/suite/model/terms.typ40
75 files changed, 1098 insertions, 451 deletions
diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs
index 1fa6aa21..25d0cd5d 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::<ParElem>() {
- 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 76d7b743..f2c7ebd1 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<Vec<Child<'a>>> {
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<Vec<Child<'a>>> {
+ fn run(self, mode: FlowMode) -> SourceResult<Vec<Child<'a>>> {
+ 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<Vec<Child<'a>>> {
for &(child, styles) in self.children {
if let Some(elem) = child.to_packed::<TagElem>() {
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<Vec<Child<'a>>> {
+ // Extract leading and trailing tags.
+ let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::<TagElem>());
+ 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::<TagElem>().unwrap();
+ self.output.push(Child::Tag(&elem.tag));
+ }
+
+ self.lines(lines, styles);
+
+ for (c, _) in &self.children[end..] {
+ let elem = c.to_packed::<TagElem>().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<VElem>, styles: StyleChain<'a>) {
self.output.push(match elem.amount {
@@ -110,23 +158,33 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<ParElem>,
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<Frame>, 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();
@@ -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 3cf66f9e..76af8f65 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 2f0ec39a..2acbbcef 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<FragmentKind> 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<Abs>,
- root: bool,
+ mode: FlowMode,
) -> SourceResult<Fragment> {
// 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 6dfbc969..e21928d3 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<BoxElem>,
diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs
index 6023f5c6..cbc490ba 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<Segment<'a>>, 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::<SpaceElem>() {
@@ -234,7 +241,13 @@ pub fn collect<'a>(
} else if let Some(elem) = child.to_packed::<TagElem>() {
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 57044f0e..7ad287c4 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<Fragment> {
- // 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 fba4bef8..9f697380 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<ItemEntry<'a>>);
impl<'a> Items<'a> {
diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs
index 7b66fcdb..87113c68 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<Line<'a>> {
- /// 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 bedc54d6..83ca82bf 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<usize>;
-/// Layouts content inline.
-pub fn layout_inline(
+/// Layouts the paragraph.
+pub fn layout_par(
+ elem: &Packed<ParElem>,
engine: &mut Engine,
- children: &StyleVec,
locator: Locator,
styles: StyleChain,
- consecutive: bool,
region: Size,
expand: bool,
+ consecutive: bool,
) -> SourceResult<Fragment> {
- 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<ParElem>,
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
@@ -73,12 +73,12 @@ fn layout_inline_impl(
route: Tracked<Route>,
locator: Tracked<Locator>,
styles: StyleChain,
- consecutive: bool,
region: Size,
expand: bool,
+ consecutive: bool,
) -> SourceResult<Fragment> {
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<Fragment> {
// 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 2dd79aec..e26c9b14 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<BidiInfo<'a>>,
/// 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<Lang>,
- /// 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<Linebreaks>,
@@ -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<Segment<'a>>,
spans: SpanMapper,
styles: StyleChain<'a>,
+ paragraph: bool,
) -> SourceResult<Preparation<'a>> {
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<T: PartialEq>(
+ children: &[Pair],
+ styles: StyleChain<'_>,
+ getter: fn(StyleChain) -> T,
+) -> Option<T> {
+ 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 2ed95f14..b688981a 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 56d7afe1..443e90d6 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 63127474..f8d910ab 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<Fragment> {
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 19176ee8..bf823541 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 702816ee..e5a3d94c 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<Frame> {
- (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 6b9703aa..5897c3c0 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 0bbae9f4..8eab18a6 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<Item<'a>> {
// The collected page-level items.
diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs
index 27002a6c..14dc0f3f 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<Vec<Page>> {
// 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 79ff5ab0..6d2d29da 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 37094dcd..98380330 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<Content>,
- /// 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<Content>) -> 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<Item = (&'a Content, StyleChain<'a>)> {
- 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<T: PartialEq>(
- &self,
- styles: StyleChain<'_>,
- getter: fn(StyleChain) -> T,
- ) -> Option<T> {
- 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 c8c74269..725f177b 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 1e346280..32be216a 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 762a97fd..a391e580 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<BibliographyElem> {
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<BibliographyElem> {
.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<BibliographyElem> {
.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)))
+ };
- let mut content = Content::sequence(seq);
- if works.hanging_indent {
- content = content.styled(ParElem::set_hanging_indent(INDENT.into()));
+ seq.push(block.pack().spanned(span));
+ }
}
- 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 4dc834ab..a4126e72 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<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ 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<EnumElem> {
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<EnumElem> {
.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 ce7460c9..78a79a8e 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<FigureElem> {
impl Show for Packed<FigureElem> {
#[typst_macros::time(name = "figure", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ let span = self.span();
let target = TargetElem::target_in(styles);
let mut realized = self.body.clone();
@@ -341,24 +344,27 @@ impl Show for Packed<FigureElem> {
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<FigureElem> {
.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<FigureCaption> {
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 f3b2a19e..dfa3933b 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<FootnoteEntry> {
impl ShowSet for Packed<FootnoteEntry> {
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 1e369d54..d93ec917 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<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ 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<ListElem> {
.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 0db056e4..1214f2b0 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<OutlineElem> {
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 8b82abdf..0bdbe4ea 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.
+///
+/// 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 `<p>` tag only for paragraphs.
///
-/// 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.
+/// 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<Linebreaks>,
/// 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<Content> {
- // 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::<Content>("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 79e9b4e3..919ab12c 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<QuoteElem> {
.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 c91eeb17..9a2ed6aa 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<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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<TermsElem> {
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<TermsElem> {
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<TermsElem> {
.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 a1126860..b283052a 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<Frame>
- /// Lays out inline content.
- fn layout_inline(
- engine: &mut Engine,
- children: &StyleVec,
- locator: Locator,
- styles: StyleChain,
- consecutive: bool,
- region: Size,
- expand: bool,
- ) -> SourceResult<Fragment>
-
- /// Lays out a [`BoxElem`].
- fn layout_box(
- elem: &Packed<BoxElem>,
- engine: &mut Engine,
- locator: Locator,
- styles: StyleChain,
- region: Size,
- ) -> SourceResult<Frame>
-
/// Lays out a [`ListElem`].
fn layout_list(
elem: &Packed<ListElem>,
@@ -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 ff42c3e9..754e89aa 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<bool> {
- if content.is::<SpaceElem>()
- && !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::<SpaceElem>() {
+ // 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::<ParbreakElem>() {
// 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::<VElem>().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<F>(s: &mut State, f: F) -> SourceResult<()>
+fn finish_grouping_while<F>(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::<HtmlElem>()
- .is_some_and(|elem| tag::is_inline_by_default(elem.tag)))
+ || (kind.is_html()
+ && content
+ .to_packed::<HtmlElem>()
+ .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::<TagElem>())
+ }
+ _ => 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 f3fe79d2..b59fe2f7 100644
--- a/crates/typst-utils/src/lib.rs
+++ b/crates/typst-utils/src/lib.rs
@@ -128,6 +128,20 @@ pub trait SliceExt<T> {
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<F>(&self, f: F) -> (usize, usize)
+ where
+ F: FnMut(&T) -> bool;
}
impl<T> SliceExt<T> for [T] {
@@ -157,6 +171,19 @@ impl<T> SliceExt<T> for [T] {
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> {
GroupByKey { slice: self, f }
}
+
+ fn split_prefix_suffix<F>(&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 7d02aa42..580ba9e8 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 00000000..5befbcc5
--- /dev/null
+++ b/tests/ref/bibliography-grid-par.png
Binary files differ
diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png
new file mode 100644
index 00000000..98a3c4d0
--- /dev/null
+++ b/tests/ref/bibliography-indent-par.png
Binary files differ
diff --git a/tests/ref/enum-par.png b/tests/ref/enum-par.png
new file mode 100644
index 00000000..ca923a52
--- /dev/null
+++ b/tests/ref/enum-par.png
Binary files differ
diff --git a/tests/ref/figure-par.png b/tests/ref/figure-par.png
new file mode 100644
index 00000000..d70bbcb1
--- /dev/null
+++ b/tests/ref/figure-par.png
Binary files differ
diff --git a/tests/ref/heading-par.png b/tests/ref/heading-par.png
new file mode 100644
index 00000000..affc0df4
--- /dev/null
+++ b/tests/ref/heading-par.png
Binary files differ
diff --git a/tests/ref/html/enum-par.html b/tests/ref/html/enum-par.html
new file mode 100644
index 00000000..60d4592b
--- /dev/null
+++ b/tests/ref/html/enum-par.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ </head>
+ <body>
+ <div>
+ <ol>
+ <li>Hello</li>
+ <li>World</li>
+ </ol>
+ </div>
+ <div>
+ <ol>
+ <li>
+ <p>Hello</p>
+ <p>From</p>
+ </li>
+ <li>World</li>
+ </ol>
+ </div>
+ <div>
+ <ol>
+ <li>
+ <p>Hello</p>
+ <p>From</p>
+ <p>The</p>
+ </li>
+ <li>
+ <p>World</p>
+ </li>
+ </ol>
+ </div>
+ </body>
+</html>
diff --git a/tests/ref/html/list-par.html b/tests/ref/html/list-par.html
new file mode 100644
index 00000000..7c747ff4
--- /dev/null
+++ b/tests/ref/html/list-par.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ </head>
+ <body>
+ <div>
+ <ul>
+ <li>Hello</li>
+ <li>World</li>
+ </ul>
+ </div>
+ <div>
+ <ul>
+ <li>
+ <p>Hello</p>
+ <p>From</p>
+ </li>
+ <li>World</li>
+ </ul>
+ </div>
+ <div>
+ <ul>
+ <li>
+ <p>Hello</p>
+ <p>From</p>
+ <p>The</p>
+ </li>
+ <li>
+ <p>World</p>
+ </li>
+ </ul>
+ </div>
+ </body>
+</html>
diff --git a/tests/ref/html/par-semantic-html.html b/tests/ref/html/par-semantic-html.html
new file mode 100644
index 00000000..09c7d2fd
--- /dev/null
+++ b/tests/ref/html/par-semantic-html.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ </head>
+ <body>
+ <h2>Heading is no paragraph</h2>
+ <p>I'm a paragraph.</p>
+ <div>I'm not.</div>
+ <div>
+ <p>We are two.</p>
+ <p>So we are paragraphs.</p>
+ </div>
+ </body>
+</html>
diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html
index 753807db..c12d2ae2 100644
--- a/tests/ref/html/quote-attribution-link.html
+++ b/tests/ref/html/quote-attribution-link.html
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
- <blockquote cite="https://typst.app/home"> Compose papers faster </blockquote>
+ <blockquote cite="https://typst.app/home">Compose papers faster</blockquote>
<p>— <a href="https://typst.app/home">typst.com</a></p>
</body>
</html>
diff --git a/tests/ref/html/quote-plato.html b/tests/ref/html/quote-plato.html
index f516adc2..03983508 100644
--- a/tests/ref/html/quote-plato.html
+++ b/tests/ref/html/quote-plato.html
@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
- <blockquote> … ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. </blockquote>
+ <blockquote>… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.</blockquote>
<p>— Plato</p>
- <blockquote> … 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. </blockquote>
+ <blockquote>… 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.</blockquote>
<p>— from the Henry Cary literal translation of 1897</p>
</body>
</html>
diff --git a/tests/ref/html/terms-par.html b/tests/ref/html/terms-par.html
new file mode 100644
index 00000000..78bc5df1
--- /dev/null
+++ b/tests/ref/html/terms-par.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ </head>
+ <body>
+ <div>
+ <dl>
+ <dt>Hello</dt>
+ <dd>A</dd>
+ <dt>World</dt>
+ <dd>B</dd>
+ </dl>
+ </div>
+ <div>
+ <dl>
+ <dt>Hello</dt>
+ <dd>
+ <p>A</p>
+ <p>From</p>
+ </dd>
+ <dt>World</dt>
+ <dd>B</dd>
+ </dl>
+ </div>
+ <div>
+ <dl>
+ <dt>Hello</dt>
+ <dd>
+ <p>A</p>
+ <p>From</p>
+ <p>The</p>
+ </dd>
+ <dt>World</dt>
+ <dd>
+ <p>B</p>
+ </dd>
+ </dl>
+ </div>
+ </body>
+</html>
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 00000000..4857e731
--- /dev/null
+++ b/tests/ref/issue-5503-enum-in-align.png
Binary files 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 9cc942b4..00000000
--- a/tests/ref/issue-5503-enum-interrupted-by-par-align.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/issue-5503-list-interrupted-by-par-align.png b/tests/ref/issue-5503-list-in-align.png
index 2b785c4b..2b785c4b 100644
--- a/tests/ref/issue-5503-list-interrupted-by-par-align.png
+++ b/tests/ref/issue-5503-list-in-align.png
Binary files differ
diff --git a/tests/ref/issue-5503-terms-interrupted-by-par-align.png b/tests/ref/issue-5503-terms-in-align.png
index 1bdacd94..1bdacd94 100644
--- a/tests/ref/issue-5503-terms-interrupted-by-par-align.png
+++ b/tests/ref/issue-5503-terms-in-align.png
Binary files differ
diff --git a/tests/ref/list-par.png b/tests/ref/list-par.png
new file mode 100644
index 00000000..5f0003d1
--- /dev/null
+++ b/tests/ref/list-par.png
Binary files differ
diff --git a/tests/ref/math-par.png b/tests/ref/math-par.png
new file mode 100644
index 00000000..30d64794
--- /dev/null
+++ b/tests/ref/math-par.png
Binary files differ
diff --git a/tests/ref/outline-par.png b/tests/ref/outline-par.png
new file mode 100644
index 00000000..04c63f62
--- /dev/null
+++ b/tests/ref/outline-par.png
Binary files differ
diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png
new file mode 100644
index 00000000..f4bd071f
--- /dev/null
+++ b/tests/ref/par-contains-block.png
Binary files differ
diff --git a/tests/ref/par-contains-parbreak.png b/tests/ref/par-contains-parbreak.png
new file mode 100644
index 00000000..f4bd071f
--- /dev/null
+++ b/tests/ref/par-contains-parbreak.png
Binary files differ
diff --git a/tests/ref/par-hanging-indent-semantic.png b/tests/ref/par-hanging-indent-semantic.png
new file mode 100644
index 00000000..e05795c7
--- /dev/null
+++ b/tests/ref/par-hanging-indent-semantic.png
Binary files differ
diff --git a/tests/ref/par-semantic-align.png b/tests/ref/par-semantic-align.png
new file mode 100644
index 00000000..eda49641
--- /dev/null
+++ b/tests/ref/par-semantic-align.png
Binary files differ
diff --git a/tests/ref/par-semantic-tag.png b/tests/ref/par-semantic-tag.png
new file mode 100644
index 00000000..2b26e5ae
--- /dev/null
+++ b/tests/ref/par-semantic-tag.png
Binary files differ
diff --git a/tests/ref/par-semantic.png b/tests/ref/par-semantic.png
new file mode 100644
index 00000000..c2eca74d
--- /dev/null
+++ b/tests/ref/par-semantic.png
Binary files differ
diff --git a/tests/ref/par-show.png b/tests/ref/par-show.png
new file mode 100644
index 00000000..1ceb26f7
--- /dev/null
+++ b/tests/ref/par-show.png
Binary files differ
diff --git a/tests/ref/quote-par.png b/tests/ref/quote-par.png
new file mode 100644
index 00000000..89d15ddf
--- /dev/null
+++ b/tests/ref/quote-par.png
Binary files differ
diff --git a/tests/ref/table-cell-par.png b/tests/ref/table-cell-par.png
new file mode 100644
index 00000000..24a2a55f
--- /dev/null
+++ b/tests/ref/table-cell-par.png
Binary files differ
diff --git a/tests/ref/terms-par.png b/tests/ref/terms-par.png
new file mode 100644
index 00000000..910b5e5d
--- /dev/null
+++ b/tests/ref/terms-par.png
Binary files differ
diff --git a/tests/suite/layout/table.typ b/tests/suite/layout/table.typ
index f59d8b42..5c2b0749 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 760910f4..8c761111 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 20eb8acd..6de44e24 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 288392d4..7176b04e 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 58ba2b2a..37fb4ecd 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 4e529fdf..4e04e5c5 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 96ddf3c1..9bed930b 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 a755151d..49fd7d7c 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 0c2b5cb5..84f2ec15 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) <hi1>
+ A
+ #metadata(none) <hi2>
+]
+
+#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)<hi>]
@@ -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 d0dcc55d..51c4bba5 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 23ac6e51..103a8033 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