strc-num.lua /size: 23 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['strc-num'] = {
2    version   = 1.001,
3    comment   = "companion to strc-num.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
9local format = string.format
10local next, type, tonumber = next, type, tonumber
11local min, max = math.min, math.max
12local insert, remove, copy = table.insert, table.remove, table.copy
13local texsetcount = tex.setcount
14
15-- Counters are managed here. They can have multiple levels which makes it easier to synchronize
16-- them. Synchronization is sort of special anyway, as it relates to document structuring.
17
18local context           = context
19
20local allocate          = utilities.storage.allocate
21local setmetatableindex = table.setmetatableindex
22local setmetatablecall  = table.setmetatablecall
23
24local trace_counters    = false  trackers.register("structures.counters", function(v) trace_counters = v end)
25local report_counters   = logs.reporter("structure","counters")
26
27local implement         = interfaces.implement
28
29local structures        = structures
30local helpers           = structures.helpers
31local sections          = structures.sections
32local counters          = structures.counters
33local documents         = structures.documents
34
35local variables         = interfaces.variables
36local v_start           = variables.start
37local v_page            = variables.page
38local v_reverse         = variables.reverse
39local v_first           = variables.first
40local v_next            = variables.next
41local v_previous        = variables.previous
42local v_prev            = variables.prev
43local v_last            = variables.last
44----- v_no              = variables.no
45
46-- states: start stop none reset
47
48-- specials are used for counters that are set and incremented in special ways, like
49-- pagecounters that get this treatment in the page builder
50
51counters.specials       = counters.specials or { }
52local counterspecials   = counters.specials
53
54local counterranges, tbs = { }, 0
55
56counters.collected = allocate()
57counters.tobesaved = counters.tobesaved or { }
58counters.data      = counters.data or { }
59
60storage.register("structures/counters/data",      counters.data,      "structures.counters.data")
61storage.register("structures/counters/tobesaved", counters.tobesaved, "structures.counters.tobesaved")
62
63local collected   = counters.collected
64local tobesaved   = counters.tobesaved
65local counterdata = counters.data
66
67local function initializer() -- not really needed
68    collected   = counters.collected
69    tobesaved   = counters.tobesaved
70    counterdata = counters.data
71end
72
73local function finalizer()
74    for name, cd in next, counterdata do
75        local cs = tobesaved[name]
76        local data = cd.data
77        for i=1,#data do
78            local d = data[i]
79            local r = d.range
80            cs[i][r] = d.number
81            d.range = r + 1
82        end
83    end
84end
85
86job.register('structures.counters.collected', tobesaved, initializer, finalizer)
87
88local constructor = { -- maybe some day we will provide an installer for more variants
89
90    last = function(t,name,i)
91        local cc = collected[name]
92        local stop = (cc and cc[i] and cc[i][t.range]) or 0 -- stop is available for diagnostics purposes only
93        t.stop = stop
94        if t.offset then
95            return stop - t.step
96        else
97            return stop
98        end
99    end,
100
101    first = function(t,name,i)
102        local start = t.start
103        if start > 0 then
104            return start -- brrr
105        elseif t.offset then
106            return start + t.step + 1
107        else
108            return start + 1
109        end
110    end,
111
112    prev = function(t,name,i)
113        return max(t.first,t.number-1) -- todo: step
114    end,
115
116    previous = function(t,name,i)
117        return max(t.first,t.number-1) -- todo: step
118    end,
119
120    next = function(t,name,i)
121        return min(t.last,t.number+1) -- todo: step
122    end,
123
124    backward =function(t,name,i)
125        if t.number - 1 < t.first then
126            return t.last
127        else
128            return t.previous
129        end
130    end,
131
132    forward = function(t,name,i)
133        if t.number + 1 > t.last then
134            return t.first
135        else
136            return t.next
137        end
138    end,
139
140    subs = function(t,name,i)
141        local cc = collected[name]
142        t.subs = (cc and cc[i+1] and cc[i+1][t.range]) or 0
143        return t.subs
144    end,
145
146}
147
148local function dummyconstructor(t,name,i)
149    return nil -- was 0, but that is fuzzy in testing for e.g. own
150end
151
152setmetatableindex(constructor,function(t,k)
153 -- if trace_counters then
154 --     report_counters("unknown constructor %a",k)
155 -- end
156    return dummyconstructor
157end)
158
159local function enhance()
160    for name, cd in next, counterdata do
161        local data = cd.data
162        for i=1,#data do
163            local ci = data[i]
164            setmetatableindex(ci, function(t,s) return constructor[s](t,name,i) end)
165        end
166    end
167    enhance = nil
168end
169
170local function allocate(name,i) -- can be metatable but it's a bit messy
171    local cd = counterdata[name]
172    if not cd then
173        cd = {
174            level   = 1,
175         -- block   = "", -- todo
176            numbers = nil,
177            state   = v_start, -- true
178            data    = { },
179            saved   = { },
180        }
181        tobesaved[name]   = { }
182        counterdata[name] = cd
183    end
184    cd = cd.data
185    local ci = cd[i]
186    if not ci then
187        for i=1,i do
188            if not cd[i] then
189                ci = {
190                    number = 0,
191                    start  = 0,
192                    saved  = 0,
193                    step   = 1,
194                    range  = 1,
195                    offset = false,
196                    stop   = 0, -- via metatable: last, first, stop only for tracing
197                }
198                setmetatableindex(ci, function(t,s) return constructor[s](t,name,i) end)
199                cd[i] = ci
200                tobesaved[name][i] = { }
201            end
202        end
203    elseif enhance then
204        enhance() -- not stored in bytecode
205    end
206    return ci
207end
208
209local pattern   = lpeg.P(variables.by)^-1 * lpeg.C(lpeg.P(1)^1)
210local lpegmatch = lpeg.match
211
212function counters.way(way)
213    if not way or way == "" then
214        return ""
215    else
216        return lpegmatch(pattern,way)
217    end
218end
219
220implement {
221    name      = "way",
222    actions   = { counters.way, context },
223    arguments = "string"
224}
225
226
227function counters.record(name,i)
228    return allocate(name,i or 1)
229end
230
231local function savevalue(name,i)
232    if name then
233        local cd = counterdata[name].data[i]
234        local cs = tobesaved[name][i]
235        local cc = collected[name]
236        if trace_counters then
237            report_counters("action %a, counter %s, value %s","save",name,cd.number)
238        end
239        local cr = cd.range
240        local old = (cc and cc[i] and cc[i][cr]) or 0
241        local number = cd.number
242        if cd.method == v_page then
243            -- we can be one page ahead
244            number = number - 1
245        end
246        cs[cr] = (number >= 0) and number or 0
247        cd.range = cr + 1
248        return old
249    else
250        return 0
251    end
252end
253
254function counters.define(specification)
255    local name = specification.name
256    if name and name ~= "" then
257        -- todo: step
258        local d = allocate(name,1)
259        d.start = tonumber(specification.start) or 0
260        d.state = v_state or ""
261        local counter = specification.counter
262        if counter and counter ~= "" then
263            d.counter = counter -- only for special purposes, cannot be false
264            d.method  = specification.method -- frozen at define time
265        end
266    end
267end
268
269function counters.raw(name)
270    return counterdata[name]
271end
272
273function counters.compact(target,name,level)
274    local cd = counterdata[name]
275    if cd then
276        local data       = cd.data
277        local numbers    = { }
278        local ownnumbers = { }
279        local depth      = #data
280        if not level or level == 0 then
281            level = depth
282        elseif level > depth then
283            level = depth
284        end
285
286        for i=1,level do
287            local d = data[i]
288            if d then
289                local n = d.number
290                local o = d.own
291                if n ~= 0 then
292                    numbers[i] = n
293                end
294                if o ~= "" then
295                    ownnumbers[i] = o
296                end
297            end
298        end
299        target.numbers = numbers
300        if next(ownnumbers) then
301            target.ownnumbers = ownnumbers
302        end
303    end
304end
305
306-- depends on when incremented, before or after (driven by d.offset)
307
308function counters.previous(name,n)
309    return allocate(name,n).previous
310end
311
312function counters.next(name,n)
313    return allocate(name,n).next
314end
315
316counters.prev = counters.previous
317
318function counters.currentvalue(name,n)
319    return allocate(name,n).number
320end
321
322function counters.first(name,n)
323    return allocate(name,n).first
324end
325
326function counters.last(name,n)
327    return allocate(name,n).last
328end
329
330function counters.subs(name,n)
331    return counterdata[name].data[n].subs or 0
332end
333
334local function setvalue(name,tag,value)
335    local cd = counterdata[name]
336    if cd then
337        cd[tag] = value
338    end
339end
340
341counters.setvalue = setvalue
342
343function counters.setstate(name,value) -- true/false
344    value = variables[value]
345    if value then
346        setvalue(name,"state",value)
347    end
348end
349
350function counters.setlevel(name,value)
351    setvalue(name,"level",value)
352end
353
354function counters.setoffset(name,value)
355    setvalue(name,"offset",value)
356end
357
358local function synchronize(name,d)
359    local dc = d.counter
360    if dc then
361        if trace_counters then
362            report_counters("action %a, name %a, counter %a, value %a","synchronize",name,dc,d.number)
363        end
364        texsetcount("global",dc,d.number)
365    end
366    local cs = counterspecials[name]
367    if cs then
368        if trace_counters then
369            report_counters("action %a, name %a, counter %a","synccommand",name,dc)
370        end
371        cs(name)
372    end
373end
374
375local function reset(name,n)
376    local cd = counterdata[name]
377    if cd then
378        local data = cd.data
379        for i=n or 1,#data do
380            local d = data[i]
381            savevalue(name,i)
382            local number = d.start or 0
383            d.number = number
384            d.own = nil
385            if trace_counters then
386                report_counters("action %a, name %a, sub %a, value %a","reset",name,i,number)
387            end
388            synchronize(name,d)
389        end
390        cd.numbers = nil
391    else
392    end
393end
394
395local function set(name,n,value)
396    local cd = counterdata[name]
397    if cd then
398        local d = allocate(name,n)
399        local number = value or 0
400        d.number = number
401        d.own = nil
402        if trace_counters then
403            report_counters("action %a, name %a, sub %a, value %a","set",name,"no",number)
404        end
405        synchronize(name,d)
406    end
407end
408
409local function check(name,data,start,stop)
410    for i=start or 1,stop or #data do
411        local d = data[i]
412        savevalue(name,i)
413        local number = d.start or 0
414        d.number = number
415        d.own = nil
416        if trace_counters then
417            report_counters("action %a, name %a, sub %a, value %a","check",name,i,number)
418        end
419        synchronize(name,d)
420    end
421end
422
423
424local function setown(name,n,value)
425    local cd = counterdata[name]
426    if cd then
427        local d = allocate(name,n)
428        d.own = value
429        d.number = (d.number or d.start or 0) + (d.step or 0)
430        local level = cd.level
431        if not level or level == -1 then
432            -- -1 is signal that we reset manually
433        elseif level > 0 or level == -3 then
434            check(name,d,n+1)
435        elseif level == 0 then
436            -- happens elsewhere, check this for block
437        end
438        synchronize(name,d)
439    end
440end
441
442local function restart(name,n,newstart,noreset)
443    local cd = counterdata[name]
444    if cd then
445        newstart = tonumber(newstart)
446        if newstart then
447            local d = allocate(name,n)
448            d.start = newstart
449            if not noreset then  -- why / when needed ?
450                reset(name,n) -- hm
451            end
452        end
453    end
454end
455
456function counters.save(name) -- or just number
457    local cd = counterdata[name]
458    if cd then
459        insert(cd.saved,copy(cd.data))
460    end
461end
462
463function counters.restore(name)
464    local cd = counterdata[name]
465    if not cd then
466        report_counters("invalid restore, no counter %a",name)
467        return
468    end
469    local saved = cd.saved
470    if not saved then
471        -- is ok
472    elseif #saved > 0 then
473        cd.data = remove(saved)
474    else
475        report_counters("restore without save for counter %a",name)
476    end
477end
478
479local function add(name,n,delta)
480    local cd = counterdata[name]
481    if cd and (cd.state == v_start or cd.state == "") then
482        local data = cd.data
483        local d = allocate(name,n)
484        d.number = (d.number or d.start or 0) + delta*(d.step or 0)
485     -- d.own = nil
486        local level = cd.level
487        if not level or level == -1 then
488            -- -1 is signal that we reset manually
489            if trace_counters then
490                report_counters("action %a, name %a, sub %a, how %a","add",name,"no","no checking")
491            end
492        elseif level == -2 then
493            -- -2 is signal that we work per text
494            if trace_counters then
495                report_counters("action %a, name %a, sub %a, how %a","add",name,"text","checking")
496            end
497            check(name,data,n+1)
498        elseif level > 0 or level == -3 then
499            -- within countergroup
500            if trace_counters then
501                report_counters("action %a, name %a, sub %a, how %a","add",name,level,"checking within group")
502            end
503            check(name,data,n+1)
504        elseif level == 0 then
505            -- happens elsewhere
506            if trace_counters then
507                report_counters("action %a, name %a, sub %a, how %a","add",name,level,"no checking")
508            end
509        else
510            if trace_counters then
511                report_counters("action %a, name %a, sub %a, how %a","add",name,"unknown","no checking")
512            end
513        end
514        synchronize(name,d)
515        return d.number -- not needed
516    end
517    return 0
518end
519
520function counters.check(level)
521    for name, cd in next, counterdata do
522        if level > 0 and cd.level == -3 then -- could become an option
523            if trace_counters then
524                report_counters("action %a, name %a, sub %a, detail %a","reset",name,level,"head")
525            end
526            reset(name)
527        elseif cd.level == level then
528            if trace_counters then
529                report_counters("action %a, name %a, sub %a, detail %a","reset",name,level,"normal")
530            end
531            reset(name)
532        end
533    end
534end
535
536local function get(name,n,key)
537    local d = allocate(name,n)
538    d = d and d[key]
539    if not d then
540        return 0
541    elseif type(d) == "function" then
542        return d()
543    else
544        return d
545    end
546end
547
548counters.reset   = reset
549counters.set     = set
550counters.add     = add
551counters.get     = get
552counters.setown  = setown
553counters.restart = restart
554
555function counters.value(name,n) -- what to do with own
556    return get(name,n or 1,'number') or 0
557end
558
559function counters.converted(name,spec) -- name can be number and reference to storage
560    local cd
561    if type(name) == "number" then
562        cd = specials.retrieve("counter",name)
563        if cd then
564            cd = cd.counter
565        end
566    else
567        cd = counterdata[name]
568    end
569    if cd then
570        local spec       = spec or { }
571        local numbers    = { }
572        local ownnumbers = { }
573        local reverse    = spec.order == v_reverse
574        local kind       = spec.type or "number"
575        local data       = cd.data
576        for k=1,#data do
577            local v = data[k]
578            -- somewhat messy, what if subnr? only last must honour kind?
579            local vn
580            if v.own then
581                numbers[k]    = v.number
582                ownnumbers[k] = v.own
583            else
584                if kind == v_first then
585                    vn = v.first
586                elseif kind == v_next then
587                    vn = v.next
588                elseif kind == v_prev or kind == v_previous then
589                    vn = v.prev
590                elseif kind == v_last then
591                    vn = v.last
592                else
593                    vn = v.number
594                    if reverse then
595                        local vf = v.first
596                        local vl = v.last
597                        if vl > 0 then
598                        --  vn = vl - vn + 1 + vf
599                            vn = vl - vn + vf -- see testbed for test
600                        end
601                    end
602                end
603                numbers[k]    = vn or v.number
604                ownnumbers[k] = nil
605            end
606        end
607        cd.numbers    = numbers
608        cd.ownnumbers = ownnumbers
609        sections.typesetnumber(cd,'number',spec)
610        cd.numbers    = nil
611        cd.ownnumbers = nil
612    end
613end
614
615-- interfacing
616
617local function showcounter(name)
618    local cd = counterdata[name]
619    if cd then
620        context("[%s:",name)
621        local data = cd.data
622        for i=1,#data do
623            local d = data[i]
624            context(" (%s: %s,%s,%s s:%s r:%s)",i,d.start or 0,d.number or 0,d.last,d.step or 0,d.range or 0)
625        end
626        context("]")
627    end
628end
629
630-- the noreset is somewhat messy ... always false messes up e.g. itemize but true the pagenumbers
631--
632-- if this fails i'll clean up this still somewhat experimental mechanism (but i need use cases)
633
634local function checkcountersetup(name,level,start,state)
635    local noreset = true -- level > 0 -- was true
636    counters.restart(name,1,start,noreset) -- was true
637    counters.setstate(name,state)
638    counters.setlevel(name,level)
639    sections.setchecker(name,level,counters.reset)
640end
641
642--
643
644implement { name = "addcounter",              actions = add,     arguments = { "string", "integer", "integer" } }
645implement { name = "setcounter",              actions = set,     arguments = { "string", 1, "integer" } }
646implement { name = "setowncounter",           actions = setown,  arguments = { "string", 1, "string" } }
647implement { name = "restartcounter",          actions = restart, arguments = { "string", 1, "integer" } }
648implement { name = "resetcounter",            actions = reset,   arguments = { "string", 1 } }
649implement { name = "incrementcounter",        actions = add,     arguments = { "string", 1,  1 } }
650implement { name = "decrementcounter",        actions = add,     arguments = { "string", 1, -1 } }
651
652implement { name = "setsubcounter",           actions = set,     arguments = { "string", "integer", "integer" } }
653implement { name = "setownsubcounter",        actions = setown,  arguments = { "string", "integer", "string" } }
654implement { name = "restartsubcounter",       actions = restart, arguments = { "string", "integer", "integer" } }
655implement { name = "resetsubcounter",         actions = reset,   arguments = { "string", "integer" } }
656implement { name = "incrementsubcounter",     actions = add,     arguments = { "string", "integer",  1 } }
657implement { name = "decrementsubcounter",     actions = add,     arguments = { "string", "integer", -1 } }
658
659implement { name = "rawcountervalue",         actions = { counters.raw     , context }, arguments = { "string", 1 } }
660implement { name = "countervalue",            actions = { counters.value   , context }, arguments = { "string", 1 } }
661implement { name = "lastcountervalue",        actions = { counters.last    , context }, arguments = { "string", 1 } }
662implement { name = "firstcountervalue",       actions = { counters.first   , context }, arguments = { "string", 1 } }
663implement { name = "nextcountervalue",        actions = { counters.next    , context }, arguments = { "string", 1 } }
664implement { name = "previouscountervalue",    actions = { counters.previous, context }, arguments = { "string", 1 } }
665implement { name = "subcountervalues",        actions = { counters.subs    , context }, arguments = { "string", 1 } }
666
667implement { name = "rawsubcountervalue",      actions = { counters.raw     , context }, arguments = { "string", "integer" } }
668implement { name = "subcountervalue",         actions = { counters.value   , context }, arguments = { "string", "integer" } }
669implement { name = "lastsubcountervalue",     actions = { counters.last    , context }, arguments = { "string", "integer" } }
670implement { name = "firstsubcountervalue",    actions = { counters.first   , context }, arguments = { "string", "integer" } }
671implement { name = "nextsubcountervalue",     actions = { counters.next    , context }, arguments = { "string", "integer" } }
672implement { name = "previoussubcountervalue", actions = { counters.previous, context }, arguments = { "string", "integer" } }
673implement { name = "subsubcountervalues",     actions = { counters.subs    , context }, arguments = { "string", "integer" } }
674
675implement { name = "savecounter",             actions = counters.save,    arguments = "string" }
676implement { name = "restorecounter",          actions = counters.restore, arguments = "string" }
677
678implement { name = "incrementedcounter",      actions = { add, context }, arguments = { "string", 1,  1 } }
679implement { name = "decrementedcounter",      actions = { add, context }, arguments = { "string", 1, -1 } }
680
681implement { name = "showcounter",             actions = showcounter,       arguments = "string" }  -- todo
682implement { name = "checkcountersetup",       actions = checkcountersetup, arguments = { "string", "integer", "integer", "string" } }
683
684setmetatablecall(counterdata,function(t,k) return t[k] end)
685
686implement { name = "doifelsecounter", actions = { counterdata, commands.doifelse }, arguments = "string" }
687implement { name = "doifcounter",     actions = { counterdata, commands.doif     }, arguments = "string" }
688implement { name = "doifnotcounter",  actions = { counterdata, commands.doifnot  }, arguments = "string" }
689
690implement {
691    name      = "definecounter",
692    actions   = counters.define,
693    arguments = {
694        {
695            { "name" } ,
696            { "start", "integer" },
697            { "counter" },
698            { "method" },
699        }
700    }
701}
702
703------------------------------------------------------------------
704------------------------------------------------------------------
705
706-- -- move to strc-pag.lua
707--
708-- function counters.analyze(name,counterspecification)
709--     local cd = counterdata[name]
710--     -- safeguard
711--     if not cd then
712--         return false, false, "no counter data"
713--     end
714--     -- section data
715--     local sectiondata = sections.current()
716--     if not sectiondata then
717--         return cd, false, "not in section"
718--     end
719--     local references = sectiondata.references
720--     if not references then
721--         return cd, false, "no references"
722--     end
723--     local section = references.section
724--     if not section then
725--         return cd, false, "no section"
726--     end
727--     sectiondata = sections.collected[references.section]
728--     if not sectiondata then
729--         return cd, false, "no section data"
730--     end
731--     -- local preferences
732--     local no = v_no
733--     if counterspecification and counterspecification.prefix == no then
734--         return cd, false, "current spec blocks prefix"
735--     end
736--     -- stored preferences (not used)
737--     if cd.prefix == no then
738--         return cd, false, "entry blocks prefix"
739--     end
740--     -- sectioning
741--     -- if sectiondata.prefix == no then
742--     --     return false, false, "sectiondata blocks prefix"
743--     -- end
744--     -- final verdict
745--     return cd, sectiondata, "okay"
746-- end
747--
748-- function counters.prefixedconverted(name,prefixspec,numberspec)
749--     local cd, prefixdata, result = counters.analyze(name,prefixspec)
750--     if cd then
751--         if prefixdata then
752--             sections.typesetnumber(prefixdata,"prefix",prefixspec or false,cd or false)
753--         end
754--         counters.converted(name,numberspec)
755--     end
756-- end
757