diff --git a/Cargo.lock b/Cargo.lock index a1193fe01..9684bb350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2575,6 +2575,7 @@ dependencies = [ "icu_provider_adapters", "icu_provider_blob", "icu_segmenter", + "if_chain", "image", "indexmap 2.1.0", "kamadak-exif", diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs index 315a2ed58..0a69523e0 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 20a4f6e4a..f4d803c37 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 { "$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 937428258..1564ef2f2 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 e78f0c121..1fef1b49c 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 { - match node.cast::() { - 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)> { + let Some(expr) = node.cast::() 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 { 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 { - 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 { 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 b58a9bcc1..946795521 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::(); 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::(); - 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, +) { 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 { @@ -862,6 +894,12 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) { "Transforms everything that follows.", ); + ctx.snippet_completion( + "context expression", + "context ${}", + "Provides contextual data.", + ); + ctx.snippet_completion( "let binding", "let ${name} = ${value}", diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 67614b9e7..2f04be87a 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 { 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 { let mut last = None; let mut pieces: Vec = 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 { } // 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 81548dd77..f14d33507 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 953df4282..728ab05b6 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, parent: Option, + contextual: bool, docs: String, vis: syn::Visibility, ident: Ident, @@ -37,6 +38,7 @@ struct Func { struct SpecialParams { self_: Option, 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, pub title: Option, pub constructor: bool, @@ -78,6 +81,7 @@ impl Parse for Meta { fn parse(input: ParseStream) -> Result { Ok(Self { scope: parse_flag::(input)?, + contextual: parse_flag::(input)?, name: parse_string::(input)?, title: parse_string::(input)?, constructor: parse_flag::(input)?, @@ -117,6 +121,7 @@ fn parse(stream: TokenStream, item: &syn::ItemFn) -> Result { 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 5d340473d..f92230eff 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 89880db17..bfe222855 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 6dd9b5f6f..df9cef0cc 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(_) @@ -1946,6 +1951,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 99fbf4fe7..19d35d0ac 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 { 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 { 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 536c9381b..e5dd4e9bb 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 f0cf8846d..cd1998a6b 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -616,6 +616,7 @@ fn keyword(ident: &str) -> Option { "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 32e15cb77..567bcbd14 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 88a9b18b8..906d5fac5 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 8c3ef084d..01f6e7c68 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 3a0f4f9be..c70dd761b 100644 --- a/crates/typst/src/diag.rs +++ b/crates/typst/src/diag.rs @@ -304,9 +304,12 @@ pub struct HintedString { pub hints: Vec, } -impl From for HintedString { - fn from(value: EcoString) -> Self { - Self { message: value, hints: vec![] } +impl From for HintedString +where + S: Into, +{ + 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 6b6dff157..ab0a64912 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 3f7224d80..97ca6c2ce 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::().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, locator: Tracked, tracer: TrackedMut, + context: &Context, mut args: Args, ) -> SourceResult { - let node = closure.node.cast::().unwrap(); + let (name, params, body) = match closure.node.cast::() { + 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 e93086255..30f5fb720 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 { 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 { + 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 aefb34367..d7d400e71 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 2a17cf348..07d270216 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 53494895e..833960417 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, warnings_set: HashSet, delayed: EcoVec, - values: EcoVec, + values: EcoVec<(Value, Option)>, } impl Tracer { @@ -43,7 +43,7 @@ impl Tracer { } /// Get the values for the inspected span. - pub fn values(self) -> EcoVec { + pub fn values(self) -> EcoVec<(Value, Option)> { 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) { 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 29deaf09a..27d141cc8 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, + /// 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 b001aa20f..1df24bd8f 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> { for item in self.iter() { if searcher - .call(engine, [item.clone()])? + .call(engine, context, [item.clone()])? .cast::() .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> { for (i, item) in self.iter().enumerate() { if searcher - .call(engine, [item.clone()])? + .call(engine, context, [item.clone()])? .cast::() .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 { let mut kept = EcoVec::new(); for item in self.iter() { - if test.call(engine, [item.clone()])?.cast::().at(test.span())? { + if test + .call(engine, context, [item.clone()])? + .cast::() + .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 { - 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 { 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 { for item in self { - if test.call(engine, [item])?.cast::().at(test.span())? { + if test.call(engine, context, [item])?.cast::().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 { for item in self { - if !test.call(engine, [item])?.cast::().at(test.span())? { + if !test.call(engine, context, [item])?.cast::().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 8e75dc71c..3b74f1fb1 100644 --- a/crates/typst/src/foundations/cast.rs +++ b/crates/typst/src/foundations/cast.rs @@ -111,6 +111,20 @@ impl Reflect for StrResult { } } +impl Reflect for HintedStrResult { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + impl Reflect for SourceResult { fn input() -> CastInfo { T::input() diff --git a/crates/typst/src/foundations/content.rs b/crates/typst/src/foundations/content.rs index 6d937dfba..54789be08 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 { 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 000000000..41fdbfecb --- /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, + /// The active styles. + pub styles: Option>, +} + +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, styles: Option>) -> Self { + Self { location, styles } + } + + /// Try to extract the location. + pub fn location(&self) -> HintedStrResult { + require(self.location) + } + + /// Try to extract the styles. + pub fn styles(&self) -> HintedStrResult> { + 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(val: Option) -> HintedStrResult { + 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 { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed { + #[typst_macros::time(name = "context", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + 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 324eceefd..b543e76f5 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 { - 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 { + 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 { + (self.0.field_from_styles)(id, styles) + } + /// The element's local name, if any. pub fn local_name(&self, lang: Lang, region: Option) -> 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; + /// Get the field with the given ID from the styles. + fn field_from_styles(id: u8, styles: StyleChain) -> Option + 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, pub field_name: fn(u8) -> Option<&'static str>, + pub field_from_styles: fn(u8, StyleChain) -> Option, pub local_name: Option) -> &'static str>, pub scope: Lazy, pub params: Lazy>, diff --git a/crates/typst/src/foundations/func.rs b/crates/typst/src/foundations/func.rs index 6062da178..bb8399942 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 { + 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 { - self.call_impl(engine, args.into_args(self.span)) + /// Call the function with the given context and arguments. + pub fn call( + &self, + engine: &mut Engine, + context: &Context, + args: A, + ) -> SourceResult { + 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 { + fn call_impl( + &self, + engine: &mut Engine, + context: &Context, + mut args: Args, + ) -> SourceResult { 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, + pub function: fn(&mut Engine, &Context, &mut Args) -> SourceResult, pub name: &'static str, pub title: &'static str, pub docs: &'static str, pub keywords: &'static [&'static str], + pub contextual: bool, pub scope: Lazy, pub params: Lazy>, pub returns: Lazy, @@ -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, /// 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::() - .unwrap() - .name() - .map(|ident| ident.as_str()) + self.node.cast::()?.name().map(|ident| ident.as_str()) } } diff --git a/crates/typst/src/foundations/mod.rs b/crates/typst/src/foundations/mod.rs index f9e15bece..eb9f6d661 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 0567b5b0e..caa82e136 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, value: impl IntoValue) { + pub fn define_captured( + &mut self, + var: impl Into, + 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 ee7691b04..d97c3408a 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, + context: &Context, + ) -> HintedStrResult { + 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::