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