strc-mar.lua /size: 25 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['strc-mar'] = {
2    version   = 1.001,
3    comment   = "companion to strc-mar.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-- todo: cleanup stack (structures.marks.reset(v_all) also does the job)
10-- todo: only commands.* print to tex, native marks return values
11
12local insert, concat = table.insert, table.concat
13local tostring, next, rawget, type = tostring, next, rawget, type
14local lpegmatch = lpeg.match
15
16local context             = context
17local commands            = commands
18
19local implement           = interfaces.implement
20
21local allocate            = utilities.storage.allocate
22local setmetatableindex   = table.setmetatableindex
23
24local nuts                = nodes.nuts
25local tonut               = nuts.tonut
26
27local getid               = nuts.getid
28local getlist             = nuts.getlist
29local getattr             = nuts.getattr
30local getbox              = nuts.getbox
31
32local nextnode            = nuts.traversers.node
33
34local nodecodes           = nodes.nodecodes
35local whatsitcodes        = nodes.whatsitcodes
36
37local glyph_code          = nodecodes.glyph
38local hlist_code          = nodecodes.hlist
39local vlist_code          = nodecodes.vlist
40local whatsit_code        = nodecodes.whatsit
41
42local lateluawhatsit_code = whatsitcodes.latelua
43
44local texsetattribute     = tex.setattribute
45
46local a_marks             = attributes.private("marks")
47
48local trace_set     = false  trackers.register("marks.set",     function(v) trace_set     = v end)
49local trace_get     = false  trackers.register("marks.get",     function(v) trace_get     = v end)
50local trace_details = false  trackers.register("marks.details", function(v) trace_details = v end)
51
52local report_marks        = logs.reporter("structure","marks")
53
54local variables           = interfaces.variables
55
56local v_first             = variables.first
57local v_last              = variables.last
58local v_previous          = variables.previous
59local v_next              = variables.next
60local v_top               = variables.top
61local v_bottom            = variables.bottom
62local v_current           = variables.current
63local v_default           = variables.default
64local v_page              = variables.page
65local v_all               = variables.all
66local v_keep              = variables.keep
67
68local v_nocheck_suffix    = ":" .. variables.nocheck
69
70local v_first_nocheck     = variables.first    .. v_nocheck_suffix
71local v_last_nocheck      = variables.last     .. v_nocheck_suffix
72local v_previous_nocheck  = variables.previous .. v_nocheck_suffix
73local v_next_nocheck      = variables.next     .. v_nocheck_suffix
74local v_top_nocheck       = variables.top      .. v_nocheck_suffix
75local v_bottom_nocheck    = variables.bottom   .. v_nocheck_suffix
76
77local structures          = structures
78local marks               = structures.marks
79local lists               = structures.lists
80
81local settings_to_array   = utilities.parsers.settings_to_array
82
83local boxes_too           = false -- at some point we can also tag boxes or use a zero char
84
85directives.register("marks.boxestoo", function(v) boxes_too = v end)
86
87local data = marks.data or allocate()
88marks.data = data
89
90storage.register("structures/marks/data", marks.data, "structures.marks.data")
91
92local stack, topofstack = { }, 0
93
94local ranges = {
95    [v_page] = {
96        first = 0,
97        last  = 0,
98    },
99}
100
101local function resolve(t,k)
102    if k then
103        if trace_set or trace_get then
104            report_marks("undefined mark, name %a",k)
105        end
106        local crap = { autodefined = true } -- maybe set = 0 and reset = 0
107        t[k] = crap
108        return crap
109    else
110        -- weird: k is nil
111    end
112end
113
114setmetatableindex(data, resolve)
115
116function marks.exists(name)
117    return rawget(data,name) ~= nil
118end
119
120-- identify range
121
122local function sweep(head,first,last)
123    for n, id, subtype in nextnode, head do
124        -- we need to handle empty heads so we test for latelua
125        if id == glyph_code or (id == whatsit_code and subtype == lateluawhatsit_code) then -- brrr
126            local a = getattr(n,a_marks)
127            if not a then
128                -- next
129            elseif first == 0 then
130                first, last = a, a
131            elseif a > last then
132                last = a
133            end
134        elseif id == hlist_code or id == vlist_code then
135            if boxes_too then
136                local a = getattr(n,a_marks)
137                if not a then
138                    -- next
139                elseif first == 0 then
140                    first, last = a, a
141                elseif a > last then
142                    last = a
143                end
144            end
145            local list = getlist(n)
146            if list then
147                first, last = sweep(list,first,last)
148            end
149        end
150    end
151    return first, last
152end
153
154local classes = { }
155
156setmetatableindex(classes, function(t,k) local s = settings_to_array(k) t[k] = s return s end)
157
158local lasts = { }
159
160function marks.synchronize(class,n,option)
161    local box = getbox(n)
162    if box then
163        local first, last = sweep(getlist(box),0,0)
164        if option == v_keep and first == 0 and last == 0 then
165            if trace_get or trace_set then
166                report_marks("action %a, class %a, box %a","retain at synchronize",class,n)
167            end
168            -- todo: check if still valid firts/last in range
169            first = lasts[class] or 0
170            last = first
171        else
172            lasts[class] = last
173            local classlist = classes[class]
174            for i=1,#classlist do
175                local class = classlist[i]
176                local range = ranges[class]
177                if range then
178                    range.first = first
179                    range.last  = last
180                else
181                    range = {
182                        first = first,
183                        last  = last,
184                    }
185                    ranges[class] = range
186                end
187                if trace_get or trace_set then
188                    report_marks("action %a, class %a, first %a, last %a","synchronize",class,range.first,range.last)
189                end
190            end
191        end
192    elseif trace_get or trace_set then
193        report_marks("action %s, class %a, box %a","synchronize without content",class,n)
194    end
195end
196
197-- define etc
198
199local function resolve(t,k)
200    if k == "fullchain" then
201        local fullchain = { }
202        local chain = t.chain
203        while chain and chain ~= "" do
204            insert(fullchain,1,chain)
205            chain = data[chain].chain
206        end
207        t[k] = fullchain
208        return fullchain
209    elseif k == "chain" then
210        t[k] = ""
211        return ""
212    elseif k == "reset" or k == "set" then
213        t[k] = 0
214        return 0
215    elseif k == "parent" then
216        t[k] = false
217        return false
218    end
219end
220
221function marks.define(name,settings)
222    if not settings then
223        settings = { }
224    elseif type(settings) == "string" then
225        settings = { parent = settings }
226    end
227    data[name] = settings
228    local parent = settings.parent
229    if parent == nil or parent == "" or parent == name then
230        settings.parent = false
231    else
232        local dp = data[parent]
233        if not dp then
234            settings.parent = false
235        elseif dp.parent then
236            settings.parent = dp.parent
237        end
238    end
239    setmetatableindex(settings, resolve)
240end
241
242for k, v in next, data do
243    setmetatableindex(v,resolve) -- runtime loaded table
244end
245
246local function parentname(name)
247    local dn = data[name]
248    return dn and dn.parent or name
249end
250
251function marks.relate(name,chain)
252    local dn = data[name]
253    if dn and not dn.parent then
254        if chain and chain ~= "" then
255            dn.chain = chain
256            local dc = data[chain]
257            if dc then
258                local children = dc.children
259                if not children then
260                    children = { }
261                    dc.children = children
262                end
263                children[#children+1] = name
264            end
265        elseif trace_set then
266            report_marks("error: invalid relation, name %a, chain %a",name,chain)
267        end
268    end
269end
270
271local function resetchildren(new,name)
272    local dn = data[name]
273    if dn and not dn.parent then
274        local children = dn.children
275        if children then
276            for i=1,#children do
277                local ci = children[i]
278                new[ci] = false
279                if trace_set then
280                    report_marks("action %a, parent %a, child %a","reset",name,ci)
281                end
282                resetchildren(new,ci)
283            end
284        end
285    end
286end
287
288function marks.set(name,value)
289    local dn = data[name]
290    if dn then
291        local child = name
292        local parent = dn.parent
293        if parent then
294            name = parent
295            dn = data[name]
296        end
297        dn.set = topofstack
298        if not dn.reset then
299            dn.reset = 0 -- in case of selfdefined
300        end
301        local top = stack[topofstack]
302        local new = { }
303        if top then
304            for k, v in next, top do
305                local d = data[k]
306                local r = d.reset or 0
307                local s = d.set or 0
308                if r <= topofstack and s < r then
309                    new[k] = false
310                else
311                    new[k] = v
312                end
313            end
314        end
315        resetchildren(new,name)
316        new[name] = value
317        topofstack = topofstack + 1
318        stack[topofstack] = new
319        if trace_set then
320            if name == child then
321                report_marks("action %a, name %a, index %a, value %a","set",name,topofstack,value)
322            else
323                report_marks("action %a, parent %a, child %a, index %a, value %a","set",parent,child,topofstack,value)
324            end
325        end
326        texsetattribute("global",a_marks,topofstack)
327    end
328end
329
330local function reset(name)
331    if v_all then
332        if trace_set then
333            report_marks("action %a","reset all")
334        end
335        stack = { }
336        for name, dn in next, data do
337            local parent = dn.parent
338            if parent then
339                dn.reset = 0
340                dn.set = 0
341            end
342        end
343    else
344        local dn = data[name]
345        if dn then
346            local parent = dn.parent
347            if parent then
348                name = parent
349                dn = data[name]
350            end
351            if trace_set then
352                report_marks("action %a, name %a, index %a","reset",name,topofstack)
353            end
354            dn.reset = topofstack
355            local children = dn.children
356            if children then
357                for i=1,#children do
358                    local ci = children[i]
359                    reset(ci)
360                end
361            end
362        end
363    end
364end
365
366marks.reset = reset
367
368function marks.get(n,name,value)
369    local dn = data[name]
370    if dn then
371        name = dn.parent or name
372        local top = stack[n]
373        if top then
374            context(top[name])
375        end
376    end
377end
378
379function marks.show(first,last)
380    if first and last then
381        for k=first,last do
382            local v = stack[k]
383            if v then
384                report_marks("% 4i: %s",k,table.sequenced(v))
385            end
386        end
387    else
388        for k, v in table.sortedpairs(stack) do
389            report_marks("% 4i: %s",k,table.sequenced(v))
390        end
391    end
392end
393
394local function resolve(name,first,last,strict,quitonfalse,notrace)
395    local dn = data[name]
396    if dn then
397        local child = name
398        local parent = dn.parent
399        name = parent or child
400        dn = data[name]
401        local step, method
402        if first > last then
403            step, method = -1, "bottom-up"
404        else
405            step, method = 1, "top-down"
406        end
407        if trace_get and not notrace then
408            report_marks("action %a, strategy %a, name %a, parent %a, strict %a","request",method,child,parent,strict or false)
409        end
410        if trace_details and not notrace then
411            marks.show(first,last)
412        end
413        local r = dn.reset
414        local s = dn.set
415        if first <= last and first <= r then
416            if trace_get and not notrace then
417                report_marks("action %a, name %a, first %a, last %a, reset %a, index %a","reset first",name,first,last,r,first)
418            end
419        elseif first >= last and last <= r then
420            if trace_get and not notrace then
421                report_marks("action %a, name %a, first %a, last %a, reset %a, index %a","reset last",name,first,last,r,last)
422            end
423        elseif not stack[first] or not stack[last] then
424            if trace_get and not notrace then
425                -- a previous or next method can give an out of range, which is valid
426                report_marks("error: out of range, name %a, reset %a, index %a",name,r,first)
427            end
428        elseif strict then
429            local top = stack[first]
430            local fullchain = dn.fullchain
431            if not fullchain or #fullchain == 0 then
432                if trace_get and not notrace then
433                    report_marks("warning: no full chain, trying again, name %a, first %a, last %a",name,first,last)
434                end
435                return resolve(name,first,last)
436            else
437                if trace_get and not notrace then
438                    report_marks("found chain [ % => T ]",fullchain)
439                end
440                local chaindata   = { }
441                local chainlength = #fullchain
442                for i=1,chainlength do
443                    local cname = fullchain[i]
444                    if data[cname].set > 0 then
445                        local value = resolve(cname,first,last,false,false,true)
446                        if value == "" then
447                            if trace_get and not notrace then
448                                report_marks("quitting chain, name %a, reset %a, start %a",name,r,first)
449                            end
450                            return ""
451                        else
452                            chaindata[i] = value
453                        end
454                    end
455                end
456                if trace_get and not notrace then
457                    report_marks("using chain  [ % => T ]",chaindata)
458                end
459                local value, index, found = resolve(name,first,last,false,false,true)
460                if value ~= ""  then
461                    if trace_get and not notrace then
462                        report_marks("following chain  [ % => T ]",chaindata)
463                    end
464                    for i=1,chainlength do
465                        local cname = fullchain[i]
466                        if data[cname].set > 0 and chaindata[i] ~= found[cname] then
467                            if trace_get and not notrace then
468                                report_marks("quiting chain, name %a, reset %a, index %a",name,r,first)
469                            end
470                            return ""
471                        end
472                    end
473                    if trace_get and not notrace then
474                        report_marks("found in chain, name %a, reset %a, start %a, index %a, value %a",name,r,first,index,value)
475                    end
476                    return value, index, found
477                elseif trace_get and not notrace then
478                    report_marks("not found, name %a, reset %a",name,r)
479                end
480            end
481        else
482            for i=first,last,step do
483                local current = stack[i]
484                local value = current and current[name]
485                if value == nil then
486                    -- search on
487                elseif value == false then
488                    if quitonfalse then
489                        return ""
490                    end
491                elseif value == true then
492                    if trace_get and not notrace then
493                        report_marks("quitting steps, name %a, reset %a, start %a, index %a",name,r,first,i)
494                    end
495                    return ""
496                elseif value ~= "" then
497                    if trace_get and not notrace then
498                        report_marks("found in steps, name %a, reset %a, start %a, index %a, value %a",name,r,first,i,value)
499                    end
500                    return value, i, current
501                end
502            end
503            if trace_get and not notrace then
504                report_marks("not found in steps, name %a, reset %a",name,r)
505            end
506        end
507    end
508    return ""
509end
510
511-- todo: column:first column:last
512
513local methods  = { }
514
515local function doresolve(name,rangename,swap,df,dl,strict)
516    local range = ranges[rangename] or ranges[v_page]
517    local first = range.first
518    local last  = range.last
519    if trace_get then
520        report_marks("action %a, name %a, range %a, swap %a, first %a, last %a, df %a, dl %a, strict %a",
521            "resolving",name,rangename,swap or false,first,last,df,dl,strict or false)
522    end
523    if swap then
524        first, last = last + df, first + dl
525    else
526        first, last = first + df, last + dl
527    end
528    local value, index, found = resolve(name,first,last,strict)
529    -- maybe something more
530    return value, index, found
531end
532
533-- previous : last before sync
534-- next     : first after sync
535
536-- top      : first in sync
537-- bottom   : last in sync
538
539-- first    : first not top in sync
540-- last     : last not bottom in sync
541
542methods[v_previous]         = function(name,range) return doresolve(name,range,false,-1,0,true ) end -- strict
543methods[v_top]              = function(name,range) return doresolve(name,range,false, 0,0,true ) end -- strict
544methods[v_bottom]           = function(name,range) return doresolve(name,range,true , 0,0,true ) end -- strict
545methods[v_next]             = function(name,range) return doresolve(name,range,true , 0,1,true ) end -- strict
546
547methods[v_previous_nocheck] = function(name,range) return doresolve(name,range,false,-1,0,false) end
548methods[v_top_nocheck]      = function(name,range) return doresolve(name,range,false, 0,0,false) end
549methods[v_bottom_nocheck]   = function(name,range) return doresolve(name,range,true , 0,0,false) end
550methods[v_next_nocheck]     = function(name,range) return doresolve(name,range,true , 0,1,false) end
551
552local function do_first(name,range,check)
553    if trace_get then
554        report_marks("action %a, name %a, range %a","resolving first",name,range)
555    end
556    local f_value, f_index, f_found = doresolve(name,range,false,0,0,check)
557    if f_found then
558        if trace_get then
559            report_marks("action %a, name %a, range %a","resolving last",name,range)
560        end
561        local l_value, l_index, l_found = doresolve(name,range,true ,0,0,check)
562        if l_found and l_index > f_index then
563            local name = parentname(name)
564            for i=f_index,l_index,1 do
565                local si = stack[i]
566                local sn = si[name]
567                if sn and sn ~= false and sn ~= true and sn ~= "" and sn ~= f_value then
568                    if trace_get then
569                        report_marks("action %a, name %a, range %a, index %a, value %a","resolving",name,range,i,sn)
570                    end
571                    return sn, i, si
572                end
573            end
574        end
575    end
576    if trace_get then
577        report_marks("resolved, name %a, range %a, using first",name,range)
578    end
579    return f_value, f_index, f_found
580end
581
582local function do_last(name,range,check)
583    if trace_get then
584        report_marks("action %a, name %a, range %a","resolving last",name,range)
585    end
586    local l_value, l_index, l_found = doresolve(name,range,true ,0,0,check)
587    if l_found then
588        if trace_get then
589            report_marks("action %a, name %a, range %a","resolving first",name,range)
590        end
591        local f_value, f_index, f_found = doresolve(name,range,false,0,0,check)
592        if f_found and l_index > f_index then
593            local name = parentname(name)
594            for i=l_index,f_index,-1 do
595                local si = stack[i]
596                local sn = si[name]
597                if sn and sn ~= false and sn ~= true and sn ~= "" and sn ~= l_value then
598                    if trace_get then
599                        report_marks("action %a, name %a, range %a, index %a, value %a","resolving",name,range,i,sn)
600                    end
601                    return sn, i, si
602                end
603            end
604        end
605    end
606    if trace_get then
607        report_marks("resolved, name %a, range %a, using first",name,range)
608    end
609    return l_value, l_index, l_found
610end
611
612methods[v_first        ] = function(name,range) return do_first(name,range,true ) end
613methods[v_last         ] = function(name,range) return do_last (name,range,true ) end
614methods[v_first_nocheck] = function(name,range) return do_first(name,range,false) end
615methods[v_last_nocheck ] = function(name,range) return do_last (name,range,false) end
616
617methods[v_current] = function(name,range) -- range is ignored here
618    local top = stack[topofstack]
619    return top and top[parentname(name)] or ""
620end
621
622local function fetched(name,range,method)
623    local value = (methods[method] or methods[v_first])(name,range) or ""
624    if not trace_get then
625        -- no report
626    elseif value == "" then
627        report_marks("nothing fetched, name %a, range %a, method %a",name,range,method)
628    else
629        report_marks("marking fetched, name %a, range %a, method %a, value %a",name,range,method,value)
630    end
631    return value or ""
632end
633
634-- can be used at the lua end:
635
636marks.fetched = fetched
637
638-- this will move to a separate runtime modules
639
640marks.tracers = marks.tracers or { }
641
642function marks.tracers.showtable()
643    context.starttabulate { "|l|l|l|lp|lp|" }
644    context.tabulaterowbold("name","parent","chain","children","fullchain")
645    context.ML()
646    for k, v in table.sortedpairs(data) do
647        local parent    = v.parent    or ""
648        local chain     = v.chain     or ""
649        local children  = v.children  or { }
650        local fullchain = v.fullchain or { }
651        table.sort(children) -- in-place but harmless
652        context.tabulaterowtyp(k,parent,chain,concat(children," "),concat(fullchain," "))
653    end
654    context.stoptabulate()
655end
656
657-- pushing to context:
658
659-- local separator = context.nested.markingseparator
660-- local command   = context.nested.markingcommand
661-- local ctxconcat = context.concat
662
663-- local function fetchonemark(name,range,method)
664--     context(command(name,fetched(name,range,method)))
665-- end
666
667-- local function fetchtwomarks(name,range)
668--     ctxconcat( {
669--         command(name,fetched(name,range,v_first)),
670--         command(name,fetched(name,range,v_last)),
671--     }, separator(name))
672-- end
673
674-- local function fetchallmarks(name,range)
675--     ctxconcat( {
676--         command(name,fetched(name,range,v_previous)),
677--         command(name,fetched(name,range,v_first)),
678--         command(name,fetched(name,range,v_last)),
679--     }, separator(name))
680-- end
681
682    local ctx_separator = context.markingseparator
683    local ctx_command   = context.markingcommand
684
685    local function fetchonemark(name,range,method)
686        ctx_command(name,fetched(name,range,method))
687    end
688
689    local function fetchtwomarks(name,range)
690        ctx_command(name,fetched(name,range,v_first))
691        ctx_separator(name)
692        ctx_command(name,fetched(name,range,v_last))
693    end
694
695    local function fetchallmarks(name,range)
696        ctx_command(name,fetched(name,range,v_previous))
697        ctx_separator(name)
698        ctx_command(name,fetched(name,range,v_first))
699        ctx_separator(name)
700        ctx_command(name,fetched(name,range,v_last))
701    end
702
703function marks.fetch(name,range,method) -- chapter page first | chapter column:1 first
704    if trace_get then
705        report_marks("marking requested, name %a, range %a, method %a",name,range,method)
706    end
707    if method == "" or method == v_default then
708        fetchonemark(name,range,v_first)
709    elseif method == v_both then
710        fetchtwomarks(name,range)
711    elseif method == v_all then
712        fetchallmarks(name,range)
713    else
714        fetchonemark(name,range,method)
715    end
716end
717
718function marks.fetchonemark (name,range,method) fetchonemark (name,range,method) end
719function marks.fetchtwomarks(name,range)        fetchtwomarks(name,range       ) end
720function marks.fetchallmarks(name,range)        fetchallmarks(name,range       ) end
721
722-- here we have a few helpers .. will become commands.*
723
724local pattern = lpeg.afterprefix("li::")
725
726function marks.title(tag,n)
727    local listindex = lpegmatch(pattern,n)
728    if listindex then
729        commands.savedlisttitle(tag,listindex,"marking")
730    else
731        context(n)
732    end
733end
734
735function marks.number(tag,n) -- no spec
736    local listindex = lpegmatch(pattern,n)
737    if listindex then
738        commands.savedlistnumber(tag,listindex)
739    else
740        -- no prefix (as it is the prefix)
741        context(n)
742    end
743end
744
745-- interface
746
747implement { name = "markingtitle",       actions = marks.title,         arguments = "2 strings" }
748implement { name = "markingnumber",      actions = marks.number,        arguments = "2 strings" }
749
750implement { name = "definemarking",      actions = marks.define,        arguments = "2 strings" }
751implement { name = "relatemarking",      actions = marks.relate,        arguments = "2 strings" }
752implement { name = "setmarking",         actions = marks.set,           arguments = "2 strings" }
753implement { name = "resetmarking",       actions = marks.reset,         arguments = "string" }
754implement { name = "synchronizemarking", actions = marks.synchronize,   arguments = { "string", "integer", "string" } }
755implement { name = "getmarking",         actions = marks.fetch,         arguments = "3 strings" }
756implement { name = "fetchonemark",       actions = marks.fetchonemark,  arguments = "3 strings" }
757implement { name = "fetchtwomarks",      actions = marks.fetchtwomarks, arguments = "2 strings" }
758implement { name = "fetchallmarks",      actions = marks.fetchallmarks, arguments = "2 strings" }
759
760implement { name = "doifelsemarking",    actions = { marks.exists, commands.doifelse }, arguments = "string" }
761