strc-not.lmt /size: 16 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['strc-not'] = {
2    version   = 1.001,
3    comment   = "companion to strc-not.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, tonumber = next, tonumber
11local sortedhash = table.sortedhash
12
13local trace_notes         = false  trackers.register("structures.notes",            function(v) trace_notes      = v end)
14local trace_references    = false  trackers.register("structures.notes.references", function(v) trace_references = v end)
15
16local report_notes        = logs.reporter("structure","notes")
17
18local structures          = structures
19local helpers             = structures.helpers
20local lists               = structures.lists
21local sections            = structures.sections
22local counters            = structures.counters
23local notes               = structures.notes
24local references          = structures.references
25local counterspecials     = counters.specials
26
27local texgetcount         = tex.getcount
28local texgetglue          = tex.getglue
29local texgetbox           = tex.getbox
30local getinsertmultiplier = tex.getinsertmultiplier
31local getmark             = tex.getmark
32
33local nuts                = nodes.nuts
34local tonut               = nodes.tonut
35local tonode              = nuts.tonode
36
37local setlink             = nuts.setlink
38local gettail             = nuts.tail
39local vpack               = nuts.vpack
40
41local newgluespec         = nuts.pool.gluespec -- nodes.pool.gluespec
42
43-- todo: allocate
44
45notes.states           = notes.states or { }
46lists.enhancers        = lists.enhancers or { }
47notes.numbers          = notes.numbers or { }
48
49storage.register("structures/notes/states",  notes.states, "structures.notes.states")
50storage.register("structures/notes/numbers", notes.numbers, "structures.notes.numbers")
51
52local notestates = notes.states
53local notedata   = table.setmetatableindex("table")
54
55local context    = context
56local commands   = commands
57
58local implement  = interfaces.implement
59
60local v_page     <const> = interfaces.variables.page
61
62-- state: store, insert, postpone
63
64local function store(tag,n)
65    -- somewhat weird but this is a cheap hook spot
66    if not counterspecials[tag] then
67        counterspecials[tag] = function(tag)
68            context.doresetlinenotecompression(tag) -- maybe flag that controls it
69        end
70    end
71    --
72    local nd = notedata[tag]
73    local nnd = #nd + 1
74    nd[nnd] = n
75    local state = notestates[tag]
76    if not state then
77        report_notes("unknown state for %a",tag)
78    elseif state.kind ~= "insert" then
79        if trace_notes then
80            report_notes("storing %a with state %a as %a",tag,state.kind,nnd)
81        end
82        state.start = state.start or nnd
83    end
84    return nnd
85end
86
87notes.store = store
88
89implement {
90    name      = "storenote",
91    actions   = { store, context },
92    arguments = { "string", "integer" }
93}
94
95local function get(tag,n) -- tricky ... only works when defined
96    local nd = notedata[tag]
97    if not n then
98        n = #nd
99    end
100    nd = nd[n]
101    if nd then
102        if trace_notes then
103            report_notes("getting note %a of %a with listindex %a",n,tag,nd)
104        end
105        -- is this right?
106        local newdata = lists.cached[nd]
107        return newdata
108    end
109end
110
111local function getn(tag)
112    return #notedata[tag]
113end
114
115notes.get  = get
116notes.getn = getn
117
118-- we could make a special enhancer
119
120local function listindex(tag,n)
121    local ndt = notedata[tag]
122    return ndt and ndt[n]
123end
124
125notes.listindex = listindex
126
127implement {
128    name      = "notelistindex",
129    actions   = { listindex, context },
130    arguments = { "string", "integer" }
131}
132
133local function setstate(tag,newkind)
134    local state = notestates[tag]
135    if trace_notes then
136        report_notes("setting state of %a from %s to %s",tag,(state and state.kind) or "unset",newkind)
137    end
138    if not state then
139        state = {
140            kind = newkind
141        }
142        notestates[tag] = state
143    elseif newkind == "insert" then
144        if not state.start then
145            state.kind = newkind
146        end
147    else
148-- if newkind == "postpone" and state.kind == "store" then
149-- else
150        state.kind = newkind
151-- end
152    end
153    --  state.start can already be set and will be set when an entry is added or flushed
154    return state
155end
156
157local function getstate(tag)
158    local state = notestates[tag]
159    return state and state.kind or "unknown"
160end
161
162notes.setstate        = setstate
163notes.getstate        = getstate
164
165implement {
166    name      = "setnotestate",
167    actions   = setstate,
168    arguments = "2 strings",
169}
170
171implement {
172    name      = "getnotestate",
173    actions   = { getstate, context },
174    arguments = "string"
175}
176
177function notes.define(tag,kind,number)
178    local state = setstate(tag,kind)
179    notes.numbers[number] = state
180    state.number = number
181end
182
183implement {
184    name      = "definenote",
185    actions   = notes.define,
186    arguments = { "string", "string", "integer" }
187}
188
189function notes.save(tag,newkind)
190    local state = notestates[tag]
191    if state and not state.saved then
192        if trace_notes then
193            report_notes("saving state of %a, old: %a, new %a",tag,state.kind,newkind or state.kind)
194        end
195        state.saveddata = notedata[tag]
196        state.savedkind = state.kind
197        state.kind = newkind or state.kind
198        state.saved = true
199        notedata[tag] = { }
200    end
201end
202
203function notes.restore(tag,forcedstate)
204    local state = notestates[tag]
205    if state and state.saved then
206        if trace_notes then
207            report_notes("restoring state of %a, old: %a, new: %a",tag,state.kind,state.savedkind)
208        end
209        notedata[tag] = state.saveddata
210        state.kind = forcedstate or state.savedkind
211        state.saveddata = nil
212        state.saved = false
213    end
214end
215
216implement { name = "savenote",    actions = notes.save,    arguments = "2 strings" }
217implement { name = "restorenote", actions = notes.restore, arguments = "2 strings" }
218
219local function hascontent(tag)
220    local ok = notestates[tag]
221    if ok then
222        if ok.kind == "insert" then
223            ok = texgetbox(ok.number)
224            if ok then
225                ok = tbs.list
226                ok = lst and lst.next
227            end
228        else
229            ok = ok.start
230        end
231    end
232    return ok and true or false
233end
234
235notes.hascontent = hascontent
236
237implement {
238    name      = "doifnotecontent",
239    actions   = { hascontent, commands.doif },
240    arguments = "string",
241}
242
243local function internal(tag,n)
244    local nd = get(tag,n)
245    if nd then
246        local r = nd.references
247        if r then
248            local i = r.internal
249            return i and references.internals[i] -- dependency on references
250        end
251    end
252    return nil
253end
254
255local function ordered(kind,name,n)
256    local o = lists.ordered[kind]
257    o = o and o[name]
258    return o and o[n]
259end
260
261notes.internal = internal
262notes.ordered  = ordered
263
264-- local function onsamepageasprevious(tag)
265--     local same     = false
266--     local n        = getn(tag,n)
267--     local current  = get(tag,n)
268--     local previous = get(tag,n-1)
269--     if current and previous then
270--         local cr = current.references
271--         local pr = previous.references
272--         same = cr and pr and cr.realpage == pr.realpage
273--     end
274--     return same and true or false
275-- end
276
277local function onsamepageasprevious(tag)
278    local n        = getn(tag,n)
279    local current  = get(tag,n)
280    if not current then
281        return false
282    end
283    local cr = current.references
284    if not cr then
285        return false
286    end
287    local previous = get(tag,n-1)
288    if not previous then
289        return false
290    end
291    local pr = previous.references
292    if not pr then
293        return false
294    end
295    return cr.realpage == pr.realpage
296end
297
298notes.doifonsamepageasprevious = onsamepageasprevious
299
300implement {
301    name      = "doifnoteonsamepageasprevious",
302    actions   = { onsamepageasprevious, commands.doifelse },
303    arguments = "string",
304}
305
306function notes.checkpagechange(tag) -- called before increment !
307    local nd = notedata[tag] -- can be unset at first entry
308    if nd then
309        local current = ordered("note",tag,#nd)
310        local nextone = ordered("note",tag,#nd+1)
311        if nextone then
312            -- we can use data from the previous pass
313            if nextone.pagenumber.number > current.pagenumber.number then
314                counters.reset(tag)
315            end
316        elseif current then
317            -- we need to locate the next one, best guess
318            if texgetcount("realpageno") > current.pagenumber.number then
319                counters.reset(tag)
320            end
321        end
322    end
323end
324
325function notes.postpone()
326    if trace_notes then
327        report_notes("postponing all insert notes")
328    end
329    for tag, state in next, notestates do
330        if state.kind ~= "store" then
331            setstate(tag,"postpone")
332        end
333    end
334end
335
336implement {
337    name    = "postponenotes",
338    actions = notes.postpone
339}
340
341local function getinternal(tag,n)
342    local li = internal(tag,n)
343    if li then
344        local references = li.references
345        if references then
346            return references.internal or 0
347        end
348    end
349    return 0
350end
351
352local function getdeltapage(tag,n)
353    -- 0:unknown 1:textbefore, 2:textafter, 3:samepage
354    local li = internal(tag,n)
355    if li then
356        local references = li.references
357        if references then
358         -- local symb = structures.references.collected[""]["symb:"..tag..":"..n]
359            local rymb = structures.references.collected[""]
360            local symb = rymb and rymb["*"..(references.internal or 0)]
361            local notepage   = references.realpage or 0
362            local symbolpage = symb and symb.references.realpage or -1
363            if trace_references then
364                report_notes("note number %a of %a points from page %a to page %a",n,tag,symbolpage,notepage)
365            end
366            if notepage < symbolpage then
367                return 3 -- after
368            elseif notepage > symbolpage then
369                return 2 -- before
370            elseif notepage > 0 then
371                return 1 -- same
372            end
373        else
374            -- might be a note that is not flushed due to to deep
375            -- nesting in a vbox
376        end
377    end
378    return 0
379end
380
381notes.getinternal  = getinternal
382notes.getdeltapage = getdeltapage
383
384implement { name = "noteinternal",  actions = { getinternal,  context }, arguments = { "string", "integer" } }
385implement { name = "notedeltapage", actions = { getdeltapage, context }, arguments = { "string", "integer" } }
386
387local function flushnotes(tag,whatkind,how) -- store and postpone
388    local state = notestates[tag]
389    local kind = state.kind
390    if kind == whatkind then
391        local nd = notedata[tag]
392        local ns = state.start -- first index
393        if kind == "postpone" then
394            if nd and ns then
395                if trace_notes then
396                    report_notes("flushing state %a of %a from %a to %a",whatkind,tag,ns,#nd)
397                end
398                for i=ns,#nd do
399                    context.handlenoteinsert(tag,i)
400                end
401            end
402            state.start = nil
403            state.kind = "insert"
404        elseif kind == "store" then
405            if nd and ns then
406                if trace_notes then
407                    report_notes("flushing state %a of %a from %a to %a",whatkind,tag,ns,#nd)
408                end
409                -- todo: as registers: start, stop, inbetween
410                for i=ns,#nd do
411                    -- tricky : trialtypesetting
412                    if how == v_page then
413                        local rp = get(tag,i)
414                        rp = rp and rp.references
415                        rp = rp and rp.symbolpage or 0
416                        if rp > texgetcount("realpageno") then
417                            state.start = i
418                            return
419                        end
420                    end
421                    if i > ns then
422                        context.betweennoteitself(tag)
423                    end
424                    context.handlenoteitself(tag,i)
425                end
426            end
427            state.start = nil
428        elseif kind == "reset" then
429            if nd and ns then
430                if trace_notes then
431                    report_notes("flushing state %a of %a from %a to %a",whatkind,tag,ns,#nd)
432                end
433            end
434            state.start = nil
435        elseif trace_notes then
436            report_notes("not flushing state %a of %a",whatkind,tag)
437        end
438    elseif trace_notes then
439        report_notes("not flushing state %a of %a",whatkind,tag)
440    end
441end
442
443local function flushpostponednotes()
444    if trace_notes then
445        report_notes("flushing all postponed notes")
446    end
447    for tag, _ in next, notestates do
448        flushnotes(tag,"postpone")
449    end
450end
451
452implement {
453    name    = "flushpostponednotes",
454    actions = flushpostponednotes
455}
456
457implement {
458    name      = "flushnotes",
459    actions   = flushnotes,
460    arguments = "3 strings",
461}
462
463function notes.resetpostponed()
464    if trace_notes then
465        report_notes("resetting all postponed notes")
466    end
467    for tag, state in next, notestates do
468        if state.kind == "postpone" then
469            state.start = nil
470            state.kind = "insert"
471        end
472    end
473end
474
475implement {
476    name      = "notetitle",
477    actions   = function(tag,n) lists.savedlisttitle(tag,notedata[tag][n]) end,
478    arguments = { "string", "integer" }
479}
480
481implement {
482    name      = "noteprefixednumber",
483    actions   = function(tag,n) lists.savedlistprefixednumber(tag,notedata[tag][n]) end,
484    arguments = { "string", "integer" }
485}
486
487function notes.internalid(tag,n)
488    local nd = get(tag,n)
489    if nd then
490        local r = nd.references
491        return r.internal
492    end
493end
494
495-- for the moment here but better in some builder modules
496
497-- gets register "n" and location "i" (where 1 is before)
498
499-- this is an experiment, we will make a more general handler instead
500-- of the current note one
501
502local report_insert = logs.reporter("pagebuilder","insert")
503local trace_insert  = false  trackers.register("pagebuilder.insert",function(v) trace_insert = v end)
504
505-- needs to be sorted out!
506
507local function check_spacing(index,slot) -- actually zero should return zero
508    -- we can also check for tex.insertheights > 0 (instead of i > 1)
509    local gn, pn, mn = tex.getinsertdistance(index) -- ,true so no testing needed
510    if not pn then pn = 0 end
511    if not mn then mn = 0 end
512    local gi, pi, mi = texgetglue(slot > 1 and "s_strc_notes_inbetween" or "s_strc_notes_before")
513    if not pi then pi = 0 end
514    if not mi then mi = 0 end
515    local gt = gn + gi
516    local pt = pn + pi
517    local mt = mn + mi
518    if trace_insert then
519        report_insert("%s %i: %p plus %p minus %p","always   ",index,gn,pn,mn)
520        report_insert("%s %i: %p plus %p minus %p",slot > 1 and "inbetween" or "before   ",index,gi,pi,mi)
521        report_insert("%s %i: %p plus %p minus %p","effective",index,gt,pt,mt)
522    end
523    return gt, pt, mt
524end
525
526notes.check_spacing = check_spacing
527
528-- only notes, kind of hardcoded .. bah
529
530local function action(index,slot)
531    local state = notes.numbers[index]
532    if state then
533        return tonode(newgluespec(check_spacing(index,slot)))
534    else
535        return tonode(newgluespec())
536    end
537end
538
539callbacks.register("insert_distance",action,"check spacing around inserts")
540
541-- New, for columnsets:
542
543local inserts = table.setmetatableindex("table")
544
545callback.register("balance_insert", function(n, cbk, index, data)
546    if cbk == 0 and getinsertmultiplier(index) == 0 then
547        inserts[index][data] = tonut(n.list)
548        n.list = nil
549    end
550end)
551
552function notes.flushrange(index,mark) -- (index,first,last)
553    local first = tonumber(getmark("first", mark)) or 0
554    local last  = tonumber(getmark("bottom",mark)) or 0
555    if first > 0 and last > 0 then
556     -- for index, byindex in sortedhash(inserts) do
557        local byindex = inserts[index]
558        if byindex then
559            local head = nil
560            local tail = nil
561            for data, list in sortedhash(byindex) do
562                if data >= first and data <= last then
563                    if head then
564                        setlink(tail,list)
565                    else
566                        head = list
567                    end
568                    tail = gettail(list)
569                    byindex[data] = nil
570                end
571            end
572            if head then
573                context(tonode(vpack(head)))
574                context.par()
575            end
576        end
577    end
578end
579
580implement {
581    name      = "flushnoterange",
582 -- arguments = "3 integers",
583    arguments = "2 integers",
584    actions   = structures.notes.flushrange,
585}
586
587