strc-num.lua /size: 23 Kb    last modification: 2020-07-01 14:35
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(name,level,onlynumbers)
274    local cd = counterdata[name]
275    if cd then
276        local data    = cd.data
277        local compact = { }
278        for i=1,level or #data do
279            local d = data[i]
280            if d.number ~= 0 then
281                compact[i] = (onlynumbers and d.number) or d
282            end
283        end
284        return compact
285    end
286end
287
288-- depends on when incremented, before or after (driven by d.offset)
289
290function counters.previous(name,n)
291    return allocate(name,n).previous
292end
293
294function counters.next(name,n)
295    return allocate(name,n).next
296end
297
298counters.prev = counters.previous
299
300function counters.currentvalue(name,n)
301    return allocate(name,n).number
302end
303
304function counters.first(name,n)
305    return allocate(name,n).first
306end
307
308function counters.last(name,n)
309    return allocate(name,n).last
310end
311
312function counters.subs(name,n)
313    return counterdata[name].data[n].subs or 0
314end
315
316local function setvalue(name,tag,value)
317    local cd = counterdata[name]
318    if cd then
319        cd[tag] = value
320    end
321end
322
323counters.setvalue = setvalue
324
325function counters.setstate(name,value) -- true/false
326    value = variables[value]
327    if value then
328        setvalue(name,"state",value)
329    end
330end
331
332function counters.setlevel(name,value)
333    setvalue(name,"level",value)
334end
335
336function counters.setoffset(name,value)
337    setvalue(name,"offset",value)
338end
339
340local function synchronize(name,d)
341    local dc = d.counter
342    if dc then
343        if trace_counters then
344            report_counters("action %a, name %a, counter %a, value %a","synchronize",name,dc,d.number)
345        end
346        texsetcount("global",dc,d.number)
347    end
348    local cs = counterspecials[name]
349    if cs then
350        if trace_counters then
351            report_counters("action %a, name %a, counter %a","synccommand",name,dc)
352        end
353        cs(name)
354    end
355end
356
357local function reset(name,n)
358    local cd = counterdata[name]
359    if cd then
360        for i=n or 1,#cd.data do
361            local d = cd.data[i]
362            savevalue(name,i)
363            local number = d.start or 0
364            d.number = number
365            d.own = nil
366            if trace_counters then
367                report_counters("action %a, name %a, sub %a, value %a","reset",name,i,number)
368            end
369            synchronize(name,d)
370        end
371        cd.numbers = nil
372    else
373    end
374end
375
376local function set(name,n,value)
377    local cd = counterdata[name]
378    if cd then
379        local d = allocate(name,n)
380        local number = value or 0
381        d.number = number
382        d.own = nil
383        if trace_counters then
384            report_counters("action %a, name %a, sub %a, value %a","set",name,"no",number)
385        end
386        synchronize(name,d)
387    end
388end
389
390local function check(name,data,start,stop)
391    for i=start or 1,stop or #data do
392        local d = data[i]
393        savevalue(name,i)
394        local number = d.start or 0
395        d.number = number
396        d.own = nil
397        if trace_counters then
398            report_counters("action %a, name %a, sub %a, value %a","check",name,i,number)
399        end
400        synchronize(name,d)
401    end
402end
403
404
405local function setown(name,n,value)
406    local cd = counterdata[name]
407    if cd then
408        local d = allocate(name,n)
409        d.own = value
410        d.number = (d.number or d.start or 0) + (d.step or 0)
411        local level = cd.level
412        if not level or level == -1 then
413            -- -1 is signal that we reset manually
414        elseif level > 0 or level == -3 then
415            check(name,d,n+1)
416        elseif level == 0 then
417            -- happens elsewhere, check this for block
418        end
419        synchronize(name,d)
420    end
421end
422
423local function restart(name,n,newstart,noreset)
424    local cd = counterdata[name]
425    if cd then
426        newstart = tonumber(newstart)
427        if newstart then
428            local d = allocate(name,n)
429            d.start = newstart
430            if not noreset then  -- why / when needed ?
431                reset(name,n) -- hm
432            end
433        end
434    end
435end
436
437function counters.save(name) -- or just number
438    local cd = counterdata[name]
439    if cd then
440        insert(cd.saved,copy(cd.data))
441    end
442end
443
444function counters.restore(name)
445    local cd = counterdata[name]
446    if not cd then
447        report_counters("invalid restore, no counter %a",name)
448        return
449    end
450    local saved = cd.saved
451    if not saved then
452        -- is ok
453    elseif #saved > 0 then
454        cd.data = remove(saved)
455    else
456        report_counters("restore without save for counter %a",name)
457    end
458end
459
460local function add(name,n,delta)
461    local cd = counterdata[name]
462    if cd and (cd.state == v_start or cd.state == "") then
463        local data = cd.data
464        local d = allocate(name,n)
465        d.number = (d.number or d.start or 0) + delta*(d.step or 0)
466     -- d.own = nil
467        local level = cd.level
468        if not level or level == -1 then
469            -- -1 is signal that we reset manually
470            if trace_counters then
471                report_counters("action %a, name %a, sub %a, how %a","add",name,"no","no checking")
472            end
473        elseif level == -2 then
474            -- -2 is signal that we work per text
475            if trace_counters then
476                report_counters("action %a, name %a, sub %a, how %a","add",name,"text","checking")
477            end
478            check(name,data,n+1)
479        elseif level > 0 or level == -3 then
480            -- within countergroup
481            if trace_counters then
482                report_counters("action %a, name %a, sub %a, how %a","add",name,level,"checking within group")
483            end
484            check(name,data,n+1)
485        elseif level == 0 then
486            -- happens elsewhere
487            if trace_counters then
488                report_counters("action %a, name %a, sub %a, how %a","add",name,level,"no checking")
489            end
490        else
491            if trace_counters then
492                report_counters("action %a, name %a, sub %a, how %a","add",name,"unknown","no checking")
493            end
494        end
495        synchronize(name,d)
496        return d.number -- not needed
497    end
498    return 0
499end
500
501function counters.check(level)
502    for name, cd in next, counterdata do
503        if level > 0 and cd.level == -3 then -- could become an option
504            if trace_counters then
505                report_counters("action %a, name %a, sub %a, detail %a","reset",name,level,"head")
506            end
507            reset(name)
508        elseif cd.level == level then
509            if trace_counters then
510                report_counters("action %a, name %a, sub %a, detail %a","reset",name,level,"normal")
511            end
512            reset(name)
513        end
514    end
515end
516
517local function get(name,n,key)
518    local d = allocate(name,n)
519    d = d and d[key]
520    if not d then
521        return 0
522    elseif type(d) == "function" then
523        return d()
524    else
525        return d
526    end
527end
528
529counters.reset   = reset
530counters.set     = set
531counters.add     = add
532counters.get     = get
533counters.setown  = setown
534counters.restart = restart
535
536function counters.value(name,n) -- what to do with own
537    return get(name,n or 1,'number') or 0
538end
539
540function counters.converted(name,spec) -- name can be number and reference to storage
541    local cd
542    if type(name) == "number" then
543        cd = specials.retrieve("counter",name)
544        if cd then
545            cd = cd.counter
546        end
547    else
548        cd = counterdata[name]
549    end
550    if cd then
551        local spec       = spec or { }
552        local numbers    = { }
553        local ownnumbers = { }
554        local reverse    = spec.order == v_reverse
555        local kind       = spec.type or "number"
556        local data       = cd.data
557        for k=1,#data do
558            local v = data[k]
559            -- somewhat messy, what if subnr? only last must honour kind?
560            local vn
561            if v.own then
562                numbers[k]    = v.number
563                ownnumbers[k] = v.own
564            else
565                if kind == v_first then
566                    vn = v.first
567                elseif kind == v_next then
568                    vn = v.next
569                elseif kind == v_prev or kind == v_previous then
570                    vn = v.prev
571                elseif kind == v_last then
572                    vn = v.last
573                else
574                    vn = v.number
575                    if reverse then
576                        local vf = v.first
577                        local vl = v.last
578                        if vl > 0 then
579                        --  vn = vl - vn + 1 + vf
580                            vn = vl - vn + vf -- see testbed for test
581                        end
582                    end
583                end
584                numbers[k]    = vn or v.number
585                ownnumbers[k] = nil
586            end
587        end
588        cd.numbers    = numbers
589        cd.ownnumbers = ownnumbers
590        sections.typesetnumber(cd,'number',spec)
591        cd.numbers    = nil
592        cd.ownnumbers = nil
593    end
594end
595
596-- interfacing
597
598local function showcounter(name)
599    local cd = counterdata[name]
600    if cd then
601        context("[%s:",name)
602        local data = cd.data
603        for i=1,#data do
604            local d = data[i]
605            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)
606        end
607        context("]")
608    end
609end
610
611-- the noreset is somewhat messy ... always false messes up e.g. itemize but true the pagenumbers
612--
613-- if this fails i'll clean up this still somewhat experimental mechanism (but i need use cases)
614
615local function checkcountersetup(name,level,start,state)
616    local noreset = true -- level > 0 -- was true
617    counters.restart(name,1,start,noreset) -- was true
618    counters.setstate(name,state)
619    counters.setlevel(name,level)
620    sections.setchecker(name,level,counters.reset)
621end
622
623--
624
625implement { name = "addcounter",              actions = add,     arguments = { "string", "integer", "integer" } }
626implement { name = "setcounter",              actions = set,     arguments = { "string", 1, "integer" } }
627implement { name = "setowncounter",           actions = setown,  arguments = { "string", 1, "string" } }
628implement { name = "restartcounter",          actions = restart, arguments = { "string", 1, "integer" } }
629implement { name = "resetcounter",            actions = reset,   arguments = { "string", 1 } }
630implement { name = "incrementcounter",        actions = add,     arguments = { "string", 1,  1 } }
631implement { name = "decrementcounter",        actions = add,     arguments = { "string", 1, -1 } }
632
633implement { name = "setsubcounter",           actions = set,     arguments = { "string", "integer", "integer" } }
634implement { name = "setownsubcounter",        actions = setown,  arguments = { "string", "integer", "string" } }
635implement { name = "restartsubcounter",       actions = restart, arguments = { "string", "integer", "integer" } }
636implement { name = "resetsubcounter",         actions = reset,   arguments = { "string", "integer" } }
637implement { name = "incrementsubcounter",     actions = add,     arguments = { "string", "integer",  1 } }
638implement { name = "decrementsubcounter",     actions = add,     arguments = { "string", "integer", -1 } }
639
640implement { name = "rawcountervalue",         actions = { counters.raw     , context }, arguments = { "string", 1 } }
641implement { name = "countervalue",            actions = { counters.value   , context }, arguments = { "string", 1 } }
642implement { name = "lastcountervalue",        actions = { counters.last    , context }, arguments = { "string", 1 } }
643implement { name = "firstcountervalue",       actions = { counters.first   , context }, arguments = { "string", 1 } }
644implement { name = "nextcountervalue",        actions = { counters.next    , context }, arguments = { "string", 1 } }
645implement { name = "previouscountervalue",    actions = { counters.previous, context }, arguments = { "string", 1 } }
646implement { name = "subcountervalues",        actions = { counters.subs    , context }, arguments = { "string", 1 } }
647
648implement { name = "rawsubcountervalue",      actions = { counters.raw     , context }, arguments = { "string", "integer" } }
649implement { name = "subcountervalue",         actions = { counters.value   , context }, arguments = { "string", "integer" } }
650implement { name = "lastsubcountervalue",     actions = { counters.last    , context }, arguments = { "string", "integer" } }
651implement { name = "firstsubcountervalue",    actions = { counters.first   , context }, arguments = { "string", "integer" } }
652implement { name = "nextsubcountervalue",     actions = { counters.next    , context }, arguments = { "string", "integer" } }
653implement { name = "previoussubcountervalue", actions = { counters.previous, context }, arguments = { "string", "integer" } }
654implement { name = "subsubcountervalues",     actions = { counters.subs    , context }, arguments = { "string", "integer" } }
655
656implement { name = "savecounter",             actions = counters.save,    arguments = "string" }
657implement { name = "restorecounter",          actions = counters.restore, arguments = "string" }
658
659implement { name = "incrementedcounter",      actions = { add, context }, arguments = { "string", 1,  1 } }
660implement { name = "decrementedcounter",      actions = { add, context }, arguments = { "string", 1, -1 } }
661
662implement { name = "showcounter",             actions = showcounter,       arguments = "string" }  -- todo
663implement { name = "checkcountersetup",       actions = checkcountersetup, arguments = { "string", "integer", "integer", "string" } }
664
665setmetatablecall(counterdata,function(t,k) return t[k] end)
666
667implement { name = "doifelsecounter", actions = { counterdata, commands.doifelse }, arguments = "string" }
668implement { name = "doifcounter",     actions = { counterdata, commands.doif     }, arguments = "string" }
669implement { name = "doifnotcounter",  actions = { counterdata, commands.doifnot  }, arguments = "string" }
670
671implement {
672    name      = "definecounter",
673    actions   = counters.define,
674    arguments = {
675        {
676            { "name" } ,
677            { "start", "integer" },
678            { "counter" },
679            { "method" },
680        }
681    }
682}
683
684------------------------------------------------------------------
685------------------------------------------------------------------
686
687-- -- move to strc-pag.lua
688--
689-- function counters.analyze(name,counterspecification)
690--     local cd = counterdata[name]
691--     -- safeguard
692--     if not cd then
693--         return false, false, "no counter data"
694--     end
695--     -- section data
696--     local sectiondata = sections.current()
697--     if not sectiondata then
698--         return cd, false, "not in section"
699--     end
700--     local references = sectiondata.references
701--     if not references then
702--         return cd, false, "no references"
703--     end
704--     local section = references.section
705--     if not section then
706--         return cd, false, "no section"
707--     end
708--     sectiondata = sections.collected[references.section]
709--     if not sectiondata then
710--         return cd, false, "no section data"
711--     end
712--     -- local preferences
713--     local no = v_no
714--     if counterspecification and counterspecification.prefix == no then
715--         return cd, false, "current spec blocks prefix"
716--     end
717--     -- stored preferences (not used)
718--     if cd.prefix == no then
719--         return cd, false, "entry blocks prefix"
720--     end
721--     -- sectioning
722--     -- if sectiondata.prefix == no then
723--     --     return false, false, "sectiondata blocks prefix"
724--     -- end
725--     -- final verdict
726--     return cd, sectiondata, "okay"
727-- end
728--
729-- function counters.prefixedconverted(name,prefixspec,numberspec)
730--     local cd, prefixdata, result = counters.analyze(name,prefixspec)
731--     if cd then
732--         if prefixdata then
733--             sections.typesetnumber(prefixdata,"prefix",prefixspec or false,cd or false)
734--         end
735--         counters.converted(name,numberspec)
736--     end
737-- end
738