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};
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 t = with_engine(lua, |e| e.nodes.id(NodeRef(this.0)))?;
let name = info().names.get(&t).cloned().unwrap_or_default();
Ok(format!("<node {} < {} > nil : {} 0>", 0, this.0, name))
});
}
}
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,
}
}
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))?))
}
}
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 => 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::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> {
get_field_impl(lua, p, key, false)
}
fn get_field_impl(lua: &Lua, p: NodeRef, key: &str, direct: bool) -> mlua::Result<Value> {
enum R {
I(i64),
N(NodeRef),
F(f64),
S(Vec<u8>),
Nil,
}
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),
"attr" => return R::Nil,
_ => {}
}
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::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::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::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" => R::I((*dir).into()),
"level" => R::I((*level).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,
},
_ => 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::Nil => Value::Nil,
})
}
fn set_field(lua: &Lua, p: NodeRef, key: &str, v: Value) -> mlua::Result<()> {
let r = to_ref(&v).unwrap_or(NodeRef::NULL);
let i = int(&v);
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" => {
n.subtype = i as u16;
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" => b.dir = i as i8,
"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,
..
} => match key {
"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;
}
}
_ => {}
}
})
}
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),
})?,
)?;
node.set("direct", direct)?;
lua.globals().set("node", node)?;
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 p = with_engine(lua, |e| e.nodes.alloc(t, st, new_data(t)))?;
wrap(lua, p, direct)
})?,
)?;
t.set(
"free",
lua.create_function(|lua, v: Value| {
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| e.nodes.free(p))?;
}
Ok(())
})?,
)?;
t.set(
"flush_node",
lua.create_function(|lua, v: Value| {
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| e.nodes.free(p))?;
}
Ok(())
})?,
)?;
t.set(
"flush_list",
lua.create_function(|lua, v: Value| {
if let Some(p) = to_ref(&v) {
with_engine(lua, |e| e.nodes.flush_list(p))?;
}
Ok(())
})?,
)?;
t.set(
"copy",
lua.create_function(move |lua, v: Value| match 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 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| e.nodes.tail_of(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())
};
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());
}
Ok(MultiValue::from_iter([
wrap(lua, found.0, direct)?,
Value::Integer(i64::from(found.1)),
Value::Integer(i64::from(found.2)),
]))
})?;
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::new());
};
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);
e.nodes.link(c, n);
let tail = e.nodes.tail_of(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::new());
};
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 = e.nodes.tail_of(h);
e.nodes.link(t, n);
h
}
(Some(h), Some(c)) => {
let pv = e.nodes.get(c).prev;
let tail = e.nodes.tail_of(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::new());
};
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;
}
let n = e.nodes.get_mut(c);
n.next = NodeRef::NULL;
n.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(()),
})?,
)?;
}
for name in [
"mlist_to_hlist",
"hpack",
"vpack",
"rangedimensions",
"kerning",
"ligaturing",
"hyphenating",
"protect_glyphs",
"unprotect_glyphs",
"write",
"last_node",
"end_of_math",
"flatten_discretionaries",
] {
t.set(
name,
lua.create_function(move |_, args: MultiValue| Ok(args))?,
)?;
}
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, i64, i64)| {
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 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 slide(e: &mut Engine, mut p: NodeRef) -> NodeRef {
loop {
let n = e.nodes.next(p);
if n.is_null() {
return p;
}
e.nodes.get_mut(n).prev = p;
p = n;
}
}
fn count(e: &Engine, mut p: NodeRef, ty: Option<u8>) -> i64 {
let mut n = 0;
while !p.is_null() {
if ty.is_none_or(|t| e.nodes.id(p) == t) {
n += 1;
}
p = e.nodes.next(p);
}
n
}