back-exp.lmt /size: 112 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['back-exp'] = {
2    version   = 1.001,
3    comment   = "companion to back-exp.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- See drawinglines.tex for some about the mathmnl namespace.
10
11-- Todo: share properties more with tagged pdf (or the reverse)
12
13-- Because we run into the 200 local limit we quite some do .. end wrappers .. not always
14-- that nice but it has to be.
15
16-- Experiments demonstrated that mapping to <div> and classes is messy because we have to
17-- package attributes (some 30) into one set of (space seperatated but prefixed classes)
18-- which only makes things worse .. so if you want something else, use xslt to get there.
19
20-- language       -> only mainlanguage, local languages should happen through start/stoplanguage
21-- tocs/registers -> maybe add a stripper (i.e. just don't flush entries in final tree)
22-- footnotes      -> css 3
23-- bodyfont       -> in styles.css
24
25-- Because we need to look ahead we now always build a tree (this was optional in
26-- the beginning). The extra overhead in the frontend is neglectable.
27--
28-- We can optimize the code ... currently the overhead is some 10% for xml + html so
29-- there is no hurry.
30
31-- todo: move critital formatters out of functions
32-- todo: delay loading (apart from basic tag stuff)
33
34-- problem : too many local variables
35
36-- check setting __i__
37
38-- In 2024 this module got updated a bit as part of the upgraded math features, which makes
39-- sense as at that time it is some 15 years old. This time the musical timestamp is the
40-- CD release of Dream Chaser by Rendezvous Point. (So whenever I got bored by exporting I
41-- watched the Presence video on YT.)
42
43local next, type, tonumber = next, type, tonumber
44local sub, gsub, match = string.sub, string.gsub, string.match
45local validstring = string.valid
46local lpegmatch = lpeg.match
47local utfchar, utfvalues, utflen = utf.char, utf.values, utf.len
48local concat, merge, sort, setmetatableindex = table.concat, table.merge, table.sort, table.setmetatableindex
49local sortedhash, sortedkeys = table.sortedhash, table.sortedkeys
50local formatters = string.formatters
51local todimen = number.todimen
52local replacetemplate = utilities.templates.replace
53local settings_to_array = utilities.parsers.settings_to_array
54local settings_to_hash = utilities.parsers.settings_to_hash
55
56local addsuffix, joinfile, nameonly, basename, filesuffix = file.addsuffix, file.join, file.nameonly, file.basename, file.suffix
57
58local trace_export  = false  trackers.register  ("export.trace",         function(v) trace_export  = v end)
59local trace_spacing = false  trackers.register  ("export.trace.spacing", function(v) trace_spacing = v end)
60local trace_details = false  trackers.register  ("export.trace.details", function(v) trace_details = v end)
61
62local less_state    = false  directives.register("export.lessstate",     function(v) less_state    = v end)
63local show_comment  = true   directives.register("export.comment",       function(v) show_comment  = v end)
64local only_images   = false  directives.register("export.images",        function(v) only_images   = v end)
65
66-- maybe we will also support these:
67--
68-- local css_hyphens       = false  directives.register("export.css.hyphens",      function(v) css_hyphens      = v end)
69-- local css_textalign     = false  directives.register("export.css.textalign",    function(v) css_textalign    = v end)
70-- local css_bodyfontsize  = false  directives.register("export.css.bodyfontsize", function(v) css_bodyfontsize = v end)
71-- local css_textwidth     = false  directives.register("export.css.textwidth",    function(v) css_textwidth    = v end)
72
73local report_export     = logs.reporter("backend","export")
74
75local nodes             = nodes
76local attributes        = attributes
77
78local variables         = interfaces.variables
79local v_yes             <const> = variables.yes
80local v_no              <const> = variables.no
81local v_xml             <const> = variables.xml
82local v_hidden          <const> = variables.hidden
83
84local implement         = interfaces.implement
85
86local tasks             = nodes.tasks
87local fontchar          = fonts.hashes.characters
88local fontquads         = fonts.hashes.quads
89local languagenames     = languages.numbers
90
91local texgetcount       = tex.getcount
92
93local references        = structures.references
94local structurestags    = structures.tags
95local taglist           = structurestags.taglist
96local specifications    = structurestags.specifications
97local properties        = structurestags.properties
98local locatedtag        = structurestags.locatedtag
99
100structurestags.usewithcare = { }
101
102local starttiming       = statistics.starttiming
103local stoptiming        = statistics.stoptiming
104
105local characterdata     = characters.data
106local overloads         = fonts.mappings.overloads
107
108-- todo: more locals (and optimize)
109
110local exportversion     <const> = "0.36"
111local mathmlns          <const> = "http://www.w3.org/1998/Math/MathML"
112local contextns         <const> = "http://www.contextgarden.net/context/export" -- whatever suits
113local cssnamespaceurl   <const> = "@namespace context url('%namespace%') ;"
114local cssnamespace      <const> = "context|"
115----- cssnamespacenop   <const> = "/* no namespace */"
116
117local usecssnamespace   = false
118
119local nofcurrentcontent = 0 -- so we don't free (less garbage collection)
120local currentcontent    = { }
121local currentnesting    = nil
122local currentattribute  = nil
123local last              = nil
124local currentparagraph  = nil
125
126local noftextblocks     = 0
127
128----- hyphencode        = 0xAD
129local hyphen            = utfchar(0xAD) -- todo: also emdash etc
130local tagsplitter       = structurestags.patterns.splitter
131----- colonsplitter     = lpeg.splitat(":")
132----- dashsplitter      = lpeg.splitat("-")
133local threshold         = 65536
134local indexing          = false
135local keephyphens       = false
136local exportproperties  = false
137
138local finetuning        = { }
139
140local treestack         = { }
141local nesting           = { }
142local currentdepth      = 0
143
144local wrapups           = { }
145
146local tree              = { data = { }, fulltag == "root" } -- root
147local roottree          = tree
148local treehash          = { }
149local extras            = { }
150local checks            = { }
151local fixes             = { }
152local finalizers        = { }
153local nofbreaks         = 0
154local used              = { }
155local exporting         = false
156local restart           = false
157local specialspaces     = { [0x20] = " "  }               -- for conversion
158local somespace         = { [0x20] = true, [" "] = true } -- for testing
159local entities          = { ["&"] = "&amp;", [">"] = "&gt;", ["<"] = "&lt;" }
160local attribentities    = { ["&"] = "&amp;", [">"] = "&gt;", ["<"] = "&lt;", ['"'] = "quot;" }
161
162local p_entity          = lpeg.replacer(entities) -- was: entityremapper = utf.remapper(entities)
163local p_attribute       = lpeg.replacer(attribentities)
164local p_escaped         = lpeg.patterns.xml.escaped
165
166local f_tagid           = formatters["%s-%04i"]
167
168-- local alignmapping = {
169--     flushright = "right",
170--     middle     = "center",
171--     flushleft  = "left",
172-- }
173
174local defaultnature = "mixed" -- "inline"
175
176setmetatableindex(used, function(t,k)
177    if k then
178        local v = { }
179        t[k] = v
180        return v
181    end
182end)
183
184local f_entity    = formatters["&#x%X;"]
185local f_attribute = formatters[" %s=%q"]
186local f_property  = formatters[" %s%s=%q"]
187
188setmetatableindex(specialspaces, function(t,k)
189    local v = utfchar(k)
190    t[k] = v
191    entities[v] = f_entity(k)
192    somespace[k] = true
193    somespace[v] = true
194    return v
195end)
196
197-- We removed (mathml) namespaces but the code can be found in the archive and in the
198-- mkiv lua file. For the independent mathml see the older lmt file.
199
200local mathtags = {
201    msubsup       = "mathml",
202    msub          = "mathml",
203    msup          = "mathml",
204    mn            = "mathml",
205    mi            = "mathml",
206    ms            = "mathml",
207    mo            = "mathml",
208    mc            = "mathml",
209    mtext         = "mathml",
210    mrow          = "mathml",
211    mfrac         = "mathml",
212    mroot         = "mathml",
213    msqrt         = "mathml",
214    munderover    = "mathml",
215    munder        = "mathml",
216    mover         = "mathml",
217    merror        = "mathml",
218    math          = "mathml",
219    mrow          = "mathml",
220    mtable        = "mathml",
221    mtr           = "mathml",
222    mtd           = "mathml",
223    mfenced       = "mathml", -- no longer valid
224    maction       = "mathml",
225    mspace        = "mathml",
226    mmultiscripts = "mathml",
227}
228
229local function attribute(key,value)
230    if value and value ~= "" then
231        return f_attribute(key,lpegmatch(p_attribute,value))
232    else
233        return ""
234    end
235end
236
237local function setattribute(di,key,value,escaped)
238    if value and value ~= "" then
239        local a = di.attributes
240        if escaped then
241            value = lpegmatch(p_escaped,value)
242        end
243        if not a then
244            di.attributes = { [key] = value }
245        else
246            a[key] = value
247        end
248    end
249end
250
251local listdata = { } -- this has to be done otherwise: each element can just point back to ...
252
253function wrapups.hashlistdata()
254    local c = structures.lists.collected
255    for i=1,#c do
256        local ci = c[i]
257        local tag = ci.references.tag
258        if tag then
259            local m = ci.metadata
260            local t = m.kind .. ">" .. tag -- todo: use internal (see strc-lst.lua where it's set)
261            listdata[t] = ci
262        end
263    end
264end
265
266function structurestags.setattributehash(attr,key,value) -- public hash
267    local specification = taglist[attr]
268    if specification then
269        specification[key] = value
270    else
271        -- some kind of error
272    end
273end
274
275local usedstyles      = { }
276local usedimages      = { }
277local referencehash   = { } -- move ?
278local destinationhash = { } -- move ?
279
280structurestags.backend = {
281    setattribute    = setattribute,
282    extras          = extras,
283    checks          = checks,
284    fixes           = fixes,
285    listdata        = listdata,
286    finalizers      = finalizers,
287    usedstyles      = usedstyles,
288    usedimages      = usedimages,
289    referencehash   = referencehash,
290    destinationhash = destinationhash,
291}
292
293local namespacetemplate <const> = [[
294/* %what% for file %filename% */
295
296%cssnamespaceurl%
297]]
298
299do
300
301local documenttemplate <const> = [[
302:root {
303	--margin: 2.5em;
304}
305
306document,
307%namespace%div.document {
308    font-family : TextNormal ;
309    font-size   : %size% ;
310    max-width   : calc(var(--margin) + %width%) ;
311    text-align  : %align% ;
312    hyphens     : %hyphens% ;
313    zoom        : %zoom% ;
314}
315
316/* There is no real solution for boldening, 900 is not enough. */
317
318math {
319    font-family : MathNormal ;
320}
321
322math[data-lmtx-family="bold"] {
323    font-family : MathNormal ;
324	font-weight : 900 ;
325}]]
326
327local styletemplate <const> = [[
328%element%[detail="%detail%"],
329%namespace%div.%element%.%detail% {
330    display      : inline ;
331    font-style   : %style% ;
332    font-variant : %variant% ;
333    font-weight  : %weight% ;
334    font-family  : %family% ;
335    color        : %color% ;
336}]]
337
338    local numbertoallign = { [0] =
339        "justify", ["0"] = "justify", [variables.normal    ] = "justify",
340        "right",   ["1"] = "right",   [variables.flushright] = "right",
341        "center",  ["2"] = "center",  [variables.middle    ] = "center",
342        "left",    ["3"] = "left",    [variables.flushleft ] = "left",
343    }
344
345    function wrapups.allusedstyles(filename)
346        local result = { replacetemplate(namespacetemplate, {
347            what            = "styles",
348            filename        = filename,
349            namespace       = contextns,
350         -- cssnamespaceurl = usecssnamespace and cssnamespaceurl or cssnamespacenop,
351            cssnamespaceurl = cssnamespaceurl,
352        },false,true) }
353        --
354        local bodyfont = finetuning.bodyfont
355        local width    = finetuning.width
356        local hyphen   = finetuning.hyphen
357        local align    = finetuning.align
358        local zoom     = finetuning.zoom
359        local margin   = finetuning.margin
360        --
361     -- if not bodyfont then
362     --     bodyfont = tex.getdimen("globalbodyfontsize")
363     -- end
364     -- if not width then
365     --     width = tex.getdimen("textwidth")
366     -- end
367        if type(bodyfont) == "number" then
368            bodyfont = todimen(bodyfont)
369        else
370            bodyfont = "12pt"
371        end
372        if type(width) == "number" then
373            width = todimen(width) or "50em"
374        else
375            width = "50em"
376        end
377        if type(margin) == "number" then
378            margin = todimen(margin) or "2.5em"
379        else
380            margin = "2.5em"
381        end
382        if hyphen == v_yes then
383            hyphen = "manual"
384        else
385            hyphen = "auto"
386        end
387        if align then
388            align = numbertoallign[align]
389        end
390        if not align then
391            align = hyphen and "justify" or "inherited"
392        end
393        if not zoom then
394            zoom = tex.sp("12pt")/tex.sp(bodyfont)
395        end
396        --
397        result[#result+1] = replacetemplate(documenttemplate,{
398            size    = bodyfont,
399            width   = width,
400            align   = align,
401            hyphens = hyphen,
402            zoom    = formatters["%0.3f"](zoom),
403        })
404        --
405        local colorspecification = xml.css.colorspecification
406        local fontspecification  = xml.css.fontspecification
407        for element, details in sortedhash(usedstyles) do
408            for detail, data in sortedhash(details) do
409                local color = data.color
410                local style = data.style
411                local c = colorspecification(color)
412                local s = structures.tags.backend.fontfaced(style)
413                if not c then
414                    c = "inherit"
415                end
416                if s and s.family and s.family ~= "" then
417                    if not s.style   then s.style   = "normal" end
418                    if not s.variant then s.variant = "normal" end
419                    if not s.weight  then s.weight  = "normal" end
420                else
421                    s = fontspecification(style)
422                    if not s.style   then s.style   = "inherit" end -- or just normal
423                    if not s.variant then s.variant = "inherit" end -- or just normal
424                    if not s.weight  then s.weight  = "inherit" end -- or just normal
425                    if not s.family  then s.family  = "inherit" end -- or just normal
426                end
427                local detail = gsub(detail,"[^A-Za-z0-9]+","-")
428                result[#result+1] = replacetemplate(styletemplate,{
429                    namespace = usecssnamespace and cssnamespace or "",
430                    element   = element,
431                    detail    = detail,
432                    style     = s.style,
433                    variant   = s.variant,
434                    weight    = s.weight,
435                    family    = s.family,
436                    color     = c,
437                    display   = s.display and "block" or nil,
438                })
439            end
440        end
441        return concat(result,"\n\n")
442    end
443
444    function wrapups.allusedfonts(filename)
445        local list, used = structures.tags.backend.tofontface(finetuning.fontfaces)
446        local result = { replacetemplate(namespacetemplate, {
447            what            = "fonts",
448            filename        = filename,
449            namespace       = contextns,
450         -- cssnamespaceurl = usecssnamespace and cssnamespaceurl or cssnamespacenop,
451            cssnamespaceurl = cssnamespaceurl,
452        },false,true) }
453        result[#result+1] = list
454        result = concat(result,"\n\n")
455        for k, v in sortedhash(used) do
456            report_export("font face %s%s : %s",v[1],v[2],v[3])
457        end
458        return result
459    end
460
461end
462
463do
464
465local imagetemplate_1 <const> = [[
466[image="%id%"]%hover% {
467    display : block ;
468    content : url('%url%') ;
469    width   : %width% ;
470    height  : %height% ;
471}]]
472
473local imagetemplate_2 <const> = [[
474[image="%id%"]%hover% {
475    display    : inline ;
476    content    : url('%url%') ;
477    width      : %width% ;
478    height     : %height% ;
479    margin-top : -%drop% ;
480    top        : %drop% ;
481    position   : relative ;
482}]]
483
484    local f_svgname = formatters["%s.svg"]
485    local f_svgpage = formatters["%s-page-%s.svg"]
486    local collected = { }
487
488    local function usedname(name,page)
489        if filesuffix(name) == "pdf" then
490            -- temp hack .. we will have a remapper
491            if page and page > 1 then
492                name = f_svgpage(nameonly(name),page)
493            else
494                name = f_svgname(nameonly(name))
495            end
496        end
497        local scheme = url.hasscheme(name)
498        if not scheme or scheme == "file" then
499            -- or can we just use the name ?
500            return joinfile("../images",basename(url.filename(name)))
501        else
502            return name
503        end
504    end
505
506    function wrapups.allusedimages(filename)
507        local result = { replacetemplate(namespacetemplate, {
508            what            = "images",
509            filename        = filename,
510            namespace       = contextns,
511         -- cssnamespaceurl = usecssnamespace and cssnamespaceurl or "",
512            cssnamespaceurl = cssnamespaceurl,
513        },false,true) }
514        local hover = (finetuning.images) and finetuning.images.hover
515        for element, details in sortedhash(usedimages) do
516            for detail, data in sortedhash(details) do
517                local name  = data.name
518                local page  = tonumber(data.page) or 1
519                local drop  = data.drop
520                local spec = {
521                    element   = element,
522--                     id        = data.id,
523id        = filename .. "-" .. data.id,
524                    name      = name,
525                    page      = page,
526                    url       = usedname(name,page),
527                    width     = data.width,
528                    height    = data.height,
529                    drop      = drop,
530                    used      = data.used,
531                    namespace = usecssnamespace and cssnamespace or "",
532                    hover     = hover and data.category == "formula" and ":hover" or "",
533                }
534                result[#result+1] = replacetemplate(
535                    drop and imagetemplate_2 or imagetemplate_1,
536                    spec
537                )
538                collected[detail] = spec
539            end
540        end
541        return concat(result,"\n\n")
542    end
543
544    function wrapups.uniqueusedimages() -- todo: combine these two
545        return collected
546    end
547
548end
549
550--
551
552properties.vspace = { export = "break",     nature = "display" }
553----------------- = { export = "pagebreak", nature = "display" }
554
555local function makebreaklist(list)
556    nofbreaks = nofbreaks + 1
557    local t = { }
558    local l = list and list.taglist
559    if l then
560        for i=1,#list do
561            t[i] = l[i]
562        end
563    end
564    t[#t+1] = "break>" .. nofbreaks -- maybe no number or 0
565    return { taglist = t }
566end
567
568local breakattributes = {
569    type = "collapse"
570}
571
572local function makebreaknode(attributes) -- maybe no fulltag
573    nofbreaks = nofbreaks + 1
574    return {
575        tg         = "break",
576        fulltag    = "break>" .. nofbreaks,
577        n          = nofbreaks,
578        element    = "break",
579        nature     = "display",
580        attributes = attributes or nil,
581     -- data       = { }, -- not needed
582     -- attribute  = 0, -- not needed
583     -- parnumber  = 0,
584    }
585end
586
587do
588
589    local fields = { "title", "subtitle", "author", "keywords", "url", "version" }
590
591    local ignoredelements = false
592
593    local function checkdocument(root)
594        local data = root.data
595        if data then
596            for i=1,#data do
597                local di = data[i]
598                local tg = di.tg
599                if tg == "noexport" then
600                    local s = specifications[di.fulltag]
601                    local u = s and s.userdata
602                    if u then
603                        local comment = u.comment
604                        if comment then
605                            di.element = "comment"
606                            di.data = { { content = comment } }
607                            u.comment = nil
608                        else
609                            data[i] = false
610                        end
611                    else
612                        data[i] = false
613                    end
614                elseif di.content then
615                    -- okay
616                elseif tg == "ignore" then
617                    di.element = ""
618                    checkdocument(di)
619                elseif ignoredelements and ignoredelements[tg] then
620                    di.element = ""
621                    checkdocument(di)
622                else
623                    checkdocument(di) -- new, else no noexport handling
624                end
625            end
626        end
627    end
628
629    function extras.document(di,element,n,fulltag)
630        local language = languagenames[texgetcount("mainlanguagenumber")]
631        setattribute(di,"language",language)
632        setattribute(di,"xml:lang",language)
633        if not less_state then
634            setattribute(di,"file",tex.jobname)
635            setattribute(di,"date",os.fulltime())
636            setattribute(di,"context",environment.version)
637            setattribute(di,"version",exportversion)
638            local identity = interactions.general.getidentity()
639            for i=1,#fields do
640                local key   = fields[i]
641                local value = identity[key]
642                if value and value ~= "" then
643                    setattribute(di,key,value)
644                end
645            end
646        end
647        checkdocument(di)
648    end
649
650    implement {
651        name      = "ignoretagsinexport",
652        arguments = "string",
653        actions   = function(list)
654            for tag in string.gmatch(list,"[a-z]+") do
655                if ignoredelements then
656                    ignoredelements[tag] = true
657                else
658                    ignoredelements = { [tag] = true }
659                end
660            end
661        end,
662    }
663
664end
665
666-- flusher
667
668do
669
670    local f_detail                     = formatters[' detail="%s"']
671    local f_chain                      = formatters[' chain="%s"']
672    local f_index                      = formatters[' n="%s"']
673    local f_spacing                    = formatters['<c p="%s">%s</c>']
674
675    local f_empty_inline               = formatters["<%s/>"]
676    local f_empty_mixed                = formatters["%w<%s/>\n"]
677    local f_empty_display              = formatters["\n%w<%s/>\n"]
678    local f_empty_inline_attr          = formatters["<%s%s/>"]
679    local f_empty_mixed_attr           = formatters["%w<%s%s/>"]
680    local f_empty_display_attr         = formatters["\n%w<%s%s/>\n"]
681
682    local f_begin_inline               = formatters["<%s>"]
683    local f_begin_mixed                = formatters["%w<%s>"]
684    local f_begin_display              = formatters["\n%w<%s>\n"]
685    local f_begin_inline_attr          = formatters["<%s%s>"]
686    local f_begin_mixed_attr           = formatters["%w<%s%s>"]
687    local f_begin_display_attr         = formatters["\n%w<%s%s>\n"]
688
689    local f_end_inline                 = formatters["</%s>"]
690    local f_end_mixed                  = formatters["</%s>\n"]
691    local f_end_display                = formatters["%w</%s>\n"]
692
693    local f_begin_inline_comment       = formatters["<!-- %s --><%s>"]
694    local f_begin_mixed_comment        = formatters["%w<!-- %s --><%s>"]
695    local f_begin_display_comment      = formatters["\n%w<!-- %s -->\n%w<%s>\n"]
696    local f_begin_inline_attr_comment  = formatters["<!-- %s --><%s%s>"]
697    local f_begin_mixed_attr_comment   = formatters["%w<!-- %s --><%s%s>"]
698    local f_begin_display_attr_comment = formatters["\n%w<!-- %s -->\n%w<%s%s>\n"]
699
700    local f_comment_begin_inline       = formatters["<!-- begin %s -->"]
701    local f_comment_begin_mixed        = formatters["%w<!-- begin %s -->"]
702    local f_comment_begin_display      = formatters["\n%w<!-- begin %s -->\n"]
703
704    local f_comment_end_inline         = formatters["<!-- end %s -->"]
705    local f_comment_end_mixed          = formatters["<!-- end %s -->\n"]
706    local f_comment_end_display        = formatters["%w<!-- end %s -->\n"]
707
708    local f_metadata_begin             = formatters["\n%w<metadata>\n"]
709    local f_metadata                   = formatters["%w<metavariable name=%q>%s</metavariable>\n"]
710    local f_metadata_end               = formatters["%w</metadata>\n"]
711
712    local function attributes(a)
713        local r = { }
714        local n = 0
715        for k, v in next, a do
716            n = n + 1
717            r[n] = f_attribute(k,tostring(v)) -- tostring because of %q
718        end
719        sort(r)
720        return concat(r,"")
721    end
722
723    local function properties(a)
724        local r = { }
725        local n = 0
726        for k, v in next, a do
727            n = n + 1
728            r[n] = f_property(exportproperties,k,tostring(v)) -- tostring because of %q
729        end
730        sort(r)
731        return concat(r,"")
732    end
733
734    local depth  = 0
735    local inline = 0
736
737    local function emptytag(result,element,nature,di) -- currently only break but at some point
738        local a = di.attributes                       -- we might add detail etc
739        if a then -- happens seldom
740            if nature == "display" then
741                result[#result+1] = f_empty_display_attr(depth,element,attributes(a))
742            elseif nature == "mixed" then
743                result[#result+1] = f_empty_mixed_attr(depth,element,attributes(a))
744            else
745                result[#result+1] = f_empty_inline_attr(element,attributes(a))
746            end
747        else
748            if nature == "display" then
749                result[#result+1] = f_empty_display(depth,element)
750            elseif nature == "mixed" then
751                result[#result+1] = f_empty_mixed(depth,element)
752            else
753                result[#result+1] = f_empty_inline(element)
754            end
755        end
756    end
757
758 -- local function stripspaces(di)
759 --     local d = di.data
760 --     local n = #d
761 --     local m = 0
762 --     for i=1,n do
763 --         local di = d[i]
764 --         if di.tg then
765 --             m = m + 1
766 --             d[m] = di
767 --         end
768 --     end
769 --     for i=n,m+1,-1 do
770 --         d[i] = nil
771 --     end
772 -- end
773 --
774 -- -- simpler:
775
776    local function stripspaces(di)
777        local d = di.data
778        for i=1,#d do
779            local di = d[i]
780            if not di.tg then
781                di.content = ""
782            end
783        end
784    end
785
786    local function begintag(result,element,nature,di,skip)
787        local index         = di.n
788        local fulltag       = di.fulltag
789        local specification = specifications[fulltag] or { } -- we can have a dummy
790        local comment       = di.comment
791        local detail        = specification.detail
792        if skip == "comment" then
793            if show_comment then
794                if nature == "inline" or inline > 0 then
795                    result[#result+1] = f_comment_begin_inline(element)
796                    inline = inline + 1
797                elseif nature == "mixed" then
798                    result[#result+1] = f_comment_begin_mixed(depth,element)
799                    depth = depth + 1
800                    inline = 1
801                else
802                    result[#result+1] = f_comment_begin_display(depth,element)
803                    depth = depth + 1
804                end
805            end
806        elseif skip then
807            -- ignore
808        else
809
810            local n = 0
811            local r = { } -- delay this
812            if detail then
813                detail = gsub(detail,"[^A-Za-z0-9]+","-")
814                specification.detail = detail -- we use it later in for the div
815                n = n + 1
816                r[n] = f_detail(detail)
817            end
818            local parents = specification.parents
819            if parents then
820                parents = gsub(parents,"[^A-Za-z0-9 ]+","-")
821                specification.parents = parents -- we use it later in for the div
822                n = n + 1
823                r[n] = f_chain(parents)
824            end
825            if indexing and index then
826                n = n + 1
827                r[n] = f_index(index)
828            end
829            --
830            local extra = extras[element]
831            if extra then
832                extra(di,element,index,fulltag)
833            end
834            --
835            if di.record then
836                stripspaces(di)
837            end
838            --
839            if exportproperties then
840                local p = specification.userdata
841                if not p then
842                    -- skip
843                elseif exportproperties == v_yes then
844                    n = n + 1
845                    r[n] = attributes(p)
846                else
847                    n = n + 1
848                    r[n] = properties(p)
849                end
850            end
851            local a = di.attributes
852            if a then
853                if trace_spacing then
854                    a.p = di.parnumber or 0
855                end
856                n = n + 1
857                r[n] = attributes(a)
858            elseif trace_spacing then
859                n = n + 1
860                r[n] = attributes { p = di.parnumber or 0 }
861            end
862            if n == 0 then
863                if nature == "inline" or inline > 0 then
864                    if show_comment and comment then
865                        result[#result+1] = f_begin_inline_comment(comment,element)
866                    else
867                        result[#result+1] = f_begin_inline(element)
868                    end
869                    inline = inline + 1
870                elseif nature == "mixed" then
871                    if show_comment and comment then
872                        result[#result+1] = f_begin_mixed_comment(depth,comment,element)
873                    else
874                        result[#result+1] = f_begin_mixed(depth,element)
875                    end
876                    depth = depth + 1
877                    inline = 1
878                else
879                    if show_comment and comment then
880                        result[#result+1] = f_begin_display_comment(depth,comment,depth,element)
881                    else
882                        result[#result+1] = f_begin_display(depth,element)
883                    end
884                    depth = depth + 1
885                end
886            else
887                r = concat(r,"",1,n)
888                if nature == "inline" or inline > 0 then
889                    if show_comment and comment then
890                        result[#result+1] = f_begin_inline_attr_comment(comment,element,r)
891                    else
892                        result[#result+1] = f_begin_inline_attr(element,r)
893                    end
894                    inline = inline + 1
895                elseif nature == "mixed" then
896                    if show_comment and comment then
897                        result[#result+1] = f_begin_mixed_attr_comment(depth,comment,element,r)
898                    else
899                        result[#result+1] = f_begin_mixed_attr(depth,element,r)
900                    end
901                    depth = depth + 1
902                    inline = 1
903                else
904                    if show_comment and comment then
905                        result[#result+1] = f_begin_display_attr_comment(depth,comment,depth,element,r)
906                    else
907                        result[#result+1] = f_begin_display_attr(depth,element,r)
908                    end
909                    depth = depth + 1
910                end
911            end
912        end
913        used[element][detail or ""] = { nature, specification.parents }  -- for template css
914        -- also in last else ?
915        local metadata = specification.metadata
916        if metadata and next(metadata) then
917            result[#result+1] = f_metadata_begin(depth)
918            for k, v in sortedhash(metadata) do
919                if v ~= "" then
920                    result[#result+1] = f_metadata(depth+1,k,lpegmatch(p_entity,v))
921                end
922            end
923            result[#result+1] = f_metadata_end(depth)
924        end
925    end
926
927    local function endtag(result,element,nature,di,skip)
928        if skip == "comment" then
929            if show_comment then
930                if nature == "display" and (inline == 0 or inline == 1) then
931                    depth = depth - 1
932                    result[#result+1] = f_comment_end_display(depth,element)
933                    inline = 0
934                elseif nature == "mixed" and (inline == 0 or inline == 1) then
935                    depth = depth - 1
936                    result[#result+1] = f_comment_end_mixed(element)
937                    inline = 0
938                else
939                    inline = inline - 1
940                    result[#result+1] = f_comment_end_inline(element)
941                end
942            end
943        elseif skip then
944            -- ignore
945        else
946            if nature == "display" and (inline == 0 or inline == 1) then
947                depth = depth - 1
948                result[#result+1] = f_end_display(depth,element)
949                inline = 0
950            elseif nature == "mixed" and (inline == 0 or inline == 1) then
951                depth = depth - 1
952                result[#result+1] = f_end_mixed(element)
953                inline = 0
954            else
955                inline = inline - 1
956                result[#result+1] = f_end_inline(element)
957            end
958        end
959    end
960
961    local function wrapupmath(di)
962        -- We arrive here twice which is a bit waste of time but for now
963        -- we just do it. The first time we have no blob set anyway.
964        local a = di.attributes
965        local order = a and (a["data-lmtx-blob"] or a["blob"])
966        if order then
967            local mth = mathematics.getmathblob("xml",order)
968            local txt = mathematics.gettextblob("xml","en",order)
969            if mth then
970                mth = match(mth,"<math[^>]*>(.*)</math>")
971            end
972            if txt and txt ~= "" then
973             -- local a = di.attributes
974                if a then
975                    a["data-lmtx-meaning"]  = txt
976                else
977                    di.attributes = {
978                        ["data-lmtx-meaning"] = txt,
979                    }
980                end
981            end
982            return mth
983        end
984    end
985
986    local function flushtree(result,data,nature)
987        local nofdata = #data
988        for i=1,nofdata do
989            local di = data[i]
990            if not di then -- hm, di can be string
991                -- whatever
992            else
993                local content = di.content
994             -- also optimize for content == "" : trace that first
995                if content then
996                    -- already has breaks
997                    local content = lpegmatch(p_entity,content)
998                    if i == nofdata and sub(content,-1) == "\n" then -- move check
999                        -- can be an end of line in par but can also be the last line
1000                        if trace_spacing then
1001                            result[#result+1] = f_spacing(di.parnumber or 0,sub(content,1,-2))
1002                        else
1003                            result[#result+1] = sub(content,1,-2)
1004                        end
1005                        result[#result+1] = " "
1006                    else
1007                        if trace_spacing then
1008                            result[#result+1] = f_spacing(di.parnumber or 0,content)
1009                        else
1010                            result[#result+1] = content
1011                        end
1012                    end
1013                elseif not di.collapsed then -- ignore collapsed data (is appended, reconstructed par)
1014                    local element = di.element
1015                    if not element then
1016                        -- skip
1017                    elseif element == "break" then -- or element == "pagebreak" -- todo: use empty flag
1018                        emptytag(result,element,nature,di)
1019                    elseif element == "mspace" then -- todo: use empty flag
1020-- can this still happen
1021                        emptytag(result,element,nature,di)
1022                    elseif element == "mprescripts" then -- todo: use empty flag
1023-- can this still happen
1024                        emptytag(result,element,nature,di) -- -why has nature to be set
1025                    elseif element == "" or di.skip == "ignore" then --
1026                        -- skip
1027                    else
1028                        local mth = element == "math" and wrapupmath(di)
1029                        if di.before then
1030                            flushtree(result,di.before,nature)
1031                        end
1032                        local natu = di.nature
1033                        local skip = di.skip
1034                        if di.breaknode then
1035                            -- this never happens as we don seem to set this field
1036                            emptytag(result,"break","display",di)
1037                        end
1038                        begintag(result,element,natu,di,skip)
1039                        if mth and mth ~= "" then
1040                            result[#result+1] = mth -- the stripped one
1041                        else
1042                            flushtree(result,di.data,natu)
1043                        end
1044                        endtag(result,element,natu,di,skip)
1045                        if di.after then
1046                            flushtree(result,di.after,nature)
1047                        end
1048                    end
1049                else
1050--                     local element = di.element
1051--                     if element == "mspace" then -- todo: use empty flag
1052--                         emptytag(result,element,nature,di)
1053--                     end
1054                end
1055            end
1056        end
1057    end
1058
1059    local function breaktree(tree,parent,parentelement) -- also removes double breaks
1060        local data = tree.data
1061        if data then
1062            local nofdata = #data
1063            local prevelement
1064            local prevnature
1065            local prevparnumber
1066            local newdata = { }
1067            local nofnewdata = 0
1068            for i=1,nofdata do
1069                local di = data[i]
1070                if not di then
1071                    -- skip
1072                elseif di.skip == "ignore" then
1073                    -- skip (new)
1074                elseif di.tg == "ignore" then
1075                    -- skip (new)
1076                elseif di.content then
1077                    if di.samepar then
1078                        prevparnumber = false
1079                    else
1080                        local parnumber = di.parnumber
1081                        if prevnature == "inline" and prevparnumber and prevparnumber ~= parnumber then
1082                            nofnewdata = nofnewdata + 1
1083                            if trace_spacing then
1084                                newdata[nofnewdata] = makebreaknode { type = "a", p = prevparnumber, n = parnumber }
1085                            else
1086                                newdata[nofnewdata] = makebreaknode()
1087                            end
1088                        end
1089                        prevelement = nil
1090                        prevparnumber = parnumber
1091                    end
1092                    prevnature = "inline"
1093                    nofnewdata = nofnewdata + 1
1094                    newdata[nofnewdata] = di
1095                elseif not di.collapsed then
1096                    local element = di.element
1097                    if element == "break" then -- or element == "pagebreak"
1098                        if prevelement == "break" then
1099                            di.element = ""
1100                        end
1101                        prevelement = element
1102                        prevnature = "display"
1103                        nofnewdata = nofnewdata + 1
1104                        newdata[nofnewdata] = di
1105                    elseif element == "" or di.skip == "ignore" then
1106                        -- skip
1107                    else
1108                        if di.samepar then
1109                            prevnature    = "inline"
1110                            prevparnumber = false
1111                        else
1112                            local nature = di.nature
1113                            local parnumber = di.parnumber
1114                            if prevnature == "inline" and nature == "inline" and prevparnumber and prevparnumber ~= parnumber then
1115                                nofnewdata = nofnewdata + 1
1116                                if trace_spacing then
1117                                    newdata[nofnewdata] = makebreaknode { type = "b", p = prevparnumber, n = parnumber }
1118                                else
1119                                    newdata[nofnewdata] = makebreaknode()
1120                                end
1121                            end
1122                            prevnature = nature
1123                            prevparnumber = parnumber
1124                        end
1125                        prevelement = element
1126                        breaktree(di,tree,element)
1127                        nofnewdata = nofnewdata + 1
1128                        newdata[nofnewdata] = di
1129                    end
1130                else
1131                    if di.samepar then
1132                        prevnature    = "inline"
1133                        prevparnumber = false
1134                    else
1135                        local nature = di.nature
1136                        local parnumber = di.parnumber
1137                        if prevnature == "inline" and nature == "inline" and prevparnumber and prevparnumber ~= parnumber then
1138                            nofnewdata = nofnewdata + 1
1139                            if trace_spacing then
1140                                newdata[nofnewdata] = makebreaknode { type = "c", p = prevparnumber, n = parnumber }
1141                            else
1142                                newdata[nofnewdata] = makebreaknode()
1143                            end
1144                        end
1145                        prevnature = nature
1146                        prevparnumber = parnumber
1147                    end
1148                    nofnewdata = nofnewdata + 1
1149                    newdata[nofnewdata] = di
1150                end
1151            end
1152            tree.data = newdata
1153        end
1154    end
1155
1156    -- also tabulaterow reconstruction .. maybe better as a checker
1157    -- i.e cell attribute
1158
1159    local function showtree(data,when,where)
1160        if data then
1161            for i=1,#data do
1162                local d = data[i]
1163                if type(d) == "table" and d.element then
1164                    print(when,where,i,d.element,d.parnumber or 0)
1165                end
1166            end
1167        end
1168    end
1169
1170    local function collapsetree(tree)
1171     -- showtree(data,"before","collapse")
1172     -- for tag, trees in sortedhash(treehash) do
1173        for tag, trees in next, treehash do
1174            local d = trees[1].data
1175            if d then
1176                local nd = #d
1177                if nd > 0 then
1178                    for i=2,#trees do
1179                        local currenttree = trees[i]
1180                        local currentdata = currenttree.data
1181                        local currentpar  = currenttree.parnumber
1182                        local previouspar = trees[i-1].parnumber
1183                        currenttree.collapsed = true
1184                        -- is the next ok?
1185                        if previouspar == 0 or not (di and di.content) then
1186                            previouspar = nil -- no need anyway so no further testing needed
1187                        end
1188                        for j=1,#currentdata do
1189                            local cd = currentdata[j]
1190                            if not cd or cd == "" then
1191                                -- skip
1192                            elseif cd.skip == "ignore" then
1193                                -- skip
1194                            elseif cd.content then
1195                                if not currentpar then
1196                                    -- add space ?
1197                                elseif not previouspar then
1198                                    -- add space ?
1199                                elseif currentpar ~= previouspar then
1200                                    nd = nd + 1
1201                                    if trace_spacing then
1202                                        d[nd] = makebreaknode { type = "d", p = previouspar, n = currentpar }
1203                                    else
1204                                        d[nd] = makebreaknode()
1205                                    end
1206                                end
1207                                previouspar = currentpar
1208                                nd = nd + 1
1209                                d[nd] = cd
1210                            else
1211                                nd = nd + 1
1212                                d[nd] = cd
1213                            end
1214                            currentdata[j] = false
1215                        end
1216                    end
1217                end
1218            end
1219        end
1220     -- showtree(data,"after","collapse")
1221    end
1222
1223    local function finalizetree(tree)
1224     -- showtree(data,"before","finalize")
1225        for _, finalizer in next, finalizers do
1226            finalizer(tree)
1227        end
1228     -- showtree(data,"after","finalize")
1229    end
1230
1231    local function indextree(tree)
1232        local data = tree.data
1233        if data then
1234         -- showtree(data,"before","index")
1235            local n, new = 0, { }
1236            for i=1,#data do
1237                local d = data[i]
1238                if not d then
1239                    -- skip
1240                elseif d.content then
1241                    n = n + 1
1242                    new[n] = d
1243                elseif not d.collapsed then
1244                    n = n + 1
1245                    d.__i__ = n
1246                    d.__p__ = tree
1247                    indextree(d)
1248                    new[n] = d
1249                end
1250            end
1251            tree.data = new
1252         -- showtree(new,"after","index")
1253        end
1254    end
1255
1256    local function checktree(tree)
1257        local data = tree.data
1258        if data then
1259         -- showtree(data,"before","check")
1260            for i=1,#data do
1261                local d = data[i]
1262                if type(d) == "table" then
1263                    local tg = d.tg
1264                    if tg then
1265                        local check = checks[tg]
1266                        if check then
1267                            check(d,data,i)
1268                        end
1269                    end
1270                    checktree(d) -- so parts can pass twice
1271                end
1272            end
1273         -- showtree(data,"after","check")
1274        end
1275    end
1276
1277    local function fixtree(tree)
1278        local data = tree.data
1279        if data then
1280         -- showtree(data,"before","fix")
1281            for i=1,#data do
1282                local d = data[i]
1283                if type(d) == "table" then
1284                    local tg = d.tg
1285                    if tg then
1286                        local fix = fixes[tg]
1287                        if fix then
1288                            fix(d,data,i)
1289                        end
1290                    end
1291                    fixtree(d) -- so parts can pass twice
1292                end
1293            end
1294         -- showtree(data,"after","fix")
1295        end
1296    end
1297
1298    wrapups.flushtree    = flushtree
1299    wrapups.breaktree    = breaktree
1300    wrapups.collapsetree = collapsetree
1301    wrapups.finalizetree = finalizetree
1302    wrapups.indextree    = indextree
1303    wrapups.checktree    = checktree
1304    wrapups.fixtree      = fixtree
1305
1306end
1307
1308-- collector code
1309
1310local function push(fulltag,depth)
1311    local tg, n, detail, element, nature, record
1312    local specification = specifications[fulltag]
1313    if specification then
1314        tg     = specification.tagname
1315        n      = specification.tagindex
1316        detail = specification.detail
1317    elseif not fulltag then
1318        report_export("%w<%s trigger=%q n=%q paragraph=%q index=%q detail=%q>",
1319            currentdepth-1,tg,n,currentattribute or 0,currentparagraph or 0,"error",detail)
1320        return
1321    else
1322        -- a break (more efficient if we don't store those in specifications)
1323        tg, n = lpegmatch(tagsplitter,fulltag)
1324        n = tonumber(n) -- to tonumber in tagsplitter
1325    end
1326    local p = properties[tg]
1327    if p then
1328        element = p.export or tg
1329        nature  = p.nature or "inline" -- defaultnature
1330        record  = p.record
1331    end
1332    local treedata = tree.data
1333    if not treedata then
1334        report_export("%w<%s trigger=%q n=%q paragraph=%q index=%q detail=%q>",
1335            currentdepth-1,tg,n,currentattribute or 0,currentparagraph or 0,"error",detail)
1336        return
1337    end
1338    local t = { -- maybe we can use the tag table
1339        tg        = tg,
1340        fulltag   = fulltag,
1341        detail    = detail,
1342        n         = n, -- already a number
1343        element   = element,
1344        nature    = nature,
1345        data      = { },
1346        attribute = currentattribute,
1347        parnumber = currentparagraph,
1348        record    = record, -- we can consider storing properties
1349    }
1350    treedata[#treedata+1] = t
1351    currentdepth = currentdepth + 1
1352    nesting[currentdepth] = fulltag
1353    treestack[currentdepth] = tree
1354    if trace_export then
1355        if detail and detail ~= "" then
1356            report_export("%w<%s trigger=%q n=%q paragraph=%q index=%q detail=%q>",
1357                currentdepth-1,tg,n,currentattribute or 0,currentparagraph or 0,#treedata,detail)
1358        else
1359            report_export("%w<%s trigger=%q n=%q paragraph=%q index=%q>",
1360                currentdepth-1,tg,n,currentattribute or 0,currentparagraph or 0,#treedata)
1361        end
1362    end
1363    tree = t
1364    if tg == "break" then
1365        -- no need for this
1366    else
1367        local h = treehash[fulltag]
1368        if h then
1369            h[#h+1] = t
1370        else
1371            treehash[fulltag] = { t }
1372        end
1373    end
1374end
1375
1376local function pop()
1377    if currentdepth > 0 then
1378        local top = nesting[currentdepth]
1379        tree = treestack[currentdepth]
1380        currentdepth = currentdepth - 1
1381        if trace_export then
1382            if top then
1383                report_export("%w</%s>",currentdepth,match(top,"[^>]+"))
1384            else
1385                report_export("</BAD>")
1386            end
1387        end
1388    else
1389        report_export("%w<!-- too many pops -->",currentdepth)
1390    end
1391end
1392
1393local function continueexport()
1394    if nofcurrentcontent > 0 then
1395        if trace_export then
1396            report_export("%w<!-- injecting pagebreak space -->",currentdepth)
1397        end
1398        nofcurrentcontent = nofcurrentcontent + 1
1399        currentcontent[nofcurrentcontent] = " " -- pagebreak
1400    end
1401end
1402
1403local function pushentry(current)
1404    if not current then
1405        -- bad news
1406        return
1407    end
1408    current = current.taglist
1409    if not current then
1410        -- even worse news
1411        return
1412    end
1413    if restart then
1414        continueexport()
1415        restart = false
1416    end
1417    local newdepth = #current
1418    local olddepth = currentdepth
1419    if trace_export then
1420        report_export("%w<!-- moving from depth %s to %s (%s) -->",currentdepth,olddepth,newdepth,current[newdepth])
1421    end
1422    if olddepth <= 0 then
1423        for i=1,newdepth do
1424            push(current[i],i)
1425        end
1426    else
1427        local difference
1428        if olddepth < newdepth then
1429            for i=1,olddepth do
1430                if current[i] ~= nesting[i] then
1431                    difference = i
1432                    break
1433                end
1434            end
1435        else
1436            for i=1,newdepth do
1437                if current[i] ~= nesting[i] then
1438                    difference = i
1439                    break
1440                end
1441            end
1442        end
1443        if difference then
1444            for i=olddepth,difference,-1 do
1445                pop()
1446            end
1447            for i=difference,newdepth do
1448                push(current[i],i)
1449            end
1450        elseif newdepth > olddepth then
1451            for i=olddepth+1,newdepth do
1452                push(current[i],i)
1453            end
1454        elseif newdepth < olddepth then
1455            for i=olddepth,newdepth,-1 do
1456                pop()
1457            end
1458        elseif trace_export then
1459            report_export("%w<!-- staying at depth %s (%s) -->",currentdepth,newdepth,nesting[newdepth] or "?")
1460        end
1461    end
1462    return olddepth, newdepth
1463end
1464
1465local function pushcontent(oldparagraph,newparagraph)
1466    if nofcurrentcontent > 0 then
1467        if oldparagraph then
1468            if currentcontent[nofcurrentcontent] == "\n" then
1469                if trace_export then
1470                    report_export("%w<!-- removing newline -->",currentdepth)
1471                end
1472                nofcurrentcontent = nofcurrentcontent - 1
1473            end
1474        end
1475        local content = concat(currentcontent,"",1,nofcurrentcontent)
1476-- print(content,currentattribute)
1477        if content == "" then
1478            -- omit; when oldparagraph we could push, remove spaces, pop
1479        elseif somespace[content] and oldparagraph then
1480            -- omit; when oldparagraph we could push, remove spaces, pop
1481-- elseif not tree or not tree.data then
1482        else
1483            local olddepth, newdepth
1484            local list = taglist[currentattribute]
1485            if list then
1486                olddepth, newdepth = pushentry(list)
1487            end
1488            if tree then
1489                local td = tree.data
1490                local nd = #td
1491                td[nd+1] = { parnumber = oldparagraph or currentparagraph, content = content }
1492                if trace_export then
1493                    report_export("%w<!-- start content with length %s -->",currentdepth,utflen(content))
1494                    report_export("%w%s",currentdepth,(gsub(content,"\n","\\n")))
1495                    report_export("%w<!-- stop content -->",currentdepth)
1496                end
1497                if olddepth then
1498                    for i=newdepth-1,olddepth,-1 do
1499                        pop()
1500                    end
1501                end
1502            end
1503        end
1504        nofcurrentcontent = 0
1505    end
1506    if oldparagraph then
1507        pushentry(makebreaklist(currentnesting))
1508        if trace_export then
1509            report_export("%w<!-- break added between paragraph %a and %a -->",currentdepth,oldparagraph,newparagraph)
1510        end
1511    end
1512end
1513
1514local function finishexport()
1515    if trace_export then
1516        report_export("%w<!-- start finalizing -->",currentdepth)
1517    end
1518    if nofcurrentcontent > 0 then
1519        if somespace[currentcontent[nofcurrentcontent]] then
1520            if trace_export then
1521                report_export("%w<!-- removing space -->",currentdepth)
1522            end
1523            nofcurrentcontent = nofcurrentcontent - 1
1524        end
1525        pushcontent()
1526    end
1527    for i=currentdepth,1,-1 do
1528        pop()
1529    end
1530    currentcontent = { } -- we're nice and do a cleanup
1531    if trace_export then
1532        report_export("%w<!-- stop finalizing -->",currentdepth)
1533    end
1534end
1535
1536-- inserts ?
1537
1538local collectresults  do -- too many locals otherwise
1539
1540    local nodecodes          = nodes.nodecodes
1541    local gluecodes          = nodes.gluecodes
1542    local listcodes          = nodes.listcodes
1543    local whatsitcodes       = nodes.whatsitcodes
1544
1545    local subtypes           = nodes.subtypes
1546
1547    local userkern_code      <const> = nodes.kerncodes.userkern
1548
1549    local hlist_code         <const> = nodecodes.hlist
1550    local vlist_code         <const> = nodecodes.vlist
1551    local glyph_code         <const> = nodecodes.glyph
1552    local glue_code          <const> = nodecodes.glue
1553    local kern_code          <const> = nodecodes.kern
1554    local disc_code          <const> = nodecodes.disc
1555    local whatsit_code       <const> = nodecodes.whatsit
1556    local par_code           <const> = nodecodes.par
1557
1558    local userskip_code      <const> = gluecodes.userskip
1559    local rightskip_code     <const> = gluecodes.rightskip
1560    local parfillskip_code   <const> = gluecodes.parfillskip
1561    local spaceskip_code     <const> = gluecodes.spaceskip
1562    local xspaceskip_code    <const> = gluecodes.xspaceskip
1563    local intermathskip_code <const> = gluecodes.intermathskip
1564
1565    local linelist_code      <const> = listcodes.line
1566
1567    local userdefinedwhatsit_code  <const> = whatsitcodes.userdefined
1568
1569    local privateattribute = attributes.private
1570
1571    local a_image          <const> = privateattribute('image')
1572    local a_reference      <const> = privateattribute('reference')
1573    local a_destination    <const> = privateattribute('destination')
1574    local a_characters     <const> = privateattribute('characters')
1575    local a_exportstatus   <const> = privateattribute('exportstatus')
1576    local a_tagged         <const> = privateattribute('tagged')
1577    local a_taggedpar      <const> = privateattribute("taggedpar")
1578    local a_textblock      <const> = privateattribute("textblock")
1579
1580    local inline_mark      <const> = nodes.pool.userids["margins.inline"]
1581
1582    local nuts             = nodes.nuts
1583
1584    local getnext          = nuts.getnext
1585    local getdisc          = nuts.getdisc
1586    local getlist          = nuts.getlist
1587    local getid            = nuts.getid
1588    local getattrs         = nuts.getattrs
1589    local getattr          = nuts.getattr
1590    local setattr          = nuts.setattr -- maybe use properties
1591    local isglyph          = nuts.isglyph
1592    local getkern          = nuts.getkern
1593    local getwidth         = nuts.getwidth
1594    local getclass         = nuts.getclass
1595
1596    local startofpar       = nuts.startofpar
1597
1598    local nexthlist        = nuts.traversers.hlist
1599    local nextnode         = nuts.traversers.node
1600
1601    local function addtomaybe(maybewrong,c,paragraph,ap,case)
1602        if trace_export then
1603            report_export("%w<!-- possible paragraph (%i,%i) mixup at %C case %i -->",currentdepth,paragraph or 0,ap or 0,c,case)
1604        else
1605            local s = formatters["%C"](c)
1606            if maybewrong then
1607                maybewrong[#maybewrong+1] = s
1608            else
1609                maybewrong = { s }
1610            end
1611            return maybewrong
1612        end
1613    end
1614
1615    local function showmaybe(maybewrong)
1616        if not trace_export then
1617--             report_export("fuzzy paragraph: % t",maybewrong)
1618        end
1619    end
1620
1621    local function showdetail(n,id,subtype)
1622        local a = getattr(n,a_tagged)
1623        local t = taglist[a]
1624        local c = nodecodes[id]
1625        local s = subtypes[id][subtype]
1626        if a and t then
1627            report_export("node %a, subtype %a, tag %a, element %a, tree '% t'",c,s,a,t.tagname,t.taglist)
1628        else
1629            report_export("node %a, subtype %a, untagged",c,s)
1630        end
1631    end
1632
1633    local function collectresults(head,list,pat,pap) -- is last used (we also have currentattribute)
1634        local p
1635        local paragraph
1636        local maybewrong
1637        local pid
1638        for n, id, subtype in nextnode, head do
1639            if trace_details then
1640                showdetail(n,id,subtype)
1641            end
1642            if id == glyph_code then
1643                local c, f = isglyph(n)
1644                local at, ap = getattrs(n,a_tagged,a_taggedpar)
1645                if not at then
1646                    at = pat
1647                end
1648                if not at then
1649                 -- we need to tag the pagebody stuff as being valid skippable
1650                 --
1651                 -- report_export("skipping character: %C (no attribute)",n.char)
1652                elseif at == 0 then
1653                    -- skip (needs checking)
1654                elseif at < 1 then
1655                    -- skip (needs checking)
1656                else
1657                    if last ~= at then
1658                        local tl = taglist[at]
1659                        if not ap then
1660                            ap = pap
1661                        end
1662-- if paragraph and (not ap or ap < paragraph) then
1663--     maybewrong = addtomaybe(maybewrong,c,paragraph,ap,1)
1664-- end
1665                        tl.class = getclass(n)
1666                        pushcontent()
1667                        currentnesting   = tl
1668                        currentparagraph = ap
1669                        currentattribute = at
1670                        last = at
1671                        pushentry(currentnesting)
1672                        if trace_export then
1673                            report_export("%w<!-- processing glyph %C tagged %a -->",currentdepth,c,at)
1674                        end
1675                        -- We need to intercept this here; maybe I will also move this
1676                        -- to a regular setter at the tex end.
1677                        local r, d = getattrs(n,a_reference,a_destination)
1678                        if r then
1679                            local t = tl.taglist
1680                            referencehash[t[#t]] = r -- fulltag
1681                        end
1682                        if d then
1683                            local t = tl.taglist
1684                            destinationhash[t[#t]] = d -- fulltag
1685                        end
1686                        --
1687                    elseif last then
1688                        -- we can consider tagging the pars (lines) in the parbuilder but then we loose some
1689                        -- information unless we inject a special node (but even then we can run into nesting
1690                        -- issues)
1691                        if not ap then
1692                            ap = pap
1693                        end
1694                        if ap ~= currentparagraph then
1695                            pushcontent(currentparagraph,ap)
1696                            pushentry(currentnesting)
1697                            currentattribute = last
1698                            currentparagraph = ap
1699                        end
1700-- if paragraph and (not ap or ap < paragraph) then
1701--     maybewrong = addtomaybe(maybewrong,c,paragraph,ap,2)
1702-- end
1703                        if trace_export then
1704                            report_export("%w<!-- processing glyph %C tagged %a -->",currentdepth,c,last)
1705                        end
1706                    else
1707                        if trace_export then
1708                            report_export("%w<!-- processing glyph %C tagged %a -->",currentdepth,c,at)
1709                        end
1710                    end
1711                    local s = getattr(n,a_exportstatus)
1712                    if s then
1713                        c = s
1714                    end
1715                    if c == 0 or c == 0xFFFD then
1716                        if trace_export then
1717                            report_export("%w<!-- skipping glyph %U -->",currentdepth,c)
1718                        end
1719                    elseif c == 0x20 then
1720                        local a = getattr(n,a_characters)
1721                        nofcurrentcontent = nofcurrentcontent + 1
1722                        if a then
1723                            if trace_export then
1724                                report_export("%w<!-- turning last space into special space %U -->",currentdepth,a)
1725                            end
1726                            currentcontent[nofcurrentcontent] = specialspaces[a] -- special space
1727                        else
1728                            currentcontent[nofcurrentcontent] = " "
1729                        end
1730                    else
1731                        local fc = fontchar[f]
1732                        if fc then
1733                            fc = fc and fc[c]
1734                            if fc then
1735                                local u = fc.unicode
1736                                if u == 0 or u == 0xFFFD then
1737                                    -- ignore (can make disappear)
1738                                elseif not u then
1739                                    nofcurrentcontent = nofcurrentcontent + 1
1740                                    currentcontent[nofcurrentcontent] = utfchar(c)
1741                                elseif type(u) == "table" then
1742                                    for i=1,#u do
1743                                        nofcurrentcontent = nofcurrentcontent + 1
1744                                        currentcontent[nofcurrentcontent] = utfchar(u[i])
1745                                    end
1746                                else
1747                                    nofcurrentcontent = nofcurrentcontent + 1
1748                                    currentcontent[nofcurrentcontent] = utfchar(u)
1749                                end
1750                            elseif c > 0 then
1751                                nofcurrentcontent = nofcurrentcontent + 1
1752                                currentcontent[nofcurrentcontent] = utfchar(c)
1753                            else
1754                                -- we can have -1 as side effect of an explicit hyphen (unless we expand)
1755                            end
1756                        elseif c > 0 then
1757                            nofcurrentcontent = nofcurrentcontent + 1
1758                            currentcontent[nofcurrentcontent] = utfchar(c)
1759                        else
1760                            -- we can have -1 as side effect of an explicit hyphen (unless we expand)
1761                        end
1762                    end
1763                end
1764            elseif id == glue_code then
1765                -- we need to distinguish between hskips and vskips
1766                local ca, a = getattrs(n,a_characters,a_tagged)
1767                if ca == 0 then
1768                    -- skip this one ... already converted special character (node-acc)
1769                elseif ca then
1770                    if not a then
1771                        a = pat
1772                    end
1773                    if a then
1774                        local c = specialspaces[ca]
1775                        if last ~= a then
1776                            local tl = taglist[a]
1777                            if trace_export then
1778                                report_export("%w<!-- processing space glyph %U tagged %a case 1 -->",currentdepth,ca,a)
1779                            end
1780                            pushcontent()
1781                            currentnesting = tl
1782                            currentparagraph = getattr(n,a_taggedpar) or pap
1783                            currentattribute = a
1784                            last = a
1785                            pushentry(currentnesting)
1786                            -- no reference check (see above)
1787                        elseif last then
1788                            local ap = getattr(n,a_taggedpar) or pap
1789                            if ap ~= currentparagraph then
1790                                pushcontent(currentparagraph,ap)
1791                                pushentry(currentnesting)
1792                                currentattribute = last
1793                                currentparagraph = ap
1794                            end
1795                            if trace_export then
1796                                report_export("%w<!-- processing space glyph %U tagged %a case 2 -->",currentdepth,ca,last)
1797                            end
1798                        end
1799                        -- if somespace[currentcontent[nofcurrentcontent]] then
1800                        --     if trace_export then
1801                        --         report_export("%w<!-- removing space -->",currentdepth)
1802                        --     end
1803                        --     nofcurrentcontent = nofcurrentcontent - 1
1804                        -- end
1805                        nofcurrentcontent = nofcurrentcontent + 1
1806                        currentcontent[nofcurrentcontent] = c
1807                    end
1808                elseif subtype == userskip_code then
1809                    if getwidth(n) > threshold then
1810                        if last and not somespace[currentcontent[nofcurrentcontent]] then
1811                            local a = getattr(n,a_tagged) or pat
1812                            if a == last then
1813                                if trace_export then
1814                                    report_export("%w<!-- injecting spacing 5a -->",currentdepth)
1815                                end
1816                                nofcurrentcontent = nofcurrentcontent + 1
1817                                currentcontent[nofcurrentcontent] = " "
1818                            elseif a then
1819                                -- e.g LOGO<space>LOGO
1820                                if trace_export then
1821                                    report_export("%w<!-- processing glue > threshold tagged %s becomes %s -->",currentdepth,last,a)
1822                                end
1823                                pushcontent()
1824                                if trace_export then
1825                                    report_export("%w<!-- injecting spacing 5b -->",currentdepth)
1826                                end
1827                                last = a
1828                                nofcurrentcontent = nofcurrentcontent + 1
1829                                currentcontent[nofcurrentcontent] = " "
1830                                currentnesting = taglist[last]
1831                                pushentry(currentnesting)
1832                                currentattribute = last
1833                            end
1834                        end
1835                    end
1836                elseif subtype == spaceskip_code or subtype == xspaceskip_code then
1837                    if not somespace[currentcontent[nofcurrentcontent]] then
1838                        local a = getattr(n,a_tagged) or pat
1839                        if a == last then
1840                            if trace_export then
1841                                report_export("%w<!-- injecting spacing 7 (stay in element) -->",currentdepth)
1842                            end
1843                            nofcurrentcontent = nofcurrentcontent + 1
1844                            currentcontent[nofcurrentcontent] = " "
1845                        else
1846                            if trace_export then
1847                                report_export("%w<!-- injecting spacing 7 (end of element) -->",currentdepth)
1848                            end
1849                            last = a
1850                            pushcontent()
1851                            nofcurrentcontent = nofcurrentcontent + 1
1852                            currentcontent[nofcurrentcontent] = " "
1853                            currentnesting = taglist[last]
1854                            pushentry(currentnesting)
1855                            currentattribute = last
1856                        end
1857                    end
1858                elseif subtype == intermathskip_code then
1859                    -- put this as attribute when it differs, maybe more ... check mathml
1860                elseif subtype == rightskip_code then
1861                    -- a line
1862                    if nofcurrentcontent > 0 then
1863                        local r = currentcontent[nofcurrentcontent]
1864                        if r == hyphen then
1865                            if not keephyphens then
1866                                nofcurrentcontent = nofcurrentcontent - 1
1867                            end
1868                        elseif pid == disc_code then
1869                            -- go on .. tricky: we should mark the glyhs as coming from a disc
1870                        elseif not somespace[r] then
1871                            local a = getattr(n,a_tagged) or pat
1872                            if a == last then
1873                                if trace_export then
1874                                    report_export("%w<!-- injecting spacing 1 (end of line, stay in element) -->",currentdepth)
1875                                end
1876                                nofcurrentcontent = nofcurrentcontent + 1
1877                                currentcontent[nofcurrentcontent] = " "
1878                            else
1879                                if trace_export then
1880                                    report_export("%w<!-- injecting spacing 1 (end of line, end of element) -->",currentdepth)
1881                                end
1882                                last = a
1883                                pushcontent()
1884                                nofcurrentcontent = nofcurrentcontent + 1
1885                                currentcontent[nofcurrentcontent] = " "
1886                                currentnesting = taglist[last]
1887                                pushentry(currentnesting)
1888                                currentattribute = last
1889                            end
1890                        end
1891                    end
1892                elseif subtype == parfillskip_code then
1893                    -- deal with paragraph endings (crossings) elsewhere and we quit here
1894                    -- as we don't want the rightskip space addition
1895                    if maybewrong then
1896                        showmaybe(maybewrong)
1897                    end
1898--                     return
1899                end
1900            elseif id == hlist_code or id == vlist_code then
1901                local ai = getattr(n,a_image)
1902                if ai then
1903                    local at = getattr(n,a_tagged) or pat
1904                    if nofcurrentcontent > 0 then
1905                        pushcontent()
1906                        pushentry(currentnesting) -- ??
1907                    end
1908                    pushentry(taglist[at]) -- has an index, todo: flag empty element
1909                    if trace_export then
1910                        report_export("%w<!-- processing image tagged %a",currentdepth,last)
1911                    end
1912                    last = nil
1913                    currentparagraph = nil
1914                else
1915                    -- we need to determine an end-of-line
1916                    local list = getlist(n)
1917                    if list then
1918                        -- todo: no par checking needed in math
1919                        local at = getattr(n,a_tagged) or pat
1920                        collectresults(list,n,at)
1921                    end
1922                end
1923            elseif id == kern_code then
1924                if subtype == userkern_code then
1925                    local kern = getkern(n)
1926                    if kern > 0 then
1927                        local a = getattr(n,a_tagged) or pat
1928                        local t = taglist[a]
1929                        if not t or t.tagname ~= "ignore" then -- maybe earlier on top)
1930                            local limit = threshold
1931                            if p then
1932                                local c, f = isglyph(p)
1933                                if c then
1934                                    limit = fontquads[f] / 4
1935                                end
1936                            end
1937                            if kern > limit then
1938                                if last and not somespace[currentcontent[nofcurrentcontent]] then
1939                                 -- local a = getattr(n,a_tagged) or pat
1940                                    if a == last then
1941                                        if not somespace[currentcontent[nofcurrentcontent]] then
1942                                            if trace_export then
1943                                                report_export("%w<!-- injecting spacing 8 (kern %p) -->",currentdepth,kern)
1944                                            end
1945                                            nofcurrentcontent = nofcurrentcontent + 1
1946                                            currentcontent[nofcurrentcontent] = " "
1947                                        end
1948                                    elseif a then
1949                                        -- e.g LOGO<space>LOGO
1950                                        if trace_export then
1951                                            report_export("%w<!-- processing kern, threshold %p, tag %s => %s -->",currentdepth,limit,last,a)
1952                                        end
1953                                        last = a
1954                                        pushcontent()
1955                                        if trace_export then
1956                                            report_export("%w<!-- injecting spacing 9 (kern %p) -->",currentdepth,kern)
1957                                        end
1958                                        nofcurrentcontent = nofcurrentcontent + 1
1959                                        currentcontent[nofcurrentcontent] = " "
1960                                     -- currentnesting = taglist[last]
1961                                        currentnesting = t
1962                                        pushentry(currentnesting)
1963                                        currentattribute = last
1964                                    end
1965                                end
1966                            end
1967                        end
1968                    end
1969                end
1970            elseif id == whatsit_code then
1971                -- todo (lmtx)
1972                if subtype == userdefinedwhatsit_code then
1973                    -- similar to images, see above
1974                    local at = getattr(n,a_tagged)
1975                    if nofcurrentcontent > 0 then
1976                        pushcontent()
1977                        pushentry(currentnesting) -- ??
1978                    end
1979                    pushentry(taglist[at])
1980                    if trace_export then
1981                        report_export("%w<!-- processing anchor tagged %a",currentdepth,last)
1982                    end
1983                    last = nil
1984                    currentparagraph = nil
1985                end
1986            elseif id == par_code then
1987                if startofpar(n) then
1988                    paragraph = getattr(n,a_taggedpar)
1989                end
1990            elseif id == disc_code then
1991                -- very unlikely because we stripped them
1992                local pre, post, replace = getdisc(n)
1993                if keephyphens then
1994                    if pre and not getnext(pre) and isglyph(pre) == 0xAD then -- hyphencode then
1995                        nofcurrentcontent = nofcurrentcontent + 1
1996                        currentcontent[nofcurrentcontent] = hyphen
1997                    end
1998                end
1999                if replace then
2000                    collectresults(replace,nil)
2001                end
2002            end
2003            p   = n
2004            pid = id
2005        end
2006        if maybewrong then
2007            showmaybe(maybewrong)
2008        end
2009    end
2010
2011    local enabled = true
2012
2013    updaters.register("tagging.state.disable",function() enabled = false end)
2014    updaters.register("tagging.state.enable", function() enabled = true  end)
2015
2016    -- maybe also some fast enable/disable stack
2017
2018    function nodes.handlers.export(head) -- hooks into the page builder
2019        if enabled then
2020            starttiming(treehash)
2021            if trace_export then
2022                report_export("%w<!-- start flushing page -->",currentdepth)
2023            end
2024         -- continueexport()
2025            restart = true
2026            collectresults(head)
2027            if trace_export then
2028                report_export("%w<!-- stop flushing page -->",currentdepth)
2029            end
2030            stoptiming(treehash)
2031        end
2032        return head
2033    end
2034
2035    local c_tagparcounter <const> = tex.iscount("tagparcounter")
2036
2037    function nodes.handlers.checkparcounter(p,mode)
2038        setattr(p,a_taggedpar,texgetcount(c_tagparcounter) + 1)
2039        return p
2040    end
2041
2042    -- needs checking!
2043
2044    function builders.paragraphs.tag(head)
2045        noftextblocks = noftextblocks + 1
2046        for n, subtype in nexthlist, head do
2047            if subtype == linelist_code then
2048                setattr(n,a_textblock,noftextblocks)
2049         -- elseif subtype == glue_code or subtype == kern_code then -- weird, no list
2050         --     setattr(n,a_textblock,0)
2051            end
2052        end
2053        return false
2054    end
2055
2056end
2057
2058do
2059
2060    local xmlcollected  = xml.collected
2061    local xmlsetcomment = xml.setcomment
2062
2063local xmlpreamble_nop = [[
2064<?xml version="1.0" encoding="UTF-8" standalone="%standalone%" ?>
2065]]
2066
2067local xmlpreamble_yes = [[
2068<?xml version="1.0" encoding="UTF-8" standalone="%standalone%" ?>
2069
2070<!--
2071
2072    input filename   : %filename%
2073    processing date  : %date%
2074    context version  : %contextversion%
2075    exporter version : %exportversion%
2076
2077-->
2078
2079]]
2080
2081    local flushtree = wrapups.flushtree
2082
2083    local function wholepreamble(standalone,nocomment)
2084        return replacetemplate(nocomment and xmlpreamble_nop or xmlpreamble_yes, {
2085            standalone     = standalone and "yes" or "no",
2086            filename       = tex.jobname,
2087            date           = os.fulltime(),
2088            contextversion = environment.version,
2089            exportversion  = exportversion,
2090        })
2091    end
2092
2093
2094local csspreamble = [[
2095<?xml-stylesheet type="text/css" href="%filename%" ?>
2096]]
2097
2098local cssheadlink = [[
2099<link type="text/css" rel="stylesheet" href="%filename%" />
2100]]
2101
2102-- great, these suggested values attributes ... maybe we should just drop this
2103-- mathjax and accept suboptimal rendering
2104
2105local mathmlheadscript = [[
2106<script
2107    type="text/javascript"
2108    id="MathJax-script"
2109    async="async"
2110    src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/mml-chtml.js">
2111</script>
2112]]
2113
2114    local function allusedstylesheets(cssfiles,files,path,extra)
2115        local done   = { }
2116        local result = { }
2117        local extras = { }
2118        for i=1,#cssfiles do
2119            local cssfile = cssfiles[i]
2120            if type(cssfile) ~= "string" then
2121                -- error
2122            elseif cssfile == "export-example.css" then
2123                -- ignore
2124            elseif not done[cssfile] then
2125                cssfile = joinfile(path,basename(cssfile))
2126                report_export("adding css reference '%s'",cssfile)
2127                files[#files+1]   = cssfile
2128                result[#result+1] = replacetemplate(csspreamble, { filename = cssfile })
2129                extras[#extras+1] = replacetemplate(cssheadlink, { filename = cssfile })
2130                done[cssfile]     = true
2131            end
2132        end
2133        if extra then
2134            extras[#extras+1] = extra
2135        end
2136        return concat(result), concat(extras)
2137    end
2138
2139local elementtemplate <const> = [[
2140/* element="%element%" detail="%detail%" chain="%chain%" */
2141
2142%element%,
2143%namespace%div.%element% {
2144    display: %display% ;
2145}]]
2146
2147local detailtemplate <const> = [[
2148/* element="%element%" detail="%detail%" chain="%chain%" */
2149
2150%element%[detail=%detail%],
2151%namespace%div.%element%.%detail% {
2152    display: %display% ;
2153}]]
2154
2155-- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd" >
2156
2157local htmltemplate <const> = [[
2158%preamble%
2159
2160<html xmlns="http://www.w3.org/1999/xhtml"%?mathmlns: xmlns:m="%mathmlns%" lang="%language%" xml:lang="language%" ?%>
2161
2162    <head>
2163
2164        <meta charset="utf-8"/>
2165
2166        <title>%title%</title>
2167
2168%style%
2169
2170    </head>
2171    <body>
2172        <div class="document" xmlns="http://www.pragma-ade.com/context/export">
2173
2174<div class="warning">Rendering can be suboptimal because there is no default/fallback css loaded.</div>
2175
2176%body%
2177
2178        </div>
2179    </body>
2180</html>
2181]]
2182
2183    local displaymapping = {
2184        inline  = "inline",
2185        display = "block",
2186        mixed   = "inline",
2187    }
2188
2189    local function allusedelements(filename)
2190        local result = { replacetemplate(namespacetemplate, {
2191            what            = "template",
2192            filename        = filename,
2193            namespace       = contextns,
2194         -- cssnamespaceurl = usecssnamespace and cssnamespaceurl or "",
2195            cssnamespaceurl = cssnamespaceurl,
2196        },false,true) }
2197        for element, details in sortedhash(used) do
2198            if mathtags[element] then
2199                -- skip math
2200            else
2201                for detail, what in sortedhash(details) do
2202                    local nature  = what[1] or "display"
2203                    local chain   = what[2]
2204                    local display = displaymapping[nature] or "block"
2205                    if detail == "" then
2206                        result[#result+1] = replacetemplate(elementtemplate, {
2207                            element   = element,
2208                            display   = display,
2209                            chain     = chain,
2210                            namespace = usecssnamespace and namespace or "",
2211                        })
2212                    else
2213                        result[#result+1] = replacetemplate(detailtemplate, {
2214                            element   = element,
2215                            display   = display,
2216                            detail    = detail,
2217                            chain     = chain,
2218                            namespace = usecssnamespace and cssnamespace or "",
2219                        })
2220                    end
2221                end
2222            end
2223        end
2224        return concat(result,"\n\n")
2225    end
2226
2227    local function allcontent(tree,nocheck,final)
2228        local result = { }
2229        local data   = tree.data
2230        if nocheck then
2231            -- maybe mathml
2232        else
2233            -- we need to get rid of crap and also now need to get rid of "documentpart"
2234            -- which is needed for tagged pdf
2235            local d = { }
2236            for i=1,#data do
2237                local di = data[i]
2238                if di.tg == "document" then
2239                    local d = di.data
2240                    for i=1,#d do
2241                        local ddi = d[i]
2242                        if ddi.tg == "documentpart" then
2243                            di.data = ddi.data
2244                            break
2245                        end
2246                    end
2247                    break
2248                end
2249            end
2250        end
2251if final then
2252    -- maybe strip 2..#
2253end
2254        flushtree(result,tree.data,"display") -- we need to collect images
2255        -- no need to lpeg .. fast enough
2256        local r = result[1]
2257        if r then
2258            result[1] = gsub(result[1],"^%s+","")
2259            result[#result] = gsub(result[#result],"%s+$","")
2260            result = concat(result)
2261            result = gsub(result,"\n *\n","\n")
2262            result = gsub(result,"\n +([^< ])","\n%1")
2263            return result
2264        else
2265            -- inspect(result)
2266        end
2267    end
2268
2269    -- local xhtmlpreamble = [[
2270    --     <!DOCTYPE html PUBLIC
2271    --         "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN"
2272    --         "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd"
2273    --     >
2274    -- ]]
2275
2276    local function cleanxhtmltree(xmltree)
2277        if xmltree then
2278            local implicits = { }
2279            local explicits = { }
2280            local overloads = { }
2281            for e in xmlcollected(xmltree,"*") do
2282                local at = e.at
2283                if at then
2284                    local explicit = at.explicit
2285                    local implicit = at.implicit
2286                    if explicit then
2287                        if not explicits[explicit] then
2288                            explicits[explicit] = true
2289                            at.id = explicit
2290                            if implicit then
2291                                overloads[implicit] = explicit
2292                            end
2293                        end
2294                    else
2295                        if implicit and not implicits[implicit] then
2296                            implicits[implicit] = true
2297                            at.id = "aut:" .. implicit
2298                        end
2299                    end
2300                end
2301            end
2302            for e in xmlcollected(xmltree,"*") do
2303                local at = e.at
2304                if at then
2305                    local internal = at.internal
2306                    local location = at.location
2307                    if internal then
2308                        if location then
2309                            local explicit = overloads[location]
2310                            if explicit then
2311                                at.href = "#" .. explicit
2312                            else
2313                                at.href = "#aut:" .. internal
2314                            end
2315                        else
2316                            at.href = "#aut:" .. internal
2317                        end
2318                    else
2319                        if location then
2320                            at.href = "#" .. location
2321                        else
2322                            local url = at.url
2323                            if url then
2324                                at.href = url
2325                            else
2326                                local file = at.file
2327                                if file then
2328                                    at.href = file
2329                                end
2330                            end
2331                        end
2332                    end
2333                end
2334            end
2335            return xmltree
2336        else
2337            return xml.convert('<?xml version="1.0"?>\n<error>invalid xhtml tree</error>')
2338        end
2339    end
2340
2341    -- maybe the reverse: be explicit about what is permitted
2342
2343    local private = {
2344        destination = true,
2345        prefix      = true,
2346        reference   = true,
2347        --
2348        id          = true,
2349        href        = true,
2350        --
2351        implicit    = true,
2352        explicit    = true,
2353        --
2354        url         = true,
2355        file        = true,
2356        internal    = true,
2357        location    = true,
2358        --
2359        name        = true, -- image name
2360        used        = true, -- image name
2361        page        = true, -- image name
2362        width       = true,
2363        height      = true,
2364        --
2365        image       = true,
2366    }
2367
2368    local addclicks   = true
2369    local f_onclick   = formatters[ [[location.href='%s']] ]
2370
2371    local p_cleanid   = lpeg.replacer { [":"] = "-" }
2372    local p_cleanhref = lpeg.Cs(lpeg.P("#") * p_cleanid)
2373
2374    local p_splitter  = lpeg.Ct ( (
2375        lpeg.Carg(1) * lpeg.C((1-lpeg.P(" "))^1) / function(d,s) if not d[s] then d[s] = true return s end end
2376      * lpeg.P(" ")^0 )^1 )
2377
2378
2379    local classes = setmetatableindex(function(t,k)
2380        local v = concat(lpegmatch(p_splitter,k,1,{})," ")
2381        t[k] = v
2382        return v
2383    end)
2384
2385    local function makeclass(tg,at)
2386        local detail     = at.detail
2387        local chain      = at.chain
2388        local extra      = nil
2389        local classes    = { }
2390        local nofclasses = 0
2391        at.detail        = nil
2392        at.chain         = nil
2393        for k, v in next, at do
2394            if not private[k] then
2395                nofclasses = nofclasses + 1
2396                classes[nofclasses] = k .. "-" .. v
2397            end
2398        end
2399        if detail and detail ~= "" then
2400            if chain and chain ~= "" then
2401                if chain ~= detail then
2402                    extra = classes[tg .. " " .. chain .. " " .. detail]
2403                elseif tg ~= detail then
2404                    extra = detail
2405                end
2406            elseif tg ~= detail then
2407                extra = detail
2408            end
2409        elseif chain and chain ~= "" then
2410            if tg ~= chain then
2411                extra = chain
2412            end
2413        end
2414        -- in this order
2415        if nofclasses > 0 then
2416            sort(classes)
2417            classes = concat(classes," ")
2418            if extra then
2419                return tg .. " " .. extra .. " " .. classes
2420            else
2421                return tg .. " " .. classes
2422            end
2423        else
2424            if extra then
2425                return tg .. " " .. extra
2426            else
2427                return tg
2428            end
2429        end
2430    end
2431
2432    -- Some elements are not supported (well) in css so we need to retain them. For
2433    -- instance, tablecells have no colspan so basically that renders css table div
2434    -- elements quite useless. A side effect is that we nwo can have conflicts when
2435    -- we mix in with other html (as there is no reset). Of course, when it eventually
2436    -- gets added, there is a change then that those not using the div abstraction
2437    -- will be rediculed.
2438    --
2439    -- a table tr td th thead tbody tfoot
2440
2441    local crappycss = {
2442        table     = "table", tabulate      = "table",
2443        tablehead = "thead", tabulatehead  = "thead",
2444        tablebody = "tbody", tabulatebody  = "tbody",
2445        tablefoot = "tfoot", tabulatefoot  = "tfoot",
2446        tablerow  = "tr",    tabulaterow   = "tr",
2447        tablecell = "td",    tabulatecell  = "td",
2448    }
2449
2450    local cssmapping = false
2451
2452    directives.register("export.nativetags", function(v)
2453        cssmapping = v and crappycss or false
2454    end)
2455
2456    local function remap(specification,source,target)
2457        local comment = nil -- share comments
2458        for c in xmlcollected(source,"*") do
2459            if not c.special then
2460                local tg = c.tg
2461             -- local ns = c.ns
2462                local at = c.at
2463                if tg == "math" then
2464                    at["xmlns"] = mathmlns
2465                    c.ns = ""
2466                elseif mathtags[tg] then
2467                    -- mathml
2468                else
2469                    local dt = c.dt
2470                    local nt = #dt
2471                    if nt == 0 or (nt == 1 and dt[1] == "") then
2472                        if comment then
2473                            c.dt = comment
2474                        else
2475                            xmlsetcomment(c,"empty")
2476                            comment = c.dt
2477                        end
2478                    end
2479                    local class = nil
2480                    local label = nil
2481local image = at.image
2482                    if tg == "document" then
2483                        at.href   = nil
2484                        at.detail = nil
2485                        at.chain  = nil
2486                    elseif tg == "metavariable" then
2487                        label = at.name
2488                        at.detail = "metaname-" .. label
2489                        class = makeclass(tg,at)
2490                    else
2491                        class = makeclass(tg,at)
2492                    end
2493                    local id   = at.id
2494                    local href = at.href
2495                    local attr = nil
2496                    if id then
2497                        id = lpegmatch(p_cleanid, id) or id
2498                        if href then
2499                            href = lpegmatch(p_cleanhref,href) or href
2500                            attr = {
2501                                class   = class,
2502                                id      = id,
2503                                href    = href,
2504                                onclick = addclicks and f_onclick(href) or nil,
2505                            }
2506                        else
2507                            attr = {
2508                                class = class,
2509                                id    = id,
2510                            }
2511                        end
2512                    else
2513                        if href then
2514                            href = lpegmatch(p_cleanhref,href) or href
2515                            attr = {
2516                                class   = class,
2517                                href    = href,
2518                                onclick = addclicks and f_onclick(href) or nil,
2519                            }
2520                        else
2521                            attr = {
2522                                class = class,
2523                            }
2524                        end
2525                    end
2526if image then
2527    attr.image = image
2528end
2529                    c.at = attr
2530                    if label then
2531                        attr.label = label
2532                    end
2533                    c.tg = cssmapping and cssmapping[tg] or "div"
2534                end
2535            end
2536        end
2537    end
2538
2539 -- local cssfile = nil  directives.register("backend.export.css", function(v) cssfile = v end)
2540
2541    local embedfile = false  directives.register("export.embed",function(v) embedfile = v end)
2542
2543    local justexport = nodes.handlers.export
2544
2545    local function wrapuptree(tree)
2546        wrapups.fixtree(tree)
2547        wrapups.collapsetree(tree)
2548        wrapups.indextree(tree)
2549        wrapups.checktree(tree)
2550        wrapups.breaktree(tree)
2551        wrapups.finalizetree(tree)
2552        --
2553        wrapups.hashlistdata()
2554        --
2555    end
2556
2557    local function locate(tree,simple,level)
2558        if tree then
2559            if tree.tg == simple then
2560                return tree.__p__
2561            end
2562            local data = tree.data
2563            if data then
2564                for i=1,#data do
2565                    local d = locate(data[i],simple,level+1)
2566                    if d then
2567                        return d
2568                    end
2569                end
2570            end
2571        end
2572    end
2573
2574    local function localexport(head,simple)
2575        starttiming(treehash)
2576
2577        local saved_treestack    = treestack    ; treestack    = { }
2578        local saved_nesting      = nesting      ; nesting      = { }
2579        local saved_currentdepth = currentdepth ; currentdepth = 0
2580        local saved_tree         = tree         ; tree         = { data = { }, fulltag == "root" } -- root
2581        local saved_roottree     = roottree     ; roottree     = tree
2582        local saved_treehash     = treehash     ; treehash     = { }
2583        local saved_nofbreaks    = nofbreaks    ; nofbreaks    = 0
2584        local saved_show_comment = show_comment ; show_comment = false
2585
2586local saved_nofcurrentcontent = nofcurrentcontent ; nofcurrentcontent = 0
2587local saved_currentcontent    = currentcontent    ; currentcontent    = { }
2588local saved_currentnesting    = currentnesting    ; currentnesting    = nil
2589local saved_currentattribute  = currentattribute  ; currentattribute  = nil
2590local saved_last              = last              ; last              = nil
2591local saved_currentparagraph  = currentparagraph  ; currentparagraph  = nil
2592
2593        justexport(head)
2594        finishexport()
2595        wrapuptree(tree)
2596
2597        local result
2598
2599        if simple then
2600            local d = locate(tree,simple,1)
2601            if d then
2602                result = allcontent(d,true)
2603            end
2604        else
2605            result = concat {
2606                wholepreamble(true),
2607                allcontent(tree),
2608            }
2609        end
2610
2611        treestack    = saved_treestack
2612        nesting      = saved_nesting
2613        currentdepth = saved_currentdepth
2614        tree         = saved_tree
2615        roottree     = saved_roottree
2616        treehash     = saved_treehash
2617        nofbreaks    = saved_nofbreaks
2618        show_comment = saved_show_comment
2619
2620nofcurrentcontent = saved_nofcurrentcontent
2621currentcontent    = saved_currentcontent
2622currentnesting    = saved_currentnesting
2623currentattribute  = saved_currentattribute
2624last              = saved_last
2625currentparagraph  = saved_currentparagraph
2626
2627        stoptiming(treehash)
2628
2629        return result
2630
2631    end
2632
2633    structurestags.localexport = localexport
2634
2635    function structures.tags.exportbox(n,filename,buffername)
2636        local list = nodes.nuts.getbox(n)
2637        if n then
2638            local e = localexport(list)
2639            if filename and filename ~= "" then
2640                io.savedata(filename,e)
2641            elseif buffername then
2642                buffers.assign(buffername == interfaces.variables.yes and "" or buffername,e)
2643            else
2644                return e
2645            end
2646        end
2647    end
2648
2649    interfaces.implement {
2650        name      = "exportbox",
2651        arguments = { "integer", "string", "string" },
2652        actions   = structures.tags.exportbox
2653    }
2654
2655    function structurestags.finishexport()
2656
2657        if only_images then
2658            exporting = false
2659        end
2660
2661        if exporting then
2662            exporting = false
2663        else
2664            return
2665        end
2666
2667        local onlyxml = finetuning.export == v_xml
2668
2669        starttiming(treehash)
2670        --
2671        finishexport()
2672        --
2673        report_export("")
2674        if onlyxml then
2675            report_export("exporting xml, no other files")
2676        else
2677            report_export("exporting xml, xhtml, html and css files")
2678        end
2679        report_export("")
2680        --
2681        wrapuptree(tree)
2682        --
2683        local exportxml   = true
2684        local exportxhtml = true
2685        local exporthtml  = true
2686        local useextra    = true -- compatibility for now
2687        --
2688        if finetuning.output == "xml" then
2689            exporthtml  = false
2690            exportxhtml = false
2691            useextra    = false
2692        elseif finetuning.output == "xhtml" then
2693            exportxml   = false
2694            exporthtml  = false
2695            useextra    = false
2696        elseif finetuning.output == "html" then
2697            exportxml   = false
2698            exportxhtml = false
2699            useextra    = false
2700        end
2701        --
2702        local askedname = finetuning.file
2703        local pathname  = finetuning.path
2704        --
2705        -- normally we use a dedicated subpath:
2706        --
2707        -- ./jobname-export
2708        -- ./jobname-export/images
2709        -- ./jobname-export/styles
2710        -- ./jobname-export/jobname-export.xml
2711        -- ./jobname-export/jobname-export.xhtml
2712        -- ./jobname-export/jobname-export.html
2713        -- ./jobname-export/jobname-specification.lua
2714        -- ./jobname-export/styles/jobname-defaults.css
2715        -- ./jobname-export/styles/jobname-fonts.css
2716        -- ./jobname-export/styles/jobname-styles.css
2717        -- ./jobname-export/styles/jobname-images.css
2718        -- ./jobname-export/styles/jobname-templates.css
2719
2720        if type(askedname) ~= "string" or askedname == "" then
2721            askedname = tex.jobname
2722        end
2723        if type(pathname) ~= "string" or pathname == "" then
2724            pathname = false
2725        end
2726
2727        local usedname  = nameonly(askedname)
2728        local basepath  = usedname .. "-export"
2729
2730if not useextra then
2731    basepath = "."
2732end
2733
2734        local imagepath = joinfile(basepath,"images")
2735        local stylepath = joinfile(basepath,"styles")
2736
2737        if pathname then
2738            basepath  = joinfile(pathname,basepath)
2739            imagepath = joinfile(pathname,imagepath)
2740            stylepath = joinfile(pathname,stylepath)
2741        end
2742
2743        local function validpath(what,pathname)
2744            if lfs.isdir(pathname) then
2745                report_export("using existing %s path %a",what,pathname)
2746                return pathname
2747            end
2748            lfs.mkdirs(pathname)
2749            if lfs.isdir(pathname) then
2750                report_export("using cretated %s path %a",what,basepath)
2751                return pathname
2752            else
2753                report_export("unable to create %s path %a",what,basepath)
2754                return false
2755            end
2756        end
2757
2758        if not (validpath("export",basepath )
2759            and validpath("images",imagepath)
2760            and validpath("styles",stylepath)) then
2761            return
2762        end
2763
2764        -- we're now on the dedicated export subpath so we can't clash names
2765        --
2766        -- a xhtml suffix no longer seems to be work well with browsers
2767
2768        local xmlfilebase           = addsuffix(usedname .. (useextra and "-raw" or ""),"xml"  )
2769        local xhtmlfilebase         = addsuffix(usedname .. (useextra and "-tag" or ""),"xhtml")
2770        local htmlfilebase          = addsuffix(usedname .. (useextra and "-div" or ""),"html")
2771        local specificationfilebase = addsuffix(usedname .. (useextra and "-pub" or ""),"lua"  )
2772
2773        local xmlfilename           = joinfile(basepath, xmlfilebase          )
2774        local xhtmlfilename         = joinfile(basepath, xhtmlfilebase        )
2775        local htmlfilename          = joinfile(basepath, htmlfilebase         )
2776        local specificationfilename = joinfile(basepath, specificationfilebase)
2777        --
2778        local defaultfilebase       = addsuffix(usedname .. "-defaults", "css")
2779        local imagefilebase         = addsuffix(usedname .. "-images",   "css")
2780        local fontfilebase          = addsuffix(usedname .. "-fonts",    "css")
2781        local stylefilebase         = addsuffix(usedname .. "-styles",   "css")
2782        local templatefilebase      = addsuffix(usedname .. "-templates","css")
2783        --
2784        local defaultfilename       = joinfile(stylepath,defaultfilebase )
2785        local imagefilename         = joinfile(stylepath,imagefilebase   )
2786        local stylefilename         = joinfile(stylepath,stylefilebase   )
2787        local fontfilename          = joinfile(stylepath,fontfilebase    )
2788        local templatefilename      = joinfile(stylepath,templatefilebase)
2789
2790        local cssfile               = finetuning.cssfile
2791
2792        -- we keep track of all used files
2793
2794        local files = {
2795        }
2796
2797        -- we always load the defaults and optionally extra css files; we also copy the example
2798        -- css file so that we always have the latest version
2799
2800        local cssfiles = {
2801            defaultfilebase,
2802            imagefilebase,
2803            fontfilebase,
2804            stylefilebase,
2805        }
2806
2807        local cssextra = cssfile and table.unique(settings_to_array(cssfile)) or { }
2808
2809        -- at this point we're ready for the content; the collector also does some
2810        -- housekeeping and data collecting; at this point we still have an xml
2811        -- representation that uses verbose element names and carries information in
2812        -- attributes
2813
2814        local result = allcontent(tree,false,true)
2815
2816        -- ugly but so be it:
2817        local extradata = structures.tags.getextradata()
2818
2819        if extradata then
2820            local t = { "" }
2821            t[#t+1] = "<extradata>"
2822            for name, action in sortedhash(extradata) do
2823                t[#t+1] = action()
2824            end
2825            t[#t+1] = "</extradata>"
2826            t[#t+1] = "</document>"
2827            -- we use a function because otherwise we can have a bad capture index
2828            result = gsub(result,"</document>",function()
2829                return concat(t,"\n")
2830            end)
2831        end
2832
2833        -- done with ugly
2834
2835        if onlyxml then
2836
2837            os.remove(defaultfilename)
2838         -- os.remove(imagefilename)
2839         -- os.remove(fontfilename)
2840         -- os.remove(stylefilename)
2841         -- os.remove(templatefilename)
2842
2843            for i=1,#cssextra do
2844                os.remove(joinfile(stylepath,basename(source)))
2845            end
2846
2847         -- os.remove(xmlfilename)
2848
2849            os.remove(imagefilename)
2850            os.remove(fontfilename)
2851            os.remove(stylefilename)
2852            os.remove(templatefilename)
2853            os.remove(xhtmlfilename)
2854            os.remove(specificationfilename)
2855            os.remove(htmlfilename)
2856
2857            result = concat {
2858                wholepreamble(true,true),
2859                "<!-- This export file is used for filtering runtime only! -->\n",
2860                result,
2861            }
2862
2863            report_export("saving xml data in %a",xmlfilename)
2864            io.savedata(xmlfilename,result)
2865
2866            return
2867
2868        end
2869
2870        local examplefilename = resolvers.findfile("export-example.css")
2871        if examplefilename then
2872            local data = io.loaddata(examplefilename)
2873            if not data or data == "" then
2874                data = "/* missing css file */"
2875            elseif not usecssnamespace then
2876                data = gsub(data,cssnamespace,"")
2877            end
2878            io.savedata(defaultfilename,data)
2879        end
2880
2881        if cssfile then
2882            for i=1,#cssextra do
2883                local source = addsuffix(cssextra[i],"css")
2884                local target = joinfile(stylepath,basename(source))
2885                cssfiles[#cssfiles+1] = source
2886                if not lfs.isfile(source) then
2887                    source = joinfile("../",source)
2888                end
2889                if lfs.isfile(source) then
2890                    report_export("copying %s",source)
2891                    file.copy(source,target)
2892                end
2893            end
2894        end
2895
2896
2897        local script = settings_to_hash(finetuning.option or "").mathjax and mathmlheadscript or nil
2898
2899        local x_styles, h_styles = allusedstylesheets(cssfiles,files,"styles",script)
2900
2901        local attach = backends.nodeinjections.attachfile
2902
2903        if embedfile and attach then
2904            -- only for testing
2905            attach {
2906                data       = concat{ wholepreamble(true), result },
2907                name       = basename(xmlfilename),
2908                registered = "export",
2909                title      = "raw xml export",
2910                method     = v_hidden,
2911                mimetype   = "application/mathml+xml",
2912            }
2913        end
2914
2915        result = concat {
2916            wholepreamble(true),
2917            x_styles, -- adds to files
2918            result,
2919        }
2920
2921        cssfiles = table.unique(cssfiles)
2922
2923        -- we're now ready for saving the result in the xml file
2924
2925        if exportxml then
2926
2927            report_export("saving xml data in %a",xmlfilename)
2928            io.savedata(xmlfilename,result)
2929
2930        end
2931
2932        report_export("saving css image definitions in %a",imagefilename)
2933        io.savedata(imagefilename,wrapups.allusedimages(usedname))
2934
2935        report_export("saving css font definitions in %a",fontfilename)
2936        io.savedata(fontfilename,wrapups.allusedfonts(usedname))
2937
2938        report_export("saving css style definitions in %a",stylefilename)
2939        io.savedata(stylefilename,wrapups.allusedstyles(usedname))
2940
2941        report_export("saving css template in %a",templatefilename)
2942        io.savedata(templatefilename,allusedelements(usedname))
2943
2944        -- additionally we save an xhtml file; for that we load the file as xml tree
2945
2946        local xmltree = cleanxhtmltree(xml.convert(result))
2947
2948        if exportxhtml then
2949
2950            report_export("saving xhtml variant in %a",xhtmlfilename)
2951
2952            -- for c in xml.collected(xmltree,"listitem[@href]") do
2953            --     local h = c.at.href
2954            --     if h then
2955            --         local l = xml.first(c,"listtag")
2956            --         if l then
2957            --             table.insert(l.dt,1,xml.convert("<A href='" .. c.at.href .. "'></A>").dt)
2958            --         end
2959            --     end
2960            -- end
2961
2962            xml.save(xmltree,xhtmlfilename)
2963
2964-- remap(specification,xmltree)
2965
2966        end
2967
2968        -- now we save a specification file that can be used for generating an epub file
2969
2970        -- looking at identity is somewhat redundant as we also inherit from interaction
2971        -- at the tex end
2972
2973        if exporthtml then
2974
2975            local identity  = interactions.general.getidentity()
2976            local metadata  = structures.tags.getmetadata()
2977
2978            local specification = {
2979                name       = usedname,
2980                identifier = os.uuid(),
2981                images     = wrapups.uniqueusedimages(),
2982                imagefile  = joinfile("styles",imagefilebase),
2983                imagepath  = "images",
2984                stylepath  = "styles",
2985                xmlfiles   = { xmlfilebase },
2986                xhtmlfiles = { xhtmlfilebase },
2987                htmlfiles  = { htmlfilebase },
2988                styles     = cssfiles,
2989                htmlroot   = htmlfilebase,
2990                language   = languagenames[texgetcount("mainlanguagenumber")],
2991                title      = validstring(finetuning.title) or validstring(identity.title),
2992                subtitle   = validstring(finetuning.subtitle) or validstring(identity.subtitle),
2993                author     = validstring(finetuning.author) or validstring(identity.author),
2994                firstpage  = validstring(finetuning.firstpage),
2995                lastpage   = validstring(finetuning.lastpage),
2996                metadata   = metadata,
2997            }
2998
2999            report_export("saving specification in %a",specificationfilename,specificationfilename)
3000
3001            xml.wipe(xmltree,"metadata") -- maybe optional
3002
3003            io.savedata(specificationfilename,table.serialize(specification,true))
3004
3005        -- the html export for epub is different in the sense that it uses div's instead of
3006        -- specific tags
3007
3008            report_export("saving div based alternative in %a",htmlfilename)
3009
3010            remap(specification,xmltree)
3011
3012            -- believe it or not, but a <title/> can prevent viewing in browsers
3013
3014            local title = specification.title
3015
3016            if not title or title == "" then
3017                title = metadata.title
3018                if not title or title == "" then
3019                    title = usedname -- was: "no title"
3020                end
3021            end
3022
3023            local language = languagenames[texgetcount("mainlanguagenumber")]
3024
3025            local variables = {
3026                style    = h_styles,
3027                body     = xml.tostring(xml.first(xmltree,"/div")),
3028                preamble = wholepreamble(false),
3029                title    = title,
3030                language = languagenames[texgetcount("mainlanguagenumber")] or "en",
3031            }
3032
3033            io.savedata(htmlfilename,replacetemplate(htmltemplate,variables,"xml"))
3034
3035            -- finally we report how an epub file can be made (using the specification)
3036
3037            report_export("")
3038            report_export('create epub with: mtxrun --script epub --make "%s" [--purge --rename --svgmath]',usedname)
3039            report_export("")
3040
3041        end
3042
3043        stoptiming(treehash)
3044    end
3045
3046    local enableaction = nodes.tasks.enableaction
3047
3048    function structurestags.initializeexport()
3049        if not exporting then
3050            report_export("enabling export to xml")
3051            enableaction("shipouts","nodes.handlers.export")
3052         -- enableaction("shipouts","nodes.handlers.accessibility")
3053            enableaction("math",    "noads.handlers.tags")
3054            enableaction("everypar","nodes.handlers.checkparcounter")
3055            luatex.registerstopactions(structurestags.finishexport)
3056            exporting = true
3057        end
3058    end
3059
3060    function structurestags.setupexport(t)
3061        merge(finetuning,t)
3062        keephyphens      = finetuning.hyphen == v_yes
3063        exportproperties = finetuning.properties
3064        if exportproperties == v_no then
3065            exportproperties = false
3066        end
3067    end
3068
3069    statistics.register("xml exporting time", function()
3070        if exporting then
3071            return string.format("%s seconds, version %s", statistics.elapsedtime(treehash),exportversion)
3072        end
3073    end)
3074
3075end
3076
3077-- These are called at the tex end:
3078
3079implement {
3080    name      = "setupexport",
3081    actions   = structurestags.setupexport,
3082    arguments = {
3083        {
3084            { "align" }, -- normally a number
3085            { "bodyfont", "dimen" },
3086            { "width", "dimen" },
3087            { "properties" },
3088            { "hyphen" },
3089            { "title" },
3090            { "subtitle" },
3091            { "author" },
3092            { "firstpage" },
3093            { "lastpage" },
3094            { "svgstyle" },
3095            { "cssfile" },
3096            { "file" },
3097            { "option" },
3098            { "export" },
3099        }
3100    }
3101}
3102
3103implement {
3104    name      = "finishexport",
3105    actions   = structurestags.finishexport,
3106}
3107
3108implement {
3109    name      = "initializeexport",
3110    actions   = structurestags.initializeexport,
3111}
3112