New context system (#3497)
This commit is contained in:
parent
e9ee00a7c0
commit
145723b1ef
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2575,6 +2575,7 @@ dependencies = [
|
||||
"icu_provider_adapters",
|
||||
"icu_provider_blob",
|
||||
"icu_segmenter",
|
||||
"if_chain",
|
||||
"image",
|
||||
"indexmap 2.1.0",
|
||||
"kamadak-exif",
|
||||
|
@ -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_,
|
||||
|
@ -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"),
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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}",
|
||||
|
@ -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();
|
||||
|
@ -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)*
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
|
@ -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![] }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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.
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
}
|
||||
|
80
crates/typst/src/foundations/context.rs
Normal file
80
crates/typst/src/foundations/context.rs
Normal 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())
|
||||
}
|
||||
}
|
@ -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>>,
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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::*;
|
||||
|
@ -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",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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())?;
|
||||
|
51
crates/typst/src/introspection/here.rs
Normal file
51
crates/typst/src/introspection/here.rs
Normal 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()
|
||||
}
|
@ -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(|| {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 })
|
||||
|
@ -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(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)?,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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}');
|
||||
}
|
||||
|
@ -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([
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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())?,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)))
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
235
docs/reference/context.md
Normal 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.
|
@ -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
|
||||
|
@ -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) |
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
// https://github.com/typst/typst/issues/2650
|
||||
#let with-locate(body) = locate(loc => body)
|
||||
|
||||
测a试
|
||||
|
||||
测#with-locate[a]试
|
||||
测#context [a]试
|
||||
|
@ -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")
|
||||
|
@ -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 $
|
||||
|
@ -10,4 +10,3 @@ This and next page should not be numbered
|
||||
#counter(page).update(1)
|
||||
|
||||
This page should
|
||||
|
||||
|
@ -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>
|
||||
|
||||
---
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
---
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
29
tests/typ/meta/context-compatibility.typ
Normal file
29
tests/typ/meta/context-compatibility.typ
Normal 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
181
tests/typ/meta/context.typ
Normal 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)
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user