Introspection

This commit is contained in:
Laurenz 2022-12-02 15:41:39 +01:00
parent 5110a41de1
commit 9bc90c371f
24 changed files with 385 additions and 112 deletions

View File

@ -216,7 +216,7 @@ pub const ENUM: ListKind = 1;
pub const DESC: ListKind = 2;
/// How to label a list or enumeration.
#[derive(Debug, Clone, PartialEq, Hash)]
#[derive(Debug, Clone, Hash)]
pub enum Label {
/// The default labelling.
Default,

View File

@ -59,9 +59,11 @@ impl LayoutRoot for Content {
fn cached(
node: &Content,
world: comemo::Tracked<dyn World>,
provider: TrackedMut<StabilityProvider>,
introspector: Tracked<Introspector>,
styles: StyleChain,
) -> SourceResult<Document> {
let mut vt = Vt { world };
let mut vt = Vt { world, provider, introspector };
let scratch = Scratch::default();
let (realized, styles) = realize_root(&mut vt, &scratch, node, styles)?;
realized
@ -70,7 +72,13 @@ impl LayoutRoot for Content {
.layout_root(&mut vt, styles)
}
cached(self, vt.world, styles)
cached(
self,
vt.world,
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
styles,
)
}
}
@ -97,10 +105,12 @@ impl Layout for Content {
fn cached(
node: &Content,
world: comemo::Tracked<dyn World>,
provider: TrackedMut<StabilityProvider>,
introspector: Tracked<Introspector>,
styles: StyleChain,
regions: &Regions,
) -> SourceResult<Fragment> {
let mut vt = Vt { world };
let mut vt = Vt { world, provider, introspector };
let scratch = Scratch::default();
let (realized, styles) = realize_block(&mut vt, &scratch, node, styles)?;
let barrier = Style::Barrier(realized.id());
@ -111,7 +121,14 @@ impl Layout for Content {
.layout(&mut vt, styles, regions)
}
cached(self, vt.world, styles, regions)
cached(
self,
vt.world,
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
styles,
regions,
)
}
}
@ -290,6 +307,15 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<()> {
// Prepare only if this is the first application for this node.
if let Some(node) = content.with::<dyn Prepare>() {
if !content.is_prepared() {
let prepared = node.prepare(self.vt, content.clone().prepared(), styles);
let stored = self.scratch.content.alloc(prepared);
return self.accept(stored, styles);
}
}
if let Some(styled) = content.to::<StyledNode>() {
return self.styled(styled, styles);
}

View File

@ -5,7 +5,7 @@ use crate::prelude::*;
use crate::text::TextNode;
/// Layouts its child onto one or multiple pages.
#[derive(PartialEq, Clone, Hash)]
#[derive(Clone, Hash)]
pub struct PageNode(pub Content);
#[node]
@ -157,7 +157,7 @@ impl PagebreakNode {
}
/// A header, footer, foreground or background definition.
#[derive(Debug, Clone, PartialEq, Hash)]
#[derive(Debug, Clone, Hash)]
pub enum Marginal {
/// Nothing,
None,

View File

@ -57,13 +57,15 @@ impl ParNode {
fn cached(
par: &ParNode,
world: Tracked<dyn World>,
provider: TrackedMut<StabilityProvider>,
introspector: Tracked<Introspector>,
styles: StyleChain,
consecutive: bool,
width: Abs,
base: Size,
expand: bool,
) -> SourceResult<Fragment> {
let mut vt = Vt { world };
let mut vt = Vt { world, provider, introspector };
// Collect all text into one string for BiDi analysis.
let (text, segments) = collect(par, &styles, consecutive);
@ -80,7 +82,17 @@ impl ParNode {
finalize(&mut vt, &p, &lines, width, base, expand)
}
cached(self, vt.world, styles, consecutive, width, base, expand)
cached(
self,
vt.world,
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
styles,
consecutive,
width,
base,
expand,
)
}
}

View File

@ -27,11 +27,9 @@ impl LayoutRoot for DocumentNode {
}
Ok(Document {
metadata: Metadata {
title: styles.get(Self::TITLE).clone(),
author: styles.get(Self::AUTHOR).clone(),
},
pages,
title: styles.get(Self::TITLE).clone(),
author: styles.get(Self::AUTHOR).clone(),
})
}
}

View File

@ -25,10 +25,6 @@ impl LinkNode {
#[node(Show, Finalize)]
impl LinkNode {
/// A destination the text should be linked to.
#[property(skip, referenced)]
pub(crate) const DEST: Option<Destination> = None;
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let dest = args.expect::<Destination>("destination")?;
Ok(match dest {
@ -61,6 +57,6 @@ impl Show for LinkNode {
impl Finalize for LinkNode {
fn finalize(&self, realized: Content) -> Content {
realized.styled(Self::DEST, Some(self.dest.clone()))
realized.styled(Meta::DATA, vec![Meta::Link(self.dest.clone())])
}
}

View File

@ -16,8 +16,9 @@ pub use typst::geom::*;
#[doc(no_inline)]
pub use typst::model::{
array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast,
Content, Dict, Finalize, Fold, Func, Label, Node, NodeId, Resolve, Selector, Show,
Smart, Str, StyleChain, StyleMap, StyleVec, Unlabellable, Value, Vm, Vt,
Content, Dict, Finalize, Fold, Func, Introspector, Label, Node, NodeId, Prepare,
Resolve, Selector, Show, Smart, StabilityProvider, Str, StyleChain, StyleMap,
StyleVec, Unlabellable, Value, Vm, Vt,
};
#[doc(no_inline)]
pub use typst::syntax::{Span, Spanned};

View File

@ -13,6 +13,9 @@ pub trait ContentExt {
/// Underline this content.
fn underlined(self) -> Self;
/// Link the content to a destination.
fn linked(self, dest: Destination) -> Self;
/// Force a size for this content.
fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self;
@ -45,6 +48,10 @@ impl ContentExt for Content {
crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack()
}
fn linked(self, dest: Destination) -> Self {
self.styled(Meta::DATA, vec![Meta::Link(dest.clone())])
}
fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
crate::layout::BoxNode { sizing, child: self }.pack()
}

View File

@ -42,6 +42,19 @@ impl RawNode {
}
}
impl Prepare for RawNode {
fn prepare(&self, _: &mut Vt, mut this: Content, styles: StyleChain) -> Content {
this.push_field(
"lang",
match styles.get(Self::LANG) {
Some(lang) => Value::Str(lang.clone().into()),
None => Value::None,
},
);
this
}
}
impl Show for RawNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> Content {
let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase());

View File

@ -6,7 +6,6 @@ use typst::font::{Font, FontVariant};
use typst::util::SliceExt;
use super::*;
use crate::meta::LinkNode;
use crate::prelude::*;
/// The result of shaping text.
@ -93,7 +92,6 @@ impl<'a> ShapedText<'a> {
let lang = self.styles.get(TextNode::LANG);
let decos = self.styles.get(TextNode::DECO);
let fill = self.styles.get(TextNode::FILL);
let link = self.styles.get(LinkNode::DEST);
for ((font, y_offset), group) in
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
@ -128,10 +126,8 @@ impl<'a> ShapedText<'a> {
offset += width;
}
// Apply link if it exists.
if let Some(dest) = link {
frame.link(dest.clone());
}
// Apply metadata.
frame.meta(self.styles);
frame
}

View File

@ -2,7 +2,6 @@ use std::ffi::OsStr;
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::meta::LinkNode;
use crate::prelude::*;
/// Show a raster or vector graphic.
@ -89,10 +88,8 @@ impl Layout for ImageNode {
frame.clip();
}
// Apply link if it exists.
if let Some(url) = styles.get(LinkNode::DEST) {
frame.link(url.clone());
}
// Apply metadata.
frame.meta(styles);
Ok(Fragment::frame(frame))
}

View File

@ -1,6 +1,5 @@
use std::f64::consts::SQRT_2;
use crate::meta::LinkNode;
use crate::prelude::*;
/// A sizable and fillable shape with optional content.
@ -160,10 +159,8 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
}
}
// Apply link if it exists.
if let Some(url) = styles.get(LinkNode::DEST) {
frame.link(url.clone());
}
// Apply metadata.
frame.meta(styles);
Ok(Fragment::frame(frame))
}

View File

@ -10,21 +10,14 @@ use crate::geom::{
Abs, Align, Axes, Dir, Em, Numeric, Paint, Point, Shape, Size, Transform,
};
use crate::image::Image;
use crate::model::{dict, Dict, Value};
use crate::model::{dict, node, Content, Dict, Fold, StableId, StyleChain, Value};
use crate::util::EcoString;
/// A finished document with metadata and page frames.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Document {
/// The document's metadata.
pub metadata: Metadata,
/// The page frames.
pub pages: Vec<Frame>,
}
/// Document metadata.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Metadata {
/// The document's title.
pub title: Option<EcoString>,
/// The document's author.
@ -32,7 +25,7 @@ pub struct Metadata {
}
/// A partial layout result.
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone)]
pub struct Fragment(Vec<Frame>);
impl Fragment {
@ -113,7 +106,7 @@ impl<'a> IntoIterator for &'a mut Fragment {
}
/// A finished layout with elements at fixed positions.
#[derive(Default, Clone, Eq, PartialEq)]
#[derive(Default, Clone)]
pub struct Frame {
/// The size of the frame.
size: Size,
@ -336,9 +329,11 @@ impl Frame {
}
}
/// Link the whole frame to a resource.
pub fn link(&mut self, dest: Destination) {
self.push(Point::zero(), Element::Link(dest, self.size));
/// Attach the metadata from this style chain to the frame.
pub fn meta(&mut self, styles: StyleChain) {
for meta in styles.get(Meta::DATA) {
self.push(Point::zero(), Element::Meta(meta, self.size));
}
}
/// Arbitrarily transform the contents of the frame.
@ -374,7 +369,7 @@ impl Debug for Frame {
}
/// The building block frames are composed of.
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone)]
pub enum Element {
/// A group of elements.
Group(Group),
@ -384,8 +379,8 @@ pub enum Element {
Shape(Shape),
/// An image and its size.
Image(Image, Size),
/// A link to an external resource and its trigger region.
Link(Destination, Size),
/// Meta information and the region it applies to.
Meta(Meta, Size),
}
impl Debug for Element {
@ -395,13 +390,13 @@ impl Debug for Element {
Self::Text(text) => write!(f, "{text:?}"),
Self::Shape(shape) => write!(f, "{shape:?}"),
Self::Image(image, _) => write!(f, "{image:?}"),
Self::Link(dest, _) => write!(f, "Link({dest:?})"),
Self::Meta(meta, _) => write!(f, "{meta:?}"),
}
}
}
/// A group of elements with optional clipping.
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone)]
pub struct Group {
/// The group's frame.
pub frame: Frame,
@ -543,6 +538,33 @@ impl FromStr for Region {
}
}
/// Meta information that isn't visible or renderable.
#[derive(Debug, Clone, Hash)]
pub enum Meta {
/// An internal or external link.
Link(Destination),
/// An identifiable piece of content that produces something within the
/// area this metadata is attached to.
Node(StableId, Content),
}
#[node]
impl Meta {
/// Metadata that should be attached to all elements affected by this style
/// property.
#[property(fold, skip)]
pub const DATA: Vec<Meta> = vec![];
}
impl Fold for Vec<Meta> {
type Output = Self;
fn fold(mut self, outer: Self::Output) -> Self::Output {
self.extend(outer);
self
}
}
/// A link destination.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Destination {

View File

@ -14,7 +14,7 @@ use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr};
use self::outline::HeadingNode;
use self::page::Page;
use crate::doc::{Document, Lang, Metadata};
use crate::doc::{Document, Lang};
use crate::font::Font;
use crate::geom::{Abs, Dir, Em};
use crate::image::Image;
@ -23,7 +23,7 @@ use crate::image::Image;
///
/// Returns the raw bytes making up the PDF file.
pub fn pdf(document: &Document) -> Vec<u8> {
let mut ctx = PdfContext::new(&document.metadata);
let mut ctx = PdfContext::new(document);
page::construct_pages(&mut ctx, &document.pages);
font::write_fonts(&mut ctx);
image::write_images(&mut ctx);
@ -38,7 +38,7 @@ const D65_GRAY: Name<'static> = Name(b"d65gray");
/// Context for exporting a whole PDF document.
pub struct PdfContext<'a> {
metadata: &'a Metadata,
document: &'a Document,
writer: PdfWriter,
pages: Vec<Page>,
page_heights: Vec<f32>,
@ -55,11 +55,11 @@ pub struct PdfContext<'a> {
}
impl<'a> PdfContext<'a> {
fn new(metadata: &'a Metadata) -> Self {
fn new(document: &'a Document) -> Self {
let mut alloc = Ref::new(1);
let page_tree_ref = alloc.bump();
Self {
metadata,
document,
writer: PdfWriter::new(),
pages: vec![],
page_heights: vec![],
@ -116,10 +116,10 @@ fn write_catalog(ctx: &mut PdfContext) {
// Write the document information.
let mut info = ctx.writer.document_info(ctx.alloc.bump());
if let Some(title) = &ctx.metadata.title {
if let Some(title) = &ctx.document.title {
info.title(TextStr(title));
}
if let Some(author) = &ctx.metadata.author {
if let Some(author) = &ctx.document.author {
info.author(TextStr(author));
}
info.creator(TextStr("Typst"));

View File

@ -3,7 +3,7 @@ use pdf_writer::writers::ColorSpace;
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB};
use crate::doc::{Destination, Element, Frame, Group, Text};
use crate::doc::{Destination, Element, Frame, Group, Meta, Text};
use crate::font::Font;
use crate::geom::{
self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
@ -287,7 +287,10 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) {
Element::Text(text) => write_text(ctx, x, y, text),
Element::Shape(shape) => write_shape(ctx, x, y, shape),
Element::Image(image, size) => write_image(ctx, x, y, image, *size),
Element::Link(dest, size) => write_link(ctx, pos, dest, *size),
Element::Meta(meta, size) => match meta {
Meta::Link(dest) => write_link(ctx, pos, dest, *size),
_ => {}
},
}
}
}

View File

@ -57,7 +57,7 @@ fn render_frame(
Element::Image(image, size) => {
render_image(canvas, ts, mask, image, *size);
}
Element::Link(_, _) => {}
Element::Meta(_, _) => {}
}
}
}

View File

@ -255,17 +255,23 @@ castable! {
}
castable! {
Destination,
Expected: "string or dictionary with `page`, `x`, and `y` keys",
Value::Str(string) => Self::Url(string.into()),
Location,
Expected: "dictionary with `page`, `x`, and `y` keys",
Value::Dict(dict) => {
let page = dict.get("page")?.clone().cast()?;
let x: Length = dict.get("x")?.clone().cast()?;
let y: Length = dict.get("y")?.clone().cast()?;
Self::Internal(Location { page, pos: Point::new(x.abs, y.abs) })
Self { page, pos: Point::new(x.abs, y.abs) }
},
}
castable! {
Destination,
Expected: "string or dictionary with `page`, `x`, and `y` keys",
Value::Str(string) => Self::Url(string.into()),
v @ Value::Dict(_) => Self::Internal(v.cast()?),
}
castable! {
FontStyle,
Expected: "string",

View File

@ -21,14 +21,16 @@ use crate::World;
pub struct Content {
obj: Arc<dyn Bounds>,
span: Option<Span>,
meta: ThinVec<Meta>,
modifiers: ThinVec<Modifier>,
}
/// Metadata that can be attached to content.
/// Modifiers that can be attached to content.
#[derive(Debug, Clone, PartialEq, Hash)]
enum Meta {
enum Modifier {
Prepared,
Guard(Guard),
Label(Label),
Field(EcoString, Value),
}
impl Content {
@ -62,17 +64,22 @@ impl Content {
/// Attach a label to the content.
pub fn labelled(mut self, label: Label) -> Self {
for meta in &mut self.meta {
if let Meta::Label(prev) = meta {
for modifier in &mut self.modifiers {
if let Modifier::Label(prev) = modifier {
*prev = label;
return self;
}
}
self.meta.push(Meta::Label(label));
self.modifiers.push(Modifier::Label(label));
self
}
/// Attach a field to the content.
pub fn push_field(&mut self, name: impl Into<EcoString>, value: Value) {
self.modifiers.push(Modifier::Field(name.into(), value));
}
/// Style this content with a single style property.
pub fn styled<K: Key>(self, key: K, value: K::Value) -> Self {
self.styled_with_entry(Style::Property(Property::new(key, value)))
@ -146,8 +153,8 @@ impl Content {
/// The content's label.
pub fn label(&self) -> Option<&Label> {
self.meta.iter().find_map(|meta| match meta {
Meta::Label(label) => Some(label),
self.modifiers.iter().find_map(|modifier| match modifier {
Modifier::Label(label) => Some(label),
_ => None,
})
}
@ -161,6 +168,14 @@ impl Content {
});
}
for modifier in &self.modifiers {
if let Modifier::Field(other, value) = modifier {
if name == other.as_str() {
return Some(value.clone());
}
}
}
self.obj.field(name)
}
@ -201,10 +216,23 @@ impl Content {
/// Disable a show rule recipe.
#[doc(hidden)]
pub fn guarded(mut self, id: Guard) -> Self {
self.meta.push(Meta::Guard(id));
self.modifiers.push(Modifier::Guard(id));
self
}
/// Mark this content as prepared.
#[doc(hidden)]
pub fn prepared(mut self) -> Self {
self.modifiers.push(Modifier::Prepared);
self
}
/// Whether this node was prepared.
#[doc(hidden)]
pub fn is_prepared(&self) -> bool {
self.modifiers.contains(&Modifier::Prepared)
}
/// Whether a label can be attached to the content.
pub(super) fn labellable(&self) -> bool {
!self.has::<dyn Unlabellable>()
@ -212,18 +240,21 @@ impl Content {
/// Whether no show rule was executed for this node so far.
pub(super) fn is_pristine(&self) -> bool {
!self.meta.iter().any(|meta| matches!(meta, Meta::Guard(_)))
!self
.modifiers
.iter()
.any(|modifier| matches!(modifier, Modifier::Guard(_)))
}
/// Check whether a show rule recipe is disabled.
pub(super) fn is_guarded(&self, id: Guard) -> bool {
self.meta.contains(&Meta::Guard(id))
self.modifiers.contains(&Modifier::Guard(id))
}
/// Copy the metadata from other content.
pub(super) fn copy_meta(&mut self, from: &Content) {
/// Copy the modifiers from another piece of content.
pub(super) fn copy_modifiers(&mut self, from: &Content) {
self.span = from.span;
self.meta = from.meta.clone();
self.modifiers = from.modifiers.clone();
}
}
@ -239,12 +270,6 @@ impl Default for Content {
}
}
impl PartialEq for Content {
fn eq(&self, other: &Self) -> bool {
(*self.obj).hash128() == (*other.obj).hash128()
}
}
impl Add for Content {
type Output = Self;
@ -371,7 +396,7 @@ pub trait Node: 'static + Capable {
Content {
obj: Arc::new(self),
span: None,
meta: ThinVec::new(),
modifiers: ThinVec::new(),
}
}

View File

@ -309,7 +309,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Color(a), Color(b)) => a == b,
(Str(a), Str(b)) => a == b,
(Label(a), Label(b)) => a == b,
(Content(a), Content(b)) => a == b,
(Array(a), Array(b)) => a == b,
(Dict(a), Dict(b)) => a == b,
(Func(a), Func(b)) => a == b,

View File

@ -3,6 +3,10 @@ use crate::diag::SourceResult;
/// Whether the target is affected by show rules in the given style chain.
pub fn applicable(target: &Content, styles: StyleChain) -> bool {
if target.has::<dyn Prepare>() && !target.is_prepared() {
return true;
}
// Find out how many recipes there are.
let mut n = styles.recipes().count();
@ -90,7 +94,7 @@ fn try_apply(
let make = |s| {
let mut content = item!(text)(s);
content.copy_meta(&target);
content.copy_modifiers(&target);
content
};
@ -124,6 +128,13 @@ fn try_apply(
}
}
/// Preparations before execution of any show rule.
#[capability]
pub trait Prepare {
/// Prepare the node for show rule application.
fn prepare(&self, vt: &mut Vt, this: Content, styles: StyleChain) -> Content;
}
/// The base recipe for a node.
#[capability]
pub trait Show {

View File

@ -17,7 +17,7 @@ use crate::util::ReadableTypeId;
use crate::World;
/// A map of style properties.
#[derive(Default, Clone, PartialEq, Hash)]
#[derive(Default, Clone, Hash)]
pub struct StyleMap(Vec<Style>);
impl StyleMap {
@ -117,7 +117,7 @@ impl Debug for StyleMap {
}
/// A single style property, recipe or barrier.
#[derive(Clone, PartialEq, Hash)]
#[derive(Clone, Hash)]
pub enum Style {
/// A style property originating from a set rule or constructor.
Property(Property),
@ -302,7 +302,7 @@ impl Debug for KeyId {
}
/// A show rule recipe.
#[derive(Clone, PartialEq, Hash)]
#[derive(Clone, Hash)]
pub struct Recipe {
/// The span errors are reported with.
pub span: Span,
@ -374,6 +374,11 @@ pub enum Selector {
}
impl Selector {
/// Define a simple node selector.
pub fn node<T: 'static>() -> Self {
Self::Node(NodeId::of::<T>(), None)
}
/// Define a simple text selector.
pub fn text(text: &str) -> Self {
Self::Regex(Regex::new(&regex::escape(text)).unwrap())
@ -399,7 +404,7 @@ impl Selector {
}
/// A show rule transformation that can be applied to a match.
#[derive(Debug, Clone, PartialEq, Hash)]
#[derive(Debug, Clone, Hash)]
pub enum Transform {
/// Replacement content.
Content(Content),

View File

@ -1,8 +1,15 @@
use comemo::Tracked;
use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::Hash;
use std::num::NonZeroUsize;
use super::{Content, StyleChain};
use comemo::{Track, Tracked, TrackedMut};
use super::{Content, Selector, StyleChain, Value};
use crate::diag::SourceResult;
use crate::doc::Document;
use crate::doc::{Document, Element, Frame, Location, Meta};
use crate::geom::Transform;
use crate::util::hash128;
use crate::World;
/// Typeset content into a fully layouted document.
@ -10,8 +17,30 @@ use crate::World;
pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Document> {
let library = world.library();
let styles = StyleChain::new(&library.styles);
let mut vt = Vt { world };
(library.items.layout)(&mut vt, content, styles)
let mut document;
let mut iter = 0;
let mut introspector = Introspector::new();
// Relayout until all introspections stabilize.
// If that doesn't happen within five attempts, we give up.
loop {
let mut provider = StabilityProvider::new();
let mut vt = Vt {
world,
provider: provider.track_mut(),
introspector: introspector.track(),
};
document = (library.items.layout)(&mut vt, content, styles)?;
iter += 1;
if iter >= 5 || introspector.update(&document) {
break;
}
}
Ok(document)
}
/// A virtual typesetter.
@ -22,6 +51,12 @@ pub struct Vt<'a> {
/// The compilation environment.
#[doc(hidden)]
pub world: Tracked<'a, dyn World>,
/// Provides stable identities to nodes.
#[doc(hidden)]
pub provider: TrackedMut<'a, StabilityProvider>,
/// Provides access to information about the document.
#[doc(hidden)]
pub introspector: Tracked<'a, Introspector>,
}
impl<'a> Vt<'a> {
@ -29,4 +64,132 @@ impl<'a> Vt<'a> {
pub fn world(&self) -> Tracked<'a, dyn World> {
self.world
}
/// Produce a stable identifier for this call site.
///
/// The key should be something that identifies the call site, but is not
/// necessarily unique. The stable marker incorporates the key's hash plus
/// additional disambiguation from other call sites with the same key.
///
/// The returned id can be attached to content as metadata is the then
/// locatable through [`locate`](Self::locate).
pub fn identify<T: Hash>(&mut self, key: &T) -> StableId {
self.provider.identify(hash128(key))
}
/// Locate all metadata matches for the given selector.
pub fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> {
self.introspector.locate(selector)
}
}
/// Stably identifies a call site across multiple layout passes.
///
/// This struct is created by [`Vt::identify`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StableId(u128, u64);
/// Provides stable identities to nodes.
#[derive(Clone)]
#[doc(hidden)]
pub struct StabilityProvider(HashMap<u128, u64>);
impl StabilityProvider {
/// Create a new stability provider.
fn new() -> Self {
Self(HashMap::new())
}
}
#[comemo::track]
impl StabilityProvider {
/// Produce a stable identifier for this call site.
fn identify(&mut self, hash: u128) -> StableId {
let slot = self.0.entry(hash).or_default();
let id = StableId(hash, *slot);
*slot += 1;
id
}
}
/// Provides access to information about the document.
#[doc(hidden)]
pub struct Introspector {
nodes: Vec<(StableId, Content)>,
queries: RefCell<Vec<(Selector, u128)>>,
}
impl Introspector {
/// Create a new introspector.
fn new() -> Self {
Self { nodes: vec![], queries: RefCell::new(vec![]) }
}
/// Update the information given new frames and return whether we can stop
/// layouting.
fn update(&mut self, document: &Document) -> bool {
self.nodes.clear();
for (i, frame) in document.pages.iter().enumerate() {
let page = NonZeroUsize::new(1 + i).unwrap();
self.extract(frame, page, Transform::identity());
}
let queries = std::mem::take(&mut self.queries).into_inner();
for (selector, hash) in queries {
let nodes = self.locate_impl(&selector);
if hash128(&nodes) != hash {
return false;
}
}
true
}
/// Extract metadata from a frame.
fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) {
for (pos, element) in frame.elements() {
match *element {
Element::Group(ref group) => {
let ts = ts
.pre_concat(Transform::translate(pos.x, pos.y))
.pre_concat(group.transform);
self.extract(&group.frame, page, ts);
}
Element::Meta(Meta::Node(id, ref content), _) => {
if !self.nodes.iter().any(|&(prev, _)| prev == id) {
let pos = pos.transform(ts);
let mut node = content.clone();
let loc = Location { page, pos };
node.push_field("loc", Value::Dict(loc.encode()));
self.nodes.push((id, node));
}
}
_ => {}
}
}
}
}
#[comemo::track]
impl Introspector {
/// Locate all metadata matches for the given selector.
fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> {
let nodes = self.locate_impl(&selector);
let mut queries = self.queries.borrow_mut();
if !queries.iter().any(|(prev, _)| prev == &selector) {
queries.push((selector, hash128(&nodes)));
}
nodes
}
}
impl Introspector {
fn locate_impl(&self, selector: &Selector) -> Vec<(StableId, &Content)> {
self.nodes
.iter()
.map(|(id, node)| (*id, node))
.filter(|(_, target)| selector.matches(target))
.collect()
}
}

View File

@ -12,7 +12,7 @@ use elsa::FrozenVec;
use once_cell::unsync::OnceCell;
use tiny_skia as sk;
use typst::diag::{bail, FileError, FileResult};
use typst::doc::{Document, Element, Frame, Metadata};
use typst::doc::{Document, Element, Frame, Meta};
use typst::font::{Font, FontBook};
use typst::geom::{Abs, RgbaColor, Sides};
use typst::model::{Library, Smart, Value};
@ -349,7 +349,7 @@ fn test(
line += part.lines().count() + 1;
}
let document = Document { pages: frames, metadata: Metadata::default() };
let document = Document { pages: frames, ..Default::default() };
if compare_ever {
if let Some(pdf_path) = pdf_path {
let pdf_data = typst::export::pdf(&document);
@ -699,7 +699,7 @@ fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) {
let ts = ts.pre_concat(group.transform.into());
render_links(canvas, ts, &group.frame);
}
Element::Link(_, size) => {
Element::Meta(Meta::Link(_), size) => {
let w = size.x.to_pt() as f32;
let h = size.y.to_pt() as f32;
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();

View File

@ -152,15 +152,11 @@
#test(test == test, true)
#test((() => {}) == (() => {}), false)
// Content compares by hash equality.
// Content cannot be compared.
#let t = [a]
#test(t == t, true)
#test([] == [], true)
#test([a] == [a], true)
#test([[a]] == [a], true)
#test([] == [a], false)
#test(box[] == box[], true)
#test(box[a] == box[], false)
#test(t == t, false)
#test([] == [], false)
#test([a] == [a], false)
---
// Test comparison operators.