summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2024-08-29 11:32:18 -0300
committerGitHub <noreply@github.com>2024-08-29 14:32:18 +0000
commite6cdcc53f3ba4dc1a0375f09249f3a09ea177cd4 (patch)
tree0c7a4a9d203c12995b563e215291f7de57c477da
parentef4482ce4b084aa97155dbde89f02dda3f7fb219 (diff)
Line numbers (#4516)
-rw-r--r--crates/typst/src/layout/flow.rs273
-rw-r--r--crates/typst/src/layout/inline/collect.rs5
-rw-r--r--crates/typst/src/layout/inline/finalize.rs4
-rw-r--r--crates/typst/src/layout/inline/line.rs56
-rw-r--r--crates/typst/src/layout/inline/mod.rs6
-rw-r--r--crates/typst/src/model/par.rs158
-rw-r--r--tests/ref/line-numbers-auto-alignment.pngbin0 -> 1099 bytes
-rw-r--r--tests/ref/line-numbers-clearance.pngbin0 -> 880 bytes
-rw-r--r--tests/ref/line-numbers-columns-alignment.pngbin0 -> 1255 bytes
-rw-r--r--tests/ref/line-numbers-columns-override.pngbin0 -> 1206 bytes
-rw-r--r--tests/ref/line-numbers-columns-rtl.pngbin0 -> 1208 bytes
-rw-r--r--tests/ref/line-numbers-columns.pngbin0 -> 1224 bytes
-rw-r--r--tests/ref/line-numbers-deduplication-tall-line.pngbin0 -> 4021 bytes
-rw-r--r--tests/ref/line-numbers-deduplication-zero-height-number.pngbin0 -> 1701 bytes
-rw-r--r--tests/ref/line-numbers-deduplication.pngbin0 -> 1668 bytes
-rw-r--r--tests/ref/line-numbers-default-alignment.pngbin0 -> 1347 bytes
-rw-r--r--tests/ref/line-numbers-enable.pngbin0 -> 909 bytes
-rw-r--r--tests/ref/line-numbers-margin.pngbin0 -> 1038 bytes
-rw-r--r--tests/ref/line-numbers-multi-columns.pngbin0 -> 815 bytes
-rw-r--r--tests/ref/line-numbers-nested-content.pngbin0 -> 1802 bytes
-rw-r--r--tests/ref/line-numbers-page-scope-quasi-empty-first-column.pngbin0 -> 917 bytes
-rw-r--r--tests/ref/line-numbers-page-scope-with-columns.pngbin0 -> 2316 bytes
-rw-r--r--tests/ref/line-numbers-page-scope.pngbin0 -> 2220 bytes
-rw-r--r--tests/ref/line-numbers-place-out-of-order.pngbin0 -> 791 bytes
-rw-r--r--tests/ref/line-numbers-rtl.pngbin0 -> 1364 bytes
-rw-r--r--tests/ref/line-numbers-start-alignment.pngbin0 -> 469 bytes
-rw-r--r--tests/suite/layout/line-numbers.typ249
27 files changed, 730 insertions, 21 deletions
diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs
index f99f8bea..fdec898d 100644
--- a/crates/typst/src/layout/flow.rs
+++ b/crates/typst/src/layout/flow.rs
@@ -10,20 +10,23 @@ use comemo::{Track, Tracked, TrackedMut};
use crate::diag::{bail, At, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
- Content, NativeElement, Packed, Resolve, Smart, StyleChain, Styles,
+ Content, NativeElement, Packed, Resolve, SequenceElem, Smart, StyleChain, Styles,
};
use crate::introspection::{
- Counter, CounterDisplayElem, CounterKey, Introspector, Location, Locator,
- LocatorLink, ManualPageCounter, SplitLocator, Tag, TagElem, TagKind,
+ Counter, CounterDisplayElem, CounterKey, CounterState, CounterUpdate, Introspector,
+ Location, Locator, LocatorLink, ManualPageCounter, SplitLocator, Tag, TagElem,
+ TagKind,
};
use crate::layout::{
Abs, AlignElem, Alignment, Axes, Binding, BlockElem, ColbreakElem, ColumnsElem, Dir,
FixedAlignment, FlushElem, Fr, Fragment, Frame, FrameItem, HAlignment, Length,
- OuterVAlignment, Page, PageElem, PagebreakElem, Paper, Parity, PlaceElem, Point,
- Ratio, Region, Regions, Rel, Sides, Size, Spacing, VAlignment, VElem,
+ OuterHAlignment, OuterVAlignment, Page, PageElem, PagebreakElem, Paper, Parity,
+ PlaceElem, Point, Ratio, Region, Regions, Rel, Sides, Size, Spacing, VAlignment,
+ VElem,
};
use crate::model::{
- Document, DocumentInfo, FootnoteElem, FootnoteEntry, Numbering, ParElem,
+ Document, DocumentInfo, FootnoteElem, FootnoteEntry, Numbering, ParElem, ParLine,
+ ParLineMarker, ParLineNumberingScope,
};
use crate::realize::{first_span, realize, Arenas, Pair};
use crate::syntax::Span;
@@ -799,6 +802,12 @@ struct FootnoteConfig {
gap: Abs,
}
+/// Information needed to generate a line number.
+struct CollectedParLine {
+ y: Abs,
+ marker: Packed<ParLineMarker>,
+}
+
/// A prepared item in a flow layout.
#[derive(Debug)]
enum FlowItem {
@@ -814,6 +823,12 @@ enum FlowItem {
align: Axes<FixedAlignment>,
/// Whether the frame sticks to the item after it (for orphan prevention).
sticky: bool,
+ /// Whether the frame comes from a rootable block, which may be laid
+ /// out as a root flow and thus display its own line numbers.
+ /// Therefore, we do not display line numbers for these frames.
+ ///
+ /// Currently, this is only used by columns.
+ rootable: bool,
/// Whether the frame is movable; that is, kept together with its
/// footnotes.
///
@@ -1094,6 +1109,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
frame,
align,
sticky: false,
+ rootable: false,
movable: true,
})?;
}
@@ -1111,12 +1127,13 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
// Fetch properties.
let sticky = block.sticky(styles);
let align = AlignElem::alignment_in(styles).resolve(styles);
+ let rootable = block.rootable(styles);
// If the block is "rootable" it may host footnotes. In that case, we
// defer rootness to it temporarily. We disable our own rootness to
// prevent duplicate footnotes.
let is_root = self.root;
- if is_root && block.rootable(styles) {
+ if is_root && rootable {
self.root = false;
self.regions.root = true;
}
@@ -1147,7 +1164,13 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
self.drain_tag(&mut frame);
frame.post_process(styles);
- self.handle_item(FlowItem::Frame { frame, align, sticky, movable: false })?;
+ self.handle_item(FlowItem::Frame {
+ frame,
+ align,
+ sticky,
+ rootable,
+ movable: false,
+ })?;
}
self.try_handle_footnotes(notes)?;
@@ -1347,7 +1370,14 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
&& !self.items.is_empty()
&& self.items.iter().all(FlowItem::is_out_of_flow)
{
- self.finished.push(Frame::soft(self.initial));
+ // Run line number layout here even though we have no line numbers
+ // to ensure we reset line numbers at the start of the page if
+ // requested, which is still necessary if e.g. the first column is
+ // empty when the others aren't.
+ let mut output = Frame::soft(self.initial);
+ self.layout_line_numbers(&mut output, self.initial, vec![])?;
+
+ self.finished.push(output);
self.regions.next();
self.initial = self.regions.size;
return Ok(());
@@ -1421,6 +1451,8 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
let mut float_bottom_offset = Abs::zero();
let mut footnote_offset = Abs::zero();
+ let mut lines: Vec<CollectedParLine> = vec![];
+
// Place all frames.
for item in self.items.drain(..) {
match item {
@@ -1432,12 +1464,20 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
let length = v.share(fr, remaining);
offset += length;
}
- FlowItem::Frame { frame, align, .. } => {
+ FlowItem::Frame { frame, align, rootable, .. } => {
ruler = ruler.max(align.y);
let x = align.x.position(size.x - frame.width());
let y = offset + ruler.position(size.y - used.y);
let pos = Point::new(x, y);
offset += frame.height();
+
+ // Do not display line numbers for frames coming from
+ // rootable blocks as they will display their own line
+ // numbers when laid out as a root flow themselves.
+ if self.root && !rootable {
+ collect_par_lines(&mut lines, &frame, pos, Abs::zero());
+ }
+
output.push_frame(pos, frame);
}
FlowItem::Placed { frame, x_align, y_align, delta, float, .. } => {
@@ -1469,6 +1509,10 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
let pos = Point::new(x, y)
+ delta.zip_map(size, Rel::relative_to).to_point();
+ if self.root {
+ collect_par_lines(&mut lines, &frame, pos, Abs::zero());
+ }
+
output.push_frame(pos, frame);
}
FlowItem::Footnote(frame) => {
@@ -1479,6 +1523,15 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
}
}
+ // Sort, deduplicate and layout line numbers.
+ //
+ // We do this after placing all frames since they might not necessarily
+ // be ordered by height (e.g. you can have a `place(bottom)` followed
+ // by a paragraph, but the paragraph appears at the top), so we buffer
+ // all line numbers to later sort and deduplicate them based on how
+ // close they are to each other in `layout_line_numbers`.
+ self.layout_line_numbers(&mut output, size, lines)?;
+
if force && !self.pending_tags.is_empty() {
let pos = Point::with_y(offset);
output.push_multiple(
@@ -1670,6 +1723,158 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
Ok(())
}
+ /// Layout the given collected lines' line numbers to an output frame.
+ ///
+ /// The numbers are placed either on the left margin (left border of the
+ /// frame) or on the right margin (right border). Before they are placed,
+ /// a line number counter reset is inserted if we're in the first column of
+ /// the page being currently laid out and the user requested for line
+ /// numbers to be reset at the start of every page.
+ fn layout_line_numbers(
+ &mut self,
+ output: &mut Frame,
+ size: Size,
+ mut lines: Vec<CollectedParLine>,
+ ) -> SourceResult<()> {
+ // Reset page-scoped line numbers if currently at the first column.
+ if self.root
+ && (self.columns == 1 || self.finished.len() % self.columns == 0)
+ && ParLine::numbering_scope_in(self.shared) == ParLineNumberingScope::Page
+ {
+ let reset =
+ CounterState::init(&CounterKey::Selector(ParLineMarker::elem().select()));
+ let counter = Counter::of(ParLineMarker::elem());
+ let update = counter.update(Span::detached(), CounterUpdate::Set(reset));
+ let locator = self.locator.next(&update);
+ let pod = Region::new(Axes::splat(Abs::zero()), Axes::splat(false));
+ let reset_frame =
+ layout_frame(self.engine, &update, locator, self.shared, pod)?;
+ output.push_frame(Point::zero(), reset_frame);
+ }
+
+ if lines.is_empty() {
+ // We always stop here if this is not the root flow.
+ return Ok(());
+ }
+
+ // Assume the line numbers aren't sorted by height.
+ // They must be sorted so we can deduplicate line numbers below based
+ // on vertical proximity.
+ lines.sort_by_key(|line| line.y);
+
+ // Buffer line number frames so we can align them horizontally later
+ // before placing, based on the width of the largest line number.
+ let mut line_numbers = vec![];
+ // Used for horizontal alignment.
+ let mut max_number_width = Abs::zero();
+ let mut prev_bottom = None;
+ for line in lines {
+ if prev_bottom.is_some_and(|prev_bottom| line.y < prev_bottom) {
+ // Lines are too close together. Display as the same line
+ // number.
+ continue;
+ }
+
+ let current_column = self.finished.len() % self.columns;
+ let number_margin = if self.columns >= 2 && current_column + 1 == self.columns
+ {
+ // The last column will always place line numbers at the end
+ // margin. This should become configurable in the future.
+ OuterHAlignment::End.resolve(self.shared)
+ } else {
+ line.marker.number_margin().resolve(self.shared)
+ };
+
+ let number_align = line
+ .marker
+ .number_align()
+ .map(|align| align.resolve(self.shared))
+ .unwrap_or_else(|| number_margin.inv());
+
+ let number_clearance = line.marker.number_clearance().resolve(self.shared);
+ let number = self.layout_line_number(line.marker)?;
+ let number_x = match number_margin {
+ FixedAlignment::Start => -number_clearance,
+ FixedAlignment::End => size.x + number_clearance,
+
+ // Shouldn't be specifiable by the user due to
+ // 'OuterHAlignment'.
+ FixedAlignment::Center => unreachable!(),
+ };
+ let number_pos = Point::new(number_x, line.y);
+
+ // Note that this line.y is larger than the previous due to
+ // sorting. Therefore, the check at the top of the loop ensures no
+ // line numbers will reasonably intersect with each other.
+ //
+ // We enforce a minimum spacing of 1pt between consecutive line
+ // numbers in case a zero-height frame is used.
+ prev_bottom = Some(line.y + number.height().max(Abs::pt(1.0)));
+
+ // Collect line numbers and compute the max width so we can align
+ // them later.
+ max_number_width.set_max(number.width());
+ line_numbers.push((number_pos, number, number_align, number_margin));
+ }
+
+ for (mut pos, number, align, margin) in line_numbers {
+ if matches!(margin, FixedAlignment::Start) {
+ // Move the line number backwards the more aligned to the left
+ // it is, instead of moving to the right when it's right
+ // aligned. We do it this way, without fully overriding the
+ // 'x' coordinate, to preserve the original clearance between
+ // the line numbers and the text.
+ pos.x -=
+ max_number_width - align.position(max_number_width - number.width());
+ } else {
+ // Move the line number forwards when aligned to the right.
+ // Leave as is when aligned to the left.
+ pos.x += align.position(max_number_width - number.width());
+ }
+
+ output.push_frame(pos, number);
+ }
+
+ Ok(())
+ }
+
+ /// Layout the line number associated with the given line marker.
+ ///
+ /// Produces a counter update and counter display with counter key
+ /// `ParLineMarker`. We use `ParLineMarker` as it is an element which is
+ /// not exposed to the user, as we don't want to expose the line number
+ /// counter at the moment, given that its semantics are inconsistent with
+ /// that of normal counters (the counter is updated based on height and not
+ /// on frame order / layer). When we find a solution to this, we should
+ /// switch to a counter on `ParLine` instead, thus exposing the counter as
+ /// `counter(par.line)` to the user.
+ fn layout_line_number(
+ &mut self,
+ marker: Packed<ParLineMarker>,
+ ) -> SourceResult<Frame> {
+ let counter = Counter::of(ParLineMarker::elem());
+ let counter_update = counter
+ .clone()
+ .update(Span::detached(), CounterUpdate::Step(NonZeroUsize::ONE));
+ let counter_display = CounterDisplayElem::new(
+ counter,
+ Smart::Custom(marker.numbering().clone()),
+ false,
+ );
+ let number = SequenceElem::new(vec![counter_update, counter_display.pack()]);
+ let locator = self.locator.next(&number);
+
+ let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
+ let mut frame =
+ layout_frame(self.engine, &number.pack(), locator, self.shared, pod)?;
+
+ // Ensure the baseline of the line number aligns with the line's own
+ // baseline.
+ frame.translate(Point::with_y(-frame.baseline()));
+
+ Ok(frame)
+ }
+
/// Collect all footnotes in a frame.
fn collect_footnotes(
&mut self,
@@ -1692,3 +1897,51 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
}
}
}
+
+/// Collect all numbered paragraph lines in the frame.
+/// The 'prev_y' parameter starts at 0 on the first call to 'collect_par_lines'.
+/// On each subframe we encounter, we add that subframe's position to 'prev_y',
+/// until we reach a line's tag, at which point we add the tag's position
+/// and finish. That gives us the relative height of the line from the start of
+/// the initial frame.
+fn collect_par_lines(
+ lines: &mut Vec<CollectedParLine>,
+ frame: &Frame,
+ frame_pos: Point,
+ prev_y: Abs,
+) {
+ for (pos, item) in frame.items() {
+ match item {
+ FrameItem::Group(group) => {
+ collect_par_lines(lines, &group.frame, frame_pos, prev_y + pos.y)
+ }
+
+ // Unlike footnotes, we don't need to guard against duplicate tags
+ // here, since we already deduplicate line markers based on their
+ // height later on, in `finish_region`.
+ FrameItem::Tag(tag) => {
+ let Some(marker) = tag.elem().to_packed::<ParLineMarker>() else {
+ continue;
+ };
+
+ // 1. 'prev_y' is the accumulated relative height from the top
+ // of the frame we're searching so far;
+ // 2. 'prev_y + pos.y' gives us the final relative height of
+ // the line we just found from the top of the initial frame;
+ // 3. 'frame_pos.y' is the height of the initial frame relative
+ // to the root flow (and thus its absolute 'y');
+ // 4. Therefore, 'y' will be the line's absolute 'y' in the
+ // page based on its marker's position, and thus the 'y' we
+ // should use for line numbers. In particular, this represents
+ // the 'y' at the line's general baseline, due to the marker
+ // placement logic within the 'line::commit()' function in the
+ // 'inline' module. We only account for the line number's own
+ // baseline later, upon layout.
+ let y = frame_pos.y + prev_y + pos.y;
+
+ lines.push(CollectedParLine { y, marker: marker.clone() });
+ }
+ _ => {}
+ }
+ }
+}
diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst/src/layout/inline/collect.rs
index 624eedf3..dbebcf91 100644
--- a/crates/typst/src/layout/inline/collect.rs
+++ b/crates/typst/src/layout/inline/collect.rs
@@ -1,7 +1,7 @@
use super::*;
use crate::diag::bail;
use crate::foundations::{Packed, Resolve};
-use crate::introspection::{Tag, TagElem};
+use crate::introspection::{SplitLocator, Tag, TagElem};
use crate::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
Spacing,
@@ -117,13 +117,12 @@ impl Segment<'_> {
pub fn collect<'a>(
children: &'a StyleVec,
engine: &mut Engine<'_>,
- locator: Locator<'a>,
+ locator: &mut SplitLocator<'a>,
styles: &'a StyleChain<'a>,
region: Size,
consecutive: bool,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len());
- let mut locator = locator.split();
let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(*styles);
diff --git a/crates/typst/src/layout/inline/finalize.rs b/crates/typst/src/layout/inline/finalize.rs
index 03493af5..082e3613 100644
--- a/crates/typst/src/layout/inline/finalize.rs
+++ b/crates/typst/src/layout/inline/finalize.rs
@@ -1,4 +1,5 @@
use super::*;
+use crate::introspection::SplitLocator;
use crate::utils::Numeric;
/// Turns the selected lines into frames.
@@ -10,6 +11,7 @@ pub fn finalize(
styles: StyleChain,
region: Size,
expand: bool,
+ locator: &mut SplitLocator<'_>,
) -> SourceResult<Fragment> {
// Determine the paragraph's width: Full width of the region if we should
// expand or there's fractional spacing, fit-to-width otherwise.
@@ -27,7 +29,7 @@ pub fn finalize(
let shrink = ParElem::shrink_in(styles);
lines
.iter()
- .map(|line| commit(engine, p, line, width, region.y, shrink))
+ .map(|line| commit(engine, p, line, width, region.y, shrink, locator, styles))
.collect::<SourceResult<_>>()
.map(Fragment::frames)
}
diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst/src/layout/inline/line.rs
index d9218e82..b1ac11ca 100644
--- a/crates/typst/src/layout/inline/line.rs
+++ b/crates/typst/src/layout/inline/line.rs
@@ -3,7 +3,10 @@ use std::ops::{Deref, DerefMut};
use super::*;
use crate::engine::Engine;
+use crate::foundations::NativeElement;
+use crate::introspection::{SplitLocator, Tag};
use crate::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
+use crate::model::{ParLine, ParLineMarker};
use crate::text::{Lang, TextElem};
use crate::utils::Numeric;
@@ -406,6 +409,7 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
}
/// Commit to a line and build its frame.
+#[allow(clippy::too_many_arguments)]
pub fn commit(
engine: &mut Engine,
p: &Preparation,
@@ -413,6 +417,8 @@ pub fn commit(
width: Abs,
full: Abs,
shrink: bool,
+ locator: &mut SplitLocator<'_>,
+ styles: StyleChain,
) -> SourceResult<Frame> {
let mut remaining = width - line.width - p.hang;
let mut offset = Abs::zero();
@@ -546,6 +552,8 @@ pub fn commit(
let mut output = Frame::soft(size);
output.set_baseline(top);
+ add_par_line_marker(&mut output, styles, engine, locator, top);
+
// Construct the line's frame.
for (offset, frame) in frames {
let x = offset + p.align.position(remaining);
@@ -556,6 +564,54 @@ pub fn commit(
Ok(output)
}
+/// Adds a paragraph line marker to a paragraph line's output frame if
+/// line numbering is not `None` at this point. Ensures other style properties,
+/// namely number margin, number align and number clearance, are stored in the
+/// marker as well.
+///
+/// The `top` parameter is used to ensure the marker, and thus the line's
+/// number in the margin, is aligned to the line's baseline.
+fn add_par_line_marker(
+ output: &mut Frame,
+ styles: StyleChain,
+ engine: &mut Engine,
+ locator: &mut SplitLocator,
+ top: Abs,
+) {
+ if let Some(numbering) = ParLine::numbering_in(styles) {
+ let number_margin = ParLine::number_margin_in(styles);
+ let number_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 number_clearance = ParLine::number_clearance_in(styles);
+
+ let mut par_line =
+ ParLineMarker::new(numbering, number_align, number_margin, number_clearance)
+ .pack();
+
+ // 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 hash = crate::utils::hash128(&par_line);
+ let location = locator.next_location(engine.introspector, hash);
+ par_line.set_location(location);
+
+ // Create a tag through which we can search for this line's marker
+ // later. Its 'x' coordinate is not important, just the 'y'
+ // coordinate, as that's what is used for line numbers. We will place
+ // the tag among other subframes in the line such that it is aligned
+ // with the line's general baseline. However, the line number will
+ // still need to manually adjust its own 'y' position based on its own
+ // baseline.
+ let tag = Tag::new(par_line, hash);
+ output.push(Point::with_y(top), FrameItem::Tag(tag));
+ }
+}
+
/// How much a character should hang into the end margin.
///
/// For more discussion, see:
diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs
index 192b37e9..889a028d 100644
--- a/crates/typst/src/layout/inline/mod.rs
+++ b/crates/typst/src/layout/inline/mod.rs
@@ -78,9 +78,11 @@ fn layout_inline_impl(
route: Route::extend(route),
};
+ let mut locator = locator.split();
+
// Collect all text into one string for BiDi analysis.
let (text, segments, spans) =
- collect(children, &mut engine, locator, &styles, region, consecutive)?;
+ collect(children, &mut engine, &mut locator, &styles, region, consecutive)?;
// Perform BiDi analysis and then prepares paragraph layout.
let p = prepare(&mut engine, children, &text, segments, spans, styles)?;
@@ -89,5 +91,5 @@ fn layout_inline_impl(
let lines = linebreak(&engine, &p, region.x - p.hang);
// Turn the selected lines into frames.
- finalize(&mut engine, &p, &lines, styles, region, expand)
+ finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator)
}
diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs
index 7867c89d..326d151e 100644
--- a/crates/typst/src/model/par.rs
+++ b/crates/typst/src/model/par.rs
@@ -1,12 +1,14 @@
use std::fmt::{self, Debug, Formatter};
-use crate::diag::SourceResult;
+use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
- elem, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, StyleVec,
- Unlabellable,
+ elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart,
+ StyleVec, Unlabellable,
};
-use crate::layout::{Em, Length};
+use crate::introspection::{Count, CounterUpdate, Locatable};
+use crate::layout::{Abs, Em, HAlignment, Length, OuterHAlignment};
+use crate::model::Numbering;
use crate::utils::singleton;
/// Arranges text, spacing and inline-level elements into a paragraph.
@@ -34,7 +36,7 @@ use crate::utils::singleton;
/// let $a$ be the smallest of the
/// three integers. Then, we ...
/// ```
-#[elem(title = "Paragraph", Debug, Construct)]
+#[elem(scope, title = "Paragraph", Debug, Construct)]
pub struct ParElem {
/// The spacing between lines.
///
@@ -143,6 +145,12 @@ pub struct ParElem {
pub children: StyleVec,
}
+#[scope]
+impl ParElem {
+ #[elem]
+ 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
@@ -206,3 +214,143 @@ impl ParbreakElem {
}
impl Unlabellable for Packed<ParbreakElem> {}
+
+/// A paragraph line.
+///
+/// This element is exclusively used for line number configuration and cannot
+/// be placed.
+#[elem(name = "line", title = "Paragraph Line", Construct, Locatable)]
+pub struct ParLine {
+ /// How to number each line. Accepts a
+ /// [numbering pattern or function]($numbering).
+ ///
+ /// ```example
+ /// #set par.line(numbering: "1")
+ ///
+ /// Roses are red. \
+ /// Violets are blue. \
+ /// Typst is awesome.
+ /// ```
+ #[ghost]
+ pub numbering: Option<Numbering>,
+
+ /// The alignment of line numbers associated with each line.
+ ///
+ /// The default of `auto` will provide a smart default where numbers grow
+ /// horizontally away from the text, considering the margin they're in and
+ /// the current text direction.
+ ///
+ /// ```example
+ /// #set par.line(numbering: "I", number-align: left)
+ ///
+ /// Hello world! \
+ /// Today is a beautiful day \
+ /// For exploring the world.
+ /// ```
+ #[ghost]
+ pub number_align: Smart<HAlignment>,
+
+ /// The margin at which line numbers appear.
+ ///
+ /// ```example
+ /// #set par.line(numbering: "1", number-margin: right)
+ ///
+ /// = Report
+ /// - Brightness: Dark, yet darker
+ /// - Readings: Negative
+ /// ```
+ #[ghost]
+ #[default(OuterHAlignment::Start)]
+ pub number_margin: OuterHAlignment,
+
+ /// The distance between line numbers and text.
+ ///
+ /// ```example
+ /// #set par.line(
+ /// numbering: "1",
+ /// number-clearance: 0.5pt
+ /// )
+ ///
+ /// Typesetting \
+ /// Styling \
+ /// Layout
+ /// ```
+ #[ghost]
+ #[default(Length::from(Abs::cm(1.0)))]
+ pub number_clearance: Length,
+
+ /// Controls when to reset line numbering.
+ ///
+ /// Possible options are `"document"`, indicating the line number counter
+ /// is never reset, or `"page"`, indicating it is reset on every page.
+ ///
+ /// ```example
+ /// #set par.line(
+ /// numbering: "1.",
+ /// numbering-scope: "page"
+ /// )
+ ///
+ /// First line \
+ /// Second line
+ /// #pagebreak()
+ /// First line again \
+ /// Second line again
+ /// ```
+ #[ghost]
+ #[default(ParLineNumberingScope::Document)]
+ pub numbering_scope: ParLineNumberingScope,
+}
+
+impl Construct for ParLine {
+ fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
+ bail!(args.span, "cannot be constructed manually");
+ }
+}
+
+/// Possible line numbering scope options, indicating how often the line number
+/// counter should be reset.
+#[derive(Debug, Cast, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum ParLineNumberingScope {
+ /// Indicates the line number counter spans the whole document, that is,
+ /// is never automatically reset.
+ Document,
+ /// Indicates the line number counter should be reset at the start of every
+ /// new page.
+ Page,
+}
+
+/// A marker used to indicate the presence of a line.
+///
+/// This element is added to each line in a paragraph and later searched to
+/// find out where to add line numbers.
+#[elem(Construct, Locatable, Count)]
+pub struct ParLineMarker {
+ #[internal]
+ #[required]
+ pub numbering: Numbering,
+
+ #[internal]
+ #[required]
+ pub number_align: Smart<HAlignment>,
+
+ #[internal]
+ #[required]
+ pub number_margin: OuterHAlignment,
+
+ #[internal]
+ #[required]
+ pub number_clearance: Length,
+}
+
+impl Construct for ParLineMarker {
+ fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
+ bail!(args.span, "cannot be constructed manually");
+ }
+}
+
+impl Count for Packed<ParLineMarker> {
+ fn update(&self) -> Option<CounterUpdate> {
+ // The line counter must be updated manually by the root flow.
+ None
+ }
+}
diff --git a/tests/ref/line-numbers-auto-alignment.png b/tests/ref/line-numbers-auto-alignment.png
new file mode 100644
index 00000000..80f8d45d
--- /dev/null
+++ b/tests/ref/line-numbers-auto-alignment.png
Binary files differ
diff --git a/tests/ref/line-numbers-clearance.png b/tests/ref/line-numbers-clearance.png
new file mode 100644
index 00000000..142d3a2f
--- /dev/null
+++ b/tests/ref/line-numbers-clearance.png
Binary files differ
diff --git a/tests/ref/line-numbers-columns-alignment.png b/tests/ref/line-numbers-columns-alignment.png
new file mode 100644
index 00000000..6367bf08
--- /dev/null
+++ b/tests/ref/line-numbers-columns-alignment.png
Binary files differ
diff --git a/tests/ref/line-numbers-columns-override.png b/tests/ref/line-numbers-columns-override.png
new file mode 100644
index 00000000..d89b6007
--- /dev/null
+++ b/tests/ref/line-numbers-columns-override.png
Binary files differ
diff --git a/tests/ref/line-numbers-columns-rtl.png b/tests/ref/line-numbers-columns-rtl.png
new file mode 100644
index 00000000..e2108016
--- /dev/null
+++ b/tests/ref/line-numbers-columns-rtl.png
Binary files differ
diff --git a/tests/ref/line-numbers-columns.png b/tests/ref/line-numbers-columns.png
new file mode 100644
index 00000000..e6e3cd51
--- /dev/null
+++ b/tests/ref/line-numbers-columns.png
Binary files differ
diff --git a/tests/ref/line-numbers-deduplication-tall-line.png b/tests/ref/line-numbers-deduplication-tall-line.png
new file mode 100644
index 00000000..d34a13d6
--- /dev/null
+++ b/tests/ref/line-numbers-deduplication-tall-line.png
Binary files differ
diff --git a/tests/ref/line-numbers-deduplication-zero-height-number.png b/tests/ref/line-numbers-deduplication-zero-height-number.png
new file mode 100644
index 00000000..b203a455
--- /dev/null
+++ b/tests/ref/line-numbers-deduplication-zero-height-number.png
Binary files differ
diff --git a/tests/ref/line-numbers-deduplication.png b/tests/ref/line-numbers-deduplication.png
new file mode 100644
index 00000000..82493421
--- /dev/null
+++ b/tests/ref/line-numbers-deduplication.png
Binary files differ
diff --git a/tests/ref/line-numbers-default-alignment.png b/tests/ref/line-numbers-default-alignment.png
new file mode 100644
index 00000000..5a39674d
--- /dev/null
+++ b/tests/ref/line-numbers-default-alignment.png
Binary files differ
diff --git a/tests/ref/line-numbers-enable.png b/tests/ref/line-numbers-enable.png
new file mode 100644
index 00000000..927096a5
--- /dev/null
+++ b/tests/ref/line-numbers-enable.png
Binary files differ
diff --git a/tests/ref/line-numbers-margin.png b/tests/ref/line-numbers-margin.png
new file mode 100644
index 00000000..94e03b26
--- /dev/null
+++ b/tests/ref/line-numbers-margin.png
Binary files differ
diff --git a/tests/ref/line-numbers-multi-columns.png b/tests/ref/line-numbers-multi-columns.png
new file mode 100644
index 00000000..45cb5378
--- /dev/null
+++ b/tests/ref/line-numbers-multi-columns.png
Binary files differ
diff --git a/tests/ref/line-numbers-nested-content.png b/tests/ref/line-numbers-nested-content.png
new file mode 100644
index 00000000..570aa6b0
--- /dev/null
+++ b/tests/ref/line-numbers-nested-content.png
Binary files differ
diff --git a/tests/ref/line-numbers-page-scope-quasi-empty-first-column.png b/tests/ref/line-numbers-page-scope-quasi-empty-first-column.png
new file mode 100644
index 00000000..4606311d
--- /dev/null
+++ b/tests/ref/line-numbers-page-scope-quasi-empty-first-column.png
Binary files differ
diff --git a/tests/ref/line-numbers-page-scope-with-columns.png b/tests/ref/line-numbers-page-scope-with-columns.png
new file mode 100644
index 00000000..99864a60
--- /dev/null
+++ b/tests/ref/line-numbers-page-scope-with-columns.png
Binary files differ
diff --git a/tests/ref/line-numbers-page-scope.png b/tests/ref/line-numbers-page-scope.png
new file mode 100644
index 00000000..330cfe10
--- /dev/null
+++ b/tests/ref/line-numbers-page-scope.png
Binary files differ
diff --git a/tests/ref/line-numbers-place-out-of-order.png b/tests/ref/line-numbers-place-out-of-order.png
new file mode 100644
index 00000000..b1a4050a
--- /dev/null
+++ b/tests/ref/line-numbers-place-out-of-order.png
Binary files differ
diff --git a/tests/ref/line-numbers-rtl.png b/tests/ref/line-numbers-rtl.png
new file mode 100644
index 00000000..58ed9d09
--- /dev/null
+++ b/tests/ref/line-numbers-rtl.png
Binary files differ
diff --git a/tests/ref/line-numbers-start-alignment.png b/tests/ref/line-numbers-start-alignment.png
new file mode 100644
index 00000000..edc3dd15
--- /dev/null
+++ b/tests/ref/line-numbers-start-alignment.png
Binary files differ
diff --git a/tests/suite/layout/line-numbers.typ b/tests/suite/layout/line-numbers.typ
new file mode 100644
index 00000000..5ee53e25
--- /dev/null
+++ b/tests/suite/layout/line-numbers.typ
@@ -0,0 +1,249 @@
+--- line-numbers-enable ---
+#set page(margin: (left: 1.5cm))
+#set par.line(numbering: "1")
+
+First line \
+Second line \
+Third line
+
+--- line-numbers-clearance ---
+#set page(margin: (left: 1.5cm))
+#set par.line(numbering: "1", number-clearance: 0cm)
+
+First line \
+Second line \
+Third line
+
+--- line-numbers-margin ---
+#set page(margin: (right: 3cm))
+#set par.line(numbering: "1", number-clearance: 1.5cm, number-margin: end)
+
+First line \
+Second line \
+Third line
+
+--- line-numbers-default-alignment ---
+#set page(margin: (left: 2cm))
+#set par.line(numbering: "1")
+a
+#([\ a] * 15)
+
+--- line-numbers-start-alignment ---
+#set page(margin: (left: 2cm))
+#set par.line(numbering: "i", number-align: start)
+a \
+a
+#pagebreak()
+a \
+a \
+a
+
+--- line-numbers-auto-alignment ---
+#set page(margin: (right: 3cm))
+#set par.line(numbering: "i", number-clearance: 1.5cm, number-margin: end)
+
+First line \
+Second line \
+Third line
+
+--- line-numbers-rtl ---
+#set page(margin: (right: 2cm))
+#set text(dir: rtl)
+#set par.line(numbering: "1")
+a
+#([\ a] * 15)
+
+--- line-numbers-columns ---
+#set page(columns: 2, margin: (x: 1.5em))
+#set par.line(numbering: "1", number-clearance: 0.5em)
+
+Hello \
+Beautiful \
+World
+#colbreak()
+Birds \
+In the \
+Sky
+
+--- line-numbers-columns-alignment ---
+#set page(columns: 2, margin: (x: 1.5em))
+#set par.line(numbering: "i", number-clearance: 0.5em)
+
+Hello \
+Beautiful \
+World
+#colbreak()
+Birds \
+In the \
+Sky
+
+--- line-numbers-multi-columns ---
+#set page(columns: 3, margin: (x: 1.5em))
+#set par.line(numbering: "1", number-clearance: 0.5em)
+
+A \
+B \
+C
+#colbreak()
+D \
+E \
+F
+#colbreak()
+G \
+H \
+I
+
+--- line-numbers-columns-rtl ---
+#set page(columns: 2, margin: (x: 1.5em))
+#set par.line(numbering: "1", number-clearance: 0.5em)
+#set text(dir: rtl)
+
+Hello \
+Beautiful \
+World
+#colbreak()
+Birds \
+In the \
+Sky
+
+--- line-numbers-columns-override ---
+#set columns(gutter: 1.5em)
+#set page(columns: 2, margin: (x: 1.5em))
+#set par.line(numbering: "1", number-margin: end, number-clearance: 0.5em)
+
+Hello \
+Beautiful \
+World
+#colbreak()
+Birds \
+In the \
+Sky
+
+--- line-numbers-page-scope ---
+#set page(margin: (left: 2cm))
+#set par.line(numbering: "1", numbering-scope: "page")
+
+First line \
+Second line
+#pagebreak()
+Back to first line \
+Second line again
+#page[
+ Once again, first \
+ And second
+]
+Back to first
+
+--- line-numbers-page-scope-with-columns ---
+#set page(margin: (x: 1.1cm), columns: 2)
+#set par.line(
+ numbering: "1",
+ number-clearance: 0.5cm,
+ numbering-scope: "page"
+)
+
+A \
+A \
+A
+#colbreak()
+B \
+B \
+B
+#pagebreak()
+One \
+Two \
+Three
+#colbreak()
+Four \
+Five \
+Six
+#page[
+ Page \
+ Elem
+ #colbreak()
+ Number \
+ Reset
+]
+We're back
+#colbreak()
+Bye!
+
+--- line-numbers-page-scope-quasi-empty-first-column ---
+// Ensure this case (handled separately internally) is properly handled.
+#set page(margin: (x: 1.1cm), height: 2cm, columns: 2)
+#set par.line(
+ numbering: "1",
+ number-clearance: 0.5cm,
+ numbering-scope: "page"
+)
+
+First line
+#colbreak()
+Second line
+#pagebreak()
+#place[]
+#box(height: 2cm)[First!]
+
+--- line-numbers-nested-content ---
+#set page(margin: (left: 1.5cm))
+#set par.line(numbering: "1", number-clearance: 0.5cm)
+
+#grid(
+ columns: (1fr, 1fr),
+ column-gutter: 0.5cm,
+ inset: 5pt,
+ block[A\ #box(lorem(5))], [Roses\ are\ red],
+ [AAA], [],
+ [], block[BBB\ CCC],
+)
+
+--- line-numbers-place-out-of-order ---
+#set page(margin: (left: 1.5cm))
+#set par.line(numbering: "1", number-clearance: 0.5cm)
+
+#place(bottom)[Line 4]
+
+Line 1\
+Line 2\
+Line 3
+#v(1cm)
+
+--- line-numbers-deduplication ---
+#set page(margin: (left: 1.5cm))
+#set par.line(numbering: "1", number-clearance: 0.5cm)
+
+#grid(
+ columns: (1fr, 1fr),
+ column-gutter: 0.5cm,
+ row-gutter: 5pt,
+ lorem(5), [A\ B\ C],
+ [DDD], [DDD],
+ [This is], move(dy: 2pt)[tough]
+)
+
+--- line-numbers-deduplication-tall-line ---
+#set page(margin: (left: 1.5cm))
+#set par.line(numbering: "1", number-clearance: 0.5cm)
+
+#grid(
+ columns: (1fr, 1fr),
+ column-gutter: 0.5cm,
+ stroke: 0.5pt,
+
+ grid.cell(rowspan: 2)[very #box(fill: red, height: 4cm)[tall]],
+ grid.cell(inset: (y: 0.5pt))[Line 1\ Line 2\ Line 3],
+ grid.cell(inset: (y: 0.5pt))[Line 4\ Line 5\ Line 6\ Line 7\ Line 8\ Line 9\ End]
+)
+
+--- line-numbers-deduplication-zero-height-number ---
+#set page(margin: (left: 1.5cm))
+#set par.line(numbering: n => move(dy: -0.6em, box(height: 0pt)[#n]), number-clearance: 0.5cm)
+
+#grid(
+ columns: (1fr, 1fr),
+ column-gutter: 0.5cm,
+ row-gutter: 5pt,
+ lorem(5), [A\ B\ C],
+ [DDD], [DDD],
+ [This is], move(dy: 3pt)[tough]
+)