Hanging indent

This commit is contained in:
Laurenz 2023-03-14 22:30:21 +01:00
parent 724e9b140c
commit 2bacbaf2bd
5 changed files with 74 additions and 50 deletions

View File

@ -20,7 +20,7 @@ use crate::text::{
/// ///
/// ## Example /// ## Example
/// ```example /// ```example
/// #set par(indent: 1em, justify: true) /// #set par(first-line-indent: 1em, justify: true)
/// #show par: set block(spacing: 0.65em) /// #show par: set block(spacing: 0.65em)
/// ///
/// We proceed by contradiction. /// We proceed by contradiction.
@ -39,17 +39,6 @@ use crate::text::{
/// Category: layout /// Category: layout
#[node(Construct)] #[node(Construct)]
pub struct ParNode { pub struct ParNode {
/// The indent the first line of a consecutive paragraph should have.
///
/// The first paragraph on a page will never be indented.
///
/// By typographic convention, paragraph breaks are indicated by either some
/// space between paragraphs or indented first lines. Consider turning the
/// [paragraph spacing]($func/block.spacing) off when using this property
/// (e.g. using `[#show par: set block(spacing: 0pt)]`).
#[resolve]
pub indent: Length,
/// The spacing between lines. /// The spacing between lines.
/// ///
/// The default value is `{0.65em}`. /// The default value is `{0.65em}`.
@ -95,6 +84,21 @@ pub struct ParNode {
#[default] #[default]
pub linebreaks: Smart<Linebreaks>, pub linebreaks: Smart<Linebreaks>,
/// The indent the first line of a consecutive paragraph should have.
///
/// The first paragraph on a page will never be indented.
///
/// By typographic convention, paragraph breaks are indicated by either some
/// space between paragraphs or indented first lines. Consider turning the
/// [paragraph spacing]($func/block.spacing) off when using this property
/// (e.g. using `[#show par: set block(spacing: 0pt)]`).
#[resolve]
pub first_line_indent: Length,
/// The indent all but the first line of a paragraph should have.
#[resolve]
pub hanging_indent: Length,
/// The contents of the paragraph. /// The contents of the paragraph.
#[external] #[external]
pub body: Content, pub body: Content,
@ -153,7 +157,7 @@ impl ParNode {
let p = prepare(&mut vt, &children, &text, segments, spans, styles, region)?; let p = prepare(&mut vt, &children, &text, segments, spans, styles, region)?;
// Break the paragraph into lines. // Break the paragraph into lines.
let lines = linebreak(&vt, &p, region.x); let lines = linebreak(&vt, &p, region.x - p.hang);
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
finalize(&mut vt, &p, &lines, region, expand) finalize(&mut vt, &p, &lines, region, expand)
@ -261,6 +265,8 @@ struct Preparation<'a> {
align: Align, align: Align,
/// Whether to justify the paragraph. /// Whether to justify the paragraph.
justify: bool, justify: bool,
/// The paragraph's hanging indent.
hang: Abs,
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
@ -509,8 +515,8 @@ fn collect<'a>(
let mut iter = children.iter().peekable(); let mut iter = children.iter().peekable();
if consecutive { if consecutive {
let indent = ParNode::indent_in(*styles); let first_line_indent = ParNode::first_line_indent_in(*styles);
if !indent.is_zero() if !first_line_indent.is_zero()
&& children && children
.iter() .iter()
.find_map(|child| { .find_map(|child| {
@ -527,10 +533,16 @@ fn collect<'a>(
.unwrap_or_default() .unwrap_or_default()
{ {
full.push(SPACING_REPLACE); full.push(SPACING_REPLACE);
segments.push((Segment::Spacing(indent.into()), *styles)); segments.push((Segment::Spacing(first_line_indent.into()), *styles));
} }
} }
let hang = ParNode::hanging_indent_in(*styles);
if !hang.is_zero() {
full.push(SPACING_REPLACE);
segments.push((Segment::Spacing((-hang).into()), *styles));
}
while let Some(mut child) = iter.next() { while let Some(mut child) = iter.next() {
let outer = styles; let outer = styles;
let mut styles = *styles; let mut styles = *styles;
@ -681,6 +693,7 @@ fn prepare<'a>(
lang: shared_get(styles, children, TextNode::lang_in), lang: shared_get(styles, children, TextNode::lang_in),
align: AlignNode::alignment_in(styles).x.resolve(styles), align: AlignNode::alignment_in(styles).x.resolve(styles),
justify: ParNode::justify_in(styles), justify: ParNode::justify_in(styles),
hang: ParNode::hanging_indent_in(styles),
}) })
} }
@ -1158,7 +1171,7 @@ fn finalize(
let width = if !region.x.is_finite() let width = if !region.x.is_finite()
|| (!expand && lines.iter().all(|line| line.fr().is_zero())) || (!expand && lines.iter().all(|line| line.fr().is_zero()))
{ {
lines.iter().map(|line| line.width).max().unwrap_or_default() p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()
} else { } else {
region.x region.x
}; };
@ -1204,11 +1217,14 @@ fn commit(
width: Abs, width: Abs,
full: Abs, full: Abs,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let mut remaining = width - line.width; let mut remaining = width - line.width - p.hang;
let mut offset = Abs::zero(); let mut offset = Abs::zero();
// Reorder the line from logical to visual order. // Reorder the line from logical to visual order.
let reordered = reorder(line); let (reordered, starts_rtl) = reorder(line);
if !starts_rtl {
offset += p.hang;
}
// Handle hanging punctuation to the left. // Handle hanging punctuation to the left.
if let Some(Item::Text(text)) = reordered.first() { if let Some(Item::Text(text)) = reordered.first() {
@ -1308,12 +1324,12 @@ fn commit(
} }
/// Return a line's items in visual order. /// Return a line's items in visual order.
fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> { fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) {
let mut reordered = vec![]; let mut reordered = vec![];
// The bidi crate doesn't like empty lines. // The bidi crate doesn't like empty lines.
if line.trimmed.is_empty() { if line.trimmed.is_empty() {
return line.slice(line.trimmed.clone()).collect(); return (line.slice(line.trimmed.clone()).collect(), false);
} }
// Find the paragraph that contains the line. // Find the paragraph that contains the line.
@ -1326,6 +1342,7 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> {
// Compute the reordered ranges in visual order (left to right). // Compute the reordered ranges in visual order (left to right).
let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone()); let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone());
let starts_rtl = levels.first().map_or(false, |level| level.is_rtl());
// Collect the reordered items. // Collect the reordered items.
for run in runs { for run in runs {
@ -1343,7 +1360,7 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> {
} }
} }
reordered (reordered, starts_rtl)
} }
/// How much a character should hang into the end margin. /// How much a character should hang into the end margin.

View File

@ -1,4 +1,5 @@
use crate::layout::{BlockNode, GridLayouter, HNode, ParNode, Sizing, Spacing}; use super::{HNode, VNode};
use crate::layout::{BlockNode, ParNode, Spacing};
use crate::prelude::*; use crate::prelude::*;
use crate::text::{SpaceNode, TextNode}; use crate::text::{SpaceNode, TextNode};
@ -42,7 +43,6 @@ pub struct TermsNode {
pub tight: bool, pub tight: bool,
/// The indentation of each item's term. /// The indentation of each item's term.
#[resolve]
pub indent: Length, pub indent: Length,
/// The hanging indent of the description. /// The hanging indent of the description.
@ -52,7 +52,6 @@ pub struct TermsNode {
/// / Term: This term list does not /// / Term: This term list does not
/// make use of hanging indents. /// make use of hanging indents.
/// ``` /// ```
#[resolve]
#[default(Em::new(1.0).into())] #[default(Em::new(1.0).into())]
pub hanging_indent: Length, pub hanging_indent: Length,
@ -85,7 +84,7 @@ impl Layout for TermsNode {
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = self.indent(styles); let indent = self.indent(styles);
let body_indent = self.hanging_indent(styles); let hanging_indent = self.hanging_indent(styles);
let gutter = if self.tight(styles) { let gutter = if self.tight(styles) {
ParNode::leading_in(styles).into() ParNode::leading_in(styles).into()
} else { } else {
@ -93,29 +92,22 @@ impl Layout for TermsNode {
.unwrap_or_else(|| BlockNode::below_in(styles).amount()) .unwrap_or_else(|| BlockNode::below_in(styles).amount())
}; };
let mut cells = vec![]; let mut seq = vec![];
for child in self.children() { for (i, child) in self.children().into_iter().enumerate() {
let body = Content::sequence(vec![ if i > 0 {
HNode::new((-body_indent).into()).pack(), seq.push(VNode::new(gutter).with_weakness(1).pack());
(child.term() + TextNode::packed(':')).strong(), }
SpaceNode::new().pack(), if indent.is_zero() {
child.description(), seq.push(HNode::new(indent.into()).pack());
]); }
seq.push((child.term() + TextNode::packed(':')).strong());
cells.push(Content::empty()); seq.push(SpaceNode::new().pack());
cells.push(body); seq.push(child.description());
} }
let layouter = GridLayouter::new( Content::sequence(seq)
vt, .styled(ParNode::set_hanging_indent(hanging_indent + indent))
Axes::with_x(&[Sizing::Rel((indent + body_indent).into()), Sizing::Auto]), .layout(vt, styles, regions)
Axes::with_y(&[gutter.into()]),
&cells,
regions,
styles,
);
Ok(layouter.layout()?.fragment)
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,7 +1,7 @@
// Test paragraph indent. // Test paragraph indent.
--- ---
#set par(indent: 12pt, leading: 5pt) #set par(first-line-indent: 12pt, leading: 5pt)
#set block(spacing: 5pt) #set block(spacing: 5pt)
#show heading: set text(size: 10pt) #show heading: set text(size: 10pt)
@ -31,7 +31,22 @@ starts a paragraph without indent.
--- ---
// This is madness. // This is madness.
#set par(indent: 12pt) #set par(first-line-indent: 12pt)
Why would anybody ever ... Why would anybody ever ...
... want spacing and indent? ... want spacing and indent?
---
// Test hanging indent.
#set par(hanging-indent: 15pt, justify: true)
#lorem(10)
---
#set par(hanging-indent: 1em)
Welcome \ here. Does this work well?
---
#set par(hanging-indent: 2em)
#set text(dir: rtl)
لآن وقد أظلم الليل وبدأت النجوم
تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار

View File

@ -2,7 +2,7 @@
--- ---
#set page(width: 180pt) #set page(width: 180pt)
#set block(spacing: 5pt) #set block(spacing: 5pt)
#set par(justify: true, indent: 14pt, leading: 5pt) #set par(justify: true, first-line-indent: 14pt, leading: 5pt)
This text is justified, meaning that spaces are stretched so that the text This text is justified, meaning that spaces are stretched so that the text
forms a "block" with flush edges at both sides. forms a "block" with flush edges at both sides.