diff options
Diffstat (limited to 'macros/luatex/latex/luamml')
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 = { + ['"'] = """, + ['<'] = "<", + ['>'] = ">", + ['&'] = "&", +} +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 Binary files differnew file mode 100644 index 0000000000..18061ce3c0 --- /dev/null +++ b/macros/luatex/latex/luamml/luamml.pdf 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> |