use std::rc::Rc;
use mlua::{AnyUserData, Lua, Value};
use retex_engine::input::LuaItem;
use retex_engine::{Engine, LuaHost};
mod callback;
mod compat;
mod extras;
mod fontlib;
mod fontloaderlib;
mod fonttable;
mod kpselib;
mod langlib;
mod lfslib;
mod lualib;
pub mod luaotfload;
mod nestlib;
pub mod nodelib;
mod otfaux;
mod otfnodes;
mod pdflib;
mod siolib;
mod statuslib;
mod texiolib;
mod texlib;
mod tokenlib;
pub const ENGINE_KEY: &str = "retex.engine";
pub(crate) struct PrintStack(pub Vec<Vec<LuaItem>>);
impl Default for PrintStack {
fn default() -> Self {
Self(vec![Vec::new()])
}
}
pub(crate) fn with_engine<R>(lua: &Lua, f: impl FnOnce(&mut Engine) -> R) -> mlua::Result<R> {
let ud: AnyUserData = lua
.named_registry_value(ENGINE_KEY)
.map_err(|e| mlua::Error::runtime(format!("no engine: {e}")))?;
ud.borrow_mut_scoped::<Engine, R>(f)
.map_err(|e| mlua::Error::runtime(format!("engine borrow: {e}")))
}
const PLAIN_ERRORS_KEY: &str = "retex.plainerrors";
const SETNAME_KEY: &str = "retex.plainerrors.setname";
thread_local! {
static LAST_WRAPPED_ERROR: std::cell::RefCell<Option<String>> =
const { std::cell::RefCell::new(None) };
}
pub(crate) fn create_function_plain_errors<A, R, F>(lua: &Lua, f: F) -> mlua::Result<mlua::Function>
where
A: mlua::FromLuaMulti,
R: mlua::IntoLuaMulti,
F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
{
plain_errors(lua, f, 0)
}
pub(crate) fn create_function_caller_errors<A, R, F>(
lua: &Lua,
f: F,
) -> mlua::Result<mlua::Function>
where
A: mlua::FromLuaMulti,
R: mlua::IntoLuaMulti,
F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
{
plain_errors(lua, f, 2)
}
fn plain_errors<A, R, F>(lua: &Lua, f: F, level: u8) -> mlua::Result<mlua::Function>
where
A: mlua::FromLuaMulti,
R: mlua::IntoLuaMulti,
F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
{
let inner = lua.create_function(move |lua, args: A| {
let mut out = match f(lua, args) {
Ok(r) => r.into_lua_multi(lua)?,
Err(e) => {
let msg = lua.create_string(lua_error_message(&e))?;
return Ok(mlua::MultiValue::from_vec(vec![
Value::Boolean(false),
Value::String(msg),
]));
}
};
out.push_front(Value::Boolean(true));
Ok(out)
})?;
let key = format!("{PLAIN_ERRORS_KEY}{level}");
let factory: mlua::Function = match lua.named_registry_value::<Value>(&key)? {
Value::Function(f) => f,
_ => {
let f = lua
.load(format!(
"local f, setname = ...
local wrapper
local function finish(ok, ...)
if ok then return ... end
setname(wrapper)
error((...), {level})
end
wrapper = function(...) return finish(f(...)) end
return wrapper"
))
.set_name("=[retex]")
.into_function()?;
lua.set_named_registry_value(&key, &f)?;
f
}
};
let setname: mlua::Function = match lua.named_registry_value::<Value>(SETNAME_KEY)? {
Value::Function(f) => f,
_ => {
let f = lua
.load(
"local set = ...
return function(fn)
for k, v in pairs(package.loaded) do
if type(v) == 'table' then
for k2, v2 in pairs(v) do
if v2 == fn then
local n = k .. '.' .. tostring(k2)
if k == '_G' then n = tostring(k2) end
set(n)
return
end
end
end
end
set(nil)
end",
)
.set_name("=[retex]")
.into_function()?
.call::<mlua::Function>(lua.create_function(|_, n: Option<String>| {
LAST_WRAPPED_ERROR.with(|c| *c.borrow_mut() = n);
Ok(())
})?)?;
lua.set_named_registry_value(SETNAME_KEY, &f)?;
f
}
};
factory.call((inner, setname))
}
pub(crate) fn take_printed(lua: &Lua) -> Vec<LuaItem> {
lua.app_data_mut::<PrintStack>()
.map(|mut ps| ps.0.iter_mut().flat_map(std::mem::take).collect::<Vec<_>>())
.unwrap_or_default()
}
pub(crate) fn push_print(lua: &Lua, item: LuaItem) {
if let Some(mut ps) = lua.app_data_mut::<PrintStack>()
&& let Some(top) = ps.0.last_mut()
{
top.push(item);
}
}
pub struct LuaTexHost {
lua: Lua,
}
const PROFILER_START: &str = r#"
local samples, getinfo = {}, debug.getinfo
debug.sethook(function()
local i = getinfo(2, "S")
if i then
local k = i.short_src .. ":" .. i.linedefined
samples[k] = (samples[k] or 0) + 1
end
end, "", 1000)
__retex_profile = samples
"#;
const PROFILER_REPORT: &str = r#"
local s = __retex_profile
if not s then return end
debug.sethook()
local t, total = {}, 0
for k, n in pairs(s) do t[#t + 1] = {k, n}; total = total + n end
table.sort(t, function(a, b) return a[2] > b[2] end)
io.stderr:write(string.format("[lua profile] %d samples (1 per 1000 instructions)\n", total))
for i = 1, math.min(25, #t) do
io.stderr:write(string.format("[lua profile] %6d %5.1f%% %s\n", t[i][2], 100 * t[i][2] / total, t[i][1]))
end
"#;
impl Drop for LuaTexHost {
fn drop(&mut self) {
if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
let _ = self.lua.load(PROFILER_REPORT).exec();
}
}
}
impl LuaTexHost {
pub fn new() -> mlua::Result<Rc<Self>> {
#[allow(unsafe_code)]
let lua = unsafe {
Lua::unsafe_new_with(
mlua::StdLib::ALL,
mlua::LuaOptions::new().catch_rust_panics(false),
)
};
lua.set_app_data(PrintStack::default());
retex_engine::timing::mark("create: Lua state");
compat::install(&lua)?;
texlib::install(&lua)?;
texiolib::install(&lua)?;
lualib::install(&lua)?;
callback::install(&lua)?;
kpselib::install(&lua)?;
statuslib::install(&lua)?;
lfslib::install(&lua)?;
nodelib::install(&lua)?;
tokenlib::install(&lua)?;
extras::install(&lua)?;
luaotfload::install(&lua)?;
langlib::install(&lua)?;
fontlib::install(&lua)?;
fontloaderlib::install(&lua)?;
pdflib::install(&lua)?;
retex_engine::timing::mark("create: Lua libraries");
retex_lpeg::install(&lua)?;
retex_engine::timing::mark("create: LPeg");
siolib::install(&lua)?;
let arg = lua.create_table()?;
for (i, a) in std::env::args_os().enumerate() {
arg.raw_set(i as i64, lua.create_string(a.as_encoded_bytes())?)?;
}
lua.globals().set("arg", arg)?;
let output_directory = lua.create_function(|lua, ()| {
Ok(crate::with_engine(lua, |e| {
e.files
.output_dir
.as_ref()
.map(|d| d.to_string_lossy().into_owned())
})
.ok()
.flatten())
})?;
let private_output_directory = lua.create_function(|lua, ()| {
Ok(crate::with_engine(lua, |e| e.files.private_output_dir).unwrap_or(false))
})?;
lua.load(include_str!("shell.lua"))
.set_name("=shell")
.call::<()>((output_directory, private_output_directory))?;
lua.load(
"for _, n in ipairs({'callback', 'fio', 'font', 'fontloader', 'gzip', 'img',
'kpse', 'lang', 'lua', 'md5', 'mplib', 'node', 'pdf', 'pdfe', 'pdfscanner',
'sha2', 'sio', 'status', 'tex', 'texio', 'token', 'unicode', 'vf', 'zip',
'zlib'}) do
if type(_G[n]) == 'table' and package.loaded[n] == nil then
package.loaded[n] = _G[n]
end
end",
)
.set_name("=[retex]")
.exec()?;
if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
lua.load(PROFILER_START).set_name("=profiler").exec()?;
}
Ok(Rc::new(LuaTexHost { lua }))
}
pub fn lua(&self) -> &Lua {
&self.lua
}
pub fn attach(engine: &mut Engine) -> mlua::Result<Rc<Self>> {
let host = LuaTexHost::new()?;
engine.lua = Some(host.clone() as Rc<dyn LuaHost>);
Ok(host)
}
pub fn check_io_names(&self) -> mlua::Result<()> {
kpselib::check_io_names(&self.lua)
}
pub fn make_safer(&self) -> mlua::Result<()> {
self.lua.load(SAFER).set_name("=[retex]").exec()
}
pub fn set_script_args(&self, script: &str, args: &[String]) -> mlua::Result<()> {
let t = self.lua.create_table()?;
t.set(0, script)?;
for (i, a) in args.iter().enumerate() {
t.set(i as i64 + 1, a.as_str())?;
}
self.lua.globals().set("arg", t)
}
}
const SAFER: &str = "local function none() return nil end
for _, f in ipairs({'execute', 'remove', 'rename'}) do os[f] = none end
os.tempdir = none
io.popen = none
io.open = none
io.tmpfile = none
if lfs then
for _, f in ipairs({'mkdir', 'chdir', 'rmdir', 'touch'}) do lfs[f] = none end
end
if debug then debug.traceback = nil end";
pub fn luac(args: &[String]) -> Result<(), String> {
let mut out = "texluac.out".to_owned();
let (mut strip, mut parse_only) = (false, false);
let mut files = Vec::new();
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"-o" => out = it.next().ok_or("'-o' needs argument")?.clone(),
"-s" => strip = true,
"-p" => parse_only = true,
"-v" => println!("Lua 5.3.6 Copyright (C) 1994-2020 Lua.org, PUC-Rio"),
"-l" => return Err("listings (-l) are not supported".to_owned()),
"--" => files.extend(it.by_ref().cloned()),
s if s.starts_with('-') && s != "-" => {
return Err(format!("unrecognized option '{s}'"));
}
_ => files.push(a.clone()),
}
}
if files.is_empty() {
return Err("no input files given".to_owned());
}
if files.len() > 1 && !parse_only {
return Err("combining several files into one chunk is not supported".to_owned());
}
let lua = Lua::new();
let mut chunks = Vec::new();
for f in &files {
let code = if f == "-" {
let mut v = Vec::new();
std::io::Read::read_to_end(&mut std::io::stdin(), &mut v).map_err(|e| e.to_string())?;
v
} else {
std::fs::read(f).map_err(|e| format!("cannot open {f}: {e}"))?
};
let func = lua
.load(code)
.set_name(format!("@{f}"))
.into_function()
.map_err(|e| e.to_string())?;
chunks.push(func);
}
if parse_only {
return Ok(());
}
let Some(main) = chunks.pop() else {
return Ok(());
};
let bytes = main.dump(strip);
std::fs::write(&out, bytes).map_err(|e| format!("cannot write {out}: {e}"))
}
impl LuaTexHost {
fn lend<R>(
&self,
engine: &mut Engine,
f: impl FnOnce(&Lua) -> mlua::Result<R>,
) -> mlua::Result<R> {
let lua = &self.lua;
lua.scope(|scope| {
let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
let prev: Value = lua.named_registry_value(ENGINE_KEY)?;
lua.set_named_registry_value(ENGINE_KEY, ud)?;
let r = f(lua);
lua.set_named_registry_value(ENGINE_KEY, prev)?;
r
})
}
}
fn not_a_node(engine: &mut Engine, v: &Value) -> ! {
let ty = match v {
Value::Integer(_) => "number",
_ => v.type_name(),
};
engine.luatex_fatal(
"node lib",
&format!("lua <node> expected, not an object with type {ty}"),
)
}
fn live(engine: &Engine, p: retex_engine::nodes::NodeRef) -> retex_engine::nodes::NodeRef {
if engine.nodes.is_free(p) {
retex_engine::nodes::NodeRef::NULL
} else {
p
}
}
impl LuaHost for LuaTexHost {
fn run(&self, engine: &mut Engine, name: &str, code: &[u8]) -> Result<(), String> {
let lua = &self.lua;
if let Some(mut ps) = lua.app_data_mut::<PrintStack>() {
ps.0.push(Vec::new());
}
let chunk_name = format!("=[{name}]");
let result = lua.scope(|scope| {
let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
let prev: Value = lua.named_registry_value(ENGINE_KEY)?;
lua.set_named_registry_value(ENGINE_KEY, ud)?;
let r = lua.load(code).set_name(chunk_name.as_str()).exec();
lua.set_named_registry_value(ENGINE_KEY, prev)?;
r
});
let items = lua
.app_data_mut::<PrintStack>()
.and_then(|mut ps| if ps.0.len() > 1 { ps.0.pop() } else { None })
.unwrap_or_default();
engine.push_lua_output(items);
result.map_err(|e| lua_error_message(&e))
}
fn call_simple(&self, engine: &mut Engine, name: &str) -> Result<(), String> {
let Some(f) = callback::get(&self.lua, name) else {
return Ok(());
};
self.lend(engine, |_lua| f.call::<()>(()))
.map_err(|e| lua_error_message(&e))
}
fn call_with_string(&self, engine: &mut Engine, name: &str, arg: &str) -> Result<(), String> {
let Some(f) = callback::get(&self.lua, name) else {
return Ok(());
};
self.lend(engine, |_lua| f.call::<()>(arg))
.map_err(|e| lua_error_message(&e))
}
fn has_callback(&self, name: &str) -> bool {
if let Some((f, _)) = callback::hot(&self.lua, name) {
return f;
}
callback::get(&self.lua, name).is_some()
}
fn define_font(&self, engine: &mut Engine, name: &[u8], size: i32) -> Option<u32> {
let f = callback::get(&self.lua, "define_font")?;
let next_id = engine.fonts.fonts.len() as i64;
let name = self.lua.create_string(name).ok()?;
let r = self.lend(engine, |_lua| {
f.call::<Value>((name, i64::from(size), next_id))
});
match r {
Ok(Value::Integer(n)) if n > 0 => Some(n as u32),
Ok(Value::Number(n)) if n > 0.0 => Some(n as u32),
Ok(Value::Table(t)) => {
if matches!(t.raw_get::<Value>("name"), Ok(Value::Nil)) {
engine
.luatex_fatal("font", &format!("lua-loaded font '{next_id}' has no name!"));
}
let info = fonttable::font_from_table(&t).ok()?;
let f = engine.register_font(info);
engine.check_lua_loaded_font(f);
Some(f)
}
Ok(_) => None,
Err(e) => {
engine.lua_error(&lua_error_message(&e));
None
}
}
}
fn list_filter(
&self,
engine: &mut Engine,
callback_name: &str,
head: retex_engine::nodes::NodeRef,
group: &str,
) -> retex_engine::ListFilter {
use retex_engine::ListFilter;
let Some(f) = callback::get(&self.lua, callback_name) else {
return ListFilter::Keep(head);
};
let r = self.lend(engine, |lua| {
let h = if head.is_null() {
Value::Nil
} else {
Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
};
f.call::<Value>((h, group))
});
let r = match r {
Ok(Value::Boolean(false)) => return ListFilter::Flush,
Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
Ok(n) => retex_engine::nodes::NodeRef(n.0),
Err(_) => not_a_node(engine, &Value::UserData(u.clone())),
},
Ok(Value::Nil) => return ListFilter::Flush,
Ok(Value::Boolean(true)) => head,
Ok(other) => not_a_node(engine, &other),
Err(e) => {
engine.lua_error(&lua_error_message(&e));
head
}
};
ListFilter::Keep(live(engine, r))
}
fn pack_quality(
&self,
engine: &mut Engine,
cb: &str,
incident: &str,
detail: i32,
b: retex_engine::nodes::NodeRef,
first: i32,
last: i32,
) -> retex_engine::nodes::NodeRef {
use retex_engine::nodes::NodeRef;
let Some(f) = callback::get(&self.lua, cb) else {
return NodeRef::NULL;
};
let r = self.lend(engine, |lua| {
let h = Value::UserData(lua.create_userdata(nodelib::LNode(b.0))?);
f.call::<Value>((incident, detail, h, first, last))
});
match r {
Ok(Value::UserData(u)) if cb == "hpack_quality" => match u.borrow::<nodelib::LNode>() {
Ok(n) => live(engine, NodeRef(n.0)),
Err(_) => NodeRef::NULL,
},
Ok(Value::Nil) => NodeRef::NULL,
Ok(other) if cb == "hpack_quality" => not_a_node(engine, &other),
Ok(_) => NodeRef::NULL,
Err(e) => {
engine.lua_error(&lua_error_message(&e));
NodeRef::NULL
}
}
}
fn node_pass(&self, engine: &mut Engine, cb: &str, head: retex_engine::nodes::NodeRef) -> bool {
if callback::hot(&self.lua, cb).is_some_and(|(f, d)| !f && !d) {
return false;
}
let Ok(reg) = self
.lua
.named_registry_value::<mlua::Table>("retex.callbacks")
else {
return false;
};
match reg.get::<Value>(cb) {
Ok(Value::Boolean(false)) => true,
Ok(Value::Function(f)) => {
let first = engine.nodes.next(head);
if first.is_null() {
return true;
}
let tail = engine.nodes.tail_of(first);
let r = self.lend(engine, |lua| {
let h = Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?);
let t = Value::UserData(lua.create_userdata(nodelib::LNode(tail.0))?);
f.call::<()>((h, t))
});
if let Err(e) = r {
engine.lua_error(&lua_error_message(&e));
}
true
}
_ => false,
}
}
fn linebreak_filter(
&self,
engine: &mut Engine,
head: retex_engine::nodes::NodeRef,
display: bool,
) -> Option<retex_engine::nodes::NodeRef> {
let f = callback::get(&self.lua, "linebreak_filter")?;
let r = self.lend(engine, |lua| {
let h = if head.is_null() {
Value::Nil
} else {
Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
};
f.call::<Value>((h, display))
});
let r = match r {
Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
Ok(n) => Some(retex_engine::nodes::NodeRef(n.0)),
Err(_) => None,
},
Ok(_) => None,
Err(e) => {
engine.lua_error(&lua_error_message(&e));
None
}
};
r.map(|p| live(engine, p)).filter(|p| !p.is_null())
}
fn mlist_to_hlist(
&self,
engine: &mut Engine,
head: retex_engine::nodes::NodeRef,
style: i32,
penalties: bool,
) -> Option<retex_engine::nodes::NodeRef> {
let f = callback::get(&self.lua, "mlist_to_hlist")?;
let name = texlib::MATH_STYLE_NAMES
.get(style as usize)
.copied()
.unwrap_or("text");
let r = self.lend(engine, |lua| {
let h = if head.is_null() {
Value::Nil
} else {
Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
};
f.call::<Value>((h, name, penalties))
});
let r = match r {
Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
Ok(n) => Some(retex_engine::nodes::NodeRef(n.0)),
Err(_) => None,
},
Ok(v @ (Value::Integer(_) | Value::Number(_) | Value::String(_) | Value::Table(_))) => {
not_a_node(engine, &v)
}
Ok(_) => None,
Err(e) => {
engine.lua_error(&lua_error_message(&e));
None
}
};
r.map(|p| live(engine, p))
}
fn pipe_read(&self, engine: &mut Engine, cmd: &[u8]) -> Option<Vec<u8>> {
let cmd = self.lua.create_string(cmd).ok()?;
let f: mlua::Function = self
.lua
.load(
"local cmd = ...
local f = io.popen(cmd, 'r')
if not f then return nil end
local s = f:read('a')
f:close()
return s",
)
.set_name("=[retex]")
.into_function()
.ok()?;
match self.lend(engine, |_| f.call::<Value>(cmd)) {
Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
_ => None,
}
}
fn input_level_string(&self, engine: &mut Engine, n: i64) -> Option<Vec<u8>> {
let f = callback::get(&self.lua, "input_level_string")?;
let r = self.lend(engine, |_| f.call::<Value>(n));
match r {
Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
Ok(_) => None,
Err(e) => {
engine.lua_error(&lua_error_message(&e));
None
}
}
}
fn glyph_info(&self, engine: &mut Engine, p: retex_engine::nodes::NodeRef) -> Option<Vec<u8>> {
let f = callback::get(&self.lua, "glyph_info")?;
let r = self.lend(engine, |lua| {
let n = lua.create_userdata(nodelib::LNode(p.0))?;
f.call::<Value>(n)
});
match r {
Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
Ok(_) => None,
Err(e) => {
engine.lua_error(&lua_error_message(&e));
None
}
}
}
fn dump(&self, _engine: &mut Engine) -> Vec<u8> {
let mut out = retex_engine::format::Writer::default();
let mut entries: Vec<(i64, Vec<u8>)> = Vec::new();
if let Ok(lt) = self.lua.globals().get::<mlua::Table>("lua")
&& let Ok(bc) = lt.get::<mlua::Table>("bytecode")
{
for (k, v) in bc.pairs::<i64, mlua::Function>().flatten() {
entries.push((k, v.dump(false)));
}
}
entries.sort_by_key(|(k, _)| *k);
out.u(entries.len() as u64);
for (k, b) in entries {
out.i(k);
out.bytes(&b);
}
out.buf
}
fn undump(&self, _engine: &mut Engine, data: &[u8]) -> Result<(), String> {
if data.is_empty() {
return Ok(());
}
let mut r = retex_engine::format::Reader::new(data);
let n = r.u().map_err(|e| e.to_string())?;
let lt: mlua::Table = self.lua.globals().get("lua").map_err(|e| e.to_string())?;
let bc: mlua::Table = lt.get("bytecode").map_err(|e| e.to_string())?;
for _ in 0..n {
let k = r.i().map_err(|e| e.to_string())?;
let b = r.bytes().map_err(|e| e.to_string())?;
let f = self
.lua
.load(b)
.set_mode(mlua::ChunkMode::Binary)
.into_function()
.map_err(|e| e.to_string())?;
bc.set(k, f).map_err(|e| e.to_string())?;
}
Ok(())
}
}
pub fn lua_error_message(e: &mlua::Error) -> String {
match e {
mlua::Error::RuntimeError(s) => luatex_traceback(s),
mlua::Error::SyntaxError { message: s, .. } => s.clone(),
mlua::Error::CallbackError { cause, .. } => lua_error_message(cause),
other => other.to_string(),
}
}
fn luatex_traceback(s: &str) -> String {
let Some(at) = s.find("\nstack traceback:\n") else {
return s.to_string();
};
let (head, tb) = s.split_at(at + "\nstack traceback:\n".len());
let lines: Vec<&str> = tb.split('\n').collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut i = 0;
if lines
.first()
.is_some_and(|l| *l == "\t[C]: in ?" || l.starts_with("\t[C]: in metamethod "))
{
i = 1;
}
while i < lines.len() {
let l = lines[i];
let next_is_wrapper = lines
.get(i + 1)
.is_some_and(|n| n.starts_with("\t[retex]:"));
if l == "\t[C]: in function 'error'" && next_is_wrapper {
let name = LAST_WRAPPED_ERROR.with(|c| c.borrow().clone());
out.push(match name {
Some(n) => format!("\t[C]: in function '{n}'"),
None => "\t[C]: in ?".to_string(),
});
i += 1;
while i < lines.len()
&& (lines[i].starts_with("\t[retex]:") || lines[i] == "\t(...tail calls...)")
{
i += 1;
}
continue;
}
out.push(l.to_string());
i += 1;
}
format!("{head}{}", out.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arg_holds_the_command_line() {
let host = LuaTexHost::new().expect("host");
let (n, first): (i64, String) = host.lua().load("return #arg, arg[0]").eval().expect("arg");
assert_eq!(n as usize, std::env::args().count() - 1);
assert_eq!(first, std::env::args().next().unwrap());
}
#[test]
fn lpeg_is_installed() {
let host = LuaTexHost::new().expect("host");
let r: (i64, bool, String, bool) = host
.lua()
.load(
r#"return lpeg.match(lpeg.R"az"^1, "abc1"), package.loaded.lpeg == lpeg,
lpeg.version(), package.loaded.re == nil"#,
)
.eval()
.expect("lpeg");
assert_eq!(r, (4, true, "1.0.1".to_string(), true));
}
}