summaryrefslogtreecommitdiff
path: root/crates
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 /crates
parentef4482ce4b084aa97155dbde89f02dda3f7fb219 (diff)
Line numbers (#4516)
Diffstat (limited to 'crates')
-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
6 files changed, 481 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
+ }
+}