Semantic paragraphs (#5746)
@ -16,7 +16,7 @@ use typst_library::introspection::{
|
|||||||
};
|
};
|
||||||
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
|
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
|
||||||
use typst_library::model::{DocumentInfo, ParElem};
|
use typst_library::model::{DocumentInfo, ParElem};
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
|
||||||
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
@ -139,7 +139,9 @@ fn html_fragment_impl(
|
|||||||
|
|
||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let children = (engine.routines.realize)(
|
let children = (engine.routines.realize)(
|
||||||
RealizationKind::HtmlFragment,
|
// No need to know about the `FragmentKind` because we handle both
|
||||||
|
// uniformly.
|
||||||
|
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
@ -189,7 +191,8 @@ fn handle(
|
|||||||
};
|
};
|
||||||
output.push(element.into());
|
output.push(element.into());
|
||||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||||
let children = handle_list(engine, locator, elem.children.iter(&styles))?;
|
let children =
|
||||||
|
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
|
||||||
output.push(
|
output.push(
|
||||||
HtmlElement::new(tag::p)
|
HtmlElement::new(tag::p)
|
||||||
.with_children(children)
|
.with_children(children)
|
||||||
|
@ -20,13 +20,15 @@ use typst_library::model::ParElem;
|
|||||||
use typst_library::routines::{Pair, Routines};
|
use typst_library::routines::{Pair, Routines};
|
||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
|
use typst_utils::SliceExt;
|
||||||
|
|
||||||
use super::{layout_multi_block, layout_single_block};
|
use super::{layout_multi_block, layout_single_block, FlowMode};
|
||||||
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
|
||||||
/// simpler to handle than the raw elements.
|
/// simpler to handle than the raw elements.
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn collect<'a>(
|
pub fn collect<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
bump: &'a Bump,
|
bump: &'a Bump,
|
||||||
@ -34,6 +36,7 @@ pub fn collect<'a>(
|
|||||||
locator: Locator<'a>,
|
locator: Locator<'a>,
|
||||||
base: Size,
|
base: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
|
mode: FlowMode,
|
||||||
) -> SourceResult<Vec<Child<'a>>> {
|
) -> SourceResult<Vec<Child<'a>>> {
|
||||||
Collector {
|
Collector {
|
||||||
engine,
|
engine,
|
||||||
@ -45,7 +48,7 @@ pub fn collect<'a>(
|
|||||||
output: Vec::with_capacity(children.len()),
|
output: Vec::with_capacity(children.len()),
|
||||||
last_was_par: false,
|
last_was_par: false,
|
||||||
}
|
}
|
||||||
.run()
|
.run(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State for collection.
|
/// State for collection.
|
||||||
@ -62,7 +65,15 @@ struct Collector<'a, 'x, 'y> {
|
|||||||
|
|
||||||
impl<'a> Collector<'a, '_, '_> {
|
impl<'a> Collector<'a, '_, '_> {
|
||||||
/// Perform the collection.
|
/// Perform the collection.
|
||||||
fn run(mut self) -> SourceResult<Vec<Child<'a>>> {
|
fn run(self, mode: FlowMode) -> SourceResult<Vec<Child<'a>>> {
|
||||||
|
match mode {
|
||||||
|
FlowMode::Root | FlowMode::Block => self.run_block(),
|
||||||
|
FlowMode::Inline => self.run_inline(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform collection for block-level children.
|
||||||
|
fn run_block(mut self) -> SourceResult<Vec<Child<'a>>> {
|
||||||
for &(child, styles) in self.children {
|
for &(child, styles) in self.children {
|
||||||
if let Some(elem) = child.to_packed::<TagElem>() {
|
if let Some(elem) = child.to_packed::<TagElem>() {
|
||||||
self.output.push(Child::Tag(&elem.tag));
|
self.output.push(Child::Tag(&elem.tag));
|
||||||
@ -95,6 +106,43 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
Ok(self.output)
|
Ok(self.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform collection for inline-level children.
|
||||||
|
fn run_inline(mut self) -> SourceResult<Vec<Child<'a>>> {
|
||||||
|
// Extract leading and trailing tags.
|
||||||
|
let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::<TagElem>());
|
||||||
|
let inner = &self.children[start..end];
|
||||||
|
|
||||||
|
// Compute the shared styles, ignoring tags.
|
||||||
|
let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default();
|
||||||
|
|
||||||
|
// Layout the lines.
|
||||||
|
let lines = crate::inline::layout_inline(
|
||||||
|
self.engine,
|
||||||
|
inner,
|
||||||
|
&mut self.locator,
|
||||||
|
styles,
|
||||||
|
self.base,
|
||||||
|
self.expand,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)?
|
||||||
|
.into_frames();
|
||||||
|
|
||||||
|
for (c, _) in &self.children[..start] {
|
||||||
|
let elem = c.to_packed::<TagElem>().unwrap();
|
||||||
|
self.output.push(Child::Tag(&elem.tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lines(lines, styles);
|
||||||
|
|
||||||
|
for (c, _) in &self.children[end..] {
|
||||||
|
let elem = c.to_packed::<TagElem>().unwrap();
|
||||||
|
self.output.push(Child::Tag(&elem.tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.output)
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect vertical spacing into a relative or fractional child.
|
/// Collect vertical spacing into a relative or fractional child.
|
||||||
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
|
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
|
||||||
self.output.push(match elem.amount {
|
self.output.push(match elem.amount {
|
||||||
@ -110,24 +158,34 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
elem: &'a Packed<ParElem>,
|
elem: &'a Packed<ParElem>,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
let lines = crate::inline::layout_par(
|
||||||
let leading = ParElem::leading_in(styles);
|
elem,
|
||||||
let spacing = ParElem::spacing_in(styles);
|
|
||||||
let costs = TextElem::costs_in(styles);
|
|
||||||
|
|
||||||
let lines = crate::layout_inline(
|
|
||||||
self.engine,
|
self.engine,
|
||||||
&elem.children,
|
|
||||||
self.locator.next(&elem.span()),
|
self.locator.next(&elem.span()),
|
||||||
styles,
|
styles,
|
||||||
self.last_was_par,
|
|
||||||
self.base,
|
self.base,
|
||||||
self.expand,
|
self.expand,
|
||||||
|
self.last_was_par,
|
||||||
)?
|
)?
|
||||||
.into_frames();
|
.into_frames();
|
||||||
|
|
||||||
|
let spacing = ParElem::spacing_in(styles);
|
||||||
self.output.push(Child::Rel(spacing.into(), 4));
|
self.output.push(Child::Rel(spacing.into(), 4));
|
||||||
|
|
||||||
|
self.lines(lines, styles);
|
||||||
|
|
||||||
|
self.output.push(Child::Rel(spacing.into(), 4));
|
||||||
|
self.last_was_par = true;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect laid-out lines.
|
||||||
|
fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) {
|
||||||
|
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||||
|
let leading = ParElem::leading_in(styles);
|
||||||
|
let costs = TextElem::costs_in(styles);
|
||||||
|
|
||||||
// Determine whether to prevent widow and orphans.
|
// Determine whether to prevent widow and orphans.
|
||||||
let len = lines.len();
|
let len = lines.len();
|
||||||
let prevent_orphans =
|
let prevent_orphans =
|
||||||
@ -166,11 +224,6 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
self.output
|
self.output
|
||||||
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
|
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.output.push(Child::Rel(spacing.into(), 4));
|
|
||||||
self.last_was_par = true;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
|
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
|
||||||
|
@ -17,7 +17,9 @@ use typst_library::model::{
|
|||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::{NonZeroExt, Numeric};
|
use typst_utils::{NonZeroExt, Numeric};
|
||||||
|
|
||||||
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
|
use super::{
|
||||||
|
distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work,
|
||||||
|
};
|
||||||
|
|
||||||
/// Composes the contents of a single page/region. A region can have multiple
|
/// Composes the contents of a single page/region. A region can have multiple
|
||||||
/// columns/subregions.
|
/// columns/subregions.
|
||||||
@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
|||||||
migratable: bool,
|
migratable: bool,
|
||||||
) -> FlowResult<()> {
|
) -> FlowResult<()> {
|
||||||
// Footnotes are only supported at the root level.
|
// Footnotes are only supported at the root level.
|
||||||
if !self.config.root {
|
if self.config.mode != FlowMode::Root {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ use typst_library::layout::{
|
|||||||
Regions, Rel, Size,
|
Regions, Rel, Size,
|
||||||
};
|
};
|
||||||
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
|
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
|
||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
use typst_utils::{NonZeroExt, Numeric};
|
use typst_utils::{NonZeroExt, Numeric};
|
||||||
@ -140,9 +140,10 @@ fn layout_fragment_impl(
|
|||||||
|
|
||||||
engine.route.check_layout_depth().at(content.span())?;
|
engine.route.check_layout_depth().at(content.span())?;
|
||||||
|
|
||||||
|
let mut kind = FragmentKind::Block;
|
||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let children = (engine.routines.realize)(
|
let children = (engine.routines.realize)(
|
||||||
RealizationKind::LayoutFragment,
|
RealizationKind::LayoutFragment(&mut kind),
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
@ -158,25 +159,46 @@ fn layout_fragment_impl(
|
|||||||
regions,
|
regions,
|
||||||
columns,
|
columns,
|
||||||
column_gutter,
|
column_gutter,
|
||||||
false,
|
kind.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The mode a flow can be laid out in.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum FlowMode {
|
||||||
|
/// A root flow with block-level elements. Like `FlowMode::Block`, but can
|
||||||
|
/// additionally host footnotes and line numbers.
|
||||||
|
Root,
|
||||||
|
/// A flow whose children are block-level elements.
|
||||||
|
Block,
|
||||||
|
/// A flow whose children are inline-level elements.
|
||||||
|
Inline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FragmentKind> for FlowMode {
|
||||||
|
fn from(value: FragmentKind) -> Self {
|
||||||
|
match value {
|
||||||
|
FragmentKind::Inline => Self::Inline,
|
||||||
|
FragmentKind::Block => Self::Block,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Lays out realized content into regions, potentially with columns.
|
/// Lays out realized content into regions, potentially with columns.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn layout_flow(
|
pub fn layout_flow<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &[Pair],
|
children: &[Pair<'a>],
|
||||||
locator: &mut SplitLocator,
|
locator: &mut SplitLocator<'a>,
|
||||||
shared: StyleChain,
|
shared: StyleChain<'a>,
|
||||||
mut regions: Regions,
|
mut regions: Regions,
|
||||||
columns: NonZeroUsize,
|
columns: NonZeroUsize,
|
||||||
column_gutter: Rel<Abs>,
|
column_gutter: Rel<Abs>,
|
||||||
root: bool,
|
mode: FlowMode,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
// Prepare configuration that is shared across the whole flow.
|
// Prepare configuration that is shared across the whole flow.
|
||||||
let config = Config {
|
let config = Config {
|
||||||
root,
|
mode,
|
||||||
shared,
|
shared,
|
||||||
columns: {
|
columns: {
|
||||||
let mut count = columns.get();
|
let mut count = columns.get();
|
||||||
@ -195,7 +217,7 @@ pub(crate) fn layout_flow(
|
|||||||
gap: FootnoteEntry::gap_in(shared),
|
gap: FootnoteEntry::gap_in(shared),
|
||||||
expand: regions.expand.x,
|
expand: regions.expand.x,
|
||||||
},
|
},
|
||||||
line_numbers: root.then(|| LineNumberConfig {
|
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
|
||||||
scope: ParLine::numbering_scope_in(shared),
|
scope: ParLine::numbering_scope_in(shared),
|
||||||
default_clearance: {
|
default_clearance: {
|
||||||
let width = if PageElem::flipped_in(shared) {
|
let width = if PageElem::flipped_in(shared) {
|
||||||
@ -225,6 +247,7 @@ pub(crate) fn layout_flow(
|
|||||||
locator.next(&()),
|
locator.next(&()),
|
||||||
Size::new(config.columns.width, regions.full),
|
Size::new(config.columns.width, regions.full),
|
||||||
regions.expand.x,
|
regions.expand.x,
|
||||||
|
mode,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut work = Work::new(&children);
|
let mut work = Work::new(&children);
|
||||||
@ -318,7 +341,7 @@ impl<'a, 'b> Work<'a, 'b> {
|
|||||||
struct Config<'x> {
|
struct Config<'x> {
|
||||||
/// Whether this is the root flow, which can host footnotes and line
|
/// Whether this is the root flow, which can host footnotes and line
|
||||||
/// numbers.
|
/// numbers.
|
||||||
root: bool,
|
mode: FlowMode,
|
||||||
/// The styles shared by the whole flow. This is used for footnotes and line
|
/// The styles shared by the whole flow. This is used for footnotes and line
|
||||||
/// numbers.
|
/// numbers.
|
||||||
shared: StyleChain<'x>,
|
shared: StyleChain<'x>,
|
||||||
|
@ -11,7 +11,7 @@ use typst_utils::Numeric;
|
|||||||
use crate::flow::unbreakable_pod;
|
use crate::flow::unbreakable_pod;
|
||||||
use crate::shapes::{clip_rect, fill_and_stroke};
|
use crate::shapes::{clip_rect, fill_and_stroke};
|
||||||
|
|
||||||
/// Lay out a box as part of a paragraph.
|
/// Lay out a box as part of inline layout.
|
||||||
#[typst_macros::time(name = "box", span = elem.span())]
|
#[typst_macros::time(name = "box", span = elem.span())]
|
||||||
pub fn layout_box(
|
pub fn layout_box(
|
||||||
elem: &Packed<BoxElem>,
|
elem: &Packed<BoxElem>,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use typst_library::diag::bail;
|
use typst_library::diag::warning;
|
||||||
use typst_library::foundations::{Packed, Resolve};
|
use typst_library::foundations::{Packed, Resolve};
|
||||||
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
||||||
use typst_library::layout::{
|
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::routines::Pair;
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
||||||
SpaceElem, TextElem,
|
SpaceElem, TextElem,
|
||||||
@ -16,7 +17,7 @@ use super::*;
|
|||||||
use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
|
use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
|
||||||
|
|
||||||
// The characters by which spacing, inline content and pins are replaced in the
|
// The characters by which spacing, inline content and pins are replaced in the
|
||||||
// paragraph's full text.
|
// full text.
|
||||||
const SPACING_REPLACE: &str = " "; // Space
|
const SPACING_REPLACE: &str = " "; // Space
|
||||||
const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character
|
const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ const POP_EMBEDDING: &str = "\u{202C}";
|
|||||||
const LTR_ISOLATE: &str = "\u{2066}";
|
const LTR_ISOLATE: &str = "\u{2066}";
|
||||||
const POP_ISOLATE: &str = "\u{2069}";
|
const POP_ISOLATE: &str = "\u{2069}";
|
||||||
|
|
||||||
/// A prepared item in a paragraph layout.
|
/// A prepared item in a inline layout.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Item<'a> {
|
pub enum Item<'a> {
|
||||||
/// A shaped text run with consistent style and direction.
|
/// A shaped text run with consistent style and direction.
|
||||||
@ -113,38 +114,44 @@ impl Segment<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collects all text of the paragraph into one string and a collection of
|
/// Collects all text into one string and a collection of segments that
|
||||||
/// segments that correspond to pieces of that string. This also performs
|
/// correspond to pieces of that string. This also performs string-level
|
||||||
/// string-level preprocessing like case transformations.
|
/// preprocessing like case transformations.
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
pub fn collect<'a>(
|
pub fn collect<'a>(
|
||||||
children: &'a StyleVec,
|
children: &[Pair<'a>],
|
||||||
engine: &mut Engine<'_>,
|
engine: &mut Engine<'_>,
|
||||||
locator: &mut SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
styles: &'a StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
region: Size,
|
region: Size,
|
||||||
consecutive: bool,
|
consecutive: bool,
|
||||||
|
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);
|
||||||
let first_line_indent = ParElem::first_line_indent_in(*styles);
|
|
||||||
if !first_line_indent.is_zero()
|
if paragraph && consecutive {
|
||||||
&& consecutive
|
let first_line_indent = ParElem::first_line_indent_in(styles);
|
||||||
&& AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into()
|
if !first_line_indent.is_zero()
|
||||||
{
|
&& AlignElem::alignment_in(styles).resolve(styles).x
|
||||||
collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false));
|
== outer_dir.start().into()
|
||||||
collector.spans.push(1, Span::detached());
|
{
|
||||||
|
collector.push_item(Item::Absolute(first_line_indent.resolve(styles), false));
|
||||||
|
collector.spans.push(1, Span::detached());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hang = ParElem::hanging_indent_in(*styles);
|
if paragraph {
|
||||||
if !hang.is_zero() {
|
let hang = ParElem::hanging_indent_in(styles);
|
||||||
collector.push_item(Item::Absolute(-hang, false));
|
if !hang.is_zero() {
|
||||||
collector.spans.push(1, Span::detached());
|
collector.push_item(Item::Absolute(-hang, false));
|
||||||
|
collector.spans.push(1, Span::detached());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (child, styles) in children.iter(styles) {
|
for &(child, styles) in children {
|
||||||
let prev_len = collector.full.len();
|
let prev_len = collector.full.len();
|
||||||
|
|
||||||
if child.is::<SpaceElem>() {
|
if child.is::<SpaceElem>() {
|
||||||
@ -234,7 +241,13 @@ pub fn collect<'a>(
|
|||||||
} else if let Some(elem) = child.to_packed::<TagElem>() {
|
} else if let Some(elem) = child.to_packed::<TagElem>() {
|
||||||
collector.push_item(Item::Tag(&elem.tag));
|
collector.push_item(Item::Tag(&elem.tag));
|
||||||
} else {
|
} else {
|
||||||
bail!(child.span(), "unexpected paragraph child");
|
// Non-paragraph inline layout should never trigger this since it
|
||||||
|
// only won't be triggered if we see any non-inline content.
|
||||||
|
engine.sink.warn(warning!(
|
||||||
|
child.span(),
|
||||||
|
"{} may not occur inside of a paragraph and was ignored",
|
||||||
|
child.func().name()
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let len = collector.full.len() - prev_len;
|
let len = collector.full.len() - prev_len;
|
||||||
|
@ -14,7 +14,7 @@ pub fn finalize(
|
|||||||
expand: bool,
|
expand: bool,
|
||||||
locator: &mut SplitLocator<'_>,
|
locator: &mut SplitLocator<'_>,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
// Determine the paragraph's width: Full width of the region if we should
|
// Determine the resulting 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.
|
||||||
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()))
|
||||||
|
@ -18,12 +18,12 @@ const EN_DASH: char = '–';
|
|||||||
const EM_DASH: char = '—';
|
const EM_DASH: char = '—';
|
||||||
const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks.
|
const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks.
|
||||||
|
|
||||||
/// A layouted line, consisting of a sequence of layouted paragraph items that
|
/// A layouted line, consisting of a sequence of layouted inline items that are
|
||||||
/// are mostly borrowed from the preparation phase. This type enables you to
|
/// mostly borrowed from the preparation phase. This type enables you to measure
|
||||||
/// measure the size of a line in a range before committing to building the
|
/// the size of a line in a range before committing to building the line's
|
||||||
/// line's frame.
|
/// frame.
|
||||||
///
|
///
|
||||||
/// At most two paragraph items must be created individually for this line: The
|
/// At most two inline items must be created individually for this line: The
|
||||||
/// first and last one since they may be broken apart by the start or end of the
|
/// first and last one since they may be broken apart by the start or end of the
|
||||||
/// line, respectively. But even those can partially reuse previous results when
|
/// line, respectively. But even those can partially reuse previous results when
|
||||||
/// the break index is safe-to-break per rustybuzz.
|
/// the break index is safe-to-break per rustybuzz.
|
||||||
@ -430,7 +430,7 @@ pub fn commit(
|
|||||||
let mut offset = Abs::zero();
|
let mut offset = Abs::zero();
|
||||||
|
|
||||||
// We always build the line from left to right. In an LTR paragraph, we must
|
// We always build the line from left to right. In an LTR paragraph, we must
|
||||||
// thus add the hanging indent to the offset. When the paragraph is RTL, the
|
// thus add the hanging indent to the offset. In an RTL paragraph, the
|
||||||
// hanging indent arises naturally due to the line width.
|
// hanging indent arises naturally due to the line width.
|
||||||
if p.dir == Dir::LTR {
|
if p.dir == Dir::LTR {
|
||||||
offset += p.hang;
|
offset += p.hang;
|
||||||
@ -631,7 +631,7 @@ fn overhang(c: char) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collection of owned or borrowed paragraph items.
|
/// A collection of owned or borrowed inline items.
|
||||||
pub struct Items<'a>(Vec<ItemEntry<'a>>);
|
pub struct Items<'a>(Vec<ItemEntry<'a>>);
|
||||||
|
|
||||||
impl<'a> Items<'a> {
|
impl<'a> Items<'a> {
|
||||||
|
@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// The cost of a line or paragraph layout.
|
/// The cost of a line or inline layout.
|
||||||
type Cost = f64;
|
type Cost = f64;
|
||||||
|
|
||||||
// Cost parameters.
|
// Cost parameters.
|
||||||
@ -104,7 +104,7 @@ impl Breakpoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Breaks the paragraph into lines.
|
/// Breaks the text into lines.
|
||||||
pub fn linebreak<'a>(
|
pub fn linebreak<'a>(
|
||||||
engine: &Engine,
|
engine: &Engine,
|
||||||
p: &'a Preparation<'a>,
|
p: &'a Preparation<'a>,
|
||||||
@ -181,13 +181,12 @@ fn linebreak_simple<'a>(
|
|||||||
/// lines with hyphens even more.
|
/// lines with hyphens even more.
|
||||||
///
|
///
|
||||||
/// To find the layout with the minimal total cost the algorithm uses dynamic
|
/// To find the layout with the minimal total cost the algorithm uses dynamic
|
||||||
/// programming: For each possible breakpoint it determines the optimal
|
/// programming: For each possible breakpoint, it determines the optimal layout
|
||||||
/// paragraph layout _up to that point_. It walks over all possible start points
|
/// _up to that point_. It walks over all possible start points for a line
|
||||||
/// for a line ending at that point and finds the one for which the cost of the
|
/// ending at that point and finds the one for which the cost of the line plus
|
||||||
/// line plus the cost of the optimal paragraph up to the start point (already
|
/// the cost of the optimal layout up to the start point (already computed and
|
||||||
/// computed and stored in dynamic programming table) is minimal. The final
|
/// stored in dynamic programming table) is minimal. The final result is simply
|
||||||
/// result is simply the layout determined for the last breakpoint at the end of
|
/// the layout determined for the last breakpoint at the end of text.
|
||||||
/// text.
|
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
fn linebreak_optimized<'a>(
|
fn linebreak_optimized<'a>(
|
||||||
engine: &Engine,
|
engine: &Engine,
|
||||||
@ -215,7 +214,7 @@ fn linebreak_optimized_bounded<'a>(
|
|||||||
metrics: &CostMetrics,
|
metrics: &CostMetrics,
|
||||||
upper_bound: Cost,
|
upper_bound: Cost,
|
||||||
) -> Vec<Line<'a>> {
|
) -> Vec<Line<'a>> {
|
||||||
/// An entry in the dynamic programming table for paragraph optimization.
|
/// An entry in the dynamic programming table for inline layout optimization.
|
||||||
struct Entry<'a> {
|
struct Entry<'a> {
|
||||||
pred: usize,
|
pred: usize,
|
||||||
total: Cost,
|
total: Cost,
|
||||||
@ -321,7 +320,7 @@ fn linebreak_optimized_bounded<'a>(
|
|||||||
// This should only happen if our bound was faulty. Which shouldn't happen!
|
// This should only happen if our bound was faulty. Which shouldn't happen!
|
||||||
if table[idx].end != p.text.len() {
|
if table[idx].end != p.text.len() {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
panic!("bounded paragraph layout is incomplete");
|
panic!("bounded inline layout is incomplete");
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY);
|
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY);
|
||||||
@ -342,7 +341,7 @@ fn linebreak_optimized_bounded<'a>(
|
|||||||
/// (which is costly) to determine costs, it determines approximate costs using
|
/// (which is costly) to determine costs, it determines approximate costs using
|
||||||
/// cumulative arrays.
|
/// cumulative arrays.
|
||||||
///
|
///
|
||||||
/// This results in a likely good paragraph layouts, for which we then compute
|
/// This results in a likely good inline layouts, for which we then compute
|
||||||
/// the exact cost. This cost is an upper bound for proper optimized
|
/// the exact cost. This cost is an upper bound for proper optimized
|
||||||
/// linebreaking. We can use it to heavily prune the search space.
|
/// linebreaking. We can use it to heavily prune the search space.
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
@ -355,7 +354,7 @@ fn linebreak_optimized_approximate(
|
|||||||
// Determine the cumulative estimation metrics.
|
// Determine the cumulative estimation metrics.
|
||||||
let estimates = Estimates::compute(p);
|
let estimates = Estimates::compute(p);
|
||||||
|
|
||||||
/// An entry in the dynamic programming table for paragraph optimization.
|
/// An entry in the dynamic programming table for inline layout optimization.
|
||||||
struct Entry {
|
struct Entry {
|
||||||
pred: usize,
|
pred: usize,
|
||||||
total: Cost,
|
total: Cost,
|
||||||
@ -862,7 +861,7 @@ struct CostMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CostMetrics {
|
impl CostMetrics {
|
||||||
/// Compute shared metrics for paragraph optimization.
|
/// Compute shared metrics for inline layout optimization.
|
||||||
fn compute(p: &Preparation) -> Self {
|
fn compute(p: &Preparation) -> Self {
|
||||||
Self {
|
Self {
|
||||||
// When justifying, we may stretch spaces below their natural width.
|
// When justifying, we may stretch spaces below their natural width.
|
||||||
|
@ -13,11 +13,11 @@ pub use self::box_::layout_box;
|
|||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{StyleChain, StyleVec};
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
use typst_library::introspection::{Introspector, Locator, LocatorLink};
|
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
|
||||||
use typst_library::layout::{Fragment, Size};
|
use typst_library::layout::{Fragment, Size};
|
||||||
use typst_library::model::ParElem;
|
use typst_library::model::ParElem;
|
||||||
use typst_library::routines::Routines;
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
|
|
||||||
use self::collect::{collect, Item, Segment, SpanMapper};
|
use self::collect::{collect, Item, Segment, SpanMapper};
|
||||||
@ -34,18 +34,18 @@ use self::shaping::{
|
|||||||
/// Range of a substring of text.
|
/// Range of a substring of text.
|
||||||
type Range = std::ops::Range<usize>;
|
type Range = std::ops::Range<usize>;
|
||||||
|
|
||||||
/// Layouts content inline.
|
/// Layouts the paragraph.
|
||||||
pub fn layout_inline(
|
pub fn layout_par(
|
||||||
|
elem: &Packed<ParElem>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &StyleVec,
|
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
consecutive: bool,
|
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
|
consecutive: bool,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
layout_inline_impl(
|
layout_par_impl(
|
||||||
children,
|
elem,
|
||||||
engine.routines,
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
@ -54,17 +54,17 @@ pub fn layout_inline(
|
|||||||
engine.route.track(),
|
engine.route.track(),
|
||||||
locator.track(),
|
locator.track(),
|
||||||
styles,
|
styles,
|
||||||
consecutive,
|
|
||||||
region,
|
region,
|
||||||
expand,
|
expand,
|
||||||
|
consecutive,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The internal, memoized implementation of `layout_inline`.
|
/// The internal, memoized implementation of `layout_par`.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_inline_impl(
|
fn layout_par_impl(
|
||||||
children: &StyleVec,
|
elem: &Packed<ParElem>,
|
||||||
routines: &Routines,
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
@ -73,12 +73,12 @@ fn layout_inline_impl(
|
|||||||
route: Tracked<Route>,
|
route: Tracked<Route>,
|
||||||
locator: Tracked<Locator>,
|
locator: Tracked<Locator>,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
consecutive: bool,
|
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
|
consecutive: bool,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let link = LocatorLink::new(locator);
|
let link = LocatorLink::new(locator);
|
||||||
let locator = Locator::link(&link);
|
let mut locator = Locator::link(&link).split();
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
routines,
|
routines,
|
||||||
world,
|
world,
|
||||||
@ -88,18 +88,51 @@ fn layout_inline_impl(
|
|||||||
route: Route::extend(route),
|
route: Route::extend(route),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut locator = locator.split();
|
let arenas = Arenas::default();
|
||||||
|
let children = (engine.routines.realize)(
|
||||||
|
RealizationKind::LayoutPar,
|
||||||
|
&mut engine,
|
||||||
|
&mut locator,
|
||||||
|
&arenas,
|
||||||
|
&elem.body,
|
||||||
|
styles,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
layout_inline(
|
||||||
|
&mut engine,
|
||||||
|
&children,
|
||||||
|
&mut locator,
|
||||||
|
styles,
|
||||||
|
region,
|
||||||
|
expand,
|
||||||
|
true,
|
||||||
|
consecutive,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out realized content with inline layout.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn layout_inline<'a>(
|
||||||
|
engine: &mut Engine,
|
||||||
|
children: &[Pair<'a>],
|
||||||
|
locator: &mut SplitLocator<'a>,
|
||||||
|
styles: StyleChain<'a>,
|
||||||
|
region: Size,
|
||||||
|
expand: bool,
|
||||||
|
paragraph: bool,
|
||||||
|
consecutive: bool,
|
||||||
|
) -> 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, &mut engine, &mut locator, &styles, region, consecutive)?;
|
collect(children, engine, locator, styles, region, consecutive, paragraph)?;
|
||||||
|
|
||||||
// Perform BiDi analysis and then prepares paragraph layout.
|
// Perform BiDi analysis and performs some preparation steps before we
|
||||||
let p = prepare(&mut engine, children, &text, segments, spans, styles)?;
|
// proceed to line breaking.
|
||||||
|
let p = prepare(engine, children, &text, segments, spans, styles, paragraph)?;
|
||||||
|
|
||||||
// Break the paragraph 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);
|
||||||
|
|
||||||
// Turn the selected lines into frames.
|
// Turn the selected lines into frames.
|
||||||
finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator)
|
finalize(engine, &p, &lines, styles, region, expand, locator)
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
use typst_library::foundations::{Resolve, Smart};
|
use typst_library::foundations::{Resolve, Smart};
|
||||||
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
|
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
|
||||||
use typst_library::model::Linebreaks;
|
use typst_library::model::Linebreaks;
|
||||||
|
use typst_library::routines::Pair;
|
||||||
use typst_library::text::{Costs, Lang, TextElem};
|
use typst_library::text::{Costs, Lang, TextElem};
|
||||||
|
use typst_utils::SliceExt;
|
||||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// A paragraph representation in which children are already layouted and text
|
/// A representation in which children are already layouted and text is already
|
||||||
/// is already preshaped.
|
/// preshaped.
|
||||||
///
|
///
|
||||||
/// In many cases, we can directly reuse these results when constructing a line.
|
/// In many cases, we can directly reuse these results when constructing a line.
|
||||||
/// Only when a line break falls onto a text index that is not safe-to-break per
|
/// Only when a line break falls onto a text index that is not safe-to-break per
|
||||||
/// rustybuzz, we have to reshape that portion.
|
/// rustybuzz, we have to reshape that portion.
|
||||||
pub struct Preparation<'a> {
|
pub struct Preparation<'a> {
|
||||||
/// The paragraph's full text.
|
/// The full text.
|
||||||
pub text: &'a str,
|
pub text: &'a str,
|
||||||
/// Bidirectional text embedding levels for the paragraph.
|
/// Bidirectional text embedding levels.
|
||||||
///
|
///
|
||||||
/// This is `None` if the paragraph is BiDi-uniform (all the base direction).
|
/// This is `None` if all text directions are uniform (all the base
|
||||||
|
/// direction).
|
||||||
pub bidi: Option<BidiInfo<'a>>,
|
pub bidi: Option<BidiInfo<'a>>,
|
||||||
/// Text runs, spacing and layouted elements.
|
/// Text runs, spacing and layouted elements.
|
||||||
pub items: Vec<(Range, Item<'a>)>,
|
pub items: Vec<(Range, Item<'a>)>,
|
||||||
@ -33,15 +36,15 @@ pub struct Preparation<'a> {
|
|||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
/// The text language if it's the same for all children.
|
/// The text language if it's the same for all children.
|
||||||
pub lang: Option<Lang>,
|
pub lang: Option<Lang>,
|
||||||
/// The paragraph's resolved horizontal alignment.
|
/// The resolved horizontal alignment.
|
||||||
pub align: FixedAlignment,
|
pub align: FixedAlignment,
|
||||||
/// Whether to justify the paragraph.
|
/// Whether to justify text.
|
||||||
pub justify: bool,
|
pub justify: bool,
|
||||||
/// The paragraph's hanging indent.
|
/// Hanging indent to apply.
|
||||||
pub hang: Abs,
|
pub hang: Abs,
|
||||||
/// Whether to add spacing between CJK and Latin characters.
|
/// Whether to add spacing between CJK and Latin characters.
|
||||||
pub cjk_latin_spacing: bool,
|
pub cjk_latin_spacing: bool,
|
||||||
/// Whether font fallback is enabled for this paragraph.
|
/// Whether font fallback is enabled.
|
||||||
pub fallback: bool,
|
pub fallback: bool,
|
||||||
/// How to determine line breaks.
|
/// How to determine line breaks.
|
||||||
pub linebreaks: Smart<Linebreaks>,
|
pub linebreaks: Smart<Linebreaks>,
|
||||||
@ -71,17 +74,18 @@ impl<'a> Preparation<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs BiDi analysis and then prepares paragraph layout by building a
|
/// Performs BiDi analysis and then prepares further layout by building a
|
||||||
/// representation on which we can do line breaking without layouting each and
|
/// representation on which we can do line breaking without layouting each and
|
||||||
/// every line from scratch.
|
/// every line from scratch.
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
pub fn prepare<'a>(
|
pub fn prepare<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &'a StyleVec,
|
children: &[Pair<'a>],
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
segments: Vec<Segment<'a>>,
|
segments: Vec<Segment<'a>>,
|
||||||
spans: SpanMapper,
|
spans: SpanMapper,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
|
paragraph: bool,
|
||||||
) -> 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 {
|
||||||
@ -125,19 +129,22 @@ pub fn prepare<'a>(
|
|||||||
add_cjk_latin_spacing(&mut items);
|
add_cjk_latin_spacing(&mut items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only apply hanging indent to real paragraphs.
|
||||||
|
let hang = if paragraph { ParElem::hanging_indent_in(styles) } else { Abs::zero() };
|
||||||
|
|
||||||
Ok(Preparation {
|
Ok(Preparation {
|
||||||
text,
|
text,
|
||||||
bidi: is_bidi.then_some(bidi),
|
bidi: is_bidi.then_some(bidi),
|
||||||
items,
|
items,
|
||||||
indices,
|
indices,
|
||||||
spans,
|
spans,
|
||||||
hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
|
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
|
||||||
costs: TextElem::costs_in(styles),
|
costs: TextElem::costs_in(styles),
|
||||||
dir,
|
dir,
|
||||||
lang: children.shared_get(styles, TextElem::lang_in),
|
lang: shared_get(children, styles, TextElem::lang_in),
|
||||||
align: AlignElem::alignment_in(styles).resolve(styles).x,
|
align: AlignElem::alignment_in(styles).resolve(styles).x,
|
||||||
justify: ParElem::justify_in(styles),
|
justify: ParElem::justify_in(styles),
|
||||||
hang: ParElem::hanging_indent_in(styles),
|
hang,
|
||||||
cjk_latin_spacing,
|
cjk_latin_spacing,
|
||||||
fallback: TextElem::fallback_in(styles),
|
fallback: TextElem::fallback_in(styles),
|
||||||
linebreaks: ParElem::linebreaks_in(styles),
|
linebreaks: ParElem::linebreaks_in(styles),
|
||||||
@ -145,6 +152,19 @@ pub fn prepare<'a>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a style property, but only if it is the same for all of the children.
|
||||||
|
fn shared_get<T: PartialEq>(
|
||||||
|
children: &[Pair],
|
||||||
|
styles: StyleChain<'_>,
|
||||||
|
getter: fn(StyleChain) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
let value = getter(styles);
|
||||||
|
children
|
||||||
|
.group_by_key(|&(_, s)| s)
|
||||||
|
.all(|(s, _)| getter(s) == value)
|
||||||
|
.then_some(value)
|
||||||
|
}
|
||||||
|
|
||||||
/// Add some spacing between Han characters and western characters. See
|
/// Add some spacing between Han characters and western characters. See
|
||||||
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
|
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
|
||||||
/// in Horizontal Written Mode
|
/// in Horizontal Written Mode
|
||||||
|
@ -29,7 +29,7 @@ use crate::modifiers::{FrameModifiers, FrameModify};
|
|||||||
/// frame.
|
/// frame.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ShapedText<'a> {
|
pub struct ShapedText<'a> {
|
||||||
/// The start of the text in the full paragraph.
|
/// The start of the text in the full text.
|
||||||
pub base: usize,
|
pub base: usize,
|
||||||
/// The text that was shaped.
|
/// The text that was shaped.
|
||||||
pub text: &'a str,
|
pub text: &'a str,
|
||||||
@ -66,9 +66,9 @@ pub struct ShapedGlyph {
|
|||||||
pub y_offset: Em,
|
pub y_offset: Em,
|
||||||
/// The adjustability of the glyph.
|
/// The adjustability of the glyph.
|
||||||
pub adjustability: Adjustability,
|
pub adjustability: Adjustability,
|
||||||
/// The byte range of this glyph's cluster in the full paragraph. A cluster
|
/// The byte range of this glyph's cluster in the full inline layout. A
|
||||||
/// is a sequence of one or multiple glyphs that cannot be separated and
|
/// cluster is a sequence of one or multiple glyphs that cannot be separated
|
||||||
/// must always be treated as a union.
|
/// and must always be treated as a union.
|
||||||
///
|
///
|
||||||
/// The range values of the glyphs in a [`ShapedText`] should not overlap
|
/// The range values of the glyphs in a [`ShapedText`] should not overlap
|
||||||
/// with each other, and they should be monotonically increasing (for
|
/// with each other, and they should be monotonically increasing (for
|
||||||
@ -405,7 +405,7 @@ impl<'a> ShapedText<'a> {
|
|||||||
/// Reshape a range of the shaped text, reusing information from this
|
/// Reshape a range of the shaped text, reusing information from this
|
||||||
/// shaping process if possible.
|
/// shaping process if possible.
|
||||||
///
|
///
|
||||||
/// The text `range` is relative to the whole paragraph.
|
/// The text `range` is relative to the whole inline layout.
|
||||||
pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
|
pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
|
||||||
let text = &self.text[text_range.start - self.base..text_range.end - self.base];
|
let text = &self.text[text_range.start - self.base..text_range.end - self.base];
|
||||||
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
|
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
|
||||||
|
@ -17,7 +17,6 @@ mod transforms;
|
|||||||
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
|
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
|
||||||
pub use self::grid::{layout_grid, layout_table};
|
pub use self::grid::{layout_grid, layout_table};
|
||||||
pub use self::image::layout_image;
|
pub use self::image::layout_image;
|
||||||
pub use self::inline::{layout_box, layout_inline};
|
|
||||||
pub use self::lists::{layout_enum, layout_list};
|
pub use self::lists::{layout_enum, layout_list};
|
||||||
pub use self::math::{layout_equation_block, layout_equation_inline};
|
pub use self::math::{layout_equation_block, layout_equation_inline};
|
||||||
pub use self::pad::layout_pad;
|
pub use self::pad::layout_pad;
|
||||||
|
@ -6,7 +6,7 @@ use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
|
|||||||
use typst_library::introspection::Locator;
|
use typst_library::introspection::Locator;
|
||||||
use typst_library::layout::grid::resolve::{Cell, CellGrid};
|
use typst_library::layout::grid::resolve::{Cell, CellGrid};
|
||||||
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
|
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
|
||||||
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem};
|
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
|
||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
|
|
||||||
use crate::grid::GridLayouter;
|
use crate::grid::GridLayouter;
|
||||||
@ -22,8 +22,9 @@ pub fn layout_list(
|
|||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let indent = elem.indent(styles);
|
let indent = elem.indent(styles);
|
||||||
let body_indent = elem.body_indent(styles);
|
let body_indent = elem.body_indent(styles);
|
||||||
|
let tight = elem.tight(styles);
|
||||||
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
||||||
if elem.tight(styles) {
|
if tight {
|
||||||
ParElem::leading_in(styles).into()
|
ParElem::leading_in(styles).into()
|
||||||
} else {
|
} else {
|
||||||
ParElem::spacing_in(styles).into()
|
ParElem::spacing_in(styles).into()
|
||||||
@ -41,11 +42,17 @@ pub fn layout_list(
|
|||||||
let mut locator = locator.split();
|
let mut locator = locator.split();
|
||||||
|
|
||||||
for item in &elem.children {
|
for item in &elem.children {
|
||||||
|
// Text in wide lists shall always turn into paragraphs.
|
||||||
|
let mut body = item.body.clone();
|
||||||
|
if !tight {
|
||||||
|
body += ParbreakElem::shared();
|
||||||
|
}
|
||||||
|
|
||||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
|
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
|
||||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
cells.push(Cell::new(
|
cells.push(Cell::new(
|
||||||
item.body.clone().styled(ListElem::set_depth(Depth(1))),
|
body.styled(ListElem::set_depth(Depth(1))),
|
||||||
locator.next(&item.body.span()),
|
locator.next(&item.body.span()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -78,8 +85,9 @@ pub fn layout_enum(
|
|||||||
let reversed = elem.reversed(styles);
|
let reversed = elem.reversed(styles);
|
||||||
let indent = elem.indent(styles);
|
let indent = elem.indent(styles);
|
||||||
let body_indent = elem.body_indent(styles);
|
let body_indent = elem.body_indent(styles);
|
||||||
|
let tight = elem.tight(styles);
|
||||||
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
||||||
if elem.tight(styles) {
|
if tight {
|
||||||
ParElem::leading_in(styles).into()
|
ParElem::leading_in(styles).into()
|
||||||
} else {
|
} else {
|
||||||
ParElem::spacing_in(styles).into()
|
ParElem::spacing_in(styles).into()
|
||||||
@ -124,11 +132,17 @@ pub fn layout_enum(
|
|||||||
let resolved =
|
let resolved =
|
||||||
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
|
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
|
||||||
|
|
||||||
|
// Text in wide enums shall always turn into paragraphs.
|
||||||
|
let mut body = item.body.clone();
|
||||||
|
if !tight {
|
||||||
|
body += ParbreakElem::shared();
|
||||||
|
}
|
||||||
|
|
||||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
cells.push(Cell::new(resolved, locator.next(&())));
|
cells.push(Cell::new(resolved, locator.next(&())));
|
||||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
cells.push(Cell::new(
|
cells.push(Cell::new(
|
||||||
item.body.clone().styled(EnumElem::set_parents(smallvec![number])),
|
body.styled(EnumElem::set_parents(smallvec![number])),
|
||||||
locator.next(&item.body.span()),
|
locator.next(&item.body.span()),
|
||||||
));
|
));
|
||||||
number =
|
number =
|
||||||
|
@ -2,6 +2,7 @@ use typst_library::diag::SourceResult;
|
|||||||
use typst_library::foundations::{Packed, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
use typst_library::layout::{Abs, Axis, Rel};
|
use typst_library::layout::{Abs, Axis, Rel};
|
||||||
use typst_library::math::{EquationElem, LrElem, MidElem};
|
use typst_library::math::{EquationElem, LrElem, MidElem};
|
||||||
|
use typst_utils::SliceExt;
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
|
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
|
||||||
@ -29,15 +30,7 @@ pub fn layout_lr(
|
|||||||
let mut fragments = ctx.layout_into_fragments(body, styles)?;
|
let mut fragments = ctx.layout_into_fragments(body, styles)?;
|
||||||
|
|
||||||
// Ignore leading and trailing ignorant fragments.
|
// Ignore leading and trailing ignorant fragments.
|
||||||
let start_idx = fragments
|
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
|
||||||
.iter()
|
|
||||||
.position(|f| !f.is_ignorant())
|
|
||||||
.unwrap_or(fragments.len());
|
|
||||||
let end_idx = fragments
|
|
||||||
.iter()
|
|
||||||
.skip(start_idx)
|
|
||||||
.rposition(|f| !f.is_ignorant())
|
|
||||||
.map_or(start_idx, |i| start_idx + i + 1);
|
|
||||||
let inner_fragments = &mut fragments[start_idx..end_idx];
|
let inner_fragments = &mut fragments[start_idx..end_idx];
|
||||||
|
|
||||||
let axis = scaled!(ctx, styles, axis_height);
|
let axis = scaled!(ctx, styles, axis_height);
|
||||||
|
@ -202,8 +202,7 @@ pub fn layout_equation_block(
|
|||||||
let counter = Counter::of(EquationElem::elem())
|
let counter = Counter::of(EquationElem::elem())
|
||||||
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
|
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
|
||||||
.spanned(span);
|
.spanned(span);
|
||||||
let number =
|
let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
|
||||||
(engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
|
|
||||||
|
|
||||||
static NUMBER_GUTTER: Em = Em::new(0.5);
|
static NUMBER_GUTTER: Em = Em::new(0.5);
|
||||||
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
|
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
|
||||||
@ -619,7 +618,7 @@ fn layout_box(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let frame = (ctx.engine.routines.layout_box)(
|
let frame = crate::inline::layout_box(
|
||||||
elem,
|
elem,
|
||||||
ctx.engine,
|
ctx.engine,
|
||||||
ctx.locator.next(&elem.span()),
|
ctx.locator.next(&elem.span()),
|
||||||
@ -692,7 +691,7 @@ fn layout_external(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<Frame> {
|
) -> SourceResult<Frame> {
|
||||||
(ctx.engine.routines.layout_frame)(
|
crate::layout_frame(
|
||||||
ctx.engine,
|
ctx.engine,
|
||||||
content,
|
content,
|
||||||
ctx.locator.next(&content.span()),
|
ctx.locator.next(&content.span()),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use std::f64::consts::SQRT_2;
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
use ecow::{eco_vec, EcoString};
|
use ecow::EcoString;
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::foundations::{Packed, StyleChain, StyleVec, SymbolElem};
|
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||||
use typst_library::layout::{Abs, Size};
|
use typst_library::layout::{Abs, Size};
|
||||||
use typst_library::math::{EquationElem, MathSize, MathVariant};
|
use typst_library::math::{EquationElem, MathSize, MathVariant};
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
@ -100,14 +100,15 @@ fn layout_inline_text(
|
|||||||
// because it will be placed somewhere probably not at the left margin
|
// because it will be placed somewhere probably not at the left margin
|
||||||
// it will overflow. So emulate an `hbox` instead and allow the
|
// it will overflow. So emulate an `hbox` instead and allow the
|
||||||
// paragraph to extend as far as needed.
|
// paragraph to extend as far as needed.
|
||||||
let frame = (ctx.engine.routines.layout_inline)(
|
let frame = crate::inline::layout_inline(
|
||||||
ctx.engine,
|
ctx.engine,
|
||||||
&StyleVec::wrap(eco_vec![elem]),
|
&[(&elem, styles)],
|
||||||
ctx.locator.next(&span),
|
&mut ctx.locator.next(&span).split(),
|
||||||
styles,
|
styles,
|
||||||
false,
|
|
||||||
Size::splat(Abs::inf()),
|
Size::splat(Abs::inf()),
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
)?
|
)?
|
||||||
.into_frame();
|
.into_frame();
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ pub enum Item<'a> {
|
|||||||
/// things like tags and weak pagebreaks.
|
/// things like tags and weak pagebreaks.
|
||||||
pub fn collect<'a>(
|
pub fn collect<'a>(
|
||||||
mut children: &'a mut [Pair<'a>],
|
mut children: &'a mut [Pair<'a>],
|
||||||
mut locator: SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
mut initial: StyleChain<'a>,
|
mut initial: StyleChain<'a>,
|
||||||
) -> Vec<Item<'a>> {
|
) -> Vec<Item<'a>> {
|
||||||
// The collected page-level items.
|
// The collected page-level items.
|
||||||
|
@ -83,7 +83,7 @@ fn layout_document_impl(
|
|||||||
styles,
|
styles,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
|
let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
|
||||||
let introspector = Introspector::paged(&pages);
|
let introspector = Introspector::paged(&pages);
|
||||||
|
|
||||||
Ok(PagedDocument { pages, info, introspector })
|
Ok(PagedDocument { pages, info, introspector })
|
||||||
@ -93,7 +93,7 @@ fn layout_document_impl(
|
|||||||
fn layout_pages<'a>(
|
fn layout_pages<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &'a mut [Pair<'a>],
|
children: &'a mut [Pair<'a>],
|
||||||
locator: SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
) -> SourceResult<Vec<Page>> {
|
) -> SourceResult<Vec<Page>> {
|
||||||
// Slice up the children into logical parts.
|
// Slice up the children into logical parts.
|
||||||
|
@ -19,7 +19,7 @@ use typst_library::visualize::Paint;
|
|||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use crate::flow::layout_flow;
|
use crate::flow::{layout_flow, FlowMode};
|
||||||
|
|
||||||
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
||||||
/// page number to be finalized into a `Page`. (Because the margins can depend
|
/// page number to be finalized into a `Page`. (Because the margins can depend
|
||||||
@ -181,7 +181,7 @@ fn layout_page_run_impl(
|
|||||||
Regions::repeat(area, area.map(Abs::is_finite)),
|
Regions::repeat(area, area.map(Abs::is_finite)),
|
||||||
PageElem::columns_in(styles),
|
PageElem::columns_in(styles),
|
||||||
ColumnsElem::gutter_in(styles),
|
ColumnsElem::gutter_in(styles),
|
||||||
true,
|
FlowMode::Root,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Layouts a single marginal.
|
// Layouts a single marginal.
|
||||||
|
@ -776,107 +776,6 @@ impl<'a> Iterator for Links<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sequence of elements with associated styles.
|
|
||||||
#[derive(Clone, PartialEq, Hash)]
|
|
||||||
pub struct StyleVec {
|
|
||||||
/// The elements themselves.
|
|
||||||
elements: EcoVec<Content>,
|
|
||||||
/// A run-length encoded list of style lists.
|
|
||||||
///
|
|
||||||
/// Each element is a (styles, count) pair. Any elements whose
|
|
||||||
/// style falls after the end of this list is considered to
|
|
||||||
/// have an empty style list.
|
|
||||||
styles: EcoVec<(Styles, usize)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StyleVec {
|
|
||||||
/// Create a style vector from an unstyled vector content.
|
|
||||||
pub fn wrap(elements: EcoVec<Content>) -> Self {
|
|
||||||
Self { elements, styles: EcoVec::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a `StyleVec` from a list of content with style chains.
|
|
||||||
pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) {
|
|
||||||
let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default();
|
|
||||||
let depth = trunk.links().count();
|
|
||||||
|
|
||||||
let mut elements = EcoVec::with_capacity(buf.len());
|
|
||||||
let mut styles = EcoVec::<(Styles, usize)>::new();
|
|
||||||
let mut last: Option<(StyleChain<'a>, usize)> = None;
|
|
||||||
|
|
||||||
for &(element, chain) in buf {
|
|
||||||
elements.push(element.clone());
|
|
||||||
|
|
||||||
if let Some((prev, run)) = &mut last {
|
|
||||||
if chain == *prev {
|
|
||||||
*run += 1;
|
|
||||||
} else {
|
|
||||||
styles.push((prev.suffix(depth), *run));
|
|
||||||
last = Some((chain, 1));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
last = Some((chain, 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((last, run)) = last {
|
|
||||||
let skippable = styles.is_empty() && last == trunk;
|
|
||||||
if !skippable {
|
|
||||||
styles.push((last.suffix(depth), run));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(StyleVec { elements, styles }, trunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether there are no elements.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.elements.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of elements.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.elements.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterate over the contained content and style chains.
|
|
||||||
pub fn iter<'a>(
|
|
||||||
&'a self,
|
|
||||||
outer: &'a StyleChain<'_>,
|
|
||||||
) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> {
|
|
||||||
static EMPTY: Styles = Styles::new();
|
|
||||||
self.elements
|
|
||||||
.iter()
|
|
||||||
.zip(
|
|
||||||
self.styles
|
|
||||||
.iter()
|
|
||||||
.flat_map(|(local, count)| std::iter::repeat(local).take(*count))
|
|
||||||
.chain(std::iter::repeat(&EMPTY)),
|
|
||||||
)
|
|
||||||
.map(|(element, local)| (element, outer.chain(local)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a style property, but only if it is the same for all children of the
|
|
||||||
/// style vector.
|
|
||||||
pub fn shared_get<T: PartialEq>(
|
|
||||||
&self,
|
|
||||||
styles: StyleChain<'_>,
|
|
||||||
getter: fn(StyleChain) -> T,
|
|
||||||
) -> Option<T> {
|
|
||||||
let value = getter(styles);
|
|
||||||
self.styles
|
|
||||||
.iter()
|
|
||||||
.all(|(local, _)| getter(styles.chain(local)) == value)
|
|
||||||
.then_some(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for StyleVec {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
|
||||||
f.debug_list().entries(&self.elements).finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A property that is resolved with other properties from the style chain.
|
/// A property that is resolved with other properties from the style chain.
|
||||||
pub trait Resolve {
|
pub trait Resolve {
|
||||||
/// The type of the resolved output.
|
/// The type of the resolved output.
|
||||||
|
@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke};
|
|||||||
/// An inline-level container that sizes content.
|
/// An inline-level container that sizes content.
|
||||||
///
|
///
|
||||||
/// All elements except inline math, text, and boxes are block-level and cannot
|
/// All elements except inline math, text, and boxes are block-level and cannot
|
||||||
/// occur inside of a paragraph. The box function can be used to integrate such
|
/// occur inside of a [paragraph]($par). The box function can be used to
|
||||||
/// elements into a paragraph. Boxes take the size of their contents by default
|
/// integrate such elements into a paragraph. Boxes take the size of their
|
||||||
/// but can also be sized explicitly.
|
/// contents by default but can also be sized explicitly.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```example
|
/// ```example
|
||||||
@ -184,6 +184,10 @@ pub enum InlineItem {
|
|||||||
/// Such a container can be used to separate content, size it, and give it a
|
/// Such a container can be used to separate content, size it, and give it a
|
||||||
/// background or border.
|
/// background or border.
|
||||||
///
|
///
|
||||||
|
/// Blocks are also the primary way to control whether text becomes part of a
|
||||||
|
/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph)
|
||||||
|
/// for more details.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// With a block, you can give a background to content while still allowing it
|
/// With a block, you can give a background to content while still allowing it
|
||||||
/// to break across multiple pages.
|
/// to break across multiple pages.
|
||||||
|
@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
|
|||||||
|
|
||||||
/// A mathematical equation.
|
/// A mathematical equation.
|
||||||
///
|
///
|
||||||
/// Can be displayed inline with text or as a separate block.
|
/// Can be displayed inline with text or as a separate block. An equation
|
||||||
|
/// becomes block-level through the presence of at least one space after the
|
||||||
|
/// opening dollar sign and one space before the closing dollar sign.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```example
|
/// ```example
|
||||||
|
@ -17,7 +17,7 @@ use hayagriva::{
|
|||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
use typst_utils::{ManuallyHash, NonZeroExt, PicoStr};
|
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
|
||||||
|
|
||||||
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
|
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
@ -29,7 +29,7 @@ use crate::foundations::{
|
|||||||
use crate::introspection::{Introspector, Locatable, Location};
|
use crate::introspection::{Introspector, Locatable, Location};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
|
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
|
||||||
Sizing, TrackSizings, VElem,
|
Sides, Sizing, TrackSizings,
|
||||||
};
|
};
|
||||||
use crate::loading::{DataSource, Load};
|
use crate::loading::{DataSource, Load};
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
@ -206,19 +206,20 @@ impl Show for Packed<BibliographyElem> {
|
|||||||
const COLUMN_GUTTER: Em = Em::new(0.65);
|
const COLUMN_GUTTER: Em = Em::new(0.65);
|
||||||
const INDENT: Em = Em::new(1.5);
|
const INDENT: Em = Em::new(1.5);
|
||||||
|
|
||||||
|
let span = self.span();
|
||||||
|
|
||||||
let mut seq = vec![];
|
let mut seq = vec![];
|
||||||
if let Some(title) = self.title(styles).unwrap_or_else(|| {
|
if let Some(title) = self.title(styles).unwrap_or_else(|| {
|
||||||
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
|
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
|
||||||
}) {
|
}) {
|
||||||
seq.push(
|
seq.push(
|
||||||
HeadingElem::new(title)
|
HeadingElem::new(title)
|
||||||
.with_depth(NonZeroUsize::ONE)
|
.with_depth(NonZeroUsize::ONE)
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()),
|
.spanned(span),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let span = self.span();
|
|
||||||
let works = Works::generate(engine).at(span)?;
|
let works = Works::generate(engine).at(span)?;
|
||||||
let references = works
|
let references = works
|
||||||
.references
|
.references
|
||||||
@ -226,10 +227,9 @@ impl Show for Packed<BibliographyElem> {
|
|||||||
.ok_or("CSL style is not suitable for bibliographies")
|
.ok_or("CSL style is not suitable for bibliographies")
|
||||||
.at(span)?;
|
.at(span)?;
|
||||||
|
|
||||||
let row_gutter = ParElem::spacing_in(styles);
|
|
||||||
let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack();
|
|
||||||
|
|
||||||
if references.iter().any(|(prefix, _)| prefix.is_some()) {
|
if references.iter().any(|(prefix, _)| prefix.is_some()) {
|
||||||
|
let row_gutter = ParElem::spacing_in(styles);
|
||||||
|
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
for (prefix, reference) in references {
|
for (prefix, reference) in references {
|
||||||
cells.push(GridChild::Item(GridItem::Cell(
|
cells.push(GridChild::Item(GridItem::Cell(
|
||||||
@ -246,23 +246,27 @@ impl Show for Packed<BibliographyElem> {
|
|||||||
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
|
||||||
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
|
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()),
|
.spanned(span),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
for (i, (_, reference)) in references.iter().enumerate() {
|
for (_, reference) in references {
|
||||||
if i > 0 {
|
let realized = reference.clone();
|
||||||
seq.push(row_gutter_elem.clone());
|
let block = if works.hanging_indent {
|
||||||
}
|
let body = HElem::new((-INDENT).into()).pack() + realized;
|
||||||
seq.push(reference.clone());
|
let inset = Sides::default()
|
||||||
|
.with(TextElem::dir_in(styles).start(), Some(INDENT.into()));
|
||||||
|
BlockElem::new()
|
||||||
|
.with_body(Some(BlockBody::Content(body)))
|
||||||
|
.with_inset(inset)
|
||||||
|
} else {
|
||||||
|
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
|
||||||
|
};
|
||||||
|
|
||||||
|
seq.push(block.pack().spanned(span));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut content = Content::sequence(seq);
|
Ok(Content::sequence(seq))
|
||||||
if works.hanging_indent {
|
|
||||||
content = content.styled(ParElem::set_hanging_indent(INDENT.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ use crate::foundations::{
|
|||||||
};
|
};
|
||||||
use crate::html::{attr, tag, HtmlElem};
|
use crate::html::{attr, tag, HtmlElem};
|
||||||
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
|
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
|
||||||
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem};
|
use crate::model::{
|
||||||
|
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
|
||||||
|
};
|
||||||
|
|
||||||
/// A numbered list.
|
/// A numbered list.
|
||||||
///
|
///
|
||||||
@ -226,6 +228,8 @@ impl EnumElem {
|
|||||||
|
|
||||||
impl Show for Packed<EnumElem> {
|
impl Show for Packed<EnumElem> {
|
||||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
|
let tight = self.tight(styles);
|
||||||
|
|
||||||
if TargetElem::target_in(styles).is_html() {
|
if TargetElem::target_in(styles).is_html() {
|
||||||
let mut elem = HtmlElem::new(tag::ol);
|
let mut elem = HtmlElem::new(tag::ol);
|
||||||
if self.reversed(styles) {
|
if self.reversed(styles) {
|
||||||
@ -239,7 +243,12 @@ impl Show for Packed<EnumElem> {
|
|||||||
if let Some(nr) = item.number(styles) {
|
if let Some(nr) = item.number(styles) {
|
||||||
li = li.with_attr(attr::value, eco_format!("{nr}"));
|
li = li.with_attr(attr::value, eco_format!("{nr}"));
|
||||||
}
|
}
|
||||||
li.with_body(Some(item.body.clone())).pack().spanned(item.span())
|
// Text in wide enums shall always turn into paragraphs.
|
||||||
|
let mut body = item.body.clone();
|
||||||
|
if !tight {
|
||||||
|
body += ParbreakElem::shared();
|
||||||
|
}
|
||||||
|
li.with_body(Some(body)).pack().spanned(item.span())
|
||||||
}));
|
}));
|
||||||
return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
|
return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
|
||||||
}
|
}
|
||||||
@ -249,7 +258,7 @@ impl Show for Packed<EnumElem> {
|
|||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span());
|
.spanned(self.span());
|
||||||
|
|
||||||
if self.tight(styles) {
|
if tight {
|
||||||
let leading = ParElem::leading_in(styles);
|
let leading = ParElem::leading_in(styles);
|
||||||
let spacing =
|
let spacing =
|
||||||
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
|
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
|
||||||
|
@ -19,7 +19,9 @@ use crate::layout::{
|
|||||||
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
|
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
|
||||||
PlaceElem, PlacementScope, VAlignment, VElem,
|
PlaceElem, PlacementScope, VAlignment, VElem,
|
||||||
};
|
};
|
||||||
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
|
use crate::model::{
|
||||||
|
Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement,
|
||||||
|
};
|
||||||
use crate::text::{Lang, Region, TextElem};
|
use crate::text::{Lang, Region, TextElem};
|
||||||
use crate::visualize::ImageElem;
|
use crate::visualize::ImageElem;
|
||||||
|
|
||||||
@ -328,6 +330,7 @@ impl Synthesize for Packed<FigureElem> {
|
|||||||
impl Show for Packed<FigureElem> {
|
impl Show for Packed<FigureElem> {
|
||||||
#[typst_macros::time(name = "figure", span = self.span())]
|
#[typst_macros::time(name = "figure", span = self.span())]
|
||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
|
let span = self.span();
|
||||||
let target = TargetElem::target_in(styles);
|
let target = TargetElem::target_in(styles);
|
||||||
let mut realized = self.body.clone();
|
let mut realized = self.body.clone();
|
||||||
|
|
||||||
@ -341,24 +344,27 @@ impl Show for Packed<FigureElem> {
|
|||||||
seq.push(first);
|
seq.push(first);
|
||||||
if !target.is_html() {
|
if !target.is_html() {
|
||||||
let v = VElem::new(self.gap(styles).into()).with_weak(true);
|
let v = VElem::new(self.gap(styles).into()).with_weak(true);
|
||||||
seq.push(v.pack().spanned(self.span()))
|
seq.push(v.pack().spanned(span))
|
||||||
}
|
}
|
||||||
seq.push(second);
|
seq.push(second);
|
||||||
realized = Content::sequence(seq)
|
realized = Content::sequence(seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that the body is considered a paragraph.
|
||||||
|
realized += ParbreakElem::shared().clone().spanned(span);
|
||||||
|
|
||||||
if target.is_html() {
|
if target.is_html() {
|
||||||
return Ok(HtmlElem::new(tag::figure)
|
return Ok(HtmlElem::new(tag::figure)
|
||||||
.with_body(Some(realized))
|
.with_body(Some(realized))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()));
|
.spanned(span));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the contents in a block.
|
// Wrap the contents in a block.
|
||||||
realized = BlockElem::new()
|
realized = BlockElem::new()
|
||||||
.with_body(Some(BlockBody::Content(realized)))
|
.with_body(Some(BlockBody::Content(realized)))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span());
|
.spanned(span);
|
||||||
|
|
||||||
// Wrap in a float.
|
// Wrap in a float.
|
||||||
if let Some(align) = self.placement(styles) {
|
if let Some(align) = self.placement(styles) {
|
||||||
@ -367,10 +373,10 @@ impl Show for Packed<FigureElem> {
|
|||||||
.with_scope(self.scope(styles))
|
.with_scope(self.scope(styles))
|
||||||
.with_float(true)
|
.with_float(true)
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span());
|
.spanned(span);
|
||||||
} else if self.scope(styles) == PlacementScope::Parent {
|
} else if self.scope(styles) == PlacementScope::Parent {
|
||||||
bail!(
|
bail!(
|
||||||
self.span(),
|
span,
|
||||||
"parent-scoped placement is only available for floating figures";
|
"parent-scoped placement is only available for floating figures";
|
||||||
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
|
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
|
||||||
);
|
);
|
||||||
@ -604,14 +610,17 @@ impl Show for Packed<FigureCaption> {
|
|||||||
realized = supplement + numbers + self.get_separator(styles) + realized;
|
realized = supplement + numbers + self.get_separator(styles) + realized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if TargetElem::target_in(styles).is_html() {
|
Ok(if TargetElem::target_in(styles).is_html() {
|
||||||
return Ok(HtmlElem::new(tag::figcaption)
|
HtmlElem::new(tag::figcaption)
|
||||||
.with_body(Some(realized))
|
.with_body(Some(realized))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()));
|
.spanned(self.span())
|
||||||
}
|
} else {
|
||||||
|
BlockElem::new()
|
||||||
Ok(realized)
|
.with_body(Some(BlockBody::Content(realized)))
|
||||||
|
.pack()
|
||||||
|
.spanned(self.span())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,11 +310,9 @@ impl Show for Packed<FootnoteEntry> {
|
|||||||
|
|
||||||
impl ShowSet for Packed<FootnoteEntry> {
|
impl ShowSet for Packed<FootnoteEntry> {
|
||||||
fn show_set(&self, _: StyleChain) -> Styles {
|
fn show_set(&self, _: StyleChain) -> Styles {
|
||||||
let text_size = Em::new(0.85);
|
|
||||||
let leading = Em::new(0.5);
|
|
||||||
let mut out = Styles::new();
|
let mut out = Styles::new();
|
||||||
out.set(ParElem::set_leading(leading.into()));
|
out.set(ParElem::set_leading(Em::new(0.5).into()));
|
||||||
out.set(TextElem::set_size(TextSize(text_size.into())));
|
out.set(TextElem::set_size(TextSize(Em::new(0.85).into())));
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ use crate::foundations::{
|
|||||||
};
|
};
|
||||||
use crate::html::{tag, HtmlElem};
|
use crate::html::{tag, HtmlElem};
|
||||||
use crate::layout::{BlockElem, Em, Length, VElem};
|
use crate::layout::{BlockElem, Em, Length, VElem};
|
||||||
use crate::model::ParElem;
|
use crate::model::{ParElem, ParbreakElem};
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
|
|
||||||
/// A bullet list.
|
/// A bullet list.
|
||||||
@ -141,11 +141,18 @@ impl ListElem {
|
|||||||
|
|
||||||
impl Show for Packed<ListElem> {
|
impl Show for Packed<ListElem> {
|
||||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
|
let tight = self.tight(styles);
|
||||||
|
|
||||||
if TargetElem::target_in(styles).is_html() {
|
if TargetElem::target_in(styles).is_html() {
|
||||||
return Ok(HtmlElem::new(tag::ul)
|
return Ok(HtmlElem::new(tag::ul)
|
||||||
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
|
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
|
||||||
|
// Text in wide lists shall always turn into paragraphs.
|
||||||
|
let mut body = item.body.clone();
|
||||||
|
if !tight {
|
||||||
|
body += ParbreakElem::shared();
|
||||||
|
}
|
||||||
HtmlElem::new(tag::li)
|
HtmlElem::new(tag::li)
|
||||||
.with_body(Some(item.body.clone()))
|
.with_body(Some(body))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(item.span())
|
.spanned(item.span())
|
||||||
}))))
|
}))))
|
||||||
@ -158,7 +165,7 @@ impl Show for Packed<ListElem> {
|
|||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span());
|
.spanned(self.span());
|
||||||
|
|
||||||
if self.tight(styles) {
|
if tight {
|
||||||
let leading = ParElem::leading_in(styles);
|
let leading = ParElem::leading_in(styles);
|
||||||
let spacing =
|
let spacing =
|
||||||
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
|
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
|
||||||
|
@ -297,7 +297,6 @@ impl ShowSet for Packed<OutlineElem> {
|
|||||||
let mut out = Styles::new();
|
let mut out = Styles::new();
|
||||||
out.set(HeadingElem::set_outlined(false));
|
out.set(HeadingElem::set_outlined(false));
|
||||||
out.set(HeadingElem::set_numbering(None));
|
out.set(HeadingElem::set_numbering(None));
|
||||||
out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
|
|
||||||
out.set(ParElem::set_justify(false));
|
out.set(ParElem::set_justify(false));
|
||||||
out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
|
out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
|
||||||
// Makes the outline itself available to its entries. Should be
|
// Makes the outline itself available to its entries. Should be
|
||||||
|
@ -1,22 +1,78 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
|
||||||
|
|
||||||
use typst_utils::singleton;
|
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, Set, Smart,
|
elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Smart,
|
||||||
StyleVec, Unlabellable,
|
Unlabellable,
|
||||||
};
|
};
|
||||||
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};
|
||||||
use crate::model::Numbering;
|
use crate::model::Numbering;
|
||||||
|
|
||||||
/// Arranges text, spacing and inline-level elements into a paragraph.
|
/// A logical subdivison of textual content.
|
||||||
///
|
///
|
||||||
/// Although this function is primarily used in set rules to affect paragraph
|
/// Typst automatically collects _inline-level_ elements into paragraphs.
|
||||||
/// properties, it can also be used to explicitly render its argument onto a
|
/// Inline-level elements include [text], [horizontal spacing]($h),
|
||||||
/// paragraph of its own.
|
/// [boxes]($box), and [inline equations]($math.equation).
|
||||||
|
///
|
||||||
|
/// To separate paragraphs, use a blank line (or an explicit [`parbreak`]).
|
||||||
|
/// Paragraphs are also automatically interrupted by any block-level element
|
||||||
|
/// (like [`block`], [`place`], or anything that shows itself as one of these).
|
||||||
|
///
|
||||||
|
/// The `par` element is primarily used in set rules to affect paragraph
|
||||||
|
/// properties, but it can also be used to explicitly display its argument as a
|
||||||
|
/// paragraph of its own. Then, the paragraph's body may not contain any
|
||||||
|
/// block-level content.
|
||||||
|
///
|
||||||
|
/// # Boxes and blocks
|
||||||
|
/// As explained above, usually paragraphs only contain inline-level content.
|
||||||
|
/// However, you can integrate any kind of block-level content into a paragraph
|
||||||
|
/// by wrapping it in a [`box`].
|
||||||
|
///
|
||||||
|
/// Conversely, you can separate inline-level content from a paragraph by
|
||||||
|
/// wrapping it in a [`block`]. In this case, it will not become part of any
|
||||||
|
/// paragraph at all. Read the following section for an explanation of why that
|
||||||
|
/// matters and how it differs from just adding paragraph breaks around the
|
||||||
|
/// content.
|
||||||
|
///
|
||||||
|
/// # What becomes a paragraph?
|
||||||
|
/// When you add inline-level content to your document, Typst will automatically
|
||||||
|
/// wrap it in paragraphs. However, a typical document also contains some text
|
||||||
|
/// that is not semantically part of a paragraph, for example in a heading or
|
||||||
|
/// caption.
|
||||||
|
///
|
||||||
|
/// The rules for when Typst wraps inline-level content in a paragraph are as
|
||||||
|
/// follows:
|
||||||
|
///
|
||||||
|
/// - All text at the root of a document is wrapped in paragraphs.
|
||||||
|
///
|
||||||
|
/// - Text in a container (like a `block`) is only wrapped in a paragraph if the
|
||||||
|
/// container holds any block-level content. If all of the contents are
|
||||||
|
/// inline-level, no paragraph is created.
|
||||||
|
///
|
||||||
|
/// In the laid-out document, it's not immediately visible whether text became
|
||||||
|
/// part of a paragraph. However, it is still important for various reasons:
|
||||||
|
///
|
||||||
|
/// - Certain paragraph styling like `first-line-indent` will only apply to
|
||||||
|
/// proper paragraphs, not any text. Similarly, `par` show rules of course
|
||||||
|
/// only trigger on paragraphs.
|
||||||
|
///
|
||||||
|
/// - A proper distinction between paragraphs and other text helps people who
|
||||||
|
/// rely on assistive technologies (such as screen readers) navigate and
|
||||||
|
/// understand the document properly. Currently, this only applies to HTML
|
||||||
|
/// export since Typst does not yet output accessible PDFs, but support for
|
||||||
|
/// this is planned for the near future.
|
||||||
|
///
|
||||||
|
/// - HTML export will generate a `<p>` tag only for paragraphs.
|
||||||
|
///
|
||||||
|
/// When creating custom reusable components, you can and should take charge
|
||||||
|
/// over whether Typst creates paragraphs. By wrapping text in a [`block`]
|
||||||
|
/// instead of just adding paragraph breaks around it, you can force the absence
|
||||||
|
/// of a paragraph. Conversely, by adding a [`parbreak`] after some content in a
|
||||||
|
/// container, you can force it to become a paragraph even if it's just one
|
||||||
|
/// word. This is, for example, what [non-`tight`]($list.tight) lists do to
|
||||||
|
/// force their items to become paragraphs.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```example
|
/// ```example
|
||||||
@ -37,7 +93,7 @@ use crate::model::Numbering;
|
|||||||
/// let $a$ be the smallest of the
|
/// let $a$ be the smallest of the
|
||||||
/// three integers. Then, we ...
|
/// three integers. Then, we ...
|
||||||
/// ```
|
/// ```
|
||||||
#[elem(scope, title = "Paragraph", Debug, Construct)]
|
#[elem(scope, title = "Paragraph")]
|
||||||
pub struct ParElem {
|
pub struct ParElem {
|
||||||
/// The spacing between lines.
|
/// The spacing between lines.
|
||||||
///
|
///
|
||||||
@ -53,7 +109,6 @@ pub struct ParElem {
|
|||||||
/// distribution of the top- and bottom-edge values affects the bounds of
|
/// distribution of the top- and bottom-edge values affects the bounds of
|
||||||
/// the first and last line.
|
/// the first and last line.
|
||||||
#[resolve]
|
#[resolve]
|
||||||
#[ghost]
|
|
||||||
#[default(Em::new(0.65).into())]
|
#[default(Em::new(0.65).into())]
|
||||||
pub leading: Length,
|
pub leading: Length,
|
||||||
|
|
||||||
@ -68,7 +123,6 @@ pub struct ParElem {
|
|||||||
/// takes precedence over the paragraph spacing. Headings, for instance,
|
/// takes precedence over the paragraph spacing. Headings, for instance,
|
||||||
/// reduce the spacing below them by default for a better look.
|
/// reduce the spacing below them by default for a better look.
|
||||||
#[resolve]
|
#[resolve]
|
||||||
#[ghost]
|
|
||||||
#[default(Em::new(1.2).into())]
|
#[default(Em::new(1.2).into())]
|
||||||
pub spacing: Length,
|
pub spacing: Length,
|
||||||
|
|
||||||
@ -81,7 +135,6 @@ pub struct ParElem {
|
|||||||
/// Note that the current [alignment]($align.alignment) still has an effect
|
/// Note that the current [alignment]($align.alignment) still has an effect
|
||||||
/// on the placement of the last line except if it ends with a
|
/// on the placement of the last line except if it ends with a
|
||||||
/// [justified line break]($linebreak.justify).
|
/// [justified line break]($linebreak.justify).
|
||||||
#[ghost]
|
|
||||||
#[default(false)]
|
#[default(false)]
|
||||||
pub justify: bool,
|
pub justify: bool,
|
||||||
|
|
||||||
@ -106,7 +159,6 @@ pub struct ParElem {
|
|||||||
/// challenging to break in a visually
|
/// challenging to break in a visually
|
||||||
/// pleasing way.
|
/// pleasing way.
|
||||||
/// ```
|
/// ```
|
||||||
#[ghost]
|
|
||||||
pub linebreaks: Smart<Linebreaks>,
|
pub linebreaks: Smart<Linebreaks>,
|
||||||
|
|
||||||
/// The indent the first line of a paragraph should have.
|
/// The indent the first line of a paragraph should have.
|
||||||
@ -118,23 +170,15 @@ pub struct ParElem {
|
|||||||
/// space between paragraphs or by indented first lines. Consider reducing
|
/// space between paragraphs or by indented first lines. Consider reducing
|
||||||
/// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading)
|
/// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading)
|
||||||
/// when using this property (e.g. using `[#set par(spacing: 0.65em)]`).
|
/// when using this property (e.g. using `[#set par(spacing: 0.65em)]`).
|
||||||
#[ghost]
|
|
||||||
pub first_line_indent: Length,
|
pub first_line_indent: Length,
|
||||||
|
|
||||||
/// The indent all but the first line of a paragraph should have.
|
/// The indent that all but the first line of a paragraph should have.
|
||||||
#[ghost]
|
|
||||||
#[resolve]
|
#[resolve]
|
||||||
pub hanging_indent: Length,
|
pub hanging_indent: Length,
|
||||||
|
|
||||||
/// The contents of the paragraph.
|
/// The contents of the paragraph.
|
||||||
#[external]
|
|
||||||
#[required]
|
#[required]
|
||||||
pub body: Content,
|
pub body: Content,
|
||||||
|
|
||||||
/// The paragraph's children.
|
|
||||||
#[internal]
|
|
||||||
#[variadic]
|
|
||||||
pub children: StyleVec,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
@ -143,28 +187,6 @@ impl ParElem {
|
|||||||
type ParLine;
|
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
|
|
||||||
// element. Instead, it just ensures that the passed content lives in a
|
|
||||||
// separate paragraph and styles it.
|
|
||||||
let styles = Self::set(engine, args)?;
|
|
||||||
let body = args.expect::<Content>("body")?;
|
|
||||||
Ok(Content::sequence([
|
|
||||||
ParbreakElem::shared().clone(),
|
|
||||||
body.styled_with_map(styles),
|
|
||||||
ParbreakElem::shared().clone(),
|
|
||||||
]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for ParElem {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
write!(f, "Par ")?;
|
|
||||||
self.children.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How to determine line breaks in a paragraph.
|
/// How to determine line breaks in a paragraph.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum Linebreaks {
|
pub enum Linebreaks {
|
||||||
|
@ -212,17 +212,24 @@ impl Show for Packed<QuoteElem> {
|
|||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()),
|
.spanned(self.span()),
|
||||||
};
|
};
|
||||||
let attribution =
|
let attribution = Content::sequence([
|
||||||
[TextElem::packed('—'), SpaceElem::shared().clone(), attribution];
|
TextElem::packed('—'),
|
||||||
|
SpaceElem::shared().clone(),
|
||||||
|
attribution,
|
||||||
|
]);
|
||||||
|
|
||||||
if !html {
|
if html {
|
||||||
// Use v(0.9em, weak: true) to bring the attribution closer
|
realized += attribution;
|
||||||
// to the quote.
|
} else {
|
||||||
|
// Bring the attribution a bit closer to the quote.
|
||||||
let gap = Spacing::Rel(Em::new(0.9).into());
|
let gap = Spacing::Rel(Em::new(0.9).into());
|
||||||
let v = VElem::new(gap).with_weak(true).pack();
|
let v = VElem::new(gap).with_weak(true).pack();
|
||||||
realized += v;
|
realized += v;
|
||||||
|
realized += BlockElem::new()
|
||||||
|
.with_body(Some(BlockBody::Content(attribution)))
|
||||||
|
.pack()
|
||||||
|
.aligned(Alignment::END);
|
||||||
}
|
}
|
||||||
realized += Content::sequence(attribution).aligned(Alignment::END);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !html {
|
if !html {
|
||||||
|
@ -8,7 +8,7 @@ use crate::foundations::{
|
|||||||
};
|
};
|
||||||
use crate::html::{tag, HtmlElem};
|
use crate::html::{tag, HtmlElem};
|
||||||
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
|
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
|
||||||
use crate::model::{ListItemLike, ListLike, ParElem};
|
use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
|
|
||||||
/// A list of terms and their descriptions.
|
/// A list of terms and their descriptions.
|
||||||
@ -116,17 +116,25 @@ impl TermsElem {
|
|||||||
impl Show for Packed<TermsElem> {
|
impl Show for Packed<TermsElem> {
|
||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
let span = self.span();
|
let span = self.span();
|
||||||
|
let tight = self.tight(styles);
|
||||||
|
|
||||||
if TargetElem::target_in(styles).is_html() {
|
if TargetElem::target_in(styles).is_html() {
|
||||||
return Ok(HtmlElem::new(tag::dl)
|
return Ok(HtmlElem::new(tag::dl)
|
||||||
.with_body(Some(Content::sequence(self.children.iter().flat_map(
|
.with_body(Some(Content::sequence(self.children.iter().flat_map(
|
||||||
|item| {
|
|item| {
|
||||||
|
// Text in wide term lists shall always turn into paragraphs.
|
||||||
|
let mut description = item.description.clone();
|
||||||
|
if !tight {
|
||||||
|
description += ParbreakElem::shared();
|
||||||
|
}
|
||||||
|
|
||||||
[
|
[
|
||||||
HtmlElem::new(tag::dt)
|
HtmlElem::new(tag::dt)
|
||||||
.with_body(Some(item.term.clone()))
|
.with_body(Some(item.term.clone()))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(item.term.span()),
|
.spanned(item.term.span()),
|
||||||
HtmlElem::new(tag::dd)
|
HtmlElem::new(tag::dd)
|
||||||
.with_body(Some(item.description.clone()))
|
.with_body(Some(description))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(item.description.span()),
|
.spanned(item.description.span()),
|
||||||
]
|
]
|
||||||
@ -139,7 +147,7 @@ impl Show for Packed<TermsElem> {
|
|||||||
let indent = self.indent(styles);
|
let indent = self.indent(styles);
|
||||||
let hanging_indent = self.hanging_indent(styles);
|
let hanging_indent = self.hanging_indent(styles);
|
||||||
let gutter = self.spacing(styles).unwrap_or_else(|| {
|
let gutter = self.spacing(styles).unwrap_or_else(|| {
|
||||||
if self.tight(styles) {
|
if tight {
|
||||||
ParElem::leading_in(styles).into()
|
ParElem::leading_in(styles).into()
|
||||||
} else {
|
} else {
|
||||||
ParElem::spacing_in(styles).into()
|
ParElem::spacing_in(styles).into()
|
||||||
@ -157,6 +165,12 @@ impl Show for Packed<TermsElem> {
|
|||||||
seq.push(child.term.clone().strong());
|
seq.push(child.term.clone().strong());
|
||||||
seq.push((*separator).clone());
|
seq.push((*separator).clone());
|
||||||
seq.push(child.description.clone());
|
seq.push(child.description.clone());
|
||||||
|
|
||||||
|
// Text in wide term lists shall always turn into paragraphs.
|
||||||
|
if !tight {
|
||||||
|
seq.push(ParbreakElem::shared().clone());
|
||||||
|
}
|
||||||
|
|
||||||
children.push(StackChild::Block(Content::sequence(seq)));
|
children.push(StackChild::Block(Content::sequence(seq)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +182,7 @@ impl Show for Packed<TermsElem> {
|
|||||||
.spanned(span)
|
.spanned(span)
|
||||||
.padded(padding);
|
.padded(padding);
|
||||||
|
|
||||||
if self.tight(styles) {
|
if tight {
|
||||||
let leading = ParElem::leading_in(styles);
|
let leading = ParElem::leading_in(styles);
|
||||||
let spacing = VElem::new(leading.into())
|
let spacing = VElem::new(leading.into())
|
||||||
.with_weak(true)
|
.with_weak(true)
|
||||||
|
@ -10,8 +10,7 @@ use typst_utils::LazyHash;
|
|||||||
use crate::diag::SourceResult;
|
use crate::diag::SourceResult;
|
||||||
use crate::engine::{Engine, Route, Sink, Traced};
|
use crate::engine::{Engine, Route, Sink, Traced};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, StyleVec,
|
Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value,
|
||||||
Styles, Value,
|
|
||||||
};
|
};
|
||||||
use crate::introspection::{Introspector, Locator, SplitLocator};
|
use crate::introspection::{Introspector, Locator, SplitLocator};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
@ -104,26 +103,6 @@ routines! {
|
|||||||
region: Region,
|
region: Region,
|
||||||
) -> SourceResult<Frame>
|
) -> SourceResult<Frame>
|
||||||
|
|
||||||
/// Lays out inline content.
|
|
||||||
fn layout_inline(
|
|
||||||
engine: &mut Engine,
|
|
||||||
children: &StyleVec,
|
|
||||||
locator: Locator,
|
|
||||||
styles: StyleChain,
|
|
||||||
consecutive: bool,
|
|
||||||
region: Size,
|
|
||||||
expand: bool,
|
|
||||||
) -> SourceResult<Fragment>
|
|
||||||
|
|
||||||
/// Lays out a [`BoxElem`].
|
|
||||||
fn layout_box(
|
|
||||||
elem: &Packed<BoxElem>,
|
|
||||||
engine: &mut Engine,
|
|
||||||
locator: Locator,
|
|
||||||
styles: StyleChain,
|
|
||||||
region: Size,
|
|
||||||
) -> SourceResult<Frame>
|
|
||||||
|
|
||||||
/// Lays out a [`ListElem`].
|
/// Lays out a [`ListElem`].
|
||||||
fn layout_list(
|
fn layout_list(
|
||||||
elem: &Packed<ListElem>,
|
elem: &Packed<ListElem>,
|
||||||
@ -348,17 +327,62 @@ pub enum RealizationKind<'a> {
|
|||||||
/// This the root realization for layout. Requires a mutable reference
|
/// This the root realization for layout. Requires a mutable reference
|
||||||
/// to document metadata that will be filled from `set document` rules.
|
/// to document metadata that will be filled from `set document` rules.
|
||||||
LayoutDocument(&'a mut DocumentInfo),
|
LayoutDocument(&'a mut DocumentInfo),
|
||||||
/// A nested realization in a container (e.g. a `block`).
|
/// A nested realization in a container (e.g. a `block`). Requires a mutable
|
||||||
LayoutFragment,
|
/// reference to an enum that will be set to `FragmentKind::Inline` if the
|
||||||
|
/// fragment's content was fully inline.
|
||||||
|
LayoutFragment(&'a mut FragmentKind),
|
||||||
|
/// A nested realization in a paragraph (i.e. a `par`)
|
||||||
|
LayoutPar,
|
||||||
/// This the root realization for HTML. Requires a mutable reference
|
/// This the root realization for HTML. Requires a mutable reference
|
||||||
/// to document metadata that will be filled from `set document` rules.
|
/// to document metadata that will be filled from `set document` rules.
|
||||||
HtmlDocument(&'a mut DocumentInfo),
|
HtmlDocument(&'a mut DocumentInfo),
|
||||||
/// A nested realization in a container (e.g. a `block`).
|
/// A nested realization in a container (e.g. a `block`). Requires a mutable
|
||||||
HtmlFragment,
|
/// reference to an enum that will be set to `FragmentKind::Inline` if the
|
||||||
|
/// fragment's content was fully inline.
|
||||||
|
HtmlFragment(&'a mut FragmentKind),
|
||||||
/// A realization within math.
|
/// A realization within math.
|
||||||
Math,
|
Math,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RealizationKind<'_> {
|
||||||
|
/// It this a realization for HTML export?
|
||||||
|
pub fn is_html(&self) -> bool {
|
||||||
|
matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// It this a realization for a container?
|
||||||
|
pub fn is_fragment(&self) -> bool {
|
||||||
|
matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If this is a document-level realization, accesses the document info.
|
||||||
|
pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> {
|
||||||
|
match self {
|
||||||
|
Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If this is a container-level realization, accesses the fragment kind.
|
||||||
|
pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> {
|
||||||
|
match self {
|
||||||
|
Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of fragment output that realization produced.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum FragmentKind {
|
||||||
|
/// The fragment's contents were fully inline, and as a result, the output
|
||||||
|
/// elements are too.
|
||||||
|
Inline,
|
||||||
|
/// The fragment contained non-inline content, so inline content was forced
|
||||||
|
/// into paragraphs, and as a result, the output elements are not inline.
|
||||||
|
Block,
|
||||||
|
}
|
||||||
|
|
||||||
/// Temporary storage arenas for lifetime extension during realization.
|
/// Temporary storage arenas for lifetime extension during realization.
|
||||||
///
|
///
|
||||||
/// Must be kept live while the content returned from realization is processed.
|
/// Must be kept live while the content returned from realization is processed.
|
||||||
|
@ -15,8 +15,8 @@ use typst_library::diag::{bail, At, SourceResult};
|
|||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector,
|
Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector,
|
||||||
SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles,
|
SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem,
|
||||||
SymbolElem, Synthesize, Transformation,
|
Synthesize, Transformation,
|
||||||
};
|
};
|
||||||
use typst_library::html::{tag, HtmlElem};
|
use typst_library::html::{tag, HtmlElem};
|
||||||
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
|
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
|
||||||
@ -28,7 +28,7 @@ use typst_library::model::{
|
|||||||
CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike,
|
CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike,
|
||||||
ParElem, ParbreakElem, TermsElem,
|
ParElem, ParbreakElem, TermsElem,
|
||||||
};
|
};
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind};
|
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
|
||||||
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::{SliceExt, SmallBitSet};
|
use typst_utils::{SliceExt, SmallBitSet};
|
||||||
@ -48,17 +48,18 @@ pub fn realize<'a>(
|
|||||||
locator,
|
locator,
|
||||||
arenas,
|
arenas,
|
||||||
rules: match kind {
|
rules: match kind {
|
||||||
RealizationKind::LayoutDocument(_) | RealizationKind::LayoutFragment => {
|
RealizationKind::LayoutDocument(_) => LAYOUT_RULES,
|
||||||
LAYOUT_RULES
|
RealizationKind::LayoutFragment(_) => LAYOUT_RULES,
|
||||||
}
|
RealizationKind::LayoutPar => LAYOUT_PAR_RULES,
|
||||||
RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES,
|
RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES,
|
||||||
RealizationKind::HtmlFragment => HTML_FRAGMENT_RULES,
|
RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES,
|
||||||
RealizationKind::Math => MATH_RULES,
|
RealizationKind::Math => MATH_RULES,
|
||||||
},
|
},
|
||||||
sink: vec![],
|
sink: vec![],
|
||||||
groupings: ArrayVec::new(),
|
groupings: ArrayVec::new(),
|
||||||
outside: matches!(kind, RealizationKind::LayoutDocument(_)),
|
outside: matches!(kind, RealizationKind::LayoutDocument(_)),
|
||||||
may_attach: false,
|
may_attach: false,
|
||||||
|
saw_parbreak: false,
|
||||||
kind,
|
kind,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,6 +99,8 @@ struct State<'a, 'x, 'y, 'z> {
|
|||||||
outside: bool,
|
outside: bool,
|
||||||
/// Whether now following attach spacing can survive.
|
/// Whether now following attach spacing can survive.
|
||||||
may_attach: bool,
|
may_attach: bool,
|
||||||
|
/// Whether we visited any paragraph breaks.
|
||||||
|
saw_parbreak: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defines a rule for how certain elements shall be grouped during realization.
|
/// Defines a rule for how certain elements shall be grouped during realization.
|
||||||
@ -125,6 +128,10 @@ struct GroupingRule {
|
|||||||
struct Grouping<'a> {
|
struct Grouping<'a> {
|
||||||
/// The position in `s.sink` where the group starts.
|
/// The position in `s.sink` where the group starts.
|
||||||
start: usize,
|
start: usize,
|
||||||
|
/// Only applies to `PAR` grouping: Whether this paragraph group is
|
||||||
|
/// interrupted, but not yet finished because it may be ignored due to being
|
||||||
|
/// fully inline.
|
||||||
|
interrupted: bool,
|
||||||
/// The rule used for this grouping.
|
/// The rule used for this grouping.
|
||||||
rule: &'a GroupingRule,
|
rule: &'a GroupingRule,
|
||||||
}
|
}
|
||||||
@ -575,19 +582,21 @@ fn visit_styled<'a>(
|
|||||||
for style in local.iter() {
|
for style in local.iter() {
|
||||||
let Some(elem) = style.element() else { continue };
|
let Some(elem) = style.element() else { continue };
|
||||||
if elem == DocumentElem::elem() {
|
if elem == DocumentElem::elem() {
|
||||||
match &mut s.kind {
|
if let Some(info) = s.kind.as_document_mut() {
|
||||||
RealizationKind::LayoutDocument(info)
|
info.populate(&local)
|
||||||
| RealizationKind::HtmlDocument(info) => info.populate(&local),
|
} else {
|
||||||
_ => bail!(
|
bail!(
|
||||||
style.span(),
|
style.span(),
|
||||||
"document set rules are not allowed inside of containers"
|
"document set rules are not allowed inside of containers"
|
||||||
),
|
);
|
||||||
}
|
}
|
||||||
} else if elem == PageElem::elem() {
|
} else if elem == PageElem::elem() {
|
||||||
let RealizationKind::LayoutDocument(_) = s.kind else {
|
if !matches!(s.kind, RealizationKind::LayoutDocument(_)) {
|
||||||
let span = style.span();
|
bail!(
|
||||||
bail!(span, "page configuration is not allowed inside of containers");
|
style.span(),
|
||||||
};
|
"page configuration is not allowed inside of containers"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// When there are page styles, we "break free" from our show rule cage.
|
// When there are page styles, we "break free" from our show rule cage.
|
||||||
pagebreak = true;
|
pagebreak = true;
|
||||||
@ -650,7 +659,9 @@ fn visit_grouping_rules<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the element can be added to the active grouping, do it.
|
// If the element can be added to the active grouping, do it.
|
||||||
if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) {
|
if !active.interrupted
|
||||||
|
&& ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content))
|
||||||
|
{
|
||||||
s.sink.push((content, styles));
|
s.sink.push((content, styles));
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@ -661,7 +672,7 @@ fn visit_grouping_rules<'a>(
|
|||||||
// Start a new grouping.
|
// Start a new grouping.
|
||||||
if let Some(rule) = matching {
|
if let Some(rule) = matching {
|
||||||
let start = s.sink.len();
|
let start = s.sink.len();
|
||||||
s.groupings.push(Grouping { start, rule });
|
s.groupings.push(Grouping { start, rule, interrupted: false });
|
||||||
s.sink.push((content, styles));
|
s.sink.push((content, styles));
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@ -676,22 +687,24 @@ fn visit_filter_rules<'a>(
|
|||||||
content: &'a Content,
|
content: &'a Content,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
) -> SourceResult<bool> {
|
) -> SourceResult<bool> {
|
||||||
if content.is::<SpaceElem>()
|
if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) {
|
||||||
&& !matches!(s.kind, RealizationKind::Math | RealizationKind::HtmlFragment)
|
return Ok(false);
|
||||||
{
|
}
|
||||||
// Outside of maths, spaces that were not collected by the paragraph
|
|
||||||
// grouper don't interest us.
|
if content.is::<SpaceElem>() {
|
||||||
|
// Outside of maths and paragraph realization, spaces that were not
|
||||||
|
// collected by the paragraph grouper don't interest us.
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
} else if content.is::<ParbreakElem>() {
|
} else if content.is::<ParbreakElem>() {
|
||||||
// Paragraph breaks are only a boundary for paragraph grouping, we don't
|
// Paragraph breaks are only a boundary for paragraph grouping, we don't
|
||||||
// need to store them.
|
// need to store them.
|
||||||
s.may_attach = false;
|
s.may_attach = false;
|
||||||
|
s.saw_parbreak = true;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
} else if !s.may_attach
|
} else if !s.may_attach
|
||||||
&& content.to_packed::<VElem>().is_some_and(|elem| elem.attach(styles))
|
&& content.to_packed::<VElem>().is_some_and(|elem| elem.attach(styles))
|
||||||
{
|
{
|
||||||
// Delete attach spacing collapses if not immediately following a
|
// Attach spacing collapses if not immediately following a paragraph.
|
||||||
// paragraph.
|
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -703,7 +716,18 @@ fn visit_filter_rules<'a>(
|
|||||||
|
|
||||||
/// Finishes all grouping.
|
/// Finishes all grouping.
|
||||||
fn finish(s: &mut State) -> SourceResult<()> {
|
fn finish(s: &mut State) -> SourceResult<()> {
|
||||||
finish_grouping_while(s, |s| !s.groupings.is_empty())?;
|
finish_grouping_while(s, |s| {
|
||||||
|
// If this is a fragment realization and all we've got is inline
|
||||||
|
// content, don't turn it into a paragraph.
|
||||||
|
if is_fully_inline(s) {
|
||||||
|
*s.kind.as_fragment_mut().unwrap() = FragmentKind::Inline;
|
||||||
|
s.groupings.pop();
|
||||||
|
collapse_spaces(&mut s.sink, 0);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
!s.groupings.is_empty()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
// In math, spaces are top-level.
|
// In math, spaces are top-level.
|
||||||
if let RealizationKind::Math = s.kind {
|
if let RealizationKind::Math = s.kind {
|
||||||
@ -722,6 +746,12 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> {
|
|||||||
}
|
}
|
||||||
finish_grouping_while(s, |s| {
|
finish_grouping_while(s, |s| {
|
||||||
s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem))
|
s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem))
|
||||||
|
&& if is_fully_inline(s) {
|
||||||
|
s.groupings[0].interrupted = true;
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
last = Some(elem);
|
last = Some(elem);
|
||||||
}
|
}
|
||||||
@ -729,9 +759,9 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Finishes groupings while `f` returns `true`.
|
/// Finishes groupings while `f` returns `true`.
|
||||||
fn finish_grouping_while<F>(s: &mut State, f: F) -> SourceResult<()>
|
fn finish_grouping_while<F>(s: &mut State, mut f: F) -> SourceResult<()>
|
||||||
where
|
where
|
||||||
F: Fn(&State) -> bool,
|
F: FnMut(&mut State) -> bool,
|
||||||
{
|
{
|
||||||
// Finishing of a group may result in new content and new grouping. This
|
// Finishing of a group may result in new content and new grouping. This
|
||||||
// can, in theory, go on for a bit. To prevent it from becoming an infinite
|
// can, in theory, go on for a bit. To prevent it from becoming an infinite
|
||||||
@ -750,7 +780,7 @@ where
|
|||||||
/// Finishes the currently innermost grouping.
|
/// Finishes the currently innermost grouping.
|
||||||
fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
|
fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
|
||||||
// The grouping we are interrupting.
|
// The grouping we are interrupting.
|
||||||
let Grouping { start, rule } = s.groupings.pop().unwrap();
|
let Grouping { start, rule, .. } = s.groupings.pop().unwrap();
|
||||||
|
|
||||||
// Trim trailing non-trigger elements.
|
// Trim trailing non-trigger elements.
|
||||||
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind));
|
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind));
|
||||||
@ -794,12 +824,16 @@ const MAX_GROUP_NESTING: usize = 3;
|
|||||||
/// Grouping rules used in layout realization.
|
/// Grouping rules used in layout realization.
|
||||||
static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
|
/// Grouping rules used in paragraph layout realization.
|
||||||
|
static LAYOUT_PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
/// Grouping rules used in HTML root realization.
|
/// Grouping rules used in HTML root realization.
|
||||||
static HTML_DOCUMENT_RULES: &[&GroupingRule] =
|
static HTML_DOCUMENT_RULES: &[&GroupingRule] =
|
||||||
&[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
&[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
/// Grouping rules used in HTML fragment realization.
|
/// Grouping rules used in HTML fragment realization.
|
||||||
static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
|
static HTML_FRAGMENT_RULES: &[&GroupingRule] =
|
||||||
|
&[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
/// Grouping rules used in math realization.
|
/// Grouping rules used in math realization.
|
||||||
static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
|
static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
|
||||||
@ -836,12 +870,10 @@ static PAR: GroupingRule = GroupingRule {
|
|||||||
|| elem == SmartQuoteElem::elem()
|
|| elem == SmartQuoteElem::elem()
|
||||||
|| elem == InlineElem::elem()
|
|| elem == InlineElem::elem()
|
||||||
|| elem == BoxElem::elem()
|
|| elem == BoxElem::elem()
|
||||||
|| (matches!(
|
|| (kind.is_html()
|
||||||
kind,
|
&& content
|
||||||
RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment
|
.to_packed::<HtmlElem>()
|
||||||
) && content
|
.is_some_and(|elem| tag::is_inline_by_default(elem.tag)))
|
||||||
.to_packed::<HtmlElem>()
|
|
||||||
.is_some_and(|elem| tag::is_inline_by_default(elem.tag)))
|
|
||||||
},
|
},
|
||||||
inner: |content| content.elem() == SpaceElem::elem(),
|
inner: |content| content.elem() == SpaceElem::elem(),
|
||||||
interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(),
|
interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(),
|
||||||
@ -914,17 +946,31 @@ fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> {
|
|||||||
// transparently become part of it.
|
// transparently become part of it.
|
||||||
// 2. There is no group at all. In this case, we create one.
|
// 2. There is no group at all. In this case, we create one.
|
||||||
if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) {
|
if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) {
|
||||||
s.groupings.push(Grouping { start, rule: &PAR });
|
s.groupings.push(Grouping { start, rule: &PAR, interrupted: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether there is an active grouping, but it is not a `PAR` grouping.
|
/// Whether there is an active grouping, but it is not a `PAR` grouping.
|
||||||
fn in_non_par_grouping(s: &State) -> bool {
|
fn in_non_par_grouping(s: &mut State) -> bool {
|
||||||
s.groupings
|
s.groupings.last().is_some_and(|grouping| {
|
||||||
.last()
|
!std::ptr::eq(grouping.rule, &PAR) || grouping.interrupted
|
||||||
.is_some_and(|grouping| !std::ptr::eq(grouping.rule, &PAR))
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether there is exactly one active grouping, it is a `PAR` grouping, and it
|
||||||
|
/// spans the whole sink (with the exception of leading tags).
|
||||||
|
fn is_fully_inline(s: &State) -> bool {
|
||||||
|
s.kind.is_fragment()
|
||||||
|
&& !s.saw_parbreak
|
||||||
|
&& match s.groupings.as_slice() {
|
||||||
|
[grouping] => {
|
||||||
|
std::ptr::eq(grouping.rule, &PAR)
|
||||||
|
&& s.sink[..grouping.start].iter().all(|(c, _)| c.is::<TagElem>())
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the `ParElem` from inline-level elements.
|
/// Builds the `ParElem` from inline-level elements.
|
||||||
@ -936,11 +982,11 @@ fn finish_par(mut grouped: Grouped) -> SourceResult<()> {
|
|||||||
// Collect the children.
|
// Collect the children.
|
||||||
let elems = grouped.get();
|
let elems = grouped.get();
|
||||||
let span = select_span(elems);
|
let span = select_span(elems);
|
||||||
let (children, trunk) = StyleVec::create(elems);
|
let (body, trunk) = repack(elems);
|
||||||
|
|
||||||
// Create and visit the paragraph.
|
// Create and visit the paragraph.
|
||||||
let s = grouped.end();
|
let s = grouped.end();
|
||||||
let elem = ParElem::new(children).pack().spanned(span);
|
let elem = ParElem::new(body).pack().spanned(span);
|
||||||
visit(s, s.store(elem), trunk)
|
visit(s, s.store(elem), trunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1277,3 +1323,26 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) {
|
|||||||
fn select_span(children: &[Pair]) -> Span {
|
fn select_span(children: &[Pair]) -> Span {
|
||||||
Span::find(children.iter().map(|(c, _)| c.span()))
|
Span::find(children.iter().map(|(c, _)| c.span()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn realized content with styles back into owned content and a trunk style
|
||||||
|
/// chain.
|
||||||
|
fn repack<'a>(buf: &[Pair<'a>]) -> (Content, StyleChain<'a>) {
|
||||||
|
let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default();
|
||||||
|
let depth = trunk.links().count();
|
||||||
|
|
||||||
|
let mut seq = Vec::with_capacity(buf.len());
|
||||||
|
|
||||||
|
for (chain, group) in buf.group_by_key(|&(_, s)| s) {
|
||||||
|
let iter = group.iter().map(|&(c, _)| c.clone());
|
||||||
|
let suffix = chain.suffix(depth);
|
||||||
|
if suffix.is_empty() {
|
||||||
|
seq.extend(iter);
|
||||||
|
} else if let &[(element, _)] = group {
|
||||||
|
seq.push(element.clone().styled_with_map(suffix));
|
||||||
|
} else {
|
||||||
|
seq.push(Content::sequence(iter).styled_with_map(suffix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(Content::sequence(seq), trunk)
|
||||||
|
}
|
||||||
|
@ -128,6 +128,20 @@ pub trait SliceExt<T> {
|
|||||||
where
|
where
|
||||||
F: FnMut(&T) -> K,
|
F: FnMut(&T) -> K,
|
||||||
K: PartialEq;
|
K: PartialEq;
|
||||||
|
|
||||||
|
/// Computes two indices which split a slice into three parts.
|
||||||
|
///
|
||||||
|
/// - A prefix which matches `f`
|
||||||
|
/// - An inner portion
|
||||||
|
/// - A suffix which matches `f` and does not overlap with the prefix
|
||||||
|
///
|
||||||
|
/// If all elements match `f`, the prefix becomes `self` and the suffix
|
||||||
|
/// will be empty.
|
||||||
|
///
|
||||||
|
/// Returns the indices at which the inner portion and the suffix start.
|
||||||
|
fn split_prefix_suffix<F>(&self, f: F) -> (usize, usize)
|
||||||
|
where
|
||||||
|
F: FnMut(&T) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> SliceExt<T> for [T] {
|
impl<T> SliceExt<T> for [T] {
|
||||||
@ -157,6 +171,19 @@ impl<T> SliceExt<T> for [T] {
|
|||||||
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> {
|
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> {
|
||||||
GroupByKey { slice: self, f }
|
GroupByKey { slice: self, f }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn split_prefix_suffix<F>(&self, mut f: F) -> (usize, usize)
|
||||||
|
where
|
||||||
|
F: FnMut(&T) -> bool,
|
||||||
|
{
|
||||||
|
let start = self.iter().position(|v| !f(v)).unwrap_or(self.len());
|
||||||
|
let end = self
|
||||||
|
.iter()
|
||||||
|
.skip(start)
|
||||||
|
.rposition(|v| !f(v))
|
||||||
|
.map_or(start, |i| start + i + 1);
|
||||||
|
(start, end)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This struct is created by [`SliceExt::group_by_key`].
|
/// This struct is created by [`SliceExt::group_by_key`].
|
||||||
|
@ -333,8 +333,6 @@ pub static ROUTINES: Routines = Routines {
|
|||||||
realize: typst_realize::realize,
|
realize: typst_realize::realize,
|
||||||
layout_fragment: typst_layout::layout_fragment,
|
layout_fragment: typst_layout::layout_fragment,
|
||||||
layout_frame: typst_layout::layout_frame,
|
layout_frame: typst_layout::layout_frame,
|
||||||
layout_inline: typst_layout::layout_inline,
|
|
||||||
layout_box: typst_layout::layout_box,
|
|
||||||
layout_list: typst_layout::layout_list,
|
layout_list: typst_layout::layout_list,
|
||||||
layout_enum: typst_layout::layout_enum,
|
layout_enum: typst_layout::layout_enum,
|
||||||
layout_grid: typst_layout::layout_grid,
|
layout_grid: typst_layout::layout_grid,
|
||||||
|
BIN
tests/ref/bibliography-grid-par.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
tests/ref/bibliography-indent-par.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
tests/ref/enum-par.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
tests/ref/figure-par.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/ref/heading-par.png
Normal file
After Width: | Height: | Size: 555 B |
36
tests/ref/html/enum-par.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<ol>
|
||||||
|
<li>Hello</li>
|
||||||
|
<li>World</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p>Hello</p>
|
||||||
|
<p>From</p>
|
||||||
|
</li>
|
||||||
|
<li>World</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p>Hello</p>
|
||||||
|
<p>From</p>
|
||||||
|
<p>The</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>World</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
36
tests/ref/html/list-par.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li>Hello</li>
|
||||||
|
<li>World</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Hello</p>
|
||||||
|
<p>From</p>
|
||||||
|
</li>
|
||||||
|
<li>World</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Hello</p>
|
||||||
|
<p>From</p>
|
||||||
|
<p>The</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>World</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
tests/ref/html/par-semantic-html.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Heading is no paragraph</h2>
|
||||||
|
<p>I'm a paragraph.</p>
|
||||||
|
<div>I'm not.</div>
|
||||||
|
<div>
|
||||||
|
<p>We are two.</p>
|
||||||
|
<p>So we are paragraphs.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<blockquote cite="https://typst.app/home"> Compose papers faster </blockquote>
|
<blockquote cite="https://typst.app/home">Compose papers faster</blockquote>
|
||||||
<p>— <a href="https://typst.app/home">typst.com</a></p>
|
<p>— <a href="https://typst.app/home">typst.com</a></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<blockquote> … ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. </blockquote>
|
<blockquote>… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.</blockquote>
|
||||||
<p>— Plato</p>
|
<p>— Plato</p>
|
||||||
<blockquote> … I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either. </blockquote>
|
<blockquote>… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.</blockquote>
|
||||||
<p>— from the Henry Cary literal translation of 1897</p>
|
<p>— from the Henry Cary literal translation of 1897</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
42
tests/ref/html/terms-par.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>Hello</dt>
|
||||||
|
<dd>A</dd>
|
||||||
|
<dt>World</dt>
|
||||||
|
<dd>B</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>Hello</dt>
|
||||||
|
<dd>
|
||||||
|
<p>A</p>
|
||||||
|
<p>From</p>
|
||||||
|
</dd>
|
||||||
|
<dt>World</dt>
|
||||||
|
<dd>B</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>Hello</dt>
|
||||||
|
<dd>
|
||||||
|
<p>A</p>
|
||||||
|
<p>From</p>
|
||||||
|
<p>The</p>
|
||||||
|
</dd>
|
||||||
|
<dt>World</dt>
|
||||||
|
<dd>
|
||||||
|
<p>B</p>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
tests/ref/issue-5503-enum-in-align.png
Normal file
After Width: | Height: | Size: 421 B |
Before Width: | Height: | Size: 1004 B |
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 415 B |
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 569 B |
BIN
tests/ref/list-par.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
tests/ref/math-par.png
Normal file
After Width: | Height: | Size: 387 B |
BIN
tests/ref/outline-par.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
tests/ref/par-contains-block.png
Normal file
After Width: | Height: | Size: 426 B |
BIN
tests/ref/par-contains-parbreak.png
Normal file
After Width: | Height: | Size: 426 B |
BIN
tests/ref/par-hanging-indent-semantic.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
tests/ref/par-semantic-align.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/ref/par-semantic-tag.png
Normal file
After Width: | Height: | Size: 278 B |
BIN
tests/ref/par-semantic.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
tests/ref/par-show.png
Normal file
After Width: | Height: | Size: 932 B |
BIN
tests/ref/quote-par.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
tests/ref/table-cell-par.png
Normal file
After Width: | Height: | Size: 645 B |
BIN
tests/ref/terms-par.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
@ -310,6 +310,17 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- table-cell-par ---
|
||||||
|
// Ensure that table cells aren't considered paragraphs by default.
|
||||||
|
#show par: highlight
|
||||||
|
|
||||||
|
#table(
|
||||||
|
columns: 3,
|
||||||
|
[A],
|
||||||
|
block[B],
|
||||||
|
par[C],
|
||||||
|
)
|
||||||
|
|
||||||
--- grid-cell-in-table ---
|
--- grid-cell-in-table ---
|
||||||
// Error: 8-19 cannot use `grid.cell` as a table cell
|
// Error: 8-19 cannot use `grid.cell` as a table cell
|
||||||
// Hint: 8-19 use `table.cell` instead
|
// Hint: 8-19 use `table.cell` instead
|
||||||
|
@ -43,3 +43,8 @@ $sum_(k in NN)^prime 1/k^2$
|
|||||||
// Test script-script in a fraction.
|
// Test script-script in a fraction.
|
||||||
$ 1/(x^A) $
|
$ 1/(x^A) $
|
||||||
#[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$]
|
#[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$]
|
||||||
|
|
||||||
|
--- math-par ---
|
||||||
|
// Ensure that math does not produce paragraphs.
|
||||||
|
#show par: highlight
|
||||||
|
$ a + "bc" + #[c] + #box[d] + #block[e] $
|
||||||
|
@ -53,6 +53,24 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read
|
|||||||
@Zee04
|
@Zee04
|
||||||
#bibliography("/assets/bib/works_too.bib", style: "mla")
|
#bibliography("/assets/bib/works_too.bib", style: "mla")
|
||||||
|
|
||||||
|
--- bibliography-grid-par ---
|
||||||
|
// Ensure that a grid-based bibliography does not produce paragraphs.
|
||||||
|
#show par: highlight
|
||||||
|
|
||||||
|
@Zee04
|
||||||
|
@keshav2007read
|
||||||
|
|
||||||
|
#bibliography("/assets/bib/works_too.bib")
|
||||||
|
|
||||||
|
--- bibliography-indent-par ---
|
||||||
|
// Ensure that an indent-based bibliography does not produce paragraphs.
|
||||||
|
#show par: highlight
|
||||||
|
|
||||||
|
@Zee04
|
||||||
|
@keshav2007read
|
||||||
|
|
||||||
|
#bibliography("/assets/bib/works_too.bib", style: "mla")
|
||||||
|
|
||||||
--- issue-4618-bibliography-set-heading-level ---
|
--- issue-4618-bibliography-set-heading-level ---
|
||||||
// Test that the bibliography block's heading is set to 2 by the show rule,
|
// Test that the bibliography block's heading is set to 2 by the show rule,
|
||||||
// and therefore should be rendered like a level-2 heading. Notably, this
|
// and therefore should be rendered like a level-2 heading. Notably, this
|
||||||
|
@ -183,22 +183,44 @@ a + 0.
|
|||||||
#set enum(number-align: horizon)
|
#set enum(number-align: horizon)
|
||||||
#set enum(number-align: bottom)
|
#set enum(number-align: bottom)
|
||||||
|
|
||||||
|
--- enum-par render html ---
|
||||||
|
// Check whether the contents of enum items become paragraphs.
|
||||||
|
#show par: it => if target() != "html" { highlight(it) } else { it }
|
||||||
|
|
||||||
|
// No paragraphs.
|
||||||
|
#block[
|
||||||
|
+ Hello
|
||||||
|
+ World
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
+ Hello // Paragraphs
|
||||||
|
|
||||||
|
From
|
||||||
|
+ World // No paragraph because it's a tight enum
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
+ Hello // Paragraphs
|
||||||
|
|
||||||
|
From
|
||||||
|
|
||||||
|
The
|
||||||
|
|
||||||
|
+ World // Paragraph because it's a wide enum
|
||||||
|
]
|
||||||
|
|
||||||
--- issue-2530-enum-item-panic ---
|
--- issue-2530-enum-item-panic ---
|
||||||
// Enum item (pre-emptive)
|
// Enum item (pre-emptive)
|
||||||
#enum.item(none)[Hello]
|
#enum.item(none)[Hello]
|
||||||
#enum.item(17)[Hello]
|
#enum.item(17)[Hello]
|
||||||
|
|
||||||
--- issue-5503-enum-interrupted-by-par-align ---
|
--- issue-5503-enum-in-align ---
|
||||||
// `align` is block-level and should interrupt an enum
|
// `align` is block-level and should interrupt an enum.
|
||||||
// but not a `par`
|
|
||||||
+ a
|
+ a
|
||||||
+ b
|
+ b
|
||||||
#par(leading: 5em)[+ par]
|
#align(right)[+ c]
|
||||||
+ d
|
+ d
|
||||||
#par[+ par]
|
|
||||||
+ f
|
|
||||||
#align(right)[+ align]
|
|
||||||
+ h
|
|
||||||
|
|
||||||
--- issue-5719-enum-nested ---
|
--- issue-5719-enum-nested ---
|
||||||
// Enums can be immediately nested.
|
// Enums can be immediately nested.
|
||||||
|
@ -180,6 +180,17 @@ We can clearly see that @fig-cylinder and
|
|||||||
caption: [Underlined],
|
caption: [Underlined],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- figure-par ---
|
||||||
|
// Ensure that a figure body is considered a paragraph.
|
||||||
|
#show par: highlight
|
||||||
|
|
||||||
|
#figure[Text]
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
[Text],
|
||||||
|
caption: [A caption]
|
||||||
|
)
|
||||||
|
|
||||||
--- figure-and-caption-show ---
|
--- figure-and-caption-show ---
|
||||||
// Test creating custom figure and custom caption
|
// Test creating custom figure and custom caption
|
||||||
|
|
||||||
|
@ -128,6 +128,11 @@ Not in heading
|
|||||||
// Hint: 1:19-1:25 you can enable heading numbering with `#set heading(numbering: "1.")`
|
// Hint: 1:19-1:25 you can enable heading numbering with `#set heading(numbering: "1.")`
|
||||||
Cannot be used as @intro
|
Cannot be used as @intro
|
||||||
|
|
||||||
|
--- heading-par ---
|
||||||
|
// Ensure that heading text isn't considered a paragraph.
|
||||||
|
#show par: highlight
|
||||||
|
= Heading
|
||||||
|
|
||||||
--- heading-html-basic html ---
|
--- heading-html-basic html ---
|
||||||
// level 1 => h2
|
// level 1 => h2
|
||||||
// ...
|
// ...
|
||||||
|
@ -238,6 +238,33 @@ World
|
|||||||
#text(red)[- World]
|
#text(red)[- World]
|
||||||
#text(green)[- What up?]
|
#text(green)[- What up?]
|
||||||
|
|
||||||
|
--- list-par render html ---
|
||||||
|
// Check whether the contents of list items become paragraphs.
|
||||||
|
#show par: it => if target() != "html" { highlight(it) } else { it }
|
||||||
|
|
||||||
|
#block[
|
||||||
|
// No paragraphs.
|
||||||
|
- Hello
|
||||||
|
- World
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
- Hello // Paragraphs
|
||||||
|
|
||||||
|
From
|
||||||
|
- World // No paragraph because it's a tight list.
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
- Hello // Paragraphs either way
|
||||||
|
|
||||||
|
From
|
||||||
|
|
||||||
|
The
|
||||||
|
|
||||||
|
- World // Paragraph because it's a wide list.
|
||||||
|
]
|
||||||
|
|
||||||
--- issue-2530-list-item-panic ---
|
--- issue-2530-list-item-panic ---
|
||||||
// List item (pre-emptive)
|
// List item (pre-emptive)
|
||||||
#list.item[Hello]
|
#list.item[Hello]
|
||||||
@ -262,18 +289,11 @@ World
|
|||||||
part($ x $ + parbreak() + parbreak() + list[A])
|
part($ x $ + parbreak() + parbreak() + list[A])
|
||||||
}
|
}
|
||||||
|
|
||||||
--- issue-5503-list-interrupted-by-par-align ---
|
--- issue-5503-list-in-align ---
|
||||||
// `align` is block-level and should interrupt a list
|
// `align` is block-level and should interrupt a list.
|
||||||
// but not a `par`
|
|
||||||
#show list: [List]
|
#show list: [List]
|
||||||
- a
|
- a
|
||||||
- b
|
- b
|
||||||
#par(leading: 5em)[- c]
|
|
||||||
- d
|
|
||||||
- e
|
|
||||||
#par[- f]
|
|
||||||
- g
|
|
||||||
- h
|
|
||||||
#align(right)[- i]
|
#align(right)[- i]
|
||||||
- j
|
- j
|
||||||
|
|
||||||
|
@ -242,6 +242,15 @@ A
|
|||||||
#outline(target: metadata)
|
#outline(target: metadata)
|
||||||
#metadata("hello")
|
#metadata("hello")
|
||||||
|
|
||||||
|
--- outline-par ---
|
||||||
|
// Ensure that an outline does not produce paragraphs.
|
||||||
|
#show par: highlight
|
||||||
|
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
= A
|
||||||
|
= B
|
||||||
|
= C
|
||||||
|
|
||||||
--- issue-2048-outline-multiline ---
|
--- issue-2048-outline-multiline ---
|
||||||
// Without the word joiner between the dots and the page number,
|
// Without the word joiner between the dots and the page number,
|
||||||
|
@ -19,6 +19,105 @@ heaven Would through the airy region stream so bright That birds would sing and
|
|||||||
think it were not night. See, how she leans her cheek upon her hand! O, that I
|
think it were not night. See, how she leans her cheek upon her hand! O, that I
|
||||||
were a glove upon that hand, That I might touch that cheek!
|
were a glove upon that hand, That I might touch that cheek!
|
||||||
|
|
||||||
|
--- par-semantic ---
|
||||||
|
#show par: highlight
|
||||||
|
|
||||||
|
I'm a paragraph.
|
||||||
|
|
||||||
|
#align(center, table(
|
||||||
|
columns: 3,
|
||||||
|
|
||||||
|
// No paragraphs.
|
||||||
|
[A],
|
||||||
|
block[B],
|
||||||
|
block[C *D*],
|
||||||
|
|
||||||
|
// Paragraphs.
|
||||||
|
par[E],
|
||||||
|
[
|
||||||
|
|
||||||
|
F
|
||||||
|
],
|
||||||
|
[
|
||||||
|
G
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
// Paragraphs.
|
||||||
|
parbreak() + [H],
|
||||||
|
[I] + parbreak(),
|
||||||
|
parbreak() + [J] + parbreak(),
|
||||||
|
|
||||||
|
// Paragraphs.
|
||||||
|
[K #v(10pt)],
|
||||||
|
[#v(10pt) L],
|
||||||
|
[#place[] M],
|
||||||
|
|
||||||
|
// Paragraphs.
|
||||||
|
[
|
||||||
|
N
|
||||||
|
|
||||||
|
O
|
||||||
|
],
|
||||||
|
[#par[P]#par[Q]],
|
||||||
|
// No paragraphs.
|
||||||
|
[#block[R]#block[S]],
|
||||||
|
))
|
||||||
|
|
||||||
|
--- par-semantic-html html ---
|
||||||
|
= Heading is no paragraph
|
||||||
|
|
||||||
|
I'm a paragraph.
|
||||||
|
|
||||||
|
#html.elem("div")[I'm not.]
|
||||||
|
|
||||||
|
#html.elem("div")[
|
||||||
|
We are two.
|
||||||
|
|
||||||
|
So we are paragraphs.
|
||||||
|
]
|
||||||
|
|
||||||
|
--- par-semantic-tag ---
|
||||||
|
#show par: highlight
|
||||||
|
#block[
|
||||||
|
#metadata(none) <hi1>
|
||||||
|
A
|
||||||
|
#metadata(none) <hi2>
|
||||||
|
]
|
||||||
|
|
||||||
|
#block(width: 100%, metadata(none) + align(center)[A])
|
||||||
|
#block(width: 100%, align(center)[A] + metadata(none))
|
||||||
|
|
||||||
|
--- par-semantic-align ---
|
||||||
|
#show par: highlight
|
||||||
|
#show bibliography: none
|
||||||
|
#set block(width: 100%, stroke: 1pt, inset: 5pt)
|
||||||
|
|
||||||
|
#bibliography("/assets/bib/works.bib")
|
||||||
|
|
||||||
|
#block[
|
||||||
|
#set align(right)
|
||||||
|
Hello
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
#set align(right)
|
||||||
|
Hello
|
||||||
|
@netwok
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
Hello
|
||||||
|
#align(right)[World]
|
||||||
|
You
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
Hello
|
||||||
|
#align(right)[@netwok]
|
||||||
|
You
|
||||||
|
]
|
||||||
|
|
||||||
--- par-leading-and-spacing ---
|
--- par-leading-and-spacing ---
|
||||||
// Test changing leading and spacing.
|
// Test changing leading and spacing.
|
||||||
#set par(spacing: 1em, leading: 2pt)
|
#set par(spacing: 1em, leading: 2pt)
|
||||||
@ -69,6 +168,12 @@ Why would anybody ever ...
|
|||||||
#set par(hanging-indent: 15pt, justify: true)
|
#set par(hanging-indent: 15pt, justify: true)
|
||||||
#lorem(10)
|
#lorem(10)
|
||||||
|
|
||||||
|
--- par-hanging-indent-semantic ---
|
||||||
|
#set par(hanging-indent: 15pt)
|
||||||
|
= I am not affected
|
||||||
|
|
||||||
|
I am affected by hanging indent.
|
||||||
|
|
||||||
--- par-hanging-indent-manual-linebreak ---
|
--- par-hanging-indent-manual-linebreak ---
|
||||||
#set par(hanging-indent: 1em)
|
#set par(hanging-indent: 1em)
|
||||||
Welcome \ here. Does this work well?
|
Welcome \ here. Does this work well?
|
||||||
@ -83,6 +188,22 @@ Welcome \ here. Does this work well?
|
|||||||
// Ensure that trailing whitespace layouts as intended.
|
// Ensure that trailing whitespace layouts as intended.
|
||||||
#box(fill: aqua, " ")
|
#box(fill: aqua, " ")
|
||||||
|
|
||||||
|
--- par-contains-parbreak ---
|
||||||
|
#par[
|
||||||
|
Hello
|
||||||
|
// Warning: 4-14 parbreak may not occur inside of a paragraph and was ignored
|
||||||
|
#parbreak()
|
||||||
|
World
|
||||||
|
]
|
||||||
|
|
||||||
|
--- par-contains-block ---
|
||||||
|
#par[
|
||||||
|
Hello
|
||||||
|
// Warning: 4-11 block may not occur inside of a paragraph and was ignored
|
||||||
|
#block[]
|
||||||
|
World
|
||||||
|
]
|
||||||
|
|
||||||
--- par-empty-metadata ---
|
--- par-empty-metadata ---
|
||||||
// Check that metadata still works in a zero length paragraph.
|
// Check that metadata still works in a zero length paragraph.
|
||||||
#block(height: 0pt)[#""#metadata(false)<hi>]
|
#block(height: 0pt)[#""#metadata(false)<hi>]
|
||||||
@ -94,6 +215,26 @@ Welcome \ here. Does this work well?
|
|||||||
#set text(hyphenate: false)
|
#set text(hyphenate: false)
|
||||||
Lorem ipsum dolor #metadata(none) nonumy eirmod tempor.
|
Lorem ipsum dolor #metadata(none) nonumy eirmod tempor.
|
||||||
|
|
||||||
|
--- par-show ---
|
||||||
|
// This is only slightly cursed.
|
||||||
|
#let revoke = metadata("revoke")
|
||||||
|
#show par: it => {
|
||||||
|
if bibliography.title == revoke { return it }
|
||||||
|
set bibliography(title: revoke)
|
||||||
|
let p = counter("p")
|
||||||
|
par[#p.step() §#context p.display() #it.body]
|
||||||
|
}
|
||||||
|
|
||||||
|
= A
|
||||||
|
|
||||||
|
B
|
||||||
|
|
||||||
|
C #parbreak() D
|
||||||
|
|
||||||
|
#block[E]
|
||||||
|
|
||||||
|
#block[F #parbreak() G]
|
||||||
|
|
||||||
--- issue-4278-par-trim-before-equation ---
|
--- issue-4278-par-trim-before-equation ---
|
||||||
#set par(justify: true)
|
#set par(justify: true)
|
||||||
#lorem(6) aa $a = c + b$
|
#lorem(6) aa $a = c + b$
|
||||||
|
@ -107,3 +107,14 @@ When you said that #quote[he surely meant that #quote[she intended to say #quote
|
|||||||
)[
|
)[
|
||||||
Compose papers faster
|
Compose papers faster
|
||||||
]
|
]
|
||||||
|
|
||||||
|
--- quote-par ---
|
||||||
|
// Ensure that an inline quote is part of a paragraph, but a block quote
|
||||||
|
// does not result in paragraphs.
|
||||||
|
#show par: highlight
|
||||||
|
|
||||||
|
An inline #quote[quote.]
|
||||||
|
|
||||||
|
#quote(block: true, attribution: [The Test Author])[
|
||||||
|
A block-level quote.
|
||||||
|
]
|
||||||
|
@ -59,6 +59,34 @@ Not in list
|
|||||||
// Error: 8 expected colon
|
// Error: 8 expected colon
|
||||||
/ Hello
|
/ Hello
|
||||||
|
|
||||||
|
--- terms-par render html ---
|
||||||
|
// Check whether the contents of term list items become paragraphs.
|
||||||
|
#show par: it => if target() != "html" { highlight(it) } else { it }
|
||||||
|
|
||||||
|
// No paragraphs.
|
||||||
|
#block[
|
||||||
|
/ Hello: A
|
||||||
|
/ World: B
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
/ Hello: A // Paragraphs
|
||||||
|
|
||||||
|
From
|
||||||
|
/ World: B // No paragraphs because it's a tight term list.
|
||||||
|
]
|
||||||
|
|
||||||
|
#block[
|
||||||
|
/ Hello: A // Paragraphs
|
||||||
|
|
||||||
|
From
|
||||||
|
|
||||||
|
The
|
||||||
|
|
||||||
|
/ World: B // Paragraph because it's a wide term list.
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
--- issue-1050-terms-indent ---
|
--- issue-1050-terms-indent ---
|
||||||
#set page(width: 110pt)
|
#set page(width: 110pt)
|
||||||
#set par(first-line-indent: 0.5cm)
|
#set par(first-line-indent: 0.5cm)
|
||||||
@ -76,18 +104,10 @@ Not in list
|
|||||||
// Term item (pre-emptive)
|
// Term item (pre-emptive)
|
||||||
#terms.item[Hello][World!]
|
#terms.item[Hello][World!]
|
||||||
|
|
||||||
--- issue-5503-terms-interrupted-by-par-align ---
|
--- issue-5503-terms-in-align ---
|
||||||
// `align` is block-level and should interrupt a `terms`
|
// `align` is block-level and should interrupt a `terms`.
|
||||||
// but not a `par`
|
|
||||||
#show terms: [Terms]
|
#show terms: [Terms]
|
||||||
/ a: a
|
/ a: a
|
||||||
/ b: b
|
|
||||||
#par(leading: 5em)[/ c: c]
|
|
||||||
/ d: d
|
|
||||||
/ e: e
|
|
||||||
#par[/ f: f]
|
|
||||||
/ g: g
|
|
||||||
/ h: h
|
|
||||||
#align(right)[/ i: i]
|
#align(right)[/ i: i]
|
||||||
/ j: j
|
/ j: j
|
||||||
|
|
||||||
|