strc-num.lua /size: 23 Kb    last modification: 2025-02-21 11:03
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 or 1).previous
310end
311
312function counters.next(name,n)
313    return allocate(name,n or 1).next
314end
315
316counters.prev = counters.previous
317
318function counters.currentvalue(name,n)
319    return allocate(name,n or 1).number
320end
321
322function counters.first(name,n)
323    return allocate(name,n or 1).first
324end
325
326function counters.last(name,n)
327    return allocate(name,n or 1).last
328end
329
330function counters.subs(name,n)
331    return counterdata[name].data[n or 1].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 or 1)
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 or 1)
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 or 1)
448            d.start = newstart
449            if not noreset then  -- why / when needed ?
450                reset(name,n or 1) -- 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 or 1)
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