New realization / Text show rules now work across elements (#4876)
2
Cargo.lock
generated
@ -2632,8 +2632,10 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
name = "typst"
|
||||
version = "0.11.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"az",
|
||||
"bitflags 2.6.0",
|
||||
"bumpalo",
|
||||
"chinese-number",
|
||||
"ciborium",
|
||||
"comemo",
|
||||
|
@ -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"] }
|
||||
|
@ -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 }
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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> {}
|
||||
|
@ -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")?,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
@ -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)))
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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> {
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 703 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 727 B After Width: | Height: | Size: 716 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 458 B |
Before Width: | Height: | Size: 249 B After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 653 B After Width: | Height: | Size: 574 B |
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 574 B |
BIN
tests/ref/show-text-after-normal-show.png
Normal file
After Width: | Height: | Size: 736 B |
BIN
tests/ref/show-text-apostrophe.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/ref/show-text-citation-smartquote.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
tests/ref/show-text-citation.png
Normal file
After Width: | Height: | Size: 527 B |
BIN
tests/ref/show-text-linebreak.png
Normal file
After Width: | Height: | Size: 829 B |
BIN
tests/ref/show-text-list.png
Normal file
After Width: | Height: | Size: 312 B |
BIN
tests/ref/show-text-outer-space.png
Normal file
After Width: | Height: | Size: 548 B |
BIN
tests/ref/show-text-smartquote.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/show-text-space-collapsing.png
Normal file
After Width: | Height: | Size: 566 B |
BIN
tests/ref/show-text-style-boundary.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/ref/show-text-within-par.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
@ -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)
|
||||
|
@ -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)
|
||||
]
|
||||
|
@ -184,8 +184,8 @@
|
||||
Top
|
||||
#align(bottom)[
|
||||
Bottom \
|
||||
Bottom \
|
||||
#v(0pt)
|
||||
Bottom
|
||||
|
||||
Top
|
||||
]
|
||||
],
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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$
|
||||
|