Line numbers (#4516)
@ -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() });
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
)
|