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