This commit is contained in:
Laurenz 2023-03-17 14:39:30 +01:00
parent 312197b276
commit c47e4cb496
16 changed files with 392 additions and 113 deletions

View File

@ -2,7 +2,7 @@ use std::ptr;
use std::str::FromStr;
use super::{AlignNode, ColumnsNode};
use crate::meta::{Counter, CounterAction, CounterNode, Numbering};
use crate::meta::{Counter, CounterAction, CounterKey, CounterNode, Numbering};
use crate::prelude::*;
/// Layouts its child onto one or multiple pages.
@ -311,9 +311,12 @@ impl PageNode {
let header_ascent = self.header_ascent(styles);
let footer = self.footer(styles).or_else(|| {
self.numbering(styles).map(|numbering| {
CounterNode::new(Counter::Page, CounterAction::Both(numbering))
.pack()
.aligned(self.number_align(styles))
CounterNode::new(
Counter::new(CounterKey::Page),
CounterAction::Both(numbering),
)
.pack()
.aligned(self.number_align(styles))
})
});
let footer_descent = self.footer_descent(styles);

View File

@ -10,6 +10,7 @@ pub mod symbols;
pub mod text;
pub mod visualize;
use typst::diag::At;
use typst::eval::{LangItems, Library, Module, Scope};
use typst::geom::{Align, Color, Dir, GenAlign, Smart};
use typst::model::{Node, NodeId, StyleMap};
@ -93,6 +94,7 @@ fn global(math: Module, calc: Module) -> Module {
global.define("bibliography", meta::BibliographyNode::id());
global.define("counter", meta::counter);
global.define("numbering", meta::numbering);
global.define("state", meta::state);
// Symbols.
global.define("sym", symbols::sym());
@ -225,6 +227,15 @@ fn items() -> LangItems {
math::AccentNode::new(base, math::Accent::new(accent)).pack()
},
math_frac: |num, denom| math::FracNode::new(num, denom).pack(),
counter_method: meta::counter_method,
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)
} else {
Err(format!("type {} has no method `{method}`", dynamic.type_name()))
.at(span)
}
},
}
}

View File

@ -3,7 +3,6 @@ use std::str::FromStr;
use ecow::{eco_vec, EcoVec};
use smallvec::{smallvec, SmallVec};
use typst::eval::Dynamic;
use super::{Numbering, NumberingPattern};
use crate::layout::PageNode;
@ -13,20 +12,65 @@ use crate::prelude::*;
///
/// Display: Counter
/// Category: meta
/// Returns: content
/// Returns: counter
#[func]
pub fn counter(key: Counter) -> Value {
Value::dynamic(key)
pub fn counter(
/// The key that identifies this counter.
key: CounterKey,
) -> Value {
Value::dynamic(Counter::new(key))
}
/// Identifies a counter.
#[derive(Clone, PartialEq, Hash)]
pub enum CounterKey {
/// The page counter.
Page,
/// Counts elements matching the given selectors. Only works for locatable
/// elements or labels.
Selector(Selector),
/// Counts through manual counters with the same key.
Str(Str),
}
cast_from_value! {
CounterKey,
v: Str => Self::Str(v),
label: Label => Self::Selector(Selector::Label(label)),
func: Func => {
let Some(id) = func.id() else {
return Err("this function is not selectable".into());
};
if id == NodeId::of::<PageNode>() {
return Ok(Self::Page);
}
if !Content::new(id).can::<dyn Locatable>() {
Err(eco_format!("cannot count through {}s", id.name))?;
}
Self::Selector(Selector::Node(id, None))
}
}
impl Debug for CounterKey {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Page => f.pad("page"),
Self::Selector(selector) => selector.fmt(f),
Self::Str(str) => str.fmt(f),
}
}
}
/// Call a method on counter.
pub fn counter_method(
dynamic: &Dynamic,
counter: Counter,
method: &str,
mut args: Args,
span: Span,
) -> SourceResult<Value> {
let counter = dynamic.downcast::<Counter>().unwrap();
let pattern = |s| NumberingPattern::from_str(s).unwrap().into();
let action = match method {
"get" => CounterAction::Get(args.eat()?.unwrap_or_else(|| pattern("1.1"))),
@ -41,7 +85,7 @@ pub fn counter_method(
args.finish()?;
let content = CounterNode::new(counter.clone(), action).pack();
let content = CounterNode::new(counter, action).pack();
Ok(Value::Content(content))
}
@ -53,7 +97,7 @@ pub fn counter_method(
pub struct CounterNode {
/// The counter key.
#[required]
pub key: Counter,
pub counter: Counter,
/// The action.
#[required]
@ -64,24 +108,30 @@ impl Show for CounterNode {
fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
match self.action() {
CounterAction::Get(numbering) => {
self.key().resolve(vt, self.0.stable_id(), &numbering)
self.counter().resolve(vt, self.0.stable_id(), &numbering)
}
CounterAction::Final(numbering) => {
self.counter().resolve(vt, None, &numbering)
}
CounterAction::Final(numbering) => self.key().resolve(vt, None, &numbering),
CounterAction::Both(numbering) => {
let both = match &numbering {
Numbering::Pattern(pattern) => pattern.pieces() >= 2,
_ => false,
};
let key = self.key();
let counter = self.counter();
let id = self.0.stable_id();
if !both {
return key.resolve(vt, id, &numbering);
return counter.resolve(vt, id, &numbering);
}
let sequence = key.sequence(vt.world, vt.introspector)?;
let numbers = [sequence.single(id), sequence.single(None)];
Ok(numbering.apply(vt.world, &numbers)?.display())
let sequence = counter.sequence(vt.world, vt.introspector)?;
Ok(match (sequence.single(id), sequence.single(None)) {
(Some(current), Some(total)) => {
numbering.apply(vt.world, &[current, total])?.display()
}
_ => Content::empty(),
})
}
CounterAction::Update(_) => Ok(Content::empty()),
}
@ -109,7 +159,12 @@ cast_from_value! {
impl Debug for CounterAction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("..")
match self {
Self::Get(_) => f.pad("get(..)"),
Self::Final(_) => f.pad("final(..)"),
Self::Both(_) => f.pad("both(..)"),
Self::Update(_) => f.pad("update(..)"),
}
}
}
@ -138,17 +193,22 @@ pub trait Count {
/// Counts through pages, elements, and more.
#[derive(Clone, PartialEq, Hash)]
pub enum Counter {
/// The page counter.
Page,
/// Counts elements matching the given selectors. Only works for locatable
/// elements or labels.
Selector(Selector),
/// Counts through manual counters with the same key.
Str(Str),
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(
@ -157,9 +217,15 @@ impl Counter {
stop: Option<StableId>,
numbering: &Numbering,
) -> SourceResult<Content> {
if !vt.introspector.init() {
return Ok(Content::empty());
}
let sequence = self.sequence(vt.world, vt.introspector)?;
let numbers = sequence.at(stop).0;
Ok(numbering.apply(vt.world, &numbers)?.display())
Ok(match sequence.at(stop) {
Some(state) => numbering.apply(vt.world, &state.0)?.display(),
None => Content::empty(),
})
}
/// Produce the whole sequence of counter states.
@ -174,21 +240,21 @@ impl Counter {
) -> SourceResult<CounterSequence> {
let mut search = Selector::Node(
NodeId::of::<CounterNode>(),
Some(dict! { "key" => self.clone() }),
Some(dict! { "counter" => self.clone() }),
);
if let Counter::Selector(selector) = self {
if let CounterKey::Selector(selector) = &self.key {
search = Selector::Any(eco_vec![search, selector.clone()]);
}
let mut state = CounterState::new();
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;
let is_page = *self == Self::Page;
if is_page {
state.0.push(prev_page);
}
for node in introspector.query(search) {
let id = node.stable_id().unwrap();
@ -221,40 +287,18 @@ impl Counter {
}
}
cast_from_value! {
Counter: "counter",
v: Str => Self::Str(v),
v: Selector => {
match v {
Selector::Node(id, _) => {
if id == NodeId::of::<PageNode>() {
return Ok(Self::Page);
}
if !Content::new_of(id).can::<dyn Locatable>() {
Err(eco_format!("cannot count through {}s", id.name))?;
}
}
Selector::Label(_) => {}
Selector::Regex(_) => Err("cannot count through text")?,
Selector::Any(_) => {}
}
Self::Selector(v)
}
}
impl Debug for Counter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("counter(")?;
match self {
Self::Page => f.pad("page")?,
Self::Selector(selector) => selector.fmt(f)?,
Self::Str(str) => str.fmt(f)?,
}
self.key.fmt(f)?;
f.write_char(')')
}
}
cast_from_value! {
Counter: "counter",
}
/// A sequence of counter values.
#[derive(Debug, Clone)]
struct CounterSequence {
@ -263,38 +307,33 @@ struct CounterSequence {
}
impl CounterSequence {
fn at(&self, stop: Option<StableId>) -> CounterState {
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 state.clone();
return Some(state.clone());
}
if self.is_page {
return CounterState(smallvec![NonZeroUsize::ONE]);
return Some(CounterState(smallvec![NonZeroUsize::ONE]));
}
CounterState::default()
None
}
fn single(&self, stop: Option<StableId>) -> NonZeroUsize {
self.at(stop).0.first().copied().unwrap_or(NonZeroUsize::ONE)
fn single(&self, stop: Option<StableId>) -> Option<NonZeroUsize> {
Some(*self.at(stop)?.0.first()?)
}
}
/// Counts through elements with different levels.
#[derive(Debug, Default, Clone, PartialEq, Hash)]
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct CounterState(pub SmallVec<[NonZeroUsize; 3]>);
impl CounterState {
/// Create a new levelled counter.
pub fn new() -> Self {
Self::default()
}
/// Advance the counter and return the numbers for the given heading.
pub fn update(
&mut self,

View File

@ -60,7 +60,7 @@ impl Show for FigureNode {
let name = self.local_name(TextNode::lang_in(styles));
caption = TextNode::packed(eco_format!("{name}\u{a0}"))
+ CounterNode::new(
Counter::Selector(Selector::node::<Self>()),
Counter::of(Self::id()),
CounterAction::Get(numbering),
)
.pack()

View File

@ -91,14 +91,12 @@ impl Show for HeadingNode {
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body();
if let Some(numbering) = self.numbering(styles) {
realized = CounterNode::new(
Counter::Selector(Selector::node::<Self>()),
CounterAction::Get(numbering),
)
.pack()
.spanned(self.span())
+ HNode::new(Em::new(0.3).into()).with_weak(true).pack()
+ realized;
realized =
CounterNode::new(Counter::of(Self::id()), CounterAction::Get(numbering))
.pack()
.spanned(self.span())
+ HNode::new(Em::new(0.3).into()).with_weak(true).pack()
+ realized;
}
Ok(BlockNode::new().with_body(Some(realized)).pack())
}

View File

@ -9,6 +9,7 @@ mod link;
mod numbering;
mod outline;
mod reference;
mod state;
pub use self::bibliography::*;
pub use self::counter::*;
@ -19,6 +20,7 @@ pub use self::link::*;
pub use self::numbering::*;
pub use self::outline::*;
pub use self::reference::*;
pub use self::state::*;
use typst::doc::Lang;

View File

@ -118,8 +118,11 @@ impl Show for OutlineNode {
let mut hidden = Content::empty();
for ancestor in &ancestors {
if let Some(numbering) = ancestor.numbering(StyleChain::default()) {
let numbers = Counter::Selector(Selector::node::<HeadingNode>())
.resolve(vt, ancestor.0.stable_id(), &numbering)?;
let numbers = Counter::of(HeadingNode::id()).resolve(
vt,
ancestor.0.stable_id(),
&numbering,
)?;
hidden += numbers + SpaceNode::new().pack();
};
}
@ -133,8 +136,11 @@ impl Show for OutlineNode {
// Format the numbering.
let mut start = heading.body();
if let Some(numbering) = heading.numbering(StyleChain::default()) {
let numbers = Counter::Selector(Selector::node::<HeadingNode>())
.resolve(vt, Some(stable_id), &numbering)?;
let numbers = Counter::of(HeadingNode::id()).resolve(
vt,
Some(stable_id),
&numbering,
)?;
start = numbers + SpaceNode::new().pack() + start;
};

View File

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

209
library/src/meta/state.rs Normal file
View File

@ -0,0 +1,209 @@
use std::fmt::{self, Debug, Formatter, Write};
use ecow::EcoVec;
use crate::prelude::*;
/// Handle stateful tasks.
///
/// Display: State
/// Category: meta
/// Returns: state
#[func]
pub fn state(
/// The key that identifies this state.
key: Str,
/// The initial value of the state.
#[default]
init: Value,
) -> Value {
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 {
/// The key that identifies the state.
key: Str,
/// The initial value of the state.
init: Value,
}
impl State {
/// Display the state at the postition of the given stable id.
fn resolve(
&self,
vt: &Vt,
stop: Option<StableId>,
func: Option<Func>,
) -> SourceResult<Content> {
if !vt.introspector.init() {
return Ok(Content::empty());
}
let sequence = self.sequence(vt.world, vt.introspector)?;
Ok(match sequence.at(stop) {
Some(value) => {
if let Some(func) = func {
let args = Args::new(func.span(), [value]);
func.call_detached(vt.world, args)?.display()
} else {
value.display()
}
}
None => Content::empty(),
})
}
/// 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.
#[comemo::memoize]
fn sequence(
&self,
world: Tracked<dyn World>,
introspector: Tracked<Introspector>,
) -> SourceResult<StateSequence> {
let search = Selector::Node(
NodeId::of::<StateNode>(),
Some(dict! { "state" => self.clone() }),
);
let mut stops = EcoVec::new();
let mut state = self.init.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) => {
let args = Args::new(func.span(), [state]);
state = func.call_detached(world, args)?;
}
}
}
stops.push((id, state.clone()));
}
Ok(StateSequence(stops))
}
}
impl Debug for State {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("state(")?;
self.key.fmt(f)?;
f.write_str(", ")?;
self.init.fmt(f)?;
f.write_char(')')
}
}
cast_from_value! {
State: "state",
}
/// A sequence of state values.
#[derive(Debug, Clone)]
struct StateSequence(EcoVec<(StableId, Value)>);
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())
}
}

View File

@ -234,7 +234,7 @@ fn create_new_func(node: &Node) -> TokenStream {
quote! {
/// Create a new node.
pub fn new(#(#params),*) -> Self {
Self(::typst::model::Content::new::<Self>())
Self(::typst::model::Content::new(<Self as ::typst::model::Node>::id()))
#(#builder_calls)*
}
}
@ -388,7 +388,7 @@ fn create_vtable_func(node: &Node) -> TokenStream {
quote! {
|id| {
let null = Self(::typst::model::Content::new::<#ident>());
let null = Self(::typst::model::Content::new(<#ident as ::typst::model::Node>::id()));
#(#checks)*
None
}
@ -456,7 +456,7 @@ fn create_construct_impl(node: &Node) -> TokenStream {
vm: &::typst::eval::Vm,
args: &mut ::typst::eval::Args,
) -> ::typst::diag::SourceResult<::typst::model::Content> {
let mut node = Self(::typst::model::Content::new::<Self>());
let mut node = Self(::typst::model::Content::new(<Self as ::typst::model::Node>::id()));
#(#handlers)*
Ok(node.0)
}

View File

@ -90,9 +90,8 @@ pub struct LangItems {
pub math_accent: fn(base: Content, accent: char) -> Content,
/// A fraction in a formula: `x/2`.
pub math_frac: fn(num: Content, denom: Content) -> Content,
/// Dispatch a method on a counter. This is hacky and should be superseded
/// by more dynamic method dispatch.
pub counter_method: fn(
/// Dispatch a method on a library value.
pub library_method: fn(
dynamic: &Dynamic,
method: &str,
args: Args,

View File

@ -135,11 +135,7 @@ pub fn call(
},
Value::Dyn(dynamic) => {
if dynamic.type_name() == "counter" {
return (vm.items.counter_method)(&dynamic, method, args, span);
}
return missing();
return (vm.items.library_method)(&dynamic, method, args, span);
}
_ => return missing(),
@ -296,6 +292,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
("step", true),
("update", true),
],
"state" => &[("get", true), ("final", true), ("update", true)],
_ => &[],
}
}

View File

@ -40,12 +40,7 @@ enum Modifier {
impl Content {
/// Create a content of the given node kind.
pub fn new<T: Node>() -> Self {
Self::new_of(T::id())
}
/// Create a content of the given node kind.
pub fn new_of(id: NodeId) -> Self {
pub fn new(id: NodeId) -> Self {
Self {
id,
span: Span::detached(),

BIN
tests/ref/meta/state.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -6,7 +6,6 @@
Final: #mine.final() \
#mine.step()
#mine.step()
First: #mine.get() \
#mine.update(7)
#mine.both("1 of 1") \

24
tests/typ/meta/state.typ Normal file
View File

@ -0,0 +1,24 @@
// Test state.
---
#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.update(list => list.slice(count))
}
#let fs = state("fader", red)
#let trait(title) = block[
#fs.get(color => text(fill: color)[
*#title:* #loremum(1)
])
#fs.update(color => color.lighten(30%))
]
#trait[Boldness]
#trait[Adventure]
#trait[Fear]
#trait[Anger]