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_blob",
"icu_segmenter",
"if_chain",
"image",
"indexmap 2.1.0",
"kamadak-exif",

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ use if_chain::if_chain;
use serde::{Deserialize, Serialize};
use typst::foundations::{
fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label,
NoneValue, Repr, Scope, Type, Value,
NoneValue, Repr, Scope, StyleChain, Styles, Type, Value,
};
use typst::model::Document;
use typst::syntax::{
@ -135,6 +135,17 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
}
}
// Behind a half-completed context block: "#context |".
if_chain! {
if let Some(prev) = ctx.leaf.prev_leaf();
if prev.kind() == SyntaxKind::Context;
then {
ctx.from = ctx.cursor;
code_completions(ctx, false);
return true;
}
}
// Directly after a raw block.
let mut s = Scanner::new(ctx.text);
s.jump(ctx.leaf.offset());
@ -333,10 +344,10 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if prev.is::<ast::Expr>();
if prev.parent_kind() != Some(SyntaxKind::Markup) ||
prev.prev_sibling_kind() == Some(SyntaxKind::Hash);
if let Some(value) = analyze_expr(ctx.world, &prev).into_iter().next();
if let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next();
then {
ctx.from = ctx.cursor;
field_access_completions(ctx, &value);
field_access_completions(ctx, &value, &styles);
return true;
}
}
@ -348,10 +359,10 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if prev.kind() == SyntaxKind::Dot;
if let Some(prev_prev) = prev.prev_sibling();
if prev_prev.is::<ast::Expr>();
if let Some(value) = analyze_expr(ctx.world, &prev_prev).into_iter().next();
if let Some((value, styles)) = analyze_expr(ctx.world, &prev_prev).into_iter().next();
then {
ctx.from = ctx.leaf.offset();
field_access_completions(ctx, &value);
field_access_completions(ctx, &value, &styles);
return true;
}
}
@ -360,7 +371,11 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
}
/// Add completions for all fields on a value.
fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
fn field_access_completions(
ctx: &mut CompletionContext,
value: &Value,
styles: &Option<Styles>,
) {
for (name, value) in value.ty().scope().iter() {
ctx.value_completion(Some(name.clone()), value, true, None);
}
@ -421,6 +436,23 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
ctx.value_completion(Some(name.clone().into()), value, false, None);
}
}
Value::Func(func) => {
// Autocomplete get rules.
if let Some((elem, styles)) = func.element().zip(styles.as_ref()) {
for param in elem.params().iter().filter(|param| !param.required) {
if let Some(value) = elem.field_id(param.name).and_then(|id| {
elem.field_from_styles(id, StyleChain::new(styles))
}) {
ctx.value_completion(
Some(param.name.into()),
&value,
false,
None,
);
}
}
}
}
Value::Plugin(plugin) => {
for name in plugin.iter() {
ctx.completions.push(Completion {
@ -862,6 +894,12 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
"Transforms everything that follows.",
);
ctx.snippet_completion(
"context expression",
"context ${}",
"Provides contextual data.",
);
ctx.snippet_completion(
"let binding",
"let ${name} = ${value}",

View File

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

View File

@ -617,6 +617,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
vtable: <#ident as #foundations::Capable>::vtable,
field_id: |name| name.parse().ok().map(|id: Fields| id as u8),
field_name: |id| id.try_into().ok().map(Fields::to_str),
field_from_styles: <#ident as #foundations::Fields>::field_from_styles,
local_name: #local_name,
scope: #foundations::Lazy::new(|| #scope),
params: #foundations::Lazy::new(|| ::std::vec![#(#params),*])
@ -866,6 +867,20 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
quote! { Fields::#enum_ident => #expr }
});
// Fields that can be accessed using the `field_from_styles` method.
let field_from_styles_arms = element.visible_fields().map(|field| {
let Field { enum_ident, .. } = field;
let expr = if field.required || field.synthesized {
quote! { None }
} else {
let value = create_style_chain_access(field, false, quote!(None));
quote! { Some(#into_value(#value)) }
};
quote! { Fields::#enum_ident => #expr }
});
// Sets fields from the style chain.
let materializes = visible_non_ghost()
.filter(|field| !field.required && !field.synthesized)
@ -939,6 +954,14 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
}
}
fn field_from_styles(id: u8, styles: #foundations::StyleChain) -> Option<#foundations::Value> {
let id = Fields::try_from(id).ok()?;
match id {
#(#field_from_styles_arms,)*
_ => None,
}
}
fn materialize(&mut self, styles: #foundations::StyleChain) {
#(#materializes)*
}

View File

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

View File

@ -40,6 +40,8 @@ use syn::DeriveInput;
/// You can customize some properties of the resulting function:
/// - `scope`: Indicates that the function has an associated scope defined by
/// the `#[scope]` macro.
/// - `contextual`: Indicates that the function makes use of context. This has
/// no effect on the behaviour itself, but is used for the docs.
/// - `name`: The functions's normal name (e.g. `min`). Defaults to the Rust
/// name in kebab-case.
/// - `title`: The functions's title case name (e.g. `Minimum`). Defaults to the

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
use ecow::{eco_vec, EcoVec};
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use crate::eval::{ops, Eval, Vm};
use crate::foundations::{Array, Content, Dict, Str, Value};
use crate::eval::{ops, CapturesVisitor, Eval, Vm};
use crate::foundations::{
Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, Value,
};
use crate::syntax::ast::{self, AstNode};
impl Eval for ast::Code<'_> {
@ -40,7 +42,11 @@ fn eval_code<'a>(
}
let tail = eval_code(vm, exprs)?.display();
Value::Content(tail.styled_with_recipe(&mut vm.engine, recipe)?)
Value::Content(tail.styled_with_recipe(
&mut vm.engine,
vm.context,
recipe,
)?)
}
_ => expr.eval(vm)?,
};
@ -117,6 +123,7 @@ impl Eval for ast::Expr<'_> {
Self::DestructAssign(v) => v.eval(vm),
Self::Set(_) => bail!(forbidden("set")),
Self::Show(_) => bail!(forbidden("show")),
Self::Contextual(v) => v.eval(vm).map(Value::Content),
Self::Conditional(v) => v.eval(vm),
Self::While(v) => v.eval(vm),
Self::For(v) => v.eval(vm),
@ -129,7 +136,7 @@ impl Eval for ast::Expr<'_> {
.spanned(span);
if vm.inspected == Some(span) {
vm.engine.tracer.value(v.clone());
vm.trace(v.clone());
}
Ok(v)
@ -296,6 +303,56 @@ impl Eval for ast::FieldAccess<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.target().eval(vm)?;
let field = self.field();
value.field(&field).at(field.span())
let err = match value.field(&field).at(field.span()) {
Ok(value) => return Ok(value),
Err(err) => err,
};
// Check whether this is a get rule field access.
if_chain::if_chain! {
if let Value::Func(func) = &value;
if let Some(element) = func.element();
if let Some(id) = element.field_id(&field);
let styles = vm.context.styles().at(field.span());
if let Some(value) = element.field_from_styles(
id,
styles.as_ref().map(|&s| s).unwrap_or_default(),
);
then {
// Only validate the context once we know that this is indeed
// a field from the style chain.
let _ = styles?;
return Ok(value);
}
}
Err(err)
}
}
impl Eval for ast::Contextual<'_> {
type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let body = self.body();
// Collect captured variables.
let captured = {
let mut visitor = CapturesVisitor::new(Some(&vm.scopes), Capturer::Context);
visitor.visit(body.to_untyped());
visitor.finish()
};
// Define the closure.
let closure = Closure {
node: self.body().to_untyped().clone(),
defaults: vec![],
captured,
num_pos_params: 0,
};
let func = Func::from(closure).spanned(body.span());
Ok(ContextElem::new(func).pack())
}
}

View File

@ -43,7 +43,7 @@ fn eval_markup<'a>(
}
let tail = eval_markup(vm, exprs)?;
seq.push(tail.styled_with_recipe(&mut vm.engine, recipe)?)
seq.push(tail.styled_with_recipe(&mut vm.engine, vm.context, recipe)?)
}
expr => match expr.eval(vm)? {
Value::Label(label) => {

View File

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

View File

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

View File

@ -2,7 +2,7 @@ use comemo::Tracked;
use crate::engine::Engine;
use crate::eval::FlowEvent;
use crate::foundations::{IntoValue, Scopes};
use crate::foundations::{Context, IntoValue, Scopes, Value};
use crate::syntax::ast::{self, AstNode};
use crate::syntax::Span;
use crate::World;
@ -20,13 +20,20 @@ pub struct Vm<'a> {
pub(crate) scopes: Scopes<'a>,
/// A span that is currently under inspection.
pub(crate) inspected: Option<Span>,
/// Data that is contextually made accessible to code behind the scenes.
pub(crate) context: &'a Context<'a>,
}
impl<'a> Vm<'a> {
/// Create a new virtual machine.
pub fn new(engine: Engine<'a>, scopes: Scopes<'a>, target: Span) -> Self {
pub fn new(
engine: Engine<'a>,
context: &'a Context<'a>,
scopes: Scopes<'a>,
target: Span,
) -> Self {
let inspected = target.id().and_then(|id| engine.tracer.inspected(id));
Self { engine, flow: None, scopes, inspected }
Self { engine, context, flow: None, scopes, inspected }
}
/// Access the underlying world.
@ -38,8 +45,16 @@ impl<'a> Vm<'a> {
pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) {
let value = value.into_value();
if self.inspected == Some(var.span()) {
self.engine.tracer.value(value.clone());
self.trace(value.clone());
}
self.scopes.top.define(var.get().clone(), value);
}
/// Trace a value.
#[cold]
pub fn trace(&mut self, value: Value) {
self.engine
.tracer
.value(value.clone(), self.context.styles.map(|s| s.to_map()));
}
}

View File

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

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> {
fn input() -> CastInfo {
T::input()

View File

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

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

View File

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

View File

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

View File

@ -169,10 +169,15 @@ impl Scope {
}
/// Define a captured, immutable binding.
pub fn define_captured(&mut self, var: impl Into<EcoString>, value: impl IntoValue) {
pub fn define_captured(
&mut self,
var: impl Into<EcoString>,
value: impl IntoValue,
capturer: Capturer,
) {
self.map.insert(
var.into(),
Slot::new(value.into_value(), Kind::Captured, self.category),
Slot::new(value.into_value(), Kind::Captured(capturer), self.category),
);
}
@ -246,7 +251,16 @@ enum Kind {
/// A normal, mutable binding.
Normal,
/// A captured copy of another variable.
Captured,
Captured(Capturer),
}
/// What the variable was captured by.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Capturer {
/// Captured by a function / closure.
Function,
/// Captured by a context expression.
Context,
}
impl Slot {
@ -264,10 +278,14 @@ impl Slot {
fn write(&mut self) -> StrResult<&mut Value> {
match self.kind {
Kind::Normal => Ok(&mut self.value),
Kind::Captured => {
Kind::Captured(capturer) => {
bail!(
"variables from outside the function are \
read-only and cannot be modified"
"variables from outside the {} are \
read-only and cannot be modified",
match capturer {
Capturer::Function => "function",
Capturer::Context => "context expression",
}
)
}
}

View File

@ -1,15 +1,16 @@
use std::any::{Any, TypeId};
use std::sync::Arc;
use comemo::Tracked;
use ecow::{eco_format, EcoString, EcoVec};
use smallvec::SmallVec;
use crate::diag::{bail, StrResult};
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{
cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func,
Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value,
cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue,
Func, Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value,
};
use crate::introspection::{Locatable, Location};
use crate::introspection::{Introspector, Locatable, Location};
use crate::symbols::Symbol;
use crate::text::TextElem;
@ -66,11 +67,10 @@ pub use crate::__select_where as select_where;
///
/// # Example
/// ```example
/// #locate(loc => query(
/// #context query(
/// heading.where(level: 1)
/// .or(heading.where(level: 2)),
/// loc,
/// ))
/// .or(heading.where(level: 2))
/// )
///
/// = This will be found
/// == So will this
@ -300,11 +300,29 @@ cast! {
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct LocatableSelector(pub Selector);
impl LocatableSelector {
/// Resolve this selector into a location that is guaranteed to be unique.
pub fn resolve_unique(
&self,
introspector: Tracked<Introspector>,
context: &Context,
) -> HintedStrResult<Location> {
match &self.0 {
Selector::Location(loc) => Ok(*loc),
other => {
context.introspect()?;
Ok(introspector.query_unique(other).map(|c| c.location().unwrap())?)
}
}
}
}
impl Reflect for LocatableSelector {
fn input() -> CastInfo {
CastInfo::Union(vec![
CastInfo::Type(Type::of::<Label>()),
CastInfo::Type(Type::of::<Func>()),
CastInfo::Type(Type::of::<Location>()),
CastInfo::Type(Type::of::<Selector>()),
])
}
@ -314,7 +332,10 @@ impl Reflect for LocatableSelector {
}
fn castable(value: &Value) -> bool {
Label::castable(value) || Func::castable(value) || Selector::castable(value)
Label::castable(value)
|| Func::castable(value)
|| Location::castable(value)
|| Selector::castable(value)
}
}

View File

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

View File

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

View File

@ -5,13 +5,13 @@ use comemo::{Tracked, TrackedMut};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use smallvec::{smallvec, SmallVec};
use crate::diag::{At, SourceResult, StrResult};
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::{Engine, Route};
use crate::eval::Tracer;
use crate::foundations::{
cast, elem, func, scope, select_where, ty, Array, Content, Element, Func, IntoValue,
Label, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str,
StyleChain, Value,
cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context,
Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr,
Selector, Show, Smart, Str, StyleChain, Value,
};
use crate::introspection::{Introspector, Locatable, Location, Locator, Meta};
use crate::layout::{Frame, FrameItem, PageElem};
@ -27,14 +27,30 @@ use crate::World;
/// headings, figures, and more. Moreover, you can define custom counters for
/// other things you want to count.
///
/// # Displaying a counter { #displaying }
/// To display the current value of the heading counter, you call the `counter`
/// function with the `key` set to `heading` and then call the `display` method
/// on the counter. To see any output, you also have to enable heading
/// [numbering]($heading.numbering).
/// Since counters change throughout the course of the document, their current
/// value is _contextual_ It is recommended to read the chapter on
/// [context]($context) before continuing here.
///
/// The `display` method optionally takes an argument telling it how to format
/// the counter. This can be a [numbering pattern or a function]($numbering).
/// # Accessing a counter { #accessing }
/// To access the raw value of a counter, we can use the [`get`]($counter.get)
/// function. This function returns an [array]($array): Counters can have
/// multiple levels (in the case of headings for sections, subsections, and so
/// on), and each item in the array corresponds to one level.
///
/// ```example
/// #set heading(numbering: "1.")
///
/// = Introduction
/// Raw value of heading counter is
/// #context counter(heading).get()
/// ```
///
/// # Displaying a counter { #displaying }
/// Often, we want to display the value of a counter in a more human-readable
/// way. To do that, we can call the [`display`]($counter.display) function on
/// the counter. This function retrieves the current counter value and formats
/// it either with a provided or with an automatically inferred
/// [numbering]($numbering).
///
/// ```example
/// #set heading(numbering: "1.")
@ -43,25 +59,26 @@ use crate::World;
/// Some text here.
///
/// = Background
/// The current value is:
/// #counter(heading).display()
/// The current value is: #context {
/// counter(heading).display()
/// }
///
/// Or in roman numerals:
/// #counter(heading).display("I")
/// Or in roman numerals: #context {
/// counter(heading).display("I")
/// }
/// ```
///
/// # Modifying a counter { #modifying }
/// To modify a counter, you can use the `step` and `update` methods:
///
/// - The `step` method increases the value of the counter by one. Because
/// counters can have multiple levels (in the case of headings for sections,
/// subsections, and so on), the `step` method optionally takes a `level`
/// counters can have multiple levels , it optionally takes a `level`
/// argument. If given, the counter steps at the given depth.
///
/// - The `update` method allows you to arbitrarily modify the counter. In its
/// basic form, you give it an integer (or multiple for multiple levels). For
/// more flexibility, you can instead also give it a function that gets the
/// current value and returns a new value.
/// basic form, you give it an integer (or an array for multiple levels). For
/// more flexibility, you can instead also give it a function that receives
/// the current value and returns a new value.
///
/// The heading counter is stepped before the heading is displayed, so
/// `Analysis` gets the number seven even though the counter is at six after the
@ -82,50 +99,11 @@ use crate::World;
/// #counter(heading).step(level: 2)
///
/// == 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
/// 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
@ -153,6 +131,49 @@ use crate::World;
/// 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
/// Counters can travel through time! You can find out the final value of the
/// counter before it is reached and even determine what the value was at any
@ -162,17 +183,11 @@ use crate::World;
/// #let mine = counter("mycounter")
///
/// = Values
/// #locate(loc => {
/// let start-val = mine.at(loc)
/// let elements = query(<intro>, loc)
/// let intro-val = mine.at(
/// elements.first().location()
/// )
/// let final-val = mine.final(loc)
/// [Starts as: #start-val \
/// Value at intro is: #intro-val \
/// Final value is: #final-val \ ]
/// })
/// #context [
/// Value here: #mine.get() \
/// At intro: #mine.at(<intro>) \
/// Final value: #mine.final()
/// ]
///
/// #mine.update(n => n + 3)
///
@ -183,27 +198,6 @@ use crate::World;
/// #mine.step()
/// ```
///
/// Let's dissect what happens in the example above:
///
/// - We call [`locate`]($locate) to get access to the current location in the
/// document. We then pass this location to our counter's `at` method to get
/// its value at the current location. The `at` method always returns an array
/// because counters can have multiple levels. As the counter starts at zero,
/// the first value is thus `{(0,)}`.
///
/// - We now [`query`]($query) the document for all elements with the
/// `{<intro>}` label. The result is an array from which we extract the first
/// (and only) element's [location]($content.location). We then look up the
/// value of the counter at that location. The first update to the counter
/// sets it to `{0 + 3 = 3}`. At the introduction heading, the value is thus
/// `{(3,)}`.
///
/// - Last but not least, we call the `final` method on the counter. It tells us
/// what the counter's value will be at the end of the document. We also need
/// to give it a location to prove that we are inside of a `locate` call, but
/// which one doesn't matter. After the heading follow two calls to `step()`,
/// so the final value is `{(5,)}`.
///
/// # Other kinds of state { #other-state }
/// The `counter` type is closely related to [state]($state) type. Read its
/// documentation for more details on state management in Typst and why it
@ -247,6 +241,41 @@ impl Counter {
Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
}
/// Gets the value of the counter at the given location. Always returns an
/// array of integers, even if the counter has just one number.
pub fn at_loc(
&self,
engine: &mut Engine,
loc: Location,
) -> SourceResult<CounterState> {
let sequence = self.sequence(engine)?;
let offset = engine
.introspector
.query(&self.selector().before(loc.into(), true))
.len();
let (mut state, page) = sequence[offset].clone();
if self.is_page() {
let delta = engine.introspector.page(loc).get().saturating_sub(page.get());
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
}
/// Displays the value of the counter at the given location.
pub fn display_at_loc(
&self,
engine: &mut Engine,
loc: Location,
styles: StyleChain,
numbering: &Numbering,
) -> SourceResult<Content> {
let context = Context::new(Some(loc), Some(styles));
Ok(self
.at_loc(engine, loc)?
.display(engine, &context, numbering)?
.display())
}
/// Produce the whole sequence of counter states.
///
/// This has to happen just once for all counters, cutting down the number
@ -313,7 +342,7 @@ impl Counter {
/// The selector relevant for this counter's updates.
fn selector(&self) -> Selector {
let mut selector = select_where!(UpdateElem, Key => self.0.clone());
let mut selector = select_where!(CounterUpdateElem, Key => self.0.clone());
if let CounterKey::Selector(key) = &self.0 {
selector = Selector::Or(eco_vec![selector, key.clone()]);
@ -326,6 +355,46 @@ impl Counter {
fn is_page(&self) -> bool {
self.0 == CounterKey::Page
}
/// Shared implementation of displaying between `counter.display` and
/// `DisplayElem`, which will be deprecated.
fn display_impl(
&self,
engine: &mut Engine,
location: Location,
numbering: Smart<Numbering>,
both: bool,
styles: Option<StyleChain>,
) -> SourceResult<Value> {
let numbering = numbering
.as_custom()
.or_else(|| {
let styles = styles?;
let CounterKey::Selector(Selector::Elem(func, _)) = self.0 else {
return None;
};
if func == HeadingElem::elem() {
HeadingElem::numbering_in(styles).clone()
} else if func == FigureElem::elem() {
FigureElem::numbering_in(styles).clone()
} else if func == EquationElem::elem() {
EquationElem::numbering_in(styles).clone()
} else {
None
}
})
.unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into());
let state = if both {
self.both(engine, location)?
} else {
self.at_loc(engine, location)?
};
let context = Context::new(Some(location), styles);
state.display(engine, &context, &numbering)
}
}
#[scope]
@ -347,10 +416,38 @@ impl Counter {
Self(key)
}
/// Displays the current value of the counter.
#[func]
/// Retrieves the value of the counter at the current location. Always
/// returns an array of integers, even if the counter has just one number.
///
/// This is equivalent to `{counter.at(here())}`.
#[func(contextual)]
pub fn get(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span,
) -> SourceResult<CounterState> {
let loc = context.location().at(span)?;
self.at_loc(engine, loc)
}
/// Displays the current value of the counter with a numbering and returns
/// the formatted output.
///
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
/// function also works without an established context. Then, it will create
/// opaque contextual content rather than directly returning the output of
/// the numbering. This behaviour will be removed in a future release.
#[func(contextual)]
pub fn display(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The call span of the display.
span: Span,
/// A [numbering pattern or a function]($numbering), which specifies how
@ -359,11 +456,11 @@ impl Counter {
/// numbers varies, e.g. for the heading argument, you can use an
/// [argument sink]($arguments).
///
/// If this is omitted, displays the counter with the numbering style
/// for the counted element or with the pattern `{"1.1"}` if no such
/// style exists.
/// If this is omitted or set to `{auto}`, displays the counter with the
/// numbering style for the counted element or with the pattern
/// `{"1.1"}` if no such style exists.
#[default]
numbering: Option<Numbering>,
numbering: Smart<Numbering>,
/// If enabled, displays the current and final top-level count together.
/// Both can be styled through a single numbering pattern. This is used
/// by the page numbering property to display the current and total
@ -371,8 +468,70 @@ impl Counter {
#[named]
#[default(false)]
both: bool,
) -> Content {
DisplayElem::new(self, numbering, both).pack().spanned(span)
) -> SourceResult<Value> {
if let Some(loc) = context.location {
self.display_impl(engine, loc, numbering, both, context.styles)
} else {
Ok(CounterDisplayElem::new(self, numbering, both)
.pack()
.spanned(span)
.into_value())
}
}
/// Retrieves the value of the counter at the given location. Always returns
/// an array of integers, even if the counter has just one number.
///
/// The `selector` must match exactly one element in the document. The most
/// useful kinds of selectors for this are [labels]($label) and
/// [locations]($location).
///
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
/// function also works without a known context if the `selector` is a
/// location. This behaviour will be removed in a future release.
#[func(contextual)]
pub fn at(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span,
/// The place at which the counter's value should be retrieved.
selector: LocatableSelector,
) -> SourceResult<CounterState> {
let loc = selector.resolve_unique(engine.introspector, context).at(span)?;
self.at_loc(engine, loc)
}
/// Retrieves the value of the counter at the end of the document. Always
/// returns an array of integers, even if the counter has just one number.
#[func(contextual)]
pub fn final_(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span,
/// _Compatibility:_ This argument only exists for compatibility with
/// Typst 0.10 and lower and shouldn't be used anymore.
#[default]
location: Option<Location>,
) -> SourceResult<CounterState> {
if location.is_none() {
context.location().at(span)?;
}
let sequence = self.sequence(engine)?;
let (mut state, page) = sequence.last().unwrap().clone();
if self.is_page() {
let delta = engine.introspector.pages().get().saturating_sub(page.get());
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
}
/// Increases the value of the counter by one.
@ -411,61 +570,7 @@ impl Counter {
/// return the new value (integer or array).
update: CounterUpdate,
) -> Content {
UpdateElem::new(self.0, update).pack().spanned(span)
}
/// Gets the value of the counter at the given location. Always returns an
/// array of integers, even if the counter has just one number.
#[func]
pub fn at(
&self,
/// The engine.
engine: &mut Engine,
/// The location at which the counter value should be retrieved. A
/// suitable location can be retrieved from [`locate`]($locate) or
/// [`query`]($query).
location: Location,
) -> SourceResult<CounterState> {
let sequence = self.sequence(engine)?;
let offset = engine
.introspector
.query(&self.selector().before(location.into(), true))
.len();
let (mut state, page) = sequence[offset].clone();
if self.is_page() {
let delta =
engine.introspector.page(location).get().saturating_sub(page.get());
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
}
/// Gets the value of the counter at the end of the document. Always returns
/// an array of integers, even if the counter has just one number.
#[func]
pub fn final_(
&self,
/// The engine.
engine: &mut Engine,
/// Can be an arbitrary location, as its value is irrelevant for the
/// method's return value. Why is it required then? Typst has to
/// evaluate parts of your code multiple times to determine all counter
/// values. By only allowing this method within [`locate`]($locate)
/// calls, the amount of code that can depend on the method's result is
/// reduced. If you could call `final` directly at the top level of a
/// module, the evaluation of the whole module and its exports could
/// depend on the counter's value.
location: Location,
) -> SourceResult<CounterState> {
let _ = location;
let sequence = self.sequence(engine)?;
let (mut state, page) = sequence.last().unwrap().clone();
if self.is_page() {
let delta = engine.introspector.pages().get().saturating_sub(page.get());
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
CounterUpdateElem::new(self.0, update).pack().spanned(span)
}
}
@ -480,7 +585,8 @@ impl Repr for Counter {
pub enum CounterKey {
/// The page counter.
Page,
/// Counts elements matching the given selectors. Only works for locatable
/// Counts elements matching the given selectors. Only works for
/// [locatable]($location/#locatable)
/// elements or labels.
Selector(Selector),
/// Counts through manual counters with the same key.
@ -517,7 +623,6 @@ impl Repr for CounterKey {
}
/// An update to perform on a counter.
#[ty(cast)]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum CounterUpdate {
/// Set the counter to the specified state.
@ -528,14 +633,8 @@ pub enum CounterUpdate {
Func(Func),
}
impl Repr for CounterUpdate {
fn repr(&self) -> EcoString {
"..".into()
}
}
cast! {
type CounterUpdate,
CounterUpdate,
v: CounterState => Self::Set(v),
v: Func => Self::Func(v),
}
@ -570,8 +669,10 @@ impl CounterState {
CounterUpdate::Set(state) => *self = state,
CounterUpdate::Step(level) => self.step(level, 1),
CounterUpdate::Func(func) => {
*self =
func.call(engine, self.0.iter().copied())?.cast().at(func.span())?
*self = func
.call(engine, &Context::none(), self.0.iter().copied())?
.cast()
.at(func.span())?
}
}
Ok(())
@ -600,9 +701,10 @@ impl CounterState {
pub fn display(
&self,
engine: &mut Engine,
context: &Context,
numbering: &Numbering,
) -> SourceResult<Content> {
Ok(numbering.apply(engine, &self.0)?.display())
) -> SourceResult<Value> {
numbering.apply(engine, context, &self.0)
}
}
@ -616,81 +718,80 @@ cast! {
.collect::<StrResult<_>>()?),
}
/// Executes a display of a state.
#[elem(Locatable, Show)]
struct DisplayElem {
/// The counter.
#[required]
counter: Counter,
/// The numbering to display the counter with.
#[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.
#[elem(Locatable, Show, Count)]
struct UpdateElem {
#[elem(Construct, Locatable, Show, Count)]
struct CounterUpdateElem {
/// The key that identifies the counter.
#[required]
key: CounterKey,
/// The update to perform on the counter.
#[required]
#[internal]
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> {
Ok(Content::empty())
}
}
impl Count for Packed<UpdateElem> {
impl Count for Packed<CounterUpdateElem> {
fn update(&self) -> Option<CounterUpdate> {
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
/// and the logical page counter.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@ -721,7 +822,9 @@ impl ManualPageCounter {
match item {
FrameItem::Group(group) => self.visit(engine, &group.frame)?,
FrameItem::Meta(Meta::Elem(elem), _) => {
let Some(elem) = elem.to_packed::<UpdateElem>() else { continue };
let Some(elem) = elem.to_packed::<CounterUpdateElem>() else {
continue;
};
if *elem.key() == CounterKey::Page {
let mut state = CounterState(smallvec![self.logical]);
state.update(engine, elem.update.clone())?;

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.
pub fn query_label(&self, label: Label) -> StrResult<&Content> {
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::foundations::{
elem, func, Content, Func, NativeElement, Packed, Show, StyleChain,
cast, elem, func, Content, Context, Func, LocatableSelector, NativeElement, Packed,
Show, StyleChain, Value,
};
use crate::introspection::Locatable;
use crate::introspection::{Locatable, Location};
use crate::syntax::Span;
/// Provides access to the location of content.
/// Determines the location of an element in the document.
///
/// This is useful in combination with [queries]($query), [counters]($counter),
/// [state]($state), and [links]($link). See their documentation for more
/// details.
/// Takes a selector that must match exactly one element and returns that
/// element's [`location`]($location). This location can, in particular, be used
/// to retrieve the physical [`page`]($location.page) number and
/// [`position`]($location.position) (page, x, y) for that element.
///
/// # Examples
/// Locating a specific element:
/// ```example
/// #locate(loc => [
/// My location: \
/// #loc.position()!
/// ])
/// #context [
/// Introduction is at: \
/// #locate(<intro>).position()
/// ]
///
/// = Introduction <intro>
/// ```
#[func]
///
/// # Compatibility
/// In Typst 0.10 and lower, the `locate` function took a closure that made the
/// current location in the document available (like [`here`]($here) does now).
/// Compatibility with the old way will remain for a while to give package
/// authors time to upgrade. To that effect, `locate` detects whether it
/// received a selector or a user-defined function and adjusts its semantics
/// accordingly. This behaviour will be removed in the future.
#[func(contextual)]
pub fn locate(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The span of the `locate` call.
span: Span,
/// A function that receives a [`location`]($location). Its return value is
/// displayed in the document.
/// A selector that should match exactly one element. This element will be
/// located.
///
/// This function is called once for each time the content returned by
/// `locate` appears in the document. That makes it possible to generate
/// content that depends on its own location in the document.
func: Func,
) -> Content {
LocateElem::new(func).pack().spanned(span)
/// Especially useful in combination with
/// - [`here`]($here) to locate the current context,
/// - a [`location`]($location) retrieved from some queried element via the
/// [`location()`]($content.location) method on content.
selector: LocateInput,
) -> HintedStrResult<LocateOutput> {
Ok(match selector {
LocateInput::Selector(selector) => {
LocateOutput::Location(selector.resolve_unique(engine.introspector, context)?)
}
LocateInput::Func(func) => {
LocateOutput::Content(LocateElem::new(func).pack().spanned(span))
}
})
}
/// Compatible input type.
pub enum LocateInput {
Selector(LocatableSelector),
Func(Func),
}
cast! {
LocateInput,
v: Func => {
if v.element().is_some() {
Self::Selector(Value::Func(v).cast()?)
} else {
Self::Func(v)
}
},
v: LocatableSelector => Self::Selector(v),
}
/// Compatible output type.
pub enum LocateOutput {
Location(Location),
Content(Content),
}
cast! {
LocateOutput,
self => match self {
Self::Location(v) => v.into_value(),
Self::Content(v) => v.into_value(),
},
v: Location => Self::Location(v),
v: Content => Self::Content(v),
}
/// Executes a `locate` call.
@ -43,8 +103,9 @@ struct LocateElem {
impl Show for Packed<LocateElem> {
#[typst_macros::time(name = "locate", span = self.span())]
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let location = self.location().unwrap();
Ok(self.func().call(engine, [location])?.display())
let context = Context::new(Some(location), Some(styles));
Ok(self.func().call(engine, &context, [location])?.display())
}
}

View File

@ -3,16 +3,25 @@ use std::num::NonZeroUsize;
use ecow::EcoString;
use crate::engine::Engine;
use crate::foundations::{func, scope, ty, Dict, Repr};
use crate::foundations::{func, scope, ty, Repr};
use crate::layout::Position;
use crate::model::Numbering;
/// Identifies an element in the document.
///
/// A location uniquely identifies an element in the document and lets you
/// access its absolute position on the pages. You can retrieve the current
/// location with the [`locate`]($locate) function and the location of a queried
/// location with the [`here`]($here) function and the location of a queried
/// or shown element with the [`location()`]($content.location) method on
/// content.
///
/// # Locatable elements { #locatable }
/// Currently, only a subset of element functions is locatable. Aside from
/// headings and figures, this includes equations, references and all
/// elements with an explicit label. As a result, you _can_ query for e.g.
/// [`strong`]($strong) elements, but you will find only those that have an
/// explicit label attached to them. This limitation will be resolved in the
/// future.
#[ty(scope)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Location {
@ -37,27 +46,36 @@ impl Location {
#[scope]
impl Location {
/// Return the page number for this location.
/// Returns the page number for this location.
///
/// Note that this does not return the value of the [page counter]($counter)
/// at this location, but the true page number (starting from one).
///
/// If you want to know the value of the page counter, use
/// `{counter(page).at(loc)}` instead.
///
/// Can be used with [`here`]($here) to retrieve the physical page position
/// of the current context:
/// ```example
/// #context [
/// I am located on
/// page #here().page()
/// ]
/// ```
#[func]
pub fn page(self, engine: &mut Engine) -> NonZeroUsize {
engine.introspector.page(self)
}
/// Return a dictionary with the page number and the x, y position for this
/// Returns a dictionary with the page number and the x, y position for this
/// location. The page number starts at one and the coordinates are measured
/// from the top-left of the page.
///
/// If you only need the page number, use `page()` instead as it allows
/// Typst to skip unnecessary work.
#[func]
pub fn position(self, engine: &mut Engine) -> Dict {
engine.introspector.position(self).into()
pub fn position(self, engine: &mut Engine) -> Position {
engine.introspector.position(self)
}
/// Returns the page numbering pattern of the page at this location. This

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
use comemo::{Tracked, TrackedMut};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use crate::diag::SourceResult;
use crate::diag::{bail, At, SourceResult};
use crate::engine::{Engine, Route};
use crate::eval::Tracer;
use crate::foundations::{
cast, elem, func, scope, select_where, ty, Content, Func, NativeElement, Packed,
Repr, Selector, Show, Str, StyleChain, Value,
cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func,
LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain,
Value,
};
use crate::introspection::{Introspector, Locatable, Location, Locator};
use crate::syntax::Span;
@ -22,6 +23,7 @@ use crate::World;
/// outside the function are read-only and cannot be modified._
///
/// ```typ
/// // This doesn't work!
/// #let x = 0
/// #let compute(expr) = {
/// x = eval(
@ -70,17 +72,18 @@ use crate::World;
/// # Managing state in Typst { #state-in-typst }
/// So what do we do instead? We use Typst's state management system. Calling
/// the `state` function with an identifying string key and an optional initial
/// value gives you a state value which exposes a few methods. The two most
/// important ones are `display` and `update`:
/// value gives you a state value which exposes a few function. The two most
/// important ones are `get` and `update`:
///
/// - The `display` method shows the current value of the state. You can
/// optionally give it a function that receives the value and formats it in
/// some way.
/// - The [`get`]($state.get) function retrieves the current value of the state.
/// Because the value can vary over the course of the document, it is a
/// _contextual_ function that can only be used when [context]($context) is
/// available.
///
/// - The `update` method modifies the state. You can give it any value. If
/// given a non-function value, it sets the state to that value. If given a
/// function, that function receives the previous state and has to return the
/// new state.
/// - The [`update`]($state.update) function modifies the state. You can give it
/// any value. If given a non-function value, it sets the state to that value.
/// If given a function, that function receives the previous state and has to
/// return the new state.
///
/// Our initial example would now look like this:
///
@ -90,7 +93,7 @@ use crate::World;
/// #s.update(x =>
/// eval(expr.replace("x", str(x)))
/// )
/// New value is #s.display().
/// New value is #context s.get().
/// ]
///
/// #compute("10") \
@ -103,8 +106,8 @@ use crate::World;
/// order. The `update` method returns content and its effect occurs at the
/// position where the returned content is inserted into the document.
///
/// As a result, we can now also store some of the computations in
/// variables, but they still show the correct results:
/// As a result, we can now also store some of the computations in variables,
/// but they still show the correct results:
///
/// ```example
/// >>> #let s = state("x", 0)
@ -112,7 +115,7 @@ use crate::World;
/// >>> #s.update(x =>
/// >>> eval(expr.replace("x", str(x)))
/// >>> )
/// >>> New value is #s.display().
/// >>> New value is #context s.get().
/// >>> ]
/// <<< ...
///
@ -132,10 +135,9 @@ use crate::World;
///
/// # Time Travel
/// By using Typst's state management system you also get time travel
/// capabilities! By combining the state system with [`locate`]($locate) and
/// [`query`]($query), we can find out what the value of the state will be at
/// any position in the document from anywhere else. In particular, the `at`
/// method gives us the value of the state at any location and the `final`
/// capabilities! We can find out what the value of the state will be at any
/// position in the document from anywhere else. In particular, the `at` method
/// gives us the value of the state at any particular location and the `final`
/// methods gives us the value of the state at the end of the document.
///
/// ```example
@ -144,16 +146,12 @@ use crate::World;
/// >>> #s.update(x => {
/// >>> eval(expr.replace("x", str(x)))
/// >>> })
/// >>> New value is #s.display().
/// >>> New value is #context s.get().
/// >>> ]
/// <<< ...
///
/// Value at `<here>` is
/// #locate(loc => s.at(
/// query(<here>, loc)
/// .first()
/// .location()
/// ))
/// #context s.at(<here>)
///
/// #compute("10") \
/// #compute("x + 3") \
@ -171,21 +169,21 @@ use crate::World;
/// a state, the results might never converge. The example below illustrates
/// this. We initialize our state with `1` and then update it to its own final
/// value plus 1. So it should be `2`, but then its final value is `2`, so it
/// should be `3`, and so on. This example displays a finite value because
/// Typst simply gives up after a few attempts.
/// should be `3`, and so on. This example displays a finite value because Typst
/// simply gives up after a few attempts.
///
/// ```example
/// // This is bad!
/// #let s = state("x", 1)
/// #locate(loc => {
/// s.update(s.final(loc) + 1)
/// })
/// #s.display()
/// #context s.update(s.final() + 1)
/// #context s.get()
/// ```
///
/// In general, you should _typically_ not generate state updates from within
/// `locate` calls or `display` calls of state or counters. Instead, pass a
/// function to `update` that determines the value of the state based on its
/// previous value.
/// In general, you should try not to generate state updates from within context
/// expressions. If possible, try to express your updates as non-contextual
/// values or functions that compute the new value from the previous value.
/// Sometimes, it cannot be helped, but in those cases it is up to you to ensure
/// that the result converges.
#[ty(scope)]
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct State {
@ -201,6 +199,16 @@ impl State {
Self { key, init }
}
/// Get the value of the state at the given location.
pub fn at_loc(&self, engine: &mut Engine, loc: Location) -> SourceResult<Value> {
let sequence = self.sequence(engine)?;
let offset = engine
.introspector
.query(&self.selector().before(loc.into(), true))
.len();
Ok(sequence[offset].clone())
}
/// Produce the whole sequence of states.
///
/// This has to happen just once for all states, cutting down the number
@ -237,10 +245,12 @@ impl State {
let mut stops = eco_vec![state.clone()];
for elem in introspector.query(&self.selector()) {
let elem = elem.to_packed::<UpdateElem>().unwrap();
let elem = elem.to_packed::<StateUpdateElem>().unwrap();
match elem.update() {
StateUpdate::Set(value) => state = value.clone(),
StateUpdate::Func(func) => state = func.call(&mut engine, [state])?,
StateUpdate::Func(func) => {
state = func.call(&mut engine, &Context::none(), [state])?
}
}
stops.push(state.clone());
}
@ -250,7 +260,7 @@ impl State {
/// The selector for this state's updates.
fn selector(&self) -> Selector {
select_where!(UpdateElem, Key => self.key.clone())
select_where!(StateUpdateElem, Key => self.key.clone())
}
}
@ -268,19 +278,69 @@ impl State {
Self::new(key, init)
}
/// Displays the current value of the state.
#[func]
pub fn display(
self,
/// The span of the `display` call.
/// Retrieves the value of the state at the current location.
///
/// This is equivalent to `{state.at(here())}`.
#[func(contextual)]
pub fn get(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span,
/// A function which receives the value of the state and can return
/// arbitrary content which is then displayed. If this is omitted, the
/// value is directly displayed.
) -> SourceResult<Value> {
let loc = context.location().at(span)?;
self.at_loc(engine, loc)
}
/// Retrieves the value of the state at the given selector's unique match.
///
/// The `selector` must match exactly one element in the document. The most
/// useful kinds of selectors for this are [labels]($label) and
/// [locations]($location).
///
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
/// function also works without a known context if the `selector` is a
/// location. This behaviour will be removed in a future release.
#[func(contextual)]
pub fn at(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span,
/// The place at which the state's value should be retrieved.
selector: LocatableSelector,
) -> SourceResult<Value> {
let loc = selector.resolve_unique(engine.introspector, context).at(span)?;
self.at_loc(engine, loc)
}
/// Retrieves the value of the state at the end of the document.
#[func(contextual)]
pub fn final_(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: &Context,
/// The callsite span.
span: Span,
/// _Compatibility:_ This argument only exists for compatibility with
/// Typst 0.10 and lower and shouldn't be used anymore.
#[default]
func: Option<Func>,
) -> Content {
DisplayElem::new(self, func).pack().spanned(span)
location: Option<Location>,
) -> SourceResult<Value> {
if location.is_none() {
context.location().at(span)?;
}
let sequence = self.sequence(engine)?;
Ok(sequence.last().unwrap().clone())
}
/// Update the value of the state.
@ -301,47 +361,24 @@ impl State {
/// to return the new state.
update: StateUpdate,
) -> Content {
UpdateElem::new(self.key, update).pack().spanned(span)
StateUpdateElem::new(self.key, update).pack().spanned(span)
}
/// Get the value of the state at the given location.
/// **Deprection planned:** Use [`get`]($state.get) instead.
///
/// Displays the current value of the state.
#[func]
pub fn at(
&self,
/// The engine.
engine: &mut Engine,
/// The location at which the state's value should be retrieved. A
/// suitable location can be retrieved from [`locate`]($locate) or
/// [`query`]($query).
location: Location,
) -> SourceResult<Value> {
let sequence = self.sequence(engine)?;
let offset = engine
.introspector
.query(&self.selector().before(location.into(), true))
.len();
Ok(sequence[offset].clone())
}
/// Get the value of the state at the end of the document.
#[func]
pub fn final_(
&self,
/// The engine.
engine: &mut Engine,
/// Can be an arbitrary location, as its value is irrelevant for the
/// method's return value. Why is it required then? As noted before,
/// Typst has to evaluate parts of your code multiple times to determine
/// the values of all state. By only allowing this method within
/// [`locate`]($locate) calls, the amount of code that can depend on the
/// method's result is reduced. If you could call `final` directly at
/// the top level of a module, the evaluation of the whole module and
/// its exports could depend on the state's value.
location: Location,
) -> SourceResult<Value> {
let _ = location;
let sequence = self.sequence(engine)?;
Ok(sequence.last().unwrap().clone())
pub fn display(
self,
/// The span of the `display` call.
span: Span,
/// A function which receives the value of the state and can return
/// arbitrary content which is then displayed. If this is omitted, the
/// value is directly displayed.
#[default]
func: Option<Func>,
) -> Content {
StateDisplayElem::new(self, func).pack().spanned(span)
}
}
@ -352,7 +389,6 @@ impl Repr for State {
}
/// An update to perform on a state.
#[ty(cast)]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum StateUpdate {
/// Set the state to the specified value.
@ -361,56 +397,68 @@ pub enum StateUpdate {
Func(Func),
}
impl Repr for StateUpdate {
fn repr(&self) -> EcoString {
"..".into()
}
}
cast! {
type StateUpdate,
StateUpdate,
v: Func => Self::Func(v),
v: Value => Self::Set(v),
}
/// Executes a display of a state.
#[elem(Locatable, Show)]
struct DisplayElem {
/// 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 {
#[elem(Construct, Locatable, Show)]
struct StateUpdateElem {
/// The key that identifies the state.
#[required]
key: Str,
/// The update to perform on the state.
#[required]
#[internal]
update: StateUpdate,
}
impl 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> {
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::foundations::{
Array, CastInfo, Content, Fold, FromValue, Func, IntoValue, Reflect, Resolve, Smart,
StyleChain, Value,
Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
Resolve, Smart, StyleChain, Value,
};
use crate::layout::{
Abs, Alignment, Axes, Dir, Fr, Fragment, Frame, FrameItem, LayoutMultiple, Length,
@ -39,10 +39,19 @@ pub enum Celled<T> {
impl<T: Default + Clone + FromValue> Celled<T> {
/// Resolve the value based on the cell position.
pub fn resolve(&self, engine: &mut Engine, x: usize, y: usize) -> SourceResult<T> {
pub fn resolve(
&self,
engine: &mut Engine,
styles: StyleChain,
x: usize,
y: usize,
) -> SourceResult<T> {
Ok(match self {
Self::Value(value) => value.clone(),
Self::Func(func) => func.call(engine, [x, y])?.cast().at(func.span())?,
Self::Func(func) => func
.call(engine, &Context::new(None, Some(styles)), [x, y])?
.cast()
.at(func.span())?,
Self::Array(array) => x
.checked_rem(array.len())
.and_then(|i| array.get(i))
@ -141,7 +150,7 @@ where
Ok(match &self.0 {
Celled::Value(value) => value.clone(),
Celled::Func(func) => func
.call(engine, [x, y])?
.call(engine, &Context::new(None, Some(styles)), [x, y])?
.cast::<T>()
.at(func.span())?
.resolve(styles),
@ -484,9 +493,9 @@ impl CellGrid {
let cell = cell.resolve_cell(
x,
y,
&fill.resolve(engine, x, y)?,
align.resolve(engine, x, y)?,
inset.resolve(engine, x, y)?,
&fill.resolve(engine, styles, x, y)?,
align.resolve(engine, styles, x, y)?,
inset.resolve(engine, styles, x, y)?,
stroke.resolve(engine, styles, x, y)?,
styles,
);
@ -570,9 +579,9 @@ impl CellGrid {
let new_cell = T::default().resolve_cell(
x,
y,
&fill.resolve(engine, x, y)?,
align.resolve(engine, x, y)?,
inset.resolve(engine, x, y)?,
&fill.resolve(engine, styles, x, y)?,
align.resolve(engine, styles, x, y)?,
inset.resolve(engine, styles, x, y)?,
stroke.resolve(engine, styles, x, y)?,
styles,
);

View File

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

View File

@ -4,8 +4,8 @@ use std::ops::{Add, Div, Mul, Neg};
use ecow::{eco_format, EcoString};
use crate::diag::{At, Hint, SourceResult};
use crate::foundations::{func, scope, ty, Fold, Repr, Resolve, StyleChain, Styles};
use crate::diag::{At, Hint, HintedStrResult, SourceResult};
use crate::foundations::{func, scope, ty, Context, Fold, Repr, Resolve, StyleChain};
use crate::layout::{Abs, Em};
use crate::syntax::Span;
use crate::util::Numeric;
@ -137,32 +137,22 @@ impl Length {
///
/// ```example
/// #set text(size: 12pt)
/// #style(styles => [
/// #(6pt).to-absolute(styles) \
/// #(6pt + 10em).to-absolute(styles) \
/// #(10em).to-absolute(styles)
/// ])
/// #context [
/// #(6pt).to-absolute() \
/// #(6pt + 10em).to-absolute() \
/// #(10em).to-absolute()
/// ]
///
/// #set text(size: 6pt)
/// #style(styles => [
/// #(6pt).to-absolute(styles) \
/// #(6pt + 10em).to-absolute(styles) \
/// #(10em).to-absolute(styles)
/// ])
/// #context [
/// #(6pt).to-absolute() \
/// #(6pt + 10em).to-absolute() \
/// #(10em).to-absolute()
/// ]
/// ```
#[func]
pub fn to_absolute(
&self,
/// The styles to resolve the length with.
///
/// Since a length can use font-relative em units, resolving it to an
/// absolute length requires knowledge of the font size. This size is
/// provided through these styles. You can obtain the styles using
/// the [`style`]($style) function.
styles: Styles,
) -> Length {
let styles = StyleChain::new(&styles);
self.resolve(styles).into()
pub fn to_absolute(&self, context: &Context) -> HintedStrResult<Length> {
Ok(self.resolve(context.styles()?).into())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, Depth, Func, Packed, Smart, StyleChain, Value,
cast, elem, scope, Array, Content, Context, Depth, Func, Packed, Smart, StyleChain,
Value,
};
use crate::layout::{
Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment,
@ -154,7 +155,7 @@ impl LayoutMultiple for Packed<ListElem> {
let Depth(depth) = ListElem::depth_in(styles);
let marker = self
.marker(styles)
.resolve(engine, depth)?
.resolve(engine, styles, depth)?
// avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top);
@ -206,12 +207,19 @@ pub enum ListMarker {
impl ListMarker {
/// Resolve the marker for the given depth.
fn resolve(&self, engine: &mut Engine, depth: usize) -> SourceResult<Content> {
fn resolve(
&self,
engine: &mut Engine,
styles: StyleChain,
depth: usize,
) -> SourceResult<Content> {
Ok(match self {
Self::Content(list) => {
list.get(depth % list.len()).cloned().unwrap_or_default()
}
Self::Func(func) => func.call(engine, [depth])?.display(),
Self::Func(func) => func
.call(engine, &Context::new(None, Some(styles)), [depth])?
.display(),
})
}
}

View File

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

View File

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

View File

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

View File

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

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
a reusable template?
## Bibliographies { #bibliographies }
## Bibliographies
Typst includes a fully-featured bibliography system that is compatible with
BibTeX files. You can continue to use your `.bib` literature libraries by
loading them with the [`bibliography`]($bibliography) function. Another
@ -627,7 +627,7 @@ use in prose (cf. `\citet` and `\textcite`) are available with
You can find more information on the documentation page of the [`bibliography`]($bibliography) function.
## Installation { #installation }
## Installation
You have two ways to use Typst: In [our web app](https://typst.app/signup/) or
by [installing the compiler](https://github.com/typst/typst/releases) on your
computer. When you use the web app, we provide a batteries-included

View File

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

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

View File

@ -12,7 +12,7 @@ All this is backed by a tightly integrated scripting language with built-in and
user-defined functions.
## Modes
Typst has three syntactical modes: Markup, math, and code. Markup mode is the
Typst has three syntactical modes: Markup, math, and code. Markup mode is the
default in a Typst document, math mode lets you write mathematical formulas, and
code mode lets you use Typst's scripting features.
@ -111,6 +111,7 @@ a table listing all syntax that is available in code mode:
| Show-set rule | `{show par: set block(..)}` | [Styling]($styling/#show-rules) |
| Show rule with function | `{show raw: it => {..}}` | [Styling]($styling/#show-rules) |
| Show-everything rule | `{show: columns.with(2)}` | [Styling]($styling/#show-rules) |
| Context expression | `{context text.lang}` | [Context]($context) |
| Conditional | `{if x == 1 {..} else {..}}` | [Scripting]($scripting/#conditionals) |
| For loop | `{for x in (1, 2, 3) {..}}` | [Scripting]($scripting/#loops) |
| While loop | `{while x < 10 {..}}` | [Scripting]($scripting/#loops) |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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