summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-layout/src/flow/collect.rs13
-rw-r--r--crates/typst-layout/src/flow/mod.rs77
-rw-r--r--crates/typst-layout/src/inline/collect.rs54
-rw-r--r--crates/typst-layout/src/inline/finalize.rs10
-rw-r--r--crates/typst-layout/src/inline/line.rs51
-rw-r--r--crates/typst-layout/src/inline/linebreak.rs44
-rw-r--r--crates/typst-layout/src/inline/mod.rs191
-rw-r--r--crates/typst-layout/src/inline/prepare.rs73
-rw-r--r--crates/typst-layout/src/math/text.rs1
-rw-r--r--crates/typst-library/src/model/link.rs4
-rw-r--r--crates/typst-library/src/text/mod.rs25
-rw-r--r--crates/typst-library/src/text/raw.rs6
12 files changed, 300 insertions, 249 deletions
diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs
index 34362a6c..2c14f7a3 100644
--- a/crates/typst-layout/src/flow/collect.rs
+++ b/crates/typst-layout/src/flow/collect.rs
@@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> {
styles,
self.base,
self.expand,
- None,
)?
.into_frames();
@@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> {
self.output.push(Child::Tag(&elem.tag));
}
- self.lines(lines, styles);
+ let leading = ParElem::leading_in(styles);
+ self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] {
let elem = c.to_packed::<TagElem>().unwrap();
@@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> {
)?
.into_frames();
- let spacing = ParElem::spacing_in(styles);
+ let spacing = elem.spacing(styles);
+ let leading = elem.leading(styles);
+
self.output.push(Child::Rel(spacing.into(), 4));
- self.lines(lines, styles);
+ self.lines(lines, leading, styles);
self.output.push(Child::Rel(spacing.into(), 4));
self.par_situation = ParSituation::Consecutive;
@@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> {
}
/// Collect laid-out lines.
- fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) {
+ fn lines(&mut self, lines: Vec<Frame>, leading: Abs, 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.
diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs
index 2acbbcef..cba228bc 100644
--- a/crates/typst-layout/src/flow/mod.rs
+++ b/crates/typst-layout/src/flow/mod.rs
@@ -197,7 +197,50 @@ pub fn layout_flow<'a>(
mode: FlowMode,
) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow.
- let config = Config {
+ let config = configuration(shared, regions, columns, column_gutter, mode);
+
+ // Collect the elements into pre-processed children. These are much easier
+ // to handle than the raw elements.
+ let bump = Bump::new();
+ let children = collect(
+ engine,
+ &bump,
+ children,
+ locator.next(&()),
+ Size::new(config.columns.width, regions.full),
+ regions.expand.x,
+ mode,
+ )?;
+
+ let mut work = Work::new(&children);
+ let mut finished = vec![];
+
+ // This loop runs once per region produced by the flow layout.
+ loop {
+ let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
+ finished.push(frame);
+
+ // Terminate the loop when everything is processed, though draining the
+ // backlog if necessary.
+ if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
+ break;
+ }
+
+ regions.next();
+ }
+
+ Ok(Fragment::frames(finished))
+}
+
+/// Determine the flow's configuration.
+fn configuration<'x>(
+ shared: StyleChain<'x>,
+ regions: Regions,
+ columns: NonZeroUsize,
+ column_gutter: Rel<Abs>,
+ mode: FlowMode,
+) -> Config<'x> {
+ Config {
mode,
shared,
columns: {
@@ -235,39 +278,7 @@ pub fn layout_flow<'a>(
)
},
}),
- };
-
- // Collect the elements into pre-processed children. These are much easier
- // to handle than the raw elements.
- let bump = Bump::new();
- let children = collect(
- engine,
- &bump,
- children,
- locator.next(&()),
- Size::new(config.columns.width, regions.full),
- regions.expand.x,
- mode,
- )?;
-
- let mut work = Work::new(&children);
- let mut finished = vec![];
-
- // This loop runs once per region produced by the flow layout.
- loop {
- let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
- finished.push(frame);
-
- // Terminate the loop when everything is processed, though draining the
- // backlog if necessary.
- if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
- break;
- }
-
- regions.next();
}
-
- Ok(Fragment::frames(finished))
}
/// The work that is left to do by flow layout.
diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs
index 14cf2e3b..5a1b7b4f 100644
--- a/crates/typst-layout/src/inline/collect.rs
+++ b/crates/typst-layout/src/inline/collect.rs
@@ -2,10 +2,8 @@ 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,
+ Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
};
-use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair;
use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
@@ -123,40 +121,20 @@ pub fn collect<'a>(
children: &[Pair<'a>],
engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>,
- styles: StyleChain<'a>,
+ config: &Config,
region: Size,
- situation: Option<ParSituation>,
) -> 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);
-
- if let Some(situation) = situation {
- let first_line_indent = ParElem::first_line_indent_in(styles);
- if !first_line_indent.amount.is_zero()
- && match situation {
- // First-line indent for the first paragraph after a list bullet
- // just looks bad.
- ParSituation::First => first_line_indent.all && !in_list(styles),
- ParSituation::Consecutive => true,
- ParSituation::Other => first_line_indent.all,
- }
- && AlignElem::alignment_in(styles).resolve(styles).x
- == outer_dir.start().into()
- {
- collector.push_item(Item::Absolute(
- first_line_indent.amount.resolve(styles),
- false,
- ));
- collector.spans.push(1, Span::detached());
- }
+ if !config.first_line_indent.is_zero() {
+ collector.push_item(Item::Absolute(config.first_line_indent, 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 !config.hanging_indent.is_zero() {
+ collector.push_item(Item::Absolute(-config.hanging_indent, false));
+ collector.spans.push(1, Span::detached());
}
for &(child, styles) in children {
@@ -167,7 +145,7 @@ pub fn collect<'a>(
} else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles);
- if dir != outer_dir {
+ if dir != config.dir {
// Insert "Explicit Directional Embedding".
match dir {
Dir::LTR => full.push_str(LTR_EMBEDDING),
@@ -182,7 +160,7 @@ pub fn collect<'a>(
full.push_str(&elem.text);
}
- if dir != outer_dir {
+ if dir != config.dir {
// Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING);
}
@@ -265,16 +243,6 @@ pub fn collect<'a>(
Ok((collector.full, collector.segments, collector.spans))
}
-/// Whether we have a list ancestor.
-///
-/// When we support some kind of more general ancestry mechanism, this can
-/// become more elegant.
-fn in_list(styles: StyleChain) -> bool {
- ListElem::depth_in(styles).0 > 0
- || !EnumElem::parents_in(styles).is_empty()
- || TermsElem::within_in(styles)
-}
-
/// Collects segments.
struct Collector<'a> {
full: String,
diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs
index 7ad287c4..c9de0085 100644
--- a/crates/typst-layout/src/inline/finalize.rs
+++ b/crates/typst-layout/src/inline/finalize.rs
@@ -9,7 +9,6 @@ pub fn finalize(
engine: &mut Engine,
p: &Preparation,
lines: &[Line],
- styles: StyleChain,
region: Size,
expand: bool,
locator: &mut SplitLocator<'_>,
@@ -19,9 +18,10 @@ pub fn finalize(
let width = if !region.x.is_finite()
|| (!expand && lines.iter().all(|line| line.fr().is_zero()))
{
- region
- .x
- .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default())
+ region.x.min(
+ p.config.hanging_indent
+ + lines.iter().map(|line| line.width).max().unwrap_or_default(),
+ )
} else {
region.x
};
@@ -29,7 +29,7 @@ pub fn finalize(
// Stack the lines into one frame per region.
lines
.iter()
- .map(|line| commit(engine, p, line, width, region.y, locator, styles))
+ .map(|line| commit(engine, p, line, width, region.y, locator))
.collect::<SourceResult<_>>()
.map(Fragment::frames)
}
diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs
index 9f697380..bd08f30e 100644
--- a/crates/typst-layout/src/inline/line.rs
+++ b/crates/typst-layout/src/inline/line.rs
@@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine;
-use typst_library::foundations::NativeElement;
use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
-use typst_library::model::{ParLine, ParLineMarker};
+use typst_library::model::ParLineMarker;
use typst_library::text::{Lang, TextElem};
use typst_utils::Numeric;
@@ -135,7 +134,7 @@ pub fn line<'a>(
// Whether the line is justified.
let justify = full.ends_with(LINE_SEPARATOR)
- || (p.justify && breakpoint != Breakpoint::Mandatory);
+ || (p.config.justify && breakpoint != Breakpoint::Mandatory);
// Process dashes.
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
@@ -157,14 +156,14 @@ pub fn line<'a>(
// Add a hyphen at the line start, if a previous dash should be repeated.
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
if let Some(shaped) = items.first_text_mut() {
- shaped.prepend_hyphen(engine, p.fallback);
+ shaped.prepend_hyphen(engine, p.config.fallback);
}
}
// Add a hyphen at the line end, if we ended on a soft hyphen.
if dash == Some(Dash::Soft) {
if let Some(shaped) = items.last_text_mut() {
- shaped.push_hyphen(engine, p.fallback);
+ shaped.push_hyphen(engine, p.config.fallback);
}
}
@@ -234,13 +233,13 @@ where
{
// If there is nothing bidirectional going on, skip reordering.
let Some(bidi) = &p.bidi else {
- f(range, p.dir == Dir::RTL);
+ f(range, p.config.dir == Dir::RTL);
return;
};
// The bidi crate panics for empty lines.
if range.is_empty() {
- f(range, p.dir == Dir::RTL);
+ f(range, p.config.dir == Dir::RTL);
return;
}
@@ -308,13 +307,13 @@ fn collect_range<'a>(
/// punctuation marks at line start or line end.
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
if text.starts_with(BEGIN_PUNCT_PAT)
- || (p.cjk_latin_spacing && text.starts_with(is_of_cj_script))
+ || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script))
{
adjust_cj_at_line_start(p, items);
}
if text.ends_with(END_PUNCT_PAT)
- || (p.cjk_latin_spacing && text.ends_with(is_of_cj_script))
+ || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script))
{
adjust_cj_at_line_end(p, items);
}
@@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size);
- } else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() {
+ } else if p.config.cjk_latin_spacing
+ && glyph.is_cj_script()
+ && glyph.x_offset > Em::zero()
+ {
// If the first glyph is a CJK character adjusted by
// [`add_cjk_latin_spacing`], restore the original width.
let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
@@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size);
- } else if p.cjk_latin_spacing
+ } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one()
{
@@ -424,16 +426,15 @@ pub fn commit(
width: Abs,
full: Abs,
locator: &mut SplitLocator<'_>,
- styles: StyleChain,
) -> SourceResult<Frame> {
- let mut remaining = width - line.width - p.hang;
+ let mut remaining = width - line.width - p.config.hanging_indent;
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. In an RTL paragraph, the
// hanging indent arises naturally due to the line width.
- if p.dir == Dir::LTR {
- offset += p.hang;
+ if p.config.dir == Dir::LTR {
+ offset += p.config.hanging_indent;
}
// Handle hanging punctuation to the left.
@@ -554,11 +555,13 @@ pub fn commit(
let mut output = Frame::soft(size);
output.set_baseline(top);
- add_par_line_marker(&mut output, styles, engine, locator, top);
+ if let Some(marker) = &p.config.numbering_marker {
+ add_par_line_marker(&mut output, marker, engine, locator, top);
+ }
// Construct the line's frame.
for (offset, frame) in frames {
- let x = offset + p.align.position(remaining);
+ let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame);
}
@@ -575,26 +578,18 @@ pub fn commit(
/// number in the margin, is aligned to the line's baseline.
fn add_par_line_marker(
output: &mut Frame,
- styles: StyleChain,
+ marker: &Packed<ParLineMarker>,
engine: &mut Engine,
locator: &mut SplitLocator,
top: Abs,
) {
- let Some(numbering) = ParLine::numbering_in(styles) else { return };
- let margin = ParLine::number_margin_in(styles);
- let align = ParLine::number_align_in(styles);
-
- // Delay resolving the number clearance until line numbers are laid out to
- // avoid inconsistent spacing depending on varying font size.
- let clearance = ParLine::number_clearance_in(styles);
-
// Elements in tags must have a location for introspection to work. We do
// the work here instead of going through all of the realization process
// just for this, given we don't need to actually place the marker as we
// manually search for it in the frame later (when building a root flow,
// where line numbers can be displayed), so we just need it to be in a tag
// and to be valid (to have a location).
- let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
+ let mut marker = marker.clone();
let key = typst_utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc);
@@ -606,7 +601,7 @@ fn add_par_line_marker(
// line's general baseline. However, the line number will still need to
// manually adjust its own 'y' position based on its own baseline.
let pos = Point::with_y(top);
- output.push(pos, FrameItem::Tag(Tag::Start(marker)));
+ output.push(pos, FrameItem::Tag(Tag::Start(marker.pack())));
output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
}
diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs
index 87113c68..a9f21188 100644
--- a/crates/typst-layout/src/inline/linebreak.rs
+++ b/crates/typst-layout/src/inline/linebreak.rs
@@ -110,15 +110,7 @@ pub fn linebreak<'a>(
p: &'a Preparation<'a>,
width: Abs,
) -> Vec<Line<'a>> {
- let linebreaks = p.linebreaks.unwrap_or_else(|| {
- if p.justify {
- Linebreaks::Optimized
- } else {
- Linebreaks::Simple
- }
- });
-
- match linebreaks {
+ match p.config.linebreaks {
Linebreaks::Simple => linebreak_simple(engine, p, width),
Linebreaks::Optimized => linebreak_optimized(engine, p, width),
}
@@ -384,7 +376,7 @@ fn linebreak_optimized_approximate(
// Whether the line is justified. This is not 100% accurate w.r.t
// to line()'s behaviour, but good enough.
- let justify = p.justify && breakpoint != Breakpoint::Mandatory;
+ let justify = p.config.justify && breakpoint != Breakpoint::Mandatory;
// We don't really know whether the line naturally ends with a dash
// here, so we can miss that case, but it's ok, since all of this
@@ -573,7 +565,7 @@ fn raw_ratio(
// calculate the extra amount. Also, don't divide by zero.
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
// Normalize the amount by half the em size.
- ratio = 1.0 + extra_stretch / (p.size / 2.0);
+ ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0);
}
// The min value must be < MIN_RATIO, but how much smaller doesn't matter
@@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
return;
}
- let hyphenate = p.hyphenate != Some(false);
+ let hyphenate = p.config.hyphenate != Some(false);
let lb = LINEBREAK_DATA.as_borrowed();
- let segmenter = match p.lang {
+ let segmenter = match p.config.lang {
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
_ => &SEGMENTER,
};
@@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
/// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
- p.hyphenate
- .or_else(|| {
- let (_, item) = p.get(offset);
- let styles = item.text()?.styles;
- Some(TextElem::hyphenate_in(styles))
- })
- .unwrap_or(false)
+ p.config.hyphenate.unwrap_or_else(|| {
+ let (_, item) = p.get(offset);
+ match item.text() {
+ Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
+ None => false,
+ }
+ })
}
/// The text language at the given offset.
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
- let lang = p.lang.or_else(|| {
+ let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset);
let styles = item.text()?.styles;
Some(TextElem::lang_in(styles))
@@ -865,13 +857,13 @@ impl CostMetrics {
fn compute(p: &Preparation) -> Self {
Self {
// When justifying, we may stretch spaces below their natural width.
- min_ratio: if p.justify { MIN_RATIO } else { 0.0 },
- min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 },
+ min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
+ min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
// Approximate hyphen width for estimates.
- approx_hyphen_width: Em::new(0.33).at(p.size),
+ approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
// Costs.
- hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
- runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
+ hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
+ runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
}
}
diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs
index f8a36368..5ef820d0 100644
--- a/crates/typst-layout/src/inline/mod.rs
+++ b/crates/typst-layout/src/inline/mod.rs
@@ -13,12 +13,17 @@ 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::{Packed, StyleChain};
+use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
-use typst_library::layout::{Fragment, Size};
-use typst_library::model::ParElem;
+use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
+use typst_library::model::{
+ EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
+ TermsElem,
+};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
+use typst_library::text::{Costs, Lang, TextElem};
use typst_library::World;
+use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate;
@@ -98,7 +103,7 @@ fn layout_par_impl(
styles,
)?;
- layout_inline(
+ layout_inline_impl(
&mut engine,
&children,
&mut locator,
@@ -106,33 +111,134 @@ fn layout_par_impl(
region,
expand,
Some(situation),
+ &ConfigBase {
+ justify: elem.justify(styles),
+ linebreaks: elem.linebreaks(styles),
+ first_line_indent: elem.first_line_indent(styles),
+ hanging_indent: elem.hanging_indent(styles),
+ },
)
}
/// 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>,
+ shared: StyleChain<'a>,
+ region: Size,
+ expand: bool,
+) -> SourceResult<Fragment> {
+ layout_inline_impl(
+ engine,
+ children,
+ locator,
+ shared,
+ region,
+ expand,
+ None,
+ &ConfigBase {
+ justify: ParElem::justify_in(shared),
+ linebreaks: ParElem::linebreaks_in(shared),
+ first_line_indent: ParElem::first_line_indent_in(shared),
+ hanging_indent: ParElem::hanging_indent_in(shared),
+ },
+ )
+}
+
+/// The internal implementation of [`layout_inline`].
+#[allow(clippy::too_many_arguments)]
+fn layout_inline_impl<'a>(
+ engine: &mut Engine,
+ children: &[Pair<'a>],
+ locator: &mut SplitLocator<'a>,
+ shared: StyleChain<'a>,
region: Size,
expand: bool,
par: Option<ParSituation>,
+ base: &ConfigBase,
) -> SourceResult<Fragment> {
+ // Prepare configuration that is shared across the whole inline layout.
+ let config = configuration(base, children, shared, par);
+
// Collect all text into one string for BiDi analysis.
- let (text, segments, spans) =
- collect(children, engine, locator, styles, region, par)?;
+ let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
// Perform BiDi analysis and performs some preparation steps before we
// proceed to line breaking.
- let p = prepare(engine, children, &text, segments, spans, styles, par)?;
+ let p = prepare(engine, &config, &text, segments, spans)?;
// Break the text into lines.
- let lines = linebreak(engine, &p, region.x - p.hang);
+ let lines = linebreak(engine, &p, region.x - config.hanging_indent);
// Turn the selected lines into frames.
- finalize(engine, &p, &lines, styles, region, expand, locator)
+ finalize(engine, &p, &lines, region, expand, locator)
+}
+
+/// Determine the inline layout's configuration.
+fn configuration(
+ base: &ConfigBase,
+ children: &[Pair],
+ shared: StyleChain,
+ situation: Option<ParSituation>,
+) -> Config {
+ let justify = base.justify;
+ let font_size = TextElem::size_in(shared);
+ let dir = TextElem::dir_in(shared);
+
+ Config {
+ justify,
+ linebreaks: base.linebreaks.unwrap_or_else(|| {
+ if justify {
+ Linebreaks::Optimized
+ } else {
+ Linebreaks::Simple
+ }
+ }),
+ first_line_indent: {
+ let FirstLineIndent { amount, all } = base.first_line_indent;
+ if !amount.is_zero()
+ && match situation {
+ // First-line indent for the first paragraph after a list
+ // bullet just looks bad.
+ Some(ParSituation::First) => all && !in_list(shared),
+ Some(ParSituation::Consecutive) => true,
+ Some(ParSituation::Other) => all,
+ None => false,
+ }
+ && AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
+ {
+ amount.at(font_size)
+ } else {
+ Abs::zero()
+ }
+ },
+ hanging_indent: if situation.is_some() {
+ base.hanging_indent
+ } else {
+ Abs::zero()
+ },
+ numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
+ Packed::new(ParLineMarker::new(
+ numbering,
+ ParLine::number_align_in(shared),
+ ParLine::number_margin_in(shared),
+ // Delay resolving the number clearance until line numbers are
+ // laid out to avoid inconsistent spacing depending on varying
+ // font size.
+ ParLine::number_clearance_in(shared),
+ ))
+ }),
+ align: AlignElem::alignment_in(shared).fix(dir).x,
+ font_size,
+ dir,
+ hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
+ .map(|uniform| uniform.unwrap_or(justify)),
+ lang: shared_get(children, shared, TextElem::lang_in),
+ fallback: TextElem::fallback_in(shared),
+ cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
+ costs: TextElem::costs_in(shared),
+ }
}
/// Distinguishes between a few different kinds of paragraphs.
@@ -148,3 +254,66 @@ pub enum ParSituation {
/// Any other kind of paragraph.
Other,
}
+
+/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
+struct ConfigBase {
+ justify: bool,
+ linebreaks: Smart<Linebreaks>,
+ first_line_indent: FirstLineIndent,
+ hanging_indent: Abs,
+}
+
+/// Shared configuration for the whole inline layout.
+struct Config {
+ /// Whether to justify text.
+ justify: bool,
+ /// How to determine line breaks.
+ linebreaks: Linebreaks,
+ /// The indent the first line of a paragraph should have.
+ first_line_indent: Abs,
+ /// The indent that all but the first line of a paragraph should have.
+ hanging_indent: Abs,
+ /// Configuration for line numbering.
+ numbering_marker: Option<Packed<ParLineMarker>>,
+ /// The resolved horizontal alignment.
+ align: FixedAlignment,
+ /// The text size.
+ font_size: Abs,
+ /// The dominant direction.
+ dir: Dir,
+ /// A uniform hyphenation setting (only `Some(_)` if it's the same for all
+ /// children, otherwise `None`).
+ hyphenate: Option<bool>,
+ /// The text language (only `Some(_)` if it's the same for all
+ /// children, otherwise `None`).
+ lang: Option<Lang>,
+ /// Whether font fallback is enabled.
+ fallback: bool,
+ /// Whether to add spacing between CJK and Latin characters.
+ cjk_latin_spacing: bool,
+ /// Costs for various layout decisions.
+ costs: Costs,
+}
+
+/// 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)
+}
+
+/// Whether we have a list ancestor.
+///
+/// When we support some kind of more general ancestry mechanism, this can
+/// become more elegant.
+fn in_list(styles: StyleChain) -> bool {
+ ListElem::depth_in(styles).0 > 0
+ || !EnumElem::parents_in(styles).is_empty()
+ || TermsElem::within_in(styles)
+}
diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs
index 0344d433..5d7fcd7c 100644
--- a/crates/typst-layout/src/inline/prepare.rs
+++ b/crates/typst-layout/src/inline/prepare.rs
@@ -1,9 +1,4 @@
-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 typst_library::layout::{Dir, Em};
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
@@ -17,6 +12,8 @@ use super::*;
pub struct Preparation<'a> {
/// The full text.
pub text: &'a str,
+ /// Configuration for inline layout.
+ pub config: &'a Config,
/// Bidirectional text embedding levels.
///
/// This is `None` if all text directions are uniform (all the base
@@ -28,28 +25,6 @@ pub struct Preparation<'a> {
pub indices: Vec<usize>,
/// The span mapper.
pub spans: SpanMapper,
- /// Whether to hyphenate if it's the same for all children.
- pub hyphenate: Option<bool>,
- /// Costs for various layout decisions.
- pub costs: Costs,
- /// The dominant direction.
- pub dir: Dir,
- /// The text language if it's the same for all children.
- pub lang: Option<Lang>,
- /// The resolved horizontal alignment.
- pub align: FixedAlignment,
- /// Whether to justify text.
- pub justify: bool,
- /// 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.
- pub fallback: bool,
- /// How to determine line breaks.
- pub linebreaks: Smart<Linebreaks>,
- /// The text size.
- pub size: Abs,
}
impl<'a> Preparation<'a> {
@@ -80,15 +55,12 @@ impl<'a> Preparation<'a> {
#[typst_macros::time]
pub fn prepare<'a>(
engine: &mut Engine,
- children: &[Pair<'a>],
+ config: &'a Config,
text: &'a str,
segments: Vec<Segment<'a>>,
spans: SpanMapper,
- styles: StyleChain<'a>,
- situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> {
- let dir = TextElem::dir_in(styles);
- let default_level = match dir {
+ let default_level = match config.dir {
Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(),
};
@@ -124,51 +96,20 @@ pub fn prepare<'a>(
indices.extend(range.clone().map(|_| i));
}
- let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
- if cjk_latin_spacing {
+ if config.cjk_latin_spacing {
add_cjk_latin_spacing(&mut items);
}
- // Only apply hanging indent to real paragraphs.
- let hang = if situation.is_some() {
- ParElem::hanging_indent_in(styles)
- } else {
- Abs::zero()
- };
-
Ok(Preparation {
+ config,
text,
bidi: is_bidi.then_some(bidi),
items,
indices,
spans,
- hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
- costs: TextElem::costs_in(styles),
- dir,
- lang: shared_get(children, styles, TextElem::lang_in),
- align: AlignElem::alignment_in(styles).resolve(styles).x,
- justify: ParElem::justify_in(styles),
- hang,
- cjk_latin_spacing,
- fallback: TextElem::fallback_in(styles),
- linebreaks: ParElem::linebreaks_in(styles),
- size: TextElem::size_in(styles),
})
}
-/// 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/math/text.rs b/crates/typst-layout/src/math/text.rs
index 9a64992a..59ac5b08 100644
--- a/crates/typst-layout/src/math/text.rs
+++ b/crates/typst-layout/src/math/text.rs
@@ -107,7 +107,6 @@ fn layout_inline_text(
styles,
Size::splat(Abs::inf()),
false,
- None,
)?
.into_frame();
diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs
index 24b746b7..ea85aa94 100644
--- a/crates/typst-library/src/model/link.rs
+++ b/crates/typst-library/src/model/link.rs
@@ -11,7 +11,7 @@ use crate::foundations::{
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location;
use crate::layout::Position;
-use crate::text::{Hyphenate, TextElem};
+use crate::text::TextElem;
/// Links to a URL or a location in the document.
///
@@ -138,7 +138,7 @@ impl Show for Packed<LinkElem> {
impl ShowSet for Packed<LinkElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new();
- out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
+ out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out
}
}
diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs
index 12f4e4c5..30c2ea1d 100644
--- a/crates/typst-library/src/text/mod.rs
+++ b/crates/typst-library/src/text/mod.rs
@@ -51,7 +51,6 @@ use crate::foundations::{
};
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
use crate::math::{EquationElem, MathSize};
-use crate::model::ParElem;
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
use crate::World;
@@ -504,9 +503,8 @@ pub struct TextElem {
/// enabling hyphenation can
/// improve justification.
/// ```
- #[resolve]
#[ghost]
- pub hyphenate: Hyphenate,
+ pub hyphenate: Smart<bool>,
/// The "cost" of various choices when laying out text. A higher cost means
/// the layout engine will make the choice less often. Costs are specified
@@ -1110,27 +1108,6 @@ impl Resolve for TextDir {
}
}
-/// Whether to hyphenate text.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Hyphenate(pub Smart<bool>);
-
-cast! {
- Hyphenate,
- self => self.0.into_value(),
- v: Smart<bool> => Self(v),
-}
-
-impl Resolve for Hyphenate {
- type Output = bool;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- match self.0 {
- Smart::Auto => ParElem::justify_in(styles),
- Smart::Custom(v) => v,
- }
- }
-}
-
/// A set of stylistic sets to enable.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct StylisticSets(u32);
diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs
index 5bb21e43..b330c01e 100644
--- a/crates/typst-library/src/text/raw.rs
+++ b/crates/typst-library/src/text/raw.rs
@@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem};
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem};
-use crate::text::{
- FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
-};
+use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
use crate::visualize::Color;
use crate::World;
@@ -472,7 +470,7 @@ impl ShowSet for Packed<RawElem> {
let mut out = Styles::new();
out.set(TextElem::set_overhang(false));
out.set(TextElem::set_lang(Lang::ENGLISH));
- out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
+ out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));