diff options
Diffstat (limited to 'crates')
61 files changed, 1543 insertions, 710 deletions
diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs index 315a2ed5..0a69523e 100644 --- a/crates/typst-docs/src/lib.rs +++ b/crates/typst-docs/src/lib.rs @@ -165,6 +165,11 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { &format!("{}reference/", resolver.base()), "reference/scripting.md", ), + markdown_page( + resolver, + &format!("{}reference/", resolver.base()), + "reference/context.md", + ), category_page(resolver, FOUNDATIONS).with_part("Library"), category_page(resolver, MODEL), category_page(resolver, TEXT), @@ -400,6 +405,7 @@ fn func_model( keywords: func.keywords(), oneliner: oneliner(details), element: func.element().is_some(), + contextual: func.contextual().unwrap_or(false), details: Html::markdown(resolver, details, nesting), example: example.map(|md| Html::markdown(resolver, md, None)), self_, diff --git a/crates/typst-docs/src/link.rs b/crates/typst-docs/src/link.rs index 20a4f6e4..f4d803c3 100644 --- a/crates/typst-docs/src/link.rs +++ b/crates/typst-docs/src/link.rs @@ -43,6 +43,7 @@ fn resolve_known(head: &str, base: &str) -> Option<String> { "$syntax" => format!("{base}reference/syntax"), "$styling" => format!("{base}reference/styling"), "$scripting" => format!("{base}reference/scripting"), + "$context" => format!("{base}reference/context"), "$guides" => format!("{base}guides"), "$packages" => format!("{base}packages"), "$changelog" => format!("{base}changelog"), diff --git a/crates/typst-docs/src/model.rs b/crates/typst-docs/src/model.rs index 93742825..1564ef2f 100644 --- a/crates/typst-docs/src/model.rs +++ b/crates/typst-docs/src/model.rs @@ -87,6 +87,7 @@ pub struct FuncModel { pub keywords: &'static [&'static str], pub oneliner: &'static str, pub element: bool, + pub contextual: bool, pub details: Html, /// This example is only for nested function models. Others can have /// their example directly in their details. diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index e78f0c12..1fef1b49 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -2,32 +2,36 @@ use comemo::Track; use ecow::{eco_vec, EcoString, EcoVec}; use typst::engine::{Engine, Route}; use typst::eval::{Tracer, Vm}; -use typst::foundations::{Label, Scopes, Value}; +use typst::foundations::{Context, Label, Scopes, Styles, Value}; use typst::introspection::{Introspector, Locator}; use typst::model::{BibliographyElem, Document}; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use typst::World; /// Try to determine a set of possible values for an expression. -pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<Value> { - match node.cast::<ast::Expr>() { - Some(ast::Expr::None(_)) => eco_vec![Value::None], - Some(ast::Expr::Auto(_)) => eco_vec![Value::Auto], - Some(ast::Expr::Bool(v)) => eco_vec![Value::Bool(v.get())], - Some(ast::Expr::Int(v)) => eco_vec![Value::Int(v.get())], - Some(ast::Expr::Float(v)) => eco_vec![Value::Float(v.get())], - Some(ast::Expr::Numeric(v)) => eco_vec![Value::numeric(v.get())], - Some(ast::Expr::Str(v)) => eco_vec![Value::Str(v.get().into())], +pub fn analyze_expr( + world: &dyn World, + node: &LinkedNode, +) -> EcoVec<(Value, Option<Styles>)> { + let Some(expr) = node.cast::<ast::Expr>() else { + return eco_vec![]; + }; - Some(ast::Expr::FieldAccess(access)) => { - let Some(child) = node.children().next() else { return eco_vec![] }; - analyze_expr(world, &child) - .into_iter() - .filter_map(|target| target.field(&access.field()).ok()) - .collect() - } + let val = match expr { + ast::Expr::None(_) => Value::None, + ast::Expr::Auto(_) => Value::Auto, + ast::Expr::Bool(v) => Value::Bool(v.get()), + ast::Expr::Int(v) => Value::Int(v.get()), + ast::Expr::Float(v) => Value::Float(v.get()), + ast::Expr::Numeric(v) => Value::numeric(v.get()), + ast::Expr::Str(v) => Value::Str(v.get().into()), + _ => { + if node.kind() == SyntaxKind::Contextual { + if let Some(child) = node.children().last() { + return analyze_expr(world, &child); + } + } - Some(_) => { if let Some(parent) = node.parent() { if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { return analyze_expr(world, parent); @@ -37,16 +41,16 @@ pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<Value> { let mut tracer = Tracer::new(); tracer.inspect(node.span()); typst::compile(world, &mut tracer).ok(); - tracer.values() + return tracer.values(); } + }; - _ => eco_vec![], - } + eco_vec![(val, None)] } /// Try to load a module from the current source file. pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> { - let source = analyze_expr(world, source).into_iter().next()?; + let (source, _) = analyze_expr(world, source).into_iter().next()?; if source.scope().is_some() { return Some(source); } @@ -62,7 +66,9 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> { tracer: tracer.track_mut(), }; - let mut vm = Vm::new(engine, Scopes::new(Some(world.library())), Span::detached()); + let context = Context::none(); + let mut vm = + Vm::new(engine, &context, Scopes::new(Some(world.library())), Span::detached()); typst::eval::import(&mut vm, source, Span::detached(), true) .ok() .map(Value::Module) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index b58a9bcc..94679552 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -6,7 +6,7 @@ use if_chain::if_chain; use serde::{Deserialize, Serialize}; use typst::foundations::{ fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, - NoneValue, Repr, Scope, Type, Value, + NoneValue, Repr, Scope, StyleChain, Styles, Type, Value, }; use typst::model::Document; use typst::syntax::{ @@ -135,6 +135,17 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { } } + // Behind a half-completed context block: "#context |". + if_chain! { + if let Some(prev) = ctx.leaf.prev_leaf(); + if prev.kind() == SyntaxKind::Context; + then { + ctx.from = ctx.cursor; + code_completions(ctx, false); + return true; + } + } + // Directly after a raw block. let mut s = Scanner::new(ctx.text); s.jump(ctx.leaf.offset()); @@ -333,10 +344,10 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { if prev.is::<ast::Expr>(); if prev.parent_kind() != Some(SyntaxKind::Markup) || prev.prev_sibling_kind() == Some(SyntaxKind::Hash); - if let Some(value) = analyze_expr(ctx.world, &prev).into_iter().next(); + if let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next(); then { ctx.from = ctx.cursor; - field_access_completions(ctx, &value); + field_access_completions(ctx, &value, &styles); return true; } } @@ -348,10 +359,10 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { if prev.kind() == SyntaxKind::Dot; if let Some(prev_prev) = prev.prev_sibling(); if prev_prev.is::<ast::Expr>(); - if let Some(value) = analyze_expr(ctx.world, &prev_prev).into_iter().next(); + if let Some((value, styles)) = analyze_expr(ctx.world, &prev_prev).into_iter().next(); then { ctx.from = ctx.leaf.offset(); - field_access_completions(ctx, &value); + field_access_completions(ctx, &value, &styles); return true; } } @@ -360,7 +371,11 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { } /// Add completions for all fields on a value. -fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { +fn field_access_completions( + ctx: &mut CompletionContext, + value: &Value, + styles: &Option<Styles>, +) { for (name, value) in value.ty().scope().iter() { ctx.value_completion(Some(name.clone()), value, true, None); } @@ -421,6 +436,23 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { ctx.value_completion(Some(name.clone().into()), value, false, None); } } + Value::Func(func) => { + // Autocomplete get rules. + if let Some((elem, styles)) = func.element().zip(styles.as_ref()) { + for param in elem.params().iter().filter(|param| !param.required) { + if let Some(value) = elem.field_id(param.name).and_then(|id| { + elem.field_from_styles(id, StyleChain::new(styles)) + }) { + ctx.value_completion( + Some(param.name.into()), + &value, + false, + None, + ); + } + } + } + } Value::Plugin(plugin) => { for name in plugin.iter() { ctx.completions.push(Completion { @@ -863,6 +895,12 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) { ); ctx.snippet_completion( + "context expression", + "context ${}", + "Provides contextual data.", + ); + + ctx.snippet_completion( "let binding", "let ${name} = ${value}", "Saves a value in a variable.", diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 67614b9e..2f04be87 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use typst::eval::{CapturesVisitor, Tracer}; -use typst::foundations::{repr, CastInfo, Repr, Value}; +use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; use typst::layout::Length; use typst::model::Document; use typst::syntax::{ast, LinkedNode, Source, SyntaxKind}; @@ -59,7 +59,7 @@ fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> { let values = analyze_expr(world, ancestor); - if let [value] = values.as_slice() { + if let [(value, _)] = values.as_slice() { if let Some(docs) = value.docs() { return Some(Tooltip::Text(plain_docs_sentence(docs))); } @@ -78,7 +78,7 @@ fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> { let mut last = None; let mut pieces: Vec<EcoString> = vec![]; let mut iter = values.iter(); - for value in (&mut iter).take(Tracer::MAX_VALUES - 1) { + for (value, _) in (&mut iter).take(Tracer::MAX_VALUES - 1) { if let Some((prev, count)) = &mut last { if *prev == value { *count += 1; @@ -120,7 +120,7 @@ fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> { } // Analyze the closure's captures. - let mut visitor = CapturesVisitor::new(None); + let mut visitor = CapturesVisitor::new(None, Capturer::Function); visitor.visit(parent); let captures = visitor.finish(); diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index 81548dd7..f14d3350 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -617,6 +617,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { vtable: <#ident as #foundations::Capable>::vtable, field_id: |name| name.parse().ok().map(|id: Fields| id as u8), field_name: |id| id.try_into().ok().map(Fields::to_str), + field_from_styles: <#ident as #foundations::Fields>::field_from_styles, local_name: #local_name, scope: #foundations::Lazy::new(|| #scope), params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]) @@ -866,6 +867,20 @@ fn create_fields_impl(element: &Elem) -> TokenStream { quote! { Fields::#enum_ident => #expr } }); + // Fields that can be accessed using the `field_from_styles` method. + let field_from_styles_arms = element.visible_fields().map(|field| { + let Field { enum_ident, .. } = field; + + let expr = if field.required || field.synthesized { + quote! { None } + } else { + let value = create_style_chain_access(field, false, quote!(None)); + quote! { Some(#into_value(#value)) } + }; + + quote! { Fields::#enum_ident => #expr } + }); + // Sets fields from the style chain. let materializes = visible_non_ghost() .filter(|field| !field.required && !field.synthesized) @@ -939,6 +954,14 @@ fn create_fields_impl(element: &Elem) -> TokenStream { } } + fn field_from_styles(id: u8, styles: #foundations::StyleChain) -> Option<#foundations::Value> { + let id = Fields::try_from(id).ok()?; + match id { + #(#field_from_styles_arms,)* + _ => None, + } + } + fn materialize(&mut self, styles: #foundations::StyleChain) { #(#materializes)* } diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs index 953df428..728ab05b 100644 --- a/crates/typst-macros/src/func.rs +++ b/crates/typst-macros/src/func.rs @@ -24,6 +24,7 @@ struct Func { constructor: bool, keywords: Vec<String>, parent: Option<syn::Type>, + contextual: bool, docs: String, vis: syn::Visibility, ident: Ident, @@ -37,6 +38,7 @@ struct Func { struct SpecialParams { self_: Option<Param>, engine: bool, + context: bool, args: bool, span: bool, } @@ -67,6 +69,7 @@ enum Binding { /// The `..` in `#[func(..)]`. pub struct Meta { pub scope: bool, + pub contextual: bool, pub name: Option<String>, pub title: Option<String>, pub constructor: bool, @@ -78,6 +81,7 @@ impl Parse for Meta { fn parse(input: ParseStream) -> Result<Self> { Ok(Self { scope: parse_flag::<kw::scope>(input)?, + contextual: parse_flag::<kw::contextual>(input)?, name: parse_string::<kw::name>(input)?, title: parse_string::<kw::title>(input)?, constructor: parse_flag::<kw::constructor>(input)?, @@ -117,6 +121,7 @@ fn parse(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> { constructor: meta.constructor, keywords: meta.keywords, parent: meta.parent, + contextual: meta.contextual, docs, vis: item.vis.clone(), ident: item.sig.ident.clone(), @@ -171,6 +176,7 @@ fn parse_param( match ident.to_string().as_str() { "engine" => special.engine = true, + "context" => special.context = true, "args" => special.args = true, "span" => special.span = true, _ => { @@ -247,6 +253,7 @@ fn create_func_data(func: &Func) -> TokenStream { scope, parent, constructor, + contextual, .. } = func; @@ -272,6 +279,7 @@ fn create_func_data(func: &Func) -> TokenStream { title: #title, docs: #docs, keywords: &[#(#keywords),*], + contextual: #contextual, scope: #foundations::Lazy::new(|| #scope), params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]), returns: #foundations::Lazy::new(|| <#returns as #foundations::Reflect>::output()), @@ -320,12 +328,13 @@ fn create_wrapper_closure(func: &Func) -> TokenStream { .as_ref() .map(bind) .map(|tokens| quote! { #tokens, }); - let vt_ = func.special.engine.then(|| quote! { engine, }); + let engine_ = func.special.engine.then(|| quote! { engine, }); + let context_ = func.special.context.then(|| quote! { context, }); let args_ = func.special.args.then(|| quote! { args, }); let span_ = func.special.span.then(|| quote! { args.span, }); let forwarded = func.params.iter().filter(|param| !param.external).map(bind); quote! { - __typst_func(#self_ #vt_ #args_ #span_ #(#forwarded,)*) + __typst_func(#self_ #engine_ #context_ #args_ #span_ #(#forwarded,)*) } }; @@ -333,7 +342,7 @@ fn create_wrapper_closure(func: &Func) -> TokenStream { let ident = &func.ident; let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); quote! { - |engine, args| { + |engine, context, args| { let __typst_func = #parent #ident; #handlers #finish diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs index 5d340473..f92230ef 100644 --- a/crates/typst-macros/src/lib.rs +++ b/crates/typst-macros/src/lib.rs @@ -40,6 +40,8 @@ use syn::DeriveInput; /// You can customize some properties of the resulting function: /// - `scope`: Indicates that the function has an associated scope defined by /// the `#[scope]` macro. +/// - `contextual`: Indicates that the function makes use of context. This has +/// no effect on the behaviour itself, but is used for the docs. /// - `name`: The functions's normal name (e.g. `min`). Defaults to the Rust /// name in kebab-case. /// - `title`: The functions's title case name (e.g. `Minimum`). Defaults to the diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs index 89880db1..bfe22285 100644 --- a/crates/typst-macros/src/util.rs +++ b/crates/typst-macros/src/util.rs @@ -255,6 +255,7 @@ pub mod kw { syn::custom_keyword!(span); syn::custom_keyword!(title); syn::custom_keyword!(scope); + syn::custom_keyword!(contextual); syn::custom_keyword!(cast); syn::custom_keyword!(constructor); syn::custom_keyword!(keywords); diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 6dd9b5f6..df9cef0c 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -187,6 +187,8 @@ pub enum Expr<'a> { Set(SetRule<'a>), /// A show rule: `show heading: it => emph(it.body)`. Show(ShowRule<'a>), + /// A contextual expression: `context text.lang`. + Contextual(Contextual<'a>), /// An if-else conditional: `if x { y } else { z }`. Conditional(Conditional<'a>), /// A while loop: `while x { y }`. @@ -264,6 +266,7 @@ impl<'a> AstNode<'a> for Expr<'a> { SyntaxKind::DestructAssignment => node.cast().map(Self::DestructAssign), SyntaxKind::SetRule => node.cast().map(Self::Set), SyntaxKind::ShowRule => node.cast().map(Self::Show), + SyntaxKind::Contextual => node.cast().map(Self::Contextual), SyntaxKind::Conditional => node.cast().map(Self::Conditional), SyntaxKind::WhileLoop => node.cast().map(Self::While), SyntaxKind::ForLoop => node.cast().map(Self::For), @@ -326,6 +329,7 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::DestructAssign(v) => v.to_untyped(), Self::Set(v) => v.to_untyped(), Self::Show(v) => v.to_untyped(), + Self::Contextual(v) => v.to_untyped(), Self::Conditional(v) => v.to_untyped(), Self::While(v) => v.to_untyped(), Self::For(v) => v.to_untyped(), @@ -361,6 +365,7 @@ impl Expr<'_> { | Self::Let(_) | Self::Set(_) | Self::Show(_) + | Self::Contextual(_) | Self::Conditional(_) | Self::While(_) | Self::For(_) @@ -1947,6 +1952,18 @@ impl<'a> ShowRule<'a> { } node! { + /// A contextual expression: `context text.lang`. + Contextual +} + +impl<'a> Contextual<'a> { + /// The expression which depends on the context. + pub fn body(self) -> Expr<'a> { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { /// An if-else conditional: `if x { y } else { z }`. Conditional } diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs index 99fbf4fe..19d35d0a 100644 --- a/crates/typst-syntax/src/highlight.rs +++ b/crates/typst-syntax/src/highlight.rs @@ -230,6 +230,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> { SyntaxKind::Let => Some(Tag::Keyword), SyntaxKind::Set => Some(Tag::Keyword), SyntaxKind::Show => Some(Tag::Keyword), + SyntaxKind::Context => Some(Tag::Keyword), SyntaxKind::If => Some(Tag::Keyword), SyntaxKind::Else => Some(Tag::Keyword), SyntaxKind::For => Some(Tag::Keyword), @@ -267,6 +268,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> { SyntaxKind::LetBinding => None, SyntaxKind::SetRule => None, SyntaxKind::ShowRule => None, + SyntaxKind::Contextual => None, SyntaxKind::Conditional => None, SyntaxKind::WhileLoop => None, SyntaxKind::ForLoop => None, diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs index 536c9381..e5dd4e9b 100644 --- a/crates/typst-syntax/src/kind.rs +++ b/crates/typst-syntax/src/kind.rs @@ -159,6 +159,8 @@ pub enum SyntaxKind { Set, /// The `show` keyword. Show, + /// The `context` keyword. + Context, /// The `if` keyword. If, /// The `else` keyword. @@ -232,6 +234,8 @@ pub enum SyntaxKind { SetRule, /// A show rule: `show heading: it => emph(it.body)`. ShowRule, + /// A contextual expression: `context text.lang`. + Contextual, /// An if-else conditional: `if x { y } else { z }`. Conditional, /// A while loop: `while x { y }`. @@ -322,6 +326,7 @@ impl SyntaxKind { | Self::Let | Self::Set | Self::Show + | Self::Context | Self::If | Self::Else | Self::For @@ -426,6 +431,7 @@ impl SyntaxKind { Self::Let => "keyword `let`", Self::Set => "keyword `set`", Self::Show => "keyword `show`", + Self::Context => "keyword `context`", Self::If => "keyword `if`", Self::Else => "keyword `else`", Self::For => "keyword `for`", @@ -462,6 +468,7 @@ impl SyntaxKind { Self::LetBinding => "`let` expression", Self::SetRule => "`set` expression", Self::ShowRule => "`show` expression", + Self::Contextual => "`context` expression", Self::Conditional => "`if` expression", Self::WhileLoop => "while-loop expression", Self::ForLoop => "for-loop expression", diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index f0cf8846..cd1998a6 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -616,6 +616,7 @@ fn keyword(ident: &str) -> Option<SyntaxKind> { "let" => SyntaxKind::Let, "set" => SyntaxKind::Set, "show" => SyntaxKind::Show, + "context" => SyntaxKind::Context, "if" => SyntaxKind::If, "else" => SyntaxKind::Else, "for" => SyntaxKind::For, diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 32e15cb7..567bcbd1 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -750,6 +750,7 @@ fn code_primary(p: &mut Parser, atomic: bool) { SyntaxKind::Let => let_binding(p), SyntaxKind::Set => set_rule(p), SyntaxKind::Show => show_rule(p), + SyntaxKind::Context => contextual(p, atomic), SyntaxKind::If => conditional(p), SyntaxKind::While => while_loop(p), SyntaxKind::For => for_loop(p), @@ -889,6 +890,14 @@ fn show_rule(p: &mut Parser) { p.wrap(m, SyntaxKind::ShowRule); } +/// Parses a contextual expression: `context text.lang`. +fn contextual(p: &mut Parser, atomic: bool) { + let m = p.marker(); + p.assert(SyntaxKind::Context); + code_expr_prec(p, atomic, 0); + p.wrap(m, SyntaxKind::Contextual); +} + /// Parses an if-else conditional: `if x { y } else { z }`. fn conditional(p: &mut Parser) { let m = p.marker(); diff --git a/crates/typst-syntax/src/set.rs b/crates/typst-syntax/src/set.rs index 88a9b18b..906d5fac 100644 --- a/crates/typst-syntax/src/set.rs +++ b/crates/typst-syntax/src/set.rs @@ -102,6 +102,7 @@ pub const ATOMIC_CODE_PRIMARY: SyntaxSet = SyntaxSet::new() .add(SyntaxKind::Let) .add(SyntaxKind::Set) .add(SyntaxKind::Show) + .add(SyntaxKind::Context) .add(SyntaxKind::If) .add(SyntaxKind::While) .add(SyntaxKind::For) diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index 8c3ef084..01f6e7c6 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -34,6 +34,7 @@ icu_provider = { workspace = true } icu_provider_adapters = { workspace = true } icu_provider_blob = { workspace = true } icu_segmenter = { workspace = true } +if_chain = { workspace = true } image = { workspace = true } indexmap = { workspace = true } kamadak-exif = { workspace = true } diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs index 3a0f4f9b..c70dd761 100644 --- a/crates/typst/src/diag.rs +++ b/crates/typst/src/diag.rs @@ -304,9 +304,12 @@ pub struct HintedString { pub hints: Vec<EcoString>, } -impl From<EcoString> for HintedString { - fn from(value: EcoString) -> Self { - Self { message: value, hints: vec![] } +impl<S> From<S> for HintedString +where + S: Into<EcoString>, +{ + fn from(value: S) -> Self { + Self { message: value.into(), hints: vec![] } } } diff --git a/crates/typst/src/eval/access.rs b/crates/typst/src/eval/access.rs index 6b6dff15..ab0a6491 100644 --- a/crates/typst/src/eval/access.rs +++ b/crates/typst/src/eval/access.rs @@ -29,10 +29,12 @@ impl Access for ast::Expr<'_> { impl Access for ast::Ident<'_> { fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { let span = self.span(); - let value = vm.scopes.get_mut(&self).at(span)?; if vm.inspected == Some(span) { - vm.engine.tracer.value(value.clone()); + if let Ok(value) = vm.scopes.get(&self).cloned() { + vm.trace(value); + } } + let value = vm.scopes.get_mut(&self).at(span)?; Ok(value) } } diff --git a/crates/typst/src/eval/call.rs b/crates/typst/src/eval/call.rs index 3f7224d8..97ca6c2c 100644 --- a/crates/typst/src/eval/call.rs +++ b/crates/typst/src/eval/call.rs @@ -5,8 +5,8 @@ use crate::diag::{bail, error, At, HintedStrResult, SourceResult, Trace, Tracepo use crate::engine::Engine; use crate::eval::{Access, Eval, FlowEvent, Route, Tracer, Vm}; use crate::foundations::{ - call_method_mut, is_mutating_method, Arg, Args, Bytes, Closure, Content, Func, - IntoValue, NativeElement, Scope, Scopes, Value, + call_method_mut, is_mutating_method, Arg, Args, Bytes, Capturer, Closure, Content, + Context, Func, IntoValue, NativeElement, Scope, Scopes, Value, }; use crate::introspection::{Introspector, Locator}; use crate::math::{Accent, AccentElem, LrElem}; @@ -165,7 +165,11 @@ impl Eval for ast::FuncCall<'_> { let callee = callee.cast::<Func>().at(callee_span)?; let point = || Tracepoint::Call(callee.name().map(Into::into)); - let f = || callee.call(&mut vm.engine, args).trace(vm.world(), point, span); + let f = || { + callee + .call(&mut vm.engine, vm.context, args) + .trace(vm.world(), point, span) + }; // Stacker is broken on WASM. #[cfg(target_arch = "wasm32")] @@ -242,7 +246,7 @@ impl Eval for ast::Closure<'_> { // Collect captured variables. let captured = { - let mut visitor = CapturesVisitor::new(Some(&vm.scopes)); + let mut visitor = CapturesVisitor::new(Some(&vm.scopes), Capturer::Function); visitor.visit(self.to_untyped()); visitor.finish() }; @@ -252,6 +256,11 @@ impl Eval for ast::Closure<'_> { node: self.to_untyped().clone(), defaults, captured, + num_pos_params: self + .params() + .children() + .filter(|p| matches!(p, ast::Param::Pos(_))) + .count(), }; Ok(Value::Func(Func::from(closure).spanned(self.params().span()))) @@ -269,9 +278,13 @@ pub(crate) fn call_closure( route: Tracked<Route>, locator: Tracked<Locator>, tracer: TrackedMut<Tracer>, + context: &Context, mut args: Args, ) -> SourceResult<Value> { - let node = closure.node.cast::<ast::Closure>().unwrap(); + let (name, params, body) = match closure.node.cast::<ast::Closure>() { + Some(node) => (node.name(), node.params(), node.body()), + None => (None, ast::Params::default(), closure.node.cast().unwrap()), + }; // Don't leak the scopes from the call site. Instead, we use the scope // of captured variables we collected earlier. @@ -289,27 +302,20 @@ pub(crate) fn call_closure( }; // Prepare VM. - let mut vm = Vm::new(engine, scopes, node.span()); + let mut vm = Vm::new(engine, context, scopes, body.span()); // Provide the closure itself for recursive calls. - if let Some(name) = node.name() { + if let Some(name) = name { vm.define(name, Value::Func(func.clone())); } - // Parse the arguments according to the parameter list. - let num_pos_params = node - .params() - .children() - .filter(|p| matches!(p, ast::Param::Pos(_))) - .count(); - let num_pos_args = args.to_pos().len(); - let sink_size = num_pos_args.checked_sub(num_pos_params); + let sink_size = num_pos_args.checked_sub(closure.num_pos_params); let mut sink = None; let mut sink_pos_values = None; let mut defaults = closure.defaults.iter(); - for p in node.params().children() { + for p in params.children() { match p { ast::Param::Pos(pattern) => match pattern { ast::Pattern::Normal(ast::Expr::Ident(ident)) => { @@ -354,7 +360,7 @@ pub(crate) fn call_closure( args.finish()?; // Handle control flow. - let output = node.body().eval(&mut vm)?; + let output = body.eval(&mut vm)?; match vm.flow { Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit), Some(FlowEvent::Return(_, None)) => {} @@ -378,15 +384,17 @@ pub struct CapturesVisitor<'a> { external: Option<&'a Scopes<'a>>, internal: Scopes<'a>, captures: Scope, + capturer: Capturer, } impl<'a> CapturesVisitor<'a> { /// Create a new visitor for the given external scopes. - pub fn new(external: Option<&'a Scopes<'a>>) -> Self { + pub fn new(external: Option<&'a Scopes<'a>>, capturer: Capturer) -> Self { Self { external, internal: Scopes::new(None), captures: Scope::new(), + capturer, } } @@ -530,7 +538,7 @@ impl<'a> CapturesVisitor<'a> { return; }; - self.captures.define_captured(ident, value.clone()); + self.captures.define_captured(ident, value.clone(), self.capturer); } } } @@ -548,7 +556,7 @@ mod tests { scopes.top.define("y", 0); scopes.top.define("z", 0); - let mut visitor = CapturesVisitor::new(Some(&scopes)); + let mut visitor = CapturesVisitor::new(Some(&scopes), Capturer::Function); let root = parse(text); visitor.visit(&root); diff --git a/crates/typst/src/eval/code.rs b/crates/typst/src/eval/code.rs index e9308625..30f5fb72 100644 --- a/crates/typst/src/eval/code.rs +++ b/crates/typst/src/eval/code.rs @@ -1,8 +1,10 @@ use ecow::{eco_vec, EcoVec}; use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult}; -use crate::eval::{ops, Eval, Vm}; -use crate::foundations::{Array, Content, Dict, Str, Value}; +use crate::eval::{ops, CapturesVisitor, Eval, Vm}; +use crate::foundations::{ + Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, Value, +}; use crate::syntax::ast::{self, AstNode}; impl Eval for ast::Code<'_> { @@ -40,7 +42,11 @@ fn eval_code<'a>( } let tail = eval_code(vm, exprs)?.display(); - Value::Content(tail.styled_with_recipe(&mut vm.engine, recipe)?) + Value::Content(tail.styled_with_recipe( + &mut vm.engine, + vm.context, + recipe, + )?) } _ => expr.eval(vm)?, }; @@ -117,6 +123,7 @@ impl Eval for ast::Expr<'_> { Self::DestructAssign(v) => v.eval(vm), Self::Set(_) => bail!(forbidden("set")), Self::Show(_) => bail!(forbidden("show")), + Self::Contextual(v) => v.eval(vm).map(Value::Content), Self::Conditional(v) => v.eval(vm), Self::While(v) => v.eval(vm), Self::For(v) => v.eval(vm), @@ -129,7 +136,7 @@ impl Eval for ast::Expr<'_> { .spanned(span); if vm.inspected == Some(span) { - vm.engine.tracer.value(v.clone()); + vm.trace(v.clone()); } Ok(v) @@ -296,6 +303,56 @@ impl Eval for ast::FieldAccess<'_> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { let value = self.target().eval(vm)?; let field = self.field(); - value.field(&field).at(field.span()) + + let err = match value.field(&field).at(field.span()) { + Ok(value) => return Ok(value), + Err(err) => err, + }; + + // Check whether this is a get rule field access. + if_chain::if_chain! { + if let Value::Func(func) = &value; + if let Some(element) = func.element(); + if let Some(id) = element.field_id(&field); + let styles = vm.context.styles().at(field.span()); + if let Some(value) = element.field_from_styles( + id, + styles.as_ref().map(|&s| s).unwrap_or_default(), + ); + then { + // Only validate the context once we know that this is indeed + // a field from the style chain. + let _ = styles?; + return Ok(value); + } + } + + Err(err) + } +} + +impl Eval for ast::Contextual<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let body = self.body(); + + // Collect captured variables. + let captured = { + let mut visitor = CapturesVisitor::new(Some(&vm.scopes), Capturer::Context); + visitor.visit(body.to_untyped()); + visitor.finish() + }; + + // Define the closure. + let closure = Closure { + node: self.body().to_untyped().clone(), + defaults: vec![], + captured, + num_pos_params: 0, + }; + + let func = Func::from(closure).spanned(body.span()); + Ok(ContextElem::new(func).pack()) } } diff --git a/crates/typst/src/eval/markup.rs b/crates/typst/src/eval/markup.rs index aefb3436..d7d400e7 100644 --- a/crates/typst/src/eval/markup.rs +++ b/crates/typst/src/eval/markup.rs @@ -43,7 +43,7 @@ fn eval_markup<'a>( } let tail = eval_markup(vm, exprs)?; - seq.push(tail.styled_with_recipe(&mut vm.engine, recipe)?) + seq.push(tail.styled_with_recipe(&mut vm.engine, vm.context, recipe)?) } expr => match expr.eval(vm)? { Value::Label(label) => { diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index 2a17cf34..07d27021 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -27,7 +27,7 @@ use comemo::{Track, Tracked, TrackedMut}; use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route}; -use crate::foundations::{Cast, Module, NativeElement, Scope, Scopes, Value}; +use crate::foundations::{Cast, Context, Module, NativeElement, Scope, Scopes, Value}; use crate::introspection::{Introspector, Locator}; use crate::math::EquationElem; use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span}; @@ -60,9 +60,10 @@ pub fn eval( }; // Prepare VM. - let root = source.root(); + let context = Context::none(); let scopes = Scopes::new(Some(world.library())); - let mut vm = Vm::new(engine, scopes, root.span()); + let root = source.root(); + let mut vm = Vm::new(engine, &context, scopes, root.span()); // Check for well-formedness unless we are in trace mode. let errors = root.errors(); @@ -128,8 +129,9 @@ pub fn eval_string( }; // Prepare VM. + let context = Context::none(); let scopes = Scopes::new(Some(world.library())); - let mut vm = Vm::new(engine, scopes, root.span()); + let mut vm = Vm::new(engine, &context, scopes, root.span()); vm.scopes.scopes.push(scope); // Evaluate the code. diff --git a/crates/typst/src/eval/tracer.rs b/crates/typst/src/eval/tracer.rs index 53494895..83396041 100644 --- a/crates/typst/src/eval/tracer.rs +++ b/crates/typst/src/eval/tracer.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use ecow::EcoVec; use crate::diag::SourceDiagnostic; -use crate::foundations::Value; +use crate::foundations::{Styles, Value}; use crate::syntax::{FileId, Span}; use crate::util::hash128; @@ -14,7 +14,7 @@ pub struct Tracer { warnings: EcoVec<SourceDiagnostic>, warnings_set: HashSet<u128>, delayed: EcoVec<SourceDiagnostic>, - values: EcoVec<Value>, + values: EcoVec<(Value, Option<Styles>)>, } impl Tracer { @@ -43,7 +43,7 @@ impl Tracer { } /// Get the values for the inspected span. - pub fn values(self) -> EcoVec<Value> { + pub fn values(self) -> EcoVec<(Value, Option<Styles>)> { self.values } } @@ -74,9 +74,9 @@ impl Tracer { } /// Trace a value for the span. - pub fn value(&mut self, v: Value) { + pub fn value(&mut self, v: Value, s: Option<Styles>) { if self.values.len() < Self::MAX_VALUES { - self.values.push(v); + self.values.push((v, s)); } } } diff --git a/crates/typst/src/eval/vm.rs b/crates/typst/src/eval/vm.rs index 29deaf09..27d141cc 100644 --- a/crates/typst/src/eval/vm.rs +++ b/crates/typst/src/eval/vm.rs @@ -2,7 +2,7 @@ use comemo::Tracked; use crate::engine::Engine; use crate::eval::FlowEvent; -use crate::foundations::{IntoValue, Scopes}; +use crate::foundations::{Context, IntoValue, Scopes, Value}; use crate::syntax::ast::{self, AstNode}; use crate::syntax::Span; use crate::World; @@ -20,13 +20,20 @@ pub struct Vm<'a> { pub(crate) scopes: Scopes<'a>, /// A span that is currently under inspection. pub(crate) inspected: Option<Span>, + /// Data that is contextually made accessible to code behind the scenes. + pub(crate) context: &'a Context<'a>, } impl<'a> Vm<'a> { /// Create a new virtual machine. - pub fn new(engine: Engine<'a>, scopes: Scopes<'a>, target: Span) -> Self { + pub fn new( + engine: Engine<'a>, + context: &'a Context<'a>, + scopes: Scopes<'a>, + target: Span, + ) -> Self { let inspected = target.id().and_then(|id| engine.tracer.inspected(id)); - Self { engine, flow: None, scopes, inspected } + Self { engine, context, flow: None, scopes, inspected } } /// Access the underlying world. @@ -38,8 +45,16 @@ impl<'a> Vm<'a> { pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { let value = value.into_value(); if self.inspected == Some(var.span()) { - self.engine.tracer.value(value.clone()); + self.trace(value.clone()); } self.scopes.top.define(var.get().clone(), value); } + + /// Trace a value. + #[cold] + pub fn trace(&mut self, value: Value) { + self.engine + .tracer + .value(value.clone(), self.context.styles.map(|s| s.to_map())); + } } diff --git a/crates/typst/src/foundations/array.rs b/crates/typst/src/foundations/array.rs index b001aa20..1df24bd8 100644 --- a/crates/typst/src/foundations/array.rs +++ b/crates/typst/src/foundations/array.rs @@ -11,8 +11,8 @@ use crate::diag::{At, SourceResult, StrResult}; use crate::engine::Engine; use crate::eval::ops; use crate::foundations::{ - cast, func, repr, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue, - Reflect, Repr, Value, Version, + cast, func, repr, scope, ty, Args, Bytes, CastInfo, Context, FromValue, Func, + IntoValue, Reflect, Repr, Value, Version, }; use crate::syntax::Span; @@ -300,12 +300,14 @@ impl Array { &self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The function to apply to each item. Must return a boolean. searcher: Func, ) -> SourceResult<Option<Value>> { for item in self.iter() { if searcher - .call(engine, [item.clone()])? + .call(engine, context, [item.clone()])? .cast::<bool>() .at(searcher.span())? { @@ -322,12 +324,14 @@ impl Array { &self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The function to apply to each item. Must return a boolean. searcher: Func, ) -> SourceResult<Option<i64>> { for (i, item) in self.iter().enumerate() { if searcher - .call(engine, [item.clone()])? + .call(engine, context, [item.clone()])? .cast::<bool>() .at(searcher.span())? { @@ -397,12 +401,18 @@ impl Array { &self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The function to apply to each item. Must return a boolean. test: Func, ) -> SourceResult<Array> { let mut kept = EcoVec::new(); for item in self.iter() { - if test.call(engine, [item.clone()])?.cast::<bool>().at(test.span())? { + if test + .call(engine, context, [item.clone()])? + .cast::<bool>() + .at(test.span())? + { kept.push(item.clone()) } } @@ -416,10 +426,14 @@ impl Array { self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The function to apply to each item. mapper: Func, ) -> SourceResult<Array> { - self.into_iter().map(|item| mapper.call(engine, [item])).collect() + self.into_iter() + .map(|item| mapper.call(engine, context, [item])) + .collect() } /// Returns a new array with the values alongside their indices. @@ -521,6 +535,8 @@ impl Array { self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The initial value to start with. init: Value, /// The folding function. Must have two parameters: One for the @@ -529,7 +545,7 @@ impl Array { ) -> SourceResult<Value> { let mut acc = init; for item in self { - acc = folder.call(engine, [acc, item])?; + acc = folder.call(engine, context, [acc, item])?; } Ok(acc) } @@ -581,11 +597,13 @@ impl Array { self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The function to apply to each item. Must return a boolean. test: Func, ) -> SourceResult<bool> { for item in self { - if test.call(engine, [item])?.cast::<bool>().at(test.span())? { + if test.call(engine, context, [item])?.cast::<bool>().at(test.span())? { return Ok(true); } } @@ -599,11 +617,13 @@ impl Array { self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The function to apply to each item. Must return a boolean. test: Func, ) -> SourceResult<bool> { for item in self { - if !test.call(engine, [item])?.cast::<bool>().at(test.span())? { + if !test.call(engine, context, [item])?.cast::<bool>().at(test.span())? { return Ok(false); } } @@ -714,6 +734,8 @@ impl Array { self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The callsite span. span: Span, /// If given, applies this function to the elements in the array to @@ -726,7 +748,7 @@ impl Array { let mut key_of = |x: Value| match &key { // NOTE: We are relying on `comemo`'s memoization of function // evaluation to not excessively reevaluate the `key`. - Some(f) => f.call(engine, [x]), + Some(f) => f.call(engine, context, [x]), None => Ok(x), }; vec.make_mut().sort_by(|a, b| { @@ -762,6 +784,8 @@ impl Array { self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// If given, applies this function to the elements in the array to /// determine the keys to deduplicate by. #[named] @@ -771,7 +795,7 @@ impl Array { let mut key_of = |x: Value| match &key { // NOTE: We are relying on `comemo`'s memoization of function // evaluation to not excessively reevaluate the `key`. - Some(f) => f.call(engine, [x]), + Some(f) => f.call(engine, context, [x]), None => Ok(x), }; diff --git a/crates/typst/src/foundations/cast.rs b/crates/typst/src/foundations/cast.rs index 8e75dc71..3b74f1fb 100644 --- a/crates/typst/src/foundations/cast.rs +++ b/crates/typst/src/foundations/cast.rs @@ -111,6 +111,20 @@ impl<T: Reflect> Reflect for StrResult<T> { } } +impl<T: Reflect> Reflect for HintedStrResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + impl<T: Reflect> Reflect for SourceResult<T> { fn input() -> CastInfo { T::input() diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index 6d937dfb..54789be0 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -13,8 +13,9 @@ use smallvec::smallvec; use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - elem, func, scope, ty, Dict, Element, Fields, IntoValue, Label, NativeElement, - Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value, + elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label, + NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, + Value, }; use crate::introspection::{Location, Meta, MetaElem}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; @@ -344,10 +345,11 @@ impl Content { pub fn styled_with_recipe( self, engine: &mut Engine, + context: &Context, recipe: Recipe, ) -> SourceResult<Self> { if recipe.selector.is_none() { - recipe.apply(engine, self) + recipe.apply(engine, context, self) } else { Ok(self.styled(recipe)) } diff --git a/crates/typst/src/foundations/context.rs b/crates/typst/src/foundations/context.rs new file mode 100644 index 00000000..41fdbfec --- /dev/null +++ b/crates/typst/src/foundations/context.rs @@ -0,0 +1,80 @@ +use crate::diag::{bail, Hint, HintedStrResult, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, Args, Construct, Content, Func, Packed, Show, StyleChain, Value, +}; +use crate::introspection::{Locatable, Location}; + +/// Data that is contextually made available to code. +/// +/// _Contextual_ functions and expressions require the presence of certain +/// pieces of context to be evaluated. This includes things like `text.lang`, +/// `measure`, or `counter(heading).get()`. +#[derive(Debug, Default, Clone, Hash)] +pub struct Context<'a> { + /// The location in the document. + pub location: Option<Location>, + /// The active styles. + pub styles: Option<StyleChain<'a>>, +} + +impl<'a> Context<'a> { + /// An empty context. + pub fn none() -> Self { + Self::default() + } + + /// Create a new context from its parts. + pub fn new(location: Option<Location>, styles: Option<StyleChain<'a>>) -> Self { + Self { location, styles } + } + + /// Try to extract the location. + pub fn location(&self) -> HintedStrResult<Location> { + require(self.location) + } + + /// Try to extract the styles. + pub fn styles(&self) -> HintedStrResult<StyleChain<'a>> { + require(self.styles) + } + + /// Guard access to the introspector by requiring at least some piece of context. + pub fn introspect(&self) -> HintedStrResult<()> { + require(self.location.map(|_| ()).or(self.styles.map(|_| ()))) + } +} + +/// Extracts an optional piece of context, yielding an error with hints if +/// it isn't available. +fn require<T>(val: Option<T>) -> HintedStrResult<T> { + val.ok_or("can only be used when context is known") + .hint("try wrapping this in a `context` expression") + .hint( + "the `context` expression should wrap everything that depends on this function", + ) +} + +/// Executes a `context` block. +#[elem(Construct, Locatable, Show)] +pub struct ContextElem { + /// The function to call with the context. + #[required] + #[internal] + func: Func, +} + +impl Construct for ContextElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed<ContextElem> { + #[typst_macros::time(name = "context", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let loc = self.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + Ok(self.func.call::<[Value; 0]>(engine, &context, [])?.display()) + } +} diff --git a/crates/typst/src/foundations/element.rs b/crates/typst/src/foundations/element.rs index 324eceef..b543e76f 100644 --- a/crates/typst/src/foundations/element.rs +++ b/crates/typst/src/foundations/element.rs @@ -29,22 +29,6 @@ impl Element { T::elem() } - /// Extract the field ID for the given field name. - pub fn field_id(&self, name: &str) -> Option<u8> { - if name == "label" { - return Some(255); - } - (self.0.field_id)(name) - } - - /// Extract the field name for the given field ID. - pub fn field_name(&self, id: u8) -> Option<&'static str> { - if id == 255 { - return Some("label"); - } - (self.0.field_name)(id) - } - /// The element's normal name (e.g. `enum`). pub fn name(self) -> &'static str { self.0.name @@ -122,6 +106,27 @@ impl Element { &(self.0).0.params } + /// Extract the field ID for the given field name. + pub fn field_id(&self, name: &str) -> Option<u8> { + if name == "label" { + return Some(255); + } + (self.0.field_id)(name) + } + + /// Extract the field name for the given field ID. + pub fn field_name(&self, id: u8) -> Option<&'static str> { + if id == 255 { + return Some("label"); + } + (self.0.field_name)(id) + } + + /// Extract the field name for the given field ID. + pub fn field_from_styles(&self, id: u8, styles: StyleChain) -> Option<Value> { + (self.0.field_from_styles)(id, styles) + } + /// The element's local name, if any. pub fn local_name(&self, lang: Lang, region: Option<Region>) -> Option<&'static str> { (self.0).0.local_name.map(|f| f(lang, region)) @@ -222,6 +227,11 @@ pub trait Fields { /// Get the field with the given ID in the presence of styles. fn field_with_styles(&self, id: u8, styles: StyleChain) -> Option<Value>; + /// Get the field with the given ID from the styles. + fn field_from_styles(id: u8, styles: StyleChain) -> Option<Value> + where + Self: Sized; + /// Resolve all fields with the styles and save them in-place. fn materialize(&mut self, styles: StyleChain); @@ -260,6 +270,7 @@ pub struct NativeElementData { pub vtable: fn(capability: TypeId) -> Option<*const ()>, pub field_id: fn(name: &str) -> Option<u8>, pub field_name: fn(u8) -> Option<&'static str>, + pub field_from_styles: fn(u8, StyleChain) -> Option<Value>, pub local_name: Option<fn(Lang, Option<Region>) -> &'static str>, pub scope: Lazy<Scope>, pub params: Lazy<Vec<ParamInfo>>, diff --git a/crates/typst/src/foundations/func.rs b/crates/typst/src/foundations/func.rs index 6062da17..bb839994 100644 --- a/crates/typst/src/foundations/func.rs +++ b/crates/typst/src/foundations/func.rs @@ -8,8 +8,8 @@ use once_cell::sync::Lazy; use crate::diag::{bail, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, repr, scope, ty, Args, CastInfo, Content, Element, IntoArgs, Scope, Selector, - Type, Value, + cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, + Selector, Type, Value, }; use crate::syntax::{ast, Span, SyntaxNode}; use crate::util::{LazyHash, Static}; @@ -181,6 +181,14 @@ impl Func { } } + /// Whether the function is known to be contextual. + pub fn contextual(&self) -> Option<bool> { + match &self.repr { + Repr::Native(native) => Some(native.contextual), + _ => None, + } + } + /// Get details about this function's parameters if available. pub fn params(&self) -> Option<&'static [ParamInfo]> { match &self.repr { @@ -249,17 +257,27 @@ impl Func { } } - /// Call the function with the given arguments. - pub fn call(&self, engine: &mut Engine, args: impl IntoArgs) -> SourceResult<Value> { - self.call_impl(engine, args.into_args(self.span)) + /// Call the function with the given context and arguments. + pub fn call<A: IntoArgs>( + &self, + engine: &mut Engine, + context: &Context, + args: A, + ) -> SourceResult<Value> { + self.call_impl(engine, context, args.into_args(self.span)) } /// Non-generic implementation of `call`. #[typst_macros::time(name = "func call", span = self.span())] - fn call_impl(&self, engine: &mut Engine, mut args: Args) -> SourceResult<Value> { + fn call_impl( + &self, + engine: &mut Engine, + context: &Context, + mut args: Args, + ) -> SourceResult<Value> { match &self.repr { Repr::Native(native) => { - let value = (native.function)(engine, &mut args)?; + let value = (native.function)(engine, context, &mut args)?; args.finish()?; Ok(value) } @@ -276,11 +294,12 @@ impl Func { engine.route.track(), engine.locator.track(), TrackedMut::reborrow_mut(&mut engine.tracer), + context, args, ), Repr::With(with) => { args.items = with.1.items.iter().cloned().chain(args.items).collect(); - with.0.call(engine, args) + with.0.call(engine, context, args) } } } @@ -414,11 +433,12 @@ pub trait NativeFunc { /// Defines a native function. #[derive(Debug)] pub struct NativeFuncData { - pub function: fn(&mut Engine, &mut Args) -> SourceResult<Value>, + pub function: fn(&mut Engine, &Context, &mut Args) -> SourceResult<Value>, pub name: &'static str, pub title: &'static str, pub docs: &'static str, pub keywords: &'static [&'static str], + pub contextual: bool, pub scope: Lazy<Scope>, pub params: Lazy<Vec<ParamInfo>>, pub returns: Lazy<CastInfo>, @@ -464,22 +484,22 @@ pub struct ParamInfo { /// A user-defined closure. #[derive(Debug, Hash)] pub struct Closure { - /// The closure's syntax node. Must be castable to `ast::Closure`. + /// The closure's syntax node. Must be either castable to `ast::Closure` or + /// `ast::Expr`. In the latter case, this is a synthesized closure without + /// any parameters (used by `context` expressions). pub node: SyntaxNode, /// Default values of named parameters. pub defaults: Vec<Value>, /// Captured values from outer scopes. pub captured: Scope, + /// The number of positional parameters in the closure. + pub num_pos_params: usize, } impl Closure { /// The name of the closure. pub fn name(&self) -> Option<&str> { - self.node - .cast::<ast::Closure>() - .unwrap() - .name() - .map(|ident| ident.as_str()) + self.node.cast::<ast::Closure>()?.name().map(|ident| ident.as_str()) } } diff --git a/crates/typst/src/foundations/mod.rs b/crates/typst/src/foundations/mod.rs index f9e15bec..eb9f6d66 100644 --- a/crates/typst/src/foundations/mod.rs +++ b/crates/typst/src/foundations/mod.rs @@ -11,6 +11,7 @@ mod bool; mod bytes; mod cast; mod content; +mod context; mod datetime; mod dict; mod duration; @@ -38,6 +39,7 @@ pub use self::auto::*; pub use self::bytes::*; pub use self::cast::*; pub use self::content::*; +pub use self::context::*; pub use self::datetime::*; pub use self::dict::*; pub use self::duration::*; diff --git a/crates/typst/src/foundations/scope.rs b/crates/typst/src/foundations/scope.rs index 0567b5b0..caa82e13 100644 --- a/crates/typst/src/foundations/scope.rs +++ b/crates/typst/src/foundations/scope.rs @@ -169,10 +169,15 @@ impl Scope { } /// Define a captured, immutable binding. - pub fn define_captured(&mut self, var: impl Into<EcoString>, value: impl IntoValue) { + pub fn define_captured( + &mut self, + var: impl Into<EcoString>, + value: impl IntoValue, + capturer: Capturer, + ) { self.map.insert( var.into(), - Slot::new(value.into_value(), Kind::Captured, self.category), + Slot::new(value.into_value(), Kind::Captured(capturer), self.category), ); } @@ -246,7 +251,16 @@ enum Kind { /// A normal, mutable binding. Normal, /// A captured copy of another variable. - Captured, + Captured(Capturer), +} + +/// What the variable was captured by. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Capturer { + /// Captured by a function / closure. + Function, + /// Captured by a context expression. + Context, } impl Slot { @@ -264,10 +278,14 @@ impl Slot { fn write(&mut self) -> StrResult<&mut Value> { match self.kind { Kind::Normal => Ok(&mut self.value), - Kind::Captured => { + Kind::Captured(capturer) => { bail!( - "variables from outside the function are \ - read-only and cannot be modified" + "variables from outside the {} are \ + read-only and cannot be modified", + match capturer { + Capturer::Function => "function", + Capturer::Context => "context expression", + } ) } } diff --git a/crates/typst/src/foundations/selector.rs b/crates/typst/src/foundations/selector.rs index ee7691b0..d97c3408 100644 --- a/crates/typst/src/foundations/selector.rs +++ b/crates/typst/src/foundations/selector.rs @@ -1,15 +1,16 @@ use std::any::{Any, TypeId}; use std::sync::Arc; +use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; use smallvec::SmallVec; -use crate::diag::{bail, StrResult}; +use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{ - cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func, - Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value, + cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue, + Func, Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value, }; -use crate::introspection::{Locatable, Location}; +use crate::introspection::{Introspector, Locatable, Location}; use crate::symbols::Symbol; use crate::text::TextElem; @@ -66,11 +67,10 @@ pub use crate::__select_where as select_where; /// /// # Example /// ```example -/// #locate(loc => query( +/// #context query( /// heading.where(level: 1) -/// .or(heading.where(level: 2)), -/// loc, -/// )) +/// .or(heading.where(level: 2)) +/// ) /// /// = This will be found /// == So will this @@ -300,11 +300,29 @@ cast! { #[derive(Debug, Clone, PartialEq, Hash)] pub struct LocatableSelector(pub Selector); +impl LocatableSelector { + /// Resolve this selector into a location that is guaranteed to be unique. + pub fn resolve_unique( + &self, + introspector: Tracked<Introspector>, + context: &Context, + ) -> HintedStrResult<Location> { + match &self.0 { + Selector::Location(loc) => Ok(*loc), + other => { + context.introspect()?; + Ok(introspector.query_unique(other).map(|c| c.location().unwrap())?) + } + } + } +} + impl Reflect for LocatableSelector { fn input() -> CastInfo { CastInfo::Union(vec![ CastInfo::Type(Type::of::<Label>()), CastInfo::Type(Type::of::<Func>()), + CastInfo::Type(Type::of::<Location>()), CastInfo::Type(Type::of::<Selector>()), ]) } @@ -314,7 +332,10 @@ impl Reflect for LocatableSelector { } fn castable(value: &Value) -> bool { - Label::castable(value) || Func::castable(value) || Selector::castable(value) + Label::castable(value) + || Func::castable(value) + || Location::castable(value) + || Selector::castable(value) } } diff --git a/crates/typst/src/foundations/str.rs b/crates/typst/src/foundations/str.rs index 553c2170..48b5baf4 100644 --- a/crates/typst/src/foundations/str.rs +++ b/crates/typst/src/foundations/str.rs @@ -10,8 +10,8 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, dict, func, repr, scope, ty, Array, Bytes, Dict, Func, IntoValue, Label, Repr, - Type, Value, Version, + cast, dict, func, repr, scope, ty, Array, Bytes, Context, Dict, Func, IntoValue, + Label, Repr, Type, Value, Version, }; use crate::layout::Alignment; use crate::syntax::{Span, Spanned}; @@ -424,6 +424,8 @@ impl Str { &self, /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// The pattern to search for. pattern: StrPattern, /// The string to replace the matches with or a function that gets a @@ -449,8 +451,10 @@ impl Str { match &replacement { Replacement::Str(s) => output.push_str(s), Replacement::Func(func) => { - let piece = - func.call(engine, [dict])?.cast::<Str>().at(func.span())?; + let piece = func + .call(engine, context, [dict])? + .cast::<Str>() + .at(func.span())?; output.push_str(&piece); } } diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index 7a36c4ca..07793e82 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -9,9 +9,10 @@ use smallvec::SmallVec; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, func, ty, Content, Element, Func, NativeElement, Packed, Repr, Selector, - Show, + cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr, + Selector, Show, }; +use crate::introspection::Locatable; use crate::syntax::Span; use crate::text::{FontFamily, FontList, TextElem}; use crate::util::LazyHash; @@ -24,10 +25,10 @@ use crate::util::LazyHash; /// styles defined by [set rules]($styling/#set-rules). /// /// ```example -/// #let thing(body) = style(styles => { -/// let size = measure(body, styles) +/// #let thing(body) = context { +/// let size = measure(body) /// [Width of "#body" is #size.width] -/// }) +/// } /// /// #thing[Hey] \ /// #thing[Welcome] @@ -48,7 +49,7 @@ pub fn style( } /// Executes a style access. -#[elem(Show)] +#[elem(Locatable, Show)] struct StyleElem { /// The function to call with the styles. #[required] @@ -58,7 +59,8 @@ struct StyleElem { impl Show for Packed<StyleElem> { #[typst_macros::time(name = "style", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - Ok(self.func().call(engine, [styles.to_map()])?.display()) + let context = Context::new(self.location(), Some(styles)); + Ok(self.func().call(engine, &context, [styles.to_map()])?.display()) } } @@ -383,11 +385,16 @@ impl Recipe { } /// Apply the recipe to the given content. - pub fn apply(&self, engine: &mut Engine, content: Content) -> SourceResult<Content> { + pub fn apply( + &self, + engine: &mut Engine, + context: &Context, + content: Content, + ) -> SourceResult<Content> { let mut content = match &self.transform { Transformation::Content(content) => content.clone(), Transformation::Func(func) => { - let mut result = func.call(engine, [content.clone()]); + let mut result = func.call(engine, context, [content.clone()]); if self.selector.is_some() { let point = || Tracepoint::Show(content.func().name().into()); result = result.trace(engine.world, point, content.span()); diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index 0031ea5b..5c3e573a 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -5,13 +5,13 @@ use comemo::{Tracked, TrackedMut}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use smallvec::{smallvec, SmallVec}; -use crate::diag::{At, SourceResult, StrResult}; +use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::{Engine, Route}; use crate::eval::Tracer; use crate::foundations::{ - cast, elem, func, scope, select_where, ty, Array, Content, Element, Func, IntoValue, - Label, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, - StyleChain, Value, + cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context, + Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, + Selector, Show, Smart, Str, StyleChain, Value, }; use crate::introspection::{Introspector, Locatable, Location, Locator, Meta}; use crate::layout::{Frame, FrameItem, PageElem}; @@ -27,14 +27,30 @@ use crate::World; /// headings, figures, and more. Moreover, you can define custom counters for /// other things you want to count. /// -/// # Displaying a counter { #displaying } -/// To display the current value of the heading counter, you call the `counter` -/// function with the `key` set to `heading` and then call the `display` method -/// on the counter. To see any output, you also have to enable heading -/// [numbering]($heading.numbering). +/// Since counters change throughout the course of the document, their current +/// value is _contextual_ It is recommended to read the chapter on +/// [context]($context) before continuing here. +/// +/// # Accessing a counter { #accessing } +/// To access the raw value of a counter, we can use the [`get`]($counter.get) +/// function. This function returns an [array]($array): Counters can have +/// multiple levels (in the case of headings for sections, subsections, and so +/// on), and each item in the array corresponds to one level. +/// +/// ```example +/// #set heading(numbering: "1.") /// -/// The `display` method optionally takes an argument telling it how to format -/// the counter. This can be a [numbering pattern or a function]($numbering). +/// = Introduction +/// Raw value of heading counter is +/// #context counter(heading).get() +/// ``` +/// +/// # Displaying a counter { #displaying } +/// Often, we want to display the value of a counter in a more human-readable +/// way. To do that, we can call the [`display`]($counter.display) function on +/// the counter. This function retrieves the current counter value and formats +/// it either with a provided or with an automatically inferred +/// [numbering]($numbering). /// /// ```example /// #set heading(numbering: "1.") @@ -43,25 +59,26 @@ use crate::World; /// Some text here. /// /// = Background -/// The current value is: -/// #counter(heading).display() +/// The current value is: #context { +/// counter(heading).display() +/// } /// -/// Or in roman numerals: -/// #counter(heading).display("I") +/// Or in roman numerals: #context { +/// counter(heading).display("I") +/// } /// ``` /// /// # Modifying a counter { #modifying } /// To modify a counter, you can use the `step` and `update` methods: /// /// - The `step` method increases the value of the counter by one. Because -/// counters can have multiple levels (in the case of headings for sections, -/// subsections, and so on), the `step` method optionally takes a `level` +/// counters can have multiple levels , it optionally takes a `level` /// argument. If given, the counter steps at the given depth. /// /// - The `update` method allows you to arbitrarily modify the counter. In its -/// basic form, you give it an integer (or multiple for multiple levels). For -/// more flexibility, you can instead also give it a function that gets the -/// current value and returns a new value. +/// basic form, you give it an integer (or an array for multiple levels). For +/// more flexibility, you can instead also give it a function that receives +/// the current value and returns a new value. /// /// The heading counter is stepped before the heading is displayed, so /// `Analysis` gets the number seven even though the counter is at six after the @@ -82,19 +99,49 @@ use crate::World; /// #counter(heading).step(level: 2) /// /// == Analysis -/// Still at #counter(heading).display(). +/// Still at #context { +/// counter(heading).display() +/// } +/// ``` +/// +/// # Page counter +/// The page counter is special. It is automatically stepped at each pagebreak. +/// But like other counters, you can also step it manually. For example, you +/// could have Roman page numbers for your preface, then switch to Arabic page +/// numbers for your main content and reset the page counter to one. +/// +/// ```example +/// >>> #set page( +/// >>> height: 100pt, +/// >>> margin: (bottom: 24pt, rest: 16pt), +/// >>> ) +/// #set page(numbering: "(i)") +/// +/// = Preface +/// The preface is numbered with +/// roman numerals. +/// +/// #set page(numbering: "1 / 1") +/// #counter(page).update(1) +/// +/// = Main text +/// Here, the counter is reset to one. +/// We also display both the current +/// page and total number of pages in +/// Arabic numbers. /// ``` /// +/// # Custom counters /// To define your own counter, call the `counter` function with a string as a /// key. This key identifies the counter globally. /// /// ```example /// #let mine = counter("mycounter") -/// #mine.display() \ +/// #context mine.display() \ /// #mine.step() -/// #mine.display() \ +/// #context mine.display() \ /// #mine.update(c => c * 3) -/// #mine.display() \ +/// #context mine.display() /// ``` /// /// # How to step @@ -109,7 +156,8 @@ use crate::World; /// #let c = counter("theorem") /// #let theorem(it) = block[ /// #c.step() -/// *Theorem #c.display():* #it +/// *Theorem #context c.display():* +/// #it /// ] /// /// #theorem[$1 = 1$] @@ -117,8 +165,8 @@ use crate::World; /// ``` /// /// The rationale behind this is best explained on the example of the heading -/// counter: An update to the heading counter depends on the heading's level. -/// By stepping directly before the heading, we can correctly step from `1` to +/// counter: An update to the heading counter depends on the heading's level. By +/// stepping directly before the heading, we can correctly step from `1` to /// `1.1` when encountering a level 2 heading. If we were to step after the /// heading, we wouldn't know what to step to. /// @@ -126,33 +174,6 @@ use crate::World; /// they always start at zero. This way, they are at one for the first display /// (which happens after the first step). /// -/// # Page counter -/// The page counter is special. It is automatically stepped at each pagebreak. -/// But like other counters, you can also step it manually. For example, you -/// could have Roman page numbers for your preface, then switch to Arabic page -/// numbers for your main content and reset the page counter to one. -/// -/// ```example -/// >>> #set page( -/// >>> height: 100pt, -/// >>> margin: (bottom: 24pt, rest: 16pt), -/// >>> ) -/// #set page(numbering: "(i)") -/// -/// = Preface -/// The preface is numbered with -/// roman numerals. -/// -/// #set page(numbering: "1 / 1") -/// #counter(page).update(1) -/// -/// = Main text -/// Here, the counter is reset to one. -/// We also display both the current -/// page and total number of pages in -/// Arabic numbers. -/// ``` -/// /// # Time travel /// Counters can travel through time! You can find out the final value of the /// counter before it is reached and even determine what the value was at any @@ -162,17 +183,11 @@ use crate::World; /// #let mine = counter("mycounter") /// /// = Values -/// #locate(loc => { -/// let start-val = mine.at(loc) -/// let elements = query(<intro>, loc) -/// let intro-val = mine.at( -/// elements.first().location() -/// ) -/// let final-val = mine.final(loc) -/// [Starts as: #start-val \ -/// Value at intro is: #intro-val \ -/// Final value is: #final-val \ ] -/// }) +/// #context [ +/// Value here: #mine.get() \ +/// At intro: #mine.at(<intro>) \ +/// Final value: #mine.final() +/// ] /// /// #mine.update(n => n + 3) /// @@ -183,27 +198,6 @@ use crate::World; /// #mine.step() /// ``` /// -/// Let's dissect what happens in the example above: -/// -/// - We call [`locate`]($locate) to get access to the current location in the -/// document. We then pass this location to our counter's `at` method to get -/// its value at the current location. The `at` method always returns an array -/// because counters can have multiple levels. As the counter starts at zero, -/// the first value is thus `{(0,)}`. -/// -/// - We now [`query`]($query) the document for all elements with the -/// `{<intro>}` label. The result is an array from which we extract the first -/// (and only) element's [location]($content.location). We then look up the -/// value of the counter at that location. The first update to the counter -/// sets it to `{0 + 3 = 3}`. At the introduction heading, the value is thus -/// `{(3,)}`. -/// -/// - Last but not least, we call the `final` method on the counter. It tells us -/// what the counter's value will be at the end of the document. We also need -/// to give it a location to prove that we are inside of a `locate` call, but -/// which one doesn't matter. After the heading follow two calls to `step()`, -/// so the final value is `{(5,)}`. -/// /// # Other kinds of state { #other-state } /// The `counter` type is closely related to [state]($state) type. Read its /// documentation for more details on state management in Typst and why it @@ -247,6 +241,41 @@ impl Counter { Ok(CounterState(smallvec![at_state.first(), final_state.first()])) } + /// Gets the value of the counter at the given location. Always returns an + /// array of integers, even if the counter has just one number. + pub fn at_loc( + &self, + engine: &mut Engine, + loc: Location, + ) -> SourceResult<CounterState> { + let sequence = self.sequence(engine)?; + let offset = engine + .introspector + .query(&self.selector().before(loc.into(), true)) + .len(); + let (mut state, page) = sequence[offset].clone(); + if self.is_page() { + let delta = engine.introspector.page(loc).get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + Ok(state) + } + + /// Displays the value of the counter at the given location. + pub fn display_at_loc( + &self, + engine: &mut Engine, + loc: Location, + styles: StyleChain, + numbering: &Numbering, + ) -> SourceResult<Content> { + let context = Context::new(Some(loc), Some(styles)); + Ok(self + .at_loc(engine, loc)? + .display(engine, &context, numbering)? + .display()) + } + /// Produce the whole sequence of counter states. /// /// This has to happen just once for all counters, cutting down the number @@ -313,7 +342,7 @@ impl Counter { /// The selector relevant for this counter's updates. fn selector(&self) -> Selector { - let mut selector = select_where!(UpdateElem, Key => self.0.clone()); + let mut selector = select_where!(CounterUpdateElem, Key => self.0.clone()); if let CounterKey::Selector(key) = &self.0 { selector = Selector::Or(eco_vec![selector, key.clone()]); @@ -326,6 +355,46 @@ impl Counter { fn is_page(&self) -> bool { self.0 == CounterKey::Page } + + /// Shared implementation of displaying between `counter.display` and + /// `DisplayElem`, which will be deprecated. + fn display_impl( + &self, + engine: &mut Engine, + location: Location, + numbering: Smart<Numbering>, + both: bool, + styles: Option<StyleChain>, + ) -> SourceResult<Value> { + let numbering = numbering + .as_custom() + .or_else(|| { + let styles = styles?; + let CounterKey::Selector(Selector::Elem(func, _)) = self.0 else { + return None; + }; + + if func == HeadingElem::elem() { + HeadingElem::numbering_in(styles).clone() + } else if func == FigureElem::elem() { + FigureElem::numbering_in(styles).clone() + } else if func == EquationElem::elem() { + EquationElem::numbering_in(styles).clone() + } else { + None + } + }) + .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into()); + + let state = if both { + self.both(engine, location)? + } else { + self.at_loc(engine, location)? + }; + + let context = Context::new(Some(location), styles); + state.display(engine, &context, &numbering) + } } #[scope] @@ -347,10 +416,38 @@ impl Counter { Self(key) } - /// Displays the current value of the counter. - #[func] + /// Retrieves the value of the counter at the current location. Always + /// returns an array of integers, even if the counter has just one number. + /// + /// This is equivalent to `{counter.at(here())}`. + #[func(contextual)] + pub fn get( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, + /// The callsite span. + span: Span, + ) -> SourceResult<CounterState> { + let loc = context.location().at(span)?; + self.at_loc(engine, loc) + } + + /// Displays the current value of the counter with a numbering and returns + /// the formatted output. + /// + /// _Compatibility:_ For compatibility with Typst 0.10 and lower, this + /// function also works without an established context. Then, it will create + /// opaque contextual content rather than directly returning the output of + /// the numbering. This behaviour will be removed in a future release. + #[func(contextual)] pub fn display( self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, /// The call span of the display. span: Span, /// A [numbering pattern or a function]($numbering), which specifies how @@ -359,11 +456,11 @@ impl Counter { /// numbers varies, e.g. for the heading argument, you can use an /// [argument sink]($arguments). /// - /// If this is omitted, displays the counter with the numbering style - /// for the counted element or with the pattern `{"1.1"}` if no such - /// style exists. + /// If this is omitted or set to `{auto}`, displays the counter with the + /// numbering style for the counted element or with the pattern + /// `{"1.1"}` if no such style exists. #[default] - numbering: Option<Numbering>, + numbering: Smart<Numbering>, /// If enabled, displays the current and final top-level count together. /// Both can be styled through a single numbering pattern. This is used /// by the page numbering property to display the current and total @@ -371,8 +468,70 @@ impl Counter { #[named] #[default(false)] both: bool, - ) -> Content { - DisplayElem::new(self, numbering, both).pack().spanned(span) + ) -> SourceResult<Value> { + if let Some(loc) = context.location { + self.display_impl(engine, loc, numbering, both, context.styles) + } else { + Ok(CounterDisplayElem::new(self, numbering, both) + .pack() + .spanned(span) + .into_value()) + } + } + + /// Retrieves the value of the counter at the given location. Always returns + /// an array of integers, even if the counter has just one number. + /// + /// The `selector` must match exactly one element in the document. The most + /// useful kinds of selectors for this are [labels]($label) and + /// [locations]($location). + /// + /// _Compatibility:_ For compatibility with Typst 0.10 and lower, this + /// function also works without a known context if the `selector` is a + /// location. This behaviour will be removed in a future release. + #[func(contextual)] + pub fn at( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, + /// The callsite span. + span: Span, + /// The place at which the counter's value should be retrieved. + selector: LocatableSelector, + ) -> SourceResult<CounterState> { + let loc = selector.resolve_unique(engine.introspector, context).at(span)?; + self.at_loc(engine, loc) + } + + /// Retrieves the value of the counter at the end of the document. Always + /// returns an array of integers, even if the counter has just one number. + #[func(contextual)] + pub fn final_( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, + /// The callsite span. + span: Span, + /// _Compatibility:_ This argument only exists for compatibility with + /// Typst 0.10 and lower and shouldn't be used anymore. + #[default] + location: Option<Location>, + ) -> SourceResult<CounterState> { + if location.is_none() { + context.location().at(span)?; + } + + let sequence = self.sequence(engine)?; + let (mut state, page) = sequence.last().unwrap().clone(); + if self.is_page() { + let delta = engine.introspector.pages().get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + Ok(state) } /// Increases the value of the counter by one. @@ -411,61 +570,7 @@ impl Counter { /// return the new value (integer or array). update: CounterUpdate, ) -> Content { - UpdateElem::new(self.0, update).pack().spanned(span) - } - - /// Gets the value of the counter at the given location. Always returns an - /// array of integers, even if the counter has just one number. - #[func] - pub fn at( - &self, - /// The engine. - engine: &mut Engine, - /// The location at which the counter value should be retrieved. A - /// suitable location can be retrieved from [`locate`]($locate) or - /// [`query`]($query). - location: Location, - ) -> SourceResult<CounterState> { - let sequence = self.sequence(engine)?; - let offset = engine - .introspector - .query(&self.selector().before(location.into(), true)) - .len(); - let (mut state, page) = sequence[offset].clone(); - if self.is_page() { - let delta = - engine.introspector.page(location).get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); - } - - Ok(state) - } - - /// Gets the value of the counter at the end of the document. Always returns - /// an array of integers, even if the counter has just one number. - #[func] - pub fn final_( - &self, - /// The engine. - engine: &mut Engine, - /// Can be an arbitrary location, as its value is irrelevant for the - /// method's return value. Why is it required then? Typst has to - /// evaluate parts of your code multiple times to determine all counter - /// values. By only allowing this method within [`locate`]($locate) - /// calls, the amount of code that can depend on the method's result is - /// reduced. If you could call `final` directly at the top level of a - /// module, the evaluation of the whole module and its exports could - /// depend on the counter's value. - location: Location, - ) -> SourceResult<CounterState> { - let _ = location; - let sequence = self.sequence(engine)?; - let (mut state, page) = sequence.last().unwrap().clone(); - if self.is_page() { - let delta = engine.introspector.pages().get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); - } - Ok(state) + CounterUpdateElem::new(self.0, update).pack().spanned(span) } } @@ -480,7 +585,8 @@ impl Repr for Counter { pub enum CounterKey { /// The page counter. Page, - /// Counts elements matching the given selectors. Only works for locatable + /// Counts elements matching the given selectors. Only works for + /// [locatable]($location/#locatable) /// elements or labels. Selector(Selector), /// Counts through manual counters with the same key. @@ -517,7 +623,6 @@ impl Repr for CounterKey { } /// An update to perform on a counter. -#[ty(cast)] #[derive(Debug, Clone, PartialEq, Hash)] pub enum CounterUpdate { /// Set the counter to the specified state. @@ -528,14 +633,8 @@ pub enum CounterUpdate { Func(Func), } -impl Repr for CounterUpdate { - fn repr(&self) -> EcoString { - "..".into() - } -} - cast! { - type CounterUpdate, + CounterUpdate, v: CounterState => Self::Set(v), v: Func => Self::Func(v), } @@ -570,8 +669,10 @@ impl CounterState { CounterUpdate::Set(state) => *self = state, CounterUpdate::Step(level) => self.step(level, 1), CounterUpdate::Func(func) => { - *self = - func.call(engine, self.0.iter().copied())?.cast().at(func.span())? + *self = func + .call(engine, &Context::none(), self.0.iter().copied())? + .cast() + .at(func.span())? } } Ok(()) @@ -600,9 +701,10 @@ impl CounterState { pub fn display( &self, engine: &mut Engine, + context: &Context, numbering: &Numbering, - ) -> SourceResult<Content> { - Ok(numbering.apply(engine, &self.0)?.display()) + ) -> SourceResult<Value> { + numbering.apply(engine, context, &self.0) } } @@ -616,78 +718,77 @@ cast! { .collect::<StrResult<_>>()?), } -/// Executes a display of a state. -#[elem(Locatable, Show)] -struct DisplayElem { - /// The counter. - #[required] - counter: Counter, - - /// The numbering to display the counter with. +/// Executes an update of a counter. +#[elem(Construct, Locatable, Show, Count)] +struct CounterUpdateElem { + /// The key that identifies the counter. #[required] - numbering: Option<Numbering>, + key: CounterKey, - /// Whether to display both the current and final value. + /// The update to perform on the counter. #[required] - both: bool, + #[internal] + update: CounterUpdate, } -impl Show for Packed<DisplayElem> { - #[typst_macros::time(name = "counter.display", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - let location = self.location().unwrap(); - let counter = self.counter(); - let numbering = self - .numbering() - .clone() - .or_else(|| { - let CounterKey::Selector(Selector::Elem(func, _)) = counter.0 else { - return None; - }; - - if func == HeadingElem::elem() { - HeadingElem::numbering_in(styles).clone() - } else if func == FigureElem::elem() { - FigureElem::numbering_in(styles).clone() - } else if func == EquationElem::elem() { - EquationElem::numbering_in(styles).clone() - } else { - None - } - }) - .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into()); +impl Construct for CounterUpdateElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} - let state = if *self.both() { - counter.both(engine, location)? - } else { - counter.at(engine, location)? - }; +impl Show for Packed<CounterUpdateElem> { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} - state.display(engine, &numbering) +impl Count for Packed<CounterUpdateElem> { + fn update(&self) -> Option<CounterUpdate> { + Some(self.update.clone()) } } -/// Executes an update of a counter. -#[elem(Locatable, Show, Count)] -struct UpdateElem { - /// The key that identifies the counter. +/// **Deprection planned.** +/// +/// Executes a display of a counter. +#[elem(Construct, Locatable, Show)] +pub struct CounterDisplayElem { + /// The counter. #[required] - key: CounterKey, + #[internal] + counter: Counter, - /// The update to perform on the counter. + /// The numbering to display the counter with. #[required] - update: CounterUpdate, + #[internal] + numbering: Smart<Numbering>, + + /// Whether to display both the current and final value. + #[required] + #[internal] + both: bool, } -impl Show for Packed<UpdateElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(Content::empty()) +impl Construct for CounterDisplayElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); } } -impl Count for Packed<UpdateElem> { - fn update(&self) -> Option<CounterUpdate> { - Some(self.update.clone()) +impl Show for Packed<CounterDisplayElem> { + #[typst_macros::time(name = "counter.display", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self + .counter + .display_impl( + engine, + self.location().unwrap(), + self.numbering.clone(), + self.both, + Some(styles), + )? + .display()) } } @@ -721,7 +822,9 @@ impl ManualPageCounter { match item { FrameItem::Group(group) => self.visit(engine, &group.frame)?, FrameItem::Meta(Meta::Elem(elem), _) => { - let Some(elem) = elem.to_packed::<UpdateElem>() else { continue }; + let Some(elem) = elem.to_packed::<CounterUpdateElem>() else { + continue; + }; if *elem.key() == CounterKey::Page { let mut state = CounterState(smallvec![self.logical]); state.update(engine, elem.update.clone())?; diff --git a/crates/typst/src/introspection/here.rs b/crates/typst/src/introspection/here.rs new file mode 100644 index 00000000..310c38d3 --- /dev/null +++ b/crates/typst/src/introspection/here.rs @@ -0,0 +1,51 @@ +use crate::diag::HintedStrResult; +use crate::foundations::{func, Context}; +use crate::introspection::Location; + +/// Provides the current location in the document. +/// +/// You can think of `here` as a low-level building block that directly extracts +/// the current location from the active [context]($context). Some other +/// functions use it internally: For instance, `{counter.get()}` is equivalent +/// to `{counter.at(here())}`. +/// +/// Within show rules on [locatable]($location/#locatable) elements, `{here()}` +/// will match the location of the shown element. +/// +/// If you want to display the current page number, refer to the documentation +/// of the [`counter`]($counter) type. While `here` can be used to determine the +/// physical page number, typically you want the logical page number that may, +/// for instance, have been reset after a preface. +/// +/// # Examples +/// Determining the current position in the document in combination with +/// [`locate`]($locate): +/// ```example +/// #context [ +/// I am located at +/// #here().position() +/// ] +/// ``` +/// +/// Running a [query]($query) for elements before the current position: +/// ```example +/// = Introduction +/// = Background +/// +/// There are +/// #context query( +/// selector(heading).before(here()) +/// ).len() +/// headings before me. +/// +/// = Conclusion +/// ``` +/// Refer to the [`selector`]($selector) type for more details on before/after +/// selectors. +#[func(contextual)] +pub fn here( + /// The callsite context. + context: &Context, +) -> HintedStrResult<Location> { + context.location() +} diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst/src/introspection/introspector.rs index 47d40476..719064c0 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -202,6 +202,27 @@ impl Introspector { } } + /// Query for the first element that matches the selector. + pub fn query_unique(&self, selector: &Selector) -> StrResult<Content> { + match selector { + Selector::Location(location) => self + .get(location) + .cloned() + .ok_or_else(|| "element does not exist in the document".into()), + Selector::Label(label) => self.query_label(*label).cloned(), + _ => { + let elems = self.query(selector); + if elems.len() > 1 { + bail!("selector matches multiple elements",); + } + elems + .into_iter() + .next() + .ok_or_else(|| "selector does not match any element".into()) + } + } + } + /// Query for a unique element with the label. pub fn query_label(&self, label: Label) -> StrResult<&Content> { let indices = self.labels.get(&label).ok_or_else(|| { diff --git a/crates/typst/src/introspection/locate.rs b/crates/typst/src/introspection/locate.rs index 46d03705..147bfb0e 100644 --- a/crates/typst/src/introspection/locate.rs +++ b/crates/typst/src/introspection/locate.rs @@ -1,36 +1,96 @@ -use crate::diag::SourceResult; +use crate::diag::{HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, func, Content, Func, NativeElement, Packed, Show, StyleChain, + cast, elem, func, Content, Context, Func, LocatableSelector, NativeElement, Packed, + Show, StyleChain, Value, }; -use crate::introspection::Locatable; +use crate::introspection::{Locatable, Location}; use crate::syntax::Span; -/// Provides access to the location of content. +/// Determines the location of an element in the document. /// -/// This is useful in combination with [queries]($query), [counters]($counter), -/// [state]($state), and [links]($link). See their documentation for more -/// details. +/// Takes a selector that must match exactly one element and returns that +/// element's [`location`]($location). This location can, in particular, be used +/// to retrieve the physical [`page`]($location.page) number and +/// [`position`]($location.position) (page, x, y) for that element. /// +/// # Examples +/// Locating a specific element: /// ```example -/// #locate(loc => [ -/// My location: \ -/// #loc.position()! -/// ]) +/// #context [ +/// Introduction is at: \ +/// #locate(<intro>).position() +/// ] +/// +/// = Introduction <intro> /// ``` -#[func] +/// +/// # Compatibility +/// In Typst 0.10 and lower, the `locate` function took a closure that made the +/// current location in the document available (like [`here`]($here) does now). +/// Compatibility with the old way will remain for a while to give package +/// authors time to upgrade. To that effect, `locate` detects whether it +/// received a selector or a user-defined function and adjusts its semantics +/// accordingly. This behaviour will be removed in the future. +#[func(contextual)] pub fn locate( + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, /// The span of the `locate` call. span: Span, - /// A function that receives a [`location`]($location). Its return value is - /// displayed in the document. + /// A selector that should match exactly one element. This element will be + /// located. /// - /// This function is called once for each time the content returned by - /// `locate` appears in the document. That makes it possible to generate - /// content that depends on its own location in the document. - func: Func, -) -> Content { - LocateElem::new(func).pack().spanned(span) + /// Especially useful in combination with + /// - [`here`]($here) to locate the current context, + /// - a [`location`]($location) retrieved from some queried element via the + /// [`location()`]($content.location) method on content. + selector: LocateInput, +) -> HintedStrResult<LocateOutput> { + Ok(match selector { + LocateInput::Selector(selector) => { + LocateOutput::Location(selector.resolve_unique(engine.introspector, context)?) + } + LocateInput::Func(func) => { + LocateOutput::Content(LocateElem::new(func).pack().spanned(span)) + } + }) +} + +/// Compatible input type. +pub enum LocateInput { + Selector(LocatableSelector), + Func(Func), +} + +cast! { + LocateInput, + v: Func => { + if v.element().is_some() { + Self::Selector(Value::Func(v).cast()?) + } else { + Self::Func(v) + } + }, + v: LocatableSelector => Self::Selector(v), +} + +/// Compatible output type. +pub enum LocateOutput { + Location(Location), + Content(Content), +} + +cast! { + LocateOutput, + self => match self { + Self::Location(v) => v.into_value(), + Self::Content(v) => v.into_value(), + }, + v: Location => Self::Location(v), + v: Content => Self::Content(v), } /// Executes a `locate` call. @@ -43,8 +103,9 @@ struct LocateElem { impl Show for Packed<LocateElem> { #[typst_macros::time(name = "locate", span = self.span())] - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { let location = self.location().unwrap(); - Ok(self.func().call(engine, [location])?.display()) + let context = Context::new(Some(location), Some(styles)); + Ok(self.func().call(engine, &context, [location])?.display()) } } diff --git a/crates/typst/src/introspection/location.rs b/crates/typst/src/introspection/location.rs index 23ed81d6..215303f5 100644 --- a/crates/typst/src/introspection/location.rs +++ b/crates/typst/src/introspection/location.rs @@ -3,16 +3,25 @@ use std::num::NonZeroUsize; use ecow::EcoString; use crate::engine::Engine; -use crate::foundations::{func, scope, ty, Dict, Repr}; +use crate::foundations::{func, scope, ty, Repr}; +use crate::layout::Position; use crate::model::Numbering; /// Identifies an element in the document. /// /// A location uniquely identifies an element in the document and lets you /// access its absolute position on the pages. You can retrieve the current -/// location with the [`locate`]($locate) function and the location of a queried +/// location with the [`here`]($here) function and the location of a queried /// or shown element with the [`location()`]($content.location) method on /// content. +/// +/// # Locatable elements { #locatable } +/// Currently, only a subset of element functions is locatable. Aside from +/// headings and figures, this includes equations, references and all +/// elements with an explicit label. As a result, you _can_ query for e.g. +/// [`strong`]($strong) elements, but you will find only those that have an +/// explicit label attached to them. This limitation will be resolved in the +/// future. #[ty(scope)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Location { @@ -37,27 +46,36 @@ impl Location { #[scope] impl Location { - /// Return the page number for this location. + /// Returns the page number for this location. /// /// Note that this does not return the value of the [page counter]($counter) /// at this location, but the true page number (starting from one). /// /// If you want to know the value of the page counter, use /// `{counter(page).at(loc)}` instead. + /// + /// Can be used with [`here`]($here) to retrieve the physical page position + /// of the current context: + /// ```example + /// #context [ + /// I am located on + /// page #here().page() + /// ] + /// ``` #[func] pub fn page(self, engine: &mut Engine) -> NonZeroUsize { engine.introspector.page(self) } - /// Return a dictionary with the page number and the x, y position for this + /// Returns a dictionary with the page number and the x, y position for this /// location. The page number starts at one and the coordinates are measured /// from the top-left of the page. /// /// If you only need the page number, use `page()` instead as it allows /// Typst to skip unnecessary work. #[func] - pub fn position(self, engine: &mut Engine) -> Dict { - engine.introspector.position(self).into() + pub fn position(self, engine: &mut Engine) -> Position { + engine.introspector.position(self) } /// Returns the page numbering pattern of the page at this location. This diff --git a/crates/typst/src/introspection/metadata.rs b/crates/typst/src/introspection/metadata.rs index d8fb1574..962d57e4 100644 --- a/crates/typst/src/introspection/metadata.rs +++ b/crates/typst/src/introspection/metadata.rs @@ -7,8 +7,8 @@ use crate::realize::{Behave, Behaviour}; /// Exposes a value to the query system without producing visible content. /// /// This element can be retrieved with the [`query`]($query) function and from -/// the command with [`typst query`]($reference/meta/query/#cli-queries). Its -/// purpose is to expose an arbitrary value to the introspection system. To +/// the command line with [`typst query`]($reference/meta/query/#cli-queries). +/// Its purpose is to expose an arbitrary value to the introspection system. To /// identify a metadata value among others, you can attach a [`label`]($label) /// to it and query for that label. /// @@ -20,9 +20,9 @@ use crate::realize::{Behave, Behaviour}; /// #metadata("This is a note") <note> /// /// // And find it from anywhere else. -/// #locate(loc => { -/// query(<note>, loc).first().value -/// }) +/// #context { +/// query(<note>).first().value +/// } /// ``` #[elem(Behave, Show, Locatable)] pub struct MetadataElem { diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst/src/introspection/mod.rs index af5faf26..2019111e 100644 --- a/crates/typst/src/introspection/mod.rs +++ b/crates/typst/src/introspection/mod.rs @@ -1,6 +1,8 @@ //! Interaction between document parts. mod counter; +#[path = "here.rs"] +mod here_; mod introspector; #[path = "locate.rs"] mod locate_; @@ -12,6 +14,7 @@ mod query_; mod state; pub use self::counter::*; +pub use self::here_::*; pub use self::introspector::*; pub use self::locate_::*; pub use self::location::*; @@ -25,9 +28,8 @@ use std::fmt::{self, Debug, Formatter}; use ecow::{eco_format, EcoString}; use smallvec::SmallVec; -use crate::foundations::Packed; use crate::foundations::{ - category, elem, ty, Category, Content, Repr, Scope, Unlabellable, + category, elem, ty, Category, Content, Packed, Repr, Scope, Unlabellable, }; use crate::model::Destination; use crate::realize::{Behave, Behaviour}; @@ -39,6 +41,9 @@ use crate::realize::{Behave, Behaviour}; /// equation counters or create custom ones. Meanwhile, the `query` function /// lets you search for elements in the document to construct things like a list /// of figures or headers which show the current chapter title. +/// +/// Most of the functions are _contextual._ It is recommended to read the chapter +/// on [context]($context) before continuing here. #[category] pub static INTROSPECTION: Category; @@ -49,8 +54,9 @@ pub fn define(global: &mut Scope) { global.define_type::<Counter>(); global.define_type::<State>(); global.define_elem::<MetadataElem>(); - global.define_func::<locate>(); + global.define_func::<here>(); global.define_func::<query>(); + global.define_func::<locate>(); } /// Hosts metadata and ensures metadata is produced even for empty elements. diff --git a/crates/typst/src/introspection/query.rs b/crates/typst/src/introspection/query.rs index e3446576..a698a267 100644 --- a/crates/typst/src/introspection/query.rs +++ b/crates/typst/src/introspection/query.rs @@ -1,5 +1,6 @@ +use crate::diag::HintedStrResult; use crate::engine::Engine; -use crate::foundations::{func, Array, LocatableSelector, Value}; +use crate::foundations::{func, Array, Context, LocatableSelector, Value}; use crate::introspection::Location; /// Finds elements in the document. @@ -38,10 +39,9 @@ use crate::introspection::Location; /// >>> margin: (top: 35pt, rest: 15pt), /// >>> header-ascent: 12pt, /// >>> ) -/// #set page(header: locate(loc => { +/// #set page(header: context { /// let elems = query( -/// selector(heading).before(loc), -/// loc, +/// selector(heading).before(here()), /// ) /// let academy = smallcaps[ /// Typst Academy @@ -52,7 +52,7 @@ use crate::introspection::Location; /// let body = elems.last().body /// academy + h(1fr) + emph(body) /// } -/// })) +/// }) /// /// = Introduction /// #lorem(23) @@ -84,11 +84,11 @@ use crate::introspection::Location; /// /// ```example /// = Real -/// #locate(loc => { -/// let elems = query(heading, loc) +/// #context { +/// let elems = query(heading) /// let count = elems.len() /// count * [= Fake] -/// }) +/// } /// ``` /// /// # Command line queries @@ -130,31 +130,29 @@ use crate::introspection::Location; /// $ typst query example.typ "<note>" --field value --one /// "This is a note" /// ``` -#[func] +#[func(contextual)] pub fn query( /// The engine. engine: &mut Engine, - /// Can be an element function like a `heading` or `figure`, a `{<label>}` - /// or a more complex selector like `{heading.where(level: 1)}`. + /// The callsite context. + context: &Context, + /// Can be + /// - an element function like a `heading` or `figure`, + /// - a `{<label>}`, + /// - a more complex selector like `{heading.where(level: 1)}`, + /// - or `{selector(heading).before(here())}`. /// - /// Currently, only a subset of element functions is supported. Aside from - /// headings and figures, this includes equations, references and all - /// elements with an explicit label. As a result, you _can_ query for e.g. - /// [`strong`]($strong) elements, but you will find only those that have an - /// explicit label attached to them. This limitation will be resolved in the - /// future. + /// Only [locatable]($location/#locatable) element functions are supported. target: LocatableSelector, - /// Can be an arbitrary location, as its value is irrelevant for the - /// function's return value. Why is it required then? As noted before, Typst - /// has to evaluate parts of your code multiple times to determine the - /// values of all state. By only allowing this function within - /// [`locate`]($locate) calls, the amount of code that can depend on the - /// query's result is reduced. If you could call it directly at the top - /// level of a module, the evaluation of the whole module and its exports - /// could depend on the query's result. - location: Location, -) -> Array { - let _ = location; + /// _Compatibility:_ This argument only exists for compatibility with + /// Typst 0.10 and lower and shouldn't be used anymore. + #[default] + location: Option<Location>, +) -> HintedStrResult<Array> { + if location.is_none() { + context.introspect()?; + } + let vec = engine.introspector.query(&target.0); - vec.into_iter().map(Value::Content).collect() + Ok(vec.into_iter().map(Value::Content).collect()) } diff --git a/crates/typst/src/introspection/state.rs b/crates/typst/src/introspection/state.rs index a4a358c9..a92b03ff 100644 --- a/crates/typst/src/introspection/state.rs +++ b/crates/typst/src/introspection/state.rs @@ -1,12 +1,13 @@ use comemo::{Tracked, TrackedMut}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; -use crate::diag::SourceResult; +use crate::diag::{bail, At, SourceResult}; use crate::engine::{Engine, Route}; use crate::eval::Tracer; use crate::foundations::{ - cast, elem, func, scope, select_where, ty, Content, Func, NativeElement, Packed, - Repr, Selector, Show, Str, StyleChain, Value, + cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func, + LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain, + Value, }; use crate::introspection::{Introspector, Locatable, Location, Locator}; use crate::syntax::Span; @@ -22,6 +23,7 @@ use crate::World; /// outside the function are read-only and cannot be modified._ /// /// ```typ +/// // This doesn't work! /// #let x = 0 /// #let compute(expr) = { /// x = eval( @@ -70,17 +72,18 @@ use crate::World; /// # Managing state in Typst { #state-in-typst } /// So what do we do instead? We use Typst's state management system. Calling /// the `state` function with an identifying string key and an optional initial -/// value gives you a state value which exposes a few methods. The two most -/// important ones are `display` and `update`: +/// value gives you a state value which exposes a few function. The two most +/// important ones are `get` and `update`: /// -/// - The `display` method shows the current value of the state. You can -/// optionally give it a function that receives the value and formats it in -/// some way. +/// - The [`get`]($state.get) function retrieves the current value of the state. +/// Because the value can vary over the course of the document, it is a +/// _contextual_ function that can only be used when [context]($context) is +/// available. /// -/// - The `update` method modifies the state. You can give it any value. If -/// given a non-function value, it sets the state to that value. If given a -/// function, that function receives the previous state and has to return the -/// new state. +/// - The [`update`]($state.update) function modifies the state. You can give it +/// any value. If given a non-function value, it sets the state to that value. +/// If given a function, that function receives the previous state and has to +/// return the new state. /// /// Our initial example would now look like this: /// @@ -90,7 +93,7 @@ use crate::World; /// #s.update(x => /// eval(expr.replace("x", str(x))) /// ) -/// New value is #s.display(). +/// New value is #context s.get(). /// ] /// /// #compute("10") \ @@ -103,8 +106,8 @@ use crate::World; /// order. The `update` method returns content and its effect occurs at the /// position where the returned content is inserted into the document. /// -/// As a result, we can now also store some of the computations in -/// variables, but they still show the correct results: +/// As a result, we can now also store some of the computations in variables, +/// but they still show the correct results: /// /// ```example /// >>> #let s = state("x", 0) @@ -112,7 +115,7 @@ use crate::World; /// >>> #s.update(x => /// >>> eval(expr.replace("x", str(x))) /// >>> ) -/// >>> New value is #s.display(). +/// >>> New value is #context s.get(). /// >>> ] /// <<< ... /// @@ -132,10 +135,9 @@ use crate::World; /// /// # Time Travel /// By using Typst's state management system you also get time travel -/// capabilities! By combining the state system with [`locate`]($locate) and -/// [`query`]($query), we can find out what the value of the state will be at -/// any position in the document from anywhere else. In particular, the `at` -/// method gives us the value of the state at any location and the `final` +/// capabilities! We can find out what the value of the state will be at any +/// position in the document from anywhere else. In particular, the `at` method +/// gives us the value of the state at any particular location and the `final` /// methods gives us the value of the state at the end of the document. /// /// ```example @@ -144,16 +146,12 @@ use crate::World; /// >>> #s.update(x => { /// >>> eval(expr.replace("x", str(x))) /// >>> }) -/// >>> New value is #s.display(). +/// >>> New value is #context s.get(). /// >>> ] /// <<< ... /// /// Value at `<here>` is -/// #locate(loc => s.at( -/// query(<here>, loc) -/// .first() -/// .location() -/// )) +/// #context s.at(<here>) /// /// #compute("10") \ /// #compute("x + 3") \ @@ -171,21 +169,21 @@ use crate::World; /// a state, the results might never converge. The example below illustrates /// this. We initialize our state with `1` and then update it to its own final /// value plus 1. So it should be `2`, but then its final value is `2`, so it -/// should be `3`, and so on. This example displays a finite value because -/// Typst simply gives up after a few attempts. +/// should be `3`, and so on. This example displays a finite value because Typst +/// simply gives up after a few attempts. /// /// ```example +/// // This is bad! /// #let s = state("x", 1) -/// #locate(loc => { -/// s.update(s.final(loc) + 1) -/// }) -/// #s.display() +/// #context s.update(s.final() + 1) +/// #context s.get() /// ``` /// -/// In general, you should _typically_ not generate state updates from within -/// `locate` calls or `display` calls of state or counters. Instead, pass a -/// function to `update` that determines the value of the state based on its -/// previous value. +/// In general, you should try not to generate state updates from within context +/// expressions. If possible, try to express your updates as non-contextual +/// values or functions that compute the new value from the previous value. +/// Sometimes, it cannot be helped, but in those cases it is up to you to ensure +/// that the result converges. #[ty(scope)] #[derive(Debug, Clone, PartialEq, Hash)] pub struct State { @@ -201,6 +199,16 @@ impl State { Self { key, init } } + /// Get the value of the state at the given location. + pub fn at_loc(&self, engine: &mut Engine, loc: Location) -> SourceResult<Value> { + let sequence = self.sequence(engine)?; + let offset = engine + .introspector + .query(&self.selector().before(loc.into(), true)) + .len(); + Ok(sequence[offset].clone()) + } + /// Produce the whole sequence of states. /// /// This has to happen just once for all states, cutting down the number @@ -237,10 +245,12 @@ impl State { let mut stops = eco_vec![state.clone()]; for elem in introspector.query(&self.selector()) { - let elem = elem.to_packed::<UpdateElem>().unwrap(); + let elem = elem.to_packed::<StateUpdateElem>().unwrap(); match elem.update() { StateUpdate::Set(value) => state = value.clone(), - StateUpdate::Func(func) => state = func.call(&mut engine, [state])?, + StateUpdate::Func(func) => { + state = func.call(&mut engine, &Context::none(), [state])? + } } stops.push(state.clone()); } @@ -250,7 +260,7 @@ impl State { /// The selector for this state's updates. fn selector(&self) -> Selector { - select_where!(UpdateElem, Key => self.key.clone()) + select_where!(StateUpdateElem, Key => self.key.clone()) } } @@ -268,19 +278,69 @@ impl State { Self::new(key, init) } - /// Displays the current value of the state. - #[func] - pub fn display( - self, - /// The span of the `display` call. + /// Retrieves the value of the state at the current location. + /// + /// This is equivalent to `{state.at(here())}`. + #[func(contextual)] + pub fn get( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, + /// The callsite span. span: Span, - /// A function which receives the value of the state and can return - /// arbitrary content which is then displayed. If this is omitted, the - /// value is directly displayed. + ) -> SourceResult<Value> { + let loc = context.location().at(span)?; + self.at_loc(engine, loc) + } + + /// Retrieves the value of the state at the given selector's unique match. + /// + /// The `selector` must match exactly one element in the document. The most + /// useful kinds of selectors for this are [labels]($label) and + /// [locations]($location). + /// + /// _Compatibility:_ For compatibility with Typst 0.10 and lower, this + /// function also works without a known context if the `selector` is a + /// location. This behaviour will be removed in a future release. + #[func(contextual)] + pub fn at( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, + /// The callsite span. + span: Span, + /// The place at which the state's value should be retrieved. + selector: LocatableSelector, + ) -> SourceResult<Value> { + let loc = selector.resolve_unique(engine.introspector, context).at(span)?; + self.at_loc(engine, loc) + } + + /// Retrieves the value of the state at the end of the document. + #[func(contextual)] + pub fn final_( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: &Context, + /// The callsite span. + span: Span, + /// _Compatibility:_ This argument only exists for compatibility with + /// Typst 0.10 and lower and shouldn't be used anymore. #[default] - func: Option<Func>, - ) -> Content { - DisplayElem::new(self, func).pack().spanned(span) + location: Option<Location>, + ) -> SourceResult<Value> { + if location.is_none() { + context.location().at(span)?; + } + + let sequence = self.sequence(engine)?; + Ok(sequence.last().unwrap().clone()) } /// Update the value of the state. @@ -301,47 +361,24 @@ impl State { /// to return the new state. update: StateUpdate, ) -> Content { - UpdateElem::new(self.key, update).pack().spanned(span) + StateUpdateElem::new(self.key, update).pack().spanned(span) } - /// Get the value of the state at the given location. - #[func] - pub fn at( - &self, - /// The engine. - engine: &mut Engine, - /// The location at which the state's value should be retrieved. A - /// suitable location can be retrieved from [`locate`]($locate) or - /// [`query`]($query). - location: Location, - ) -> SourceResult<Value> { - let sequence = self.sequence(engine)?; - let offset = engine - .introspector - .query(&self.selector().before(location.into(), true)) - .len(); - Ok(sequence[offset].clone()) - } - - /// Get the value of the state at the end of the document. + /// **Deprection planned:** Use [`get`]($state.get) instead. + /// + /// Displays the current value of the state. #[func] - pub fn final_( - &self, - /// The engine. - engine: &mut Engine, - /// Can be an arbitrary location, as its value is irrelevant for the - /// method's return value. Why is it required then? As noted before, - /// Typst has to evaluate parts of your code multiple times to determine - /// the values of all state. By only allowing this method within - /// [`locate`]($locate) calls, the amount of code that can depend on the - /// method's result is reduced. If you could call `final` directly at - /// the top level of a module, the evaluation of the whole module and - /// its exports could depend on the state's value. - location: Location, - ) -> SourceResult<Value> { - let _ = location; - let sequence = self.sequence(engine)?; - Ok(sequence.last().unwrap().clone()) + pub fn display( + self, + /// The span of the `display` call. + span: Span, + /// A function which receives the value of the state and can return + /// arbitrary content which is then displayed. If this is omitted, the + /// value is directly displayed. + #[default] + func: Option<Func>, + ) -> Content { + StateDisplayElem::new(self, func).pack().spanned(span) } } @@ -352,7 +389,6 @@ impl Repr for State { } /// An update to perform on a state. -#[ty(cast)] #[derive(Debug, Clone, PartialEq, Hash)] pub enum StateUpdate { /// Set the state to the specified value. @@ -361,56 +397,68 @@ pub enum StateUpdate { Func(Func), } -impl Repr for StateUpdate { - fn repr(&self) -> EcoString { - "..".into() - } -} - cast! { - type StateUpdate, + StateUpdate, v: Func => Self::Func(v), v: Value => Self::Set(v), } /// Executes a display of a state. -#[elem(Locatable, Show)] -struct DisplayElem { +#[elem(Construct, Locatable, Show)] +struct StateUpdateElem { + /// The key that identifies the state. + #[required] + key: Str, + + /// The update to perform on the state. + #[required] + #[internal] + update: StateUpdate, +} + +impl Construct for StateUpdateElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed<StateUpdateElem> { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} + +/// **Deprection planned.** +/// +/// Executes a display of a state. +#[elem(Construct, Locatable, Show)] +struct StateDisplayElem { /// The state. #[required] + #[internal] state: State, /// The function to display the state with. #[required] + #[internal] func: Option<Func>, } -impl Show for Packed<DisplayElem> { +impl Show for Packed<StateDisplayElem> { #[typst_macros::time(name = "state.display", span = self.span())] - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { let location = self.location().unwrap(); - let value = self.state().at(engine, location)?; + let context = Context::new(Some(location), Some(styles)); + let value = self.state().at_loc(engine, location)?; Ok(match self.func() { - Some(func) => func.call(engine, [value])?.display(), + Some(func) => func.call(engine, &context, [value])?.display(), None => value.display(), }) } } -/// Executes a display of a state. -#[elem(Locatable, Show)] -struct UpdateElem { - /// The key that identifies the state. - #[required] - key: Str, - - /// The update to perform on the state. - #[required] - update: StateUpdate, -} - -impl Show for Packed<UpdateElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(Content::empty()) +impl Construct for StateDisplayElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); } } diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs index 543d736d..83dbf069 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst/src/layout/grid/layout.rs @@ -14,8 +14,8 @@ use crate::diag::{ }; use crate::engine::Engine; use crate::foundations::{ - Array, CastInfo, Content, Fold, FromValue, Func, IntoValue, Reflect, Resolve, Smart, - StyleChain, Value, + Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect, + Resolve, Smart, StyleChain, Value, }; use crate::layout::{ Abs, Alignment, Axes, Dir, Fr, Fragment, Frame, FrameItem, LayoutMultiple, Length, @@ -39,10 +39,19 @@ pub enum Celled<T> { impl<T: Default + Clone + FromValue> Celled<T> { /// Resolve the value based on the cell position. - pub fn resolve(&self, engine: &mut Engine, x: usize, y: usize) -> SourceResult<T> { + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + x: usize, + y: usize, + ) -> SourceResult<T> { Ok(match self { Self::Value(value) => value.clone(), - Self::Func(func) => func.call(engine, [x, y])?.cast().at(func.span())?, + Self::Func(func) => func + .call(engine, &Context::new(None, Some(styles)), [x, y])? + .cast() + .at(func.span())?, Self::Array(array) => x .checked_rem(array.len()) .and_then(|i| array.get(i)) @@ -141,7 +150,7 @@ where Ok(match &self.0 { Celled::Value(value) => value.clone(), Celled::Func(func) => func - .call(engine, [x, y])? + .call(engine, &Context::new(None, Some(styles)), [x, y])? .cast::<T>() .at(func.span())? .resolve(styles), @@ -484,9 +493,9 @@ impl CellGrid { let cell = cell.resolve_cell( x, y, - &fill.resolve(engine, x, y)?, - align.resolve(engine, x, y)?, - inset.resolve(engine, x, y)?, + &fill.resolve(engine, styles, x, y)?, + align.resolve(engine, styles, x, y)?, + inset.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?, styles, ); @@ -570,9 +579,9 @@ impl CellGrid { let new_cell = T::default().resolve_cell( x, y, - &fill.resolve(engine, x, y)?, - align.resolve(engine, x, y)?, - inset.resolve(engine, x, y)?, + &fill.resolve(engine, styles, x, y)?, + align.resolve(engine, styles, x, y)?, + inset.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?, styles, ); diff --git a/crates/typst/src/layout/layout.rs b/crates/typst/src/layout/layout.rs index aa1ca63a..360f0254 100644 --- a/crates/typst/src/layout/layout.rs +++ b/crates/typst/src/layout/layout.rs @@ -1,8 +1,9 @@ use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - dict, elem, func, Content, Func, NativeElement, Packed, StyleChain, + dict, elem, func, Content, Context, Func, NativeElement, Packed, StyleChain, }; +use crate::introspection::Locatable; use crate::layout::{Fragment, LayoutMultiple, Regions, Size}; use crate::syntax::Span; @@ -14,15 +15,14 @@ use crate::syntax::Span; /// /// ```example /// #let text = lorem(30) -/// #layout(size => style(styles => [ +/// #layout(size => [ /// #let (height,) = measure( /// block(width: size.width, text), -/// styles, /// ) /// This text is #height high with /// the current page width: \ /// #text -/// ])) +/// ]) /// ``` /// /// If the `layout` call is placed inside of a box width a width of `{800pt}` @@ -63,7 +63,7 @@ pub fn layout( } /// Executes a `layout` call. -#[elem(LayoutMultiple)] +#[elem(Locatable, LayoutMultiple)] struct LayoutElem { /// The function to call with the outer container's (or page's) size. #[required] @@ -81,9 +81,11 @@ impl LayoutMultiple for Packed<LayoutElem> { // Gets the current region's base size, which will be the size of the // outer container, or of the page if there is no such container. let Size { x, y } = regions.base(); + let loc = self.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); let result = self .func() - .call(engine, [dict! { "width" => x, "height" => y }])? + .call(engine, &context, [dict! { "width" => x, "height" => y }])? .display(); result.layout(engine, styles, regions) } diff --git a/crates/typst/src/layout/length.rs b/crates/typst/src/layout/length.rs index 7365ca19..ff1db8f0 100644 --- a/crates/typst/src/layout/length.rs +++ b/crates/typst/src/layout/length.rs @@ -4,8 +4,8 @@ use std::ops::{Add, Div, Mul, Neg}; use ecow::{eco_format, EcoString}; -use crate::diag::{At, Hint, SourceResult}; -use crate::foundations::{func, scope, ty, Fold, Repr, Resolve, StyleChain, Styles}; +use crate::diag::{At, Hint, HintedStrResult, SourceResult}; +use crate::foundations::{func, scope, ty, Context, Fold, Repr, Resolve, StyleChain}; use crate::layout::{Abs, Em}; use crate::syntax::Span; use crate::util::Numeric; @@ -137,32 +137,22 @@ impl Length { /// /// ```example /// #set text(size: 12pt) - /// #style(styles => [ - /// #(6pt).to-absolute(styles) \ - /// #(6pt + 10em).to-absolute(styles) \ - /// #(10em).to-absolute(styles) - /// ]) + /// #context [ + /// #(6pt).to-absolute() \ + /// #(6pt + 10em).to-absolute() \ + /// #(10em).to-absolute() + /// ] /// /// #set text(size: 6pt) - /// #style(styles => [ - /// #(6pt).to-absolute(styles) \ - /// #(6pt + 10em).to-absolute(styles) \ - /// #(10em).to-absolute(styles) - /// ]) + /// #context [ + /// #(6pt).to-absolute() \ + /// #(6pt + 10em).to-absolute() \ + /// #(10em).to-absolute() + /// ] /// ``` #[func] - pub fn to_absolute( - &self, - /// The styles to resolve the length with. - /// - /// Since a length can use font-relative em units, resolving it to an - /// absolute length requires knowledge of the font size. This size is - /// provided through these styles. You can obtain the styles using - /// the [`style`]($style) function. - styles: Styles, - ) -> Length { - let styles = StyleChain::new(&styles); - self.resolve(styles).into() + pub fn to_absolute(&self, context: &Context) -> HintedStrResult<Length> { + Ok(self.resolve(context.styles()?).into()) } } diff --git a/crates/typst/src/layout/measure.rs b/crates/typst/src/layout/measure.rs index d5635e87..52ceaae0 100644 --- a/crates/typst/src/layout/measure.rs +++ b/crates/typst/src/layout/measure.rs @@ -1,7 +1,8 @@ -use crate::diag::SourceResult; +use crate::diag::{At, SourceResult}; use crate::engine::Engine; -use crate::foundations::{dict, func, Content, Dict, StyleChain, Styles}; +use crate::foundations::{dict, func, Content, Context, Dict, StyleChain, Styles}; use crate::layout::{Abs, Axes, LayoutMultiple, Regions, Size}; +use crate::syntax::Span; /// Measures the layouted size of content. /// @@ -28,10 +29,10 @@ use crate::layout::{Abs, Axes, LayoutMultiple, Regions, Size}; /// the `measure` function. /// /// ```example -/// #let thing(body) = style(styles => { -/// let size = measure(body, styles) +/// #let thing(body) = context { +/// let size = measure(body) /// [Width of "#body" is #size.width] -/// }) +/// } /// /// #thing[Hey] \ /// #thing[Welcome] @@ -39,17 +40,26 @@ use crate::layout::{Abs, Axes, LayoutMultiple, Regions, Size}; /// /// The measure function returns a dictionary with the entries `width` and /// `height`, both of type [`length`]($length). -#[func] +#[func(contextual)] pub fn measure( /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, + /// The callsite span. + span: Span, /// The content whose size to measure. content: Content, /// The styles with which to layout the content. - styles: Styles, + #[default] + styles: Option<Styles>, ) -> SourceResult<Dict> { + let styles = match &styles { + Some(styles) => StyleChain::new(styles), + None => context.styles().at(span)?, + }; + let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false)); - let styles = StyleChain::new(&styles); let frame = content.measure(engine, styles, pod)?.into_frame(); let Size { x, y } = frame.size(); Ok(dict! { "width" => x, "height" => y }) diff --git a/crates/typst/src/layout/page.rs b/crates/typst/src/layout/page.rs index 081415c0..04e7dd60 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -6,10 +6,10 @@ use std::str::FromStr; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, AutoValue, Cast, Content, Dict, Fold, Func, NativeElement, Packed, - Resolve, Smart, StyleChain, Value, + cast, elem, AutoValue, Cast, Content, Context, Dict, Fold, Func, NativeElement, + Packed, Resolve, Smart, StyleChain, Value, }; -use crate::introspection::{Counter, CounterKey, ManualPageCounter}; +use crate::introspection::{Counter, CounterDisplayElem, CounterKey, ManualPageCounter}; use crate::layout::{ Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, LayoutMultiple, Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment, @@ -258,7 +258,7 @@ pub struct PageElem { /// #set page( /// height: 100pt, /// margin: 20pt, - /// footer: [ + /// footer: context [ /// #set align(right) /// #set text(8pt) /// #counter(page).display( @@ -416,11 +416,13 @@ impl Packed<PageElem> { Numbering::Func(_) => true, }; - let mut counter = Counter::new(CounterKey::Page).display( - self.span(), - Some(numbering.clone()), + let mut counter = CounterDisplayElem::new( + Counter::new(CounterKey::Page), + Smart::Custom(numbering.clone()), both, - ); + ) + .pack() + .spanned(self.span()); // We interpret the Y alignment as selecting header or footer // and then ignore it for aligning the actual number. @@ -512,7 +514,7 @@ impl Packed<PageElem> { } /// A finished page. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct Page { /// The frame that defines the page. pub frame: Frame, @@ -524,7 +526,7 @@ pub struct Page { } /// Specification of the page's margins. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Margin { /// The margins for each side. pub sides: Sides<Option<Smart<Rel<Length>>>>, @@ -540,6 +542,15 @@ impl Margin { } } +impl Default for Margin { + fn default() -> Self { + Self { + sides: Sides::splat(Some(Smart::Auto)), + two_sided: None, + } + } +} + impl Fold for Margin { fn fold(self, outer: Self) -> Self { Margin { @@ -552,22 +563,28 @@ impl Fold for Margin { cast! { Margin, self => { + let two_sided = self.two_sided.unwrap_or(false); + if !two_sided && self.sides.is_uniform() { + if let Some(left) = self.sides.left { + return left.into_value(); + } + } + let mut dict = Dict::new(); - let mut handle = |key: &str, component: Value| { - let value = component.into_value(); - if value != Value::None { - dict.insert(key.into(), value); + let mut handle = |key: &str, component: Option<Smart<Rel<Length>>>| { + if let Some(c) = component { + dict.insert(key.into(), c.into_value()); } }; - handle("top", self.sides.top.into_value()); - handle("bottom", self.sides.bottom.into_value()); - if self.two_sided.unwrap_or(false) { - handle("inside", self.sides.left.into_value()); - handle("outside", self.sides.right.into_value()); + handle("top", self.sides.top); + handle("bottom", self.sides.bottom); + if two_sided { + handle("inside", self.sides.left); + handle("outside", self.sides.right); } else { - handle("left", self.sides.left.into_value()); - handle("right", self.sides.right.into_value()); + handle("left", self.sides.left); + handle("right", self.sides.right); } Value::Dict(dict) @@ -668,11 +685,15 @@ impl Marginal { pub fn resolve( &self, engine: &mut Engine, + styles: StyleChain, page: usize, ) -> SourceResult<Cow<'_, Content>> { Ok(match self { Self::Content(content) => Cow::Borrowed(content), - Self::Func(func) => Cow::Owned(func.call(engine, [page])?.display()), + Self::Func(func) => Cow::Owned( + func.call(engine, &Context::new(None, Some(styles)), [page])? + .display(), + ), }) } } diff --git a/crates/typst/src/math/cancel.rs b/crates/typst/src/math/cancel.rs index 7287696b..36f56160 100644 --- a/crates/typst/src/math/cancel.rs +++ b/crates/typst/src/math/cancel.rs @@ -1,5 +1,7 @@ use crate::diag::{At, SourceResult}; -use crate::foundations::{cast, elem, Content, Func, Packed, Resolve, Smart, StyleChain}; +use crate::foundations::{ + cast, elem, Content, Context, Func, Packed, Resolve, Smart, StyleChain, +}; use crate::layout::{ Abs, Angle, Frame, FrameItem, Length, Point, Ratio, Rel, Size, Transform, }; @@ -135,6 +137,7 @@ impl LayoutMath for Packed<CancelElem> { invert_first_line, &angle, body_size, + styles, span, )?; @@ -144,8 +147,9 @@ impl LayoutMath for Packed<CancelElem> { if cross { // Draw the second line. - let second_line = - draw_cancel_line(ctx, length, stroke, true, &angle, body_size, span)?; + let second_line = draw_cancel_line( + ctx, length, stroke, true, &angle, body_size, styles, span, + )?; body.push_frame(center, second_line); } @@ -180,6 +184,7 @@ cast! { } /// Draws a cancel line. +#[allow(clippy::too_many_arguments)] fn draw_cancel_line( ctx: &mut MathContext, length_scale: Rel<Abs>, @@ -187,6 +192,7 @@ fn draw_cancel_line( invert: bool, angle: &Smart<CancelAngle>, body_size: Size, + styles: StyleChain, span: Span, ) -> SourceResult<Frame> { let default = default_angle(body_size); @@ -197,9 +203,10 @@ fn draw_cancel_line( // This specifies the absolute angle w.r.t y-axis clockwise. CancelAngle::Angle(v) => *v, // This specifies a function that takes the default angle as input. - CancelAngle::Func(func) => { - func.call(ctx.engine, [default])?.cast().at(span)? - } + CancelAngle::Func(func) => func + .call(ctx.engine, &Context::new(None, Some(styles)), [default])? + .cast() + .at(span)?, }, }; diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs index ff34f40e..77eac78c 100644 --- a/crates/typst/src/math/equation.rs +++ b/crates/typst/src/math/equation.rs @@ -161,7 +161,7 @@ impl Synthesize for Packed<EquationElem> { Smart::Auto => TextElem::packed(Self::local_name_in(styles)), Smart::Custom(None) => Content::empty(), Smart::Custom(Some(supplement)) => { - supplement.resolve(engine, [self.clone().pack()])? + supplement.resolve(engine, styles, [self.clone().pack()])? } }; @@ -265,8 +265,7 @@ impl LayoutSingle for Packed<EquationElem> { let pod = Regions::one(regions.base(), Axes::splat(false)); let number = Counter::of(EquationElem::elem()) - .at(engine, self.location().unwrap())? - .display(engine, numbering)? + .display_at_loc(engine, self.location().unwrap(), styles, numbering)? .spanned(span) .layout(engine, styles, pod)? .into_frame(); @@ -357,7 +356,11 @@ impl Refable for Packed<EquationElem> { } impl Outlinable for Packed<EquationElem> { - fn outline(&self, engine: &mut Engine) -> SourceResult<Option<Content>> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { if !self.block(StyleChain::default()) { return Ok(None); } @@ -375,10 +378,12 @@ impl Outlinable for Packed<EquationElem> { supplement += TextElem::packed("\u{a0}"); } - let numbers = self - .counter() - .at(engine, self.location().unwrap())? - .display(engine, numbering)?; + let numbers = self.counter().display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; Ok(Some(supplement + numbers)) } diff --git a/crates/typst/src/model/enum.rs b/crates/typst/src/model/enum.rs index 4233cf0e..ba0b697a 100644 --- a/crates/typst/src/model/enum.rs +++ b/crates/typst/src/model/enum.rs @@ -4,7 +4,9 @@ use smallvec::{smallvec, SmallVec}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; -use crate::foundations::{cast, elem, scope, Array, Content, Packed, Smart, StyleChain}; +use crate::foundations::{ + cast, elem, scope, Array, Content, Context, Packed, Smart, StyleChain, +}; use crate::layout::{ Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment, @@ -242,9 +244,10 @@ impl LayoutMultiple for Packed<EnumElem> { for item in self.children() { number = item.number(styles).unwrap_or(number); + let context = Context::new(None, Some(styles)); let resolved = if full { parents.push(number); - let content = numbering.apply(engine, &parents)?.display(); + let content = numbering.apply(engine, &context, &parents)?.display(); parents.pop(); content } else { @@ -252,7 +255,7 @@ impl LayoutMultiple for Packed<EnumElem> { Numbering::Pattern(pattern) => { TextElem::packed(pattern.apply_kth(parents.len(), number)) } - other => other.apply(engine, &[number])?.display(), + other => other.apply(engine, &context, &[number])?.display(), } }; diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index 950934d9..c43c2ff9 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -274,7 +274,7 @@ impl Synthesize for Packed<FigureElem> { }; let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body())); - Some(supplement.resolve(engine, [target])?) + Some(supplement.resolve(engine, styles, [target])?) } }; @@ -377,7 +377,11 @@ impl Refable for Packed<FigureElem> { } impl Outlinable for Packed<FigureElem> { - fn outline(&self, engine: &mut Engine) -> SourceResult<Option<Content>> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { if !self.outlined(StyleChain::default()) { return Ok(None); } @@ -396,9 +400,12 @@ impl Outlinable for Packed<FigureElem> { (**self).counter(), self.numbering(), ) { - let numbers = counter - .at(engine, self.location().unwrap())? - .display(engine, numbering)?; + let numbers = counter.display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; if !supplement.is_empty() { supplement += TextElem::packed('\u{a0}'); @@ -483,7 +490,8 @@ pub struct FigureCaption { /// ```example /// #show figure.caption: it => [ /// #underline(it.body) | - /// #it.supplement #it.counter.display(it.numbering) + /// #it.supplement + /// #context it.counter.display(it.numbering) /// ] /// /// #figure( @@ -554,7 +562,7 @@ impl Show for Packed<FigureCaption> { self.counter(), self.figure_location(), ) { - let numbers = counter.at(engine, *location)?.display(engine, numbering)?; + let numbers = counter.display_at_loc(engine, *location, styles, numbering)?; if !supplement.is_empty() { supplement += TextElem::packed('\u{a0}'); } diff --git a/crates/typst/src/model/footnote.rs b/crates/typst/src/model/footnote.rs index 383389ed..9350e853 100644 --- a/crates/typst/src/model/footnote.rs +++ b/crates/typst/src/model/footnote.rs @@ -126,11 +126,12 @@ impl Packed<FootnoteElem> { impl Show for Packed<FootnoteElem> { #[typst_macros::time(name = "footnote", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - let loc = self.declaration_location(engine).at(self.span())?; + let span = self.span(); + let loc = self.declaration_location(engine).at(span)?; let numbering = self.numbering(styles); let counter = Counter::of(FootnoteElem::elem()); - let num = counter.at(engine, loc)?.display(engine, numbering)?; - let sup = SuperElem::new(num).pack().spanned(self.span()); + let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let sup = SuperElem::new(num).pack().spanned(span); let loc = loc.variant(1); // Add zero-width weak spacing to make the footnote "sticky". Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc))) @@ -266,6 +267,7 @@ pub struct FootnoteEntry { impl Show for Packed<FootnoteEntry> { #[typst_macros::time(name = "footnote.entry", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let span = self.span(); let note = self.note(); let number_gap = Em::new(0.05); let default = StyleChain::default(); @@ -273,15 +275,15 @@ impl Show for Packed<FootnoteEntry> { let counter = Counter::of(FootnoteElem::elem()); let Some(loc) = note.location() else { bail!( - self.span(), "footnote entry must have a location"; + span, "footnote entry must have a location"; hint: "try using a query or a show rule to customize the footnote instead" ); }; - let num = counter.at(engine, loc)?.display(engine, numbering)?; + let num = counter.display_at_loc(engine, loc, styles, numbering)?; let sup = SuperElem::new(num) .pack() - .spanned(self.span()) + .spanned(span) .linked(Destination::Location(loc)) .backlinked(loc.variant(1)); Ok(Content::sequence([ diff --git a/crates/typst/src/model/heading.rs b/crates/typst/src/model/heading.rs index 02f40a15..b958438b 100644 --- a/crates/typst/src/model/heading.rs +++ b/crates/typst/src/model/heading.rs @@ -24,7 +24,7 @@ use crate::util::{option_eq, NonZeroExt}; /// specify how you want your headings to be numbered with a /// [numbering pattern or function]($numbering). /// -/// Independently from the numbering, Typst can also automatically generate an +/// Independently of the numbering, Typst can also automatically generate an /// [outline]($outline) of all headings for you. To exclude one or more headings /// from this outline, you can set the `outlined` parameter to `{false}`. /// @@ -136,7 +136,7 @@ impl Synthesize for Packed<HeadingElem> { Smart::Auto => TextElem::packed(Self::local_name_in(styles)), Smart::Custom(None) => Content::empty(), Smart::Custom(Some(supplement)) => { - supplement.resolve(engine, [self.clone().pack()])? + supplement.resolve(engine, styles, [self.clone().pack()])? } }; @@ -148,16 +148,16 @@ impl Synthesize for Packed<HeadingElem> { impl Show for Packed<HeadingElem> { #[typst_macros::time(name = "heading", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let span = self.span(); let mut realized = self.body().clone(); if let Some(numbering) = (**self).numbering(styles).as_ref() { realized = Counter::of(HeadingElem::elem()) - .at(engine, self.location().unwrap())? - .display(engine, numbering)? - .spanned(self.span()) + .display_at_loc(engine, self.location().unwrap(), styles, numbering)? + .spanned(span) + HElem::new(Em::new(0.3).into()).with_weak(true).pack() + realized; } - Ok(BlockElem::new().with_body(Some(realized)).pack().spanned(self.span())) + Ok(BlockElem::new().with_body(Some(realized)).pack().spanned(span)) } } @@ -212,16 +212,23 @@ impl Refable for Packed<HeadingElem> { } impl Outlinable for Packed<HeadingElem> { - fn outline(&self, engine: &mut Engine) -> SourceResult<Option<Content>> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { if !self.outlined(StyleChain::default()) { return Ok(None); } let mut content = self.body().clone(); if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { - let numbers = Counter::of(HeadingElem::elem()) - .at(engine, self.location().unwrap())? - .display(engine, numbering)?; + let numbers = Counter::of(HeadingElem::elem()).display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; content = numbers + SpaceElem::new().pack() + content; }; diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs index 12a698d1..6d53b6d7 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst/src/model/list.rs @@ -1,7 +1,8 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, Depth, Func, Packed, Smart, StyleChain, Value, + cast, elem, scope, Array, Content, Context, Depth, Func, Packed, Smart, StyleChain, + Value, }; use crate::layout::{ Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, @@ -154,7 +155,7 @@ impl LayoutMultiple for Packed<ListElem> { let Depth(depth) = ListElem::depth_in(styles); let marker = self .marker(styles) - .resolve(engine, depth)? + .resolve(engine, styles, depth)? // avoid '#set align' interference with the list .aligned(HAlignment::Start + VAlignment::Top); @@ -206,12 +207,19 @@ pub enum ListMarker { impl ListMarker { /// Resolve the marker for the given depth. - fn resolve(&self, engine: &mut Engine, depth: usize) -> SourceResult<Content> { + fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + depth: usize, + ) -> SourceResult<Content> { Ok(match self { Self::Content(list) => { list.get(depth % list.len()).cloned().unwrap_or_default() } - Self::Func(func) => func.call(engine, [depth])?.display(), + Self::Func(func) => func + .call(engine, &Context::new(None, Some(styles)), [depth])? + .display(), }) } } diff --git a/crates/typst/src/model/numbering.rs b/crates/typst/src/model/numbering.rs index 4f1cc1d3..9de09046 100644 --- a/crates/typst/src/model/numbering.rs +++ b/crates/typst/src/model/numbering.rs @@ -5,7 +5,7 @@ use ecow::{eco_format, EcoString, EcoVec}; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{cast, func, Func, Str, Value}; +use crate::foundations::{cast, func, Context, Func, Str, Value}; use crate::text::Case; /// Applies a numbering to a sequence of numbers. @@ -35,6 +35,8 @@ use crate::text::Case; pub fn numbering( /// The engine. engine: &mut Engine, + /// The callsite context. + context: &Context, /// Defines how the numbering works. /// /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `一`, `壹`, `あ`, `い`, `ア`, `イ`, `א`, `가`, @@ -66,7 +68,7 @@ pub fn numbering( #[variadic] numbers: Vec<usize>, ) -> SourceResult<Value> { - numbering.apply(engine, &numbers) + numbering.apply(engine, context, &numbers) } /// How to number a sequence of things. @@ -80,10 +82,15 @@ pub enum Numbering { impl Numbering { /// Apply the pattern to the given numbers. - pub fn apply(&self, engine: &mut Engine, numbers: &[usize]) -> SourceResult<Value> { + pub fn apply( + &self, + engine: &mut Engine, + context: &Context, + numbers: &[usize], + ) -> SourceResult<Value> { Ok(match self { Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), - Self::Func(func) => func.call(engine, numbers.iter().copied())?, + Self::Func(func) => func.call(engine, context, numbers.iter().copied())?, }) } diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs index a50bd937..355c9c09 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -4,8 +4,8 @@ use std::str::FromStr; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, select_where, Content, Func, LocatableSelector, NativeElement, - Packed, Show, ShowSet, Smart, StyleChain, Styles, + cast, elem, scope, select_where, Content, Context, Func, LocatableSelector, + NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, }; use crate::introspection::{Counter, CounterKey, Locatable}; use crate::layout::{BoxElem, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing}; @@ -215,6 +215,7 @@ impl Show for Packed<OutlineElem> { self.span(), elem.clone(), self.fill(styles), + styles, )? else { continue; @@ -235,7 +236,14 @@ impl Show for Packed<OutlineElem> { ancestors.pop(); } - OutlineIndent::apply(indent, engine, &ancestors, &mut seq, self.span())?; + OutlineIndent::apply( + indent, + engine, + &ancestors, + &mut seq, + styles, + self.span(), + )?; // Add the overridable outline entry, followed by a line break. seq.push(entry.pack()); @@ -302,7 +310,12 @@ impl LocalName for Packed<OutlineElem> { /// `#outline()` element. pub trait Outlinable: Refable { /// Produce an outline item for this element. - fn outline(&self, engine: &mut Engine) -> SourceResult<Option<Content>>; + fn outline( + &self, + engine: &mut Engine, + + styles: StyleChain, + ) -> SourceResult<Option<Content>>; /// Returns the nesting level of this element. fn level(&self) -> NonZeroUsize { @@ -324,6 +337,7 @@ impl OutlineIndent { engine: &mut Engine, ancestors: &Vec<&Content>, seq: &mut Vec<Content>, + styles: StyleChain, span: Span, ) -> SourceResult<()> { match indent { @@ -338,10 +352,12 @@ impl OutlineIndent { let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap(); if let Some(numbering) = ancestor_outlinable.numbering() { - let numbers = ancestor_outlinable - .counter() - .at(engine, ancestor.location().unwrap())? - .display(engine, numbering)?; + let numbers = ancestor_outlinable.counter().display_at_loc( + engine, + ancestor.location().unwrap(), + styles, + numbering, + )?; hidden += numbers + SpaceElem::new().pack(); }; @@ -364,8 +380,10 @@ impl OutlineIndent { // the returned content Some(Smart::Custom(OutlineIndent::Func(func))) => { let depth = ancestors.len(); - let LengthOrContent(content) = - func.call(engine, [depth])?.cast().at(span)?; + let LengthOrContent(content) = func + .call(engine, &Context::new(None, Some(styles)), [depth])? + .cast() + .at(span)?; if !content.is_empty() { seq.push(content); } @@ -469,12 +487,13 @@ impl OutlineEntry { span: Span, elem: Content, fill: Option<Content>, + styles: StyleChain, ) -> SourceResult<Option<Self>> { let Some(outlinable) = elem.with::<dyn Outlinable>() else { bail!(span, "cannot outline {}", elem.func().name()); }; - let Some(body) = outlinable.outline(engine)? else { + let Some(body) = outlinable.outline(engine, styles)? else { return Ok(None); }; @@ -485,9 +504,12 @@ impl OutlineEntry { .cloned() .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); - let page = Counter::new(CounterKey::Page) - .at(engine, location)? - .display(engine, &page_numbering)?; + let page = Counter::new(CounterKey::Page).display_at_loc( + engine, + location, + styles, + &page_numbering, + )?; Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) } diff --git a/crates/typst/src/model/reference.rs b/crates/typst/src/model/reference.rs index 085313f1..0d998054 100644 --- a/crates/typst/src/model/reference.rs +++ b/crates/typst/src/model/reference.rs @@ -3,8 +3,8 @@ use ecow::eco_format; use crate::diag::{bail, At, Hint, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Func, IntoValue, Label, NativeElement, Packed, Show, Smart, - StyleChain, Synthesize, + cast, elem, Content, Context, Func, IntoValue, Label, NativeElement, Packed, Show, + Smart, StyleChain, Synthesize, }; use crate::introspection::{Counter, Locatable}; use crate::math::EquationElem; @@ -213,15 +213,19 @@ impl Show for Packed<RefElem> { .at(span)?; let loc = elem.location().unwrap(); - let numbers = refable - .counter() - .at(engine, loc)? - .display(engine, &numbering.clone().trimmed())?; + let numbers = refable.counter().display_at_loc( + engine, + loc, + styles, + &numbering.clone().trimmed(), + )?; let supplement = match self.supplement(styles).as_ref() { Smart::Auto => refable.supplement(), Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => supplement.resolve(engine, [elem])?, + Smart::Custom(Some(supplement)) => { + supplement.resolve(engine, styles, [elem])? + } }; let mut content = numbers; @@ -267,11 +271,14 @@ impl Supplement { pub fn resolve<T: IntoValue>( &self, engine: &mut Engine, + styles: StyleChain, args: impl IntoIterator<Item = T>, ) -> SourceResult<Content> { Ok(match self { Supplement::Content(content) => content.clone(), - Supplement::Func(func) => func.call(engine, args)?.display(), + Supplement::Func(func) => { + func.call(engine, &Context::new(None, Some(styles)), args)?.display() + } }) } } diff --git a/crates/typst/src/realize/process.rs b/crates/typst/src/realize/process.rs index a5acd8fc..cb11686b 100644 --- a/crates/typst/src/realize/process.rs +++ b/crates/typst/src/realize/process.rs @@ -5,7 +5,7 @@ use smallvec::smallvec; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - Content, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style, + Content, Context, Packed, Recipe, RecipeIndex, Regex, Selector, Show, ShowSet, Style, StyleChain, Styles, Synthesize, Transformation, }; use crate::introspection::{Locatable, Meta, MetaElem}; @@ -248,17 +248,20 @@ fn show( ) -> SourceResult<Content> { match step { // Apply a user-defined show rule. - ShowStep::Recipe(recipe, guard) => match &recipe.selector { - // If the selector is a regex, the `target` is guaranteed to be a - // text element. This invokes special regex handling. - Some(Selector::Regex(regex)) => { - let text = target.into_packed::<TextElem>().unwrap(); - show_regex(engine, &text, regex, recipe, guard) - } + ShowStep::Recipe(recipe, guard) => { + let context = Context::new(target.location(), Some(styles)); + match &recipe.selector { + // If the selector is a regex, the `target` is guaranteed to be a + // text element. This invokes special regex handling. + Some(Selector::Regex(regex)) => { + let text = target.into_packed::<TextElem>().unwrap(); + show_regex(engine, &text, regex, recipe, guard, &context) + } - // Just apply the recipe. - _ => recipe.apply(engine, target.guarded(guard)), - }, + // Just apply the recipe. + _ => recipe.apply(engine, &context, target.guarded(guard)), + } + } // If the verdict picks this step, the `target` is guaranteed to have a // built-in show rule. @@ -269,13 +272,14 @@ fn show( /// Apply a regex show rule recipe to a target. fn show_regex( engine: &mut Engine, - elem: &Packed<TextElem>, + target: &Packed<TextElem>, regex: &Regex, recipe: &Recipe, index: RecipeIndex, + context: &Context, ) -> SourceResult<Content> { let make = |s: &str| { - let mut fresh = elem.clone(); + let mut fresh = target.clone(); fresh.push_text(s.into()); fresh.pack() }; @@ -283,16 +287,16 @@ fn show_regex( let mut result = vec![]; let mut cursor = 0; - let text = elem.text(); + let text = target.text(); - for m in regex.find_iter(elem.text()) { + for m in regex.find_iter(target.text()) { let start = m.start(); if cursor < start { result.push(make(&text[cursor..start])); } let piece = make(m.as_str()); - let transformed = recipe.apply(engine, piece)?; + let transformed = recipe.apply(engine, context, piece)?; result.push(transformed); cursor = m.end(); } |
