summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--crates/typst-docs/src/lib.rs6
-rw-r--r--crates/typst-docs/src/link.rs1
-rw-r--r--crates/typst-docs/src/model.rs1
-rw-r--r--crates/typst-ide/src/analyze.rs52
-rw-r--r--crates/typst-ide/src/complete.rs50
-rw-r--r--crates/typst-ide/src/tooltip.rs8
-rw-r--r--crates/typst-macros/src/elem.rs23
-rw-r--r--crates/typst-macros/src/func.rs15
-rw-r--r--crates/typst-macros/src/lib.rs2
-rw-r--r--crates/typst-macros/src/util.rs1
-rw-r--r--crates/typst-syntax/src/ast.rs17
-rw-r--r--crates/typst-syntax/src/highlight.rs2
-rw-r--r--crates/typst-syntax/src/kind.rs7
-rw-r--r--crates/typst-syntax/src/lexer.rs1
-rw-r--r--crates/typst-syntax/src/parser.rs9
-rw-r--r--crates/typst-syntax/src/set.rs1
-rw-r--r--crates/typst/Cargo.toml1
-rw-r--r--crates/typst/src/diag.rs9
-rw-r--r--crates/typst/src/eval/access.rs6
-rw-r--r--crates/typst/src/eval/call.rs48
-rw-r--r--crates/typst/src/eval/code.rs67
-rw-r--r--crates/typst/src/eval/markup.rs2
-rw-r--r--crates/typst/src/eval/mod.rs10
-rw-r--r--crates/typst/src/eval/tracer.rs10
-rw-r--r--crates/typst/src/eval/vm.rs23
-rw-r--r--crates/typst/src/foundations/array.rs46
-rw-r--r--crates/typst/src/foundations/cast.rs14
-rw-r--r--crates/typst/src/foundations/content.rs8
-rw-r--r--crates/typst/src/foundations/context.rs80
-rw-r--r--crates/typst/src/foundations/element.rs43
-rw-r--r--crates/typst/src/foundations/func.rs50
-rw-r--r--crates/typst/src/foundations/mod.rs2
-rw-r--r--crates/typst/src/foundations/scope.rs30
-rw-r--r--crates/typst/src/foundations/selector.rs39
-rw-r--r--crates/typst/src/foundations/str.rs12
-rw-r--r--crates/typst/src/foundations/styles.rs25
-rw-r--r--crates/typst/src/introspection/counter.rs539
-rw-r--r--crates/typst/src/introspection/here.rs51
-rw-r--r--crates/typst/src/introspection/introspector.rs21
-rw-r--r--crates/typst/src/introspection/locate.rs105
-rw-r--r--crates/typst/src/introspection/location.rs30
-rw-r--r--crates/typst/src/introspection/metadata.rs10
-rw-r--r--crates/typst/src/introspection/mod.rs12
-rw-r--r--crates/typst/src/introspection/query.rs56
-rw-r--r--crates/typst/src/introspection/state.rs282
-rw-r--r--crates/typst/src/layout/grid/layout.rs31
-rw-r--r--crates/typst/src/layout/layout.rs14
-rw-r--r--crates/typst/src/layout/length.rs38
-rw-r--r--crates/typst/src/layout/measure.rs26
-rw-r--r--crates/typst/src/layout/page.rs65
-rw-r--r--crates/typst/src/math/cancel.rs19
-rw-r--r--crates/typst/src/math/equation.rs21
-rw-r--r--crates/typst/src/model/enum.rs9
-rw-r--r--crates/typst/src/model/figure.rs22
-rw-r--r--crates/typst/src/model/footnote.rs14
-rw-r--r--crates/typst/src/model/heading.rs27
-rw-r--r--crates/typst/src/model/list.rs16
-rw-r--r--crates/typst/src/model/numbering.rs15
-rw-r--r--crates/typst/src/model/outline.rs50
-rw-r--r--crates/typst/src/model/reference.rs23
-rw-r--r--crates/typst/src/realize/process.rs36
-rw-r--r--docs/guides/guide-for-latex-users.md4
-rw-r--r--docs/guides/page-setup.md45
-rw-r--r--docs/reference/context.md235
-rw-r--r--docs/reference/styling.md10
-rw-r--r--docs/reference/syntax.md3
-rw-r--r--tests/src/tests.rs12
-rw-r--r--tests/typ/bugs/2650-cjk-latin-spacing-meta.typ3
-rw-r--r--tests/typ/bugs/cite-locate.typ2
-rw-r--r--tests/typ/bugs/math-realize.typ2
-rw-r--r--tests/typ/bugs/pagebreak-numbering.typ1
-rw-r--r--tests/typ/compiler/content-field.typ5
-rw-r--r--tests/typ/compiler/methods.typ21
-rw-r--r--tests/typ/compiler/recursion.typ4
-rw-r--r--tests/typ/compiler/selector-logical.typ7
-rw-r--r--tests/typ/layout/page-marginals.typ6
-rw-r--r--tests/typ/math/attach-p3.typ4
-rw-r--r--tests/typ/meta/context-compatibility.typ29
-rw-r--r--tests/typ/meta/context.typ181
-rw-r--r--tests/typ/meta/counter.typ18
-rw-r--r--tests/typ/meta/figure-caption.typ2
-rw-r--r--tests/typ/meta/query-before-after.typ7
-rw-r--r--tests/typ/meta/query-figure.typ6
-rw-r--r--tests/typ/meta/query-header.typ20
-rw-r--r--tests/typ/meta/state.typ26
-rw-r--r--tools/support/typst.tmLanguage.json4
87 files changed, 2091 insertions, 820 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a1193fe0..9684bb35 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 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();
}
diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md
index 4b8c43b1..d991ef78 100644
--- a/docs/guides/guide-for-latex-users.md
+++ b/docs/guides/guide-for-latex-users.md
@@ -603,7 +603,7 @@ The example below
This should be a good starting point! If you want to go further, why not create
a reusable template?
-## Bibliographies { #bibliographies }
+## Bibliographies
Typst includes a fully-featured bibliography system that is compatible with
BibTeX files. You can continue to use your `.bib` literature libraries by
loading them with the [`bibliography`]($bibliography) function. Another
@@ -627,7 +627,7 @@ use in prose (cf. `\citet` and `\textcite`) are available with
You can find more information on the documentation page of the [`bibliography`]($bibliography) function.
-## Installation { #installation }
+## Installation
You have two ways to use Typst: In [our web app](https://typst.app/signup/) or
by [installing the compiler](https://github.com/typst/typst/releases) on your
computer. When you use the web app, we provide a batteries-included
diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md
index 046bf9f4..a70f025f 100644
--- a/docs/guides/page-setup.md
+++ b/docs/guides/page-setup.md
@@ -174,13 +174,13 @@ conditionally remove the header on the first page:
```typ
>>> #set page("a5", margin: (x: 2.5cm, y: 3cm))
-#set page(header: locate(loc => {
- if counter(page).at(loc).first() > 1 [
+#set page(header: context {
+ if counter(page).get().first() > 1 [
_Lisa Strassner's Thesis_
#h(1fr)
National Academy of Sciences
]
-}))
+})
#lorem(150)
```
@@ -206,12 +206,12 @@ such a label exists on the current page:
```typ
>>> #set page("a5", margin: (x: 2.5cm, y: 3cm))
-#set page(header: locate(loc => {
- let page-counter = counter(page)
- let matches = query(<big-table>, loc)
- let current = page-counter.at(loc)
+#set page(header: context {
+ let page-counter =
+ let matches = query(<big-table>)
+ let current = counter(page).get()
let has-table = matches.any(m =>
- page-counter.at(m.location()) == current
+ counter(page).at(m.location()) == current
)
if not has-table [
@@ -291,7 +291,7 @@ a custom footer with page numbers and more.
```example
>>> #set page("iso-b6", margin: 1.75cm)
-#set page(footer: [
+#set page(footer: context [
*American Society of Proceedings*
#h(1fr)
#counter(page).display(
@@ -314,21 +314,20 @@ circle for each page.
```example
>>> #set page("iso-b6", margin: 1.75cm)
-#set page(footer: [
+#set page(footer: context [
*Fun Typography Club*
#h(1fr)
- #counter(page).display(num => {
- let circles = num * (
- box(circle(
- radius: 2pt,
- fill: navy,
- )),
- )
- box(
- inset: (bottom: 1pt),
- circles.join(h(1pt))
- )
- })
+ #let (num,) = counter(page).get()
+ #let circles = num * (
+ box(circle(
+ radius: 2pt,
+ fill: navy,
+ )),
+ )
+ #box(
+ inset: (bottom: 1pt),
+ circles.join(h(1pt))
+ )
])
This page has a custom footer.
@@ -382,7 +381,7 @@ page counter, you can use the [`page`]($locate) method on the argument of the
// This returns one even though the
// page counter was incremented by 5.
-#locate(loc => loc.page())
+#context here().page()
```
You can also obtain the page numbering pattern from the `{locate}` closure
diff --git a/docs/reference/context.md b/docs/reference/context.md
new file mode 100644
index 00000000..5ebcfbe7
--- /dev/null
+++ b/docs/reference/context.md
@@ -0,0 +1,235 @@
+---
+description: |
+ How to deal with content that reacts to its location in the document.
+---
+
+# Context
+Sometimes, we want to create content that reacts to its location in the
+document. This could be a localized phrase that depends on the configured text
+language or something as simple as a heading number which prints the right
+value based on how many headings came before it. However, Typst code isn't
+directly aware of its location in the document. Some code at the beginning of
+the source text could yield content that ends up at the back of the document.
+
+To produce content that is reactive to its surroundings, we must thus
+specifically instruct Typst: We do this with the `{context}` keyword, which
+precedes an expression and ensures that it is computed with knowledge of its
+environment. In return, the context expression itself ends up opaque. We cannot
+directly access whatever results from it in our code, precisely because it is
+contextual: There is no one correct result, there may be multiple results in
+different places of the document. For this reason, everything that depends on
+the contextual data must happen inside of the context expression.
+
+Aside from explicit context expressions, context is also established implicitly
+in some places that are also aware of their location in the document:
+[Show rules]($styling/#show-rules) provide context[^1] and numberings in the
+outline, for instance, also provide the proper context to resolve counters.
+
+## Style context
+With set rules, we can adjust style properties for parts or the whole of our
+document. We cannot access these without a known context, as they may change
+throughout the course of the document. When context is available, we can
+retrieve them simply by accessing them as fields on the respective element
+function.
+
+```example
+#set text(lang: "de")
+#context text.lang
+```
+
+As explained above, a context expression is reactive to the different
+environments it is placed into. In the example below, we create a single context
+expression, store it in the `value` variable and use it multiple times. Each use
+properly reacts to the current surroundings.
+
+```example
+#let value = context text.lang
+#value
+
+#set text(lang: "de")
+#value
+
+#set text(lang: "fr")
+#value
+```
+
+Crucially, upon creation, `value` becomes opaque [content]($content) that we
+cannot peek into. It can only be resolved when placed somewhere because only
+then the context is known. The body of a context expression may be evaluated
+zero, one, or multiple times, depending on how many different places it is put
+into.
+
+## Location context
+Context can not only give us access to set rule values. It can also let us know
+_where_ in the document we currently are, relative to other elements, and
+absolutely on the pages. We can use this information to create very flexible
+interactions between different document parts. This underpins features like
+heading numbering, the table of contents, or page headers dependant on section
+headings.
+
+Some functions like [`counter.get`]($counter.get) implicitly access the current
+location. In the example below, we want to retrieve the value of the heading
+counter. Since it changes throughout the document, we need to first enter a
+context expression. Then, we use `get` to retrieve the counter's current value.
+This function accesses the current location from the context to resolve the
+counter value. Counters have multiple levels and `get` returns an array with the
+resolved numbers. Thus, we get the following result:
+
+```example
+#set heading(numbering: "1.")
+
+= Introduction
+#lorem(5)
+
+#context counter(heading).get()
+
+= Background
+#lorem(5)
+
+#context counter(heading).get()
+```
+
+For more flexibility, we can also use the [`here`]($here) function to directly
+extract the current [location]($location) from the context. The example below
+demonstrates this:
+
+- We first have `{counter(heading).get()}`, which resolves to `{(2,)}` as
+ before.
+- We then use the more powerful [`counter.at`]($counter.at) with
+ [`here`]($here), which in combination is equivalent to `get`, and thus get
+ `{(2,)}`.
+- Finally, we use `at` with a [label]($label) to retrieve the value of the
+ counter at a _different_ location in the document, in our case that of the
+ introduction heading. This yields `{(1,)}`. Typst's context system gives us
+ time travel abilities and lets us retrieve the values of any counters and
+ states at _any_ location in the document.
+
+```example
+#set heading(numbering: "1.")
+
+= Introduction <intro>
+#lorem(5)
+
+= Background <back>
+#lorem(5)
+
+#context [
+ #counter(heading).get() \
+ #counter(heading).at(here()) \
+ #counter(heading).at(<intro>)
+]
+```
+
+As mentioned before, we can also use context to get the physical position of
+elements on the pages. We do this with the [`locate`]($locate) function, which
+works similarly to `counter.at`: It takes a location or other
+[selector]($selector) that resolves to a unique element (could also be a label)
+and returns the position on the pages for that element.
+
+```example
+Background is at: \
+#context locate(<back>).position()
+
+= Introduction <intro>
+#lorem(5)
+#pagebreak()
+
+= Background <back>
+#lorem(5)
+```
+
+There are other functions that make use of the location context, most
+prominently [`query`]($query). Take a look at the
+[introspection]($category/introspection) category for more details on those.
+
+## Nested contexts
+Context is also accessible from within function calls nested in context blocks.
+In the example below, `foo` itself becomes a contextual function, just like
+[`to-absolute`]($length.to-absolute) is.
+
+```example
+#let foo() = 1em.to-absolute()
+#context {
+ foo() == text.size
+}
+```
+
+Context blocks can be nested. Contextual code will then always access the
+innermost context. The example below demonstrates this: The first `text.lang`
+will access the outer context block's styles and as such, it will **not**
+see the effect of `{set text(lang: "fr")}`. The nested context block around the
+second `text.lang`, however, starts after the set rule and will thus show
+its effect.
+
+```example
+#set text(lang: "de")
+#context [
+ #set text(lang: "fr")
+ #text.lang \
+ #context text.lang
+]
+```
+
+You might wonder why Typst ignores the French set rule when computing the first
+`text.lang` in the example above. The reason is that, in the general case, Typst
+cannot know all the styles that will apply as set rules can be applied to
+content after it has been constructed. Below, `text.lang` is already computed
+when the template function is applied. As such, it cannot possibly be aware of
+the language change to French in the template.
+
+```example
+#let template(body) = {
+ set text(lang: "fr")
+ upper(body)
+}
+
+#set text(lang: "de")
+#context [
+ #show: template
+ #text.lang \
+ #context text.lang
+]
+```
+
+The second `text.lang`, however, _does_ react to the language change because
+evaluation of its surrounding context block is deferred until the styles for it
+are known. This illustrates the importance of picking the right insertion point for a context to get access to precisely the right styles.
+
+The same also holds true for the location context. Below, the first
+`{c.display()}` call will access the outer context block and will thus not see
+the effect of `{c.update(2)}` while the second `{c.display()}` accesses the inner context and will thus see it.
+
+```example
+#let c = counter("mycounter")
+#c.update(1)
+#context [
+ #c.update(2)
+ #c.display() \
+ #context c.display()
+]
+```
+
+## Compiler iterations
+To resolve contextual interactions, the Typst compiler processes your document
+multiple times. For instance, to resolve a `locate` call, Typst first provides a
+placeholder position, layouts your document and then recompiles with the known
+position from the finished layout. The same approach is taken to resolve
+counters, states, and queries. In certain cases, Typst may even need more than
+two iterations to resolve everything. While that's sometimes a necessity, it may
+also be a sign of misuse of contextual functions (e.g. of
+[state]($state/#caution)). If Typst cannot resolve everything within five
+attempts, it will stop and output the warning "layout did not converge within 5
+attempts."
+
+A very careful reader might have noticed that not all of the functions presented
+above actually make use of the current location. While
+`{counter(heading).get()}` definitely depends on it,
+`{counter(heading).at(<intro>)}`, for instance, does not. However, it still
+requires context. While its value is always the same _within_ one compilation
+iteration, it may change over the course of multiple compiler iterations. If one
+could call it directly at the top level of a module, the whole module and its
+exports could change over the course of multiple compiler iterations, which
+would not be desirable.
+
+[^1]: Currently, all show rules provide styling context, but only show rules on
+ [locatable]($location/#locatable) elements provide a location context.
diff --git a/docs/reference/styling.md b/docs/reference/styling.md
index 687fed79..1c3fd173 100644
--- a/docs/reference/styling.md
+++ b/docs/reference/styling.md
@@ -81,9 +81,9 @@ in Typst. For maximum flexibility, you can instead write a show rule that
defines how to format an element from scratch. To write such a show rule,
replace the set rule after the colon with an arbitrary [function]($function).
This function receives the element in question and can return arbitrary content.
-Different [fields]($scripting/#fields) are available on the element passed to
-the function. Below, we define a show rule that formats headings for a fantasy
-encyclopedia.
+The available [fields]($scripting/#fields) on the element passed to the function
+again match the parameters of the respective element function. Below, we define
+a show rule that formats headings for a fantasy encyclopedia.
```example
#set heading(numbering: "(I)")
@@ -91,7 +91,9 @@ encyclopedia.
#set align(center)
#set text(font: "Inria Serif")
\~ #emph(it.body)
- #counter(heading).display() \~
+ #counter(heading).display(
+ it.numbering
+ ) \~
]
= Dragon
diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md
index 643dc954..40213fe9 100644
--- a/docs/reference/syntax.md
+++ b/docs/reference/syntax.md
@@ -12,7 +12,7 @@ All this is backed by a tightly integrated scripting language with built-in and
user-defined functions.
## Modes
-Typst has three syntactical modes: Markup, math, and code. Markup mode is the
+Typst has three syntactical modes: Markup, math, and code. Markup mode is the
default in a Typst document, math mode lets you write mathematical formulas, and
code mode lets you use Typst's scripting features.
@@ -111,6 +111,7 @@ a table listing all syntax that is available in code mode:
| Show-set rule | `{show par: set block(..)}` | [Styling]($styling/#show-rules) |
| Show rule with function | `{show raw: it => {..}}` | [Styling]($styling/#show-rules) |
| Show-everything rule | `{show: columns.with(2)}` | [Styling]($styling/#show-rules) |
+| Context expression | `{context text.lang}` | [Context]($context) |
| Conditional | `{if x == 1 {..} else {..}}` | [Scripting]($scripting/#conditionals) |
| For loop | `{for x in (1, 2, 3) {..}}` | [Scripting]($scripting/#loops) |
| While loop | `{while x < 10 {..}}` | [Scripting]($scripting/#loops) |
diff --git a/tests/src/tests.rs b/tests/src/tests.rs
index 09baa190..05b53a73 100644
--- a/tests/src/tests.rs
+++ b/tests/src/tests.rs
@@ -753,10 +753,10 @@ fn test_autocomplete<'a>(
{
writeln!(output, " Subtest {i} does not match expected completions.")
.unwrap();
- write!(output, " for annotation | ").unwrap();
+ write!(output, " for annotation | ").unwrap();
print_annotation(output, source, line, annotation);
- write!(output, " Not contained | ").unwrap();
+ write!(output, " Not contained // ").unwrap();
for item in missing {
write!(output, "{item:?}, ").unwrap()
}
@@ -772,10 +772,10 @@ fn test_autocomplete<'a>(
{
writeln!(output, " Subtest {i} does not match expected completions.")
.unwrap();
- write!(output, " for annotation | ").unwrap();
+ write!(output, " for annotation | ").unwrap();
print_annotation(output, source, line, annotation);
- write!(output, " Not excluded| ").unwrap();
+ write!(output, " Not excluded // ").unwrap();
for item in undesired {
write!(output, "{item:?}, ").unwrap()
}
@@ -850,12 +850,12 @@ fn test_diagnostics<'a>(
*ok = false;
for unexpected in unexpected_outputs {
- write!(output, " Not annotated | ").unwrap();
+ write!(output, " Not annotated // ").unwrap();
print_annotation(output, source, line, unexpected)
}
for missing in missing_outputs {
- write!(output, " Not emitted | ").unwrap();
+ write!(output, " Not emitted // ").unwrap();
print_annotation(output, source, line, missing)
}
}
diff --git a/tests/typ/bugs/2650-cjk-latin-spacing-meta.typ b/tests/typ/bugs/2650-cjk-latin-spacing-meta.typ
index e234e651..12c7ea41 100644
--- a/tests/typ/bugs/2650-cjk-latin-spacing-meta.typ
+++ b/tests/typ/bugs/2650-cjk-latin-spacing-meta.typ
@@ -1,6 +1,5 @@
// https://github.com/typst/typst/issues/2650
-#let with-locate(body) = locate(loc => body)
测a试
-测#with-locate[a]试
+测#context [a]试
diff --git a/tests/typ/bugs/cite-locate.typ b/tests/typ/bugs/cite-locate.typ
index 4eb95b54..c576c45e 100644
--- a/tests/typ/bugs/cite-locate.typ
+++ b/tests/typ/bugs/cite-locate.typ
@@ -17,7 +17,7 @@
caption: [A pirate @arrgh in @intro],
)
-#locate(loc => [Citation @distress on page #loc.page()])
+#context [Citation @distress on page #here().page()]
#pagebreak()
#bibliography("/files/works.bib", style: "chicago-notes")
diff --git a/tests/typ/bugs/math-realize.typ b/tests/typ/bugs/math-realize.typ
index d7991542..10d8b78e 100644
--- a/tests/typ/bugs/math-realize.typ
+++ b/tests/typ/bugs/math-realize.typ
@@ -4,7 +4,7 @@
---
#let my = $pi$
#let f1 = box(baseline: 10pt, [f])
-#let f2 = style(sty => f1)
+#let f2 = context f1
#show math.vec: [nope]
$ pi a $
diff --git a/tests/typ/bugs/pagebreak-numbering.typ b/tests/typ/bugs/pagebreak-numbering.typ
index d805c2c6..a9fae3e4 100644
--- a/tests/typ/bugs/pagebreak-numbering.typ
+++ b/tests/typ/bugs/pagebreak-numbering.typ
@@ -10,4 +10,3 @@ This and next page should not be numbered
#counter(page).update(1)
This page should
-
diff --git a/tests/typ/compiler/content-field.typ b/tests/typ/compiler/content-field.typ
index dab4ec4b..96ce1dca 100644
--- a/tests/typ/compiler/content-field.typ
+++ b/tests/typ/compiler/content-field.typ
@@ -17,10 +17,7 @@
---
// Test it with query.
#set raw(lang: "rust")
-#locate(loc => {
- let elem = query(<myraw>, loc).first()
- elem.lang
-})
+#context query(<myraw>).first().lang
`raw` <myraw>
---
diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ
index 9eed5aed..2dce8892 100644
--- a/tests/typ/compiler/methods.typ
+++ b/tests/typ/compiler/methods.typ
@@ -83,20 +83,19 @@
---
// Test length `to-absolute` method.
-
#set text(size: 12pt)
-#style(styles => {
- test((6pt).to-absolute(styles), 6pt)
- test((6pt + 10em).to-absolute(styles), 126pt)
- test((10em).to-absolute(styles), 120pt)
-})
+#context {
+ test((6pt).to-absolute(), 6pt)
+ test((6pt + 10em).to-absolute(), 126pt)
+ test((10em).to-absolute(), 120pt)
+}
#set text(size: 64pt)
-#style(styles => {
- test((6pt).to-absolute(styles), 6pt)
- test((6pt + 10em).to-absolute(styles), 646pt)
- test((10em).to-absolute(styles), 640pt)
-})
+#context {
+ test((6pt).to-absolute(), 6pt)
+ test((6pt + 10em).to-absolute(), 646pt)
+ test((10em).to-absolute(), 640pt)
+}
---
// Error: 2-21 cannot convert a length with non-zero em units (`-6pt + 10.5em`) to pt
diff --git a/tests/typ/compiler/recursion.typ b/tests/typ/compiler/recursion.typ
index f5f870b5..421b638b 100644
--- a/tests/typ/compiler/recursion.typ
+++ b/tests/typ/compiler/recursion.typ
@@ -44,8 +44,8 @@
---
// Test cyclic imports during layout.
-// Error: 14-37 maximum layout depth exceeded
-// Hint: 14-37 try to reduce the amount of nesting in your layout
+// Error: 2-38 maximum show rule depth exceeded
+// Hint: 2-38 check whether the show rule matches its own output
#layout(_ => include "recursion.typ")
---
diff --git a/tests/typ/compiler/selector-logical.typ b/tests/typ/compiler/selector-logical.typ
index 64f97384..5369e4c7 100644
--- a/tests/typ/compiler/selector-logical.typ
+++ b/tests/typ/compiler/selector-logical.typ
@@ -13,10 +13,9 @@
#figure([Iguana], kind: "iguana", supplement: none)
== I
-#let test-selector(selector, ref) = locate(loc => {
- let elems = query(selector, loc)
- test(elems.map(e => e.body), ref)
-})
+#let test-selector(selector, ref) = context {
+ test(query(selector).map(e => e.body), ref)
+}
// Test `or`.
#test-selector(
diff --git a/tests/typ/layout/page-marginals.typ b/tests/typ/layout/page-marginals.typ
index d65591e6..589fb299 100644
--- a/tests/typ/layout/page-marginals.typ
+++ b/tests/typ/layout/page-marginals.typ
@@ -6,10 +6,10 @@
h(1fr)
text(0.8em)[_Chapter 1_]
},
- footer: align(center)[\~ #counter(page).display() \~],
- background: counter(page).display(n => if n <= 2 {
+ footer: context align(center)[\~ #counter(page).display() \~],
+ background: context if counter(page).get().first() <= 2 {
place(center + horizon, circle(radius: 1cm, fill: luma(90%)))
- })
+ }
)
But, soft! what light through yonder window breaks? It is the east, and Juliet
diff --git a/tests/typ/math/attach-p3.typ b/tests/typ/math/attach-p3.typ
index c681af2a..3591c248 100644
--- a/tests/typ/math/attach-p3.typ
+++ b/tests/typ/math/attach-p3.typ
@@ -11,8 +11,8 @@ $ scripts(sum)_1^2 != sum_1^2 $
$ limits(integral)_a^b != integral_a^b $
---
-// Error: 30-34 unknown variable: oops
-$ attach(A, t: #locate(it => oops)) $
+// Error: 25-29 unknown variable: oops
+$ attach(A, t: #context oops) $
---
// Show and let rules for limits and scripts
diff --git a/tests/typ/meta/context-compatibility.typ b/tests/typ/meta/context-compatibility.typ
new file mode 100644
index 00000000..60124255
--- /dev/null
+++ b/tests/typ/meta/context-compatibility.typ
@@ -0,0 +1,29 @@
+// Test compatibility with the pre-context way of things.
+// Ref: false
+
+---
+#let s = state("x", 0)
+#let compute(expr) = [
+ #s.update(x =>
+ eval(expr.replace("x", str(x)))
+ )
+ New value is #s.display().
+]
+
+#locate(loc => {
+ let elem = query(<here>, loc).first()
+ test(s.at(elem.location()), 13)
+})
+
+#compute("10") \
+#compute("x + 3") \
+*Here.* <here> \
+#compute("x * 2") \
+#compute("x - 5")
+
+---
+#style(styles => measure([it], styles).width < 20pt)
+
+---
+#counter(heading).update(10)
+#counter(heading).display(n => test(n, 10))
diff --git a/tests/typ/meta/context.typ b/tests/typ/meta/context.typ
new file mode 100644
index 00000000..729d9fa2
--- /dev/null
+++ b/tests/typ/meta/context.typ
@@ -0,0 +1,181 @@
+// Test context expressions.
+// Ref: false
+
+---
+// Test that context body is parsed as atomic expression.
+#let c = [#context "hello".]
+#test(c.children.first().func(), (context none).func())
+#test(c.children.last(), [.])
+
+---
+// Test that manual construction is forbidden.
+// Error: 2-25 cannot be constructed manually
+#(context none).func()()
+
+---
+// Test that `here()` yields the context element's location.
+#context test(query(here()).first().func(), (context none).func())
+
+---
+// Test whether context is retained in nested function.
+#let translate(..args) = args.named().at(text.lang)
+#set text(lang: "de")
+#context test(translate(de: "Inhalt", en: "Contents"), "Inhalt")
+
+---
+// Test whether context is retained in built-in callback.
+#set text(lang: "de")
+#context test(
+ ("en", "de", "fr").sorted(key: v => v != text.lang),
+ ("de", "en", "fr"),
+)
+
+---
+// Test `locate` + `here`.
+#context test(here().position().y, 10pt)
+
+---
+// Test `locate`.
+#v(10pt)
+= Introduction <intro>
+#context test(locate(<intro>).position().y, 20pt)
+
+---
+// Error: 10-25 label `<intro>` does not exist in the document
+#context locate(<intro>)
+
+---
+= Introduction <intro>
+= Introduction <intro>
+
+// Error: 10-25 label `<intro>` occurs multiple times in the document
+#context locate(<intro>)
+
+---
+#v(10pt)
+= Introduction <intro>
+#context test(locate(heading).position().y, 20pt)
+
+---
+// Error: 10-25 selector does not match any element
+#context locate(heading)
+
+---
+= Introduction <intro>
+= Introduction <intro>
+
+// Error: 10-25 selector matches multiple elements
+#context locate(heading)
+
+---
+// Test `counter`.
+#let c = counter("heading")
+#c.update(2)
+#c.update(n => n + 2)
+#context test(c.get(), (4,))
+#c.update(n => n - 3)
+#context test(c.at(here()), (1,))
+
+---
+// Test `state.at` outside of context.
+// Error: 2-26 can only be used when context is known
+// Hint: 2-26 try wrapping this in a `context` expression
+// Hint: 2-26 the `context` expression should wrap everything that depends on this function
+#state("key").at(<label>)
+
+---
+// Test `counter.at` outside of context.
+// Error: 2-28 can only be used when context is known
+// Hint: 2-28 try wrapping this in a `context` expression
+// Hint: 2-28 the `context` expression should wrap everything that depends on this function
+#counter("key").at(<label>)
+
+---
+// Test `measure`.
+#let f(lo, hi) = context {
+ let h = measure[Hello].height
+ assert(h > lo)
+ assert(h < hi)
+}
+#text(10pt, f(6pt, 8pt))
+#text(20pt, f(13pt, 14pt))
+
+---
+// Test basic get rule.
+#context test(text.lang, "en")
+#set text(lang: "de")
+#context test(text.lang, "de")
+#text(lang: "es", context test(text.lang, "es"))
+
+---
+// Test folding.
+#set rect(stroke: red)
+#context {
+ test(type(rect.stroke), stroke)
+ test(rect.stroke.paint, red)
+}
+#[
+ #set rect(stroke: 4pt)
+ #context test(rect.stroke, 4pt + red)
+]
+#context test(rect.stroke, stroke(red))
+
+---
+// We have one collision: `figure.caption` could be both the element and a get
+// rule for the `caption` field, which is settable. We always prefer the
+// element. It's unfortunate, but probably nobody writes
+// `set figure(caption: ..)` anyway.
+#test(type(figure.caption), function)
+#context test(type(figure.caption), function)
+
+---
+// Error: 10-31 Assertion failed: "en" != "de"
+#context test(text.lang, "de")
+
+---
+// Error: 15-20 function `text` does not contain field `langs`
+#context text.langs
+
+---
+// Error: 18-22 function `heading` does not contain field `body`
+#context heading.body
+
+---
+// Error: 7-11 can only be used when context is known
+// Hint: 7-11 try wrapping this in a `context` expression
+// Hint: 7-11 the `context` expression should wrap everything that depends on this function
+#text.lang
+
+---
+// Error: 7-12 function `text` does not contain field `langs`
+#text.langs
+
+---
+// Error: 10-14 function `heading` does not contain field `body`
+#heading.body
+
+---
+// Test that show rule establishes context.
+#set heading(numbering: "1.")
+#show heading: it => test(
+ counter(heading).get(),
+ (intro: (1,), back: (2,)).at(str(it.label)),
+)
+
+= Introduction <intro>
+= Background <back>
+
+---
+// Test that show rule on non-locatable element allows `query`.
+// Error: 18-47 Assertion failed: 2 != 3
+#show emph: _ => test(query(heading).len(), 3)
+#show strong: _ => test(query(heading).len(), 2)
+= Introduction
+= Background
+*Hi* _there_
+
+---
+// Test error when captured variable is assigned to.
+#let i = 0
+// Error: 11-12 variables from outside the context expression are read-only and cannot be modified
+#context (i = 1)
diff --git a/tests/typ/meta/counter.typ b/tests/typ/meta/counter.typ
index 6b5797de..6d72f246 100644
--- a/tests/typ/meta/counter.typ
+++ b/tests/typ/meta/counter.typ
@@ -4,21 +4,21 @@
// Count with string key.
#let mine = counter("mine!")
-Final: #locate(loc => mine.final(loc).at(0)) \
+Final: #context mine.final().at(0) \
#mine.step()
-First: #mine.display() \
+First: #context mine.display() \
#mine.update(7)
-#mine.display("1 of 1", both: true) \
+#context mine.display("1 of 1", both: true) \
#mine.step()
#mine.step()
-Second: #mine.display("I")
+Second: #context mine.display("I")
#mine.update(n => n * 2)
#mine.step()
---
// Count labels.
#let label = <heya>
-#let count = counter(label).display()
+#let count = context counter(label).display()
#let elem(it) = [#box(it) #label]
#elem[hey, there!] #count \
@@ -31,17 +31,17 @@ Second: #mine.display("I")
#counter(heading).step()
= Alpha
-In #counter(heading).display()
+In #context counter(heading).display()
== Beta
#set heading(numbering: none)
= Gamma
#heading(numbering: "I.")[Delta]
-At Beta, it was #locate(loc => {
- let it = query(heading, loc).find(it => it.body == [Beta])
+At Beta, it was #context {
+ let it = query(heading).find(it => it.body == [Beta])
numbering(it.numbering, ..counter(heading).at(it.location()))
-})
+}
---
// Count figures.
diff --git a/tests/typ/meta/figure-caption.typ b/tests/typ/meta/figure-caption.typ
index 0188ebca..0cdc2bbb 100644
--- a/tests/typ/meta/figure-caption.typ
+++ b/tests/typ/meta/figure-caption.typ
@@ -45,7 +45,7 @@
#show figure.caption: it => emph[
#it.body
(#it.supplement
- #it.counter.display(it.numbering))
+ #context it.counter.display(it.numbering))
]
#figure(
diff --git a/tests/typ/meta/query-before-after.typ b/tests/typ/meta/query-before-after.typ
index 3ddc9402..5f134093 100644
--- a/tests/typ/meta/query-before-after.typ
+++ b/tests/typ/meta/query-before-after.typ
@@ -57,12 +57,11 @@
#set heading(outlined: true, numbering: "1.")
-// This is purposefully an empty
-#locate(loc => [
+#context [
Non-outlined elements:
- #(query(selector(heading).and(heading.where(outlined: false)), loc)
+ #(query(selector(heading).and(heading.where(outlined: false)))
.map(it => it.body).join(", "))
-])
+]
#heading("A", outlined: false)
#heading("B", outlined: true)
diff --git a/tests/typ/meta/query-figure.typ b/tests/typ/meta/query-figure.typ
index 0540d65a..590c8e87 100644
--- a/tests/typ/meta/query-figure.typ
+++ b/tests/typ/meta/query-figure.typ
@@ -11,8 +11,8 @@
#show figure: set image(width: 80%)
= List of Figures
-#locate(it => {
- let elements = query(selector(figure).after(it), it)
+#context {
+ let elements = query(selector(figure).after(here()))
for it in elements [
Figure
#numbering(it.numbering,
@@ -21,7 +21,7 @@
#box(width: 1fr, repeat[.])
#counter(page).at(it.location()).first() \
]
-})
+}
#figure(
image("/files/glacier.jpg"),
diff --git a/tests/typ/meta/query-header.typ b/tests/typ/meta/query-header.typ
index dd83c128..5cbaa995 100644
--- a/tests/typ/meta/query-header.typ
+++ b/tests/typ/meta/query-header.typ
@@ -4,19 +4,17 @@
#set page(
paper: "a7",
margin: (y: 1cm, x: 0.5cm),
- header: {
+ header: context {
smallcaps[Typst Academy]
h(1fr)
- locate(it => {
- let after = query(selector(heading).after(it), it)
- let before = query(selector(heading).before(it), it)
- let elem = if before.len() != 0 {
- before.last()
- } else if after.len() != 0 {
- after.first()
- }
- emph(elem.body)
- })
+ let after = query(selector(heading).after(here()))
+ let before = query(selector(heading).before(here()))
+ let elem = if before.len() != 0 {
+ before.last()
+ } else if after.len() != 0 {
+ after.first()
+ }
+ emph(elem.body)
}
)
diff --git a/tests/typ/meta/state.typ b/tests/typ/meta/state.typ
index 86dc70a5..3fa8ece7 100644
--- a/tests/typ/meta/state.typ
+++ b/tests/typ/meta/state.typ
@@ -9,20 +9,20 @@
$ 2 + 3 $
#s.update(double)
-Is: #s.display(),
-Was: #locate(location => {
- let it = query(math.equation, location).first()
+Is: #context s.get(),
+Was: #context {
+ let it = query(math.equation).first()
s.at(it.location())
-}).
+}.
---
// Try same key with different initial value.
-#state("key", 2).display()
+#context state("key", 2).get()
#state("key").update(x => x + 1)
-#state("key", 2).display()
-#state("key", 3).display()
+#context state("key", 2).get()
+#context state("key", 3).get()
#state("key").update(x => x + 1)
-#state("key", 2).display()
+#context state("key", 2).get()
---
#set page(width: 200pt)
@@ -30,15 +30,15 @@ Was: #locate(location => {
#let ls = state("lorem", lorem(1000).split("."))
#let loremum(count) = {
- ls.display(list => list.slice(0, count).join(".").trim() + ".")
+ context ls.get().slice(0, count).join(".").trim() + "."
ls.update(list => list.slice(count))
}
#let fs = state("fader", red)
#let trait(title) = block[
- #fs.display(color => text(fill: color)[
+ #context text(fill: fs.get())[
*#title:* #loremum(1)
- ])
+ ]
#fs.update(color => color.lighten(30%))
]
@@ -52,5 +52,5 @@ Was: #locate(location => {
// Warning: layout did not converge within 5 attempts
// Hint: check if any states or queries are updating themselves
#let s = state("s", 1)
-#locate(loc => s.update(s.final(loc) + 1))
-#s.display()
+#context s.update(s.final() + 1)
+#context s.get()
diff --git a/tools/support/typst.tmLanguage.json b/tools/support/typst.tmLanguage.json
index 7e01c41a..104df060 100644
--- a/tools/support/typst.tmLanguage.json
+++ b/tools/support/typst.tmLanguage.json
@@ -132,7 +132,7 @@
"captures": { "1": { "name": "punctuation.definition.reference.typst" } }
},
{
- "begin": "(#)(let|set|show)\\b",
+ "begin": "(#)(let|set|show|context)\\b",
"end": "\n|(;)|(?=])",
"beginCaptures": {
"0": { "name": "keyword.other.typst" },
@@ -263,7 +263,7 @@
},
{
"name": "keyword.other.typst",
- "match": "\\b(let|as|in|set|show)\\b"
+ "match": "\\b(let|as|in|set|show|context)\\b"
},
{
"name": "keyword.control.conditional.typst",