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