Skip to main content

typst_library/math/
lr.rs

1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4use bumpalo::Bump;
5use comemo::Tracked;
6
7use crate::engine::Engine;
8use crate::foundations::{
9    Args, CastInfo, Content, Context, Func, IntoValue, NativeElement, NativeFunc,
10    NativeFuncData, NativeFuncPtr, NativeParamInfo, Reflect, Scope, SymbolElem, Type,
11    elem, func,
12};
13use crate::layout::{Em, Length, Rel};
14use crate::math::Mathy;
15
16/// How much less high scaled delimiters can be than what they wrap.
17pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
18
19/// Scales delimiters.
20///
21/// While matched delimiters scale by default, this can be used to scale
22/// unmatched delimiters and to control the delimiter scaling more precisely.
23#[elem(title = "Left/Right", Mathy)]
24pub struct LrElem {
25    /// The size of the brackets, relative to the height of the wrapped content.
26    #[default(Rel::one())]
27    pub size: Rel<Length>,
28
29    /// The delimited content, including the delimiters.
30    #[required]
31    #[parse(
32        let mut arguments = args.all::<Content>()?.into_iter();
33        let mut body = arguments.next().unwrap_or_default();
34        arguments.for_each(|arg| body += SymbolElem::packed(',') + arg);
35        body
36    )]
37    pub body: Content,
38}
39
40/// Scales delimiters vertically to the nearest surrounding `{lr()}` group.
41///
42/// ```example
43/// $ { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } $
44/// ```
45#[elem(Mathy)]
46pub struct MidElem {
47    /// The content to be scaled.
48    #[required]
49    pub body: Content,
50}
51
52/// Floors an expression.
53///
54/// ```example
55/// $ floor(x/2) $
56/// ```
57#[func]
58pub fn floor(
59    /// The size of the brackets, relative to the height of the wrapped content.
60    ///
61    /// Default: The current value of @math.lr.size[`lr.size`].
62    #[named]
63    size: Option<Rel<Length>>,
64    /// The expression to floor.
65    body: Content,
66) -> Content {
67    delimited(body, '⌊', '⌋', size)
68}
69
70/// Ceils an expression.
71///
72/// ```example
73/// $ ceil(x/2) $
74/// ```
75#[func]
76pub fn ceil(
77    /// The size of the brackets, relative to the height of the wrapped content.
78    ///
79    /// Default: The current value of @math.lr.size[`lr.size`].
80    #[named]
81    size: Option<Rel<Length>>,
82    /// The expression to ceil.
83    body: Content,
84) -> Content {
85    delimited(body, '⌈', '⌉', size)
86}
87
88/// Rounds an expression.
89///
90/// ```example
91/// $ round(x/2) $
92/// ```
93#[func]
94pub fn round(
95    /// The size of the brackets, relative to the height of the wrapped content.
96    ///
97    /// Default: The current value of @math.lr.size[`lr.size`].
98    #[named]
99    size: Option<Rel<Length>>,
100    /// The expression to round.
101    body: Content,
102) -> Content {
103    delimited(body, '⌊', '⌉', size)
104}
105
106/// Takes the absolute value of an expression.
107///
108/// ```example
109/// $ abs(x/2) $
110/// ```
111#[func]
112pub fn abs(
113    /// The size of the brackets, relative to the height of the wrapped content.
114    ///
115    /// Default: The current value of @math.lr.size[`lr.size`].
116    #[named]
117    size: Option<Rel<Length>>,
118    /// The expression to take the absolute value of.
119    body: Content,
120) -> Content {
121    delimited(body, '|', '|', size)
122}
123
124/// Takes the norm of an expression.
125///
126/// ```example
127/// $ norm(x/2) $
128/// ```
129#[func]
130pub fn norm(
131    /// The size of the brackets, relative to the height of the wrapped content.
132    ///
133    /// Default: The current value of @math.lr.size[`lr.size`].
134    #[named]
135    size: Option<Rel<Length>>,
136    /// The expression to take the norm of.
137    body: Content,
138) -> Content {
139    delimited(body, '‖', '‖', size)
140}
141
142/// Gets the Left/Right wrapper function corresponding to a symbol value, if
143/// any.
144pub fn get_lr_wrapper_func(value: &str) -> Option<Func> {
145    let left = value.parse::<char>().ok()?;
146    match left {
147        // Unlike `round`, `abs`, and `norm`, `floor` and `ceil` are of type
148        // `symbol` and cast to a function like other L/R symbols. We could thus
149        // rely on autogeneration for these as well, but since they are
150        // specifically called out in the documentation on the L/R page (via the
151        // group mechanism), it's nice for them to have a bit of extra
152        // documentation.
153        '⌈' => Some(ceil::func()),
154        '⌊' => Some(floor::func()),
155        l => FUNCS.get(&l).map(Func::from),
156    }
157}
158
159/// The delimiter pairings supported for use as callable symbols.
160const DELIMS: &[(char, char)] = &[
161    // The `ceil` and `floor` pairs are omitted here because they are handled
162    // manually.
163    ('(', ')'),
164    ('⟮', '⟯'),
165    ('⦇', '⦈'),
166    ('⦅', '⦆'),
167    ('⦓', '⦔'),
168    ('⦕', '⦖'),
169    ('{', '}'),
170    ('⦃', '⦄'),
171    ('[', ']'),
172    ('⦍', '⦐'),
173    ('⦏', '⦎'),
174    ('⟦', '⟧'),
175    ('⦋', '⦌'),
176    ('❲', '❳'),
177    ('⟬', '⟭'),
178    ('⦗', '⦘'),
179    ('⟅', '⟆'),
180    ('⎰', '⎱'),
181    ('⎱', '⎰'),
182    ('⧘', '⧙'),
183    ('⧚', '⧛'),
184    ('⟨', '⟩'),
185    ('⧼', '⧽'),
186    ('⦑', '⦒'),
187    ('⦉', '⦊'),
188    ('⟪', '⟫'),
189    ('⌜', '⌝'),
190    ('⌞', '⌟'),
191    // Fences.
192    ('|', '|'),
193    ('‖', '‖'),
194    ('⦀', '⦀'),
195    ('⦙', '⦙'),
196    ('⦚', '⦚'),
197];
198
199/// Lazily created left/right wrapper functions.
200static FUNCS: LazyLock<HashMap<char, NativeFuncData>> = LazyLock::new(|| {
201    let bump = Box::leak(Box::new(Bump::new()));
202    DELIMS
203        .iter()
204        .copied()
205        .map(|(l, r)| (l, create_lr_func_data(l, r, bump)))
206        .collect()
207});
208
209/// Creates metadata for an L/R wrapper function.
210fn create_lr_func_data(left: char, right: char, bump: &'static Bump) -> NativeFuncData {
211    let title = bumpalo::format!(in bump, "{}{} Left/Right", left, right).into_bump_str();
212    let docs = bumpalo::format!(in bump, "Wraps an expression in {}{}.", left, right)
213        .into_bump_str();
214    NativeFuncData {
215        function: NativeFuncPtr(bump.alloc(
216            move |_: &mut Engine, _: Tracked<Context>, args: &mut Args| {
217                let size = args.named("size")?;
218                let body = args.expect("body")?;
219                Ok(delimited(body, left, right, size).into_value())
220            },
221        )),
222        name: "(..) => ..",
223        title,
224        docs,
225        def_site: None,
226        keywords: &[],
227        contextual: false,
228        scope: LazyLock::new(&|| Scope::new()),
229        params: LazyLock::new(&|| create_lr_param_info()),
230        returns: LazyLock::new(&|| CastInfo::Type(Type::of::<Content>())),
231    }
232}
233
234/// Creates parameter signature metadata for an L/R function.
235fn create_lr_param_info() -> Vec<NativeParamInfo> {
236    vec![
237        NativeParamInfo {
238            name: "size",
239            docs: "\
240            The size of the brackets, relative to the height of the wrapped content.\n\
241            \n\
242            Default: The current value of [`lr.size`]($math.lr.size).",
243            def_site: None,
244            input: Rel::<Length>::input(),
245            default: None,
246            positional: false,
247            named: true,
248            variadic: false,
249            required: false,
250            settable: false,
251        },
252        NativeParamInfo {
253            name: "body",
254            docs: "The expression to wrap.",
255            def_site: None,
256            input: Content::input(),
257            default: None,
258            positional: true,
259            named: false,
260            variadic: false,
261            required: true,
262            settable: false,
263        },
264    ]
265}
266
267/// Creates an L/R element with the given delimiters.
268fn delimited(
269    body: Content,
270    left: char,
271    right: char,
272    size: Option<Rel<Length>>,
273) -> Content {
274    let span = body.span();
275    let mut elem = LrElem::new(Content::sequence([
276        SymbolElem::packed(left),
277        body,
278        SymbolElem::packed(right),
279    ]));
280    // Push size only if size is provided
281    if let Some(size) = size {
282        elem.size.set(size);
283    }
284    elem.pack().spanned(span)
285}