Improve realization and page layout (#4840)
@ -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 }
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
111
crates/typst/src/introspection/tag.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -177,7 +177,7 @@ pub struct FlushElem {}
|
||||
|
||||
impl Behave for Packed<FlushElem> {
|
||||
fn behaviour(&self) -> Behaviour {
|
||||
Behaviour::Invisible
|
||||
Behaviour::Ignorant
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)) => {
|
||||
|
BIN
tests/ref/counter-page-between-pages.png
Normal file
After Width: | Height: | Size: 299 B |
BIN
tests/ref/counter-page-footer-before-set-page.png
Normal file
After Width: | Height: | Size: 535 B |
BIN
tests/ref/counter-page-footer-only-update.png
Normal file
After Width: | Height: | Size: 205 B |
BIN
tests/ref/counter-page-header-before-set-page.png
Normal file
After Width: | Height: | Size: 551 B |
BIN
tests/ref/counter-page-header-only-update.png
Normal file
After Width: | Height: | Size: 131 B |
BIN
tests/ref/issue-1833-locate-place.png
Normal file
After Width: | Height: | Size: 136 B |
BIN
tests/ref/issue-1886-locate-after-metadata.png
Normal file
After Width: | Height: | Size: 830 B |
BIN
tests/ref/issue-2326-context-set-page.png
Normal file
After Width: | Height: | Size: 676 B |
BIN
tests/ref/issue-2841-pagebreak-to-weak.png
Normal file
After Width: | Height: | Size: 391 B |
BIN
tests/ref/issue-4029-locate-after-pagebreak.png
Normal file
After Width: | Height: | Size: 698 B |
BIN
tests/ref/issue-4029-locate-after-par-and-pagebreak.png
Normal file
After Width: | Height: | Size: 785 B |
BIN
tests/ref/issue-4029-locate-after-spacing.png
Normal file
After Width: | Height: | Size: 706 B |
BIN
tests/ref/issue-4363-set-page-after-tag.png
Normal file
After Width: | Height: | Size: 278 B |
BIN
tests/ref/list-item-styling.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
tests/ref/list-items-context.png
Normal file
After Width: | Height: | Size: 382 B |
BIN
tests/ref/locate-between-pages.png
Normal file
After Width: | Height: | Size: 231 B |
BIN
tests/ref/page-marginal-style-context.png
Normal file
After Width: | Height: | Size: 348 B |
BIN
tests/ref/page-marginal-style-empty.png
Normal file
After Width: | Height: | Size: 152 B |
BIN
tests/ref/page-marginal-style-page-call.png
Normal file
After Width: | Height: | Size: 300 B |
BIN
tests/ref/page-marginal-style-shared-initial-interaction.png
Normal file
After Width: | Height: | Size: 504 B |
BIN
tests/ref/page-marginal-style-show-rule-with-page-call.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/ref/page-marginal-style-show-rule-with-pagebreak.png
Normal file
After Width: | Height: | Size: 716 B |
BIN
tests/ref/page-marginal-style-show-rule-with-set-page.png
Normal file
After Width: | Height: | Size: 727 B |
BIN
tests/ref/page-marginal-style-show-rule.png
Normal file
After Width: | Height: | Size: 716 B |
BIN
tests/ref/page-marginal-style-text-call-around-page-call.png
Normal file
After Width: | Height: | Size: 529 B |
BIN
tests/ref/page-marginal-style-text-call-around-pagebreak.png
Normal file
After Width: | Height: | Size: 510 B |
BIN
tests/ref/page-marginal-style-text-call-around-set-page.png
Normal file
After Width: | Height: | Size: 531 B |
BIN
tests/ref/page-marginal-style-text-call-code.png
Normal file
After Width: | Height: | Size: 473 B |
BIN
tests/ref/page-marginal-style-text-call.png
Normal file
After Width: | Height: | Size: 473 B |
BIN
tests/ref/page-marginal-style-text-set-first.png
Normal file
After Width: | Height: | Size: 457 B |
BIN
tests/ref/page-marginal-style-text-set.png
Normal file
After Width: | Height: | Size: 457 B |
@ -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!_]
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|