if not modules then modules = { } end modules ['lxml-mms'] = { version = 1.001, author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", comment = "written together with Mikael Sundqvist", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- Although it sits in the lxml name space this module is pretty much hooked into -- ConTeXt math rendering and backend code. It is one of the first applications of -- math dictionary support. After experimenting with hooking serialization of math -- into the rendering we decided that it made more sense to use the export -- facilities instead. It is also a fun project that might lead so more similar -- functionality. Of course we don't limit ourselves to English, if only because -- our test more advanced cases are Swedish math books. -- -- The rationale for this serialization can be found in demands for documents to be -- accessible and math is a tricky part of that. We embed MathML in tagged PDF -- documents and the serialization hooks into the actual text features. This is -- needed in order to make accessibility validators (like the ones used at Lunds -- University) happy. We let users decide how useful and reliable it is. In ConTeXt -- you can overload and adapt bits and pieces as part of the document style. For the -- record: we are not involved in accesibility projects, so we might get some things -- wrong. -- -- Timestamp: yet another entertaining drum track by Gavin Harrison "Pick Up The -- Pieces" | Zildjian 400th UK (posted 2024/2/5) - three weeks before a PT concert -- (YT: "The Pineapple Thief - It Leads to This). -- -- While we worked on this we decovered Mohini Dey so that's another timestamp on -- this development (one of the Rick Beato interviews). -- -- This is experimental code that will be optimized. Also, some of it can be a bit -- but we do that once we're done. In the end it might look simple but quite a bit -- of exploration was involved. local tonumber, tostring, type, next = tonumber, tostring, type, next local concat = table.concat local formatters = string.formatters local xmlall = xml.all local xmlfirst = xml.first local xmltext = xml.text local xmlconvert = xml.convert local xmlcollected = xml.collected local report = logs.reporter("mms") local getname = mathematics.dictionaries.name local classes = mathematics.classes local getverboselabel = mathematics.getverboselabel local getoptionallabel = mathematics.getoptionallabel local functions = mathematics.categories.functions local functiontype = mathematics.functiontype local function xmlevery(x) local dt = x.dt local tt local nn = 0 for i=1,#dt do local di = dt[i] if di.tg and not di.special then if tt then nn = nn + 1 tt[nn] = di else nn = 1 tt = { di } end end end return tt, nn end local function xmlfixattributes(x) for c in xmlcollected(x,"*") do local at = c.at for k, v in next, at do at[k] = tostring(v) end end end local function xmlwipeattributes(x) local at = { } for c in xmlcollected(x,"*") do local at = c.at local colspan = at.colspan local stretchy = at.stretchy local scriptlevel = at.scriptlevel local rulethickness = at.rulethickness if colspan or stretchy or scriptlevel or rulethickness then c.at = { columnspan = colspan, -- watch the name, different from tables stretchy = stretchy, scriptlevel = scriptlevel, linethickness = rulethickness, -- watch the name } else c.at = { -- Don't set them afterwards because they are shared! } end end end local prepare do local expandtimes, expandtimesone expandtimesone = function(c,all,i,start,stop) if c then local tg = c.tg if tg == "mrow" then local at = c.at if at.mathunit then -- nothing else local a, na = xmlevery(c) if na > 0 then -- local mfs = at.mathfractionstack -- if mfs then -- local a = xmlall(c,"/mfrac") or { } -- expandtimesone(a[1],a,1) -- goto DONE -- end local times = false local block = true local closed = false for i=1,na do local ai = a[i] local tg = ai.tg local at = ai.at local class = at.mathclass if at.mathsymbolic or at.mathgroup == "postfix operator" then -- has to come before at.mathfunction check at.checkfunction = "true" -- saves a check later if closed then at.mathtimes = 9.1 closed = false elseif block then block = false else at.mathtimes = 9.2 end block = false -- registered functions like f and g times = true expandtimesone(ai,a,i) elseif at.mathfunction -- or at.mathfractionstack -- ADDED or at.mathfunctionstack or class == "integral" or at.mathgroup == "unary set" or at.mathgroup == "number set" -- or at.mathapplication then at.checkfunction = "true" -- saves a check later if closed then at.mathtimes = 1.1 closed = false elseif block then block = false -- elseif at.mathgroup == "postfix operator" then -- block = false elseif at.mathgroup ~= "unary set" then at.mathtimes = 1.2 end -- if at.mathsymbolic then -- block = false -- registered functions like f and g -- else block = true -- lim etc .. we need to intercept kind == 1 -- end times = false expandtimesone(ai,a,i) elseif tg == "mo" then -- combine more here if class == "open" then if closed then at.mathtimes = 2.1 closed = false elseif times and not block then at.mathtimes = 2.2 end elseif i > 1 and class == "close" then -- block ? closed = true else closed = false end expandtimes(ai) times = false block = true elseif tg == "mn" then if closed then at.mathtimes = 3.1 closed = false elseif times and not block then at.mathtimes = 3.2 end expandtimesone(ai) times = false block = false elseif tg == "msub" or tg == "msup" or tg == "msubsup" then local first = xmlfirst(ai,"/*") local fat = first and first.at if closed then at.mathtimes = 4.1 closed = false elseif first and at.mathclass == "close" then closed = true elseif times and not block then at.mathtimes = 4.2 end if first then if fat.mathfunction or fat.mathfunctionstack or fat.mathclass == "integral" or fat.mathclass == "operator" or fat.mathclass == "differential" then if not fat.mathfunction then fat.mathfunction = "true" -- maybe also needed for the stack end at.checkfunction = "true" -- saves a check later expandtimesone(ai,a,i) block = true times = false else expandtimesone(ai,a,i) block = false times = true end else expandtimesone(ai,a,i) block = false times = true end elseif at.mathfractionstack or tg == "mroot" or tg == "mfrac" or tg == "msqrt" or at.mathsymbolic then if closed then at.mathtimes = 5.1 closed = false elseif block then block = false else at.mathtimes = 5.2 end times = true expandtimesone(ai,a,i) elseif class == "differential" then closed = false block = true times = true expandtimesone(ai,a,i) elseif class == "variable" then -- local c = at.mathcharacter -- if c == "." or c == "," then -- period comma -- block = true -- times = false -- else if closed then at.mathtimes = 6.1 closed = false elseif times and not block then at.mathtimes = 6.2 end block = false times = true -- end expandtimesone(ai,a,i) elseif tg == "mrow" then if closed or times then at.mathtimes = 7.1 closed = false end times = true expandtimesone(ai,a,i) elseif tg == "mtext" or tg == "ms" then if closed then at.mathtimes = 8.1 closed = false end expandtimesone(ai) times = false block = false else if closed then at.mathtimes = 9.1 closed = false end times = false expandtimesone(ai,a,i) end end end end ::DONE:: elseif tg == "mtable" then for cell in xmlcollected(c,"mtd") do local a, na = xmlevery(cell) if a then expandtimes(a) end end elseif tg == "mtext" or tg == "mspace" or tg == "ms" then -- nothing to do elseif tg == "mfrac" then local a, na = xmlevery(c) if i > 1 and na == 2 and all[i-1].tg == "mn" and a[1].tg == "mn" and a[2].tg == "mn" and not c.__p__.at.mathfractionstack then -- mark not times c.at.mathplus = 1 -- invisible plus end -- inspect(c.at) else local a, na = xmlevery(c) if a then expandtimes(a) end end end end expandtimes = function(all) if all then for i=1,#all do expandtimesone(all[i],all,i) end end end prepare = function(x) local all = xmlall(x,"mi[not @mathfunction]") if all then for i=1,#all do local a = all[i] local t = a.dt[1] local f = functions[t] if f then local at = a.at at.mathfunction = "true" at.mathsymbolic = "true" end end end -- left is a function, right is an operator for c in xmlcollected(x,"mrow[@mathfunctionstack]") do -- for c in xmlcollected(x,"(msub|msup|msubsup|mrow)[@mathfunctionstack]") do local cat = c.at local s = cat.mathstack for cc in xmlcollected(c,"(mi|mo)[@mathstack]") do local ccat = cc.at if ccat.mathstack == s then cat.mathfunction = ccat.mathfunction or cat.mathfunction cat.mathcharacter = ccat.mathcharacter or cat.mathcharacter cat.mathgroup = ccat.mathgroup or cat.mathgroup if cc.tg == "mo" then ccat.mathignore = "true" -- ccat.mathmeaning = cat.mathfunctionstack end end end end -- local all = xmlall(x,"(msub|msup|msubsup)/mo[@mathclass='open' or @mathclass='close' or @mathclass='middle']") if all then for i=1,#all do local ai = all[i] local at = ai.at at.mathignore = "true" ai.__p__.at.mathclass = at.mathclass end end -- expandtimesone(x) end end local tomeaning, getlast do -- we don't really need to handle mfenced local t, n, expand, expandone, okay, language, domain -- todo: a getlabel wrapper so that we don't need to pass language and domain local function getlabel(tag) return getverboselabel(tag,language,domain) end local function getoptional(tag) return getoptionallabel(tag,language,domain) end local utfsplit = utf.split local utfbyte = utf.byte local isprivate = fonts.helpers.isprivate local unknown = " ? " local function sanitize(s) local t = utfsplit(s) local n = #t if n == 1 then if isprivate(utfbyte(s)) then return unknown end else local done = false for i=1,n do if isprivate(utfbyte(t[i])) then done = true t[i] = unknown end end if done then return concat(t) end end return s end local function expandsymbol(c) local at = c.at if not at.mathignore then local result = at.mathidentity if not result then -- better: mathmeaning as overload -- if at.mathfunctionstack then -- result = getlabel(at.mathfunctionstack) -- else local group = at.mathgroup if group then local character = tonumber(at.mathcharacter) local index = tonumber(at.mathindex) local s = getname(group,character) or getname(group,index) if type(s) == "string" then result = getlabel(s) else result = sanitize(xmltext(c)) end else result = sanitize(xmltext(c)) end -- end end if result ~= "" then n = n + 1 ; t[n] = result end end end -- replace method by category and kind local function hasmiddle(a) if a then for i=1,#a do local class = a[i].at.mathclass if class == "middle" then return true end end end return false end local function haslimits(at) local f = at.mathfunction if f then f = functions[f] if f then return f.method == "limits" end end return false end local function isintegral(at) return at.mathclass == "integral" end local function isoperator(at) return at.mathclass == "operator" end -- see if we can avoid passing all and i local function expandsupindex(c,a,all,i) if c.at.mathsupindex then n = n + 1 ; t[n] = getlabel("supindex") expandone(a,all,i) else n = n + 1 ; t[n] = getlabel("to the power of") expandone(a,all,i) local last = t[n] if last == "2" then n = n - 1 ; t[n] = getlabel("squared") elseif last == "3" then n = n - 1 ; t[n] = getlabel("cubed") end end end local function expandsubindex(c,a,all,i) if c.at.mathsubindex then n = n + 1 ; t[n] = getlabel("subindex") else n = n + 1 ; t[n] = getlabel("sub") end expandone(a,all,i) end local function checkfunction(a,i,na) if i + 3 <= na then local a1 = a[i+1] if a1.tg == "mo" then local a3 = a[i+3] if a3.tg == "mo" then local a2 = a[i+2] local t2 = a2.tg if t2 == "mn" or t2 == "mi" then local at1 = a1.at local at3 = a3.at local class1 = at1.mathclass local class2 = at3.mathclass if class1 and class2 and class1 == "open" and class2 == "close" then at1.mathnogroup = "true" at3.mathnogroup = "true" end end end end end end local partials = { ["∂"] = "d", -- todo: bold math } local function isdifferential(a) local a1 = a[1] local a2 = a[2] if a1 and a2 then local tg2 = a2.tg local ps = false if tg2 == "mrow" or tg2 == "msub" or tg2 == "msup" or tg2 == "msubsup" then local aa = xmlfirst(a2,"/*") if aa then if aa.tg == "mi" and aa.at.mathclass == "differential" then ps = aa else local aa = xmlfirst(aa,"/*") if aa and aa.tg == "mi" and aa.at.mathclass == "differential" then ps = aa else return false end end else return false end end -- can be helper local tg1 = a1.tg if tg1 == "mrow" or tg1 == "msub" or tg1 == "msup" or tg1 == "msubsup" then local aa = xmlfirst(a1,"/*") if aa then if aa.tg == "mi" and aa.at.mathclass == "differential" then local p = partials[aa.dt[1]] if p then -- aa.at.mathidentity = p -- ps.at.mathidentity = p return true, true else return true, false end else local aa = xmlfirst(aa,"/*") if aa and aa.tg == "mi" and aa.at.mathclass == "differential" then local p = partials[aa.dt[1]] if p then -- aa.at.mathidentity = p -- ps.at.mathidentity = p return true, true else return true, false end else return false end end else return false end elseif tg1 == "mi" and a1.at.mathclass == "differential" then local p = partials[a1.dt[1]] if partials[a1.dt[1]] then -- a1.at.mathidentity = p -- ps.at.mathidentity = p return true, true else return true, false end end end return false end local function applyof(all,i,label) local ai1 = all[i + 1] if ai1 and ai1.tg == "mo" then local c = ai1.at.mathclass if c == "open" then n = n + 1 ; t[n] = getlabel(label) ai1.at.mathtimes = nil end end end local function expandfenced(ai,fenced,all,i) local at = ai.at local class = at.mathclass if not class then if at.mathnogroup then -- ignore else expandone(ai,all,i) end elseif class == "open" then if fenced or at.mathnogroup then -- ignore else n = n + 1 ; t[n] = getlabel("begin group") end at.mathignore = "true" expandone(ai) elseif class == "close" then if fenced or at.mathnogroup then -- ignore else n = n + 1 ; t[n] = getlabel("end group") end at.mathignore = "true" expandone(ai) elseif class == "middle" then if fenced then n = n + 1 ; t[n] = getlabel(fenced.tag .. ":fence") at.mathignore = "true" expandone(ai) local aa = all and all[i+1] if aa then aa.at.mathnogroup = "true" end else expandone(ai) end else expandone(ai,all,i) end end local trace_times = false trackers.register("structures.tags.math.times", function(v) trace_times = v end) expandone = function(c,all,i,start,stop) if c then local tg = c.tg if tg == "mrow" then local at = c.at if at.mathunit then n = n + 1 ; t[n] = at.mathunit else local category = tonumber(at.mathcategory) local kind = category and functiontype(category) local fenced = kind == "fence" and functions[category] local a, na = xmlevery(c) if na > 0 then local subfence = hasmiddle(a) if at.mathfunctionstack then if at.mathfunctionstack then n = n + 1 ; t[n] = getlabel(at.mathfunctionstack) else expandsymbol(c) end at.mathnogroup = "true" else local mfs = at.mathfractionstack -- maybe also check for tg == "mfrac" if mfs then at.mathnogroup = "true" -- fenced = false local s = getlabel(mfs) if na > 1 and s and s ~= "" then -- maybe check if there is a meaning set at.nofraction = mfs n = n + 1 ; t[n] = s end local a = xmlall(c,"/mfrac") or { } expandone(a[1],a,1) goto DONE end end -- how about na == 1 local isgroup = false if fenced then -- isgroup = false elseif at.mathnogroup then -- isgroup = false else isgroup = n > 1 end if fenced then local s = getlabel(fenced.tag) n = n + 1 ; t[n] = getlabel("optional begin") if s == "" then n = n + 1 ; t[n] = getlabel("begin fenced") else n = n + 1 ; t[n] = s end elseif isgroup then n = n + 1 ; t[n] = getlabel(start or "begin group") end if fenced then if na == 3 then a[2].at.mathnogroup = "true" elseif na == 5 and hasmiddle then a[2].at.mathnogroup = "true" a[3].at.mathnogroup = "true" end end -- for i=1,na do local ai = a[i] local tg = ai.tg local at = ai.at if at.mathtimes then local s -- if at.mathplus then -- s = getlabel("fractionplus") -- else s = getlabel("times") -- end if s and s ~= "" then if trace_times then n = n + 1 ; t[n] = s .. at.mathtimes else n = n + 1 ; t[n] = s end end end if at.checkfunction then checkfunction(a,i,na) expandone(ai,a,i) elseif tg == "mo" then expandfenced(ai,fenced,a,i) -- elseif tg == "mn" or tg == "mtext" or tg == "ms" then -- expandone(ai) elseif tg == "msub" or tg == "msup" or tg == "msubsup" then local first = xmlfirst(ai,"/*") local fat = first and first.at if first then if fat.checkfunction then checkfunction(a,i,na) expandone(ai,a,i) else expandfenced(ai,fenced,a,i) end else expandfenced(ai,fenced,a,i) end else expandone(ai,a,i) end end -- if fenced then local s = getlabel(fenced.tag) if s == "" then n = n + 1 ; t[n] = getlabel("end fenced") else n = n + 1 ; t[n] = getlabel("end") n = n + 1 ; t[n] = s end elseif isgroup then n = n + 1 ; t[n] = getlabel(stop or "end group") end end end ::DONE:: elseif tg == "mo" then expandsymbol(c) elseif tg == "mn" then n = n + 1 ; t[n] = xmltext(c) elseif tg == "mi" then local at = c.at if at.mathunit then n = n + 1 ; t[n] = at.mathunit elseif at.mathsymbolic then n = n + 1 ; t[n] = getlabel("function") expandsymbol(c) -- can be done directly local tg = c.__p__.tg if tg == "msub" or tg == "msup" or tg == "msubsup" then else if all then applyof(all,i,"functionof") end end elseif at.mathfunction then n = n + 1 ; t[n] = getlabel(c.dt[1]) local tg = c.__p__.tg if tg == "msub" or tg == "msup" or tg == "msubsup" then else if all then applyof(all,i,"functionof") end end else local s = getoptional(c.dt[1]) if s and s ~= "" then n = n + 1 ; t[n] = s end expandsymbol(c) end elseif tg == "msub" then local a, na = xmlevery(c) if a then local a1 = a[1] local at = a1.at expandone(a1,all,i) if haslimits(at) or isintegral(at) or isoperator(at) then if isintegral(at) or isoperator(at) then n = n + 1 ; t[n] = getlabel("integralsub") -- integral with lower limit else n = n + 1 ; t[n] = getlabel("limitsub") -- limit type with lower limit end expandone(a[2],all,i) n = n + 1 ; t[n] = getlabel("pause") if all and #all > i then n = n + 1 ; t[n] = getlabel("operatorof") end else expandsubindex(c,a[2],all,i) if at.mathfunction or at.mathsymbolic then applyof(all,i,"functionof") end end end elseif tg == "msup" then local a, na = xmlevery(c) if a then local a1 = a[1] local a2 = a[2] local at = a1.at local group = a2.at.mathgroup if group == "postfix operator" then -- kind of ugly: expandone(a2) -- ignored anyway n = n + 1 ; t[n] = getlabel("operatorof") expandone(a1) elseif group == "prime" then expandone(a1) expandone(a2) if all then applyof(all,i,"primeof") end elseif haslimits(at) or isintegral(at) or isoperator(at) then expandone(a1,all,i) n = n + 1 ; t[n] = getlabel("operatorsup") expandone(a2,all,i) n = n + 1 ; t[n] = getlabel("pause") if all and #all > i then n = n + 1 ; t[n] = getlabel("operatorof") end else expandone(a1,all,i) expandsupindex(c,a2,all,i) if at.mathfunction or at.mathsymbolic then applyof(all,i,"functionof") end end end elseif tg == "msubsup" then local a, na = xmlevery(c) if a then local a1 = a[1] local a2 = a[2] local a3 = a[3] local at = a1.at if a2.at.mathclass == "prime" then expandone(a2) expandone(a1) if haslimits(at) or isintegral(at) or isoperator(at) then n = n + 1 ; t[n] = getlabel("operatorsubsupfrom") -- from was: with lower limit expandone(a3,all,i) n = n + 1 ; t[n] = getlabel("pause") end if all and #all > i then n = n + 1 ; t[n] = getlabel("operatorof") end elseif haslimits(at) or isintegral(at) or isoperator(at) then expandone(a1,all,i) n = n + 1 ; t[n] = getlabel("operatorsubsupfrom") -- from was: with lower limit expandone(a2,all,i) n = n + 1 ; t[n] = getlabel("operatorsubsupto") -- upto was: and upper limit expandone(a3,all,i) n = n + 1 ; t[n] = getlabel("pause") if all and #all > i then n = n + 1 ; t[n] = getlabel("operatorof") end else expandone(a1,all,i) expandsubindex(c,a2,all,i) expandsupindex(c,a3,all,i) if at.mathfunction or at.mathsymbolic then applyof(all,i,"functionof") end end end elseif tg == "mfrac" then local a, na = xmlevery(c) if a then local ok, partial = isdifferential(a) if ok then if partial then n = n + 1 ; t[n] = getlabel("the partial derivative") else n = n + 1 ; t[n] = getlabel("the derivative") end expandone(a[1],all,i,"","") n = n + 1 ; t[n] = getlabel("over") expandone(a[2],all,i,"","end derivative") elseif c.__p__.at.nofraction then -- uggly expandone(a[1],all,i,"","") n = n + 1 ; t[n] = getlabel("over") expandone(a[2],all,i,"","") n = n + 1 ; t[n] = getlabel("end " .. c.__p__.at.nofraction) -- n = n + 1 ; t[n] = getlabel(c.__p__.at.nofraction) else n = n + 1 ; t[n] = getlabel("the fraction of") expandone(a[1],all,i,"begin numerator","end numerator") n = n + 1 ; t[n] = getlabel("and") expandone(a[2],all,i,"begin denominator","end denominator") end end elseif tg == "msqrt" then n = n + 1 ; t[n] = getlabel("the square root") local a, na = xmlevery(c) if a then n = n + 1 ; t[n] = getlabel("rootof") expand(a,all,i) end elseif tg == "mroot" then local a, na = xmlevery(c) n = n + 1 ; t[n] = getlabel("the root with degree") if a then expandone(a[2],all,i) n = n + 1 ; t[n] = getlabel("rootof") expandone(a[1],all,i) end elseif tg == "munder" then -- needs checking local a, na = xmlevery(c) if a then local category = tonumber(c.at.mathcategory) if functiontype(category) == "accent" then local fnc = functions[category] n = n + 1 ; t[n] = getlabel(fnc.tag) expandone(a[1],all,i) else expandone(a[1],all,i) n = n + 1 ; t[n] = getlabel("under") expandone(a[2],all,i) end end elseif tg == "mover" then -- needs checking local a, na = xmlevery(c) if a then local category = tonumber(c.at.mathcategory) if functiontype(category) == "accent" then local fnc = functions[category] n = n + 1 ; t[n] = getlabel(fnc.tag) expandone(a[1],all,i) else expandone(a[1],all,i) n = n + 1 ; t[n] = getlabel("over") expandone(a[2],all,i) end end elseif tg == "munderover" then -- needs checking local a, na = xmlevery(c) if a then expandone(a[1],all,i) n = n + 1 ; t[n] = getlabel("under") expandone(a[2],all,i) n = n + 1 ; t[n] = getlabel("and over") expandone(a[3],all,i) end elseif tg == "mtext" then n = n + 1 ; t[n] = xmltext(c) elseif tg == "mspace" then n = n + 1 ; t[n] = "" elseif tg == "ms" then n = n + 1 ; t[n] = xmltext(c) elseif tg == "mmultiscripts" then -- needs checking, todo prime local a, na = xmlevery(c) if a then local p = false local s = true expandone(a[1],all,i) for i=2,#a do local ai = a[i] if ai.tg == "mprescripts" then p = true n = n + 1 ; t[n] = getlabel("prescripts") elseif p then if ai.tg ~= "mtext" then n = n + 1 ; t[n] = getlabel(s and "presub" or "presuper") expandone(ai,all,i) end s = not s end end s = true for i=2,#a do local ai = a[i] if ai.tg == "mprescripts" then break elseif i == 2 then n = n + 1 ; t[n] = getlabel("postscripts") end if ai.tg ~= "mtext" then n = n + 1 ; t[n] = getlabel(s and "postsub" or "postsuper") expandone(ai,all,i) end s = not s end n = n + 1 ; t[n] = getlabel("end scripts") end elseif tg == "math" then local a, na = xmlevery(c) if a then expand(a,all,i) end elseif tg == "mtable" then local detail = c.at.detail if detail == "cases" then n = n + 1 ; t[n] = getlabel("begin cases") local nr = 0 for row in xmlcollected(c,"/mtr") do nr = nr + 1 n = n + 1 ; t[n] = getlabel("case") n = n + 1 ; t[n] = tostring(nr) for cell in xmlcollected(row,"/mtd") do local a, na = xmlevery(cell) if a then expand(a) end end -- n = n + 1 ; t[n] = getlabel("end case") end n = n + 1 ; t[n] = getlabel("end cases") else n = n + 1 ; t[n] = getlabel("begin table") local nr = 0 for row in xmlcollected(c,"/mtr") do local nc = 0 nr = nr + 1 -- n = n + 1 ; t[n] = getlabel("row") -- n = n + 1 ; t[n] = tostring(nr) for cell in xmlcollected(row,"/mtd") do nc = nc + 1 -- n = n + 1 ; t[n] = getlabel("column") -- n = n + 1 ; t[n] = tostring(nc) n = n + 1 ; t[n] = getlabel("cell") n = n + 1 ; t[n] = tostring(nr) n = n + 1 ; t[n] = tostring(nc) local a, na = xmlevery(cell) if a then expand(a) end end end n = n + 1 ; t[n] = getlabel("end table") end elseif not c.special then okay = false report("todo: %s",tostring(c)) end end end expand = function(all) if all then for i=1,#all do expandone(all[i],all,i) end end end local keep_last = false trackers.register("structures.tags.math.keeplast", function(v) keep_last = v end) tomeaning = function(x,l) t = { } n = 0 okay = true language = l or "en" domain = x.at["data-lmtx-domain"] or x.at["domain"] or "default" -- expandone(x) if okay then -- we can have a helper for this local m = false for i=1,n do local ti = t[i] if ti == "" then if not m then m = i - 1 end elseif m then m = m + 1 t[m] = ti end end if m then n = m end if t[n] == "." then n = n - 1 end if keep_last then xmlfixattributes(x) buffers.assign(type(keep_last) == "string" and keep_last or "lastmms",tostring(x)) else lastxml = false end return concat(t," ",1,n) end end getlast = function() return lastxml or "" end end local stripped do local strip = true directives.register("structures.tags.math.strip", function(v) strip = v end) stripped = function(s) if strip and #s > 0 then -- print(s) local x = xmlconvert(s) xmlwipeattributes(x) return tostring(x) else return s end end end local verbose do -- We could save the prepared if we do more languages but that is only relevant when -- we develop so it has little gain. local warned = false -- local saved = false -- local saving = false -- trackers.register("structures.tags.math.save",function(v) -- saving = v -- if saving and not saved then -- saved = table.setmetatableindex("table") -- luatex.registerstopactions(function() table.save(tex.jobname .. "-mms-meanings.lua",saved) end) -- end -- end) verbose = function(s,l) if not warned then report("this feature is experimental and under construction") warned = true end local root = xmlconvert(s) if not root.error then local x = xmlfirst(root,"/math") if x then prepare(x) -- inspect(x) local meaning = tomeaning(x,l) -- if saving then -- saved[s][l] = meaning -- end return meaning end end end end xml.mml = { verbose = verbose, stripped = stripped, }