supp-box.lua /size: 24 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['supp-box'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to supp-box.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
10local report_hyphenation = logs.reporter("languages","hyphenation")
11
12local tonumber, next, type = tonumber, next, type
13
14local lpegmatch     = lpeg.match
15
16local tex           = tex
17local context       = context
18local nodes         = nodes
19
20local implement     = interfaces.implement
21
22local nodecodes     = nodes.nodecodes
23
24local disc_code     = nodecodes.disc
25local hlist_code    = nodecodes.hlist
26local vlist_code    = nodecodes.vlist
27local glue_code     = nodecodes.glue
28local penalty_code  = nodecodes.penalty
29local glyph_code    = nodecodes.glyph
30local par_code      = nodecodes.par
31
32local indent_code   = nodes.listcodes.indent
33
34local hmode_code    = tex.modelevels.horizontal
35
36local nuts          = nodes.nuts
37local tonut         = nuts.tonut
38local tonode        = nuts.tonode
39
40----- getfield      = nuts.getfield
41local getnext       = nuts.getnext
42local getprev       = nuts.getprev
43local getboth       = nuts.getboth
44local getdisc       = nuts.getdisc
45local getid         = nuts.getid
46local getsubtype    = nuts.getsubtype
47local getlist       = nuts.getlist
48local getattribute  = nuts.getattribute
49local getbox        = nuts.getbox
50local getdirection  = nuts.getdirection
51local getwidth      = nuts.getwidth
52local getwhd        = nuts.getwhd
53local takebox       = nuts.takebox
54
55----- setfield      = nuts.setfield
56local setlink       = nuts.setlink
57local setboth       = nuts.setboth
58local setnext       = nuts.setnext
59local setprev       = nuts.setprev
60local setbox        = nuts.setbox
61local setlist       = nuts.setlist
62local setdisc       = nuts.setdisc
63local setwidth      = nuts.setwidth
64local setheight     = nuts.setheight
65local setdepth      = nuts.setdepth
66local setshift      = nuts.setshift
67local setsplit      = nuts.setsplit
68local setattrlist   = nuts.setattrlist
69
70local flushnode     = nuts.flushnode
71local flushlist     = nuts.flushlist
72local copy_node     = nuts.copy
73local copylist      = nuts.copylist
74local find_tail     = nuts.tail
75local getdimensions = nuts.dimensions
76local hpack         = nuts.hpack
77local vpack         = nuts.vpack
78local traverseid    = nuts.traverseid
79local traverse      = nuts.traverse
80local free          = nuts.free
81local findtail      = nuts.tail
82
83local nextdisc      = nuts.traversers.disc
84local nextdir       = nuts.traversers.dir
85local nexthlist     = nuts.traversers.hlist
86
87local listtoutf     = nodes.listtoutf
88
89local nodepool      = nuts.pool
90local new_penalty   = nodepool.penalty
91local new_hlist     = nodepool.hlist
92local new_glue      = nodepool.glue
93
94local setlistcolor  = nodes.tracers.colors.setlist
95
96local texget        = tex.get
97local texgetbox     = tex.getbox
98local texsetdimen   = tex.setdimen
99local texgetnest    = tex.getnest
100
101local function hyphenatedlist(head,usecolor)
102    local current = head and tonut(head)
103    while current do
104        local id = getid(current)
105        local prev, next = getboth(current)
106        if id == disc_code then
107            local pre, post, replace = getdisc(current)
108            if not usecolor then
109                -- nothing fancy done
110            elseif pre and post then
111                setlistcolor(pre,"darkmagenta")
112                setlistcolor(post,"darkcyan")
113            elseif pre then
114                setlistcolor(pre,"darkyellow")
115            elseif post then
116                setlistcolor(post,"darkyellow")
117            end
118            if replace then
119                flushlist(replace)
120            end
121            setdisc(current)
122            if pre then
123                setlink(prev,new_penalty(10000),pre)
124                setlink(find_tail(pre),current)
125            end
126            if post then
127                setlink(current,new_penalty(10000),post)
128                setlink(find_tail(post),next)
129            end
130        elseif id == vlist_code or id == hlist_code then
131            hyphenatedlist(getlist(current))
132        end
133        current = next
134    end
135end
136
137implement {
138    name      = "hyphenatedlist",
139    arguments = { "integer", "boolean" },
140    actions   = function(n,color)
141        local b = texgetbox(n)
142        if b then
143            hyphenatedlist(b.list,color)
144        end
145    end
146}
147
148-- local function hyphenatedhack(head,pre)
149--     pre = tonut(pre)
150--     for n in nextdisc, tonut(head) do
151--         local hyphen = getfield(n,"pre")
152--         if hyphen then
153--             flushlist(hyphen)
154--         end
155--         setfield(n,"pre",copylist(pre))
156--     end
157-- end
158--
159-- commands.hyphenatedhack = hyphenatedhack
160
161local function checkedlist(list)
162    if type(list) == "number" then
163        return getlist(getbox(tonut(list)))
164    else
165        return tonut(list)
166    end
167end
168
169implement {
170    name      = "showhyphenatedinlist",
171    arguments = "integer",
172    actions   = function(n)
173        -- we just hyphenate (as we pass a hpack) .. a bit too much casting but ...
174        local l = languages.hyphenators.handler(checkedlist(n))
175        report_hyphenation("show: %s",listtoutf(l,false,true))
176    end
177}
178
179local function applytochars(current,doaction,noaction,nested)
180    while current do
181        local id = getid(current)
182        if nested and (id == hlist_code or id == vlist_code) then
183            context.beginhbox()
184            applytochars(getlist(current),doaction,noaction,nested)
185            context.endhbox()
186        elseif id ~= glyph_code then
187            noaction(tonode(copy_node(current)))
188        else
189            doaction(tonode(copy_node(current)))
190        end
191        current = getnext(current)
192    end
193end
194
195local function applytowords(current,doaction,noaction,nested)
196    local start
197    while current do
198        local id = getid(current)
199        if id == glue_code then
200            if start then
201                doaction(tonode(copylist(start,current)))
202                start = nil
203            end
204            noaction(tonode(copy_node(current)))
205        elseif nested and (id == hlist_code or id == vlist_code) then
206            context.beginhbox()
207            applytowords(getlist(current),doaction,noaction,nested)
208            context.egroup()
209        elseif not start then
210            start = current
211        end
212        current = getnext(current)
213    end
214    if start then
215        doaction(tonode(copylist(start)))
216    end
217end
218
219local methods = {
220    char       = applytochars,
221    characters = applytochars,
222    word       = applytowords,
223    words      = applytowords,
224}
225
226implement {
227    name      = "applytobox",
228    arguments = {
229        {
230            { "box", "integer" },
231            { "command" },
232            { "method" },
233            { "nested", "boolean" },
234        }
235    },
236    actions   = function(specification)
237        local list   = checkedlist(specification.box)
238        local action = methods[specification.method or "char"]
239        if list and action then
240            action(list,context[specification.command or "ruledhbox"],context,specification.nested)
241        end
242     end
243}
244
245local split_char = lpeg.Ct(lpeg.C(1)^0)
246local split_word = lpeg.tsplitat(lpeg.patterns.space)
247local split_line = lpeg.tsplitat(lpeg.patterns.eol)
248
249local function processsplit(specification)
250    local str     = specification.data    or ""
251    local command = specification.command or "ruledhbox"
252    local method  = specification.method  or "word"
253    local spaced  = specification.spaced
254    if method == "char" or method == "character" then
255        local words = lpegmatch(split_char,str)
256        for i=1,#words do
257            local word = words[i]
258            if word == " " then
259                if spaced then
260                    context.space()
261                end
262            elseif command then
263                context[command](word)
264            else
265                context(word)
266            end
267        end
268    elseif method == "word" then
269        local words = lpegmatch(split_word,str)
270        for i=1,#words do
271            local word = words[i]
272            if spaced and i > 1 then
273                context.space()
274            end
275            if command then
276                context[command](word)
277            else
278                context(word)
279            end
280        end
281    elseif method == "line" then
282        local words = lpegmatch(split_line,str)
283        for i=1,#words do
284            local word = words[i]
285            if spaced and i > 1 then
286                context.par()
287            end
288            if command then
289                context[command](word)
290            else
291                context(word)
292            end
293        end
294    else
295        context(str)
296    end
297end
298
299implement {
300    name      = "processsplit",
301    actions   = processsplit,
302    arguments = {
303        {
304            { "data" },
305            { "command" },
306            { "method" },
307            { "spaced", "boolean" },
308        }
309    }
310}
311
312local a_vboxtohboxseparator = attributes.private("vboxtohboxseparator")
313
314implement {
315    name      = "vboxlisttohbox",
316    arguments = { "integer", "integer", "dimen" },
317    actions   = function(original,target,inbetween)
318        local current = getlist(getbox(original))
319        local head = nil
320        local tail = nil
321        while current do
322            local id   = getid(current)
323            local next = getnext(current)
324            if id == hlist_code then
325                local list = getlist(current)
326                if head then
327                    if inbetween > 0 then
328                        local n = new_glue(0,0,inbetween)
329                        setlink(tail,n)
330                        tail = n
331                    end
332                    setlink(tail,list)
333                else
334                    head = list
335                end
336                tail = find_tail(list)
337                -- remove last separator
338                if getid(tail) == hlist_code and getattribute(tail,a_vboxtohboxseparator) == 1 then
339                    local temp = tail
340                    local prev = getprev(tail)
341                    if next then
342                        local list = getlist(tail)
343                        setlink(prev,list)
344                        setlist(tail)
345                        tail = find_tail(list)
346                    else
347                        tail = prev
348                    end
349                    flushnode(temp)
350                end
351                -- done
352                setnext(tail)
353                setlist(current)
354            end
355            current = next
356        end
357        local result = new_hlist()
358        setlist(result,head)
359        setbox(target,result)
360     -- setbox(target,new_hlist(head))
361    end
362}
363
364implement {
365    name      = "hboxtovbox",
366    arguments = "integer",
367    actions   = function(n)
368        local b = getbox(n)
369        local factor = texget("baselineskip",false) / texget("hsize")
370        setdepth(b,0)
371        setheight(b,getwidth(b) * factor)
372    end
373}
374
375implement {
376    name      = "boxtostring",
377    arguments = "integer",
378    actions   = function(n)
379        context.puretext(nodes.toutf(texgetbox(n).list)) -- helper is defined later
380    end
381}
382
383local function getnaturaldimensions(n)
384    local w = 0
385    local h = 0
386    local d = 0
387    local l = getlist(getbox(n))
388    if l then
389        w, h, d = getdimensions(l)
390    end
391    texsetdimen("lastnaturalboxwd",w)
392    texsetdimen("lastnaturalboxht",h)
393    texsetdimen("lastnaturalboxdp",d)
394    return w, h, d
395end
396
397implement {
398    name      = "getnaturaldimensions",
399    arguments = "integer",
400    actions   = getnaturaldimensions
401}
402
403implement {
404    name      = "naturalwd",
405    arguments = "integer",
406    actions   = function(n)
407        getnaturaldimensions(n)
408        context.lastnaturalboxwd(false)
409    end
410}
411
412implement {
413    name      = "getnaturalwd",
414    arguments = "integer",
415    actions   = function(n)
416        local w = 0
417        local h = 0
418        local d = 0
419        local l = getlist(getbox(n))
420        if l then
421            w, h, d = getdimensions(l)
422        end
423        context("\\dimexpr%i\\scaledpoint\\relax",w)
424    end
425}
426
427local function setboxtonaturalwd(n)
428    local old = takebox(n)
429    local new = hpack(getlist(old))
430    setlist(old,nil)
431    flushnode(old)
432    setbox(n,new)
433end
434
435implement {
436    name      = "setnaturalwd",
437    arguments = "integer",
438    actions   = setboxtonaturalwd
439}
440
441nodes.setboxtonaturalwd = setboxtonaturalwd
442
443local doifelse = commands.doifelse
444
445do
446
447    local dirvalues        = nodes.dirvalues
448    local lefttoright_code = dirvalues.lefttoright
449    local righttoleft_code = dirvalues.righttoleft
450
451    local function firstdirinbox(n)
452        local b = getbox(n)
453        if b then
454            local l = getlist(b)
455            if l then
456                for d in nextdir, l do
457                    return getdirection(d)
458                end
459                for h in nexthlist, l do
460                    return getdirection(h)
461                end
462            end
463        end
464        return lefttoright_code
465    end
466
467    nodes.firstdirinbox = firstdirinbox
468
469    implement {
470        name      = "doifelserighttoleftinbox",
471        arguments = "integer",
472        actions   = function(n)
473            doifelse(firstdirinbox(n) == righttoleft_code)
474        end
475    }
476
477end
478
479-- new (handy for mp) .. might move to its own module
480
481do
482
483    local nuts      = nodes.nuts
484    local tonode    = nuts.tonode
485    local takebox   = nuts.takebox
486    local flushlist = nuts.flushlist
487    local copylist  = nuts.copylist
488    local getwhd    = nuts.getwhd
489    local setbox    = nuts.setbox
490    local new_hlist = nuts.pool.hlist
491
492    local boxes     = { }
493    nodes.boxes     = boxes
494    local cache     = table.setmetatableindex("table")
495    local report    = logs.reporter("boxes","cache")
496    local trace     = false
497
498    trackers.register("nodes.boxes",function(v) trace = v end)
499
500    function boxes.save(category,name,b)
501        name = tonumber(name) or name
502        local b = takebox(b)
503        if trace then
504            report("category %a, name %a, %s (%s)",category,name,"save",b and "content" or "empty")
505        end
506        cache[category][name] = b or false
507    end
508
509    function boxes.savenode(category,name,n)
510        name = tonumber(name) or name
511        if trace then
512            report("category %a, name %a, %s (%s)",category,name,"save",n and "content" or "empty")
513        end
514        cache[category][name] = tonut(n) or false
515    end
516
517    function boxes.found(category,name)
518        name = tonumber(name) or name
519        return cache[category][name] and true or false
520    end
521
522    function boxes.direct(category,name,copy)
523        name = tonumber(name) or name
524        local c = cache[category]
525        local b = c[name]
526        if not b then
527            -- do nothing, maybe trace
528        elseif copy then
529            b = copylist(b)
530        else
531            c[name] = false
532        end
533        if trace then
534            report("category %a, name %a, %s (%s)",category,name,"direct",b and "content" or "empty")
535        end
536        if b then
537            return tonode(b)
538        end
539    end
540
541    function boxes.restore(category,name,box,copy)
542        name = tonumber(name) or name
543        local c = cache[category]
544        local b = takebox(box)
545        if b then
546            flushlist(b)
547        end
548        local b = c[name]
549        if not b then
550            -- do nothing, maybe trace
551        elseif copy then
552            b = copylist(b)
553        else
554            c[name] = false
555        end
556        if trace then
557            report("category %a, name %a, %s (%s)",category,name,"restore",b and "content" or "empty")
558        end
559        setbox(box,b or nil)
560    end
561
562    function boxes.dimensions(category,name)
563        name = tonumber(name) or name
564        local b = cache[category][name]
565        if b then
566            return getwhd(b)
567        else
568            return 0, 0, 0
569        end
570    end
571
572    function boxes.reset(category,name)
573        name = tonumber(name) or name
574        local c = cache[category]
575        if name and name ~= "" then
576            local b = c[name]
577            if b then
578                flushlist(b)
579                c[name] = false
580            end
581            if trace then
582                report("category %a, name %a, reset",category,name)
583            end
584        else
585            for k, b in next, c do
586                if b then
587                    flushlist(b)
588                end
589            end
590            cache[category] = { }
591            if trace then
592                report("category %a, reset",category)
593            end
594        end
595    end
596
597    implement {
598        name      = "putboxincache",
599        arguments = { "string", "string", "integer" },
600        actions   = boxes.save,
601    }
602
603    implement {
604        name      = "getboxfromcache",
605        arguments = { "string", "string", "integer" },
606        actions   = boxes.restore,
607    }
608
609    implement {
610        name      = "directboxfromcache",
611        arguments = "2 strings",
612        actions   = { boxes.direct, context },
613     -- actions   = function(category,name) local b = boxes.direct(category,name) if b then context(b) end end,
614    }
615
616    implement {
617        name      = "directcopyboxfromcache",
618        arguments = { "string", "string", true },
619        actions   = { boxes.direct, context },
620     -- actions   = function(category,name) local b = boxes.direct(category,name,true) if b then context(b) end end,
621    }
622
623    implement {
624        name      = "copyboxfromcache",
625        arguments = { "string", "string", "integer", true },
626        actions   = boxes.restore,
627    }
628
629    implement {
630        name      = "doifelseboxincache",
631        arguments = "2 strings",
632        actions   = { boxes.found, doifelse },
633    }
634
635    implement {
636        name      = "resetboxesincache",
637        arguments = "string",
638        actions   = boxes.reset,
639    }
640
641end
642
643implement {
644    name    = "lastlinewidth",
645    actions = function()
646        local head = tex.lists.page_head
647        -- list dimensions returns 3 value but we take the first
648        context(head and getdimensions(getlist(find_tail(tonut(tex.lists.page_head)))) or 0)
649    end
650}
651
652implement {
653    name      = "shiftbox",
654    arguments = { "integer", "dimension" },
655    actions   = function(n,d)
656        setshift(getbox(n),d)
657    end,
658}
659
660implement { name = "vpackbox", arguments = "integer", actions = function(n) setbox(n,(vpack(takebox(n)))) end }
661implement { name = "hpackbox", arguments = "integer", actions = function(n) setbox(n,(hpack(takebox(n)))) end }
662
663implement { name = "vpackedbox", arguments = "integer", actions = function(n) context(vpack(takebox(n))) end }
664implement { name = "hpackedbox", arguments = "integer", actions = function(n) context(hpack(takebox(n))) end }
665
666implement {
667    name      = "scangivendimensions",
668    public    = true,
669    protected = true,
670    arguments = {
671        {
672            { "width",  "dimension" },
673            { "height", "dimension" },
674            { "depth",  "dimension" },
675        },
676    },
677    actions   = function(t)
678        texsetdimen("givenwidth", t.width  or 0)
679        texsetdimen("givenheight",t.height or 0)
680        texsetdimen("givendepth", t.depth  or 0)
681    end,
682}
683
684local function stripglue(list)
685    local done  = false
686    local first = list
687    while first do
688        local id = getid(first)
689        if id == glue_code or id == penalty_code then
690            first = getnext(first)
691        else
692            break
693        end
694    end
695    if first and first ~= list then
696        -- we have discardables
697        setsplit(getprev(first),first)
698        flushlist(list)
699        list = first
700        done = true
701    end
702    if list then
703        local tail = findtail(list)
704        local last = tail
705        while last do
706            local id = getid(last)
707            if id == glue_code or id == penalty_code then
708                last = getprev(last)
709            else
710                break
711            end
712        end
713        if last ~= tail then
714            -- we have discardables
715            flushlist(getnext(last))
716            setnext(last)
717            done = true
718        end
719    end
720    return list, done
721end
722
723local function limitate(t) -- don't pack the result !
724    local text = t.text
725    if text then
726        text = tonut(text)
727    else
728        return
729    end
730    local sentinel = t.sentinel
731    if sentinel then
732        sentinel = tonut(sentinel)
733        local s = getlist(sentinel)
734        setlist(sentinel)
735        free(sentinel)
736        sentinel = s
737    else
738        return tonode(text)
739    end
740    local width = getwidth(text)
741    local list  = getlist(text)
742    local done  = false
743    if t.strip then
744        list, done = stripglue(list)
745        if not list then
746            setlist(text)
747            setwidth(text,0)
748            return text
749        elseif done then
750            width = getdimensions(list)
751            setlist(text,list)
752        end
753    end
754    local left  = t.left or 0
755    local right = t.right or 0
756    if left + right < width then
757        local last     = nil
758        local first    = nil
759        local maxleft  = left
760        local maxright = right
761        local swidth   = getwidth(sentinel)
762        if maxright > 0 then
763            maxleft  = maxleft  - swidth/2
764            maxright = maxright - swidth/2
765        else
766            maxleft  = maxleft  - swidth
767        end
768        for n in traverseid(glue_code,list) do
769            local width = getdimensions(list,n)
770            if width > maxleft then
771                if not last then
772                    last = n
773                end
774                break
775            else
776                last = n
777            end
778        end
779        if last and maxright > 0 then
780            for n in traverseid(glue_code,last) do
781                local width = getdimensions(n)
782                if width < maxright then
783                    first = n
784                    break
785                else
786                    first = n
787                end
788            end
789        end
790        if last then
791            local rest = getnext(last)
792            if rest then
793                local tail = findtail(sentinel)
794                if first and getid(first) == glue_code and getid(tail) == glue_code then
795                    setwidth(first,0)
796                end
797                if last and getid(last) == glue_code and getid(sentinel) == glue_code then
798                    setwidth(last,0)
799                end
800                if first and first ~= last then
801                    local prev = getprev(first)
802                    if prev then
803                        setnext(prev)
804                    end
805                    setlink(tail,first)
806                end
807                setlink(last,sentinel)
808                setprev(rest)
809                flushlist(rest)
810            end
811        end
812    end
813    setlist(text)
814    free(text)
815    return tonode(list)
816end
817
818implement {
819    name      = "limitated",
820    public    = true,
821    protected = true,
822    arguments = {
823        {
824            { "left",     "dimension" },
825            { "right",    "dimension" },
826            { "text",     "hbox" },
827            { "sentinel", "hbox" },
828            { "strip",    "boolean" },
829        }
830    },
831    actions   = function(t)
832        context.dontleavehmode()
833        context(limitate(t))
834    end,
835}
836
837implement {
838    name      = "doifelseindented",
839    public    = true,
840    protected = true,
841    actions   = function()
842        local n = texgetnest()
843        local b = false
844        if n.mode == hmode_code then
845            n = tonut(n.head)
846            while n do
847                n = getnext(n)
848                if n then
849                    local id = getid(n)
850                    if id == hlist_code then
851                        if getsubtype(n) == indent_code then
852                            b = getwidth(n) > 0
853                            break
854                        end
855                    elseif id ~= par_code then
856                        break
857                    end
858                end
859            end
860        end
861        commands.doifelse(b)
862    end,
863}
864
865implement {
866    name      = "noflinesinbox",
867    public    = true,
868    protected = false,
869    arguments = "integer",
870    actions   = function(n)
871        local c = 0
872        local b = getbox(n)
873        if b then
874            b = getlist(b)
875            if b then
876                for n, id in traverse(b) do
877                    if id == hlist_code or id == vlist_code then
878                        c = c + 1
879                    end
880                end
881            end
882        end
883        context(c)
884    end,
885}
886
887do
888
889    local takebox = tex.takebox
890
891    interfaces.implement {
892        name      = "thebox",
893        public    = true,
894        arguments = "integer",
895        actions   = function(n)
896            context(takebox(n))
897        end
898    }
899
900end
901