Line numbers (#4516)

This commit is contained in:
PgBiel 2024-08-29 11:32:18 -03:00 committed by GitHub
parent ef4482ce4b
commit e6cdcc53f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 730 additions and 21 deletions

View File

@ -10,20 +10,23 @@ use comemo::{Track, Tracked, TrackedMut};
use crate::diag::{bail, At, SourceResult}; use crate::diag::{bail, At, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced}; use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{ use crate::foundations::{
Content, NativeElement, Packed, Resolve, Smart, StyleChain, Styles, Content, NativeElement, Packed, Resolve, SequenceElem, Smart, StyleChain, Styles,
}; };
use crate::introspection::{ use crate::introspection::{
Counter, CounterDisplayElem, CounterKey, Introspector, Location, Locator, Counter, CounterDisplayElem, CounterKey, CounterState, CounterUpdate, Introspector,
LocatorLink, ManualPageCounter, SplitLocator, Tag, TagElem, TagKind, Location, Locator, LocatorLink, ManualPageCounter, SplitLocator, Tag, TagElem,
TagKind,
}; };
use crate::layout::{ use crate::layout::{
Abs, AlignElem, Alignment, Axes, Binding, BlockElem, ColbreakElem, ColumnsElem, Dir, Abs, AlignElem, Alignment, Axes, Binding, BlockElem, ColbreakElem, ColumnsElem, Dir,
FixedAlignment, FlushElem, Fr, Fragment, Frame, FrameItem, HAlignment, Length, FixedAlignment, FlushElem, Fr, Fragment, Frame, FrameItem, HAlignment, Length,
OuterVAlignment, Page, PageElem, PagebreakElem, Paper, Parity, PlaceElem, Point, OuterHAlignment, OuterVAlignment, Page, PageElem, PagebreakElem, Paper, Parity,
Ratio, Region, Regions, Rel, Sides, Size, Spacing, VAlignment, VElem, PlaceElem, Point, Ratio, Region, Regions, Rel, Sides, Size, Spacing, VAlignment,
VElem,
}; };
use crate::model::{ 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::realize::{first_span, realize, Arenas, Pair};
use crate::syntax::Span; use crate::syntax::Span;
@ -799,6 +802,12 @@ struct FootnoteConfig {
gap: Abs, gap: Abs,
} }
/// Information needed to generate a line number.
struct CollectedParLine {
y: Abs,
marker: Packed<ParLineMarker>,
}
/// A prepared item in a flow layout. /// A prepared item in a flow layout.
#[derive(Debug)] #[derive(Debug)]
enum FlowItem { enum FlowItem {
@ -814,6 +823,12 @@ enum FlowItem {
align: Axes<FixedAlignment>, align: Axes<FixedAlignment>,
/// Whether the frame sticks to the item after it (for orphan prevention). /// Whether the frame sticks to the item after it (for orphan prevention).
sticky: bool, 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 /// Whether the frame is movable; that is, kept together with its
/// footnotes. /// footnotes.
/// ///
@ -1094,6 +1109,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
frame, frame,
align, align,
sticky: false, sticky: false,
rootable: false,
movable: true, movable: true,
})?; })?;
} }
@ -1111,12 +1127,13 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
// Fetch properties. // Fetch properties.
let sticky = block.sticky(styles); let sticky = block.sticky(styles);
let align = AlignElem::alignment_in(styles).resolve(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 // If the block is "rootable" it may host footnotes. In that case, we
// defer rootness to it temporarily. We disable our own rootness to // defer rootness to it temporarily. We disable our own rootness to
// prevent duplicate footnotes. // prevent duplicate footnotes.
let is_root = self.root; let is_root = self.root;
if is_root && block.rootable(styles) { if is_root && rootable {
self.root = false; self.root = false;
self.regions.root = true; self.regions.root = true;
} }
@ -1147,7 +1164,13 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
self.drain_tag(&mut frame); self.drain_tag(&mut frame);
frame.post_process(styles); 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)?; self.try_handle_footnotes(notes)?;
@ -1347,7 +1370,14 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
&& !self.items.is_empty() && !self.items.is_empty()
&& self.items.iter().all(FlowItem::is_out_of_flow) && 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.regions.next();
self.initial = self.regions.size; self.initial = self.regions.size;
return Ok(()); return Ok(());
@ -1421,6 +1451,8 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
let mut float_bottom_offset = Abs::zero(); let mut float_bottom_offset = Abs::zero();
let mut footnote_offset = Abs::zero(); let mut footnote_offset = Abs::zero();
let mut lines: Vec<CollectedParLine> = vec![];
// Place all frames. // Place all frames.
for item in self.items.drain(..) { for item in self.items.drain(..) {
match item { match item {
@ -1432,12 +1464,20 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
let length = v.share(fr, remaining); let length = v.share(fr, remaining);
offset += length; offset += length;
} }
FlowItem::Frame { frame, align, .. } => { FlowItem::Frame { frame, align, rootable, .. } => {
ruler = ruler.max(align.y); ruler = ruler.max(align.y);
let x = align.x.position(size.x - frame.width()); let x = align.x.position(size.x - frame.width());
let y = offset + ruler.position(size.y - used.y); let y = offset + ruler.position(size.y - used.y);
let pos = Point::new(x, y); let pos = Point::new(x, y);
offset += frame.height(); 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); output.push_frame(pos, frame);
} }
FlowItem::Placed { frame, x_align, y_align, delta, float, .. } => { 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) let pos = Point::new(x, y)
+ delta.zip_map(size, Rel::relative_to).to_point(); + 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); output.push_frame(pos, frame);
} }
FlowItem::Footnote(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() { if force && !self.pending_tags.is_empty() {
let pos = Point::with_y(offset); let pos = Point::with_y(offset);
output.push_multiple( output.push_multiple(
@ -1670,6 +1723,158 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
Ok(()) 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. /// Collect all footnotes in a frame.
fn collect_footnotes( fn collect_footnotes(
&mut self, &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() });
}
_ => {}
}
}
}

View File

@ -1,7 +1,7 @@
use super::*; use super::*;
use crate::diag::bail; use crate::diag::bail;
use crate::foundations::{Packed, Resolve}; use crate::foundations::{Packed, Resolve};
use crate::introspection::{Tag, TagElem}; use crate::introspection::{SplitLocator, Tag, TagElem};
use crate::layout::{ use crate::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
Spacing, Spacing,
@ -117,13 +117,12 @@ impl Segment<'_> {
pub fn collect<'a>( pub fn collect<'a>(
children: &'a StyleVec, children: &'a StyleVec,
engine: &mut Engine<'_>, engine: &mut Engine<'_>,
locator: Locator<'a>, locator: &mut SplitLocator<'a>,
styles: &'a StyleChain<'a>, styles: &'a StyleChain<'a>,
region: Size, region: Size,
consecutive: bool, consecutive: bool,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> { ) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len()); let mut collector = Collector::new(2 + children.len());
let mut locator = locator.split();
let mut quoter = SmartQuoter::new(); let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(*styles); let outer_dir = TextElem::dir_in(*styles);

View File

@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::introspection::SplitLocator;
use crate::utils::Numeric; use crate::utils::Numeric;
/// Turns the selected lines into frames. /// Turns the selected lines into frames.
@ -10,6 +11,7 @@ pub fn finalize(
styles: StyleChain, styles: StyleChain,
region: Size, region: Size,
expand: bool, expand: bool,
locator: &mut SplitLocator<'_>,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Determine the paragraph's width: Full width of the region if we should // Determine the paragraph's width: Full width of the region if we should
// expand or there's fractional spacing, fit-to-width otherwise. // expand or there's fractional spacing, fit-to-width otherwise.
@ -27,7 +29,7 @@ pub fn finalize(
let shrink = ParElem::shrink_in(styles); let shrink = ParElem::shrink_in(styles);
lines lines
.iter() .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<_>>() .collect::<SourceResult<_>>()
.map(Fragment::frames) .map(Fragment::frames)
} }

View File

@ -3,7 +3,10 @@ use std::ops::{Deref, DerefMut};
use super::*; use super::*;
use crate::engine::Engine; 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::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use crate::model::{ParLine, ParLineMarker};
use crate::text::{Lang, TextElem}; use crate::text::{Lang, TextElem};
use crate::utils::Numeric; 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. /// Commit to a line and build its frame.
#[allow(clippy::too_many_arguments)]
pub fn commit( pub fn commit(
engine: &mut Engine, engine: &mut Engine,
p: &Preparation, p: &Preparation,
@ -413,6 +417,8 @@ pub fn commit(
width: Abs, width: Abs,
full: Abs, full: Abs,
shrink: bool, shrink: bool,
locator: &mut SplitLocator<'_>,
styles: StyleChain,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let mut remaining = width - line.width - p.hang; let mut remaining = width - line.width - p.hang;
let mut offset = Abs::zero(); let mut offset = Abs::zero();
@ -546,6 +552,8 @@ pub fn commit(
let mut output = Frame::soft(size); let mut output = Frame::soft(size);
output.set_baseline(top); output.set_baseline(top);
add_par_line_marker(&mut output, styles, engine, locator, top);
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame) in frames {
let x = offset + p.align.position(remaining); let x = offset + p.align.position(remaining);
@ -556,6 +564,54 @@ pub fn commit(
Ok(output) 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. /// How much a character should hang into the end margin.
/// ///
/// For more discussion, see: /// For more discussion, see:

View File

@ -78,9 +78,11 @@ fn layout_inline_impl(
route: Route::extend(route), route: Route::extend(route),
}; };
let mut locator = locator.split();
// Collect all text into one string for BiDi analysis. // Collect all text into one string for BiDi analysis.
let (text, segments, spans) = 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. // Perform BiDi analysis and then prepares paragraph layout.
let p = prepare(&mut engine, children, &text, segments, spans, styles)?; 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); let lines = linebreak(&engine, &p, region.x - p.hang);
// Turn the selected lines into frames. // Turn the selected lines into frames.
finalize(&mut engine, &p, &lines, styles, region, expand) finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator)
} }

View File

@ -1,12 +1,14 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use crate::diag::SourceResult; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, StyleVec, elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart,
Unlabellable, 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; use crate::utils::singleton;
/// Arranges text, spacing and inline-level elements into a paragraph. /// 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 /// let $a$ be the smallest of the
/// three integers. Then, we ... /// three integers. Then, we ...
/// ``` /// ```
#[elem(title = "Paragraph", Debug, Construct)] #[elem(scope, title = "Paragraph", Debug, Construct)]
pub struct ParElem { pub struct ParElem {
/// The spacing between lines. /// The spacing between lines.
/// ///
@ -143,6 +145,12 @@ pub struct ParElem {
pub children: StyleVec, pub children: StyleVec,
} }
#[scope]
impl ParElem {
#[elem]
type ParLine;
}
impl Construct for ParElem { impl Construct for ParElem {
fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> { fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph // The paragraph constructor is special: It doesn't create a paragraph
@ -206,3 +214,143 @@ impl ParbreakElem {
} }
impl Unlabellable for Packed<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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

View File

@ -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]
)