Line numbers (#4516)
@ -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() });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
BIN
tests/ref/line-numbers-auto-alignment.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/line-numbers-clearance.png
Normal file
After Width: | Height: | Size: 880 B |
BIN
tests/ref/line-numbers-columns-alignment.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/line-numbers-columns-override.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/line-numbers-columns-rtl.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/line-numbers-columns.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/line-numbers-deduplication-tall-line.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
tests/ref/line-numbers-deduplication-zero-height-number.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/ref/line-numbers-deduplication.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
tests/ref/line-numbers-default-alignment.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/line-numbers-enable.png
Normal file
After Width: | Height: | Size: 909 B |
BIN
tests/ref/line-numbers-margin.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/line-numbers-multi-columns.png
Normal file
After Width: | Height: | Size: 815 B |
BIN
tests/ref/line-numbers-nested-content.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/ref/line-numbers-page-scope-quasi-empty-first-column.png
Normal file
After Width: | Height: | Size: 917 B |
BIN
tests/ref/line-numbers-page-scope-with-columns.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
tests/ref/line-numbers-page-scope.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
tests/ref/line-numbers-place-out-of-order.png
Normal file
After Width: | Height: | Size: 791 B |
BIN
tests/ref/line-numbers-rtl.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/line-numbers-start-alignment.png
Normal file
After Width: | Height: | Size: 469 B |
249
tests/suite/layout/line-numbers.typ
Normal 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]
|
||||
)
|