New context system (#3497)

This commit is contained in:
Laurenz 2024-02-27 11:05:16 +01:00 committed by GitHub
parent e9ee00a7c0
commit 145723b1ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 2106 additions and 835 deletions

1
Cargo.lock generated
View File

@ -2575,6 +2575,7 @@ dependencies = [
"icu_provider_adapters", "icu_provider_adapters",
"icu_provider_blob", "icu_provider_blob",
"icu_segmenter", "icu_segmenter",
"if_chain",
"image", "image",
"indexmap 2.1.0", "indexmap 2.1.0",
"kamadak-exif", "kamadak-exif",

View File

@ -165,6 +165,11 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel {
&format!("{}reference/", resolver.base()), &format!("{}reference/", resolver.base()),
"reference/scripting.md", "reference/scripting.md",
), ),
markdown_page(
resolver,
&format!("{}reference/", resolver.base()),
"reference/context.md",
),
category_page(resolver, FOUNDATIONS).with_part("Library"), category_page(resolver, FOUNDATIONS).with_part("Library"),
category_page(resolver, MODEL), category_page(resolver, MODEL),
category_page(resolver, TEXT), category_page(resolver, TEXT),
@ -400,6 +405,7 @@ fn func_model(
keywords: func.keywords(), keywords: func.keywords(),
oneliner: oneliner(details), oneliner: oneliner(details),
element: func.element().is_some(), element: func.element().is_some(),
contextual: func.contextual().unwrap_or(false),
details: Html::markdown(resolver, details, nesting), details: Html::markdown(resolver, details, nesting),
example: example.map(|md| Html::markdown(resolver, md, None)), example: example.map(|md| Html::markdown(resolver, md, None)),
self_, self_,

View File

@ -43,6 +43,7 @@ fn resolve_known(head: &str, base: &str) -> Option<String> {
"$syntax" => format!("{base}reference/syntax"), "$syntax" => format!("{base}reference/syntax"),
"$styling" => format!("{base}reference/styling"), "$styling" => format!("{base}reference/styling"),
"$scripting" => format!("{base}reference/scripting"), "$scripting" => format!("{base}reference/scripting"),
"$context" => format!("{base}reference/context"),
"$guides" => format!("{base}guides"), "$guides" => format!("{base}guides"),
"$packages" => format!("{base}packages"), "$packages" => format!("{base}packages"),
"$changelog" => format!("{base}changelog"), "$changelog" => format!("{base}changelog"),

View File

@ -87,6 +87,7 @@ pub struct FuncModel {
pub keywords: &'static [&'static str], pub keywords: &'static [&'static str],
pub oneliner: &'static str, pub oneliner: &'static str,
pub element: bool, pub element: bool,
pub contextual: bool,
pub details: Html, pub details: Html,
/// This example is only for nested function models. Others can have /// This example is only for nested function models. Others can have
/// their example directly in their details. /// their example directly in their details.

View File

@ -2,32 +2,36 @@ use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec}; use ecow::{eco_vec, EcoString, EcoVec};
use typst::engine::{Engine, Route}; use typst::engine::{Engine, Route};
use typst::eval::{Tracer, Vm}; 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::introspection::{Introspector, Locator};
use typst::model::{BibliographyElem, Document}; use typst::model::{BibliographyElem, Document};
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use typst::World; use typst::World;
/// Try to determine a set of possible values for an expression. /// Try to determine a set of possible values for an expression.
pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<Value> { pub fn analyze_expr(
match node.cast::<ast::Expr>() { world: &dyn World,
Some(ast::Expr::None(_)) => eco_vec![Value::None], node: &LinkedNode,
Some(ast::Expr::Auto(_)) => eco_vec![Value::Auto], ) -> EcoVec<(Value, Option<Styles>)> {
Some(ast::Expr::Bool(v)) => eco_vec![Value::Bool(v.get())], let Some(expr) = node.cast::<ast::Expr>() else {
Some(ast::Expr::Int(v)) => eco_vec![Value::Int(v.get())], return eco_vec![];
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())],
Some(ast::Expr::FieldAccess(access)) => { let val = match expr {
let Some(child) = node.children().next() else { return eco_vec![] }; ast::Expr::None(_) => Value::None,
analyze_expr(world, &child) ast::Expr::Auto(_) => Value::Auto,
.into_iter() ast::Expr::Bool(v) => Value::Bool(v.get()),
.filter_map(|target| target.field(&access.field()).ok()) ast::Expr::Int(v) => Value::Int(v.get()),
.collect() 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 let Some(parent) = node.parent() {
if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 {
return analyze_expr(world, parent); 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(); let mut tracer = Tracer::new();
tracer.inspect(node.span()); tracer.inspect(node.span());
typst::compile(world, &mut tracer).ok(); 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. /// Try to load a module from the current source file.
pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> { 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() { if source.scope().is_some() {
return Some(source); return Some(source);
} }
@ -62,7 +66,9 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
tracer: tracer.track_mut(), 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) typst::eval::import(&mut vm, source, Span::detached(), true)
.ok() .ok()
.map(Value::Module) .map(Value::Module)

View File

@ -6,7 +6,7 @@ use if_chain::if_chain;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typst::foundations::{ use typst::foundations::{
fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, 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::model::Document;
use typst::syntax::{ 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. // Directly after a raw block.
let mut s = Scanner::new(ctx.text); let mut s = Scanner::new(ctx.text);
s.jump(ctx.leaf.offset()); s.jump(ctx.leaf.offset());
@ -333,10 +344,10 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if prev.is::<ast::Expr>(); if prev.is::<ast::Expr>();
if prev.parent_kind() != Some(SyntaxKind::Markup) || if prev.parent_kind() != Some(SyntaxKind::Markup) ||
prev.prev_sibling_kind() == Some(SyntaxKind::Hash); 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 { then {
ctx.from = ctx.cursor; ctx.from = ctx.cursor;
field_access_completions(ctx, &value); field_access_completions(ctx, &value, &styles);
return true; return true;
} }
} }
@ -348,10 +359,10 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if prev.kind() == SyntaxKind::Dot; if prev.kind() == SyntaxKind::Dot;
if let Some(prev_prev) = prev.prev_sibling(); if let Some(prev_prev) = prev.prev_sibling();
if prev_prev.is::<ast::Expr>(); 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 { then {
ctx.from = ctx.leaf.offset(); ctx.from = ctx.leaf.offset();
field_access_completions(ctx, &value); field_access_completions(ctx, &value, &styles);
return true; return true;
} }
} }
@ -360,7 +371,11 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
} }
/// Add completions for all fields on a value. /// 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() { for (name, value) in value.ty().scope().iter() {
ctx.value_completion(Some(name.clone()), value, true, None); 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); 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) => { Value::Plugin(plugin) => {
for name in plugin.iter() { for name in plugin.iter() {
ctx.completions.push(Completion { ctx.completions.push(Completion {
@ -862,6 +894,12 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
"Transforms everything that follows.", "Transforms everything that follows.",
); );
ctx.snippet_completion(
"context expression",
"context ${}",
"Provides contextual data.",
);
ctx.snippet_completion( ctx.snippet_completion(
"let binding", "let binding",
"let ${name} = ${value}", "let ${name} = ${value}",

View File

@ -3,7 +3,7 @@ use std::fmt::Write;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use typst::eval::{CapturesVisitor, Tracer}; 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::layout::Length;
use typst::model::Document; use typst::model::Document;
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind}; 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); let values = analyze_expr(world, ancestor);
if let [value] = values.as_slice() { if let [(value, _)] = values.as_slice() {
if let Some(docs) = value.docs() { if let Some(docs) = value.docs() {
return Some(Tooltip::Text(plain_docs_sentence(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 last = None;
let mut pieces: Vec<EcoString> = vec![]; let mut pieces: Vec<EcoString> = vec![];
let mut iter = values.iter(); 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 let Some((prev, count)) = &mut last {
if *prev == value { if *prev == value {
*count += 1; *count += 1;
@ -120,7 +120,7 @@ fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> {
} }
// Analyze the closure's captures. // Analyze the closure's captures.
let mut visitor = CapturesVisitor::new(None); let mut visitor = CapturesVisitor::new(None, Capturer::Function);
visitor.visit(parent); visitor.visit(parent);
let captures = visitor.finish(); let captures = visitor.finish();

View File

@ -617,6 +617,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
vtable: <#ident as #foundations::Capable>::vtable, vtable: <#ident as #foundations::Capable>::vtable,
field_id: |name| name.parse().ok().map(|id: Fields| id as u8), field_id: |name| name.parse().ok().map(|id: Fields| id as u8),
field_name: |id| id.try_into().ok().map(Fields::to_str), 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, local_name: #local_name,
scope: #foundations::Lazy::new(|| #scope), scope: #foundations::Lazy::new(|| #scope),
params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]) params: #foundations::Lazy::new(|| ::std::vec![#(#params),*])
@ -866,6 +867,20 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
quote! { Fields::#enum_ident => #expr } 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. // Sets fields from the style chain.
let materializes = visible_non_ghost() let materializes = visible_non_ghost()
.filter(|field| !field.required && !field.synthesized) .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) { fn materialize(&mut self, styles: #foundations::StyleChain) {
#(#materializes)* #(#materializes)*
} }

View File

@ -24,6 +24,7 @@ struct Func {
constructor: bool, constructor: bool,
keywords: Vec<String>, keywords: Vec<String>,
parent: Option<syn::Type>, parent: Option<syn::Type>,
contextual: bool,
docs: String, docs: String,
vis: syn::Visibility, vis: syn::Visibility,
ident: Ident, ident: Ident,
@ -37,6 +38,7 @@ struct Func {
struct SpecialParams { struct SpecialParams {
self_: Option<Param>, self_: Option<Param>,
engine: bool, engine: bool,
context: bool,
args: bool, args: bool,
span: bool, span: bool,
} }
@ -67,6 +69,7 @@ enum Binding {
/// The `..` in `#[func(..)]`. /// The `..` in `#[func(..)]`.
pub struct Meta { pub struct Meta {
pub scope: bool, pub scope: bool,
pub contextual: bool,
pub name: Option<String>, pub name: Option<String>,
pub title: Option<String>, pub title: Option<String>,
pub constructor: bool, pub constructor: bool,
@ -78,6 +81,7 @@ impl Parse for Meta {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
Ok(Self { Ok(Self {
scope: parse_flag::<kw::scope>(input)?, scope: parse_flag::<kw::scope>(input)?,
contextual: parse_flag::<kw::contextual>(input)?,
name: parse_string::<kw::name>(input)?, name: parse_string::<kw::name>(input)?,
title: parse_string::<kw::title>(input)?, title: parse_string::<kw::title>(input)?,
constructor: parse_flag::<kw::constructor>(input)?, constructor: parse_flag::<kw::constructor>(input)?,
@ -117,6 +121,7 @@ fn parse(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> {
constructor: meta.constructor, constructor: meta.constructor,
keywords: meta.keywords, keywords: meta.keywords,
parent: meta.parent, parent: meta.parent,
contextual: meta.contextual,
docs, docs,
vis: item.vis.clone(), vis: item.vis.clone(),
ident: item.sig.ident.clone(), ident: item.sig.ident.clone(),
@ -171,6 +176,7 @@ fn parse_param(
match ident.to_string().as_str() { match ident.to_string().as_str() {
"engine" => special.engine = true, "engine" => special.engine = true,
"context" => special.context = true,
"args" => special.args = true, "args" => special.args = true,
"span" => special.span = true, "span" => special.span = true,
_ => { _ => {
@ -247,6 +253,7 @@ fn create_func_data(func: &Func) -> TokenStream {
scope, scope,
parent, parent,
constructor, constructor,
contextual,
.. ..
} = func; } = func;
@ -272,6 +279,7 @@ fn create_func_data(func: &Func) -> TokenStream {
title: #title, title: #title,
docs: #docs, docs: #docs,
keywords: &[#(#keywords),*], keywords: &[#(#keywords),*],
contextual: #contextual,
scope: #foundations::Lazy::new(|| #scope), scope: #foundations::Lazy::new(|| #scope),
params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]), params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]),
returns: #foundations::Lazy::new(|| <#returns as #foundations::Reflect>::output()), returns: #foundations::Lazy::new(|| <#returns as #foundations::Reflect>::output()),
@ -320,12 +328,13 @@ fn create_wrapper_closure(func: &Func) -> TokenStream {
.as_ref() .as_ref()
.map(bind) .map(bind)
.map(|tokens| quote! { #tokens, }); .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 args_ = func.special.args.then(|| quote! { args, });
let span_ = func.special.span.then(|| quote! { args.span, }); let span_ = func.special.span.then(|| quote! { args.span, });
let forwarded = func.params.iter().filter(|param| !param.external).map(bind); let forwarded = func.params.iter().filter(|param| !param.external).map(bind);
quote! { 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 ident = &func.ident;
let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); let parent = func.parent.as_ref().map(|ty| quote! { #ty:: });
quote! { quote! {
|engine, args| { |engine, context, args| {
let __typst_func = #parent #ident; let __typst_func = #parent #ident;
#handlers #handlers
#finish #finish

View File

@ -40,6 +40,8 @@ use syn::DeriveInput;
/// You can customize some properties of the resulting function: /// You can customize some properties of the resulting function:
/// - `scope`: Indicates that the function has an associated scope defined by /// - `scope`: Indicates that the function has an associated scope defined by
/// the `#[scope]` macro. /// 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`: The functions's normal name (e.g. `min`). Defaults to the Rust
/// name in kebab-case. /// name in kebab-case.
/// - `title`: The functions's title case name (e.g. `Minimum`). Defaults to the /// - `title`: The functions's title case name (e.g. `Minimum`). Defaults to the

View File

@ -255,6 +255,7 @@ pub mod kw {
syn::custom_keyword!(span); syn::custom_keyword!(span);
syn::custom_keyword!(title); syn::custom_keyword!(title);
syn::custom_keyword!(scope); syn::custom_keyword!(scope);
syn::custom_keyword!(contextual);
syn::custom_keyword!(cast); syn::custom_keyword!(cast);
syn::custom_keyword!(constructor); syn::custom_keyword!(constructor);
syn::custom_keyword!(keywords); syn::custom_keyword!(keywords);

View File

@ -187,6 +187,8 @@ pub enum Expr<'a> {
Set(SetRule<'a>), Set(SetRule<'a>),
/// A show rule: `show heading: it => emph(it.body)`. /// A show rule: `show heading: it => emph(it.body)`.
Show(ShowRule<'a>), Show(ShowRule<'a>),
/// A contextual expression: `context text.lang`.
Contextual(Contextual<'a>),
/// An if-else conditional: `if x { y } else { z }`. /// An if-else conditional: `if x { y } else { z }`.
Conditional(Conditional<'a>), Conditional(Conditional<'a>),
/// A while loop: `while x { y }`. /// 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::DestructAssignment => node.cast().map(Self::DestructAssign),
SyntaxKind::SetRule => node.cast().map(Self::Set), SyntaxKind::SetRule => node.cast().map(Self::Set),
SyntaxKind::ShowRule => node.cast().map(Self::Show), SyntaxKind::ShowRule => node.cast().map(Self::Show),
SyntaxKind::Contextual => node.cast().map(Self::Contextual),
SyntaxKind::Conditional => node.cast().map(Self::Conditional), SyntaxKind::Conditional => node.cast().map(Self::Conditional),
SyntaxKind::WhileLoop => node.cast().map(Self::While), SyntaxKind::WhileLoop => node.cast().map(Self::While),
SyntaxKind::ForLoop => node.cast().map(Self::For), 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::DestructAssign(v) => v.to_untyped(),
Self::Set(v) => v.to_untyped(), Self::Set(v) => v.to_untyped(),
Self::Show(v) => v.to_untyped(), Self::Show(v) => v.to_untyped(),
Self::Contextual(v) => v.to_untyped(),
Self::Conditional(v) => v.to_untyped(), Self::Conditional(v) => v.to_untyped(),
Self::While(v) => v.to_untyped(), Self::While(v) => v.to_untyped(),
Self::For(v) => v.to_untyped(), Self::For(v) => v.to_untyped(),
@ -361,6 +365,7 @@ impl Expr<'_> {
| Self::Let(_) | Self::Let(_)
| Self::Set(_) | Self::Set(_)
| Self::Show(_) | Self::Show(_)
| Self::Contextual(_)
| Self::Conditional(_) | Self::Conditional(_)
| Self::While(_) | Self::While(_)
| Self::For(_) | Self::For(_)
@ -1946,6 +1951,18 @@ impl<'a> ShowRule<'a> {
} }
} }
node! {
/// A contextual expression: `context text.lang`.
Contextual
}
impl<'a> Contextual<'a> {
/// The expression which depends on the context.
pub fn body(self) -> Expr<'a> {
self.0.cast_first_match().unwrap_or_default()
}
}
node! { node! {
/// An if-else conditional: `if x { y } else { z }`. /// An if-else conditional: `if x { y } else { z }`.
Conditional Conditional

View File

@ -230,6 +230,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
SyntaxKind::Let => Some(Tag::Keyword), SyntaxKind::Let => Some(Tag::Keyword),
SyntaxKind::Set => Some(Tag::Keyword), SyntaxKind::Set => Some(Tag::Keyword),
SyntaxKind::Show => Some(Tag::Keyword), SyntaxKind::Show => Some(Tag::Keyword),
SyntaxKind::Context => Some(Tag::Keyword),
SyntaxKind::If => Some(Tag::Keyword), SyntaxKind::If => Some(Tag::Keyword),
SyntaxKind::Else => Some(Tag::Keyword), SyntaxKind::Else => Some(Tag::Keyword),
SyntaxKind::For => Some(Tag::Keyword), SyntaxKind::For => Some(Tag::Keyword),
@ -267,6 +268,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
SyntaxKind::LetBinding => None, SyntaxKind::LetBinding => None,
SyntaxKind::SetRule => None, SyntaxKind::SetRule => None,
SyntaxKind::ShowRule => None, SyntaxKind::ShowRule => None,
SyntaxKind::Contextual => None,
SyntaxKind::Conditional => None, SyntaxKind::Conditional => None,
SyntaxKind::WhileLoop => None, SyntaxKind::WhileLoop => None,
SyntaxKind::ForLoop => None, SyntaxKind::ForLoop => None,

View File

@ -159,6 +159,8 @@ pub enum SyntaxKind {
Set, Set,
/// The `show` keyword. /// The `show` keyword.
Show, Show,
/// The `context` keyword.
Context,
/// The `if` keyword. /// The `if` keyword.
If, If,
/// The `else` keyword. /// The `else` keyword.
@ -232,6 +234,8 @@ pub enum SyntaxKind {
SetRule, SetRule,
/// A show rule: `show heading: it => emph(it.body)`. /// A show rule: `show heading: it => emph(it.body)`.
ShowRule, ShowRule,
/// A contextual expression: `context text.lang`.
Contextual,
/// An if-else conditional: `if x { y } else { z }`. /// An if-else conditional: `if x { y } else { z }`.
Conditional, Conditional,
/// A while loop: `while x { y }`. /// A while loop: `while x { y }`.
@ -322,6 +326,7 @@ impl SyntaxKind {
| Self::Let | Self::Let
| Self::Set | Self::Set
| Self::Show | Self::Show
| Self::Context
| Self::If | Self::If
| Self::Else | Self::Else
| Self::For | Self::For
@ -426,6 +431,7 @@ impl SyntaxKind {
Self::Let => "keyword `let`", Self::Let => "keyword `let`",
Self::Set => "keyword `set`", Self::Set => "keyword `set`",
Self::Show => "keyword `show`", Self::Show => "keyword `show`",
Self::Context => "keyword `context`",
Self::If => "keyword `if`", Self::If => "keyword `if`",
Self::Else => "keyword `else`", Self::Else => "keyword `else`",
Self::For => "keyword `for`", Self::For => "keyword `for`",
@ -462,6 +468,7 @@ impl SyntaxKind {
Self::LetBinding => "`let` expression", Self::LetBinding => "`let` expression",
Self::SetRule => "`set` expression", Self::SetRule => "`set` expression",
Self::ShowRule => "`show` expression", Self::ShowRule => "`show` expression",
Self::Contextual => "`context` expression",
Self::Conditional => "`if` expression", Self::Conditional => "`if` expression",
Self::WhileLoop => "while-loop expression", Self::WhileLoop => "while-loop expression",
Self::ForLoop => "for-loop expression", Self::ForLoop => "for-loop expression",

View File

@ -616,6 +616,7 @@ fn keyword(ident: &str) -> Option<SyntaxKind> {
"let" => SyntaxKind::Let, "let" => SyntaxKind::Let,
"set" => SyntaxKind::Set, "set" => SyntaxKind::Set,
"show" => SyntaxKind::Show, "show" => SyntaxKind::Show,
"context" => SyntaxKind::Context,
"if" => SyntaxKind::If, "if" => SyntaxKind::If,
"else" => SyntaxKind::Else, "else" => SyntaxKind::Else,
"for" => SyntaxKind::For, "for" => SyntaxKind::For,

View File

@ -750,6 +750,7 @@ fn code_primary(p: &mut Parser, atomic: bool) {
SyntaxKind::Let => let_binding(p), SyntaxKind::Let => let_binding(p),
SyntaxKind::Set => set_rule(p), SyntaxKind::Set => set_rule(p),
SyntaxKind::Show => show_rule(p), SyntaxKind::Show => show_rule(p),
SyntaxKind::Context => contextual(p, atomic),
SyntaxKind::If => conditional(p), SyntaxKind::If => conditional(p),
SyntaxKind::While => while_loop(p), SyntaxKind::While => while_loop(p),
SyntaxKind::For => for_loop(p), SyntaxKind::For => for_loop(p),
@ -889,6 +890,14 @@ fn show_rule(p: &mut Parser) {
p.wrap(m, SyntaxKind::ShowRule); 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 }`. /// Parses an if-else conditional: `if x { y } else { z }`.
fn conditional(p: &mut Parser) { fn conditional(p: &mut Parser) {
let m = p.marker(); let m = p.marker();

View File

@ -102,6 +102,7 @@ pub const ATOMIC_CODE_PRIMARY: SyntaxSet = SyntaxSet::new()
.add(SyntaxKind::Let) .add(SyntaxKind::Let)
.add(SyntaxKind::Set) .add(SyntaxKind::Set)
.add(SyntaxKind::Show) .add(SyntaxKind::Show)
.add(SyntaxKind::Context)
.add(SyntaxKind::If) .add(SyntaxKind::If)
.add(SyntaxKind::While) .add(SyntaxKind::While)
.add(SyntaxKind::For) .add(SyntaxKind::For)

View File

@ -34,6 +34,7 @@ icu_provider = { workspace = true }
icu_provider_adapters = { workspace = true } icu_provider_adapters = { workspace = true }
icu_provider_blob = { workspace = true } icu_provider_blob = { workspace = true }
icu_segmenter = { workspace = true } icu_segmenter = { workspace = true }
if_chain = { workspace = true }
image = { workspace = true } image = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
kamadak-exif = { workspace = true } kamadak-exif = { workspace = true }

View File

@ -304,9 +304,12 @@ pub struct HintedString {
pub hints: Vec<EcoString>, pub hints: Vec<EcoString>,
} }
impl From<EcoString> for HintedString { impl<S> From<S> for HintedString
fn from(value: EcoString) -> Self { where
Self { message: value, hints: vec![] } S: Into<EcoString>,
{
fn from(value: S) -> Self {
Self { message: value.into(), hints: vec![] }
} }
} }

View File

@ -29,10 +29,12 @@ impl Access for ast::Expr<'_> {
impl Access for ast::Ident<'_> { impl Access for ast::Ident<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
let span = self.span(); let span = self.span();
let value = vm.scopes.get_mut(&self).at(span)?;
if vm.inspected == Some(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) Ok(value)
} }
} }

View File

@ -5,8 +5,8 @@ use crate::diag::{bail, error, At, HintedStrResult, SourceResult, Trace, Tracepo
use crate::engine::Engine; use crate::engine::Engine;
use crate::eval::{Access, Eval, FlowEvent, Route, Tracer, Vm}; use crate::eval::{Access, Eval, FlowEvent, Route, Tracer, Vm};
use crate::foundations::{ use crate::foundations::{
call_method_mut, is_mutating_method, Arg, Args, Bytes, Closure, Content, Func, call_method_mut, is_mutating_method, Arg, Args, Bytes, Capturer, Closure, Content,
IntoValue, NativeElement, Scope, Scopes, Value, Context, Func, IntoValue, NativeElement, Scope, Scopes, Value,
}; };
use crate::introspection::{Introspector, Locator}; use crate::introspection::{Introspector, Locator};
use crate::math::{Accent, AccentElem, LrElem}; use crate::math::{Accent, AccentElem, LrElem};
@ -165,7 +165,11 @@ impl Eval for ast::FuncCall<'_> {
let callee = callee.cast::<Func>().at(callee_span)?; let callee = callee.cast::<Func>().at(callee_span)?;
let point = || Tracepoint::Call(callee.name().map(Into::into)); 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. // Stacker is broken on WASM.
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -242,7 +246,7 @@ impl Eval for ast::Closure<'_> {
// Collect captured variables. // Collect captured variables.
let captured = { 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.visit(self.to_untyped());
visitor.finish() visitor.finish()
}; };
@ -252,6 +256,11 @@ impl Eval for ast::Closure<'_> {
node: self.to_untyped().clone(), node: self.to_untyped().clone(),
defaults, defaults,
captured, 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()))) Ok(Value::Func(Func::from(closure).spanned(self.params().span())))
@ -269,9 +278,13 @@ pub(crate) fn call_closure(
route: Tracked<Route>, route: Tracked<Route>,
locator: Tracked<Locator>, locator: Tracked<Locator>,
tracer: TrackedMut<Tracer>, tracer: TrackedMut<Tracer>,
context: &Context,
mut args: Args, mut args: Args,
) -> SourceResult<Value> { ) -> 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 // Don't leak the scopes from the call site. Instead, we use the scope
// of captured variables we collected earlier. // of captured variables we collected earlier.
@ -289,27 +302,20 @@ pub(crate) fn call_closure(
}; };
// Prepare VM. // 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. // 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())); 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 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 = None;
let mut sink_pos_values = None; let mut sink_pos_values = None;
let mut defaults = closure.defaults.iter(); let mut defaults = closure.defaults.iter();
for p in node.params().children() { for p in params.children() {
match p { match p {
ast::Param::Pos(pattern) => match pattern { ast::Param::Pos(pattern) => match pattern {
ast::Pattern::Normal(ast::Expr::Ident(ident)) => { ast::Pattern::Normal(ast::Expr::Ident(ident)) => {
@ -354,7 +360,7 @@ pub(crate) fn call_closure(
args.finish()?; args.finish()?;
// Handle control flow. // Handle control flow.
let output = node.body().eval(&mut vm)?; let output = body.eval(&mut vm)?;
match vm.flow { match vm.flow {
Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit), Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit),
Some(FlowEvent::Return(_, None)) => {} Some(FlowEvent::Return(_, None)) => {}
@ -378,15 +384,17 @@ pub struct CapturesVisitor<'a> {
external: Option<&'a Scopes<'a>>, external: Option<&'a Scopes<'a>>,
internal: Scopes<'a>, internal: Scopes<'a>,
captures: Scope, captures: Scope,
capturer: Capturer,
} }
impl<'a> CapturesVisitor<'a> { impl<'a> CapturesVisitor<'a> {
/// Create a new visitor for the given external scopes. /// 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 { Self {
external, external,
internal: Scopes::new(None), internal: Scopes::new(None),
captures: Scope::new(), captures: Scope::new(),
capturer,
} }
} }
@ -530,7 +538,7 @@ impl<'a> CapturesVisitor<'a> {
return; 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("y", 0);
scopes.top.define("z", 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); let root = parse(text);
visitor.visit(&root); visitor.visit(&root);

View File

@ -1,8 +1,10 @@
use ecow::{eco_vec, EcoVec}; use ecow::{eco_vec, EcoVec};
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult}; use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use crate::eval::{ops, Eval, Vm}; use crate::eval::{ops, CapturesVisitor, Eval, Vm};
use crate::foundations::{Array, Content, Dict, Str, Value}; use crate::foundations::{
Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, Value,
};
use crate::syntax::ast::{self, AstNode}; use crate::syntax::ast::{self, AstNode};
impl Eval for ast::Code<'_> { impl Eval for ast::Code<'_> {
@ -40,7 +42,11 @@ fn eval_code<'a>(
} }
let tail = eval_code(vm, exprs)?.display(); 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)?, _ => expr.eval(vm)?,
}; };
@ -117,6 +123,7 @@ impl Eval for ast::Expr<'_> {
Self::DestructAssign(v) => v.eval(vm), Self::DestructAssign(v) => v.eval(vm),
Self::Set(_) => bail!(forbidden("set")), Self::Set(_) => bail!(forbidden("set")),
Self::Show(_) => bail!(forbidden("show")), Self::Show(_) => bail!(forbidden("show")),
Self::Contextual(v) => v.eval(vm).map(Value::Content),
Self::Conditional(v) => v.eval(vm), Self::Conditional(v) => v.eval(vm),
Self::While(v) => v.eval(vm), Self::While(v) => v.eval(vm),
Self::For(v) => v.eval(vm), Self::For(v) => v.eval(vm),
@ -129,7 +136,7 @@ impl Eval for ast::Expr<'_> {
.spanned(span); .spanned(span);
if vm.inspected == Some(span) { if vm.inspected == Some(span) {
vm.engine.tracer.value(v.clone()); vm.trace(v.clone());
} }
Ok(v) Ok(v)
@ -296,6 +303,56 @@ impl Eval for ast::FieldAccess<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.target().eval(vm)?; let value = self.target().eval(vm)?;
let field = self.field(); 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())
} }
} }

View File

@ -43,7 +43,7 @@ fn eval_markup<'a>(
} }
let tail = eval_markup(vm, exprs)?; 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)? { expr => match expr.eval(vm)? {
Value::Label(label) => { Value::Label(label) => {

View File

@ -27,7 +27,7 @@ use comemo::{Track, Tracked, TrackedMut};
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route}; 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::introspection::{Introspector, Locator};
use crate::math::EquationElem; use crate::math::EquationElem;
use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span}; use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span};
@ -60,9 +60,10 @@ pub fn eval(
}; };
// Prepare VM. // Prepare VM.
let root = source.root(); let context = Context::none();
let scopes = Scopes::new(Some(world.library())); 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. // Check for well-formedness unless we are in trace mode.
let errors = root.errors(); let errors = root.errors();
@ -128,8 +129,9 @@ pub fn eval_string(
}; };
// Prepare VM. // Prepare VM.
let context = Context::none();
let scopes = Scopes::new(Some(world.library())); 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); vm.scopes.scopes.push(scope);
// Evaluate the code. // Evaluate the code.

View File

@ -3,7 +3,7 @@ use std::collections::HashSet;
use ecow::EcoVec; use ecow::EcoVec;
use crate::diag::SourceDiagnostic; use crate::diag::SourceDiagnostic;
use crate::foundations::Value; use crate::foundations::{Styles, Value};
use crate::syntax::{FileId, Span}; use crate::syntax::{FileId, Span};
use crate::util::hash128; use crate::util::hash128;
@ -14,7 +14,7 @@ pub struct Tracer {
warnings: EcoVec<SourceDiagnostic>, warnings: EcoVec<SourceDiagnostic>,
warnings_set: HashSet<u128>, warnings_set: HashSet<u128>,
delayed: EcoVec<SourceDiagnostic>, delayed: EcoVec<SourceDiagnostic>,
values: EcoVec<Value>, values: EcoVec<(Value, Option<Styles>)>,
} }
impl Tracer { impl Tracer {
@ -43,7 +43,7 @@ impl Tracer {
} }
/// Get the values for the inspected span. /// Get the values for the inspected span.
pub fn values(self) -> EcoVec<Value> { pub fn values(self) -> EcoVec<(Value, Option<Styles>)> {
self.values self.values
} }
} }
@ -74,9 +74,9 @@ impl Tracer {
} }
/// Trace a value for the span. /// 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 { if self.values.len() < Self::MAX_VALUES {
self.values.push(v); self.values.push((v, s));
} }
} }
} }

View File

@ -2,7 +2,7 @@ use comemo::Tracked;
use crate::engine::Engine; use crate::engine::Engine;
use crate::eval::FlowEvent; 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::ast::{self, AstNode};
use crate::syntax::Span; use crate::syntax::Span;
use crate::World; use crate::World;
@ -20,13 +20,20 @@ pub struct Vm<'a> {
pub(crate) scopes: Scopes<'a>, pub(crate) scopes: Scopes<'a>,
/// A span that is currently under inspection. /// A span that is currently under inspection.
pub(crate) inspected: Option<Span>, 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> { impl<'a> Vm<'a> {
/// Create a new virtual machine. /// 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)); 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. /// Access the underlying world.
@ -38,8 +45,16 @@ impl<'a> Vm<'a> {
pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) {
let value = value.into_value(); let value = value.into_value();
if self.inspected == Some(var.span()) { if self.inspected == Some(var.span()) {
self.engine.tracer.value(value.clone()); self.trace(value.clone());
} }
self.scopes.top.define(var.get().clone(), value); 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()));
}
} }

View File

@ -11,8 +11,8 @@ use crate::diag::{At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::eval::ops; use crate::eval::ops;
use crate::foundations::{ use crate::foundations::{
cast, func, repr, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue, cast, func, repr, scope, ty, Args, Bytes, CastInfo, Context, FromValue, Func,
Reflect, Repr, Value, Version, IntoValue, Reflect, Repr, Value, Version,
}; };
use crate::syntax::Span; use crate::syntax::Span;
@ -300,12 +300,14 @@ impl Array {
&self, &self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
searcher: Func, searcher: Func,
) -> SourceResult<Option<Value>> { ) -> SourceResult<Option<Value>> {
for item in self.iter() { for item in self.iter() {
if searcher if searcher
.call(engine, [item.clone()])? .call(engine, context, [item.clone()])?
.cast::<bool>() .cast::<bool>()
.at(searcher.span())? .at(searcher.span())?
{ {
@ -322,12 +324,14 @@ impl Array {
&self, &self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
searcher: Func, searcher: Func,
) -> SourceResult<Option<i64>> { ) -> SourceResult<Option<i64>> {
for (i, item) in self.iter().enumerate() { for (i, item) in self.iter().enumerate() {
if searcher if searcher
.call(engine, [item.clone()])? .call(engine, context, [item.clone()])?
.cast::<bool>() .cast::<bool>()
.at(searcher.span())? .at(searcher.span())?
{ {
@ -397,12 +401,18 @@ impl Array {
&self, &self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
) -> SourceResult<Array> { ) -> SourceResult<Array> {
let mut kept = EcoVec::new(); let mut kept = EcoVec::new();
for item in self.iter() { 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()) kept.push(item.clone())
} }
} }
@ -416,10 +426,14 @@ impl Array {
self, self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The function to apply to each item. /// The function to apply to each item.
mapper: Func, mapper: Func,
) -> SourceResult<Array> { ) -> 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. /// Returns a new array with the values alongside their indices.
@ -521,6 +535,8 @@ impl Array {
self, self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The initial value to start with. /// The initial value to start with.
init: Value, init: Value,
/// The folding function. Must have two parameters: One for the /// The folding function. Must have two parameters: One for the
@ -529,7 +545,7 @@ impl Array {
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let mut acc = init; let mut acc = init;
for item in self { for item in self {
acc = folder.call(engine, [acc, item])?; acc = folder.call(engine, context, [acc, item])?;
} }
Ok(acc) Ok(acc)
} }
@ -581,11 +597,13 @@ impl Array {
self, self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
) -> SourceResult<bool> { ) -> SourceResult<bool> {
for item in self { 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); return Ok(true);
} }
} }
@ -599,11 +617,13 @@ impl Array {
self, self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
) -> SourceResult<bool> { ) -> SourceResult<bool> {
for item in self { 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); return Ok(false);
} }
} }
@ -714,6 +734,8 @@ impl Array {
self, self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span. /// The callsite span.
span: Span, span: Span,
/// If given, applies this function to the elements in the array to /// 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 { let mut key_of = |x: Value| match &key {
// NOTE: We are relying on `comemo`'s memoization of function // NOTE: We are relying on `comemo`'s memoization of function
// evaluation to not excessively reevaluate the `key`. // evaluation to not excessively reevaluate the `key`.
Some(f) => f.call(engine, [x]), Some(f) => f.call(engine, context, [x]),
None => Ok(x), None => Ok(x),
}; };
vec.make_mut().sort_by(|a, b| { vec.make_mut().sort_by(|a, b| {
@ -762,6 +784,8 @@ impl Array {
self, self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// If given, applies this function to the elements in the array to /// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by. /// determine the keys to deduplicate by.
#[named] #[named]
@ -771,7 +795,7 @@ impl Array {
let mut key_of = |x: Value| match &key { let mut key_of = |x: Value| match &key {
// NOTE: We are relying on `comemo`'s memoization of function // NOTE: We are relying on `comemo`'s memoization of function
// evaluation to not excessively reevaluate the `key`. // evaluation to not excessively reevaluate the `key`.
Some(f) => f.call(engine, [x]), Some(f) => f.call(engine, context, [x]),
None => Ok(x), None => Ok(x),
}; };

View File

@ -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> { impl<T: Reflect> Reflect for SourceResult<T> {
fn input() -> CastInfo { fn input() -> CastInfo {
T::input() T::input()

View File

@ -13,8 +13,9 @@ use smallvec::smallvec;
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, func, scope, ty, Dict, Element, Fields, IntoValue, Label, NativeElement, elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label,
Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value, NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles,
Value,
}; };
use crate::introspection::{Location, Meta, MetaElem}; use crate::introspection::{Location, Meta, MetaElem};
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
@ -344,10 +345,11 @@ impl Content {
pub fn styled_with_recipe( pub fn styled_with_recipe(
self, self,
engine: &mut Engine, engine: &mut Engine,
context: &Context,
recipe: Recipe, recipe: Recipe,
) -> SourceResult<Self> { ) -> SourceResult<Self> {
if recipe.selector.is_none() { if recipe.selector.is_none() {
recipe.apply(engine, self) recipe.apply(engine, context, self)
} else { } else {
Ok(self.styled(recipe)) Ok(self.styled(recipe))
} }

View File

@ -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())
}
}

View File

@ -29,22 +29,6 @@ impl Element {
T::elem() 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`). /// The element's normal name (e.g. `enum`).
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
self.0.name self.0.name
@ -122,6 +106,27 @@ impl Element {
&(self.0).0.params &(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. /// The element's local name, if any.
pub fn local_name(&self, lang: Lang, region: Option<Region>) -> Option<&'static str> { pub fn local_name(&self, lang: Lang, region: Option<Region>) -> Option<&'static str> {
(self.0).0.local_name.map(|f| f(lang, region)) (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. /// Get the field with the given ID in the presence of styles.
fn field_with_styles(&self, id: u8, styles: StyleChain) -> Option<Value>; 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. /// Resolve all fields with the styles and save them in-place.
fn materialize(&mut self, styles: StyleChain); fn materialize(&mut self, styles: StyleChain);
@ -260,6 +270,7 @@ pub struct NativeElementData {
pub vtable: fn(capability: TypeId) -> Option<*const ()>, pub vtable: fn(capability: TypeId) -> Option<*const ()>,
pub field_id: fn(name: &str) -> Option<u8>, pub field_id: fn(name: &str) -> Option<u8>,
pub field_name: fn(u8) -> Option<&'static str>, 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 local_name: Option<fn(Lang, Option<Region>) -> &'static str>,
pub scope: Lazy<Scope>, pub scope: Lazy<Scope>,
pub params: Lazy<Vec<ParamInfo>>, pub params: Lazy<Vec<ParamInfo>>,

View File

@ -8,8 +8,8 @@ use once_cell::sync::Lazy;
use crate::diag::{bail, SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, repr, scope, ty, Args, CastInfo, Content, Element, IntoArgs, Scope, Selector, cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope,
Type, Value, Selector, Type, Value,
}; };
use crate::syntax::{ast, Span, SyntaxNode}; use crate::syntax::{ast, Span, SyntaxNode};
use crate::util::{LazyHash, Static}; 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. /// Get details about this function's parameters if available.
pub fn params(&self) -> Option<&'static [ParamInfo]> { pub fn params(&self) -> Option<&'static [ParamInfo]> {
match &self.repr { match &self.repr {
@ -249,17 +257,27 @@ impl Func {
} }
} }
/// Call the function with the given arguments. /// Call the function with the given context and arguments.
pub fn call(&self, engine: &mut Engine, args: impl IntoArgs) -> SourceResult<Value> { pub fn call<A: IntoArgs>(
self.call_impl(engine, args.into_args(self.span)) &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`. /// Non-generic implementation of `call`.
#[typst_macros::time(name = "func call", span = self.span())] #[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 { match &self.repr {
Repr::Native(native) => { Repr::Native(native) => {
let value = (native.function)(engine, &mut args)?; let value = (native.function)(engine, context, &mut args)?;
args.finish()?; args.finish()?;
Ok(value) Ok(value)
} }
@ -276,11 +294,12 @@ impl Func {
engine.route.track(), engine.route.track(),
engine.locator.track(), engine.locator.track(),
TrackedMut::reborrow_mut(&mut engine.tracer), TrackedMut::reborrow_mut(&mut engine.tracer),
context,
args, args,
), ),
Repr::With(with) => { Repr::With(with) => {
args.items = with.1.items.iter().cloned().chain(args.items).collect(); 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. /// Defines a native function.
#[derive(Debug)] #[derive(Debug)]
pub struct NativeFuncData { 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 name: &'static str,
pub title: &'static str, pub title: &'static str,
pub docs: &'static str, pub docs: &'static str,
pub keywords: &'static [&'static str], pub keywords: &'static [&'static str],
pub contextual: bool,
pub scope: Lazy<Scope>, pub scope: Lazy<Scope>,
pub params: Lazy<Vec<ParamInfo>>, pub params: Lazy<Vec<ParamInfo>>,
pub returns: Lazy<CastInfo>, pub returns: Lazy<CastInfo>,
@ -464,22 +484,22 @@ pub struct ParamInfo {
/// A user-defined closure. /// A user-defined closure.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct Closure { 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, pub node: SyntaxNode,
/// Default values of named parameters. /// Default values of named parameters.
pub defaults: Vec<Value>, pub defaults: Vec<Value>,
/// Captured values from outer scopes. /// Captured values from outer scopes.
pub captured: Scope, pub captured: Scope,
/// The number of positional parameters in the closure.
pub num_pos_params: usize,
} }
impl Closure { impl Closure {
/// The name of the closure. /// The name of the closure.
pub fn name(&self) -> Option<&str> { pub fn name(&self) -> Option<&str> {
self.node self.node.cast::<ast::Closure>()?.name().map(|ident| ident.as_str())
.cast::<ast::Closure>()
.unwrap()
.name()
.map(|ident| ident.as_str())
} }
} }

View File

@ -11,6 +11,7 @@ mod bool;
mod bytes; mod bytes;
mod cast; mod cast;
mod content; mod content;
mod context;
mod datetime; mod datetime;
mod dict; mod dict;
mod duration; mod duration;
@ -38,6 +39,7 @@ pub use self::auto::*;
pub use self::bytes::*; pub use self::bytes::*;
pub use self::cast::*; pub use self::cast::*;
pub use self::content::*; pub use self::content::*;
pub use self::context::*;
pub use self::datetime::*; pub use self::datetime::*;
pub use self::dict::*; pub use self::dict::*;
pub use self::duration::*; pub use self::duration::*;

View File

@ -169,10 +169,15 @@ impl Scope {
} }
/// Define a captured, immutable binding. /// 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( self.map.insert(
var.into(), 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. /// A normal, mutable binding.
Normal, Normal,
/// A captured copy of another variable. /// 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 { impl Slot {
@ -264,10 +278,14 @@ impl Slot {
fn write(&mut self) -> StrResult<&mut Value> { fn write(&mut self) -> StrResult<&mut Value> {
match self.kind { match self.kind {
Kind::Normal => Ok(&mut self.value), Kind::Normal => Ok(&mut self.value),
Kind::Captured => { Kind::Captured(capturer) => {
bail!( bail!(
"variables from outside the function are \ "variables from outside the {} are \
read-only and cannot be modified" read-only and cannot be modified",
match capturer {
Capturer::Function => "function",
Capturer::Context => "context expression",
}
) )
} }
} }

View File

@ -1,15 +1,16 @@
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
use std::sync::Arc; use std::sync::Arc;
use comemo::Tracked;
use ecow::{eco_format, EcoString, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{ use crate::foundations::{
cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func, cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue,
Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value, 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::symbols::Symbol;
use crate::text::TextElem; use crate::text::TextElem;
@ -66,11 +67,10 @@ pub use crate::__select_where as select_where;
/// ///
/// # Example /// # Example
/// ```example /// ```example
/// #locate(loc => query( /// #context query(
/// heading.where(level: 1) /// heading.where(level: 1)
/// .or(heading.where(level: 2)), /// .or(heading.where(level: 2))
/// loc, /// )
/// ))
/// ///
/// = This will be found /// = This will be found
/// == So will this /// == So will this
@ -300,11 +300,29 @@ cast! {
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub struct LocatableSelector(pub Selector); 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 { impl Reflect for LocatableSelector {
fn input() -> CastInfo { fn input() -> CastInfo {
CastInfo::Union(vec![ CastInfo::Union(vec![
CastInfo::Type(Type::of::<Label>()), CastInfo::Type(Type::of::<Label>()),
CastInfo::Type(Type::of::<Func>()), CastInfo::Type(Type::of::<Func>()),
CastInfo::Type(Type::of::<Location>()),
CastInfo::Type(Type::of::<Selector>()), CastInfo::Type(Type::of::<Selector>()),
]) ])
} }
@ -314,7 +332,10 @@ impl Reflect for LocatableSelector {
} }
fn castable(value: &Value) -> bool { 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)
} }
} }

View File

@ -10,8 +10,8 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, dict, func, repr, scope, ty, Array, Bytes, Dict, Func, IntoValue, Label, Repr, cast, dict, func, repr, scope, ty, Array, Bytes, Context, Dict, Func, IntoValue,
Type, Value, Version, Label, Repr, Type, Value, Version,
}; };
use crate::layout::Alignment; use crate::layout::Alignment;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
@ -424,6 +424,8 @@ impl Str {
&self, &self,
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The pattern to search for. /// The pattern to search for.
pattern: StrPattern, pattern: StrPattern,
/// The string to replace the matches with or a function that gets a /// The string to replace the matches with or a function that gets a
@ -449,8 +451,10 @@ impl Str {
match &replacement { match &replacement {
Replacement::Str(s) => output.push_str(s), Replacement::Str(s) => output.push_str(s),
Replacement::Func(func) => { Replacement::Func(func) => {
let piece = let piece = func
func.call(engine, [dict])?.cast::<Str>().at(func.span())?; .call(engine, context, [dict])?
.cast::<Str>()
.at(func.span())?;
output.push_str(&piece); output.push_str(&piece);
} }
} }

View File

@ -9,9 +9,10 @@ use smallvec::SmallVec;
use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, func, ty, Content, Element, Func, NativeElement, Packed, Repr, Selector, cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr,
Show, Selector, Show,
}; };
use crate::introspection::Locatable;
use crate::syntax::Span; use crate::syntax::Span;
use crate::text::{FontFamily, FontList, TextElem}; use crate::text::{FontFamily, FontList, TextElem};
use crate::util::LazyHash; use crate::util::LazyHash;
@ -24,10 +25,10 @@ use crate::util::LazyHash;
/// styles defined by [set rules]($styling/#set-rules). /// styles defined by [set rules]($styling/#set-rules).
/// ///
/// ```example /// ```example
/// #let thing(body) = style(styles => { /// #let thing(body) = context {
/// let size = measure(body, styles) /// let size = measure(body)
/// [Width of "#body" is #size.width] /// [Width of "#body" is #size.width]
/// }) /// }
/// ///
/// #thing[Hey] \ /// #thing[Hey] \
/// #thing[Welcome] /// #thing[Welcome]
@ -48,7 +49,7 @@ pub fn style(
} }
/// Executes a style access. /// Executes a style access.
#[elem(Show)] #[elem(Locatable, Show)]
struct StyleElem { struct StyleElem {
/// The function to call with the styles. /// The function to call with the styles.
#[required] #[required]
@ -58,7 +59,8 @@ struct StyleElem {
impl Show for Packed<StyleElem> { impl Show for Packed<StyleElem> {
#[typst_macros::time(name = "style", span = self.span())] #[typst_macros::time(name = "style", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { 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. /// 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 { let mut content = match &self.transform {
Transformation::Content(content) => content.clone(), Transformation::Content(content) => content.clone(),
Transformation::Func(func) => { 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() { if self.selector.is_some() {
let point = || Tracepoint::Show(content.func().name().into()); let point = || Tracepoint::Show(content.func().name().into());
result = result.trace(engine.world, point, content.span()); result = result.trace(engine.world, point, content.span());

View File

@ -5,13 +5,13 @@ use comemo::{Tracked, TrackedMut};
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use crate::diag::{At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::{Engine, Route}; use crate::engine::{Engine, Route};
use crate::eval::Tracer; use crate::eval::Tracer;
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, select_where, ty, Array, Content, Element, Func, IntoValue, cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context,
Label, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr,
StyleChain, Value, Selector, Show, Smart, Str, StyleChain, Value,
}; };
use crate::introspection::{Introspector, Locatable, Location, Locator, Meta}; use crate::introspection::{Introspector, Locatable, Location, Locator, Meta};
use crate::layout::{Frame, FrameItem, PageElem}; use crate::layout::{Frame, FrameItem, PageElem};
@ -27,14 +27,30 @@ use crate::World;
/// headings, figures, and more. Moreover, you can define custom counters for /// headings, figures, and more. Moreover, you can define custom counters for
/// other things you want to count. /// other things you want to count.
/// ///
/// # Displaying a counter { #displaying } /// Since counters change throughout the course of the document, their current
/// To display the current value of the heading counter, you call the `counter` /// value is _contextual_ It is recommended to read the chapter on
/// function with the `key` set to `heading` and then call the `display` method /// [context]($context) before continuing here.
/// on the counter. To see any output, you also have to enable heading
/// [numbering]($heading.numbering).
/// ///
/// The `display` method optionally takes an argument telling it how to format /// # Accessing a counter { #accessing }
/// the counter. This can be a [numbering pattern or a function]($numbering). /// 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.")
///
/// = 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 /// ```example
/// #set heading(numbering: "1.") /// #set heading(numbering: "1.")
@ -43,25 +59,26 @@ use crate::World;
/// Some text here. /// Some text here.
/// ///
/// = Background /// = Background
/// The current value is: /// The current value is: #context {
/// #counter(heading).display() /// counter(heading).display()
/// }
/// ///
/// Or in roman numerals: /// Or in roman numerals: #context {
/// #counter(heading).display("I") /// counter(heading).display("I")
/// }
/// ``` /// ```
/// ///
/// # Modifying a counter { #modifying } /// # Modifying a counter { #modifying }
/// To modify a counter, you can use the `step` and `update` methods: /// To modify a counter, you can use the `step` and `update` methods:
/// ///
/// - The `step` method increases the value of the counter by one. Because /// - The `step` method increases the value of the counter by one. Because
/// counters can have multiple levels (in the case of headings for sections, /// counters can have multiple levels , it optionally takes a `level`
/// subsections, and so on), the `step` method optionally takes a `level`
/// argument. If given, the counter steps at the given depth. /// argument. If given, the counter steps at the given depth.
/// ///
/// - The `update` method allows you to arbitrarily modify the counter. In its /// - 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 /// 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 gets the /// more flexibility, you can instead also give it a function that receives
/// current value and returns a new value. /// the current value and returns a new value.
/// ///
/// The heading counter is stepped before the heading is displayed, so /// 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 /// `Analysis` gets the number seven even though the counter is at six after the
@ -82,50 +99,11 @@ use crate::World;
/// #counter(heading).step(level: 2) /// #counter(heading).step(level: 2)
/// ///
/// == Analysis /// == Analysis
/// Still at #counter(heading).display(). /// Still at #context {
/// counter(heading).display()
/// }
/// ``` /// ```
/// ///
/// 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() \
/// #mine.step()
/// #mine.display() \
/// #mine.update(c => c * 3)
/// #mine.display() \
/// ```
///
/// # How to step
/// When you define and use a custom counter, in general, you should first step
/// the counter and then display it. This way, the stepping behaviour of a
/// counter can depend on the element it is stepped for. If you were writing a
/// counter for, let's say, theorems, your theorem's definition would thus first
/// include the counter step and only then display the counter and the theorem's
/// contents.
///
/// ```example
/// #let c = counter("theorem")
/// #let theorem(it) = block[
/// #c.step()
/// *Theorem #c.display():* #it
/// ]
///
/// #theorem[$1 = 1$]
/// #theorem[$2 < 3$]
/// ```
///
/// 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
/// `1.1` when encountering a level 2 heading. If we were to step after the
/// heading, we wouldn't know what to step to.
///
/// Because counters should always be stepped before the elements they count,
/// they always start at zero. This way, they are at one for the first display
/// (which happens after the first step).
///
/// # Page counter /// # Page counter
/// The page counter is special. It is automatically stepped at each pagebreak. /// 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 /// But like other counters, you can also step it manually. For example, you
@ -153,6 +131,49 @@ use crate::World;
/// Arabic numbers. /// 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")
/// #context mine.display() \
/// #mine.step()
/// #context mine.display() \
/// #mine.update(c => c * 3)
/// #context mine.display()
/// ```
///
/// # How to step
/// When you define and use a custom counter, in general, you should first step
/// the counter and then display it. This way, the stepping behaviour of a
/// counter can depend on the element it is stepped for. If you were writing a
/// counter for, let's say, theorems, your theorem's definition would thus first
/// include the counter step and only then display the counter and the theorem's
/// contents.
///
/// ```example
/// #let c = counter("theorem")
/// #let theorem(it) = block[
/// #c.step()
/// *Theorem #context c.display():*
/// #it
/// ]
///
/// #theorem[$1 = 1$]
/// #theorem[$2 < 3$]
/// ```
///
/// 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
/// `1.1` when encountering a level 2 heading. If we were to step after the
/// heading, we wouldn't know what to step to.
///
/// Because counters should always be stepped before the elements they count,
/// they always start at zero. This way, they are at one for the first display
/// (which happens after the first step).
///
/// # Time travel /// # Time travel
/// Counters can travel through time! You can find out the final value of the /// 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 /// 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") /// #let mine = counter("mycounter")
/// ///
/// = Values /// = Values
/// #locate(loc => { /// #context [
/// let start-val = mine.at(loc) /// Value here: #mine.get() \
/// let elements = query(<intro>, loc) /// At intro: #mine.at(<intro>) \
/// let intro-val = mine.at( /// Final value: #mine.final()
/// elements.first().location() /// ]
/// )
/// let final-val = mine.final(loc)
/// [Starts as: #start-val \
/// Value at intro is: #intro-val \
/// Final value is: #final-val \ ]
/// })
/// ///
/// #mine.update(n => n + 3) /// #mine.update(n => n + 3)
/// ///
@ -183,27 +198,6 @@ use crate::World;
/// #mine.step() /// #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 } /// # Other kinds of state { #other-state }
/// The `counter` type is closely related to [state]($state) type. Read its /// The `counter` type is closely related to [state]($state) type. Read its
/// documentation for more details on state management in Typst and why it /// 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()])) 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. /// Produce the whole sequence of counter states.
/// ///
/// This has to happen just once for all counters, cutting down the number /// 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. /// The selector relevant for this counter's updates.
fn selector(&self) -> Selector { 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 { if let CounterKey::Selector(key) = &self.0 {
selector = Selector::Or(eco_vec![selector, key.clone()]); selector = Selector::Or(eco_vec![selector, key.clone()]);
@ -326,6 +355,46 @@ impl Counter {
fn is_page(&self) -> bool { fn is_page(&self) -> bool {
self.0 == CounterKey::Page 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] #[scope]
@ -347,10 +416,38 @@ impl Counter {
Self(key) Self(key)
} }
/// Displays the current value of the counter. /// Retrieves the value of the counter at the current location. Always
#[func] /// 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( pub fn display(
self, self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The call span of the display. /// The call span of the display.
span: Span, span: Span,
/// A [numbering pattern or a function]($numbering), which specifies how /// 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 /// numbers varies, e.g. for the heading argument, you can use an
/// [argument sink]($arguments). /// [argument sink]($arguments).
/// ///
/// If this is omitted, displays the counter with the numbering style /// If this is omitted or set to `{auto}`, displays the counter with the
/// for the counted element or with the pattern `{"1.1"}` if no such /// numbering style for the counted element or with the pattern
/// style exists. /// `{"1.1"}` if no such style exists.
#[default] #[default]
numbering: Option<Numbering>, numbering: Smart<Numbering>,
/// If enabled, displays the current and final top-level count together. /// If enabled, displays the current and final top-level count together.
/// Both can be styled through a single numbering pattern. This is used /// Both can be styled through a single numbering pattern. This is used
/// by the page numbering property to display the current and total /// by the page numbering property to display the current and total
@ -371,8 +468,70 @@ impl Counter {
#[named] #[named]
#[default(false)] #[default(false)]
both: bool, both: bool,
) -> Content { ) -> SourceResult<Value> {
DisplayElem::new(self, numbering, both).pack().spanned(span) 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. /// Increases the value of the counter by one.
@ -411,61 +570,7 @@ impl Counter {
/// return the new value (integer or array). /// return the new value (integer or array).
update: CounterUpdate, update: CounterUpdate,
) -> Content { ) -> Content {
UpdateElem::new(self.0, update).pack().spanned(span) CounterUpdateElem::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)
} }
} }
@ -480,7 +585,8 @@ impl Repr for Counter {
pub enum CounterKey { pub enum CounterKey {
/// The page counter. /// The page counter.
Page, 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. /// elements or labels.
Selector(Selector), Selector(Selector),
/// Counts through manual counters with the same key. /// Counts through manual counters with the same key.
@ -517,7 +623,6 @@ impl Repr for CounterKey {
} }
/// An update to perform on a counter. /// An update to perform on a counter.
#[ty(cast)]
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum CounterUpdate { pub enum CounterUpdate {
/// Set the counter to the specified state. /// Set the counter to the specified state.
@ -528,14 +633,8 @@ pub enum CounterUpdate {
Func(Func), Func(Func),
} }
impl Repr for CounterUpdate {
fn repr(&self) -> EcoString {
"..".into()
}
}
cast! { cast! {
type CounterUpdate, CounterUpdate,
v: CounterState => Self::Set(v), v: CounterState => Self::Set(v),
v: Func => Self::Func(v), v: Func => Self::Func(v),
} }
@ -570,8 +669,10 @@ impl CounterState {
CounterUpdate::Set(state) => *self = state, CounterUpdate::Set(state) => *self = state,
CounterUpdate::Step(level) => self.step(level, 1), CounterUpdate::Step(level) => self.step(level, 1),
CounterUpdate::Func(func) => { CounterUpdate::Func(func) => {
*self = *self = func
func.call(engine, self.0.iter().copied())?.cast().at(func.span())? .call(engine, &Context::none(), self.0.iter().copied())?
.cast()
.at(func.span())?
} }
} }
Ok(()) Ok(())
@ -600,9 +701,10 @@ impl CounterState {
pub fn display( pub fn display(
&self, &self,
engine: &mut Engine, engine: &mut Engine,
context: &Context,
numbering: &Numbering, numbering: &Numbering,
) -> SourceResult<Content> { ) -> SourceResult<Value> {
Ok(numbering.apply(engine, &self.0)?.display()) numbering.apply(engine, context, &self.0)
} }
} }
@ -616,81 +718,80 @@ cast! {
.collect::<StrResult<_>>()?), .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.
#[required]
numbering: Option<Numbering>,
/// Whether to display both the current and final value.
#[required]
both: bool,
}
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());
let state = if *self.both() {
counter.both(engine, location)?
} else {
counter.at(engine, location)?
};
state.display(engine, &numbering)
}
}
/// Executes an update of a counter. /// Executes an update of a counter.
#[elem(Locatable, Show, Count)] #[elem(Construct, Locatable, Show, Count)]
struct UpdateElem { struct CounterUpdateElem {
/// The key that identifies the counter. /// The key that identifies the counter.
#[required] #[required]
key: CounterKey, key: CounterKey,
/// The update to perform on the counter. /// The update to perform on the counter.
#[required] #[required]
#[internal]
update: CounterUpdate, update: CounterUpdate,
} }
impl Show for Packed<UpdateElem> { impl Construct for CounterUpdateElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
impl Show for Packed<CounterUpdateElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty()) Ok(Content::empty())
} }
} }
impl Count for Packed<UpdateElem> { impl Count for Packed<CounterUpdateElem> {
fn update(&self) -> Option<CounterUpdate> { fn update(&self) -> Option<CounterUpdate> {
Some(self.update.clone()) Some(self.update.clone())
} }
} }
/// **Deprection planned.**
///
/// Executes a display of a counter.
#[elem(Construct, Locatable, Show)]
pub struct CounterDisplayElem {
/// The counter.
#[required]
#[internal]
counter: Counter,
/// The numbering to display the counter with.
#[required]
#[internal]
numbering: Smart<Numbering>,
/// Whether to display both the current and final value.
#[required]
#[internal]
both: bool,
}
impl Construct for CounterDisplayElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
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())
}
}
/// An specialized handler of the page counter that tracks both the physical /// An specialized handler of the page counter that tracks both the physical
/// and the logical page counter. /// and the logical page counter.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@ -721,7 +822,9 @@ impl ManualPageCounter {
match item { match item {
FrameItem::Group(group) => self.visit(engine, &group.frame)?, FrameItem::Group(group) => self.visit(engine, &group.frame)?,
FrameItem::Meta(Meta::Elem(elem), _) => { 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 { if *elem.key() == CounterKey::Page {
let mut state = CounterState(smallvec![self.logical]); let mut state = CounterState(smallvec![self.logical]);
state.update(engine, elem.update.clone())?; state.update(engine, elem.update.clone())?;

View File

@ -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()
}

View File

@ -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. /// Query for a unique element with the label.
pub fn query_label(&self, label: Label) -> StrResult<&Content> { pub fn query_label(&self, label: Label) -> StrResult<&Content> {
let indices = self.labels.get(&label).ok_or_else(|| { let indices = self.labels.get(&label).ok_or_else(|| {

View File

@ -1,36 +1,96 @@
use crate::diag::SourceResult; use crate::diag::{HintedStrResult, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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; 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), /// Takes a selector that must match exactly one element and returns that
/// [state]($state), and [links]($link). See their documentation for more /// element's [`location`]($location). This location can, in particular, be used
/// details. /// to retrieve the physical [`page`]($location.page) number and
/// [`position`]($location.position) (page, x, y) for that element.
/// ///
/// # Examples
/// Locating a specific element:
/// ```example /// ```example
/// #locate(loc => [ /// #context [
/// My location: \ /// Introduction is at: \
/// #loc.position()! /// #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( pub fn locate(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The span of the `locate` call. /// The span of the `locate` call.
span: Span, span: Span,
/// A function that receives a [`location`]($location). Its return value is /// A selector that should match exactly one element. This element will be
/// displayed in the document. /// located.
/// ///
/// This function is called once for each time the content returned by /// Especially useful in combination with
/// `locate` appears in the document. That makes it possible to generate /// - [`here`]($here) to locate the current context,
/// content that depends on its own location in the document. /// - a [`location`]($location) retrieved from some queried element via the
func: Func, /// [`location()`]($content.location) method on content.
) -> Content { selector: LocateInput,
LocateElem::new(func).pack().spanned(span) ) -> 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. /// Executes a `locate` call.
@ -43,8 +103,9 @@ struct LocateElem {
impl Show for Packed<LocateElem> { impl Show for Packed<LocateElem> {
#[typst_macros::time(name = "locate", span = self.span())] #[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(); 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())
} }
} }

View File

@ -3,16 +3,25 @@ use std::num::NonZeroUsize;
use ecow::EcoString; use ecow::EcoString;
use crate::engine::Engine; 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; use crate::model::Numbering;
/// Identifies an element in the document. /// Identifies an element in the document.
/// ///
/// A location uniquely identifies an element in the document and lets you /// A location uniquely identifies an element in the document and lets you
/// access its absolute position on the pages. You can retrieve the current /// 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 /// or shown element with the [`location()`]($content.location) method on
/// content. /// 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)] #[ty(scope)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Location { pub struct Location {
@ -37,27 +46,36 @@ impl Location {
#[scope] #[scope]
impl Location { 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) /// Note that this does not return the value of the [page counter]($counter)
/// at this location, but the true page number (starting from one). /// at this location, but the true page number (starting from one).
/// ///
/// If you want to know the value of the page counter, use /// If you want to know the value of the page counter, use
/// `{counter(page).at(loc)}` instead. /// `{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] #[func]
pub fn page(self, engine: &mut Engine) -> NonZeroUsize { pub fn page(self, engine: &mut Engine) -> NonZeroUsize {
engine.introspector.page(self) 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 /// location. The page number starts at one and the coordinates are measured
/// from the top-left of the page. /// from the top-left of the page.
/// ///
/// If you only need the page number, use `page()` instead as it allows /// If you only need the page number, use `page()` instead as it allows
/// Typst to skip unnecessary work. /// Typst to skip unnecessary work.
#[func] #[func]
pub fn position(self, engine: &mut Engine) -> Dict { pub fn position(self, engine: &mut Engine) -> Position {
engine.introspector.position(self).into() engine.introspector.position(self)
} }
/// Returns the page numbering pattern of the page at this location. This /// Returns the page numbering pattern of the page at this location. This

View File

@ -7,8 +7,8 @@ use crate::realize::{Behave, Behaviour};
/// Exposes a value to the query system without producing visible content. /// Exposes a value to the query system without producing visible content.
/// ///
/// This element can be retrieved with the [`query`]($query) function and from /// This element can be retrieved with the [`query`]($query) function and from
/// the command with [`typst query`]($reference/meta/query/#cli-queries). Its /// the command line with [`typst query`]($reference/meta/query/#cli-queries).
/// purpose is to expose an arbitrary value to the introspection system. To /// 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) /// identify a metadata value among others, you can attach a [`label`]($label)
/// to it and query for that label. /// to it and query for that label.
/// ///
@ -20,9 +20,9 @@ use crate::realize::{Behave, Behaviour};
/// #metadata("This is a note") <note> /// #metadata("This is a note") <note>
/// ///
/// // And find it from anywhere else. /// // And find it from anywhere else.
/// #locate(loc => { /// #context {
/// query(<note>, loc).first().value /// query(<note>).first().value
/// }) /// }
/// ``` /// ```
#[elem(Behave, Show, Locatable)] #[elem(Behave, Show, Locatable)]
pub struct MetadataElem { pub struct MetadataElem {

View File

@ -1,6 +1,8 @@
//! Interaction between document parts. //! Interaction between document parts.
mod counter; mod counter;
#[path = "here.rs"]
mod here_;
mod introspector; mod introspector;
#[path = "locate.rs"] #[path = "locate.rs"]
mod locate_; mod locate_;
@ -12,6 +14,7 @@ mod query_;
mod state; mod state;
pub use self::counter::*; pub use self::counter::*;
pub use self::here_::*;
pub use self::introspector::*; pub use self::introspector::*;
pub use self::locate_::*; pub use self::locate_::*;
pub use self::location::*; pub use self::location::*;
@ -25,9 +28,8 @@ use std::fmt::{self, Debug, Formatter};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::foundations::Packed;
use crate::foundations::{ 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::model::Destination;
use crate::realize::{Behave, Behaviour}; use crate::realize::{Behave, Behaviour};
@ -39,6 +41,9 @@ use crate::realize::{Behave, Behaviour};
/// equation counters or create custom ones. Meanwhile, the `query` function /// equation counters or create custom ones. Meanwhile, the `query` function
/// lets you search for elements in the document to construct things like a list /// lets you search for elements in the document to construct things like a list
/// of figures or headers which show the current chapter title. /// 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] #[category]
pub static INTROSPECTION: Category; pub static INTROSPECTION: Category;
@ -49,8 +54,9 @@ pub fn define(global: &mut Scope) {
global.define_type::<Counter>(); global.define_type::<Counter>();
global.define_type::<State>(); global.define_type::<State>();
global.define_elem::<MetadataElem>(); global.define_elem::<MetadataElem>();
global.define_func::<locate>(); global.define_func::<here>();
global.define_func::<query>(); global.define_func::<query>();
global.define_func::<locate>();
} }
/// Hosts metadata and ensures metadata is produced even for empty elements. /// Hosts metadata and ensures metadata is produced even for empty elements.

View File

@ -1,5 +1,6 @@
use crate::diag::HintedStrResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, Array, LocatableSelector, Value}; use crate::foundations::{func, Array, Context, LocatableSelector, Value};
use crate::introspection::Location; use crate::introspection::Location;
/// Finds elements in the document. /// Finds elements in the document.
@ -38,10 +39,9 @@ use crate::introspection::Location;
/// >>> margin: (top: 35pt, rest: 15pt), /// >>> margin: (top: 35pt, rest: 15pt),
/// >>> header-ascent: 12pt, /// >>> header-ascent: 12pt,
/// >>> ) /// >>> )
/// #set page(header: locate(loc => { /// #set page(header: context {
/// let elems = query( /// let elems = query(
/// selector(heading).before(loc), /// selector(heading).before(here()),
/// loc,
/// ) /// )
/// let academy = smallcaps[ /// let academy = smallcaps[
/// Typst Academy /// Typst Academy
@ -52,7 +52,7 @@ use crate::introspection::Location;
/// let body = elems.last().body /// let body = elems.last().body
/// academy + h(1fr) + emph(body) /// academy + h(1fr) + emph(body)
/// } /// }
/// })) /// })
/// ///
/// = Introduction /// = Introduction
/// #lorem(23) /// #lorem(23)
@ -84,11 +84,11 @@ use crate::introspection::Location;
/// ///
/// ```example /// ```example
/// = Real /// = Real
/// #locate(loc => { /// #context {
/// let elems = query(heading, loc) /// let elems = query(heading)
/// let count = elems.len() /// let count = elems.len()
/// count * [= Fake] /// count * [= Fake]
/// }) /// }
/// ``` /// ```
/// ///
/// # Command line queries /// # Command line queries
@ -130,31 +130,29 @@ use crate::introspection::Location;
/// $ typst query example.typ "<note>" --field value --one /// $ typst query example.typ "<note>" --field value --one
/// "This is a note" /// "This is a note"
/// ``` /// ```
#[func] #[func(contextual)]
pub fn query( pub fn query(
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Can be an element function like a `heading` or `figure`, a `{<label>}` /// The callsite context.
/// or a more complex selector like `{heading.where(level: 1)}`. 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 /// Only [locatable]($location/#locatable) element functions are supported.
/// 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.
target: LocatableSelector, target: LocatableSelector,
/// Can be an arbitrary location, as its value is irrelevant for the /// _Compatibility:_ This argument only exists for compatibility with
/// function's return value. Why is it required then? As noted before, Typst /// Typst 0.10 and lower and shouldn't be used anymore.
/// has to evaluate parts of your code multiple times to determine the #[default]
/// values of all state. By only allowing this function within location: Option<Location>,
/// [`locate`]($locate) calls, the amount of code that can depend on the ) -> HintedStrResult<Array> {
/// query's result is reduced. If you could call it directly at the top if location.is_none() {
/// level of a module, the evaluation of the whole module and its exports context.introspect()?;
/// could depend on the query's result. }
location: Location,
) -> Array {
let _ = location;
let vec = engine.introspector.query(&target.0); let vec = engine.introspector.query(&target.0);
vec.into_iter().map(Value::Content).collect() Ok(vec.into_iter().map(Value::Content).collect())
} }

View File

@ -1,12 +1,13 @@
use comemo::{Tracked, TrackedMut}; use comemo::{Tracked, TrackedMut};
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; 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::engine::{Engine, Route};
use crate::eval::Tracer; use crate::eval::Tracer;
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, select_where, ty, Content, Func, NativeElement, Packed, cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func,
Repr, Selector, Show, Str, StyleChain, Value, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain,
Value,
}; };
use crate::introspection::{Introspector, Locatable, Location, Locator}; use crate::introspection::{Introspector, Locatable, Location, Locator};
use crate::syntax::Span; use crate::syntax::Span;
@ -22,6 +23,7 @@ use crate::World;
/// outside the function are read-only and cannot be modified._ /// outside the function are read-only and cannot be modified._
/// ///
/// ```typ /// ```typ
/// // This doesn't work!
/// #let x = 0 /// #let x = 0
/// #let compute(expr) = { /// #let compute(expr) = {
/// x = eval( /// x = eval(
@ -70,17 +72,18 @@ use crate::World;
/// # Managing state in Typst { #state-in-typst } /// # Managing state in Typst { #state-in-typst }
/// So what do we do instead? We use Typst's state management system. Calling /// 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 /// 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 /// value gives you a state value which exposes a few function. The two most
/// important ones are `display` and `update`: /// important ones are `get` and `update`:
/// ///
/// - The `display` method shows the current value of the state. You can /// - The [`get`]($state.get) function retrieves the current value of the state.
/// optionally give it a function that receives the value and formats it in /// Because the value can vary over the course of the document, it is a
/// some way. /// _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 /// - The [`update`]($state.update) function modifies the state. You can give it
/// given a non-function value, it sets the state to that value. If given a /// any value. If given a non-function value, it sets the state to that value.
/// function, that function receives the previous state and has to return the /// If given a function, that function receives the previous state and has to
/// new state. /// return the new state.
/// ///
/// Our initial example would now look like this: /// Our initial example would now look like this:
/// ///
@ -90,7 +93,7 @@ use crate::World;
/// #s.update(x => /// #s.update(x =>
/// eval(expr.replace("x", str(x))) /// eval(expr.replace("x", str(x)))
/// ) /// )
/// New value is #s.display(). /// New value is #context s.get().
/// ] /// ]
/// ///
/// #compute("10") \ /// #compute("10") \
@ -103,8 +106,8 @@ use crate::World;
/// order. The `update` method returns content and its effect occurs at the /// order. The `update` method returns content and its effect occurs at the
/// position where the returned content is inserted into the document. /// position where the returned content is inserted into the document.
/// ///
/// As a result, we can now also store some of the computations in /// As a result, we can now also store some of the computations in variables,
/// variables, but they still show the correct results: /// but they still show the correct results:
/// ///
/// ```example /// ```example
/// >>> #let s = state("x", 0) /// >>> #let s = state("x", 0)
@ -112,7 +115,7 @@ use crate::World;
/// >>> #s.update(x => /// >>> #s.update(x =>
/// >>> eval(expr.replace("x", str(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 /// # Time Travel
/// By using Typst's state management system you also get time travel /// By using Typst's state management system you also get time travel
/// capabilities! By combining the state system with [`locate`]($locate) and /// capabilities! We can find out what the value of the state will be at any
/// [`query`]($query), we can find out what the value of the state will be at /// position in the document from anywhere else. In particular, the `at` method
/// any position in the document from anywhere else. In particular, the `at` /// gives us the value of the state at any particular location and the `final`
/// method gives us the value of the state at any location and the `final`
/// methods gives us the value of the state at the end of the document. /// methods gives us the value of the state at the end of the document.
/// ///
/// ```example /// ```example
@ -144,16 +146,12 @@ use crate::World;
/// >>> #s.update(x => { /// >>> #s.update(x => {
/// >>> eval(expr.replace("x", str(x))) /// >>> eval(expr.replace("x", str(x)))
/// >>> }) /// >>> })
/// >>> New value is #s.display(). /// >>> New value is #context s.get().
/// >>> ] /// >>> ]
/// <<< ... /// <<< ...
/// ///
/// Value at `<here>` is /// Value at `<here>` is
/// #locate(loc => s.at( /// #context s.at(<here>)
/// query(<here>, loc)
/// .first()
/// .location()
/// ))
/// ///
/// #compute("10") \ /// #compute("10") \
/// #compute("x + 3") \ /// #compute("x + 3") \
@ -171,21 +169,21 @@ use crate::World;
/// a state, the results might never converge. The example below illustrates /// 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 /// 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 /// 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 /// should be `3`, and so on. This example displays a finite value because Typst
/// Typst simply gives up after a few attempts. /// simply gives up after a few attempts.
/// ///
/// ```example /// ```example
/// // This is bad!
/// #let s = state("x", 1) /// #let s = state("x", 1)
/// #locate(loc => { /// #context s.update(s.final() + 1)
/// s.update(s.final(loc) + 1) /// #context s.get()
/// })
/// #s.display()
/// ``` /// ```
/// ///
/// In general, you should _typically_ not generate state updates from within /// In general, you should try not to generate state updates from within context
/// `locate` calls or `display` calls of state or counters. Instead, pass a /// expressions. If possible, try to express your updates as non-contextual
/// function to `update` that determines the value of the state based on its /// values or functions that compute the new value from the previous value.
/// previous value. /// Sometimes, it cannot be helped, but in those cases it is up to you to ensure
/// that the result converges.
#[ty(scope)] #[ty(scope)]
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub struct State { pub struct State {
@ -201,6 +199,16 @@ impl State {
Self { key, init } 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. /// Produce the whole sequence of states.
/// ///
/// This has to happen just once for all states, cutting down the number /// 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()]; let mut stops = eco_vec![state.clone()];
for elem in introspector.query(&self.selector()) { for elem in introspector.query(&self.selector()) {
let elem = elem.to_packed::<UpdateElem>().unwrap(); let elem = elem.to_packed::<StateUpdateElem>().unwrap();
match elem.update() { match elem.update() {
StateUpdate::Set(value) => state = value.clone(), 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()); stops.push(state.clone());
} }
@ -250,7 +260,7 @@ impl State {
/// The selector for this state's updates. /// The selector for this state's updates.
fn selector(&self) -> Selector { 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) Self::new(key, init)
} }
/// Displays the current value of the state. /// Retrieves the value of the state at the current location.
#[func] ///
pub fn display( /// This is equivalent to `{state.at(here())}`.
self, #[func(contextual)]
/// The span of the `display` call. pub fn get(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span, span: Span,
/// A function which receives the value of the state and can return ) -> SourceResult<Value> {
/// arbitrary content which is then displayed. If this is omitted, the let loc = context.location().at(span)?;
/// value is directly displayed. 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] #[default]
func: Option<Func>, location: Option<Location>,
) -> Content { ) -> SourceResult<Value> {
DisplayElem::new(self, func).pack().spanned(span) if location.is_none() {
context.location().at(span)?;
}
let sequence = self.sequence(engine)?;
Ok(sequence.last().unwrap().clone())
} }
/// Update the value of the state. /// Update the value of the state.
@ -301,47 +361,24 @@ impl State {
/// to return the new state. /// to return the new state.
update: StateUpdate, update: StateUpdate,
) -> Content { ) -> 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. /// **Deprection planned:** Use [`get`]($state.get) instead.
///
/// Displays the current value of the state.
#[func] #[func]
pub fn at( pub fn display(
&self, self,
/// The engine. /// The span of the `display` call.
engine: &mut Engine, span: Span,
/// The location at which the state's value should be retrieved. A /// A function which receives the value of the state and can return
/// suitable location can be retrieved from [`locate`]($locate) or /// arbitrary content which is then displayed. If this is omitted, the
/// [`query`]($query). /// value is directly displayed.
location: Location, #[default]
) -> SourceResult<Value> { func: Option<Func>,
let sequence = self.sequence(engine)?; ) -> Content {
let offset = engine StateDisplayElem::new(self, func).pack().spanned(span)
.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.
#[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())
} }
} }
@ -352,7 +389,6 @@ impl Repr for State {
} }
/// An update to perform on a state. /// An update to perform on a state.
#[ty(cast)]
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum StateUpdate { pub enum StateUpdate {
/// Set the state to the specified value. /// Set the state to the specified value.
@ -361,56 +397,68 @@ pub enum StateUpdate {
Func(Func), Func(Func),
} }
impl Repr for StateUpdate {
fn repr(&self) -> EcoString {
"..".into()
}
}
cast! { cast! {
type StateUpdate, StateUpdate,
v: Func => Self::Func(v), v: Func => Self::Func(v),
v: Value => Self::Set(v), v: Value => Self::Set(v),
} }
/// Executes a display of a state. /// Executes a display of a state.
#[elem(Locatable, Show)] #[elem(Construct, Locatable, Show)]
struct DisplayElem { struct StateUpdateElem {
/// The state.
#[required]
state: State,
/// The function to display the state with.
#[required]
func: Option<Func>,
}
impl Show for Packed<DisplayElem> {
#[typst_macros::time(name = "state.display", span = self.span())]
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
let location = self.location().unwrap();
let value = self.state().at(engine, location)?;
Ok(match self.func() {
Some(func) => func.call(engine, [value])?.display(),
None => value.display(),
})
}
}
/// Executes a display of a state.
#[elem(Locatable, Show)]
struct UpdateElem {
/// The key that identifies the state. /// The key that identifies the state.
#[required] #[required]
key: Str, key: Str,
/// The update to perform on the state. /// The update to perform on the state.
#[required] #[required]
#[internal]
update: StateUpdate, update: StateUpdate,
} }
impl Show for Packed<UpdateElem> { 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> { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty()) 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<StateDisplayElem> {
#[typst_macros::time(name = "state.display", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let location = self.location().unwrap();
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, &context, [value])?.display(),
None => value.display(),
})
}
}
impl Construct for StateDisplayElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}

View File

@ -14,8 +14,8 @@ use crate::diag::{
}; };
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
Array, CastInfo, Content, Fold, FromValue, Func, IntoValue, Reflect, Resolve, Smart, Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
StyleChain, Value, Resolve, Smart, StyleChain, Value,
}; };
use crate::layout::{ use crate::layout::{
Abs, Alignment, Axes, Dir, Fr, Fragment, Frame, FrameItem, LayoutMultiple, Length, 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> { impl<T: Default + Clone + FromValue> Celled<T> {
/// Resolve the value based on the cell position. /// 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 { Ok(match self {
Self::Value(value) => value.clone(), 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 Self::Array(array) => x
.checked_rem(array.len()) .checked_rem(array.len())
.and_then(|i| array.get(i)) .and_then(|i| array.get(i))
@ -141,7 +150,7 @@ where
Ok(match &self.0 { Ok(match &self.0 {
Celled::Value(value) => value.clone(), Celled::Value(value) => value.clone(),
Celled::Func(func) => func Celled::Func(func) => func
.call(engine, [x, y])? .call(engine, &Context::new(None, Some(styles)), [x, y])?
.cast::<T>() .cast::<T>()
.at(func.span())? .at(func.span())?
.resolve(styles), .resolve(styles),
@ -484,9 +493,9 @@ impl CellGrid {
let cell = cell.resolve_cell( let cell = cell.resolve_cell(
x, x,
y, y,
&fill.resolve(engine, x, y)?, &fill.resolve(engine, styles, x, y)?,
align.resolve(engine, x, y)?, align.resolve(engine, styles, x, y)?,
inset.resolve(engine, x, y)?, inset.resolve(engine, styles, x, y)?,
stroke.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?,
styles, styles,
); );
@ -570,9 +579,9 @@ impl CellGrid {
let new_cell = T::default().resolve_cell( let new_cell = T::default().resolve_cell(
x, x,
y, y,
&fill.resolve(engine, x, y)?, &fill.resolve(engine, styles, x, y)?,
align.resolve(engine, x, y)?, align.resolve(engine, styles, x, y)?,
inset.resolve(engine, x, y)?, inset.resolve(engine, styles, x, y)?,
stroke.resolve(engine, styles, x, y)?, stroke.resolve(engine, styles, x, y)?,
styles, styles,
); );

View File

@ -1,8 +1,9 @@
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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::layout::{Fragment, LayoutMultiple, Regions, Size};
use crate::syntax::Span; use crate::syntax::Span;
@ -14,15 +15,14 @@ use crate::syntax::Span;
/// ///
/// ```example /// ```example
/// #let text = lorem(30) /// #let text = lorem(30)
/// #layout(size => style(styles => [ /// #layout(size => [
/// #let (height,) = measure( /// #let (height,) = measure(
/// block(width: size.width, text), /// block(width: size.width, text),
/// styles,
/// ) /// )
/// This text is #height high with /// This text is #height high with
/// the current page width: \ /// the current page width: \
/// #text /// #text
/// ])) /// ])
/// ``` /// ```
/// ///
/// If the `layout` call is placed inside of a box width a width of `{800pt}` /// 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. /// Executes a `layout` call.
#[elem(LayoutMultiple)] #[elem(Locatable, LayoutMultiple)]
struct LayoutElem { struct LayoutElem {
/// The function to call with the outer container's (or page's) size. /// The function to call with the outer container's (or page's) size.
#[required] #[required]
@ -81,9 +81,11 @@ impl LayoutMultiple for Packed<LayoutElem> {
// Gets the current region's base size, which will be the size of the // 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. // outer container, or of the page if there is no such container.
let Size { x, y } = regions.base(); let Size { x, y } = regions.base();
let loc = self.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
let result = self let result = self
.func() .func()
.call(engine, [dict! { "width" => x, "height" => y }])? .call(engine, &context, [dict! { "width" => x, "height" => y }])?
.display(); .display();
result.layout(engine, styles, regions) result.layout(engine, styles, regions)
} }

View File

@ -4,8 +4,8 @@ use std::ops::{Add, Div, Mul, Neg};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use crate::diag::{At, Hint, SourceResult}; use crate::diag::{At, Hint, HintedStrResult, SourceResult};
use crate::foundations::{func, scope, ty, Fold, Repr, Resolve, StyleChain, Styles}; use crate::foundations::{func, scope, ty, Context, Fold, Repr, Resolve, StyleChain};
use crate::layout::{Abs, Em}; use crate::layout::{Abs, Em};
use crate::syntax::Span; use crate::syntax::Span;
use crate::util::Numeric; use crate::util::Numeric;
@ -137,32 +137,22 @@ impl Length {
/// ///
/// ```example /// ```example
/// #set text(size: 12pt) /// #set text(size: 12pt)
/// #style(styles => [ /// #context [
/// #(6pt).to-absolute(styles) \ /// #(6pt).to-absolute() \
/// #(6pt + 10em).to-absolute(styles) \ /// #(6pt + 10em).to-absolute() \
/// #(10em).to-absolute(styles) /// #(10em).to-absolute()
/// ]) /// ]
/// ///
/// #set text(size: 6pt) /// #set text(size: 6pt)
/// #style(styles => [ /// #context [
/// #(6pt).to-absolute(styles) \ /// #(6pt).to-absolute() \
/// #(6pt + 10em).to-absolute(styles) \ /// #(6pt + 10em).to-absolute() \
/// #(10em).to-absolute(styles) /// #(10em).to-absolute()
/// ]) /// ]
/// ``` /// ```
#[func] #[func]
pub fn to_absolute( pub fn to_absolute(&self, context: &Context) -> HintedStrResult<Length> {
&self, Ok(self.resolve(context.styles()?).into())
/// 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()
} }
} }

View File

@ -1,7 +1,8 @@
use crate::diag::SourceResult; use crate::diag::{At, SourceResult};
use crate::engine::Engine; 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::layout::{Abs, Axes, LayoutMultiple, Regions, Size};
use crate::syntax::Span;
/// Measures the layouted size of content. /// Measures the layouted size of content.
/// ///
@ -28,10 +29,10 @@ use crate::layout::{Abs, Axes, LayoutMultiple, Regions, Size};
/// the `measure` function. /// the `measure` function.
/// ///
/// ```example /// ```example
/// #let thing(body) = style(styles => { /// #let thing(body) = context {
/// let size = measure(body, styles) /// let size = measure(body)
/// [Width of "#body" is #size.width] /// [Width of "#body" is #size.width]
/// }) /// }
/// ///
/// #thing[Hey] \ /// #thing[Hey] \
/// #thing[Welcome] /// #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 /// The measure function returns a dictionary with the entries `width` and
/// `height`, both of type [`length`]($length). /// `height`, both of type [`length`]($length).
#[func] #[func(contextual)]
pub fn measure( pub fn measure(
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span,
/// The content whose size to measure. /// The content whose size to measure.
content: Content, content: Content,
/// The styles with which to layout the content. /// The styles with which to layout the content.
styles: Styles, #[default]
styles: Option<Styles>,
) -> SourceResult<Dict> { ) -> 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 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 frame = content.measure(engine, styles, pod)?.into_frame();
let Size { x, y } = frame.size(); let Size { x, y } = frame.size();
Ok(dict! { "width" => x, "height" => y }) Ok(dict! { "width" => x, "height" => y })

View File

@ -6,10 +6,10 @@ use std::str::FromStr;
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, AutoValue, Cast, Content, Dict, Fold, Func, NativeElement, Packed, cast, elem, AutoValue, Cast, Content, Context, Dict, Fold, Func, NativeElement,
Resolve, Smart, StyleChain, Value, Packed, Resolve, Smart, StyleChain, Value,
}; };
use crate::introspection::{Counter, CounterKey, ManualPageCounter}; use crate::introspection::{Counter, CounterDisplayElem, CounterKey, ManualPageCounter};
use crate::layout::{ use crate::layout::{
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, LayoutMultiple, Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, LayoutMultiple,
Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment, Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment,
@ -258,7 +258,7 @@ pub struct PageElem {
/// #set page( /// #set page(
/// height: 100pt, /// height: 100pt,
/// margin: 20pt, /// margin: 20pt,
/// footer: [ /// footer: context [
/// #set align(right) /// #set align(right)
/// #set text(8pt) /// #set text(8pt)
/// #counter(page).display( /// #counter(page).display(
@ -416,11 +416,13 @@ impl Packed<PageElem> {
Numbering::Func(_) => true, Numbering::Func(_) => true,
}; };
let mut counter = Counter::new(CounterKey::Page).display( let mut counter = CounterDisplayElem::new(
self.span(), Counter::new(CounterKey::Page),
Some(numbering.clone()), Smart::Custom(numbering.clone()),
both, both,
); )
.pack()
.spanned(self.span());
// We interpret the Y alignment as selecting header or footer // We interpret the Y alignment as selecting header or footer
// and then ignore it for aligning the actual number. // and then ignore it for aligning the actual number.
@ -512,7 +514,7 @@ impl Packed<PageElem> {
} }
/// A finished page. /// A finished page.
#[derive(Debug, Default, Clone)] #[derive(Debug, Clone)]
pub struct Page { pub struct Page {
/// The frame that defines the page. /// The frame that defines the page.
pub frame: Frame, pub frame: Frame,
@ -524,7 +526,7 @@ pub struct Page {
} }
/// Specification of the page's margins. /// Specification of the page's margins.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Margin { pub struct Margin {
/// The margins for each side. /// The margins for each side.
pub sides: Sides<Option<Smart<Rel<Length>>>>, 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 { impl Fold for Margin {
fn fold(self, outer: Self) -> Self { fn fold(self, outer: Self) -> Self {
Margin { Margin {
@ -552,22 +563,28 @@ impl Fold for Margin {
cast! { cast! {
Margin, Margin,
self => { 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 dict = Dict::new();
let mut handle = |key: &str, component: Value| { let mut handle = |key: &str, component: Option<Smart<Rel<Length>>>| {
let value = component.into_value(); if let Some(c) = component {
if value != Value::None { dict.insert(key.into(), c.into_value());
dict.insert(key.into(), value);
} }
}; };
handle("top", self.sides.top.into_value()); handle("top", self.sides.top);
handle("bottom", self.sides.bottom.into_value()); handle("bottom", self.sides.bottom);
if self.two_sided.unwrap_or(false) { if two_sided {
handle("inside", self.sides.left.into_value()); handle("inside", self.sides.left);
handle("outside", self.sides.right.into_value()); handle("outside", self.sides.right);
} else { } else {
handle("left", self.sides.left.into_value()); handle("left", self.sides.left);
handle("right", self.sides.right.into_value()); handle("right", self.sides.right);
} }
Value::Dict(dict) Value::Dict(dict)
@ -668,11 +685,15 @@ impl Marginal {
pub fn resolve( pub fn resolve(
&self, &self,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain,
page: usize, page: usize,
) -> SourceResult<Cow<'_, Content>> { ) -> SourceResult<Cow<'_, Content>> {
Ok(match self { Ok(match self {
Self::Content(content) => Cow::Borrowed(content), 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(),
),
}) })
} }
} }

View File

@ -1,5 +1,7 @@
use crate::diag::{At, SourceResult}; 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::{ use crate::layout::{
Abs, Angle, Frame, FrameItem, Length, Point, Ratio, Rel, Size, Transform, Abs, Angle, Frame, FrameItem, Length, Point, Ratio, Rel, Size, Transform,
}; };
@ -135,6 +137,7 @@ impl LayoutMath for Packed<CancelElem> {
invert_first_line, invert_first_line,
&angle, &angle,
body_size, body_size,
styles,
span, span,
)?; )?;
@ -144,8 +147,9 @@ impl LayoutMath for Packed<CancelElem> {
if cross { if cross {
// Draw the second line. // Draw the second line.
let second_line = let second_line = draw_cancel_line(
draw_cancel_line(ctx, length, stroke, true, &angle, body_size, span)?; ctx, length, stroke, true, &angle, body_size, styles, span,
)?;
body.push_frame(center, second_line); body.push_frame(center, second_line);
} }
@ -180,6 +184,7 @@ cast! {
} }
/// Draws a cancel line. /// Draws a cancel line.
#[allow(clippy::too_many_arguments)]
fn draw_cancel_line( fn draw_cancel_line(
ctx: &mut MathContext, ctx: &mut MathContext,
length_scale: Rel<Abs>, length_scale: Rel<Abs>,
@ -187,6 +192,7 @@ fn draw_cancel_line(
invert: bool, invert: bool,
angle: &Smart<CancelAngle>, angle: &Smart<CancelAngle>,
body_size: Size, body_size: Size,
styles: StyleChain,
span: Span, span: Span,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let default = default_angle(body_size); 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. // This specifies the absolute angle w.r.t y-axis clockwise.
CancelAngle::Angle(v) => *v, CancelAngle::Angle(v) => *v,
// This specifies a function that takes the default angle as input. // This specifies a function that takes the default angle as input.
CancelAngle::Func(func) => { CancelAngle::Func(func) => func
func.call(ctx.engine, [default])?.cast().at(span)? .call(ctx.engine, &Context::new(None, Some(styles)), [default])?
} .cast()
.at(span)?,
}, },
}; };

View File

@ -161,7 +161,7 @@ impl Synthesize for Packed<EquationElem> {
Smart::Auto => TextElem::packed(Self::local_name_in(styles)), Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
Smart::Custom(None) => Content::empty(), Smart::Custom(None) => Content::empty(),
Smart::Custom(Some(supplement)) => { 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 pod = Regions::one(regions.base(), Axes::splat(false));
let number = Counter::of(EquationElem::elem()) let number = Counter::of(EquationElem::elem())
.at(engine, self.location().unwrap())? .display_at_loc(engine, self.location().unwrap(), styles, numbering)?
.display(engine, numbering)?
.spanned(span) .spanned(span)
.layout(engine, styles, pod)? .layout(engine, styles, pod)?
.into_frame(); .into_frame();
@ -357,7 +356,11 @@ impl Refable for Packed<EquationElem> {
} }
impl Outlinable 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()) { if !self.block(StyleChain::default()) {
return Ok(None); return Ok(None);
} }
@ -375,10 +378,12 @@ impl Outlinable for Packed<EquationElem> {
supplement += TextElem::packed("\u{a0}"); supplement += TextElem::packed("\u{a0}");
} }
let numbers = self let numbers = self.counter().display_at_loc(
.counter() engine,
.at(engine, self.location().unwrap())? self.location().unwrap(),
.display(engine, numbering)?; styles,
numbering,
)?;
Ok(Some(supplement + numbers)) Ok(Some(supplement + numbers))
} }

View File

@ -4,7 +4,9 @@ use smallvec::{smallvec, SmallVec};
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; 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::{ use crate::layout::{
Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment,
LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment, LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment,
@ -242,9 +244,10 @@ impl LayoutMultiple for Packed<EnumElem> {
for item in self.children() { for item in self.children() {
number = item.number(styles).unwrap_or(number); number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
let resolved = if full { let resolved = if full {
parents.push(number); parents.push(number);
let content = numbering.apply(engine, &parents)?.display(); let content = numbering.apply(engine, &context, &parents)?.display();
parents.pop(); parents.pop();
content content
} else { } else {
@ -252,7 +255,7 @@ impl LayoutMultiple for Packed<EnumElem> {
Numbering::Pattern(pattern) => { Numbering::Pattern(pattern) => {
TextElem::packed(pattern.apply_kth(parents.len(), number)) TextElem::packed(pattern.apply_kth(parents.len(), number))
} }
other => other.apply(engine, &[number])?.display(), other => other.apply(engine, &context, &[number])?.display(),
} }
}; };

View File

@ -274,7 +274,7 @@ impl Synthesize for Packed<FigureElem> {
}; };
let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body())); 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> { 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()) { if !self.outlined(StyleChain::default()) {
return Ok(None); return Ok(None);
} }
@ -396,9 +400,12 @@ impl Outlinable for Packed<FigureElem> {
(**self).counter(), (**self).counter(),
self.numbering(), self.numbering(),
) { ) {
let numbers = counter let numbers = counter.display_at_loc(
.at(engine, self.location().unwrap())? engine,
.display(engine, numbering)?; self.location().unwrap(),
styles,
numbering,
)?;
if !supplement.is_empty() { if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}'); supplement += TextElem::packed('\u{a0}');
@ -483,7 +490,8 @@ pub struct FigureCaption {
/// ```example /// ```example
/// #show figure.caption: it => [ /// #show figure.caption: it => [
/// #underline(it.body) | /// #underline(it.body) |
/// #it.supplement #it.counter.display(it.numbering) /// #it.supplement
/// #context it.counter.display(it.numbering)
/// ] /// ]
/// ///
/// #figure( /// #figure(
@ -554,7 +562,7 @@ impl Show for Packed<FigureCaption> {
self.counter(), self.counter(),
self.figure_location(), 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() { if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}'); supplement += TextElem::packed('\u{a0}');
} }

View File

@ -126,11 +126,12 @@ impl Packed<FootnoteElem> {
impl Show for Packed<FootnoteElem> { impl Show for Packed<FootnoteElem> {
#[typst_macros::time(name = "footnote", span = self.span())] #[typst_macros::time(name = "footnote", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { 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 numbering = self.numbering(styles);
let counter = Counter::of(FootnoteElem::elem()); let counter = Counter::of(FootnoteElem::elem());
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()); let sup = SuperElem::new(num).pack().spanned(span);
let loc = loc.variant(1); let loc = loc.variant(1);
// Add zero-width weak spacing to make the footnote "sticky". // Add zero-width weak spacing to make the footnote "sticky".
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc))) Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
@ -266,6 +267,7 @@ pub struct FootnoteEntry {
impl Show for Packed<FootnoteEntry> { impl Show for Packed<FootnoteEntry> {
#[typst_macros::time(name = "footnote.entry", span = self.span())] #[typst_macros::time(name = "footnote.entry", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let note = self.note(); let note = self.note();
let number_gap = Em::new(0.05); let number_gap = Em::new(0.05);
let default = StyleChain::default(); let default = StyleChain::default();
@ -273,15 +275,15 @@ impl Show for Packed<FootnoteEntry> {
let counter = Counter::of(FootnoteElem::elem()); let counter = Counter::of(FootnoteElem::elem());
let Some(loc) = note.location() else { let Some(loc) = note.location() else {
bail!( 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" 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) let sup = SuperElem::new(num)
.pack() .pack()
.spanned(self.span()) .spanned(span)
.linked(Destination::Location(loc)) .linked(Destination::Location(loc))
.backlinked(loc.variant(1)); .backlinked(loc.variant(1));
Ok(Content::sequence([ Ok(Content::sequence([

View File

@ -24,7 +24,7 @@ use crate::util::{option_eq, NonZeroExt};
/// specify how you want your headings to be numbered with a /// specify how you want your headings to be numbered with a
/// [numbering pattern or function]($numbering). /// [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 /// [outline]($outline) of all headings for you. To exclude one or more headings
/// from this outline, you can set the `outlined` parameter to `{false}`. /// 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::Auto => TextElem::packed(Self::local_name_in(styles)),
Smart::Custom(None) => Content::empty(), Smart::Custom(None) => Content::empty(),
Smart::Custom(Some(supplement)) => { 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> { impl Show for Packed<HeadingElem> {
#[typst_macros::time(name = "heading", span = self.span())] #[typst_macros::time(name = "heading", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let mut realized = self.body().clone(); let mut realized = self.body().clone();
if let Some(numbering) = (**self).numbering(styles).as_ref() { if let Some(numbering) = (**self).numbering(styles).as_ref() {
realized = Counter::of(HeadingElem::elem()) realized = Counter::of(HeadingElem::elem())
.at(engine, self.location().unwrap())? .display_at_loc(engine, self.location().unwrap(), styles, numbering)?
.display(engine, numbering)? .spanned(span)
.spanned(self.span())
+ HElem::new(Em::new(0.3).into()).with_weak(true).pack() + HElem::new(Em::new(0.3).into()).with_weak(true).pack()
+ realized; + 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> { 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()) { if !self.outlined(StyleChain::default()) {
return Ok(None); return Ok(None);
} }
let mut content = self.body().clone(); let mut content = self.body().clone();
if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() {
let numbers = Counter::of(HeadingElem::elem()) let numbers = Counter::of(HeadingElem::elem()).display_at_loc(
.at(engine, self.location().unwrap())? engine,
.display(engine, numbering)?; self.location().unwrap(),
styles,
numbering,
)?;
content = numbers + SpaceElem::new().pack() + content; content = numbers + SpaceElem::new().pack() + content;
}; };

View File

@ -1,7 +1,8 @@
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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::{ use crate::layout::{
Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, 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 Depth(depth) = ListElem::depth_in(styles);
let marker = self let marker = self
.marker(styles) .marker(styles)
.resolve(engine, depth)? .resolve(engine, styles, depth)?
// avoid '#set align' interference with the list // avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top); .aligned(HAlignment::Start + VAlignment::Top);
@ -206,12 +207,19 @@ pub enum ListMarker {
impl ListMarker { impl ListMarker {
/// Resolve the marker for the given depth. /// 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 { Ok(match self {
Self::Content(list) => { Self::Content(list) => {
list.get(depth % list.len()).cloned().unwrap_or_default() 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(),
}) })
} }
} }

View File

@ -5,7 +5,7 @@ use ecow::{eco_format, EcoString, EcoVec};
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; 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; use crate::text::Case;
/// Applies a numbering to a sequence of numbers. /// Applies a numbering to a sequence of numbers.
@ -35,6 +35,8 @@ use crate::text::Case;
pub fn numbering( pub fn numbering(
/// The engine. /// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: &Context,
/// Defines how the numbering works. /// Defines how the numbering works.
/// ///
/// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `一`, `壹`, `あ`, `い`, `ア`, `イ`, `א`, `가`, /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `一`, `壹`, `あ`, `い`, `ア`, `イ`, `א`, `가`,
@ -66,7 +68,7 @@ pub fn numbering(
#[variadic] #[variadic]
numbers: Vec<usize>, numbers: Vec<usize>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
numbering.apply(engine, &numbers) numbering.apply(engine, context, &numbers)
} }
/// How to number a sequence of things. /// How to number a sequence of things.
@ -80,10 +82,15 @@ pub enum Numbering {
impl Numbering { impl Numbering {
/// Apply the pattern to the given numbers. /// 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 { Ok(match self {
Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), 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())?,
}) })
} }

View File

@ -4,8 +4,8 @@ use std::str::FromStr;
use crate::diag::{bail, At, SourceResult}; use crate::diag::{bail, At, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, select_where, Content, Func, LocatableSelector, NativeElement, cast, elem, scope, select_where, Content, Context, Func, LocatableSelector,
Packed, Show, ShowSet, Smart, StyleChain, Styles, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
}; };
use crate::introspection::{Counter, CounterKey, Locatable}; use crate::introspection::{Counter, CounterKey, Locatable};
use crate::layout::{BoxElem, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing}; use crate::layout::{BoxElem, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing};
@ -215,6 +215,7 @@ impl Show for Packed<OutlineElem> {
self.span(), self.span(),
elem.clone(), elem.clone(),
self.fill(styles), self.fill(styles),
styles,
)? )?
else { else {
continue; continue;
@ -235,7 +236,14 @@ impl Show for Packed<OutlineElem> {
ancestors.pop(); 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. // Add the overridable outline entry, followed by a line break.
seq.push(entry.pack()); seq.push(entry.pack());
@ -302,7 +310,12 @@ impl LocalName for Packed<OutlineElem> {
/// `#outline()` element. /// `#outline()` element.
pub trait Outlinable: Refable { pub trait Outlinable: Refable {
/// Produce an outline item for this element. /// 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. /// Returns the nesting level of this element.
fn level(&self) -> NonZeroUsize { fn level(&self) -> NonZeroUsize {
@ -324,6 +337,7 @@ impl OutlineIndent {
engine: &mut Engine, engine: &mut Engine,
ancestors: &Vec<&Content>, ancestors: &Vec<&Content>,
seq: &mut Vec<Content>, seq: &mut Vec<Content>,
styles: StyleChain,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
match indent { match indent {
@ -338,10 +352,12 @@ impl OutlineIndent {
let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap(); let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap();
if let Some(numbering) = ancestor_outlinable.numbering() { if let Some(numbering) = ancestor_outlinable.numbering() {
let numbers = ancestor_outlinable let numbers = ancestor_outlinable.counter().display_at_loc(
.counter() engine,
.at(engine, ancestor.location().unwrap())? ancestor.location().unwrap(),
.display(engine, numbering)?; styles,
numbering,
)?;
hidden += numbers + SpaceElem::new().pack(); hidden += numbers + SpaceElem::new().pack();
}; };
@ -364,8 +380,10 @@ impl OutlineIndent {
// the returned content // the returned content
Some(Smart::Custom(OutlineIndent::Func(func))) => { Some(Smart::Custom(OutlineIndent::Func(func))) => {
let depth = ancestors.len(); let depth = ancestors.len();
let LengthOrContent(content) = let LengthOrContent(content) = func
func.call(engine, [depth])?.cast().at(span)?; .call(engine, &Context::new(None, Some(styles)), [depth])?
.cast()
.at(span)?;
if !content.is_empty() { if !content.is_empty() {
seq.push(content); seq.push(content);
} }
@ -469,12 +487,13 @@ impl OutlineEntry {
span: Span, span: Span,
elem: Content, elem: Content,
fill: Option<Content>, fill: Option<Content>,
styles: StyleChain,
) -> SourceResult<Option<Self>> { ) -> SourceResult<Option<Self>> {
let Some(outlinable) = elem.with::<dyn Outlinable>() else { let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name()); 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); return Ok(None);
}; };
@ -485,9 +504,12 @@ impl OutlineEntry {
.cloned() .cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
let page = Counter::new(CounterKey::Page) let page = Counter::new(CounterKey::Page).display_at_loc(
.at(engine, location)? engine,
.display(engine, &page_numbering)?; location,
styles,
&page_numbering,
)?;
Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
} }

View File

@ -3,8 +3,8 @@ use ecow::eco_format;
use crate::diag::{bail, At, Hint, SourceResult}; use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Content, Func, IntoValue, Label, NativeElement, Packed, Show, Smart, cast, elem, Content, Context, Func, IntoValue, Label, NativeElement, Packed, Show,
StyleChain, Synthesize, Smart, StyleChain, Synthesize,
}; };
use crate::introspection::{Counter, Locatable}; use crate::introspection::{Counter, Locatable};
use crate::math::EquationElem; use crate::math::EquationElem;
@ -213,15 +213,19 @@ impl Show for Packed<RefElem> {
.at(span)?; .at(span)?;
let loc = elem.location().unwrap(); let loc = elem.location().unwrap();
let numbers = refable let numbers = refable.counter().display_at_loc(
.counter() engine,
.at(engine, loc)? loc,
.display(engine, &numbering.clone().trimmed())?; styles,
&numbering.clone().trimmed(),
)?;
let supplement = match self.supplement(styles).as_ref() { let supplement = match self.supplement(styles).as_ref() {
Smart::Auto => refable.supplement(), Smart::Auto => refable.supplement(),
Smart::Custom(None) => Content::empty(), 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; let mut content = numbers;
@ -267,11 +271,14 @@ impl Supplement {
pub fn resolve<T: IntoValue>( pub fn resolve<T: IntoValue>(
&self, &self,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain,
args: impl IntoIterator<Item = T>, args: impl IntoIterator<Item = T>,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
Ok(match self { Ok(match self {
Supplement::Content(content) => content.clone(), 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()
}
}) })
} }
} }

View File

@ -5,7 +5,7 @@ use smallvec::smallvec;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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, StyleChain, Styles, Synthesize, Transformation,
}; };
use crate::introspection::{Locatable, Meta, MetaElem}; use crate::introspection::{Locatable, Meta, MetaElem};
@ -248,17 +248,20 @@ fn show(
) -> SourceResult<Content> { ) -> SourceResult<Content> {
match step { match step {
// Apply a user-defined show rule. // Apply a user-defined show rule.
ShowStep::Recipe(recipe, guard) => match &recipe.selector { ShowStep::Recipe(recipe, guard) => {
// If the selector is a regex, the `target` is guaranteed to be a let context = Context::new(target.location(), Some(styles));
// text element. This invokes special regex handling. match &recipe.selector {
Some(Selector::Regex(regex)) => { // If the selector is a regex, the `target` is guaranteed to be a
let text = target.into_packed::<TextElem>().unwrap(); // text element. This invokes special regex handling.
show_regex(engine, &text, regex, recipe, guard) Some(Selector::Regex(regex)) => {
} let text = target.into_packed::<TextElem>().unwrap();
show_regex(engine, &text, regex, recipe, guard, &context)
}
// Just apply the recipe. // Just apply the recipe.
_ => recipe.apply(engine, target.guarded(guard)), _ => recipe.apply(engine, &context, target.guarded(guard)),
}, }
}
// If the verdict picks this step, the `target` is guaranteed to have a // If the verdict picks this step, the `target` is guaranteed to have a
// built-in show rule. // built-in show rule.
@ -269,13 +272,14 @@ fn show(
/// Apply a regex show rule recipe to a target. /// Apply a regex show rule recipe to a target.
fn show_regex( fn show_regex(
engine: &mut Engine, engine: &mut Engine,
elem: &Packed<TextElem>, target: &Packed<TextElem>,
regex: &Regex, regex: &Regex,
recipe: &Recipe, recipe: &Recipe,
index: RecipeIndex, index: RecipeIndex,
context: &Context,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let make = |s: &str| { let make = |s: &str| {
let mut fresh = elem.clone(); let mut fresh = target.clone();
fresh.push_text(s.into()); fresh.push_text(s.into());
fresh.pack() fresh.pack()
}; };
@ -283,16 +287,16 @@ fn show_regex(
let mut result = vec![]; let mut result = vec![];
let mut cursor = 0; 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(); let start = m.start();
if cursor < start { if cursor < start {
result.push(make(&text[cursor..start])); result.push(make(&text[cursor..start]));
} }
let piece = make(m.as_str()); let piece = make(m.as_str());
let transformed = recipe.apply(engine, piece)?; let transformed = recipe.apply(engine, context, piece)?;
result.push(transformed); result.push(transformed);
cursor = m.end(); cursor = m.end();
} }

View File

@ -603,7 +603,7 @@ The example below
This should be a good starting point! If you want to go further, why not create This should be a good starting point! If you want to go further, why not create
a reusable template? a reusable template?
## Bibliographies { #bibliographies } ## Bibliographies
Typst includes a fully-featured bibliography system that is compatible with Typst includes a fully-featured bibliography system that is compatible with
BibTeX files. You can continue to use your `.bib` literature libraries by BibTeX files. You can continue to use your `.bib` literature libraries by
loading them with the [`bibliography`]($bibliography) function. Another 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. 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 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 by [installing the compiler](https://github.com/typst/typst/releases) on your
computer. When you use the web app, we provide a batteries-included computer. When you use the web app, we provide a batteries-included

View File

@ -174,13 +174,13 @@ conditionally remove the header on the first page:
```typ ```typ
>>> #set page("a5", margin: (x: 2.5cm, y: 3cm)) >>> #set page("a5", margin: (x: 2.5cm, y: 3cm))
#set page(header: locate(loc => { #set page(header: context {
if counter(page).at(loc).first() > 1 [ if counter(page).get().first() > 1 [
_Lisa Strassner's Thesis_ _Lisa Strassner's Thesis_
#h(1fr) #h(1fr)
National Academy of Sciences National Academy of Sciences
] ]
})) })
#lorem(150) #lorem(150)
``` ```
@ -206,12 +206,12 @@ such a label exists on the current page:
```typ ```typ
>>> #set page("a5", margin: (x: 2.5cm, y: 3cm)) >>> #set page("a5", margin: (x: 2.5cm, y: 3cm))
#set page(header: locate(loc => { #set page(header: context {
let page-counter = counter(page) let page-counter =
let matches = query(<big-table>, loc) let matches = query(<big-table>)
let current = page-counter.at(loc) let current = counter(page).get()
let has-table = matches.any(m => let has-table = matches.any(m =>
page-counter.at(m.location()) == current counter(page).at(m.location()) == current
) )
if not has-table [ if not has-table [
@ -291,7 +291,7 @@ a custom footer with page numbers and more.
```example ```example
>>> #set page("iso-b6", margin: 1.75cm) >>> #set page("iso-b6", margin: 1.75cm)
#set page(footer: [ #set page(footer: context [
*American Society of Proceedings* *American Society of Proceedings*
#h(1fr) #h(1fr)
#counter(page).display( #counter(page).display(
@ -314,21 +314,20 @@ circle for each page.
```example ```example
>>> #set page("iso-b6", margin: 1.75cm) >>> #set page("iso-b6", margin: 1.75cm)
#set page(footer: [ #set page(footer: context [
*Fun Typography Club* *Fun Typography Club*
#h(1fr) #h(1fr)
#counter(page).display(num => { #let (num,) = counter(page).get()
let circles = num * ( #let circles = num * (
box(circle( box(circle(
radius: 2pt, radius: 2pt,
fill: navy, fill: navy,
)), )),
) )
box( #box(
inset: (bottom: 1pt), inset: (bottom: 1pt),
circles.join(h(1pt)) circles.join(h(1pt))
) )
})
]) ])
This page has a custom footer. 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 // This returns one even though the
// page counter was incremented by 5. // 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 You can also obtain the page numbering pattern from the `{locate}` closure

235
docs/reference/context.md Normal file
View File

@ -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.

View File

@ -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, 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). replace the set rule after the colon with an arbitrary [function]($function).
This function receives the element in question and can return arbitrary content. This function receives the element in question and can return arbitrary content.
Different [fields]($scripting/#fields) are available on the element passed to The available [fields]($scripting/#fields) on the element passed to the function
the function. Below, we define a show rule that formats headings for a fantasy again match the parameters of the respective element function. Below, we define
encyclopedia. a show rule that formats headings for a fantasy encyclopedia.
```example ```example
#set heading(numbering: "(I)") #set heading(numbering: "(I)")
@ -91,7 +91,9 @@ encyclopedia.
#set align(center) #set align(center)
#set text(font: "Inria Serif") #set text(font: "Inria Serif")
\~ #emph(it.body) \~ #emph(it.body)
#counter(heading).display() \~ #counter(heading).display(
it.numbering
) \~
] ]
= Dragon = Dragon

View File

@ -12,7 +12,7 @@ All this is backed by a tightly integrated scripting language with built-in and
user-defined functions. user-defined functions.
## Modes ## 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 default in a Typst document, math mode lets you write mathematical formulas, and
code mode lets you use Typst's scripting features. 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-set rule | `{show par: set block(..)}` | [Styling]($styling/#show-rules) |
| Show rule with function | `{show raw: it => {..}}` | [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) | | 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) | | Conditional | `{if x == 1 {..} else {..}}` | [Scripting]($scripting/#conditionals) |
| For loop | `{for x in (1, 2, 3) {..}}` | [Scripting]($scripting/#loops) | | For loop | `{for x in (1, 2, 3) {..}}` | [Scripting]($scripting/#loops) |
| While loop | `{while x < 10 {..}}` | [Scripting]($scripting/#loops) | | While loop | `{while x < 10 {..}}` | [Scripting]($scripting/#loops) |

View File

@ -753,10 +753,10 @@ fn test_autocomplete<'a>(
{ {
writeln!(output, " Subtest {i} does not match expected completions.") writeln!(output, " Subtest {i} does not match expected completions.")
.unwrap(); .unwrap();
write!(output, " for annotation | ").unwrap(); write!(output, " for annotation | ").unwrap();
print_annotation(output, source, line, annotation); print_annotation(output, source, line, annotation);
write!(output, " Not contained | ").unwrap(); write!(output, " Not contained // ").unwrap();
for item in missing { for item in missing {
write!(output, "{item:?}, ").unwrap() write!(output, "{item:?}, ").unwrap()
} }
@ -772,10 +772,10 @@ fn test_autocomplete<'a>(
{ {
writeln!(output, " Subtest {i} does not match expected completions.") writeln!(output, " Subtest {i} does not match expected completions.")
.unwrap(); .unwrap();
write!(output, " for annotation | ").unwrap(); write!(output, " for annotation | ").unwrap();
print_annotation(output, source, line, annotation); print_annotation(output, source, line, annotation);
write!(output, " Not excluded| ").unwrap(); write!(output, " Not excluded // ").unwrap();
for item in undesired { for item in undesired {
write!(output, "{item:?}, ").unwrap() write!(output, "{item:?}, ").unwrap()
} }
@ -850,12 +850,12 @@ fn test_diagnostics<'a>(
*ok = false; *ok = false;
for unexpected in unexpected_outputs { for unexpected in unexpected_outputs {
write!(output, " Not annotated | ").unwrap(); write!(output, " Not annotated // ").unwrap();
print_annotation(output, source, line, unexpected) print_annotation(output, source, line, unexpected)
} }
for missing in missing_outputs { for missing in missing_outputs {
write!(output, " Not emitted | ").unwrap(); write!(output, " Not emitted // ").unwrap();
print_annotation(output, source, line, missing) print_annotation(output, source, line, missing)
} }
} }

View File

@ -1,6 +1,5 @@
// https://github.com/typst/typst/issues/2650 // https://github.com/typst/typst/issues/2650
#let with-locate(body) = locate(loc => body)
测a试 测a试
#with-locate[a] #context [a]

View File

@ -17,7 +17,7 @@
caption: [A pirate @arrgh in @intro], caption: [A pirate @arrgh in @intro],
) )
#locate(loc => [Citation @distress on page #loc.page()]) #context [Citation @distress on page #here().page()]
#pagebreak() #pagebreak()
#bibliography("/files/works.bib", style: "chicago-notes") #bibliography("/files/works.bib", style: "chicago-notes")

View File

@ -4,7 +4,7 @@
--- ---
#let my = $pi$ #let my = $pi$
#let f1 = box(baseline: 10pt, [f]) #let f1 = box(baseline: 10pt, [f])
#let f2 = style(sty => f1) #let f2 = context f1
#show math.vec: [nope] #show math.vec: [nope]
$ pi a $ $ pi a $

View File

@ -10,4 +10,3 @@ This and next page should not be numbered
#counter(page).update(1) #counter(page).update(1)
This page should This page should

View File

@ -17,10 +17,7 @@
--- ---
// Test it with query. // Test it with query.
#set raw(lang: "rust") #set raw(lang: "rust")
#locate(loc => { #context query(<myraw>).first().lang
let elem = query(<myraw>, loc).first()
elem.lang
})
`raw` <myraw> `raw` <myraw>
--- ---

View File

@ -83,20 +83,19 @@
--- ---
// Test length `to-absolute` method. // Test length `to-absolute` method.
#set text(size: 12pt) #set text(size: 12pt)
#style(styles => { #context {
test((6pt).to-absolute(styles), 6pt) test((6pt).to-absolute(), 6pt)
test((6pt + 10em).to-absolute(styles), 126pt) test((6pt + 10em).to-absolute(), 126pt)
test((10em).to-absolute(styles), 120pt) test((10em).to-absolute(), 120pt)
}) }
#set text(size: 64pt) #set text(size: 64pt)
#style(styles => { #context {
test((6pt).to-absolute(styles), 6pt) test((6pt).to-absolute(), 6pt)
test((6pt + 10em).to-absolute(styles), 646pt) test((6pt + 10em).to-absolute(), 646pt)
test((10em).to-absolute(styles), 640pt) test((10em).to-absolute(), 640pt)
}) }
--- ---
// Error: 2-21 cannot convert a length with non-zero em units (`-6pt + 10.5em`) to pt // Error: 2-21 cannot convert a length with non-zero em units (`-6pt + 10.5em`) to pt

View File

@ -44,8 +44,8 @@
--- ---
// Test cyclic imports during layout. // Test cyclic imports during layout.
// Error: 14-37 maximum layout depth exceeded // Error: 2-38 maximum show rule depth exceeded
// Hint: 14-37 try to reduce the amount of nesting in your layout // Hint: 2-38 check whether the show rule matches its own output
#layout(_ => include "recursion.typ") #layout(_ => include "recursion.typ")
--- ---

View File

@ -13,10 +13,9 @@
#figure([Iguana], kind: "iguana", supplement: none) #figure([Iguana], kind: "iguana", supplement: none)
== I == I
#let test-selector(selector, ref) = locate(loc => { #let test-selector(selector, ref) = context {
let elems = query(selector, loc) test(query(selector).map(e => e.body), ref)
test(elems.map(e => e.body), ref) }
})
// Test `or`. // Test `or`.
#test-selector( #test-selector(

View File

@ -6,10 +6,10 @@
h(1fr) h(1fr)
text(0.8em)[_Chapter 1_] text(0.8em)[_Chapter 1_]
}, },
footer: align(center)[\~ #counter(page).display() \~], footer: context align(center)[\~ #counter(page).display() \~],
background: counter(page).display(n => if n <= 2 { background: context if counter(page).get().first() <= 2 {
place(center + horizon, circle(radius: 1cm, fill: luma(90%))) place(center + horizon, circle(radius: 1cm, fill: luma(90%)))
}) }
) )
But, soft! what light through yonder window breaks? It is the east, and Juliet But, soft! what light through yonder window breaks? It is the east, and Juliet

View File

@ -11,8 +11,8 @@ $ scripts(sum)_1^2 != sum_1^2 $
$ limits(integral)_a^b != integral_a^b $ $ limits(integral)_a^b != integral_a^b $
--- ---
// Error: 30-34 unknown variable: oops // Error: 25-29 unknown variable: oops
$ attach(A, t: #locate(it => oops)) $ $ attach(A, t: #context oops) $
--- ---
// Show and let rules for limits and scripts // Show and let rules for limits and scripts

View File

@ -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))

181
tests/typ/meta/context.typ Normal file
View File

@ -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)

View File

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

View File

@ -45,7 +45,7 @@
#show figure.caption: it => emph[ #show figure.caption: it => emph[
#it.body #it.body
(#it.supplement (#it.supplement
#it.counter.display(it.numbering)) #context it.counter.display(it.numbering))
] ]
#figure( #figure(

View File

@ -57,12 +57,11 @@
#set heading(outlined: true, numbering: "1.") #set heading(outlined: true, numbering: "1.")
// This is purposefully an empty #context [
#locate(loc => [
Non-outlined elements: 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(", ")) .map(it => it.body).join(", "))
]) ]
#heading("A", outlined: false) #heading("A", outlined: false)
#heading("B", outlined: true) #heading("B", outlined: true)

View File

@ -11,8 +11,8 @@
#show figure: set image(width: 80%) #show figure: set image(width: 80%)
= List of Figures = List of Figures
#locate(it => { #context {
let elements = query(selector(figure).after(it), it) let elements = query(selector(figure).after(here()))
for it in elements [ for it in elements [
Figure Figure
#numbering(it.numbering, #numbering(it.numbering,
@ -21,7 +21,7 @@
#box(width: 1fr, repeat[.]) #box(width: 1fr, repeat[.])
#counter(page).at(it.location()).first() \ #counter(page).at(it.location()).first() \
] ]
}) }
#figure( #figure(
image("/files/glacier.jpg"), image("/files/glacier.jpg"),

View File

@ -4,19 +4,17 @@
#set page( #set page(
paper: "a7", paper: "a7",
margin: (y: 1cm, x: 0.5cm), margin: (y: 1cm, x: 0.5cm),
header: { header: context {
smallcaps[Typst Academy] smallcaps[Typst Academy]
h(1fr) h(1fr)
locate(it => { let after = query(selector(heading).after(here()))
let after = query(selector(heading).after(it), it) let before = query(selector(heading).before(here()))
let before = query(selector(heading).before(it), it) let elem = if before.len() != 0 {
let elem = if before.len() != 0 { before.last()
before.last() } else if after.len() != 0 {
} else if after.len() != 0 { after.first()
after.first() }
} emph(elem.body)
emph(elem.body)
})
} }
) )

View File

@ -9,20 +9,20 @@
$ 2 + 3 $ $ 2 + 3 $
#s.update(double) #s.update(double)
Is: #s.display(), Is: #context s.get(),
Was: #locate(location => { Was: #context {
let it = query(math.equation, location).first() let it = query(math.equation).first()
s.at(it.location()) s.at(it.location())
}). }.
--- ---
// Try same key with different initial value. // Try same key with different initial value.
#state("key", 2).display() #context state("key", 2).get()
#state("key").update(x => x + 1) #state("key").update(x => x + 1)
#state("key", 2).display() #context state("key", 2).get()
#state("key", 3).display() #context state("key", 3).get()
#state("key").update(x => x + 1) #state("key").update(x => x + 1)
#state("key", 2).display() #context state("key", 2).get()
--- ---
#set page(width: 200pt) #set page(width: 200pt)
@ -30,15 +30,15 @@ Was: #locate(location => {
#let ls = state("lorem", lorem(1000).split(".")) #let ls = state("lorem", lorem(1000).split("."))
#let loremum(count) = { #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)) ls.update(list => list.slice(count))
} }
#let fs = state("fader", red) #let fs = state("fader", red)
#let trait(title) = block[ #let trait(title) = block[
#fs.display(color => text(fill: color)[ #context text(fill: fs.get())[
*#title:* #loremum(1) *#title:* #loremum(1)
]) ]
#fs.update(color => color.lighten(30%)) #fs.update(color => color.lighten(30%))
] ]
@ -52,5 +52,5 @@ Was: #locate(location => {
// Warning: layout did not converge within 5 attempts // Warning: layout did not converge within 5 attempts
// Hint: check if any states or queries are updating themselves // Hint: check if any states or queries are updating themselves
#let s = state("s", 1) #let s = state("s", 1)
#locate(loc => s.update(s.final(loc) + 1)) #context s.update(s.final() + 1)
#s.display() #context s.get()

View File

@ -132,7 +132,7 @@
"captures": { "1": { "name": "punctuation.definition.reference.typst" } } "captures": { "1": { "name": "punctuation.definition.reference.typst" } }
}, },
{ {
"begin": "(#)(let|set|show)\\b", "begin": "(#)(let|set|show|context)\\b",
"end": "\n|(;)|(?=])", "end": "\n|(;)|(?=])",
"beginCaptures": { "beginCaptures": {
"0": { "name": "keyword.other.typst" }, "0": { "name": "keyword.other.typst" },
@ -263,7 +263,7 @@
}, },
{ {
"name": "keyword.other.typst", "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", "name": "keyword.control.conditional.typst",