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