Fix logical ordering of floats and footnotes (#5185)

This commit is contained in:
Laurenz 2024-10-12 14:01:31 +02:00 committed by GitHub
parent 16736feb13
commit bb0e089474
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 457 additions and 319 deletions

View File

@ -10,7 +10,7 @@ use crate::foundations::{
cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue,
Func, Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value,
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::introspection::{Introspector, Locatable, Location, Unqueriable};
use crate::symbols::Symbol;
/// A helper macro to create a field selector used in [`Selector::Elem`]
@ -339,7 +339,7 @@ impl FromValue for LocatableSelector {
fn validate(selector: &Selector) -> StrResult<()> {
match selector {
Selector::Elem(elem, _) => {
if !elem.can::<dyn Locatable>() {
if !elem.can::<dyn Locatable>() || elem.can::<dyn Unqueriable>() {
Err(eco_format!("{} is not locatable", elem.name()))?
}
}

View File

@ -12,7 +12,7 @@ use crate::foundations::{
Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr,
Selector, Show, Smart, Str, StyleChain, Value,
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::introspection::{Introspector, Locatable, Location, Tag};
use crate::layout::{Frame, FrameItem, PageElem};
use crate::math::EquationElem;
use crate::model::{FigureElem, FootnoteElem, HeadingElem, Numbering, NumberingPattern};
@ -821,8 +821,8 @@ impl ManualPageCounter {
for (_, item) in page.items() {
match item {
FrameItem::Group(group) => self.visit(engine, &group.frame)?,
FrameItem::Tag(tag) => {
let Some(elem) = tag.elem().to_packed::<CounterUpdateElem>() else {
FrameItem::Tag(Tag::Start(elem)) => {
let Some(elem) = elem.to_packed::<CounterUpdateElem>() else {
continue;
};
if *elem.key() == CounterKey::Page {

View File

@ -1,16 +1,15 @@
use std::collections::{BTreeSet, HashMap};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
use std::num::NonZeroUsize;
use std::sync::RwLock;
use ecow::{eco_format, EcoVec};
use indexmap::IndexMap;
use ecow::EcoVec;
use smallvec::SmallVec;
use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector};
use crate::introspection::{Location, TagKind};
use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering;
use crate::utils::NonZeroExt;
@ -20,16 +19,20 @@ use crate::utils::NonZeroExt;
pub struct Introspector {
/// The number of pages in the document.
pages: usize,
/// All introspectable elements.
elems: IndexMap<Location, (Content, Position)>,
/// Maps labels to their indices in the element list. We use a smallvec such
/// that if the label is unique, we don't need to allocate.
labels: HashMap<Label, SmallVec<[usize; 1]>>,
/// Maps from element keys to the locations of all elements that had this
/// key. Used for introspector-assisted location assignment.
keys: HashMap<u128, SmallVec<[Location; 1]>>,
/// The page numberings, indexed by page number minus 1.
page_numberings: Vec<Option<Numbering>>,
/// All introspectable elements.
elems: Vec<Pair>,
/// Lists all elements with a specific hash key. This is used for
/// introspector-assisted location assignment during measurement.
keys: MultiMap<u128, Location>,
/// Accelerates lookup of elements by location.
locations: HashMap<Location, usize>,
/// Accelerates lookup of elements by label.
labels: MultiMap<Label, usize>,
/// Caches queries done on the introspector. This is important because
/// even if all top-level queries are distinct, they often have shared
/// subqueries. Example: Individual counter queries with `before` that
@ -37,81 +40,56 @@ pub struct Introspector {
queries: QueryCache,
}
/// A pair of content and its position.
type Pair = (Content, Position);
impl Introspector {
/// Applies new frames in-place, reusing the existing allocations.
/// Creates an introspector for a page list.
#[typst_macros::time(name = "introspect")]
pub fn rebuild(&mut self, pages: &[Page]) {
self.pages = pages.len();
self.elems.clear();
self.labels.clear();
self.keys.clear();
self.page_numberings.clear();
self.queries.clear();
for (i, page) in pages.iter().enumerate() {
let page_nr = NonZeroUsize::new(1 + i).unwrap();
self.extract(&page.frame, page_nr, Transform::identity());
self.page_numberings.push(page.numbering.clone());
}
pub fn new(pages: &[Page]) -> Self {
IntrospectorBuilder::new().build(pages)
}
/// Extract metadata from a frame.
fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) {
for (pos, item) in frame.items() {
match item {
FrameItem::Group(group) => {
let ts = ts
.pre_concat(Transform::translate(pos.x, pos.y))
.pre_concat(group.transform);
self.extract(&group.frame, page, ts);
}
FrameItem::Tag(tag)
if tag.kind() == TagKind::Start
&& !self.elems.contains_key(&tag.location()) =>
{
let pos = pos.transform(ts);
let loc = tag.location();
let ret = self
.elems
.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);
// Build the label cache.
if let Some(label) = tag.elem().label() {
self.labels.entry(label).or_default().push(self.elems.len() - 1);
}
}
_ => {}
}
}
}
/// Iterate over all locatable elements.
/// Iterates over all locatable elements.
pub fn all(&self) -> impl Iterator<Item = &Content> + '_ {
self.elems.values().map(|(c, _)| c)
self.elems.iter().map(|(c, _)| c)
}
/// Perform a binary search for `elem` among the `list`.
/// Retrieves the element with the given index.
#[track_caller]
fn get_by_idx(&self, idx: usize) -> &Content {
&self.elems[idx].0
}
/// Retrieves the position of the element with the given index.
#[track_caller]
fn get_pos_by_idx(&self, idx: usize) -> Position {
self.elems[idx].1
}
/// Retrieves an element by its location.
fn get_by_loc(&self, location: &Location) -> Option<&Content> {
self.locations.get(location).map(|&idx| self.get_by_idx(idx))
}
/// Retrieves the position of the element with the given index.
fn get_pos_by_loc(&self, location: &Location) -> Option<Position> {
self.locations.get(location).map(|&idx| self.get_pos_by_idx(idx))
}
/// Performs a binary search for `elem` among the `list`.
fn binary_search(&self, list: &[Content], elem: &Content) -> Result<usize, usize> {
list.binary_search_by_key(&self.elem_index(elem), |elem| self.elem_index(elem))
}
/// Get an element by its location.
fn get(&self, location: &Location) -> Option<&Content> {
self.elems.get(location).map(|(elem, _)| elem)
}
/// Get the index of this element among all.
/// Gets the index of this element.
fn elem_index(&self, elem: &Content) -> usize {
self.loc_index(&elem.location().unwrap())
}
/// Get the index of the element with this location among all.
/// Gets the index of the element with this location among all.
fn loc_index(&self, location: &Location) -> usize {
self.elems.get_index_of(location).unwrap_or(usize::MAX)
self.locations.get(location).copied().unwrap_or(usize::MAX)
}
}
@ -125,20 +103,50 @@ impl Introspector {
}
let output = match selector {
Selector::Label(label) => self
.labels
.get(label)
.map(|indices| {
indices.iter().map(|&index| self.elems[index].0.clone()).collect()
})
.unwrap_or_default(),
Selector::Elem(..) | Selector::Can(_) => self
Selector::Elem(..) => self
.all()
.filter(|elem| selector.matches(elem, None))
.cloned()
.collect(),
Selector::Location(location) => {
self.get(location).cloned().into_iter().collect()
self.get_by_loc(location).cloned().into_iter().collect()
}
Selector::Label(label) => self
.labels
.get(label)
.iter()
.map(|&idx| self.get_by_idx(idx).clone())
.collect(),
Selector::Or(selectors) => selectors
.iter()
.flat_map(|sel| self.query(sel))
.map(|elem| self.elem_index(&elem))
.collect::<BTreeSet<usize>>()
.into_iter()
.map(|idx| self.get_by_idx(idx).clone())
.collect(),
Selector::And(selectors) => {
let mut results: Vec<_> =
selectors.iter().map(|sel| self.query(sel)).collect();
// Extract the smallest result list and then keep only those
// elements in the smallest list that are also in all other
// lists.
results
.iter()
.enumerate()
.min_by_key(|(_, vec)| vec.len())
.map(|(i, _)| i)
.map(|i| results.swap_remove(i))
.iter()
.flatten()
.filter(|candidate| {
results
.iter()
.all(|other| self.binary_search(other, candidate).is_ok())
})
.cloned()
.collect()
}
Selector::Before { selector, end, inclusive } => {
let mut list = self.query(selector);
@ -168,39 +176,8 @@ impl Introspector {
}
list
}
Selector::And(selectors) => {
let mut results: Vec<_> =
selectors.iter().map(|sel| self.query(sel)).collect();
// Extract the smallest result list and then keep only those
// elements in the smallest list that are also in all other
// lists.
results
.iter()
.enumerate()
.min_by_key(|(_, vec)| vec.len())
.map(|(i, _)| i)
.map(|i| results.swap_remove(i))
.iter()
.flatten()
.filter(|candidate| {
results
.iter()
.all(|other| self.binary_search(other, candidate).is_ok())
})
.cloned()
.collect()
}
Selector::Or(selectors) => selectors
.iter()
.flat_map(|sel| self.query(sel))
.map(|elem| self.elem_index(&elem))
.collect::<BTreeSet<usize>>()
.into_iter()
.map(|index| self.elems[index].0.clone())
.collect(),
// Not supported here.
Selector::Regex(_) => EcoVec::new(),
Selector::Can(_) | Selector::Regex(_) => EcoVec::new(),
};
self.queries.insert(hash, output.clone());
@ -210,12 +187,12 @@ impl Introspector {
/// Query for the first element that matches the selector.
pub fn query_first(&self, selector: &Selector) -> Option<Content> {
match selector {
Selector::Location(location) => self.get(location).cloned(),
Selector::Location(location) => self.get_by_loc(location).cloned(),
Selector::Label(label) => self
.labels
.get(label)
.and_then(|indices| indices.first())
.map(|&index| self.elems[index].0.clone()),
.first()
.map(|&idx| self.get_by_idx(idx).clone()),
_ => self.query(selector).first().cloned(),
}
}
@ -224,7 +201,7 @@ impl Introspector {
pub fn query_unique(&self, selector: &Selector) -> StrResult<Content> {
match selector {
Selector::Location(location) => self
.get(location)
.get_by_loc(location)
.cloned()
.ok_or_else(|| "element does not exist in the document".into()),
Selector::Label(label) => self.query_label(*label).cloned(),
@ -243,15 +220,11 @@ impl Introspector {
/// Query for a unique element with the label.
pub fn query_label(&self, label: Label) -> StrResult<&Content> {
let indices = self.labels.get(&label).ok_or_else(|| {
eco_format!("label `{}` does not exist in the document", label.repr())
})?;
if indices.len() > 1 {
bail!("label `{}` occurs multiple times in the document", label.repr());
match *self.labels.get(&label) {
[idx] => Ok(self.get_by_idx(idx)),
[] => bail!("label `{}` does not exist in the document", label.repr()),
_ => bail!("label `{}` occurs multiple times in the document", label.repr()),
}
Ok(&self.elems[indices[0]].0)
}
/// This is an optimized version of
@ -259,7 +232,7 @@ impl Introspector {
pub fn query_count_before(&self, selector: &Selector, end: Location) -> usize {
// See `query()` for details.
let list = self.query(selector);
if let Some(end) = self.get(&end) {
if let Some(end) = self.get_by_loc(&end) {
match self.binary_search(&list, end) {
Ok(i) => i + 1,
Err(i) => i,
@ -274,14 +247,6 @@ impl Introspector {
NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE)
}
/// Gets the page numbering for the given location, if any.
pub fn page_numbering(&self, location: Location) -> Option<&Numbering> {
let page = self.page(location);
self.page_numberings
.get(page.get() - 1)
.and_then(|slot| slot.as_ref())
}
/// Find the page number for the given location.
pub fn page(&self, location: Location) -> NonZeroUsize {
self.position(location).page
@ -289,12 +254,18 @@ impl Introspector {
/// Find the position for the given location.
pub fn position(&self, location: Location) -> Position {
self.elems
.get(&location)
.map(|&(_, pos)| pos)
self.get_pos_by_loc(&location)
.unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() })
}
/// Gets the page numbering for the given location, if any.
pub fn page_numbering(&self, location: Location) -> Option<&Numbering> {
let page = self.page(location);
self.page_numberings
.get(page.get() - 1)
.and_then(|slot| slot.as_ref())
}
/// Try to find a location for an element with the given `key` hash
/// that is closest after the `anchor`.
///
@ -304,7 +275,7 @@ impl Introspector {
pub fn locator(&self, key: u128, anchor: Location) -> Option<Location> {
let anchor = self.loc_index(&anchor);
self.keys
.get(&key)?
.get(&key)
.iter()
.copied()
.min_by_key(|loc| self.loc_index(loc).wrapping_sub(anchor))
@ -317,6 +288,33 @@ impl Debug for Introspector {
}
}
/// A map from one keys to multiple elements.
#[derive(Clone)]
struct MultiMap<K, V>(HashMap<K, SmallVec<[V; 1]>>);
impl<K, V> MultiMap<K, V>
where
K: Hash + Eq,
{
fn get(&self, key: &K) -> &[V] {
self.0.get(key).map_or(&[], |vec| vec.as_slice())
}
fn insert(&mut self, key: K, value: V) {
self.0.entry(key).or_default().push(value);
}
fn take(&mut self, key: &K) -> Option<impl Iterator<Item = V>> {
self.0.remove(key).map(|vec| vec.into_iter())
}
}
impl<K, V> Default for MultiMap<K, V> {
fn default() -> Self {
Self(HashMap::new())
}
}
/// Caches queries.
#[derive(Default)]
struct QueryCache(RwLock<HashMap<u128, EcoVec<Content>>>);
@ -329,10 +327,6 @@ impl QueryCache {
fn insert(&self, hash: u128, output: EcoVec<Content>) {
self.0.write().unwrap().insert(hash, output);
}
fn clear(&mut self) {
self.0.get_mut().unwrap().clear();
}
}
impl Clone for QueryCache {
@ -340,3 +334,120 @@ impl Clone for QueryCache {
Self(RwLock::new(self.0.read().unwrap().clone()))
}
}
/// Builds the introspector.
#[derive(Default)]
struct IntrospectorBuilder {
page_numberings: Vec<Option<Numbering>>,
seen: HashSet<Location>,
insertions: MultiMap<Location, Vec<Pair>>,
keys: MultiMap<u128, Location>,
locations: HashMap<Location, usize>,
labels: MultiMap<Label, usize>,
}
impl IntrospectorBuilder {
/// Create an empty builder.
fn new() -> Self {
Self::default()
}
/// Build the introspector.
fn build(mut self, pages: &[Page]) -> Introspector {
self.page_numberings.reserve(pages.len());
// Discover all elements.
let mut root = Vec::new();
for (i, page) in pages.iter().enumerate() {
self.page_numberings.push(page.numbering.clone());
self.discover(
&mut root,
&page.frame,
NonZeroUsize::new(1 + i).unwrap(),
Transform::identity(),
);
}
self.locations.reserve(self.seen.len());
// Save all pairs and their descendants in the correct order.
let mut elems = Vec::with_capacity(self.seen.len());
for pair in root {
self.visit(&mut elems, pair);
}
Introspector {
pages: pages.len(),
page_numberings: self.page_numberings,
elems,
keys: self.keys,
locations: self.locations,
labels: self.labels,
queries: QueryCache::default(),
}
}
/// Processes the tags in the frame.
fn discover(
&mut self,
sink: &mut Vec<Pair>,
frame: &Frame,
page: NonZeroUsize,
ts: Transform,
) {
for (pos, item) in frame.items() {
match item {
FrameItem::Group(group) => {
let ts = ts
.pre_concat(Transform::translate(pos.x, pos.y))
.pre_concat(group.transform);
if let Some(parent) = group.parent {
let mut nested = vec![];
self.discover(&mut nested, &group.frame, page, ts);
self.insertions.insert(parent, nested);
} else {
self.discover(sink, &group.frame, page, ts);
}
}
FrameItem::Tag(Tag::Start(elem)) => {
let loc = elem.location().unwrap();
if self.seen.insert(loc) {
let point = pos.transform(ts);
sink.push((elem.clone(), Position { page, point }));
}
}
FrameItem::Tag(Tag::End(loc, key)) => {
self.keys.insert(*key, *loc);
}
_ => {}
}
}
}
/// Saves a pair and all its descendants into `elems` and populates the
/// acceleration structures.
fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) {
let elem = &pair.0;
let loc = elem.location().unwrap();
let idx = elems.len();
// Populate the location acceleration map.
self.locations.insert(loc, idx);
// Populate the label acceleration map.
if let Some(label) = elem.label() {
self.labels.insert(label, idx);
}
// Save the element.
elems.push(pair);
// Process potential descendants.
if let Some(insertions) = self.insertions.take(&loc) {
for pair in insertions.flatten() {
self.visit(elems, pair);
}
}
}
}

View File

@ -105,5 +105,9 @@ impl Repr for Location {
}
}
/// Makes this element locatable through `engine.locate`.
/// Makes this element as locatable through the introspector.
pub trait Locatable {}
/// Marks this element as not being queryable even though it is locatable for
/// internal reasons.
pub trait Unqueriable {}

View File

@ -7,79 +7,48 @@ use crate::foundations::{
};
use crate::introspection::Location;
/// Holds a locatable element that was realized.
/// Marks the start or end of a locatable element.
#[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,
pub enum Tag {
/// The stored element starts here.
///
/// Content placed in a tag **must** have a [`Location`] or there will be
/// panics.
Start(Content),
/// The element with the given location and key hash ends here.
///
/// Note: The key hash is stored here instead of in `Start` simply to make
/// the two enum variants more balanced in size, keeping a `Tag`'s memory
/// size down. There are no semantic reasons for this.
End(Location, 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.
/// Access the location of the tag.
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
match self {
Tag::Start(elem) => elem.location().unwrap(),
Tag::End(loc, _) => *loc,
}
}
}
impl Debug for Tag {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Tag({:?}, {:?})", self.kind, self.elem.elem().name())
match self {
Tag::Start(elem) => write!(f, "Start({:?})", elem.elem().name()),
Tag::End(..) => f.pad("End"),
}
}
}
/// 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(Construct, Unlabellable)]
pub struct TagElem {
/// The introspectible element.
/// The introspectable element.
#[required]
#[internal]
pub tag: Tag,

View File

@ -190,7 +190,7 @@ impl Packed<BoxElem> {
// Assign label to the frame.
if let Some(label) = self.label() {
frame.group(|group| group.label = Some(label))
frame.label(label);
}
// Apply baseline shift. Do this after setting the size and applying the
@ -562,7 +562,7 @@ impl Packed<BlockElem> {
// Assign label to each frame in the fragment.
if let Some(label) = self.label() {
frame.group(|group| group.label = Some(label));
frame.label(label);
}
Ok(frame)
@ -723,7 +723,7 @@ impl Packed<BlockElem> {
// Assign label to each frame in the fragment.
if let Some(label) = self.label() {
for frame in fragment.iter_mut() {
frame.group(|group| group.label = Some(label))
frame.label(label);
}
}

View File

@ -11,7 +11,7 @@ use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{Packed, Resolve, Smart, StyleChain};
use crate::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, Tag, TagElem,
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem,
};
use crate::layout::{
layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem,
@ -62,7 +62,7 @@ struct Collector<'a, 'x, 'y> {
impl<'a> Collector<'a, '_, '_> {
/// Perform the collection.
fn run(mut self) -> SourceResult<Vec<Child<'a>>> {
for (idx, &(child, styles)) in self.children.iter().enumerate() {
for &(child, styles) in self.children {
if let Some(elem) = child.to_packed::<TagElem>() {
self.output.push(Child::Tag(&elem.tag));
} else if let Some(elem) = child.to_packed::<VElem>() {
@ -72,7 +72,7 @@ impl<'a> Collector<'a, '_, '_> {
} else if let Some(elem) = child.to_packed::<BlockElem>() {
self.block(elem, styles);
} else if let Some(elem) = child.to_packed::<PlaceElem>() {
self.place(idx, elem, styles)?;
self.place(elem, styles)?;
} else if child.is::<FlushElem>() {
self.output.push(Child::Flush);
} else if let Some(elem) = child.to_packed::<ColbreakElem>() {
@ -220,7 +220,6 @@ impl<'a> Collector<'a, '_, '_> {
/// Collects a placed element into a [`PlacedChild`].
fn place(
&mut self,
idx: usize,
elem: &'a Packed<PlaceElem>,
styles: StyleChain<'a>,
) -> SourceResult<()> {
@ -257,7 +256,6 @@ impl<'a> Collector<'a, '_, '_> {
let clearance = elem.clearance(styles);
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
self.output.push(Child::Placed(self.boxed(PlacedChild {
idx,
align_x,
align_y,
scope,
@ -553,7 +551,6 @@ impl MultiSpill<'_, '_> {
/// A child that encapsulates a prepared placed element.
#[derive(Debug)]
pub struct PlacedChild<'a> {
pub idx: usize,
pub align_x: FixedAlignment,
pub align_y: Smart<Option<FixedAlignment>>,
pub scope: PlacementScope,
@ -573,16 +570,27 @@ impl PlacedChild<'_> {
self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap();
layout_frame(
let mut frame = layout_frame(
engine,
&self.elem.body,
self.locator.relayout(),
self.styles.chain(&aligned),
Region::new(base, Axes::splat(false)),
)
.map(|frame| frame.post_processed(self.styles))
)?;
if self.float {
frame.set_parent(self.elem.location().unwrap());
}
Ok(frame.post_processed(self.styles))
})
}
/// The element's location.
pub fn location(&self) -> Location {
self.elem.location().unwrap()
}
}
/// Wraps a parameterized computation and caches its latest output.

View File

@ -1,18 +1,16 @@
use std::num::NonZeroUsize;
use super::{
distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Skip, Stop, Work,
};
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart};
use crate::introspection::{
Counter, CounterDisplayElem, CounterState, CounterUpdate, Locator, SplitLocator,
TagKind,
Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator,
SplitLocator, Tag,
};
use crate::layout::{
layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Frame, FrameItem,
OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size,
layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame,
FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size,
};
use crate::model::{
FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker,
@ -246,7 +244,8 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
clearance: bool,
) -> FlowResult<()> {
// If the float is already processed, skip it.
if self.skipped(Skip::Placed(placed.idx)) {
let loc = placed.location();
if self.skipped(loc) {
return Ok(());
}
@ -317,7 +316,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
// Put the float there.
area.push_float(placed, frame, align_y);
area.skips.push(Skip::Placed(placed.idx));
area.skips.push(loc);
// Trigger relayout.
Err(Stop::Relayout(placed.scope))
@ -391,7 +390,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
) -> FlowResult<()> {
// Ignore reference footnotes and already processed ones.
let loc = elem.location().unwrap();
if elem.is_ref() || self.skipped(Skip::Footnote(loc)) {
if elem.is_ref() || self.skipped(loc) {
return Ok(());
}
@ -420,14 +419,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
pod.size.y -= flow_need + separator_need + self.config.footnote.gap;
// Layout the footnote entry.
let frames = layout_fragment(
self.engine,
&FootnoteEntry::new(elem.clone()).pack(),
Locator::synthesize(elem.location().unwrap()),
self.config.shared,
pod,
)?
.into_frames();
let frames = layout_footnote(self.engine, self.config, &elem, pod)?.into_frames();
// Find nested footnotes in the entry.
let nested = find_in_frames::<FootnoteElem>(&frames);
@ -458,7 +450,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
// Save the footnote's frame.
area.push_footnote(self.config, first);
area.skips.push(Skip::Footnote(loc));
area.skips.push(loc);
regions.size.y -= note_need;
// Save the spill.
@ -501,10 +493,10 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
/// Checks whether an insertion was already processed and doesn't need to be
/// handled again.
fn skipped(&self, skip: Skip) -> bool {
self.work.skips.contains(&skip)
|| self.page_insertions.skips.contains(&skip)
|| self.column_insertions.skips.contains(&skip)
fn skipped(&self, loc: Location) -> bool {
self.work.skips.contains(&loc)
|| self.page_insertions.skips.contains(&loc)
|| self.column_insertions.skips.contains(&loc)
}
/// The amount of width needed by insertions.
@ -528,6 +520,29 @@ fn layout_footnote_separator(
)
}
/// Lay out a footnote.
fn layout_footnote(
engine: &mut Engine,
config: &Config,
elem: &Packed<FootnoteElem>,
pod: Regions,
) -> SourceResult<Fragment> {
let loc = elem.location().unwrap();
layout_fragment(
engine,
&FootnoteEntry::new(elem.clone()).pack(),
Locator::synthesize(loc),
config.shared,
pod,
)
.map(|mut fragment| {
for frame in &mut fragment {
frame.set_parent(loc);
}
fragment
})
}
/// An additive list of insertions.
#[derive(Default)]
struct Insertions<'a, 'b> {
@ -538,7 +553,7 @@ struct Insertions<'a, 'b> {
top_size: Abs,
bottom_size: Abs,
width: Abs,
skips: Vec<Skip>,
skips: Vec<Location>,
}
impl<'a, 'b> Insertions<'a, 'b> {
@ -836,8 +851,8 @@ fn find_in_frame_impl<T: NativeElement>(
let y = y_offset + pos.y;
match item {
FrameItem::Group(group) => find_in_frame_impl(output, &group.frame, y),
FrameItem::Tag(tag) if tag.kind() == TagKind::Start => {
if let Some(elem) = tag.elem().to_packed::<T>() {
FrameItem::Tag(Tag::Start(elem)) => {
if let Some(elem) = elem.to_packed::<T>() {
output.push((y, elem.clone()));
}
}

View File

@ -20,7 +20,7 @@ pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult<Frame
};
let init = distributor.snapshot();
let forced = match distributor.run() {
Ok(()) => true,
Ok(()) => distributor.composer.work.done(),
Err(Stop::Finish(forced)) => forced,
Err(err) => return Err(err),
};

View File

@ -255,18 +255,7 @@ struct Work<'a, 'b> {
/// Identifies floats and footnotes that can be skipped if visited because
/// they were already handled and incorporated as column or page level
/// insertions.
skips: Rc<HashSet<Skip>>,
}
/// Identifies an element that that can be skipped if visited because it was
/// already processed.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum Skip {
/// Uniquely identifies a placed elements. We can't use a [`Location`]
/// because `PlaceElem` is not currently locatable.
Placed(usize),
/// Uniquely identifies a footnote.
Footnote(Location),
skips: Rc<HashSet<Location>>,
}
impl<'a, 'b> Work<'a, 'b> {
@ -304,7 +293,7 @@ impl<'a, 'b> Work<'a, 'b> {
/// Add skipped floats and footnotes from the insertion areas to the skip
/// set.
fn extend_skips(&mut self, skips: &[Skip]) {
fn extend_skips(&mut self, skips: &[Location]) {
if !skips.is_empty() {
Rc::make_mut(&mut self.skips).extend(skips.iter().copied());
}

View File

@ -7,7 +7,7 @@ use std::sync::Arc;
use smallvec::SmallVec;
use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value};
use crate::introspection::Tag;
use crate::introspection::{Location, Tag};
use crate::layout::{
Abs, Axes, Corners, FixedAlignment, HideElem, Length, Point, Rel, Sides, Size,
Transform,
@ -407,8 +407,21 @@ impl Frame {
}
}
/// Add a label to the frame.
pub fn label(&mut self, label: Label) {
self.group(|g| g.label = Some(label));
}
/// Set a parent for the frame. As a result, all elements in the frame
/// become logically ordered immediately after the given location.
pub fn set_parent(&mut self, parent: Location) {
if !self.is_empty() {
self.group(|g| g.parent = Some(parent));
}
}
/// Wrap the frame's contents in a group and modify that group with `f`.
pub fn group<F>(&mut self, f: F)
fn group<F>(&mut self, f: F)
where
F: FnOnce(&mut GroupItem),
{
@ -557,6 +570,9 @@ pub struct GroupItem {
pub clip_path: Option<Path>,
/// The group's label.
pub label: Option<Label>,
/// The group's logical parent. All elements in this group are logically
/// ordered immediately after the parent's start location.
pub parent: Option<Location>,
}
impl GroupItem {
@ -567,6 +583,7 @@ impl GroupItem {
transform: Transform::identity(),
clip_path: None,
label: None,
parent: None,
}
}
}

View File

@ -575,38 +575,34 @@ fn add_par_line_marker(
locator: &mut SplitLocator,
top: Abs,
) {
if let Some(numbering) = ParLine::numbering_in(styles) {
let number_margin = ParLine::number_margin_in(styles);
let number_align = ParLine::number_align_in(styles);
let Some(numbering) = ParLine::numbering_in(styles) else { return };
let margin = ParLine::number_margin_in(styles);
let align = ParLine::number_align_in(styles);
// Delay resolving the number clearance until line numbers are laid out
// to avoid inconsistent spacing depending on varying font size.
let number_clearance = ParLine::number_clearance_in(styles);
// Delay resolving the number clearance until line numbers are laid out to
// avoid inconsistent spacing depending on varying font size.
let clearance = ParLine::number_clearance_in(styles);
let mut par_line =
ParLineMarker::new(numbering, number_align, number_margin, number_clearance)
.pack();
// Elements in tags must have a location for introspection to work. We do
// the work here instead of going through all of the realization process
// just for this, given we don't need to actually place the marker as we
// manually search for it in the frame later (when building a root flow,
// where line numbers can be displayed), so we just need it to be in a tag
// and to be valid (to have a location).
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
let key = crate::utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc);
// Elements in tags must have a location for introspection to work.
// We do the work here instead of going through all of the realization
// process just for this, given we don't need to actually place the
// marker as we manually search for it in the frame later (when
// building a root flow, where line numbers can be displayed), so we
// just need it to be in a tag and to be valid (to have a location).
let hash = crate::utils::hash128(&par_line);
let location = locator.next_location(engine.introspector, hash);
par_line.set_location(location);
// Create a tag through which we can search for this line's marker
// later. Its 'x' coordinate is not important, just the 'y'
// coordinate, as that's what is used for line numbers. We will place
// the tag among other subframes in the line such that it is aligned
// with the line's general baseline. However, the line number will
// still need to manually adjust its own 'y' position based on its own
// baseline.
let tag = Tag::new(par_line, hash);
output.push(Point::with_y(top), FrameItem::Tag(tag));
}
// Create start and end tags through which we can search for this line's
// marker later. The 'x' coordinate is not important, just the 'y'
// coordinate, as that's what is used for line numbers. We will place the
// tags among other subframes in the line such that it is aligned with the
// line's general baseline. However, the line number will still need to
// manually adjust its own 'y' position based on its own baseline.
let pos = Point::with_y(top);
output.push(pos, FrameItem::Tag(Tag::Start(marker)));
output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
}
/// How much a character should hang into the end margin.

View File

@ -1,7 +1,7 @@
use std::collections::HashSet;
use crate::foundations::StyleChain;
use crate::introspection::{Locator, SplitLocator, TagElem, TagKind};
use crate::introspection::{Locator, SplitLocator, Tag, TagElem};
use crate::layout::{PagebreakElem, Parity};
use crate::realize::Pair;
@ -121,7 +121,7 @@ pub fn collect<'a>(
/// a pagebreak to after it. Returns the position right after the last
/// non-migrated tag.
///
/// This is important because we want the positions of introspectible elements
/// This is important because we want the positions of introspectable elements
/// that technically started before a pagebreak, but have no visible content
/// yet, to be after the pagebreak. A typical case where this happens is `show
/// heading: it => pagebreak() + it`.
@ -136,9 +136,10 @@ fn migrate_unterminated_tags(children: &mut [Pair], mid: usize) -> usize {
// are terminated).
let excluded: HashSet<_> = children[start..mid]
.iter()
.filter_map(|(c, _)| c.to_packed::<TagElem>())
.filter(|elem| elem.tag.kind() == TagKind::End)
.map(|elem| elem.tag.location())
.filter_map(|(c, _)| match c.to_packed::<TagElem>()?.tag {
Tag::Start(_) => None,
Tag::End(loc, _) => Some(loc),
})
.collect();
// A key function that partitions the area of interest into three groups:

View File

@ -40,7 +40,7 @@ pub fn finalize(
}
// Add the "before" marginals. The order in which we push things here is
// important as it affects the relative ordering of introspectible elements
// important as it affects the relative ordering of introspectable elements
// and thus how counters resolve.
if let Some(background) = background {
frame.push_frame(Point::zero(), background);

View File

@ -80,8 +80,9 @@ fn layout_document_impl(
)?;
let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
let introspector = Introspector::new(&pages);
Ok(Document { pages, info, introspector: Introspector::default() })
Ok(Document { pages, info, introspector })
}
/// Layouts the document's pages.

View File

@ -1,4 +1,5 @@
use crate::foundations::{elem, scope, Cast, Content, Smart};
use crate::foundations::{elem, scope, Cast, Content, Packed, Smart};
use crate::introspection::{Locatable, Unqueriable};
use crate::layout::{Alignment, Em, Length, Rel};
/// Places content relatively to its parent container.
@ -65,7 +66,7 @@ use crate::layout::{Alignment, Em, Length, Rel};
///
/// The zero-width weak spacing serves to discard spaces between the function
/// call and the next word.
#[elem(scope)]
#[elem(scope, Locatable, Unqueriable)]
pub struct PlaceElem {
/// Relative to which position in the parent container to place the content.
///
@ -163,6 +164,10 @@ pub struct PlaceElem {
pub body: Content,
}
/// `PlaceElem` must be locatable to support logical ordering of floats, but I
/// do not want to expose `query(place)` for now.
impl Unqueriable for Packed<PlaceElem> {}
#[scope]
impl PlaceElem {
#[elem]

View File

@ -152,7 +152,6 @@ fn compile_impl(
// Layout!
document = crate::layout::layout_document(&mut engine, &content, styles)?;
document.introspector.rebuild(&document.pages);
iter += 1;
if timed!("check stabilized", document.introspector.validate(&constraint)) {

View File

@ -18,7 +18,7 @@ use crate::foundations::{
SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles,
Synthesize, Transformation,
};
use crate::introspection::{Locatable, SplitLocator, Tag, TagElem, TagKind};
use crate::introspection::{Locatable, SplitLocator, Tag, TagElem};
use crate::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
};
@ -352,9 +352,9 @@ fn visit_show_rules<'a>(
// If the element isn't yet prepared (we're seeing it for the first time),
// prepare it.
let mut tag = None;
let mut tags = None;
if !prepared {
tag = prepare(s.engine, s.locator, output.to_mut(), &mut map, styles)?;
tags = prepare(s.engine, s.locator, output.to_mut(), &mut map, styles)?;
}
// Apply a show rule step, if there is one.
@ -393,9 +393,9 @@ fn visit_show_rules<'a>(
};
// Push start tag.
if let Some(tag) = &tag {
let start_tag = TagElem::packed(tag.clone());
visit(s, s.store(start_tag), styles)?;
let (start, end) = tags.unzip();
if let Some(tag) = start {
visit(s, s.store(TagElem::packed(tag)), styles)?;
}
let prev_outside = s.outside;
@ -409,9 +409,8 @@ fn visit_show_rules<'a>(
s.engine.route.decrease();
// Push end tag.
if let Some(tag) = tag {
let end_tag = TagElem::packed(tag.with_kind(TagKind::End));
visit(s, s.store(end_tag), styles)?;
if let Some(tag) = end {
visit(s, s.store(TagElem::packed(tag)), styles)?;
}
Ok(true)
@ -517,21 +516,19 @@ fn prepare(
target: &mut Content,
map: &mut Styles,
styles: StyleChain,
) -> SourceResult<Option<Tag>> {
) -> SourceResult<Option<(Tag, Tag)>> {
// Generate a location for the element, which uniquely identifies it in
// the document. This has some overhead, so we only do it for elements
// that are explicitly marked as locatable and labelled elements.
//
// The element could already have a location even if it is not prepared
// when it stems from a query.
let mut key = None;
if target.location().is_some() {
key = Some(crate::utils::hash128(&target));
} else if target.can::<dyn Locatable>() || target.label().is_some() {
let hash = crate::utils::hash128(&target);
let location = locator.next_location(engine.introspector, hash);
target.set_location(location);
key = Some(hash);
let key = crate::utils::hash128(&target);
if target.location().is_none()
&& (target.can::<dyn Locatable>() || target.label().is_some())
{
let loc = locator.next_location(engine.introspector, key);
target.set_location(loc);
}
// Apply built-in show-set rules. User-defined show-set rules are already
@ -551,18 +548,20 @@ fn prepare(
// available in rules.
target.materialize(styles.chain(map));
// If the element is locatable, create a tag element to be able to find the
// element in the frames after layout. Do this after synthesis and
// If the element is locatable, create start and end tags to be able to find
// the element in the frames after layout. Do this after synthesis and
// materialization, so that it includes the synthesized fields. Do it before
// marking as prepared so that show-set rules will apply to this element
// when queried.
let tag = key.map(|key| Tag::new(target.clone(), key));
let tags = target
.location()
.map(|loc| (Tag::Start(target.clone()), Tag::End(loc, key)));
// Ensure that this preparation only runs once by marking the element as
// prepared.
target.mark_prepared();
Ok(tag)
Ok(tags)
}
/// Handles a styled element.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 670 B

View File

@ -9,13 +9,16 @@ A#footnote[A] \
A #footnote[A]
--- footnote-nested ---
// Currently, numbers a bit out of order if a nested footnote ends up in the
// same frame as another one. :(
First \
Second #footnote[A, #footnote[B, #footnote[C]]]
Third #footnote[D, #footnote[E]] \
Fourth #footnote[F]
--- footnote-nested-break-across-pages ---
#set page(height: 80pt)
A #footnote([I: ] + lines(6) + footnote[II])
B #footnote[III]
--- footnote-entry ---
// Test customization.
#show footnote: set text(red)

View File

@ -165,7 +165,6 @@ C
place(auto, float: true, block(width: 100%, height: 100%, fill: aqua))
)
--- place-float-column-align-auto ---
#set page(height: 150pt, columns: 2)
#set place(auto, float: true, clearance: 10pt)

View File

@ -267,3 +267,25 @@ HI#footnote.entry(clearance: 2.5em)[There]
// Test that figure caption separator is synthesized correctly.
#show figure.caption: c => test(c.separator, [#": "])
#figure(table[], caption: [This is a test caption])
--- issue-4966-figure-float-counter ---
#let c = context counter(figure.where(kind: image)).display()
#set align(center)
#c
#figure(
square(c),
placement: bottom,
caption: [A]
)
#c
#figure(
circle(c),
placement: top,
caption: [B]
)
#c