node-rul.lmt /size: 21 Kb    last modification: 2021-10-28 13:51
1if not modules then modules = { } end modules ['node-rul'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to node-rul.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- this will go to an auxiliary module
11-- beware: rules now have a dir field
12--
13-- todo: make robust for layers ... order matters
14
15-- todo: collect successive bit and pieces and combine them
16--
17-- path s ; s := shaped(p) ; % p[] has rectangles
18-- fill s withcolor .5white ;
19-- draw boundingbox s withcolor yellow;
20
21local tonumber           = tonumber
22
23local context            = context
24local attributes         = attributes
25local nodes              = nodes
26local properties         = nodes.properties.data
27
28local enableaction       = nodes.tasks.enableaction
29
30local nuts               = nodes.nuts
31local tonode             = nuts.tonode
32local tonut              = nuts.tonut
33
34local setnext            = nuts.setnext
35local setprev            = nuts.setprev
36local setlink            = nuts.setlink
37local getnext            = nuts.getnext
38local getprev            = nuts.getprev
39local getid              = nuts.getid
40local getdirection       = nuts.getdirection
41local getattr            = nuts.getattr
42local setattr            = nuts.setattr
43local setattrs           = nuts.setattrs
44local getfont            = nuts.getfont
45local getsubtype         = nuts.getsubtype
46local getlist            = nuts.getlist
47local setwhd             = nuts.setwhd
48local setattrlist        = nuts.setattrlist
49local setshift           = nuts.setshift
50local getwidth           = nuts.getwidth
51local setwidth           = nuts.setwidth
52local setoffsets         = nuts.setoffsets
53local setfield           = nuts.setfield
54local getruledata        = nuts.getruledata
55
56local isglyph            = nuts.isglyph
57
58local flushlist          = nuts.flushlist
59local effectiveglue      = nuts.effectiveglue
60local insertnodeafter    = nuts.insertafter
61local insertnodebefore   = nuts.insertbefore
62local find_tail          = nuts.tail
63local setglue            = nuts.setglue
64local getrangedimensions = nuts.rangedimensions
65local hpack_nodes        = nuts.hpack
66local copylist           = nuts.copylist
67
68local nextlist           = nuts.traversers.list
69local nextglue           = nuts.traversers.glue
70
71local nodecodes          = nodes.nodecodes
72local rulecodes          = nodes.rulecodes
73local gluecodes          = nodes.gluecodes
74local listcodes          = nodes.listcodes
75
76local glyph_code         = nodecodes.glyph
77local par_code           = nodecodes.par
78local dir_code           = nodecodes.dir
79local glue_code          = nodecodes.glue
80local hlist_code         = nodecodes.hlist
81
82local indentlist_code    = listcodes.indent
83local linelist_code      = listcodes.line
84
85local leftskip_code         = gluecodes.leftskip
86local rightskip_code        = gluecodes.rightskip
87local parfillleftskip_code  = gluecodes.parfillleftskip
88local parfillrightskip_code = gluecodes.parfillrightskip
89local indentskip_code       = gluecodes.indentskip
90
91local nodepool           = nuts.pool
92
93local new_rule           = nodepool.rule
94local new_userrule       = nodepool.userrule
95local new_kern           = nodepool.kern
96local new_leader         = nodepool.leader
97
98local n_tostring         = nodes.idstostring
99local n_tosequence       = nodes.tosequence
100
101local variables          = interfaces.variables
102local implement          = interfaces.implement
103
104local privateattributes  = attributes.private
105
106local a_ruled            = privateattributes('ruled')
107local a_runningtext      = privateattributes('runningtext')
108local a_color            = privateattributes('color')
109local a_transparency     = privateattributes('transparency')
110local a_colormodel       = privateattributes('colormodel')
111local a_linefiller       = privateattributes("linefiller")
112local a_viewerlayer      = privateattributes("viewerlayer")
113
114local v_both             = variables.both
115local v_left             = variables.left
116local v_right            = variables.right
117local v_local            = variables["local"]
118local v_yes              = variables.yes
119local v_foreground       = variables.foreground
120
121local fonthashes         = fonts.hashes
122local fontdata           = fonthashes.identifiers
123local fontresources      = fonthashes.resources
124
125local dimenfactor        = fonts.helpers.dimenfactor
126local splitdimen         = number.splitdimen
127local setmetatableindex  = table.setmetatableindex
128
129local runningrule        = tex.magicconstants.runningrule
130
131local striprange         = nuts.striprange
132local processwords       = nuts.processwords
133
134local setcoloring        = nuts.colors.set
135
136local rules              = nodes.rules or { }
137nodes.rules              = rules
138rules.data               = rules.data  or { }
139
140local nutrules           = nuts.rules or { }
141nuts.rules               = nutrules -- not that many
142
143storage.register("nodes/rules/data", rules.data, "nodes.rules.data")
144
145local data = rules.data
146
147-- we implement user rules here as it takes less code this way
148
149local function usernutrule(t,noattributes)
150    local r = new_userrule(t.width or 0,t.height or 0,t.depth or 0)
151    if noattributes == false or noattributes == nil then
152        -- avoid fuzzy ones
153    else
154        setattrlist(r,true)
155    end
156    properties[r] = t
157    return r
158end
159
160nutrules.userrule = usernutrule
161
162local function userrule(t,noattributes)
163    return tonode(usernutrule(t,noattributes))
164end
165
166rules.userrule       = userrule
167local ruleactions    = { }
168
169rules   .ruleactions = ruleactions
170nutrules.ruleactions = ruleactions -- convenient
171
172local function mathaction(n,h,v,what)
173    local font    = getruledata(n)
174    local actions = fontresources[font].mathruleactions
175    if actions then
176        local action = actions[what]
177        if action then
178            action(n,h,v,font)
179        end
180    end
181end
182
183local function mathradical(n,h,v)
184    mathaction(n,h,v,"radicalaction")
185end
186
187local function mathrule(n,h,v)
188    mathaction(n,h,v,"hruleaction")
189end
190
191local x
192
193local function useraction(n,h,v)
194    local p = properties[n]
195    if p then
196        local i = p.type or "draw"
197        local a = ruleactions[i]
198        if a then
199            a(p,h,v,i,n)
200        end
201    end
202end
203
204local subtypeactions = {
205    [rulecodes.user]     = useraction,
206    [rulecodes.over]     = mathrule,
207    [rulecodes.under]    = mathrule,
208    [rulecodes.fraction] = mathrule,
209    [rulecodes.radical]  = mathradical,
210}
211
212function rules.process(n,h,v)
213    local n = tonut(n) -- already a nut
214    local s = getsubtype(n)
215    local a = subtypeactions[s]
216    if a then
217        a(n,h,v)
218    end
219end
220
221--
222
223local trace_ruled   = false  trackers.register("nodes.rules", function(v) trace_ruled = v end)
224local report_ruled  = logs.reporter("nodes","rules")
225
226function rules.define(settings)
227    local nofdata = #data + 1
228    data[nofdata] = settings
229    local text = settings.text
230    if text then
231        local b = nuts.takebox(text)
232        if b then
233            nodepool.register(b)
234            settings.text = getlist(b)
235        else
236            settings.text = nil
237        end
238    end
239    return nofdata
240end
241
242local function flush_ruled(head,f,l,d,level,parent,strip) -- not that fast but acceptable for this purpose
243    local font = nil
244    local char, id = isglyph(f)
245    if char then
246        font = id
247    elseif id == hlist_code then
248        font = getattr(f,a_runningtext)
249    end
250    if not font then
251        -- saveguard ... we need to deal with rules and so (math)
252        return head
253    end
254    local r, m
255    if strip then
256        if trace_ruled then
257            local before = n_tosequence(f,l,true)
258            f, l = striprange(f,l)
259            local after = n_tosequence(f,l,true)
260            report_ruled("range stripper, before %a, after %a",before,after)
261        else
262            f, l = striprange(f,l)
263        end
264    end
265    if not f then
266        return head
267    end
268    local wd, ht, dp    = getrangedimensions(parent,f,getnext(l))
269    local method        = d.method
270    local empty         = d.empty == v_yes
271    local offset        = d.offset
272    local dy            = d.dy
273    local order         = d.order
274    local max           = d.max
275    local mp            = d.mp
276    local rulethickness = d.rulethickness
277    local unit          = d.unit
278    local ma            = d.ma
279    local ca            = d.ca
280    local ta            = d.ta
281    local colorspace    = ma > 0 and ma or getattr(f,a_colormodel) or 1
282    local color         = ca > 0 and ca or getattr(f,a_color)
283    local transparency  = ta > 0 and ta or getattr(f,a_transparency)
284    local foreground    = order == v_foreground
285    local layer         = getattr(f,a_viewerlayer)
286    local e             = dimenfactor(unit,font) -- what if no glyph node
287    local rt            = tonumber(rulethickness)
288    if rt then
289        rulethickness = e * rulethickness / 2
290    else
291        local n, u = splitdimen(rulethickness)
292        if n and u then -- we need to intercept ex and em and % and ...
293            rulethickness = n * dimenfactor(u,fontdata[font]) / 2
294        else
295            rulethickness = 1/5
296        end
297    end
298    --
299    if level > max then
300        level = max
301    end
302    if method == 0 then -- center
303        offset = 2*offset
304        m = (offset+(level-1)*dy)*e/2 + rulethickness/2
305    else
306        m = 0
307    end
308
309    local function inject(r,wd,ht,dp)
310        if layer then
311            setattr(r,a_viewerlayer,layer)
312        end
313        if empty then
314            head = insertnodebefore(head,f,r)
315            setlink(r,getnext(l))
316            setprev(f)
317            setnext(l)
318            flushlist(f)
319        else
320            local k = new_kern(-wd)
321            if foreground then
322                insertnodeafter(head,l,k)
323                insertnodeafter(head,k,r)
324                l = r
325            else
326                head = insertnodebefore(head,f,r)
327                insertnodeafter(head,r,k)
328            end
329        end
330        if trace_ruled then
331            report_ruled("level %a, width %p, height %p, depth %p, nodes %a, text %a",
332                level,wd,ht,dp,n_tostring(f,l),n_tosequence(f,l,true))
333        end
334    end
335
336    if mp and mp ~= "" then
337        local r = usernutrule {
338            width  = wd,
339            height = ht,
340            depth  = dp,
341            type   = "mp",
342            factor = e,
343            offset = offset,
344            line   = rulethickness,
345            data   = mp,
346            ma     = colorspace,
347            ca     = color,
348            ta     = transparency,
349        }
350        inject(r,wd,ht,dp)
351    else
352        local tx = d.text
353        if tx then
354            local l = copylist(tx)
355            if d["repeat"] == v_yes then
356                l = new_leader(wd,l)
357                setattrlist(l,tx)
358            end
359            l = hpack_nodes(l,wd,"exactly")
360            inject(l,wd,ht,dp)
361        else
362            for i=1,level do
363                local hd = (offset+(i-1)*dy)*e - m
364                local ht =  hd + rulethickness
365                local dp = -hd + rulethickness
366                inject(setcoloring(new_rule(wd,ht,dp),colorspace,color,transparency),wd,ht,dp)
367            end
368        end
369    end
370    return head
371end
372
373rules.handler = function(head)
374    return processwords(a_ruled,data,flush_ruled,head)
375end
376
377function rules.enable()
378    enableaction("shipouts","nodes.rules.handler")
379end
380
381local trace_shifted = false  trackers.register("nodes.shifting", function(v) trace_shifted = v end)
382
383local report_shifted = logs.reporter("nodes","shifting")
384
385local a_shifted = attributes.private('shifted')
386
387local shifts = nodes.shifts or { }
388nodes.shifts = shifts
389shifts.data  = shifts.data or { }
390
391storage.register("nodes/shifts/data", shifts.data, "nodes.shifts.data")
392
393local data = shifts.data
394
395function shifts.define(settings)
396    local nofdata = #data + 1
397    data[nofdata] = settings
398    return nofdata
399end
400
401local function flush_shifted(head,first,last,data,level,parent,strip) -- not that fast but acceptable for this purpose
402    if true then
403        first, last = striprange(first,last)
404    end
405    local prev = getprev(first)
406    local next = getnext(last)
407    setprev(first)
408    setnext(last)
409    local width, height, depth = getrangedimensions(parent,first,next)
410    local list = hpack_nodes(first,width,"exactly") -- we can use a simple pack
411    if first == head then
412        head = list
413    end
414    if prev then
415        setlink(prev,list)
416    end
417    if next then
418        setlink(list,next)
419    end
420    local raise = data.dy * dimenfactor(data.unit,fontdata[getfont(first)])
421    setshift(list,raise)
422    setwhd(list,width,height,depth)
423    if trace_shifted then
424        report_shifted("width %p, nodes %a, text %a",width,n_tostring(first,last),n_tosequence(first,last,true))
425    end
426    return head
427end
428
429shifts.handler = function(head)
430    return processwords(a_shifted,data,flush_shifted,head)
431end
432
433function shifts.enable()
434    enableaction("shipouts","nodes.shifts.handler")
435end
436
437-- linefillers
438
439local linefillers = nodes.linefillers or { }
440nodes.linefillers = linefillers
441linefillers.data  = linefillers.data or { }
442
443storage.register("nodes/linefillers/data", linefillers.data, "nodes.linefillers.data")
444
445local data = linefillers.data
446
447function linefillers.define(settings)
448    local nofdata = #data + 1
449    data[nofdata] = settings
450    return nofdata
451end
452
453local function linefiller(current,data,width,location)
454    local height = data.height
455    local depth  = data.depth
456    local mp     = data.mp
457    local ma     = data.ma
458    local ca     = data.ca
459    local ta     = data.ta
460    if mp and mp ~= "" then
461        return usernutrule {
462            width     = width,
463            height    = height,
464            depth     = depth,
465            type      = "mp",
466            line      = data.rulethickness,
467            data      = mp,
468            ma        = ma,
469            ca        = ca,
470            ta        = ta,
471            option    = location,
472            direction = getdirection(current),
473        }
474    else
475        return setcoloring(new_rule(width,height,depth),ma,ca,ta)
476    end
477end
478
479function linefillers.filler(current,data,width,height,depth)
480    if width and width > 0 then
481        local height = height or data.height or 0
482        local depth  = depth  or data.depth  or 0
483        if (height + depth) ~= 0 then
484            local mp = data.mp
485            local ma = data.ma
486            local ca = data.ca
487            local ta = data.ta
488            if mp and mp ~= "" then
489                return usernutrule {
490                    width     = width,
491                    height    = height,
492                    depth     = depth,
493                    type      = "mp",
494                    line      = data.rulethickness,
495                    data      = mp,
496                    ma        = ma,
497                    ca        = ca,
498                    ta        = ta,
499                    option    = location,
500                    direction = getdirection(current),
501                }
502            else
503                return setcoloring(new_rule(width,height,depth),ma,ca,ta)
504            end
505        end
506    end
507end
508
509local function getskips(list) -- this could be a helper
510    local ls = nil
511    local rs = nil
512    local is = nil
513    local pl = nil
514    local pr = nil
515    local ok = false
516    for n, subtype in nextglue, list do
517        if subtype == rightskip_code then
518            rs = n
519        elseif subtype == parfillrightskip_code then
520            pr = n
521        elseif subtype == leftskip_code then
522            ls = n
523        elseif subtype == indentskip_code then
524            is = n
525        elseif subtype == parfillleftskip_code then
526            pl = n
527        end
528    end
529    return is, ls, pl, pr, rs
530end
531
532function linefillers.handler(head)
533    -- we have a normalized line ..
534    for current, id, subtype, list in nextlist, head do
535        if subtype == linelist_code and list then
536            local a = getattr(current,a_linefiller)
537            if a then
538                local class = a % 1000
539                local data  = data[class]
540                if data then
541                    local location   = data.location
542                    local scope      = data.scope
543                    local distance   = data.distance
544                    local threshold  = data.threshold
545                    local leftlocal  = false
546                    local rightlocal = false
547                    --
548                    if scope == v_right then
549                        leftlocal = true
550                    elseif scope == v_left then
551                        rightlocal = true
552                    elseif scope == v_local then
553                        leftlocal  = true
554                        rightlocal = true
555                    end
556                    --
557                    local is, ls, pl, pr, rs = getskips(list)
558                    if ls and rs then
559                        if location == v_left or location == v_both then
560                            local indentation = is and getwidth(is) or 0
561                            local leftfixed   = ls and getwidth(ls) or 0
562                            local lefttotal   = ls and effectiveglue(ls,current) or 0
563                            local width = lefttotal - (leftlocal and leftfixed or 0) + indentation - distance
564                            if width > threshold then
565                                if is then
566                                    setwidth(is,0)
567                                end
568                                setglue(ls,leftlocal and getwidth(ls) or nil)
569                                if distance > 0 then
570                                    insertnodeafter(list,ls,new_kern(distance))
571                                end
572                                insertnodeafter(list,ls,linefiller(current,data,width,"left"))
573                            end
574                        end
575                        --
576                        if location == v_right or location == v_both then
577                            local rightfixed = rs and getwidth(rs) or 0
578                            local righttotal = rs and effectiveglue(rs,current) or 0
579                            local parfixed   = pr and getwidth(pr) or 0
580                            local partotal   = pr and effectiveglue(pr,current) or 0
581                            local width = righttotal - (rightlocal and rightfixed or 0) + partotal - distance
582                            if width > threshold then
583                                if pr then
584                                    setglue(pr)
585                                end
586                                setglue(rs,rightlocal and getwidth(rs) or nil)
587                                if distance > 0 then
588                                    insertnodebefore(list,rs,new_kern(distance))
589                                end
590                                insertnodebefore(list,rs,linefiller(current,data,width,"right"))
591                            end
592                        end
593                    else
594                        -- error, not a properly normalized line
595                    end
596                end
597            end
598        end
599    end
600    return head
601end
602
603local enable = false
604
605function linefillers.enable()
606    if not enable then
607    -- we could now nil it
608        enableaction("finalizers","nodes.linefillers.handler")
609        enable = true
610    end
611end
612
613-- interface
614
615implement {
616    name      = "definerule",
617    actions   = { rules.define, context },
618    arguments = {
619        {
620            { "continue" },
621            { "unit" },
622            { "order" },
623            { "method", "integer" },
624            { "offset", "number" },
625            { "rulethickness" },
626            { "dy", "number" },
627            { "max", "number" },
628            { "ma", "integer" },
629            { "ca", "integer" },
630            { "ta", "integer" },
631            { "mp" },
632            { "empty" },
633            { "text", "integer" },
634            { "repeat" },
635        }
636    }
637}
638
639implement {
640    name     = "enablerules",
641    onlyonce = true,
642    actions  = rules.enable
643}
644
645implement {
646    name      = "defineshift",
647    actions   = { shifts.define, context },
648    arguments = {
649        {
650            { "continue" },
651            { "unit" },
652            { "method", "integer" },
653            { "dy", "number" },
654        }
655    }
656}
657
658implement {
659    name     = "enableshifts",
660    onlyonce = true,
661    actions  = shifts.enable
662}
663
664implement {
665    name      = "definelinefiller",
666    actions   = { linefillers.define, context },
667    arguments = {
668        {
669            { "method", "integer" },
670            { "location", "string" },
671            { "scope", "string" },
672            { "mp", "string" },
673            { "ma", "integer" },
674            { "ca", "integer" },
675            { "ta", "integer" },
676            { "depth", "dimension" },
677            { "height", "dimension" },
678            { "distance", "dimension" },
679            { "threshold", "dimension" },
680            { "rulethickness", "dimension" },
681        }
682    }
683}
684
685implement {
686    name     = "enablelinefillers",
687    onlyonce = true,
688    actions  = linefillers.enable
689}
690
691-- We add a bonus feature here (experiment):
692
693interfaces.implement {
694    name      = "autorule",
695    arguments = {
696        {
697            { "width", "dimension" },
698            { "height", "dimension" },
699            { "depth", "dimension" },
700            { "xoffset", "dimension" },
701            { "yoffset", "dimension" },
702            { "left", "dimension" },
703            { "right", "dimension" },
704        },
705    },
706    actions   = function(t)
707        local n = new_rule(
708            t.width or runningrule,
709            t.height or runningrule,
710            t.depth or runningrule
711        )
712        setattrlist(n,true)
713        setoffsets(n,t.xoffset,t.yoffset) -- ,t.left, t.right
714        local l = t.left
715        local r = t.right
716        if l then
717            setfield(n,"left",l)
718        end
719        if r then
720            setfield(n,"right",r)
721        end
722        context(tonode(n))
723    end
724}
725