Semantic paragraphs

This commit is contained in:
Laurenz 2025-01-24 12:21:45 +01:00
parent 09841ad821
commit 75179eabd5
24 changed files with 447 additions and 350 deletions

View File

@ -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)

View File

@ -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,24 +158,34 @@ 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();
let prevent_orphans =
@ -166,11 +224,6 @@ impl<'a> Collector<'a, '_, '_> {
self.output
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
}
self.output.push(Child::Rel(spacing.into(), 4));
self.last_was_par = true;
Ok(())
}
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on

View File

@ -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(());
}

View File

@ -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>,

View File

@ -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>,

View File

@ -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;

View File

@ -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()))

View File

@ -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> {

View File

@ -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.

View File

@ -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)
}

View File

@ -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

View File

@ -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()) {

View File

@ -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;

View File

@ -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()),

View File

@ -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();

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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
}
}

View File

@ -1,12 +1,10 @@
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};
@ -95,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.
///
@ -111,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,
@ -126,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,
@ -139,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,
@ -164,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.
@ -176,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]
@ -201,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 {

View File

@ -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.

View File

@ -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)
}

View File

@ -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,