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        "prepend_prevdepth",
1547        // The box with the interline glue TeX would put before it after
1548        // depth `pd`, and the box's depth (as LuaTeX: for an
1549        // append_to_vlist_filter callback).
1550        lua.create_function(move |lua, (n, pd): (Value, Option<i64>)| {
1551            let Some(n) = to_ref(&n) else {
1552                return Ok(MultiValue::from_iter([Value::Nil]));
1553            };
1554            let pd = pd.unwrap_or(i64::from(retex_engine::maincontrol::IGNORE_DEPTH)) as i32;
1555            let (head, depth) = with_engine(lua, |e| {
1556                let depth = e.node_depth(n);
1557                match e.interline_glue(n, pd) {
1558                    Some(g) => {
1559                        e.nodes.link(g, n);
1560                        (g, depth)
1561                    }
1562                    None => (n, depth),
1563                }
1564            })?;
1565            Ok(MultiValue::from_iter([
1566                wrap(lua, head, direct)?,
1567                Value::Integer(i64::from(depth)),
1568            ]))
1569        })?,
1570    )?;
1571    t.set(
1572        "remove",
1573        lua.create_function(move |lua, (h, c): (Value, Value)| {
1574            // Observed: without a node to remove, the head comes back.
1575            let (Some(h), Some(c)) = (to_ref(&h), to_ref(&c)) else {
1576                return Ok(MultiValue::from_iter([h, Value::Nil]));
1577            };
1578            let (head, next) = with_engine(lua, |e| {
1579                let pv = e.nodes.get(c).prev;
1580                let nx = e.nodes.next(c);
1581                let head = if c == h {
1582                    nx
1583                } else {
1584                    e.nodes.link(pv, nx);
1585                    h
1586                };
1587                if !nx.is_null() && c == h {
1588                    e.nodes.get_mut(nx).prev = NodeRef::NULL;
1589                }
1590                // Observed: the removed node keeps its links.
1591                (head, nx)
1592            })?;
1593            Ok(MultiValue::from_iter([
1594                wrap(lua, head, direct)?,
1595                wrap(lua, next, direct)?,
1596            ]))
1597        })?,
1598    )?;
1599    t.set(
1600        "has_field",
1601        lua.create_function(|lua, (v, f): (Value, String)| match to_ref(&v) {
1602            Some(p) => {
1603                let t = with_engine(lua, |e| e.nodes.id(p))?;
1604                Ok(info()
1605                    .fields
1606                    .get(&t)
1607                    .is_some_and(|fl| fl.iter().any(|(_, n)| *n == f)))
1608            }
1609            None => Ok(false),
1610        })?,
1611    )?;
1612    t.set(
1613        "getfield",
1614        lua.create_function(move |lua, (v, f): (Value, String)| match to_ref(&v) {
1615            Some(p) => get_field_impl(lua, p, &f, direct),
1616            None => Ok(Value::Nil),
1617        })?,
1618    )?;
1619    t.set(
1620        "setfield",
1621        lua.create_function(|lua, (v, f, x): (Value, String, Value)| match to_ref(&v) {
1622            Some(p) => set_field(lua, p, &f, x),
1623            None => Ok(()),
1624        })?,
1625    )?;
1626    // Direct-style accessors (also accepted by node.* in LuaTeX).
1627    for name in [
1628        "id", "subtype", "next", "prev", "char", "font", "width", "height", "depth", "list",
1629        "penalty", "kern", "leader",
1630    ] {
1631        let key = if name == "list" { "head" } else { name };
1632        if name != "id" && name != "subtype" {
1633            t.set(
1634                format!("get{name}"),
1635                lua.create_function(move |lua, v: Value| match to_ref(&v) {
1636                    Some(p) => get_field_impl(lua, p, key, direct),
1637                    None => Ok(Value::Nil),
1638                })?,
1639            )?;
1640        } else {
1641            t.set(
1642                format!("get{name}"),
1643                lua.create_function(move |lua, v: Value| match to_ref(&v) {
1644                    Some(p) => get_field_impl(lua, p, key, direct),
1645                    None => Ok(Value::Nil),
1646                })?,
1647            )?;
1648        }
1649        t.set(
1650            format!("set{name}"),
1651            lua.create_function(move |lua, (v, x): (Value, Value)| match to_ref(&v) {
1652                Some(p) => set_field(lua, p, key, x),
1653                None => Ok(()),
1654            })?,
1655        )?;
1656    }
1657    install_accessors(lua, t, direct)?;
1658    // Engine operations that ltluatex installs as callback defaults; the
1659    // real implementations come with the typesetting code.
1660    // node.mlist_to_hlist(head, style, penalties): the engine's conversion.
1661    t.set(
1662        "mlist_to_hlist",
1663        lua.create_function(move |lua, args: MultiValue| {
1664            let v: Vec<Value> = args.into_iter().collect();
1665            let head = v.first().and_then(to_ref).unwrap_or(NodeRef::NULL);
1666            // Observed: nodes that cannot be in a math list are LuaTeX's
1667            // "This can't happen (mlist1)", a boundary "(mlist3)".
1668            with_engine(lua, |e| {
1669                let mut p = head;
1670                let mut steps = e.nodes.capacity();
1671                while !p.is_null() && steps > 0 {
1672                    steps -= 1;
1673                    match e.nodes.id(p) {
1674                        id::GLYPH
1675                        | id::MATH
1676                        | id::MARGIN_KERN
1677                        | id::LOCAL_PAR
1678                        | id::DIR
1679                        | id::SUB_BOX
1680                        | id::UNSET
1681                        | id::GLUE_SPEC
1682                        | id::HLIST
1683                        | id::VLIST
1684                        | id::TEMP
1685                        | id::DELIM
1686                        | id::MATH_CHAR
1687                        | id::SUB_MLIST
1688                        | id::MATH_TEXT_CHAR
1689                        | 38 => e.confusion("mlist1"),
1690                        id::BOUNDARY => e.confusion("mlist3"),
1691                        _ => {}
1692                    }
1693                    p = e.nodes.next(p);
1694                }
1695            })?;
1696            let style = match v.get(1) {
1697                Some(Value::String(s)) => {
1698                    let s = s.to_str()?.to_string();
1699                    crate::texlib::MATH_STYLE_NAMES
1700                        .iter()
1701                        .position(|n| *n == s)
1702                        .unwrap_or(2) as i32
1703                }
1704                Some(Value::Integer(n)) => (*n as i32).clamp(0, 7),
1705                _ => 2,
1706            };
1707            let pen = matches!(v.get(2), Some(Value::Boolean(true)));
1708            if head.is_null() {
1709                return Ok(Value::Nil);
1710            }
1711            let r = with_engine(lua, |e| e.mlist_to_hlist(head, style, pen))?;
1712            if r.is_null() {
1713                return Ok(Value::Nil);
1714            }
1715            wrap(lua, r, direct)
1716        })?,
1717    )?;
1718    // rangedimensions(parent, first [, last]): the natural size of the
1719    // list from first up to last (exclusive).
1720    t.set(
1721        "rangedimensions",
1722        lua.create_function(|lua, args: MultiValue| {
1723            let first = args.get(1).and_then(to_ref).unwrap_or(NodeRef::NULL);
1724            let stop = args.get(2).and_then(to_ref).unwrap_or(NodeRef::NULL);
1725            if first.is_null() {
1726                return Ok((0, 0, 0));
1727            }
1728            with_engine(lua, |e| e.natural_dimensions(first, stop))
1729        })?,
1730    )?;
1731    // ligaturing(head) and kerning(head): the built-in passes; they give
1732    // the head, the tail and success.
1733    for (name, pass) in [
1734        ("ligaturing", retex_engine::nodeproc::NodePass::Ligaturing),
1735        ("kerning", retex_engine::nodeproc::NodePass::Kerning),
1736    ] {
1737        t.set(
1738            name,
1739            lua.create_function(move |lua, args: MultiValue| {
1740                let Some(h) = args.front().and_then(to_ref) else {
1741                    return Ok(MultiValue::from_iter([Value::Nil]));
1742                };
1743                let (h, tl) = with_engine(lua, |e| e.lua_node_pass(pass, h))?;
1744                Ok(MultiValue::from_iter([
1745                    wrap(lua, h, direct)?,
1746                    wrap(lua, tl, direct)?,
1747                    Value::Boolean(true),
1748                ]))
1749            })?,
1750        )?;
1751    }
1752    for name in ["hyphenating", "flatten_discretionaries"] {
1753        t.set(
1754            name,
1755            lua.create_function(move |_, args: MultiValue| Ok(args))?,
1756        )?;
1757    }
1758    install_list_ops(lua, t, direct)?;
1759    // The font of a math family in text size.
1760    t.set(
1761        "family_font",
1762        lua.create_function(|lua, fam: i64| {
1763            if !(0..retex_engine::eqtb::NUM_FAMILIES as i64).contains(&fam) {
1764                return Ok(0);
1765            }
1766            with_engine(lua, |e| i64::from(e.fam_fnt(fam as u32, 0)))
1767        })?,
1768    )?;
1769    t.set(
1770        "dimensions",
1771        lua.create_function(|lua, args: MultiValue| {
1772            // dimensions([glue_set, glue_sign, glue_order,] n [, t]): the
1773            // size of the list from n up to t (exclusive), its glue set
1774            // as given. Direct nodes are numbers too, so four or more
1775            // arguments start with the glue settings.
1776            let args: Vec<Value> = args.into_iter().collect();
1777            let (set, rest) = if args.len() >= 4 {
1778                let num = |v: &Value| match v {
1779                    Value::Integer(i) => *i as f64,
1780                    Value::Number(f) => *f,
1781                    _ => 0.0,
1782                };
1783                (
1784                    Some((num(&args[0]), num(&args[1]) as i32, num(&args[2]) as i32)),
1785                    &args[3..],
1786                )
1787            } else {
1788                (None, &args[..])
1789            };
1790            let refs: Vec<NodeRef> = rest.iter().filter_map(to_ref).collect();
1791            let Some(&first) = refs.first() else {
1792                return Ok((0, 0, 0));
1793            };
1794            let stop = refs.get(1).copied().unwrap_or(NodeRef::NULL);
1795            with_engine(lua, |e| match set {
1796                Some((gs, sign, order)) => e.set_dimensions(first, stop, gs, sign, order),
1797                None => e.natural_dimensions(first, stop),
1798            })
1799        })?,
1800    )?;
1801    t.set(
1802        "has_attribute",
1803        lua.create_function(|lua, (v, n, val): (Value, i64, Option<i64>)| {
1804            let Some(p) = to_ref(&v) else {
1805                return Ok(None);
1806            };
1807            let a = with_engine(lua, |e| {
1808                e.nodes
1809                    .get(p)
1810                    .attr
1811                    .as_ref()
1812                    .and_then(|l| l.iter().find(|(k, _)| i64::from(*k) == n).map(|x| x.1))
1813            })?;
1814            Ok(match (a, val) {
1815                (Some(x), Some(w)) if i64::from(x) != w => None,
1816                (a, _) => a.map(i64::from),
1817            })
1818        })?,
1819    )?;
1820    t.set(
1821        "set_attribute",
1822        lua.create_function(|lua, (v, n, val): (Value, Value, Value)| {
1823            // Observed: both numbers are read as `lua_tointeger` does,
1824            // anything else being 0 (tagpdf passes nil).
1825            let (n, val) = (to_integer(&n), to_integer(&val));
1826            if let Some(p) = to_ref(&v) {
1827                with_engine(lua, |e| set_attr(e, p, n as u32, Some(val as i32)))?;
1828            }
1829            Ok(())
1830        })?,
1831    )?;
1832    t.set(
1833        "unset_attribute",
1834        lua.create_function(|lua, (v, n): (Value, i64)| {
1835            if let Some(p) = to_ref(&v) {
1836                with_engine(lua, |e| set_attr(e, p, n as u32, None))?;
1837            }
1838            Ok(())
1839        })?,
1840    )?;
1841    Ok(())
1842}
1843
1844/// A value as `lua_tointeger` converts it: integers, floats with an
1845/// integral value and strings holding one; 0 otherwise.
1846fn to_integer(v: &Value) -> i64 {
1847    let float = |f: f64| {
1848        if f.fract() == 0.0 && f >= i64::MIN as f64 && f < i64::MAX as f64 {
1849            f as i64
1850        } else {
1851            0
1852        }
1853    };
1854    match v {
1855        Value::Integer(n) => *n,
1856        Value::Number(f) => float(*f),
1857        Value::String(s) => match s.to_str() {
1858            Ok(t) => {
1859                let t = t.trim();
1860                t.parse::<i64>()
1861                    .ok()
1862                    .or_else(|| t.parse::<f64>().ok().map(float))
1863                    .unwrap_or(0)
1864            }
1865            Err(_) => 0,
1866        },
1867        _ => 0,
1868    }
1869}
1870
1871fn set_attr(e: &mut Engine, p: NodeRef, n: u32, v: Option<i32>) {
1872    let node = e.nodes.get_mut(p);
1873    let mut list: Vec<(u32, i32)> = node.attr.as_ref().map(|l| l.to_vec()).unwrap_or_default();
1874    list.retain(|(k, _)| *k != n);
1875    if let Some(v) = v {
1876        list.push((n, v));
1877        list.sort();
1878    }
1879    node.attr = if list.is_empty() {
1880        None
1881    } else {
1882        Some(std::rc::Rc::from(list))
1883    };
1884}
1885
1886/// LuaTeX's fatal error for a direct node number that names no node
1887/// (negative, or beyond the node memory).
1888fn impossible(lua: &Lua, v: &Value, what: &str) -> mlua::Result<()> {
1889    // Observed: freeing the temporary head that the node processing
1890    // callbacks get for a paragraph is "attempt to free an impossible
1891    // node" too (in a box, it is freed twice later).
1892    if what == "free"
1893        && let Some(p) = to_ref(v)
1894        && with_engine(lua, |e| p == e.nodes.par_temp)?
1895    {
1896        let msg = format!("attempt to free an impossible node {}", p.0);
1897        return match with_engine(lua, |e| e.luatex_fatal("nodes", &msg)) {
1898            Err(e) => Err(e),
1899        };
1900    }
1901    if let Value::Integer(n) = *v {
1902        let cap = with_engine(lua, |e| e.nodes.capacity())?;
1903        if n < 0 || n as u64 >= cap as u64 {
1904            let msg = format!("attempt to {what} an impossible node {n}");
1905            return match with_engine(lua, |e| e.luatex_fatal("nodes", &msg)) {
1906                Err(e) => Err(e),
1907            };
1908        }
1909    }
1910    Ok(())
1911}
1912
1913/// Ends the run for a list that Lua code linked into a loop: walking it
1914/// would never end (LuaTeX hangs).
1915fn circular(e: &mut Engine) -> ! {
1916    e.luatex_fatal("nodes", "circular node list")
1917}
1918
1919/// The last node of the list `p` (`node.tail`).
1920fn tail(e: &mut Engine, p: NodeRef) -> NodeRef {
1921    match e.nodes.bounded_tail(p) {
1922        Some(t) => t,
1923        None => circular(e),
1924    }
1925}
1926
1927fn slide(e: &mut Engine, mut p: NodeRef) -> NodeRef {
1928    for _ in 0..e.nodes.capacity() {
1929        let n = e.nodes.next(p);
1930        if n.is_null() {
1931            return p;
1932        }
1933        e.nodes.get_mut(n).prev = p;
1934        p = n;
1935    }
1936    circular(e)
1937}
1938
1939fn count(e: &mut Engine, mut p: NodeRef, ty: Option<u8>) -> i64 {
1940    let mut n = 0;
1941    let mut left = e.nodes.capacity();
1942    while !p.is_null() {
1943        if left == 0 {
1944            circular(e);
1945        }
1946        left -= 1;
1947        if ty.is_none_or(|t| e.nodes.id(p) == t) {
1948            n += 1;
1949        }
1950        p = e.nodes.next(p);
1951    }
1952    n
1953}
1954
1955/// Creates `name` in `t`.
1956fn set_fn<A, R, F>(lua: &Lua, t: &Table, name: &str, f: F) -> mlua::Result<()>
1957where
1958    A: mlua::FromLuaMulti,
1959    R: mlua::IntoLuaMulti,
1960    F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
1961{
1962    t.set(name, lua.create_function(f)?)
1963}
1964
1965/// The glyph's character, font and subtype, if `p` is a glyph.
1966fn glyph_of(e: &Engine, p: NodeRef) -> Option<(u32, u32, u16)> {
1967    match &e.nodes.get(p).data {
1968        NodeData::Glyph(g) => Some((g.chr, g.font, e.nodes.get(p).subtype)),
1969        _ => None,
1970    }
1971}
1972
1973/// What a list iterator returns besides the node.
1974type Pick = fn(&Engine, NodeRef) -> Option<Vec<Value>>;
1975
1976/// An iterator over the list from `start` returning, for each node that
1977/// `pick` accepts, the node and the values `pick` gives.
1978fn list_iterator(
1979    lua: &Lua,
1980    start: NodeRef,
1981    direct: bool,
1982    pick: Pick,
1983) -> mlua::Result<(mlua::Function, Value, Value)> {
1984    let iter = lua.create_function(move |lua, (_s, cur): (Value, Value)| {
1985        let mut p = match to_ref(&cur) {
1986            Some(c) => with_engine(lua, |e| e.nodes.next(c))?,
1987            None => start,
1988        };
1989        let found = with_engine(lua, |e| {
1990            while !p.is_null() {
1991                if let Some(v) = pick(e, p) {
1992                    return Some((p, v));
1993                }
1994                p = e.nodes.next(p);
1995            }
1996            None
1997        })?;
1998        let Some((p, vals)) = found else {
1999            return Ok(MultiValue::new());
2000        };
2001        let mut out = vec![wrap(lua, p, direct)?];
2002        out.extend(vals);
2003        Ok(MultiValue::from_iter(out))
2004    })?;
2005    Ok((iter, Value::Nil, Value::Nil))
2006}
2007
2008/// The `get…`/`set…` accessors and helpers of LuaTeX's `node.direct`
2009/// (most are also in `node`), LuaTeX manual §10.6.
2010fn install_accessors(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
2011    // Plain field accessors.
2012    for (name, key) in [
2013        ("data", "data"),
2014        ("lang", "lang"),
2015        ("fam", "fam"),
2016        ("sub", "sub"),
2017        ("sup", "sup"),
2018        ("nucleus", "nucleus"),
2019        ("shift", "shift"),
2020        ("dir", "dir"),
2021        ("direction", "direction"),
2022        ("components", "components"),
2023        ("expansion", "expansion_factor"),
2024    ] {
2025        set_fn(
2026            lua,
2027            t,
2028            &format!("get{name}"),
2029            move |lua, v: Value| match to_ref(&v) {
2030                Some(p) => get_field_impl(lua, p, key, direct),
2031                None => Ok(Value::Nil),
2032            },
2033        )?;
2034        set_fn(
2035            lua,
2036            t,
2037            &format!("set{name}"),
2038            move |lua, (v, x): (Value, Value)| match to_ref(&v) {
2039                Some(p) => set_field(lua, p, key, x),
2040                None => Ok(()),
2041            },
2042        )?;
2043    }
2044    if direct {
2045        // SyncTeX fields: those of glyphs, kerns, glue, math nodes, boxes
2046        // and rules (others have none: nil). Zeros are ignored when
2047        // setting (observed).
2048        fn has_sync(id: u8) -> bool {
2049            matches!(
2050                id,
2051                id::GLYPH | id::KERN | id::GLUE | id::MATH | id::HLIST | id::VLIST | id::RULE
2052            )
2053        }
2054        set_fn(lua, t, "get_synctex_fields", |lua, v: Value| {
2055            let Some(p) = to_ref(&v) else {
2056                return Ok(MultiValue::new());
2057            };
2058            let f = with_engine(lua, |e| {
2059                has_sync(e.nodes.id(p)).then(|| e.nodes.sync_fields(p))
2060            })?;
2061            Ok(match f {
2062                Some((tag, line)) => MultiValue::from_iter([
2063                    Value::Integer(i64::from(tag)),
2064                    Value::Integer(i64::from(line)),
2065                ]),
2066                None => MultiValue::new(),
2067            })
2068        })?;
2069        set_fn(
2070            lua,
2071            t,
2072            "set_synctex_fields",
2073            |lua, (v, tag, line): (Value, Option<i64>, Option<i64>)| {
2074                let Some(p) = to_ref(&v) else {
2075                    return Ok(());
2076                };
2077                with_engine(lua, |e| {
2078                    if !has_sync(e.nodes.id(p)) {
2079                        return;
2080                    }
2081                    let (mut t0, mut l0) = e.nodes.sync_fields(p);
2082                    if let Some(t) = tag.filter(|&t| t > 0) {
2083                        t0 = t as u32;
2084                    }
2085                    if let Some(l) = line.filter(|&l| l > 0) {
2086                        l0 = l as i32;
2087                    }
2088                    e.nodes.set_sync_fields(p, (t0, l0));
2089                })
2090            },
2091        )?;
2092    }
2093    set_fn(lua, t, "current_attr", |lua, ()| {
2094        match with_engine(lua, |e| e.cur_attr())? {
2095            Some(a) => Ok(Value::UserData(lua.create_userdata(LAttr(a))?)),
2096            None => Ok(Value::Nil),
2097        }
2098    })?;
2099    set_fn(lua, t, "getattributelist", |lua, v: Value| {
2100        match to_ref(&v) {
2101            Some(p) => attr_value(lua, p),
2102            None => Ok(Value::Nil),
2103        }
2104    })?;
2105    set_fn(
2106        lua,
2107        t,
2108        "setattributelist",
2109        |lua, (v, a): (Value, Value)| match to_ref(&v) {
2110            Some(p) => set_attr_value(lua, p, &a),
2111            None => Ok(()),
2112        },
2113    )?;
2114    set_fn(lua, t, "getboth", move |lua, v: Value| {
2115        let Some(p) = to_ref(&v) else {
2116            return Ok((Value::Nil, Value::Nil));
2117        };
2118        let (pv, nx) = with_engine(lua, |e| (e.nodes.get(p).prev, e.nodes.next(p)))?;
2119        Ok((wrap(lua, pv, direct)?, wrap(lua, nx, direct)?))
2120    })?;
2121    set_fn(
2122        lua,
2123        t,
2124        "setboth",
2125        |lua, (v, a, b): (Value, Value, Value)| {
2126            if let Some(p) = to_ref(&v) {
2127                let a = to_ref(&a).unwrap_or(NodeRef::NULL);
2128                let b = to_ref(&b).unwrap_or(NodeRef::NULL);
2129                with_engine(lua, |e| {
2130                    let n = e.nodes.get_mut(p);
2131                    n.prev = a;
2132                    n.next = b;
2133                })?;
2134            }
2135            Ok(())
2136        },
2137    )?;
2138    set_fn(lua, t, "setlink", move |lua, args: MultiValue| {
2139        let refs: Vec<NodeRef> = args.iter().filter_map(to_ref).collect();
2140        with_engine(lua, |e| {
2141            for w in refs.windows(2) {
2142                e.nodes.link(w[0], w[1]);
2143            }
2144        })?;
2145        match refs.first() {
2146            Some(p) => wrap(lua, *p, direct),
2147            None => Ok(Value::Nil),
2148        }
2149    })?;
2150    set_fn(lua, t, "setsplit", |lua, (a, b): (Value, Value)| {
2151        let (a, b) = (to_ref(&a), to_ref(&b));
2152        with_engine(lua, |e| {
2153            if let Some(a) = a {
2154                e.nodes.get_mut(a).next = NodeRef::NULL;
2155            }
2156            if let Some(b) = b {
2157                e.nodes.get_mut(b).prev = NodeRef::NULL;
2158            }
2159        })
2160    })?;
2161    set_fn(
2162        lua,
2163        t,
2164        "getwhd",
2165        move |lua, (v, ex): (Value, Option<bool>)| {
2166            let Some(p) = to_ref(&v) else {
2167                return Ok(MultiValue::new());
2168            };
2169            let mut out = Vec::new();
2170            for k in ["width", "height", "depth"] {
2171                out.push(get_field_impl(lua, p, k, direct)?);
2172            }
2173            if ex == Some(true) {
2174                out.push(get_field_impl(lua, p, "expansion_factor", direct)?);
2175            }
2176            Ok(MultiValue::from_iter(out))
2177        },
2178    )?;
2179    set_fn(
2180        lua,
2181        t,
2182        "setwhd",
2183        |lua, (v, w, h, d): (Value, Value, Value, Value)| {
2184            if let Some(p) = to_ref(&v) {
2185                for (k, x) in [("width", w), ("height", h), ("depth", d)] {
2186                    if !x.is_nil() {
2187                        set_field(lua, p, k, x)?;
2188                    }
2189                }
2190            }
2191            Ok(())
2192        },
2193    )?;
2194    set_fn(
2195        lua,
2196        t,
2197        "getdisc",
2198        move |lua, (v, tails): (Value, Option<bool>)| {
2199            let Some(p) = to_ref(&v) else {
2200                return Ok(MultiValue::new());
2201            };
2202            let lists = with_engine(lua, |e| match &e.nodes.get(p).data {
2203                NodeData::Disc {
2204                    pre, post, replace, ..
2205                } => {
2206                    let l = [*pre, *post, *replace];
2207                    let t = l.map(|h| if h.is_null() { h } else { e.nodes.tail_of(h) });
2208                    Some((l, t))
2209                }
2210                _ => None,
2211            })?;
2212            let Some((l, tl)) = lists else {
2213                return Ok(MultiValue::new());
2214            };
2215            let mut out = Vec::new();
2216            for h in l {
2217                out.push(wrap(lua, h, direct)?);
2218            }
2219            if tails == Some(true) {
2220                for h in tl {
2221                    out.push(wrap(lua, h, direct)?);
2222                }
2223            }
2224            Ok(MultiValue::from_iter(out))
2225        },
2226    )?;
2227    set_fn(lua, t, "setdisc", |lua, args: MultiValue| {
2228        let v: Vec<Value> = args.into_iter().collect();
2229        let Some(p) = v.first().and_then(to_ref) else {
2230            return Ok(());
2231        };
2232        let get = |i: usize| v.get(i).and_then(to_ref).unwrap_or(NodeRef::NULL);
2233        let (pre, post, rep) = (get(1), get(2), get(3));
2234        let sub = v.get(4).filter(|x| !x.is_nil()).map(int);
2235        let pen = v.get(5).filter(|x| !x.is_nil()).map(int);
2236        with_engine(lua, |e| {
2237            if e.nodes.id(p) != id::DISC {
2238                return;
2239            }
2240            e.nodes.set_disc_lists(p, (pre, post, rep));
2241            if let Some(s) = sub {
2242                e.nodes.get_mut(p).subtype = s as u16;
2243            }
2244            if let (Some(x), NodeData::Disc { penalty, .. }) = (pen, &mut e.nodes.get_mut(p).data) {
2245                *penalty = x as i32;
2246            }
2247        })
2248    })?;
2249    set_fn(lua, t, "getglue", |lua, v: Value| {
2250        let Some(p) = to_ref(&v) else {
2251            return Ok(MultiValue::new());
2252        };
2253        let vals: Vec<Value> = with_engine(lua, |e| match &e.nodes.get(p).data {
2254            NodeData::Glue { spec: g, .. } | NodeData::Math { glue: g, .. } => vec![
2255                Value::Integer(g.width.into()),
2256                Value::Integer(g.stretch.into()),
2257                Value::Integer(g.shrink.into()),
2258                Value::Integer(g.stretch_order.into()),
2259                Value::Integer(g.shrink_order.into()),
2260            ],
2261            // Observed order for boxes: set, order, sign.
2262            NodeData::Box(b) => vec![
2263                Value::Number(b.glue_set),
2264                Value::Integer(b.glue_order.into()),
2265                Value::Integer(b.glue_sign.into()),
2266            ],
2267            _ => Vec::new(),
2268        })?;
2269        Ok(MultiValue::from_iter(vals))
2270    })?;
2271    set_fn(lua, t, "setglue", |lua, args: MultiValue| {
2272        let v: Vec<Value> = args.into_iter().collect();
2273        let Some(p) = v.first().and_then(to_ref) else {
2274            return Ok(());
2275        };
2276        let n = |i: usize| v.get(i).map(int).unwrap_or(0);
2277        with_engine(lua, |e| {
2278            let g = match &mut e.nodes.get_mut(p).data {
2279                NodeData::Glue { spec, .. } => spec,
2280                NodeData::Math { glue, .. } => glue,
2281                _ => return,
2282            };
2283            g.width = n(1) as i32;
2284            g.stretch = n(2) as i32;
2285            g.shrink = n(3) as i32;
2286            g.stretch_order = n(4) as u8;
2287            g.shrink_order = n(5) as u8;
2288        })
2289    })?;
2290    set_fn(lua, t, "is_zero_glue", |lua, v: Value| {
2291        let Some(p) = to_ref(&v) else {
2292            return Ok(false);
2293        };
2294        with_engine(lua, |e| match &e.nodes.get(p).data {
2295            NodeData::Glue { spec: g, .. } | NodeData::Math { glue: g, .. } => {
2296                g.width == 0 && g.stretch == 0 && g.shrink == 0
2297            }
2298            _ => true,
2299        })
2300    })?;
2301    set_fn(lua, t, "getoffsets", |lua, v: Value| {
2302        let Some(p) = to_ref(&v) else {
2303            return Ok((Value::Nil, Value::Nil));
2304        };
2305        with_engine(lua, |e| match &e.nodes.get(p).data {
2306            NodeData::Glyph(g) => (
2307                Value::Integer(g.xoffset.into()),
2308                Value::Integer(g.yoffset.into()),
2309            ),
2310            _ => (Value::Nil, Value::Nil),
2311        })
2312    })?;
2313    set_fn(
2314        lua,
2315        t,
2316        "setoffsets",
2317        |lua, (v, x, y): (Value, Value, Value)| {
2318            if let Some(p) = to_ref(&v) {
2319                if !x.is_nil() {
2320                    set_field(lua, p, "xoffset", x)?;
2321                }
2322                if !y.is_nil() {
2323                    set_field(lua, p, "yoffset", y)?;
2324                }
2325            }
2326            Ok(())
2327        },
2328    )?;
2329    // is_char: the character of an unprocessed glyph (subtype below 256),
2330    // false for other glyphs or another font, nil for other nodes.
2331    set_fn(lua, t, "is_char", |lua, (v, f): (Value, Option<i64>)| {
2332        let Some(p) = to_ref(&v) else {
2333            return Ok(Value::Nil);
2334        };
2335        Ok(match with_engine(lua, |e| glyph_of(e, p))? {
2336            Some((c, font, sub)) if sub < 256 && f.is_none_or(|f| f == i64::from(font)) => {
2337                Value::Integer(c.into())
2338            }
2339            Some(_) => Value::Boolean(false),
2340            None => Value::Nil,
2341        })
2342    })?;
2343    set_fn(lua, t, "is_glyph", |lua, v: Value| {
2344        let Some(p) = to_ref(&v) else {
2345            return Ok(MultiValue::from_iter([Value::Boolean(false)]));
2346        };
2347        Ok(match with_engine(lua, |e| glyph_of(e, p))? {
2348            Some((c, font, _)) => {
2349                MultiValue::from_iter([Value::Integer(c.into()), Value::Integer(font.into())])
2350            }
2351            None => MultiValue::from_iter([Value::Boolean(false)]),
2352        })
2353    })?;
2354    set_fn(lua, t, "traverse_char", move |lua, v: Value| {
2355        let start = to_ref(&v).unwrap_or(NodeRef::NULL);
2356        list_iterator(lua, start, direct, |e, p| match glyph_of(e, p) {
2357            Some((c, f, s)) if s < 256 => {
2358                Some(vec![Value::Integer(c.into()), Value::Integer(f.into())])
2359            }
2360            _ => None,
2361        })
2362    })?;
2363    set_fn(lua, t, "traverse_glyph", move |lua, v: Value| {
2364        let start = to_ref(&v).unwrap_or(NodeRef::NULL);
2365        list_iterator(lua, start, direct, |e, p| {
2366            glyph_of(e, p).map(|(c, f, _)| vec![Value::Integer(c.into()), Value::Integer(f.into())])
2367        })
2368    })?;
2369    set_fn(lua, t, "traverse_list", move |lua, v: Value| {
2370        let start = to_ref(&v).unwrap_or(NodeRef::NULL);
2371        let (iter, s, c) = list_iterator(lua, start, direct, |e, p| {
2372            let n = e.nodes.get(p);
2373            match &n.data {
2374                NodeData::Box(b) if n.id == id::HLIST || n.id == id::VLIST => Some(vec![
2375                    Value::Integer(n.id.into()),
2376                    Value::Integer(n.subtype.into()),
2377                    // The list as a direct number, wrapped below.
2378                    Value::Integer(i64::from(b.head.0)),
2379                ]),
2380                _ => None,
2381            }
2382        })?;
2383        let wrapper = lua
2384            .load(
2385                "local iter, direct = ...
2386                 return function(s, c)
2387                   local n, i, st, l = iter(s, c)
2388                   if n == nil then return nil end
2389                   if l == 0 then l = nil elseif not direct then l = node.direct.tonode(l) end
2390                   return n, i, st, l
2391                 end",
2392            )
2393            .set_name("=[retex]")
2394            .call::<mlua::Function>((iter, direct))?;
2395        Ok((wrapper, s, c))
2396    })?;
2397    set_fn(lua, t, "has_glyph", move |lua, v: Value| {
2398        let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
2399        let found = with_engine(lua, |e| {
2400            while !p.is_null() {
2401                let i = e.nodes.id(p);
2402                if i == id::GLYPH || i == id::DISC {
2403                    return p;
2404                }
2405                p = e.nodes.next(p);
2406            }
2407            NodeRef::NULL
2408        })?;
2409        wrap(lua, found, direct)
2410    })?;
2411    set_fn(
2412        lua,
2413        t,
2414        "first_glyph",
2415        move |lua, (v, stop): (Value, Value)| {
2416            let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
2417            let stop = to_ref(&stop);
2418            // Observed: the first glyph whose subtype is 1 or 257 (a plain
2419            // character); `node.direct` gives 0 when there is none.
2420            let found = with_engine(lua, |e| {
2421                while !p.is_null() {
2422                    if glyph_of(e, p).is_some_and(|(_, _, s)| s & 0xFF == 1) {
2423                        return p;
2424                    }
2425                    if Some(p) == stop {
2426                        break;
2427                    }
2428                    p = e.nodes.next(p);
2429                }
2430                NodeRef::NULL
2431            })?;
2432            if direct && found.is_null() {
2433                return Ok(Value::Integer(0));
2434            }
2435            wrap(lua, found, direct)
2436        },
2437    )?;
2438    set_fn(lua, t, "get_attribute", |lua, (v, n): (Value, i64)| {
2439        let Some(p) = to_ref(&v) else {
2440            return Ok(None);
2441        };
2442        with_engine(lua, |e| {
2443            e.nodes.get(p).attr.as_ref().and_then(|l| {
2444                l.iter()
2445                    .find(|(k, _)| i64::from(*k) == n)
2446                    .map(|x| i64::from(x.1))
2447            })
2448        })
2449    })?;
2450    set_fn(
2451        lua,
2452        t,
2453        "find_attribute",
2454        move |lua, (v, n): (Value, i64)| {
2455            let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
2456            let found =
2457                with_engine(lua, |e| {
2458                    while !p.is_null() {
2459                        if let Some(x) =
2460                            e.nodes.get(p).attr.as_ref().and_then(|l| {
2461                                l.iter().find(|(k, _)| i64::from(*k) == n).map(|x| x.1)
2462                            })
2463                        {
2464                            return Some((x, p));
2465                        }
2466                        p = e.nodes.next(p);
2467                    }
2468                    None
2469                })?;
2470            match found {
2471                Some((x, p)) => Ok(MultiValue::from_iter([
2472                    Value::Integer(x.into()),
2473                    wrap(lua, p, direct)?,
2474                ])),
2475                None => Ok(MultiValue::new()),
2476            }
2477        },
2478    )?;
2479    set_fn(
2480        lua,
2481        t,
2482        "effective_glue",
2483        |lua, (g, parent): (Value, Value)| {
2484            let (Some(g), Some(b)) = (to_ref(&g), to_ref(&parent)) else {
2485                return Ok(Value::Nil);
2486            };
2487            with_engine(lua, |e| {
2488                let spec = match &e.nodes.get(g).data {
2489                    NodeData::Glue { spec, .. } => *spec,
2490                    _ => return Value::Nil,
2491                };
2492                let Some(bx) = e.nodes.box_data(b) else {
2493                    return Value::Number(spec.width.into());
2494                };
2495                let mut w = f64::from(spec.width);
2496                if bx.glue_sign == 1 && spec.stretch_order == bx.glue_order {
2497                    w += bx.glue_set * f64::from(spec.stretch);
2498                } else if bx.glue_sign == 2 && spec.shrink_order == bx.glue_order {
2499                    w -= bx.glue_set * f64::from(spec.shrink);
2500                }
2501                Value::Number(w.round())
2502            })
2503        },
2504    )?;
2505    for (name, protect) in [("protect_glyph", true), ("unprotect_glyph", false)] {
2506        set_fn(lua, t, name, move |lua, v: Value| {
2507            if let Some(p) = to_ref(&v) {
2508                with_engine(lua, |e| {
2509                    // Observed: protecting turns subtype 1 into 256 and
2510                    // adds 256 to subtypes up to 256; unprotecting takes
2511                    // 256 from subtypes above 256.
2512                    if e.nodes.id(p) == id::GLYPH {
2513                        let n = e.nodes.get_mut(p);
2514                        let s = n.subtype;
2515                        n.subtype = match (protect, s) {
2516                            (true, 1) => 256,
2517                            (true, s) if s <= 256 => s + 256,
2518                            (false, s) if s > 256 => s - 256,
2519                            (_, s) => s,
2520                        };
2521                    }
2522                })?;
2523            }
2524            Ok(())
2525        })?;
2526    }
2527    set_fn(lua, t, "uses_font", |lua, (v, f): (Value, i64)| {
2528        let Some(p) = to_ref(&v) else {
2529            return Ok(false);
2530        };
2531        with_engine(lua, |e| {
2532            let uses = |e: &Engine, mut q: NodeRef| {
2533                while !q.is_null() {
2534                    if glyph_of(e, q).is_some_and(|(_, font, _)| i64::from(font) == f) {
2535                        return true;
2536                    }
2537                    q = e.nodes.next(q);
2538                }
2539                false
2540            };
2541            match &e.nodes.get(p).data {
2542                NodeData::Glyph(g) => i64::from(g.font) == f,
2543                NodeData::Disc {
2544                    pre, post, replace, ..
2545                } => uses(e, *pre) || uses(e, *post) || uses(e, *replace),
2546                _ => false,
2547            }
2548        })
2549    })?;
2550    set_fn(lua, t, "getbox", move |lua, n: i64| {
2551        let p = with_engine(lua, |e| {
2552            match e.eqtb.get(retex_engine::eqtb::Loc::Box(n as u32)) {
2553                retex_engine::eqtb::Val::Node(p) => p,
2554                _ => NodeRef::NULL,
2555            }
2556        })?;
2557        wrap(lua, p, direct)
2558    })?;
2559    set_fn(lua, t, "setbox", |lua, args: MultiValue| {
2560        let mut v: Vec<Value> = args.into_iter().collect();
2561        let global = matches!(v.first(), Some(Value::String(s)) if &*s.to_str()? == "global");
2562        if global {
2563            v.remove(0);
2564        }
2565        let n = v.first().map(int).unwrap_or(0) as u32;
2566        let p = v.get(1).and_then(to_ref).unwrap_or(NodeRef::NULL);
2567        with_engine(lua, |e| {
2568            e.define(
2569                global,
2570                retex_engine::eqtb::Loc::Box(n),
2571                retex_engine::eqtb::Val::Node(p),
2572            )
2573        })
2574    })?;
2575    if direct {
2576        // Observed: the number itself for a direct node.
2577        set_fn(lua, t, "is_direct", |_, v: Value| {
2578            Ok(match v {
2579                Value::Integer(n) if n > 0 => Value::Integer(n),
2580                _ => Value::Boolean(false),
2581            })
2582        })?;
2583    } else {
2584        for (name, next) in [("next", true), ("prev", false)] {
2585            set_fn(lua, t, name, move |lua, v: Value| {
2586                let Some(p) = to_ref(&v) else {
2587                    return Ok(Value::Nil);
2588                };
2589                let q = with_engine(lua, |e| {
2590                    if next {
2591                        e.nodes.next(p)
2592                    } else {
2593                        e.nodes.get(p).prev
2594                    }
2595                })?;
2596                wrap(lua, q, false)
2597            })?;
2598        }
2599    }
2600    Ok(())
2601}
2602
2603/// Registry key of the table of Lua values kept by reference.
2604const LUA_REFS_KEY: &str = "retex.node.luarefs";
2605
2606fn lua_refs(lua: &Lua) -> mlua::Result<Table> {
2607    match lua.named_registry_value::<Value>(LUA_REFS_KEY)? {
2608        Value::Table(t) => Ok(t),
2609        _ => {
2610            let t = lua.create_table()?;
2611            lua.set_named_registry_value(LUA_REFS_KEY, &t)?;
2612            Ok(t)
2613        }
2614    }
2615}
2616
2617/// The table of Lua values kept by reference, with the values of the
2618/// references node copies made since the last call.
2619pub(crate) fn lua_refs_synced(lua: &Lua) -> mlua::Result<Table> {
2620    let refs = lua_refs(lua)?;
2621    let copies = with_engine(lua, |e| std::mem::take(&mut e.nodes.lua_refs.copies))?;
2622    for (new, old) in copies {
2623        let v: Value = refs.raw_get(old)?;
2624        refs.raw_set(new, v)?;
2625    }
2626    Ok(refs)
2627}
2628
2629/// Keeps a Lua value by reference; returns its number (see
2630/// [`retex_engine::nodes::LuaRefs`]; they only show in box displays).
2631pub(crate) fn lua_ref(lua: &Lua, v: Value) -> mlua::Result<i32> {
2632    let refs = lua_refs_synced(lua)?;
2633    let n = with_engine(lua, |e| e.nodes.lua_refs.take())?;
2634    refs.raw_set(n, v)?;
2635    Ok(n)
2636}
2637
2638/// A whatsit of subtype `sub` as `node.new` makes it.
2639fn new_whatsit(sub: u16) -> retex_engine::nodes::Whatsit {
2640    use retex_engine::nodes::{LateData, UserValue, Whatsit, whatsit_sub as ws};
2641    match sub {
2642        ws::SPECIAL => Whatsit::Special { data: Vec::new() },
2643        ws::LATE_LUA => Whatsit::LateLua {
2644            data: LateData::Bytes(Vec::new()),
2645            name: Vec::new(),
2646        },
2647        ws::PDF_LITERAL | ws::PDF_LATE_LITERAL => Whatsit::PdfLiteral {
2648            mode: 0,
2649            data: LateData::Bytes(Vec::new()),
2650            late: sub == ws::PDF_LATE_LITERAL,
2651        },
2652        ws::PDF_COLORSTACK => Whatsit::PdfColorstack {
2653            stack: 0,
2654            cmd: 0,
2655            data: Vec::new(),
2656        },
2657        ws::PDF_SETMATRIX => Whatsit::PdfSetmatrix { data: Vec::new() },
2658        ws::PDF_SAVE => Whatsit::PdfSave,
2659        ws::PDF_RESTORE => Whatsit::PdfRestore,
2660        ws::PDF_REFOBJ => Whatsit::PdfRefobj { objnum: 0 },
2661        ws::PDF_END_LINK => Whatsit::PdfEndLink,
2662        // Observed: new links and annotations have zero dimensions.
2663        ws::PDF_START_LINK => Whatsit::PdfStartLink {
2664            dims: [0; 3],
2665            objnum: 0,
2666            attr: Vec::new(),
2667            action: retex_engine::pdfext::PdfAction::User(Vec::new()),
2668        },
2669        ws::PDF_ANNOT => Whatsit::PdfAnnot {
2670            dims: [0; 3],
2671            objnum: 0,
2672            data: Vec::new(),
2673        },
2674        ws::SAVE_POS => Whatsit::SavePos,
2675        ws::PDF_LINK_STATE => Whatsit::PdfLinkState { state: 0 },
2676        ws::USER_DEFINED => Whatsit::UserDefined {
2677            user_id: 0,
2678            value: UserValue::Int(0),
2679        },
2680        _ => Whatsit::Other {
2681            fields: Vec::new(),
2682            data: Vec::new(),
2683        },
2684    }
2685}
2686
2687/// The index of `width`, `height` or `depth` in a whatsit's dimensions.
2688fn dim_index(key: &str) -> usize {
2689    match key {
2690        "width" => 0,
2691        "height" => 1,
2692        _ => 2,
2693    }
2694}
2695
2696/// Reads a whatsit field.
2697fn whatsit_field(e: &mut Engine, w: &retex_engine::nodes::Whatsit, key: &str) -> R {
2698    use retex_engine::nodes::{LateData, UserValue, Whatsit};
2699    let mut late = |d: &LateData| match d {
2700        LateData::Bytes(b) | LateData::Lua { data: b, .. } => R::S(b.clone()),
2701        LateData::Tokens(t) => {
2702            R::S(e.print_to_string(|e| e.show_token_list(t, 0, t.len(), i64::MAX)))
2703        }
2704        LateData::Function { reference } => R::L(*reference),
2705    };
2706    match (w, key) {
2707        (Whatsit::Special { data }, "data") => R::S(data.clone()),
2708        (Whatsit::LateLua { data, .. }, "data" | "string") => late(data),
2709        (Whatsit::LateLua { name, .. }, "name") => R::S(name.clone()),
2710        (Whatsit::PdfLiteral { mode, .. }, "mode") => R::I((*mode).into()),
2711        (Whatsit::PdfLiteral { data, .. }, "data") => late(data),
2712        (Whatsit::PdfColorstack { stack, .. }, "stack") => R::I((*stack).into()),
2713        (Whatsit::PdfColorstack { cmd, .. }, "command") => R::I((*cmd).into()),
2714        (Whatsit::PdfColorstack { data, .. }, "data") => R::S(data.clone()),
2715        (Whatsit::PdfSetmatrix { data }, "data") => R::S(data.clone()),
2716        (Whatsit::PdfRefobj { objnum }, "objnum") => R::I((*objnum).into()),
2717        // Observed: `link_attr` reads as a string; running dimensions
2718        // read as -2^30.
2719        (Whatsit::PdfStartLink { objnum, .. } | Whatsit::PdfAnnot { objnum, .. }, "objnum") => {
2720            R::I((*objnum).into())
2721        }
2722        (Whatsit::PdfStartLink { attr, .. }, "link_attr") => R::S(attr.clone()),
2723        (Whatsit::PdfAnnot { data, .. }, "data") => R::S(data.clone()),
2724        (
2725            Whatsit::PdfStartLink { dims, .. } | Whatsit::PdfAnnot { dims, .. },
2726            "width" | "height" | "depth",
2727        ) => R::I(dims[dim_index(key)].into()),
2728        // lualinksplit reads a link state as `objnum` with the subtype
2729        // switched to pdf_refobj (the field it shares in LuaTeX).
2730        (Whatsit::PdfLinkState { state }, "objnum") => R::I((*state).into()),
2731        (Whatsit::UserDefined { user_id, .. }, "user_id") => R::I((*user_id).into()),
2732        (Whatsit::UserDefined { value, .. }, "type") => R::I(i64::from(user_type(value))),
2733        (Whatsit::UserDefined { value, .. }, "value") => match value {
2734            UserValue::Int(n) => R::I((*n).into()),
2735            UserValue::Str(s) => R::S(s.clone()),
2736            UserValue::Tokens(t) => {
2737                R::S(e.print_to_string(|e| e.show_token_list(t, 0, t.len(), i64::MAX)))
2738            }
2739            UserValue::Nodes(p) => R::N(*p),
2740            UserValue::Lua(n) => R::L(*n),
2741            UserValue::Attr => R::Nil,
2742        },
2743        _ => R::Nil,
2744    }
2745}
2746
2747/// The type letter of a `user_defined` value.
2748fn user_type(v: &retex_engine::nodes::UserValue) -> u8 {
2749    use retex_engine::nodes::UserValue;
2750    match v {
2751        UserValue::Int(_) => b'd',
2752        UserValue::Str(_) => b's',
2753        UserValue::Tokens(_) => b't',
2754        UserValue::Nodes(_) => b'n',
2755        UserValue::Lua(_) => b'l',
2756        UserValue::Attr => b'a',
2757    }
2758}
2759
2760/// Writes a whatsit field.
2761fn set_whatsit_field(lua: &Lua, p: NodeRef, key: &str, v: Value) -> mlua::Result<()> {
2762    use retex_engine::nodes::{LateData, Whatsit};
2763    let s: Option<Vec<u8>> = match &v {
2764        Value::String(s) => Some(s.as_bytes().to_vec()),
2765        Value::Integer(n) => Some(n.to_string().into_bytes()),
2766        Value::Number(n) => Some(crate::texlib::lua_number_string(*n).into_bytes()),
2767        _ => None,
2768    };
2769    let node = to_ref(&v);
2770    let i = int(&v);
2771    // The type of a user_defined node decides how `value` is stored.
2772    let (is_user, utype) = with_engine(lua, |e| match &e.nodes.get(p).data {
2773        NodeData::Whatsit(w) => match &**w {
2774            Whatsit::UserDefined { value, .. } => (true, user_type(value)),
2775            _ => (false, 0),
2776        },
2777        _ => (false, 0),
2778    })?;
2779    let lref = if is_user && key == "value" && utype == b'l' {
2780        Some(lua_ref(lua, v.clone())?)
2781    } else {
2782        None
2783    };
2784    // Observed: outside IniTeX, a string set from Lua is kept as a Lua
2785    // reference, and box displays show the reference.
2786    let ini = with_engine(lua, |e| e.ini_version)?;
2787    let lua_string = |s: Vec<u8>| -> mlua::Result<LateData> {
2788        if ini {
2789            Ok(LateData::Bytes(s))
2790        } else {
2791            let reference = lua_ref(lua, Value::String(lua.create_string(&s)?))?;
2792            Ok(LateData::Lua { data: s, reference })
2793        }
2794    };
2795    let late = match (&s, key) {
2796        (Some(s), "data") => Some(lua_string(s.clone())?),
2797        // Observed: a function as `\latelua` data is kept by reference,
2798        // except in IniTeX, which keeps an empty string.
2799        (None, "data" | "string") if matches!(v, Value::Function(_)) && late_lua(lua, p)? => {
2800            Some(if ini {
2801                LateData::Bytes(Vec::new())
2802            } else {
2803                LateData::Function {
2804                    reference: lua_ref(lua, v.clone())?,
2805                }
2806            })
2807        }
2808        _ => None,
2809    };
2810    with_engine(lua, |e| {
2811        let tokens = |e: &Engine, s: &[u8]| -> retex_engine::token::TokenList {
2812            let _ = e;
2813            let toks: Vec<retex_engine::token::Token> = String::from_utf8_lossy(s)
2814                .chars()
2815                .map(|c| {
2816                    if c == ' ' {
2817                        retex_engine::token::Token::SPACE
2818                    } else {
2819                        retex_engine::token::Token::other(c as u32)
2820                    }
2821                })
2822                .collect();
2823            std::rc::Rc::from(toks)
2824        };
2825        let toks = s.as_ref().map(|s| tokens(e, s));
2826        // Observed: a new value takes its reference before the old one is
2827        // released.
2828        let old_ref = e.nodes.lua_ref_of(p);
2829        set_whatsit_data(e, p, key, &s, &late, &toks, node, i, lref);
2830        if let Some(r) = old_ref
2831            && e.nodes.lua_ref_of(p) != Some(r)
2832        {
2833            e.nodes.lua_refs.release(r);
2834        }
2835    })
2836}
2837
2838/// Whether `p` is a `late_lua` whatsit.
2839fn late_lua(lua: &Lua, p: NodeRef) -> mlua::Result<bool> {
2840    with_engine(lua, |e| match &e.nodes.get(p).data {
2841        NodeData::Whatsit(w) => matches!(&**w, retex_engine::nodes::Whatsit::LateLua { .. }),
2842        _ => false,
2843    })
2844}
2845
2846/// Stores a whatsit field (for [`set_whatsit_field`]).
2847#[allow(clippy::too_many_arguments)]
2848fn set_whatsit_data(
2849    e: &mut Engine,
2850    p: NodeRef,
2851    key: &str,
2852    s: &Option<Vec<u8>>,
2853    late: &Option<retex_engine::nodes::LateData>,
2854    toks: &Option<retex_engine::token::TokenList>,
2855    node: Option<NodeRef>,
2856    i: i64,
2857    lref: Option<i32>,
2858) {
2859    use retex_engine::nodes::{LateData, UserValue, Whatsit};
2860    {
2861        let NodeData::Whatsit(w) = &mut e.nodes.get_mut(p).data else {
2862            return;
2863        };
2864        match (&mut **w, key) {
2865            (Whatsit::Special { data }, "data") => {
2866                if let Some(s) = &s {
2867                    *data = s.clone();
2868                }
2869            }
2870            (Whatsit::LateLua { data, .. }, "data" | "string") => {
2871                if let Some(l) = late.clone().or_else(|| s.clone().map(LateData::Bytes)) {
2872                    *data = l;
2873                }
2874            }
2875            (Whatsit::LateLua { name, .. }, "name") => {
2876                if let Some(s) = &s {
2877                    *name = s.clone();
2878                }
2879            }
2880            (Whatsit::PdfLiteral { mode, .. }, "mode") => *mode = i as u8,
2881            (Whatsit::PdfLiteral { data, .. }, "data") => {
2882                if let Some(l) = late.clone() {
2883                    *data = l;
2884                }
2885            }
2886            (Whatsit::PdfColorstack { stack, .. }, "stack") => *stack = i as i32,
2887            (Whatsit::PdfColorstack { cmd, .. }, "command") => *cmd = i as u8,
2888            (Whatsit::PdfColorstack { data, .. }, "data")
2889            | (Whatsit::PdfSetmatrix { data }, "data") => {
2890                if let Some(s) = &s {
2891                    *data = s.clone();
2892                }
2893            }
2894            (Whatsit::PdfRefobj { objnum }, "objnum")
2895            | (Whatsit::PdfStartLink { objnum, .. }, "objnum")
2896            | (Whatsit::PdfAnnot { objnum, .. }, "objnum") => *objnum = i as i32,
2897            (Whatsit::PdfStartLink { attr: data, .. }, "link_attr")
2898            | (Whatsit::PdfAnnot { data, .. }, "data") => {
2899                if let Some(s) = &s {
2900                    *data = s.clone();
2901                }
2902            }
2903            (
2904                Whatsit::PdfStartLink { dims, .. } | Whatsit::PdfAnnot { dims, .. },
2905                "width" | "height" | "depth",
2906            ) => dims[dim_index(key)] = i as i32,
2907            (Whatsit::PdfLinkState { state }, "value" | "objnum") => *state = i as i32,
2908            (Whatsit::UserDefined { user_id, .. }, "user_id") => *user_id = i as i32,
2909            (Whatsit::UserDefined { value, .. }, "type") => {
2910                *value = match i as u8 {
2911                    b's' => UserValue::Str(Vec::new()),
2912                    b't' => UserValue::Tokens(retex_engine::token::empty_list()),
2913                    b'n' => UserValue::Nodes(NodeRef::NULL),
2914                    b'l' => UserValue::Lua(0),
2915                    b'a' => UserValue::Attr,
2916                    _ => UserValue::Int(0),
2917                }
2918            }
2919            (Whatsit::UserDefined { value, .. }, "value") => {
2920                *value = match user_type(value) {
2921                    b's' => UserValue::Str(s.clone().unwrap_or_default()),
2922                    b't' => UserValue::Tokens(
2923                        toks.clone().unwrap_or_else(retex_engine::token::empty_list),
2924                    ),
2925                    b'n' => UserValue::Nodes(node.unwrap_or(NodeRef::NULL)),
2926                    b'l' => UserValue::Lua(lref.unwrap_or(0)),
2927                    b'a' => UserValue::Attr,
2928                    _ => UserValue::Int(i as i32),
2929                }
2930            }
2931            _ => {}
2932        }
2933    }
2934}
2935
2936/// `hpack`, `vpack`, `write`, `last_node`, `end_of_math`,
2937/// `protect_glyphs` and `unprotect_glyphs` (LuaTeX manual §10.6).
2938fn install_list_ops(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
2939    use retex_engine::build::PackSpec;
2940    for (name, horizontal) in [("hpack", true), ("vpack", false)] {
2941        set_fn(lua, t, name, move |lua, args: MultiValue| {
2942            let v: Vec<Value> = args.into_iter().collect();
2943            let head = v.first().and_then(to_ref).unwrap_or(NodeRef::NULL);
2944            let w = v.get(1).map(int).unwrap_or(0) as i32;
2945            let exactly = matches!(v.get(2), Some(Value::String(s)) if &*s.to_str()? == "exactly");
2946            let spec = if exactly {
2947                PackSpec::Exactly(w)
2948            } else {
2949                PackSpec::Additional(w)
2950            };
2951            let dir = match v.get(3) {
2952                Some(Value::String(s)) => match &*s.to_str()? {
2953                    "TLT" => 0,
2954                    "TRT" => 1,
2955                    "LTL" => 2,
2956                    "RTT" => 3,
2957                    _ => -1,
2958                },
2959                Some(Value::Integer(n)) => *n as i8,
2960                _ => -1,
2961            };
2962            let (b, badness) = with_engine(lua, |e| {
2963                e.pack_dir = dir;
2964                let b = if horizontal {
2965                    e.hpack(head, spec)
2966                } else {
2967                    e.vpack(head, spec)
2968                };
2969                e.pack_dir = -1;
2970                (b, e.last_badness)
2971            })?;
2972            Ok((wrap(lua, b, direct)?, Value::Integer(badness.into())))
2973        })?;
2974    }
2975    // Appends a list to the current list.
2976    set_fn(lua, t, "write", |lua, v: Value| {
2977        if let Some(p) = to_ref(&v) {
2978            with_engine(lua, |e| {
2979                let tail = tail(e, p);
2980                e.tail_append(p);
2981                e.cur_list_mut().tail = tail;
2982            })?;
2983        }
2984        Ok(())
2985    })?;
2986    // Removes the last node of the current list and returns it.
2987    set_fn(lua, t, "last_node", move |lua, ()| {
2988        let p = with_engine(lua, |e| {
2989            let l = e.cur_list();
2990            let (head, tail) = (l.head, l.tail);
2991            if tail == head {
2992                return NodeRef::NULL;
2993            }
2994            let mut q = head;
2995            while e.nodes.next(q) != tail {
2996                q = e.nodes.next(q);
2997            }
2998            e.nodes.get_mut(q).next = NodeRef::NULL;
2999            let n = e.nodes.get_mut(tail);
3000            n.prev = NodeRef::NULL;
3001            e.cur_list_mut().tail = q;
3002            tail
3003        })?;
3004        wrap(lua, p, direct)
3005    })?;
3006    // The math node that ends the formula starting at a begin math node.
3007    set_fn(lua, t, "end_of_math", move |lua, v: Value| {
3008        let Some(mut p) = to_ref(&v) else {
3009            return Ok(Value::Nil);
3010        };
3011        let found = with_engine(lua, |e| {
3012            if e.nodes.id(p) == id::MATH && e.nodes.subtype(p) == 0 {
3013                p = e.nodes.next(p);
3014            }
3015            while !p.is_null() {
3016                if e.nodes.id(p) == id::MATH && e.nodes.subtype(p) == 1 {
3017                    return p;
3018                }
3019                p = e.nodes.next(p);
3020            }
3021            NodeRef::NULL
3022        })?;
3023        wrap(lua, found, direct)
3024    })?;
3025    for (name, protect) in [("protect_glyphs", true), ("unprotect_glyphs", false)] {
3026        set_fn(lua, t, name, move |lua, (h, stop): (Value, Value)| {
3027            let mut p = to_ref(&h).unwrap_or(NodeRef::NULL);
3028            let stop = to_ref(&stop);
3029            with_engine(lua, |e| {
3030                while !p.is_null() {
3031                    if e.nodes.id(p) == id::GLYPH {
3032                        let n = e.nodes.get_mut(p);
3033                        let s = n.subtype;
3034                        n.subtype = match (protect, s) {
3035                            (true, 1) => 256,
3036                            (true, s) if s <= 256 => s + 256,
3037                            (false, s) if s > 256 => s - 256,
3038                            (_, s) => s,
3039                        };
3040                    }
3041                    if Some(p) == stop {
3042                        break;
3043                    }
3044                    p = e.nodes.next(p);
3045                }
3046            })
3047        })?;
3048    }
3049    Ok(())
3050}