mirror of
https://github.com/typst/typst
synced 2025-05-13 12:36:23 +08:00
Hanging indent
This commit is contained in:
parent
724e9b140c
commit
2bacbaf2bd
@ -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.
|
||||||
|
@ -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 |
@ -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)
|
||||||
|
لآن وقد أظلم الليل وبدأت النجوم
|
||||||
|
تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user