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