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 000000000..80f8d45d4 Binary files /dev/null and b/tests/ref/line-numbers-auto-alignment.png differ diff --git a/tests/ref/line-numbers-clearance.png b/tests/ref/line-numbers-clearance.png new file mode 100644 index 000000000..142d3a2f7 Binary files /dev/null and b/tests/ref/line-numbers-clearance.png differ diff --git a/tests/ref/line-numbers-columns-alignment.png b/tests/ref/line-numbers-columns-alignment.png new file mode 100644 index 000000000..6367bf08a Binary files /dev/null and b/tests/ref/line-numbers-columns-alignment.png differ diff --git a/tests/ref/line-numbers-columns-override.png b/tests/ref/line-numbers-columns-override.png new file mode 100644 index 000000000..d89b6007c Binary files /dev/null and b/tests/ref/line-numbers-columns-override.png differ diff --git a/tests/ref/line-numbers-columns-rtl.png b/tests/ref/line-numbers-columns-rtl.png new file mode 100644 index 000000000..e2108016b Binary files /dev/null and b/tests/ref/line-numbers-columns-rtl.png differ diff --git a/tests/ref/line-numbers-columns.png b/tests/ref/line-numbers-columns.png new file mode 100644 index 000000000..e6e3cd512 Binary files /dev/null and b/tests/ref/line-numbers-columns.png 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 000000000..d34a13d6c Binary files /dev/null and b/tests/ref/line-numbers-deduplication-tall-line.png 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 000000000..b203a455a Binary files /dev/null and b/tests/ref/line-numbers-deduplication-zero-height-number.png differ diff --git a/tests/ref/line-numbers-deduplication.png b/tests/ref/line-numbers-deduplication.png new file mode 100644 index 000000000..824934213 Binary files /dev/null and b/tests/ref/line-numbers-deduplication.png differ diff --git a/tests/ref/line-numbers-default-alignment.png b/tests/ref/line-numbers-default-alignment.png new file mode 100644 index 000000000..5a39674db Binary files /dev/null and b/tests/ref/line-numbers-default-alignment.png differ diff --git a/tests/ref/line-numbers-enable.png b/tests/ref/line-numbers-enable.png new file mode 100644 index 000000000..927096a54 Binary files /dev/null and b/tests/ref/line-numbers-enable.png differ diff --git a/tests/ref/line-numbers-margin.png b/tests/ref/line-numbers-margin.png new file mode 100644 index 000000000..94e03b262 Binary files /dev/null and b/tests/ref/line-numbers-margin.png differ diff --git a/tests/ref/line-numbers-multi-columns.png b/tests/ref/line-numbers-multi-columns.png new file mode 100644 index 000000000..45cb53788 Binary files /dev/null and b/tests/ref/line-numbers-multi-columns.png differ diff --git a/tests/ref/line-numbers-nested-content.png b/tests/ref/line-numbers-nested-content.png new file mode 100644 index 000000000..570aa6b0e Binary files /dev/null and b/tests/ref/line-numbers-nested-content.png 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 000000000..4606311d7 Binary files /dev/null and b/tests/ref/line-numbers-page-scope-quasi-empty-first-column.png 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 000000000..99864a607 Binary files /dev/null and b/tests/ref/line-numbers-page-scope-with-columns.png differ diff --git a/tests/ref/line-numbers-page-scope.png b/tests/ref/line-numbers-page-scope.png new file mode 100644 index 000000000..330cfe104 Binary files /dev/null and b/tests/ref/line-numbers-page-scope.png 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 000000000..b1a4050a6 Binary files /dev/null and b/tests/ref/line-numbers-place-out-of-order.png differ diff --git a/tests/ref/line-numbers-rtl.png b/tests/ref/line-numbers-rtl.png new file mode 100644 index 000000000..58ed9d09f Binary files /dev/null and b/tests/ref/line-numbers-rtl.png differ diff --git a/tests/ref/line-numbers-start-alignment.png b/tests/ref/line-numbers-start-alignment.png new file mode 100644 index 000000000..edc3dd15a Binary files /dev/null and b/tests/ref/line-numbers-start-alignment.png differ diff --git a/tests/suite/layout/line-numbers.typ b/tests/suite/layout/line-numbers.typ new file mode 100644 index 000000000..5ee53e25a --- /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] +)