summaryrefslogtreecommitdiff
path: root/macros/luatex/latex/luamml
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/latex/luamml')
-rw-r--r--macros/luatex/latex/luamml/README.md14
-rw-r--r--macros/luatex/latex/luamml/luamml-amsmath.lua133
-rw-r--r--macros/luatex/latex/luamml/luamml-array.lua84
-rw-r--r--macros/luatex/latex/luamml/luamml-convert.lua662
-rw-r--r--macros/luatex/latex/luamml/luamml-data-combining.lua27
-rw-r--r--macros/luatex/latex/luamml/luamml-data-stretchy.lua500
-rw-r--r--macros/luatex/latex/luamml/luamml-demo.sty82
-rw-r--r--macros/luatex/latex/luamml/luamml-legacy-mappings.lua73
-rw-r--r--macros/luatex/latex/luamml/luamml-lr.lua71
-rw-r--r--macros/luatex/latex/luamml/luamml-patches-amsmath.sty417
-rw-r--r--macros/luatex/latex/luamml/luamml-patches-amstext.sty37
-rw-r--r--macros/luatex/latex/luamml/luamml-patches-array.sty86
-rw-r--r--macros/luatex/latex/luamml/luamml-patches-kernel.sty63
-rw-r--r--macros/luatex/latex/luamml/luamml-patches-lab-math.sty20
-rw-r--r--macros/luatex/latex/luamml/luamml-pdf-demo.sty50
-rw-r--r--macros/luatex/latex/luamml/luamml-structelemwriter.lua105
-rw-r--r--macros/luatex/latex/luamml/luamml-table.lua110
-rw-r--r--macros/luatex/latex/luamml/luamml-tex-annotate.lua119
-rw-r--r--macros/luatex/latex/luamml/luamml-tex.lua232
-rw-r--r--macros/luatex/latex/luamml/luamml-xmlwriter.lua58
-rw-r--r--macros/luatex/latex/luamml/luamml.dtx531
-rw-r--r--macros/luatex/latex/luamml/luamml.pdfbin0 -> 536611 bytes
-rw-r--r--macros/luatex/latex/luamml/test-mathml-2.txt8
23 files changed, 3482 insertions, 0 deletions
diff --git a/macros/luatex/latex/luamml/README.md b/macros/luatex/latex/luamml/README.md
new file mode 100644
index 0000000000..afdf86e6f0
--- /dev/null
+++ b/macros/luatex/latex/luamml/README.md
@@ -0,0 +1,14 @@
+# LuaMML: Automated LuaLaTeX math to MathML conversion
+This is an attempt to implement automatic conversion of LuaLaTeX inline and display math expressions into MathML code to aid with tagging.
+It works best with `unicode-math`, but it can also be used with traditional math fonts if mappings to Unicode are provided.
+
+## Installation
+Run `l3build install` to install `luamml` into your local `texmf` tree.
+
+## Usage
+Add `\usepackage[tracing]{luamml-demo}` to print MathML to the terminal or `\usepackage[files]{luamml-demo}` to generate separate files with MathML output.
+Alternatively it can be used with latex-lab to automatically integrate with tagging infrastucture.
+
+<!-- Also see a [`tagpdf` experiment using this to tag PDF formulas](https://github.com/u-fischer/tagpdf/blob/develop/experiments/exp-mathml-lua.tex). -->
+
+<!-- If you are very brave you can also try running `pdflatex test_pdf` and afterwards run `./pdfmml.lua test_pdf.lua` to get pdflatex formulas converted. -->
diff --git a/macros/luatex/latex/luamml/luamml-amsmath.lua b/macros/luatex/latex/luamml/luamml-amsmath.lua
new file mode 100644
index 0000000000..2b74265501
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-amsmath.lua
@@ -0,0 +1,133 @@
+local write_xml = require'luamml-xmlwriter'
+local make_root = require'luamml-convert'.make_root
+local save_result = require'luamml-tex'.save_result
+local store_column = require'luamml-table'.store_column
+local store_tag = require'luamml-table'.store_tag
+local get_table = require'luamml-table'.get_table
+local set_row_attribute = require'luamml-table'.set_row_attribute
+local to_text = require'luamml-lr'
+
+local properties = node.get_properties_table()
+
+local math_t = node.id'math'
+
+local funcid = luatexbase.new_luafunction'__luamml_amsmath_add_last_to_row:'
+token.set_lua('__luamml_amsmath_add_last_to_row:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc
+ -- local box = token.scan_int()
+ local nest = tex.nest.top
+ local head, startmath = nest.head, nest.tail
+ repeat
+ startmath = startmath.prev
+ until startmath == head or (startmath.id == math_t and startmath.subtype == 0)
+ if startmath == head then return end
+ assert(startmath.id == node.id"math")
+ store_column(startmath)
+end
+
+local funcid = luatexbase.new_luafunction'__luamml_amsmath_add_box_to_row:'
+token.set_lua('__luamml_amsmath_add_box_to_row:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc
+ -- local box = token.scan_int()
+ local boxnum = 0
+ local startmath = tex.box[boxnum].list
+ assert(startmath.id == math_t)
+ store_column(startmath)
+end
+
+local funcid = luatexbase.new_luafunction'__luamml_amsmath_set_row_columnalign:n'
+token.set_lua('__luamml_amsmath_set_row_columnalign:n', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ set_row_attribute('columnalign', token.scan_argument())
+end
+
+do
+ local saved
+ funcid = luatexbase.new_luafunction'__luamml_amsmath_save_inner_table:n'
+ token.set_lua('__luamml_amsmath_save_inner_table:n', funcid)
+ lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc
+ local kind = token.scan_argument()
+ local mml_table = get_table()
+ if not mml_table then return end
+ mml_table.displaystyle = true
+ local columns = node.count(node.id'align_record', tex.lists.align_head)//2
+ mml_table.columnalign = kind == 'gathered' and 'center' or string.rep('right left', columns, ' ')
+ local spacing = {}
+ for n in node.traverse_id(node.id'glue', tex.lists.align_head) do
+ spacing[#spacing+1] = n.width == 0 and '0' or string.format('%.3fpt', n.width/65781.76)
+ end
+ mml_table.columnspacing = #spacing > 3 and table.concat(spacing, ' ', 2, #spacing-2) or nil
+ saved = mml_table
+ end
+
+ funcid = luatexbase.new_luafunction'__luamml_amsmath_save_smallmatrix:'
+ token.set_lua('__luamml_amsmath_save_smallmatrix:', funcid)
+ lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc
+ local mml_table = get_table()
+ mml_table.align = 'axis'
+ mml_table.columnalign = 'center'
+ mml_table.columnspacing = '0.278em'
+ mml_table.rowspacing = string.format('%.3fpt', tex.lineskip.width/65781.76)
+ saved = {[0] = 'mpadded', width = '+0.333em', lspace = '0.167em', mml_table}
+ saved = mml_table
+ end
+
+ funcid = luatexbase.new_luafunction'__luamml_amsmath_finalize_inner_table:'
+ token.set_lua('__luamml_amsmath_finalize_inner_table:', funcid)
+ lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc
+ local vcenter = tex.nest.top.tail.nucleus
+ local props = properties[vcenter]
+ if not props then
+ props = {}
+ properties[vcenter] = props
+ end
+ props.mathml_table = assert(saved)
+ saved = nil
+ end
+end
+
+funcid = luatexbase.new_luafunction'__luamml_amsmath_finalize_table:n'
+token.set_lua('__luamml_amsmath_finalize_table:n', funcid)
+lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc
+ local kind = token.scan_argument()
+ local mml_table = get_table()
+ if not mml_table then return end
+ mml_table.displaystyle = true
+ local columns = node.count(node.id'align_record', tex.lists.align_head)//2
+ mml_table.columnalign = kind == 'align' and string.rep('right left', columns, ' ') or nil
+ mml_table.width = kind == 'multline' and '100%' or nil
+ -- mml_table.side = kind == 'multline' and 'rightoverlap' or nil
+ local spacing = {}
+ for n in node.traverse_id(node.id'glue', tex.lists.align_head) do
+ spacing[#spacing+1] = n.width == 0 and '0' or '.8em'
+ end
+ mml_table.columnspacing = #spacing > 3 and table.concat(spacing, ' ', 2, #spacing-2) or nil
+ save_result(mml_table, true)
+end
+
+local last_tag
+
+funcid = luatexbase.new_luafunction'__luamml_amsmath_save_tag:'
+token.set_lua('__luamml_amsmath_save_tag:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ local nest = tex.nest.top
+ local chars = {}
+ last_tag = to_text(nest.head)
+end
+
+funcid = luatexbase.new_luafunction'__luamml_amsmath_set_tag:'
+token.set_lua('__luamml_amsmath_set_tag:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ if not last_tag then
+ texio.write_nl'WARNING: Tag extraction failed'
+ return
+ end
+ store_tag({[0] = 'mtd', last_tag})
+ last_tag = nil
+end
diff --git a/macros/luatex/latex/luamml/luamml-array.lua b/macros/luatex/latex/luamml/luamml-array.lua
new file mode 100644
index 0000000000..9a7b3581f2
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-array.lua
@@ -0,0 +1,84 @@
+local write_xml = require'luamml-xmlwriter'
+local make_root = require'luamml-convert'.make_root
+local save_result = require'luamml-tex'.save_result
+local store_column = require'luamml-table'.store_column
+local store_column_xml = require'luamml-table'.store_column_xml
+local store_tag = require'luamml-table'.store_tag
+local get_table = require'luamml-table'.get_table
+local to_text = require'luamml-lr'
+
+local properties = node.get_properties_table()
+
+local funcid = luatexbase.new_luafunction'__luamml_array_init_col:'
+token.set_lua('__luamml_array_init_col:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc
+ local nest = tex.nest[tex.nest.ptr-1]
+ -- The special will be deleted again, it just marks the right math list since the start math node is not there yet
+ local special = node.new('whatsit', 'special')
+ node.insert_after(nest.tail, nest.tail, special)
+ nest.tail = special
+ local temp = nest.head
+ local props = properties[temp]
+ if not props then
+ props = {}
+ properties[temp] = props
+ end
+ props.luamml_array_startmath = special
+end
+
+local funcid = luatexbase.new_luafunction'__luamml_array_finalize_col:w'
+token.set_lua('__luamml_array_finalize_col:w', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ local alignment = token.scan_int() -- Do it first to consume number even if we end early
+ -- TODO: Error handling etc
+ local temp = tex.nest.top.head
+ local props = properties[temp]
+ local special = props and props.luamml_array_startmath
+ if not special then return end
+ node.remove(tex.nest.top.head, special)
+ local startmath = node.free(special)
+ props.luamml_array_startmath = nil
+
+ alignment = alignment == 1 and 'left' or alignment == 2 and 'right' or nil
+
+ if node.end_of_math(startmath) == tex.nest.top.tail then
+ if startmath.next == tex.nest.top.tail then return end
+ store_column(startmath).columnalign = alignment
+ else
+ -- Oh no, we got text. Let't complain to the user, it's probably their fault
+ print'We are mathematicians, don\'t bother us with text'
+ store_column_xml(to_text(startmath, tex.nest.top.tail)).columnalign = alignment
+ end
+end
+
+local saved_array
+
+funcid = luatexbase.new_luafunction'__luamml_array_finalize_array:'
+token.set_lua('__luamml_array_save_array:', funcid)
+lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc.
+ local colsep = tex.dimen['col@sep']
+ saved_array = get_table()
+ if colsep ~= 0 then
+ saved_array = {[0] = 'mpadded',
+ width = string.format('%+.3fpt', 2*colsep/65781.76),
+ lspace = string.format('%+.3fpt', colsep/65781.76),
+ saved_array
+ }
+ end
+end
+
+funcid = luatexbase.new_luafunction'__luamml_array_finalize_array:'
+token.set_lua('__luamml_array_finalize_array:', funcid)
+lua.get_functions_table()[funcid] = function()
+ -- TODO: Error handling etc.
+ local nucl = tex.nest.top.tail.nucleus
+ local props = properties[nucl]
+ if not props then
+ props = {}
+ properties[nucl] = props
+ end
+ props.mathml_table = saved_array
+ saved_array = nil
+end
diff --git a/macros/luatex/latex/luamml/luamml-convert.lua b/macros/luatex/latex/luamml/luamml-convert.lua
new file mode 100644
index 0000000000..fe529debc0
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-convert.lua
@@ -0,0 +1,662 @@
+local remap_comb = require'luamml-data-combining'
+local stretchy = require'luamml-data-stretchy'
+local to_text = require'luamml-lr'
+
+local properties = node.get_properties_table()
+
+local hlist_t, kern_t, glue_t, rule_t = node.id'hlist', node.id'kern', node.id'glue', node.id'rule'
+
+local noad_t, accent_t, style_t, choice_t = node.id'noad', node.id'accent', node.id'style', node.id'choice'
+local radical_t, fraction_t, fence_t = node.id'radical', node.id'fraction', node.id'fence'
+
+local math_char_t, sub_box_t, sub_mlist_t = node.id'math_char', node.id'sub_box', node.id'sub_mlist'
+
+local function invert_table(t)
+ local t_inv = {}
+ for k, v in next, t do
+ t_inv[v] = k
+ end
+ return t_inv
+end
+
+local noad_names = node.subtypes'noad'
+
+--[[ We could determine the noad subtypes dynamically:
+local noad_sub = invert_table(noad_names)
+local noad_ord = noad_sub.ord
+local noad_op = noad_sub.opdisplaylimits
+local noad_oplimits = noad_sub.oplimits
+local noad_opnolimits = noad_sub.opnolimits
+local noad_bin = noad_sub.bin
+local noad_rel = noad_sub.rel
+local noad_open = noad_sub.open
+local noad_close = noad_sub.close
+local noad_punct = noad_sub.punct
+local noad_inner = noad_sub.inner
+local noad_under = noad_sub.under
+local noad_over = noad_sub.over
+local noad_vcenter = noad_sub.vcenter
+-- But the spacing table depends on their specific values anyway, so we just verify the values
+]]
+local noad_ord, noad_op, noad_oplimits, noad_opnolimits = 0, 1, 2, 3
+local noad_bin, noad_rel, noad_open, noad_close, noad_punct = 4, 5, 6, 7, 8
+local noad_inner, noad_under, noad_over, noad_vcenter = 9, 10, 11, 12
+
+for i, n in ipairs{'ord', 'opdisplaylimits', 'oplimits', 'opnolimits', 'bin',
+ 'rel', 'open', 'close', 'punct', 'inner', 'under', 'over', 'vcenter'} do
+ assert(noad_names[i-1] == n)
+end
+-- Attention, the spacing_table is indexed by subtype+1 since 1-based tables are faster in Lua
+local spacing_table = {
+ {0 , '0.167em', '0.167em', '0.167em', '0.222em', '0.278em', 0 , 0 , 0 , '0.167em', 0 , 0 , 0 , },
+ {'0.167em', '0.167em', '0.167em', '0.167em', nil , '0.278em', 0 , 0 , 0 , '0.167em', '0.167em', '0.167em', '0.167em', },
+ nil,
+ nil,
+ {'0.222em', '0.222em', '0.222em', '0.222em', nil , nil , '0.222em', nil , nil , '0.222em', '0.222em', '0.222em', '0.222em', },
+ {'0.278em', '0.278em', '0.278em', '0.278em', nil , 0 , '0.278em', 0 , 0 , '0.278em', '0.278em', '0.278em', '0.278em', },
+ {0 , 0 , 0 , 0 , nil , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , },
+ {0 , '0.167em', '0.167em', '0.167em', '0.222em', '0.278em', 0 , 0 , 0 , '0.167em', 0 , 0 , 0 , },
+ {'0.167em', '0.167em', '0.167em', '0.167em', nil , '0.167em', '0.167em', '0.167em', '0.167em', '0.167em', '0.167em', '0.167em', '0.167em', },
+ {'0.167em', '0.167em', '0.167em', '0.167em', '0.222em', '0.278em', '0.167em', 0 , '0.167em', '0.167em', '0.167em', '0.167em', '0.167em', },
+ nil,
+ nil,
+ nil,
+}
+local spacing_table_script = {
+ {0 , '0.167em', '0.167em', '0.167em', 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , },
+ {'0.167em', '0.167em', '0.167em', '0.167em', nil , 0 , 0 , 0 , 0 , 0 , '0.167em', '0.167em', '0.167em', },
+ nil,
+ nil,
+ {0 , 0 , 0 , 0 , nil , nil , 0 , nil , nil , 0 , 0 , 0 , 0 , },
+ {0 , 0 , 0 , 0 , nil , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , },
+ {0 , 0 , 0 , 0 , nil , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , },
+ {0 , '0.167em', '0.167em', '0.167em', 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , },
+ {0 , 0 , 0 , 0 , nil , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , },
+ {0 , '0.167em', '0.167em', '0.167em', 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , },
+ nil,
+ nil,
+ nil,
+}
+do -- Fill the blanks
+ local st, sts = spacing_table, spacing_table_script
+
+ local st_op, sts_op = st[noad_op+1], sts[noad_op+1]
+ st[noad_oplimits+1], sts[noad_oplimits+1] = st_op, sts_op
+ st[noad_opnolimits+1], sts[noad_opnolimits+1] = st_op, sts_op
+
+ local st_ord, sts_ord = st[noad_ord+1], sts[noad_ord+1]
+ st[noad_under+1], sts[noad_under+1] = st_ord, sts_ord
+ st[noad_over+1], sts[noad_over+1] = st_ord, sts_ord
+ st[noad_vcenter+1], sts[noad_vcenter+1] = st_ord, sts_ord
+end
+
+
+local radical_sub = node.subtypes'radical'
+local fence_sub = node.subtypes'fence'
+
+local remap_lookup = setmetatable({}, {__index = function(t, k)
+ local ch = utf8.char(k & 0x1FFFFF)
+ t[k] = ch
+ return ch
+end})
+local digit_map = {["0"] = true, ["1"] = true,
+ ["2"] = true, ["3"] = true, ["4"] = true,
+ ["5"] = true, ["6"] = true, ["7"] = true,
+ ["8"] = true, ["9"] = true,}
+
+local always_mo = {["%"] = true, ["&"] = true, ["."] = true, ["/"] = true,
+ ["\\"] = true, ["¬"] = true, ["′"] = true, ["″"] = true, ["‴"] = true,
+ ["⁗"] = true, ["‵"] = true, ["‶"] = true, ["‷"] = true, ["|"] = true,
+ ["∀"] = true, ["∁"] = true, ["∃"] = true, ["∂"] = true, ["∄"] = true,}
+
+-- Marker tables replacing the core operator for space like elements
+local space_like = {}
+
+local nodes_to_table
+
+local function sub_style(s) return s//4*2+5 end
+local function sup_style(s) return s//4*2+4+s%2 end
+
+-- The _to_table functions generally return a second argument which is
+-- could be (if it were a <mo>) a core operator of the embellishe operator
+-- or space_like
+-- acc_to_table is special since it's return value should
+-- always be considered a core operator
+
+-- We ignore large_... since they aren't used for modern fonts
+local function delim_to_table(delim)
+ if not delim then return end
+ local props = properties[delim]
+ local mathml_core = props and props.mathml_core
+ local mathml_table = props and (props.mathml_table or mathml_core)
+ if mathml_table ~= nil then return mathml_table, mathml_core end
+ local mathml_filter = props and props.mathml_filter -- Kind of pointless since the arguments are literals, but present for consistency
+ local char = delim.small_char
+ if char == 0 then
+ local result = {[0] = 'mspace', width = string.format("%.3fpt", tex.nulldelimiterspace/65781.76)}
+ if mathml_filter then
+ return mathml_filter(result, space_like)
+ else
+ return result, space_like
+ end
+ else
+ local fam = delim.small_fam
+ char = remap_lookup[fam << 21 | char]
+ local result = {[0] = 'mo', char, ['tex:family'] = fam ~= 0 and fam or nil, stretchy = not stretchy[char] or nil, lspace = 0, rspace = 0, [':nodes'] = {delim}, [':actual'] = char}
+ if mathml_filter then
+ return mathml_filter(result, result)
+ else
+ return result, result
+ end
+ end
+end
+
+-- Like kernel_to_table but always a math_char_t. Also creating a mo and potentially remapping to handle combining chars.
+-- No lspace or space is set here since these never appear as core operators in an mrow.
+local function acc_to_table(acc, cur_style, stretch)
+ if not acc then return end
+ local props = properties[acc]
+ local mathml_core = props and props.mathml_core
+ local mathml_table = props and (props.mathml_table or mathml_core)
+ if mathml_table ~= nil then return mathml_table, mathml_core end
+ if acc.id ~= math_char_t then
+ error'confusion'
+ end
+ local mathml_filter = props and props.mathml_filter -- Kind of pointless since the arguments are literals, but present for consistency
+ local fam = acc.fam
+ local char = remap_lookup[fam << 21 | acc.char]
+ char = remap_comb[char] or char
+ if stretch ~= not stretchy[char] then -- Handle nil gracefully in stretchy
+ stretch = nil
+ end
+ local result = {[0] = 'mo', char, ['tex:family'] = fam ~= 0 and fam or nil, stretchy = stretch, [':nodes'] = {acc}, [':actual'] = stretch and char or nil}
+ if mathml_filter then
+ return mathml_filter(result)
+ else
+ return result
+ end
+end
+
+local function kernel_to_table(kernel, cur_style, text_families)
+ if not kernel then return end
+ local props = properties[kernel]
+ local mathml_core = props and props.mathml_core
+ local mathml_table = props and (props.mathml_table or mathml_core)
+ if mathml_table ~= nil then return mathml_table, mathml_core end
+ local mathml_filter = props and props.mathml_filter -- Kind of pointless since the arguments are literals, but present for consistency
+ local id = kernel.id
+ if id == math_char_t then
+ local fam = kernel.fam
+ local char = remap_lookup[fam << 21 | kernel.char]
+ local elem = digit_map[char] and 'mn' or 'mi'
+ local result = {[0] = elem,
+ char,
+ ['tex:family'] = fam ~= 0 and fam or nil,
+ mathvariant = utf8.len(char) == 1 and elem == 'mi' and utf8.codepoint(char) < 0x10000 and 'normal' or nil,
+ [':nodes'] = {kernel},
+ }
+ if mathml_filter then
+ return mathml_filter(result, result)
+ else
+ return result, result
+ end
+ elseif id == sub_box_t then
+ local result
+ if kernel.list.id == hlist_t then -- We directly give up for vlists
+ result = to_text(kernel.list.head)
+ else
+ result = {[0] = 'mi', {[0] = 'mglyph', ['tex:box'] = kernel.list, [':nodes'] = {kernel}}}
+ end
+ if mathml_filter then
+ return mathml_filter(result, result)
+ else
+ return result, result
+ end
+ elseif id == sub_mlist_t then
+ if mathml_filter then
+ return mathml_filter(nodes_to_table(kernel.list, cur_style, text_families))
+ else
+ return nodes_to_table(kernel.list, cur_style, text_families)
+ end
+ else
+ error'confusion'
+ end
+end
+
+local function do_sub_sup(t, core, n, cur_style, text_families)
+ local sub = kernel_to_table(n.sub, sub_style(cur_style), text_families)
+ local sup = kernel_to_table(n.sup, sup_style(cur_style), text_families)
+ if sub then
+ if sup then
+ return {[0] = 'msubsup', t, sub, sup}, core
+ else
+ return {[0] = 'msub', t, sub}, core
+ end
+ elseif sup then
+ return {[0] = 'msup', t, sup}, core
+ else
+ return t, core
+ end
+end
+
+
+-- If we encounter a . or , after a number, test if it's followed by another number and in that case convert it into a mn
+local function maybe_to_mn(noad, core)
+ if noad.sub or noad.sup then return end
+ local after = noad.next
+ if not after then return end
+ if after.id ~= noad_t then return end
+ if after.subtype ~= noad_ord then return end
+ after = after.nucleus
+ if not after then return end
+ if after.id ~= math_char_t then return end
+ if not digit_map[remap_lookup[after.fam << 21 | after.char]] then return end
+ core[0] = 'mn'
+end
+
+local function noad_to_table(noad, sub, cur_style, joining, bin_replacements, text_families)
+ local nucleus, core = kernel_to_table(noad.nucleus, sub == noad_over and cur_style//2*2+1 or cur_style, text_families)
+ if not nucleus then return end
+ if core and core[0] == 'mo' and core.minsize and not core.maxsize then
+ core.maxsize = core.minsize -- This happens when a half-specified delimiter appears alone in a list.
+ -- If it has a minimal size, it should be fixed to that size (since there is nothing bigger in it's list)
+ end
+ if sub == noad_ord and not (bin_replacements[node.direct.todirect(noad)] or (nucleus == core and #core == 1 and always_mo[core[1]])) then
+ if core and core[0] == 'mo' then
+ core['tex:class'] = nil
+ if not core.minsize and not core.movablelimits then
+ core[0] = 'mi'
+ core.movablelimits = nil
+ core.mathvariant = #core == 1 and type(core[1]) == 'string' and utf8.len(core[1]) == 1 and utf8.codepoint(core[1]) < 0x10000 and 'normal' or nil
+ core.stretchy, core.lspace, core.rspace = nil
+ end
+ end
+ if nucleus == core and #core == 1 then
+ if joining and joining[0] == 'mn' and core[0] == 'mi' and (core[1] == '.' or core[1] == ',') and maybe_to_mn(noad, core)
+ or core[0] == 'mn' or text_families[core['tex:family'] or 0] then
+ if joining and core[0] == joining[0] and core['tex:family'] == joining['tex:family'] then
+ joining[#joining+1] = core[1]
+ local cnodes = core[':nodes']
+ if cnodes then -- very likely
+ local jnodes = joining[':nodes']
+ if jnodes then -- very likely
+ table.move(cnodes, 1, #cnodes, #jnodes+1, jnodes)
+ else
+ joining[':nodes'] = cnodes
+ end
+ end
+ nucleus = do_sub_sup(joining, joining, noad, cur_style, text_families)
+ if nucleus == joining then
+ return nil, joining, joining
+ else
+ return nucleus, joining, false
+ end
+ elseif not noad.sub and not noad.sup then
+ return core, core, core
+ end
+ end
+ end
+ elseif sub == noad_op or sub == noad_oplimits or sub == noad_opnolimits or sub == noad_bin or sub == noad_rel or sub == noad_open
+ or sub == noad_close or sub == noad_punct or sub == noad_inner or sub == noad_ord then
+ if not core or not core[0] then
+ -- TODO
+ else
+ core[0] = 'mo'
+ if not core.minsize then
+ if stretchy[core[1]] then core.stretchy = false end
+ end
+ if core.mathvariant == 'normal' then core.mathvariant = nil end
+ core.lspace, core.rspace = 0, 0
+ end
+ nucleus['tex:class'] = noad_names[sub]
+
+ if (noad.sup or noad.sub) and (sub == noad_op or sub == noad_oplimits) then
+ if core and core[0] == 'mo' then core.movablelimits = sub == noad_op end
+ local sub = kernel_to_table(noad.sub, sub_style(cur_style), text_families)
+ local sup = kernel_to_table(noad.sup, sup_style(cur_style), text_families)
+ return {[0] = sup and (sub and 'munderover' or 'mover') or 'munder',
+ nucleus,
+ sub or sup,
+ sub and sup,
+ }, core
+ end
+ elseif sub == noad_under then
+ return {[0] = 'munder',
+ nucleus,
+ {[0] = 'mo', '_',},
+ }, core
+ elseif sub == noad_over then
+ return {[0] = 'mover',
+ nucleus,
+ {[0] = 'mo', '\u{203E}',},
+ }, core
+ elseif sub == noad_vcenter then -- Ignored. Nucleus will need special handling anyway
+ else
+ error[[confusion]]
+ end
+ return do_sub_sup(nucleus, core, noad, cur_style, text_families)
+end
+
+local function accent_to_table(accent, sub, cur_style, text_families)
+ local nucleus, core = kernel_to_table(accent.nucleus, cur_style//2*2+1, text_families)
+ local top_acc = acc_to_table(accent.accent, cur_style, sub & 1 == 1)
+ local bot_acc = acc_to_table(accent.bot_accent, cur_style, sub & 2 == 2)
+ return {[0] = top_acc and (bot_acc and 'munderover' or 'mover') or 'munder',
+ nucleus,
+ bot_acc or top_acc,
+ bot_acc and top_acc,
+ }, core
+end
+
+local style_table = {
+ display = {displaystyle = "true", scriptlevel = "0"},
+ text = {displaystyle = "false", scriptlevel = "0"},
+ script = {displaystyle = "false", scriptlevel = "1"},
+ scriptscript = {displaystyle = "false", scriptlevel = "2"},
+}
+
+style_table.crampeddisplay, style_table.crampedtext,
+style_table.crampedscript, style_table.crampedscriptscript =
+ style_table.display, style_table.text,
+ style_table.script, style_table.scriptscript
+
+local function radical_to_table(radical, sub, cur_style, text_families)
+ local kind = radical_sub[sub]
+ local nucleus, core = kernel_to_table(radical.nucleus, cur_style//2*2+1, text_families)
+ local left = delim_to_table(radical.left)
+ local elem
+ if kind == 'radical' or kind == 'uradical' then
+ -- FIXME: Check that this is really a square root
+ elem, core = {[0] = 'msqrt', nucleus, }, nil
+ elseif kind == 'uroot' then
+ -- FIXME: Check that this is really a root
+ elem, core = {[0] = 'msqrt', nucleus, kernel_to_table(radical.degree, 7, text_families)}, nil
+ elseif kind == 'uunderdelimiter' then
+ elem, core = {[0] = 'munder', left, nucleus}, left
+ elseif kind == 'uoverdelimiter' then
+ elem, core = {[0] = 'mover', left, nucleus}, left
+ elseif kind == 'udelimiterunder' then
+ elem = {[0] = 'munder', nucleus, left}
+ elseif kind == 'udelimiterover' then
+ elem = {[0] = 'mover', nucleus, left}
+ else
+ error[[confusion]]
+ end
+ return do_sub_sup(elem, core, radical, cur_style, text_families)
+end
+
+local function fraction_to_table(fraction, sub, cur_style, text_families)
+ local num, core = kernel_to_table(fraction.num, cur_style + 2 - cur_style//6*2, text_families)
+ local denom = kernel_to_table(fraction.denom, cur_style//2*2 + 3 - cur_style//6*2, text_families)
+ local left = delim_to_table(fraction.left)
+ local right = delim_to_table(fraction.right)
+ local mfrac = {[0] = 'mfrac',
+ linethickness = fraction.width and fraction.width == 0 and 0 or nil,
+ bevelled = fraction.middle and "true" or nil,
+ num,
+ denom,
+ }
+ if left then
+ return {[0] = 'mrow',
+ left,
+ mfrac,
+ right, -- might be nil
+ }
+ elseif right then
+ return {[0] = 'mrow',
+ mfrac,
+ right,
+ }
+ else
+ return mfrac, core
+ end
+end
+
+local function fence_to_table(fence, sub, cur_style)
+ local delim, core = delim_to_table(fence.delim)
+ if core[0] ~= 'mo' then
+ return delim, core
+ end
+ core.fence, core.symmetric = 'true', 'true'
+ local options = fence.options
+ local axis
+ if fence.height ~= 0 or fence.depth ~= 0 then
+ axis = 0xA == options & 0xA
+ local exact = 0x18 == options & 0x18
+ -- We treat them always as exact. mpadded would allow us to support
+ -- non-exact ones too and I will implement that if I ever encounter
+ -- someone who does that intentionally. Until then, we warn people
+ -- since such fences are absurd.
+ if not exact then
+ texio.write_nl'luamml: The document uses a fence with \z
+ explicit dimensions but without the "exact" option. \z
+ This is probably a mistake.'
+ end
+ core.minsize = string.format("%.3fpt", (fence.height + fence.depth)/65781.76)
+ core.maxsize = core.minsize
+ else
+ axis = 0xC ~= options & 0xC
+ end
+ if not axis then
+ texio.write_nl'luamml: Baseline centered fence will be centered around math axis instead'
+ end
+ return delim, core
+end
+
+local function space_to_table(amount, sub, cur_style)
+ if amount == 0 then return end
+ if sub == 99 then -- TODO magic number
+ -- 18*2^16=1179648
+ return {[0] = 'mspace', width = string.format("%.3fem", amount/1179648)}, space_like
+ else
+ -- 65781.76=tex.sp'100bp'/100
+ return {[0] = 'mspace', width = string.format("%.3fpt", amount/65781.76)}, space_like
+ end
+end
+
+local running_length = -1073741824
+local function rule_to_table(rule, sub, cur_style)
+ local width = string.format("%.3fpt", rule.width/65781.76)
+ local height = rule.height
+ if height == running_length then
+ height = '0.8em'
+ else
+ height = string.format("%.3fpt", height/65781.76)
+ end
+ local depth = rule.depth
+ if depth == running_length then
+ depth = '0.2em'
+ else
+ depth = string.format("%.3fpt", depth/65781.76)
+ end
+ return {[0] = 'mspace', mathbackground = 'currentColor', width = width, height = height, depth = depth}, space_like
+end
+
+-- The only part which changes the nodelist, we are converting bin into ord
+-- nodes in the same way TeX would do it later anyway.
+local function cleanup_mathbin(head)
+ local replacements = {}
+ local last = 'open' -- last sub if id was noad_t, left fence acts fakes being a open noad, bin are themselves. Every other noad is ord
+ for n, id, sub in node.traverse(head) do
+ if id == noad_t then
+ if sub == noad_bin then
+ if node.is_node(last) or last == noad_opdisplaylimits
+ or last == noad_oplimits or last == noad_opnolimits
+ or last == noad_rel or last == noad_open or last == noad_punct then
+ replacements[node.direct.todirect(n)] = true
+ n.subtype, last = noad_ord, noad_ord
+ else
+ last = n
+ end
+ else
+ if (sub == noad_rel or sub == noad_close or sub == noad_punct)
+ and node.is_node(last) then
+ replacements[node.direct.todirect(last)] = true
+ last.subtype = noad_ord
+ end
+ last = sub
+ end
+ elseif id == fence_t then
+ if sub == fence_sub.left then
+ last = noad_open
+ else
+ if node.is_node(last) then
+ replacements[node.direct.todirect(last)] = true
+ last.subtype = noad_ord, noad_ord
+ end
+ last = noad_ord
+ end
+ elseif id == fraction_t or id == radical_t or id == accent_t then
+ last = noad_ord
+ end
+ end
+ if node.is_node(last) then
+ replacements[node.direct.todirect(last)] = true
+ last.subtype = noad_ord
+ end
+ return replacements
+end
+
+function nodes_to_table(head, cur_style, text_families)
+ local bin_replacements = cleanup_mathbin(head)
+ local t = {[0] = 'mrow'}
+ local result = t
+ local nonscript
+ local core, last_noad, last_core, joining = space_like, nil, nil, nil
+ for n, id, sub in node.traverse(head) do
+ local new_core, new_joining, new_node, new_noad
+ local props = properties[n]
+ local mathml_core = props and props.mathml_core
+ local mathml_table = props and (props.mathml_table or mathml_core)
+ if mathml_table ~= nil then
+ new_node, new_core = mathml_table, mathml_core
+ elseif id == noad_t then
+ local new_n
+ new_n, new_core, new_joining = noad_to_table(n, sub, cur_style, joining, bin_replacements, text_families)
+ if new_joining == false then
+ t[#t], new_joining = new_n, nil
+ else
+ new_node = new_n -- might be nil
+ end
+ new_noad = sub
+ elseif id == accent_t then
+ new_node, new_core = accent_to_table(n, sub, cur_style, text_families)
+ new_noad = noad_ord
+ elseif id == style_t then
+ if sub ~= cur_style then
+ if #t == 0 then
+ t[0] = 'mstyle'
+ else
+ local new_t = {[0] = 'mstyle'}
+ t[#t+1] = new_t
+ t = new_t
+ end
+ if sub < 2 then
+ t.displaystyle, t.scriptlevel = true, 0
+ else
+ t.displaystyle, t.scriptlevel = false, sub//2 - 1
+ end
+ cur_style = sub
+ end
+ new_core = space_like
+ elseif id == choice_t then
+ local size = cur_style//2
+ new_node, new_core = nodes_to_table(n[size == 0 and 'display'
+ or size == 1 and 'text'
+ or size == 2 and 'script'
+ or size == 3 and 'scriptscript'
+ or assert(false)], 2*size, text_families), space_like
+ elseif id == radical_t then
+ new_node, new_core = radical_to_table(n, sub, cur_style, text_families)
+ new_noad = noad_ord
+ elseif id == fraction_t then
+ new_node, new_core = fraction_to_table(n, sub, cur_style, text_families)
+ new_noad = noad_inner
+ elseif id == fence_t then
+ new_node, new_core = fence_to_table(n, sub, cur_style)
+ local class = n.class
+ new_noad = class >= 0 and class or sub == fence_sub.left and noad_open or noad_close
+ elseif id == kern_t then
+ if not nonscript then
+ new_node, new_core = space_to_table(n.kern, sub, cur_style)
+ end
+ elseif id == glue_t then
+ if cur_style >= 4 or not nonscript then
+ if sub == 98 then -- TODO magic number
+ nonscript = true
+ else
+ new_node, new_core = space_to_table(n.width, sub, cur_style)
+ end
+ end
+ elseif id == rule_t then
+ new_node, new_core = rule_to_table(n, sub, cur_style)
+ -- elseif id == disc_t then -- Uncommon, does not play nicely with math mode and no sensible mapping anyway
+ end -- The other possible ids are whatsit, penalty, adjust, ins, mark. Ignore them.
+ nonscript = nil
+ if core and new_core ~= space_like then
+ core = core == space_like and new_core or nil
+ end
+ if new_node then
+ if new_noad then
+ local space = last_noad and (cur_style >= 4 and spacing_table_script or spacing_table)[last_noad + 1][new_noad + 1] or 0
+ if assert(space) ~= 0 then
+ if new_core and new_core[0] == 'mo' then
+ new_core.lspace = space
+ elseif last_core and last_core[0] == 'mo' then
+ last_core.rspace = space
+ else
+ t[#t+1] = {[0] = 'mspace', width = space} -- TODO Move into operators whenever possible
+ end
+ end
+ last_noad, last_core = new_noad, new_core
+ elseif new_node[0] ~= 'mspace' or new_node.mathbackground then
+ last_core = nil
+ end
+ t[#t+1] = new_node
+ end
+ joining = new_joining
+ end
+ -- In TeX, groups are never space like, so we insert an artificial node instead.
+ -- This node should be ignored for most purposes
+ if core == space_like then
+ core = {[0] = 'mi', ['tex:ignore'] = 'true'}
+ result[#result+1] = core
+ end
+ if t[0] == 'mrow' and #t == 1 then
+ assert(t == result)
+ result = t[1]
+ end
+ local mathml_filter = props and props.mathml_filter
+ if mathml_filter then
+ return mathml_filter(result, core)
+ else
+ return result, core
+ end
+end
+
+local function register_remap(family, mapping)
+ family = family << 21
+ for from, to in next, mapping do
+ remap_lookup[family | from] = utf8.char(to)
+ end
+end
+
+local function to_math(root, style)
+ if root[0] == 'mrow' then
+ root[0] = 'math'
+ else
+ root = {[0] = 'math', root}
+ end
+ root.xmlns = 'http://www.w3.org/1998/Math/MathML'
+ root['xmlns:tex'] = 'http://typesetting.eu/2021/LuaMathML'
+ if style < 2 then
+ root.display = 'block'
+ end
+ return root
+end
+
+return {
+ register_family = register_remap,
+ process = function(head, style, families) return nodes_to_table(head, style or 2, families) end,
+ make_root = to_math,
+}
diff --git a/macros/luatex/latex/luamml/luamml-data-combining.lua b/macros/luatex/latex/luamml/luamml-data-combining.lua
new file mode 100644
index 0000000000..2083a3f654
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-data-combining.lua
@@ -0,0 +1,27 @@
+return {
+ ["\u{0332}"] = "\u{2212}",
+ ["\u{0330}"] = "\u{02DC}",
+ ["\u{0328}"] = "\u{02DB}",
+ ["\u{20EF}"] = "\u{2192}",
+ ["\u{032C}"] = "\u{02C7}",
+ ["\u{032E}"] = "\u{02D8}",
+ ["\u{0306}"] = "\u{02D8}",
+ ["\u{030B}"] = "\u{02DD}",
+ ["\u{0302}"] = "\u{02C6}",
+ ["\u{0324}"] = "\u{00A8}",
+ ["\u{0317}"] = "\u{00B4}",
+ ["\u{031F}"] = "\u{002B}",
+ ["\u{0307}"] = "\u{002E}",
+ ["\u{0305}"] = "\u{2212}",
+ ["\u{0303}"] = "\u{02DC}",
+ ["\u{0316}"] = "\u{0060}",
+ ["\u{0301}"] = "\u{00B4}",
+ ["\u{030C}"] = "\u{02C7}",
+ ["\u{0327}"] = "\u{00B8}",
+ ["\u{0308}"] = "\u{00A8}",
+ ["\u{0300}"] = "\u{0060}",
+ ["\u{0323}"] = "\u{002E}",
+ ["\u{0304}"] = "\u{00AF}",
+ ["\u{032D}"] = "\u{005E}",
+ ["\u{20D7}"] = "\u{2192}",
+}
diff --git a/macros/luatex/latex/luamml/luamml-data-stretchy.lua b/macros/luatex/latex/luamml/luamml-data-stretchy.lua
new file mode 100644
index 0000000000..72f84065c0
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-data-stretchy.lua
@@ -0,0 +1,500 @@
+return {
+ -- The following is
+ -- xq -r '.unicode.charlist.character[] | select(.["operator-dictionary"] | if type == "array" then .[] else . end | .["@stretchy"]) | .["@id"] | sub("U"; "") | "[\"\\u{\(.)}\"] = true," ' < unicode.xml
+["\u{00028}"] = true,
+["\u{00029}"] = true,
+["\u{0005B}"] = true,
+["\u{0005D}"] = true,
+["\u{0005E}"] = true,
+["\u{0005F}"] = true,
+["\u{0007B}"] = true,
+["\u{0007C}"] = true,
+["\u{0007C}"] = true,
+["\u{0007D}"] = true,
+["\u{0007E}"] = true,
+["\u{000AF}"] = true,
+["\u{002C6}"] = true,
+["\u{002C7}"] = true,
+["\u{002C9}"] = true,
+["\u{002CD}"] = true,
+["\u{002DC}"] = true,
+["\u{002F7}"] = true,
+["\u{00302}"] = true,
+["\u{02016}"] = true,
+["\u{02016}"] = true,
+["\u{0203E}"] = true,
+["\u{02190}"] = true,
+["\u{02191}"] = true,
+["\u{02192}"] = true,
+["\u{02193}"] = true,
+["\u{02194}"] = true,
+["\u{02195}"] = true,
+["\u{0219A}"] = true,
+["\u{0219B}"] = true,
+["\u{0219C}"] = true,
+["\u{0219D}"] = true,
+["\u{0219E}"] = true,
+["\u{0219F}"] = true,
+["\u{021A0}"] = true,
+["\u{021A1}"] = true,
+["\u{021A2}"] = true,
+["\u{021A3}"] = true,
+["\u{021A4}"] = true,
+["\u{021A5}"] = true,
+["\u{021A6}"] = true,
+["\u{021A7}"] = true,
+["\u{021A8}"] = true,
+["\u{021A9}"] = true,
+["\u{021AA}"] = true,
+["\u{021AB}"] = true,
+["\u{021AC}"] = true,
+["\u{021AD}"] = true,
+["\u{021AE}"] = true,
+["\u{021B0}"] = true,
+["\u{021B1}"] = true,
+["\u{021B2}"] = true,
+["\u{021B3}"] = true,
+["\u{021B4}"] = true,
+["\u{021B5}"] = true,
+["\u{021B9}"] = true,
+["\u{021BC}"] = true,
+["\u{021BD}"] = true,
+["\u{021BE}"] = true,
+["\u{021BF}"] = true,
+["\u{021C0}"] = true,
+["\u{021C1}"] = true,
+["\u{021C2}"] = true,
+["\u{021C3}"] = true,
+["\u{021C4}"] = true,
+["\u{021C5}"] = true,
+["\u{021C6}"] = true,
+["\u{021C7}"] = true,
+["\u{021C8}"] = true,
+["\u{021C9}"] = true,
+["\u{021CA}"] = true,
+["\u{021CB}"] = true,
+["\u{021CC}"] = true,
+["\u{021CD}"] = true,
+["\u{021CE}"] = true,
+["\u{021CF}"] = true,
+["\u{021D0}"] = true,
+["\u{021D1}"] = true,
+["\u{021D2}"] = true,
+["\u{021D3}"] = true,
+["\u{021D4}"] = true,
+["\u{021D5}"] = true,
+["\u{021DA}"] = true,
+["\u{021DB}"] = true,
+["\u{021DC}"] = true,
+["\u{021DD}"] = true,
+["\u{021DE}"] = true,
+["\u{021DF}"] = true,
+["\u{021E0}"] = true,
+["\u{021E1}"] = true,
+["\u{021E2}"] = true,
+["\u{021E3}"] = true,
+["\u{021E4}"] = true,
+["\u{021E5}"] = true,
+["\u{021E6}"] = true,
+["\u{021E7}"] = true,
+["\u{021E8}"] = true,
+["\u{021E9}"] = true,
+["\u{021EA}"] = true,
+["\u{021EB}"] = true,
+["\u{021EC}"] = true,
+["\u{021ED}"] = true,
+["\u{021EE}"] = true,
+["\u{021EF}"] = true,
+["\u{021F0}"] = true,
+["\u{021F3}"] = true,
+["\u{021F4}"] = true,
+["\u{021F5}"] = true,
+["\u{021F6}"] = true,
+["\u{021F7}"] = true,
+["\u{021F8}"] = true,
+["\u{021F9}"] = true,
+["\u{021FA}"] = true,
+["\u{021FB}"] = true,
+["\u{021FC}"] = true,
+["\u{021FD}"] = true,
+["\u{021FE}"] = true,
+["\u{021FF}"] = true,
+["\u{02308}"] = true,
+["\u{02309}"] = true,
+["\u{0230A}"] = true,
+["\u{0230B}"] = true,
+["\u{02322}"] = true,
+["\u{02323}"] = true,
+["\u{02329}"] = true,
+["\u{0232A}"] = true,
+["\u{023B4}"] = true,
+["\u{023B5}"] = true,
+["\u{023DC}"] = true,
+["\u{023DD}"] = true,
+["\u{023DE}"] = true,
+["\u{023DF}"] = true,
+["\u{023E0}"] = true,
+["\u{023E1}"] = true,
+["\u{02772}"] = true,
+["\u{02773}"] = true,
+["\u{02794}"] = true,
+["\u{02799}"] = true,
+["\u{0279B}"] = true,
+["\u{0279C}"] = true,
+["\u{0279D}"] = true,
+["\u{0279E}"] = true,
+["\u{0279F}"] = true,
+["\u{027A0}"] = true,
+["\u{027A1}"] = true,
+["\u{027A5}"] = true,
+["\u{027A6}"] = true,
+["\u{027A8}"] = true,
+["\u{027A9}"] = true,
+["\u{027AA}"] = true,
+["\u{027AB}"] = true,
+["\u{027AC}"] = true,
+["\u{027AD}"] = true,
+["\u{027AE}"] = true,
+["\u{027AF}"] = true,
+["\u{027B1}"] = true,
+["\u{027B3}"] = true,
+["\u{027B5}"] = true,
+["\u{027B8}"] = true,
+["\u{027BA}"] = true,
+["\u{027BB}"] = true,
+["\u{027BC}"] = true,
+["\u{027BD}"] = true,
+["\u{027BE}"] = true,
+["\u{027E6}"] = true,
+["\u{027E7}"] = true,
+["\u{027E8}"] = true,
+["\u{027E9}"] = true,
+["\u{027EA}"] = true,
+["\u{027EB}"] = true,
+["\u{027EC}"] = true,
+["\u{027ED}"] = true,
+["\u{027EE}"] = true,
+["\u{027EF}"] = true,
+["\u{027F0}"] = true,
+["\u{027F1}"] = true,
+["\u{027F4}"] = true,
+["\u{027F5}"] = true,
+["\u{027F6}"] = true,
+["\u{027F7}"] = true,
+["\u{027F8}"] = true,
+["\u{027F9}"] = true,
+["\u{027FA}"] = true,
+["\u{027FB}"] = true,
+["\u{027FC}"] = true,
+["\u{027FD}"] = true,
+["\u{027FE}"] = true,
+["\u{027FF}"] = true,
+["\u{02900}"] = true,
+["\u{02901}"] = true,
+["\u{02902}"] = true,
+["\u{02903}"] = true,
+["\u{02904}"] = true,
+["\u{02905}"] = true,
+["\u{02906}"] = true,
+["\u{02907}"] = true,
+["\u{02908}"] = true,
+["\u{02909}"] = true,
+["\u{0290A}"] = true,
+["\u{0290B}"] = true,
+["\u{0290C}"] = true,
+["\u{0290D}"] = true,
+["\u{0290E}"] = true,
+["\u{0290F}"] = true,
+["\u{02910}"] = true,
+["\u{02911}"] = true,
+["\u{02912}"] = true,
+["\u{02913}"] = true,
+["\u{02914}"] = true,
+["\u{02915}"] = true,
+["\u{02916}"] = true,
+["\u{02917}"] = true,
+["\u{02918}"] = true,
+["\u{02919}"] = true,
+["\u{0291A}"] = true,
+["\u{0291B}"] = true,
+["\u{0291C}"] = true,
+["\u{0291D}"] = true,
+["\u{0291E}"] = true,
+["\u{0291F}"] = true,
+["\u{02920}"] = true,
+["\u{02934}"] = true,
+["\u{02935}"] = true,
+["\u{02936}"] = true,
+["\u{02937}"] = true,
+["\u{02942}"] = true,
+["\u{02943}"] = true,
+["\u{02944}"] = true,
+["\u{02945}"] = true,
+["\u{02946}"] = true,
+["\u{02947}"] = true,
+["\u{02948}"] = true,
+["\u{02949}"] = true,
+["\u{0294A}"] = true,
+["\u{0294B}"] = true,
+["\u{0294C}"] = true,
+["\u{0294D}"] = true,
+["\u{0294E}"] = true,
+["\u{0294F}"] = true,
+["\u{02950}"] = true,
+["\u{02951}"] = true,
+["\u{02952}"] = true,
+["\u{02953}"] = true,
+["\u{02954}"] = true,
+["\u{02955}"] = true,
+["\u{02956}"] = true,
+["\u{02957}"] = true,
+["\u{02958}"] = true,
+["\u{02959}"] = true,
+["\u{0295A}"] = true,
+["\u{0295B}"] = true,
+["\u{0295C}"] = true,
+["\u{0295D}"] = true,
+["\u{0295E}"] = true,
+["\u{0295F}"] = true,
+["\u{02960}"] = true,
+["\u{02961}"] = true,
+["\u{02962}"] = true,
+["\u{02963}"] = true,
+["\u{02964}"] = true,
+["\u{02965}"] = true,
+["\u{02966}"] = true,
+["\u{02967}"] = true,
+["\u{02968}"] = true,
+["\u{02969}"] = true,
+["\u{0296A}"] = true,
+["\u{0296B}"] = true,
+["\u{0296C}"] = true,
+["\u{0296D}"] = true,
+["\u{0296E}"] = true,
+["\u{0296F}"] = true,
+["\u{02970}"] = true,
+["\u{02971}"] = true,
+["\u{02972}"] = true,
+["\u{02973}"] = true,
+["\u{02974}"] = true,
+["\u{02975}"] = true,
+["\u{0297C}"] = true,
+["\u{0297D}"] = true,
+["\u{0297E}"] = true,
+["\u{0297F}"] = true,
+["\u{02980}"] = true,
+["\u{02980}"] = true,
+["\u{02983}"] = true,
+["\u{02984}"] = true,
+["\u{02985}"] = true,
+["\u{02986}"] = true,
+["\u{02987}"] = true,
+["\u{02988}"] = true,
+["\u{02989}"] = true,
+["\u{0298A}"] = true,
+["\u{0298B}"] = true,
+["\u{0298C}"] = true,
+["\u{0298D}"] = true,
+["\u{0298E}"] = true,
+["\u{0298F}"] = true,
+["\u{02990}"] = true,
+["\u{02991}"] = true,
+["\u{02992}"] = true,
+["\u{02993}"] = true,
+["\u{02994}"] = true,
+["\u{02995}"] = true,
+["\u{02996}"] = true,
+["\u{02997}"] = true,
+["\u{02998}"] = true,
+["\u{02999}"] = true,
+["\u{02999}"] = true,
+["\u{029D8}"] = true,
+["\u{029D9}"] = true,
+["\u{029DA}"] = true,
+["\u{029DB}"] = true,
+["\u{029FC}"] = true,
+["\u{029FD}"] = true,
+["\u{02B04}"] = true,
+["\u{02B05}"] = true,
+["\u{02B06}"] = true,
+["\u{02B07}"] = true,
+["\u{02B0C}"] = true,
+["\u{02B0D}"] = true,
+["\u{02B0E}"] = true,
+["\u{02B0F}"] = true,
+["\u{02B10}"] = true,
+["\u{02B11}"] = true,
+["\u{02B30}"] = true,
+["\u{02B31}"] = true,
+["\u{02B32}"] = true,
+["\u{02B33}"] = true,
+["\u{02B34}"] = true,
+["\u{02B35}"] = true,
+["\u{02B36}"] = true,
+["\u{02B37}"] = true,
+["\u{02B38}"] = true,
+["\u{02B39}"] = true,
+["\u{02B3A}"] = true,
+["\u{02B3B}"] = true,
+["\u{02B3C}"] = true,
+["\u{02B3D}"] = true,
+["\u{02B3E}"] = true,
+["\u{02B40}"] = true,
+["\u{02B41}"] = true,
+["\u{02B42}"] = true,
+["\u{02B43}"] = true,
+["\u{02B44}"] = true,
+["\u{02B45}"] = true,
+["\u{02B46}"] = true,
+["\u{02B47}"] = true,
+["\u{02B48}"] = true,
+["\u{02B49}"] = true,
+["\u{02B4A}"] = true,
+["\u{02B4B}"] = true,
+["\u{02B4C}"] = true,
+["\u{02B60}"] = true,
+["\u{02B61}"] = true,
+["\u{02B62}"] = true,
+["\u{02B63}"] = true,
+["\u{02B64}"] = true,
+["\u{02B65}"] = true,
+["\u{02B6A}"] = true,
+["\u{02B6B}"] = true,
+["\u{02B6C}"] = true,
+["\u{02B6D}"] = true,
+["\u{02B70}"] = true,
+["\u{02B71}"] = true,
+["\u{02B72}"] = true,
+["\u{02B73}"] = true,
+["\u{02B7A}"] = true,
+["\u{02B7B}"] = true,
+["\u{02B7C}"] = true,
+["\u{02B7D}"] = true,
+["\u{02B80}"] = true,
+["\u{02B81}"] = true,
+["\u{02B82}"] = true,
+["\u{02B83}"] = true,
+["\u{02B84}"] = true,
+["\u{02B85}"] = true,
+["\u{02B86}"] = true,
+["\u{02B87}"] = true,
+["\u{02B95}"] = true,
+["\u{02BA0}"] = true,
+["\u{02BA1}"] = true,
+["\u{02BA2}"] = true,
+["\u{02BA3}"] = true,
+["\u{02BA4}"] = true,
+["\u{02BA5}"] = true,
+["\u{02BA6}"] = true,
+["\u{02BA7}"] = true,
+["\u{02BA8}"] = true,
+["\u{02BA9}"] = true,
+["\u{02BAA}"] = true,
+["\u{02BAB}"] = true,
+["\u{02BAC}"] = true,
+["\u{02BAD}"] = true,
+["\u{02BAE}"] = true,
+["\u{02BAF}"] = true,
+["\u{02BB8}"] = true,
+["\u{1EEF0}"] = true,
+["\u{1EEF1}"] = true,
+["\u{1F800}"] = true,
+["\u{1F801}"] = true,
+["\u{1F802}"] = true,
+["\u{1F803}"] = true,
+["\u{1F804}"] = true,
+["\u{1F805}"] = true,
+["\u{1F806}"] = true,
+["\u{1F807}"] = true,
+["\u{1F808}"] = true,
+["\u{1F809}"] = true,
+["\u{1F80A}"] = true,
+["\u{1F80B}"] = true,
+["\u{1F810}"] = true,
+["\u{1F811}"] = true,
+["\u{1F812}"] = true,
+["\u{1F813}"] = true,
+["\u{1F814}"] = true,
+["\u{1F815}"] = true,
+["\u{1F816}"] = true,
+["\u{1F817}"] = true,
+["\u{1F818}"] = true,
+["\u{1F819}"] = true,
+["\u{1F81A}"] = true,
+["\u{1F81B}"] = true,
+["\u{1F81C}"] = true,
+["\u{1F81D}"] = true,
+["\u{1F81E}"] = true,
+["\u{1F81F}"] = true,
+["\u{1F820}"] = true,
+["\u{1F821}"] = true,
+["\u{1F822}"] = true,
+["\u{1F823}"] = true,
+["\u{1F824}"] = true,
+["\u{1F825}"] = true,
+["\u{1F826}"] = true,
+["\u{1F827}"] = true,
+["\u{1F828}"] = true,
+["\u{1F829}"] = true,
+["\u{1F82A}"] = true,
+["\u{1F82B}"] = true,
+["\u{1F82C}"] = true,
+["\u{1F82D}"] = true,
+["\u{1F82E}"] = true,
+["\u{1F82F}"] = true,
+["\u{1F830}"] = true,
+["\u{1F831}"] = true,
+["\u{1F832}"] = true,
+["\u{1F833}"] = true,
+["\u{1F834}"] = true,
+["\u{1F835}"] = true,
+["\u{1F836}"] = true,
+["\u{1F837}"] = true,
+["\u{1F844}"] = true,
+["\u{1F845}"] = true,
+["\u{1F846}"] = true,
+["\u{1F847}"] = true,
+["\u{1F850}"] = true,
+["\u{1F851}"] = true,
+["\u{1F852}"] = true,
+["\u{1F853}"] = true,
+["\u{1F858}"] = true,
+["\u{1F859}"] = true,
+["\u{1F860}"] = true,
+["\u{1F861}"] = true,
+["\u{1F862}"] = true,
+["\u{1F863}"] = true,
+["\u{1F868}"] = true,
+["\u{1F869}"] = true,
+["\u{1F86A}"] = true,
+["\u{1F86B}"] = true,
+["\u{1F870}"] = true,
+["\u{1F871}"] = true,
+["\u{1F872}"] = true,
+["\u{1F873}"] = true,
+["\u{1F878}"] = true,
+["\u{1F879}"] = true,
+["\u{1F87A}"] = true,
+["\u{1F87B}"] = true,
+["\u{1F880}"] = true,
+["\u{1F881}"] = true,
+["\u{1F882}"] = true,
+["\u{1F883}"] = true,
+["\u{1F898}"] = true,
+["\u{1F899}"] = true,
+["\u{1F89A}"] = true,
+["\u{1F89B}"] = true,
+["\u{1F8A0}"] = true,
+["\u{1F8A1}"] = true,
+["\u{1F8A2}"] = true,
+["\u{1F8A3}"] = true,
+["\u{1F8A4}"] = true,
+["\u{1F8A5}"] = true,
+["\u{1F8A6}"] = true,
+["\u{1F8A7}"] = true,
+["\u{1F8A8}"] = true,
+["\u{1F8A9}"] = true,
+["\u{1F8AA}"] = true,
+["\u{1F8AB}"] = true,
+-- till here
+}
diff --git a/macros/luatex/latex/luamml/luamml-demo.sty b/macros/luatex/latex/luamml/luamml-demo.sty
new file mode 100644
index 0000000000..64e4e3ff7f
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-demo.sty
@@ -0,0 +1,82 @@
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesExplPackage{luamml-demo}{2024-08-14}{v0.1.0}{Reasonable default definitions for luamml}
+
+\sys_if_engine_luatex:F {
+ \msg_new:nnn {luamml-demo} {pdftex-option-ignored} {Option~`#1'~is~being~ignored~in~pdfTeX~mode.}
+ \DeclareOption*{\msg_warning:nnx {luamml-demo} {pdftex-option-ignored} {\CurrentOption}}
+ \ProcessOptions\relax
+ \RequirePackage{luamml-pdf-demo}
+ \endinput
+}
+
+\RequirePackage{luamml}% Loading luamml is pretty much the point
+\RequirePackage{amsmath,array}% These are more or less expected in luamml especially for advanced constructs
+
+\AtBeginDocument{%
+ \@ifpackageloaded{unicode-math}{}{%
+ \RegisterFamilyMapping\symsymbols{oms}%
+ \RegisterFamilyMapping\symletters{oml}%
+ \RegisterFamilyMapping\symlargesymbols{omx}%
+ }
+}
+
+\bool_new:N \l__luamml_demo_structelem_bool
+
+\DeclareOption{tracing}{
+ \tracingmathml=2
+}
+\DeclareOption{structelem}{
+ \bool_set_true:N \l__luamml_demo_structelem_bool
+ \luamml_flag_structelem:
+}
+\DeclareOption{files}{
+ \int_new:N \g__luamml_demo_mathml_int
+ \luamml_set_filename:n {
+ \immediateassignment \int_gincr:N \g__luamml_demo_mathml_int
+ \jobname -formula- \int_use:N \g__luamml_demo_mathml_int .xml
+ }
+}
+\DeclareOption{l3build}{
+ \luamml_set_filename:n {
+ \jobname .mml
+ }
+ \luamml_begin_single_file:
+}
+\ProcessOptions\relax
+
+\cs_new_eq:NN \LuaMMLSetFilename \luamml_set_filename:n
+
+\cs_generate_variant:Nn \pdffile_filespec:nnn {ene}
+\int_new:N \g__luamml_demo_af_int
+\cs_new_protected:Npn \LuaMMLTagAF #1#2 {
+ \tag_mc_end_push:
+ \int_gincr:N \g__luamml_demo_af_int
+ \exp_args:Ne \pdf_object_new:nn{__luamml_demo_\int_use:N \g__luamml_demo_af_int}{dict}
+ \exp_args:Ne \tag_struct_begin:n{tag=Formula,AF=__luamml_demo_\int_use:N \g__luamml_demo_af_int,#1}
+ \bool_if:NF \l__luamml_demo_structelem_bool {
+ \tag_mc_begin:n{tag=Formula}
+ }
+ #2
+ \group_begin:
+ \pdfdict_put:nnn {l_pdffile/Filespec} {AFRelationship}{/Supplement}
+ \pdffile_filespec:ene
+ { __luamml_demo_ \int_use:N \g__luamml_demo_af_int }
+ { test.xml }
+ { \luamml_get_last_mathml_stream:e{}\c_space_tl 0~R}
+ \group_end:
+ \bool_if:NF \l__luamml_demo_structelem_bool {
+ \tag_mc_end:
+ }
+ \tag_struct_end:
+ \tag_mc_begin_pop:n{}
+}
+
+\NewDocumentCommand\AnnotateFormula{ o m m }{%
+ \IfValueTF{#1}{%
+ \luamml_annotate:nen{#1}%
+ }{
+ \luamml_annotate:en
+ }{#2}{#3}
+}
+
+\cs_set_eq:NN \WriteoutFormula \luamml_pdf_write:
diff --git a/macros/luatex/latex/luamml/luamml-legacy-mappings.lua b/macros/luatex/latex/luamml/luamml-legacy-mappings.lua
new file mode 100644
index 0000000000..6a56b70dbd
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-legacy-mappings.lua
@@ -0,0 +1,73 @@
+-- local remap_ot1 = {
+ -- 0x0393, 0x0394, 0x0398, 0x039B, 0x039E, 0x03A0, 0x03A3, 0x03A5,
+ -- 0x03A6, 0x03A8, 0x03A9, nil, nil, nil, nil, nil,
+-- }
+
+local remap_oml = { [0] =
+ -- Greek italic
+ 0x1D6E4, 0x1D6E5, 0x1D6E9, 0x1D6EC, 0x1D6EF, 0x1D6F1, 0x1D6F4, 0x1D6F6,
+ 0x1D6F7, 0x1D6F9, 0x1D6FA, 0x1D6FC, 0x1D6FD, 0x1D6FE, 0x1D6FF, 0x1D716,
+ 0x1D701, 0x1D702, 0x1D703, 0x1D704, 0x1D705, 0x1D706, 0x1D707, 0x1D708,
+ 0x1D709, 0x1D70B, 0x1D70C, 0x1D70E, 0x1D70F, 0x1D710, 0x1D719, 0x1D712,
+ 0x1D713, 0x1D714, 0x1D700, 0x1D717, 0x1D71B, 0x1D71A, 0x1D70D, 0x1D711,
+ -- Symbols. (The nils are hook parts)
+ 0x21BC, 0x21BD, 0x21C0, 0x21C1, nil, nil, 0x22BB, 0x22BC,
+ -- old style numerals (nobody should ever use these in math) and some punctuation
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, 0x2E, 0x2C, 0x3C, 0x2F, 0x3E, 0x226D,
+ -- letters filled up with symbols
+ 0x2202, 0x1D434, 0x1D435, 0x1D436, 0x1D437, 0x1D438, 0x1D439, 0x1D43A,
+ 0x1D43B, 0x1D43C, 0x1D43D, 0x1D43E, 0x1D43F, 0x1D440, 0x1D441, 0x1D442,
+ 0x1D443, 0x1D444, 0x1D445, 0x1D446, 0x1D447, 0x1D448, 0x1D449, 0x1D44A,
+ 0x1D44B, 0x1D44C, 0x1D44D, 0x266D, 0x266E, 0x266F, 0x2323, 0x2322,
+ 0x2113, 0x1D44E, 0x1D44F, 0x1D450, 0x1D451, 0x1D452, 0x1D453, 0x1D454,
+ 0x210E, 0x1D456, 0x1D457, 0x1D458, 0x1D459, 0x1D45A, 0x1D45B, 0x1D45C,
+ 0x1D45D, 0x1D45E, 0x1D45F, 0x1D460, 0x1D461, 0x1D462, 0x1D463, 0x1D464,
+ 0x1D465, 0x1D466, 0x1D467, 0x1D6A4, 0x1D6A5, 0x2118, 0x2192, nil,
+}
+
+-- Something fishy here. Starting with "3D the entries seem wrong
+local remap_oms = { [0] =
+ 0x2212, 0x22C5, 0xD7, 0x2A, 0xF7, 0x22C4, 0xB1, 0x2213,
+ 0x2295, 0x2296, 0x2297, 0x2298, 0x2299, 0x25CB, 0x2218, 0x2219,
+ 0x224D, 0x2261, 0x2286, 0x2287, 0x2264, 0x2265, 0x2AAF, 0x2AB0,
+ 0x223C, 0x2248, 0x2282, 0x2283, 0x226A, 0x226B, 0x227A, 0x227B,
+ 0x2190, 0x2192, 0x2191, 0x2193, 0x2194, 0x2197, 0x2198, 0x2243,
+ 0x21D0, 0x21D2, 0x21D1, 0x21D3, 0x21D4, 0x2196, 0x2199, 0x221D,
+ 0x2032, 0x221E, 0x2208, 0x220B, 0x25B3, 0x25BD, 0x0338, 0x21A6,
+ 0x2200, 0x2203, 0xAC, 0x2205, 0x211C, 0x2111, 0x22A4, 0x22A5,
+ 0x2135, 0x1D49C, 0x212C, 0x1D49E, 0x1D49F, 0x2130, 0x2131, 0x1D4A2,
+ 0x210B, 0x2110, 0x1D4A5, 0x1D4A6, 0x2112, 0x2133, 0x1D4A9, 0x1D4AA,
+ 0x1D4AB, 0x1D4AC, 0x211B, 0x1D4AE, 0x1D4AF, 0x1D4B0, 0x1D4B1, 0x1D4B2,
+ 0x1D4B3, 0x1D4B4, 0x1D4B5, 0x222A, 0x2229, 0x228E, 0x2227, 0x2228,
+ 0x22A2, 0x22A3, 0x230A, 0x230B, 0x2308, 0x2309, 0x7B, 0x7D,
+ 0x27E8, 0x27E9, 0x7C, 0x2016, 0x2195, 0x21D5, 0x5C, 0x2240,
+ 0x221A, 0x2A3F, 0x2207, 0x222B, 0x2294, 0x2293, 0x2291, 0x2292,
+ 0xA7, 0x2020, 0x2021, 0xB6, 0x2663, 0x2662, 0x2661, 0x2660,
+}
+
+-- We are not remapping symbols which are only used as large variants
+local remap_omx = { [0] =
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, 0x27EE, 0x27EF, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, 0x2A06, nil,
+ 0x222E, nil, 0x2A00, nil, 0x2A01, nil, 0x2A02, nil,
+ 0x2211, 0x220F, 0x222B, 0x22C3, 0x22C2, 0x2A04, 0x22C0, 0x22C1,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ 0x2210, nil, 0x5E, 0x5E, 0x5E, 0x7E, 0x7E, 0x7E,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, 0x23B0, 0x23B1, nil, nil, nil, nil,
+}
+
+return {
+ oml = remap_oml,
+ oms = remap_oms,
+ omx = remap_omx,
+}
diff --git a/macros/luatex/latex/luamml/luamml-lr.lua b/macros/luatex/latex/luamml/luamml-lr.lua
new file mode 100644
index 0000000000..b78a7c3803
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-lr.lua
@@ -0,0 +1,71 @@
+local properties = node.get_properties_table()
+
+local function to_unicode(head, tail)
+ local result, subresult, i = {[0] = 'mtext'}, {}, 0
+ local characters, last_fid
+ local iter, state, n = node.traverse(head)
+ while true do
+ local id, sub n, id, sub = iter(state, n)
+ if not n or n == tail then break end
+ local props = properties[n]
+ if props and props.glyph_info then
+ i = i+1
+ result[i] = glyph_info
+ else
+ local char, fid = node.is_glyph(n)
+ if char then
+ if fid ~= last_fid then
+ local fontdir = font.getfont(fid)
+ characters, last_fid = fontdir.characters, fid
+ end
+ local uni = characters[char]
+ local uni = uni and uni.unicode
+ i = i+1
+ if uni then
+ if type(uni) == 'number' then
+ result[i] = utf.char(uni)
+ else
+ result[i] = utf.char(table.unpack(uni))
+ end
+ else
+ if char < 0x110000 then
+ result[i] = utf.char(char)
+ else
+ result[i] = '\u{FFFD}'
+ end
+ end
+ elseif node.id'math' == id then
+ if props then
+ local mml = props.saved_mathml_table or props.saved_mathml_core
+ if mml then
+ i = i+1
+ result[i] = mml
+ n = node.end_of_math(n)
+ end
+ end
+ -- elseif node.id'whatsit' == id then
+ -- TODO(?)
+ elseif node.id'glue' == id then
+ if n.width > 1000 then -- FIXME: Coordinate constant with tagpdf
+ i = i+1
+ result[i] = '\u{00A0}' -- non breaking space... There is no real reason why it has to be non breaking, except that MathML often ignore other spaces
+ end
+ elseif node.id'hlist' == id then
+ local nested = to_unicode(n.head)
+ table.move(nested, 1, #nested, i+1, result)
+ i = i+#nested
+ elseif node.id'vlist' == id then
+ i = i+1
+ result[i] = '\u{FFFD}'
+ elseif node.id'rule' == id then
+ if n.width ~= 0 then
+ i = i+1
+ result[i] = '\u{FFFD}'
+ end
+ end -- CHECK: Everything else can probably be ignored, otherwise shout at me
+ end
+ end
+ return result
+end
+
+return to_unicode
diff --git a/macros/luatex/latex/luamml/luamml-patches-amsmath.sty b/macros/luatex/latex/luamml/luamml-patches-amsmath.sty
new file mode 100644
index 0000000000..c0df5ed963
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-patches-amsmath.sty
@@ -0,0 +1,417 @@
+\ProvidesExplPackage {luamml-patches-amsmath} {2024-08-14} {0.1.0}
+ {Feel free to add a description here}
+
+\lua_now:n { require'luamml-amsmath' }
+
+% For all of these changes, the redefinitions appear huge.
+% But they are almost identical to the original and only
+% add luamml commands in appropriate places, so they would
+% mostly disappear if there were enough hooks in amsmath.
+
+% aligned and friends
+\cs_set:Npn \start@aligned #1#2 {
+ \RIfM@
+ \else
+ \nonmatherr@ { \begin { \@currenvir } }
+ \fi
+ \savecolumn@ % Assumption: called inside a group
+ \luamml_annotate:en {
+ core = false
+ } {
+ \alignedspace@left
+ }
+ \ams@start@box {#1} \bgroup
+ \maxfields@ #2 \relax
+ \ifnum \maxfields@ > \m@ne
+ \multiply \maxfields@ \tw@
+ \let \math@cr@@@ \math@cr@@@alignedat
+ \alignsep@ \z@skip
+ \else
+ \let \math@cr@@@ \math@cr@@@aligned
+ \alignsep@ \minalignsep
+ \fi
+ \Let@ \chardef \dspbrk@context \@ne
+ \default@tag
+ \spread@equation % no-op if already called
+ \global \column@ \z@
+ \ialign \bgroup
+ & \column@plus
+ \hfil
+ \strut@
+ $
+ \m@th
+ \displaystyle
+ {##}
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ $
+ \__luamml_amsmath_add_last_to_row:
+ \tabskip \z@skip
+ & \column@plus
+ $
+ \m@th
+ \displaystyle
+ {
+ {}
+ ##
+ }
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ $
+ \__luamml_amsmath_add_last_to_row:
+ \hfil
+ \tabskip\alignsep@
+ \crcr
+ \ams@return@opt@arg
+}
+
+\renewcommand \gathered [1] [c] {
+ \RIfM@
+ \else
+ \nonmatherr@ { \begin {gathered} }
+ \fi
+ \luamml_annotate:en {
+ core = false
+ } {
+ \alignedspace@left
+ }
+ \ams@start@box {#1} \bgroup
+ \Let@
+ \chardef \dspbrk@context \@ne
+ \restore@math@cr
+ \spread@equation
+ \ialign \bgroup
+ \hfil
+ \strut@
+ $
+ \m@th
+ \displaystyle
+ ##
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ $
+ \__luamml_amsmath_add_last_to_row:
+ \hfil
+ \crcr
+ \ams@return@opt@arg
+}
+
+\cs_set:Npn \endaligned {
+ \crcr
+ \__luamml_amsmath_save_inner_table:n \@currenvir
+ \egroup
+ \restorecolumn@
+ \egroup
+ \__luamml_amsmath_finalize_inner_table:
+}
+
+% gather
+\cs_set:Npn \gather@ #1 {
+ \ingather@true
+ \let \split \insplit@
+ \let \tag \tag@in@align
+ \let \label \label@in@display
+ \chardef \dspbrk@context \z@
+ \intertext@ \displ@y@ \Let@
+ \let \math@cr@@@ \math@cr@@@gather
+ \gmeasure@ {#1}
+ \global \shifttag@false
+ \tabskip \z@skip
+ \global \row@ \@ne
+ \halign to \displaywidth \bgroup
+ \strut@
+ \setboxz@h {
+ $
+ \m@th
+ \displaystyle
+ {##}
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ $
+ }
+ \__luamml_amsmath_add_box_to_row:
+ \calc@shift@gather
+ \set@gather@field
+ \tabskip\@centering
+ &
+ \setboxz@h {
+ \strut@
+ {##}
+ }
+ \dim_compare:nNnF {0pt} = {
+ \box_wd:N \c_zero_int
+ } {
+ \__luamml_amsmath_set_tag:
+ }
+ \place@tag@gather
+ \tabskip \iftagsleft@
+ \gdisplaywidth@
+ \else
+ \z@skip
+ \span \fi
+ \crcr
+ #1
+}
+
+\cs_new_eq:NN \__luamml_amsmath_original_gmeasure:n \gmeasure@
+\cs_set:Npn \gmeasure@ #1 {
+ \exp_last_unbraced:Nno
+ \use_ii_i:nn
+ { \luamml_flag_ignore: }
+ { \__luamml_amsmath_original_gmeasure:n {#1} }
+}
+
+\cs_set:Npn \endgather {
+ \math@cr
+ \black@ \totwidth@
+ \__luamml_amsmath_finalize_table:n {gather}
+ \egroup
+ $$
+ \ignorespacesafterend
+}
+
+% align and friends
+\cs_set:Npn \align@preamble {
+ &
+ \hfil
+ \strut@
+ \setboxz@h {
+ \@lign
+ $
+ \m@th
+ \displaystyle
+ {##}
+ \ifmeasuring@
+ \luamml_flag_ignore:
+ \else
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ \fi
+ $
+ }
+ \ifmeasuring@
+ \savefieldlength@
+ \else
+ \__luamml_amsmath_add_box_to_row:
+ \fi
+ \set@field
+ \tabskip\z@skip
+ &
+ \setboxz@h {
+ \@lign
+ $
+ \m@th
+ \displaystyle
+ {
+ {}
+ ##
+ }
+ \ifmeasuring@
+ \luamml_flag_ignore:
+ \else
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ \fi
+ $
+ }
+ \ifmeasuring@
+ \savefieldlength@
+ \else
+ \__luamml_amsmath_add_box_to_row:
+ \fi
+ \set@field
+ \hfil
+ \tabskip\alignsep@
+}
+
+\cs_set:Npn \math@cr@@@align {
+ \ifst@rred
+ \nonumber
+ \fi
+ \if@eqnsw
+ \global \tag@true
+ \fi
+ \global \advance \row@ \@ne
+ \add@amps \maxfields@
+ \omit
+ \kern -\alignsep@
+ \iftag@
+ \setboxz@h {
+ \@lign
+ \strut@
+ { \make@display@tag }
+ }
+ \place@tag
+ \__luamml_amsmath_set_tag:
+ \fi
+ \ifst@rred
+ \else
+ \global \@eqnswtrue
+ \fi
+ \global \lineht@ \z@
+ \cr
+}
+
+\cs_set:Npn \maketag@@@ #1 {
+ \hbox {
+ \m@th
+ \normalfont
+ #1
+ \__luamml_amsmath_save_tag:
+ }
+}
+
+\cs_set:Npn \endalign {
+ \math@cr
+ \black@ \totwidth@
+ \__luamml_amsmath_finalize_table:n {align}
+ \egroup
+ \ifingather@
+ \restorealignstate@
+ \egroup
+ \nonumber
+ \ifnum0=`{\fi\iffalse}\fi
+ \else
+ $$
+ \fi
+ \ignorespacesafterend
+}
+
+% For a more interesting one, let's consider multline:
+\cs_new_eq:NN \__luamml_amsmath_original_multline:n \multline@
+\cs_set:Npn \multline@ #1 {
+ \__luamml_amsmath_original_multline:n {
+ \ifmeasuring@ \else
+ \__luamml_amsmath_set_row_columnalign:n {left}
+ \fi
+ #1
+ \ifmeasuring@ \else
+ \__luamml_amsmath_set_row_columnalign:n {right}
+ \fi
+ }
+}
+
+\cs_new_eq:NN \__luamml_amsmath_original_mmeasure:n \mmeasure@
+\cs_set:Npn \mmeasure@ #1 {
+ \exp_last_unbraced:Nno
+ \use_ii_i:nn
+ { \luamml_flag_ignore: }
+ { \__luamml_amsmath_original_mmeasure:n {#1} }
+}
+
+% Luckily, {multline} uses \endmultline@math in exactly
+% the spot where we have to set the flag.
+% Less luckily, \endmultline@math sometimes get overwritten for the last line.
+% But that isn't a problem since we want special behavior there anyway.
+\cs_set:Npn \endmultline@math {
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ $
+ \__luamml_amsmath_add_last_to_row:
+}
+
+\cs_set:Npn \rendmultline@ {
+ \iftag@
+ \luamml_flag_save:nNn {} \displaystyle {mtd}
+ $
+ \__luamml_amsmath_add_last_to_row:
+ \let \endmultline@math \relax
+ \ifshifttag@
+ \hskip \multlinegap
+ \llap {
+ \vtop {
+ \raise@tag
+ \normalbaselines
+ \setbox \@ne \null
+ \dp \@ne \lineht@
+ \box \@ne
+ \hbox {
+ \strut@
+ \make@display@tag
+ }
+ }
+ }
+ \else
+ \hskip \multlinetaggap
+ \make@display@tag
+ \fi
+ \__luamml_amsmath_set_tag:
+ \else
+ \hskip \multlinegap
+ \fi
+ \hfilneg
+ \math@cr
+ \__luamml_amsmath_finalize_table:n {multline}
+ \egroup
+ $$
+}
+
+\cs_set:Npn \lendmultline@ {
+ \hfilneg
+ \hskip\multlinegap
+ \math@cr
+ \__luamml_amsmath_finalize_table:n {multline}
+ \egroup
+ $$
+}
+
+% Finally some slightly different stuff.
+% While {matrix} is covered by {array}, we still have {smallmatrix}:
+\renewenvironment {smallmatrix} {
+ \luamml_annotate:en {
+ core = false
+ } {
+ \null
+ \,
+ }
+ \vcenter \bgroup
+ \Let@
+ \restore@math@cr
+ \default@tag
+ \baselineskip 6 \ex@
+ \lineskip 1.5 \ex@
+ \lineskiplimit \lineskip
+ \ialign \bgroup
+ \hfil
+ $
+ \m@th
+ \scriptstyle
+ ##
+ \luamml_flag_save:nn {} {mtd} % No \scriptsize here since we want to add the mstyle nodes
+ $
+ \__luamml_amsmath_add_last_to_row:
+ \hfil
+ &&
+ \thickspace
+ \hfil
+ $
+ \m@th
+ \scriptstyle
+ ##
+ \luamml_flag_save:nn {} {mtd} % No \scriptsize here since we want to add the mstyle nodes
+ $
+ \__luamml_amsmath_add_last_to_row:
+ \hfil
+ \crcr
+}{%
+ \crcr
+ \__luamml_amsmath_save_smallmatrix:
+ \egroup
+ \egroup
+ \__luamml_amsmath_finalize_inner_table:
+ \luamml_annotate:en {
+ core = false
+ } {
+ \,
+ }
+}
+
+% {cases} is defined by the kernel, but we patch the overwritten version by amsmath.
+\cs_set:Npn \env@cases {
+ \let \@ifnextchar \new@ifnextchar
+ \left \lbrace
+ \def \arraystretch {1.2}
+ \array {@{}l@{\quad \luamml_flag_ignore:}l@{}}
+}
+
+
+\cs_set:Npn \bBigg@ #1 #2 {
+ {
+ \ensuremath {
+ \Uvextensible height~#1 \big@size axis~exact~#2
+ }
+ }
+}
diff --git a/macros/luatex/latex/luamml/luamml-patches-amstext.sty b/macros/luatex/latex/luamml/luamml-patches-amstext.sty
new file mode 100644
index 0000000000..2217600716
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-patches-amstext.sty
@@ -0,0 +1,37 @@
+\ProvidesExplPackage {luamml-patches-amstext} {2024-08-14} {0.1.0}
+ {Feel free to add a description here}
+
+\int_new:N \g__luamml_amsmath_text_struct_int
+\cs_set:Npn \textdef@ #1 #2 #3 {
+ \int_if_odd:nTF { \int_div_truncate:nn { \l__luamml_flag_int } { 8 } } {
+ \int_gincr:N \g__luamml_amsmath_text_struct_int
+ \tag_struct_begin:n {
+ tag = mtext/mathml,
+ stash,
+ label = __luamml_amsmath_text_ \int_use:N \g__luamml_amsmath_text_struct_int
+ }
+ \tag_mc_begin:n {
+ tag = mtext
+ }
+ \AnnotateFormula {
+ nucleus = true,
+ struct = "__luamml_amsmath_text_ \int_use:N \g__luamml_amsmath_text_struct_int"
+ }
+ } {
+ \use:n
+ }
+ {
+ \hbox {
+ {
+ \everymath {#1}
+ \let \f@size #2
+ \selectfont
+ #3
+ }
+ }
+ }
+ \int_if_odd:nT { \int_div_truncate:nn { \l__luamml_flag_int } { 8 } } {
+ \tag_mc_end:
+ \tag_struct_end:
+ }
+}
diff --git a/macros/luatex/latex/luamml/luamml-patches-array.sty b/macros/luatex/latex/luamml/luamml-patches-array.sty
new file mode 100644
index 0000000000..ee40b2c489
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-patches-array.sty
@@ -0,0 +1,86 @@
+\ProvidesExplPackage {luamml-patches-array} {2024-08-14} {0.1.0}
+ {Feel free to add a description here}
+
+\lua_now:n { require'luamml-array' }
+
+\cs_set:Npn \@classz {
+ \@classx
+ \@tempcnta \count@
+ \prepnext@tok
+ \@addtopreamble {
+ \ifcase \@chnum
+ \hfil
+ \hskip 1sp
+ \d@llarbegin
+ \cs_if_eq:NNTF \d@llarbegin \begingroup {
+ \insert@column
+ \d@llarend
+ } {
+ \__luamml_array_init_col:
+ \insert@column
+ \luamml_flag_save:nn {} {mtd}
+ \d@llarend
+ \__luamml_array_finalize_col:w 0~
+ }
+ \do@row@strut
+ \hfil
+ \or
+ \hskip 1sp
+ \d@llarbegin
+ \cs_if_eq:NNTF \d@llarbegin \begingroup {
+ \insert@column
+ \d@llarend
+ } {
+ \__luamml_array_init_col:
+ \insert@column
+ \luamml_flag_save:nn {} {mtd}
+ \d@llarend
+ \__luamml_array_finalize_col:w 1~
+ }
+ \do@row@strut
+ \hfil
+ \or
+ \hfil
+ \hskip 1sp
+ \d@llarbegin
+ \cs_if_eq:NNTF \d@llarbegin \begingroup {
+ \insert@column
+ \d@llarend
+ } {
+ \__luamml_array_init_col:
+ \insert@column
+ \luamml_flag_save:nn {} {mtd}
+ \d@llarend
+ \__luamml_array_finalize_col:w 2~
+ }
+ \do@row@strut
+ \or
+ \setbox \ar@mcellbox \vbox \@startpbox { \@nextchar }
+ \insert@column
+ \@endpbox
+ \ar@align@mcell
+ \do@row@strut
+ \or
+ \vtop \@startpbox { \@nextchar }
+ \insert@column
+ \@endpbox
+ \do@row@strut
+ \or
+ \vbox \@startpbox { \@nextchar }
+ \insert@column
+ \@endpbox
+ \do@row@strut
+ \fi
+ }
+ \prepnext@tok
+}
+
+\cs_set:Npn \endarray {
+ \crcr
+ \__luamml_array_save_array:
+ \egroup
+ \egroup
+ \mode_if_math:T { \__luamml_array_finalize_array: }
+ \@arrayright
+ \gdef \@preamble {}
+}
diff --git a/macros/luatex/latex/luamml/luamml-patches-kernel.sty b/macros/luatex/latex/luamml/luamml-patches-kernel.sty
new file mode 100644
index 0000000000..fa3aaae0ec
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-patches-kernel.sty
@@ -0,0 +1,63 @@
+\ProvidesExplPackage {luamml-patches-kernel} {2024-08-14} {0.1.0}
+ {Feel free to add a description here}
+
+\cs_new:Npn \__luamml_kernel_phantom:nnn #1#2#3 {
+ \setbox \z@ = \hbox {
+ $
+ \m@th
+ #2
+ {#3}
+ \luamml_flag_save:nNn {mathphant_#1} #2 {mphantom}
+ \luamml_pdf_write:
+ $
+ }
+ \luamml_annotate:nen {1} {
+ nucleus = true,
+ core = {[0] = 'mpadded',
+ \ifh@\else
+ width = 0,
+ \fi
+ \ifv@\else
+ height = 0, depth = 0,
+ \fi
+ data.mathml.mathphant_#1,
+ }
+ } {
+ \finph@nt
+ }
+}
+\cs_generate_variant:Nn \__luamml_kernel_phantom:nnn {V}
+
+\int_new:N \g__luamml_kernel_phantom_int
+\cs_set:Npn \mathph@nt {
+ \int_gincr:N \g__luamml_kernel_phantom_int
+ \__luamml_kernel_phantom:Vnn \g__luamml_kernel_phantom_int
+}
+
+\@ifpackageloaded {unicode-math} {} {
+ \cs_new:Npn \__luamml_kernel_define_character:Nnn #1#2#3 {
+ \cs_set:cpx { \cs_to_str:N #1 ~ } {
+ \luamml_annotate:nen {#2} {
+ nucleus = true, core = {[0] = 'mi', '\string\u{#3}'},
+ } {
+ \exp_not:v { \cs_to_str:N #1 ~ }
+ }
+ }
+ }
+
+ \__luamml_kernel_define_character:Nnn \models {3} {22a7}
+ \__luamml_kernel_define_character:Nnn \hookrightarrow {3} {21aa}
+ \__luamml_kernel_define_character:Nnn \hookleftarrow {3} {21a9}
+ \__luamml_kernel_define_character:Nnn \bowtie {3} {22c8}
+ \__luamml_kernel_define_character:Nnn \Longrightarrow {3} {27f9}
+ \__luamml_kernel_define_character:Nnn \longrightarrow {3} {27f6}
+ \__luamml_kernel_define_character:Nnn \Longleftarrow {3} {27f8}
+ \__luamml_kernel_define_character:Nnn \longleftarrow {3} {27f5}
+ \__luamml_kernel_define_character:Nnn \Longleftrightarrow {3} {27fa}
+ \__luamml_kernel_define_character:Nnn \longleftrightarrow {3} {27f7}
+ \__luamml_kernel_define_character:Nnn \longmapsto {4} {27fc}
+}
+
+\IfFileLoadedT {latex-lab-math.ltx} {
+ \RequirePackage{luamml-patches-lab-math}
+}
diff --git a/macros/luatex/latex/luamml/luamml-patches-lab-math.sty b/macros/luatex/latex/luamml/luamml-patches-lab-math.sty
new file mode 100644
index 0000000000..e2d12c3c73
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-patches-lab-math.sty
@@ -0,0 +1,20 @@
+\ProvidesExplPackage {luamml-patches-lab-math} {2024-08-14} {0.1.0}
+ {Feel free to add a description here}
+
+\AddToHook{begindocument} {
+ \cs_set:Npn \common@align@ending {
+ \math@cr
+ \black@ \totwidth@
+ \__luamml_amsmath_finalize_table:n {align}
+ \egroup
+ \ifingather@
+ \restorealignstate@
+ \egroup
+ \nonumber
+ \ifnum0=`{\fi\iffalse}\fi
+ \else
+ $$
+ \fi
+ \ignorespacesafterend
+ }
+}
diff --git a/macros/luatex/latex/luamml/luamml-pdf-demo.sty b/macros/luatex/latex/luamml/luamml-pdf-demo.sty
new file mode 100644
index 0000000000..fe63814d05
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-pdf-demo.sty
@@ -0,0 +1,50 @@
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesExplPackage{luamml-pdf-demo}{2024-08-14}{v0.1.0}{Reasonable default definitions for luamml-pdf}
+
+\RequirePackage{luamml-pdf}% Loading luamml-pdf is pretty much the point
+% \RequirePackage{amsmath,array}% May come back if the patches get ported
+
+% Delay family mappings to allow for replacements
+\AddToHook{begindocument/before}{%
+ \@ifpackageloaded{unicode-math}{}{%
+ \RegisterFamilyMapping\symsymbols{oms}%
+ \RegisterFamilyMapping\symletters{oml}%
+ \RegisterFamilyMapping\symlargesymbols{omx}%
+ }
+}
+
+\cs_new_protected:Npn \LuaMMLSetFilename #1 {}
+
+% TODO.
+% \cs_generate_variant:Nn \pdffile_filespec:nnn {ene}
+% \int_new:N \g__luamml_demo_af_int
+% \cs_new_protected:Npn \LuaMMLTagAF #1#2 {
+% \int_gincr:N \g__luamml_demo_af_int
+% \exp_args:Ne \pdf_object_new:nn{__luamml_demo_\int_use:N \g__luamml_demo_af_int}{dict}
+% \exp_args:Ne \tagstructbegin{tag=Formula,AF=__luamml_demo_\int_use:N \g__luamml_demo_af_int,#1}
+% \bool_if:NF \l__luamml_demo_structelem_bool {
+% \tagmcbegin{tag=Formula}
+% }
+% #2
+% \group_begin:
+% \pdfdict_put:nnn {l_pdffile/Filespec} {AFRelationship}{/Supplement}
+% \pdffile_filespec:ene
+% { __luamml_demo_ \int_use:N \g__luamml_demo_af_int }
+% { test.xml }
+% { \luamml_get_last_mathml_stream:e{}\c_space_tl 0~R}
+% \group_end:
+% \bool_if:NF \l__luamml_demo_structelem_bool {
+% \tagmcend
+% }
+% \tagstructend
+% }
+
+\NewDocumentCommand\AnnotateFormula{ o m m }{%
+ \IfValueTF{#1}{%
+ \luamml_annotate:nen{#1}%
+ }{
+ \luamml_annotate:en
+ }{#2}{#3}
+}
+
+\cs_set_eq:NN \WriteoutFormula \luamml_pdf_write:
diff --git a/macros/luatex/latex/luamml/luamml-structelemwriter.lua b/macros/luatex/latex/luamml/luamml-structelemwriter.lua
new file mode 100644
index 0000000000..eb266a1e9f
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-structelemwriter.lua
@@ -0,0 +1,105 @@
+local struct_begin = token.create'tag_struct_begin:n'
+local struct_use = token.create'tag_struct_use:n'
+local struct_end = token.create'tag_struct_end:'
+
+local mc_begin = token.create'tag_mc_begin:n'
+local mc_end = token.create'tag_mc_end:'
+
+local function escape_name(name)
+ return name
+end
+
+local function escape_string(str)
+ return str
+end
+
+local mathml_ns_obj
+local function get_mathml_ns_obj()
+ mathml_ns_obj = mathml_ns_obj or token.create'c__pdf_backend_object_tag/NS/mathml_int'.index
+ return mathml_ns_obj
+end
+
+local attribute_counter = 0
+local attributes = setmetatable({}, {__index = function(t, k)
+ attribute_counter = attribute_counter + 1
+ local attr_name = string.format('luamml_attr_%i', attribute_counter)
+ t[k] = attr_name
+ tex.runtoks(function()
+ tex.sprint(string.format('\\tagpdfsetup{newattribute={%s}{/O/NSO/NS %i 0 R',
+ attr_name, mathml_ns_obj or get_mathml_ns_obj()))
+ -- tex.sprint(string.format('\\tagpdfsetup{newattribute={%s}{/O/MathML-3',
+ -- attr_name))
+ tex.cprint(12, k)
+ tex.sprint'}}'
+ end)
+ return attr_name
+end})
+
+local mc_type = luatexbase.attributes.g__tag_mc_type_attr
+local mc_cnt = luatexbase.attributes.g__tag_mc_cnt_attr
+-- print('!!!', mc_type, mc_cnt)
+
+local stash_cnt = 0
+local attrs = {}
+local function write_elem(tree, stash)
+ if tree[':struct'] then
+ return tex.runtoks(function()
+ return tex.sprint(struct_use, '{', tree[':struct'], '}')
+ end)
+ end
+ if not tree[0] then print('ERR', require'inspect'(tree)) end
+ local i = 0
+ for attr, val in next, tree do if type(attr) == 'string' and not string.find(attr, ':') and attr ~= 'xmlns' then
+ -- for attr, val in next, tree do if type(attr) == 'string' and string.byte(attr) ~= 0x3A then
+ i = i + 1
+ attrs[i] = string.format('/%s(%s)', escape_name(attr), escape_string(val))
+ end end
+ table.sort(attrs)
+
+ if stash then
+ stash_cnt = stash_cnt + 1
+ stash = '__luamml_stashed_' .. stash_cnt
+ tree[':struct'] = stash
+ stash = ', stash, label = ' .. stash
+ end
+
+ local attr_flag = i ~= 0 and ', attribute=' .. attributes[table.concat(attrs)]
+ tex.sprint(struct_begin, '{tag=' .. tree[0] .. '/mathml')
+ if stash then tex.sprint(stash) end
+ if attr_flag then tex.sprint(attr_flag) end
+ if tree[':actual'] then
+ tex.sprint(', actualtext = {')
+ tex.cprint(12, tree[':actual'])
+ tex.sprint'}'
+ end
+ tex.sprint'}'
+ for j = 1, i do attrs[j] = nil end
+
+ if tree[':nodes'] then
+ local n = tree[':nodes']
+ tex.runtoks(function()
+ tex.sprint{mc_begin, string.format('{tag=%s}', tree[0])}
+ -- NOTE: This will also flush all previous sprint's... That's often annoying, but in this case actually intentional.
+ end)
+ local mct, mcc = tex.attribute[mc_type], tex.attribute[mc_cnt]
+ for i = 1, #n do
+ node.set_attribute(n[i], mc_type, mct)
+ node.set_attribute(n[i], mc_cnt, mcc)
+ end
+ tex.runtoks(function()
+ tex.sprint(mc_end)
+ end)
+ end
+ for _, elem in ipairs(tree) do
+ if type(elem) ~= 'string' then
+ write_elem(elem)
+ end
+ end
+ tex.runtoks(function()
+ tex.sprint(struct_end)
+ end)
+end
+
+return function(element, stash)
+ return write_elem(element, stash)
+end
diff --git a/macros/luatex/latex/luamml/luamml-table.lua b/macros/luatex/latex/luamml/luamml-table.lua
new file mode 100644
index 0000000000..8b703953f8
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-table.lua
@@ -0,0 +1,110 @@
+local write_xml = require'luamml-xmlwriter'
+local make_root = require'luamml-convert'.make_root
+local save_result = require'luamml-tex'.save_result
+
+local properties = node.get_properties_table()
+
+local glue_id = node.id'glue'
+local tabskip_sub = 12
+assert(node.subtypes'glue'[tabskip_sub] == 'tabskip')
+
+local function store_get_row()
+ local row_temp
+ for i=tex.nest.ptr-1, 0, -1 do
+ local head = tex.nest[i].head
+ local glue = head.next
+ if glue and glue.id == glue_id and glue.subtype == tabskip_sub then
+ row_temp = head
+ break
+ end
+ end
+ if not row_temp then
+ error[[luamml_table's store function called outside of table]]
+ end
+ local props = properties[row_temp]
+ if not props then
+ props = {}
+ properties[row_temp] = props
+ end
+ local mml_row = props.mathml_row
+ if not mml_row then
+ mml_row = {[0] = 'mtr'}
+ props.mathml_row = mml_row
+ end
+ return mml_row
+end
+
+local function store_column_xml(mml, display)
+ if mml[0] ~= 'mtd' then
+ if display and mml[0] == 'mstyle' and mml.displaystyle == true then
+ mml[0], mml.displaystyle, mml.scriptlevel = 'mtd', nil, nil
+ else
+ if display and mml[0] ~= 'mstyle' then
+ mml = {[0] = 'mstyle', displaystyle = false, mml}
+ end
+ mml = {[0] = 'mtd', mml}
+ end
+ end
+ table.insert(store_get_row(), mml)
+ return mml
+end
+
+local function store_column(startmath)
+ local props = properties[startmath]
+ if not props then return end
+ local mml = props.saved_mathml_table or props.saved_mathml_core
+ if mml then return store_column_xml(mml) end
+end
+
+local function store_tag(xml)
+ local mml_row = store_get_row()
+ mml_row[0] = 'mlabeledtr'
+ table.insert(mml_row, 1, xml)
+ last_tag = nil
+end
+
+local function set_row_attribute(name, value)
+ local mml_row = store_get_row()
+ mml_row[name] = value
+end
+
+luatexbase.add_to_callback('hpack_filter', function(_, group)
+ if group ~= 'fin_row' then return true end
+
+ local temp = tex.nest.top.head
+ local props = properties[temp]
+ if not props then return true end
+ local mml_row = props.mathml_row
+ if not mml_row then return true end
+ props.mathml_row = nil
+
+ props = properties[tex.lists.align_head]
+ if not props then
+ props = {}
+ properties[tex.lists.align_head] = props
+ end
+ local mml_table = props.mathml_table_node_table
+ if not mml_table then
+ mml_table = {[0] = 'mtable'}
+ props.mathml_table_node_table = mml_table
+ end
+ table.insert(mml_table, mml_row)
+ return true
+end, 'mathml amsmath processing')
+
+local function get_table()
+ -- TODO: Error handling etc
+ local props = properties[tex.lists.align_head]
+ if not props then return end
+ local mml_table = props.mathml_table_node_table
+ props.mathml_table_node_table = nil
+ return mml_table
+end
+
+return {
+ store_column = store_column,
+ store_column_xml = store_column_xml,
+ store_tag = store_tag,
+ set_row_attribute = set_row_attribute,
+ get_table = get_table,
+}
diff --git a/macros/luatex/latex/luamml/luamml-tex-annotate.lua b/macros/luatex/latex/luamml/luamml-tex-annotate.lua
new file mode 100644
index 0000000000..4b22374ec2
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-tex-annotate.lua
@@ -0,0 +1,119 @@
+local nest = tex.nest
+
+local properties = node.get_properties_table()
+
+local mark_environment = {
+ data = {
+ },
+}
+
+local function annotate()
+ local annotation, err = load( 'return {'
+ .. token.scan_argument()
+ .. '}', nil, 't', mark_environment)
+ if not annotation then
+ tex.error('Error while parsing LuaMML annotation', {err})
+ return 0
+ end
+ annotation = annotation()
+ local nesting = nest.top
+ local props = properties[nesting.head]
+ local current = props and props.luamml__annotate_context
+ if current then
+ current, props.luamml__annotate_context = current.head, current.prev
+ else
+ tex.error('Mismatched LuaMML annotation',
+ {'Something odd happened. Maybe you forgot braces around an annotated symbol in a subscript or superscript?'})
+ return 0
+ end
+ local after = nesting.tail
+ local count, offset = 0, annotation.offset
+ local marked
+ if current == after then
+ tex.error'Empty LuaMML annotation'
+ else
+ repeat
+ current = current.next
+ count = count + 1
+ if count == offset then
+ marked = current
+ elseif offset or current ~= after then
+ local props = properties[current]
+ if not props then
+ props = {}
+ properties[current] = props
+ end
+ props.mathml_table, props.mathml_core = nil, false
+ end
+ until current == after
+ if offset and not marked then
+ tex.error'Invalid offset in LuaMML annotation'
+ end
+ marked = marked or current
+ if annotation.nucleus then
+ marked = marked.nucleus
+ end
+ if marked then
+ local props = properties[marked]
+ if not props then
+ props = {}
+ properties[marked] = props
+ end
+ if annotation.core then
+ props.mathml_core = annotation.core
+ end
+ if annotation.struct then
+ local saved = props.mathml_filter
+ local struct = annotation.struct
+ function props.mathml_filter(mml, core)
+ mml[':struct'] = struct
+ if saved then
+ return saved(mml, core)
+ else
+ return mml, core
+ end
+ end
+ end
+ else
+ tex.error'Unable to annotate nucleus of node without nucleus'
+ end
+ end
+ return count
+end
+
+local funcid = luatexbase.new_luafunction'__luamml_annotate_begin:'
+token.set_lua('__luamml_annotate_begin:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ local top = nest.top
+ local temp = top.head
+ local props = properties[temp]
+ if not props then
+ props = {}
+ properties[temp] = props
+ end
+ props.luamml__annotate_context = {
+ prev = props.luamml__annotate_context,
+ head = top.tail,
+ }
+end
+
+funcid = luatexbase.new_luafunction'__luamml_annotate_end:we'
+token.set_lua('__luamml_annotate_end:we', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ local count = token.scan_int()
+ local real_count = annotate()
+ if count ~= real_count then
+ tex.error('Incorrect count in LuaMML annotation', {
+ 'A LuaMML annotation was discovered with an explicit count \z
+ which was not the same as the number of top-level nodes annotated.',
+ string.format('This can be fixed by changing the supplied count from %i to %i \z
+ or by omitting the count value entirely.', count, real_count)
+ })
+ end
+end
+
+funcid = luatexbase.new_luafunction'__luamml_annotate_end:e'
+token.set_lua('__luamml_annotate_end:e', funcid, 'protected')
+lua.get_functions_table()[funcid] = annotate
+
+return mark_environment
diff --git a/macros/luatex/latex/luamml/luamml-tex.lua b/macros/luatex/latex/luamml/luamml-tex.lua
new file mode 100644
index 0000000000..4182fbb801
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-tex.lua
@@ -0,0 +1,232 @@
+local mlist_to_mml = require'luamml-convert'
+local process_mlist = mlist_to_mml.process
+local make_root = mlist_to_mml.make_root
+local register_family = mlist_to_mml.register_family
+
+local mappings = require'luamml-legacy-mappings'
+local write_xml = require'luamml-xmlwriter'
+local write_struct = require'luamml-structelemwriter'
+
+local filename_token = token.create'l__luamml_filename_tl'
+local label_token = token.create'l__luamml_label_tl'
+local left_brace = token.new(string.byte'{', 1)
+local right_brace = token.new(string.byte'}', 2)
+
+local output_hook_token
+local global_text_families = {}
+local text_families_meta = {__index = function(t, fam)
+ if fam == nil then return nil end
+ local assignment = global_text_families[fam]
+ if assignment == nil then
+ local fid = node.family_font(fam)
+ local fontdir = font.getfont(fid)
+ if not fontdir then
+ -- FIXME(?): If there is no font...
+ error'Please load your fonts?!?'
+ end
+ assignment = not (fontdir.MathConstants and next(fontdir.MathConstants))
+ end
+ t[fam] = assignment
+ return assignment
+end}
+
+local properties = node.get_properties_table()
+local mmode, hmode, vmode do
+ local result, input = {}, tex.getmodevalues()
+ for k,v in next, tex.getmodevalues() do
+ if v == 'math' then mmode = k
+ elseif v == 'horizontal' then hmode = k
+ elseif v == 'vertical' then vmode = k
+ else assert(v == 'unset')
+ end
+ end
+ assert(mmode and hmode and vmode)
+end
+
+local funcid = luatexbase.new_luafunction'RegisterFamilyMapping'
+token.set_lua('RegisterFamilyMapping', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ local fam = token.scan_int()
+ local mapping = token.scan_string()
+ if mappings[mapping] then
+ register_family(fam, mappings[mapping])
+ if global_text_families[fam] == nil then
+ global_text_families[fam] = false
+ end
+ else
+ tex.error(string.format('Unknown font mapping %q', mapping))
+ end
+end
+
+local funcid = luatexbase.new_luafunction'RegisterFamilyMapping'
+token.set_lua('RegisterTextFamily', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ local fam = token.scan_int()
+ local _kind = token.scan_string()
+ global_text_families[fam] = true
+end
+
+local function shallow_copy(t)
+ local tt = {}
+ for k,v in next, t do
+ tt[k] = v
+ end
+ return tt
+end
+
+-- Possible flag values:
+-- 0: Skip
+-- 1: Generate MathML, but only save it for later usage in startmath node
+-- 3: Normal (This is the only supported one in display mode)
+-- 11: Generate MathML structure elements
+--
+-- More generally, flags is a bitfield with the defined bits:
+-- Bit 5-7: See Bit 4
+-- Bit 4: Overwrite mathstyle with bit 9-11
+-- Bit 3: Generate MathML structure elements
+-- Bit 2: Change root element name for saved element
+-- Bit 1: Save MathML as a fully converted formula
+-- Bit 0: Save MathML for later usage in startmath node. Ignored for display math.
+
+local out_file
+
+local mlist_result
+
+local undefined_cmd = token.command_id'undefined_cs'
+local call_cmd = token.command_id'call'
+
+local labelled_mathml = {}
+
+local function save_result(xml, display, structelem)
+ mlist_result = make_root(xml, display and 0 or 2)
+ if out_file then
+ out_file:write(write_xml(mlist_result, tex.count.l__luamml_pretty_int & 1 == 1):sub(2) .. '\n')
+ else
+ token.put_next(filename_token)
+ local filename = token.scan_argument()
+ if filename ~= '' then
+ assert(io.open(filename, 'w'))
+ :write(write_xml(mlist_result, tex.count.l__luamml_pretty_int & 1 == 1):sub(2) .. '\n')
+ :close()
+ end
+ end
+ local tracing = tex.count.tracingmathml > 1
+ if tracing then
+ texio.write_nl(write_xml(mlist_result, tex.count.l__luamml_pretty_int & 2 == 2) .. '\n')
+ end
+ if output_hook_token then
+ tex.runtoks(function()
+ tex.sprint(-2, output_hook_token, left_brace, write_xml(mlist_result, tex.count.l__luamml_pretty_int & 4 == 4), right_brace)
+ end)
+ end
+ if tex.count.l__luamml_flag_int & 8 == 8 then
+ write_struct(mlist_result)
+ end
+ return mlist_result
+end
+
+luatexbase.add_to_callback('pre_mlist_to_hlist_filter', function(mlist, style)
+ if tex.nest.top.mode == mmode then -- This is a equation label generated with \eqno
+ return true
+ end
+ local flag = tex.count.l__luamml_flag_int
+ if flag & 3 == 0 then
+ return true
+ end
+ local display = style == 'display'
+ local startmath = tex.nest.top.tail -- Must come before any write_struct calls which adds nodes
+ style = flag & 16 == 16 and flag>>5 & 0x7 or display and 0 or 2
+ local xml, core = process_mlist(mlist, style, setmetatable({}, text_families_meta))
+ if flag & 2 == 2 then
+ xml = save_result(shallow_copy(xml), display)
+ end
+ if flag & 4 == 4 then
+ local element_type = token.get_macro'l__luamml_root_tl'
+ if element_type ~= 'mrow' then
+ if xml[0] == 'mrow' then
+ xml[0] = element_type
+ else
+ xml = {[0] = element_type, xml}
+ end
+ end
+ end
+ if not display and flag & 1 == 1 then
+ local props = properties[startmath]
+ if not props then
+ props = {}
+ properties[startmath] = props
+ end
+ props.saved_mathml_table, props.saved_mathml_core = xml, core
+ token.put_next(label_token)
+ local label = token.scan_argument()
+ if label ~= '' then
+ if labelled_mathml[label] then
+ tex.error('MathML Label already in use', {
+ 'A MathML expression has a label which is already used by another \z
+ formula. If you do not want to label this formula with a unique \z
+ label, set a empty label instead.'})
+ else
+ labelled_mathml[label] = xml
+ end
+ end
+ if flag & 10 == 8 then
+ write_struct(xml, true) -- This modifies xml in-place to reference the struture element
+ end
+ end
+ return true
+end, 'dump_list')
+
+funcid = luatexbase.new_luafunction'luamml_get_last_mathml_stream:e'
+token.set_lua('luamml_get_last_mathml_stream:e', funcid)
+lua.get_functions_table()[funcid] = function()
+ if not mlist_result then
+ tex.error('No current MathML data', {
+ "I was asked to provide MathML code for the last formula, but there weren't any new formulas since you last asked."
+ })
+ end
+ local mml = write_xml(mlist_result, tex.count.l__luamml_pretty_int & 8 == 8)
+ if tex.count.tracingmathml == 1 then
+ texio.write_nl(mml .. '\n')
+ end
+ tex.sprint(-2, tostring(pdf.immediateobj('stream', mml, '/Subtype/application#2Fmathml+xml' .. token.scan_argument(true))))
+ mlist_result = nil
+end
+
+funcid = luatexbase.new_luafunction'luamml_begin_single_file:'
+token.set_lua('luamml_begin_single_file:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ token.put_next(filename_token)
+ local filename = token.scan_argument()
+ if filename ~= '' then
+ out_file = assert(io.open(filename, 'w'))
+ end
+end
+
+funcid = luatexbase.new_luafunction'luamml_end_single_file:'
+token.set_lua('luamml_end_single_file:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ if out_file then
+ out_file:close()
+ out_file = nil
+ end
+end
+
+funcid = luatexbase.new_luafunction'luamml_register_output_hook:N'
+token.set_lua('__luamml_register_output_hook:N', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ output_hook_token = token.get_next()
+end
+
+funcid = luatexbase.new_luafunction'luamml_disable_output_hook:'
+token.set_lua('__luamml_disable_output_hook:', funcid, 'protected')
+lua.get_functions_table()[funcid] = function()
+ output_hook_token = nil
+end
+
+local annotate_context = require'luamml-tex-annotate'
+annotate_context.data.mathml = labelled_mathml
+
+return {
+ save_result = save_result,
+ labelled = labelled_mathml,
+}
diff --git a/macros/luatex/latex/luamml/luamml-xmlwriter.lua b/macros/luatex/latex/luamml/luamml-xmlwriter.lua
new file mode 100644
index 0000000000..a2b5abefbf
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml-xmlwriter.lua
@@ -0,0 +1,58 @@
+-- FIXME: Not sure yet if this will be needed
+local function escape_name(name)
+ return name
+end
+
+local escapes = {
+ ['"'] = "&quot;",
+ ['<'] = "&lt;",
+ ['>'] = "&gt;",
+ ['&'] = "&amp;",
+}
+local function escape_text(text)
+ return string.gsub(string.gsub(tostring(text), '["<>&]', escapes), '[\x00-\x08\x0B\x0C\x0E-\x1F]', function(x)
+ return string.format('^^%02x', string.byte(x))
+ end)
+end
+
+local attrs = {}
+local function write_elem(tree, indent)
+ if not tree[0] then print('ERR', require'inspect'(tree)) end
+ local escaped_name = escape_name(assert(tree[0]))
+ local i = 0
+ for attr, val in next, tree do if type(attr) == 'string' then
+ if not string.find(attr, ':', 1, true) then
+ -- if string.byte(attr) ~= 0x3A then
+ i = i + 1
+ attrs[i] = string.format(' %s="%s"', escape_name(attr), escape_text(val))
+ end
+ end end
+ table.sort(attrs)
+ local out = string.format('%s<%s%s', indent or '', escaped_name, table.concat(attrs))
+ for j = 1, i do attrs[j] = nil end
+ if not tree[1] then
+ return out .. '/>'
+ end
+ out = out .. '>'
+ local inner_indent = indent and indent .. ' '
+ local is_string
+ for _, elem in ipairs(tree) do
+ if type(elem) == 'string' then
+ if inner_indent and not is_string then
+ out = out .. inner_indent
+ end
+ out = out .. escape_text(elem)
+ is_string = true
+ else
+ out = out .. write_elem(elem, inner_indent)
+ is_string = nil
+ end
+ end
+ if indent then out = out .. indent end
+ return out .. '</' .. escaped_name .. '>'
+end
+
+return function(element, indent, version)
+ return (version == '11' and '<?xml version="1.1"?>' or '') ..
+ write_elem(element, indent and '\n' or nil)
+end
diff --git a/macros/luatex/latex/luamml/luamml.dtx b/macros/luatex/latex/luamml/luamml.dtx
new file mode 100644
index 0000000000..7815ac58e0
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml.dtx
@@ -0,0 +1,531 @@
+% \iffalse meta-comment
+%
+%% Copyright (C) 2020-2024 by Marcel Krueger
+%%
+%% This file may be distributed and/or modified under the
+%% conditions of the LaTeX Project Public License, either
+%% version 1.3c of this license or (at your option) any later
+%% version. The latest version of this license is in:
+%%
+%% http://www.latex-project.org/lppl.txt
+%%
+%% and version 1.3 or later is part of all distributions of
+%% LaTeX version 2005/12/01 or later.
+%
+%<*batch>
+%<*gobble>
+\ifx\jobname\relax\let\documentclass\undefined\fi
+\ifx\documentclass\undefined
+\csname fi\endcsname
+%</gobble>
+\input docstrip.tex
+\keepsilent
+\generate{
+ \file{luamml.sty}{\from{luamml.dtx}{package,luatex}}
+ \file{luamml-pdf.sty}{\from{luamml.dtx}{package,pdftex}}
+}
+\endbatchfile
+%</batch>
+%<*gobble>
+\fi
+\expandafter\ifx\csname @currname\endcsname\empty
+\csname fi\endcsname
+%</gobble>
+%<*driver>
+\documentclass{l3doc}
+\usepackage{luamml}
+\usepackage{csquotes,luacolor}
+\MakeShortVerb{\|}
+\RecordChanges
+\begin{document}
+\tracingmathml2
+\DocInput{luamml.dtx}
+\PrintIndex
+\PrintChanges
+\end{document}
+%</driver>
+%<*gobble>
+\fi
+%</gobble>
+% \fi
+%
+% \GetFileInfo{luamml.sty}
+% \title{The \pkg{luamml} package%
+% \thanks{This document corresponds to \pkg{luamml}~\fileversion, dated~\filedate.}%
+% }
+% \author{Marcel Krüger}
+%
+% \maketitle
+%
+% \begin{documentation}
+% \section{Use case}
+% When generating output for the web or tagged output, mathematical content should often be represented as MathML.
+% This uses Lua\TeX~callbacks to automatically attempt to convert Lua\TeX~math mode output into MathML.
+%
+% \section{Usage}
+% The \pkg{luamml} package is designed to be used in automated ways by other packages and usually should not be invoked directly by the end user.
+% For experiments, \texttt{luamml-demo} is included which provides easier to use interfaces.
+%
+% Add in your preamble
+% \begin{verbatim}
+% \usepackage[files]{luamml-demo}
+% \end{verbatim}
+% This will trigger the output of individual files for each block of math output containing corresponding MathML.
+%
+% Alternatively
+% \begin{verbatim}
+% \usepackage[l3build]{luamml-demo}
+% \end{verbatim}
+% will generate a single file with a concatenation of all MathML blocks.
+%
+% For automated use, the \pkg{luamml} package can be included directly, followed by enclosing blocks which should generate files with \cmd{luamml_begin_single_file:} and \cmd{luamml_end_single_file:}.
+% The filename can be set with \cmd{luamml_set_filename:n}.
+%
+% \section{Improving MathML conversion}
+% When using constructs which do not automatically get converted in acceptable form, conversion hints can be provided with \cmd{luamml_annotate:en}.
+% This allows to provide a replacement MathML structure in Lua table form, for example
+% \begin{verbatim}
+% \luamml_annotate:en {
+% nucleus = true,
+% core = {[0] = 'mi', 'TeX'},
+% }{
+% \hbox{\TeX}
+% }
+% \end{verbatim}
+% produces a |<mi>TeX</mi>| element in the output instead of trying to import \TeX~as a mathematical expression.
+% The table structure is explained in an appendix.
+%
+% \section{Features \& Limitations}
+% Currently all mathematical expressions which purely contain Unicode encoded math mode material without embedded non-math should get converted successfully.
+% Usage with non-Unicode math (\TeX's 8-bit math fonts) is highly experimental and undocumented.
+% Any attempt to build complicated structures by embedding arbitrary \TeX\ code in the middle of math mode needs to have a MathML replacement specified.
+% We try to automate more cases in the future.
+%
+% \appendix
+% \input{luamml-algorithm}
+% \end{documentation}
+%
+% \begin{implementation}
+% \section{Package Implementation}
+% \subsection{Initialization}
+% \iffalse
+%<*package>
+% \fi
+% \begin{macrocode}
+%<@@=luamml>
+%<*luatex>
+\ProvidesExplPackage {luamml} {2024-08-14} {0.1.0}
+ {Automatically generate presentational MathML from LuaTeX math expressions}
+%</luatex>
+%<*pdftex>
+\ProvidesExplPackage {luamml-pdf} {2024-08-14} {0.1.0}
+ {MathML generation for L̶u̶a̶pdfLaTeX}
+%</pdftex>
+% \end{macrocode}
+%
+% \subsection{Initialization}
+% These variable have to appear before the Lua module is loaded and will be used to
+% communicate information to the callback.
+%
+% Here \cs{tracingmathml} does not use a expl3 name since it is not intended for
+% programming use but only as a debugging helper for the user.
+% The other variables are internal, but we provide public interfaces for setting
+% them later.
+% \begin{macrocode}
+\int_new:N \l__luamml_flag_int
+\int_new:N \l__luamml_pretty_int
+%<luatex>\tl_new:N \l__luamml_filename_tl
+\tl_new:N \l__luamml_root_tl
+\tl_set:Nn \l__luamml_root_tl { mrow }
+\tl_new:N \l__luamml_label_tl
+%<pdftex>\int_new:N \g__luamml_formula_id_int
+%<luatex>\int_new:N \tracingmathml
+
+\int_set:Nn \l__luamml_pretty_int { 1 }
+% \end{macrocode}
+%
+% Now we can load the Lua module which defines the callback.
+% Of course until pdf\TeX starts implementing \cs{directlua} this is only
+% done in Lua\TeX.
+% \begin{macrocode}
+%<luatex>\lua_now:n { require'luamml-tex' }
+% \end{macrocode}
+%
+% \subsection{Hook}
+% We also call a hook with arguments at the end of every MathML conversion with the result.
+% Currently only implemented in Lua\TeX{} since it immediately provides the output.
+% \begin{macrocode}
+%<*luatex>
+\hook_new_with_args:nn { luamml / converted } { 1 }
+
+\cs_new_protected:Npn \__luamml_output_hook:n {
+ \hook_use:nnw { luamml / converted } { 1 }
+}
+\__luamml_register_output_hook:N \__luamml_output_hook:n
+%</luatex>
+% \end{macrocode}
+
+%
+% \subsection{Flags}
+% The most important interface is for setting the flag which controls how the
+% formulas should be converted.
+%
+% \begin{macro}{\luamml_flag_process:}
+% Consider the current formula to be a complete, free-standing mathematical
+% expression which should be converted to MathML. Additionally, the formula
+% is also saved in the \texttt{start\_math} node as with
+% \cs{luamml_flag_save:}.
+% \begin{macrocode}
+\cs_new_protected:Npn \luamml_flag_process: {
+ \tl_set:Nn \l__luamml_label_tl {}
+ \int_set:Nn \l__luamml_flag_int { 3 }
+}
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\__luamml_maybe_structelem:}
+% A internal helper which can be added to a tag to preserve the external state
+% of the structelem flag.
+% \begin{macrocode}
+\cs_new:Npn \__luamml_maybe_structelem: {
+ (
+ 8 * \int_mod:nn {
+ \int_div_truncate:nn { \l__luamml_flag_int } {8}
+ } {2}
+ ) +
+}
+% \end{macro}
+%
+% \begin{macro}{\__luamml_style_to_num:N}
+% \begin{macrocode}
+\cs_new:Npn \__luamml_style_to_num:N #1 {
+%<luatex> 32 * #1
+%<*pdftex>
+ \token_case_meaning:NnF #1 {
+ \displaystyle {0}
+ \textstyle {32}
+ \scriptstyle {64}
+ \scriptscriptstyle {96}
+ } {
+ \Invalid_mathstyle
+ }
+%</pdftex>
+}
+% \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\luamml_flag_save:n,
+% \luamml_flag_save:nN,
+% \luamml_flag_save:nn,
+% \luamml_flag_save:nNn}
+% Convert the current formula but only save it's representation in the math
+% node without emitting it as a complete formula. This is useful when the
+% expression forms part of a bigger formula and will be intergrated into it's
+% MathML tables later by special code.
+% It optinally accepts three parameters: A label, one math style command
+% (\cs{displaystyle}, \cs{textstyle}, etc.) which is the implicit math style
+% (so the style which the surrounding code expects this style to have) and a
+% name for the root element (defaults to \texttt{mrow}).
+% If the root element name is \texttt{mrow}, it will get suppressed in some
+% cases.
+% \begin{macrocode}
+\cs_new_protected:Npn \luamml_flag_save:n #1 {
+ \tl_set:Nn \l__luamml_label_tl {#1}
+ \int_set:Nn \l__luamml_flag_int { \__luamml_maybe_structelem: 1 }
+}
+\cs_new_protected:Npn \luamml_flag_save:nN #1#2 {
+ \tl_set:Nn \l__luamml_label_tl {#1}
+ \int_set:Nn \l__luamml_flag_int { \__luamml_maybe_structelem: 17 + \__luamml_style_to_num:N #2 }
+}
+\cs_new_protected:Npn \luamml_flag_save:nn #1 {
+ \tl_set:Nn \l__luamml_label_tl {#1}
+ \int_set:Nn \l__luamml_flag_int { \__luamml_maybe_structelem: 5 }
+ \tl_set:Nn \l__luamml_root_tl
+}
+\cs_new_protected:Npn \luamml_flag_save:nNn #1#2 {
+ \tl_set:Nn \l__luamml_label_tl {#1}
+ \int_set:Nn \l__luamml_flag_int { \__luamml_maybe_structelem: 21 + \__luamml_style_to_num:N #2 }
+ \tl_set:Nn \l__luamml_root_tl
+}
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\luamml_flag_ignore:}
+% Completely ignore the math mode material.
+% \begin{macrocode}
+\cs_new_protected:Npn \luamml_flag_ignore: {
+ \int_set:Nn \l__luamml_flag_int { 0 }
+}
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\luamml_flag_structelem:}
+% Like \cs{luamml_flag_process:}, but additionally add PDF structure
+% elements. This only works in Lua\TeX\ and requires that the \pkg{tagpdf} package
+% has been loaded \emph{before} \texttt{luamml}.
+% \begin{macrocode}
+%<*luatex>
+\cs_new_protected:Npn \luamml_flag_structelem: {
+ \tl_set:Nn \l__luamml_label_tl {}
+ \int_set:Nn \l__luamml_flag_int { 11 }
+}
+%</luatex>
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\luamml_set_filename:n}
+% Allows to set a filename to which the generated MathML gets written.
+% Previous content from the file will get overwritten. This includes results
+% written by a previous formula. Therefore this has to be called separately
+% for every formula or it must expand to different values to be useful.
+% The value is fully expanded when the file is written.
+%
+% Only complete formulas get written into files (so formulas where
+% \cs{luamml_flag_process:} or \cs{luamml_flag_structelem:} are in effect).
+%
+% Only implemented in Lua\TeX, in pdf\TeX\ the arguments for \texttt{pdfmml}
+% determine the output location.
+% \begin{macrocode}
+%<*luatex>
+\cs_new_protected:Npn \luamml_set_filename:n {
+ \tl_set:Nn \l__luamml_filename_tl
+}
+%</luatex>
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\luamml_begin_single_file:, \luamml_end_single_file:}
+% Everything between these two commands gets written into the same XML file.
+% The filename is expanded when \cs{luamml_begin_single_file:} gets executed.
+%
+% (Implemented in Lua)
+% \end{macro}
+%
+% By default, the flag is set to assume complete formulas.
+% \begin{macrocode}
+\luamml_flag_process:
+% \end{macrocode}
+%
+% \subsection{Annotations}
+% These are implemented very differently depending on the engine, but the interface
+% should be the same.
+% \subsubsection{Lua\TeX}
+% \begin{macrocode}
+%<*luatex>
+% \end{macrocode}
+% \begin{macro}{\luamml_annotate:nen, \luamml_annotate:en}
+% A simple annotation scheme: The first argument is the number of top level
+% noads to be annotated, the second parameter the annotation and the third
+% parameter the actual list of math tokens. The first argument can be omitted to
+% let Lua\TeX determine the number itself.
+%
+% Passing the first parameter explicitly is useful for any annotations which
+% should be compatible with fututre pdf\TeX versions of this functionality.
+% \begin{macrocode}
+\cs_new_protected:Npn \luamml_annotate:nen #1#2#3 {
+ \__luamml_annotate_begin:
+ #3
+ \__luamml_annotate_end:we \tex_numexpr:D #1 \scan_stop: {#2}
+}
+
+\cs_new_protected:Npn \luamml_annotate:en #1#2 {
+ \__luamml_annotate_begin:
+ #2
+ \__luamml_annotate_end:e {#1}
+}
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macrocode}
+%</luatex>
+% \end{macrocode}
+
+% \subsubsection{pdf\TeX}
+% \begin{macrocode}
+%<*pdftex>
+% \end{macrocode}
+% \begin{macro}{\__luamml_pdf_showlists:}
+% Here and in many other locations the \pdfTeX{} implementation is based on \cs{showlists},
+% so we define a internal wrapper which sets all relevant parameters.
+% \begin{macrocode}
+\cs_if_exist:NTF \showstream {
+ \iow_new:N \l__luamml_pdf_stream
+ \iow_open:Nn \l__luamml_pdf_stream { \jobname .tml }
+ \cs_new_protected:Npn \__luamml_pdf_showlists: {
+ \group_begin:
+ \int_set:Nn \tex_showboxdepth:D { \c_max_int }
+ \int_set:Nn \tex_showboxbreadth:D { \c_max_int }
+ \showstream = \l__luamml_pdf_stream
+ \tex_showlists:D
+ \group_end:
+ }
+} {
+ \cs_set_eq:NN \l__luamml_pdf_stream \c_log_iow
+ \cs_set_eq:NN \__luamml_pdf_set_showstream: \scan_stop:
+ \cs_new_protected:Npn \__luamml_pdf_showlists: {
+ \group_begin:
+ \int_set:Nn \l_tmpa_int { \tex_interactionmode:D }
+ \int_set:Nn \tex_interactionmode:D { 0 }
+ \int_set:Nn \tex_showboxdepth:D { \c_max_int }
+ \int_set:Nn \tex_showboxbreadth:D { \c_max_int }
+ \tex_showlists:D
+ \int_set:Nn \tex_interactionmode:D { \l_tmpa_int }
+ \group_end:
+ }
+}
+% \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\luamml_annotate:nen, \luamml_annotate:en}
+% Now we can define the annotation commands for pdf\TeX.
+% \begin{macrocode}
+\cs_generate_variant:Nn \tl_to_str:n { e }
+\int_new:N \g__luamml_annotation_id_int
+\cs_new_protected:Npn \luamml_annotate:nen #1#2#3 {
+ \int_gincr:N \g__luamml_annotation_id_int
+ \iow_shipout_x:Nx \l__luamml_pdf_stream {
+ LUAMML_MARK_REF:
+ \int_use:N \g__luamml_annotation_id_int
+ :
+ }
+ \iow_now:Nx \l__luamml_pdf_stream {
+ LUAMML_MARK:
+ \int_use:N \g__luamml_annotation_id_int
+ :
+ count = \int_eval:n {#1},
+ #2
+ \iow_newline:
+ LUAMML_MARK_END
+ }
+ #3
+}
+\cs_new_protected:Npn \luamml_annotate:en #1#2 {
+ \int_gincr:N \g__luamml_annotation_id_int
+ \iow_shipout_x:Nx \l__luamml_pdf_stream {
+ LUAMML_MARK_REF:
+ \int_use:N \g__luamml_annotation_id_int
+ :
+ }
+ \iow_now:Nx \l__luamml_pdf_stream {
+ LUAMML_MARK:
+ \int_use:N \g__luamml_annotation_id_int
+ :
+ count = data.count[\int_use:N \g__luamml_annotation_id_int],
+ #1
+ \iow_newline:
+ LUAMML_MARK_END
+ }
+ \use:x {
+ \iow_now:Nn \l__luamml_pdf_stream {
+ LUAMML_COUNT:
+ \int_use:N \g__luamml_annotation_id_int
+ }
+ \__luamml_pdf_showlists:
+ \exp_not:n {#2}
+ \iow_now:Nn \l__luamml_pdf_stream {
+ LUAMML_COUNT_END:
+ \int_use:N \g__luamml_annotation_id_int
+ }
+ \__luamml_pdf_showlists:
+ }
+}
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macrocode}
+%</pdftex>
+% \end{macrocode}
+%
+% \subsection{Trigger for specific formula}
+% This only applies for pdf\TeX\ since in Lua\TeX\ everything is controlled by the callback,
+% but for compatibility the function is defined anyway.
+%
+% \begin{macro}{\luamml_pdf_write:}
+% We could accept parameters for the flag and tag here, but for compatibility
+% with Lua\TeX they are passed in macros instead.
+% \begin{macrocode}
+%<*pdftex>
+\cs_new_protected:Npn \luamml_pdf_write: {
+ \int_gincr:N \g__luamml_formula_id_int
+ \iow_now:Nx \l__luamml_pdf_stream {
+ LUAMML_FORMULA_BEGIN:
+ \int_use:N \g__luamml_formula_id_int
+ :
+ \int_use:N \l__luamml_flag_int
+ :
+ \l__luamml_root_tl
+ :
+ \l__luamml_label_tl
+ }
+ \__luamml_pdf_showlists:
+ \iow_now:Nx \l__luamml_pdf_stream {
+ LUAMML_FORMULA_END
+ }
+}
+%</pdftex>
+%<luatex>\cs_new_eq:NN \luamml_pdf_write: \scan_stop:
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macrocode}
+% \end{macrocode}
+%
+% \subsection{Further helpers}
+%
+% \begin{macro}{\RegisterFamilyMapping}
+% The Lua version of this is defined in the Lua module.
+% \begin{macrocode}
+%<*pdftex>
+\NewDocumentCommand \RegisterFamilyMapping {m m} {
+ \iow_now:Nx \l__luamml_pdf_stream {
+ LUAMML_INSTRUCTION:REGISTER_MAPPING: \int_use:N #1 : #2
+ }
+}
+%</pdftex>
+% \end{macrocode}
+% \end{macro}
+%
+%
+% \subsection{Patching}
+% For some packages, we ship with patches to make them more compatible and to
+% demonstrate how other code can be patched to work with \texttt{luamml}.
+%
+% These are either loaded directly if the packages are loaded or delayed using
+% \LaTeX's hook system otherwise.
+% \begin{macro}{\__luamml_patch_package:nn, \__luamml_patch_package:n}
+% For this, we use two helpers: First a wrapper which runs arbitrary code either
+% now (if the package is already loaded) or as soon as the package loads, second
+% an application of the first one to load packages following \texttt{luamml}'s
+% naming scheme for these patch packages.
+% \begin{macrocode}
+\cs_new_protected:Npn \__luamml_patch_package:nn #1 #2 {
+ \@ifpackageloaded {#1} {#2} {
+ \hook_gput_code:nnn {package/#1/after} {luamml} {#2}
+ }
+}
+\cs_new_protected:Npn \__luamml_patch_package:n #1 {
+ \__luamml_patch_package:nn {#1} {
+ \RequirePackage { luamml-patches-#1 }
+ }
+}
+% \end{macrocode}
+% \end{macro}
+%
+% We currently provide minimal patching for the kernel, \pkg{amsmath} and \pkg{array}.
+% Currently only the kernel code supports pdf\TeX, but it's planned to extend this.
+% \begin{macrocode}
+\RequirePackage { luamml-patches-kernel }
+%<*luatex>
+\__luamml_patch_package:n {amstext}
+\__luamml_patch_package:n {amsmath}
+\__luamml_patch_package:n {array}
+%</luatex>
+% \end{macrocode}
+
+% \iffalse
+%</package>
+% \fi
+% \end{implementation}
+% \Finale
diff --git a/macros/luatex/latex/luamml/luamml.pdf b/macros/luatex/latex/luamml/luamml.pdf
new file mode 100644
index 0000000000..18061ce3c0
--- /dev/null
+++ b/macros/luatex/latex/luamml/luamml.pdf
Binary files differ
diff --git a/macros/luatex/latex/luamml/test-mathml-2.txt b/macros/luatex/latex/luamml/test-mathml-2.txt
new file mode 100644
index 0000000000..c8116c78c0
--- /dev/null
+++ b/macros/luatex/latex/luamml/test-mathml-2.txt
@@ -0,0 +1,8 @@
+<math xmlns:tex="http://typesetting.eu/2021/LuaMathML" xmlns="http://www.w3.org/1998/Math/MathML">
+ <mo lspace="0" rspace="0.167em" tex:class="opnolimits">
+ sin
+ </mo>
+ <mi>
+ 𝑥
+ </mi>
+</math>