1use std::cell::RefCell;
18use std::collections::HashMap;
19use std::rc::Rc;
20use std::sync::Arc;
21
22use mlua::{Lua, Value};
23use retex_engine::Engine;
24use retex_engine::fonts::{CharMetrics, FontData, FontInfo, FontKind};
25use retex_engine::nodes::NodeRef;
26use retex_font::LoadedFont;
27
28use crate::with_engine;
29
30#[derive(Default)]
32pub struct FontState {
33 loader: Option<retex_font::FontLoader>,
34 ids: HashMap<(usize, bool), u32>,
41 legacy_design_size: Option<bool>,
46 db_announced: bool,
48}
49
50const MAIN_LUA: &str = r#"
53local native = ...
54local lb = luatexbase
55local function noop(...) return ... end
56local function log(cat, msg)
57 texio.write_nl("log", "luaotfload | " .. cat .. " : " .. msg)
58end
59luaotfload = luaotfload or {}
60luaotfload.define_font = native.define_font
61luaotfload.node_processor = native.node_processor
62luaotfload.version = luaotfload.version or "3.29"
63-- Letterspacing options (microtype replaces keepligature).
64luaotfload.letterspace = luaotfload.letterspace or
65 { keepligature = true, keeptogether = false, keepwordspacing = false }
66luaotfload.features = luaotfload.features or { defaults = {}, global = {} }
67local aux = luaotfload.aux or {}
68luaotfload.aux = aux
69for _, k in ipairs{"provides_script", "provides_language", "provides_feature",
70 "get_math_dimension", "font_has_glyph", "slot_of_name", "gid_of_name"} do
71 aux[k] = native["aux_" .. k]
72end
73aux.sprint_math_dimension = function(f, name)
74 tex.sprint(-2, tostring(aux.get_math_dimension(f, name)))
75end
76-- The parts of the fontloader's `fonts` table that packages touch.
77fonts = fonts or {}
78fonts.names = fonts.names or {}
79fonts.names.set_location_precedence = fonts.names.set_location_precedence or function() end
80fonts.hashes = fonts.hashes or {}
81fonts.hashes.identifiers = fonts.hashes.identifiers or setmetatable({}, {
82 __index = function(_, id) if type(id) == "number" then return font.getfont(id) end end })
83local initialized = false
84luaotfload.main = function()
85 if initialized then
86 log("load", "Luaotfload initialization requested but is already loaded, ignoring.")
87 return
88 end
89 initialized = true
90 lb.provides_module({ name = "luaotfload", date = "2024-12-03", version = "3.29",
91 description = "Lua based OpenType font support" })
92 lb.provides_module({ name = "lualibs", date = "2023-07-13", version = "2.76",
93 description = "ConTeXt Lua standard libraries." })
94 lb.provides_module({ name = "lualibs-extended", date = "2023-07-13", version = "2.76",
95 description = "ConTeXt Lua libraries -- extended collection." })
96 -- lualibs' global `utf` (luamml uses it).
97 if not utf then utf = native.lualibs_utf() end
98 local cache = native.cache_dir()
99 log("conf", 'Root cache directory is "' .. cache .. '".')
100 -- As luaotfload does before loading the fontloader, which wants the
101 -- attribute with index zero to be zero.
102 tex.attribute[0] = 0
103 -- luaotfload's set-up holds six Lua references (observed in the
104 -- numbers box displays show for Lua data).
105 native.reserve_lua_refs(6)
106 local loader = kpse.find_file("fontloader-2023-12-28.lua", "lua") or "fontloader-2023-12-28.lua"
107 log("init", 'Loading fontloader "fontloader-2023-12-28.lua" from kpse-resolved path "' .. loader .. '".')
108 -- As the fontloader does: node properties are flushed with their
109 -- nodes and inherited by copies, for good.
110 node.direct.set_properties_mode(true, true)
111 function node.direct.set_properties_mode() end
112 lb.new_attribute("luaotfload@noligature")
113 log("init", "Context OpenType loader version 3.134")
114 -- luaotfload's node processor runs the pre_shaping_filter and
115 -- post_shaping_filter callbacks around the shaping. They are only
116 -- called once something is added to them.
117 if not lb.callbacktypes.pre_shaping_filter then
118 lb.create_callback("pre_shaping_filter", "list")
119 lb.create_callback("post_shaping_filter", "reverselist")
120 end
121 local shape = native.node_processor
122 local dispatch = shape
123 local call_callback = lb.call_callback
124 local function with_shaping_filters(head, groupcode, _, _, direction)
125 if not direction then
126 direction = tex.get("textdirection")
127 else
128 direction = direction == "TRT" and 1 or 0
129 end
130 local result = call_callback("pre_shaping_filter", head, groupcode, direction)
131 if result == false then
132 return false
133 elseif result ~= true then
134 head = result
135 end
136 result = shape(head, groupcode)
137 if result == false then
138 node.flush_list(head)
139 return nil
140 elseif result ~= true then
141 head = result
142 end
143 result = call_callback("post_shaping_filter", head, groupcode, direction)
144 if result == false then
145 node.flush_list(head)
146 return nil
147 elseif result == true then
148 return head
149 else
150 return result
151 end
152 end
153 local add_to_callback = lb.add_to_callback
154 function lb.add_to_callback(name, ...)
155 if name == "pre_shaping_filter" or name == "post_shaping_filter" then
156 dispatch = with_shaping_filters
157 end
158 return add_to_callback(name, ...)
159 end
160 local function node_processor(head, groupcode, size, packtype, direction)
161 return dispatch(head, groupcode, size, packtype, direction)
162 end
163 lb.add_to_callback("pre_linebreak_filter", node_processor, "luaotfload.node_processor")
164 lb.add_to_callback("hpack_filter", node_processor, "luaotfload.node_processor")
165 lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.glyph_stream")
166 -- Font colours (`color=RRGGBB[AA]`): a font with one adds the colour
167 -- handler to the callbacks below, which brackets its glyphs with
168 -- colour stack 0 whatsits (observed; the alpha part is not supported).
169 local colors = {}
170 local color_handler
171 -- The colour literal for `value`, a colour in hex (RRGGBB, the alpha
172 -- of RRGGBBAA left out) or a name: the `luaotfload.parse_color`
173 -- callback (LaTeX's l3color) makes it if set, else hex is converted.
174 local function color_literal(value)
175 local hex = value:match("^%x%x%x%x%x%x$") or value:match("^(%x%x%x%x%x%x)%x%x$")
176 local ok, s = pcall(lb.call_callback, "luaotfload.parse_color", hex or value)
177 if ok and type(s) == "string" then return s end
178 if not hex then return nil end
179 local function c(i)
180 local v = tonumber(hex:sub(i, i + 1), 16) / 255
181 return (string.format("%.3f", v):gsub("0+$", ""):gsub("%.$", ""))
182 end
183 return c(1) .. " " .. c(3) .. " " .. c(5) .. " rg"
184 end
185 local function define_font(name, size, id)
186 local f = native.define_font(name, size, id)
187 if type(f) == "number" and type(name) == "string" then
188 local value = name:match("[:;]color={?([^;}]+)}?")
189 local literal = value and not colors[f] and color_literal(value)
190 if literal then
191 colors[f] = literal
192 if not color_handler then
193 color_handler = luaotfload.color_handler
194 lb.add_to_callback("post_linebreak_filter", color_handler, "luaotfload.color_handler")
195 lb.add_to_callback("hpack_filter", color_handler, "luaotfload.color_handler")
196 lb.add_to_callback("post_mlist_to_hlist_filter", color_handler, "luaotfload.color_handler")
197 end
198 end
199 end
200 return f
201 end
202 local GLYPH, HLIST, VLIST, WHATSIT = node.id("glyph"), node.id("hlist"), node.id("vlist"), node.id("whatsit")
203 local LINE = 1
204 local function stack(command, data)
205 local w = node.new("whatsit", "pdf_colorstack")
206 w.stack = 0
207 w.command = command
208 if data then w.data = data end
209 return w
210 end
211 -- Colours the glyphs of `head` starting with colour `current`; returns
212 -- the new head and the colour at its end. Paragraph lines are coloured
213 -- on their own; other boxes and whatsits end a coloured run.
214 local function colorize(head, current)
215 local n = head
216 while n do
217 local id = n.id
218 if id == GLYPH then
219 local c = colors[n.font]
220 if c ~= current then
221 local w = not c and stack(2) or stack(current and 0 or 1, c)
222 head = node.insert_before(head, n, w)
223 current = c
224 end
225 elseif id == HLIST and n.subtype == LINE then
226 if current then
227 head = node.insert_before(head, n, stack(2))
228 current = nil
229 end
230 local list, last = colorize(n.list, nil)
231 if last then
232 list = node.insert_after(list, node.tail(list), stack(2))
233 end
234 n.list = list
235 elseif id == HLIST or id == VLIST or id == WHATSIT then
236 if current then
237 head = node.insert_before(head, n, stack(2))
238 current = nil
239 end
240 end
241 n = n.next
242 end
243 return head, current
244 end
245 function luaotfload.color_handler(head)
246 if not head then return head end
247 local h, last = colorize(head, nil)
248 if last then
249 h = node.insert_after(h, node.tail(h), stack(2))
250 end
251 return h
252 end
253 lb.add_to_callback("define_font", define_font, "luaotfload.define_font")
254 lb.new_attribute("luaotfload_color_attribute")
255 log("conf", 'Root cache directory is "' .. cache .. '".')
256 lb.add_to_callback("find_opentype_file", noop, "luaotfload.harf.strip_prefix")
257 lb.add_to_callback("find_truetype_file", noop, "luaotfload.harf.strip_prefix")
258 lb.remove_from_callback("glyph_stream_provider", "luaotfload.glyph_stream")
259 lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.harf.glyphstream")
260 lb.add_to_callback("post_linebreak_filter", noop, "luaotfload.harf.finalize_vlist")
261 lb.add_to_callback("hpack_filter", noop, "luaotfload.harf.finalize_hlist")
262 lb.add_to_callback("wrapup_run", noop, "luaotfload.cleanup_files")
263 lb.add_to_callback("finish_pdffile", noop, "luaotfload.harf.finalize_unicode")
264 -- Box displays show the character as UTF-8 (observed: "^^@" for 0
265 -- and ^^^^^^ notation beyond Unicode).
266 lb.add_to_callback("glyph_info", function(n)
267 local c = n.char
268 if c == 0 then return "^^@" end
269 if c > 0x10FFFF then return string.format("^^^^^^%06x", c) end
270 return utf8.char(c)
271 end, "luaotfload.glyphinfo")
272 lb.new_attribute("luaotfload.letterspace_done")
273 lb.create_callback("luaotfload.patch_font", "simple", noop)
274 lb.create_callback("luaotfload.patch_font_unsafe", "simple", noop)
275 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_sscale_dimens")
276 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_font_index")
277 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.patch_cambria_domh")
278 lb.add_to_callback("luaotfload.patch_font_unsafe", noop, "luaotfload.aux.fixup_fontdata")
279 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_capheight")
280 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_xheight")
281 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.rewrite_fontname")
282 -- The colour interface of luaotfload's colors module, set up by
283 -- main() (l3backend checks for set_transparent_colorstack before
284 -- hooking into parse_color).
285 function luaotfload.add_colorscheme() end
286 function luaotfload.set_colorhandler() end
287 function luaotfload.set_transparenthandler() end
288 function luaotfload.set_transparent_colorstack() end
289 lb.create_callback("luaotfload.split_color", "exclusive", noop)
290 lb.create_callback("luaotfload.parse_color", "exclusive", noop)
291 lb.create_callback("luaotfload.parse_transparent", "exclusive", noop)
292 return true
293end
294return luaotfload
295"#;
296
297const UTF_LUA: &str = r#"
302local u = {}
303u.char = utf8.char
304u.len = utf8.len
305function u.byte(s)
306 if s and #s > 0 then return utf8.codepoint(s, 1) end
307end
308function u.sub(s, i, j)
309 local n = utf8.len(s) or 0
310 i = i or 1
311 j = j or -1
312 if i < 0 then i = math.max(n + i + 1, 1) elseif i == 0 then i = 1 end
313 if j < 0 then j = n + j + 1 elseif j > n then j = n end
314 if i > j then return "" end
315 local b = utf8.offset(s, i)
316 local e = utf8.offset(s, j + 1)
317 return s:sub(b, (e or #s + 1) - 1)
318end
319function u.characters(s)
320 return s:gmatch(utf8.charpattern)
321end
322function u.values(s)
323 local f = s:gmatch(utf8.charpattern)
324 return function()
325 local c = f()
326 if c then return utf8.codepoint(c) end
327 end
328end
329return u
330"#;
331
332pub fn install(lua: &Lua) -> mlua::Result<()> {
334 lua.set_app_data(Rc::new(RefCell::new(FontState::default())));
335 let native = lua.create_table()?;
336 native.set(
337 "define_font",
338 lua.create_function(|lua, (name, size, _id): (mlua::String, i64, Option<i64>)| {
339 let name = name.to_str()?.to_owned();
340 let t0 = std::time::Instant::now();
341 let before = with_engine(lua, |e| e.fonts.fonts.len())?;
342 let r = define_font(lua, &name, size as i32);
343 retex_engine::timing::FONTS.with(|a| a.add(t0));
344 with_engine(lua, |e| {
347 for _ in before..e.fonts.fonts.len() {
348 e.nodes.lua_refs.font_defined(true);
349 }
350 })?;
351 r
352 })?,
353 )?;
354 native.set(
355 "reserve_lua_refs",
356 lua.create_function(|lua, n: i64| {
357 with_engine(lua, |e| {
358 for _ in 0..n {
359 e.nodes.lua_refs.take();
360 }
361 })
362 })?,
363 )?;
364 native.set(
365 "node_processor",
366 lua.create_function(|lua, (head, _group): (Value, Value)| {
367 let h = match &head {
368 Value::UserData(u) => u
369 .borrow::<crate::nodelib::LNode>()
370 .ok()
371 .map(|n| NodeRef(n.0)),
372 _ => None,
373 };
374 if let Some(h) = h {
375 let nh = with_engine(lua, |e| process_list(e, h))?;
376 if nh != h {
377 return Ok(Value::UserData(
378 lua.create_userdata(crate::nodelib::LNode(nh.0))?,
379 ));
380 }
381 }
382 Ok(Value::Boolean(true))
383 })?,
384 )?;
385 native.set(
386 "cache_dir",
387 lua.create_function(|lua, ()| with_engine(lua, names_cache_dir))?,
388 )?;
389 let utf = lua
390 .load(UTF_LUA)
391 .set_name("=lualibs-unicode")
392 .into_function()?;
393 native.set("lualibs_utf", utf)?;
394 crate::otfaux::install(lua, &native)?;
395 let loader = lua.create_function(move |lua, ()| {
396 let kpse: mlua::Table = lua.globals().get("kpse")?;
399 let find: mlua::Function = kpse.get("find_file")?;
400 if find.call::<Value>(("luaotfload-main.lua", "lua"))?.is_nil() {
401 return Err(mlua::Error::runtime("module 'luaotfload-main' not found"));
402 }
403 let f = lua
404 .load(MAIN_LUA)
405 .set_name("=luaotfload-main")
406 .into_function()?;
407 let native: mlua::Table = lua.named_registry_value("retex.luaotfload.native")?;
408 f.call::<Value>(native)
409 })?;
410 lua.set_named_registry_value("retex.luaotfload.native", native)?;
411 let package: mlua::Table = lua.globals().get("package")?;
412 let preload: mlua::Table = package.get("preload")?;
413 preload.set("luaotfload-main", loader)?;
414 let harfbuzz = lua
419 .load(
420 r#"local hb = { shapers = function() return "graphite2", "ot", "fallback" end,
421 version = function() return "12.3.2" end }
422 for _, k in ipairs{"Blob", "Buffer", "Direction", "Face", "Feature", "Font",
423 "Language", "Script", "Tag", "Variation", "ot", "unicode"} do
424 hb[k] = {}
425 end
426 return function() return hb end"#,
427 )
428 .set_name("=[luaharfbuzz]")
429 .call::<mlua::Function>(())?;
430 preload.set("luaharfbuzz", harfbuzz)?;
431 Ok(())
432}
433
434fn names_cache_dir(e: &mut retex_engine::Engine) -> String {
439 let Some(k) = e.kpse.as_ref() else {
440 return String::new();
441 };
442 let k = k.borrow();
443 let writable = |dir: &str| {
444 let probe = std::path::Path::new(dir).join(format!(".retex-probe-{}", std::process::id()));
445 let ok = std::fs::File::create(&probe).is_ok();
446 let _ = std::fs::remove_file(&probe);
447 ok
448 };
449 let root = k
450 .var_value("TEXMFCACHE")
451 .unwrap_or_default()
452 .split(':')
453 .find(|d| {
454 !d.is_empty() && k.name_ok(d, retex_kpath::Access::Write, true).is_ok() && writable(d)
455 })
456 .map(str::to_owned)
457 .or_else(|| k.var_value("TEXMFVAR"))
458 .unwrap_or_default();
459 format!("{root}/luatex-cache/generic/names")
460}
461
462fn legacy_luaotfload(e: &mut Engine) -> bool {
465 let Some(path) = e.kpse.as_ref().and_then(|k| {
466 k.borrow_mut()
467 .find_file("luaotfload.sty", retex_kpath::FileFormat::Tex)
468 }) else {
469 return false;
470 };
471 let Ok(text) = std::fs::read(path) else {
472 return false;
473 };
474 let text = String::from_utf8_lossy(&text);
475 let Some(at) = text.find("\\ProvidesPackage{luaotfload}") else {
476 return false;
477 };
478 let rest = &text[at..];
479 let Some(v) = rest.find(" v").map(|i| &rest[i + 2..]) else {
480 return false;
481 };
482 let ver: String = v
483 .chars()
484 .take_while(|c| c.is_ascii_digit() || *c == '.')
485 .collect();
486 let mut parts = ver.split('.').map(|p| p.parse::<u32>().unwrap_or(0));
487 let (major, minor) = (parts.next().unwrap_or(0), parts.next().unwrap_or(0));
488 (major, minor) < (3, 29) && major > 0
489}
490
491fn define_font(lua: &Lua, name: &str, size: i32) -> mlua::Result<Value> {
494 let state = lua
495 .app_data_ref::<Rc<RefCell<FontState>>>()
496 .map(|s| s.clone())
497 .ok_or_else(|| mlua::Error::runtime("font state missing"))?;
498 if let Ok(req) = retex_font::FontRequest::parse(name)
501 && let retex_font::FontSource::Any(base) = &req.source
502 {
503 let id = with_engine(lua, |e| {
504 let f = e.load_tfm_font(base.as_bytes(), size, name.as_bytes());
505 if let Some(f) = f {
506 e.check_lua_loaded_font(f);
507 }
508 f
509 })?;
510 if let Some(i) = id {
511 return Ok(Value::Integer(i64::from(i)));
512 }
513 }
514 {
517 if !state.borrow().db_announced {
518 state.borrow_mut().db_announced = true;
519 let dir = with_engine(lua, names_cache_dir)?;
520 let texio: mlua::Table = lua.globals().get("texio")?;
521 let write: mlua::Function = texio.get("write_nl")?;
522 write.call::<()>((
523 "log",
524 format!(
525 "luaotfload | db : Font names database loaded from {dir}/luaotfload-names.luc.gz"
526 ),
527 ))?;
528 }
529 }
530 let loaded: Result<Arc<LoadedFont>, String> = with_engine(lua, |e| {
531 let mut st = state.borrow_mut();
532 if st.loader.is_none() {
533 let k = e
534 .kpse
535 .clone()
536 .ok_or_else(|| "kpathsea unavailable".to_owned())?;
537 st.loader = Some(retex_font::FontLoader::new(k));
538 }
539 st.loader
540 .as_mut()
541 .expect("loader initialized")
542 .load(name, size)
543 .map_err(|e| e.to_string())
544 })?;
545 let Ok(font) = loaded else {
546 let plain = name.strip_prefix("file:").unwrap_or(name);
548 let plain = plain.split(':').next().unwrap_or(plain).to_owned();
549 let id = with_engine(lua, |e| {
550 let f = e.load_tfm_font(plain.as_bytes(), size, name.as_bytes());
551 if let Some(f) = f {
552 e.check_lua_loaded_font(f);
553 }
554 f
555 })?;
556 return Ok(id.map_or(Value::Nil, |i| Value::Integer(i64::from(i))));
557 };
558 let key = (Arc::as_ptr(&font) as usize, size < 0);
559 if let Some(&id) = state.borrow().ids.get(&key) {
560 return Ok(Value::Integer(i64::from(id)));
561 }
562 let known = state.borrow().legacy_design_size;
563 let legacy = match known {
564 Some(l) => l,
565 None => with_engine(lua, legacy_luaotfload)?,
566 };
567 state.borrow_mut().legacy_design_size = Some(legacy);
568 let id = with_engine(lua, |e| {
569 let tex = &font.tex;
570 let design_size = if legacy {
571 retex_font::texfont::design_size(&font.file).round() as i32
572 } else {
573 tex.design_size
574 };
575 let mut params = vec![0; tex.params.len() + 1];
576 for (i, p) in tex.params.iter().enumerate() {
577 params[i + 1] = *p;
578 }
579 let mut chars = HashMap::with_capacity_and_hasher(
580 tex.num_chars(),
581 retex_engine::hash::FastHash::default(),
582 );
583 for (code, m) in tex.chars() {
584 chars.insert(
585 code,
586 CharMetrics {
587 width: m.width,
588 height: m.height,
589 depth: m.depth,
590 italic: m.italic,
591 index: u32::from(m.glyph),
592 },
593 );
594 }
595 let info = FontInfo {
596 name: tex.name.clone().into_bytes(),
597 size: tex.size,
598 design_size,
599 checksum: 0,
600 params,
601 chars,
602 hyphen_char: i32::MIN,
603 skew_char: i32::MIN,
604 kind: FontKind::OpenType,
605 handle: 0,
606 char_codes: Default::default(),
607 ignore_ligatures: 0,
608 data: Some(FontData(Rc::new(font.clone()))),
609 lig_kern: Default::default(),
610 right_boundary: None,
611 expansion: None,
612 size_spec: i32::MIN,
613 vf: None,
614 math_chars: {
615 let mc = tex.math_chars(&font.file);
616 (!mc.is_empty()).then(|| Rc::new(convert_math_chars(mc)))
617 },
618 math_constants: tex.math.as_ref().map(|m| {
619 Rc::new(
620 m.iter()
621 .map(|(k, v)| ((*k).to_owned(), v.to_sp()))
622 .collect(),
623 )
624 }),
625 };
626 e.register_font(info)
627 })?;
628 state.borrow_mut().ids.insert(key, id);
629 Ok(Value::Integer(i64::from(id)))
630}
631
632fn convert_math_chars(
635 mc: HashMap<u32, retex_font::texfont::MathChar>,
636) -> HashMap<u32, retex_engine::fonts::MathCharInfo> {
637 use retex_engine::fonts::{ExtPart, MathCharInfo};
638 let parts = |v: &Option<Vec<retex_font::texfont::MathPart>>| {
639 v.as_ref().map(|ps| {
640 ps.iter()
641 .map(|p| ExtPart {
642 code: p.code,
643 start: retex_font::texfont::round_sp(p.start),
644 end: retex_font::texfont::round_sp(p.end),
645 advance: retex_font::texfont::round_sp(p.advance),
646 extender: p.extender,
647 })
648 .collect::<Rc<[ExtPart]>>()
649 })
650 };
651 mc.into_iter()
652 .map(|(c, m)| {
653 (
654 c,
655 MathCharInfo {
656 next: m.next,
657 top_accent: m.top_accent.map(retex_font::texfont::round_sp),
658 vert: parts(&m.vert),
659 horiz: parts(&m.horiz),
660 },
661 )
662 })
663 .collect()
664}
665
666pub fn loaded_font(e: &Engine, f: u32) -> Option<Arc<LoadedFont>> {
668 let data = e.font(f).data.as_ref()?;
669 data.0.downcast_ref::<Arc<LoadedFont>>().cloned()
670}
671
672pub use crate::otfnodes::process_list;
673
674#[cfg(test)]
675mod tests {
676 #[test]
677 fn lualibs_utf() {
678 let lua = mlua::Lua::new();
679 let utf: mlua::Table = lua.load(super::UTF_LUA).eval().expect("utf");
680 lua.globals().set("utf", utf).expect("set");
681 let r: String = lua
682 .load(
683 r##"local o = {}
684 local function add(...) o[#o + 1] = table.concat({...}, ",") end
685 add(utf.char(65, 0x3B1), utf.byte("\u{3B1}b"), select("#", utf.byte("")))
686 add(utf.len("\u{3B1}b"), utf.sub("\u{3B1}\u{3B2}c", 2, 3),
687 utf.sub("\u{3B1}\u{3B2}c", 2), utf.sub("\u{3B1}\u{3B2}c", -1))
688 for c in utf.characters("a\u{3B1}") do add(c) end
689 for c in utf.values("a\u{3B1}") do add(c) end
690 return table.concat(o, "|")"##,
691 )
692 .eval()
693 .expect("run");
694 assert_eq!(r, "A\u{3B1},945,0|2,\u{3B2}c,\u{3B2}c,c|a|\u{3B1}|97|945");
696 }
697}