Skip to main content

retex_lua/
nodelib.rs

1//! The `node` library (LuaTeX manual §10.5) and `node.direct`.
2//!
3//! Node userdata ([`LNode`]) and direct integers both refer to nodes in the
4//! engine's arena. Type ids, subtypes and field names come from the data
5//! recorded from LuaTeX's own `node` library
6//! ([`retex_engine::nodes::LUATEX_NODE_TYPES`]).
7
8use std::collections::HashMap;
9use std::sync::OnceLock;
10
11use mlua::{Lua, MetaMethod, MultiValue, Table, UserData, UserDataMethods, Value};
12use retex_engine::Engine;
13use retex_engine::nodes::{NodeData, NodeRef, id, noad_opt};
14
15use crate::with_engine;
16
17/// Node type information parsed from LuaTeX's API dump.
18struct TypeInfo {
19    names: HashMap<u8, String>,
20    ids: HashMap<String, u8>,
21    fields: HashMap<u8, Vec<(i32, String)>>,
22    subtypes: HashMap<u8, Vec<(i32, String)>>,
23    whatsits: Vec<(u16, String)>,
24    whatsit_fields: HashMap<u16, Vec<(i32, String)>>,
25}
26
27fn info() -> &'static TypeInfo {
28    static T: OnceLock<TypeInfo> = OnceLock::new();
29    T.get_or_init(|| {
30        let data = retex_engine::nodes::LUATEX_NODE_TYPES;
31        let mut ti = TypeInfo {
32            names: HashMap::new(),
33            ids: HashMap::new(),
34            fields: HashMap::new(),
35            subtypes: HashMap::new(),
36            whatsits: Vec::new(),
37            whatsit_fields: HashMap::new(),
38        };
39        let pairs = |s: &str| -> Vec<(i32, String)> {
40            s.split_whitespace()
41                .filter_map(|kv| {
42                    let (k, v) = kv.split_once('=')?;
43                    Some((k.parse().ok()?, v.to_owned()))
44                })
45                .collect()
46        };
47        let mut cur: u8 = 0;
48        for line in data.lines() {
49            let l = line.trim_start();
50            if let Some(rest) = line.strip_prefix("type ") {
51                let mut it = rest.split_whitespace();
52                let i: u8 = it.next().and_then(|x| x.parse().ok()).unwrap_or(0);
53                let name = it.next().unwrap_or("").to_owned();
54                ti.names.insert(i, name.clone());
55                ti.ids.insert(name, i);
56                cur = i;
57            } else if let Some(rest) = l.strip_prefix("fields ") {
58                ti.fields.insert(cur, pairs(rest));
59            } else if let Some(rest) = l.strip_prefix("subtypes ") {
60                ti.subtypes.insert(cur, pairs(rest));
61            } else if let Some(rest) = line.strip_prefix("whatsits ") {
62                ti.whatsits = pairs(rest)
63                    .into_iter()
64                    .map(|(k, v)| (k as u16, v))
65                    .collect();
66            } else if let Some(rest) = l.strip_prefix("whatsit ") {
67                let mut it = rest.splitn(4, ' ');
68                let _name = it.next();
69                let n: u16 = it.next().and_then(|x| x.parse().ok()).unwrap_or(0);
70                let _ = it.next();
71                ti.whatsit_fields.insert(n, pairs(it.next().unwrap_or("")));
72            }
73        }
74        ti
75    })
76}
77
78/// A node userdata.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct LNode(pub u32);
81
82impl UserData for LNode {
83    fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
84        m.add_meta_method(MetaMethod::Index, |lua, this, key: String| {
85            get_field(lua, NodeRef(this.0), &key)
86        });
87        m.add_meta_method(
88            MetaMethod::NewIndex,
89            |lua, this, (key, v): (String, Value)| set_field(lua, NodeRef(this.0), &key, v),
90        );
91        m.add_meta_method(MetaMethod::Eq, |_, this, other: mlua::AnyUserData| {
92            Ok(other
93                .borrow::<LNode>()
94                .map(|o| o.0 == this.0)
95                .unwrap_or(false))
96        });
97        m.add_meta_method(MetaMethod::ToString, |lua, this, ()| {
98            let p = NodeRef(this.0);
99            let (t, sub, prev, next) = with_engine(lua, |e| {
100                (
101                    e.nodes.id(p),
102                    e.nodes.subtype(p),
103                    e.nodes.prev(p),
104                    e.nodes.next(p),
105                )
106            })?;
107            let name = info().names.get(&t).cloned().unwrap_or_default();
108            // As LuaTeX: `<node prev < this > next : type subtype>`.
109            let num = |q: NodeRef| {
110                if q.is_null() {
111                    "nil".to_owned()
112                } else {
113                    q.0.to_string()
114                }
115            };
116            Ok(format!(
117                "<node {:>6} < {:>6} > {:>6} : {name} {sub}>",
118                num(prev),
119                this.0,
120                num(next)
121            ))
122        });
123    }
124}
125
126/// The Lua name of node type `t` (`glyph`).
127pub(crate) fn type_name(t: u8) -> String {
128    info().names.get(&t).cloned().unwrap_or_default()
129}
130
131pub(crate) fn to_ref(v: &Value) -> Option<NodeRef> {
132    match v {
133        Value::UserData(u) => u.borrow::<LNode>().ok().map(|n| NodeRef(n.0)),
134        Value::Integer(n) if *n > 0 => Some(NodeRef(*n as u32)),
135        _ => None,
136    }
137}
138
139pub(crate) fn wrap(lua: &Lua, p: NodeRef, direct: bool) -> mlua::Result<Value> {
140    if p.is_null() {
141        return Ok(Value::Nil);
142    }
143    if direct {
144        Ok(Value::Integer(i64::from(p.0)))
145    } else {
146        Ok(Value::UserData(lua.create_userdata(LNode(p.0))?))
147    }
148}
149
150/// An attribute list as Lua sees it (`n.attr`, `node.current_attr`): a
151/// value that can be given to other nodes, compared, and traversed. LuaTeX
152/// returns an `attribute_list` node (id 40), followed by an `attribute`
153/// node (id 38) per attribute set; here only `id` and `subtype` are
154/// fields, and [`attr_entries`] is what `node.traverse` yields.
155#[derive(Clone, Debug)]
156pub struct LAttr(pub retex_engine::nodes::AttrList);
157
158impl UserData for LAttr {
159    fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
160        m.add_meta_method(MetaMethod::Eq, |_, this, other: mlua::AnyUserData| {
161            Ok(other
162                .borrow::<LAttr>()
163                .map(|o| o.0 == this.0)
164                .unwrap_or(false))
165        });
166        m.add_meta_method(MetaMethod::Index, |_, _, key: Value| {
167            Ok(match &key {
168                Value::String(k) if k.as_bytes().as_ref() == b"id" => Value::Integer(40),
169                Value::String(k) if k.as_bytes().as_ref() == b"subtype" => Value::Integer(0),
170                _ => Value::Nil,
171            })
172        });
173        m.add_meta_method(MetaMethod::ToString, |_, _, ()| {
174            Ok("<node attribute_list>".to_string())
175        });
176    }
177}
178
179/// One `attribute` node of a traversed attribute list: its number and
180/// value.
181#[derive(Clone, Copy, Debug)]
182struct LAttrEntry(u32, i32);
183
184impl UserData for LAttrEntry {
185    fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
186        m.add_meta_method(MetaMethod::Index, |_, this, key: Value| {
187            let Value::String(k) = &key else {
188                return Ok(Value::Nil);
189            };
190            Ok(match k.as_bytes().as_ref() {
191                b"id" => Value::Integer(38),
192                b"subtype" => Value::Integer(0),
193                b"number" => Value::Integer(i64::from(this.0)),
194                b"value" => Value::Integer(i64::from(this.1)),
195                _ => Value::Nil,
196            })
197        });
198        m.add_meta_method(MetaMethod::ToString, |_, _, ()| {
199            Ok("<node attribute>".to_string())
200        });
201    }
202}
203
204/// What `node.traverse` yields for an attribute list: the list itself,
205/// then each attribute (node, id and subtype).
206fn attr_entries(lua: &Lua, list: &mlua::AnyUserData) -> mlua::Result<Vec<[Value; 3]>> {
207    let a = list.borrow::<LAttr>()?.0.clone();
208    let mut out = vec![[
209        Value::UserData(list.clone()),
210        Value::Integer(40),
211        Value::Integer(0),
212    ]];
213    for &(n, v) in a.iter() {
214        out.push([
215            Value::UserData(lua.create_userdata(LAttrEntry(n, v))?),
216            Value::Integer(38),
217            Value::Integer(0),
218        ]);
219    }
220    Ok(out)
221}
222
223/// `n.attr`: nil for a node without attributes.
224fn attr_value(lua: &Lua, p: NodeRef) -> mlua::Result<Value> {
225    match with_engine(lua, |e| e.nodes.attr(p).cloned())? {
226        Some(a) => Ok(Value::UserData(lua.create_userdata(LAttr(a))?)),
227        None => Ok(Value::Nil),
228    }
229}
230
231/// `n.attr = v`: another node's list, or nil.
232fn set_attr_value(lua: &Lua, p: NodeRef, v: &Value) -> mlua::Result<()> {
233    let a = match v {
234        Value::UserData(u) => match u.borrow::<LAttr>() {
235            Ok(a) => Some(a.0.clone()),
236            Err(_) => return Ok(()),
237        },
238        Value::Nil => None,
239        _ => return Ok(()),
240    };
241    with_engine(lua, |e| e.nodes.set_attr(p, a))
242}
243
244/// Default data for a new node of type `t`.
245fn new_data(t: u8) -> NodeData {
246    use retex_engine::glue::Glue;
247    match t {
248        id::HLIST | id::VLIST => NodeData::Box(Box::default()),
249        id::RULE => NodeData::Rule {
250            width: retex_engine::nodes::RUNNING,
251            depth: retex_engine::nodes::RUNNING,
252            height: retex_engine::nodes::RUNNING,
253            dir: 0,
254            index: 0,
255            left: 0,
256            right: 0,
257        },
258        id::GLUE | id::GLUE_SPEC => NodeData::Glue {
259            spec: Glue::ZERO,
260            leader: NodeRef::NULL,
261        },
262        id::KERN => NodeData::Kern {
263            kern: 0,
264            expansion_factor: 0,
265        },
266        id::PENALTY => NodeData::Penalty { penalty: 0 },
267        id::MARGIN_KERN => NodeData::MarginKern {
268            width: 0,
269            glyph: NodeRef::NULL,
270        },
271        id::GLYPH => NodeData::Glyph(Default::default()),
272        id::DISC => NodeData::Disc {
273            pre: NodeRef::NULL,
274            post: NodeRef::NULL,
275            replace: NodeRef::NULL,
276            penalty: 0,
277        },
278        id::BOUNDARY => NodeData::Boundary { value: 0 },
279        id::MATH => NodeData::Math {
280            surround: 0,
281            glue: Glue::ZERO,
282        },
283        id::ADJUST => NodeData::Adjust {
284            head: NodeRef::NULL,
285        },
286        id::MARK => NodeData::Mark {
287            class: 0,
288            mark: retex_engine::token::empty_list(),
289        },
290        id::DIR => NodeData::Dir { dir: 0, level: 0 },
291        id::STYLE => NodeData::Style { style: 0 },
292        id::LOCAL_PAR => NodeData::LocalPar {
293            pen_inter: 0,
294            pen_broken: 0,
295            dir: 0,
296            box_left: NodeRef::NULL,
297            box_left_width: 0,
298            box_right: NodeRef::NULL,
299            box_right_width: 0,
300        },
301        id::WHATSIT => NodeData::Whatsit(Box::new(retex_engine::nodes::Whatsit::Other {
302            fields: Vec::new(),
303            data: Vec::new(),
304        })),
305        _ => NodeData::Temp,
306    }
307}
308
309fn int(v: &Value) -> i64 {
310    match v {
311        Value::Integer(n) => *n,
312        Value::Number(n) => n.round() as i64,
313        Value::Boolean(b) => i64::from(*b),
314        _ => 0,
315    }
316}
317
318/// Reads field `key` of node `p`.
319fn get_field(lua: &Lua, p: NodeRef, key: &str) -> mlua::Result<Value> {
320    // Observed: nodes of the types without subtypes read their subtype
321    // field as nil (getsubtype, getfield and traverse give 0).
322    if key == "subtype"
323        && matches!(with_engine(lua, |e| e.nodes.id(p))?, 9 | 30..=32 | 35..=37 | 39 | 41..=49)
324    {
325        return Ok(Value::Nil);
326    }
327    get_field_impl(lua, p, key, false)
328}
329
330/// A field value read under the engine borrow.
331enum R {
332    I(i64),
333    N(NodeRef),
334    F(f64),
335    S(Vec<u8>),
336    /// A Lua value kept by reference (see [`lua_ref`]).
337    L(i32),
338    Nil,
339}
340
341fn get_field_impl(lua: &Lua, p: NodeRef, key: &str, direct: bool) -> mlua::Result<Value> {
342    if key == "attr" {
343        return attr_value(lua, p);
344    }
345    let r = with_engine(lua, |e| -> R {
346        let n = e.nodes.get(p);
347        match key {
348            "id" => return R::I(i64::from(n.id)),
349            "subtype" => return R::I(i64::from(n.subtype)),
350            "next" => return R::N(n.next),
351            "prev" => return R::N(n.prev),
352            _ => {}
353        }
354        match &n.data {
355            NodeData::Box(b) | NodeData::Unset { b, .. } => match key {
356                "width" => R::I(b.width.into()),
357                "height" => R::I(b.height.into()),
358                "depth" => R::I(b.depth.into()),
359                "shift" => R::I(b.shift.into()),
360                "dir" => R::S(dir_name(b.dir).into()),
361                "direction" => R::I(b.dir.into()),
362                "glue_order" => R::I(b.glue_order.into()),
363                "glue_sign" => R::I(b.glue_sign.into()),
364                "glue_set" => R::F(b.glue_set),
365                "head" | "list" => R::N(b.head),
366                _ => R::Nil,
367            },
368            NodeData::Glyph(g) => match key {
369                "char" => R::I(g.chr.into()),
370                "font" => R::I(g.font.into()),
371                "lang" => R::I(g.lang.into()),
372                "left" => R::I(g.left.into()),
373                "right" => R::I(g.right.into()),
374                "uchyph" => R::I(i64::from(g.uchyph)),
375                "components" => R::N(g.components),
376                "xoffset" => R::I(g.xoffset.into()),
377                "yoffset" => R::I(g.yoffset.into()),
378                "expansion_factor" => R::I(g.expansion_factor.into()),
379                "data" => R::I(g.data.into()),
380                "width" => R::I(e.font_char_dimen(g.font, g.chr, 0).into()),
381                "height" => R::I(e.font_char_dimen(g.font, g.chr, 1).into()),
382                "depth" => R::I(e.font_char_dimen(g.font, g.chr, 2).into()),
383                _ => R::Nil,
384            },
385            NodeData::Glue { spec, leader } => match key {
386                "width" => R::I(spec.width.into()),
387                "stretch" => R::I(spec.stretch.into()),
388                "shrink" => R::I(spec.shrink.into()),
389                "stretch_order" => R::I(spec.stretch_order.into()),
390                "shrink_order" => R::I(spec.shrink_order.into()),
391                "leader" => R::N(*leader),
392                _ => R::Nil,
393            },
394            NodeData::Kern {
395                kern,
396                expansion_factor,
397            } => match key {
398                "kern" | "width" => R::I((*kern).into()),
399                "expansion_factor" => R::I((*expansion_factor).into()),
400                _ => R::Nil,
401            },
402            NodeData::Penalty { penalty } => match key {
403                "penalty" => R::I((*penalty).into()),
404                _ => R::Nil,
405            },
406            NodeData::MarginKern { width, glyph } => match key {
407                "width" => R::I((*width).into()),
408                "glyph" => R::N(*glyph),
409                _ => R::Nil,
410            },
411            NodeData::Rule {
412                width,
413                depth,
414                height,
415                dir,
416                index,
417                left,
418                right,
419            } => match key {
420                "width" => R::I((*width).into()),
421                "depth" => R::I((*depth).into()),
422                "height" => R::I((*height).into()),
423                "dir" => R::S(dir_name(*dir).into()),
424                "direction" => R::I((*dir).into()),
425                "index" => R::I((*index).into()),
426                "left" => R::I((*left).into()),
427                "right" => R::I((*right).into()),
428                _ => R::Nil,
429            },
430            NodeData::Disc {
431                pre,
432                post,
433                replace,
434                penalty,
435            } => match key {
436                "pre" => R::N(*pre),
437                "post" => R::N(*post),
438                "replace" => R::N(*replace),
439                "penalty" => R::I((*penalty).into()),
440                _ => R::Nil,
441            },
442            NodeData::Boundary { value } => match key {
443                "value" => R::I((*value).into()),
444                _ => R::Nil,
445            },
446            NodeData::Whatsit(w) => {
447                let w = (**w).clone();
448                whatsit_field(e, &w, key)
449            }
450            NodeData::Mark { class, mark } => match key {
451                "class" => R::I((*class).into()),
452                "mark" => {
453                    let mark = mark.clone();
454                    R::S(e.print_to_string(|e| e.show_token_list(&mark, 0, mark.len(), i64::MAX)))
455                }
456                _ => R::Nil,
457            },
458            NodeData::Ins {
459                cost,
460                depth,
461                height,
462                head,
463                ..
464            } => match key {
465                "cost" => R::I((*cost).into()),
466                "depth" => R::I((*depth).into()),
467                "height" => R::I((*height).into()),
468                "head" | "list" => R::N(*head),
469                _ => R::Nil,
470            },
471            NodeData::Adjust { head } => match key {
472                "head" | "list" => R::N(*head),
473                _ => R::Nil,
474            },
475            // Observed: "+TRT" opens, "-TRT" (subtype 1) closes.
476            NodeData::Dir { dir, level } => match key {
477                "dir" => {
478                    let sign = if n.subtype == 0 { "+" } else { "-" };
479                    R::S(format!("{sign}{}", dir_name(*dir)).into_bytes())
480                }
481                "direction" => R::I((*dir).into()),
482                "level" => R::I((*level).into()),
483                _ => R::Nil,
484            },
485            NodeData::LocalPar { dir, .. } => match key {
486                "dir" => R::S(dir_name(*dir).into()),
487                "direction" => R::I((*dir).into()),
488                _ => R::Nil,
489            },
490            NodeData::Math { surround, glue } => match key {
491                "surround" => R::I((*surround).into()),
492                "width" => R::I(glue.width.into()),
493                "stretch" => R::I(glue.stretch.into()),
494                "shrink" => R::I(glue.shrink.into()),
495                "stretch_order" => R::I(glue.stretch_order.into()),
496                "shrink_order" => R::I(glue.shrink_order.into()),
497                _ => R::Nil,
498            },
499            NodeData::Noad(d) => match key {
500                "nucleus" => R::N(d.nucleus),
501                "sub" => R::N(d.sub),
502                "sup" => R::N(d.sup),
503                "options" => R::I(d.options.into()),
504                "left" => R::N(d.left),
505                "degree" => R::N(d.degree),
506                "width" => R::I(d.width.into()),
507                // Observed: top_accent is accent; an overlay accent
508                // reads as overlay_accent only.
509                "accent" | "top_accent" if d.options & noad_opt::OVERLAY == 0 => R::N(d.accent),
510                "overlay_accent" if d.options & noad_opt::OVERLAY != 0 => R::N(d.accent),
511                "bot_accent" => R::N(d.bot_accent),
512                "fraction" => R::I(d.fraction.into()),
513                _ => R::Nil,
514            },
515            NodeData::Fraction(d) => match key {
516                "width" => R::I(d.thickness.into()),
517                "num" => R::N(d.num),
518                "denom" => R::N(d.denom),
519                "left" => R::N(d.left),
520                "right" => R::N(d.right),
521                "middle" => R::N(d.middle),
522                _ => R::Nil,
523            },
524            NodeData::Fence(d) => match key {
525                "delim" => R::N(d.delim),
526                "italic" => R::I(d.italic.into()),
527                "height" => R::I(d.height.into()),
528                "depth" => R::I(d.depth.into()),
529                "options" => R::I(d.options.into()),
530                "class" => R::I(d.class.into()),
531                _ => R::Nil,
532            },
533            // Observed: the style of a style node reads as its name.
534            NodeData::Style { style } => match key {
535                "style" => R::S(
536                    crate::texlib::MATH_STYLE_NAMES
537                        .get(usize::from(*style))
538                        .copied()
539                        .unwrap_or("display")
540                        .as_bytes()
541                        .to_vec(),
542                ),
543                _ => R::Nil,
544            },
545            NodeData::Choice(c) => match key {
546                "display" => R::N(c[0]),
547                "text" => R::N(c[1]),
548                "script" => R::N(c[2]),
549                "scriptscript" => R::N(c[3]),
550                _ => R::Nil,
551            },
552            NodeData::MathChar { fam, chr } => match key {
553                "fam" => R::I((*fam).into()),
554                "char" => R::I((*chr).into()),
555                _ => R::Nil,
556            },
557            NodeData::SubList { head } => match key {
558                "head" | "list" => R::N(*head),
559                _ => R::Nil,
560            },
561            NodeData::Delim {
562                small_fam,
563                small_char,
564                large_fam,
565                large_char,
566            } => match key {
567                "small_fam" => R::I((*small_fam).into()),
568                "small_char" => R::I((*small_char).into()),
569                "large_fam" => R::I((*large_fam).into()),
570                "large_char" => R::I((*large_char).into()),
571                _ => R::Nil,
572            },
573            _ => R::Nil,
574        }
575    })?;
576    Ok(match r {
577        R::I(i) => Value::Integer(i),
578        R::N(n) => wrap(lua, n, direct)?,
579        R::F(f) => Value::Number(f),
580        R::S(s) => Value::String(lua.create_string(&s)?),
581        R::L(n) => lua_refs_synced(lua)?.raw_get(n)?,
582        R::Nil => Value::Nil,
583    })
584}
585
586/// The name of direction `d` (`TLT`, `TRT`, …).
587fn dir_name(d: i8) -> &'static str {
588    retex_engine::showbox::DIR_NAMES[i32::from(d).rem_euclid(4) as usize]
589}
590
591/// A direction given to a `dir` or `direction` field: a number, or a name
592/// with an optional sign (`"-TRT"` closes a direction: `Some(true)`).
593fn dir_value(v: &Value) -> (i8, Option<bool>) {
594    match v {
595        Value::String(s) => {
596            let b = s.as_bytes();
597            let (cancel, name) = match b.first() {
598                Some(b'+') => (Some(false), &b[1..]),
599                Some(b'-') => (Some(true), &b[1..]),
600                _ => (None, &b[..]),
601            };
602            let d = retex_engine::showbox::DIR_NAMES
603                .iter()
604                .position(|n| n.as_bytes() == name)
605                .unwrap_or(0);
606            (d as i8, cancel)
607        }
608        _ => (int(v).clamp(0, 3) as i8, None),
609    }
610}
611
612/// Writes field `key` of node `p`.
613fn set_field(lua: &Lua, p: NodeRef, key: &str, v: Value) -> mlua::Result<()> {
614    if !matches!(key, "next" | "prev" | "subtype" | "attr")
615        && with_engine(lua, |e| e.nodes.id(p) == id::WHATSIT)?
616    {
617        return set_whatsit_field(lua, p, key, v);
618    }
619    if key == "attr" {
620        return set_attr_value(lua, p, &v);
621    }
622    let r = to_ref(&v).unwrap_or(NodeRef::NULL);
623    let i = int(&v);
624    // A style is set by name or by number.
625    let style_value = match &v {
626        Value::String(name) => crate::texlib::MATH_STYLE_NAMES
627            .iter()
628            .position(|n| n.as_bytes() == name.as_bytes().as_ref())
629            .unwrap_or(0) as i64,
630        _ => i.clamp(0, 7),
631    };
632    let f = match &v {
633        Value::Number(x) => *x,
634        Value::Integer(x) => *x as f64,
635        _ => 0.0,
636    };
637    with_engine(lua, |e| {
638        let n = e.nodes.get_mut(p);
639        match key {
640            "next" => {
641                n.next = r;
642                return;
643            }
644            "prev" => {
645                n.prev = r;
646                return;
647            }
648            // Observed: a style node's subtype follows its style and
649            // cannot be set.
650            "subtype" if n.id == id::STYLE => return,
651            "subtype" => {
652                n.subtype = i as u16;
653                return;
654            }
655            // A sign also sets whether the node opens or closes.
656            "dir" | "direction" if n.id == id::DIR => {
657                let (d, cancel) = dir_value(&v);
658                if let Some(c) = cancel {
659                    n.subtype = u16::from(c);
660                }
661                if let NodeData::Dir { dir, .. } = &mut n.data {
662                    *dir = d;
663                }
664                return;
665            }
666            "dir" | "direction" if n.id == id::LOCAL_PAR => {
667                if let NodeData::LocalPar { dir, .. } = &mut n.data {
668                    *dir = dir_value(&v).0;
669                }
670                return;
671            }
672            _ => {}
673        }
674        match &mut n.data {
675            NodeData::Box(b) | NodeData::Unset { b, .. } => match key {
676                "width" => b.width = i as i32,
677                "height" => b.height = i as i32,
678                "depth" => b.depth = i as i32,
679                "shift" => b.shift = i as i32,
680                "dir" | "direction" => b.dir = dir_value(&v).0,
681                "glue_order" => b.glue_order = i as u8,
682                "glue_sign" => b.glue_sign = i as u8,
683                "glue_set" => b.glue_set = f64::from(f as f32),
684                "head" | "list" => b.head = r,
685                _ => {}
686            },
687            NodeData::Glyph(g) => match key {
688                "char" => g.chr = i as u32,
689                "font" => g.font = i as u32,
690                "lang" => g.lang = i as u16,
691                "left" => g.left = i as u8,
692                "right" => g.right = i as u8,
693                "uchyph" => g.uchyph = i != 0,
694                "components" => g.components = r,
695                "xoffset" => g.xoffset = i as i32,
696                "yoffset" => g.yoffset = i as i32,
697                "expansion_factor" => g.expansion_factor = i as i32,
698                "data" => g.data = i as i32,
699                _ => {}
700            },
701            NodeData::Glue { spec, leader } => match key {
702                "width" => spec.width = i as i32,
703                "stretch" => spec.stretch = i as i32,
704                "shrink" => spec.shrink = i as i32,
705                "stretch_order" => spec.stretch_order = i as u8,
706                "shrink_order" => spec.shrink_order = i as u8,
707                "leader" => *leader = r,
708                _ => {}
709            },
710            NodeData::Kern {
711                kern,
712                expansion_factor,
713            } => {
714                if key == "kern" || key == "width" {
715                    *kern = i as i32;
716                } else if key == "expansion_factor" {
717                    *expansion_factor = i as i32;
718                }
719            }
720            NodeData::MarginKern { width, glyph } => match key {
721                "width" => *width = i as i32,
722                "glyph" => *glyph = r,
723                _ => {}
724            },
725            NodeData::Penalty { penalty } => {
726                if key == "penalty" {
727                    *penalty = i as i32;
728                }
729            }
730            NodeData::Rule {
731                width,
732                depth,
733                height,
734                dir,
735                ..
736            } => match key {
737                "dir" | "direction" => *dir = dir_value(&v).0,
738                "width" => *width = i as i32,
739                "depth" => *depth = i as i32,
740                "height" => *height = i as i32,
741                _ => {}
742            },
743            NodeData::Disc {
744                pre,
745                post,
746                replace,
747                penalty,
748            } => match key {
749                "pre" => *pre = r,
750                "post" => *post = r,
751                "replace" => *replace = r,
752                "penalty" => *penalty = i as i32,
753                _ => {}
754            },
755            NodeData::Boundary { value } => {
756                if key == "value" {
757                    *value = i as i32;
758                }
759            }
760            NodeData::Mark { class, .. } => {
761                if key == "class" {
762                    *class = i as u32;
763                }
764            }
765            NodeData::Noad(d) => match key {
766                "nucleus" => d.nucleus = r,
767                "sub" => d.sub = r,
768                "sup" => d.sup = r,
769                "options" => d.options = i as u32,
770                "left" => d.left = r,
771                "degree" => d.degree = r,
772                "width" => d.width = i as i32,
773                "accent" | "top_accent" => {
774                    d.accent = r;
775                    d.options &= !noad_opt::OVERLAY;
776                }
777                "overlay_accent" => {
778                    d.accent = r;
779                    d.options |= noad_opt::OVERLAY;
780                }
781                "bot_accent" => d.bot_accent = r,
782                "fraction" => d.fraction = i as i32,
783                _ => {}
784            },
785            NodeData::Fraction(d) => match key {
786                "width" => d.thickness = i as i32,
787                "num" => d.num = r,
788                "denom" => d.denom = r,
789                "left" => d.left = r,
790                "right" => d.right = r,
791                "middle" => d.middle = r,
792                _ => {}
793            },
794            NodeData::Fence(d) => match key {
795                "delim" => d.delim = r,
796                "italic" => d.italic = i as i32,
797                "height" => d.height = i as i32,
798                "depth" => d.depth = i as i32,
799                "options" => d.options = i as u32,
800                "class" => d.class = i as i32,
801                _ => {}
802            },
803            NodeData::Style { style } => {
804                if key == "style" {
805                    *style = style_value as u8;
806                    n.subtype = u16::from(*style);
807                }
808            }
809            NodeData::Choice(c) => match key {
810                "display" => c[0] = r,
811                "text" => c[1] = r,
812                "script" => c[2] = r,
813                "scriptscript" => c[3] = r,
814                _ => {}
815            },
816            NodeData::MathChar { fam, chr } => match key {
817                "fam" => *fam = i as u32,
818                "char" => *chr = i as u32,
819                _ => {}
820            },
821            NodeData::SubList { head } => {
822                if key == "head" || key == "list" {
823                    *head = r;
824                }
825            }
826            NodeData::Delim {
827                small_fam,
828                small_char,
829                large_fam,
830                large_char,
831            } => match key {
832                "small_fam" => *small_fam = i as u32,
833                "small_char" => *small_char = i as u32,
834                "large_fam" => *large_fam = i as u32,
835                "large_char" => *large_char = i as u32,
836                _ => {}
837            },
838            NodeData::Ins { head, .. } | NodeData::Adjust { head } => {
839                if key == "head" || key == "list" {
840                    *head = r;
841                }
842            }
843            NodeData::Math { surround, glue } => match key {
844                "surround" => *surround = i as i32,
845                "width" => glue.width = i as i32,
846                "stretch" => glue.stretch = i as i32,
847                "shrink" => glue.shrink = i as i32,
848                "stretch_order" => glue.stretch_order = i as u8,
849                "shrink_order" => glue.shrink_order = i as u8,
850                _ => {}
851            },
852            _ => {}
853        }
854    })
855}
856
857/// Installs `node` and `node.direct`.
858pub fn install(lua: &Lua) -> mlua::Result<()> {
859    let node = lua.create_table()?;
860    let direct = lua.create_table()?;
861    for (t, is_direct) in [(&node, false), (&direct, true)] {
862        install_common(lua, t, is_direct)?;
863    }
864    direct.set(
865        "todirect",
866        lua.create_function(|_, v: Value| Ok(to_ref(&v).map(|p| i64::from(p.0))))?,
867    )?;
868    direct.set(
869        "tonode",
870        lua.create_function(|lua, v: Value| match to_ref(&v) {
871            Some(p) => wrap(lua, p, false),
872            None => Ok(Value::Nil),
873        })?,
874    )?;
875    install_properties(lua, &node, &direct)?;
876    require_nodes(lua, &node)?;
877    node.set("direct", direct)?;
878    lua.globals().set("node", node)?;
879    lua.load(ARGUMENT_COUNT_CHECKS)
880        .set_name("=[retex node checks]")
881        .exec()?;
882    Ok(())
883}
884
885/// How a function of the `node` library treats its node argument when it
886/// is missing, nil, or not a node (observed in LuaTeX; "fatal" is its
887/// error "lua <node> expected, not an object with type ...").
888#[derive(Clone, Copy)]
889struct NodeArg {
890    /// The argument's position (0-based).
891    pos: usize,
892    /// A missing argument is fine (the function returns nothing).
893    absent_ok: bool,
894    /// nil is fine.
895    nil_ok: bool,
896}
897
898const STRICT: NodeArg = NodeArg {
899    pos: 0,
900    absent_ok: false,
901    nil_ok: false,
902};
903const NIL_OK: NodeArg = NodeArg {
904    pos: 0,
905    absent_ok: false,
906    nil_ok: true,
907};
908const ABSENT_OK: NodeArg = NodeArg {
909    pos: 0,
910    absent_ok: true,
911    nil_ok: false,
912};
913const NOTHING_OK: NodeArg = NodeArg {
914    pos: 0,
915    absent_ok: true,
916    nil_ok: true,
917};
918
919/// The functions of `node` (not `node.direct`) that check their node
920/// argument. Before, such values were taken as no node and the run went
921/// on where LuaTeX stops, or stopped where LuaTeX goes on.
922const NODE_ARGUMENT: &[(&str, NodeArg)] = &[
923    ("hpack", STRICT),
924    ("vpack", STRICT),
925    ("remove", STRICT),
926    ("mlist_to_hlist", STRICT),
927    ("has_attribute", STRICT),
928    ("set_attribute", STRICT),
929    ("unset_attribute", STRICT),
930    ("protect_glyphs", STRICT),
931    ("protect_glyph", STRICT),
932    ("unprotect_glyphs", STRICT),
933    ("unprotect_glyph", STRICT),
934    ("flatten_discretionaries", STRICT),
935    ("is_char", STRICT),
936    ("is_glyph", STRICT),
937    ("has_glyph", STRICT),
938    ("getglue", STRICT),
939    ("setglue", STRICT),
940    ("is_zero_glue", STRICT),
941    ("hyphenating", STRICT),
942    ("write", ABSENT_OK),
943    ("first_glyph", ABSENT_OK),
944    ("kerning", ABSENT_OK),
945    ("ligaturing", ABSENT_OK),
946    ("copy", NIL_OK),
947    ("copy_list", NIL_OK),
948    ("tail", NIL_OK),
949    ("length", NIL_OK),
950    ("slide", NIL_OK),
951    ("end_of_math", NIL_OK),
952    ("traverse", NIL_OK),
953    ("traverse_char", NIL_OK),
954    ("traverse_glyph", NIL_OK),
955    ("count", NodeArg { pos: 1, ..NIL_OK }),
956    ("traverse_id", NodeArg { pos: 1, ..NIL_OK }),
957    ("free", NOTHING_OK),
958    ("flush_node", NOTHING_OK),
959    ("flush_list", NOTHING_OK),
960];
961
962/// Lua errors (not fatal) of `node` functions called with too few
963/// arguments, raised at the caller's position as LuaTeX does.
964const ARGUMENT_COUNT_CHECKS: &str = r##"
965local node, direct, select, error = node, node.direct, select, error
966local function check(t, name, n, msg)
967  local f = t[name]
968  if f == nil then return end
969  t[name] = function(...)
970    if select("#", ...) < n then error(msg, 2) end
971    return f(...)
972  end
973end
974check(node, "remove", 2, "Not enough arguments for node.remove()")
975check(node, "insert_after", 3, "Not enough arguments for node.insert_after()")
976check(node, "insert_before", 3, "Not enough arguments for node.insert_before()")
977check(node, "set_attribute", 3, "incorrect number of arguments")
978check(node, "unset_attribute", 2,
979  "bad argument #2 to 'unset_attribute' (number expected, got no value)")
980do
981  local f = node.unset_attribute
982  node.unset_attribute = function(...)
983    local id = select(2, ...)
984    if select("#", ...) >= 2 and type(id) ~= "number" then
985      error("bad argument #2 to 'unset_attribute' (number expected, got " .. type(id) .. ")", 2)
986    end
987    return f(...)
988  end
989end
990check(node, "dimensions", 1, "missing argument to 'dimensions' (node expected)")
991check(node, "rangedimensions", 2,
992  "missing argument to 'rangedimensions' (2 or more nodes expected)")
993check(direct, "dimensions", 1, "missing argument to 'dimensions' (direct node expected)")
994check(direct, "rangedimensions", 2,
995  "missing argument to 'rangedimensions' (2 or more direct nodes expected)")
996"##;
997
998/// Wraps the functions of [`NODE_ARGUMENT`].
999fn require_nodes(lua: &Lua, node: &Table) -> mlua::Result<()> {
1000    for &(name, rule) in NODE_ARGUMENT {
1001        let Ok(f) = node.get::<mlua::Function>(name) else {
1002            continue;
1003        };
1004        let checked = lua.create_function(move |lua, args: MultiValue| {
1005            let arg = args.get(rule.pos).cloned();
1006            let fine = match &arg {
1007                None => rule.absent_ok,
1008                Some(Value::Nil) => rule.nil_ok,
1009                // Attribute lists are nodes too, for traversal.
1010                Some(Value::UserData(u)) => {
1011                    u.is::<LNode>() || (u.is::<LAttr>() && name.starts_with("traverse"))
1012                }
1013                Some(_) => false,
1014            };
1015            if !fine {
1016                return Err(match &arg {
1017                    None => {
1018                        let msg = "lua <node> expected, not an object with type no value";
1019                        match with_engine(lua, |e| e.luatex_fatal("node lib", msg)) {
1020                            Err(e) => e,
1021                        }
1022                    }
1023                    Some(v) => crate::texlib::not_a_node(lua, v),
1024                });
1025            }
1026            if arg.is_none() {
1027                // Observed: nil comes back.
1028                return Ok(MultiValue::from_iter([Value::Nil]));
1029            }
1030            f.call::<MultiValue>(args)
1031        })?;
1032        node.set(name, checked)?;
1033    }
1034    // node.dimensions and node.rangedimensions take glue settings or a
1035    // parent before the node: nil there is fatal.
1036    for name in ["dimensions", "rangedimensions"] {
1037        if let Ok(f) = node.get::<mlua::Function>(name) {
1038            let checked = lua.create_function(move |lua, args: MultiValue| {
1039                if let Some(v) = args.iter().find(|v| {
1040                    !matches!(v, Value::Integer(_) | Value::Number(_))
1041                        && !matches!(v, Value::UserData(u) if u.is::<LNode>())
1042                }) {
1043                    return Err(crate::texlib::not_a_node(lua, v));
1044                }
1045                f.call::<MultiValue>(args)
1046            })?;
1047            node.set(name, checked)?;
1048        }
1049    }
1050    Ok(())
1051}
1052
1053/// Registry key of the table holding node properties by direct number.
1054const PROPERTIES_KEY: &str = "retex.node.properties";
1055
1056/// The property store, with the engine's pending frees and copies of
1057/// nodes with properties applied (see [`retex_engine::nodes::Properties`]).
1058fn properties(lua: &Lua) -> mlua::Result<Table> {
1059    let store: Table = match lua.named_registry_value::<Value>(PROPERTIES_KEY)? {
1060        Value::Table(t) => t,
1061        _ => {
1062            let t = lua.create_table()?;
1063            lua.set_named_registry_value(PROPERTIES_KEY, &t)?;
1064            t
1065        }
1066    };
1067    let events = with_engine(lua, |e| std::mem::take(&mut e.nodes.props.events))?;
1068    for ev in events {
1069        match ev {
1070            retex_engine::nodes::PropEvent::Flush(n) => store.raw_set(n, Value::Nil)?,
1071            retex_engine::nodes::PropEvent::Copy(from, to) => {
1072                // Observed (properties mode copy): a table is inherited
1073                // through `__index`; other values are not copied.
1074                let v = match store.raw_get::<Value>(from)? {
1075                    Value::Table(t) => {
1076                        let copy = lua.create_table()?;
1077                        let mt = lua.create_table()?;
1078                        mt.set("__index", t)?;
1079                        copy.set_metatable(Some(mt))?;
1080                        Value::Table(copy)
1081                    }
1082                    _ => Value::Nil,
1083                };
1084                if v.is_nil() {
1085                    with_engine(lua, |e| e.nodes.props.nodes.remove(&to))?;
1086                }
1087                store.raw_set(to, v)?;
1088            }
1089        }
1090    }
1091    Ok(store)
1092}
1093
1094fn set_property(lua: &Lua, p: NodeRef, v: Value) -> mlua::Result<()> {
1095    let store = properties(lua)?;
1096    with_engine(lua, |e| {
1097        let props = &mut e.nodes.props;
1098        if v.is_nil() {
1099            props.nodes.remove(&p.0);
1100        } else if props.flush || props.copy {
1101            props.nodes.insert(p.0);
1102        }
1103    })?;
1104    store.raw_set(p.0, v)
1105}
1106
1107/// `get_properties_table`, `getproperty`, `setproperty`,
1108/// `flush_properties_table` and `set_properties_mode` (LuaTeX manual
1109/// §10.6.6). The tables returned by `get_properties_table` are proxies
1110/// indexed by node (`node`) or direct number (`node.direct`), so that
1111/// properties of freed nodes go away as in LuaTeX.
1112fn install_properties(lua: &Lua, node: &Table, direct: &Table) -> mlua::Result<()> {
1113    for t in [node, direct] {
1114        let proxy = lua.create_table()?;
1115        let mt = lua.create_table()?;
1116        mt.set(
1117            "__index",
1118            lua.create_function(|lua, (_t, k): (Value, Value)| match to_ref(&k) {
1119                Some(p) => properties(lua)?.raw_get::<Value>(p.0),
1120                None => Ok(Value::Nil),
1121            })?,
1122        )?;
1123        mt.set(
1124            "__newindex",
1125            lua.create_function(|lua, (_t, k, v): (Value, Value, Value)| match to_ref(&k) {
1126                Some(p) => set_property(lua, p, v),
1127                None => Ok(()),
1128            })?,
1129        )?;
1130        proxy.set_metatable(Some(mt))?;
1131        t.set(
1132            "get_properties_table",
1133            lua.create_function(move |_, ()| Ok(proxy.clone()))?,
1134        )?;
1135        t.set(
1136            "getproperty",
1137            lua.create_function(|lua, v: Value| match to_ref(&v) {
1138                Some(p) => properties(lua)?.raw_get::<Value>(p.0),
1139                None => Ok(Value::Nil),
1140            })?,
1141        )?;
1142        t.set(
1143            "setproperty",
1144            lua.create_function(|lua, (v, x): (Value, Value)| match to_ref(&v) {
1145                Some(p) => set_property(lua, p, x),
1146                None => Ok(()),
1147            })?,
1148        )?;
1149        t.set(
1150            "flush_properties_table",
1151            lua.create_function(|lua, ()| {
1152                let store = lua.create_table()?;
1153                lua.set_named_registry_value(PROPERTIES_KEY, &store)?;
1154                with_engine(lua, |e| {
1155                    e.nodes.props.nodes.clear();
1156                    e.nodes.props.events.clear();
1157                })
1158            })?,
1159        )?;
1160        t.set(
1161            "set_properties_mode",
1162            lua.create_function(|lua, (flush, copy): (Option<bool>, Option<bool>)| {
1163                with_engine(lua, |e| {
1164                    e.nodes.props.flush = flush.unwrap_or(false);
1165                    e.nodes.props.copy = copy.unwrap_or(false);
1166                })
1167            })?,
1168        )?;
1169    }
1170    Ok(())
1171}
1172
1173fn install_common(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
1174    t.set(
1175        "types",
1176        lua.create_function(|lua, ()| {
1177            let out = lua.create_table()?;
1178            for (k, v) in &info().names {
1179                out.set(*k, v.as_str())?;
1180            }
1181            Ok(out)
1182        })?,
1183    )?;
1184    t.set(
1185        "whatsits",
1186        lua.create_function(|lua, ()| {
1187            let out = lua.create_table()?;
1188            for (k, v) in &info().whatsits {
1189                out.set(*k, v.as_str())?;
1190            }
1191            Ok(out)
1192        })?,
1193    )?;
1194    t.set(
1195        "id",
1196        lua.create_function(|_, name: String| Ok(info().ids.get(&name).copied()))?,
1197    )?;
1198    t.set(
1199        "subtype",
1200        lua.create_function(|_, name: String| {
1201            Ok(info()
1202                .whatsits
1203                .iter()
1204                .find(|(_, n)| *n == name)
1205                .map(|(k, _)| *k))
1206        })?,
1207    )?;
1208    t.set(
1209        "type",
1210        lua.create_function(|lua, v: Value| match &v {
1211            Value::Integer(n) if !matches!(v, Value::UserData(_)) => {
1212                Ok(info().names.get(&(*n as u8)).cloned())
1213            }
1214            _ => match to_ref(&v) {
1215                Some(p) => {
1216                    let t = with_engine(lua, |e| e.nodes.id(p))?;
1217                    Ok(info().names.get(&t).cloned())
1218                }
1219                None => Ok(None),
1220            },
1221        })?,
1222    )?;
1223    t.set(
1224        "subtypes",
1225        lua.create_function(|lua, v: Value| {
1226            let idn = match &v {
1227                Value::Integer(n) => *n as u8,
1228                Value::String(s) => match info().ids.get(&*s.to_str()?) {
1229                    Some(i) => *i,
1230                    None => return Ok(Value::Nil),
1231                },
1232                _ => return Ok(Value::Nil),
1233            };
1234            let out = lua.create_table()?;
1235            if let Some(st) = info().subtypes.get(&idn) {
1236                for (k, n) in st {
1237                    out.set(*k, n.as_str())?;
1238                }
1239            }
1240            Ok(Value::Table(out))
1241        })?,
1242    )?;
1243    t.set(
1244        "fields",
1245        lua.create_function(|lua, (v, sub): (Value, Option<i64>)| {
1246            let idn = match &v {
1247                Value::Integer(n) => *n as u8,
1248                Value::String(s) => match info().ids.get(&*s.to_str()?) {
1249                    Some(i) => *i,
1250                    None => return Ok(Value::Nil),
1251                },
1252                _ => return Ok(Value::Nil),
1253            };
1254            let list = if idn == id::WHATSIT {
1255                sub.and_then(|s| info().whatsit_fields.get(&(s as u16)))
1256            } else {
1257                info().fields.get(&idn)
1258            };
1259            let out = lua.create_table()?;
1260            if let Some(fl) = list {
1261                for (k, n) in fl {
1262                    out.set(*k, n.as_str())?;
1263                }
1264            }
1265            Ok(Value::Table(out))
1266        })?,
1267    )?;
1268    t.set(
1269        "is_node",
1270        lua.create_function(move |_, v: Value| {
1271            Ok(match &v {
1272                Value::UserData(u) => u.borrow::<LNode>().is_ok(),
1273                Value::Integer(n) => direct && *n > 0,
1274                _ => false,
1275            })
1276        })?,
1277    )?;
1278    t.set(
1279        "new",
1280        lua.create_function(move |lua, (ty, sub): (Value, Option<Value>)| {
1281            let t = match &ty {
1282                Value::Integer(n) => *n as u8,
1283                Value::String(s) => *info()
1284                    .ids
1285                    .get(&*s.to_str()?)
1286                    .ok_or_else(|| mlua::Error::runtime("unknown node type"))?,
1287                _ => return Err(mlua::Error::runtime("node type expected")),
1288            };
1289            let st = match &sub {
1290                Some(Value::Integer(n)) => *n as u16,
1291                Some(Value::String(s)) => {
1292                    let s = s.to_str()?.to_owned();
1293                    info()
1294                        .whatsits
1295                        .iter()
1296                        .find(|(_, n)| *n == s)
1297                        .map(|(k, _)| *k)
1298                        .unwrap_or(0)
1299                }
1300                _ => 0,
1301            };
1302            let data = if t == id::WHATSIT {
1303                NodeData::Whatsit(Box::new(new_whatsit(st)))
1304            } else if t == id::STYLE {
1305                // Observed: a style node's subtype is its style.
1306                NodeData::Style {
1307                    style: st.min(7) as u8,
1308                }
1309            } else {
1310                new_data(t)
1311            };
1312            // Observed: new nodes carry the current attributes.
1313            let p = with_engine(lua, |e| e.new_node(t, st, data))?;
1314            wrap(lua, p, direct)
1315        })?,
1316    )?;
1317    // Observed: free returns the node's successor.
1318    t.set(
1319        "free",
1320        lua.create_function(move |lua, v: Value| {
1321            impossible(lua, &v, "free")?;
1322            let Some(p) = to_ref(&v) else {
1323                return Ok(Value::Nil);
1324            };
1325            let next = with_engine(lua, |e| {
1326                let n = e.nodes.next(p);
1327                e.nodes.free(p);
1328                e.check_double_free();
1329                n
1330            })?;
1331            if next.is_null() {
1332                Ok(Value::Nil)
1333            } else {
1334                wrap(lua, next, direct)
1335            }
1336        })?,
1337    )?;
1338    t.set(
1339        "flush_node",
1340        lua.create_function(|lua, v: Value| {
1341            impossible(lua, &v, "free")?;
1342            if let Some(p) = to_ref(&v) {
1343                with_engine(lua, |e| {
1344                    e.nodes.free(p);
1345                    e.check_double_free();
1346                })?;
1347            }
1348            Ok(())
1349        })?,
1350    )?;
1351    t.set(
1352        "flush_list",
1353        lua.create_function(|lua, v: Value| {
1354            impossible(lua, &v, "free")?;
1355            if let Some(p) = to_ref(&v) {
1356                with_engine(lua, |e| {
1357                    e.nodes.flush_list(p);
1358                    e.check_double_free();
1359                })?;
1360            }
1361            Ok(())
1362        })?,
1363    )?;
1364    t.set(
1365        "copy",
1366        lua.create_function(move |lua, v: Value| {
1367            match impossible(lua, &v, "copy").map(|()| to_ref(&v))? {
1368                Some(p) => {
1369                    let q = with_engine(lua, |e| e.nodes.copy_node(p))?;
1370                    wrap(lua, q, direct)
1371                }
1372                None => Ok(Value::Nil),
1373            }
1374        })?,
1375    )?;
1376    t.set(
1377        "copy_list",
1378        lua.create_function(move |lua, v: Value| {
1379            match impossible(lua, &v, "copy").map(|()| to_ref(&v))? {
1380                Some(p) => {
1381                    let q = with_engine(lua, |e| e.nodes.copy_list(p))?;
1382                    wrap(lua, q, direct)
1383                }
1384                None => Ok(Value::Nil),
1385            }
1386        })?,
1387    )?;
1388    t.set(
1389        "tail",
1390        lua.create_function(move |lua, v: Value| match to_ref(&v) {
1391            Some(p) => {
1392                let q = with_engine(lua, |e| tail(e, p))?;
1393                wrap(lua, q, direct)
1394            }
1395            None => Ok(Value::Nil),
1396        })?,
1397    )?;
1398    t.set(
1399        "slide",
1400        lua.create_function(move |lua, v: Value| match to_ref(&v) {
1401            Some(p) => {
1402                let q = with_engine(lua, |e| slide(e, p))?;
1403                wrap(lua, q, direct)
1404            }
1405            None => Ok(Value::Nil),
1406        })?,
1407    )?;
1408    t.set(
1409        "length",
1410        lua.create_function(|lua, v: Value| match to_ref(&v) {
1411            Some(p) => with_engine(lua, |e| count(e, p, None)),
1412            None => Ok(0),
1413        })?,
1414    )?;
1415    t.set(
1416        "count",
1417        lua.create_function(|lua, (ty, v): (i64, Value)| match to_ref(&v) {
1418            Some(p) => with_engine(lua, |e| count(e, p, Some(ty as u8))),
1419            None => Ok(0),
1420        })?,
1421    )?;
1422    for (name, by_id) in [("traverse", false), ("traverse_id", true)] {
1423        t.set(
1424            name,
1425            lua.create_function(move |lua, args: MultiValue| {
1426                let v: Vec<Value> = args.into_iter().collect();
1427                let (filter, start) = if by_id {
1428                    (
1429                        Some(int(v.first().unwrap_or(&Value::Nil)) as u8),
1430                        v.get(1).cloned(),
1431                    )
1432                } else {
1433                    (None, v.first().cloned())
1434                };
1435                if let Some(Value::UserData(u)) = &start
1436                    && u.is::<LAttr>()
1437                {
1438                    let mut entries =
1439                        attr_entries(lua, u)?.into_iter().filter(move |[_, id, _]| {
1440                            filter.is_none_or(|f| *id == Value::Integer(i64::from(f)))
1441                        });
1442                    let iter = lua.create_function_mut(move |_, (_s, _cur): (Value, Value)| {
1443                        Ok(match entries.next() {
1444                            Some([n, _, sub]) if by_id => MultiValue::from_iter([n, sub]),
1445                            Some([n, id, sub]) => MultiValue::from_iter([n, id, sub]),
1446                            None => MultiValue::new(),
1447                        })
1448                    })?;
1449                    return Ok((iter, Value::Nil, Value::Nil));
1450                }
1451                let start = start.and_then(|s| to_ref(&s)).unwrap_or(NodeRef::NULL);
1452                let iter = lua.create_function(move |lua, (_s, cur): (Value, Value)| {
1453                    let next = match to_ref(&cur) {
1454                        Some(c) => with_engine(lua, |e| e.nodes.next(c))?,
1455                        None => start,
1456                    };
1457                    let mut p = next;
1458                    let found = with_engine(lua, |e| {
1459                        while !p.is_null() {
1460                            if filter.is_none_or(|f| e.nodes.id(p) == f) {
1461                                return (p, e.nodes.id(p), e.nodes.get(p).subtype);
1462                            }
1463                            p = e.nodes.next(p);
1464                        }
1465                        (NodeRef::NULL, 0, 0)
1466                    })?;
1467                    if found.0.is_null() {
1468                        return Ok(MultiValue::new());
1469                    }
1470                    // `traverse` yields node, id and subtype;
1471                    // `traverse_id` node and subtype.
1472                    let n = wrap(lua, found.0, direct)?;
1473                    let sub = Value::Integer(i64::from(found.2));
1474                    Ok(if by_id {
1475                        MultiValue::from_iter([n, sub])
1476                    } else {
1477                        MultiValue::from_iter([n, Value::Integer(i64::from(found.1)), sub])
1478                    })
1479                })?;
1480                Ok((iter, Value::Nil, Value::Nil))
1481            })?,
1482        )?;
1483    }
1484    t.set(
1485        "insert_after",
1486        lua.create_function(move |lua, (h, c, n): (Value, Value, Value)| {
1487            // Observed: without a node to insert, the head comes back.
1488            let (Some(n), head) = (to_ref(&n), to_ref(&h)) else {
1489                return Ok(MultiValue::from_iter([h, Value::Nil]));
1490            };
1491            let cur = to_ref(&c);
1492            let head = with_engine(lua, |e| match (head, cur) {
1493                (None, _) | (_, None) => n,
1494                (Some(h), Some(c)) => {
1495                    // The tail of the new list is found before linking:
1496                    // inserting a node after itself must not make a loop
1497                    // (found by fuzzing).
1498                    let nx = e.nodes.next(c);
1499                    let tail = tail(e, n);
1500                    e.nodes.link(c, n);
1501                    e.nodes.link(tail, nx);
1502                    h
1503                }
1504            })?;
1505            Ok(MultiValue::from_iter([
1506                wrap(lua, head, direct)?,
1507                wrap(lua, n, direct)?,
1508            ]))
1509        })?,
1510    )?;
1511    t.set(
1512        "insert_before",
1513        lua.create_function(move |lua, (h, c, n): (Value, Value, Value)| {
1514            let Some(n) = to_ref(&n) else {
1515                return Ok(MultiValue::from_iter([h, Value::Nil]));
1516            };
1517            let head = to_ref(&h);
1518            let cur = to_ref(&c);
1519            let head = with_engine(lua, |e| match (head, cur) {
1520                (None, _) => n,
1521                (Some(h), None) => {
1522                    let t = tail(e, h);
1523                    e.nodes.link(t, n);
1524                    h
1525                }
1526                (Some(h), Some(c)) => {
1527                    let pv = e.nodes.get(c).prev;
1528                    let tail = tail(e, n);
1529                    e.nodes.link(tail, c);
1530                    if c == h || pv.is_null() {
1531                        e.nodes.get_mut(n).prev = NodeRef::NULL;
1532                        n
1533                    } else {
1534                        e.nodes.link(pv, n);
1535                        h
1536                    }
1537                }
1538            })?;
1539            Ok(MultiValue::from_iter([
1540                wrap(lua, head, direct)?,
1541                wrap(lua, n, direct)?,
1542            ]))
1543        })?,
1544    )?;
1545    t.set(
1546        "remove",
1547        lua.create_function(move |lua, (h, c): (Value, Value)| {
1548            // Observed: without a node to remove, the head comes back.
1549            let (Some(h), Some(c)) = (to_ref(&h), to_ref(&c)) else {
1550                return Ok(MultiValue::from_iter([h, Value::Nil]));
1551            };
1552            let (head, next) = with_engine(lua, |e| {
1553                let pv = e.nodes.get(c).prev;
1554                let nx = e.nodes.next(c);
1555                let head = if c == h {
1556                    nx
1557                } else {
1558                    e.nodes.link(pv, nx);
1559                    h
1560                };
1561                if !nx.is_null() && c == h {
1562                    e.nodes.get_mut(nx).prev = NodeRef::NULL;
1563                }
1564                // Observed: the removed node keeps its links.
1565                (head, nx)
1566            })?;
1567            Ok(MultiValue::from_iter([
1568                wrap(lua, head, direct)?,
1569                wrap(lua, next, direct)?,
1570            ]))
1571        })?,
1572    )?;
1573    t.set(
1574        "has_field",
1575        lua.create_function(|lua, (v, f): (Value, String)| match to_ref(&v) {
1576            Some(p) => {
1577                let t = with_engine(lua, |e| e.nodes.id(p))?;
1578                Ok(info()
1579                    .fields
1580                    .get(&t)
1581                    .is_some_and(|fl| fl.iter().any(|(_, n)| *n == f)))
1582            }
1583            None => Ok(false),
1584        })?,
1585    )?;
1586    t.set(
1587        "getfield",
1588        lua.create_function(move |lua, (v, f): (Value, String)| match to_ref(&v) {
1589            Some(p) => get_field_impl(lua, p, &f, direct),
1590            None => Ok(Value::Nil),
1591        })?,
1592    )?;
1593    t.set(
1594        "setfield",
1595        lua.create_function(|lua, (v, f, x): (Value, String, Value)| match to_ref(&v) {
1596            Some(p) => set_field(lua, p, &f, x),
1597            None => Ok(()),
1598        })?,
1599    )?;
1600    // Direct-style accessors (also accepted by node.* in LuaTeX).
1601    for name in [
1602        "id", "subtype", "next", "prev", "char", "font", "width", "height", "depth", "list",
1603        "penalty", "kern", "leader",
1604    ] {
1605        let key = if name == "list" { "head" } else { name };
1606        if name != "id" && name != "subtype" {
1607            t.set(
1608                format!("get{name}"),
1609                lua.create_function(move |lua, v: Value| match to_ref(&v) {
1610                    Some(p) => get_field_impl(lua, p, key, direct),
1611                    None => Ok(Value::Nil),
1612                })?,
1613            )?;
1614        } else {
1615            t.set(
1616                format!("get{name}"),
1617                lua.create_function(move |lua, v: Value| match to_ref(&v) {
1618                    Some(p) => get_field_impl(lua, p, key, direct),
1619                    None => Ok(Value::Nil),
1620                })?,
1621            )?;
1622        }
1623        t.set(
1624            format!("set{name}"),
1625            lua.create_function(move |lua, (v, x): (Value, Value)| match to_ref(&v) {
1626                Some(p) => set_field(lua, p, key, x),
1627                None => Ok(()),
1628            })?,
1629        )?;
1630    }
1631    install_accessors(lua, t, direct)?;
1632    // Engine operations that ltluatex installs as callback defaults; the
1633    // real implementations come with the typesetting code.
1634    // node.mlist_to_hlist(head, style, penalties): the engine's conversion.
1635    t.set(
1636        "mlist_to_hlist",
1637        lua.create_function(move |lua, args: MultiValue| {
1638            let v: Vec<Value> = args.into_iter().collect();
1639            let head = v.first().and_then(to_ref).unwrap_or(NodeRef::NULL);
1640            // Observed: nodes that cannot be in a math list are LuaTeX's
1641            // "This can't happen (mlist1)", a boundary "(mlist3)".
1642            with_engine(lua, |e| {
1643                let mut p = head;
1644                let mut steps = e.nodes.capacity();
1645                while !p.is_null() && steps > 0 {
1646                    steps -= 1;
1647                    match e.nodes.id(p) {
1648                        id::GLYPH
1649                        | id::MATH
1650                        | id::MARGIN_KERN
1651                        | id::LOCAL_PAR
1652                        | id::DIR
1653                        | id::SUB_BOX
1654                        | id::UNSET
1655                        | id::GLUE_SPEC
1656                        | id::HLIST
1657                        | id::VLIST
1658                        | id::TEMP
1659                        | id::DELIM
1660                        | id::MATH_CHAR
1661                        | id::SUB_MLIST
1662                        | id::MATH_TEXT_CHAR
1663                        | 38 => e.confusion("mlist1"),
1664                        id::BOUNDARY => e.confusion("mlist3"),
1665                        _ => {}
1666                    }
1667                    p = e.nodes.next(p);
1668                }
1669            })?;
1670            let style = match v.get(1) {
1671                Some(Value::String(s)) => {
1672                    let s = s.to_str()?.to_string();
1673                    crate::texlib::MATH_STYLE_NAMES
1674                        .iter()
1675                        .position(|n| *n == s)
1676                        .unwrap_or(2) as i32
1677                }
1678                Some(Value::Integer(n)) => (*n as i32).clamp(0, 7),
1679                _ => 2,
1680            };
1681            let pen = matches!(v.get(2), Some(Value::Boolean(true)));
1682            if head.is_null() {
1683                return Ok(Value::Nil);
1684            }
1685            let r = with_engine(lua, |e| e.mlist_to_hlist(head, style, pen))?;
1686            if r.is_null() {
1687                return Ok(Value::Nil);
1688            }
1689            wrap(lua, r, direct)
1690        })?,
1691    )?;
1692    // rangedimensions(parent, first [, last]): the natural size of the
1693    // list from first up to last (exclusive).
1694    t.set(
1695        "rangedimensions",
1696        lua.create_function(|lua, args: MultiValue| {
1697            let first = args.get(1).and_then(to_ref).unwrap_or(NodeRef::NULL);
1698            let stop = args.get(2).and_then(to_ref).unwrap_or(NodeRef::NULL);
1699            if first.is_null() {
1700                return Ok((0, 0, 0));
1701            }
1702            with_engine(lua, |e| e.natural_dimensions(first, stop))
1703        })?,
1704    )?;
1705    // ligaturing(head) and kerning(head): the built-in passes; they give
1706    // the head, the tail and success.
1707    for (name, pass) in [
1708        ("ligaturing", retex_engine::nodeproc::NodePass::Ligaturing),
1709        ("kerning", retex_engine::nodeproc::NodePass::Kerning),
1710    ] {
1711        t.set(
1712            name,
1713            lua.create_function(move |lua, args: MultiValue| {
1714                let Some(h) = args.front().and_then(to_ref) else {
1715                    return Ok(MultiValue::from_iter([Value::Nil]));
1716                };
1717                let (h, tl) = with_engine(lua, |e| e.lua_node_pass(pass, h))?;
1718                Ok(MultiValue::from_iter([
1719                    wrap(lua, h, direct)?,
1720                    wrap(lua, tl, direct)?,
1721                    Value::Boolean(true),
1722                ]))
1723            })?,
1724        )?;
1725    }
1726    for name in ["hyphenating", "flatten_discretionaries"] {
1727        t.set(
1728            name,
1729            lua.create_function(move |_, args: MultiValue| Ok(args))?,
1730        )?;
1731    }
1732    install_list_ops(lua, t, direct)?;
1733    // The font of a math family in text size.
1734    t.set(
1735        "family_font",
1736        lua.create_function(|lua, fam: i64| {
1737            if !(0..retex_engine::eqtb::NUM_FAMILIES as i64).contains(&fam) {
1738                return Ok(0);
1739            }
1740            with_engine(lua, |e| i64::from(e.fam_fnt(fam as u32, 0)))
1741        })?,
1742    )?;
1743    t.set(
1744        "dimensions",
1745        lua.create_function(|lua, args: MultiValue| {
1746            // dimensions([glue_set, glue_sign, glue_order,] n [, t]): the
1747            // natural size of the list from n up to t (exclusive).
1748            let refs: Vec<NodeRef> = args.iter().filter_map(to_ref).collect();
1749            let Some(&first) = refs.first() else {
1750                return Ok((0, 0, 0));
1751            };
1752            let stop = refs.get(1).copied().unwrap_or(NodeRef::NULL);
1753            with_engine(lua, |e| e.natural_dimensions(first, stop))
1754        })?,
1755    )?;
1756    t.set(
1757        "has_attribute",
1758        lua.create_function(|lua, (v, n, val): (Value, i64, Option<i64>)| {
1759            let Some(p) = to_ref(&v) else {
1760                return Ok(None);
1761            };
1762            let a = with_engine(lua, |e| {
1763                e.nodes
1764                    .get(p)
1765                    .attr
1766                    .as_ref()
1767                    .and_then(|l| l.iter().find(|(k, _)| i64::from(*k) == n).map(|x| x.1))
1768            })?;
1769            Ok(match (a, val) {
1770                (Some(x), Some(w)) if i64::from(x) != w => None,
1771                (a, _) => a.map(i64::from),
1772            })
1773        })?,
1774    )?;
1775    t.set(
1776        "set_attribute",
1777        lua.create_function(|lua, (v, n, val): (Value, Value, Value)| {
1778            // Observed: both numbers are read as `lua_tointeger` does,
1779            // anything else being 0 (tagpdf passes nil).
1780            let (n, val) = (to_integer(&n), to_integer(&val));
1781            if let Some(p) = to_ref(&v) {
1782                with_engine(lua, |e| set_attr(e, p, n as u32, Some(val as i32)))?;
1783            }
1784            Ok(())
1785        })?,
1786    )?;
1787    t.set(
1788        "unset_attribute",
1789        lua.create_function(|lua, (v, n): (Value, i64)| {
1790            if let Some(p) = to_ref(&v) {
1791                with_engine(lua, |e| set_attr(e, p, n as u32, None))?;
1792            }
1793            Ok(())
1794        })?,
1795    )?;
1796    Ok(())
1797}
1798
1799/// A value as `lua_tointeger` converts it: integers, floats with an
1800/// integral value and strings holding one; 0 otherwise.
1801fn to_integer(v: &Value) -> i64 {
1802    let float = |f: f64| {
1803        if f.fract() == 0.0 && f >= i64::MIN as f64 && f < i64::MAX as f64 {
1804            f as i64
1805        } else {
1806            0
1807        }
1808    };
1809    match v {
1810        Value::Integer(n) => *n,
1811        Value::Number(f) => float(*f),
1812        Value::String(s) => match s.to_str() {
1813            Ok(t) => {
1814                let t = t.trim();
1815                t.parse::<i64>()
1816                    .ok()
1817                    .or_else(|| t.parse::<f64>().ok().map(float))
1818                    .unwrap_or(0)
1819            }
1820            Err(_) => 0,
1821        },
1822        _ => 0,
1823    }
1824}
1825
1826fn set_attr(e: &mut Engine, p: NodeRef, n: u32, v: Option<i32>) {
1827    let node = e.nodes.get_mut(p);
1828    let mut list: Vec<(u32, i32)> = node.attr.as_ref().map(|l| l.to_vec()).unwrap_or_default();
1829    list.retain(|(k, _)| *k != n);
1830    if let Some(v) = v {
1831        list.push((n, v));
1832        list.sort();
1833    }
1834    node.attr = if list.is_empty() {
1835        None
1836    } else {
1837        Some(std::rc::Rc::from(list))
1838    };
1839}
1840
1841/// LuaTeX's fatal error for a direct node number that names no node
1842/// (negative, or beyond the node memory).
1843fn impossible(lua: &Lua, v: &Value, what: &str) -> mlua::Result<()> {
1844    // Observed: freeing the temporary head that the node processing
1845    // callbacks get for a paragraph is "attempt to free an impossible
1846    // node" too (in a box, it is freed twice later).
1847    if what == "free"
1848        && let Some(p) = to_ref(v)
1849        && with_engine(lua, |e| p == e.nodes.par_temp)?
1850    {
1851        let msg = format!("attempt to free an impossible node {}", p.0);
1852        return match with_engine(lua, |e| e.luatex_fatal("nodes", &msg)) {
1853            Err(e) => Err(e),
1854        };
1855    }
1856    if let Value::Integer(n) = *v {
1857        let cap = with_engine(lua, |e| e.nodes.capacity())?;
1858        if n < 0 || n as u64 >= cap as u64 {
1859            let msg = format!("attempt to {what} an impossible node {n}");
1860            return match with_engine(lua, |e| e.luatex_fatal("nodes", &msg)) {
1861                Err(e) => Err(e),
1862            };
1863        }
1864    }
1865    Ok(())
1866}
1867
1868/// Ends the run for a list that Lua code linked into a loop: walking it
1869/// would never end (LuaTeX hangs).
1870fn circular(e: &mut Engine) -> ! {
1871    e.luatex_fatal("nodes", "circular node list")
1872}
1873
1874/// The last node of the list `p` (`node.tail`).
1875fn tail(e: &mut Engine, p: NodeRef) -> NodeRef {
1876    match e.nodes.bounded_tail(p) {
1877        Some(t) => t,
1878        None => circular(e),
1879    }
1880}
1881
1882fn slide(e: &mut Engine, mut p: NodeRef) -> NodeRef {
1883    for _ in 0..e.nodes.capacity() {
1884        let n = e.nodes.next(p);
1885        if n.is_null() {
1886            return p;
1887        }
1888        e.nodes.get_mut(n).prev = p;
1889        p = n;
1890    }
1891    circular(e)
1892}
1893
1894fn count(e: &mut Engine, mut p: NodeRef, ty: Option<u8>) -> i64 {
1895    let mut n = 0;
1896    let mut left = e.nodes.capacity();
1897    while !p.is_null() {
1898        if left == 0 {
1899            circular(e);
1900        }
1901        left -= 1;
1902        if ty.is_none_or(|t| e.nodes.id(p) == t) {
1903            n += 1;
1904        }
1905        p = e.nodes.next(p);
1906    }
1907    n
1908}
1909
1910/// Creates `name` in `t`.
1911fn set_fn<A, R, F>(lua: &Lua, t: &Table, name: &str, f: F) -> mlua::Result<()>
1912where
1913    A: mlua::FromLuaMulti,
1914    R: mlua::IntoLuaMulti,
1915    F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
1916{
1917    t.set(name, lua.create_function(f)?)
1918}
1919
1920/// The glyph's character, font and subtype, if `p` is a glyph.
1921fn glyph_of(e: &Engine, p: NodeRef) -> Option<(u32, u32, u16)> {
1922    match &e.nodes.get(p).data {
1923        NodeData::Glyph(g) => Some((g.chr, g.font, e.nodes.get(p).subtype)),
1924        _ => None,
1925    }
1926}
1927
1928/// What a list iterator returns besides the node.
1929type Pick = fn(&Engine, NodeRef) -> Option<Vec<Value>>;
1930
1931/// An iterator over the list from `start` returning, for each node that
1932/// `pick` accepts, the node and the values `pick` gives.
1933fn list_iterator(
1934    lua: &Lua,
1935    start: NodeRef,
1936    direct: bool,
1937    pick: Pick,
1938) -> mlua::Result<(mlua::Function, Value, Value)> {
1939    let iter = lua.create_function(move |lua, (_s, cur): (Value, Value)| {
1940        let mut p = match to_ref(&cur) {
1941            Some(c) => with_engine(lua, |e| e.nodes.next(c))?,
1942            None => start,
1943        };
1944        let found = with_engine(lua, |e| {
1945            while !p.is_null() {
1946                if let Some(v) = pick(e, p) {
1947                    return Some((p, v));
1948                }
1949                p = e.nodes.next(p);
1950            }
1951            None
1952        })?;
1953        let Some((p, vals)) = found else {
1954            return Ok(MultiValue::new());
1955        };
1956        let mut out = vec![wrap(lua, p, direct)?];
1957        out.extend(vals);
1958        Ok(MultiValue::from_iter(out))
1959    })?;
1960    Ok((iter, Value::Nil, Value::Nil))
1961}
1962
1963/// The `get…`/`set…` accessors and helpers of LuaTeX's `node.direct`
1964/// (most are also in `node`), LuaTeX manual §10.6.
1965fn install_accessors(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
1966    // Plain field accessors.
1967    for (name, key) in [
1968        ("data", "data"),
1969        ("lang", "lang"),
1970        ("fam", "fam"),
1971        ("sub", "sub"),
1972        ("sup", "sup"),
1973        ("nucleus", "nucleus"),
1974        ("shift", "shift"),
1975        ("dir", "dir"),
1976        ("direction", "direction"),
1977        ("components", "components"),
1978        ("expansion", "expansion_factor"),
1979    ] {
1980        set_fn(
1981            lua,
1982            t,
1983            &format!("get{name}"),
1984            move |lua, v: Value| match to_ref(&v) {
1985                Some(p) => get_field_impl(lua, p, key, direct),
1986                None => Ok(Value::Nil),
1987            },
1988        )?;
1989        set_fn(
1990            lua,
1991            t,
1992            &format!("set{name}"),
1993            move |lua, (v, x): (Value, Value)| match to_ref(&v) {
1994                Some(p) => set_field(lua, p, key, x),
1995                None => Ok(()),
1996            },
1997        )?;
1998    }
1999    if direct {
2000        // SyncTeX fields: those of glyphs, kerns, glue, math nodes, boxes
2001        // and rules (others have none: nil). Zeros are ignored when
2002        // setting (observed).
2003        fn has_sync(id: u8) -> bool {
2004            matches!(
2005                id,
2006                id::GLYPH | id::KERN | id::GLUE | id::MATH | id::HLIST | id::VLIST | id::RULE
2007            )
2008        }
2009        set_fn(lua, t, "get_synctex_fields", |lua, v: Value| {
2010            let Some(p) = to_ref(&v) else {
2011                return Ok(MultiValue::new());
2012            };
2013            let f = with_engine(lua, |e| {
2014                has_sync(e.nodes.id(p)).then(|| e.nodes.sync_fields(p))
2015            })?;
2016            Ok(match f {
2017                Some((tag, line)) => MultiValue::from_iter([
2018                    Value::Integer(i64::from(tag)),
2019                    Value::Integer(i64::from(line)),
2020                ]),
2021                None => MultiValue::new(),
2022            })
2023        })?;
2024        set_fn(
2025            lua,
2026            t,
2027            "set_synctex_fields",
2028            |lua, (v, tag, line): (Value, Option<i64>, Option<i64>)| {
2029                let Some(p) = to_ref(&v) else {
2030                    return Ok(());
2031                };
2032                with_engine(lua, |e| {
2033                    if !has_sync(e.nodes.id(p)) {
2034                        return;
2035                    }
2036                    let (mut t0, mut l0) = e.nodes.sync_fields(p);
2037                    if let Some(t) = tag.filter(|&t| t > 0) {
2038                        t0 = t as u32;
2039                    }
2040                    if let Some(l) = line.filter(|&l| l > 0) {
2041                        l0 = l as i32;
2042                    }
2043                    e.nodes.set_sync_fields(p, (t0, l0));
2044                })
2045            },
2046        )?;
2047    }
2048    set_fn(lua, t, "current_attr", |lua, ()| {
2049        match with_engine(lua, |e| e.cur_attr())? {
2050            Some(a) => Ok(Value::UserData(lua.create_userdata(LAttr(a))?)),
2051            None => Ok(Value::Nil),
2052        }
2053    })?;
2054    set_fn(lua, t, "getattributelist", |lua, v: Value| {
2055        match to_ref(&v) {
2056            Some(p) => attr_value(lua, p),
2057            None => Ok(Value::Nil),
2058        }
2059    })?;
2060    set_fn(
2061        lua,
2062        t,
2063        "setattributelist",
2064        |lua, (v, a): (Value, Value)| match to_ref(&v) {
2065            Some(p) => set_attr_value(lua, p, &a),
2066            None => Ok(()),
2067        },
2068    )?;
2069    set_fn(lua, t, "getboth", move |lua, v: Value| {
2070        let Some(p) = to_ref(&v) else {
2071            return Ok((Value::Nil, Value::Nil));
2072        };
2073        let (pv, nx) = with_engine(lua, |e| (e.nodes.get(p).prev, e.nodes.next(p)))?;
2074        Ok((wrap(lua, pv, direct)?, wrap(lua, nx, direct)?))
2075    })?;
2076    set_fn(
2077        lua,
2078        t,
2079        "setboth",
2080        |lua, (v, a, b): (Value, Value, Value)| {
2081            if let Some(p) = to_ref(&v) {
2082                let a = to_ref(&a).unwrap_or(NodeRef::NULL);
2083                let b = to_ref(&b).unwrap_or(NodeRef::NULL);
2084                with_engine(lua, |e| {
2085                    let n = e.nodes.get_mut(p);
2086                    n.prev = a;
2087                    n.next = b;
2088                })?;
2089            }
2090            Ok(())
2091        },
2092    )?;
2093    set_fn(lua, t, "setlink", move |lua, args: MultiValue| {
2094        let refs: Vec<NodeRef> = args.iter().filter_map(to_ref).collect();
2095        with_engine(lua, |e| {
2096            for w in refs.windows(2) {
2097                e.nodes.link(w[0], w[1]);
2098            }
2099        })?;
2100        match refs.first() {
2101            Some(p) => wrap(lua, *p, direct),
2102            None => Ok(Value::Nil),
2103        }
2104    })?;
2105    set_fn(lua, t, "setsplit", |lua, (a, b): (Value, Value)| {
2106        let (a, b) = (to_ref(&a), to_ref(&b));
2107        with_engine(lua, |e| {
2108            if let Some(a) = a {
2109                e.nodes.get_mut(a).next = NodeRef::NULL;
2110            }
2111            if let Some(b) = b {
2112                e.nodes.get_mut(b).prev = NodeRef::NULL;
2113            }
2114        })
2115    })?;
2116    set_fn(
2117        lua,
2118        t,
2119        "getwhd",
2120        move |lua, (v, ex): (Value, Option<bool>)| {
2121            let Some(p) = to_ref(&v) else {
2122                return Ok(MultiValue::new());
2123            };
2124            let mut out = Vec::new();
2125            for k in ["width", "height", "depth"] {
2126                out.push(get_field_impl(lua, p, k, direct)?);
2127            }
2128            if ex == Some(true) {
2129                out.push(get_field_impl(lua, p, "expansion_factor", direct)?);
2130            }
2131            Ok(MultiValue::from_iter(out))
2132        },
2133    )?;
2134    set_fn(
2135        lua,
2136        t,
2137        "setwhd",
2138        |lua, (v, w, h, d): (Value, Value, Value, Value)| {
2139            if let Some(p) = to_ref(&v) {
2140                for (k, x) in [("width", w), ("height", h), ("depth", d)] {
2141                    if !x.is_nil() {
2142                        set_field(lua, p, k, x)?;
2143                    }
2144                }
2145            }
2146            Ok(())
2147        },
2148    )?;
2149    set_fn(
2150        lua,
2151        t,
2152        "getdisc",
2153        move |lua, (v, tails): (Value, Option<bool>)| {
2154            let Some(p) = to_ref(&v) else {
2155                return Ok(MultiValue::new());
2156            };
2157            let lists = with_engine(lua, |e| match &e.nodes.get(p).data {
2158                NodeData::Disc {
2159                    pre, post, replace, ..
2160                } => {
2161                    let l = [*pre, *post, *replace];
2162                    let t = l.map(|h| if h.is_null() { h } else { e.nodes.tail_of(h) });
2163                    Some((l, t))
2164                }
2165                _ => None,
2166            })?;
2167            let Some((l, tl)) = lists else {
2168                return Ok(MultiValue::new());
2169            };
2170            let mut out = Vec::new();
2171            for h in l {
2172                out.push(wrap(lua, h, direct)?);
2173            }
2174            if tails == Some(true) {
2175                for h in tl {
2176                    out.push(wrap(lua, h, direct)?);
2177                }
2178            }
2179            Ok(MultiValue::from_iter(out))
2180        },
2181    )?;
2182    set_fn(lua, t, "setdisc", |lua, args: MultiValue| {
2183        let v: Vec<Value> = args.into_iter().collect();
2184        let Some(p) = v.first().and_then(to_ref) else {
2185            return Ok(());
2186        };
2187        let get = |i: usize| v.get(i).and_then(to_ref).unwrap_or(NodeRef::NULL);
2188        let (pre, post, rep) = (get(1), get(2), get(3));
2189        let sub = v.get(4).filter(|x| !x.is_nil()).map(int);
2190        let pen = v.get(5).filter(|x| !x.is_nil()).map(int);
2191        with_engine(lua, |e| {
2192            if e.nodes.id(p) != id::DISC {
2193                return;
2194            }
2195            e.nodes.set_disc_lists(p, (pre, post, rep));
2196            if let Some(s) = sub {
2197                e.nodes.get_mut(p).subtype = s as u16;
2198            }
2199            if let (Some(x), NodeData::Disc { penalty, .. }) = (pen, &mut e.nodes.get_mut(p).data) {
2200                *penalty = x as i32;
2201            }
2202        })
2203    })?;
2204    set_fn(lua, t, "getglue", |lua, v: Value| {
2205        let Some(p) = to_ref(&v) else {
2206            return Ok(MultiValue::new());
2207        };
2208        let vals: Vec<Value> = with_engine(lua, |e| match &e.nodes.get(p).data {
2209            NodeData::Glue { spec: g, .. } | NodeData::Math { glue: g, .. } => vec![
2210                Value::Integer(g.width.into()),
2211                Value::Integer(g.stretch.into()),
2212                Value::Integer(g.shrink.into()),
2213                Value::Integer(g.stretch_order.into()),
2214                Value::Integer(g.shrink_order.into()),
2215            ],
2216            // Observed order for boxes: set, order, sign.
2217            NodeData::Box(b) => vec![
2218                Value::Number(b.glue_set),
2219                Value::Integer(b.glue_order.into()),
2220                Value::Integer(b.glue_sign.into()),
2221            ],
2222            _ => Vec::new(),
2223        })?;
2224        Ok(MultiValue::from_iter(vals))
2225    })?;
2226    set_fn(lua, t, "setglue", |lua, args: MultiValue| {
2227        let v: Vec<Value> = args.into_iter().collect();
2228        let Some(p) = v.first().and_then(to_ref) else {
2229            return Ok(());
2230        };
2231        let n = |i: usize| v.get(i).map(int).unwrap_or(0);
2232        with_engine(lua, |e| {
2233            let g = match &mut e.nodes.get_mut(p).data {
2234                NodeData::Glue { spec, .. } => spec,
2235                NodeData::Math { glue, .. } => glue,
2236                _ => return,
2237            };
2238            g.width = n(1) as i32;
2239            g.stretch = n(2) as i32;
2240            g.shrink = n(3) as i32;
2241            g.stretch_order = n(4) as u8;
2242            g.shrink_order = n(5) as u8;
2243        })
2244    })?;
2245    set_fn(lua, t, "is_zero_glue", |lua, v: Value| {
2246        let Some(p) = to_ref(&v) else {
2247            return Ok(false);
2248        };
2249        with_engine(lua, |e| match &e.nodes.get(p).data {
2250            NodeData::Glue { spec: g, .. } | NodeData::Math { glue: g, .. } => {
2251                g.width == 0 && g.stretch == 0 && g.shrink == 0
2252            }
2253            _ => true,
2254        })
2255    })?;
2256    set_fn(lua, t, "getoffsets", |lua, v: Value| {
2257        let Some(p) = to_ref(&v) else {
2258            return Ok((Value::Nil, Value::Nil));
2259        };
2260        with_engine(lua, |e| match &e.nodes.get(p).data {
2261            NodeData::Glyph(g) => (
2262                Value::Integer(g.xoffset.into()),
2263                Value::Integer(g.yoffset.into()),
2264            ),
2265            _ => (Value::Nil, Value::Nil),
2266        })
2267    })?;
2268    set_fn(
2269        lua,
2270        t,
2271        "setoffsets",
2272        |lua, (v, x, y): (Value, Value, Value)| {
2273            if let Some(p) = to_ref(&v) {
2274                if !x.is_nil() {
2275                    set_field(lua, p, "xoffset", x)?;
2276                }
2277                if !y.is_nil() {
2278                    set_field(lua, p, "yoffset", y)?;
2279                }
2280            }
2281            Ok(())
2282        },
2283    )?;
2284    // is_char: the character of an unprocessed glyph (subtype below 256),
2285    // false for other glyphs or another font, nil for other nodes.
2286    set_fn(lua, t, "is_char", |lua, (v, f): (Value, Option<i64>)| {
2287        let Some(p) = to_ref(&v) else {
2288            return Ok(Value::Nil);
2289        };
2290        Ok(match with_engine(lua, |e| glyph_of(e, p))? {
2291            Some((c, font, sub)) if sub < 256 && f.is_none_or(|f| f == i64::from(font)) => {
2292                Value::Integer(c.into())
2293            }
2294            Some(_) => Value::Boolean(false),
2295            None => Value::Nil,
2296        })
2297    })?;
2298    set_fn(lua, t, "is_glyph", |lua, v: Value| {
2299        let Some(p) = to_ref(&v) else {
2300            return Ok(MultiValue::from_iter([Value::Boolean(false)]));
2301        };
2302        Ok(match with_engine(lua, |e| glyph_of(e, p))? {
2303            Some((c, font, _)) => {
2304                MultiValue::from_iter([Value::Integer(c.into()), Value::Integer(font.into())])
2305            }
2306            None => MultiValue::from_iter([Value::Boolean(false)]),
2307        })
2308    })?;
2309    set_fn(lua, t, "traverse_char", move |lua, v: Value| {
2310        let start = to_ref(&v).unwrap_or(NodeRef::NULL);
2311        list_iterator(lua, start, direct, |e, p| match glyph_of(e, p) {
2312            Some((c, f, s)) if s < 256 => {
2313                Some(vec![Value::Integer(c.into()), Value::Integer(f.into())])
2314            }
2315            _ => None,
2316        })
2317    })?;
2318    set_fn(lua, t, "traverse_glyph", move |lua, v: Value| {
2319        let start = to_ref(&v).unwrap_or(NodeRef::NULL);
2320        list_iterator(lua, start, direct, |e, p| {
2321            glyph_of(e, p).map(|(c, f, _)| vec![Value::Integer(c.into()), Value::Integer(f.into())])
2322        })
2323    })?;
2324    set_fn(lua, t, "traverse_list", move |lua, v: Value| {
2325        let start = to_ref(&v).unwrap_or(NodeRef::NULL);
2326        let (iter, s, c) = list_iterator(lua, start, direct, |e, p| {
2327            let n = e.nodes.get(p);
2328            match &n.data {
2329                NodeData::Box(b) if n.id == id::HLIST || n.id == id::VLIST => Some(vec![
2330                    Value::Integer(n.id.into()),
2331                    Value::Integer(n.subtype.into()),
2332                    // The list as a direct number, wrapped below.
2333                    Value::Integer(i64::from(b.head.0)),
2334                ]),
2335                _ => None,
2336            }
2337        })?;
2338        let wrapper = lua
2339            .load(
2340                "local iter, direct = ...
2341                 return function(s, c)
2342                   local n, i, st, l = iter(s, c)
2343                   if n == nil then return nil end
2344                   if l == 0 then l = nil elseif not direct then l = node.direct.tonode(l) end
2345                   return n, i, st, l
2346                 end",
2347            )
2348            .set_name("=[retex]")
2349            .call::<mlua::Function>((iter, direct))?;
2350        Ok((wrapper, s, c))
2351    })?;
2352    set_fn(lua, t, "has_glyph", move |lua, v: Value| {
2353        let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
2354        let found = with_engine(lua, |e| {
2355            while !p.is_null() {
2356                let i = e.nodes.id(p);
2357                if i == id::GLYPH || i == id::DISC {
2358                    return p;
2359                }
2360                p = e.nodes.next(p);
2361            }
2362            NodeRef::NULL
2363        })?;
2364        wrap(lua, found, direct)
2365    })?;
2366    set_fn(
2367        lua,
2368        t,
2369        "first_glyph",
2370        move |lua, (v, stop): (Value, Value)| {
2371            let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
2372            let stop = to_ref(&stop);
2373            // Observed: the first glyph whose subtype is 1 or 257 (a plain
2374            // character); `node.direct` gives 0 when there is none.
2375            let found = with_engine(lua, |e| {
2376                while !p.is_null() {
2377                    if glyph_of(e, p).is_some_and(|(_, _, s)| s & 0xFF == 1) {
2378                        return p;
2379                    }
2380                    if Some(p) == stop {
2381                        break;
2382                    }
2383                    p = e.nodes.next(p);
2384                }
2385                NodeRef::NULL
2386            })?;
2387            if direct && found.is_null() {
2388                return Ok(Value::Integer(0));
2389            }
2390            wrap(lua, found, direct)
2391        },
2392    )?;
2393    set_fn(lua, t, "get_attribute", |lua, (v, n): (Value, i64)| {
2394        let Some(p) = to_ref(&v) else {
2395            return Ok(None);
2396        };
2397        with_engine(lua, |e| {
2398            e.nodes.get(p).attr.as_ref().and_then(|l| {
2399                l.iter()
2400                    .find(|(k, _)| i64::from(*k) == n)
2401                    .map(|x| i64::from(x.1))
2402            })
2403        })
2404    })?;
2405    set_fn(
2406        lua,
2407        t,
2408        "find_attribute",
2409        move |lua, (v, n): (Value, i64)| {
2410            let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
2411            let found =
2412                with_engine(lua, |e| {
2413                    while !p.is_null() {
2414                        if let Some(x) =
2415                            e.nodes.get(p).attr.as_ref().and_then(|l| {
2416                                l.iter().find(|(k, _)| i64::from(*k) == n).map(|x| x.1)
2417                            })
2418                        {
2419                            return Some((x, p));
2420                        }
2421                        p = e.nodes.next(p);
2422                    }
2423                    None
2424                })?;
2425            match found {
2426                Some((x, p)) => Ok(MultiValue::from_iter([
2427                    Value::Integer(x.into()),
2428                    wrap(lua, p, direct)?,
2429                ])),
2430                None => Ok(MultiValue::new()),
2431            }
2432        },
2433    )?;
2434    set_fn(
2435        lua,
2436        t,
2437        "effective_glue",
2438        |lua, (g, parent): (Value, Value)| {
2439            let (Some(g), Some(b)) = (to_ref(&g), to_ref(&parent)) else {
2440                return Ok(Value::Nil);
2441            };
2442            with_engine(lua, |e| {
2443                let spec = match &e.nodes.get(g).data {
2444                    NodeData::Glue { spec, .. } => *spec,
2445                    _ => return Value::Nil,
2446                };
2447                let Some(bx) = e.nodes.box_data(b) else {
2448                    return Value::Number(spec.width.into());
2449                };
2450                let mut w = f64::from(spec.width);
2451                if bx.glue_sign == 1 && spec.stretch_order == bx.glue_order {
2452                    w += bx.glue_set * f64::from(spec.stretch);
2453                } else if bx.glue_sign == 2 && spec.shrink_order == bx.glue_order {
2454                    w -= bx.glue_set * f64::from(spec.shrink);
2455                }
2456                Value::Number(w.round())
2457            })
2458        },
2459    )?;
2460    for (name, protect) in [("protect_glyph", true), ("unprotect_glyph", false)] {
2461        set_fn(lua, t, name, move |lua, v: Value| {
2462            if let Some(p) = to_ref(&v) {
2463                with_engine(lua, |e| {
2464                    // Observed: protecting turns subtype 1 into 256 and
2465                    // adds 256 to subtypes up to 256; unprotecting takes
2466                    // 256 from subtypes above 256.
2467                    if e.nodes.id(p) == id::GLYPH {
2468                        let n = e.nodes.get_mut(p);
2469                        let s = n.subtype;
2470                        n.subtype = match (protect, s) {
2471                            (true, 1) => 256,
2472                            (true, s) if s <= 256 => s + 256,
2473                            (false, s) if s > 256 => s - 256,
2474                            (_, s) => s,
2475                        };
2476                    }
2477                })?;
2478            }
2479            Ok(())
2480        })?;
2481    }
2482    set_fn(lua, t, "uses_font", |lua, (v, f): (Value, i64)| {
2483        let Some(p) = to_ref(&v) else {
2484            return Ok(false);
2485        };
2486        with_engine(lua, |e| {
2487            let uses = |e: &Engine, mut q: NodeRef| {
2488                while !q.is_null() {
2489                    if glyph_of(e, q).is_some_and(|(_, font, _)| i64::from(font) == f) {
2490                        return true;
2491                    }
2492                    q = e.nodes.next(q);
2493                }
2494                false
2495            };
2496            match &e.nodes.get(p).data {
2497                NodeData::Glyph(g) => i64::from(g.font) == f,
2498                NodeData::Disc {
2499                    pre, post, replace, ..
2500                } => uses(e, *pre) || uses(e, *post) || uses(e, *replace),
2501                _ => false,
2502            }
2503        })
2504    })?;
2505    set_fn(lua, t, "getbox", move |lua, n: i64| {
2506        let p = with_engine(lua, |e| {
2507            match e.eqtb.get(retex_engine::eqtb::Loc::Box(n as u32)) {
2508                retex_engine::eqtb::Val::Node(p) => p,
2509                _ => NodeRef::NULL,
2510            }
2511        })?;
2512        wrap(lua, p, direct)
2513    })?;
2514    set_fn(lua, t, "setbox", |lua, args: MultiValue| {
2515        let mut v: Vec<Value> = args.into_iter().collect();
2516        let global = matches!(v.first(), Some(Value::String(s)) if &*s.to_str()? == "global");
2517        if global {
2518            v.remove(0);
2519        }
2520        let n = v.first().map(int).unwrap_or(0) as u32;
2521        let p = v.get(1).and_then(to_ref).unwrap_or(NodeRef::NULL);
2522        with_engine(lua, |e| {
2523            e.define(
2524                global,
2525                retex_engine::eqtb::Loc::Box(n),
2526                retex_engine::eqtb::Val::Node(p),
2527            )
2528        })
2529    })?;
2530    if direct {
2531        // Observed: the number itself for a direct node.
2532        set_fn(lua, t, "is_direct", |_, v: Value| {
2533            Ok(match v {
2534                Value::Integer(n) if n > 0 => Value::Integer(n),
2535                _ => Value::Boolean(false),
2536            })
2537        })?;
2538    } else {
2539        for (name, next) in [("next", true), ("prev", false)] {
2540            set_fn(lua, t, name, move |lua, v: Value| {
2541                let Some(p) = to_ref(&v) else {
2542                    return Ok(Value::Nil);
2543                };
2544                let q = with_engine(lua, |e| {
2545                    if next {
2546                        e.nodes.next(p)
2547                    } else {
2548                        e.nodes.get(p).prev
2549                    }
2550                })?;
2551                wrap(lua, q, false)
2552            })?;
2553        }
2554    }
2555    Ok(())
2556}
2557
2558/// Registry key of the table of Lua values kept by reference.
2559const LUA_REFS_KEY: &str = "retex.node.luarefs";
2560
2561fn lua_refs(lua: &Lua) -> mlua::Result<Table> {
2562    match lua.named_registry_value::<Value>(LUA_REFS_KEY)? {
2563        Value::Table(t) => Ok(t),
2564        _ => {
2565            let t = lua.create_table()?;
2566            lua.set_named_registry_value(LUA_REFS_KEY, &t)?;
2567            Ok(t)
2568        }
2569    }
2570}
2571
2572/// The table of Lua values kept by reference, with the values of the
2573/// references node copies made since the last call.
2574pub(crate) fn lua_refs_synced(lua: &Lua) -> mlua::Result<Table> {
2575    let refs = lua_refs(lua)?;
2576    let copies = with_engine(lua, |e| std::mem::take(&mut e.nodes.lua_refs.copies))?;
2577    for (new, old) in copies {
2578        let v: Value = refs.raw_get(old)?;
2579        refs.raw_set(new, v)?;
2580    }
2581    Ok(refs)
2582}
2583
2584/// Keeps a Lua value by reference; returns its number (see
2585/// [`retex_engine::nodes::LuaRefs`]; they only show in box displays).
2586pub(crate) fn lua_ref(lua: &Lua, v: Value) -> mlua::Result<i32> {
2587    let refs = lua_refs_synced(lua)?;
2588    let n = with_engine(lua, |e| e.nodes.lua_refs.take())?;
2589    refs.raw_set(n, v)?;
2590    Ok(n)
2591}
2592
2593/// A whatsit of subtype `sub` as `node.new` makes it.
2594fn new_whatsit(sub: u16) -> retex_engine::nodes::Whatsit {
2595    use retex_engine::nodes::{LateData, UserValue, Whatsit, whatsit_sub as ws};
2596    match sub {
2597        ws::SPECIAL => Whatsit::Special { data: Vec::new() },
2598        ws::LATE_LUA => Whatsit::LateLua {
2599            data: LateData::Bytes(Vec::new()),
2600            name: Vec::new(),
2601        },
2602        ws::PDF_LITERAL | ws::PDF_LATE_LITERAL => Whatsit::PdfLiteral {
2603            mode: 0,
2604            data: LateData::Bytes(Vec::new()),
2605            late: sub == ws::PDF_LATE_LITERAL,
2606        },
2607        ws::PDF_COLORSTACK => Whatsit::PdfColorstack {
2608            stack: 0,
2609            cmd: 0,
2610            data: Vec::new(),
2611        },
2612        ws::PDF_SETMATRIX => Whatsit::PdfSetmatrix { data: Vec::new() },
2613        ws::PDF_SAVE => Whatsit::PdfSave,
2614        ws::PDF_RESTORE => Whatsit::PdfRestore,
2615        ws::PDF_REFOBJ => Whatsit::PdfRefobj { objnum: 0 },
2616        ws::PDF_END_LINK => Whatsit::PdfEndLink,
2617        // Observed: new links and annotations have zero dimensions.
2618        ws::PDF_START_LINK => Whatsit::PdfStartLink {
2619            dims: [0; 3],
2620            objnum: 0,
2621            attr: Vec::new(),
2622            action: retex_engine::pdfext::PdfAction::User(Vec::new()),
2623        },
2624        ws::PDF_ANNOT => Whatsit::PdfAnnot {
2625            dims: [0; 3],
2626            objnum: 0,
2627            data: Vec::new(),
2628        },
2629        ws::SAVE_POS => Whatsit::SavePos,
2630        ws::PDF_LINK_STATE => Whatsit::PdfLinkState { state: 0 },
2631        ws::USER_DEFINED => Whatsit::UserDefined {
2632            user_id: 0,
2633            value: UserValue::Int(0),
2634        },
2635        _ => Whatsit::Other {
2636            fields: Vec::new(),
2637            data: Vec::new(),
2638        },
2639    }
2640}
2641
2642/// The index of `width`, `height` or `depth` in a whatsit's dimensions.
2643fn dim_index(key: &str) -> usize {
2644    match key {
2645        "width" => 0,
2646        "height" => 1,
2647        _ => 2,
2648    }
2649}
2650
2651/// Reads a whatsit field.
2652fn whatsit_field(e: &mut Engine, w: &retex_engine::nodes::Whatsit, key: &str) -> R {
2653    use retex_engine::nodes::{LateData, UserValue, Whatsit};
2654    let mut late = |d: &LateData| match d {
2655        LateData::Bytes(b) | LateData::Lua { data: b, .. } => R::S(b.clone()),
2656        LateData::Tokens(t) => {
2657            R::S(e.print_to_string(|e| e.show_token_list(t, 0, t.len(), i64::MAX)))
2658        }
2659        LateData::Function { reference } => R::L(*reference),
2660    };
2661    match (w, key) {
2662        (Whatsit::Special { data }, "data") => R::S(data.clone()),
2663        (Whatsit::LateLua { data, .. }, "data" | "string") => late(data),
2664        (Whatsit::LateLua { name, .. }, "name") => R::S(name.clone()),
2665        (Whatsit::PdfLiteral { mode, .. }, "mode") => R::I((*mode).into()),
2666        (Whatsit::PdfLiteral { data, .. }, "data") => late(data),
2667        (Whatsit::PdfColorstack { stack, .. }, "stack") => R::I((*stack).into()),
2668        (Whatsit::PdfColorstack { cmd, .. }, "command") => R::I((*cmd).into()),
2669        (Whatsit::PdfColorstack { data, .. }, "data") => R::S(data.clone()),
2670        (Whatsit::PdfSetmatrix { data }, "data") => R::S(data.clone()),
2671        (Whatsit::PdfRefobj { objnum }, "objnum") => R::I((*objnum).into()),
2672        // Observed: `link_attr` reads as a string; running dimensions
2673        // read as -2^30.
2674        (Whatsit::PdfStartLink { objnum, .. } | Whatsit::PdfAnnot { objnum, .. }, "objnum") => {
2675            R::I((*objnum).into())
2676        }
2677        (Whatsit::PdfStartLink { attr, .. }, "link_attr") => R::S(attr.clone()),
2678        (Whatsit::PdfAnnot { data, .. }, "data") => R::S(data.clone()),
2679        (
2680            Whatsit::PdfStartLink { dims, .. } | Whatsit::PdfAnnot { dims, .. },
2681            "width" | "height" | "depth",
2682        ) => R::I(dims[dim_index(key)].into()),
2683        // lualinksplit reads a link state as `objnum` with the subtype
2684        // switched to pdf_refobj (the field it shares in LuaTeX).
2685        (Whatsit::PdfLinkState { state }, "objnum") => R::I((*state).into()),
2686        (Whatsit::UserDefined { user_id, .. }, "user_id") => R::I((*user_id).into()),
2687        (Whatsit::UserDefined { value, .. }, "type") => R::I(i64::from(user_type(value))),
2688        (Whatsit::UserDefined { value, .. }, "value") => match value {
2689            UserValue::Int(n) => R::I((*n).into()),
2690            UserValue::Str(s) => R::S(s.clone()),
2691            UserValue::Tokens(t) => {
2692                R::S(e.print_to_string(|e| e.show_token_list(t, 0, t.len(), i64::MAX)))
2693            }
2694            UserValue::Nodes(p) => R::N(*p),
2695            UserValue::Lua(n) => R::L(*n),
2696            UserValue::Attr => R::Nil,
2697        },
2698        _ => R::Nil,
2699    }
2700}
2701
2702/// The type letter of a `user_defined` value.
2703fn user_type(v: &retex_engine::nodes::UserValue) -> u8 {
2704    use retex_engine::nodes::UserValue;
2705    match v {
2706        UserValue::Int(_) => b'd',
2707        UserValue::Str(_) => b's',
2708        UserValue::Tokens(_) => b't',
2709        UserValue::Nodes(_) => b'n',
2710        UserValue::Lua(_) => b'l',
2711        UserValue::Attr => b'a',
2712    }
2713}
2714
2715/// Writes a whatsit field.
2716fn set_whatsit_field(lua: &Lua, p: NodeRef, key: &str, v: Value) -> mlua::Result<()> {
2717    use retex_engine::nodes::{LateData, Whatsit};
2718    let s: Option<Vec<u8>> = match &v {
2719        Value::String(s) => Some(s.as_bytes().to_vec()),
2720        Value::Integer(n) => Some(n.to_string().into_bytes()),
2721        Value::Number(n) => Some(crate::texlib::lua_number_string(*n).into_bytes()),
2722        _ => None,
2723    };
2724    let node = to_ref(&v);
2725    let i = int(&v);
2726    // The type of a user_defined node decides how `value` is stored.
2727    let (is_user, utype) = with_engine(lua, |e| match &e.nodes.get(p).data {
2728        NodeData::Whatsit(w) => match &**w {
2729            Whatsit::UserDefined { value, .. } => (true, user_type(value)),
2730            _ => (false, 0),
2731        },
2732        _ => (false, 0),
2733    })?;
2734    let lref = if is_user && key == "value" && utype == b'l' {
2735        Some(lua_ref(lua, v.clone())?)
2736    } else {
2737        None
2738    };
2739    // Observed: outside IniTeX, a string set from Lua is kept as a Lua
2740    // reference, and box displays show the reference.
2741    let ini = with_engine(lua, |e| e.ini_version)?;
2742    let lua_string = |s: Vec<u8>| -> mlua::Result<LateData> {
2743        if ini {
2744            Ok(LateData::Bytes(s))
2745        } else {
2746            let reference = lua_ref(lua, Value::String(lua.create_string(&s)?))?;
2747            Ok(LateData::Lua { data: s, reference })
2748        }
2749    };
2750    let late = match (&s, key) {
2751        (Some(s), "data") => Some(lua_string(s.clone())?),
2752        // Observed: a function as `\latelua` data is kept by reference,
2753        // except in IniTeX, which keeps an empty string.
2754        (None, "data" | "string") if matches!(v, Value::Function(_)) && late_lua(lua, p)? => {
2755            Some(if ini {
2756                LateData::Bytes(Vec::new())
2757            } else {
2758                LateData::Function {
2759                    reference: lua_ref(lua, v.clone())?,
2760                }
2761            })
2762        }
2763        _ => None,
2764    };
2765    with_engine(lua, |e| {
2766        let tokens = |e: &Engine, s: &[u8]| -> retex_engine::token::TokenList {
2767            let _ = e;
2768            let toks: Vec<retex_engine::token::Token> = String::from_utf8_lossy(s)
2769                .chars()
2770                .map(|c| {
2771                    if c == ' ' {
2772                        retex_engine::token::Token::SPACE
2773                    } else {
2774                        retex_engine::token::Token::other(c as u32)
2775                    }
2776                })
2777                .collect();
2778            std::rc::Rc::from(toks)
2779        };
2780        let toks = s.as_ref().map(|s| tokens(e, s));
2781        // Observed: a new value takes its reference before the old one is
2782        // released.
2783        let old_ref = e.nodes.lua_ref_of(p);
2784        set_whatsit_data(e, p, key, &s, &late, &toks, node, i, lref);
2785        if let Some(r) = old_ref
2786            && e.nodes.lua_ref_of(p) != Some(r)
2787        {
2788            e.nodes.lua_refs.release(r);
2789        }
2790    })
2791}
2792
2793/// Whether `p` is a `late_lua` whatsit.
2794fn late_lua(lua: &Lua, p: NodeRef) -> mlua::Result<bool> {
2795    with_engine(lua, |e| match &e.nodes.get(p).data {
2796        NodeData::Whatsit(w) => matches!(&**w, retex_engine::nodes::Whatsit::LateLua { .. }),
2797        _ => false,
2798    })
2799}
2800
2801/// Stores a whatsit field (for [`set_whatsit_field`]).
2802#[allow(clippy::too_many_arguments)]
2803fn set_whatsit_data(
2804    e: &mut Engine,
2805    p: NodeRef,
2806    key: &str,
2807    s: &Option<Vec<u8>>,
2808    late: &Option<retex_engine::nodes::LateData>,
2809    toks: &Option<retex_engine::token::TokenList>,
2810    node: Option<NodeRef>,
2811    i: i64,
2812    lref: Option<i32>,
2813) {
2814    use retex_engine::nodes::{LateData, UserValue, Whatsit};
2815    {
2816        let NodeData::Whatsit(w) = &mut e.nodes.get_mut(p).data else {
2817            return;
2818        };
2819        match (&mut **w, key) {
2820            (Whatsit::Special { data }, "data") => {
2821                if let Some(s) = &s {
2822                    *data = s.clone();
2823                }
2824            }
2825            (Whatsit::LateLua { data, .. }, "data" | "string") => {
2826                if let Some(l) = late.clone().or_else(|| s.clone().map(LateData::Bytes)) {
2827                    *data = l;
2828                }
2829            }
2830            (Whatsit::LateLua { name, .. }, "name") => {
2831                if let Some(s) = &s {
2832                    *name = s.clone();
2833                }
2834            }
2835            (Whatsit::PdfLiteral { mode, .. }, "mode") => *mode = i as u8,
2836            (Whatsit::PdfLiteral { data, .. }, "data") => {
2837                if let Some(l) = late.clone() {
2838                    *data = l;
2839                }
2840            }
2841            (Whatsit::PdfColorstack { stack, .. }, "stack") => *stack = i as i32,
2842            (Whatsit::PdfColorstack { cmd, .. }, "command") => *cmd = i as u8,
2843            (Whatsit::PdfColorstack { data, .. }, "data")
2844            | (Whatsit::PdfSetmatrix { data }, "data") => {
2845                if let Some(s) = &s {
2846                    *data = s.clone();
2847                }
2848            }
2849            (Whatsit::PdfRefobj { objnum }, "objnum")
2850            | (Whatsit::PdfStartLink { objnum, .. }, "objnum")
2851            | (Whatsit::PdfAnnot { objnum, .. }, "objnum") => *objnum = i as i32,
2852            (Whatsit::PdfStartLink { attr: data, .. }, "link_attr")
2853            | (Whatsit::PdfAnnot { data, .. }, "data") => {
2854                if let Some(s) = &s {
2855                    *data = s.clone();
2856                }
2857            }
2858            (
2859                Whatsit::PdfStartLink { dims, .. } | Whatsit::PdfAnnot { dims, .. },
2860                "width" | "height" | "depth",
2861            ) => dims[dim_index(key)] = i as i32,
2862            (Whatsit::PdfLinkState { state }, "value" | "objnum") => *state = i as i32,
2863            (Whatsit::UserDefined { user_id, .. }, "user_id") => *user_id = i as i32,
2864            (Whatsit::UserDefined { value, .. }, "type") => {
2865                *value = match i as u8 {
2866                    b's' => UserValue::Str(Vec::new()),
2867                    b't' => UserValue::Tokens(retex_engine::token::empty_list()),
2868                    b'n' => UserValue::Nodes(NodeRef::NULL),
2869                    b'l' => UserValue::Lua(0),
2870                    b'a' => UserValue::Attr,
2871                    _ => UserValue::Int(0),
2872                }
2873            }
2874            (Whatsit::UserDefined { value, .. }, "value") => {
2875                *value = match user_type(value) {
2876                    b's' => UserValue::Str(s.clone().unwrap_or_default()),
2877                    b't' => UserValue::Tokens(
2878                        toks.clone().unwrap_or_else(retex_engine::token::empty_list),
2879                    ),
2880                    b'n' => UserValue::Nodes(node.unwrap_or(NodeRef::NULL)),
2881                    b'l' => UserValue::Lua(lref.unwrap_or(0)),
2882                    b'a' => UserValue::Attr,
2883                    _ => UserValue::Int(i as i32),
2884                }
2885            }
2886            _ => {}
2887        }
2888    }
2889}
2890
2891/// `hpack`, `vpack`, `write`, `last_node`, `end_of_math`,
2892/// `protect_glyphs` and `unprotect_glyphs` (LuaTeX manual §10.6).
2893fn install_list_ops(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
2894    use retex_engine::build::PackSpec;
2895    for (name, horizontal) in [("hpack", true), ("vpack", false)] {
2896        set_fn(lua, t, name, move |lua, args: MultiValue| {
2897            let v: Vec<Value> = args.into_iter().collect();
2898            let head = v.first().and_then(to_ref).unwrap_or(NodeRef::NULL);
2899            let w = v.get(1).map(int).unwrap_or(0) as i32;
2900            let exactly = matches!(v.get(2), Some(Value::String(s)) if &*s.to_str()? == "exactly");
2901            let spec = if exactly {
2902                PackSpec::Exactly(w)
2903            } else {
2904                PackSpec::Additional(w)
2905            };
2906            let dir = match v.get(3) {
2907                Some(Value::String(s)) => match &*s.to_str()? {
2908                    "TLT" => 0,
2909                    "TRT" => 1,
2910                    "LTL" => 2,
2911                    "RTT" => 3,
2912                    _ => -1,
2913                },
2914                Some(Value::Integer(n)) => *n as i8,
2915                _ => -1,
2916            };
2917            let (b, badness) = with_engine(lua, |e| {
2918                e.pack_dir = dir;
2919                let b = if horizontal {
2920                    e.hpack(head, spec)
2921                } else {
2922                    e.vpack(head, spec)
2923                };
2924                e.pack_dir = -1;
2925                (b, e.last_badness)
2926            })?;
2927            Ok((wrap(lua, b, direct)?, Value::Integer(badness.into())))
2928        })?;
2929    }
2930    // Appends a list to the current list.
2931    set_fn(lua, t, "write", |lua, v: Value| {
2932        if let Some(p) = to_ref(&v) {
2933            with_engine(lua, |e| {
2934                let tail = tail(e, p);
2935                e.tail_append(p);
2936                e.cur_list_mut().tail = tail;
2937            })?;
2938        }
2939        Ok(())
2940    })?;
2941    // Removes the last node of the current list and returns it.
2942    set_fn(lua, t, "last_node", move |lua, ()| {
2943        let p = with_engine(lua, |e| {
2944            let l = e.cur_list();
2945            let (head, tail) = (l.head, l.tail);
2946            if tail == head {
2947                return NodeRef::NULL;
2948            }
2949            let mut q = head;
2950            while e.nodes.next(q) != tail {
2951                q = e.nodes.next(q);
2952            }
2953            e.nodes.get_mut(q).next = NodeRef::NULL;
2954            let n = e.nodes.get_mut(tail);
2955            n.prev = NodeRef::NULL;
2956            e.cur_list_mut().tail = q;
2957            tail
2958        })?;
2959        wrap(lua, p, direct)
2960    })?;
2961    // The math node that ends the formula starting at a begin math node.
2962    set_fn(lua, t, "end_of_math", move |lua, v: Value| {
2963        let Some(mut p) = to_ref(&v) else {
2964            return Ok(Value::Nil);
2965        };
2966        let found = with_engine(lua, |e| {
2967            if e.nodes.id(p) == id::MATH && e.nodes.subtype(p) == 0 {
2968                p = e.nodes.next(p);
2969            }
2970            while !p.is_null() {
2971                if e.nodes.id(p) == id::MATH && e.nodes.subtype(p) == 1 {
2972                    return p;
2973                }
2974                p = e.nodes.next(p);
2975            }
2976            NodeRef::NULL
2977        })?;
2978        wrap(lua, found, direct)
2979    })?;
2980    for (name, protect) in [("protect_glyphs", true), ("unprotect_glyphs", false)] {
2981        set_fn(lua, t, name, move |lua, (h, stop): (Value, Value)| {
2982            let mut p = to_ref(&h).unwrap_or(NodeRef::NULL);
2983            let stop = to_ref(&stop);
2984            with_engine(lua, |e| {
2985                while !p.is_null() {
2986                    if e.nodes.id(p) == id::GLYPH {
2987                        let n = e.nodes.get_mut(p);
2988                        let s = n.subtype;
2989                        n.subtype = match (protect, s) {
2990                            (true, 1) => 256,
2991                            (true, s) if s <= 256 => s + 256,
2992                            (false, s) if s > 256 => s - 256,
2993                            (_, s) => s,
2994                        };
2995                    }
2996                    if Some(p) == stop {
2997                        break;
2998                    }
2999                    p = e.nodes.next(p);
3000                }
3001            })
3002        })?;
3003    }
3004    Ok(())
3005}