Measurement and introspection rework

This commit is contained in:
Laurenz 2023-03-19 10:19:24 +01:00
parent c7f4d6b12e
commit 0ba99ab8aa
28 changed files with 688 additions and 557 deletions

View File

@ -118,8 +118,9 @@ compilation.
typst --watch file.typ
```
If you prefer an integrated IDE-like experience, you can also check out the
[Typst web app][app], which is currently in public beta.
If you prefer an integrated IDE-like experience with autocompletion and instant
preview, you can also check out the [Typst web app][app], which is currently in
public beta.
## Build from source
To build Typst yourself, you need to have the [latest stable Rust][rust]

View File

@ -88,7 +88,7 @@ fantasy encyclopedia.
#set text(font: "Inria Serif")
\~ #emph(it.body)
#(counter(heading)
.get(it.numbering)) \~
.display(it.numbering)) \~
]
= Dragon

View File

@ -0,0 +1,20 @@
use crate::prelude::*;
/// Measure the size of content.
///
/// Display: Measure
/// Category: layout
/// Returns: array
#[func]
pub fn measure(
/// The content whose size to measure.
content: Content,
/// The styles with which to layout the content.
styles: StyleMap,
) -> Value {
let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false));
let styles = StyleChain::new(&styles);
let frame = content.measure(&mut vm.vt, styles, pod)?.into_frame();
let Size { x, y } = frame.size();
Value::Array(array![x, y])
}

View File

@ -10,6 +10,7 @@ mod fragment;
mod grid;
mod hide;
mod list;
mod measure;
mod pad;
mod page;
mod par;
@ -31,6 +32,7 @@ pub use self::fragment::*;
pub use self::grid::*;
pub use self::hide::*;
pub use self::list::*;
pub use self::measure::*;
pub use self::pad::*;
pub use self::page::*;
pub use self::par::*;

View File

@ -2,7 +2,7 @@ use std::ptr;
use std::str::FromStr;
use super::{AlignNode, ColumnsNode};
use crate::meta::{Counter, CounterAction, CounterKey, CounterNode, Numbering};
use crate::meta::{Counter, CounterKey, Numbering};
use crate::prelude::*;
/// Layouts its child onto one or multiple pages.
@ -214,8 +214,10 @@ pub struct PageNode {
/// footer: [
/// #set align(right)
/// #set text(8pt)
/// #counter(page).get("1") of
/// #counter(page).final("I")
/// #counter(page).display(
/// "1 of I",
/// both: true,
/// )
/// ]
/// )
///
@ -311,12 +313,13 @@ impl PageNode {
let header_ascent = self.header_ascent(styles);
let footer = self.footer(styles).or_else(|| {
self.numbering(styles).map(|numbering| {
CounterNode::new(
Counter::new(CounterKey::Page),
CounterAction::Both(numbering),
)
.pack()
.aligned(self.number_align(styles))
let both = match &numbering {
Numbering::Pattern(pattern) => pattern.pieces() >= 2,
Numbering::Func(_) => true,
};
Counter::new(CounterKey::Page)
.display(numbering, both)
.aligned(self.number_align(styles))
})
});
let footer_descent = self.footer_descent(styles);

View File

@ -74,6 +74,7 @@ fn global(math: Module, calc: Module) -> Module {
global.define("scale", layout::ScaleNode::id());
global.define("rotate", layout::RotateNode::id());
global.define("hide", layout::HideNode::id());
global.define("measure", layout::measure);
// Visualize.
global.define("image", visualize::ImageNode::id());
@ -92,6 +93,8 @@ fn global(math: Module, calc: Module) -> Module {
global.define("figure", meta::FigureNode::id());
global.define("cite", meta::CiteNode::id());
global.define("bibliography", meta::BibliographyNode::id());
global.define("locate", meta::locate);
global.define("style", meta::style);
global.define("counter", meta::counter);
global.define("numbering", meta::numbering);
global.define("state", meta::state);
@ -228,11 +231,11 @@ fn items() -> LangItems {
math::AccentNode::new(base, math::Accent::new(accent)).pack()
},
math_frac: |num, denom| math::FracNode::new(num, denom).pack(),
library_method: |dynamic, method, args, span| {
if let Some(counter) = dynamic.downcast().cloned() {
meta::counter_method(counter, method, args, span)
} else if let Some(state) = dynamic.downcast().cloned() {
meta::state_method(state, method, args, span)
library_method: |vm, dynamic, method, args, span| {
if let Some(counter) = dynamic.downcast::<meta::Counter>().cloned() {
counter.call_method(vm, method, args, span)
} else if let Some(state) = dynamic.downcast::<meta::State>().cloned() {
state.call_method(vm, method, args, span)
} else {
Err(format!("type {} has no method `{method}`", dynamic.type_name()))
.at(span)

View File

@ -39,9 +39,7 @@ use self::fragment::*;
use self::row::*;
use self::spacing::*;
use crate::layout::{HNode, ParNode, Spacing};
use crate::meta::{
Count, Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering,
};
use crate::meta::{Count, Counter, CounterUpdate, LocalName, Numbering};
use crate::prelude::*;
use crate::text::{
families, variant, FontFamily, FontList, LinebreakNode, SpaceNode, TextNode, TextSize,
@ -217,29 +215,29 @@ impl Layout for EquationNode {
if block {
if let Some(numbering) = self.numbering(styles) {
let pod = Regions::one(regions.base(), Axes::splat(false));
let counter = CounterNode::new(
Counter::of(Self::id()),
CounterAction::Get(numbering),
);
let counter = Counter::of(Self::id())
.display(numbering, false)
.layout(vt, styles, pod)?
.into_frame();
let sub = counter.pack().layout(vt, styles, pod)?.into_frame();
let width = if regions.size.x.is_finite() {
regions.size.x
} else {
frame.width() + 2.0 * (sub.width() + NUMBER_GUTTER.resolve(styles))
frame.width()
+ 2.0 * (counter.width() + NUMBER_GUTTER.resolve(styles))
};
let height = frame.height().max(sub.height());
let height = frame.height().max(counter.height());
frame.resize(Size::new(width, height), Align::CENTER_HORIZON);
let x = if TextNode::dir_in(styles).is_positive() {
frame.width() - sub.width()
frame.width() - counter.width()
} else {
Abs::zero()
};
let y = (frame.height() - sub.height()) / 2.0;
let y = (frame.height() - counter.height()) / 2.0;
frame.push_frame(Point::new(x, y), sub)
frame.push_frame(Point::new(x, y), counter)
}
} else {
let slack = ParNode::leading_in(styles) * 0.7;

View File

@ -0,0 +1,66 @@
use crate::prelude::*;
/// Provide access to the location of content.
///
/// Display: Locate
/// Category: meta
/// Returns: content
#[func]
pub fn locate(
/// The function to call with the location.
func: Func,
) -> Value {
LocateNode::new(func).pack().into()
}
/// Executes a `locate` call.
///
/// Display: Styled
/// Category: special
#[node(Locatable, Show)]
struct LocateNode {
/// The function to call with the location.
#[required]
func: Func,
}
impl Show for LocateNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
if !vt.introspector.init() {
return Ok(Content::empty());
}
let id = self.0.stable_id().unwrap();
Ok(self.func().call_vt(vt, [id.into()])?.display())
}
}
/// Provide access to active styles.
///
/// Display: Styled
/// Category: layout
/// Returns: content
#[func]
pub fn style(
/// The function to call with the styles.
func: Func,
) -> Value {
StyleNode::new(func).pack().into()
}
/// Executes a style access.
///
/// Display: Style
/// Category: special
#[node(Show)]
struct StyleNode {
/// The function to call with the styles.
#[required]
func: Func,
}
impl Show for StyleNode {
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
Ok(self.func().call_vt(vt, [styles.to_map().into()])?.display())
}
}

View File

@ -22,6 +22,192 @@ pub fn counter(
Value::dynamic(Counter::new(key))
}
/// Counts through pages, elements, and more.
#[derive(Clone, PartialEq, Hash)]
pub struct Counter(CounterKey);
impl Counter {
/// Create a new counter from a key.
pub fn new(key: CounterKey) -> Self {
Self(key)
}
/// The counter for the given node.
pub fn of(id: NodeId) -> Self {
Self::new(CounterKey::Selector(Selector::Node(id, None)))
}
/// Call a method on counter.
pub fn call_method(
self,
vm: &mut Vm,
method: &str,
mut args: Args,
span: Span,
) -> SourceResult<Value> {
let pattern = |s| NumberingPattern::from_str(s).unwrap().into();
let value = match method {
"display" => self
.display(
args.eat()?.unwrap_or_else(|| pattern("1.1")),
args.named("both")?.unwrap_or(false),
)
.into(),
"at" => self.at(&mut vm.vt, args.expect("location")?)?.into(),
"final" => self.final_(&mut vm.vt, args.expect("location")?)?.into(),
"update" => self.update(args.expect("value or function")?).into(),
"step" => self
.update(CounterUpdate::Step(
args.named("level")?.unwrap_or(NonZeroUsize::ONE),
))
.into(),
_ => bail!(span, "type counter has no method `{}`", method),
};
args.finish()?;
Ok(value)
}
/// Display the current value of the counter.
pub fn display(self, numbering: Numbering, both: bool) -> Content {
DisplayNode::new(self, numbering, both).pack()
}
/// Get the value of the state at the given location.
pub fn at(&self, vt: &mut Vt, id: StableId) -> SourceResult<CounterState> {
let sequence = self.sequence(vt)?;
let offset = vt.introspector.query_before(self.selector(), id).len();
let (mut state, page) = sequence[offset].clone();
if self.is_page() {
let delta = vt.introspector.page(id).get() - page.get();
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
}
/// Get the value of the state at the final location.
pub fn final_(&self, vt: &mut Vt, _: StableId) -> SourceResult<CounterState> {
let sequence = self.sequence(vt)?;
let (mut state, page) = sequence.last().unwrap().clone();
if self.is_page() {
let delta = vt.introspector.pages().get() - page.get();
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
}
/// Get the current and final value of the state combined in one state.
pub fn both(&self, vt: &mut Vt, id: StableId) -> SourceResult<CounterState> {
let sequence = self.sequence(vt)?;
let offset = vt.introspector.query_before(self.selector(), id).len();
let (mut at_state, at_page) = sequence[offset].clone();
let (mut final_state, final_page) = sequence.last().unwrap().clone();
if self.is_page() {
let at_delta = vt.introspector.page(id).get() - at_page.get();
at_state.step(NonZeroUsize::ONE, at_delta);
let final_delta = vt.introspector.pages().get() - final_page.get();
final_state.step(NonZeroUsize::ONE, final_delta);
}
Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
}
/// Produce content that performs a state update.
pub fn update(self, update: CounterUpdate) -> Content {
UpdateNode::new(self, update).pack()
}
/// Produce the whole sequence of counter states.
///
/// This has to happen just once for all counters, cutting down the number
/// of counter updates from quadratic to linear.
fn sequence(
&self,
vt: &mut Vt,
) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
self.sequence_impl(
vt.world,
TrackedMut::reborrow_mut(&mut vt.tracer),
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
)
}
/// Memoized implementation of `sequence`.
#[comemo::memoize]
fn sequence_impl(
&self,
world: Tracked<dyn World>,
tracer: TrackedMut<Tracer>,
provider: TrackedMut<StabilityProvider>,
introspector: Tracked<Introspector>,
) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
let mut vt = Vt { world, tracer, provider, introspector };
let mut state = CounterState(match &self.0 {
CounterKey::Selector(_) => smallvec![],
_ => smallvec![NonZeroUsize::ONE],
});
let mut page = NonZeroUsize::ONE;
let mut stops = eco_vec![(state.clone(), page)];
for node in introspector.query(self.selector()) {
if self.is_page() {
let id = node.stable_id().unwrap();
let prev = page;
page = introspector.page(id);
let delta = page.get() - prev.get();
if delta > 0 {
state.step(NonZeroUsize::ONE, delta);
}
}
if let Some(update) = match node.to::<UpdateNode>() {
Some(node) => Some(node.update()),
None => match node.with::<dyn Count>() {
Some(countable) => countable.update(),
None => Some(CounterUpdate::Step(NonZeroUsize::ONE)),
},
} {
state.update(&mut vt, update)?;
}
stops.push((state.clone(), page));
}
Ok(stops)
}
/// The selector relevant for this counter's updates.
fn selector(&self) -> Selector {
let mut selector = Selector::Node(
NodeId::of::<UpdateNode>(),
Some(dict! { "counter" => self.clone() }),
);
if let CounterKey::Selector(key) = &self.0 {
selector = Selector::Any(eco_vec![selector, key.clone()]);
}
selector
}
/// Whether this is the page counter.
fn is_page(&self) -> bool {
self.0 == CounterKey::Page
}
}
impl Debug for Counter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("counter(")?;
self.0.fmt(f)?;
f.write_char(')')
}
}
cast_from_value! {
Counter: "counter",
}
/// Identifies a counter.
#[derive(Clone, PartialEq, Hash)]
pub enum CounterKey {
@ -65,118 +251,8 @@ impl Debug for CounterKey {
}
}
/// Call a method on counter.
pub fn counter_method(
counter: Counter,
method: &str,
mut args: Args,
span: Span,
) -> SourceResult<Value> {
let pattern = |s| NumberingPattern::from_str(s).unwrap().into();
let action = match method {
"get" => CounterAction::Get(args.eat()?.unwrap_or_else(|| pattern("1.1"))),
"final" => CounterAction::Final(args.eat()?.unwrap_or_else(|| pattern("1.1"))),
"both" => CounterAction::Both(args.eat()?.unwrap_or_else(|| pattern("1/1"))),
"step" => CounterAction::Update(CounterUpdate::Step(
args.named("level")?.unwrap_or(NonZeroUsize::ONE),
)),
"update" => CounterAction::Update(args.expect("value or function")?),
_ => bail!(span, "type counter has no method `{}`", method),
};
args.finish()?;
let content = CounterNode::new(counter, action).pack();
Ok(Value::Content(content))
}
/// Executes an action on a counter.
///
/// Display: Counter
/// Category: special
#[node(Locatable, Show)]
pub struct CounterNode {
/// The counter key.
#[required]
pub counter: Counter,
/// The action.
#[required]
pub action: CounterAction,
}
impl Show for CounterNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
match self.action() {
CounterAction::Get(numbering) => {
self.counter().resolve(vt, self.0.stable_id(), &numbering)
}
CounterAction::Final(numbering) => {
self.counter().resolve(vt, None, &numbering)
}
CounterAction::Both(numbering) => {
let both = match &numbering {
Numbering::Pattern(pattern) => pattern.pieces() >= 2,
_ => false,
};
let counter = self.counter();
let id = self.0.stable_id();
if !both {
return counter.resolve(vt, id, &numbering);
}
let sequence = counter.sequence(
vt.world,
TrackedMut::reborrow_mut(&mut vt.tracer),
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
)?;
Ok(match (sequence.single(id), sequence.single(None)) {
(Some(current), Some(total)) => {
numbering.apply_vt(vt, &[current, total])?.display()
}
_ => Content::empty(),
})
}
CounterAction::Update(_) => Ok(Content::empty()),
}
}
}
/// The action to perform on a counter.
#[derive(Clone, PartialEq, Hash)]
pub enum CounterAction {
/// Displays the current value.
Get(Numbering),
/// Displays the final value.
Final(Numbering),
/// If given a pattern with at least two parts, displays the current value
/// together with the final value. Otherwise, displays just the current
/// value.
Both(Numbering),
/// Updates the value, possibly based on the previous one.
Update(CounterUpdate),
}
cast_from_value! {
CounterAction: "counter action",
}
impl Debug for CounterAction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Get(_) => f.pad("get(..)"),
Self::Final(_) => f.pad("final(..)"),
Self::Both(_) => f.pad("both(..)"),
Self::Update(_) => f.pad("update(..)"),
}
}
}
/// An update to perform on a counter.
#[derive(Debug, Clone, PartialEq, Hash)]
#[derive(Clone, PartialEq, Hash)]
pub enum CounterUpdate {
/// Set the counter to the specified state.
Set(CounterState),
@ -186,8 +262,14 @@ pub enum CounterUpdate {
Func(Func),
}
impl Debug for CounterUpdate {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("..")
}
}
cast_from_value! {
CounterUpdate,
CounterUpdate: "counter update",
v: CounterState => Self::Set(v),
v: Func => Self::Func(v),
}
@ -198,153 +280,6 @@ pub trait Count {
fn update(&self) -> Option<CounterUpdate>;
}
/// Counts through pages, elements, and more.
#[derive(Clone, PartialEq, Hash)]
pub struct Counter {
/// The key that identifies the counter.
pub key: CounterKey,
}
impl Counter {
/// Create a new counter from a key.
pub fn new(key: CounterKey) -> Self {
Self { key }
}
/// The counter for the given node.
pub fn of(id: NodeId) -> Self {
Self::new(CounterKey::Selector(Selector::Node(id, None)))
}
/// Display the value of the counter at the postition of the given stable
/// id.
pub fn resolve(
&self,
vt: &mut Vt,
stop: Option<StableId>,
numbering: &Numbering,
) -> SourceResult<Content> {
if !vt.introspector.init() {
return Ok(Content::empty());
}
let sequence = self.sequence(
vt.world,
TrackedMut::reborrow_mut(&mut vt.tracer),
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
)?;
Ok(match sequence.at(stop) {
Some(state) => numbering.apply_vt(vt, &state.0)?.display(),
None => Content::empty(),
})
}
/// Produce the whole sequence of counter states.
///
/// This has to happen just once for all counters, cutting down the number
/// of counter updates from quadratic to linear.
#[comemo::memoize]
fn sequence(
&self,
world: Tracked<dyn World>,
tracer: TrackedMut<Tracer>,
provider: TrackedMut<StabilityProvider>,
introspector: Tracked<Introspector>,
) -> SourceResult<CounterSequence> {
let mut vt = Vt { world, tracer, provider, introspector };
let mut search = Selector::Node(
NodeId::of::<CounterNode>(),
Some(dict! { "counter" => self.clone() }),
);
if let CounterKey::Selector(selector) = &self.key {
search = Selector::Any(eco_vec![search, selector.clone()]);
}
let mut stops = EcoVec::new();
let mut state = CounterState(match &self.key {
CounterKey::Selector(_) => smallvec![],
_ => smallvec![NonZeroUsize::ONE],
});
let is_page = self.key == CounterKey::Page;
let mut prev_page = NonZeroUsize::ONE;
for node in introspector.query(search) {
let id = node.stable_id().unwrap();
if is_page {
let page = introspector.page(id);
let delta = page.get() - prev_page.get();
if delta > 0 {
state.step(NonZeroUsize::ONE, delta);
}
prev_page = page;
}
if let Some(update) = match node.to::<CounterNode>() {
Some(counter) => match counter.action() {
CounterAction::Update(update) => Some(update),
_ => None,
},
None => match node.with::<dyn Count>() {
Some(countable) => countable.update(),
None => Some(CounterUpdate::Step(NonZeroUsize::ONE)),
},
} {
state.update(&mut vt, update)?;
}
stops.push((id, state.clone()));
}
Ok(CounterSequence { stops, is_page })
}
}
impl Debug for Counter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("counter(")?;
self.key.fmt(f)?;
f.write_char(')')
}
}
cast_from_value! {
Counter: "counter",
}
/// A sequence of counter values.
#[derive(Debug, Clone)]
struct CounterSequence {
stops: EcoVec<(StableId, CounterState)>,
is_page: bool,
}
impl CounterSequence {
fn at(&self, stop: Option<StableId>) -> Option<CounterState> {
let entry = match stop {
Some(stop) => self.stops.iter().find(|&&(id, _)| id == stop),
None => self.stops.last(),
};
if let Some((_, state)) = entry {
return Some(state.clone());
}
if self.is_page {
return Some(CounterState(smallvec![NonZeroUsize::ONE]));
}
None
}
fn single(&self, stop: Option<StableId>) -> Option<NonZeroUsize> {
Some(*self.at(stop)?.0.first()?)
}
}
/// Counts through elements with different levels.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct CounterState(pub SmallVec<[NonZeroUsize; 3]>);
@ -378,6 +313,16 @@ impl CounterState {
self.0.push(NonZeroUsize::ONE);
}
}
/// Get the first number of the state.
pub fn first(&self) -> NonZeroUsize {
self.0.first().copied().unwrap_or(NonZeroUsize::ONE)
}
/// Display the counter state with a numbering.
pub fn display(&self, vt: &mut Vt, numbering: &Numbering) -> SourceResult<Content> {
Ok(numbering.apply_vt(vt, &self.0)?.display())
}
}
cast_from_value! {
@ -388,3 +333,57 @@ cast_from_value! {
.map(Value::cast)
.collect::<StrResult<_>>()?),
}
cast_to_value! {
v: CounterState => Value::Array(v.0.into_iter().map(Into::into).collect())
}
/// Executes a display of a state.
///
/// Display: State
/// Category: special
#[node(Locatable, Show)]
struct DisplayNode {
/// The counter.
#[required]
counter: Counter,
/// The numbering to display the counter with.
#[required]
numbering: Numbering,
/// Whether to display both the current and final value.
#[required]
both: bool,
}
impl Show for DisplayNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
let id = self.0.stable_id().unwrap();
let counter = self.counter();
let numbering = self.numbering();
let state = if self.both() { counter.both(vt, id) } else { counter.at(vt, id) }?;
state.display(vt, &numbering)
}
}
/// Executes a display of a state.
///
/// Display: State
/// Category: special
#[node(Locatable, Show)]
struct UpdateNode {
/// The counter.
#[required]
counter: Counter,
/// The update to perform on the counter.
#[required]
update: CounterUpdate,
}
impl Show for UpdateNode {
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -1,9 +1,6 @@
use std::str::FromStr;
use super::{
Count, Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering,
NumberingPattern,
};
use super::{Count, Counter, CounterUpdate, LocalName, Numbering, NumberingPattern};
use crate::layout::{BlockNode, VNode};
use crate::prelude::*;
use crate::text::TextNode;
@ -59,12 +56,9 @@ impl Show for FigureNode {
if let Some(numbering) = self.numbering(styles) {
let name = self.local_name(TextNode::lang_in(styles));
caption = TextNode::packed(eco_format!("{name}\u{a0}"))
+ CounterNode::new(
Counter::of(Self::id()),
CounterAction::Get(numbering),
)
.pack()
.spanned(self.span())
+ Counter::of(Self::id())
.display(numbering, false)
.spanned(self.span())
+ TextNode::packed(": ")
+ caption;
}

View File

@ -1,6 +1,6 @@
use typst::font::FontWeight;
use super::{Counter, CounterAction, CounterNode, CounterUpdate, LocalName, Numbering};
use super::{Counter, CounterUpdate, LocalName, Numbering};
use crate::layout::{BlockNode, HNode, VNode};
use crate::meta::Count;
use crate::prelude::*;
@ -92,9 +92,7 @@ impl Show for HeadingNode {
let mut realized = self.body();
if let Some(numbering) = self.numbering(styles) {
realized =
CounterNode::new(Counter::of(Self::id()), CounterAction::Get(numbering))
.pack()
.spanned(self.span())
Counter::of(Self::id()).display(numbering, false).spanned(self.span())
+ HNode::new(Em::new(0.3).into()).with_weak(true).pack()
+ realized;
}

View File

@ -1,6 +1,7 @@
//! Interaction between document parts.
mod bibliography;
mod context;
mod counter;
mod document;
mod figure;
@ -13,6 +14,7 @@ mod reference;
mod state;
pub use self::bibliography::*;
pub use self::context::*;
pub use self::counter::*;
pub use self::document::*;
pub use self::figure::*;

View File

@ -120,11 +120,9 @@ impl Show for OutlineNode {
let mut hidden = Content::empty();
for ancestor in &ancestors {
if let Some(numbering) = ancestor.numbering(StyleChain::default()) {
let numbers = Counter::of(HeadingNode::id()).resolve(
vt,
ancestor.0.stable_id(),
&numbering,
)?;
let numbers = Counter::of(HeadingNode::id())
.at(vt, ancestor.0.stable_id().unwrap())?
.display(vt, &numbering)?;
hidden += numbers + SpaceNode::new().pack();
};
}
@ -138,11 +136,9 @@ impl Show for OutlineNode {
// Format the numbering.
let mut start = heading.body();
if let Some(numbering) = heading.numbering(StyleChain::default()) {
let numbers = Counter::of(HeadingNode::id()).resolve(
vt,
Some(stable_id),
&numbering,
)?;
let numbers = Counter::of(HeadingNode::id())
.at(vt, stable_id)?
.display(vt, &numbering)?;
start = numbers + SpaceNode::new().pack() + start;
};

View File

@ -9,10 +9,29 @@ use crate::prelude::*;
pub fn query(
/// The thing to search for.
target: Target,
/// A function to format the results with.
format: Func,
/// The location.
#[external]
location: StableId,
/// The location before which to query.
#[named]
#[external]
before: StableId,
/// The location after which to query.
#[named]
#[external]
after: StableId,
) -> Value {
QueryNode::new(target.0, format).pack().into()
let selector = target.0;
let introspector = vm.vt.introspector;
let elements = if let Some(id) = args.named("before")? {
introspector.query_before(selector, id)
} else if let Some(id) = args.named("after")? {
introspector.query_after(selector, id)
} else {
let _: StableId = args.expect("id")?;
introspector.query(selector)
};
elements.into()
}
/// A query target.
@ -33,31 +52,3 @@ cast_from_value! {
Self(Selector::Node(id, None))
}
}
/// Executes a query.
///
/// Display: Query
/// Category: special
#[node(Locatable, Show)]
struct QueryNode {
/// The thing to search for.
#[required]
target: Selector,
/// The function to format the results with.
#[required]
format: Func,
}
impl Show for QueryNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
if !vt.introspector.init() {
return Ok(Content::empty());
}
let id = self.0.stable_id().unwrap();
let target = self.target();
let (before, after) = vt.introspector.query_split(target, id);
Ok(self.format().call_vt(vt, [before.into(), after.into()])?.display())
}
}

View File

@ -114,8 +114,9 @@ impl Show for RefNode {
bail!(self.span(), "only numbered elements can be referenced");
};
let numbers =
Counter::of(node.id()).resolve(vt, node.stable_id(), &numbering.trimmed())?;
let numbers = Counter::of(node.id())
.at(vt, node.stable_id().unwrap())?
.display(vt, &numbering.trimmed())?;
Ok((supplement + numbers).linked(Link::Node(node.stable_id().unwrap())))
}

View File

@ -1,6 +1,6 @@
use std::fmt::{self, Debug, Formatter, Write};
use ecow::EcoVec;
use ecow::{eco_vec, EcoVec};
use typst::eval::Tracer;
use crate::prelude::*;
@ -21,91 +21,6 @@ pub fn state(
Value::dynamic(State { key, init })
}
/// Call a method on a state.
pub fn state_method(
state: State,
method: &str,
mut args: Args,
span: Span,
) -> SourceResult<Value> {
let action = match method {
"get" => StateAction::Get(args.eat()?),
"final" => StateAction::Final(args.eat()?),
"update" => StateAction::Update(args.expect("value or function")?),
_ => bail!(span, "type state has no method `{}`", method),
};
args.finish()?;
let content = StateNode::new(state, action).pack();
Ok(Value::Content(content))
}
/// Executes an action on a state.
///
/// Display: State
/// Category: special
#[node(Locatable, Show)]
pub struct StateNode {
/// The state.
#[required]
pub state: State,
/// The action.
#[required]
pub action: StateAction,
}
impl Show for StateNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
match self.action() {
StateAction::Get(func) => self.state().resolve(vt, self.0.stable_id(), func),
StateAction::Final(func) => self.state().resolve(vt, None, func),
StateAction::Update(_) => Ok(Content::empty()),
}
}
}
/// The action to perform on the state.
#[derive(Clone, PartialEq, Hash)]
pub enum StateAction {
/// Displays the current state.
Get(Option<Func>),
/// Displays the final state.
Final(Option<Func>),
/// Updates the state, possibly based on the previous one.
Update(StateUpdate),
}
cast_from_value! {
StateAction: "state action",
}
impl Debug for StateAction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Get(_) => f.pad("get(..)"),
Self::Final(_) => f.pad("final(..)"),
Self::Update(_) => f.pad("update(..)"),
}
}
}
/// An update to perform on a state.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum StateUpdate {
/// Set the state to the specified value.
Set(Value),
/// Apply the given function to the state.
Func(Func),
}
cast_from_value! {
StateUpdate,
v: Func => Self::Func(v),
v: Value => Self::Set(v),
}
/// A state.
#[derive(Clone, PartialEq, Hash)]
pub struct State {
@ -116,72 +31,92 @@ pub struct State {
}
impl State {
/// Display the state at the postition of the given stable id.
fn resolve(
&self,
vt: &mut Vt,
stop: Option<StableId>,
func: Option<Func>,
) -> SourceResult<Content> {
if !vt.introspector.init() {
return Ok(Content::empty());
}
/// Call a method on a state.
pub fn call_method(
self,
vm: &mut Vm,
method: &str,
mut args: Args,
span: Span,
) -> SourceResult<Value> {
let value = match method {
"display" => self.display(args.eat()?).into(),
"at" => self.at(&mut vm.vt, args.expect("location")?)?,
"final" => self.final_(&mut vm.vt, args.expect("location")?)?,
"update" => self.update(args.expect("value or function")?).into(),
_ => bail!(span, "type state has no method `{}`", method),
};
args.finish()?;
Ok(value)
}
let sequence = self.sequence(
vt.world,
TrackedMut::reborrow_mut(&mut vt.tracer),
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
)?;
/// Display the current value of the state.
pub fn display(self, func: Option<Func>) -> Content {
DisplayNode::new(self, func).pack()
}
Ok(match sequence.at(stop) {
Some(value) => {
if let Some(func) = func {
func.call_vt(vt, [value])?.display()
} else {
value.display()
}
}
None => Content::empty(),
})
/// Get the value of the state at the given location.
pub fn at(self, vt: &mut Vt, id: StableId) -> SourceResult<Value> {
let sequence = self.sequence(vt)?;
let offset = vt.introspector.query_before(self.selector(), id).len();
Ok(sequence[offset].clone())
}
/// Get the value of the state at the final location.
pub fn final_(self, vt: &mut Vt, _: StableId) -> SourceResult<Value> {
let sequence = self.sequence(vt)?;
Ok(sequence.last().unwrap().clone())
}
/// Produce content that performs a state update.
pub fn update(self, update: StateUpdate) -> Content {
UpdateNode::new(self, update).pack()
}
/// Produce the whole sequence of states.
///
/// This has to happen just once for all states, cutting down the number
/// of state updates from quadratic to linear.
fn sequence(&self, vt: &mut Vt) -> SourceResult<EcoVec<Value>> {
self.sequence_impl(
vt.world,
TrackedMut::reborrow_mut(&mut vt.tracer),
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
)
}
/// Memoized implementation of `sequence`.
#[comemo::memoize]
fn sequence(
fn sequence_impl(
&self,
world: Tracked<dyn World>,
tracer: TrackedMut<Tracer>,
provider: TrackedMut<StabilityProvider>,
introspector: Tracked<Introspector>,
) -> SourceResult<StateSequence> {
) -> SourceResult<EcoVec<Value>> {
let mut vt = Vt { world, tracer, provider, introspector };
let search = Selector::Node(
NodeId::of::<StateNode>(),
Some(dict! { "state" => self.clone() }),
);
let mut stops = EcoVec::new();
let mut state = self.init.clone();
let mut stops = eco_vec![state.clone()];
for node in introspector.query(search) {
let id = node.stable_id().unwrap();
let node = node.to::<StateNode>().unwrap();
if let StateAction::Update(update) = node.action() {
match update {
StateUpdate::Set(value) => state = value,
StateUpdate::Func(func) => state = func.call_vt(&mut vt, [state])?,
}
for node in introspector.query(self.selector()) {
let node = node.to::<UpdateNode>().unwrap();
match node.update() {
StateUpdate::Set(value) => state = value,
StateUpdate::Func(func) => state = func.call_vt(&mut vt, [state])?,
}
stops.push((id, state.clone()));
stops.push(state.clone());
}
Ok(StateSequence(stops))
Ok(stops)
}
/// The selector for this state's updates.
fn selector(&self) -> Selector {
Selector::Node(
NodeId::of::<UpdateNode>(),
Some(dict! { "state" => self.clone() }),
)
}
}
@ -199,17 +134,70 @@ cast_from_value! {
State: "state",
}
/// A sequence of state values.
#[derive(Debug, Clone)]
struct StateSequence(EcoVec<(StableId, Value)>);
/// An update to perform on a state.
#[derive(Clone, PartialEq, Hash)]
pub enum StateUpdate {
/// Set the state to the specified value.
Set(Value),
/// Apply the given function to the state.
Func(Func),
}
impl StateSequence {
fn at(&self, stop: Option<StableId>) -> Option<Value> {
let entry = match stop {
Some(stop) => self.0.iter().find(|&&(id, _)| id == stop),
None => self.0.last(),
};
entry.map(|(_, value)| value.clone())
impl Debug for StateUpdate {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("..")
}
}
cast_from_value! {
StateUpdate: "state update",
v: Func => Self::Func(v),
v: Value => Self::Set(v),
}
/// Executes a display of a state.
///
/// Display: State
/// Category: special
#[node(Locatable, Show)]
struct DisplayNode {
/// The state.
#[required]
state: State,
/// The function to display the state with.
#[required]
func: Option<Func>,
}
impl Show for DisplayNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
let id = self.0.stable_id().unwrap();
let value = self.state().at(vt, id)?;
Ok(match self.func() {
Some(func) => func.call_vt(vt, [value])?.display(),
None => value.display(),
})
}
}
/// Executes a display of a state.
///
/// Display: State
/// Category: special
#[node(Locatable, Show)]
struct UpdateNode {
/// The state.
#[required]
state: State,
/// The update to perform on the state.
#[required]
update: StateUpdate,
}
impl Show for UpdateNode {
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -6,7 +6,7 @@ use comemo::Tracked;
use ecow::EcoString;
use once_cell::sync::OnceCell;
use super::{Args, Dynamic, Module, Value};
use super::{Args, Dynamic, Module, Value, Vm};
use crate::diag::SourceResult;
use crate::doc::Document;
use crate::geom::{Abs, Dir};
@ -92,6 +92,7 @@ pub struct LangItems {
pub math_frac: fn(num: Content, denom: Content) -> Content,
/// Dispatch a method on a library value.
pub library_method: fn(
vm: &mut Vm,
dynamic: &Dynamic,
method: &str,
args: Args,

View File

@ -4,6 +4,7 @@ use ecow::EcoString;
use super::{Args, Str, Value, Vm};
use crate::diag::{At, SourceResult};
use crate::model::StableId;
use crate::syntax::Span;
/// Call a method on a value.
@ -73,8 +74,11 @@ pub fn call(
"func" => Value::Func(content.id().into()),
"has" => Value::Bool(content.has(&args.expect::<EcoString>("field")?)),
"at" => content.at(&args.expect::<EcoString>("field")?).at(span)?.clone(),
"page" => content.page(&vm.vt).at(span)?.into(),
"location" => content.location(&vm.vt).at(span)?.into(),
"id" => content
.stable_id()
.ok_or("this method can only be called on content returned by query()")
.at(span)?
.into(),
_ => return missing(),
},
@ -137,7 +141,15 @@ pub fn call(
},
Value::Dyn(dynamic) => {
return (vm.items.library_method)(&dynamic, method, args, span);
if let Some(&id) = dynamic.downcast::<StableId>() {
match method {
"page" => vm.vt.introspector.page(id).into(),
"location" => vm.vt.introspector.location(id).into(),
_ => return missing(),
}
} else {
return (vm.items.library_method)(vm, &dynamic, method, args, span);
}
}
_ => return missing(),
@ -251,13 +263,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
("starts-with", true),
("trim", true),
],
"content" => &[
("func", false),
("has", true),
("at", true),
("page", false),
("location", false),
],
"content" => &[("func", false), ("has", true), ("at", true), ("id", false)],
"array" => &[
("all", true),
("any", true),
@ -293,14 +299,15 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
],
"function" => &[("where", true), ("with", true)],
"arguments" => &[("named", false), ("pos", false)],
"stable id" => &[("page", false), ("location", false)],
"counter" => &[
("get", true),
("display", true),
("at", true),
("final", true),
("both", true),
("step", true),
("update", true),
],
"state" => &[("get", true), ("final", true), ("update", true)],
"state" => &[("display", true), ("at", true), ("final", true), ("update", true)],
_ => &[],
}
}

View File

@ -2,7 +2,6 @@ use std::any::TypeId;
use std::fmt::{self, Debug, Formatter, Write};
use std::hash::{Hash, Hasher};
use std::iter::{self, Sum};
use std::num::NonZeroUsize;
use std::ops::{Add, AddAssign, Deref};
use ecow::{eco_format, EcoString, EcoVec};
@ -10,10 +9,10 @@ use once_cell::sync::Lazy;
use super::{
node, Behave, Behaviour, Fold, Guard, Locatable, Recipe, StableId, Style, StyleMap,
Synthesize, Vt,
Synthesize,
};
use crate::diag::{SourceResult, StrResult};
use crate::doc::{Location, Meta};
use crate::doc::Meta;
use crate::eval::{
cast_from_value, cast_to_value, Args, Cast, Func, FuncInfo, Str, Value, Vm,
};
@ -186,22 +185,6 @@ impl Content {
self.field(field).ok_or_else(|| missing_field(field))
}
/// Determine the page of this content.
pub fn page(&self, vt: &Vt) -> StrResult<NonZeroUsize> {
match self.stable_id() {
Some(id) => Ok(vt.introspector.page(id)),
None => Err("this method can only be called on queried content".into()),
}
}
/// Determine the location of this content.
pub fn location(&self, vt: &Vt) -> StrResult<Location> {
match self.stable_id() {
Some(id) => Ok(vt.introspector.location(id)),
None => Err("this method can only be called on queried content".into()),
}
}
/// The content's label.
pub fn label(&self) -> Option<&Label> {
match self.field("label")? {

View File

@ -78,13 +78,7 @@ impl PartialEq for StyleMap {
impl Debug for StyleMap {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if let [style] = self.0.as_slice() {
return style.fmt(f);
}
let pieces: Vec<_> =
self.0.iter().map(|value| eco_format!("{value:?}")).collect();
f.write_str(&pretty_array_like(&pieces, false))
f.pad("..")
}
}

View File

@ -1,3 +1,4 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
use std::num::NonZeroUsize;
@ -6,7 +7,7 @@ use comemo::{Constraint, Track, Tracked, TrackedMut};
use super::{Content, Selector, StyleChain};
use crate::diag::SourceResult;
use crate::doc::{Document, Element, Frame, Location, Meta};
use crate::eval::Tracer;
use crate::eval::{cast_from_value, Tracer};
use crate::geom::{Point, Transform};
use crate::util::NonZeroExt;
use crate::World;
@ -116,7 +117,7 @@ impl StabilityProvider {
/// Stably identifies a call site across multiple layout passes.
///
/// This struct is created by [`StabilityProvider::identify`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct StableId(u128, usize, usize);
impl StableId {
@ -126,16 +127,27 @@ impl StableId {
}
}
impl Debug for StableId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("..")
}
}
cast_from_value! {
StableId: "stable id",
}
/// Provides access to information about the document.
pub struct Introspector {
init: bool,
pages: usize,
nodes: Vec<(Content, Location)>,
}
impl Introspector {
/// Create a new introspector.
pub fn new(frames: &[Frame]) -> Self {
let mut introspector = Self { init: false, nodes: vec![] };
let mut introspector = Self { init: false, pages: frames.len(), nodes: vec![] };
for (i, frame) in frames.iter().enumerate() {
let page = NonZeroUsize::new(1 + i).unwrap();
introspector.extract(frame, page, Transform::identity());
@ -180,26 +192,37 @@ impl Introspector {
self.init
}
/// Query for all metadata matches for the given selector.
/// Query for all nodes for the given selector.
pub fn query(&self, selector: Selector) -> Vec<Content> {
self.all().filter(|node| selector.matches(node)).cloned().collect()
}
/// Query for all metadata matches before the given id.
pub fn query_split(
&self,
selector: Selector,
id: StableId,
) -> (Vec<Content>, Vec<Content>) {
let mut iter = self.all();
let before = iter
.by_ref()
.take_while(|node| node.stable_id() != Some(id))
/// Query for all nodes up to the given id.
pub fn query_before(&self, selector: Selector, id: StableId) -> Vec<Content> {
let mut matches = vec![];
for node in self.all() {
if selector.matches(node) {
matches.push(node.clone());
}
if node.stable_id() == Some(id) {
break;
}
}
matches
}
/// Query for all nodes starting from the given id.
pub fn query_after(&self, selector: Selector, id: StableId) -> Vec<Content> {
self.all()
.skip_while(|node| node.stable_id() != Some(id))
.filter(|node| selector.matches(node))
.cloned()
.collect();
let after = iter.filter(|node| selector.matches(node)).cloned().collect();
(before, after)
.collect()
}
/// The total number pages.
pub fn pages(&self) -> NonZeroUsize {
NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE)
}
/// Find the page number for the given stable id.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -6,8 +6,8 @@
h(1fr)
text(0.8em)[_Chapter 1_]
},
footer: align(center)[\~ #counter(page).get() \~],
background: counter(page).get(n => if n <= 2 {
footer: align(center)[\~ #counter(page).display() \~],
background: counter(page).display(n => if n <= 2 {
place(center + horizon, circle(radius: 1cm, fill: luma(90%)))
})
)

View File

@ -4,21 +4,21 @@
// Count with string key.
#let mine = counter("mine!")
Final: #mine.final() \
Final: #locate(loc => mine.final(loc).at(0)) \
#mine.step()
First: #mine.get() \
First: #mine.display() \
#mine.update(7)
#mine.both("1 of 1") \
#mine.display("1 of 1", both: true) \
#mine.step()
#mine.step()
Second: #mine.get("I")
Second: #mine.display("I")
#mine.update(n => n * 2)
#mine.step()
---
// Count labels.
#let label = <heya>
#let count = counter(label).get()
#let count = counter(label).display()
#let elem(it) = [#box(it) #label]
#elem[hey, there!] #count \
@ -31,13 +31,19 @@ Second: #mine.get("I")
#counter(heading).step()
= Alpha
In #counter(heading).display().
== Beta
In #counter(heading).get().
#set heading(numbering: none)
= Gamma
#heading(numbering: "I.")[Delta]
At Beta, it was #locate(loc => {
let it = query(heading, loc).find(it => it.body == [Beta])
numbering(it.numbering, ..counter(heading).at(it.id()))
})
---
// Count figures.
#figure(numbering: "A", caption: [Four 'A's])[_AAAA!_]

View File

@ -7,7 +7,9 @@
header: {
smallcaps[Typst Academy]
h(1fr)
query(heading, (before, after) => {
locate(it => {
let after = query(heading, after: it)
let before = query(heading, before: it)
let elem = if before.len() != 0 {
before.last()
} else if after.len() != 0 {
@ -28,3 +30,41 @@
= Approach
#lorem(60)
---
#set page(
paper: "a7",
numbering: "1 / 1",
margin: (bottom: 1cm, rest: 0.5cm),
)
#set figure(numbering: "I")
#show figure: set image(width: 80%)
= List of Figures
#locate(it => {
let elements = query(figure, after: it)
for it in elements [
Figure
#numbering(it.numbering,
..counter(figure).at(it.id())):
#it.caption
#box(width: 1fr, repeat[.])
#counter(page).at(it.id()).first() \
]
})
#figure(
image("/glacier.jpg"),
caption: [Glacier melting],
)
#figure(
rect[Just some stand-in text],
caption: [Stand-in text],
)
#figure(
image("/tiger.jpg"),
caption: [Tiger world],
)

View File

@ -1,18 +1,33 @@
// Test state.
---
#let s = state("hey", "a")
#let double(it) = 2 * it
#s.update(double)
#s.update(double)
$ 2 + 3 $
#s.update(double)
Is: #s.display(),
Was: #locate(id => {
let it = query(math.equation, id).first()
s.at(it.id())
}).
---
#set page(width: 200pt)
#set text(8pt)
#let ls = state("lorem", lorem(1000).split("."))
#let loremum(count) = {
ls.get(list => list.slice(0, count).join(".").trim() + ".")
ls.display(list => list.slice(0, count).join(".").trim() + ".")
ls.update(list => list.slice(count))
}
#let fs = state("fader", red)
#let trait(title) = block[
#fs.get(color => text(fill: color)[
#fs.display(color => text(fill: color)[
*#title:* #loremum(1)
])
#fs.update(color => color.lighten(30%))