use std::collections::HashMap;
use std::sync::OnceLock;
use mlua::{Lua, MetaMethod, MultiValue, Table, UserData, UserDataMethods, Value};
use retex_engine::Engine;
use retex_engine::nodes::{NodeData, NodeRef, id, noad_opt};
use crate::with_engine;
struct TypeInfo {
names: HashMap<u8, String>,
ids: HashMap<String, u8>,
fields: HashMap<u8, Vec<(i32, String)>>,
subtypes: HashMap<u8, Vec<(i32, String)>>,
whatsits: Vec<(u16, String)>,
whatsit_fields: HashMap<u16, Vec<(i32, String)>>,
}
fn info() -> &'static TypeInfo {
static T: OnceLock<TypeInfo> = OnceLock::new();
T.get_or_init(|| {
let data = retex_engine::nodes::LUATEX_NODE_TYPES;
let mut ti = TypeInfo {
names: HashMap::new(),
ids: HashMap::new(),
fields: HashMap::new(),
subtypes: HashMap::new(),
whatsits: Vec::new(),
whatsit_fields: HashMap::new(),
};
let pairs = |s: &str| -> Vec<(i32, String)> {
s.split_whitespace()
.filter_map(|kv| {
let (k, v) = kv.split_once('=')?;
Some((k.parse().ok()?, v.to_owned()))
})
.collect()
};
let mut cur: u8 = 0;
for line in data.lines() {
let l = line.trim_start();
if let Some(rest) = line.strip_prefix("type ") {
let mut it = rest.split_whitespace();
let i: u8 = it.next().and_then(|x| x.parse().ok()).unwrap_or(0);
let name = it.next().unwrap_or("").to_owned();
ti.names.insert(i, name.clone());
ti.ids.insert(name, i);
cur = i;
} else if let Some(rest) = l.strip_prefix("fields ") {
ti.fields.insert(cur, pairs(rest));
} else if let Some(rest) = l.strip_prefix("subtypes ") {
ti.subtypes.insert(cur, pairs(rest));
} else if let Some(rest) = line.strip_prefix("whatsits ") {
ti.whatsits = pairs(rest)
.into_iter()
.map(|(k, v)| (k as u16, v))
.collect();
} else if let Some(rest) = l.strip_prefix("whatsit ") {
let mut it = rest.splitn(4, ' ');
let _name = it.next();
let n: u16 = it.next().and_then(|x| x.parse().ok()).unwrap_or(0);
let _ = it.next();
ti.whatsit_fields.insert(n, pairs(it.next().unwrap_or("")));
}
}
ti
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LNode(pub u32);
impl UserData for LNode {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_meta_method(MetaMethod::Index, |lua, this, key: String| {
get_field(lua, NodeRef(this.0), &key)
});
m.add_meta_method(
MetaMethod::NewIndex,
|lua, this, (key, v): (String, Value)| set_field(lua, NodeRef(this.0), &key, v),
);
m.add_meta_method(MetaMethod::Eq, |_, this, other: mlua::AnyUserData| {
Ok(other
.borrow::<LNode>()
.map(|o| o.0 == this.0)
.unwrap_or(false))
});
m.add_meta_method(MetaMethod::ToString, |lua, this, ()| {
let p = NodeRef(this.0);
let (t, sub, prev, next) = with_engine(lua, |e| {
(
e.nodes.id(p),
e.nodes.subtype(p),
e.nodes.prev(p),
e.nodes.next(p),
)
})?;
let name = info().names.get(&t).cloned().unwrap_or_default();
let num = |q: NodeRef| {
if q.is_null() {
"nil".to_owned()
} else {
q.0.to_string()
}
};
Ok(format!(
"<node {:>6} < {:>6} > {:>6} : {name} {sub}>",
num(prev),
this.0,
num(next)
))
});
}
}
pub(crate) fn type_name(t: u8) -> String {
info().names.get(&t).cloned().unwrap_or_default()
}
pub(crate) fn to_ref(v: &Value) -> Option<NodeRef> {
match v {
Value::UserData(u) => u.borrow::<LNode>().ok().map(|n| NodeRef(n.0)),
Value::Integer(n) if *n > 0 => Some(NodeRef(*n as u32)),
_ => None,
}
}
pub(crate) fn wrap(lua: &Lua, p: NodeRef, direct: bool) -> mlua::Result<Value> {
if p.is_null() {
return Ok(Value::Nil);
}
if direct {
Ok(Value::Integer(i64::from(p.0)))
} else {
Ok(Value::UserData(lua.create_userdata(LNode(p.0))?))
}
}
#[derive(Clone, Debug)]
pub struct LAttr(pub retex_engine::nodes::AttrList);
impl UserData for LAttr {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_meta_method(MetaMethod::Eq, |_, this, other: mlua::AnyUserData| {
Ok(other
.borrow::<LAttr>()
.map(|o| o.0 == this.0)
.unwrap_or(false))
});
m.add_meta_method(MetaMethod::Index, |_, _, key: Value| {
Ok(match &key {
Value::String(k) if k.as_bytes().as_ref() == b"id" => Value::Integer(40),
Value::String(k) if k.as_bytes().as_ref() == b"subtype" => Value::Integer(0),
_ => Value::Nil,
})
});
m.add_meta_method(MetaMethod::ToString, |_, _, ()| {
Ok("<node attribute_list>".to_string())
});
}
}
#[derive(Clone, Copy, Debug)]
struct LAttrEntry(u32, i32);
impl UserData for LAttrEntry {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_meta_method(MetaMethod::Index, |_, this, key: Value| {
let Value::String(k) = &key else {
return Ok(Value::Nil);
};
Ok(match k.as_bytes().as_ref() {
b"id" => Value::Integer(38),
b"subtype" => Value::Integer(0),
b"number" => Value::Integer(i64::from(this.0)),
b"value" => Value::Integer(i64::from(this.1)),
_ => Value::Nil,
})
});
m.add_meta_method(MetaMethod::ToString, |_, _, ()| {
Ok("<node attribute>".to_string())
});
}
}
fn attr_entries(lua: &Lua, list: &mlua::AnyUserData) -> mlua::Result<Vec<[Value; 3]>> {
let a = list.borrow::<LAttr>()?.0.clone();
let mut out = vec![[
Value::UserData(list.clone()),
Value::Integer(40),
Value::Integer(0),
]];
for &(n, v) in a.iter() {
out.push([
Value::UserData(lua.create_userdata(LAttrEntry(n, v))?),
Value::Integer(38),
Value::Integer(0),
]);
}
Ok(out)
}
fn attr_value(lua: &Lua, p: NodeRef) -> mlua::Result<Value> {
match with_engine(lua, |e| e.nodes.attr(p).cloned())? {
Some(a) => Ok(Value::UserData(lua.create_userdata(LAttr(a))?)),
None => Ok(Value::Nil),
}
}
fn set_attr_value(lua: &Lua, p: NodeRef, v: &Value) -> mlua::Result<()> {
let a = match v {
Value::UserData(u) => match u.borrow::<LAttr>() {
Ok(a) => Some(a.0.clone()),
Err(_) => return Ok(()),
},
Value::Nil => None,
_ => return Ok(()),
};
with_engine(lua, |e| e.nodes.set_attr(p, a))
}
fn new_data(t: u8) -> NodeData {
use retex_engine::glue::Glue;
match t {
id::HLIST | id::VLIST => NodeData::Box(Box::default()),
id::RULE => NodeData::Rule {
width: retex_engine::nodes::RUNNING,
depth: retex_engine::nodes::RUNNING,
height: retex_engine::nodes::RUNNING,
dir: 0,
index: 0,
left: 0,
right: 0,
},
id::GLUE | id::GLUE_SPEC => NodeData::Glue {
spec: Glue::ZERO,
leader: NodeRef::NULL,
},
id::KERN => NodeData::Kern {
kern: 0,
expansion_factor: 0,
},
id::PENALTY => NodeData::Penalty { penalty: 0 },
id::MARGIN_KERN => NodeData::MarginKern {
width: 0,
glyph: NodeRef::NULL,
},
id::GLYPH => NodeData::Glyph(Default::default()),
id::DISC => NodeData::Disc {
pre: NodeRef::NULL,
post: NodeRef::NULL,
replace: NodeRef::NULL,
penalty: 0,
},
id::BOUNDARY => NodeData::Boundary { value: 0 },
id::MATH => NodeData::Math {
surround: 0,
glue: Glue::ZERO,
},
id::ADJUST => NodeData::Adjust {
head: NodeRef::NULL,
},
id::MARK => NodeData::Mark {
class: 0,
mark: retex_engine::token::empty_list(),
},
id::DIR => NodeData::Dir { dir: 0, level: 0 },
id::STYLE => NodeData::Style { style: 0 },
id::LOCAL_PAR => NodeData::LocalPar {
pen_inter: 0,
pen_broken: 0,
dir: 0,
box_left: NodeRef::NULL,
box_left_width: 0,
box_right: NodeRef::NULL,
box_right_width: 0,
},
id::WHATSIT => NodeData::Whatsit(Box::new(retex_engine::nodes::Whatsit::Other {
fields: Vec::new(),
data: Vec::new(),
})),
_ => NodeData::Temp,
}
}
fn int(v: &Value) -> i64 {
match v {
Value::Integer(n) => *n,
Value::Number(n) => n.round() as i64,
Value::Boolean(b) => i64::from(*b),
_ => 0,
}
}
fn get_field(lua: &Lua, p: NodeRef, key: &str) -> mlua::Result<Value> {
if key == "subtype"
&& matches!(with_engine(lua, |e| e.nodes.id(p))?, 9 | 30..=32 | 35..=37 | 39 | 41..=49)
{
return Ok(Value::Nil);
}
get_field_impl(lua, p, key, false)
}
enum R {
I(i64),
N(NodeRef),
F(f64),
S(Vec<u8>),
L(i32),
Nil,
}
fn get_field_impl(lua: &Lua, p: NodeRef, key: &str, direct: bool) -> mlua::Result<Value> {
if key == "attr" {
return attr_value(lua, p);
}
let r = with_engine(lua, |e| -> R {
let n = e.nodes.get(p);
match key {
"id" => return R::I(i64::from(n.id)),
"subtype" => return R::I(i64::from(n.subtype)),
"next" => return R::N(n.next),
"prev" => return R::N(n.prev),
_ => {}
}
match &n.data {
NodeData::Box(b) | NodeData::Unset { b, .. } => match key {
"width" => R::I(b.width.into()),
"height" => R::I(b.height.into()),
"depth" => R::I(b.depth.into()),
"shift" => R::I(b.shift.into()),
"dir" => R::S(dir_name(b.dir).into()),
"direction" => R::I(b.dir.into()),
"glue_order" => R::I(b.glue_order.into()),
"glue_sign" => R::I(b.glue_sign.into()),
"glue_set" => R::F(b.glue_set),
"head" | "list" => R::N(b.head),
_ => R::Nil,
},
NodeData::Glyph(g) => match key {
"char" => R::I(g.chr.into()),
"font" => R::I(g.font.into()),
"lang" => R::I(g.lang.into()),
"left" => R::I(g.left.into()),
"right" => R::I(g.right.into()),
"uchyph" => R::I(i64::from(g.uchyph)),
"components" => R::N(g.components),
"xoffset" => R::I(g.xoffset.into()),
"yoffset" => R::I(g.yoffset.into()),
"expansion_factor" => R::I(g.expansion_factor.into()),
"data" => R::I(g.data.into()),
"width" => R::I(e.font_char_dimen(g.font, g.chr, 0).into()),
"height" => R::I(e.font_char_dimen(g.font, g.chr, 1).into()),
"depth" => R::I(e.font_char_dimen(g.font, g.chr, 2).into()),
_ => R::Nil,
},
NodeData::Glue { spec, leader } => match key {
"width" => R::I(spec.width.into()),
"stretch" => R::I(spec.stretch.into()),
"shrink" => R::I(spec.shrink.into()),
"stretch_order" => R::I(spec.stretch_order.into()),
"shrink_order" => R::I(spec.shrink_order.into()),
"leader" => R::N(*leader),
_ => R::Nil,
},
NodeData::Kern {
kern,
expansion_factor,
} => match key {
"kern" | "width" => R::I((*kern).into()),
"expansion_factor" => R::I((*expansion_factor).into()),
_ => R::Nil,
},
NodeData::Penalty { penalty } => match key {
"penalty" => R::I((*penalty).into()),
_ => R::Nil,
},
NodeData::MarginKern { width, glyph } => match key {
"width" => R::I((*width).into()),
"glyph" => R::N(*glyph),
_ => R::Nil,
},
NodeData::Rule {
width,
depth,
height,
dir,
index,
left,
right,
} => match key {
"width" => R::I((*width).into()),
"depth" => R::I((*depth).into()),
"height" => R::I((*height).into()),
"dir" => R::S(dir_name(*dir).into()),
"direction" => R::I((*dir).into()),
"index" => R::I((*index).into()),
"left" => R::I((*left).into()),
"right" => R::I((*right).into()),
_ => R::Nil,
},
NodeData::Disc {
pre,
post,
replace,
penalty,
} => match key {
"pre" => R::N(*pre),
"post" => R::N(*post),
"replace" => R::N(*replace),
"penalty" => R::I((*penalty).into()),
_ => R::Nil,
},
NodeData::Boundary { value } => match key {
"value" => R::I((*value).into()),
_ => R::Nil,
},
NodeData::Whatsit(w) => {
let w = (**w).clone();
whatsit_field(e, &w, key)
}
NodeData::Mark { class, mark } => match key {
"class" => R::I((*class).into()),
"mark" => {
let mark = mark.clone();
R::S(e.print_to_string(|e| e.show_token_list(&mark, 0, mark.len(), i64::MAX)))
}
_ => R::Nil,
},
NodeData::Ins {
cost,
depth,
height,
head,
..
} => match key {
"cost" => R::I((*cost).into()),
"depth" => R::I((*depth).into()),
"height" => R::I((*height).into()),
"head" | "list" => R::N(*head),
_ => R::Nil,
},
NodeData::Adjust { head } => match key {
"head" | "list" => R::N(*head),
_ => R::Nil,
},
NodeData::Dir { dir, level } => match key {
"dir" => {
let sign = if n.subtype == 0 { "+" } else { "-" };
R::S(format!("{sign}{}", dir_name(*dir)).into_bytes())
}
"direction" => R::I((*dir).into()),
"level" => R::I((*level).into()),
_ => R::Nil,
},
NodeData::LocalPar { dir, .. } => match key {
"dir" => R::S(dir_name(*dir).into()),
"direction" => R::I((*dir).into()),
_ => R::Nil,
},
NodeData::Math { surround, glue } => match key {
"surround" => R::I((*surround).into()),
"width" => R::I(glue.width.into()),
"stretch" => R::I(glue.stretch.into()),
"shrink" => R::I(glue.shrink.into()),
"stretch_order" => R::I(glue.stretch_order.into()),
"shrink_order" => R::I(glue.shrink_order.into()),
_ => R::Nil,
},
NodeData::Noad(d) => match key {
"nucleus" => R::N(d.nucleus),
"sub" => R::N(d.sub),
"sup" => R::N(d.sup),
"options" => R::I(d.options.into()),
"left" => R::N(d.left),
"degree" => R::N(d.degree),
"width" => R::I(d.width.into()),
"accent" | "top_accent" if d.options & noad_opt::OVERLAY == 0 => R::N(d.accent),
"overlay_accent" if d.options & noad_opt::OVERLAY != 0 => R::N(d.accent),
"bot_accent" => R::N(d.bot_accent),
"fraction" => R::I(d.fraction.into()),
_ => R::Nil,
},
NodeData::Fraction(d) => match key {
"width" => R::I(d.thickness.into()),
"num" => R::N(d.num),
"denom" => R::N(d.denom),
"left" => R::N(d.left),
"right" => R::N(d.right),
"middle" => R::N(d.middle),
_ => R::Nil,
},
NodeData::Fence(d) => match key {
"delim" => R::N(d.delim),
"italic" => R::I(d.italic.into()),
"height" => R::I(d.height.into()),
"depth" => R::I(d.depth.into()),
"options" => R::I(d.options.into()),
"class" => R::I(d.class.into()),
_ => R::Nil,
},
NodeData::Style { style } => match key {
"style" => R::S(
crate::texlib::MATH_STYLE_NAMES
.get(usize::from(*style))
.copied()
.unwrap_or("display")
.as_bytes()
.to_vec(),
),
_ => R::Nil,
},
NodeData::Choice(c) => match key {
"display" => R::N(c[0]),
"text" => R::N(c[1]),
"script" => R::N(c[2]),
"scriptscript" => R::N(c[3]),
_ => R::Nil,
},
NodeData::MathChar { fam, chr } => match key {
"fam" => R::I((*fam).into()),
"char" => R::I((*chr).into()),
_ => R::Nil,
},
NodeData::SubList { head } => match key {
"head" | "list" => R::N(*head),
_ => R::Nil,
},
NodeData::Delim {
small_fam,
small_char,
large_fam,
large_char,
} => match key {
"small_fam" => R::I((*small_fam).into()),
"small_char" => R::I((*small_char).into()),
"large_fam" => R::I((*large_fam).into()),
"large_char" => R::I((*large_char).into()),
_ => R::Nil,
},
_ => R::Nil,
}
})?;
Ok(match r {
R::I(i) => Value::Integer(i),
R::N(n) => wrap(lua, n, direct)?,
R::F(f) => Value::Number(f),
R::S(s) => Value::String(lua.create_string(&s)?),
R::L(n) => lua_refs_synced(lua)?.raw_get(n)?,
R::Nil => Value::Nil,
})
}
fn dir_name(d: i8) -> &'static str {
retex_engine::showbox::DIR_NAMES[i32::from(d).rem_euclid(4) as usize]
}
fn dir_value(v: &Value) -> (i8, Option<bool>) {
match v {
Value::String(s) => {
let b = s.as_bytes();
let (cancel, name) = match b.first() {
Some(b'+') => (Some(false), &b[1..]),
Some(b'-') => (Some(true), &b[1..]),
_ => (None, &b[..]),
};
let d = retex_engine::showbox::DIR_NAMES
.iter()
.position(|n| n.as_bytes() == name)
.unwrap_or(0);
(d as i8, cancel)
}
_ => (int(v).clamp(0, 3) as i8, None),
}
}
fn set_field(lua: &Lua, p: NodeRef, key: &str, v: Value) -> mlua::Result<()> {
if !matches!(key, "next" | "prev" | "subtype" | "attr")
&& with_engine(lua, |e| e.nodes.id(p) == id::WHATSIT)?
{
return set_whatsit_field(lua, p, key, v);
}
if key == "attr" {
return set_attr_value(lua, p, &v);
}
let r = to_ref(&v).unwrap_or(NodeRef::NULL);
let i = int(&v);
let style_value = match &v {
Value::String(name) => crate::texlib::MATH_STYLE_NAMES
.iter()
.position(|n| n.as_bytes() == name.as_bytes().as_ref())
.unwrap_or(0) as i64,
_ => i.clamp(0, 7),
};
let f = match &v {
Value::Number(x) => *x,
Value::Integer(x) => *x as f64,
_ => 0.0,
};
with_engine(lua, |e| {
let n = e.nodes.get_mut(p);
match key {
"next" => {
n.next = r;
return;
}
"prev" => {
n.prev = r;
return;
}
"subtype" if n.id == id::STYLE => return,
"subtype" => {
n.subtype = i as u16;
return;
}
"dir" | "direction" if n.id == id::DIR => {
let (d, cancel) = dir_value(&v);
if let Some(c) = cancel {
n.subtype = u16::from(c);
}
if let NodeData::Dir { dir, .. } = &mut n.data {
*dir = d;
}
return;
}
"dir" | "direction" if n.id == id::LOCAL_PAR => {
if let NodeData::LocalPar { dir, .. } = &mut n.data {
*dir = dir_value(&v).0;
}
return;
}
_ => {}
}
match &mut n.data {
NodeData::Box(b) | NodeData::Unset { b, .. } => match key {
"width" => b.width = i as i32,
"height" => b.height = i as i32,
"depth" => b.depth = i as i32,
"shift" => b.shift = i as i32,
"dir" | "direction" => b.dir = dir_value(&v).0,
"glue_order" => b.glue_order = i as u8,
"glue_sign" => b.glue_sign = i as u8,
"glue_set" => b.glue_set = f64::from(f as f32),
"head" | "list" => b.head = r,
_ => {}
},
NodeData::Glyph(g) => match key {
"char" => g.chr = i as u32,
"font" => g.font = i as u32,
"lang" => g.lang = i as u16,
"left" => g.left = i as u8,
"right" => g.right = i as u8,
"uchyph" => g.uchyph = i != 0,
"components" => g.components = r,
"xoffset" => g.xoffset = i as i32,
"yoffset" => g.yoffset = i as i32,
"expansion_factor" => g.expansion_factor = i as i32,
"data" => g.data = i as i32,
_ => {}
},
NodeData::Glue { spec, leader } => match key {
"width" => spec.width = i as i32,
"stretch" => spec.stretch = i as i32,
"shrink" => spec.shrink = i as i32,
"stretch_order" => spec.stretch_order = i as u8,
"shrink_order" => spec.shrink_order = i as u8,
"leader" => *leader = r,
_ => {}
},
NodeData::Kern {
kern,
expansion_factor,
} => {
if key == "kern" || key == "width" {
*kern = i as i32;
} else if key == "expansion_factor" {
*expansion_factor = i as i32;
}
}
NodeData::MarginKern { width, glyph } => match key {
"width" => *width = i as i32,
"glyph" => *glyph = r,
_ => {}
},
NodeData::Penalty { penalty } => {
if key == "penalty" {
*penalty = i as i32;
}
}
NodeData::Rule {
width,
depth,
height,
dir,
..
} => match key {
"dir" | "direction" => *dir = dir_value(&v).0,
"width" => *width = i as i32,
"depth" => *depth = i as i32,
"height" => *height = i as i32,
_ => {}
},
NodeData::Disc {
pre,
post,
replace,
penalty,
} => match key {
"pre" => *pre = r,
"post" => *post = r,
"replace" => *replace = r,
"penalty" => *penalty = i as i32,
_ => {}
},
NodeData::Boundary { value } => {
if key == "value" {
*value = i as i32;
}
}
NodeData::Mark { class, .. } => {
if key == "class" {
*class = i as u32;
}
}
NodeData::Noad(d) => match key {
"nucleus" => d.nucleus = r,
"sub" => d.sub = r,
"sup" => d.sup = r,
"options" => d.options = i as u32,
"left" => d.left = r,
"degree" => d.degree = r,
"width" => d.width = i as i32,
"accent" | "top_accent" => {
d.accent = r;
d.options &= !noad_opt::OVERLAY;
}
"overlay_accent" => {
d.accent = r;
d.options |= noad_opt::OVERLAY;
}
"bot_accent" => d.bot_accent = r,
"fraction" => d.fraction = i as i32,
_ => {}
},
NodeData::Fraction(d) => match key {
"width" => d.thickness = i as i32,
"num" => d.num = r,
"denom" => d.denom = r,
"left" => d.left = r,
"right" => d.right = r,
"middle" => d.middle = r,
_ => {}
},
NodeData::Fence(d) => match key {
"delim" => d.delim = r,
"italic" => d.italic = i as i32,
"height" => d.height = i as i32,
"depth" => d.depth = i as i32,
"options" => d.options = i as u32,
"class" => d.class = i as i32,
_ => {}
},
NodeData::Style { style } => {
if key == "style" {
*style = style_value as u8;
n.subtype = u16::from(*style);
}
}
NodeData::Choice(c) => match key {
"display" => c[0] = r,
"text" => c[1] = r,
"script" => c[2] = r,
"scriptscript" => c[3] = r,
_ => {}
},
NodeData::MathChar { fam, chr } => match key {
"fam" => *fam = i as u32,
"char" => *chr = i as u32,
_ => {}
},
NodeData::SubList { head } => {
if key == "head" || key == "list" {
*head = r;
}
}
NodeData::Delim {
small_fam,
small_char,
large_fam,
large_char,
} => match key {
"small_fam" => *small_fam = i as u32,
"small_char" => *small_char = i as u32,
"large_fam" => *large_fam = i as u32,
"large_char" => *large_char = i as u32,
_ => {}
},
NodeData::Ins { head, .. } | NodeData::Adjust { head } => {
if key == "head" || key == "list" {
*head = r;
}
}
NodeData::Math { surround, glue } => match key {
"surround" => *surround = i as i32,
"width" => glue.width = i as i32,
"stretch" => glue.stretch = i as i32,
"shrink" => glue.shrink = i as i32,
"stretch_order" => glue.stretch_order = i as u8,
"shrink_order" => glue.shrink_order = i as u8,
_ => {}
},
_ => {}
}
})
}
pub fn install(lua: &Lua) -> mlua::Result<()> {
let node = lua.create_table()?;
let direct = lua.create_table()?;
for (t, is_direct) in [(&node, false), (&direct, true)] {
install_common(lua, t, is_direct)?;
}
direct.set(
"todirect",
lua.create_function(|_, v: Value| Ok(to_ref(&v).map(|p| i64::from(p.0))))?,
)?;
direct.set(
"tonode",
lua.create_function(|lua, v: Value| match to_ref(&v) {
Some(p) => wrap(lua, p, false),
None => Ok(Value::Nil),
})?,
)?;
install_properties(lua, &node, &direct)?;
require_nodes(lua, &node)?;
node.set("direct", direct)?;
lua.globals().set("node", node)?;
lua.load(ARGUMENT_COUNT_CHECKS)
.set_name("=[retex node checks]")
.exec()?;
Ok(())
}
#[derive(Clone, Copy)]
struct NodeArg {
pos: usize,
absent_ok: bool,
nil_ok: bool,
}
const STRICT: NodeArg = NodeArg {
pos: 0,
absent_ok: false,
nil_ok: false,
};
const NIL_OK: NodeArg = NodeArg {
pos: 0,
absent_ok: false,
nil_ok: true,
};
const ABSENT_OK: NodeArg = NodeArg {
pos: 0,
absent_ok: true,
nil_ok: false,
};
const NOTHING_OK: NodeArg = NodeArg {
pos: 0,
absent_ok: true,
nil_ok: true,
};
const NODE_ARGUMENT: &[(&str, NodeArg)] = &[
("hpack", STRICT),
("vpack", STRICT),
("remove", STRICT),
("mlist_to_hlist", STRICT),
("has_attribute", STRICT),
("set_attribute", STRICT),
("unset_attribute", STRICT),
("protect_glyphs", STRICT),
("protect_glyph", STRICT),
("unprotect_glyphs", STRICT),
("unprotect_glyph", STRICT),
("flatten_discretionaries", STRICT),
("is_char", STRICT),
("is_glyph", STRICT),
("has_glyph", STRICT),
("getglue", STRICT),
("setglue", STRICT),
("is_zero_glue", STRICT),
("hyphenating", STRICT),
("write", ABSENT_OK),
("first_glyph", ABSENT_OK),
("kerning", ABSENT_OK),
("ligaturing", ABSENT_OK),
("copy", NIL_OK),
("copy_list", NIL_OK),
("tail", NIL_OK),
("length", NIL_OK),
("slide", NIL_OK),
("end_of_math", NIL_OK),
("traverse", NIL_OK),
("traverse_char", NIL_OK),
("traverse_glyph", NIL_OK),
("count", NodeArg { pos: 1, ..NIL_OK }),
("traverse_id", NodeArg { pos: 1, ..NIL_OK }),
("free", NOTHING_OK),
("flush_node", NOTHING_OK),
("flush_list", NOTHING_OK),
];
const ARGUMENT_COUNT_CHECKS: &str = r##"
local node, direct, select, error = node, node.direct, select, error
local function check(t, name, n, msg)
local f = t[name]
if f == nil then return end
t[name] = function(...)
if select("#", ...) < n then error(msg, 2) end
return f(...)
end
end
check(node, "remove", 2, "Not enough arguments for node.remove()")
check(node, "insert_after", 3, "Not enough arguments for node.insert_after()")
check(node, "insert_before", 3, "Not enough arguments for node.insert_before()")
check(node, "set_attribute", 3, "incorrect number of arguments")
check(node, "unset_attribute", 2,
"bad argument #2 to 'unset_attribute' (number expected, got no value)")
do
local f = node.unset_attribute
node.unset_attribute = function(...)
local id = select(2, ...)
if select("#", ...) >= 2 and type(id) ~= "number" then
error("bad argument #2 to 'unset_attribute' (number expected, got " .. type(id) .. ")", 2)
end
return f(...)
end
end
check(node, "dimensions", 1, "missing argument to 'dimensions' (node expected)")
check(node, "rangedimensions", 2,
"missing argument to 'rangedimensions' (2 or more nodes expected)")
check(direct, "dimensions", 1, "missing argument to 'dimensions' (direct node expected)")
check(direct, "rangedimensions", 2,
"missing argument to 'rangedimensions' (2 or more direct nodes expected)")
"##;
fn require_nodes(lua: &Lua, node: &Table) -> mlua::Result<()> {
for &(name, rule) in NODE_ARGUMENT {
let Ok(f) = node.get::<mlua::Function>(name) else {
continue;
};
let checked = lua.create_function(move |lua, args: MultiValue| {
let arg = args.get(rule.pos).cloned();
let fine = match &arg {
None => rule.absent_ok,
Some(Value::Nil) => rule.nil_ok,
Some(Value::UserData(u)) => {
u.is::<LNode>() || (u.is::<LAttr>() && name.starts_with("traverse"))
}
Some(_) => false,
};
if !fine {
return Err(match &arg {
None => {
let msg = "lua <node> expected, not an object with type no value";
match with_engine(lua, |e| e.luatex_fatal("node lib", msg)) {
Err(e) => e,
}
}
Some(v) => crate::texlib::not_a_node(lua, v),
});
}
if arg.is_none() {
return Ok(MultiValue::from_iter([Value::Nil]));
}
f.call::<MultiValue>(args)
})?;
node.set(name, checked)?;
}
for name in ["dimensions", "rangedimensions"] {
if let Ok(f) = node.get::<mlua::Function>(name) {
let checked = lua.create_function(move |lua, args: MultiValue| {
if let Some(v) = args.iter().find(|v| {
!matches!(v, Value::Integer(_) | Value::Number(_))
&& !matches!(v, Value::UserData(u) if u.is::<LNode>())
}) {
return Err(crate::texlib::not_a_node(lua, v));
}
f.call::<MultiValue>(args)
})?;
node.set(name, checked)?;
}
}
Ok(())
}
const PROPERTIES_KEY: &str = "retex.node.properties";
fn properties(lua: &Lua) -> mlua::Result<Table> {
let store: Table = match lua.named_registry_value::<Value>(PROPERTIES_KEY)? {
Value::Table(t) => t,
_ => {
let t = lua.create_table()?;
lua.set_named_registry_value(PROPERTIES_KEY, &t)?;
t
}
};
let events = with_engine(lua, |e| std::mem::take(&mut e.nodes.props.events))?;
for ev in events {
match ev {
retex_engine::nodes::PropEvent::Flush(n) => store.raw_set(n, Value::Nil)?,
retex_engine::nodes::PropEvent::Copy(from, to) => {
let v = match store.raw_get::<Value>(from)? {
Value::Table(t) => {
let copy = lua.create_table()?;
let mt = lua.create_table()?;
mt.set("__index", t)?;
copy.set_metatable(Some(mt))?;
Value::Table(copy)
}
_ => Value::Nil,
};
if v.is_nil() {
with_engine(lua, |e| e.nodes.props.nodes.remove(&to))?;
}
store.raw_set(to, v)?;
}
}
}
Ok(store)
}
fn set_property(lua: &Lua, p: NodeRef, v: Value) -> mlua::Result<()> {
let store = properties(lua)?;
with_engine(lua, |e| {
let props = &mut e.nodes.props;
if v.is_nil() {
props.nodes.remove(&p.0);
} else if props.flush || props.copy {
props.nodes.insert(p.0);
}
})?;
store.raw_set(p.0, v)
}
fn install_properties(lua: &Lua, node: &Table, direct: &Table) -> mlua::Result<()> {
for t in [node, direct] {
let proxy = lua.create_table()?;
let mt = lua.create_table()?;
mt.set(
"__index",
lua.create_function(|lua, (_t, k): (Value, Value)| match to_ref(&k) {
Some(p) => properties(lua)?.raw_get::<Value>(p.0),
None => Ok(Value::Nil),
})?,
)?;
mt.set(
"__newindex",
lua.create_function(|lua, (_t, k, v): (Value, Value, Value)| match to_ref(&k) {
Some(p) => set_property(lua, p, v),
None => Ok(()),
})?,
)?;
proxy.set_metatable(Some(mt))?;
t.set(
"get_properties_table",
lua.create_function(move |_, ()| Ok(proxy.clone()))?,
)?;
t.set(
"getproperty",
lua.create_function(|lua, v: Value| match to_ref(&v) {
Some(p) => properties(lua)?.raw_get::<Value>(p.0),
None => Ok(Value::Nil),
})?,
)?;
t.set(
"setproperty",
lua.create_function(|lua, (v, x): (Value, Value)| match to_ref(&v) {
Some(p) => set_property(lua, p, x),
None => Ok(()),
})?,
)?;
t.set(
"flush_properties_table",
lua.create_function(|lua, ()| {
let store = lua.create_table()?;
lua.set_named_registry_value(PROPERTIES_KEY, &store)?;
with_engine(lua, |e| {
e.nodes.props.nodes.clear();
e.nodes.props.events.clear();
})
})?,
)?;
t.set(
"set_properties_mode",
lua.create_function(|lua, (flush, copy): (Option<bool>, Option<bool>)| {
with_engine(lua, |e| {
e.nodes.props.flush = flush.unwrap_or(false);
e.nodes.props.copy = copy.unwrap_or(false);
})
})?,
)?;
}
Ok(())
}
fn install_common(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
t.set(
"types",
lua.create_function(|lua, ()| {
let out = lua.create_table()?;
for (k, v) in &info().names {
out.set(*k, v.as_str())?;
}
Ok(out)
})?,
)?;
t.set(
"whatsits",
lua.create_function(|lua, ()| {
let out = lua.create_table()?;
for (k, v) in &info().whatsits {
out.set(*k, v.as_str())?;
}
Ok(out)
})?,
)?;
t.set(
"id",
lua.create_function(|_, name: String| Ok(info().ids.get(&name).copied()))?,
)?;
t.set(
"subtype",
lua.create_function(|_, name: String| {
Ok(info()
.whatsits
.iter()
.find(|(_, n)| *n == name)
.map(|(k, _)| *k))
})?,
)?;
t.set(
"type",
lua.create_function(|lua, v: Value| match &v {
Value::Integer(n) if !matches!(v, Value::UserData(_)) => {
Ok(info().names.get(&(*n as u8)).cloned())
}
_ => match to_ref(&v) {
Some(p) => {
let t = with_engine(lua, |e| e.nodes.id(p))?;
Ok(info().names.get(&t).cloned())
}
None => Ok(None),
},
})?,
)?;
t.set(
"subtypes",
lua.create_function(|lua, v: Value| {
let idn = match &v {
Value::Integer(n) => *n as u8,
Value::String(s) => match info().ids.get(&*s.to_str()?) {
Some(i) => *i,
None => return Ok(Value::Nil),
},
_ => return Ok(Value::Nil),
};
let out = lua.create_table()?;
if let Some(st) = info().subtypes.get(&idn) {
for (k, n) in st {
out.set(*k, n.as_str())?;
}
}
Ok(Value::Table(out))
})?,
)?;
t.set(
"fields",
lua.create_function(|lua, (v, sub): (Value, Option<i64>)| {
let idn = match &v {
Value::Integer(n) => *n as u8,
Value::String(s) => match info().ids.get(&*s.to_str()?) {
Some(i) => *i,
None => return Ok(Value::Nil),
},
_ => return Ok(Value::Nil),
};
let list = if idn == id::WHATSIT {
sub.and_then(|s| info().whatsit_fields.get(&(s as u16)))
} else {
info().fields.get(&idn)
};
let out = lua.create_table()?;
if let Some(fl) = list {
for (k, n) in fl {
out.set(*k, n.as_str())?;
}
}
Ok(Value::Table(out))
})?,
)?;
t.set(
"is_node",
lua.create_function(move |_, v: Value| {
Ok(match &v {
Value::UserData(u) => u.borrow::<LNode>().is_ok(),
Value::Integer(n) => direct && *n > 0,
_ => false,
})
})?,
)?;
t.set(
"new",
lua.create_function(move |lua, (ty, sub): (Value, Option<Value>)| {
let t = match &ty {
Value::Integer(n) => *n as u8,
Value::String(s) => *info()
.ids
.get(&*s.to_str()?)
.ok_or_else(|| mlua::Error::runtime("unknown node type"))?,
_ => return Err(mlua::Error::runtime("node type expected")),
};
let st = match &sub {
Some(Value::Integer(n)) => *n as u16,
Some(Value::String(s)) => {
let s = s.to_str()?.to_owned();
info()
.whatsits
.iter()
.find(|(_, n)| *n == s)
.map(|(k, _)| *k)
.unwrap_or(0)
}
_ => 0,
};
let data = if t == id::WHATSIT {
NodeData::Whatsit(Box::new(new_whatsit(st)))
} else if t == id::STYLE {
NodeData::Style {
style: st.min(7) as u8,
}
} else {
new_data(t)
};
let p = with_engine(lua, |e| e.new_node(t, st, data))?;
wrap(lua, p, direct)
})?,
)?;
t.set(
"free",
lua.create_function(move |lua, v: Value| {
impossible(lua, &v, "free")?;
let Some(p) = to_ref(&v) else {
return Ok(Value::Nil);
};
let next = with_engine(lua, |e| {
let n = e.nodes.next(p);
e.nodes.free(p);
e.check_double_free();
n
})?;
if next.is_null() {
Ok(Value::Nil)
} else {
wrap(lua, next, direct)
}
})?,
)?;
t.set(
"flush_node",
lua.create_function(|lua, v: Value| {
impossible(lua, &v, "free")?;
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| {
e.nodes.free(p);
e.check_double_free();
})?;
}
Ok(())
})?,
)?;
t.set(
"flush_list",
lua.create_function(|lua, v: Value| {
impossible(lua, &v, "free")?;
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| {
e.nodes.flush_list(p);
e.check_double_free();
})?;
}
Ok(())
})?,
)?;
t.set(
"copy",
lua.create_function(move |lua, v: Value| {
match impossible(lua, &v, "copy").map(|()| to_ref(&v))? {
Some(p) => {
let q = with_engine(lua, |e| e.nodes.copy_node(p))?;
wrap(lua, q, direct)
}
None => Ok(Value::Nil),
}
})?,
)?;
t.set(
"copy_list",
lua.create_function(move |lua, v: Value| {
match impossible(lua, &v, "copy").map(|()| to_ref(&v))? {
Some(p) => {
let q = with_engine(lua, |e| e.nodes.copy_list(p))?;
wrap(lua, q, direct)
}
None => Ok(Value::Nil),
}
})?,
)?;
t.set(
"tail",
lua.create_function(move |lua, v: Value| match to_ref(&v) {
Some(p) => {
let q = with_engine(lua, |e| tail(e, p))?;
wrap(lua, q, direct)
}
None => Ok(Value::Nil),
})?,
)?;
t.set(
"slide",
lua.create_function(move |lua, v: Value| match to_ref(&v) {
Some(p) => {
let q = with_engine(lua, |e| slide(e, p))?;
wrap(lua, q, direct)
}
None => Ok(Value::Nil),
})?,
)?;
t.set(
"length",
lua.create_function(|lua, v: Value| match to_ref(&v) {
Some(p) => with_engine(lua, |e| count(e, p, None)),
None => Ok(0),
})?,
)?;
t.set(
"count",
lua.create_function(|lua, (ty, v): (i64, Value)| match to_ref(&v) {
Some(p) => with_engine(lua, |e| count(e, p, Some(ty as u8))),
None => Ok(0),
})?,
)?;
for (name, by_id) in [("traverse", false), ("traverse_id", true)] {
t.set(
name,
lua.create_function(move |lua, args: MultiValue| {
let v: Vec<Value> = args.into_iter().collect();
let (filter, start) = if by_id {
(
Some(int(v.first().unwrap_or(&Value::Nil)) as u8),
v.get(1).cloned(),
)
} else {
(None, v.first().cloned())
};
if let Some(Value::UserData(u)) = &start
&& u.is::<LAttr>()
{
let mut entries =
attr_entries(lua, u)?.into_iter().filter(move |[_, id, _]| {
filter.is_none_or(|f| *id == Value::Integer(i64::from(f)))
});
let iter = lua.create_function_mut(move |_, (_s, _cur): (Value, Value)| {
Ok(match entries.next() {
Some([n, _, sub]) if by_id => MultiValue::from_iter([n, sub]),
Some([n, id, sub]) => MultiValue::from_iter([n, id, sub]),
None => MultiValue::new(),
})
})?;
return Ok((iter, Value::Nil, Value::Nil));
}
let start = start.and_then(|s| to_ref(&s)).unwrap_or(NodeRef::NULL);
let iter = lua.create_function(move |lua, (_s, cur): (Value, Value)| {
let next = match to_ref(&cur) {
Some(c) => with_engine(lua, |e| e.nodes.next(c))?,
None => start,
};
let mut p = next;
let found = with_engine(lua, |e| {
while !p.is_null() {
if filter.is_none_or(|f| e.nodes.id(p) == f) {
return (p, e.nodes.id(p), e.nodes.get(p).subtype);
}
p = e.nodes.next(p);
}
(NodeRef::NULL, 0, 0)
})?;
if found.0.is_null() {
return Ok(MultiValue::new());
}
let n = wrap(lua, found.0, direct)?;
let sub = Value::Integer(i64::from(found.2));
Ok(if by_id {
MultiValue::from_iter([n, sub])
} else {
MultiValue::from_iter([n, Value::Integer(i64::from(found.1)), sub])
})
})?;
Ok((iter, Value::Nil, Value::Nil))
})?,
)?;
}
t.set(
"insert_after",
lua.create_function(move |lua, (h, c, n): (Value, Value, Value)| {
let (Some(n), head) = (to_ref(&n), to_ref(&h)) else {
return Ok(MultiValue::from_iter([h, Value::Nil]));
};
let cur = to_ref(&c);
let head = with_engine(lua, |e| match (head, cur) {
(None, _) | (_, None) => n,
(Some(h), Some(c)) => {
let nx = e.nodes.next(c);
let tail = tail(e, n);
e.nodes.link(c, n);
e.nodes.link(tail, nx);
h
}
})?;
Ok(MultiValue::from_iter([
wrap(lua, head, direct)?,
wrap(lua, n, direct)?,
]))
})?,
)?;
t.set(
"insert_before",
lua.create_function(move |lua, (h, c, n): (Value, Value, Value)| {
let Some(n) = to_ref(&n) else {
return Ok(MultiValue::from_iter([h, Value::Nil]));
};
let head = to_ref(&h);
let cur = to_ref(&c);
let head = with_engine(lua, |e| match (head, cur) {
(None, _) => n,
(Some(h), None) => {
let t = tail(e, h);
e.nodes.link(t, n);
h
}
(Some(h), Some(c)) => {
let pv = e.nodes.get(c).prev;
let tail = tail(e, n);
e.nodes.link(tail, c);
if c == h || pv.is_null() {
e.nodes.get_mut(n).prev = NodeRef::NULL;
n
} else {
e.nodes.link(pv, n);
h
}
}
})?;
Ok(MultiValue::from_iter([
wrap(lua, head, direct)?,
wrap(lua, n, direct)?,
]))
})?,
)?;
t.set(
"remove",
lua.create_function(move |lua, (h, c): (Value, Value)| {
let (Some(h), Some(c)) = (to_ref(&h), to_ref(&c)) else {
return Ok(MultiValue::from_iter([h, Value::Nil]));
};
let (head, next) = with_engine(lua, |e| {
let pv = e.nodes.get(c).prev;
let nx = e.nodes.next(c);
let head = if c == h {
nx
} else {
e.nodes.link(pv, nx);
h
};
if !nx.is_null() && c == h {
e.nodes.get_mut(nx).prev = NodeRef::NULL;
}
(head, nx)
})?;
Ok(MultiValue::from_iter([
wrap(lua, head, direct)?,
wrap(lua, next, direct)?,
]))
})?,
)?;
t.set(
"has_field",
lua.create_function(|lua, (v, f): (Value, String)| match to_ref(&v) {
Some(p) => {
let t = with_engine(lua, |e| e.nodes.id(p))?;
Ok(info()
.fields
.get(&t)
.is_some_and(|fl| fl.iter().any(|(_, n)| *n == f)))
}
None => Ok(false),
})?,
)?;
t.set(
"getfield",
lua.create_function(move |lua, (v, f): (Value, String)| match to_ref(&v) {
Some(p) => get_field_impl(lua, p, &f, direct),
None => Ok(Value::Nil),
})?,
)?;
t.set(
"setfield",
lua.create_function(|lua, (v, f, x): (Value, String, Value)| match to_ref(&v) {
Some(p) => set_field(lua, p, &f, x),
None => Ok(()),
})?,
)?;
for name in [
"id", "subtype", "next", "prev", "char", "font", "width", "height", "depth", "list",
"penalty", "kern", "leader",
] {
let key = if name == "list" { "head" } else { name };
if name != "id" && name != "subtype" {
t.set(
format!("get{name}"),
lua.create_function(move |lua, v: Value| match to_ref(&v) {
Some(p) => get_field_impl(lua, p, key, direct),
None => Ok(Value::Nil),
})?,
)?;
} else {
t.set(
format!("get{name}"),
lua.create_function(move |lua, v: Value| match to_ref(&v) {
Some(p) => get_field_impl(lua, p, key, direct),
None => Ok(Value::Nil),
})?,
)?;
}
t.set(
format!("set{name}"),
lua.create_function(move |lua, (v, x): (Value, Value)| match to_ref(&v) {
Some(p) => set_field(lua, p, key, x),
None => Ok(()),
})?,
)?;
}
install_accessors(lua, t, direct)?;
t.set(
"mlist_to_hlist",
lua.create_function(move |lua, args: MultiValue| {
let v: Vec<Value> = args.into_iter().collect();
let head = v.first().and_then(to_ref).unwrap_or(NodeRef::NULL);
with_engine(lua, |e| {
let mut p = head;
let mut steps = e.nodes.capacity();
while !p.is_null() && steps > 0 {
steps -= 1;
match e.nodes.id(p) {
id::GLYPH
| id::MATH
| id::MARGIN_KERN
| id::LOCAL_PAR
| id::DIR
| id::SUB_BOX
| id::UNSET
| id::GLUE_SPEC
| id::HLIST
| id::VLIST
| id::TEMP
| id::DELIM
| id::MATH_CHAR
| id::SUB_MLIST
| id::MATH_TEXT_CHAR
| 38 => e.confusion("mlist1"),
id::BOUNDARY => e.confusion("mlist3"),
_ => {}
}
p = e.nodes.next(p);
}
})?;
let style = match v.get(1) {
Some(Value::String(s)) => {
let s = s.to_str()?.to_string();
crate::texlib::MATH_STYLE_NAMES
.iter()
.position(|n| *n == s)
.unwrap_or(2) as i32
}
Some(Value::Integer(n)) => (*n as i32).clamp(0, 7),
_ => 2,
};
let pen = matches!(v.get(2), Some(Value::Boolean(true)));
if head.is_null() {
return Ok(Value::Nil);
}
let r = with_engine(lua, |e| e.mlist_to_hlist(head, style, pen))?;
if r.is_null() {
return Ok(Value::Nil);
}
wrap(lua, r, direct)
})?,
)?;
t.set(
"rangedimensions",
lua.create_function(|lua, args: MultiValue| {
let first = args.get(1).and_then(to_ref).unwrap_or(NodeRef::NULL);
let stop = args.get(2).and_then(to_ref).unwrap_or(NodeRef::NULL);
if first.is_null() {
return Ok((0, 0, 0));
}
with_engine(lua, |e| e.natural_dimensions(first, stop))
})?,
)?;
for (name, pass) in [
("ligaturing", retex_engine::nodeproc::NodePass::Ligaturing),
("kerning", retex_engine::nodeproc::NodePass::Kerning),
] {
t.set(
name,
lua.create_function(move |lua, args: MultiValue| {
let Some(h) = args.front().and_then(to_ref) else {
return Ok(MultiValue::from_iter([Value::Nil]));
};
let (h, tl) = with_engine(lua, |e| e.lua_node_pass(pass, h))?;
Ok(MultiValue::from_iter([
wrap(lua, h, direct)?,
wrap(lua, tl, direct)?,
Value::Boolean(true),
]))
})?,
)?;
}
for name in ["hyphenating", "flatten_discretionaries"] {
t.set(
name,
lua.create_function(move |_, args: MultiValue| Ok(args))?,
)?;
}
install_list_ops(lua, t, direct)?;
t.set(
"family_font",
lua.create_function(|lua, fam: i64| {
if !(0..retex_engine::eqtb::NUM_FAMILIES as i64).contains(&fam) {
return Ok(0);
}
with_engine(lua, |e| i64::from(e.fam_fnt(fam as u32, 0)))
})?,
)?;
t.set(
"dimensions",
lua.create_function(|lua, args: MultiValue| {
let refs: Vec<NodeRef> = args.iter().filter_map(to_ref).collect();
let Some(&first) = refs.first() else {
return Ok((0, 0, 0));
};
let stop = refs.get(1).copied().unwrap_or(NodeRef::NULL);
with_engine(lua, |e| e.natural_dimensions(first, stop))
})?,
)?;
t.set(
"has_attribute",
lua.create_function(|lua, (v, n, val): (Value, i64, Option<i64>)| {
let Some(p) = to_ref(&v) else {
return Ok(None);
};
let a = with_engine(lua, |e| {
e.nodes
.get(p)
.attr
.as_ref()
.and_then(|l| l.iter().find(|(k, _)| i64::from(*k) == n).map(|x| x.1))
})?;
Ok(match (a, val) {
(Some(x), Some(w)) if i64::from(x) != w => None,
(a, _) => a.map(i64::from),
})
})?,
)?;
t.set(
"set_attribute",
lua.create_function(|lua, (v, n, val): (Value, Value, Value)| {
let (n, val) = (to_integer(&n), to_integer(&val));
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| set_attr(e, p, n as u32, Some(val as i32)))?;
}
Ok(())
})?,
)?;
t.set(
"unset_attribute",
lua.create_function(|lua, (v, n): (Value, i64)| {
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| set_attr(e, p, n as u32, None))?;
}
Ok(())
})?,
)?;
Ok(())
}
fn to_integer(v: &Value) -> i64 {
let float = |f: f64| {
if f.fract() == 0.0 && f >= i64::MIN as f64 && f < i64::MAX as f64 {
f as i64
} else {
0
}
};
match v {
Value::Integer(n) => *n,
Value::Number(f) => float(*f),
Value::String(s) => match s.to_str() {
Ok(t) => {
let t = t.trim();
t.parse::<i64>()
.ok()
.or_else(|| t.parse::<f64>().ok().map(float))
.unwrap_or(0)
}
Err(_) => 0,
},
_ => 0,
}
}
fn set_attr(e: &mut Engine, p: NodeRef, n: u32, v: Option<i32>) {
let node = e.nodes.get_mut(p);
let mut list: Vec<(u32, i32)> = node.attr.as_ref().map(|l| l.to_vec()).unwrap_or_default();
list.retain(|(k, _)| *k != n);
if let Some(v) = v {
list.push((n, v));
list.sort();
}
node.attr = if list.is_empty() {
None
} else {
Some(std::rc::Rc::from(list))
};
}
fn impossible(lua: &Lua, v: &Value, what: &str) -> mlua::Result<()> {
if what == "free"
&& let Some(p) = to_ref(v)
&& with_engine(lua, |e| p == e.nodes.par_temp)?
{
let msg = format!("attempt to free an impossible node {}", p.0);
return match with_engine(lua, |e| e.luatex_fatal("nodes", &msg)) {
Err(e) => Err(e),
};
}
if let Value::Integer(n) = *v {
let cap = with_engine(lua, |e| e.nodes.capacity())?;
if n < 0 || n as u64 >= cap as u64 {
let msg = format!("attempt to {what} an impossible node {n}");
return match with_engine(lua, |e| e.luatex_fatal("nodes", &msg)) {
Err(e) => Err(e),
};
}
}
Ok(())
}
fn circular(e: &mut Engine) -> ! {
e.luatex_fatal("nodes", "circular node list")
}
fn tail(e: &mut Engine, p: NodeRef) -> NodeRef {
match e.nodes.bounded_tail(p) {
Some(t) => t,
None => circular(e),
}
}
fn slide(e: &mut Engine, mut p: NodeRef) -> NodeRef {
for _ in 0..e.nodes.capacity() {
let n = e.nodes.next(p);
if n.is_null() {
return p;
}
e.nodes.get_mut(n).prev = p;
p = n;
}
circular(e)
}
fn count(e: &mut Engine, mut p: NodeRef, ty: Option<u8>) -> i64 {
let mut n = 0;
let mut left = e.nodes.capacity();
while !p.is_null() {
if left == 0 {
circular(e);
}
left -= 1;
if ty.is_none_or(|t| e.nodes.id(p) == t) {
n += 1;
}
p = e.nodes.next(p);
}
n
}
fn set_fn<A, R, F>(lua: &Lua, t: &Table, name: &str, f: F) -> mlua::Result<()>
where
A: mlua::FromLuaMulti,
R: mlua::IntoLuaMulti,
F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
{
t.set(name, lua.create_function(f)?)
}
fn glyph_of(e: &Engine, p: NodeRef) -> Option<(u32, u32, u16)> {
match &e.nodes.get(p).data {
NodeData::Glyph(g) => Some((g.chr, g.font, e.nodes.get(p).subtype)),
_ => None,
}
}
type Pick = fn(&Engine, NodeRef) -> Option<Vec<Value>>;
fn list_iterator(
lua: &Lua,
start: NodeRef,
direct: bool,
pick: Pick,
) -> mlua::Result<(mlua::Function, Value, Value)> {
let iter = lua.create_function(move |lua, (_s, cur): (Value, Value)| {
let mut p = match to_ref(&cur) {
Some(c) => with_engine(lua, |e| e.nodes.next(c))?,
None => start,
};
let found = with_engine(lua, |e| {
while !p.is_null() {
if let Some(v) = pick(e, p) {
return Some((p, v));
}
p = e.nodes.next(p);
}
None
})?;
let Some((p, vals)) = found else {
return Ok(MultiValue::new());
};
let mut out = vec![wrap(lua, p, direct)?];
out.extend(vals);
Ok(MultiValue::from_iter(out))
})?;
Ok((iter, Value::Nil, Value::Nil))
}
fn install_accessors(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
for (name, key) in [
("data", "data"),
("lang", "lang"),
("fam", "fam"),
("sub", "sub"),
("sup", "sup"),
("nucleus", "nucleus"),
("shift", "shift"),
("dir", "dir"),
("direction", "direction"),
("components", "components"),
("expansion", "expansion_factor"),
] {
set_fn(
lua,
t,
&format!("get{name}"),
move |lua, v: Value| match to_ref(&v) {
Some(p) => get_field_impl(lua, p, key, direct),
None => Ok(Value::Nil),
},
)?;
set_fn(
lua,
t,
&format!("set{name}"),
move |lua, (v, x): (Value, Value)| match to_ref(&v) {
Some(p) => set_field(lua, p, key, x),
None => Ok(()),
},
)?;
}
if direct {
fn has_sync(id: u8) -> bool {
matches!(
id,
id::GLYPH | id::KERN | id::GLUE | id::MATH | id::HLIST | id::VLIST | id::RULE
)
}
set_fn(lua, t, "get_synctex_fields", |lua, v: Value| {
let Some(p) = to_ref(&v) else {
return Ok(MultiValue::new());
};
let f = with_engine(lua, |e| {
has_sync(e.nodes.id(p)).then(|| e.nodes.sync_fields(p))
})?;
Ok(match f {
Some((tag, line)) => MultiValue::from_iter([
Value::Integer(i64::from(tag)),
Value::Integer(i64::from(line)),
]),
None => MultiValue::new(),
})
})?;
set_fn(
lua,
t,
"set_synctex_fields",
|lua, (v, tag, line): (Value, Option<i64>, Option<i64>)| {
let Some(p) = to_ref(&v) else {
return Ok(());
};
with_engine(lua, |e| {
if !has_sync(e.nodes.id(p)) {
return;
}
let (mut t0, mut l0) = e.nodes.sync_fields(p);
if let Some(t) = tag.filter(|&t| t > 0) {
t0 = t as u32;
}
if let Some(l) = line.filter(|&l| l > 0) {
l0 = l as i32;
}
e.nodes.set_sync_fields(p, (t0, l0));
})
},
)?;
}
set_fn(lua, t, "current_attr", |lua, ()| {
match with_engine(lua, |e| e.cur_attr())? {
Some(a) => Ok(Value::UserData(lua.create_userdata(LAttr(a))?)),
None => Ok(Value::Nil),
}
})?;
set_fn(lua, t, "getattributelist", |lua, v: Value| {
match to_ref(&v) {
Some(p) => attr_value(lua, p),
None => Ok(Value::Nil),
}
})?;
set_fn(
lua,
t,
"setattributelist",
|lua, (v, a): (Value, Value)| match to_ref(&v) {
Some(p) => set_attr_value(lua, p, &a),
None => Ok(()),
},
)?;
set_fn(lua, t, "getboth", move |lua, v: Value| {
let Some(p) = to_ref(&v) else {
return Ok((Value::Nil, Value::Nil));
};
let (pv, nx) = with_engine(lua, |e| (e.nodes.get(p).prev, e.nodes.next(p)))?;
Ok((wrap(lua, pv, direct)?, wrap(lua, nx, direct)?))
})?;
set_fn(
lua,
t,
"setboth",
|lua, (v, a, b): (Value, Value, Value)| {
if let Some(p) = to_ref(&v) {
let a = to_ref(&a).unwrap_or(NodeRef::NULL);
let b = to_ref(&b).unwrap_or(NodeRef::NULL);
with_engine(lua, |e| {
let n = e.nodes.get_mut(p);
n.prev = a;
n.next = b;
})?;
}
Ok(())
},
)?;
set_fn(lua, t, "setlink", move |lua, args: MultiValue| {
let refs: Vec<NodeRef> = args.iter().filter_map(to_ref).collect();
with_engine(lua, |e| {
for w in refs.windows(2) {
e.nodes.link(w[0], w[1]);
}
})?;
match refs.first() {
Some(p) => wrap(lua, *p, direct),
None => Ok(Value::Nil),
}
})?;
set_fn(lua, t, "setsplit", |lua, (a, b): (Value, Value)| {
let (a, b) = (to_ref(&a), to_ref(&b));
with_engine(lua, |e| {
if let Some(a) = a {
e.nodes.get_mut(a).next = NodeRef::NULL;
}
if let Some(b) = b {
e.nodes.get_mut(b).prev = NodeRef::NULL;
}
})
})?;
set_fn(
lua,
t,
"getwhd",
move |lua, (v, ex): (Value, Option<bool>)| {
let Some(p) = to_ref(&v) else {
return Ok(MultiValue::new());
};
let mut out = Vec::new();
for k in ["width", "height", "depth"] {
out.push(get_field_impl(lua, p, k, direct)?);
}
if ex == Some(true) {
out.push(get_field_impl(lua, p, "expansion_factor", direct)?);
}
Ok(MultiValue::from_iter(out))
},
)?;
set_fn(
lua,
t,
"setwhd",
|lua, (v, w, h, d): (Value, Value, Value, Value)| {
if let Some(p) = to_ref(&v) {
for (k, x) in [("width", w), ("height", h), ("depth", d)] {
if !x.is_nil() {
set_field(lua, p, k, x)?;
}
}
}
Ok(())
},
)?;
set_fn(
lua,
t,
"getdisc",
move |lua, (v, tails): (Value, Option<bool>)| {
let Some(p) = to_ref(&v) else {
return Ok(MultiValue::new());
};
let lists = with_engine(lua, |e| match &e.nodes.get(p).data {
NodeData::Disc {
pre, post, replace, ..
} => {
let l = [*pre, *post, *replace];
let t = l.map(|h| if h.is_null() { h } else { e.nodes.tail_of(h) });
Some((l, t))
}
_ => None,
})?;
let Some((l, tl)) = lists else {
return Ok(MultiValue::new());
};
let mut out = Vec::new();
for h in l {
out.push(wrap(lua, h, direct)?);
}
if tails == Some(true) {
for h in tl {
out.push(wrap(lua, h, direct)?);
}
}
Ok(MultiValue::from_iter(out))
},
)?;
set_fn(lua, t, "setdisc", |lua, args: MultiValue| {
let v: Vec<Value> = args.into_iter().collect();
let Some(p) = v.first().and_then(to_ref) else {
return Ok(());
};
let get = |i: usize| v.get(i).and_then(to_ref).unwrap_or(NodeRef::NULL);
let (pre, post, rep) = (get(1), get(2), get(3));
let sub = v.get(4).filter(|x| !x.is_nil()).map(int);
let pen = v.get(5).filter(|x| !x.is_nil()).map(int);
with_engine(lua, |e| {
if e.nodes.id(p) != id::DISC {
return;
}
e.nodes.set_disc_lists(p, (pre, post, rep));
if let Some(s) = sub {
e.nodes.get_mut(p).subtype = s as u16;
}
if let (Some(x), NodeData::Disc { penalty, .. }) = (pen, &mut e.nodes.get_mut(p).data) {
*penalty = x as i32;
}
})
})?;
set_fn(lua, t, "getglue", |lua, v: Value| {
let Some(p) = to_ref(&v) else {
return Ok(MultiValue::new());
};
let vals: Vec<Value> = with_engine(lua, |e| match &e.nodes.get(p).data {
NodeData::Glue { spec: g, .. } | NodeData::Math { glue: g, .. } => vec![
Value::Integer(g.width.into()),
Value::Integer(g.stretch.into()),
Value::Integer(g.shrink.into()),
Value::Integer(g.stretch_order.into()),
Value::Integer(g.shrink_order.into()),
],
NodeData::Box(b) => vec![
Value::Number(b.glue_set),
Value::Integer(b.glue_order.into()),
Value::Integer(b.glue_sign.into()),
],
_ => Vec::new(),
})?;
Ok(MultiValue::from_iter(vals))
})?;
set_fn(lua, t, "setglue", |lua, args: MultiValue| {
let v: Vec<Value> = args.into_iter().collect();
let Some(p) = v.first().and_then(to_ref) else {
return Ok(());
};
let n = |i: usize| v.get(i).map(int).unwrap_or(0);
with_engine(lua, |e| {
let g = match &mut e.nodes.get_mut(p).data {
NodeData::Glue { spec, .. } => spec,
NodeData::Math { glue, .. } => glue,
_ => return,
};
g.width = n(1) as i32;
g.stretch = n(2) as i32;
g.shrink = n(3) as i32;
g.stretch_order = n(4) as u8;
g.shrink_order = n(5) as u8;
})
})?;
set_fn(lua, t, "is_zero_glue", |lua, v: Value| {
let Some(p) = to_ref(&v) else {
return Ok(false);
};
with_engine(lua, |e| match &e.nodes.get(p).data {
NodeData::Glue { spec: g, .. } | NodeData::Math { glue: g, .. } => {
g.width == 0 && g.stretch == 0 && g.shrink == 0
}
_ => true,
})
})?;
set_fn(lua, t, "getoffsets", |lua, v: Value| {
let Some(p) = to_ref(&v) else {
return Ok((Value::Nil, Value::Nil));
};
with_engine(lua, |e| match &e.nodes.get(p).data {
NodeData::Glyph(g) => (
Value::Integer(g.xoffset.into()),
Value::Integer(g.yoffset.into()),
),
_ => (Value::Nil, Value::Nil),
})
})?;
set_fn(
lua,
t,
"setoffsets",
|lua, (v, x, y): (Value, Value, Value)| {
if let Some(p) = to_ref(&v) {
if !x.is_nil() {
set_field(lua, p, "xoffset", x)?;
}
if !y.is_nil() {
set_field(lua, p, "yoffset", y)?;
}
}
Ok(())
},
)?;
set_fn(lua, t, "is_char", |lua, (v, f): (Value, Option<i64>)| {
let Some(p) = to_ref(&v) else {
return Ok(Value::Nil);
};
Ok(match with_engine(lua, |e| glyph_of(e, p))? {
Some((c, font, sub)) if sub < 256 && f.is_none_or(|f| f == i64::from(font)) => {
Value::Integer(c.into())
}
Some(_) => Value::Boolean(false),
None => Value::Nil,
})
})?;
set_fn(lua, t, "is_glyph", |lua, v: Value| {
let Some(p) = to_ref(&v) else {
return Ok(MultiValue::from_iter([Value::Boolean(false)]));
};
Ok(match with_engine(lua, |e| glyph_of(e, p))? {
Some((c, font, _)) => {
MultiValue::from_iter([Value::Integer(c.into()), Value::Integer(font.into())])
}
None => MultiValue::from_iter([Value::Boolean(false)]),
})
})?;
set_fn(lua, t, "traverse_char", move |lua, v: Value| {
let start = to_ref(&v).unwrap_or(NodeRef::NULL);
list_iterator(lua, start, direct, |e, p| match glyph_of(e, p) {
Some((c, f, s)) if s < 256 => {
Some(vec![Value::Integer(c.into()), Value::Integer(f.into())])
}
_ => None,
})
})?;
set_fn(lua, t, "traverse_glyph", move |lua, v: Value| {
let start = to_ref(&v).unwrap_or(NodeRef::NULL);
list_iterator(lua, start, direct, |e, p| {
glyph_of(e, p).map(|(c, f, _)| vec![Value::Integer(c.into()), Value::Integer(f.into())])
})
})?;
set_fn(lua, t, "traverse_list", move |lua, v: Value| {
let start = to_ref(&v).unwrap_or(NodeRef::NULL);
let (iter, s, c) = list_iterator(lua, start, direct, |e, p| {
let n = e.nodes.get(p);
match &n.data {
NodeData::Box(b) if n.id == id::HLIST || n.id == id::VLIST => Some(vec![
Value::Integer(n.id.into()),
Value::Integer(n.subtype.into()),
Value::Integer(i64::from(b.head.0)),
]),
_ => None,
}
})?;
let wrapper = lua
.load(
"local iter, direct = ...
return function(s, c)
local n, i, st, l = iter(s, c)
if n == nil then return nil end
if l == 0 then l = nil elseif not direct then l = node.direct.tonode(l) end
return n, i, st, l
end",
)
.set_name("=[retex]")
.call::<mlua::Function>((iter, direct))?;
Ok((wrapper, s, c))
})?;
set_fn(lua, t, "has_glyph", move |lua, v: Value| {
let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
let found = with_engine(lua, |e| {
while !p.is_null() {
let i = e.nodes.id(p);
if i == id::GLYPH || i == id::DISC {
return p;
}
p = e.nodes.next(p);
}
NodeRef::NULL
})?;
wrap(lua, found, direct)
})?;
set_fn(
lua,
t,
"first_glyph",
move |lua, (v, stop): (Value, Value)| {
let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
let stop = to_ref(&stop);
let found = with_engine(lua, |e| {
while !p.is_null() {
if glyph_of(e, p).is_some_and(|(_, _, s)| s & 0xFF == 1) {
return p;
}
if Some(p) == stop {
break;
}
p = e.nodes.next(p);
}
NodeRef::NULL
})?;
if direct && found.is_null() {
return Ok(Value::Integer(0));
}
wrap(lua, found, direct)
},
)?;
set_fn(lua, t, "get_attribute", |lua, (v, n): (Value, i64)| {
let Some(p) = to_ref(&v) else {
return Ok(None);
};
with_engine(lua, |e| {
e.nodes.get(p).attr.as_ref().and_then(|l| {
l.iter()
.find(|(k, _)| i64::from(*k) == n)
.map(|x| i64::from(x.1))
})
})
})?;
set_fn(
lua,
t,
"find_attribute",
move |lua, (v, n): (Value, i64)| {
let mut p = to_ref(&v).unwrap_or(NodeRef::NULL);
let found =
with_engine(lua, |e| {
while !p.is_null() {
if let Some(x) =
e.nodes.get(p).attr.as_ref().and_then(|l| {
l.iter().find(|(k, _)| i64::from(*k) == n).map(|x| x.1)
})
{
return Some((x, p));
}
p = e.nodes.next(p);
}
None
})?;
match found {
Some((x, p)) => Ok(MultiValue::from_iter([
Value::Integer(x.into()),
wrap(lua, p, direct)?,
])),
None => Ok(MultiValue::new()),
}
},
)?;
set_fn(
lua,
t,
"effective_glue",
|lua, (g, parent): (Value, Value)| {
let (Some(g), Some(b)) = (to_ref(&g), to_ref(&parent)) else {
return Ok(Value::Nil);
};
with_engine(lua, |e| {
let spec = match &e.nodes.get(g).data {
NodeData::Glue { spec, .. } => *spec,
_ => return Value::Nil,
};
let Some(bx) = e.nodes.box_data(b) else {
return Value::Number(spec.width.into());
};
let mut w = f64::from(spec.width);
if bx.glue_sign == 1 && spec.stretch_order == bx.glue_order {
w += bx.glue_set * f64::from(spec.stretch);
} else if bx.glue_sign == 2 && spec.shrink_order == bx.glue_order {
w -= bx.glue_set * f64::from(spec.shrink);
}
Value::Number(w.round())
})
},
)?;
for (name, protect) in [("protect_glyph", true), ("unprotect_glyph", false)] {
set_fn(lua, t, name, move |lua, v: Value| {
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| {
if e.nodes.id(p) == id::GLYPH {
let n = e.nodes.get_mut(p);
let s = n.subtype;
n.subtype = match (protect, s) {
(true, 1) => 256,
(true, s) if s <= 256 => s + 256,
(false, s) if s > 256 => s - 256,
(_, s) => s,
};
}
})?;
}
Ok(())
})?;
}
set_fn(lua, t, "uses_font", |lua, (v, f): (Value, i64)| {
let Some(p) = to_ref(&v) else {
return Ok(false);
};
with_engine(lua, |e| {
let uses = |e: &Engine, mut q: NodeRef| {
while !q.is_null() {
if glyph_of(e, q).is_some_and(|(_, font, _)| i64::from(font) == f) {
return true;
}
q = e.nodes.next(q);
}
false
};
match &e.nodes.get(p).data {
NodeData::Glyph(g) => i64::from(g.font) == f,
NodeData::Disc {
pre, post, replace, ..
} => uses(e, *pre) || uses(e, *post) || uses(e, *replace),
_ => false,
}
})
})?;
set_fn(lua, t, "getbox", move |lua, n: i64| {
let p = with_engine(lua, |e| {
match e.eqtb.get(retex_engine::eqtb::Loc::Box(n as u32)) {
retex_engine::eqtb::Val::Node(p) => p,
_ => NodeRef::NULL,
}
})?;
wrap(lua, p, direct)
})?;
set_fn(lua, t, "setbox", |lua, args: MultiValue| {
let mut v: Vec<Value> = args.into_iter().collect();
let global = matches!(v.first(), Some(Value::String(s)) if &*s.to_str()? == "global");
if global {
v.remove(0);
}
let n = v.first().map(int).unwrap_or(0) as u32;
let p = v.get(1).and_then(to_ref).unwrap_or(NodeRef::NULL);
with_engine(lua, |e| {
e.define(
global,
retex_engine::eqtb::Loc::Box(n),
retex_engine::eqtb::Val::Node(p),
)
})
})?;
if direct {
set_fn(lua, t, "is_direct", |_, v: Value| {
Ok(match v {
Value::Integer(n) if n > 0 => Value::Integer(n),
_ => Value::Boolean(false),
})
})?;
} else {
for (name, next) in [("next", true), ("prev", false)] {
set_fn(lua, t, name, move |lua, v: Value| {
let Some(p) = to_ref(&v) else {
return Ok(Value::Nil);
};
let q = with_engine(lua, |e| {
if next {
e.nodes.next(p)
} else {
e.nodes.get(p).prev
}
})?;
wrap(lua, q, false)
})?;
}
}
Ok(())
}
const LUA_REFS_KEY: &str = "retex.node.luarefs";
fn lua_refs(lua: &Lua) -> mlua::Result<Table> {
match lua.named_registry_value::<Value>(LUA_REFS_KEY)? {
Value::Table(t) => Ok(t),
_ => {
let t = lua.create_table()?;
lua.set_named_registry_value(LUA_REFS_KEY, &t)?;
Ok(t)
}
}
}
pub(crate) fn lua_refs_synced(lua: &Lua) -> mlua::Result<Table> {
let refs = lua_refs(lua)?;
let copies = with_engine(lua, |e| std::mem::take(&mut e.nodes.lua_refs.copies))?;
for (new, old) in copies {
let v: Value = refs.raw_get(old)?;
refs.raw_set(new, v)?;
}
Ok(refs)
}
pub(crate) fn lua_ref(lua: &Lua, v: Value) -> mlua::Result<i32> {
let refs = lua_refs_synced(lua)?;
let n = with_engine(lua, |e| e.nodes.lua_refs.take())?;
refs.raw_set(n, v)?;
Ok(n)
}
fn new_whatsit(sub: u16) -> retex_engine::nodes::Whatsit {
use retex_engine::nodes::{LateData, UserValue, Whatsit, whatsit_sub as ws};
match sub {
ws::SPECIAL => Whatsit::Special { data: Vec::new() },
ws::LATE_LUA => Whatsit::LateLua {
data: LateData::Bytes(Vec::new()),
name: Vec::new(),
},
ws::PDF_LITERAL | ws::PDF_LATE_LITERAL => Whatsit::PdfLiteral {
mode: 0,
data: LateData::Bytes(Vec::new()),
late: sub == ws::PDF_LATE_LITERAL,
},
ws::PDF_COLORSTACK => Whatsit::PdfColorstack {
stack: 0,
cmd: 0,
data: Vec::new(),
},
ws::PDF_SETMATRIX => Whatsit::PdfSetmatrix { data: Vec::new() },
ws::PDF_SAVE => Whatsit::PdfSave,
ws::PDF_RESTORE => Whatsit::PdfRestore,
ws::PDF_REFOBJ => Whatsit::PdfRefobj { objnum: 0 },
ws::PDF_END_LINK => Whatsit::PdfEndLink,
ws::PDF_START_LINK => Whatsit::PdfStartLink {
dims: [0; 3],
objnum: 0,
attr: Vec::new(),
action: retex_engine::pdfext::PdfAction::User(Vec::new()),
},
ws::PDF_ANNOT => Whatsit::PdfAnnot {
dims: [0; 3],
objnum: 0,
data: Vec::new(),
},
ws::SAVE_POS => Whatsit::SavePos,
ws::PDF_LINK_STATE => Whatsit::PdfLinkState { state: 0 },
ws::USER_DEFINED => Whatsit::UserDefined {
user_id: 0,
value: UserValue::Int(0),
},
_ => Whatsit::Other {
fields: Vec::new(),
data: Vec::new(),
},
}
}
fn dim_index(key: &str) -> usize {
match key {
"width" => 0,
"height" => 1,
_ => 2,
}
}
fn whatsit_field(e: &mut Engine, w: &retex_engine::nodes::Whatsit, key: &str) -> R {
use retex_engine::nodes::{LateData, UserValue, Whatsit};
let mut late = |d: &LateData| match d {
LateData::Bytes(b) | LateData::Lua { data: b, .. } => R::S(b.clone()),
LateData::Tokens(t) => {
R::S(e.print_to_string(|e| e.show_token_list(t, 0, t.len(), i64::MAX)))
}
LateData::Function { reference } => R::L(*reference),
};
match (w, key) {
(Whatsit::Special { data }, "data") => R::S(data.clone()),
(Whatsit::LateLua { data, .. }, "data" | "string") => late(data),
(Whatsit::LateLua { name, .. }, "name") => R::S(name.clone()),
(Whatsit::PdfLiteral { mode, .. }, "mode") => R::I((*mode).into()),
(Whatsit::PdfLiteral { data, .. }, "data") => late(data),
(Whatsit::PdfColorstack { stack, .. }, "stack") => R::I((*stack).into()),
(Whatsit::PdfColorstack { cmd, .. }, "command") => R::I((*cmd).into()),
(Whatsit::PdfColorstack { data, .. }, "data") => R::S(data.clone()),
(Whatsit::PdfSetmatrix { data }, "data") => R::S(data.clone()),
(Whatsit::PdfRefobj { objnum }, "objnum") => R::I((*objnum).into()),
(Whatsit::PdfStartLink { objnum, .. } | Whatsit::PdfAnnot { objnum, .. }, "objnum") => {
R::I((*objnum).into())
}
(Whatsit::PdfStartLink { attr, .. }, "link_attr") => R::S(attr.clone()),
(Whatsit::PdfAnnot { data, .. }, "data") => R::S(data.clone()),
(
Whatsit::PdfStartLink { dims, .. } | Whatsit::PdfAnnot { dims, .. },
"width" | "height" | "depth",
) => R::I(dims[dim_index(key)].into()),
(Whatsit::PdfLinkState { state }, "objnum") => R::I((*state).into()),
(Whatsit::UserDefined { user_id, .. }, "user_id") => R::I((*user_id).into()),
(Whatsit::UserDefined { value, .. }, "type") => R::I(i64::from(user_type(value))),
(Whatsit::UserDefined { value, .. }, "value") => match value {
UserValue::Int(n) => R::I((*n).into()),
UserValue::Str(s) => R::S(s.clone()),
UserValue::Tokens(t) => {
R::S(e.print_to_string(|e| e.show_token_list(t, 0, t.len(), i64::MAX)))
}
UserValue::Nodes(p) => R::N(*p),
UserValue::Lua(n) => R::L(*n),
UserValue::Attr => R::Nil,
},
_ => R::Nil,
}
}
fn user_type(v: &retex_engine::nodes::UserValue) -> u8 {
use retex_engine::nodes::UserValue;
match v {
UserValue::Int(_) => b'd',
UserValue::Str(_) => b's',
UserValue::Tokens(_) => b't',
UserValue::Nodes(_) => b'n',
UserValue::Lua(_) => b'l',
UserValue::Attr => b'a',
}
}
fn set_whatsit_field(lua: &Lua, p: NodeRef, key: &str, v: Value) -> mlua::Result<()> {
use retex_engine::nodes::{LateData, Whatsit};
let s: Option<Vec<u8>> = match &v {
Value::String(s) => Some(s.as_bytes().to_vec()),
Value::Integer(n) => Some(n.to_string().into_bytes()),
Value::Number(n) => Some(crate::texlib::lua_number_string(*n).into_bytes()),
_ => None,
};
let node = to_ref(&v);
let i = int(&v);
let (is_user, utype) = with_engine(lua, |e| match &e.nodes.get(p).data {
NodeData::Whatsit(w) => match &**w {
Whatsit::UserDefined { value, .. } => (true, user_type(value)),
_ => (false, 0),
},
_ => (false, 0),
})?;
let lref = if is_user && key == "value" && utype == b'l' {
Some(lua_ref(lua, v.clone())?)
} else {
None
};
let ini = with_engine(lua, |e| e.ini_version)?;
let lua_string = |s: Vec<u8>| -> mlua::Result<LateData> {
if ini {
Ok(LateData::Bytes(s))
} else {
let reference = lua_ref(lua, Value::String(lua.create_string(&s)?))?;
Ok(LateData::Lua { data: s, reference })
}
};
let late = match (&s, key) {
(Some(s), "data") => Some(lua_string(s.clone())?),
(None, "data" | "string") if matches!(v, Value::Function(_)) && late_lua(lua, p)? => {
Some(if ini {
LateData::Bytes(Vec::new())
} else {
LateData::Function {
reference: lua_ref(lua, v.clone())?,
}
})
}
_ => None,
};
with_engine(lua, |e| {
let tokens = |e: &Engine, s: &[u8]| -> retex_engine::token::TokenList {
let _ = e;
let toks: Vec<retex_engine::token::Token> = String::from_utf8_lossy(s)
.chars()
.map(|c| {
if c == ' ' {
retex_engine::token::Token::SPACE
} else {
retex_engine::token::Token::other(c as u32)
}
})
.collect();
std::rc::Rc::from(toks)
};
let toks = s.as_ref().map(|s| tokens(e, s));
let old_ref = e.nodes.lua_ref_of(p);
set_whatsit_data(e, p, key, &s, &late, &toks, node, i, lref);
if let Some(r) = old_ref
&& e.nodes.lua_ref_of(p) != Some(r)
{
e.nodes.lua_refs.release(r);
}
})
}
fn late_lua(lua: &Lua, p: NodeRef) -> mlua::Result<bool> {
with_engine(lua, |e| match &e.nodes.get(p).data {
NodeData::Whatsit(w) => matches!(&**w, retex_engine::nodes::Whatsit::LateLua { .. }),
_ => false,
})
}
#[allow(clippy::too_many_arguments)]
fn set_whatsit_data(
e: &mut Engine,
p: NodeRef,
key: &str,
s: &Option<Vec<u8>>,
late: &Option<retex_engine::nodes::LateData>,
toks: &Option<retex_engine::token::TokenList>,
node: Option<NodeRef>,
i: i64,
lref: Option<i32>,
) {
use retex_engine::nodes::{LateData, UserValue, Whatsit};
{
let NodeData::Whatsit(w) = &mut e.nodes.get_mut(p).data else {
return;
};
match (&mut **w, key) {
(Whatsit::Special { data }, "data") => {
if let Some(s) = &s {
*data = s.clone();
}
}
(Whatsit::LateLua { data, .. }, "data" | "string") => {
if let Some(l) = late.clone().or_else(|| s.clone().map(LateData::Bytes)) {
*data = l;
}
}
(Whatsit::LateLua { name, .. }, "name") => {
if let Some(s) = &s {
*name = s.clone();
}
}
(Whatsit::PdfLiteral { mode, .. }, "mode") => *mode = i as u8,
(Whatsit::PdfLiteral { data, .. }, "data") => {
if let Some(l) = late.clone() {
*data = l;
}
}
(Whatsit::PdfColorstack { stack, .. }, "stack") => *stack = i as i32,
(Whatsit::PdfColorstack { cmd, .. }, "command") => *cmd = i as u8,
(Whatsit::PdfColorstack { data, .. }, "data")
| (Whatsit::PdfSetmatrix { data }, "data") => {
if let Some(s) = &s {
*data = s.clone();
}
}
(Whatsit::PdfRefobj { objnum }, "objnum")
| (Whatsit::PdfStartLink { objnum, .. }, "objnum")
| (Whatsit::PdfAnnot { objnum, .. }, "objnum") => *objnum = i as i32,
(Whatsit::PdfStartLink { attr: data, .. }, "link_attr")
| (Whatsit::PdfAnnot { data, .. }, "data") => {
if let Some(s) = &s {
*data = s.clone();
}
}
(
Whatsit::PdfStartLink { dims, .. } | Whatsit::PdfAnnot { dims, .. },
"width" | "height" | "depth",
) => dims[dim_index(key)] = i as i32,
(Whatsit::PdfLinkState { state }, "value" | "objnum") => *state = i as i32,
(Whatsit::UserDefined { user_id, .. }, "user_id") => *user_id = i as i32,
(Whatsit::UserDefined { value, .. }, "type") => {
*value = match i as u8 {
b's' => UserValue::Str(Vec::new()),
b't' => UserValue::Tokens(retex_engine::token::empty_list()),
b'n' => UserValue::Nodes(NodeRef::NULL),
b'l' => UserValue::Lua(0),
b'a' => UserValue::Attr,
_ => UserValue::Int(0),
}
}
(Whatsit::UserDefined { value, .. }, "value") => {
*value = match user_type(value) {
b's' => UserValue::Str(s.clone().unwrap_or_default()),
b't' => UserValue::Tokens(
toks.clone().unwrap_or_else(retex_engine::token::empty_list),
),
b'n' => UserValue::Nodes(node.unwrap_or(NodeRef::NULL)),
b'l' => UserValue::Lua(lref.unwrap_or(0)),
b'a' => UserValue::Attr,
_ => UserValue::Int(i as i32),
}
}
_ => {}
}
}
}
fn install_list_ops(lua: &Lua, t: &Table, direct: bool) -> mlua::Result<()> {
use retex_engine::build::PackSpec;
for (name, horizontal) in [("hpack", true), ("vpack", false)] {
set_fn(lua, t, name, move |lua, args: MultiValue| {
let v: Vec<Value> = args.into_iter().collect();
let head = v.first().and_then(to_ref).unwrap_or(NodeRef::NULL);
let w = v.get(1).map(int).unwrap_or(0) as i32;
let exactly = matches!(v.get(2), Some(Value::String(s)) if &*s.to_str()? == "exactly");
let spec = if exactly {
PackSpec::Exactly(w)
} else {
PackSpec::Additional(w)
};
let dir = match v.get(3) {
Some(Value::String(s)) => match &*s.to_str()? {
"TLT" => 0,
"TRT" => 1,
"LTL" => 2,
"RTT" => 3,
_ => -1,
},
Some(Value::Integer(n)) => *n as i8,
_ => -1,
};
let (b, badness) = with_engine(lua, |e| {
e.pack_dir = dir;
let b = if horizontal {
e.hpack(head, spec)
} else {
e.vpack(head, spec)
};
e.pack_dir = -1;
(b, e.last_badness)
})?;
Ok((wrap(lua, b, direct)?, Value::Integer(badness.into())))
})?;
}
set_fn(lua, t, "write", |lua, v: Value| {
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| {
let tail = tail(e, p);
e.tail_append(p);
e.cur_list_mut().tail = tail;
})?;
}
Ok(())
})?;
set_fn(lua, t, "last_node", move |lua, ()| {
let p = with_engine(lua, |e| {
let l = e.cur_list();
let (head, tail) = (l.head, l.tail);
if tail == head {
return NodeRef::NULL;
}
let mut q = head;
while e.nodes.next(q) != tail {
q = e.nodes.next(q);
}
e.nodes.get_mut(q).next = NodeRef::NULL;
let n = e.nodes.get_mut(tail);
n.prev = NodeRef::NULL;
e.cur_list_mut().tail = q;
tail
})?;
wrap(lua, p, direct)
})?;
set_fn(lua, t, "end_of_math", move |lua, v: Value| {
let Some(mut p) = to_ref(&v) else {
return Ok(Value::Nil);
};
let found = with_engine(lua, |e| {
if e.nodes.id(p) == id::MATH && e.nodes.subtype(p) == 0 {
p = e.nodes.next(p);
}
while !p.is_null() {
if e.nodes.id(p) == id::MATH && e.nodes.subtype(p) == 1 {
return p;
}
p = e.nodes.next(p);
}
NodeRef::NULL
})?;
wrap(lua, found, direct)
})?;
for (name, protect) in [("protect_glyphs", true), ("unprotect_glyphs", false)] {
set_fn(lua, t, name, move |lua, (h, stop): (Value, Value)| {
let mut p = to_ref(&h).unwrap_or(NodeRef::NULL);
let stop = to_ref(&stop);
with_engine(lua, |e| {
while !p.is_null() {
if e.nodes.id(p) == id::GLYPH {
let n = e.nodes.get_mut(p);
let s = n.subtype;
n.subtype = match (protect, s) {
(true, 1) => 256,
(true, s) if s <= 256 => s + 256,
(false, s) if s > 256 => s - 256,
(_, s) => s,
};
}
if Some(p) == stop {
break;
}
p = e.nodes.next(p);
}
})
})?;
}
Ok(())
}