lxml-mms.lmt /size: 46 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['lxml-mms'] = {
2    version   = 1.001,
3    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
4    comment   = "written together with Mikael Sundqvist",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Although it sits in the lxml name space this module is pretty much hooked into
10-- ConTeXt math rendering and backend code. It is one of the first applications of
11-- math dictionary support. After experimenting with hooking serialization of math
12-- into the rendering we decided that it made more sense to use the export
13-- facilities instead. It is also a fun project that might lead so more similar
14-- functionality. Of course we don't limit ourselves to English, if only because
15-- our test more advanced cases are Swedish math books.
16--
17-- The rationale for this serialization can be found in demands for documents to be
18-- accessible and math is a tricky part of that. We embed MathML in tagged PDF
19-- documents and the serialization hooks into the actual text features. This is
20-- needed in order to make accessibility validators (like the ones used at Lunds
21-- University) happy. We let users decide how useful and reliable it is. In ConTeXt
22-- you can overload and adapt bits and pieces as part of the document style. For the
23-- record: we are not involved in accesibility projects, so we might get some things
24-- wrong.
25--
26-- Timestamp: yet another entertaining drum track by Gavin Harrison "Pick Up The
27-- Pieces" | Zildjian 400th UK (posted 2024/2/5) - three weeks before a PT concert
28-- (YT: "The Pineapple Thief - It Leads to This).
29--
30-- While we worked on this we decovered Mohini Dey so that's another timestamp on
31-- this development (one of the Rick Beato interviews).
32--
33-- This is experimental code that will be optimized. Also, some of it can be a bit
34-- but we do that once we're done. In the end it might look simple but quite a bit
35-- of exploration was involved.
36
37local tonumber, tostring, type, next = tonumber, tostring, type, next
38local concat = table.concat
39local formatters = string.formatters
40
41local xmlall       = xml.all
42local xmlfirst     = xml.first
43local xmltext      = xml.text
44local xmlconvert   = xml.convert
45local xmlcollected = xml.collected
46
47local report = logs.reporter("mms")
48
49local getname          = mathematics.dictionaries.name
50local classes          = mathematics.classes
51local getverboselabel  = mathematics.getverboselabel
52local getoptionallabel = mathematics.getoptionallabel
53local functions        = mathematics.categories.functions
54local functiontype     = mathematics.functiontype
55
56local function xmlevery(x)
57    local dt = x.dt
58    local tt
59    local nn = 0
60    for i=1,#dt do
61        local di = dt[i]
62        if di.tg and not di.special then
63            if tt then
64                nn = nn + 1
65                tt[nn] = di
66            else
67                nn = 1
68                tt = { di }
69            end
70        end
71    end
72    return tt, nn
73end
74
75local function xmlfixattributes(x)
76    for c in xmlcollected(x,"*") do
77        local at = c.at
78        for k, v in next, at do
79            at[k] = tostring(v)
80        end
81    end
82end
83
84local function xmlwipeattributes(x)
85    local at = { }
86    for c in xmlcollected(x,"*") do
87        local at = c.at
88        local colspan       = at.colspan
89        local stretchy      = at.stretchy
90        local scriptlevel   = at.scriptlevel
91        local rulethickness = at.rulethickness
92        if colspan or stretchy or scriptlevel or rulethickness then
93            c.at = {
94                columnspan    = colspan,       -- watch the name, different from tables
95                stretchy      = stretchy,
96                scriptlevel   = scriptlevel,
97                linethickness = rulethickness, -- watch the name
98            }
99        else
100            c.at = {
101                -- Don't set them afterwards because they are shared!
102            }
103        end
104    end
105end
106
107local prepare  do
108
109    local expandtimes, expandtimesone
110
111    expandtimesone = function(c,all,i,start,stop)
112        if c then
113            local tg = c.tg
114            if tg == "mrow" then
115                local at = c.at
116                if at.mathunit then
117                    -- nothing
118                else
119                    local a, na = xmlevery(c)
120                    if na > 0 then
121                     -- local mfs = at.mathfractionstack
122                     -- if mfs then
123                     --     local a = xmlall(c,"/mfrac") or { }
124                     --     expandtimesone(a[1],a,1)
125                     --     goto DONE
126                     -- end
127                        local times  = false
128                        local block  = true
129                        local closed = false
130                        for i=1,na do
131                            local ai    = a[i]
132                            local tg    = ai.tg
133                            local at    = ai.at
134                            local class = at.mathclass
135                            if  at.mathsymbolic
136                             or at.mathgroup == "postfix operator"
137                            then -- has to come before at.mathfunction check
138                                at.checkfunction = "true" -- saves a check later
139                                if closed then
140                                    at.mathtimes = 9.1
141                                    closed = false
142                                elseif block then
143                                    block = false
144                                else
145                                    at.mathtimes = 9.2
146                                end
147                                block = false -- registered functions like f and g
148                                times = true
149                                expandtimesone(ai,a,i)
150                            elseif at.mathfunction
151                             -- or at.mathfractionstack -- ADDED
152                                or at.mathfunctionstack
153                                or class == "integral"
154                                or at.mathgroup == "unary set"
155                                or at.mathgroup == "number set"
156                             -- or at.mathapplication
157                            then
158                                at.checkfunction = "true" -- saves a check later
159                                if closed then
160                                    at.mathtimes = 1.1
161                                    closed = false
162                                elseif block then
163                                    block = false
164--                                 elseif at.mathgroup == "postfix operator" then
165--                                     block = false
166                                elseif at.mathgroup ~= "unary set" then
167                                    at.mathtimes = 1.2
168                                end
169                                -- if at.mathsymbolic then
170                                --     block = false -- registered functions like f and g
171                                -- else
172                                block = true -- lim etc .. we need to intercept kind == 1
173                                -- end
174                                times = false
175                                expandtimesone(ai,a,i)
176                            elseif tg == "mo" then
177                                -- combine more here
178                                if class == "open" then
179                                    if closed then
180                                        at.mathtimes = 2.1
181                                        closed = false
182                                    elseif times and not block then
183                                        at.mathtimes = 2.2
184                                    end
185                                elseif i > 1 and class == "close" then -- block ?
186                                    closed = true
187                                else
188                                    closed = false
189                                end
190                                expandtimes(ai)
191                                times = false
192                                block = true
193                            elseif tg == "mn" then
194                                if closed then
195                                    at.mathtimes = 3.1
196                                    closed = false
197                                elseif times and not block then
198                                    at.mathtimes = 3.2
199                                end
200                                expandtimesone(ai)
201                                times = false
202                                block = false
203                            elseif tg == "msub" or tg == "msup" or tg == "msubsup" then
204                                local first = xmlfirst(ai,"/*")
205                                local fat   = first and first.at
206                                if closed then
207                                    at.mathtimes = 4.1
208                                    closed = false
209                                elseif first and at.mathclass == "close" then
210                                    closed = true
211                                elseif times and not block then
212                                    at.mathtimes = 4.2
213                                end
214                                if first then
215                                    if     fat.mathfunction
216                                        or fat.mathfunctionstack
217                                        or fat.mathclass == "integral"
218                                        or fat.mathclass == "operator"
219                                        or fat.mathclass == "differential"
220                                    then
221                                        if not fat.mathfunction then
222                                            fat.mathfunction = "true" -- maybe also needed for the stack
223                                        end
224                                        at.checkfunction = "true" -- saves a check later
225                                        expandtimesone(ai,a,i)
226                                        block = true
227                                        times = false
228                                    else
229                                        expandtimesone(ai,a,i)
230                                        block = false
231                                        times = true
232                                    end
233                                else
234                                    expandtimesone(ai,a,i)
235                                    block = false
236                                    times = true
237                                end
238                            elseif at.mathfractionstack
239                                or tg == "mroot"
240                                or tg == "mfrac"
241                                or tg == "msqrt"
242                                or at.mathsymbolic
243                            then
244                                if closed then
245                                    at.mathtimes = 5.1
246                                    closed = false
247                                elseif block then
248                                    block = false
249                                else
250                                    at.mathtimes = 5.2
251                                end
252                                times = true
253                                expandtimesone(ai,a,i)
254                            elseif class == "differential" then
255                                closed = false
256                                block = true
257                                times = true
258                                expandtimesone(ai,a,i)
259                            elseif class == "variable" then
260                             -- local c = at.mathcharacter
261                             -- if c == "." or c == "," then -- period comma
262                             --     block = true
263                             --     times = false
264                             -- else
265                                    if closed then
266                                        at.mathtimes = 6.1
267                                        closed = false
268                                    elseif times and not block then
269                                        at.mathtimes = 6.2
270                                    end
271                                    block = false
272                                    times = true
273                             -- end
274                                expandtimesone(ai,a,i)
275                            elseif tg == "mrow" then
276                                if closed or times then
277                                    at.mathtimes = 7.1
278                                    closed = false
279                                end
280                                times = true
281                                expandtimesone(ai,a,i)
282                            elseif tg == "mtext" or tg == "ms" then
283                                if closed then
284                                    at.mathtimes = 8.1
285                                    closed = false
286                                end
287                                expandtimesone(ai)
288                                times = false
289                                block = false
290                            else
291                                if closed then
292                                    at.mathtimes = 9.1
293                                    closed = false
294                                end
295                                times = false
296                                expandtimesone(ai,a,i)
297                            end
298                        end
299                    end
300                end
301          ::DONE::
302            elseif tg == "mtable" then
303                for cell in xmlcollected(c,"mtd") do
304                    local a, na = xmlevery(cell)
305                    if a then
306                        expandtimes(a)
307                    end
308                end
309            elseif tg == "mtext" or tg == "mspace" or tg == "ms" then
310                -- nothing to do
311            elseif tg == "mfrac" then
312                local a, na = xmlevery(c)
313                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
314                    -- mark not times
315                    c.at.mathplus = 1 -- invisible plus
316                end
317-- inspect(c.at)
318            else
319                local a, na = xmlevery(c)
320                if a then
321                    expandtimes(a)
322                end
323            end
324        end
325    end
326
327    expandtimes = function(all)
328        if all then
329            for i=1,#all do
330                expandtimesone(all[i],all,i)
331            end
332        end
333    end
334
335    prepare = function(x)
336        local all = xmlall(x,"mi[not @mathfunction]")
337        if all then
338            for i=1,#all do
339                local a = all[i]
340                local t = a.dt[1]
341                local f = functions[t]
342                if f then
343                    local at = a.at
344                    at.mathfunction = "true"
345                    at.mathsymbolic = "true"
346                end
347            end
348        end
349        -- left is a function, right is an operator
350        for c in xmlcollected(x,"mrow[@mathfunctionstack]") do
351--         for c in xmlcollected(x,"(msub|msup|msubsup|mrow)[@mathfunctionstack]") do
352            local cat = c.at
353            local s   = cat.mathstack
354            for cc in xmlcollected(c,"(mi|mo)[@mathstack]") do
355                local ccat = cc.at
356                if ccat.mathstack == s then
357                    cat.mathfunction  = ccat.mathfunction  or cat.mathfunction
358                    cat.mathcharacter = ccat.mathcharacter or cat.mathcharacter
359                    cat.mathgroup     = ccat.mathgroup     or cat.mathgroup
360                    if cc.tg == "mo" then
361                        ccat.mathignore  = "true"
362                     -- ccat.mathmeaning = cat.mathfunctionstack
363                    end
364                end
365            end
366        end
367        --
368        local all = xmlall(x,"(msub|msup|msubsup)/mo[@mathclass='open' or @mathclass='close' or @mathclass='middle']")
369        if all then
370            for i=1,#all do
371                local ai = all[i]
372                local at = ai.at
373                at.mathignore = "true"
374                ai.__p__.at.mathclass = at.mathclass
375            end
376        end
377        --
378        expandtimesone(x)
379    end
380
381end
382
383local tomeaning, getlast  do
384
385    -- we don't really need to handle mfenced
386
387    local t, n, expand, expandone, okay, language, domain
388
389    -- todo: a getlabel wrapper so that we don't need to pass language and domain
390
391    local function getlabel(tag)
392        return getverboselabel(tag,language,domain)
393    end
394
395    local function getoptional(tag)
396        return getoptionallabel(tag,language,domain)
397    end
398
399    local utfsplit  = utf.split
400    local utfbyte   = utf.byte
401    local isprivate = fonts.helpers.isprivate
402
403    local unknown <const> = " ? "
404
405    local function sanitize(s)
406        local t = utfsplit(s)
407        local n = #t
408        if n == 1 then
409            if isprivate(utfbyte(s)) then
410                return unknown
411            end
412        else
413            local done = false
414            for i=1,n do
415                if isprivate(utfbyte(t[i])) then
416                    done = true
417                    t[i] = unknown
418                end
419            end
420            if done then
421                return concat(t)
422            end
423        end
424        return s
425    end
426
427    local function expandsymbol(c)
428        local at = c.at
429        if not at.mathignore then
430            local result = at.mathidentity
431            if not result then
432-- better: mathmeaning as overload
433-- if at.mathfunctionstack then
434--     result = getlabel(at.mathfunctionstack)
435-- else
436                local group = at.mathgroup
437                if group then
438                    local character = tonumber(at.mathcharacter)
439                    local index     = tonumber(at.mathindex)
440                    local s = getname(group,character) or getname(group,index)
441                    if type(s) == "string" then
442                        result = getlabel(s)
443                    else
444                        result = sanitize(xmltext(c))
445                    end
446                else
447                   result = sanitize(xmltext(c))
448                end
449-- end
450            end
451            if result ~= "" then
452                n = n + 1 ; t[n] = result
453            end
454        end
455    end
456
457    -- replace method by category and kind
458
459    local function hasmiddle(a)
460        if a then
461            for i=1,#a do
462                local class = a[i].at.mathclass
463                if class == "middle" then
464                    return true
465                end
466            end
467        end
468        return false
469    end
470
471    local function haslimits(at)
472        local f = at.mathfunction
473        if f then
474            f = functions[f]
475            if f then
476                return f.method == "limits"
477            end
478        end
479        return false
480    end
481
482    local function isintegral(at)
483        return at.mathclass == "integral"
484    end
485
486    local function isoperator(at)
487        return at.mathclass == "operator"
488    end
489
490    -- see if we can avoid passing all and i
491
492    local function expandsupindex(c,a,all,i)
493        if c.at.mathsupindex then
494            n = n + 1 ; t[n] = getlabel("supindex")
495            expandone(a,all,i)
496        else
497            n = n + 1 ; t[n] = getlabel("to the power of")
498            expandone(a,all,i)
499            local last = t[n]
500            if last == "2" then
501                n = n - 1 ; t[n] = getlabel("squared")
502            elseif last == "3" then
503                n = n - 1 ; t[n] = getlabel("cubed")
504            end
505        end
506    end
507
508    local function expandsubindex(c,a,all,i)
509        if c.at.mathsubindex then
510            n = n + 1 ; t[n] = getlabel("subindex")
511        else
512            n = n + 1 ; t[n] = getlabel("sub")
513        end
514        expandone(a,all,i)
515    end
516
517    local function checkfunction(a,i,na)
518        if i + 3 <= na then
519            local a1 = a[i+1]
520            if a1.tg == "mo" then
521                local a3 = a[i+3]
522                if a3.tg == "mo" then
523                    local a2 = a[i+2]
524                    local t2 = a2.tg
525                    if t2 == "mn" or t2 == "mi" then
526                        local at1 = a1.at
527                        local at3 = a3.at
528                        local class1 = at1.mathclass
529                        local class2 = at3.mathclass
530                        if class1 and class2 and class1 == "open" and class2 == "close" then
531                            at1.mathnogroup = "true"
532                            at3.mathnogroup = "true"
533                        end
534                    end
535                end
536            end
537        end
538    end
539
540    local partials = {
541        ["∂"] = "d", -- todo: bold math
542    }
543
544    local function isdifferential(a)
545        local a1 = a[1]
546        local a2 = a[2]
547        if a1 and a2 then
548            local tg2 = a2.tg
549            local ps = false
550            if tg2 == "mrow" or tg2 == "msub" or tg2 == "msup" or tg2 == "msubsup" then
551                local aa = xmlfirst(a2,"/*")
552                if aa then
553                    if aa.tg == "mi" and aa.at.mathclass == "differential" then
554                        ps = aa
555                    else
556                        local aa = xmlfirst(aa,"/*")
557                        if aa and aa.tg == "mi" and aa.at.mathclass == "differential" then
558                            ps = aa
559                        else
560                            return false
561                        end
562                    end
563                else
564                    return false
565                end
566            end
567            -- can be helper
568            local tg1 = a1.tg
569            if tg1 == "mrow" or tg1 == "msub" or tg1 == "msup" or tg1 == "msubsup" then
570                local aa = xmlfirst(a1,"/*")
571                if aa then
572                    if aa.tg == "mi" and aa.at.mathclass == "differential" then
573                        local p = partials[aa.dt[1]]
574                        if p then
575                            -- aa.at.mathidentity = p
576                            -- ps.at.mathidentity = p
577                            return true, true
578                        else
579                            return true, false
580                        end
581                    else
582                        local aa = xmlfirst(aa,"/*")
583                        if aa and aa.tg == "mi" and aa.at.mathclass == "differential" then
584                            local p = partials[aa.dt[1]]
585                            if p then
586                                -- aa.at.mathidentity = p
587                                -- ps.at.mathidentity = p
588                                return true, true
589                            else
590                                return true, false
591                            end
592                        else
593                            return false
594                        end
595                    end
596                else
597                    return false
598                end
599            elseif tg1 == "mi" and a1.at.mathclass == "differential" then
600                local p = partials[a1.dt[1]]
601                if partials[a1.dt[1]] then
602                    -- a1.at.mathidentity = p
603                    -- ps.at.mathidentity = p
604                    return true, true
605                else
606                    return true, false
607                end
608            end
609        end
610        return false
611    end
612
613    local function applyof(all,i,label)
614        local ai1 = all[i + 1]
615        if ai1 and ai1.tg == "mo" then
616            local c = ai1.at.mathclass
617            if c == "open" then
618                n = n + 1 ; t[n] = getlabel(label)
619                ai1.at.mathtimes = nil
620            end
621        end
622    end
623
624    local function expandfenced(ai,fenced,all,i)
625        local at = ai.at
626        local class = at.mathclass
627        if not class then
628            if at.mathnogroup then
629                -- ignore
630            else
631                expandone(ai,all,i)
632            end
633        elseif class == "open" then
634            if fenced or at.mathnogroup then
635                -- ignore
636            else
637                n = n + 1 ; t[n] = getlabel("begin group")
638            end
639            at.mathignore = "true"
640            expandone(ai)
641        elseif class == "close" then
642            if fenced or at.mathnogroup then
643                -- ignore
644            else
645                n = n + 1 ; t[n] = getlabel("end group")
646            end
647            at.mathignore = "true"
648            expandone(ai)
649        elseif class == "middle" then
650            if fenced then
651                n = n + 1 ; t[n] = getlabel(fenced.tag .. ":fence")
652                at.mathignore = "true"
653                expandone(ai)
654                local aa = all and all[i+1]
655                if aa then
656                    aa.at.mathnogroup = "true"
657                end
658            else
659                expandone(ai)
660            end
661        else
662            expandone(ai,all,i)
663        end
664    end
665
666    local trace_times = false
667
668    trackers.register("structures.tags.math.times", function(v) trace_times = v end)
669
670    expandone = function(c,all,i,start,stop)
671        if c then
672            local tg = c.tg
673            if tg == "mrow" then
674                local at = c.at
675                if at.mathunit then
676                    n = n + 1 ; t[n] = at.mathunit
677                else
678                    local category = tonumber(at.mathcategory)
679                    local kind     = category and functiontype(category)
680                    local fenced   = kind == "fence" and functions[category]
681                    local a, na    = xmlevery(c)
682                    if na > 0 then
683                        local subfence = hasmiddle(a)
684                        if at.mathfunctionstack then
685                            if at.mathfunctionstack then
686                                n = n + 1 ; t[n] = getlabel(at.mathfunctionstack)
687                            else
688                                expandsymbol(c)
689                            end
690                            at.mathnogroup = "true"
691                        else
692                            local mfs = at.mathfractionstack -- maybe also check for tg == "mfrac"
693                            if mfs then
694                                at.mathnogroup = "true"
695                        --         fenced = false
696                                local s = getlabel(mfs)
697                                if na > 1 and s and s ~= "" then -- maybe check if there is a meaning set
698                                    at.nofraction = mfs
699                                    n = n + 1 ; t[n] = s
700                                end
701                                local a = xmlall(c,"/mfrac") or { }
702                                expandone(a[1],a,1)
703                                goto DONE
704                            end
705                        end
706                        -- how about na == 1
707                        local isgroup = false
708                        if fenced then
709                         -- isgroup  = false
710                        elseif at.mathnogroup then
711                         -- isgroup  = false
712                        else
713                            isgroup  = n > 1
714                        end
715                        if fenced then
716                            local s = getlabel(fenced.tag)
717                            n = n + 1 ; t[n] = getlabel("optional begin")
718                            if s == "" then
719                                n = n + 1 ; t[n] = getlabel("begin fenced")
720                            else
721                                n = n + 1 ; t[n] = s
722                            end
723                        elseif isgroup then
724                            n = n + 1 ; t[n] = getlabel(start or "begin group")
725                        end
726                        if fenced then
727                            if na == 3 then
728                                a[2].at.mathnogroup = "true"
729                            elseif na == 5 and hasmiddle then
730                                a[2].at.mathnogroup = "true"
731                                a[3].at.mathnogroup = "true"
732                            end
733                        end
734                        --
735                        for i=1,na do
736                            local ai = a[i]
737                            local tg = ai.tg
738                            local at = ai.at
739                            if at.mathtimes then
740                                local s
741                             -- if at.mathplus then
742                             --     s = getlabel("fractionplus")
743                             -- else
744                                    s = getlabel("times")
745                             -- end
746                                if s and s ~= "" then
747                                    if trace_times then
748                                        n = n + 1 ; t[n] = s .. at.mathtimes
749                                    else
750                                        n = n + 1 ; t[n] = s
751                                    end
752                                end
753                            end
754                            if at.checkfunction then
755                                checkfunction(a,i,na)
756                                expandone(ai,a,i)
757                            elseif tg == "mo" then
758                                expandfenced(ai,fenced,a,i)
759                         -- elseif tg == "mn" or tg == "mtext" or tg == "ms" then
760                         --     expandone(ai)
761                            elseif tg == "msub" or tg == "msup" or tg == "msubsup" then
762                                local first = xmlfirst(ai,"/*")
763                                local fat   = first and first.at
764                                if first then
765                                    if fat.checkfunction then
766                                        checkfunction(a,i,na)
767                                        expandone(ai,a,i)
768                                    else
769                                        expandfenced(ai,fenced,a,i)
770                                    end
771                                else
772                                    expandfenced(ai,fenced,a,i)
773                                end
774                            else
775                                expandone(ai,a,i)
776                            end
777                        end
778                        --
779                        if fenced then
780                            local s = getlabel(fenced.tag)
781                            if s == "" then
782                                n = n + 1 ; t[n] = getlabel("end fenced")
783                            else
784                                n = n + 1 ; t[n] = getlabel("end")
785                                n = n + 1 ; t[n] = s
786                            end
787                        elseif isgroup then
788                            n = n + 1 ; t[n] = getlabel(stop or "end group")
789                        end
790                    end
791                end
792              ::DONE::
793            elseif tg == "mo" then
794                expandsymbol(c)
795            elseif tg == "mn" then
796                n = n + 1 ; t[n] = xmltext(c)
797            elseif tg == "mi" then
798                local at = c.at
799                if at.mathunit then
800                    n = n + 1 ; t[n] = at.mathunit
801                elseif at.mathsymbolic then
802                    n = n + 1 ; t[n] = getlabel("function")
803                    expandsymbol(c) -- can be done directly
804                    local tg = c.__p__.tg
805                    if tg == "msub" or tg == "msup" or tg == "msubsup" then
806                    else
807                        if all then
808                            applyof(all,i,"functionof")
809                        end
810                    end
811                elseif at.mathfunction then
812                    n = n + 1 ; t[n] = getlabel(c.dt[1])
813                    local tg = c.__p__.tg
814                    if tg == "msub" or tg == "msup" or tg == "msubsup" then
815                    else
816                        if all then
817                            applyof(all,i,"functionof")
818                        end
819                    end
820                else
821                    local s = getoptional(c.dt[1])
822                    if s and s ~= "" then
823                        n = n + 1 ; t[n] = s
824                    end
825                    expandsymbol(c)
826                end
827            elseif tg == "msub" then
828                local a, na = xmlevery(c)
829                if a then
830                    local a1 = a[1]
831                    local at = a1.at
832                    expandone(a1,all,i)
833                    if haslimits(at) or isintegral(at) or isoperator(at) then
834                        if isintegral(at) or isoperator(at) then
835                            n = n + 1 ; t[n] = getlabel("integralsub") -- integral with lower limit
836                        else
837                            n = n + 1 ; t[n] = getlabel("limitsub") -- limit type with lower limit
838                        end
839                        expandone(a[2],all,i)
840                        n = n + 1 ; t[n] = getlabel("pause")
841                        if all and #all > i then
842                            n = n + 1 ; t[n] = getlabel("operatorof")
843                        end
844                    else
845                        expandsubindex(c,a[2],all,i)
846                        if at.mathfunction or at.mathsymbolic then
847                            applyof(all,i,"functionof")
848                        end
849                    end
850                end
851            elseif tg == "msup" then
852                local a, na = xmlevery(c)
853                if a then
854                    local a1 = a[1]
855                    local a2 = a[2]
856                    local at = a1.at
857                    local group = a2.at.mathgroup
858                    if group == "postfix operator" then
859                        -- kind of ugly:
860                        expandone(a2) -- ignored anyway
861                        n = n + 1 ; t[n] = getlabel("operatorof")
862                        expandone(a1)
863                    elseif group == "prime" then
864                        expandone(a1)
865                        expandone(a2)
866                        if all then
867                            applyof(all,i,"primeof")
868                        end
869                    elseif haslimits(at) or isintegral(at) or isoperator(at) then
870                        expandone(a1,all,i)
871                        n = n + 1 ; t[n] = getlabel("operatorsup")
872                        expandone(a2,all,i)
873                        n = n + 1 ; t[n] = getlabel("pause")
874                        if all and #all > i then
875                            n = n + 1 ; t[n] = getlabel("operatorof")
876                        end
877                    else
878                        expandone(a1,all,i)
879                        expandsupindex(c,a2,all,i)
880                        if at.mathfunction or at.mathsymbolic then
881                            applyof(all,i,"functionof")
882                        end
883                    end
884                end
885            elseif tg == "msubsup" then
886                local a, na = xmlevery(c)
887                if a then
888                    local a1 = a[1]
889                    local a2 = a[2]
890                    local a3 = a[3]
891                    local at = a1.at
892                    if a2.at.mathclass == "prime" then
893                        expandone(a2)
894                        expandone(a1)
895                        if haslimits(at) or isintegral(at) or isoperator(at) then
896                            n = n + 1 ; t[n] = getlabel("operatorsubsupfrom") -- from was: with lower limit
897                            expandone(a3,all,i)
898                            n = n + 1 ; t[n] = getlabel("pause")
899                        end
900                        if all and #all > i then
901                            n = n + 1 ; t[n] = getlabel("operatorof")
902                        end
903                    elseif haslimits(at) or isintegral(at) or isoperator(at) then
904                        expandone(a1,all,i)
905                        n = n + 1 ; t[n] = getlabel("operatorsubsupfrom") -- from was: with lower limit
906                        expandone(a2,all,i)
907                        n = n + 1 ; t[n] = getlabel("operatorsubsupto")  -- upto was: and upper limit
908                        expandone(a3,all,i)
909                        n = n + 1 ; t[n] = getlabel("pause")
910                        if all and #all > i then
911                            n = n + 1 ; t[n] = getlabel("operatorof")
912                        end
913                    else
914                        expandone(a1,all,i)
915                        expandsubindex(c,a2,all,i)
916                        expandsupindex(c,a3,all,i)
917                        if at.mathfunction or at.mathsymbolic then
918                            applyof(all,i,"functionof")
919                        end
920                    end
921                end
922            elseif tg == "mfrac" then
923                local a, na = xmlevery(c)
924                if a then
925                    local ok, partial = isdifferential(a)
926                    if ok then
927                        if partial then
928                            n = n + 1 ; t[n] = getlabel("the partial derivative")
929                        else
930                            n = n + 1 ; t[n] = getlabel("the derivative")
931                        end
932                        expandone(a[1],all,i,"","")
933                        n = n + 1 ; t[n] = getlabel("over")
934                        expandone(a[2],all,i,"","end derivative")
935                    elseif c.__p__.at.nofraction then -- uggly
936                        expandone(a[1],all,i,"","")
937                        n = n + 1 ; t[n] = getlabel("over")
938                        expandone(a[2],all,i,"","")
939                        n = n + 1 ; t[n] = getlabel("end " .. c.__p__.at.nofraction)
940                        -- n = n + 1 ; t[n] = getlabel(c.__p__.at.nofraction)
941                    else
942                        n = n + 1 ; t[n] = getlabel("the fraction of")
943                        expandone(a[1],all,i,"begin numerator","end numerator")
944                        n = n + 1 ; t[n] = getlabel("and")
945                        expandone(a[2],all,i,"begin denominator","end denominator")
946                    end
947                end
948            elseif tg == "msqrt" then
949                n = n + 1 ; t[n] = getlabel("the square root")
950                local a, na = xmlevery(c)
951                if a then
952                    n = n + 1 ; t[n] = getlabel("rootof")
953                    expand(a,all,i)
954                end
955            elseif tg == "mroot" then
956                local a, na = xmlevery(c)
957                n = n + 1 ; t[n] = getlabel("the root with degree")
958                if a then
959                    expandone(a[2],all,i)
960                    n = n + 1 ; t[n] = getlabel("rootof")
961                    expandone(a[1],all,i)
962                end
963            elseif tg == "munder" then -- needs checking
964                local a, na = xmlevery(c)
965                if a then
966                    local category = tonumber(c.at.mathcategory)
967                    if functiontype(category) == "accent" then
968                        local fnc = functions[category]
969                        n = n + 1 ; t[n] = getlabel(fnc.tag)
970                        expandone(a[1],all,i)
971                    else
972                        expandone(a[1],all,i)
973                        n = n + 1 ; t[n] = getlabel("under")
974                        expandone(a[2],all,i)
975                    end
976                end
977            elseif tg == "mover" then -- needs checking
978                local a, na = xmlevery(c)
979                if a then
980                    local category = tonumber(c.at.mathcategory)
981                    if functiontype(category) == "accent" then
982                        local fnc = functions[category]
983                        n = n + 1 ; t[n] = getlabel(fnc.tag)
984                        expandone(a[1],all,i)
985                    else
986                        expandone(a[1],all,i)
987                        n = n + 1 ; t[n] = getlabel("over")
988                        expandone(a[2],all,i)
989                    end
990                end
991            elseif tg == "munderover" then -- needs checking
992                local a, na = xmlevery(c)
993                if a then
994                    expandone(a[1],all,i)
995                    n = n + 1 ; t[n] = getlabel("under")
996                    expandone(a[2],all,i)
997                    n = n + 1 ; t[n] = getlabel("and over")
998                    expandone(a[3],all,i)
999                end
1000            elseif tg == "mtext" then
1001                n = n + 1 ; t[n] = xmltext(c)
1002            elseif tg == "mspace" then
1003                n = n + 1 ; t[n] = ""
1004            elseif tg == "ms" then
1005                n = n + 1 ; t[n] = xmltext(c)
1006            elseif tg == "mmultiscripts" then -- needs checking, todo prime
1007                local a, na = xmlevery(c)
1008                if a then
1009                    local p = false
1010                    local s = true
1011                    expandone(a[1],all,i)
1012                    for i=2,#a do
1013                        local ai = a[i]
1014                        if ai.tg == "mprescripts" then
1015                            p = true
1016                            n = n + 1 ; t[n] = getlabel("prescripts")
1017                        elseif p then
1018                            if ai.tg ~= "mtext" then
1019                                n = n + 1 ; t[n] = getlabel(s and "presub" or "presuper")
1020                                expandone(ai,all,i)
1021                            end
1022                            s = not s
1023                        end
1024                    end
1025                    s = true
1026                    for i=2,#a do
1027                        local ai = a[i]
1028                        if ai.tg == "mprescripts" then
1029                            break
1030                        elseif i == 2 then
1031                           n = n + 1 ; t[n] = getlabel("postscripts")
1032                        end
1033                        if ai.tg ~= "mtext" then
1034                            n = n + 1 ; t[n] = getlabel(s and "postsub" or "postsuper")
1035                            expandone(ai,all,i)
1036                        end
1037                        s = not s
1038                    end
1039                    n = n + 1 ; t[n] = getlabel("end scripts")
1040                end
1041            elseif tg == "math" then
1042                local a, na = xmlevery(c)
1043                if a then
1044                    expand(a,all,i)
1045                end
1046            elseif tg == "mtable" then
1047                local detail = c.at.detail
1048                if detail == "cases" then
1049                    n = n + 1 ; t[n] = getlabel("begin cases")
1050                    local nr = 0
1051                    for row in xmlcollected(c,"/mtr") do
1052                        nr = nr + 1
1053                        n = n + 1 ; t[n] = getlabel("case")
1054                        n = n + 1 ; t[n] = tostring(nr)
1055                        for cell in xmlcollected(row,"/mtd") do
1056                            local a, na = xmlevery(cell)
1057                            if a then
1058                                expand(a)
1059                            end
1060                        end
1061                     -- n = n + 1 ; t[n] = getlabel("end case")
1062                    end
1063                    n = n + 1 ; t[n] = getlabel("end cases")
1064                else
1065                    n = n + 1 ; t[n] = getlabel("begin table")
1066                    local nr = 0
1067                    for row in xmlcollected(c,"/mtr") do
1068                        local nc = 0
1069                        nr = nr + 1
1070                     -- n = n + 1 ; t[n] = getlabel("row")
1071                     -- n = n + 1 ; t[n] = tostring(nr)
1072                        for cell in xmlcollected(row,"/mtd") do
1073                            nc = nc + 1
1074                         -- n = n + 1 ; t[n] = getlabel("column")
1075                         -- n = n + 1 ; t[n] = tostring(nc)
1076                            n = n + 1 ; t[n] = getlabel("cell")
1077                            n = n + 1 ; t[n] = tostring(nr)
1078                            n = n + 1 ; t[n] = tostring(nc)
1079                            local a, na = xmlevery(cell)
1080                            if a then
1081                                expand(a)
1082                            end
1083                        end
1084                    end
1085                    n = n + 1 ; t[n] = getlabel("end table")
1086                end
1087            elseif not c.special then
1088                okay = false
1089                report("todo: %s",tostring(c))
1090            end
1091        end
1092    end
1093
1094    expand = function(all)
1095        if all then
1096            for i=1,#all do
1097                expandone(all[i],all,i)
1098            end
1099        end
1100    end
1101
1102    local keep_last = false
1103
1104    trackers.register("structures.tags.math.keeplast", function(v) keep_last = v end)
1105
1106    tomeaning = function(x,l)
1107        t        = { }
1108        n        = 0
1109        okay     = true
1110        language = l or "en"
1111        domain   = x.at["data-lmtx-domain"] or x.at["domain"] or "default"
1112        --
1113        expandone(x)
1114        if okay then
1115            -- we can have a helper for this
1116            local m = false
1117            for i=1,n do
1118                local ti = t[i]
1119                if ti == "" then
1120                    if not m then
1121                        m = i - 1
1122                    end
1123                elseif m then
1124                    m = m + 1
1125                    t[m] = ti
1126                end
1127            end
1128            if m then
1129                n = m
1130            end
1131            if t[n] == "." then
1132                n = n - 1
1133            end
1134            if keep_last then
1135                xmlfixattributes(x)
1136                buffers.assign(type(keep_last) == "string" and keep_last or "lastmms",tostring(x))
1137            else
1138                lastxml = false
1139            end
1140            return concat(t," ",1,n)
1141        end
1142    end
1143
1144    getlast = function()
1145        return lastxml or ""
1146    end
1147
1148end
1149
1150local stripped  do
1151
1152    local strip = true
1153
1154    directives.register("structures.tags.math.strip", function(v) strip = v end)
1155
1156    stripped = function(s)
1157        if strip and #s > 0 then
1158-- print(s)
1159            local x = xmlconvert(s)
1160            xmlwipeattributes(x)
1161            return tostring(x)
1162        else
1163            return s
1164        end
1165    end
1166
1167end
1168
1169local verbose  do
1170
1171    -- We could save the prepared if we do more languages but that is only relevant when
1172    -- we develop so it has little gain.
1173
1174    local warned = false
1175 -- local saved  = false
1176 -- local saving = false
1177
1178 -- trackers.register("structures.tags.math.save",function(v)
1179 --     saving = v
1180 --     if saving and not saved then
1181 --         saved = table.setmetatableindex("table")
1182 --         luatex.registerstopactions(function() table.save(tex.jobname .. "-mms-meanings.lua",saved) end)
1183 --     end
1184 -- end)
1185
1186    verbose = function(s,l)
1187        if not warned then
1188            report("this feature is experimental and under construction")
1189            warned = true
1190        end
1191        local root = xmlconvert(s)
1192        if not root.error then
1193            local x = xmlfirst(root,"/math")
1194            if x then
1195                prepare(x)
1196-- inspect(x)
1197                local meaning = tomeaning(x,l)
1198             -- if saving then
1199             --     saved[s][l] = meaning
1200             -- end
1201                return meaning
1202            end
1203        end
1204    end
1205
1206end
1207
1208xml.mml = {
1209    verbose  = verbose,
1210    stripped = stripped,
1211}
1212