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