mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Support first-line-indent for every paragraph (#5768)
This commit is contained in:
parent
176b070c77
commit
85d1778974
@ -23,6 +23,7 @@ use typst_library::World;
|
|||||||
use typst_utils::SliceExt;
|
use typst_utils::SliceExt;
|
||||||
|
|
||||||
use super::{layout_multi_block, layout_single_block, FlowMode};
|
use super::{layout_multi_block, layout_single_block, FlowMode};
|
||||||
|
use crate::inline::ParSituation;
|
||||||
use crate::modifiers::layout_and_modify;
|
use crate::modifiers::layout_and_modify;
|
||||||
|
|
||||||
/// Collects all elements of the flow into prepared children. These are much
|
/// Collects all elements of the flow into prepared children. These are much
|
||||||
@ -46,7 +47,7 @@ pub fn collect<'a>(
|
|||||||
base,
|
base,
|
||||||
expand,
|
expand,
|
||||||
output: Vec::with_capacity(children.len()),
|
output: Vec::with_capacity(children.len()),
|
||||||
last_was_par: false,
|
par_situation: ParSituation::First,
|
||||||
}
|
}
|
||||||
.run(mode)
|
.run(mode)
|
||||||
}
|
}
|
||||||
@ -60,7 +61,7 @@ struct Collector<'a, 'x, 'y> {
|
|||||||
expand: bool,
|
expand: bool,
|
||||||
locator: SplitLocator<'a>,
|
locator: SplitLocator<'a>,
|
||||||
output: Vec<Child<'a>>,
|
output: Vec<Child<'a>>,
|
||||||
last_was_par: bool,
|
par_situation: ParSituation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Collector<'a, '_, '_> {
|
impl<'a> Collector<'a, '_, '_> {
|
||||||
@ -123,8 +124,7 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
styles,
|
styles,
|
||||||
self.base,
|
self.base,
|
||||||
self.expand,
|
self.expand,
|
||||||
false,
|
None,
|
||||||
false,
|
|
||||||
)?
|
)?
|
||||||
.into_frames();
|
.into_frames();
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
styles,
|
styles,
|
||||||
self.base,
|
self.base,
|
||||||
self.expand,
|
self.expand,
|
||||||
self.last_was_par,
|
self.par_situation,
|
||||||
)?
|
)?
|
||||||
.into_frames();
|
.into_frames();
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
self.lines(lines, styles);
|
self.lines(lines, styles);
|
||||||
|
|
||||||
self.output.push(Child::Rel(spacing.into(), 4));
|
self.output.push(Child::Rel(spacing.into(), 4));
|
||||||
self.last_was_par = true;
|
self.par_situation = ParSituation::Consecutive;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -272,7 +272,7 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.output.push(spacing(elem.below(styles)));
|
self.output.push(spacing(elem.below(styles)));
|
||||||
self.last_was_par = false;
|
self.par_situation = ParSituation::Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collects a placed element into a [`PlacedChild`].
|
/// Collects a placed element into a [`PlacedChild`].
|
||||||
|
@ -5,6 +5,7 @@ use typst_library::layout::{
|
|||||||
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
|
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
|
||||||
Spacing,
|
Spacing,
|
||||||
};
|
};
|
||||||
|
use typst_library::model::{EnumElem, ListElem, TermsElem};
|
||||||
use typst_library::routines::Pair;
|
use typst_library::routines::Pair;
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
||||||
@ -124,26 +125,33 @@ pub fn collect<'a>(
|
|||||||
locator: &mut SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
region: Size,
|
region: Size,
|
||||||
consecutive: bool,
|
situation: Option<ParSituation>,
|
||||||
paragraph: 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 quoter = SmartQuoter::new();
|
let mut quoter = SmartQuoter::new();
|
||||||
|
|
||||||
let outer_dir = TextElem::dir_in(styles);
|
let outer_dir = TextElem::dir_in(styles);
|
||||||
|
|
||||||
if paragraph && consecutive {
|
if let Some(situation) = situation {
|
||||||
let first_line_indent = ParElem::first_line_indent_in(styles);
|
let first_line_indent = ParElem::first_line_indent_in(styles);
|
||||||
if !first_line_indent.is_zero()
|
if !first_line_indent.amount.is_zero()
|
||||||
|
&& match situation {
|
||||||
|
// First-line indent for the first paragraph after a list bullet
|
||||||
|
// just looks bad.
|
||||||
|
ParSituation::First => first_line_indent.all && !in_list(styles),
|
||||||
|
ParSituation::Consecutive => true,
|
||||||
|
ParSituation::Other => first_line_indent.all,
|
||||||
|
}
|
||||||
&& AlignElem::alignment_in(styles).resolve(styles).x
|
&& AlignElem::alignment_in(styles).resolve(styles).x
|
||||||
== outer_dir.start().into()
|
== outer_dir.start().into()
|
||||||
{
|
{
|
||||||
collector.push_item(Item::Absolute(first_line_indent.resolve(styles), false));
|
collector.push_item(Item::Absolute(
|
||||||
|
first_line_indent.amount.resolve(styles),
|
||||||
|
false,
|
||||||
|
));
|
||||||
collector.spans.push(1, Span::detached());
|
collector.spans.push(1, Span::detached());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if paragraph {
|
|
||||||
let hang = ParElem::hanging_indent_in(styles);
|
let hang = ParElem::hanging_indent_in(styles);
|
||||||
if !hang.is_zero() {
|
if !hang.is_zero() {
|
||||||
collector.push_item(Item::Absolute(-hang, false));
|
collector.push_item(Item::Absolute(-hang, false));
|
||||||
@ -257,6 +265,16 @@ pub fn collect<'a>(
|
|||||||
Ok((collector.full, collector.segments, collector.spans))
|
Ok((collector.full, collector.segments, collector.spans))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether we have a list ancestor.
|
||||||
|
///
|
||||||
|
/// When we support some kind of more general ancestry mechanism, this can
|
||||||
|
/// become more elegant.
|
||||||
|
fn in_list(styles: StyleChain) -> bool {
|
||||||
|
ListElem::depth_in(styles).0 > 0
|
||||||
|
|| !EnumElem::parents_in(styles).is_empty()
|
||||||
|
|| TermsElem::within_in(styles)
|
||||||
|
}
|
||||||
|
|
||||||
/// Collects segments.
|
/// Collects segments.
|
||||||
struct Collector<'a> {
|
struct Collector<'a> {
|
||||||
full: String,
|
full: String,
|
||||||
|
@ -42,7 +42,7 @@ pub fn layout_par(
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
consecutive: bool,
|
situation: ParSituation,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
layout_par_impl(
|
layout_par_impl(
|
||||||
elem,
|
elem,
|
||||||
@ -56,7 +56,7 @@ pub fn layout_par(
|
|||||||
styles,
|
styles,
|
||||||
region,
|
region,
|
||||||
expand,
|
expand,
|
||||||
consecutive,
|
situation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ fn layout_par_impl(
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
consecutive: bool,
|
situation: ParSituation,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let link = LocatorLink::new(locator);
|
let link = LocatorLink::new(locator);
|
||||||
let mut locator = Locator::link(&link).split();
|
let mut locator = Locator::link(&link).split();
|
||||||
@ -105,8 +105,7 @@ fn layout_par_impl(
|
|||||||
styles,
|
styles,
|
||||||
region,
|
region,
|
||||||
expand,
|
expand,
|
||||||
true,
|
Some(situation),
|
||||||
consecutive,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,16 +118,15 @@ pub fn layout_inline<'a>(
|
|||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
paragraph: bool,
|
par: Option<ParSituation>,
|
||||||
consecutive: bool,
|
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
// 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, engine, locator, styles, region, consecutive, paragraph)?;
|
collect(children, engine, locator, styles, region, par)?;
|
||||||
|
|
||||||
// Perform BiDi analysis and performs some preparation steps before we
|
// Perform BiDi analysis and performs some preparation steps before we
|
||||||
// proceed to line breaking.
|
// proceed to line breaking.
|
||||||
let p = prepare(engine, children, &text, segments, spans, styles, paragraph)?;
|
let p = prepare(engine, children, &text, segments, spans, styles, par)?;
|
||||||
|
|
||||||
// Break the text into lines.
|
// Break the text into lines.
|
||||||
let lines = linebreak(engine, &p, region.x - p.hang);
|
let lines = linebreak(engine, &p, region.x - p.hang);
|
||||||
@ -136,3 +134,17 @@ pub fn layout_inline<'a>(
|
|||||||
// Turn the selected lines into frames.
|
// Turn the selected lines into frames.
|
||||||
finalize(engine, &p, &lines, styles, region, expand, locator)
|
finalize(engine, &p, &lines, styles, region, expand, locator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Distinguishes between a few different kinds of paragraphs.
|
||||||
|
///
|
||||||
|
/// In the form `Option<ParSituation>`, `None` implies that we are creating an
|
||||||
|
/// inline layout that isn't a semantic paragraph.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum ParSituation {
|
||||||
|
/// The paragraph is the first thing in the flow.
|
||||||
|
First,
|
||||||
|
/// The paragraph follows another paragraph.
|
||||||
|
Consecutive,
|
||||||
|
/// Any other kind of paragraph.
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
@ -85,7 +85,7 @@ pub fn prepare<'a>(
|
|||||||
segments: Vec<Segment<'a>>,
|
segments: Vec<Segment<'a>>,
|
||||||
spans: SpanMapper,
|
spans: SpanMapper,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
paragraph: bool,
|
situation: Option<ParSituation>,
|
||||||
) -> SourceResult<Preparation<'a>> {
|
) -> SourceResult<Preparation<'a>> {
|
||||||
let dir = TextElem::dir_in(styles);
|
let dir = TextElem::dir_in(styles);
|
||||||
let default_level = match dir {
|
let default_level = match dir {
|
||||||
@ -130,7 +130,11 @@ pub fn prepare<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only apply hanging indent to real paragraphs.
|
// Only apply hanging indent to real paragraphs.
|
||||||
let hang = if paragraph { ParElem::hanging_indent_in(styles) } else { Abs::zero() };
|
let hang = if situation.is_some() {
|
||||||
|
ParElem::hanging_indent_in(styles)
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Preparation {
|
Ok(Preparation {
|
||||||
text,
|
text,
|
||||||
|
@ -107,8 +107,7 @@ fn layout_inline_text(
|
|||||||
styles,
|
styles,
|
||||||
Size::splat(Abs::inf()),
|
Size::splat(Abs::inf()),
|
||||||
false,
|
false,
|
||||||
false,
|
None,
|
||||||
false,
|
|
||||||
)?
|
)?
|
||||||
.into_frame();
|
.into_frame();
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@ use typst_utils::singleton;
|
|||||||
use crate::diag::{bail, SourceResult};
|
use crate::diag::{bail, SourceResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Smart,
|
cast, dict, elem, scope, Args, Cast, Construct, Content, Dict, NativeElement, Packed,
|
||||||
Unlabellable,
|
Smart, Unlabellable, Value,
|
||||||
};
|
};
|
||||||
use crate::introspection::{Count, CounterUpdate, Locatable};
|
use crate::introspection::{Count, CounterUpdate, Locatable};
|
||||||
use crate::layout::{Em, HAlignment, Length, OuterHAlignment};
|
use crate::layout::{Em, HAlignment, Length, OuterHAlignment};
|
||||||
@ -163,16 +163,56 @@ pub struct ParElem {
|
|||||||
|
|
||||||
/// The indent the first line of a paragraph should have.
|
/// The indent the first line of a paragraph should have.
|
||||||
///
|
///
|
||||||
/// Only the first line of a consecutive paragraph will be indented (not
|
/// By default, only the first line of a consecutive paragraph will be
|
||||||
/// the first one in a block or on the page).
|
/// indented (not the first one in the document or container, and not
|
||||||
|
/// paragraphs immediately following other block-level elements).
|
||||||
|
///
|
||||||
|
/// If you want to indent all paragraphs instead, you can pass a dictionary
|
||||||
|
/// containing the `amount` of indent as a length and the pair
|
||||||
|
/// `{all: true}`. When `all` is omitted from the dictionary, it defaults to
|
||||||
|
/// `{false}`.
|
||||||
///
|
///
|
||||||
/// By typographic convention, paragraph breaks are indicated either by some
|
/// By typographic convention, paragraph breaks are indicated either by some
|
||||||
/// space between paragraphs or by indented first lines. Consider reducing
|
/// space between paragraphs or by indented first lines. Consider
|
||||||
/// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading)
|
/// - reducing the [paragraph `spacing`]($par.spacing) to the
|
||||||
/// when using this property (e.g. using `[#set par(spacing: 0.65em)]`).
|
/// [`leading`]($par.leading) using `{set par(spacing: 0.65em)}`
|
||||||
pub first_line_indent: Length,
|
/// - increasing the [block `spacing`]($block.spacing) (which inherits the
|
||||||
|
/// paragraph spacing by default) to the original paragraph spacing using
|
||||||
|
/// `{set block(spacing: 1.2em)}`
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #set block(spacing: 1.2em)
|
||||||
|
/// #set par(
|
||||||
|
/// first-line-indent: 1.5em,
|
||||||
|
/// spacing: 0.65em,
|
||||||
|
/// )
|
||||||
|
///
|
||||||
|
/// The first paragraph is not affected
|
||||||
|
/// by the indent.
|
||||||
|
///
|
||||||
|
/// But the second paragraph is.
|
||||||
|
///
|
||||||
|
/// #line(length: 100%)
|
||||||
|
///
|
||||||
|
/// #set par(first-line-indent: (
|
||||||
|
/// amount: 1.5em,
|
||||||
|
/// all: true,
|
||||||
|
/// ))
|
||||||
|
///
|
||||||
|
/// Now all paragraphs are affected
|
||||||
|
/// by the first line indent.
|
||||||
|
///
|
||||||
|
/// Even the first one.
|
||||||
|
/// ```
|
||||||
|
pub first_line_indent: FirstLineIndent,
|
||||||
|
|
||||||
/// The indent that all but the first line of a paragraph should have.
|
/// The indent that all but the first line of a paragraph should have.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #set par(hanging-indent: 1em)
|
||||||
|
///
|
||||||
|
/// #lorem(15)
|
||||||
|
/// ```
|
||||||
#[resolve]
|
#[resolve]
|
||||||
pub hanging_indent: Length,
|
pub hanging_indent: Length,
|
||||||
|
|
||||||
@ -199,6 +239,36 @@ pub enum Linebreaks {
|
|||||||
Optimized,
|
Optimized,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration for first line indent.
|
||||||
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)]
|
||||||
|
pub struct FirstLineIndent {
|
||||||
|
/// The amount of indent.
|
||||||
|
pub amount: Length,
|
||||||
|
/// Whether to indent all paragraphs, not just consecutive ones.
|
||||||
|
pub all: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
FirstLineIndent,
|
||||||
|
self => Value::Dict(self.into()),
|
||||||
|
amount: Length => Self { amount, all: false },
|
||||||
|
mut dict: Dict => {
|
||||||
|
let amount = dict.take("amount")?.cast()?;
|
||||||
|
let all = dict.take("all").ok().map(|v| v.cast()).transpose()?.unwrap_or(false);
|
||||||
|
dict.finish(&["amount", "all"])?;
|
||||||
|
Self { amount, all }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FirstLineIndent> for Dict {
|
||||||
|
fn from(indent: FirstLineIndent) -> Self {
|
||||||
|
dict! {
|
||||||
|
"amount" => indent.amount,
|
||||||
|
"all" => indent.all,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A paragraph break.
|
/// A paragraph break.
|
||||||
///
|
///
|
||||||
/// This starts a new paragraph. Especially useful when used within code like
|
/// This starts a new paragraph. Especially useful when used within code like
|
||||||
|
@ -105,6 +105,11 @@ pub struct TermsElem {
|
|||||||
/// ```
|
/// ```
|
||||||
#[variadic]
|
#[variadic]
|
||||||
pub children: Vec<Packed<TermItem>>,
|
pub children: Vec<Packed<TermItem>>,
|
||||||
|
|
||||||
|
/// Whether we are currently within a term list.
|
||||||
|
#[internal]
|
||||||
|
#[ghost]
|
||||||
|
pub within: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
@ -180,7 +185,8 @@ impl Show for Packed<TermsElem> {
|
|||||||
.with_spacing(Some(gutter.into()))
|
.with_spacing(Some(gutter.into()))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(span)
|
.spanned(span)
|
||||||
.padded(padding);
|
.padded(padding)
|
||||||
|
.styled(TermsElem::set_within(true));
|
||||||
|
|
||||||
if tight {
|
if tight {
|
||||||
let leading = ParElem::leading_in(styles);
|
let leading = ParElem::leading_in(styles);
|
||||||
|
BIN
tests/ref/par-first-line-indent-all-enum.png
Normal file
BIN
tests/ref/par-first-line-indent-all-enum.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 425 B |
BIN
tests/ref/par-first-line-indent-all-list.png
Normal file
BIN
tests/ref/par-first-line-indent-all-list.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 383 B |
BIN
tests/ref/par-first-line-indent-all-terms.png
Normal file
BIN
tests/ref/par-first-line-indent-all-terms.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 755 B |
BIN
tests/ref/par-first-line-indent-all.png
Normal file
BIN
tests/ref/par-first-line-indent-all.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@ -156,6 +156,57 @@ starts a paragraph, also with indent.
|
|||||||
|
|
||||||
ثم يصبح النص رطبًا وقابل للطرق ويبدو المستند رائعًا.
|
ثم يصبح النص رطبًا وقابل للطرق ويبدو المستند رائعًا.
|
||||||
|
|
||||||
|
--- par-first-line-indent-all ---
|
||||||
|
#set par(
|
||||||
|
first-line-indent: (amount: 12pt, all: true),
|
||||||
|
spacing: 5pt,
|
||||||
|
leading: 5pt,
|
||||||
|
)
|
||||||
|
#set block(spacing: 1.2em)
|
||||||
|
#show heading: set text(size: 10pt)
|
||||||
|
|
||||||
|
= Heading
|
||||||
|
All paragraphs are indented.
|
||||||
|
|
||||||
|
Even the first.
|
||||||
|
|
||||||
|
--- par-first-line-indent-all-list ---
|
||||||
|
#show list.where(tight: false): set list(spacing: 1.2em)
|
||||||
|
#set par(
|
||||||
|
first-line-indent: (amount: 12pt, all: true),
|
||||||
|
spacing: 5pt,
|
||||||
|
leading: 5pt,
|
||||||
|
)
|
||||||
|
|
||||||
|
- A #parbreak() B #line(length: 100%) C
|
||||||
|
|
||||||
|
- D
|
||||||
|
|
||||||
|
--- par-first-line-indent-all-enum ---
|
||||||
|
#show enum.where(tight: false): set enum(spacing: 1.2em)
|
||||||
|
#set par(
|
||||||
|
first-line-indent: (amount: 12pt, all: true),
|
||||||
|
spacing: 5pt,
|
||||||
|
leading: 5pt,
|
||||||
|
)
|
||||||
|
|
||||||
|
+ A #parbreak() B #line(length: 100%) C
|
||||||
|
|
||||||
|
+ D
|
||||||
|
|
||||||
|
--- par-first-line-indent-all-terms ---
|
||||||
|
#show terms.where(tight: false): set terms(spacing: 1.2em)
|
||||||
|
#set terms(hanging-indent: 10pt)
|
||||||
|
#set par(
|
||||||
|
first-line-indent: (amount: 12pt, all: true),
|
||||||
|
spacing: 5pt,
|
||||||
|
leading: 5pt,
|
||||||
|
)
|
||||||
|
|
||||||
|
/ Term A: B \ C #parbreak() D #line(length: 100%) E
|
||||||
|
|
||||||
|
/ Term F: G
|
||||||
|
|
||||||
--- par-spacing-and-first-line-indent ---
|
--- par-spacing-and-first-line-indent ---
|
||||||
// This is madness.
|
// This is madness.
|
||||||
#set par(first-line-indent: 12pt)
|
#set par(first-line-indent: 12pt)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user