node-rul.lmt /size: 31 Kb    last modification: 2025-02-21 11: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 setsubtype         = nuts.setsubtype
47local getlist            = nuts.getlist
48local setwhd             = nuts.setwhd
49local setattrlist        = nuts.setattrlist
50local setshift           = nuts.setshift
51local getwidth           = nuts.getwidth
52local setwidth           = nuts.setwidth
53local setweight          = nuts.setweight
54local setoffsets         = nuts.setoffsets
55local setfield           = nuts.setfield
56local getruledata        = nuts.getruledata
57local yscaled            = nuts.yscaled
58
59local isglyph            = nuts.isglyph
60local firstglyphnode     = nuts.firstglyphnode
61
62local flushlist          = nuts.flushlist
63local effectiveglue      = nuts.effectiveglue
64local insertnodeafter    = nuts.insertafter
65local insertnodebefore   = nuts.insertbefore
66local find_tail          = nuts.tail
67local setglue            = nuts.setglue
68local getrangedimensions = nuts.rangedimensions
69local hpack_nodes        = nuts.hpack
70local copylist           = nuts.copylist
71
72local nextlist           = nuts.traversers.list
73local nextglue           = nuts.traversers.glue
74local nextglyph          = nuts.traversers.glyph
75
76local nodecodes          = nodes.nodecodes
77local rulecodes          = nodes.rulecodes
78local gluecodes          = nodes.gluecodes
79local listcodes          = nodes.listcodes
80
81local glyph_code         <const> = nodecodes.glyph
82local par_code           <const> = nodecodes.par
83local dir_code           <const> = nodecodes.dir
84local glue_code          <const> = nodecodes.glue
85local hlist_code         <const> = nodecodes.hlist
86
87local indentlist_code    <const> = listcodes.indent
88local linelist_code      <const> = listcodes.line
89local container_code     <const> = listcodes.container
90
91local leftskip_code         <const> = gluecodes.leftskip
92local rightskip_code        <const> = gluecodes.rightskip
93local parfillleftskip_code  <const> = gluecodes.parfillleftskip
94local parfillrightskip_code <const> = gluecodes.parfillrightskip
95local indentskip_code       <const> = gluecodes.indentskip
96
97local nodepool           = nuts.pool
98
99local new_rule           = nodepool.rule
100local new_userrule       = nodepool.userrule
101local new_kern           = nodepool.kern
102local new_leader         = nodepool.leader
103
104local n_tostring         = nodes.idstostring
105local n_tosequence       = nodes.tosequence
106
107local variables          = interfaces.variables
108local implement          = interfaces.implement
109
110local privateattribute   = attributes.private
111
112local a_ruled            <const> = privateattribute('ruled')
113local a_runningtext      <const> = privateattribute('runningtext')
114local a_color            <const> = privateattribute('color')
115local a_transparency     <const> = privateattribute('transparency')
116local a_colormodel       <const> = privateattribute('colormodel')
117local a_linefiller       <const> = privateattribute("linefiller")
118local a_viewerlayer      <const> = privateattribute("viewerlayer")
119
120local registervalue      = attributes.registervalue
121local getvalue           = attributes.getvalue
122local texsetattribute    = tex.setattribute
123
124local v_both             <const> = variables.both
125local v_left             <const> = variables.left
126local v_right            <const> = variables.right
127local v_local            <const> = variables["local"]
128local v_yes              <const> = variables.yes
129local v_foreground       <const> = variables.foreground
130
131local fonthashes         = fonts.hashes
132local fontdata           = fonthashes.identifiers
133local fontresources      = fonthashes.resources
134
135local glyphdimenfactor   = fonts.helpers.glyphdimenfactor
136local splitdimen         = number.splitdimen
137local setmetatableindex  = table.setmetatableindex
138
139local runningrule        <const> = tex.magicconstants.runningrule
140
141local striprange         = nuts.striprange
142local processwords       = nuts.processwords
143
144local setcoloring        = nuts.colors.set
145
146do
147
148    local rules              = nodes.rules or { }
149    nodes.rules              = rules
150    -- rules.data               = rules.data  or { }
151
152    local nutrules           = nuts.rules or { }
153    nuts.rules               = nutrules -- not that many
154
155    -- we implement user rules here as it takes less code this way
156
157    local function usernutrule(t,noattributes)
158        local r = new_userrule(t.width or 0,t.height or 0,t.depth or 0)
159        if noattributes == false or noattributes == nil then
160            -- avoid fuzzy ones
161        else
162            setattrlist(r,true)
163        end
164        properties[r] = t
165        return r
166    end
167
168    nutrules.userrule = usernutrule
169
170    local function userrule(t,noattributes)
171        return tonode(usernutrule(t,noattributes))
172    end
173
174    rules.userrule       = userrule
175    local ruleactions    = { }
176
177    rules   .ruleactions = ruleactions
178    nutrules.ruleactions = ruleactions -- convenient
179
180    local function mathaction(n,h,v,what)
181        local font    = getruledata(n)
182        local actions = fontresources[font].mathruleactions
183        if actions then
184            local action = actions[what]
185            if action then
186                action(n,h,v,font)
187            end
188        end
189    end
190
191    local function mathradical(n,h,v)
192        mathaction(n,h,v,"radicalaction")
193    end
194
195    local function mathrule(n,h,v)
196        mathaction(n,h,v,"hruleaction")
197    end
198
199    local function useraction(n,h,v)
200        local p = properties[n]
201        if p then
202            local i = p.type or "draw"
203            local a = ruleactions[i]
204            if a then
205                a(p,h,v,i,n)
206            end
207        end
208    end
209
210    local subtypeactions = {
211        [rulecodes.user]     = useraction,
212        [rulecodes.over]     = mathrule,
213        [rulecodes.under]    = mathrule,
214        [rulecodes.fraction] = mathrule,
215        [rulecodes.radical]  = mathradical,
216    }
217
218    function rules.process(n,h,v)
219        local n = tonut(n) -- already a nut
220        local s = getsubtype(n)
221        local a = subtypeactions[s]
222        if a then
223            a(n,h,v)
224        end
225    end
226
227    local trace_ruled   = false  trackers.register("nodes.rules", function(v) trace_ruled = v end)
228    local report_ruled  = logs.reporter("nodes","rules")
229    local enabled       = false
230
231    local texgetattribute = tex.getattribute
232
233    local unsetvalue <const> = attributes.unsetvalue
234
235    -- The setter is now the performance bottleneck but it is no longer
236    -- limited to a certain number of cases before we cycle resources.
237
238    function rules.set(settings)
239        if not enabled then
240            enableaction("shipouts","nodes.rules.handler")
241            enabled = true
242        end
243        local text = settings.text
244        if text then
245            settings.text = tonut(text)
246         -- nodepool.register(text) -- todo: have a cleanup hook
247        end
248        -- todo: only when explicitly enabled
249        local attr = texgetattribute(a_ruled)
250        if attr ~= unsetvalue then
251            settings.nestingvalue = attr
252            settings.nestingdata  = getvalue(a_ruled,attr) -- so still accessible when we wipe
253        end
254        texsetattribute(a_ruled,registervalue(a_ruled,settings))
255    end
256
257    attributes.setcleaner(a_ruled,function(t)
258        local text = t.text
259        if text then
260            flushlist(text)
261        end
262    end)
263
264    -- we could check the passed level on the real one ... (no need to pass level)
265
266    local function flush_ruled(head,f,l,d,level,parent,strip) -- not that fast but acceptable for this purpose
267        local max   = d.max or 1
268        local level = d.stack or d.level or 1
269        if level > max then
270            -- todo: trace message
271            return head
272        end
273        -- id == glyph_code
274        -- id == rule_code
275        -- id == hlist_code and getattr(n,a_runningtext)
276        -- id == disc_code
277        -- id == boundary_code
278        local font = nil
279        local glph = nil
280        local char, id = isglyph(f)
281        if char then
282            font = id
283            glph = f
284        elseif id == hlist_code then
285            font = getattr(f,a_runningtext)
286        else
287            -- rare case:
288            if id == disc_code then
289                font = usesfont(f)
290            end
291            -- hope for the best
292            if not font then
293                local g = firstglyphnode(f,l)
294                if g then
295                    font = getfont(g)
296                end
297            end
298        end
299        if not font then
300            -- maybe single rule -- saveguard ... we need to deal with rules and so (math)
301            return head
302        end
303        local r, m
304        if strip then
305            if trace_ruled then
306                local before = n_tosequence(f,l,true)
307                f, l = striprange(f,l)
308                local after = n_tosequence(f,l,true)
309                report_ruled("range stripper, before %a, after %a",before,after)
310            else
311                f, l = striprange(f,l)
312            end
313        end
314        if not f then
315            return head
316        end
317        local wd, ht, dp    = getrangedimensions(parent,f,getnext(l))
318        local method        = d.method or 0
319        local empty         = d.empty == v_yes
320        local offset        = d.offset or 0
321        local dy            = d.dy or 0
322        local order         = d.order
323        local mp            = d.mp
324        local rulethickness = d.rulethickness
325        local unit          = d.unit or "ex"
326        local ma            = d.ma
327        local ca            = d.ca
328        local ta            = d.ta
329        local colorspace    = ma > 0 and ma or getattr(f,a_colormodel) or 1
330        local color         = ca > 0 and ca or getattr(f,a_color)
331        local transparency  = ta > 0 and ta or getattr(f,a_transparency)
332        local foreground    = order == v_foreground
333        local layer         = getattr(f,a_viewerlayer)
334        local e             = glyphdimenfactor(unit,glph,font)
335        local rt            = tonumber(rulethickness)
336        if rt then
337            rulethickness = e * rulethickness / 2
338        else
339            local n, u = splitdimen(rulethickness)
340            if n and u then -- we need to intercept ex and em and % and ...
341                rulethickness = n * glyphdimenfactor(u,glyph,font) / 2
342            else
343                rulethickness = 1/5
344            end
345        end
346
347        --
348        if level > max then
349            level = max
350        end
351        if method == 0 then -- center
352            offset = 2*offset
353            m = (offset+(level-1)*dy)*e/2 + rulethickness/2
354        else
355            m = 0
356        end
357
358        local function inject(r,wd,ht,dp)
359            if layer then
360                setattr(r,a_viewerlayer,layer)
361            end
362            if empty then
363                head = insertnodebefore(head,f,r)
364                setlink(r,getnext(l))
365                setprev(f)
366                setnext(l)
367                flushlist(f)
368            else
369                local k = new_kern(-wd)
370                if foreground then
371                    insertnodeafter(head,l,k)
372                    insertnodeafter(head,k,r)
373                    l = r
374                else
375                    head = insertnodebefore(head,f,r)
376                    insertnodeafter(head,r,k)
377                end
378            end
379            if trace_ruled then
380                report_ruled("level %a, width %p, height %p, depth %p, nodes %a, text %a",
381                    level,wd,ht,dp,n_tostring(f,l),n_tosequence(f,l,true))
382            end
383        end
384
385        if mp and mp ~= "" then
386            local r = usernutrule {
387                width  = wd,
388                height = ht,
389                depth  = dp,
390                type   = "mp",
391                factor = e,
392                offset = offset - (level-1)*dy, -- br ... different direction
393                line   = rulethickness,
394                data   = mp,
395                ma     = colorspace,
396                ca     = color,
397                ta     = transparency,
398            }
399            inject(r,wd,ht,dp)
400        else
401            local tx = d.text
402            if tx then
403                local l = copylist(tx)
404                if d["repeat"] == v_yes then
405                    l = new_leader(wd,l)
406                    setattrlist(l,tx)
407                end
408                l = hpack_nodes(l,wd,"exactly")
409                inject(l,wd,ht,dp)
410            else
411                local rule
412                if method == 2 or method == 3 then
413                    local height = d.height
414                    local depth  = d.depth
415                    if height > ht then ht = height end
416                    if depth  > dp then dp = depth  end
417                    local of = offset
418                    ht = ht + of
419                    dp = dp + of
420                    wd = wd + 2*of
421                    if method == 2 then
422                        rule = nodepool.outlinerule(wd,ht,dp,rulethickness)
423                    else
424                        rule = nodepool.rule(wd,ht,dp)
425                    end
426                    setoffsets(rule,-of,0)
427                else
428                    local hd = (offset+(level-1)*dy)*e - m
429                    ht =  hd + rulethickness
430                    dp = -hd + rulethickness
431                    rule = new_rule(wd,ht,dp)
432                end
433                inject(setcoloring(rule,colorspace,color,transparency),wd,ht,dp)
434            end
435        end
436        return head
437    end
438
439    rules.handler = function(head)
440        local data = attributes.values[a_ruled]
441 --or-- local data = getvalues(a_ruled)
442        if data then
443            head = processwords(a_ruled,data,flush_ruled,head)
444        end
445        return head
446    end
447
448    implement {
449        name      = "setrule",
450        actions   = rules.set,
451        arguments = {
452            {
453                { "continue" },
454                { "unit" },
455                { "order" },
456                { "level", "integer" },
457                { "stack", "integer" },
458                { "method", "integer" },
459                { "offset", "number" },
460                { "rulethickness" },
461                { "dy", "number" },
462                { "max", "number" },
463                { "ma", "integer" },
464                { "ca", "integer" },
465                { "ta", "integer" },
466                { "mp" },
467                { "empty" },
468                { "text", "box" },
469                { "repeat" },
470                { "height", "dimension" },
471                { "depth", "dimension" },
472            }
473        }
474    }
475
476end
477
478do
479
480    local trace_shifted  = false  trackers.register("nodes.shifting", function(v) trace_shifted = v end)
481    local report_shifted = logs.reporter("nodes","shifting")
482    local a_shifted      <const> = privateattribute('shifted')
483    local enabled        = false
484
485    local shifts         = nodes.shifts or { }
486    nodes.shifts         = shifts
487
488    function shifts.set(settings)
489        if not enabled then
490            -- we could disable when no more found
491            enableaction("shipouts","nodes.shifts.handler")
492            enabled = true
493        end
494        texsetattribute(a_shifted,registervalue(a_shifted,settings))
495    end
496
497    local function flush_shifted(head,first,last,data,level,parent,strip) -- not that fast but acceptable for this purpose
498        if true then
499            first, last = striprange(first,last)
500        end
501        local prev = getprev(first)
502        local next = getnext(last)
503        setprev(first)
504        setnext(last)
505        local width, height, depth = getrangedimensions(parent,first,next)
506        local list = hpack_nodes(first,width,"exactly") -- we can use a simple pack
507        if first == head then
508            head = list
509        end
510        if prev then
511            setlink(prev,list)
512        end
513        if next then
514            setlink(list,next)
515        end
516        local raise = data.dy * glyphdimenfactor(data.unit,first)
517        setshift(list,raise) -- is taken into dimensions
518        setwhd(list,width,height,depth)
519        if trace_shifted then
520            report_shifted("width %p, nodes %a, text %a",width,n_tostring(first,last),n_tosequence(first,last,true))
521        end
522        return head
523    end
524
525    shifts.handler = function(head)
526        local data = attributes.values[a_shifted]
527        if data then
528            head = processwords(a_shifted,data,flush_shifted,head)
529        end
530        return head
531    end
532
533    implement {
534        name      = "setshift",
535        actions   = shifts.set,
536        arguments = {
537            {
538                { "continue" },
539                { "unit" },
540                { "method", "integer" },
541                { "dy", "number" },
542            }
543        }
544    }
545
546end
547
548-- linefillers
549
550do
551
552    local linefillers = nodes.linefillers or { }
553    nodes.linefillers = linefillers
554    local enabled     = false
555
556    local usernutrule = nuts.rules.userrule
557
558    function linefillers.set(settings)
559        if not enabled then
560            enableaction("finalizers","nodes.linefillers.handler")
561            enabled = true
562        end
563        texsetattribute(a_linefiller,registervalue(a_linefiller,settings))
564    end
565
566    local function linefiller(current,data,width,location)
567        local height = data.height
568        local depth  = data.depth
569        local mp     = data.mp
570        local ma     = data.ma
571        local ca     = data.ca
572        local ta     = data.ta
573        if mp and mp ~= "" then
574            return usernutrule {
575                width     = width,
576                height    = height,
577                depth     = depth,
578                type      = "mp",
579                line      = data.rulethickness,
580                data      = mp,
581                ma        = ma,
582                ca        = ca,
583                ta        = ta,
584                option    = location,
585                direction = getdirection(current),
586            }
587        else
588            return setcoloring(new_rule(width,height,depth),ma,ca,ta)
589        end
590    end
591
592    function linefillers.filler(current,data,width,height,depth)
593        if width and width > 0 then
594            local height = height or data.height or 0
595            local depth  = depth  or data.depth  or 0
596            if (height + depth) ~= 0 then
597                local mp = data.mp
598                local ma = data.ma
599                local ca = data.ca
600                local ta = data.ta
601                if mp and mp ~= "" then
602                    return usernutrule {
603                        width     = width,
604                        height    = height,
605                        depth     = depth,
606                        type      = "mp",
607                        line      = data.rulethickness,
608                        data      = mp,
609                        ma        = ma,
610                        ca        = ca,
611                        ta        = ta,
612                        option    = location,
613                        direction = getdirection(current),
614                    }
615                else
616                    return setcoloring(new_rule(width,height,depth),ma,ca,ta)
617                end
618            end
619        end
620    end
621
622    local function getskips(list) -- this could be a helper .. is already one
623        local ls = nil
624        local rs = nil
625        local is = nil
626        local pl = nil
627        local pr = nil
628        local ok = false
629        for n, subtype in nextglue, list do
630            if subtype == rightskip_code then
631                rs = n
632            elseif subtype == parfillrightskip_code then
633                pr = n
634            elseif subtype == leftskip_code then
635                ls = n
636            elseif subtype == indentskip_code then
637                is = n
638            elseif subtype == parfillleftskip_code then
639                pl = n
640            end
641        end
642        return is, ls, pl, pr, rs
643    end
644
645    linefillers.handler = function(head)
646        local data = attributes.values[a_linefiller]
647        if data then
648            -- we have a normalized line ..
649            for current, id, subtype, list in nextlist, head do
650                if subtype == linelist_code and list then
651                    local a = getattr(current,a_linefiller)
652                    if a then
653                        local data = data[a]
654                        if data then
655                            local location   = data.location
656                            local scope      = data.scope
657                            local distance   = data.distance
658                            local threshold  = data.threshold
659                            local leftlocal  = false
660                            local rightlocal = false
661                            --
662                            if scope == v_right then
663                                leftlocal = true
664                            elseif scope == v_left then
665                                rightlocal = true
666                            elseif scope == v_local then
667                                leftlocal  = true
668                                rightlocal = true
669                            end
670                            -- todo: initleft initright fillleft
671                            local is, ls, pl, pr, rs = getskips(list)
672                            if ls and rs then
673                                if location == v_left or location == v_both then
674                                    local indentation = is and getwidth(is) or 0
675                                    local leftfixed   = ls and getwidth(ls) or 0
676                                    local lefttotal   = ls and effectiveglue(ls,current) or 0
677                                    local width = lefttotal - (leftlocal and leftfixed or 0) + indentation - distance
678                                    if width > threshold then
679                                        if is then
680                                            setwidth(is,0)
681                                        end
682                                        setglue(ls,leftlocal and getwidth(ls) or nil)
683                                        if distance > 0 then
684                                            insertnodeafter(list,ls,new_kern(distance))
685                                        end
686                                        insertnodeafter(list,ls,linefiller(current,data,width,"left"))
687                                    end
688                                end
689                                --
690                                if location == v_right or location == v_both then
691                                    local rightfixed = rs and getwidth(rs) or 0
692                                    local righttotal = rs and effectiveglue(rs,current) or 0
693                                    local parfixed   = pr and getwidth(pr) or 0
694                                    local partotal   = pr and effectiveglue(pr,current) or 0
695                                    local width = righttotal - (rightlocal and rightfixed or 0) + partotal - distance
696                                    if width > threshold then
697                                        if pr then
698                                            setglue(pr)
699                                        end
700                                        setglue(rs,rightlocal and getwidth(rs) or nil)
701                                        if distance > 0 then
702                                            insertnodebefore(list,rs,new_kern(distance))
703                                        end
704                                        insertnodebefore(list,rs,linefiller(current,data,width,"right"))
705                                    end
706                                end
707                            else
708                                -- error, not a properly normalized line
709                            end
710                        end
711                    end
712                end
713            end
714        end
715        return head
716    end
717
718    implement {
719        name      = "setlinefiller",
720        actions   = linefillers.set,
721        arguments = {
722            {
723                { "method", "integer" },
724                { "location", "string" },
725                { "scope", "string" },
726                { "mp", "string" },
727                { "ma", "integer" },
728                { "ca", "integer" },
729                { "ta", "integer" },
730                { "depth", "dimension" },
731                { "height", "dimension" },
732                { "distance", "dimension" },
733                { "threshold", "dimension" },
734                { "rulethickness", "dimension" },
735            }
736        }
737    }
738
739end
740
741-- We add a bonus feature here (experiment):
742
743interfaces.implement {
744    name      = "autorule",
745    protected = true,
746    public    = true,
747    arguments = {
748        {
749            { "width", "dimension" },
750            { "height", "dimension" },
751            { "depth", "dimension" },
752            { "xoffset", "dimension" },
753            { "yoffset", "dimension" },
754            { "left", "dimension" },
755            { "right", "dimension" },
756            { "on", "dimension" },
757            { "off", "dimension" },
758        },
759    },
760    actions   = function(t)
761        local n = new_rule(
762            t.width  or runningrule,
763            t.height or runningrule,
764            t.depth  or runningrule
765        )
766        setattrlist(n,true)
767        setoffsets(n,t.xoffset,t.yoffset,t.left,t.right,t.on,t.off)
768        context(tonode(n))
769    end
770}
771
772-- joke
773
774do
775
776    local setlistcolor    = nodes.tracers.colors.setlist
777
778    local trace_shadowed  = false  trackers.register("nodes.shadowing", function(v) trace_shifted = v end)
779    local report_shadowed = logs.reporter("nodes","shadowing")
780    local a_shadowed      <const> = privateattribute("shadowed")
781    local enabled         = false
782
783    local shadows         = nodes.shadows or { }
784    nodes.shadows         = shadows
785
786    function shadows.set(settings)
787        if not enabled then
788            -- we could disable when no more found
789            enableaction("shipouts","nodes.shadows.handler")
790            enabled = true
791        end
792        texsetattribute(a_shadowed,registervalue(a_shadowed,settings))
793    end
794
795    local function flush_shadowed(head,first,last,data,level,parent,strip) -- not that fast but acceptable for this purpose
796        if true then
797            first, last = striprange(first,last)
798        end
799        local next  = getnext(last)
800        local copy  = copylist(first,next)
801        local width = getrangedimensions(parent,first,next)
802        local list  = hpack_nodes(copy,width,"exactly") -- we can use a simple pack
803
804        setlistcolor(copy,data.ca,data.ma,data.ta)
805
806        head = insertnodebefore(head,first,list)
807        local xoffset = data.dx * glyphdimenfactor(data.unit,first)
808        local yoffset = data.dy * glyphdimenfactor(data.unit,first)
809        local weight  = data.weight or 0
810        setoffsets(list,xoffset,yoffset)
811        setwhd(list,0,0,0)
812        if weight > 0 then
813            for g in nextglyph, copy do
814                setweight(g,weight,true)
815            end
816        end
817     -- if trace_shadowed then
818     --     report_shadowed("width %p, nodes %a, text %a",width,n_tostring(first,last),n_tosequence(first,last,true))
819     -- end
820        return head
821    end
822
823    shadows.handler = function(head)
824        local data = attributes.values[a_shadowed]
825        if data then
826            head = processwords(a_shadowed,data,flush_shadowed,head)
827        end
828        return head
829    end
830
831    implement {
832        name      = "setshadow",
833        actions   = shadows.set,
834        arguments = {
835            {
836                { "continue" },
837                { "unit" },
838                { "weight", "integer" },
839                { "method", "integer" },
840                { "dx", "number" },
841                { "dy", "number" },
842                { "ma", "integer" },
843                { "ca", "integer" },
844                { "ta", "integer" },
845            }
846        }
847    }
848
849end
850
851do
852
853    local insert, concat, sort = table.insert, table.concat, table.sort
854
855    local ctx_latelua = context.latelua
856
857    local bpfactor    = number.dimenfactors.bp
858
859    local s_close     <const> = "h"
860    local s_stroke    <const> = "S"
861
862    local f_linewidth = string.formatters["%N w"]
863    local f_moveto    = string.formatters["%N %N m"]
864    local f_lineto    = string.formatters["%N %N l"]
865
866    local segments    = { }
867
868    -- positions can be used instead of r, c but this tracers better
869
870    -- todo: codeinjections.wrapuplinesegment
871
872    local function wrapup(t,c,x,y)
873        local n = t.n
874        local a = t.a
875        if a == 3 then -- reset
876            segments[n] = nil
877        else
878            local s = segments[n]
879            if not s then
880                s = { d = t.d }
881                segments[n] = s
882            end
883            if a == 0 then -- register
884                insert(s,{ t.r, t.c, x, y })
885            elseif a == 1 or a == 2 then -- end close+end
886                insert(s,{ t.r, t.c, x, y })
887                local l = #s
888                if l > 1 then
889                    -- needs checking
890                    sort(s,function(a,b)
891                       if b[1] < a[1] then
892                            return false
893                       else
894                            return a[2] > b[2]
895                       end
896                    end)
897                    sort(s,function(a,b)
898                       if b[3] < a[3] then
899                            return false
900                       else
901                            return a[4] > b[4]
902                       end
903                    end)
904                    --
905                    local r  = { }
906                    local si = s[1]
907                    local x  = si[3] * bpfactor
908                    local y  = si[4] * bpfactor
909                    local w  = s.d * bpfactor
910                    r[#r+1] = f_linewidth(w)
911                    r[#r+1] = f_moveto(x,y)
912                    for i=2,l do
913                        local s = s[i]
914                        local x = s[3] * bpfactor
915                        local y = s[4] * bpfactor
916                        r[#r+1] = f_lineto(x,y)
917                    end
918                    if a == 2 then
919                        r[#r+1] = s_close
920                    end
921                    r[#r+1] = s_stroke
922                    r = concat(r, " ")
923                    lpdf.print("origin",r)
924                    segments[n] = nil
925                end
926            end
927        end
928    end
929
930    implement {
931        name      = "linesegment",
932        protected = true,
933        arguments = { "string", "integer", "integer", "integer", "dimension" },
934        actions   = function(n,r,c,a,d)
935            ctx_latelua {
936                action = wrapup, -- todo: codeinjections.wrapuplinesegment
937                r = r, -- row
938                c = c, -- column
939                a = a, -- action
940                n = n, -- namespace
941                d = d, -- rulethickness
942            }
943        end
944    }
945
946end
947