Optimized labels & introspector (#2801)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Sébastien d'Herbais de Thun 2023-11-30 12:57:04 +01:00 committed by GitHub
parent 79c2d1f29e
commit 5bdec9e1d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 109 additions and 81 deletions

1
Cargo.lock generated
View File

@ -2644,6 +2644,7 @@ dependencies = [
"ciborium", "ciborium",
"comemo", "comemo",
"csv", "csv",
"dashmap",
"ecow", "ecow",
"fontdb", "fontdb",
"hayagriva", "hayagriva",

View File

@ -38,6 +38,7 @@ clap_mangen = "0.2.10"
codespan-reporting = "0.11" codespan-reporting = "0.11"
comemo = "0.3.1" comemo = "0.3.1"
csv = "1" csv = "1"
dashmap = "5.5"
dirs = "5" dirs = "5"
ecow = { version = "0.2", features = ["serde"] } ecow = { version = "0.2", features = ["serde"] }
env_proxy = "0.4" env_proxy = "0.4"

View File

@ -4,7 +4,6 @@ use serde::Serialize;
use typst::diag::{bail, StrResult}; use typst::diag::{bail, StrResult};
use typst::eval::{eval_string, EvalMode, Tracer}; use typst::eval::{eval_string, EvalMode, Tracer};
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
use typst::introspection::Introspector;
use typst::model::Document; use typst::model::Document;
use typst::syntax::Span; use typst::syntax::Span;
use typst::World; use typst::World;
@ -76,7 +75,8 @@ fn retrieve(
})? })?
.cast::<LocatableSelector>()?; .cast::<LocatableSelector>()?;
Ok(Introspector::new(&document.pages) Ok(document
.introspector
.query(&selector.0) .query(&selector.0)
.into_iter() .into_iter()
.map(|x| x.into_inner()) .map(|x| x.into_inner())

View File

@ -4,8 +4,7 @@ use typst::engine::{Engine, Route};
use typst::eval::{Tracer, Vm}; use typst::eval::{Tracer, Vm};
use typst::foundations::{Label, Scopes, Value}; use typst::foundations::{Label, Scopes, Value};
use typst::introspection::{Introspector, Locator}; use typst::introspection::{Introspector, Locator};
use typst::layout::Frame; use typst::model::{BibliographyElem, Document};
use typst::model::BibliographyElem;
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use typst::World; use typst::World;
@ -75,12 +74,11 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
/// - All labels and descriptions for them, if available /// - All labels and descriptions for them, if available
/// - A split offset: All labels before this offset belong to nodes, all after /// - A split offset: All labels before this offset belong to nodes, all after
/// belong to a bibliography. /// belong to a bibliography.
pub fn analyze_labels(frames: &[Frame]) -> (Vec<(Label, Option<EcoString>)>, usize) { pub fn analyze_labels(document: &Document) -> (Vec<(Label, Option<EcoString>)>, usize) {
let mut output = vec![]; let mut output = vec![];
let introspector = Introspector::new(frames);
// Labels in the document. // Labels in the document.
for elem in introspector.all() { for elem in document.introspector.all() {
let Some(label) = elem.label() else { continue }; let Some(label) = elem.label() else { continue };
let details = elem let details = elem
.get_by_name("caption") .get_by_name("caption")
@ -98,7 +96,7 @@ pub fn analyze_labels(frames: &[Frame]) -> (Vec<(Label, Option<EcoString>)>, usi
let split = output.len(); let split = output.len();
// Bibliography keys. // Bibliography keys.
for (key, detail) in BibliographyElem::keys(introspector.track()) { for (key, detail) in BibliographyElem::keys(document.introspector.track()) {
output.push((Label::new(&key), detail)); output.push((Label::new(&key), detail));
} }

View File

@ -8,7 +8,7 @@ use typst::foundations::{
fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label,
NoneValue, Repr, Scope, Type, Value, NoneValue, Repr, Scope, Type, Value,
}; };
use typst::layout::Frame; use typst::model::Document;
use typst::syntax::{ use typst::syntax::{
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind,
}; };
@ -27,14 +27,18 @@ use crate::{plain_docs_sentence, summarize_font_family};
/// ///
/// When `explicit` is `true`, the user requested the completion by pressing /// When `explicit` is `true`, the user requested the completion by pressing
/// control and space or something similar. /// control and space or something similar.
///
/// Passing a `document` (from a previous compilation) is optional, but enhances
/// the autocompletions. Label completions, for instance, are only generated
/// when the document is available.
pub fn autocomplete( pub fn autocomplete(
world: &dyn World, world: &dyn World,
frames: &[Frame], document: Option<&Document>,
source: &Source, source: &Source,
cursor: usize, cursor: usize,
explicit: bool, explicit: bool,
) -> Option<(usize, Vec<Completion>)> { ) -> Option<(usize, Vec<Completion>)> {
let mut ctx = CompletionContext::new(world, frames, source, cursor, explicit)?; let mut ctx = CompletionContext::new(world, document, source, cursor, explicit)?;
let _ = complete_comments(&mut ctx) let _ = complete_comments(&mut ctx)
|| complete_field_accesses(&mut ctx) || complete_field_accesses(&mut ctx)
@ -966,7 +970,7 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
/// Context for autocompletion. /// Context for autocompletion.
struct CompletionContext<'a> { struct CompletionContext<'a> {
world: &'a (dyn World + 'a), world: &'a (dyn World + 'a),
frames: &'a [Frame], document: Option<&'a Document>,
global: &'a Scope, global: &'a Scope,
math: &'a Scope, math: &'a Scope,
text: &'a str, text: &'a str,
@ -984,7 +988,7 @@ impl<'a> CompletionContext<'a> {
/// Create a new autocompletion context. /// Create a new autocompletion context.
fn new( fn new(
world: &'a (dyn World + 'a), world: &'a (dyn World + 'a),
frames: &'a [Frame], document: Option<&'a Document>,
source: &'a Source, source: &'a Source,
cursor: usize, cursor: usize,
explicit: bool, explicit: bool,
@ -994,7 +998,7 @@ impl<'a> CompletionContext<'a> {
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
Some(Self { Some(Self {
world, world,
frames, document,
global: library.global.scope(), global: library.global.scope(),
math: library.math.scope(), math: library.math.scope(),
text, text,
@ -1094,7 +1098,8 @@ impl<'a> CompletionContext<'a> {
/// Add completions for labels and references. /// Add completions for labels and references.
fn label_completions(&mut self) { fn label_completions(&mut self) {
let (labels, split) = analyze_labels(self.frames); let Some(document) = self.document else { return };
let (labels, split) = analyze_labels(document);
let head = &self.text[..self.from]; let head = &self.text[..self.from];
let at = head.ends_with('@'); let at = head.ends_with('@');

View File

@ -1,9 +1,9 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use ecow::EcoString; use ecow::EcoString;
use typst::introspection::{Introspector, Meta}; use typst::introspection::Meta;
use typst::layout::{Frame, FrameItem, Point, Position, Size}; use typst::layout::{Frame, FrameItem, Point, Position, Size};
use typst::model::Destination; use typst::model::{Destination, Document};
use typst::syntax::{FileId, LinkedNode, Source, Span, SyntaxKind}; use typst::syntax::{FileId, LinkedNode, Source, Span, SyntaxKind};
use typst::visualize::Geometry; use typst::visualize::Geometry;
use typst::World; use typst::World;
@ -31,12 +31,10 @@ impl Jump {
/// Determine where to jump to based on a click in a frame. /// Determine where to jump to based on a click in a frame.
pub fn jump_from_click( pub fn jump_from_click(
world: &dyn World, world: &dyn World,
frames: &[Frame], document: &Document,
frame: &Frame, frame: &Frame,
click: Point, click: Point,
) -> Option<Jump> { ) -> Option<Jump> {
let mut introspector = None;
// Try to find a link first. // Try to find a link first.
for (pos, item) in frame.items() { for (pos, item) in frame.items() {
if let FrameItem::Meta(Meta::Link(dest), size) = item { if let FrameItem::Meta(Meta::Link(dest), size) = item {
@ -44,11 +42,9 @@ pub fn jump_from_click(
return Some(match dest { return Some(match dest {
Destination::Url(url) => Jump::Url(url.clone()), Destination::Url(url) => Jump::Url(url.clone()),
Destination::Position(pos) => Jump::Position(*pos), Destination::Position(pos) => Jump::Position(*pos),
Destination::Location(loc) => Jump::Position( Destination::Location(loc) => {
introspector Jump::Position(document.introspector.position(*loc))
.get_or_insert_with(|| Introspector::new(frames)) }
.position(*loc),
),
}); });
} }
} }
@ -60,7 +56,7 @@ pub fn jump_from_click(
FrameItem::Group(group) => { FrameItem::Group(group) => {
// TODO: Handle transformation. // TODO: Handle transformation.
if let Some(span) = if let Some(span) =
jump_from_click(world, frames, &group.frame, click - pos) jump_from_click(world, document, &group.frame, click - pos)
{ {
return Some(span); return Some(span);
} }

View File

@ -4,7 +4,8 @@ use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use typst::eval::{CapturesVisitor, Tracer}; use typst::eval::{CapturesVisitor, Tracer};
use typst::foundations::{repr, CastInfo, Repr, Value}; use typst::foundations::{repr, CastInfo, Repr, Value};
use typst::layout::{Frame, Length}; use typst::layout::Length;
use typst::model::Document;
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind}; use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
use typst::util::{round_2, Numeric}; use typst::util::{round_2, Numeric};
use typst::World; use typst::World;
@ -13,9 +14,13 @@ use crate::analyze::{analyze_expr, analyze_labels};
use crate::{plain_docs_sentence, summarize_font_family}; use crate::{plain_docs_sentence, summarize_font_family};
/// Describe the item under the cursor. /// Describe the item under the cursor.
///
/// Passing a `document` (from a previous compilation) is optional, but enhances
/// the autocompletions. Label completions, for instance, are only generated
/// when the document is available.
pub fn tooltip( pub fn tooltip(
world: &dyn World, world: &dyn World,
frames: &[Frame], document: Option<&Document>,
source: &Source, source: &Source,
cursor: usize, cursor: usize,
) -> Option<Tooltip> { ) -> Option<Tooltip> {
@ -26,7 +31,7 @@ pub fn tooltip(
named_param_tooltip(world, &leaf) named_param_tooltip(world, &leaf)
.or_else(|| font_tooltip(world, &leaf)) .or_else(|| font_tooltip(world, &leaf))
.or_else(|| label_tooltip(frames, &leaf)) .or_else(|| document.and_then(|doc| label_tooltip(doc, &leaf)))
.or_else(|| expr_tooltip(world, &leaf)) .or_else(|| expr_tooltip(world, &leaf))
.or_else(|| closure_tooltip(&leaf)) .or_else(|| closure_tooltip(&leaf))
} }
@ -145,14 +150,14 @@ fn length_tooltip(length: Length) -> Option<Tooltip> {
} }
/// Tooltip for a hovered reference or label. /// Tooltip for a hovered reference or label.
fn label_tooltip(frames: &[Frame], leaf: &LinkedNode) -> Option<Tooltip> { fn label_tooltip(document: &Document, leaf: &LinkedNode) -> Option<Tooltip> {
let target = match leaf.kind() { let target = match leaf.kind() {
SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'), SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'),
SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'), SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'),
_ => return None, _ => return None,
}; };
for (label, detail) in analyze_labels(frames).0 { for (label, detail) in analyze_labels(document).0 {
if label.as_str() == target { if label.as_str() == target {
return Some(Tooltip::Text(detail?)); return Some(Tooltip::Text(detail?));
} }

View File

@ -19,7 +19,6 @@ use ecow::{eco_format, EcoString};
use pdf_writer::types::Direction; use pdf_writer::types::Direction;
use pdf_writer::{Finish, Name, Pdf, Ref, TextStr}; use pdf_writer::{Finish, Name, Pdf, Ref, TextStr};
use typst::foundations::Datetime; use typst::foundations::Datetime;
use typst::introspection::Introspector;
use typst::layout::{Abs, Dir, Em, Transform}; use typst::layout::{Abs, Dir, Em, Transform};
use typst::model::Document; use typst::model::Document;
use typst::text::{Font, Lang}; use typst::text::{Font, Lang};
@ -70,10 +69,6 @@ pub fn pdf(
struct PdfContext<'a> { struct PdfContext<'a> {
/// The document that we're currently exporting. /// The document that we're currently exporting.
document: &'a Document, document: &'a Document,
/// An introspector for the document, used to resolve locations links and
/// the document outline.
introspector: Introspector,
/// The writer we are writing the PDF into. /// The writer we are writing the PDF into.
pdf: Pdf, pdf: Pdf,
/// Content of exported pages. /// Content of exported pages.
@ -128,7 +123,6 @@ impl<'a> PdfContext<'a> {
let page_tree_ref = alloc.bump(); let page_tree_ref = alloc.bump();
Self { Self {
document, document,
introspector: Introspector::new(&document.pages),
pdf: Pdf::new(), pdf: Pdf::new(),
pages: vec![], pages: vec![],
glyph_sets: HashMap::new(), glyph_sets: HashMap::new(),

View File

@ -18,7 +18,7 @@ pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
// Therefore, its next descendant must be added at its level, which is // Therefore, its next descendant must be added at its level, which is
// enforced in the manner shown below. // enforced in the manner shown below.
let mut last_skipped_level = None; let mut last_skipped_level = None;
for heading in ctx.introspector.query(&HeadingElem::elem().select()).iter() { for heading in ctx.document.introspector.query(&HeadingElem::elem().select()).iter() {
let leaf = HeadingNode::leaf((**heading).clone()); let leaf = HeadingNode::leaf((**heading).clone());
if leaf.bookmarked { if leaf.bookmarked {
@ -163,7 +163,7 @@ fn write_outline_item(
outline.title(TextStr(body.plain_text().trim())); outline.title(TextStr(body.plain_text().trim()));
let loc = node.element.location().unwrap(); let loc = node.element.location().unwrap();
let pos = ctx.introspector.position(loc); let pos = ctx.document.introspector.position(loc);
let index = pos.page.get() - 1; let index = pos.page.get() - 1;
if let Some(page) = ctx.pages.get(index) { if let Some(page) = ctx.pages.get(index) {
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());

View File

@ -180,7 +180,7 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
continue; continue;
} }
Destination::Position(pos) => *pos, Destination::Position(pos) => *pos,
Destination::Location(loc) => ctx.introspector.position(*loc), Destination::Location(loc) => ctx.document.introspector.position(*loc),
}; };
let index = pos.page.get() - 1; let index = pos.page.get() - 1;

View File

@ -24,6 +24,7 @@ chinese-number = { workspace = true }
ciborium = { workspace = true } ciborium = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
csv = { workspace = true } csv = { workspace = true }
dashmap = { workspace = true }
ecow = { workspace = true} ecow = { workspace = true}
fontdb = { workspace = true } fontdb = { workspace = true }
hayagriva = { workspace = true } hayagriva = { workspace = true }

View File

@ -1,11 +1,13 @@
use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap}; use std::collections::{BTreeSet, HashMap};
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use comemo::Prehashed; use comemo::Prehashed;
use dashmap::DashMap;
use ecow::{eco_format, EcoVec}; use ecow::{eco_format, EcoVec};
use indexmap::IndexMap; use indexmap::IndexMap;
use smallvec::SmallVec;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector}; use crate::foundations::{Content, Label, Repr, Selector};
@ -15,35 +17,38 @@ use crate::model::Numbering;
use crate::util::NonZeroExt; use crate::util::NonZeroExt;
/// Can be queried for elements and their positions. /// Can be queried for elements and their positions.
#[derive(Clone)]
pub struct Introspector { pub struct Introspector {
/// The number of pages in the document. /// The number of pages in the document.
pages: usize, pages: usize,
/// All introspectable elements. /// All introspectable elements.
elems: IndexMap<Location, (Prehashed<Content>, Position)>, elems: IndexMap<Location, (Prehashed<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]>>,
/// The page numberings, indexed by page number minus 1. /// The page numberings, indexed by page number minus 1.
page_numberings: Vec<Option<Numbering>>, page_numberings: Vec<Option<Numbering>>,
/// Caches queries done on the introspector. This is important because /// Caches queries done on the introspector. This is important because
/// even if all top-level queries are distinct, they often have shared /// even if all top-level queries are distinct, they often have shared
/// subqueries. Example: Individual counter queries with `before` that /// subqueries. Example: Individual counter queries with `before` that
/// all depend on a global counter query. /// all depend on a global counter query.
queries: RefCell<HashMap<u128, EcoVec<Prehashed<Content>>>>, queries: DashMap<u128, EcoVec<Prehashed<Content>>>,
} }
impl Introspector { impl Introspector {
/// Create a new introspector. /// Applies new frames in-place, reusing the existing allocations.
#[tracing::instrument(skip(frames))] #[tracing::instrument(skip_all)]
pub fn new(frames: &[Frame]) -> Self { pub fn rebuild(&mut self, frames: &[Frame]) {
let mut introspector = Self { self.pages = frames.len();
pages: frames.len(), self.elems.clear();
elems: IndexMap::new(), self.labels.clear();
page_numberings: vec![], self.page_numberings.clear();
queries: RefCell::default(), self.queries.clear();
};
for (i, frame) in frames.iter().enumerate() { for (i, frame) in frames.iter().enumerate() {
let page = NonZeroUsize::new(1 + i).unwrap(); let page = NonZeroUsize::new(1 + i).unwrap();
introspector.extract(frame, page, Transform::identity()); self.extract(frame, page, Transform::identity());
} }
introspector
} }
/// Extract metadata from a frame. /// Extract metadata from a frame.
@ -61,11 +66,17 @@ impl Introspector {
if !self.elems.contains_key(&content.location().unwrap()) => if !self.elems.contains_key(&content.location().unwrap()) =>
{ {
let pos = pos.transform(ts); let pos = pos.transform(ts);
let content = Prehashed::new(content.clone());
let ret = self.elems.insert( let ret = self.elems.insert(
content.location().unwrap(), content.location().unwrap(),
(Prehashed::new(content.clone()), Position { page, point: pos }), (content.clone(), Position { page, point: pos }),
); );
assert!(ret.is_none(), "duplicate locations"); assert!(ret.is_none(), "duplicate locations");
// Build the label cache.
if let Some(label) = content.label() {
self.labels.entry(label).or_default().push(self.elems.len() - 1);
}
} }
FrameItem::Meta(Meta::PageNumbering(numbering), _) => { FrameItem::Meta(Meta::PageNumbering(numbering), _) => {
self.page_numberings.push(numbering.clone()); self.page_numberings.push(numbering.clone());
@ -107,15 +118,19 @@ impl Introspector {
/// Query for all matching elements. /// Query for all matching elements.
pub fn query(&self, selector: &Selector) -> EcoVec<Prehashed<Content>> { pub fn query(&self, selector: &Selector) -> EcoVec<Prehashed<Content>> {
let hash = crate::util::hash128(selector); let hash = crate::util::hash128(selector);
if let Some(output) = self.queries.borrow().get(&hash) { if let Some(output) = self.queries.get(&hash) {
return output.clone(); return output.clone();
} }
let output = match selector { let output = match selector {
Selector::Elem(..) Selector::Label(label) => self
| Selector::Label(_) .labels
| Selector::Regex(_) .get(label)
| Selector::Can(_) => { .map(|indices| {
indices.iter().map(|&index| self.elems[index].0.clone()).collect()
})
.unwrap_or_default(),
Selector::Elem(..) | Selector::Regex(_) | Selector::Can(_) => {
self.all().filter(|elem| selector.matches(elem)).cloned().collect() self.all().filter(|elem| selector.matches(elem)).cloned().collect()
} }
Selector::Location(location) => { Selector::Location(location) => {
@ -182,7 +197,7 @@ impl Introspector {
.collect(), .collect(),
}; };
self.queries.borrow_mut().insert(hash, output.clone()); self.queries.insert(hash, output.clone());
output output
} }
@ -196,16 +211,15 @@ impl Introspector {
/// Query for a unique element with the label. /// Query for a unique element with the label.
pub fn query_label(&self, label: Label) -> StrResult<&Prehashed<Content>> { pub fn query_label(&self, label: Label) -> StrResult<&Prehashed<Content>> {
let mut found = None; let indices = self.labels.get(&label).ok_or_else(|| {
for elem in self.all().filter(|elem| elem.label() == Some(label)) {
if found.is_some() {
bail!("label `{}` occurs multiple times in the document", label.repr());
}
found = Some(elem);
}
found.ok_or_else(|| {
eco_format!("label `{}` does not exist in the document", label.repr()) 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());
}
Ok(&self.elems[indices[0]].0)
} }
/// The total number pages. /// The total number pages.
@ -237,6 +251,18 @@ impl Introspector {
impl Default for Introspector { impl Default for Introspector {
fn default() -> Self { fn default() -> Self {
Self::new(&[]) Self {
pages: 0,
elems: IndexMap::new(),
labels: HashMap::new(),
page_numberings: vec![],
queries: DashMap::new(),
}
}
}
impl Debug for Introspector {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("Introspector(..)")
} }
} }

View File

@ -111,8 +111,7 @@ fn typeset(
let styles = StyleChain::new(&library.styles); let styles = StyleChain::new(&library.styles);
let mut iter = 0; let mut iter = 0;
let mut document; let mut document = Document::default();
let mut introspector = Introspector::new(&[]);
// Relayout until all introspections stabilize. // Relayout until all introspections stabilize.
// If that doesn't happen within five attempts, we give up. // If that doesn't happen within five attempts, we give up.
@ -129,16 +128,15 @@ fn typeset(
route: Route::default(), route: Route::default(),
tracer: tracer.track_mut(), tracer: tracer.track_mut(),
locator: &mut locator, locator: &mut locator,
introspector: introspector.track_with(&constraint), introspector: document.introspector.track_with(&constraint),
}; };
// Layout! // Layout!
document = content.layout_root(&mut engine, styles)?; document = content.layout_root(&mut engine, styles)?;
document.introspector.rebuild(&document.pages);
introspector = Introspector::new(&document.pages);
iter += 1; iter += 1;
if introspector.validate(&constraint) { if document.introspector.validate(&constraint) {
break; break;
} }

View File

@ -5,7 +5,7 @@ use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Args, Array, Construct, Content, Datetime, Smart, StyleChain, Value, cast, elem, Args, Array, Construct, Content, Datetime, Smart, StyleChain, Value,
}; };
use crate::introspection::ManualPageCounter; use crate::introspection::{Introspector, ManualPageCounter};
use crate::layout::{Frame, LayoutRoot, PageElem}; use crate::layout::{Frame, LayoutRoot, PageElem};
/// The root element of a document and its metadata. /// The root element of a document and its metadata.
@ -110,6 +110,7 @@ impl LayoutRoot for DocumentElem {
author: self.author(styles).0, author: self.author(styles).0,
keywords: self.keywords(styles).0, keywords: self.keywords(styles).0,
date: self.date(styles), date: self.date(styles),
introspector: Introspector::default(),
}) })
} }
} }
@ -137,7 +138,7 @@ cast! {
} }
/// A finished document with metadata and page frames. /// A finished document with metadata and page frames.
#[derive(Debug, Default, Clone, Hash)] #[derive(Debug, Default, Clone)]
pub struct Document { pub struct Document {
/// The page frames. /// The page frames.
pub pages: Vec<Frame>, pub pages: Vec<Frame>,
@ -149,6 +150,8 @@ pub struct Document {
pub keywords: Vec<EcoString>, pub keywords: Vec<EcoString>,
/// The document's creation date. /// The document's creation date.
pub date: Smart<Option<Datetime>>, pub date: Smart<Option<Datetime>>,
/// Provides the ability to execute queries on the document.
pub introspector: Introspector,
} }
#[cfg(test)] #[cfg(test)]
@ -156,8 +159,8 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_document_is_send() { fn test_document_is_send_and_sync() {
fn ensure_send<T: Send>() {} fn ensure_send_and_sync<T: Send + Sync>() {}
ensure_send::<Document>(); ensure_send_and_sync::<Document>();
} }
} }