page-lin.lmt /size: 11 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['page-lin'] = {
2    version   = 1.001,
3    comment   = "companion to page-lin.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 next, tonumber = next, tonumber
10
11local trace_numbers      = false  trackers.register("lines.numbers",  function(v) trace_numbers = v end)
12
13local report_lines       = logs.reporter("lines")
14
15local attributes         = attributes
16local nodes              = nodes
17local context            = context
18
19local implement          = interfaces.implement
20
21nodes.lines              = nodes.lines or { }
22local lines              = nodes.lines
23
24lines.data               = lines.data or { } -- start step tag
25local data               = lines.data
26local last               = #data
27
28storage.register("lines/data", data, "nodes.lines.data")
29
30local variables          = interfaces.variables
31
32local v_next             <const> = variables.next
33local v_page             <const> = variables.page
34local v_no               <const> = variables.no
35
36local properties         = nodes.properties.data
37
38local nodecodes          = nodes.nodecodes
39local hlist_code         <const> = nodecodes.hlist
40local vlist_code         <const> = nodecodes.vlist
41local whatsit_code       <const> = nodecodes.whatsit
42local glyph_code         <const> = nodecodes.glyph
43
44local linelist_code      <const> = nodes.listcodes.line
45
46local latelua_code       <const> = nodes.whatsitcodes.latelua
47
48local localtexrun        = tex.runlocal
49
50local a_linenumber       <const> = attributes.private('linenumber')
51local a_linereference    <const> = attributes.private('linereference')
52
53local cross_references   = { }
54
55local nuts               = nodes.nuts
56
57local getid              = nuts.getid
58local getattr            = nuts.getattr
59local getlist            = nuts.getlist
60local getbox             = nuts.getbox
61local gettotal           = nuts.gettotal
62
63local takebox            = nuts.takebox
64
65local nexthlist          = nuts.traversers.hlist
66local nextvlist          = nuts.traversers.vlist
67local nextcontent        = nuts.traversers.content
68local nextlist           = nuts.traversers.list
69local nextnode           = nuts.traversers.node
70local nextwhatsit        = nuts.traversers.whatsit
71
72local findattribute      = nuts.findattribute
73
74local is_display_math    = nuts.is_display_math
75
76local ctx_convertnumber  = context.convertnumber
77local ctx_makelinenumber = context.makelinenumber
78
79local paragraphs         = typesetters.paragraphs
80local addtoline          = paragraphs.addtoline
81local checkline          = paragraphs.checkline
82
83-- cross referencing
84
85function lines.number(n)
86    n = tonumber(n)
87    local cr = cross_references[n] or 0
88    cross_references[n] = nil
89    return cr
90end
91
92function lines.finalize(t)
93    local getnumber = lines.number
94    for _,p in next, t do
95        for _,r in next, p do
96            local m = r.metadata
97            if m and m.kind == "line" then
98                local e = r.entries
99                local u = r.userdata
100                e.linenumber = getnumber(e.text or 0) -- we can nil e.text
101                e.conversion = u and u.conversion
102                r.userdata = nil -- hack
103            end
104        end
105    end
106end
107
108local filters = structures.references.filters
109local helpers = structures.helpers
110
111structures.references.registerfinalizer(lines.finalize)
112
113filters.line = filters.line or { }
114
115function filters.line.default(data)
116    ctx_convertnumber(data.entries.conversion or "numbers",data.entries.linenumber or "0")
117end
118
119function filters.line.page(data,prefixspec,pagespec) -- redundant
120    helpers.prefixpage(data,prefixspec,pagespec)
121end
122
123function filters.line.linenumber(data) -- raw
124    context(data.entries.linenumber or "0")
125end
126
127-- boxed variant, todo: use number mechanism
128
129local boxed = lines.boxed or { }
130lines.boxed = boxed
131
132-- todo: cache setups, and free id no longer used
133-- use interfaces.cachesetup(t)
134
135function boxed.register(configuration)
136    last = last + 1
137    data[last] = configuration
138    if trace_numbers then
139        report_lines("registering setup %a",last)
140    end
141    return last
142end
143
144implement {
145    name      = "registerlinenumbering",
146    actions   = { boxed.register, context },
147    arguments = {
148        {
149            { "continue" },
150            { "start", "integer" },
151            { "step", "integer" },
152            { "method" },
153            { "tag" },
154        }
155    }
156}
157
158function boxed.setup(n,configuration)
159    local d = data[n]
160    if d then
161        if trace_numbers then
162            report_lines("updating setup %a",n)
163        end
164        for k,v in next, configuration do
165            d[k] = v
166        end
167    else
168        if trace_numbers then
169            report_lines("registering setup %a (br)",n)
170        end
171        data[n] = configuration
172    end
173    return n
174end
175
176implement {
177    name      = "setuplinenumbering",
178    actions   = boxed.setup,
179    arguments = {
180        "integer",
181        {
182            { "continue" },
183            { "start", "integer" },
184            { "step", "integer" },
185            { "method" },
186            { "tag" },
187        }
188    }
189}
190
191local function resolve(n,m) -- we can now check the 'line' flag (todo)
192    for current, id, subtype, content in nextlist, n do
193        if content then
194            if id == hlist_code then
195                for current, subtype in nextwhatsit, content do
196                    if subtype == latelua_code then            -- this has to change!
197                        local a = getattr(current,a_linereference)
198                        if a then
199                            cross_references[a] = m
200                        end
201                    end
202                end
203            end
204            resolve(content,m)
205        end
206    end
207end
208
209local function check_number(n,b,a,skip)
210    local d = data[a]
211    if d then
212        local tag  = d.tag or ""
213        local s    = d.start or 1
214        local okay = not skip and s % d.step == 0
215        if trace_numbers then
216            report_lines("%s number for setup %a, tag %a, line %s, continued %a",okay and "making" or "skipping",a,tag,s,d.continue or v_no)
217        end
218        if okay then
219            local p = checkline(n)
220            if p then
221                localtexrun(function() ctx_makelinenumber(tag,s,p.hsize,p.reverse and 1 or 0) end)
222                local l = takebox(b)
223                if l then
224                    addtoline(n,l)
225                else
226                    -- very unlikely
227                end
228            end
229        end
230        resolve(n,s)
231        d.start  = s + 1
232    end
233end
234
235-- print(nodes.idstostring(list))
236
237-- hlists of type line will only have an attribute when the line number attribute
238-- still set at par building time which is not always the case unless we explicitly
239-- do a par before we end the line
240
241local function lineisnumbered(n) -- content node
242 -- for n, id in nextcontent, getlist(n) do
243 --     local a = getattr(n,a_linenumber)
244 --     if a and a > 0 then
245 --         return a
246 --     end
247 -- end
248    local a = findattribute(getlist(n),a_linenumber,true)
249    if a and a > 0 then
250        return a
251    end
252end
253
254local function listisnumbered(list)
255    if list then
256        for n, subtype in nexthlist, list do
257            if subtype == linelist_code then
258                local a = getattr(n,a_linenumber)
259                if a then
260                    -- a quick test for lines (only valid when \par before \stoplinenumbering)
261                    return a > 0 and list or false
262                else
263                    -- a bit slower one, assuming that we have normalized and anchored
264                    if lineisnumbered(n) then
265                        return list
266                    end
267                end
268            end
269        end
270    end
271end
272
273local function findnumberedlist(list)
274    -- we assume wrapped boxes, only one with numbers
275    for n, id, subtype, content in nextcontent, list do
276        if id == hlist_code then
277            if subtype == linelist_code then
278                local a = getattr(n,a_linenumber)
279                if a then
280                    return a > 0 and list
281                end
282                return
283            else
284                if lineisnumbered(content) then
285                    return n
286                end
287                local okay = findnumberedlist(content)
288                if okay then
289                    return okay
290                end
291            end
292        elseif id == vlist_code then
293            if listisnumbered(content) then
294                return content
295            end
296            local okay = findnumberedlist(content)
297            if okay then
298                return okay
299            end
300        elseif id == glyph_code then
301            return
302        end
303    end
304end
305
306-- reset ranges per page
307-- store first and last per page
308-- maybe just set marks directly
309
310local function findcolumngap(list)
311    -- we assume wrapped boxes, only one with numbers
312    for n, id, subtype, content in nextlist, list do
313        if id == hlist_code or id == vlist_code then
314            local p = properties[n]
315            if p and p.columngap then
316                if trace_numbers then
317                    report_lines("first column gap %a",p.columngap)
318                end
319                return n
320            elseif content then
321                local okay = findcolumngap(content)
322                if okay then
323                    return okay
324                end
325            end
326        end
327    end
328end
329
330function boxed.addlinenumbers(n,b,nested)
331    local box = getbox(n)
332    if not box then
333        return
334    end
335    local list = getlist(box)
336    if not list then
337        return
338    end
339    local last_a = nil
340    local last_v = -1
341    local skip   = false
342
343    local function check()
344        for n, subtype in nexthlist, list do
345            if subtype ~= linelist_code then
346                -- go on
347            elseif gettotal(n) == 0 then
348                -- skip funny hlists -- todo: check line subtype
349            else
350                local a = lineisnumbered(n)
351                if a then
352                    if last_a ~= a then
353                        local da = data[a]
354                        local ma = da.method
355                        if ma == v_next then
356                            skip = true
357                        elseif ma == v_page then
358                            da.start = 1 -- eventually we will have a normal counter
359                        end
360                        last_a = a
361                        if trace_numbers then
362                            report_lines("starting line number range %s: start %s, continue %s",a,da.start,da.continue or v_no)
363                        end
364                    end
365                    check_number(n,b,a,skip)
366                    skip = false
367                end
368            end
369        end
370    end
371
372    if nested == 0 then
373        if list then
374            check()
375        end
376    elseif nested == 1 then
377        local id = getid(box)
378        if id == vlist_code then
379            if listisnumbered(list) then
380                -- ok
381            else
382                list = findnumberedlist(list)
383            end
384        else -- hlist
385            list = findnumberedlist(list)
386        end
387        if list then
388            check()
389        end
390    elseif nested == 2 then
391        list = findcolumngap(list)
392        -- we assume we have a vlist
393        if not list then
394            return
395        end
396        for n in nextvlist, list do
397            local p = properties[n]
398            if p and p.columngap then
399                if trace_numbers then
400                    report_lines("found column gap %a",p.columngap)
401                end
402                list = getlist(n)
403                if list then
404                    check()
405                end
406            end
407        end
408    else
409        -- bad call
410    end
411end
412
413-- column attribute
414
415implement {
416    name      = "addlinenumbers",
417    actions   = boxed.addlinenumbers,
418    arguments = "3 integers",
419}
420