From e6cdcc53f3ba4dc1a0375f09249f3a09ea177cd4 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:32:18 -0300 Subject: [PATCH] Line numbers (#4516) --- crates/typst/src/layout/flow.rs | 273 +++++++++++++++++- crates/typst/src/layout/inline/collect.rs | 5 +- crates/typst/src/layout/inline/finalize.rs | 4 +- crates/typst/src/layout/inline/line.rs | 56 ++++ crates/typst/src/layout/inline/mod.rs | 6 +- crates/typst/src/model/par.rs | 158 +++++++++- tests/ref/line-numbers-auto-alignment.png | Bin 0 -> 1099 bytes tests/ref/line-numbers-clearance.png | Bin 0 -> 880 bytes tests/ref/line-numbers-columns-alignment.png | Bin 0 -> 1255 bytes tests/ref/line-numbers-columns-override.png | Bin 0 -> 1206 bytes tests/ref/line-numbers-columns-rtl.png | Bin 0 -> 1208 bytes tests/ref/line-numbers-columns.png | Bin 0 -> 1224 bytes .../line-numbers-deduplication-tall-line.png | Bin 0 -> 4021 bytes ...mbers-deduplication-zero-height-number.png | Bin 0 -> 1701 bytes tests/ref/line-numbers-deduplication.png | Bin 0 -> 1668 bytes tests/ref/line-numbers-default-alignment.png | Bin 0 -> 1347 bytes tests/ref/line-numbers-enable.png | Bin 0 -> 909 bytes tests/ref/line-numbers-margin.png | Bin 0 -> 1038 bytes tests/ref/line-numbers-multi-columns.png | Bin 0 -> 815 bytes tests/ref/line-numbers-nested-content.png | Bin 0 -> 1802 bytes ...rs-page-scope-quasi-empty-first-column.png | Bin 0 -> 917 bytes .../line-numbers-page-scope-with-columns.png | Bin 0 -> 2316 bytes tests/ref/line-numbers-page-scope.png | Bin 0 -> 2220 bytes tests/ref/line-numbers-place-out-of-order.png | Bin 0 -> 791 bytes tests/ref/line-numbers-rtl.png | Bin 0 -> 1364 bytes tests/ref/line-numbers-start-alignment.png | Bin 0 -> 469 bytes tests/suite/layout/line-numbers.typ | 249 ++++++++++++++++ 27 files changed, 730 insertions(+), 21 deletions(-) create mode 100644 tests/ref/line-numbers-auto-alignment.png create mode 100644 tests/ref/line-numbers-clearance.png create mode 100644 tests/ref/line-numbers-columns-alignment.png create mode 100644 tests/ref/line-numbers-columns-override.png create mode 100644 tests/ref/line-numbers-columns-rtl.png create mode 100644 tests/ref/line-numbers-columns.png create mode 100644 tests/ref/line-numbers-deduplication-tall-line.png create mode 100644 tests/ref/line-numbers-deduplication-zero-height-number.png create mode 100644 tests/ref/line-numbers-deduplication.png create mode 100644 tests/ref/line-numbers-default-alignment.png create mode 100644 tests/ref/line-numbers-enable.png create mode 100644 tests/ref/line-numbers-margin.png create mode 100644 tests/ref/line-numbers-multi-columns.png create mode 100644 tests/ref/line-numbers-nested-content.png create mode 100644 tests/ref/line-numbers-page-scope-quasi-empty-first-column.png create mode 100644 tests/ref/line-numbers-page-scope-with-columns.png create mode 100644 tests/ref/line-numbers-page-scope.png create mode 100644 tests/ref/line-numbers-place-out-of-order.png create mode 100644 tests/ref/line-numbers-rtl.png create mode 100644 tests/ref/line-numbers-start-alignment.png create mode 100644 tests/suite/layout/line-numbers.typ diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index f99f8bea7..fdec898d9 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, +} + /// A prepared item in a flow layout. #[derive(Debug)] enum FlowItem { @@ -814,6 +823,12 @@ enum FlowItem { align: Axes, /// 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 = 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, + ) -> 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, + ) -> SourceResult { + 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, + 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::() 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 624eedf32..dbebcf91b 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>, 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 03493af5a..082e36137 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 { // 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::>() .map(Fragment::frames) } diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst/src/layout/inline/line.rs index d9218e82a..b1ac11ca5 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 { 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 192b37e9d..889a028d5 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 7867c89da..326d151eb 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 { // The paragraph constructor is special: It doesn't create a paragraph @@ -206,3 +214,143 @@ impl ParbreakElem { } impl Unlabellable for Packed {} + +/// 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, + + /// 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, + + /// 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 { + 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, + + #[internal] + #[required] + pub number_margin: OuterHAlignment, + + #[internal] + #[required] + pub number_clearance: Length, +} + +impl Construct for ParLineMarker { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Count for Packed { + fn update(&self) -> Option { + // 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 0000000000000000000000000000000000000000..80f8d45d404596667875a5697b4d72718eabaa22 GIT binary patch literal 1099 zcmV-R1ho5!P)q+N})8N-M}ZiXfn3$DtKD6ngyivx_ZWdC}(qfhE89-E;Lye#!6qJl`GI zqIR$_3$ri_Zvb5PZe>3Aan6gg1>{}oqk!=apoR0rF&N~zKm+?bTULcR&Xb~}YhsZ= z?#(`rM>ubpkD=6SIjZ;a694Q>qsu56R%jh*Gx+!2O4>6rg05X*wF`y|= zg#b{%jTdle7NH3EZEy^ryCg!1;f39}T8h|d?8IROo`I(P76iIT;riqH+{}UG21BCS ze5!m16j{>G=#rY)$;HC|2iRhLm9R^wNXJWa#7p2?@*(7To|Tcof*ZKKjF(m=gKy2O z;UT<}WN^!;^8juNM=@Db?Q!shMwF;)irwg`jPVh+arMS;Jk2~Z7~);&%*7j{;SLVd zXgV?G3SX2Yxl;N}St#=nMz#v?<4G(b3?{x|f8Cv)dq)8H11GiDFQBmVA|F*Ul1svR zfDAszph*3N2&=Pix|oH14{+Qp{|R|YMJh@J@GWjDg50zF$zbVesT@x~FV#SD>=)O> zcl>5|W|}oQ&y`wGoa%rtS&~odVy+3|jO^rM;cWshY!O@=1)Un}t966|Ug2PF8tZF( z5ukZj^Sd8^Z!XmWs#*&uvQ-SXmCf{2I7#7Zo_9~ZH4vq`zm9^Y4CUHR&%G|{O(Iw< z>!jTz@>L>`!AsGQpU1(!5eUv*FrcZR9gp@k5yIjKeOl5$be$nkJ@{Gqub{{hpW(%q zc6M^H@OFlo&49~(DB|*S69J(6zbE;G-~1-PCAgf0rAPo6oWs9;vbYH_&`=D|?$OKh z_WB1?wIiMAy%InMo5VV^c!CaoyGN1EoKP02pGEM1VxkW#<{lfj%i{z zzx;V{&F>#T*4BRt6gfWXWwKU|&}|Yk_z+_r0)aWe=(*Z}DA zhSvbuYBE^xh9Ti&y`RwVnx-SNvI|t5y@}bdH2VM8>jYg4JGoexg<1HO_YW%7@$iR@ RZ72W$002ovPDHLkV1kha3+Dg; literal 0 HcmV?d00001 diff --git a/tests/ref/line-numbers-clearance.png b/tests/ref/line-numbers-clearance.png new file mode 100644 index 0000000000000000000000000000000000000000..142d3a2f7575e7f74e23d4e7da83209723d1db38 GIT binary patch literal 880 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 UDuWV*g8%>k07*qoM6N<$f?q2$asU7T literal 0 HcmV?d00001 diff --git a/tests/ref/line-numbers-columns-rtl.png b/tests/ref/line-numbers-columns-rtl.png new file mode 100644 index 0000000000000000000000000000000000000000..e2108016bceb99824b15270ca9011990948c65c5 GIT binary patch literal 1208 zcmV;p1V{UcP)VVoH5*FjTPAi_JGUolez=5F#L9Nx{w+7bM@Xm^S&3~cM`A3Bxl$ec81@~ z@U@%#S9NW@r;K*Pr?8{QMZDt->S(Nz_F&K9X_eHt#{uy3VYTa1Y}AekS9xI1d-Qq2 zAXf4O>@m6mzDfWJ(h1Zu!kp$ZI^0?2i#;xnC5I?jXO7GYdwTj%NCM*A41}26bUK}% z3VG2%YuKD7P6eyEqr(TVAD(g{HMCz9L{>z9XE?G1v(G@QaQGdGMB=Kt`}JkN)|cU9 zBKG8s8@{u4|GOu!V;Za$KMCfTh?5(UCr~=8N;o_N_*5lb`$#$TG8|FO>CNMYfjQXo zmW8m3N`KN1bIa3|5^-0d4n&A{GT?Q;(S5LAn{xoa$I@+N$*`I_CM;Xug0Pav%)-k^ zV{XLlRFk`17TX^GTQfpLYpbY!r7^6M&gC{)m$0|exK*#=mmm%BmjwTI)Rw&w5DmIn@&6AJmeCoI4 zqnD(0Frs(%U`8GW>5+EMNb1-nS4VZ!OwGNwRP+0XCrcBFFCPx8l(-GrhKmY6>a!EZ z`?}Eszzcj7L+-4(P#$6vcFAA2ov5?&MZI|hkbjWx8%=Xzo~9?5?f^;>hr>{wg>4u% z7TOE@KErD`{NgzMTFZUg8-wSK_v>0Z$S;~fNiq>lFTxA7&%>GkWFui}CD?{7w^@80 zA)#0B9>5J?7R=G(MxcxyMyO_jZ`-0$dd}^}owHl{>f(^6g?ZgwpJDVSa|=O#0q|%S zg#)(X_zWpRnpejumYu#D)&RsMwJbk(BebS7JmoecQWqZ=Uqbm)@}H`hl6AF)%_)id zh)%z`Tq<7Kz|r`e@N26?08}qtvdXz!&hSJF4=8y>9cJL6PAJ@lLzdj)XnkTh_UPAd zAn%J7Dt}O6)&8~?{_$$YnyEI83LCdy!l8;x6rGKc*mZsh%v94<$uNIC@Z&82$_f{|XkpK|~%$khs2d235oBE%JOOMR5hPzDp$+lsg zYtbqd4wcSaVQ7qODyGMs)+>xJs-k1p=jK#``uaRADJ$<-{PpG8EU|{UG(X2S+_seB z6mV=!TH}0=K8UW$LBJo&ejS=!+-8{$fe*5B!C$|+e)y?@X|`KE5Qs>QBgqCbiW|bq zzl3`qS0LnrOb#vt_ooCIk7R?dc3T>u3IO`SW5^~<@xHw#9I1#AA17(4Zu4GPXhKN3 zhQAA;W$V#u?V5-ut-;dnZ<1o-0I1z}wBdgZh*|u4mg`PP@9PO;c777huroYy!v6xY WNQ*n-=_*b$_hn+ctAx!sMJD1 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 M3#$L1TGb}}Kh$2gbpQYW literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b203a455aa5b10235465607c24a6df1d8b4b0d40 GIT binary patch literal 1701 zcmV;W23q-vP)Hf9LMp#055vcncAs`+Um47S_=p|ts-cxB8rL#YE`g0)B}Pb)qseCAOu7tGT~q% zphgivMKP$6Q{*Ia3`aR?fC?lKk^o5-@~hDCV%MlUE72Lv_vYEzo6kJ6&-|Z@{Q@7U zFHEorHo+$N1Hhe58>OIGd>mtVOb`i5G0~vgDO<*H*JTe-isb-MMetH^F2wzjBsoZN zK8Uw9H`EW*g!LO*nZl?v4EsF7<}!)-kZwty=b1K3f_)mcT2R`TfB3{C!y21zurD;Y zLTc6}RZE=^n}SvSAlJgKtFf3wI3fbgY*q@q>noh0o7RD@=5>flgzdi|=c@*`7cqpj zq1HW=Q+<@h^(fY!Nyx@I9|_965{8m}+K-2{9ZUx)v1bW-f{)9Y!J69oKGWnf!Jj;Q zQ4CsDI~?OiGlr!Z;Z#SZ1HE?T;*U0_@UfnTWgs=;opbMKy1N9z%P}=Okshk?$_{0V zeDgqUPj0&l!4w9v{6Owx+Ss>>7F#SIDa3sA{SLMtENXikUx$xzHcA-7FAfpZS$!Q= zz)#39%Y+pMSK#bbEK?8L8rU&zY+wqj{YRxB7b3s{sMje(sztCFXtDzP5|#`oegic( zA+lVV!Uu!nyg{np%TG5wCAwSV&AWkztkf-q-hs_KZr|MA1adXIS!r=9rf_9JK@&jD z;v)@^8(%&&jm%FR*0y4zUfBnRZ~uz%H1^4OTVsj*lSh;c;mV7ly`ZMG#zZj!ye;1X zAgX{N><)>tQ3Tk1Fh z*?MGqz&5oOuUF{uUf;NCFB_zW&7rl%kAOoXM{xHbh`CtP3j?*ze=iuFi$O~N^>FV3 zSoFd{6_xySIR+fDvKxl7yAHHGV+bdY@VFqC>dIePDmff`Z_wRS!;cE(4=r&p5tBb$rA=M%<+3C>@b#w z?S-&1?vCRIsW4I&<>O$HvEjh5h29#F5_kWC17esP7Y5v_L2g@8Q0H0})<=yR6>j2Q zGYt?EY=S2RZudKM3-2hn%?NP%qZT#D_1>yRRYpUC>SiV&x%&KtWkl=b!)KCigOV2< z)txgRMY^=xb(chG7sA;XJ8pgIaSr6ezr^=60OwXCz*$4QQy`^ZeZkc~b9Viq)z58?OP2>-LFn#w=Vy}*OX8CBrU7Dt zKM>q$=Wssw0ynKs^

83+%D);ApyVbJ}P219KX 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 FV1gkwY})_; literal 0 HcmV?d00001 diff --git a/tests/ref/line-numbers-enable.png b/tests/ref/line-numbers-enable.png new file mode 100644 index 0000000000000000000000000000000000000000..927096a5469878ff31ba6793207770ea449551dc GIT binary patch literal 909 zcmV;819JR{P)R4S} zWy-;{uotXJO~9=0B7t?Z)SwB@vT-wYiz+f@8xc)4uE%T zcq4>4)rM%w=D&NElCB%})E%8Y_L(C+=i(h$o5B8$uhZRhcdG6aoUN9VImr`C>y>9_ zJS``=0|#D4vzNjl!=7JXu-_k8g`bb6hzEOWlJ71|Jl&0y=&LKNi-ZNYG$2~F0@yuD z6sY-hq_xcrY_oV{I8n`^{{DoZ-5U;UmX-X4NQ*L=m4zbZV4jZS9j$@+!f`O|KhaY# zTv6)yvX*aUT4}i?_>qBEF9YkB!h{0`LlChBJ8L!Ev+L2o^pjZR;lPEi8Usl@2Ogoj7_qjt_+V1g6bO9Nu4ABGdCQpl+W{S`ikjyHSY7SPyC5%vGspFz!}v2D zh_!qmTI+iA_ne-l&VX_zi{2t)Ov|sc+ew@Nn^Gy3v(@%}?POHwaIka<#4|rTk}z>~ z<=rtjRf+vIwm>{l|6TQ58WmbTFlV7hD zEODmwXZ*nPnBJ$Pmz^_}ML0u;^U_lK5XqO{{M}7{$ZPkk<(H15Iohx`ntJ|Rn(<1O zdha>7m^`4&87q)mbJ{%zD}IrdOM)LB*vtQNEdm}(Wbi+O^N4jNwc+im=2!ZG+cJcX z=5Ij%_Uh*zDeEN4%KZe05r(;O8>x`wv?e znm^eG%%ywFho%l)lNq>mTIJX<9LRs~&k*1ewwt|ioK5&+CCNV|z#P%AdD!_Xxl_Hl z8-_vic?j@;@R`t+Gn&D;0MpbZycbXF^%EP0*XXK2yl6M&TC$A6&GZ$s5W|IRU^db-?EpGto00000NkvXXu0mjf`uMxQ literal 0 HcmV?d00001 diff --git a/tests/ref/line-numbers-margin.png b/tests/ref/line-numbers-margin.png new file mode 100644 index 0000000000000000000000000000000000000000..94e03b26247244ed9753080b4fa568c65e8c9fc8 GIT binary patch literal 1038 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4606311d75b0cbd14f7569706fb09c1dd0036ae5 GIT binary patch literal 917 zcmV;G18V$9BMTozv#r^XX3a?C_v4w8Qs(_vH(}7e5~O-8|c{MQtOPV1fyDR)=~+ zu;#nXUb#QwnKyG~b=F&~w`NJP-y|HC6vm!w@}@Z8hQlh@bYvPTm{!N*M(d?Q0_|5g ztWL}tGa5%ObKO|j44XW)-F@|5h)ne9FE5H3b<5CVt+BmT0CMvRF!(%LV~m&PBJp{q z_ZVsuFS22iIp)m<;jD;ectCN+$ejAwjpM&q+W{Ys@qCeT2g9=rc7!zIYTM^imh>#G zA8N#4)qF#=L3joh@Sqd|Er+vhU=#346eyjTFzf+vdJGBKy#XkEDGx%O7&f^ZIN-qd zu*qW#VD>oi}1MYyh$nNNz`1zZS(NRE&tic{mPt^OXqMm{&e%%C)DpI&$%=((`j_&bAs07FK zvVn_g;dxn32-856+t`l5@cZ(USeq0|JN@xG?FH)74|Y?G8Xl6VA?f<#qMJ zd9ksAGyTrt;01jt*tw(UGD53xPV-m@9qhIp*AQHf)7sDo=)hGK$Bj*wu3S=smC8$z zbffcj+_<02ct2VRcFKz%;{89}O3(-j48bv+-*{0hnxmJCV1fxIm|%hlCYWG?3I1E* zt%|-e!2}chzrhOkC$ZPeKC811h$?~OR4%=-$2{vl9DfBu8635e?B-JVkgjvE#v6~4 zuMU?L#tiSw`H|TI$9cLImW6dP2l4K_5hrH4!3U}|n*$qQ0S`41K;xlY@PXmQO@a4K z0cLkpgkbKinUA9e)6f(g#_V<_To;rER0g%ignl!JOB+xaxlrB)hvf!V(gQ*;!2}ab rFu?>9OfbO&6HG9{1QSf~KL`H?cj|DHL$R0d00000NkvXXu0mjfziPD9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..99864a607b18b54ea2bcf02833da3937b5368450 GIT binary patch literal 2316 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 literal 0 HcmV?d00001 diff --git a/tests/ref/line-numbers-rtl.png b/tests/ref/line-numbers-rtl.png new file mode 100644 index 0000000000000000000000000000000000000000..58ed9d09fad4801901a648d58783870244aa6008 GIT binary patch literal 1364 zcmV-a1*`grP)O+0TmjjGqf+47V*7a?h`blHP96}3 z85UDlQzR~M?vO222JERbhCuaVr7KYed_7oF2fFmXWWYlfjRG?LpO67>4BgO!s-k1X zA_FcjEYI|TC@XQVg$IOThK01;(ia~%Wk%C11D>fbkATV(O&X8^8yAZkL3UwFuMBvq zOrwCV<2xDf=D=+|s21?_unf4sP@L)mQSOb)ejX5p85Y!3C_ZrLBaaN&|DbvqWTsuC zGGM(=X#rKs>-|#Td8;NLRQs$6hL{BS8=p-9m3Lz2b4Y;C8ZIaKK-Iscj0c2ahJ_TK z7@m#`Y){W1p z+T{aLj^Xea4+z5yiz*f$_~m)83^?|r8)UBifDAZ$T-ODvJ$czKDKCDQVF1+!@NAO- zC%bACP_2(G?vMfRE%|we4@3PD6u55;WNqlLk^x`p zJK6)f^Y3K9!Ojy!APeds1I~P|awK4GlLCMJd**H*sJ1`~yyAryRLdS75QZ5R)SHsL z9x3qTJjhPq8EF=T|+j-mss;2Av%rfAdW|brHEn=Hxz#E$~4){Qn z)@-Yg0WS?kT_777<^f@tVKI&5oa&bX2csZ6G9LI!23*l_Xc$zR{P<7??4CSP3aUrP zD>li1O?Rpk(4A?N0dIJjkv{36Rlu`rR^jjp=-lUY zh;2p2pkUZuojR`iP1}nkm`#G=SAN|!RoHP9)*QjGwY*$Wg-5=J@upxnVf3^v1S-qk zWgJ{c3krtawbOO)Kr~lvA3~^piU)*YhF72K9)McUn#1l49nkn$k7(F&M>C^3nLui< zX!!Bc#frGV4W)%jT;Narzhp;2WjhF9TZ?G;b-B|L0Fl>hcB0d4iSU3h%63FUP zfgNugHNs!qq4$HxtIu`LU(C5U&I7_Q!>cav40vvR7MGt!z18BAWx;U2J$FHUGGmTo z5DZs^N^I)z%->Dsy~eixRBH<7_KkAo8<$MH~S}NIN$o1 z2ZUjUS6_4h09w}^cK@IS(3l_^9yCx)AW%zlja0&C-?J!FwF4k8wI+XYzu1*yOXql)>;`63{TZx zkASRA2V@OLAQ(0UjQ7=#gt7Qz(J!7T(0ys97+aWw`PqeI(MhIbqB@8{+|Cm|=$h7x2GM W#}$O*JN0b<0000ta%F0%1UJ!4H7X%ZcMFc^#2~k=J(W+dj z$b_Vxy!?8DIYgc_kpJ%sd>97iFuyYcjhZ2tV1fyrG_064g26O`a~5!_7Ek*E;I;{z z;-j-$0dR*4KZk5V0dPYPPP(x-AOKF=U!Y#bpLccW141ytd|27VU0w?qE%d*%fD29> zWTDpUwQnE*ZY?2k0k!-?&%KyKBI|`(wTLw+;)#t&o 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] +)