New realization / Text show rules now work across elements (#4876)

This commit is contained in:
Laurenz 2024-09-02 20:43:59 +02:00 committed by GitHub
parent 9fd796e0e2
commit cfde809feb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1727 additions and 1567 deletions

2
Cargo.lock generated
View File

@ -2632,8 +2632,10 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
name = "typst"
version = "0.11.0"
dependencies = [
"arrayvec",
"az",
"bitflags 2.6.0",
"bumpalo",
"chinese-number",
"ciborium",
"comemo",

View File

@ -33,6 +33,7 @@ arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
bitflags = { version = "2", features = ["serde"] }
bumpalo = { version = "3", features = ["collections"] }
bytemuck = "1"
chinese-number = { version = "0.7.2", default-features = false, features = ["number-to-chinese"] }
chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] }

View File

@ -18,8 +18,10 @@ typst-macros = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
arrayvec = { workspace = true }
az = { workspace = true }
bitflags = { workspace = true }
bumpalo = { workspace = true }
chinese-number = { workspace = true }
ciborium = { workspace = true }
comemo = { workspace = true }

View File

@ -29,15 +29,11 @@ pub struct Engine<'a> {
}
impl Engine<'_> {
/// Performs a fallible operation that does not immediately terminate further
/// execution. Instead it produces a delayed error that is only promoted to
/// a fatal one if it remains at the end of the introspection loop.
pub fn delay<F, T>(&mut self, f: F) -> T
where
F: FnOnce(&mut Self) -> SourceResult<T>,
T: Default,
{
match f(self) {
/// Handles a result without immediately terminating execution. Instead, it
/// produces a delayed error that is only promoted to a fatal one if it
/// remains by the end of the introspection loop.
pub fn delay<T: Default>(&mut self, result: SourceResult<T>) -> T {
match result {
Ok(value) => value,
Err(errors) => {
self.sink.delay(errors);

View File

@ -21,10 +21,9 @@ use crate::foundations::{
use crate::introspection::Location;
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
use crate::model::{Destination, EmphElem, LinkElem, StrongElem};
use crate::realize::{Behave, Behaviour};
use crate::syntax::Span;
use crate::text::UnderlineElem;
use crate::utils::{fat, LazyHash, SmallBitSet};
use crate::utils::{fat, singleton, LazyHash, SmallBitSet};
/// A piece of document content.
///
@ -109,9 +108,9 @@ impl Content {
}
}
/// Creates a new empty sequence content.
/// Creates a empty sequence content.
pub fn empty() -> Self {
Self::new(SequenceElem::default())
singleton!(Content, SequenceElem::default().pack()).clone()
}
/// Get the element of this content.
@ -185,12 +184,6 @@ impl Content {
self.make_mut().lifecycle.insert(0);
}
/// How this element interacts with other elements in a stream.
pub fn behaviour(&self) -> Behaviour {
self.with::<dyn Behave>()
.map_or(Behaviour::Supportive, Behave::behaviour)
}
/// Get a field by ID.
///
/// This is the preferred way to access fields. However, you can only use it

View File

@ -12,7 +12,6 @@ use crate::foundations::{
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::symbols::Symbol;
use crate::text::TextElem;
/// A helper macro to create a field selector used in [`Selector::Elem`]
#[macro_export]
@ -126,15 +125,12 @@ impl Selector {
pub fn matches(&self, target: &Content, styles: Option<StyleChain>) -> bool {
match self {
Self::Elem(element, dict) => {
target.func() == *element
target.elem() == *element
&& dict.iter().flat_map(|dict| dict.iter()).all(|(id, value)| {
target.get(*id, styles).as_ref().ok() == Some(value)
})
}
Self::Label(label) => target.label() == Some(*label),
Self::Regex(regex) => target
.to_packed::<TextElem>()
.is_some_and(|elem| regex.is_match(elem.text())),
Self::Can(cap) => target.func().can_type_id(*cap),
Self::Or(selectors) => {
selectors.iter().any(move |sel| sel.matches(target, styles))
@ -144,7 +140,7 @@ impl Selector {
}
Self::Location(location) => target.location() == Some(*location),
// Not supported here.
Self::Before { .. } | Self::After { .. } => false,
Self::Regex(_) | Self::Before { .. } | Self::After { .. } => false,
}
}
}

View File

@ -164,12 +164,6 @@ impl Styles {
.any(|property| property.is_of(elem) && property.id == field)
}
/// Returns `Some(_)` with an optional span if this list contains
/// styles for the given element.
pub fn interruption<T: NativeElement>(&self) -> Option<Span> {
self.0.iter().find_map(|entry| entry.interruption::<T>())
}
/// Set a font family composed of a preferred family and existing families
/// from a style chain.
pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) {
@ -229,6 +223,10 @@ pub enum Style {
/// A show rule recipe.
Recipe(Recipe),
/// Disables a specific show rule recipe.
///
/// Note: This currently only works for regex recipes since it's the only
/// place we need it for the moment. Normal show rules use guards directly
/// on elements instead.
Revocation(RecipeIndex),
}
@ -249,13 +247,24 @@ impl Style {
}
}
/// Returns `Some(_)` with an optional span if this style is of
/// the given element.
pub fn interruption<T: NativeElement>(&self) -> Option<Span> {
let elem = T::elem();
/// The style's span, if any.
pub fn span(&self) -> Span {
match self {
Style::Property(property) => property.is_of(elem).then_some(property.span),
Style::Recipe(recipe) => recipe.is_of(elem).then_some(recipe.span),
Self::Property(property) => property.span,
Self::Recipe(recipe) => recipe.span,
Self::Revocation(_) => Span::detached(),
}
}
/// Returns `Some(_)` with an optional span if this style is for
/// the given element.
pub fn element(&self) -> Option<Element> {
match self {
Style::Property(property) => Some(property.elem),
Style::Recipe(recipe) => match recipe.selector {
Some(Selector::Elem(elem, _)) => Some(elem),
_ => None,
},
Style::Revocation(_) => None,
}
}
@ -279,6 +288,11 @@ impl Style {
Self::Revocation(_) => false,
}
}
/// Turn this style into prehashed style.
pub fn wrap(self) -> LazyHash<Style> {
LazyHash::new(self)
}
}
impl Debug for Style {
@ -349,7 +363,7 @@ impl Property {
/// Turn this property into prehashed style.
pub fn wrap(self) -> LazyHash<Style> {
LazyHash::new(Style::Property(self))
Style::Property(self).wrap()
}
}
@ -474,21 +488,6 @@ impl Recipe {
&self.transform
}
/// Whether this recipe is for the given type of element.
pub fn is_of(&self, element: Element) -> bool {
match self.selector {
Some(Selector::Elem(own, _)) => own == element,
_ => false,
}
}
/// Whether the recipe is applicable to the target.
pub fn applicable(&self, target: &Content, styles: StyleChain) -> bool {
self.selector
.as_ref()
.is_some_and(|selector| selector.matches(target, Some(styles)))
}
/// Apply the recipe to the given content.
pub fn apply(
&self,
@ -669,6 +668,11 @@ impl<'a> StyleChain<'a> {
Entries { inner: [].as_slice().iter(), links: self.links() }
}
/// Iterate over the recipes in the chain.
pub fn recipes(self) -> impl Iterator<Item = &'a Recipe> {
self.entries().filter_map(|style| style.recipe())
}
/// Iterate over the links of the chain.
pub fn links(self) -> Links<'a> {
Links(Some(self))

View File

@ -6,7 +6,6 @@ use crate::foundations::{
elem, Args, Construct, Content, NativeElement, Packed, Unlabellable,
};
use crate::introspection::Location;
use crate::realize::{Behave, Behaviour};
/// Holds a locatable element that was realized.
#[derive(Clone, PartialEq, Hash)]
@ -78,7 +77,7 @@ pub enum TagKind {
///
/// The `TagElem` is handled by all layouters. The held element becomes
/// available for introspection in the next compiler iteration.
#[elem(Behave, Unlabellable, Construct)]
#[elem(Construct, Unlabellable)]
pub struct TagElem {
/// The introspectible element.
#[required]
@ -103,9 +102,3 @@ impl Construct for TagElem {
}
impl Unlabellable for Packed<TagElem> {}
impl Behave for Packed<TagElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}

View File

@ -7,7 +7,6 @@ use crate::introspection::Locator;
use crate::layout::{
layout_fragment_with_columns, BlockElem, Fragment, Length, Ratio, Regions, Rel,
};
use crate::realize::{Behave, Behaviour};
/// Separates a region into multiple equally sized columns.
///
@ -109,20 +108,10 @@ fn layout_columns(
/// understanding of the fundamental
/// laws of nature.
/// ```
#[elem(title = "Column Break", Behave)]
#[elem(title = "Column Break")]
pub struct ColbreakElem {
/// If `{true}`, the column break is skipped if the current column is
/// already empty.
#[default(false)]
pub weak: bool,
}
impl Behave for Packed<ColbreakElem> {
fn behaviour(&self) -> Behaviour {
if self.weak(StyleChain::default()) {
Behaviour::Weak(1)
} else {
Behaviour::Destructive
}
}
}

View File

@ -6,6 +6,7 @@ use std::collections::HashSet;
use std::num::NonZeroUsize;
use comemo::{Track, Tracked, TrackedMut};
use once_cell::unsync::Lazy;
use crate::diag::{bail, At, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
@ -28,7 +29,7 @@ use crate::model::{
Document, DocumentInfo, FootnoteElem, FootnoteEntry, Numbering, ParElem, ParLine,
ParLineMarker, ParLineNumberingScope,
};
use crate::realize::{first_span, realize, Arenas, Pair};
use crate::realize::{realize, Arenas, Pair, RealizationKind};
use crate::syntax::Span;
use crate::text::TextElem;
use crate::utils::{NonZeroExt, Numeric};
@ -116,8 +117,14 @@ fn layout_document_impl(
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let mut children =
realize(&mut engine, &mut locator, &arenas, Some(&mut info), content, styles)?;
let mut children = realize(
RealizationKind::Root(&mut info),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
@ -393,7 +400,6 @@ fn layout_page_run_impl(
// Determine the page-wide styles.
let styles = determine_page_styles(children, initial);
let styles = StyleChain::new(&styles);
let span = first_span(children);
// When one of the lengths is infinite the page fits its content along
// that axis.
@ -449,8 +455,7 @@ fn layout_page_run_impl(
Smart::Custom(numbering.clone()),
both,
)
.pack()
.spanned(span);
.pack();
// We interpret the Y alignment as selecting header or footer
// and then ignore it for aligning the actual number.
@ -473,12 +478,12 @@ fn layout_page_run_impl(
let fragment = FlowLayouter::new(
&mut engine,
children,
locator.next(&span).split(),
&mut locator,
styles,
regions,
PageElem::columns_in(styles),
ColumnsElem::gutter_in(styles),
span,
Span::detached(),
&mut vec![],
)
.layout(regions)?;
@ -733,12 +738,19 @@ fn layout_fragment_impl(
engine.route.check_layout_depth().at(content.span())?;
let arenas = Arenas::default();
let children = realize(&mut engine, &mut locator, &arenas, None, content, styles)?;
let children = realize(
RealizationKind::Container,
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
FlowLayouter::new(
&mut engine,
&children,
locator,
&mut locator,
styles,
regions,
columns,
@ -750,9 +762,9 @@ fn layout_fragment_impl(
}
/// Layouts a collection of block-level elements.
struct FlowLayouter<'a, 'e> {
struct FlowLayouter<'a, 'b> {
/// The engine.
engine: &'a mut Engine<'e>,
engine: &'a mut Engine<'b>,
/// The children that will be arranged into a flow.
children: &'a [Pair<'a>],
/// A span to use for errors.
@ -760,7 +772,7 @@ struct FlowLayouter<'a, 'e> {
/// Whether this is the root flow.
root: bool,
/// Provides unique locations to the flow's children.
locator: SplitLocator<'a>,
locator: &'a mut SplitLocator<'b>,
/// The shared styles.
shared: StyleChain<'a>,
/// The number of columns.
@ -811,8 +823,8 @@ struct CollectedParLine {
/// A prepared item in a flow layout.
#[derive(Debug)]
enum FlowItem {
/// Spacing between other items and whether it is weak.
Absolute(Abs, bool),
/// Spacing between other items and its weakness level.
Absolute(Abs, u8),
/// Fractional spacing between other items.
Fractional(Fr),
/// A frame for a layouted block.
@ -874,13 +886,13 @@ impl FlowItem {
}
}
impl<'a, 'e> FlowLayouter<'a, 'e> {
impl<'a, 'b> FlowLayouter<'a, 'b> {
/// Create a new flow layouter.
#[allow(clippy::too_many_arguments)]
fn new(
engine: &'a mut Engine<'e>,
engine: &'a mut Engine<'b>,
children: &'a [Pair<'a>],
locator: SplitLocator<'a>,
locator: &'a mut SplitLocator<'b>,
shared: StyleChain<'a>,
mut regions: Regions<'a>,
columns: NonZeroUsize,
@ -986,8 +998,10 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
self.handle_place(elem, styles)?;
} else if let Some(elem) = child.to_packed::<FlushElem>() {
self.handle_flush(elem)?;
} else if child.is::<PagebreakElem>() {
bail!(child.span(), "pagebreaks are not allowed inside of containers");
} else {
bail!(child.span(), "unexpected flow child");
bail!(child.span(), "{} is not allowed here", child.func().name());
}
}
@ -1001,16 +1015,41 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
/// Layout vertical spacing.
fn handle_v(&mut self, v: &'a Packed<VElem>, styles: StyleChain) -> SourceResult<()> {
self.handle_item(match v.amount {
self.layout_spacing(v.amount, styles, v.weak(styles) as u8)
}
/// Layout spacing, handling weakness.
fn layout_spacing(
&mut self,
amount: impl Into<Spacing>,
styles: StyleChain,
weakness: u8,
) -> SourceResult<()> {
self.handle_item(match amount.into() {
Spacing::Rel(rel) => FlowItem::Absolute(
// Resolve the spacing relative to the current base height.
rel.resolve(styles).relative_to(self.initial.y),
v.weakness(styles) > 0,
weakness,
),
Spacing::Fr(fr) => FlowItem::Fractional(fr),
})
}
/// Trim trailing weak spacing from the items.
fn trim_weak_spacing(&mut self) {
for (i, item) in self.items.iter().enumerate().rev() {
match item {
FlowItem::Absolute(amount, 1..) => {
self.regions.size.y += *amount;
self.items.remove(i);
return;
}
FlowItem::Frame { .. } => return,
_ => {}
}
}
}
/// Layout a column break.
fn handle_colbreak(&mut self, _: &'a Packed<ColbreakElem>) -> SourceResult<()> {
// If there is still an available region, skip to it.
@ -1031,6 +1070,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
// Fetch properties.
let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
let spacing = ParElem::spacing_in(styles);
let costs = TextElem::costs_in(styles);
// Layout the paragraph into lines. This only depends on the base size,
@ -1075,10 +1115,12 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
let back_2 = height_at(len.saturating_sub(2));
let back_1 = height_at(len.saturating_sub(1));
self.layout_spacing(spacing, styles, 4)?;
// Layout the lines.
for (i, mut frame) in lines.into_iter().enumerate() {
if i > 0 {
self.handle_item(FlowItem::Absolute(leading, true))?;
self.layout_spacing(leading, styles, 5)?;
}
// To prevent widows and orphans, we require enough space for
@ -1114,7 +1156,9 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
})?;
}
self.layout_spacing(spacing, styles, 4)?;
self.last_was_par = true;
Ok(())
}
@ -1128,6 +1172,11 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
let sticky = block.sticky(styles);
let align = AlignElem::alignment_in(styles).resolve(styles);
let rootable = block.rootable(styles);
let spacing = Lazy::new(|| (ParElem::spacing_in(styles).into(), 4));
let (above, above_weakness) =
block.above(styles).map(|v| (v, 3)).unwrap_or_else(|| *spacing);
let (below, below_weakness) =
block.below(styles).map(|v| (v, 3)).unwrap_or_else(|| *spacing);
// If the block is "rootable" it may host footnotes. In that case, we
// defer rootness to it temporarily. We disable our own rootness to
@ -1143,6 +1192,8 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
self.finish_region(false)?;
}
self.layout_spacing(above, styles, above_weakness)?;
// Layout the block itself.
let fragment = block.layout(
self.engine,
@ -1174,6 +1225,7 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
}
self.try_handle_footnotes(notes)?;
self.layout_spacing(below, styles, below_weakness)?;
self.root = is_root;
self.regions.root = false;
@ -1232,18 +1284,40 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
/// Layout a finished frame.
fn handle_item(&mut self, mut item: FlowItem) -> SourceResult<()> {
match item {
FlowItem::Absolute(v, weak) => {
if weak
&& !self
.items
.iter()
.any(|item| matches!(item, FlowItem::Frame { .. },))
{
return Ok(());
FlowItem::Absolute(v, weakness) => {
if weakness > 0 {
let mut has_frame = false;
for prev in self.items.iter_mut().rev() {
match prev {
FlowItem::Frame { .. } => {
has_frame = true;
break;
}
FlowItem::Absolute(prev_amount, prev_level)
if *prev_level > 0 =>
{
if *prev_level >= weakness {
let diff = v - *prev_amount;
if *prev_level > weakness || diff > Abs::zero() {
self.regions.size.y -= diff;
*prev = item;
}
}
return Ok(());
}
FlowItem::Fractional(_) => return Ok(()),
_ => {}
}
}
if !has_frame {
return Ok(());
}
}
self.regions.size.y -= v
self.regions.size.y -= v;
}
FlowItem::Fractional(..) => {
self.trim_weak_spacing();
}
FlowItem::Fractional(..) => {}
FlowItem::Frame { ref frame, movable, .. } => {
let height = frame.height();
while !self.regions.size.y.fits(height) && !self.regions.in_last() {
@ -1289,13 +1363,16 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
// Select the closer placement, top or bottom.
if y_align.is_auto() {
let ratio = (self.regions.size.y
- (frame.height() + clearance) / 2.0)
/ self.regions.full;
// When the figure's vertical midpoint would be above the
// middle of the page if it were layouted in-flow, we use
// top alignment. Otherwise, we use bottom alignment.
let used = self.regions.full - self.regions.size.y;
let half = (frame.height() + clearance) / 2.0;
let ratio = (used + half) / self.regions.full;
let better_align = if ratio <= 0.5 {
FixedAlignment::End
} else {
FixedAlignment::Start
} else {
FixedAlignment::End
};
*y_align = Smart::Custom(Some(better_align));
}
@ -1365,6 +1442,8 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
/// only (this is used to force the creation of a frame in case the
/// remaining elements are all out-of-flow).
fn finish_region(&mut self, force: bool) -> SourceResult<()> {
self.trim_weak_spacing();
// Early return if we don't have any relevant items.
if !force
&& !self.items.is_empty()
@ -1383,15 +1462,6 @@ impl<'a, 'e> FlowLayouter<'a, 'e> {
return Ok(());
}
// Trim weak spacing.
while self
.items
.last()
.is_some_and(|item| matches!(item, FlowItem::Absolute(_, true)))
{
self.items.pop();
}
// Determine the used size.
let mut fr = Fr::zero();
let mut used = Size::zero();

View File

@ -274,16 +274,24 @@ impl<'a> Collector<'a> {
}
fn push_segment(&mut self, segment: Segment<'a>) {
if let (Some(Segment::Text(last_len, last_styles)), Segment::Text(len, styles)) =
(self.segments.last_mut(), &segment)
{
if *last_styles == *styles {
match (self.segments.last_mut(), &segment) {
// Merge adjacent text segments with the same styles.
(Some(Segment::Text(last_len, last_styles)), Segment::Text(len, styles))
if *last_styles == *styles =>
{
*last_len += *len;
return;
}
}
self.segments.push(segment);
// Merge adjacent weak spacing by taking the maximum.
(
Some(Segment::Item(Item::Absolute(prev_amount, true))),
Segment::Item(Item::Absolute(amount, true)),
) => {
*prev_amount = (*prev_amount).max(*amount);
}
_ => self.segments.push(segment),
}
}
}

View File

@ -181,7 +181,7 @@ pub fn line<'a>(
/// example, the `range` may span "hello\n", but the `trim` specifies that the
/// linebreak is trimmed.
///
/// We do not factor the `trim` diredctly into the `range` because we still want
/// We do not factor the `trim` directly into the `range` because we still want
/// to keep non-text items after the trim (e.g. tags).
fn collect_items<'a>(
engine: &Engine,

View File

@ -9,14 +9,13 @@ use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func,
NativeElement, Packed, Set, Smart, StyleChain, Value,
NativeElement, Set, Smart, StyleChain, Value,
};
use crate::layout::{
Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel,
Sides, SpecificAlignment,
};
use crate::model::Numbering;
use crate::realize::{Behave, Behaviour};
use crate::utils::{singleton, NonZeroExt, Scalar};
use crate::visualize::{Color, Paint};
@ -388,7 +387,7 @@ impl Construct for PageElem {
/// == Compound Theory
/// In 1984, the first ...
/// ```
#[elem(title = "Page Break", Behave)]
#[elem(title = "Page Break")]
pub struct PagebreakElem {
/// If `{true}`, the page break is skipped if the current page is already
/// empty.
@ -417,12 +416,6 @@ pub struct PagebreakElem {
pub boundary: bool,
}
impl Behave for Packed<PagebreakElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Destructive
}
}
impl PagebreakElem {
/// Get the globally shared weak pagebreak element.
pub fn shared_weak() -> &'static Content {

View File

@ -1,11 +1,10 @@
use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine;
use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable};
use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain};
use crate::introspection::Locator;
use crate::layout::{
layout_frame, Alignment, Axes, Em, Frame, Length, Region, Rel, Size, VAlignment,
};
use crate::realize::{Behave, Behaviour};
/// Places content at an absolute position.
///
@ -27,7 +26,7 @@ use crate::realize::{Behave, Behaviour};
/// ),
/// )
/// ```
#[elem(scope, Behave)]
#[elem(scope)]
pub struct PlaceElem {
/// Relative to which position in the parent container to place the content.
///
@ -140,12 +139,6 @@ impl Packed<PlaceElem> {
}
}
impl Behave for Packed<PlaceElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}
/// Asks the layout algorithm to place pending floating elements before
/// continuing with the content.
///
@ -172,13 +165,5 @@ impl Behave for Packed<PlaceElem> {
/// Some conclusive text that must occur
/// after the figure.
/// ```
#[elem(Behave, Unlabellable)]
#[elem]
pub struct FlushElem {}
impl Behave for Packed<FlushElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}
impl Unlabellable for Packed<FlushElem> {}

View File

@ -1,6 +1,5 @@
use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain};
use crate::foundations::{cast, elem, Content};
use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel};
use crate::realize::{Behave, Behaviour};
use crate::utils::Numeric;
/// Inserts horizontal spacing into a paragraph.
@ -20,7 +19,7 @@ use crate::utils::Numeric;
/// In [mathematical formulas]($category/math), you can additionally use these
/// constants to add spacing between elements: `thin` (1/6 em), `med`(2/9 em),
/// `thick` (5/18 em), `quad` (1 em), `wide` (2 em).
#[elem(title = "Spacing (H)", Behave)]
#[elem(title = "Spacing (H)")]
pub struct HElem {
/// How much spacing to insert.
#[required]
@ -62,29 +61,6 @@ impl HElem {
}
}
impl Behave for Packed<HElem> {
fn behaviour(&self) -> Behaviour {
if self.amount().is_fractional() {
Behaviour::Destructive
} else if self.weak(StyleChain::default()) {
Behaviour::Weak(1)
} else {
Behaviour::Ignorant
}
}
fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool {
let Some(other) = prev.0.to_packed::<HElem>() else { return false };
match (self.amount(), other.amount()) {
(Spacing::Fr(this), Spacing::Fr(other)) => this > other,
(Spacing::Rel(this), Spacing::Rel(other)) => {
this.resolve(styles) > other.resolve(prev.1)
}
_ => false,
}
}
}
/// Inserts vertical spacing into a flow of blocks.
///
/// The spacing can be absolute, relative, or fractional. In the last case,
@ -105,7 +81,7 @@ impl Behave for Packed<HElem> {
/// [A #v(1fr) B],
/// )
/// ```
#[elem(title = "Spacing (V)", Behave)]
#[elem(title = "Spacing (V)")]
pub struct VElem {
/// How much spacing to insert.
#[required]
@ -124,14 +100,8 @@ pub struct VElem {
/// #v(4pt, weak: true)
/// The proof is simple:
/// ```
#[external]
pub weak: bool,
/// The spacing's weakness level, see also [`Behaviour`].
#[internal]
#[parse(args.named("weak")?.map(|v: bool| v as usize))]
pub weakness: usize,
/// Whether the spacing collapses if not immediately preceded by a
/// paragraph.
#[internal]
@ -139,56 +109,6 @@ pub struct VElem {
pub attach: bool,
}
impl VElem {
/// Normal strong spacing.
pub fn strong(amount: Spacing) -> Self {
Self::new(amount).with_weakness(0)
}
/// User-created weak spacing.
pub fn weak(amount: Spacing) -> Self {
Self::new(amount).with_weakness(1)
}
/// Weak spacing with list attach weakness.
pub fn list_attach(amount: Spacing) -> Self {
Self::new(amount).with_weakness(2).with_attach(true)
}
/// Weak spacing with `BlockElem::spacing` weakness.
pub fn block_spacing(amount: Spacing) -> Self {
Self::new(amount).with_weakness(3)
}
/// Weak spacing with `ParElem::spacing` weakness.
pub fn par_spacing(amount: Spacing) -> Self {
Self::new(amount).with_weakness(4)
}
}
impl Behave for Packed<VElem> {
fn behaviour(&self) -> Behaviour {
if self.amount().is_fractional() {
Behaviour::Destructive
} else if self.weakness(StyleChain::default()) > 0 {
Behaviour::Weak(self.weakness(StyleChain::default()))
} else {
Behaviour::Ignorant
}
}
fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool {
let Some(other) = prev.0.to_packed::<VElem>() else { return false };
match (self.amount(), other.amount()) {
(Spacing::Fr(this), Spacing::Fr(other)) => this > other,
(Spacing::Rel(this), Spacing::Rel(other)) => {
this.resolve(styles) > other.resolve(prev.1)
}
_ => false,
}
}
}
cast! {
VElem,
v: Content => v.unpack::<Self>().map_err(|_| "expected `v` element")?,

View File

@ -11,17 +11,20 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, Packed, StyleChain, StyleVec};
use crate::introspection::{Locator, SplitLocator};
use crate::layout::{layout_frame, Abs, Axes, BoxElem, Em, Frame, Region, Size};
use crate::foundations::{Content, Packed, Resolve, StyleChain, StyleVec};
use crate::introspection::{SplitLocator, TagElem};
use crate::layout::{
layout_frame, Abs, Axes, BoxElem, Em, Frame, HElem, PlaceElem, Region, Size, Spacing,
};
use crate::math::{
scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment,
LayoutMath, MathFragment, MathRun, MathSize, THICK,
};
use crate::realize::{realize, Arenas, RealizationKind};
use crate::syntax::{is_newline, Span};
use crate::text::{
features, BottomEdge, BottomEdgeMetric, Font, TextElem, TextSize, TopEdge,
TopEdgeMetric,
features, BottomEdge, BottomEdgeMetric, Font, LinebreakElem, SpaceElem, TextElem,
TextSize, TopEdge, TopEdgeMetric,
};
macro_rules! scaled {
@ -45,10 +48,10 @@ macro_rules! percent {
}
/// The context for math layout.
pub struct MathContext<'a, 'b, 'v> {
pub struct MathContext<'a, 'v, 'e> {
// External.
pub engine: &'v mut Engine<'b>,
pub locator: SplitLocator<'v>,
pub engine: &'v mut Engine<'e>,
pub locator: &'v mut SplitLocator<'a>,
pub region: Region,
// Font-related.
pub font: &'a Font,
@ -62,10 +65,11 @@ pub struct MathContext<'a, 'b, 'v> {
pub fragments: Vec<MathFragment>,
}
impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
/// Create a new math context.
pub fn new(
engine: &'v mut Engine<'b>,
locator: Locator<'v>,
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
styles: StyleChain<'a>,
base: Size,
font: &'a Font,
@ -104,7 +108,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
Self {
engine,
locator: locator.split(),
locator,
region: Region::new(base, Axes::splat(false)),
font,
ttf: font.ttf(),
@ -117,18 +121,29 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
}
}
/// Push a fragment.
pub fn push(&mut self, fragment: impl Into<MathFragment>) {
self.fragments.push(fragment.into());
}
pub fn extend(&mut self, fragments: Vec<MathFragment>) {
/// Push multiple fragments.
pub fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) {
self.fragments.extend(fragments);
}
/// Layout the given element and return the result as a [`MathRun`].
pub fn layout_into_run(
&mut self,
elem: &Content,
styles: StyleChain,
) -> SourceResult<MathRun> {
Ok(MathRun::new(self.layout_into_fragments(elem, styles)?))
}
/// Layout the given element and return the resulting [`MathFragment`]s.
pub fn layout_into_fragments(
&mut self,
elem: &dyn LayoutMath,
elem: &Content,
styles: StyleChain,
) -> SourceResult<Vec<MathFragment>> {
// The element's layout_math() changes the fragments held in this
@ -136,24 +151,15 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
// them, so we restore the MathContext's fragments after obtaining the
// layout result.
let prev = std::mem::take(&mut self.fragments);
elem.layout_math(self, styles)?;
self.layout(elem, styles)?;
Ok(std::mem::replace(&mut self.fragments, prev))
}
/// Layout the given element and return the result as a [`MathRun`].
pub fn layout_into_run(
&mut self,
elem: &dyn LayoutMath,
styles: StyleChain,
) -> SourceResult<MathRun> {
Ok(MathRun::new(self.layout_into_fragments(elem, styles)?))
}
/// Layout the given element and return the result as a
/// unified [`MathFragment`].
pub fn layout_into_fragment(
&mut self,
elem: &dyn LayoutMath,
elem: &Content,
styles: StyleChain,
) -> SourceResult<MathFragment> {
Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles))
@ -162,14 +168,89 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
/// Layout the given element and return the result as a [`Frame`].
pub fn layout_into_frame(
&mut self,
elem: &dyn LayoutMath,
elem: &Content,
styles: StyleChain,
) -> SourceResult<Frame> {
Ok(self.layout_into_fragment(elem, styles)?.into_frame())
}
}
/// Layout the given [`BoxElem`] into a [`Frame`].
pub fn layout_box(
impl MathContext<'_, '_, '_> {
/// Layout arbitrary content.
fn layout(&mut self, content: &Content, styles: StyleChain) -> SourceResult<()> {
let arenas = Arenas::default();
let pairs = realize(
RealizationKind::Math,
self.engine,
self.locator,
&arenas,
content,
styles,
)?;
let outer = styles;
for (elem, styles) in pairs {
// Hack because the font is fixed in math.
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
let frame = self.layout_external(elem, styles)?;
self.push(FrameFragment::new(self, styles, frame).with_spaced(true));
continue;
}
self.layout_realized(elem, styles)?;
}
Ok(())
}
/// Layout an element resulting from realization.
fn layout_realized(
&mut self,
elem: &Content,
styles: StyleChain,
) -> SourceResult<()> {
if let Some(elem) = elem.to_packed::<TagElem>() {
self.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() {
let font_size = scaled_font_size(self, styles);
self.push(MathFragment::Space(self.space_width.at(font_size)));
} else if elem.is::<LinebreakElem>() {
self.push(MathFragment::Linebreak);
} else if let Some(elem) = elem.to_packed::<HElem>() {
if let Spacing::Rel(rel) = elem.amount() {
if rel.rel.is_zero() {
self.push(MathFragment::Spacing(
rel.abs.resolve(styles),
elem.weak(styles),
));
}
}
} else if let Some(elem) = elem.to_packed::<TextElem>() {
let fragment = self.layout_text(elem, styles)?;
self.push(fragment);
} else if let Some(boxed) = elem.to_packed::<BoxElem>() {
let frame = self.layout_box(boxed, styles)?;
self.push(FrameFragment::new(self, styles, frame).with_spaced(true));
} else if let Some(elem) = elem.with::<dyn LayoutMath>() {
elem.layout_math(self, styles)?;
} else {
let mut frame = self.layout_external(elem, styles)?;
if !frame.has_baseline() {
let axis = scaled!(self, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
}
self.push(
FrameFragment::new(self, styles, frame)
.with_spaced(true)
.with_ignorant(elem.is::<PlaceElem>()),
);
}
Ok(())
}
/// Layout a box into a frame.
fn layout_box(
&mut self,
boxed: &Packed<BoxElem>,
styles: StyleChain,
@ -184,8 +265,8 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
)
}
/// Layout the given [`Content`] into a [`Frame`].
pub fn layout_content(
/// Layout into a frame with normal layout.
fn layout_external(
&mut self,
content: &Content,
styles: StyleChain,
@ -202,7 +283,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
}
/// Layout the given [`TextElem`] into a [`MathFragment`].
pub fn layout_text(
fn layout_text(
&mut self,
elem: &Packed<TextElem>,
styles: StyleChain,
@ -316,6 +397,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
}
}
/// Converts some unit to an absolute length with the current font & font size.
pub(super) trait Scaled {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
}

View File

@ -15,7 +15,7 @@ use crate::layout::{
Size, SpecificAlignment, VAlignment,
};
use crate::math::{
scaled_font_size, LayoutMath, MathContext, MathRunFrameBuilder, MathSize, MathVariant,
scaled_font_size, MathContext, MathRunFrameBuilder, MathSize, MathVariant,
};
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
use crate::syntax::Span;
@ -48,10 +48,7 @@ use crate::World;
/// least one space lifts it into a separate block that is centered
/// horizontally. For more details about math syntax, see the
/// [main math page]($category/math).
#[elem(
Locatable, Synthesize, Show, ShowSet, LayoutMath, Count, LocalName, Refable,
Outlinable
)]
#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)]
pub struct EquationElem {
/// Whether the equation is displayed as a separate block.
#[default(false)]
@ -258,13 +255,6 @@ impl Outlinable for Packed<EquationElem> {
}
}
impl LayoutMath for Packed<EquationElem> {
#[typst_macros::time(name = "math.equation", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
self.body().layout_math(ctx, styles)
}
}
/// Layout an inline equation (in a paragraph).
#[typst_macros::time(span = elem.span())]
fn layout_equation_inline(
@ -278,8 +268,9 @@ fn layout_equation_inline(
let font = find_math_font(engine, styles, elem.span())?;
let mut ctx = MathContext::new(engine, locator, styles, region, &font);
let run = ctx.layout_into_run(elem, styles)?;
let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
let run = ctx.layout_into_run(&elem.body, styles)?;
let mut items = if run.row_count() == 1 {
run.into_par_items()
@ -326,10 +317,9 @@ fn layout_equation_block(
let font = find_math_font(engine, styles, span)?;
let mut locator = locator.split();
let mut ctx =
MathContext::new(engine, locator.next(&()), styles, regions.base(), &font);
let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
let full_equation_builder = ctx
.layout_into_run(elem, styles)?
.layout_into_run(&elem.body, styles)?
.multiline_frame_builder(&ctx, styles);
let width = full_equation_builder.size.x;
let can_break =

View File

@ -23,7 +23,7 @@ pub enum MathFragment {
Glyph(GlyphFragment),
Variant(VariantFragment),
Frame(FrameFragment),
Spacing(SpacingFragment),
Spacing(Abs, bool),
Space(Abs),
Linebreak,
Align,
@ -40,7 +40,7 @@ impl MathFragment {
Self::Glyph(glyph) => glyph.width,
Self::Variant(variant) => variant.frame.width(),
Self::Frame(fragment) => fragment.frame.width(),
Self::Spacing(spacing) => spacing.width,
Self::Spacing(amount, _) => *amount,
Self::Space(amount) => *amount,
_ => Abs::zero(),
}
@ -86,7 +86,7 @@ impl MathFragment {
Self::Glyph(glyph) => glyph.class,
Self::Variant(variant) => variant.class,
Self::Frame(fragment) => fragment.class,
Self::Spacing(_) => MathClass::Space,
Self::Spacing(_, _) => MathClass::Space,
Self::Space(_) => MathClass::Space,
Self::Linebreak => MathClass::Space,
Self::Align => MathClass::Special,
@ -225,12 +225,6 @@ impl From<FrameFragment> for MathFragment {
}
}
impl From<SpacingFragment> for MathFragment {
fn from(fragment: SpacingFragment) -> Self {
Self::Spacing(fragment)
}
}
#[derive(Clone)]
pub struct GlyphFragment {
pub id: GlyphId,
@ -525,12 +519,6 @@ impl FrameFragment {
}
}
#[derive(Debug, Clone)]
pub struct SpacingFragment {
pub width: Abs,
pub weak: bool,
}
/// Look up the italics correction for a glyph.
fn italics_correction(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option<Abs> {
Some(

View File

@ -5,9 +5,7 @@ use crate::foundations::{
elem, func, Content, NativeElement, Packed, Resolve, Smart, StyleChain,
};
use crate::layout::{Abs, Em, Length, Rel};
use crate::math::{
GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, SpacingFragment,
};
use crate::math::{GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled};
use crate::text::TextElem;
use super::delimiter_alignment;
@ -90,10 +88,7 @@ impl LayoutMath for Packed<LrElem> {
fragments.retain(|fragment| {
index += 1;
(index != 2 && index + 1 != original_len)
|| !matches!(
fragment,
MathFragment::Spacing(SpacingFragment { weak: true, .. })
)
|| !matches!(fragment, MathFragment::Spacing(_, true))
});
ctx.extend(fragments);

View File

@ -42,15 +42,10 @@ use self::fragment::*;
use self::row::*;
use self::spacing::*;
use crate::diag::{At, SourceResult};
use crate::foundations::{
category, Category, Content, Module, Resolve, Scope, SequenceElem, StyleChain,
StyledElem,
};
use crate::introspection::{TagElem, TagKind};
use crate::layout::{BoxElem, HElem, Spacing, VAlignment};
use crate::realize::{process, BehavedBuilder, Behaviour};
use crate::text::{LinebreakElem, SpaceElem, TextElem};
use crate::diag::SourceResult;
use crate::foundations::{category, Category, Module, Scope, StyleChain};
use crate::layout::VAlignment;
use crate::text::TextElem;
/// Typst has special [syntax]($syntax/#math) and library functions to typeset
/// mathematical formulas. Math formulas can be displayed inline with text or as
@ -223,122 +218,6 @@ pub trait LayoutMath {
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()>;
}
impl LayoutMath for Content {
#[typst_macros::time(name = "math", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
// Directly layout the body of nested equations instead of handling it
// like a normal equation so that things like this work:
// ```
// #let my = $pi$
// $ my r^2 $
// ```
if let Some(elem) = self.to_packed::<EquationElem>() {
return elem.layout_math(ctx, styles);
}
if let Some((tag, realized)) =
process(ctx.engine, &mut ctx.locator, self, styles)?
{
ctx.engine.route.increase();
ctx.engine.route.check_show_depth().at(self.span())?;
if let Some(tag) = &tag {
ctx.push(MathFragment::Tag(tag.clone()));
}
realized.layout_math(ctx, styles)?;
if let Some(tag) = tag {
ctx.push(MathFragment::Tag(tag.with_kind(TagKind::End)));
}
ctx.engine.route.decrease();
return Ok(());
}
if self.is::<SequenceElem>() {
let mut bb = BehavedBuilder::new();
self.sequence_recursive_for_each(&mut |child: &Content| {
bb.push(child, StyleChain::default());
});
for (child, _) in bb.finish() {
child.layout_math(ctx, styles)?;
}
return Ok(());
}
if let Some(styled) = self.to_packed::<StyledElem>() {
let outer = styles;
let styles = outer.chain(&styled.styles);
if TextElem::font_in(styles) != TextElem::font_in(outer) {
let frame = ctx.layout_content(&styled.child, styles)?;
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true));
return Ok(());
}
styled.child.layout_math(ctx, styles)?;
return Ok(());
}
if self.is::<SpaceElem>() {
let font_size = scaled_font_size(ctx, styles);
ctx.push(MathFragment::Space(ctx.space_width.at(font_size)));
return Ok(());
}
if self.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak);
return Ok(());
}
if let Some(elem) = self.to_packed::<HElem>() {
if let Spacing::Rel(rel) = elem.amount() {
if rel.rel.is_zero() {
ctx.push(SpacingFragment {
width: rel.abs.resolve(styles),
weak: elem.weak(styles),
});
}
}
return Ok(());
}
if let Some(elem) = self.to_packed::<TextElem>() {
let fragment = ctx.layout_text(elem, styles)?;
ctx.push(fragment);
return Ok(());
}
if let Some(boxed) = self.to_packed::<BoxElem>() {
let frame = ctx.layout_box(boxed, styles)?;
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true));
return Ok(());
}
if let Some(elem) = self.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone()));
return Ok(());
}
if let Some(elem) = self.with::<dyn LayoutMath>() {
return elem.layout_math(ctx, styles);
}
let mut frame = ctx.layout_content(self, styles)?;
if !frame.has_baseline() {
let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
}
ctx.push(
FrameFragment::new(ctx, styles, frame)
.with_spaced(true)
.with_ignorant(self.behaviour() == Behaviour::Ignorant),
);
Ok(())
}
}
fn delimiter_alignment(delimiter: char) -> VAlignment {
match delimiter {
'\u{231c}' | '\u{231d}' => VAlignment::Top,

View File

@ -10,8 +10,6 @@ use crate::math::{
};
use crate::model::ParElem;
use super::fragment::SpacingFragment;
pub const TIGHT_LEADING: Em = Em::new(0.25);
/// A linear collection of [`MathFragment`]s.
@ -37,9 +35,21 @@ impl MathRun {
}
// Explicit spacing disables automatic spacing.
MathFragment::Spacing(_) => {
MathFragment::Spacing(width, weak) => {
last = None;
space = None;
if weak {
match resolved.last_mut() {
None => continue,
Some(MathFragment::Spacing(prev, true)) => {
*prev = (*prev).max(width);
continue;
}
Some(_) => {}
}
}
resolved.push(fragment);
continue;
}
@ -91,6 +101,10 @@ impl MathRun {
resolved.push(fragment);
}
if let Some(MathFragment::Spacing(_, true)) = resolved.last() {
resolved.pop();
}
Self(resolved)
}
@ -290,15 +304,14 @@ impl MathRun {
let is_relation = |f: &MathFragment| matches!(f.class(), MathClass::Relation);
let is_space = |f: &MathFragment| {
matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_))
matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_, _))
};
let mut iter = self.0.into_iter().peekable();
while let Some(fragment) = iter.next() {
if space_is_visible {
match fragment {
MathFragment::Space(width)
| MathFragment::Spacing(SpacingFragment { width, .. }) => {
MathFragment::Space(width) | MathFragment::Spacing(width, _) => {
items.push(InlineItem::Space(width, true));
continue;
}

View File

@ -2,7 +2,7 @@ use unicode_math_class::MathClass;
use crate::foundations::{NativeElement, Scope};
use crate::layout::{Abs, Em, HElem};
use crate::math::{MathFragment, MathSize, SpacingFragment};
use crate::math::{MathFragment, MathSize};
pub(super) const THIN: Em = Em::new(1.0 / 6.0);
pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0);
@ -29,7 +29,7 @@ pub(super) fn spacing(
let resolve = |v: Em, size_ref: &MathFragment| -> Option<MathFragment> {
let width = size_ref.font_size().map_or(Abs::zero(), |size| v.at(size));
Some(SpacingFragment { width, weak: false }.into())
Some(MathFragment::Spacing(width, false))
};
let script = |f: &MathFragment| f.math_size().is_some_and(|s| s <= MathSize::Script);

View File

@ -231,7 +231,9 @@ impl Show for Packed<BibliographyElem> {
.ok_or("CSL style is not suitable for bibliographies")
.at(span)?;
let row_gutter = ParElem::spacing_in(styles).into();
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()) {
let mut cells = vec![];
for (prefix, reference) in references {
@ -244,18 +246,18 @@ impl Show for Packed<BibliographyElem> {
)));
}
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
seq.push(row_gutter_elem.clone());
seq.push(
GridElem::new(cells)
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.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()
.spanned(self.span()),
);
} else {
for (_, reference) in references {
seq.push(VElem::new(row_gutter).with_weakness(3).pack());
seq.push(row_gutter_elem.clone());
seq.push(reference.clone());
}
}

View File

@ -14,7 +14,7 @@ use crate::layout::{
Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment,
Length, Regions, Sizing, VAlignment, VElem,
};
use crate::model::{Numbering, NumberingPattern, ParElem};
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem};
use crate::text::TextElem;
/// A numbered list.
@ -224,7 +224,8 @@ impl Show for Packed<EnumElem> {
if self.tight(styles) {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into()).pack();
let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
realized = spacing + realized;
}
@ -325,14 +326,6 @@ pub struct EnumItem {
pub body: Content,
}
impl Packed<EnumItem> {
/// Apply styles to this enum item.
pub fn styled(mut self, styles: Styles) -> Self {
self.body.style_in_place(styles);
self
}
}
cast! {
EnumItem,
array: Array => {
@ -345,3 +338,18 @@ cast! {
},
v: Content => v.unpack::<Self>().unwrap_or_else(Self::new),
}
impl ListLike for EnumElem {
type Item = EnumItem;
fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
Self::new(children).with_tight(tight)
}
}
impl ListItemLike for EnumItem {
fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
item.body.style_in_place(styles);
item
}
}

View File

@ -309,7 +309,7 @@ impl Show for Packed<FigureElem> {
// Build the caption, if any.
if let Some(caption) = self.caption(styles) {
let v = VElem::weak(self.gap(styles).into()).pack();
let v = VElem::new(self.gap(styles).into()).with_weak(true).pack();
realized = match caption.position(styles) {
OuterVAlignment::Top => caption.pack() + v + realized,
OuterVAlignment::Bottom => realized + v + caption.pack(),

View File

@ -148,7 +148,8 @@ impl Show for Packed<ListElem> {
if self.tight(styles) {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into()).pack();
let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
realized = spacing + realized;
}
@ -218,14 +219,6 @@ pub struct ListItem {
pub body: Content,
}
impl Packed<ListItem> {
/// Apply styles to this list item.
pub fn styled(mut self, styles: Styles) -> Self {
self.body.style_in_place(styles);
self
}
}
cast! {
ListItem,
v: Content => v.unpack::<Self>().unwrap_or_else(Self::new)
@ -276,3 +269,33 @@ cast! {
},
v: Func => Self::Func(v),
}
/// A list, enum, or term list.
pub trait ListLike: NativeElement {
/// The kind of list item this list is composed of.
type Item: ListItemLike;
/// Create this kind of list from its children and tightness.
fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self;
}
/// A list item, enum item, or term list item.
pub trait ListItemLike: NativeElement {
/// Apply styles to the element's body.
fn styled(item: Packed<Self>, styles: Styles) -> Packed<Self>;
}
impl ListLike for ListElem {
type Item = ListItem;
fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
Self::new(children).with_tight(tight)
}
}
impl ListItemLike for ListItem {
fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
item.body.style_in_place(styles);
item
}
}

View File

@ -207,8 +207,9 @@ impl Show for Packed<QuoteElem> {
// Use v(0.9em, weak: true) bring the attribution closer to the
// quote.
let weak_v = VElem::weak(Spacing::Rel(Em::new(0.9).into())).pack();
realized += weak_v + Content::sequence(seq).aligned(Alignment::END);
let gap = Spacing::Rel(Em::new(0.9).into());
let v = VElem::new(gap).with_weak(true).pack();
realized += v + Content::sequence(seq).aligned(Alignment::END);
}
realized = PadElem::new(realized).pack();

View File

@ -5,7 +5,7 @@ use crate::foundations::{
Styles,
};
use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::ParElem;
use crate::model::{ListItemLike, ListLike, ParElem};
use crate::text::TextElem;
use crate::utils::Numeric;
@ -150,7 +150,8 @@ impl Show for Packed<TermsElem> {
if self.tight(styles) {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into()).pack();
let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
realized = spacing + realized;
}
@ -170,15 +171,6 @@ pub struct TermItem {
pub description: Content,
}
impl Packed<TermItem> {
/// Apply styles to this term item.
pub fn styled(mut self, styles: Styles) -> Self {
self.term.style_in_place(styles.clone());
self.description.style_in_place(styles);
self
}
}
cast! {
TermItem,
array: Array => {
@ -191,3 +183,19 @@ cast! {
},
v: Content => v.unpack::<Self>().map_err(|_| "expected term item or array")?,
}
impl ListLike for TermsElem {
type Item = TermItem;
fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self {
Self::new(children).with_tight(tight)
}
}
impl ListItemLike for TermItem {
fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> {
item.term.style_in_place(styles.clone());
item.description.style_in_place(styles);
item
}
}

1238
crates/typst/src/realize.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
use typed_arena::Arena;
use crate::foundations::{Content, StyleChain, Styles};
/// Temporary storage arenas for building.
#[derive(Default)]
pub struct Arenas<'a> {
chains: Arena<StyleChain<'a>>,
content: Arena<Content>,
styles: Arena<Styles>,
}
impl<'a> Arenas<'a> {
/// Store a value in the matching arena.
pub fn store<T: Store<'a>>(&'a self, val: T) -> &'a T {
val.store(self)
}
}
/// Implemented by storable types.
pub trait Store<'a> {
fn store(self, arenas: &'a Arenas<'a>) -> &'a Self;
}
impl<'a> Store<'a> for Content {
fn store(self, arenas: &'a Arenas<'a>) -> &'a Self {
arenas.content.alloc(self)
}
}
impl<'a> Store<'a> for StyleChain<'a> {
fn store(self, arenas: &'a Arenas<'a>) -> &'a Self {
arenas.chains.alloc(self)
}
}
impl<'a> Store<'a> for Styles {
fn store(self, arenas: &'a Arenas<'a>) -> &'a Self {
arenas.styles.alloc(self)
}
}

View File

@ -1,137 +0,0 @@
//! Element interaction.
use crate::foundations::{Content, StyleChain};
/// How an element interacts with other elements in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Behaviour {
/// A weak element which only survives when a supportive element is before
/// and after it. Furthermore, per consecutive run of weak elements, only
/// one survives: The one with the lowest weakness level (or the larger one
/// if there is a tie).
Weak(usize),
/// An element that enables adjacent weak elements to exist. The default.
Supportive,
/// An element that destroys adjacent weak elements.
Destructive,
/// An element that does not interact at all with other elements, having the
/// same effect on them as if it didn't exist.
Ignorant,
}
impl Behaviour {
/// Whether this of `Weak(_)` variant.
pub fn is_weak(self) -> bool {
matches!(self, Self::Weak(_))
}
}
/// How the element interacts with other elements.
pub trait Behave {
/// The element's interaction behaviour.
fn behaviour(&self) -> Behaviour;
/// Whether this weak element is larger than a previous one and thus picked
/// as the maximum when the levels are the same.
#[allow(unused_variables)]
fn larger(&self, prev: &(&Content, StyleChain), styles: StyleChain) -> bool {
false
}
}
/// Processes a sequence of content and resolves behaviour interactions between
/// them and separates local styles for each element from the shared trunk of
/// styles.
#[derive(Debug)]
pub struct BehavedBuilder<'a> {
/// The collected content with its styles.
buf: Vec<(&'a Content, StyleChain<'a>)>,
/// What the last non-ignorant, visible item was.
last: Behaviour,
}
impl<'a> BehavedBuilder<'a> {
/// Create a new style-vec builder.
pub fn new() -> Self {
Self { buf: vec![], last: Behaviour::Destructive }
}
/// Whether the builder is totally empty.
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
/// Push an item into the builder.
pub fn push(&mut self, content: &'a Content, styles: StyleChain<'a>) {
let mut behaviour = content.behaviour();
match behaviour {
Behaviour::Supportive => {}
Behaviour::Weak(level) => match self.last {
// Remove either this or the preceding weak item.
Behaviour::Weak(prev_level) => {
if level > prev_level {
return;
}
let i = self.find_last_weak().unwrap();
if level == prev_level
&& !content
.with::<dyn Behave>()
.unwrap()
.larger(&self.buf[i], styles)
{
return;
}
self.buf.remove(i);
}
Behaviour::Destructive => return,
_ => {}
},
Behaviour::Destructive => {
// Remove preceding weak item.
if self.last.is_weak() {
let i = self.find_last_weak().unwrap();
self.buf.remove(i);
}
}
Behaviour::Ignorant => {
behaviour = self.last;
}
}
self.last = behaviour;
self.buf.push((content, styles));
}
/// Iterate over the content that has been pushed so far.
pub fn items(&self) -> impl Iterator<Item = &'a Content> + '_ {
self.buf.iter().map(|&(c, _)| c)
}
/// Return the built content (possibly styled with local styles) plus a
/// trunk style chain and a span for the collection.
pub fn finish(mut self) -> Vec<(&'a Content, StyleChain<'a>)> {
self.trim_weak();
self.buf
}
/// Trim a possibly remaining weak item.
fn trim_weak(&mut self) {
if self.last.is_weak() {
let i = self.find_last_weak().unwrap();
self.buf.remove(i);
}
}
/// Get the position of the right most weak item.
fn find_last_weak(&self) -> Option<usize> {
self.buf.iter().rposition(|(c, _)| c.behaviour().is_weak())
}
}
impl<'a> Default for BehavedBuilder<'a> {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,567 +0,0 @@
//! Realization of content.
//!
//! *Realization* is the process of recursively applying styling and, in
//! particular, show rules to produce well-known elements that can be laid out.
mod arenas;
mod behaviour;
mod process;
use once_cell::unsync::Lazy;
pub use self::arenas::Arenas;
pub use self::behaviour::{Behave, BehavedBuilder, Behaviour};
pub use self::process::process;
use std::mem;
use crate::diag::{bail, At, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
Content, ContextElem, NativeElement, Packed, SequenceElem, Smart, StyleChain,
StyleVec, StyledElem, Styles,
};
use crate::introspection::{SplitLocator, TagElem, TagKind};
use crate::layout::{
AlignElem, BlockElem, BoxElem, ColbreakElem, FlushElem, HElem, InlineElem, PageElem,
PagebreakElem, PlaceElem, VElem,
};
use crate::math::{EquationElem, LayoutMath};
use crate::model::{
CiteElem, CiteGroup, DocumentElem, DocumentInfo, EnumElem, EnumItem, ListElem,
ListItem, ParElem, ParbreakElem, TermItem, TermsElem,
};
use crate::syntax::Span;
use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use crate::utils::SliceExt;
/// A pair of content and a style chain that applies to it.
pub type Pair<'a> = (&'a Content, StyleChain<'a>);
/// Realize content into a flat list of well-known, styled items.
#[typst_macros::time(name = "realize")]
pub fn realize<'a>(
engine: &mut Engine<'a>,
locator: &mut SplitLocator<'a>,
arenas: &'a Arenas<'a>,
doc_info: Option<&mut DocumentInfo>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<Vec<Pair<'a>>> {
let mut builder = Builder::new(engine, locator, arenas, doc_info);
builder.accept(content, styles)?;
builder.interrupt_par()?;
Ok(builder.sink.finish())
}
/// Realizes content into a flat list of well-known, styled elements.
struct Builder<'a, 'v> {
/// The engine.
engine: &'v mut Engine<'a>,
/// Assigns unique locations to elements.
locator: &'v mut SplitLocator<'a>,
/// Scratch arenas for building.
arenas: &'a Arenas<'a>,
/// Document metadata we have collected from `set document` rules. If this
/// is `None`, we are in a container.
doc_info: Option<&'v mut DocumentInfo>,
/// The output elements of well-known types collected by the builder.
sink: BehavedBuilder<'a>,
/// A builder for a paragraph that might be under construction.
par: ParBuilder<'a>,
/// A builder for a list that might be under construction.
list: ListBuilder<'a>,
/// A builder for a citation group that might be under construction.
cites: CiteGroupBuilder<'a>,
/// Whether we are currently not within any container or show rule output.
/// This is used to determine page styles during layout.
outside: bool,
/// Whether the last item that we visited was a paragraph (with no parbreak
/// in between). This is used for attach spacing.
last_was_par: bool,
}
impl<'a, 'v> Builder<'a, 'v> {
/// Creates a new builder.
fn new(
engine: &'v mut Engine<'a>,
locator: &'v mut SplitLocator<'a>,
arenas: &'a Arenas<'a>,
doc_info: Option<&'v mut DocumentInfo>,
) -> Self {
let outside = doc_info.is_some();
Self {
engine,
locator,
arenas,
doc_info,
sink: BehavedBuilder::default(),
par: ParBuilder::default(),
list: ListBuilder::default(),
cites: CiteGroupBuilder::default(),
outside,
last_was_par: false,
}
}
/// Adds a piece of content to this builder.
fn accept(
&mut self,
mut content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<()> {
// Implicitly wrap math content in an equation if needed
if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() {
content = self
.arenas
.store(EquationElem::new(content.clone()).pack().spanned(content.span()));
}
// Styled elements and sequences can (at least currently) also have
// labels, so this needs to happen before they are handled.
if let Some((tag, realized)) =
process(self.engine, self.locator, content, styles)?
{
self.engine.route.increase();
self.engine.route.check_show_depth().at(content.span())?;
if let Some(tag) = &tag {
self.accept(self.arenas.store(TagElem::packed(tag.clone())), styles)?;
}
let prev_outside = self.outside;
self.outside &= content.is::<ContextElem>();
self.accept(self.arenas.store(realized), styles)?;
self.outside = prev_outside;
if let Some(tag) = tag {
let end = tag.with_kind(TagKind::End);
self.accept(self.arenas.store(TagElem::packed(end)), styles)?;
}
self.engine.route.decrease();
return Ok(());
}
if let Some(styled) = content.to_packed::<StyledElem>() {
return self.styled(styled, styles);
}
if let Some(sequence) = content.to_packed::<SequenceElem>() {
for elem in &sequence.children {
self.accept(elem, styles)?;
}
return Ok(());
}
// Try to merge `content` with an element under construction
// (cite group, list, or par).
if self.cites.accept(content, styles) {
return Ok(());
}
self.interrupt_cites()?;
if self.list.accept(content, styles) {
return Ok(());
}
self.interrupt_list()?;
// Try again because it could be another kind of list.
if self.list.accept(content, styles) {
return Ok(());
}
if self.par.accept(content, styles) {
return Ok(());
}
self.interrupt_par()?;
self.save(content, styles)
}
/// Tries to save a piece of content into the sink.
fn save(&mut self, content: &'a Content, styles: StyleChain<'a>) -> SourceResult<()> {
let last_was_par = std::mem::replace(&mut self.last_was_par, false);
let par_spacing = Lazy::new(|| {
self.arenas
.store(VElem::par_spacing(ParElem::spacing_in(styles).into()).pack())
});
if content.is::<TagElem>()
|| content.is::<PlaceElem>()
|| content.is::<FlushElem>()
|| content.is::<ColbreakElem>()
{
self.sink.push(content, styles);
} else if content.is::<PagebreakElem>() {
if self.doc_info.is_none() {
bail!(content.span(), "pagebreaks are not allowed inside of containers");
}
self.sink.push(content, styles);
} else if let Some(elem) = content.to_packed::<VElem>() {
if !elem.attach(styles) || last_was_par {
self.sink.push(content, styles);
}
} else if content.is::<ParbreakElem>() {
// It's only a boundary, so we can ignore it.
} else if content.is::<ParElem>() {
self.sink.push(*par_spacing, styles);
self.sink.push(content, styles);
self.sink.push(*par_spacing, styles);
self.last_was_par = true;
} else if let Some(elem) = content.to_packed::<BlockElem>() {
let above = match elem.above(styles) {
Smart::Auto => *par_spacing,
Smart::Custom(above) => {
self.arenas.store(VElem::block_spacing(above).pack())
}
};
let below = match elem.below(styles) {
Smart::Auto => *par_spacing,
Smart::Custom(below) => {
self.arenas.store(VElem::block_spacing(below).pack())
}
};
self.sink.push(above, styles);
self.sink.push(content, styles);
self.sink.push(below, styles);
} else {
bail!(content.span(), "{} is not allowed here", content.func().name());
}
Ok(())
}
/// Handles a styled element.
fn styled(
&mut self,
styled: &'a StyledElem,
styles: StyleChain<'a>,
) -> SourceResult<()> {
if let Some(span) = styled.styles.interruption::<DocumentElem>() {
let Some(info) = &mut self.doc_info else {
bail!(span, "document set rules are not allowed inside of containers");
};
info.populate(&styled.styles);
}
let page_interruption = styled.styles.interruption::<PageElem>();
if let Some(span) = page_interruption {
if self.doc_info.is_none() {
bail!(span, "page configuration is not allowed inside of containers");
}
// When there are page styles, we "break free" from our show rule
// cage.
self.outside = true;
}
// If we are not within a container or show rule, mark the styles as
// "outside". This will allow them to be lifted to the page level.
let outer = self.arenas.store(styles);
let local = if self.outside {
self.arenas.store(styled.styles.clone().outside())
} else {
&styled.styles
};
if page_interruption.is_some() {
// For the starting pagebreak we only want the styles before and
// including the interruptions, not trailing styles that happen to
// be in the same `Styles` list.
let relevant = local
.as_slice()
.trim_end_matches(|style| style.interruption::<PageElem>().is_none());
self.accept(PagebreakElem::shared_weak(), outer.chain(relevant))?;
}
self.interrupt_styles(local)?;
self.accept(&styled.child, outer.chain(local))?;
self.interrupt_styles(local)?;
if page_interruption.is_some() {
// For the ending pagebreak, the styles don't really matter because
// the styles of a "boundary" pagebreak are ignored during layout.
self.accept(PagebreakElem::shared_boundary(), *outer)?;
}
Ok(())
}
/// Inspects the styles and dispatches to the different interruption
/// handlers.
fn interrupt_styles(&mut self, local: &Styles) -> SourceResult<()> {
if local.interruption::<ParElem>().is_some()
|| local.interruption::<AlignElem>().is_some()
{
self.interrupt_par()?;
} else if local.interruption::<ListElem>().is_some()
|| local.interruption::<EnumElem>().is_some()
|| local.interruption::<TermsElem>().is_some()
{
self.interrupt_list()?;
} else if local.interruption::<CiteElem>().is_some() {
self.interrupt_cites()?;
}
Ok(())
}
/// Interrupts paragraph building and adds the resulting paragraph element
/// to the builder.
fn interrupt_par(&mut self) -> SourceResult<()> {
self.interrupt_list()?;
if !self.par.0.is_empty() {
mem::take(&mut self.par).finish(self)?;
}
Ok(())
}
/// Interrupts list building and adds the resulting list element to the
/// builder.
fn interrupt_list(&mut self) -> SourceResult<()> {
self.interrupt_cites()?;
if !self.list.0.is_empty() {
mem::take(&mut self.list).finish(self)?;
}
Ok(())
}
/// Interrupts citation grouping and adds the resulting citation group to
/// the builder.
fn interrupt_cites(&mut self) -> SourceResult<()> {
if !self.cites.0.is_empty() {
mem::take(&mut self.cites).finish(self)?;
}
Ok(())
}
}
/// Builds a [paragraph][ParElem] from paragraph content.
#[derive(Default)]
struct ParBuilder<'a>(BehavedBuilder<'a>);
impl<'a> ParBuilder<'a> {
/// Tries to accept a piece of content.
///
/// Returns true if this content could be merged into the paragraph. If this
/// function returns false, then the content could not be merged, and
/// paragraph building should be interrupted so that the content can be
/// added elsewhere.
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
if Self::is_primary(content) || (!self.0.is_empty() && Self::is_inner(content)) {
self.0.push(content, styles);
return true;
}
false
}
/// Whether this content is of interest to the builder.
fn is_primary(content: &'a Content) -> bool {
content.is::<SpaceElem>()
|| content.is::<TextElem>()
|| content.is::<HElem>()
|| content.is::<LinebreakElem>()
|| content.is::<SmartQuoteElem>()
|| content.is::<InlineElem>()
|| content.is::<BoxElem>()
}
/// Whether this content can merely exist in between interesting items.
fn is_inner(content: &'a Content) -> bool {
content.is::<TagElem>()
}
/// Turns this builder into the resulting list, along with
/// its [style chain][StyleChain].
fn finish(self, builder: &mut Builder<'a, '_>) -> SourceResult<()> {
let buf = self.0.finish();
let trimmed = buf.trim_end_matches(|(c, _)| c.is::<TagElem>());
let staged = &buf[trimmed.len()..];
let span = first_span(trimmed);
let (children, trunk) = StyleVec::create(trimmed);
let elem = Packed::new(ParElem::new(children)).spanned(span);
builder.accept(builder.arenas.store(elem.pack()), trunk)?;
for &(tag, styles) in staged {
builder.accept(tag, styles)?;
}
Ok(())
}
}
/// Builds a list (either [`ListElem`], [`EnumElem`], or [`TermsElem`]) from
/// list or enum items, spaces, and paragraph breaks.
#[derive(Default)]
struct ListBuilder<'a>(Vec<Pair<'a>>);
impl<'a> ListBuilder<'a> {
/// Tries to accept a piece of content.
///
/// Returns true if this content could be merged into the list. If this
/// function returns false, then the content could not be merged, and list
/// building should be interrupted so that the content can be added
/// elsewhere.
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
if (Self::is_primary(content) && self.is_compatible(content))
|| (!self.0.is_empty() && Self::is_inner(content))
{
self.0.push((content, styles));
return true;
}
false
}
/// Whether this content is of interest to the builder.
fn is_primary(content: &'a Content) -> bool {
content.is::<ListItem>() || content.is::<EnumItem>() || content.is::<TermItem>()
}
/// Whether this content can merely exist in between interesting items.
fn is_inner(content: &'a Content) -> bool {
content.is::<TagElem>()
|| content.is::<SpaceElem>()
|| content.is::<ParbreakElem>()
}
/// Whether this kind of list items is compatible with the builder's type.
fn is_compatible(&self, content: &'a Content) -> bool {
self.0
.first()
.map_or(true, |(first, _)| first.func() == content.func())
}
/// Turns this builder into the resulting list, along with
/// its [style chain][StyleChain].
fn finish(self, builder: &mut Builder<'a, '_>) -> SourceResult<()> {
let trimmed = self.0.trim_end_matches(|(c, _)| Self::is_inner(c));
let tags = trimmed.iter().filter(|(c, _)| c.is::<TagElem>());
let staged = &self.0[trimmed.len()..];
let items = trimmed.iter().copied().filter(|(c, _)| Self::is_primary(c));
let first = items.clone().next().unwrap().0;
let tight = !trimmed.iter().any(|(c, _)| c.is::<ParbreakElem>());
// Determine the styles that are shared by all items. These will be
// used for the list itself.
let trunk = StyleChain::trunk(items.clone().map(|(_, s)| s)).unwrap();
let depth = trunk.links().count();
// Builder the correct element.
let iter = items.map(|(c, s)| (c, s.suffix(depth)));
let elem = if first.is::<ListItem>() {
let children = iter
.map(|(item, local)| {
item.to_packed::<ListItem>().unwrap().clone().styled(local)
})
.collect();
ListElem::new(children).with_tight(tight).pack()
} else if first.is::<EnumItem>() {
let children = iter
.map(|(item, local)| {
item.to_packed::<EnumItem>().unwrap().clone().styled(local)
})
.collect();
EnumElem::new(children).with_tight(tight).pack()
} else if first.is::<TermItem>() {
let children = iter
.map(|(item, local)| {
item.to_packed::<TermItem>().unwrap().clone().styled(local)
})
.collect();
TermsElem::new(children).with_tight(tight).pack()
} else {
unreachable!()
};
// Add the list to the builder.
let span = first_span(&self.0);
let stored = builder.arenas.store(elem.spanned(span));
builder.accept(stored, trunk)?;
// Add the tags and staged elements to the builder.
for &(content, styles) in tags.chain(staged) {
builder.accept(content, styles)?;
}
Ok(())
}
}
/// Builds a [citation group][CiteGroup] from citations.
#[derive(Default)]
struct CiteGroupBuilder<'a>(Vec<Pair<'a>>);
impl<'a> CiteGroupBuilder<'a> {
/// Tries to accept a piece of content.
///
/// Returns true if this content could be merged into the citation group. If
/// this function returns false, then the content could not be merged, and
/// citation grouping should be interrupted so that the content can be added
/// elsewhere.
fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
if Self::is_primary(content) || (!self.0.is_empty() && Self::is_inner(content)) {
self.0.push((content, styles));
return true;
}
false
}
/// Whether this content is of interest to the builder.
fn is_primary(content: &'a Content) -> bool {
content.is::<CiteElem>()
}
/// Whether this content can merely exist in between interesting items.
fn is_inner(content: &'a Content) -> bool {
content.is::<TagElem>() || content.is::<SpaceElem>()
}
/// Turns this builder into the resulting citation group, along with
/// its [style chain][StyleChain].
fn finish(self, builder: &mut Builder<'a, '_>) -> SourceResult<()> {
let trimmed = self.0.trim_end_matches(|(c, _)| Self::is_inner(c));
let tags = trimmed.iter().filter(|(c, _)| c.is::<TagElem>());
let staged = &self.0[trimmed.len()..];
let trunk = trimmed[0].1;
let children = trimmed
.iter()
.filter_map(|(c, _)| c.to_packed::<CiteElem>())
.cloned()
.collect();
// Add the citation group to the builder.
let span = first_span(&self.0);
let elem = CiteGroup::new(children).pack();
let stored = builder.arenas.store(elem.spanned(span));
builder.accept(stored, trunk)?;
// Add the tags and staged elements to the builder.
for &(content, styles) in tags.chain(staged) {
builder.accept(content, styles)?;
}
Ok(())
}
}
/// Determine a span for the built collection.
pub fn first_span(children: &[(&Content, StyleChain)]) -> Span {
children
.iter()
.map(|(c, _)| c.span())
.find(|span| !span.is_detached())
.unwrap_or(Span::detached())
}

View File

@ -1,312 +0,0 @@
use std::cell::OnceCell;
use comemo::{Track, Tracked};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
Content, Context, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style,
StyleChain, Styles, Synthesize, Transformation,
};
use crate::introspection::{Locatable, SplitLocator, Tag};
use crate::text::TextElem;
use crate::utils::SmallBitSet;
/// What to do with an element when encountering it during realization.
struct Verdict<'a> {
/// Whether the element is already prepared (i.e. things that should only
/// happen once have happened).
prepared: bool,
/// A map of styles to apply to the element.
map: Styles,
/// An optional show rule transformation to apply to the element.
step: Option<ShowStep<'a>>,
}
/// An optional show rule transformation to apply to the element.
enum ShowStep<'a> {
/// A user-defined transformational show rule.
Recipe(&'a Recipe, RecipeIndex),
/// The built-in show rule.
Builtin,
}
/// Processes the given `target` element when encountering it during realization.
pub fn process(
engine: &mut Engine,
locator: &mut SplitLocator,
target: &Content,
styles: StyleChain,
) -> SourceResult<Option<(Option<Tag>, Content)>> {
let Some(Verdict { prepared, mut map, step }) = verdict(engine, target, styles)
else {
return Ok(None);
};
// Create a fresh copy that we can mutate.
let mut target = target.clone();
// If the element isn't yet prepared (we're seeing it for the first time),
// prepare it.
let mut tag = None;
if !prepared {
tag = prepare(engine, locator, &mut target, &mut map, styles)?;
}
// Apply a step, if there is one.
let output = match step {
Some(step) => {
// Errors in show rules don't terminate compilation immediately. We
// just continue with empty content for them and show all errors
// together, if they remain by the end of the introspection loop.
//
// This way, we can ignore errors that only occur in earlier
// iterations and also show more useful errors at once.
engine.delay(|engine| show(engine, target, step, styles.chain(&map)))
}
None => target,
};
Ok(Some((tag, output.styled_with_map(map))))
}
/// Inspects a target element and the current styles and determines how to
/// proceed with the styling.
fn verdict<'a>(
engine: &mut Engine,
target: &'a Content,
styles: StyleChain<'a>,
) -> Option<Verdict<'a>> {
let mut target = target;
let mut map = Styles::new();
let mut revoked = SmallBitSet::new();
let mut step = None;
let mut slot;
let depth = OnceCell::new();
let prepared = target.is_prepared();
// Do pre-synthesis on a cloned element to be able to match on synthesized
// fields before real synthesis runs (during preparation). It's really
// unfortunate that we have to do this, but otherwise
// `show figure.where(kind: table)` won't work :(
if !prepared && target.can::<dyn Synthesize>() {
slot = target.clone();
slot.with_mut::<dyn Synthesize>()
.unwrap()
.synthesize(engine, styles)
.ok();
target = &slot;
}
let mut r = 0;
for entry in styles.entries() {
let recipe = match &**entry {
Style::Recipe(recipe) => recipe,
Style::Property(_) => continue,
Style::Revocation(index) => {
revoked.insert(index.0);
continue;
}
};
// We're not interested in recipes that don't match.
if !recipe.applicable(target, styles) {
r += 1;
continue;
}
// Special handling for show-set rules. Exception: Regex show rules,
// those need to be handled like normal transformations.
if let (Transformation::Style(transform), false) =
(recipe.transform(), matches!(recipe.selector(), Some(Selector::Regex(_))))
{
// If this is a show-set for an unprepared element, we need to apply
// it.
if !prepared {
map.apply(transform.clone());
}
} else if step.is_none() {
// Lazily compute the total number of recipes in the style chain. We
// need it to determine whether a particular show rule was already
// applied to the `target` previously. For this purpose, show rules
// are indexed from the top of the chain as the chain might grow to
// the bottom.
let depth = *depth.get_or_init(|| {
styles.entries().filter_map(|style| style.recipe()).count()
});
let index = RecipeIndex(depth - r);
if !target.is_guarded(index) && !revoked.contains(index.0) {
// If we find a matching, unguarded replacement show rule,
// remember it, but still continue searching for potential
// show-set styles that might change the verdict.
step = Some(ShowStep::Recipe(recipe, index));
// If we found a show rule and are already prepared, there is
// nothing else to do, so we can just break.
if prepared {
break;
}
}
}
r += 1;
}
// If we found no user-defined rule, also consider the built-in show rule.
if step.is_none() && target.can::<dyn Show>() {
step = Some(ShowStep::Builtin);
}
// If there's no nothing to do, there is also no verdict.
if step.is_none()
&& map.is_empty()
&& (prepared || {
target.label().is_none()
&& target.location().is_none()
&& !target.can::<dyn ShowSet>()
&& !target.can::<dyn Locatable>()
&& !target.can::<dyn Synthesize>()
})
{
return None;
}
Some(Verdict { prepared, map, step })
}
/// This is only executed the first time an element is visited.
fn prepare(
engine: &mut Engine,
locator: &mut SplitLocator,
target: &mut Content,
map: &mut Styles,
styles: StyleChain,
) -> SourceResult<Option<Tag>> {
// Generate a location for the element, which uniquely identifies it in
// the document. This has some overhead, so we only do it for elements
// that are explicitly marked as locatable and labelled elements.
//
// The element could already have a location even if it is not prepared
// when it stems from a query.
let mut key = None;
if target.location().is_some() {
key = Some(crate::utils::hash128(&target));
} else if target.can::<dyn Locatable>() || target.label().is_some() {
let hash = crate::utils::hash128(&target);
let location = locator.next_location(engine.introspector, hash);
target.set_location(location);
key = Some(hash);
}
// Apply built-in show-set rules. User-defined show-set rules are already
// considered in the map built while determining the verdict.
if let Some(show_settable) = target.with::<dyn ShowSet>() {
map.apply(show_settable.show_set(styles));
}
// If necessary, generated "synthesized" fields (which are derived from
// other fields or queries). Do this after show-set so that show-set styles
// are respected.
if let Some(synthesizable) = target.with_mut::<dyn Synthesize>() {
synthesizable.synthesize(engine, styles.chain(map))?;
}
// Copy style chain fields into the element itself, so that they are
// available in rules.
target.materialize(styles.chain(map));
// If the element is locatable, create a tag element to be able to find the
// element in the frames after layout. Do this after synthesis and
// materialization, so that it includes the synthesized fields. Do it before
// marking as prepared so that show-set rules will apply to this element
// when queried.
let tag = key.map(|key| Tag::new(target.clone(), key));
// Ensure that this preparation only runs once by marking the element as
// prepared.
target.mark_prepared();
Ok(tag)
}
/// Apply a step.
fn show(
engine: &mut Engine,
target: Content,
step: ShowStep,
styles: StyleChain,
) -> SourceResult<Content> {
match step {
// Apply a user-defined show rule.
ShowStep::Recipe(recipe, guard) => {
let context = Context::new(target.location(), Some(styles));
match recipe.selector() {
// If the selector is a regex, the `target` is guaranteed to be a
// text element. This invokes special regex handling.
Some(Selector::Regex(regex)) => {
let text = target.into_packed::<TextElem>().unwrap();
show_regex(engine, &text, regex, recipe, guard, context.track())
}
// Just apply the recipe.
_ => recipe.apply(engine, context.track(), target.guarded(guard)),
}
}
// If the verdict picks this step, the `target` is guaranteed to have a
// built-in show rule.
ShowStep::Builtin => target.with::<dyn Show>().unwrap().show(engine, styles),
}
}
/// Apply a regex show rule recipe to a target.
fn show_regex(
engine: &mut Engine,
target: &Packed<TextElem>,
regex: &Regex,
recipe: &Recipe,
index: RecipeIndex,
context: Tracked<Context>,
) -> SourceResult<Content> {
let make = |s: &str| {
let mut fresh = target.clone();
fresh.push_text(s.into());
fresh.pack()
};
let mut result = vec![];
let mut cursor = 0;
let text = target.text();
for m in regex.find_iter(target.text()) {
let start = m.start();
if cursor < start {
result.push(make(&text[cursor..start]));
}
let piece = make(m.as_str());
let transformed = recipe.apply(engine, context, piece)?;
result.push(transformed);
cursor = m.end();
}
if cursor < text.len() {
result.push(make(&text[cursor..]));
}
// In contrast to normal elements, which are guarded individually, for text
// show rules, we fully revoke the rule. This means that we can replace text
// with other text that rematches without running into infinite recursion
// problems.
//
// We do _not_ do this for all content because revoking e.g. a list show
// rule for all content resulting from that rule would be wrong: The list
// might contain nested lists. Moreover, replacing a normal element with one
// that rematches is bad practice: It can for instance also lead to
// surprising query results, so it's better to let the user deal with it.
// All these problems don't exist for text, so it's fine here.
Ok(Content::sequence(result).styled(Style::Revocation(index)))
}

View File

@ -1,5 +1,4 @@
use crate::foundations::{elem, Content, NativeElement, Packed};
use crate::realize::{Behave, Behaviour};
use crate::foundations::{elem, Content, NativeElement};
use crate::utils::singleton;
/// Inserts a line break.
@ -19,7 +18,7 @@ use crate::utils::singleton;
/// This function also has dedicated syntax: To insert a line break, simply write
/// a backslash followed by whitespace. This always creates an unjustified
/// break.
#[elem(title = "Line Break", Behave)]
#[elem(title = "Line Break")]
pub struct LinebreakElem {
/// Whether to justify the line before the break.
///
@ -44,9 +43,3 @@ impl LinebreakElem {
singleton!(Content, LinebreakElem::new().pack())
}
}
impl Behave for Packed<LinebreakElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Destructive
}
}

View File

@ -20,8 +20,7 @@ use crate::layout::{BlockChild, BlockElem, Em, HAlignment};
use crate::model::{Figurable, ParElem};
use crate::syntax::{split_newlines, LinkedNode, Span, Spanned};
use crate::text::{
FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, SmartQuoteElem, TextElem,
TextSize,
FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
};
use crate::visualize::Color;
use crate::{syntax, World};
@ -468,7 +467,6 @@ impl ShowSet for Packed<RawElem> {
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
out.set(SmartQuoteElem::set_enabled(false));
if self.block(styles) {
out.set(ParElem::set_shrink(false));
}

View File

@ -3,11 +3,10 @@ use ecow::EcoString;
use crate::foundations::{
elem, Content, NativeElement, Packed, PlainText, Repr, Unlabellable,
};
use crate::realize::{Behave, Behaviour};
use crate::utils::singleton;
/// A text space.
#[elem(Behave, Unlabellable, PlainText, Repr)]
#[elem(Unlabellable, PlainText, Repr)]
pub struct SpaceElem {}
impl SpaceElem {
@ -23,12 +22,6 @@ impl Repr for SpaceElem {
}
}
impl Behave for Packed<SpaceElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Weak(2)
}
}
impl Unlabellable for Packed<SpaceElem> {}
impl PlainText for Packed<SpaceElem> {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 727 B

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 B

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 B

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -19,15 +19,6 @@ The end.
#let b = [*B*]
#a <v> #b
--- label-on-text ---
// Test labelled text.
#show "t": it => {
set text(blue) if it.has("label") and it.label == <last>
it
}
This is a thing #[that <last>] happened.
--- label-dynamic-show-set ---
// Test abusing dynamic labels for styling.
#show <red>: set text(red)

View File

@ -56,6 +56,6 @@ Mix-and-match all the previous tests.
#counter("dummy").step()
#place(dx: -0.5cm, dy: -0.75cm, box(width: 200%)[OOF])
#line(length: 100%)
#place(dy: -0.8em)[OOF]
#place(dy: 0.2em)[OOF]
#rect(height: 2cm, fill: gray)
]

View File

@ -184,8 +184,8 @@
Top
#align(bottom)[
Bottom \
Bottom \
#v(0pt)
Bottom
Top
]
],

View File

@ -54,9 +54,9 @@ Second
--- place-float ---
#set page(height: 140pt)
#set place(clearance: 5pt)
#lorem(6)
#place(auto, float: true, rect[A])
#place(auto, float: true, rect[B])
#lorem(6)
#place(auto, float: true, rect[C])
#place(auto, float: true, rect[D])

View File

@ -131,3 +131,66 @@ Heya
#show "Heya": set text(red)
#show "yaho": set text(weight: "bold")
Heyaho
--- show-text-smartquote ---
#show "up,\" she": set text(red)
"What's up," she asked.
--- show-text-apostrophe ---
#show regex("Who's|We've"): highlight
Who's got it? \
We've got it.
--- show-text-citation ---
#show "hey": [@arrgh]
@netwok hey
#show bibliography: none
#bibliography("/assets/bib/works.bib")
--- show-text-list ---
#show "hi": [- B]
- A
hi
- C
--- show-text-citation-smartquote ---
#show "hey \"": [@arrgh]
#show "dis": [@distress]
@netwok hey " dis
#show bibliography: none
#bibliography("/assets/bib/works.bib")
--- show-text-linebreak ---
#show "lo\nwo": set text(red)
Hello #[ ] \
#[ ] #[ ] world!
--- show-text-after-normal-show ---
#show rect: "world"
#show "lo wo": set text(red)
hello #rect()
--- show-text-space-collapsing ---
#show "i ther": set text(red)
hi#[ ]#[ ]the#"re"
--- show-text-style-boundary ---
#show "What's up": set text(blue)
#show "your party": underline
What's #[ ] up at #"your" #text(red)[party?]
--- show-text-within-par ---
#show "Pythagoras'": highlight
$a^2 + b^2 = c^2$ is Pythagoras' theorem.
--- show-text-outer-space ---
// Spaces must be interior to strong textual elements for matching to work.
// For outer spaces, it is hard to say whether they would collapse.
#show "a\n": set text(blue)
#show "b\n ": set text(blue)
#show " c ": set text(blue)
a \ #h(0pt, weak: true)
b \ #h(0pt, weak: true)
$x$ c $y$