Improve realization and page layout (#4840)

This commit is contained in:
Laurenz 2024-08-26 19:17:58 +02:00 committed by GitHub
parent cb98eec609
commit 4365e18454
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 1710 additions and 1030 deletions

View File

@ -106,6 +106,18 @@ impl<T> OptionExt<T> for Option<T> {
/// Extra methods for [`[T]`](slice).
pub trait SliceExt<T> {
/// Returns a slice with all matching elements from the start of the slice
/// removed.
fn trim_start_matches<F>(&self, f: F) -> &[T]
where
F: FnMut(&T) -> bool;
/// Returns a slice with all matching elements from the end of the slice
/// removed.
fn trim_end_matches<F>(&self, f: F) -> &[T]
where
F: FnMut(&T) -> bool;
/// Split a slice into consecutive runs with the same key and yield for
/// each such run the key and the slice of elements with that key.
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F>
@ -115,6 +127,29 @@ pub trait SliceExt<T> {
}
impl<T> SliceExt<T> for [T] {
fn trim_start_matches<F>(&self, mut f: F) -> &[T]
where
F: FnMut(&T) -> bool,
{
let len = self.len();
let mut i = 0;
while i < len && f(&self[i]) {
i += 1;
}
&self[i..]
}
fn trim_end_matches<F>(&self, mut f: F) -> &[T]
where
F: FnMut(&T) -> bool,
{
let mut i = self.len();
while i > 0 && f(&self[i - 1]) {
i -= 1;
}
&self[..i]
}
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> {
GroupByKey { slice: self, f }
}

View File

@ -24,7 +24,7 @@ impl Eval for ast::SetRule<'_> {
})
.at(target.span())?;
let args = self.args().eval(vm)?.spanned(self.span());
Ok(target.set(&mut vm.engine, args)?.spanned(self.span()))
Ok(target.set(&mut vm.engine, args)?.spanned(self.span()).liftable())
}
}
@ -46,6 +46,6 @@ impl Eval for ast::ShowRule<'_> {
expr => expr.eval(vm)?.cast::<Transformation>().at(span)?,
};
Ok(Recipe { span, selector, transform })
Ok(Recipe::new(selector, transform, span))
}
}

View File

@ -369,7 +369,7 @@ impl Content {
context: Tracked<Context>,
recipe: Recipe,
) -> SourceResult<Self> {
if recipe.selector.is_none() {
if recipe.selector().is_none() {
recipe.apply(engine, context, self)
} else {
Ok(self.styled(recipe))

View File

@ -93,6 +93,11 @@ impl Styles {
self.0.iter().map(|style| &**style)
}
/// Iterate over the contained styles.
pub fn as_slice(&self) -> &[LazyHash<Style>] {
self.0.as_slice()
}
/// Set an inner value for a style property.
///
/// If the property needs folding and the value is already contained in the
@ -118,16 +123,33 @@ impl Styles {
self.0.insert(0, LazyHash::new(outer));
}
/// Apply a slice of outer styles.
pub fn apply_slice(&mut self, outer: &[LazyHash<Style>]) {
self.0 = outer.iter().cloned().chain(mem::take(self).0).collect();
}
/// Add an origin span to all contained properties.
pub fn spanned(mut self, span: Span) -> Self {
for entry in self.0.make_mut() {
if let Style::Property(property) = &mut **entry {
property.span = Some(span);
property.span = span;
}
}
self
}
/// Marks the styles as having been applied outside of any show rule.
pub fn outside(mut self) -> Self {
for entry in self.0.make_mut() {
match &mut **entry {
Style::Property(property) => property.outside = true,
Style::Recipe(recipe) => recipe.outside = true,
_ => {}
}
}
self
}
/// Marks the styles as being allowed to be lifted up to the page level.
pub fn liftable(mut self) -> Self {
for entry in self.0.make_mut() {
if let Style::Property(property) = &mut **entry {
property.liftable = true;
}
}
self
@ -144,13 +166,8 @@ impl Styles {
/// Returns `Some(_)` with an optional span if this list contains
/// styles for the given element.
pub fn interruption<T: NativeElement>(&self) -> Option<Option<Span>> {
let elem = T::elem();
self.0.iter().find_map(|entry| match &**entry {
Style::Property(property) => property.is_of(elem).then_some(property.span),
Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.span)),
Style::Revocation(_) => None,
})
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
@ -176,6 +193,21 @@ impl From<Style> for Styles {
}
}
impl IntoIterator for Styles {
type Item = LazyHash<Style>;
type IntoIter = ecow::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<LazyHash<Style>> for Styles {
fn from_iter<T: IntoIterator<Item = LazyHash<Style>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl Debug for Styles {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Styles ")?;
@ -216,6 +248,37 @@ impl Style {
_ => None,
}
}
/// 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();
match self {
Style::Property(property) => property.is_of(elem).then_some(property.span),
Style::Recipe(recipe) => recipe.is_of(elem).then_some(recipe.span),
Style::Revocation(_) => None,
}
}
/// Whether the style is allowed to be lifted up to the page level. Only
/// true for styles originating from set rules.
pub fn liftable(&self) -> bool {
match self {
Self::Property(property) => property.liftable,
Self::Recipe(_) => true,
Self::Revocation(_) => false,
}
}
/// Whether the style was applied outside of any show rule. This is set
/// during realization.
pub fn outside(&self) -> bool {
match self {
Self::Property(property) => property.outside,
Self::Recipe(recipe) => recipe.outside,
Self::Revocation(_) => false,
}
}
}
impl Debug for Style {
@ -250,7 +313,11 @@ pub struct Property {
/// The property's value.
value: Block,
/// The span of the set rule the property stems from.
span: Option<Span>,
span: Span,
/// Whether the property is allowed to be lifted up to the page level.
liftable: bool,
/// Whether the property was applied outside of any show rule.
outside: bool,
}
impl Property {
@ -264,7 +331,9 @@ impl Property {
elem: E::elem(),
id,
value: Block::new(value),
span: None,
span: Span::detached(),
liftable: false,
outside: false,
}
}
@ -370,19 +439,41 @@ impl Hash for dyn Blockable {
/// A show rule recipe.
#[derive(Clone, PartialEq, Hash)]
pub struct Recipe {
/// The span that errors are reported with.
pub span: Span,
/// Determines whether the recipe applies to an element.
///
/// If this is `None`, then this recipe is from a show rule with
/// no selector (`show: rest => ...`), which is [eagerly applied][Content::styled_with_recipe]
/// to the rest of the content in the scope.
pub selector: Option<Selector>,
selector: Option<Selector>,
/// The transformation to perform on the match.
pub transform: Transformation,
transform: Transformation,
/// The span that errors are reported with.
span: Span,
/// Relevant properties of the kind of construct the style originated from
/// and where it was applied.
outside: bool,
}
impl Recipe {
/// Create a new recipe from a key-value pair.
pub fn new(
selector: Option<Selector>,
transform: Transformation,
span: Span,
) -> Self {
Self { selector, transform, span, outside: false }
}
/// The recipe's selector.
pub fn selector(&self) -> Option<&Selector> {
self.selector.as_ref()
}
/// The recipe's transformation.
pub fn transform(&self) -> &Transformation {
&self.transform
}
/// Whether this recipe is for the given type of element.
pub fn is_of(&self, element: Element) -> bool {
match self.selector {
@ -494,7 +585,7 @@ impl<'a> StyleChain<'a> {
/// `self`. For folded properties `local` contributes the inner value.
pub fn chain<'b, C>(&'b self, local: &'b C) -> StyleChain<'b>
where
C: Chainable,
C: Chainable + ?Sized,
{
Chainable::chain(local, self)
}
@ -557,7 +648,7 @@ impl<'a> StyleChain<'a> {
) -> impl Iterator<Item = &'a T> {
inherent.into_iter().chain(
self.entries()
.filter_map(Style::property)
.filter_map(|style| style.property())
.filter(move |property| property.is(func, id))
.map(|property| &property.value)
.map(move |value| {
@ -573,15 +664,6 @@ impl<'a> StyleChain<'a> {
)
}
/// Convert to a style map.
pub fn to_map(self) -> Styles {
let mut suffix = Styles::new();
for link in self.links() {
suffix.apply_slice(link);
}
suffix
}
/// Iterate over the entries of the chain.
pub fn entries(self) -> Entries<'a> {
Entries { inner: [].as_slice().iter(), links: self.links() }
@ -592,21 +674,59 @@ impl<'a> StyleChain<'a> {
Links(Some(self))
}
/// Convert to a style map.
pub fn to_map(self) -> Styles {
let mut styles: EcoVec<_> = self.entries().cloned().collect();
styles.make_mut().reverse();
Styles(styles)
}
/// Build owned styles from the suffix (all links beyond the `len`) of the
/// chain.
pub fn suffix(self, len: usize) -> Styles {
let mut suffix = Styles::new();
let mut styles = EcoVec::new();
let take = self.links().count().saturating_sub(len);
for link in self.links().take(take) {
suffix.apply_slice(link);
styles.extend(link.iter().cloned().rev());
}
suffix
styles.make_mut().reverse();
Styles(styles)
}
/// Remove the last link from the chain.
pub fn pop(&mut self) {
*self = self.tail.copied().unwrap_or_default();
}
/// Determine the shared trunk of a collection of style chains.
pub fn trunk(iter: impl IntoIterator<Item = Self>) -> Option<Self> {
// Determine shared style depth and first span.
let mut iter = iter.into_iter();
let mut trunk = iter.next()?;
let mut depth = trunk.links().count();
for mut chain in iter {
let len = chain.links().count();
if len < depth {
for _ in 0..depth - len {
trunk.pop();
}
depth = len;
} else if len > depth {
for _ in 0..len - depth {
chain.pop();
}
}
while depth > 0 && chain != trunk {
trunk.pop();
chain.pop();
depth -= 1;
}
}
Some(trunk)
}
}
impl Debug for StyleChain<'_> {
@ -673,7 +793,7 @@ pub struct Entries<'a> {
}
impl<'a> Iterator for Entries<'a> {
type Item = &'a Style;
type Item = &'a LazyHash<Style>;
fn next(&mut self) -> Option<Self::Item> {
loop {
@ -702,6 +822,107 @@ impl<'a> Iterator for Links<'a> {
}
}
/// A sequence of elements with associated styles.
#[derive(Clone, PartialEq, Hash)]
pub struct StyleVec {
/// The elements themselves.
elements: EcoVec<Content>,
/// A run-length encoded list of style lists.
///
/// Each element is a (styles, count) pair. Any elements whose
/// style falls after the end of this list is considered to
/// have an empty style list.
styles: EcoVec<(Styles, usize)>,
}
impl StyleVec {
/// Create a style vector from an unstyled vector content.
pub fn wrap(elements: EcoVec<Content>) -> Self {
Self { elements, styles: EcoVec::new() }
}
/// Create a `StyleVec` from a list of content with style chains.
pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) {
let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default();
let depth = trunk.links().count();
let mut elements = EcoVec::with_capacity(buf.len());
let mut styles = EcoVec::<(Styles, usize)>::new();
let mut last: Option<(StyleChain<'a>, usize)> = None;
for &(element, chain) in buf {
elements.push(element.clone());
if let Some((prev, run)) = &mut last {
if chain == *prev {
*run += 1;
} else {
styles.push((prev.suffix(depth), *run));
last = Some((chain, 1));
}
} else {
last = Some((chain, 1));
}
}
if let Some((last, run)) = last {
let skippable = styles.is_empty() && last == trunk;
if !skippable {
styles.push((last.suffix(depth), run));
}
}
(StyleVec { elements, styles }, trunk)
}
/// Whether there are no elements.
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
/// The number of elements.
pub fn len(&self) -> usize {
self.elements.len()
}
/// Iterate over the contained content and style chains.
pub fn iter<'a>(
&'a self,
outer: &'a StyleChain<'_>,
) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> {
static EMPTY: Styles = Styles::new();
self.elements
.iter()
.zip(
self.styles
.iter()
.flat_map(|(local, count)| std::iter::repeat(local).take(*count))
.chain(std::iter::repeat(&EMPTY)),
)
.map(|(element, local)| (element, outer.chain(local)))
}
/// Get a style property, but only if it is the same for all children of the
/// style vector.
pub fn shared_get<T: PartialEq>(
&self,
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
self.styles
.iter()
.all(|(local, _)| getter(styles.chain(local)) == value)
.then_some(value)
}
}
impl Debug for StyleVec {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_list().entries(&self.elements).finish()
}
}
/// A property that is resolved with other properties from the style chain.
pub trait Resolve {
/// The type of the resolved output.

View File

@ -825,7 +825,7 @@ impl ManualPageCounter {
match item {
FrameItem::Group(group) => self.visit(engine, &group.frame)?,
FrameItem::Tag(tag) => {
let Some(elem) = tag.elem.to_packed::<CounterUpdateElem>() else {
let Some(elem) = tag.elem().to_packed::<CounterUpdateElem>() else {
continue;
};
if *elem.key() == CounterKey::Page {

View File

@ -10,7 +10,7 @@ use smallvec::SmallVec;
use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector};
use crate::introspection::Location;
use crate::introspection::{Location, TagKind};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering;
use crate::utils::NonZeroExt;
@ -66,20 +66,21 @@ impl Introspector {
self.extract(&group.frame, page, ts);
}
FrameItem::Tag(tag)
if !self.elems.contains_key(&tag.elem.location().unwrap()) =>
if tag.kind() == TagKind::Start
&& !self.elems.contains_key(&tag.location()) =>
{
let pos = pos.transform(ts);
let loc = tag.elem.location().unwrap();
let loc = tag.location();
let ret = self
.elems
.insert(loc, (tag.elem.clone(), Position { page, point: pos }));
.insert(loc, (tag.elem().clone(), Position { page, point: pos }));
assert!(ret.is_none(), "duplicate locations");
// Build the key map.
self.keys.entry(tag.key).or_default().push(loc);
self.keys.entry(tag.key()).or_default().push(loc);
// Build the label cache.
if let Some(label) = tag.elem.label() {
if let Some(label) = tag.elem().label() {
self.labels.entry(label).or_default().push(self.elems.len() - 1);
}
}

View File

@ -12,6 +12,7 @@ mod metadata;
#[path = "query.rs"]
mod query_;
mod state;
mod tag;
pub use self::counter::*;
pub use self::here_::*;
@ -22,16 +23,9 @@ pub use self::locator::*;
pub use self::metadata::*;
pub use self::query_::*;
pub use self::state::*;
pub use self::tag::*;
use std::fmt::{self, Debug, Formatter};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
category, elem, Args, Category, Construct, Content, NativeElement, Packed, Scope,
Unlabellable,
};
use crate::realize::{Behave, Behaviour};
use crate::foundations::{category, Category, Scope};
/// Interactions between document parts.
///
@ -57,65 +51,3 @@ pub fn define(global: &mut Scope) {
global.define_func::<query>();
global.define_func::<locate>();
}
/// Holds a tag for a locatable element that was realized.
///
/// The `TagElem` is handled by all layouters. The held element becomes
/// available for introspection in the next compiler iteration.
#[elem(Behave, Unlabellable, Construct)]
pub struct TagElem {
/// The introspectible element.
#[required]
#[internal]
pub tag: Tag,
}
impl TagElem {
/// Create a packed tag element.
pub fn packed(tag: Tag) -> Content {
let mut content = Self::new(tag).pack();
// We can skip preparation for the `TagElem`.
content.mark_prepared();
content
}
}
impl Construct for TagElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually")
}
}
impl Unlabellable for Packed<TagElem> {}
impl Behave for Packed<TagElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Invisible
}
}
/// Holds a locatable element that was realized.
#[derive(Clone, PartialEq, Hash)]
pub struct Tag {
/// The introspectible element.
pub elem: Content,
/// The element's key hash, which forms the base of its location (but is
/// locally disambiguated and combined with outer hashes).
///
/// We need to retain this for introspector-assisted location assignment
/// during measurement.
pub(crate) key: u128,
}
impl Tag {
/// Create a tag from an element and its key hash.
pub fn new(elem: Content, key: u128) -> Self {
Self { elem, key }
}
}
impl Debug for Tag {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Tag({:?})", self.elem.elem().name())
}
}

View File

@ -0,0 +1,111 @@
use std::fmt::{self, Debug, Formatter};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
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)]
pub struct Tag {
/// Whether this is a start or end tag.
kind: TagKind,
/// The introspectible element.
elem: Content,
/// The element's key hash.
key: u128,
}
impl Tag {
/// Create a start tag from an element and its key hash.
///
/// Panics if the element does not have a [`Location`].
#[track_caller]
pub fn new(elem: Content, key: u128) -> Self {
assert!(elem.location().is_some());
Self { elem, key, kind: TagKind::Start }
}
/// Returns the same tag with the given kind.
pub fn with_kind(self, kind: TagKind) -> Self {
Self { kind, ..self }
}
/// Whether this is a start or end tag.
pub fn kind(&self) -> TagKind {
self.kind
}
/// The locatable element that the tag holds.
pub fn elem(&self) -> &Content {
&self.elem
}
/// Access the location of the element.
pub fn location(&self) -> Location {
self.elem.location().unwrap()
}
/// The element's key hash, which forms the base of its location (but is
/// locally disambiguated and combined with outer hashes).
///
/// We need to retain this for introspector-assisted location assignment
/// during measurement.
pub fn key(&self) -> u128 {
self.key
}
}
impl Debug for Tag {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Tag({:?}, {:?})", self.kind, self.elem.elem().name())
}
}
/// Determines whether a tag marks the start or end of an element.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum TagKind {
/// The tag indicates that the element starts here.
Start,
/// The tag indicates that the element end here.
End,
}
/// Holds a tag for a locatable element that was realized.
///
/// The `TagElem` is handled by all layouters. The held element becomes
/// available for introspection in the next compiler iteration.
#[elem(Behave, Unlabellable, Construct)]
pub struct TagElem {
/// The introspectible element.
#[required]
#[internal]
pub tag: Tag,
}
impl TagElem {
/// Create a packed tag element.
pub fn packed(tag: Tag) -> Content {
let mut content = Self::new(tag).pack();
// We can skip preparation for the `TagElem`.
content.mark_prepared();
content
}
}
impl Construct for TagElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually")
}
}
impl Unlabellable for Packed<TagElem> {}
impl Behave for Packed<TagElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Ignorant
}
}

File diff suppressed because it is too large Load Diff

View File

@ -125,7 +125,7 @@ pub fn collect<'a>(
consecutive: bool,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len());
let mut iter = children.chain(styles).peekable();
let mut iter = children.iter(styles).peekable();
let mut locator = locator.split();
let outer_dir = TextElem::dir_in(*styles);

View File

@ -18,11 +18,10 @@ use self::shaping::{
};
use crate::diag::SourceResult;
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::StyleChain;
use crate::foundations::{StyleChain, StyleVec};
use crate::introspection::{Introspector, Locator, LocatorLink};
use crate::layout::{Fragment, Size};
use crate::model::ParElem;
use crate::realize::StyleVec;
use crate::World;
/// Range of a substring of text.

View File

@ -8,15 +8,16 @@ use comemo::Track;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, AutoValue, Cast, Content, Context, Dict, Fold, Func, Smart, StyleChain,
Value,
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func,
NativeElement, Packed, Set, Smart, StyleChain, Value,
};
use crate::layout::{
Abs, Alignment, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, Sides,
SpecificAlignment,
Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel,
Sides, SpecificAlignment,
};
use crate::model::Numbering;
use crate::utils::{NonZeroExt, Scalar};
use crate::realize::{Behave, Behaviour};
use crate::utils::{singleton, NonZeroExt, Scalar};
use crate::visualize::{Color, Paint};
/// Layouts its child onto one or multiple pages.
@ -38,11 +39,12 @@ use crate::visualize::{Color, Paint};
///
/// There you go, US friends!
/// ```
#[elem]
#[elem(Construct)]
pub struct PageElem {
/// A standard paper size to set width and height.
#[external]
#[default(Paper::A4)]
#[ghost]
pub paper: Paper,
/// The width of the page.
@ -64,6 +66,7 @@ pub struct PageElem {
.or_else(|| paper.map(|paper| Smart::Custom(paper.width().into())))
)]
#[default(Smart::Custom(Paper::A4.width().into()))]
#[ghost]
pub width: Smart<Length>,
/// The height of the page.
@ -78,6 +81,7 @@ pub struct PageElem {
.or_else(|| paper.map(|paper| Smart::Custom(paper.height().into())))
)]
#[default(Smart::Custom(Paper::A4.height().into()))]
#[ghost]
pub height: Smart<Length>,
/// Whether the page is flipped into landscape orientation.
@ -99,6 +103,7 @@ pub struct PageElem {
/// +1 555 555 5555
/// ```
#[default(false)]
#[ghost]
pub flipped: bool,
/// The page's margins.
@ -138,6 +143,7 @@ pub struct PageElem {
/// )
/// ```
#[fold]
#[ghost]
pub margin: Margin,
/// On which side the pages will be bound.
@ -149,6 +155,7 @@ pub struct PageElem {
///
/// This affects the meaning of the `inside` and `outside` options for
/// margins.
#[ghost]
pub binding: Smart<Binding>,
/// How many columns the page has.
@ -169,6 +176,7 @@ pub struct PageElem {
/// of a rapidly changing climate.
/// ```
#[default(NonZeroUsize::ONE)]
#[ghost]
pub columns: NonZeroUsize,
/// The page's background fill.
@ -192,6 +200,7 @@ pub struct PageElem {
/// *Dark mode enabled.*
/// ```
#[borrowed]
#[ghost]
pub fill: Smart<Option<Paint>>,
/// How to [number]($numbering) the pages.
@ -209,6 +218,7 @@ pub struct PageElem {
/// #lorem(48)
/// ```
#[borrowed]
#[ghost]
pub numbering: Option<Numbering>,
/// The alignment of the page numbering.
@ -228,6 +238,7 @@ pub struct PageElem {
/// #lorem(30)
/// ```
#[default(SpecificAlignment::Both(HAlignment::Center, OuterVAlignment::Bottom))]
#[ghost]
pub number_align: SpecificAlignment<HAlignment, OuterVAlignment>,
/// The page's header. Fills the top margin of each page.
@ -251,11 +262,13 @@ pub struct PageElem {
/// #lorem(19)
/// ```
#[borrowed]
#[ghost]
pub header: Smart<Option<Content>>,
/// The amount the header is raised into the top margin.
#[resolve]
#[default(Ratio::new(0.3).into())]
#[ghost]
pub header_ascent: Rel<Length>,
/// The page's footer. Fills the bottom margin of each page.
@ -287,11 +300,13 @@ pub struct PageElem {
/// #lorem(48)
/// ```
#[borrowed]
#[ghost]
pub footer: Smart<Option<Content>>,
/// The amount the footer is lowered into the bottom margin.
#[resolve]
#[default(Ratio::new(0.3).into())]
#[ghost]
pub footer_descent: Rel<Length>,
/// Content in the page's background.
@ -311,6 +326,7 @@ pub struct PageElem {
/// over the world (of typesetting).
/// ```
#[borrowed]
#[ghost]
pub background: Option<Content>,
/// Content in the page's foreground.
@ -325,6 +341,7 @@ pub struct PageElem {
/// not understand our approach...
/// ```
#[borrowed]
#[ghost]
pub foreground: Option<Content>,
/// The contents of the page(s).
@ -332,13 +349,93 @@ pub struct PageElem {
/// Multiple pages will be created if the content does not fit on a single
/// page. A new page with the page properties prior to the function invocation
/// will be created after the body has been typeset.
#[external]
#[required]
pub body: Content,
}
/// Whether the page should be aligned to an even or odd page.
impl Construct for PageElem {
fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> {
// The page constructor is special: It doesn't create a page element.
// Instead, it just ensures that the passed content lives in a separate
// page and styles it.
let styles = Self::set(engine, args)?;
let body = args.expect::<Content>("body")?;
Ok(Content::sequence([
PagebreakElem::shared_weak().clone(),
// We put an effectless, invisible non-tag element on the page.
// This has two desirable consequences:
// - The page is kept even if the body is empty
// - The page doesn't inherit shared styles from the body
FlushElem::new().pack(),
body,
PagebreakElem::shared_boundary().clone(),
])
.styled_with_map(styles))
}
}
/// A manual page break.
///
/// Must not be used inside any containers.
///
/// # Example
/// ```example
/// The next page contains
/// more details on compound theory.
/// #pagebreak()
///
/// == Compound Theory
/// In 1984, the first ...
/// ```
#[elem(title = "Page Break", Behave)]
pub struct PagebreakElem {
/// If `{true}`, the page break is skipped if the current page is already
/// empty.
#[default(false)]
pub weak: bool,
/// If given, ensures that the next page will be an even/odd page, with an
/// empty page in between if necessary.
///
/// ```example
/// #set page(height: 30pt)
///
/// First.
/// #pagebreak(to: "odd")
/// Third.
/// ```
pub to: Option<Parity>,
/// Whether this pagebreak designates an end boundary of a page run. This is
/// an even weaker version of pagebreak `weak` because it not only doesn't
/// force an empty page, but also doesn't force its initial styles onto a
/// staged empty page.
#[internal]
#[synthesized]
pub clear_to: Option<Parity>,
#[parse(None)]
#[default(false)]
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 {
singleton!(Content, PagebreakElem::new().with_weak(true).pack())
}
/// Get the globally shared boundary pagebreak element.
pub fn shared_boundary() -> &'static Content {
singleton!(
Content,
PagebreakElem::new().with_weak(true).with_boundary(true).pack()
)
}
}
/// A finished page.
@ -598,39 +695,6 @@ impl PageRanges {
}
}
/// A manual page break.
///
/// Must not be used inside any containers.
///
/// # Example
/// ```example
/// The next page contains
/// more details on compound theory.
/// #pagebreak()
///
/// == Compound Theory
/// In 1984, the first ...
/// ```
#[elem(title = "Page Break")]
pub struct PagebreakElem {
/// If `{true}`, the page break is skipped if the current page is already
/// empty.
#[default(false)]
pub weak: bool,
/// If given, ensures that the next page will be an even/odd page, with an
/// empty page in between if necessary.
///
/// ```example
/// #set page(height: 30pt)
///
/// First.
/// #pagebreak(to: "odd")
/// Third.
/// ```
pub to: Option<Parity>,
}
/// Whether something should be even or odd.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Parity {

View File

@ -177,7 +177,7 @@ pub struct FlushElem {}
impl Behave for Packed<FlushElem> {
fn behaviour(&self) -> Behaviour {
Behaviour::Invisible
Behaviour::Ignorant
}
}

View File

@ -11,14 +11,13 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, Packed, StyleChain};
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::math::{
scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment,
LayoutMath, MathFragment, MathRun, MathSize, THICK,
};
use crate::realize::StyleVec;
use crate::syntax::{is_newline, Span};
use crate::text::{
features, BottomEdge, BottomEdgeMetric, Font, TextElem, TextSize, TopEdge,

View File

@ -6,6 +6,7 @@ use ttf_parser::{GlyphId, Rect};
use unicode_math_class::MathClass;
use crate::foundations::StyleChain;
use crate::introspection::Tag;
use crate::layout::{
Abs, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
};
@ -26,6 +27,7 @@ pub enum MathFragment {
Space(Abs),
Linebreak,
Align,
Tag(Tag),
}
impl MathFragment {
@ -74,6 +76,7 @@ impl MathFragment {
pub fn is_ignorant(&self) -> bool {
match self {
Self::Frame(fragment) => fragment.ignorant,
Self::Tag(_) => true,
_ => false,
}
}
@ -87,6 +90,7 @@ impl MathFragment {
Self::Space(_) => MathClass::Space,
Self::Linebreak => MathClass::Space,
Self::Align => MathClass::Special,
Self::Tag(_) => MathClass::Special,
}
}
@ -172,6 +176,11 @@ impl MathFragment {
Self::Glyph(glyph) => glyph.into_frame(),
Self::Variant(variant) => variant.frame,
Self::Frame(fragment) => fragment.frame,
Self::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag(tag));
frame
}
_ => Frame::soft(self.size()),
}
}

View File

@ -47,10 +47,9 @@ use crate::foundations::{
category, Category, Content, Module, Resolve, Scope, SequenceElem, StyleChain,
StyledElem,
};
use crate::introspection::TagElem;
use crate::layout::{BoxElem, Frame, FrameItem, HElem, Point, Size, Spacing, VAlignment};
use crate::realize::Behaviour;
use crate::realize::{process, BehavedBuilder};
use crate::introspection::{TagElem, TagKind};
use crate::layout::{BoxElem, HElem, Spacing, VAlignment};
use crate::realize::{process, BehavedBuilder, Behaviour};
use crate::text::{LinebreakElem, SpaceElem, TextElem};
/// Typst has special [syntax]($syntax/#math) and library functions to typeset
@ -237,8 +236,17 @@ impl LayoutMath for Content {
return elem.layout_math(ctx, styles);
}
if let Some(realized) = process(ctx.engine, &mut ctx.locator, self, styles)? {
return realized.layout_math(ctx, styles);
if let Some((tag, realized)) =
process(ctx.engine, &mut ctx.locator, self, styles)?
{
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)));
}
return Ok(());
}
if self.is::<SequenceElem>() {
@ -302,9 +310,7 @@ impl LayoutMath for Content {
}
if let Some(elem) = self.to_packed::<TagElem>() {
let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag(elem.tag.clone()));
ctx.push(FrameFragment::new(ctx, styles, frame).with_ignorant(true));
ctx.push(MathFragment::Tag(elem.tag.clone()));
return Ok(());
}
@ -321,10 +327,7 @@ impl LayoutMath for Content {
ctx.push(
FrameFragment::new(ctx, styles, frame)
.with_spaced(true)
.with_ignorant(matches!(
self.behaviour(),
Behaviour::Invisible | Behaviour::Ignorant
)),
.with_ignorant(self.behaviour() == Behaviour::Ignorant),
);
Ok(())

View File

@ -3,10 +3,10 @@ use std::fmt::{self, Debug, Formatter};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, Unlabellable,
elem, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, StyleVec,
Unlabellable,
};
use crate::layout::{Em, Length};
use crate::realize::StyleVec;
use crate::utils::singleton;
/// Arranges text, spacing and inline-level elements into a paragraph.

View File

@ -1,12 +1,13 @@
use typed_arena::Arena;
use crate::foundations::{Content, StyleChain};
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> {
@ -32,3 +33,9 @@ impl<'a> Store<'a> for StyleChain<'a> {
arenas.chains.alloc(self)
}
}
impl<'a> Store<'a> for Styles {
fn store(self, arenas: &'a Arenas<'a>) -> &'a Self {
arenas.styles.alloc(self)
}
}

View File

@ -1,10 +1,6 @@
//! Element interaction.
use std::fmt::{Debug, Formatter};
use ecow::EcoVec;
use crate::foundations::{Content, StyleChain, Styles};
use crate::foundations::{Content, StyleChain};
/// How an element interacts with other elements in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@ -19,12 +15,8 @@ pub enum Behaviour {
/// An element that destroys adjacent weak elements.
Destructive,
/// An element that does not interact at all with other elements, having the
/// same effect as if it didn't exist, but has layout extent and/or a visual
/// representation.
/// same effect on them as if it didn't exist.
Ignorant,
/// An element that does not have any layout extent or visual
/// representation.
Invisible,
}
impl Behaviour {
@ -69,15 +61,6 @@ impl<'a> BehavedBuilder<'a> {
self.buf.is_empty()
}
/// Whether the builder has any proper (non-weak & visible) elements.
pub fn has_strong_elements(&self, last: bool) -> bool {
self.buf.iter().any(|(content, _)| {
let behaviour = content.behaviour();
!matches!(behaviour, Behaviour::Weak(_) | Behaviour::Invisible)
|| (last && behaviour == Behaviour::Invisible)
})
}
/// Push an item into the builder.
pub fn push(&mut self, content: &'a Content, styles: StyleChain<'a>) {
let mut behaviour = content.behaviour();
@ -112,7 +95,7 @@ impl<'a> BehavedBuilder<'a> {
self.buf.remove(i);
}
}
Behaviour::Ignorant | Behaviour::Invisible => {
Behaviour::Ignorant => {
behaviour = self.last;
}
}
@ -152,162 +135,3 @@ impl<'a> Default for BehavedBuilder<'a> {
Self::new()
}
}
/// A sequence of elements with associated styles.
#[derive(Clone, PartialEq, Hash)]
pub struct StyleVec {
/// The elements themselves.
elements: EcoVec<Content>,
/// A run-length encoded list of style lists.
///
/// Each element is a (styles, count) pair. Any elements whose
/// style falls after the end of this list is considered to
/// have an empty style list.
styles: EcoVec<(Styles, usize)>,
}
impl StyleVec {
/// Create a style vector from an unstyled vector content.
pub fn wrap(elements: EcoVec<Content>) -> Self {
Self { elements, styles: EcoVec::new() }
}
/// Create a `StyleVec` from a list of content with style chains.
pub fn create<'a>(buf: &[(&Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) {
let (trunk, depth) = determine_style_trunk(buf);
let mut elements = EcoVec::with_capacity(buf.len());
let mut styles = EcoVec::<(Styles, usize)>::new();
let mut last: Option<(StyleChain<'a>, usize)> = None;
for &(element, chain) in buf {
elements.push(element.clone());
if let Some((prev, run)) = &mut last {
if chain == *prev {
*run += 1;
} else {
styles.push((prev.suffix(depth), *run));
last = Some((chain, 1));
}
} else {
last = Some((chain, 1));
}
}
if let Some((last, run)) = last {
let skippable = styles.is_empty() && last == trunk;
if !skippable {
styles.push((last.suffix(depth), run));
}
}
(StyleVec { elements, styles }, trunk)
}
/// Whether there are no elements.
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
/// The number of elements.
pub fn len(&self) -> usize {
self.elements.len()
}
/// The raw, unstyled elements.
pub fn elements(&self) -> &[Content] {
&self.elements
}
/// Get a style property, but only if it is the same for all children of the
/// style vector.
pub fn shared_get<T: PartialEq>(
&self,
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
self.styles
.iter()
.all(|(local, _)| getter(styles.chain(local)) == value)
.then_some(value)
}
/// Iterate over the contained content and style chains.
pub fn chain<'a>(
&'a self,
outer: &'a StyleChain<'_>,
) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> {
self.iter().map(|(element, local)| (element, outer.chain(local)))
}
/// Iterate over pairs of content and styles.
pub fn iter(&self) -> impl Iterator<Item = (&Content, &Styles)> {
static EMPTY: Styles = Styles::new();
self.elements.iter().zip(
self.styles
.iter()
.flat_map(|(local, count)| std::iter::repeat(local).take(*count))
.chain(std::iter::repeat(&EMPTY)),
)
}
/// Iterate over pairs of content and styles.
#[allow(clippy::should_implement_trait)]
pub fn into_iter(self) -> impl Iterator<Item = (Content, Styles)> {
self.elements.into_iter().zip(
self.styles
.into_iter()
.flat_map(|(local, count)| std::iter::repeat(local).take(count))
.chain(std::iter::repeat(Styles::new())),
)
}
}
impl Debug for StyleVec {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_list()
.entries(self.iter().map(|(element, local)| {
typst_utils::debug(|f| {
for style in local.iter() {
writeln!(f, "#{style:?}")?;
}
element.fmt(f)
})
}))
.finish()
}
}
/// Determine the shared trunk style chain.
fn determine_style_trunk<'a, T>(buf: &[(T, StyleChain<'a>)]) -> (StyleChain<'a>, usize) {
// Determine shared style depth and first span.
let mut trunk = match buf.first() {
Some(&(_, chain)) => chain,
None => Default::default(),
};
let mut depth = trunk.links().count();
for (_, mut chain) in buf {
let len = chain.links().count();
if len < depth {
for _ in 0..depth - len {
trunk.pop();
}
depth = len;
} else if len > depth {
for _ in 0..len - depth {
chain.pop();
}
}
while depth > 0 && chain != trunk {
trunk.pop();
chain.pop();
depth -= 1;
}
}
(trunk, depth)
}

View File

@ -1,10 +1,7 @@
//! Realization of content.
//!
//! *Realization* is the process of applying show rules to produce
//! something that can be laid out directly.
//!
//! Currently, there are issues with the realization process, and
//! it is subject to changes in the future.
//! *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;
@ -13,7 +10,7 @@ mod process;
use once_cell::unsync::Lazy;
pub use self::arenas::Arenas;
pub use self::behaviour::{Behave, BehavedBuilder, Behaviour, StyleVec};
pub use self::behaviour::{Behave, BehavedBuilder, Behaviour};
pub use self::process::process;
use std::mem;
@ -21,12 +18,13 @@ use std::mem;
use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route};
use crate::foundations::{
Content, NativeElement, Packed, SequenceElem, Smart, StyleChain, StyledElem, Styles,
Content, ContextElem, NativeElement, Packed, SequenceElem, Smart, StyleChain,
StyleVec, StyledElem, Styles,
};
use crate::introspection::{SplitLocator, TagElem};
use crate::introspection::{SplitLocator, TagElem, TagKind};
use crate::layout::{
AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, InlineElem,
PageElem, PagebreakElem, Parity, PlaceElem, VElem,
AlignElem, BlockElem, BoxElem, ColbreakElem, FlushElem, HElem, InlineElem, PageElem,
PagebreakElem, PlaceElem, VElem,
};
use crate::math::{EquationElem, LayoutMath};
use crate::model::{
@ -35,41 +33,42 @@ use crate::model::{
};
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.
type Pair<'a> = (&'a Content, StyleChain<'a>);
pub type Pair<'a> = (&'a Content, StyleChain<'a>);
/// Realize at the root-level.
#[typst_macros::time(name = "realize root")]
/// Realize at the root level.
#[typst_macros::time(name = "realize")]
pub fn realize_root<'a>(
engine: &mut Engine<'a>,
locator: &mut SplitLocator<'a>,
arenas: &'a Arenas<'a>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<(StyleVec, StyleChain<'a>, DocumentInfo)> {
) -> SourceResult<(Vec<Pair<'a>>, DocumentInfo)> {
let mut builder = Builder::new(engine, locator, arenas, true);
builder.accept(content, styles)?;
builder.interrupt_page(Some(styles), true)?;
Ok(builder.doc.unwrap().finish())
builder.interrupt_par()?;
Ok((builder.sink.finish(), builder.doc_info.unwrap()))
}
/// Realize into a `FlowElem`, an element that is capable of block-level layout.
#[typst_macros::time(name = "realize flow")]
pub fn realize_flow<'a>(
/// Realize at the container level.
#[typst_macros::time(name = "realize")]
pub fn realizer_container<'a>(
engine: &mut Engine<'a>,
locator: &mut SplitLocator<'a>,
arenas: &'a Arenas<'a>,
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<(Packed<FlowElem>, StyleChain<'a>)> {
) -> SourceResult<Vec<Pair<'a>>> {
let mut builder = Builder::new(engine, locator, arenas, false);
builder.accept(content, styles)?;
builder.interrupt_par()?;
Ok(builder.flow.finish())
Ok(builder.sink.finish())
}
/// Builds a document or a flow element from content.
/// Realizes content into a flat list of well-known, styled elements.
struct Builder<'a, 'v> {
/// The engine.
engine: &'v mut Engine<'a>,
@ -77,34 +76,47 @@ struct Builder<'a, 'v> {
locator: &'v mut SplitLocator<'a>,
/// Scratch arenas for building.
arenas: &'a Arenas<'a>,
/// The current document building state.
doc: Option<DocBuilder<'a>>,
/// The current flow building state.
flow: FlowBuilder<'a>,
/// The current paragraph building state.
/// The output elements of well-known types collected by the builder.
sink: BehavedBuilder<'a>,
/// Document metadata we have collected from `set document` rules. If this
/// is `None`, we are in a container.
doc_info: Option<DocumentInfo>,
/// A builder for a paragraph that might be under construction.
par: ParBuilder<'a>,
/// The current list building state.
/// A builder for a list that might be under construction.
list: ListBuilder<'a>,
/// The current citation grouping state.
/// 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>,
top: bool,
root: bool,
) -> Self {
Self {
engine,
locator,
arenas,
doc: top.then(DocBuilder::default),
flow: FlowBuilder::default(),
sink: BehavedBuilder::default(),
doc_info: root.then(DocumentInfo::default),
par: ParBuilder::default(),
list: ListBuilder::default(),
cites: CiteGroupBuilder::default(),
outside: root,
last_was_par: false,
}
}
@ -123,7 +135,9 @@ impl<'a, 'v> Builder<'a, 'v> {
// Styled elements and sequences can (at least currently) also have
// labels, so this needs to happen before they are handled.
if let Some(realized) = process(self.engine, self.locator, content, styles)? {
if let Some((tag, realized)) =
process(self.engine, self.locator, content, styles)?
{
self.engine.route.increase();
if !self.engine.route.within(Route::MAX_SHOW_RULE_DEPTH) {
bail!(
@ -131,9 +145,23 @@ impl<'a, 'v> Builder<'a, 'v> {
hint: "check whether the show rule matches its own output"
);
}
let result = self.accept(self.arenas.store(realized), styles);
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 result;
return Ok(());
}
if let Some(styled) = content.to_packed::<StyledElem>() {
@ -148,6 +176,7 @@ impl<'a, 'v> Builder<'a, 'v> {
}
// Try to merge `content` with an element under construction
// (cite group, list, or par).
if self.cites.accept(content, styles) {
return Ok(());
@ -161,6 +190,7 @@ impl<'a, 'v> Builder<'a, 'v> {
self.interrupt_list()?;
// Try again because it could be another kind of list.
if self.list.accept(content, styles) {
return Ok(());
}
@ -171,285 +201,165 @@ impl<'a, 'v> Builder<'a, 'v> {
self.interrupt_par()?;
if self.flow.accept(self.arenas, content, styles) {
return Ok(());
self.save(content, styles)
}
let keep = content
.to_packed::<PagebreakElem>()
.is_some_and(|pagebreak| !pagebreak.weak(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())
});
self.interrupt_page(keep.then_some(styles), false)?;
if let Some(doc) = &mut self.doc {
if doc.accept(self.arenas, content, styles) {
return Ok(());
}
}
if content.is::<PagebreakElem>() {
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<()> {
let local = &styled.styles;
let stored = self.arenas.store(styles);
let styles = stored.chain(local);
if let Some(Some(span)) = local.interruption::<DocumentElem>() {
let Some(doc) = &mut self.doc else {
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");
};
doc.info.populate(local);
info.populate(&styled.styles);
}
self.interrupt_style(local, None)?;
self.accept(&styled.child, styles)?;
self.interrupt_style(local, Some(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(())
}
fn interrupt_style(
&mut self,
local: &Styles,
outer: Option<StyleChain<'a>>,
) -> SourceResult<()> {
if let Some(Some(span)) = local.interruption::<PageElem>() {
if self.doc.is_none() {
bail!(span, "page configuration is not allowed inside of containers");
}
self.interrupt_page(outer, false)?;
}
/// 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()?;
}
if local.interruption::<ListElem>().is_some()
} else if local.interruption::<ListElem>().is_some()
|| local.interruption::<EnumElem>().is_some()
|| local.interruption::<TermsElem>().is_some()
{
self.interrupt_list()?;
}
Ok(())
}
/// Interrupts citation grouping and adds the resulting citation group to the builder.
fn interrupt_cites(&mut self) -> SourceResult<()> {
if !self.cites.items.is_empty() {
let staged = mem::take(&mut self.cites.staged);
let (group, styles) = mem::take(&mut self.cites).finish();
self.accept(self.arenas.store(group.pack()), styles)?;
for (content, styles) in staged {
self.accept(content, styles)?;
}
}
Ok(())
}
/// Interrupts list building and adds the resulting list element to the builder.
fn interrupt_list(&mut self) -> SourceResult<()> {
} else if local.interruption::<CiteElem>().is_some() {
self.interrupt_cites()?;
if !self.list.items.is_empty() {
let staged = mem::take(&mut self.list.staged);
let (list, styles) = mem::take(&mut self.list).finish();
self.accept(self.arenas.store(list), styles)?;
for (content, styles) in staged {
self.accept(content, styles)?;
}
}
Ok(())
}
/// Interrupts paragraph building and adds the resulting paragraph element to the builder.
/// 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() {
let (par, styles) = mem::take(&mut self.par).finish();
self.accept(self.arenas.store(par.pack()), styles)?;
}
Ok(())
}
/// Interrupts page building and adds the resulting page element to the builder.
fn interrupt_page(
&mut self,
styles: Option<StyleChain<'a>>,
last: bool,
) -> SourceResult<()> {
self.interrupt_par()?;
let Some(doc) = &mut self.doc else { return Ok(()) };
if (doc.keep_next && styles.is_some()) || self.flow.0.has_strong_elements(last) {
let (flow, trunk) = mem::take(&mut self.flow).finish();
let span = flow.span();
let styles = if trunk == StyleChain::default() {
styles.unwrap_or_default()
} else {
trunk
};
let page = PageElem::new(flow.pack()).pack().spanned(span);
self.accept(self.arenas.store(page), styles)?;
mem::take(&mut self.par).finish(self)?;
}
Ok(())
}
}
/// Builds a [document][DocumentElem] from pagebreaks and pages.
struct DocBuilder<'a> {
/// The page runs built so far.
pages: BehavedBuilder<'a>,
/// Whether to keep a following page even if it is empty.
keep_next: bool,
/// Whether the next page should be cleared to an even or odd number.
clear_next: Option<Parity>,
/// Details about the document.
info: DocumentInfo,
}
impl<'a> DocBuilder<'a> {
/// Tries to accept a piece of content.
///
/// Returns true if this content could be merged into the document.
/// If this function returns false, then the
/// content could not be merged, and document building should be
/// interrupted so that the content can be added elsewhere.
fn accept(
&mut self,
arenas: &'a Arenas<'a>,
content: &'a Content,
styles: StyleChain<'a>,
) -> bool {
if let Some(pagebreak) = content.to_packed::<PagebreakElem>() {
self.keep_next = !pagebreak.weak(styles);
self.clear_next = pagebreak.to(styles);
return true;
/// 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(())
}
if let Some(page) = content.to_packed::<PageElem>() {
let elem = if let Some(clear_to) = self.clear_next.take() {
let mut page = page.clone();
page.push_clear_to(Some(clear_to));
arenas.store(page.pack())
} else {
content
};
self.pages.push(elem, styles);
self.keep_next = false;
return true;
/// 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)?;
}
false
}
/// Turns this builder into the resulting page runs, along with
/// its [style chain][StyleChain].
fn finish(self) -> (StyleVec, StyleChain<'a>, DocumentInfo) {
let buf = self.pages.finish();
let (children, trunk) = StyleVec::create(&buf);
(children, trunk, self.info)
}
}
impl Default for DocBuilder<'_> {
fn default() -> Self {
Self {
pages: BehavedBuilder::new(),
keep_next: true,
clear_next: None,
info: DocumentInfo::default(),
}
}
}
/// Builds a [flow][FlowElem] from flow content.
#[derive(Default)]
struct FlowBuilder<'a>(BehavedBuilder<'a>, bool);
impl<'a> FlowBuilder<'a> {
/// Tries to accept a piece of content.
///
/// Returns true if this content could be merged into the flow.
/// If this function returns false, then the
/// content could not be merged, and flow building should be
/// interrupted so that the content can be added elsewhere.
fn accept(
&mut self,
arenas: &'a Arenas<'a>,
content: &'a Content,
styles: StyleChain<'a>,
) -> bool {
let last_was_par = self.1;
self.1 = false;
if content.is::<ParbreakElem>() {
return true;
}
if let Some(elem) = content.to_packed::<VElem>() {
if !elem.attach(styles) || last_was_par {
self.0.push(content, styles);
}
return true;
}
if content.is::<ColbreakElem>()
|| content.is::<TagElem>()
|| content.is::<PlaceElem>()
|| content.is::<FlushElem>()
{
self.0.push(content, styles);
return true;
}
let par_spacing = Lazy::new(|| {
arenas.store(VElem::par_spacing(ParElem::spacing_in(styles).into()).pack())
});
if let Some(elem) = content.to_packed::<BlockElem>() {
let above = match elem.above(styles) {
Smart::Auto => *par_spacing,
Smart::Custom(above) => arenas.store(VElem::block_spacing(above).pack()),
};
let below = match elem.below(styles) {
Smart::Auto => *par_spacing,
Smart::Custom(below) => arenas.store(VElem::block_spacing(below).pack()),
};
self.0.push(above, styles);
self.0.push(content, styles);
self.0.push(below, styles);
return true;
}
if content.is::<ParElem>() {
self.0.push(*par_spacing, styles);
self.0.push(content, styles);
self.0.push(*par_spacing, styles);
self.1 = true;
return true;
}
false
}
/// Turns this builder into the resulting flow, along with
/// its [style chain][StyleChain].
fn finish(self) -> (Packed<FlowElem>, StyleChain<'a>) {
let buf = self.0.finish();
let span = determine_span(&buf);
let (children, trunk) = StyleVec::create(&buf);
(Packed::new(FlowElem::new(children)).spanned(span), trunk)
Ok(())
}
}
@ -460,180 +370,216 @@ 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.
/// 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 content.is::<TagElem>() {
if !self.0.is_empty() {
if Self::is_primary(content) || (!self.0.is_empty() && Self::is_inner(content)) {
self.0.push(content, styles);
return true;
}
} else if content.is::<SpaceElem>()
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>()
{
self.0.push(content, styles);
return true;
}
false
}
/// Turns this builder into the resulting paragraph, along with
/// its [style chain][StyleChain].
fn finish(self) -> (Packed<ParElem>, StyleChain<'a>) {
let buf = self.0.finish();
let span = determine_span(&buf);
let (children, trunk) = StyleVec::create(&buf);
(Packed::new(ParElem::new(children)).spanned(span), trunk)
}
}
/// Builds a list (either [`ListElem`], [`EnumElem`], or [`TermsElem`])
/// from list or enum items, spaces, and paragraph breaks.
struct ListBuilder<'a> {
/// The list items collected so far.
items: Vec<Pair<'a>>,
/// Trailing content for which it is unclear whether it is part of the list.
staged: Vec<Pair<'a>>,
/// Whether the list contains no paragraph breaks.
tight: bool,
}
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.items.is_empty()
&& (content.is::<SpaceElem>() || content.is::<ParbreakElem>())
{
self.staged.push((content, styles));
return true;
}
if (content.is::<ListItem>()
|| content.is::<EnumItem>()
|| content.is::<TermItem>())
&& self
.items
.first()
.map_or(true, |(first, _)| first.func() == content.func())
{
self.items.push((content, styles));
self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakElem>());
return true;
}
false
/// 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) -> (Content, StyleChain<'a>) {
let span = determine_span(&self.items);
let (children, trunk) = StyleVec::create(&self.items);
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 mut iter = children.into_iter().peekable();
let (first, _) = iter.peek().unwrap();
let output = if first.is::<ListItem>() {
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.into_packed::<ListItem>().unwrap().styled(local)
item.to_packed::<ListItem>().unwrap().clone().styled(local)
})
.collect();
ListElem::new(children).with_tight(self.tight).pack().spanned(span)
ListElem::new(children).with_tight(tight).pack()
} else if first.is::<EnumItem>() {
let children = iter
.map(|(item, local)| {
item.into_packed::<EnumItem>().unwrap().styled(local)
item.to_packed::<EnumItem>().unwrap().clone().styled(local)
})
.collect();
EnumElem::new(children).with_tight(self.tight).pack().spanned(span)
EnumElem::new(children).with_tight(tight).pack()
} else if first.is::<TermItem>() {
let children = iter
.map(|(item, local)| {
item.into_packed::<TermItem>().unwrap().styled(local)
item.to_packed::<TermItem>().unwrap().clone().styled(local)
})
.collect();
TermsElem::new(children).with_tight(self.tight).pack().spanned(span)
TermsElem::new(children).with_tight(tight).pack()
} else {
unreachable!()
};
(output, trunk)
}
}
// Add the list to the builder.
let span = first_span(&self.0);
let stored = builder.arenas.store(elem.spanned(span));
builder.accept(stored, trunk)?;
impl Default for ListBuilder<'_> {
fn default() -> Self {
Self { items: vec![], staged: vec![], tight: true }
// 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> {
/// The citations.
items: Vec<Packed<CiteElem>>,
/// Trailing content for which it is unclear whether it is part of the list.
staged: Vec<Pair<'a>>,
/// The styles.
styles: StyleChain<'a>,
}
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.
/// 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.items.is_empty()
&& (content.is::<SpaceElem>() || content.is::<TagElem>())
{
self.staged.push((content, styles));
return true;
}
if let Some(citation) = content.to_packed::<CiteElem>() {
if self.items.is_empty() {
self.styles = styles;
}
self.staged.retain(|(elem, _)| !elem.is::<SpaceElem>());
self.items.push(citation.clone());
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) -> (Packed<CiteGroup>, StyleChain<'a>) {
let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached());
(Packed::new(CiteGroup::new(self.items)).spanned(span), self.styles)
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.
fn determine_span(buf: &[(&Content, StyleChain)]) -> Span {
let mut span = Span::detached();
for &(content, _) in buf {
span = content.span();
if !span.is_detached() {
break;
}
}
span
pub fn first_span(children: &[(&Content, StyleChain)]) -> Span {
children
.iter()
.map(|(c, _)| c.span())
.find(|span| !span.is_detached())
.unwrap_or(Span::detached())
}

View File

@ -8,7 +8,7 @@ use crate::foundations::{
Content, Context, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style,
StyleChain, Styles, Synthesize, Transformation,
};
use crate::introspection::{Locatable, SplitLocator, Tag, TagElem};
use crate::introspection::{Locatable, SplitLocator, Tag};
use crate::text::TextElem;
use crate::utils::SmallBitSet;
@ -37,7 +37,7 @@ pub fn process(
locator: &mut SplitLocator,
target: &Content,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
) -> SourceResult<Option<(Option<Tag>, Content)>> {
let Some(Verdict { prepared, mut map, step }) = verdict(engine, target, styles)
else {
return Ok(None);
@ -54,7 +54,7 @@ pub fn process(
}
// Apply a step, if there is one.
let mut output = match step {
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
@ -67,12 +67,7 @@ pub fn process(
None => target,
};
// If necessary, add the tag generated in the preparation.
if let Some(tag) = tag {
output = tag + output;
}
Ok(Some(output.styled_with_map(map)))
Ok(Some((tag, output.styled_with_map(map))))
}
/// Inspects a target element and the current styles and determines how to
@ -106,7 +101,7 @@ fn verdict<'a>(
let mut r = 0;
for entry in styles.entries() {
let recipe = match entry {
let recipe = match &**entry {
Style::Recipe(recipe) => recipe,
Style::Property(_) => continue,
Style::Revocation(index) => {
@ -124,7 +119,7 @@ fn verdict<'a>(
// 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(_))))
(recipe.transform(), matches!(recipe.selector(), Some(Selector::Regex(_))))
{
// If this is a show-set for an unprepared element, we need to apply
// it.
@ -137,8 +132,9 @@ fn verdict<'a>(
// 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::recipe).count());
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) {
@ -187,7 +183,7 @@ fn prepare(
target: &mut Content,
map: &mut Styles,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
) -> 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.
@ -226,7 +222,7 @@ fn prepare(
// 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| TagElem::packed(Tag::new(target.clone(), key)));
let tag = key.map(|key| Tag::new(target.clone(), key));
// Ensure that this preparation only runs once by marking the element as
// prepared.
@ -246,7 +242,7 @@ fn show(
// Apply a user-defined show rule.
ShowStep::Recipe(recipe, guard) => {
let context = Context::new(target.location(), Some(styles));
match &recipe.selector {
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)) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

View File

@ -62,6 +62,46 @@ At Beta, it was #context {
#counter(page).update(1)
#lorem(20)
--- counter-page-footer-before-set-page ---
#set page(numbering: "1", margin: (bottom: 20pt))
A
#pagebreak()
#counter(page).update(5)
#set page(fill: aqua)
B
--- counter-page-header-before-set-page ---
#set page(numbering: "1", number-align: top + center, margin: (top: 20pt))
A
#counter(page).update(4)
#set page(fill: aqua)
B
--- counter-page-between-pages ---
// The update happens conceptually between the pages.
#set page(numbering: "1", margin: (bottom: 20pt))
A
#pagebreak()
#counter(page).update(5)
#set page(number-align: top + center, margin: (top: 20pt, bottom: 10pt))
B
--- counter-page-header-only-update ---
// Header should not be affected by default.
// To affect it, put the counter update before the `set page`.
#set page(
numbering: "1",
number-align: top + center,
margin: (top: 20pt),
)
#counter(page).update(5)
--- counter-page-footer-only-update ---
// Footer should be affected by default.
#set page(numbering: "1 / 1", margin: (bottom: 20pt))
#counter(page).update(5)
--- counter-figure ---
// Count figures.
#figure(numbering: "A", caption: [Four 'A's], kind: image, supplement: "Figure")[_AAAA!_]

View File

@ -37,3 +37,93 @@
// Error: 10-25 selector matches multiple elements
#context locate(heading)
--- locate-between-pages ---
// Test locating tags that are before or between pages.
#set page(height: 30pt)
#context [
// Before the first page.
// (= at the very start of the first page, before the header)
#test(locate(<a>).position(), (page: 1, x: 0pt, y: 0pt))
// On the first page.
#test(locate(<b>).position(), (page: 1, x: 10pt, y: 10pt))
// Between the two pages.
// (= at the very start of the first page, before the header)
#test(locate(<c>).position(), (page: 2, x: 0pt, y: 0pt))
// After the last page.
// (= at the very end of the last page, after the footer)
#test(locate(<d>).position(), (page: 2, x: 0pt, y: 30pt))
#test(locate(<e>).position(), (page: 2, x: 0pt, y: 30pt))
]
#metadata(none) <a>
#pagebreak(weak: true)
#metadata(none) <b>
A
#pagebreak()
#metadata(none) <c>
#pagebreak(weak: true)
B
#pagebreak(weak: true)
#metadata(none) <d>
#pagebreak(weak: true)
#metadata(none) <e>
--- issue-4029-locate-after-spacing ---
#set page(margin: 10pt)
#show heading: it => v(40pt) + it
= Introduction
#context test(
locate(heading).position(),
(page: 1, x: 10pt, y: 50pt),
)
--- issue-4029-locate-after-pagebreak ---
#set page(margin: 10pt)
#show heading: it => pagebreak() + it
= Introduction
#context test(
locate(heading).position(),
(page: 2, x: 10pt, y: 10pt),
)
--- issue-4029-locate-after-par-and-pagebreak ---
// Ensure that the heading's tag isn't stuck at the end of the paragraph.
#set page(margin: 10pt)
Par
#show heading: it => pagebreak() + it
= Introduction
#context test(locate(heading).page(), 2)
--- issue-1886-locate-after-metadata ---
#show heading: it => {
metadata(it.label)
pagebreak(weak: true, to: "odd")
it
}
Hi
= Hello <hello>
= World <world>
// The metadata's position does not migrate to the next page, but the heading's
// does.
#context {
test(locate(metadata.where(value: <hello>)).page(), 1)
test(locate(<hello>).page(), 3)
test(locate(metadata.where(value: <world>)).page(), 3)
test(locate(<world>).page(), 5)
}
--- issue-1833-locate-place ---
#set page(height: 60pt)
#context {
place(right + bottom, rect())
test(here().position(), (page: 1, x: 10pt, y: 10pt))
}

View File

@ -245,6 +245,98 @@ Look, ma, no page numbers!
#set page(header: auto, footer: auto)
Default page numbers now.
--- page-marginal-style-text-set ---
#set page(numbering: "1", margin: (bottom: 20pt))
#set text(red)
Red
--- page-marginal-style-text-set-first ---
#set text(red)
#set page(numbering: "1", margin: (bottom: 20pt))
Red
--- page-marginal-style-text-call ---
#set page(numbering: "1", margin: (bottom: 20pt))
#text(red)[Red]
--- page-marginal-style-text-call-code ---
#{
set page(numbering: "1", margin: (bottom: 20pt))
text(red)[Red]
}
--- page-marginal-style-text-call-around-page-call ---
#text(red, page(numbering: "1", margin: (bottom: 20pt))[Hello])
--- page-marginal-style-text-call-around-set-page ---
#text(red, {
set page(numbering: "1", margin: (bottom: 20pt))
text(style: "italic")[Hello]
})
--- page-marginal-style-text-call-around-pagebreak ---
#set page(numbering: "1", margin: (bottom: 20pt))
A
#text(red)[
#pagebreak(weak: true)
B
]
--- page-marginal-style-show-rule ---
#set page(numbering: "1", margin: (bottom: 20pt))
= Introduction
--- page-marginal-style-show-rule-with-set-page ---
#show heading: it => {
set page(numbering: "1", margin: (bottom: 20pt))
it
}
= Introduction
--- page-marginal-style-show-rule-with-page-call ---
#show heading: page.with(fill: aqua)
A
= Introduction
B
--- page-marginal-style-show-rule-with-pagebreak ---
#set page(numbering: "1", margin: (bottom: 20pt))
#show heading: it => {
pagebreak(weak: true)
it
}
= Introduction
--- page-marginal-style-context ---
#set page(numbering: "1", margin: (bottom: 20pt))
#show: it => context {
set text(red)
it
}
Hi
--- page-marginal-style-shared-initial-interaction ---
#set page(numbering: "1", margin: (bottom: 20pt))
A
#{
set text(fill: red)
pagebreak()
}
#text(fill: blue)[B]
--- page-marginal-style-empty ---
#set text(red)
#set page(numbering: "1", margin: (bottom: 20pt))
--- page-marginal-style-page-call ---
#page(numbering: "1", margin: (bottom: 20pt))[
#set text(red)
A
]
--- issue-2631-page-header-ordering ---
#set text(6pt)
#show heading: set text(6pt, weight: "regular")
@ -272,3 +364,22 @@ Hi
#set page(fill: gray)
text
#pagebreak()
--- issue-2326-context-set-page ---
#context [
#set page(fill: aqua)
On page #here().page()
]
--- issue-3671-get-from-page-call ---
#set page(margin: 5pt)
#context test(page.margin, 5pt)
#page(margin: 10pt, context test(page.margin, 10pt))
--- issue-4363-set-page-after-tag ---
#set page(fill: aqua)
1
#pagebreak()
#metadata(none)
#set page(fill: red)
2

View File

@ -141,3 +141,12 @@ Some text on page 2
#set page(fill: orange) // This sets the color of the page starting from page 4
Some text on page 4
--- issue-2591-single-weak-pagebreak ---
#pagebreak(weak: true)
--- issue-2841-pagebreak-to-weak ---
First
#pagebreak(to: "odd")
#pagebreak(weak: true)
Odd

View File

@ -142,6 +142,16 @@ Hello
#list(tight: false)[A][B]
World
--- list-items-context ---
#context [+ A]
#context [+ B]
#context [+ C]
--- list-item-styling ---
- Hello
#text(red)[- World]
#text(green)[- What up?]
--- issue-2530-list-item-panic ---
// List item (pre-emptive)
#list.item[Hello]