supp-box.lmt /size: 49 Kb    last modification: 2025-02-21 11:03
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 ctx_doifelse  = commands.doifelse
23
24local nodecodes     = nodes.nodecodes
25
26local disc_code         <const> = nodecodes.disc
27local hlist_code        <const> = nodecodes.hlist
28local vlist_code        <const> = nodecodes.vlist
29local glue_code         <const> = nodecodes.glue
30local penalty_code      <const> = nodecodes.penalty
31local glyph_code        <const> = nodecodes.glyph
32local par_code          <const> = nodecodes.par
33local rule_code         <const> = nodecodes.rule
34
35local indentlist_code   <const> = nodes.listcodes.indent
36local line_code         <const> = nodes.listcodes.line
37local balanceslot_code  <const> = nodes.listcodes.balanceslot
38----- spacing_code      <const> = nodes.listcodes.spacing
39
40local spacing_code      <const> = nodes.rulecodes.spacing
41
42local topskip_code      <const> = nodes.gluecodes.topskip
43local indentskip_code   <const> = nodes.gluecodes.indentskip
44local userskip_code     <const> = nodes.gluecodes.userskip
45local baselineskip_code <const> = nodes.gluecodes.baselineskip
46
47local hmode_code        <const> = tex.modelevels.horizontal
48
49local nuts          = nodes.nuts
50local tonut         = nuts.tonut
51local tonode        = nuts.tonode
52
53local getnext       = nuts.getnext
54local getprev       = nuts.getprev
55local getboth       = nuts.getboth
56local getdisc       = nuts.getdisc
57local getid         = nuts.getid
58local getsubtype    = nuts.getsubtype
59local getidsubtype  = nuts.getidsubtype
60local getlist       = nuts.getlist
61local getattribute  = nuts.getattribute
62local getbox        = nuts.getbox
63local getdirection  = nuts.getdirection
64local getwidth      = nuts.getwidth
65local getheight     = nuts.getheight
66local getdepth      = nuts.getdepth
67local getwhd        = nuts.getwhd
68local takebox       = nuts.takebox
69
70local setlink       = nuts.setlink
71local setboth       = nuts.setboth
72local setnext       = nuts.setnext
73local setprev       = nuts.setprev
74local setbox        = nuts.setbox
75local setlist       = nuts.setlist
76local setdisc       = nuts.setdisc
77local setwidth      = nuts.setwidth
78local setheight     = nuts.setheight
79local setdepth      = nuts.setdepth
80local setshift      = nuts.setshift
81local setsplit      = nuts.setsplit
82local setattrlist   = nuts.setattrlist
83local setwhd        = nuts.setwhd
84local setglue       = nuts.setglue
85local setsubtype    = nuts.setsubtype
86
87local flushnode     = nuts.flushnode
88local flushlist     = nuts.flushlist
89local copynode      = nuts.copy
90local copylist      = nuts.copylist
91local findtail      = nuts.tail
92local getdimensions = nuts.dimensions
93local getwidth      = nuts.getwidth
94local hpack         = nuts.hpack
95local vpack         = nuts.vpack
96local traverseid    = nuts.traverseid
97----- traverse      = nuts.traverse
98local free          = nuts.free
99local findtail      = nuts.tail
100local reverse       = nuts.reverse
101local effectiveglue = nuts.effectiveglue
102
103local insertbefore  = nuts.insertbefore
104local replacenode   = nuts.replace
105local insertafter   = nuts.insertafter
106
107local nextdisc      = nuts.traversers.disc
108local nextdir       = nuts.traversers.dir
109local nexthlist     = nuts.traversers.hlist
110
111local countnodes    = nuts.count
112----- traverselist  = nuts.traverselist
113
114local listtoutf     = nodes.listtoutf
115
116local nodepool      = nuts.pool
117local new_penalty   = nodepool.penalty
118local new_spacing   = nodepool.spacingrule
119local new_hlist     = nodepool.hlist
120local new_glue      = nodepool.glue
121
122local setlistcolor  = nodes.tracers.colors.setlist
123
124local texget        = tex.get
125local texgetbox     = tex.getbox
126local texisdimen    = tex.isdimen
127local texsetdimen   = tex.setdimen
128local texgetnest    = tex.getnest
129
130local values             = tokens.values
131local dimension_value    <const> = values.dimension
132local cardinal_value     <const> = values.cardinal
133local direct_value       <const> = values.direct
134local conditional_value  <const> = values.conditional
135local boolean_value      <const> = values.boolean
136
137local d_lastnaturalboxwd <const> = texisdimen("lastnaturalboxwd")
138local d_lastnaturalboxht <const> = texisdimen("lastnaturalboxht")
139local d_lastnaturalboxdp <const> = texisdimen("lastnaturalboxdp")
140
141local d_givenwidth       <const> = texisdimen("givenwidth")
142local d_givenheight      <const> = texisdimen("givenheight")
143local d_givendepth       <const> = texisdimen("givendepth")
144
145local function hyphenatedlist(head,usecolor)
146    local current = head and tonut(head)
147    while current do
148        local id = getid(current)
149        local prev, next = getboth(current)
150        if id == disc_code then
151            local pre, post, replace = getdisc(current)
152            if not usecolor then
153                -- nothing fancy done
154            elseif pre and post then
155                setlistcolor(pre,"darkmagenta")
156                setlistcolor(post,"darkcyan")
157            elseif pre then
158                setlistcolor(pre,"darkyellow")
159            elseif post then
160                setlistcolor(post,"darkyellow")
161            end
162            if replace then
163                flushlist(replace)
164            end
165            setdisc(current)
166            if pre then
167                setlink(prev,new_penalty(10000),pre)
168                setlink(findtail(pre),current)
169            end
170            if post then
171                setlink(current,new_penalty(10000),post)
172                setlink(findtail(post),next)
173            end
174        elseif id == vlist_code or id == hlist_code then
175            hyphenatedlist(getlist(current))
176        end
177        current = next
178    end
179end
180
181implement {
182    name      = "hyphenatedlist",
183    arguments = { "integer", "boolean" },
184    actions   = function(n,color)
185        local b = texgetbox(n)
186        if b then
187            hyphenatedlist(b.list,color)
188        end
189    end
190}
191
192local function checkedlist(list)
193    if type(list) == "number" then
194        return getlist(getbox(tonut(list)))
195    else
196        return tonut(list)
197    end
198end
199
200implement {
201    name      = "showhyphenatedinlist",
202    arguments = "integer",
203    actions   = function(n)
204        -- we just hyphenate (as we pass a hpack) .. a bit too much casting but ...
205        local l = languages.hyphenators.handler(checkedlist(n))
206        report_hyphenation("show: %s",listtoutf(l,false,true))
207    end
208}
209
210do
211
212    local function applytochars(current,doaction,noaction,nested)
213        while current do
214            local id = getid(current)
215            if nested and (id == hlist_code or id == vlist_code) then
216                context.beginhbox()
217                applytochars(getlist(current),doaction,noaction,nested)
218                context.endhbox()
219            elseif id ~= glyph_code then
220                noaction(tonode(copynode(current)))
221            else
222                doaction(tonode(copynode(current)))
223            end
224            current = getnext(current)
225        end
226    end
227
228    local function applytowords(current,doaction,noaction,nested)
229        local start
230        while current do
231            local id = getid(current)
232            if id == glue_code then
233                if start then
234                    doaction(tonode(copylist(start,current)))
235                    start = nil
236                end
237                noaction(tonode(copynode(current)))
238            elseif nested and (id == hlist_code or id == vlist_code) then
239                context.beginhbox()
240                applytowords(getlist(current),doaction,noaction,nested)
241                context.egroup()
242            elseif not start then
243                start = current
244            end
245            current = getnext(current)
246        end
247        if start then
248            doaction(tonode(copylist(start)))
249        end
250    end
251
252    local methods = {
253        char       = applytochars,
254        characters = applytochars,
255        word       = applytowords,
256        words      = applytowords,
257    }
258
259    implement {
260        name      = "applytobox",
261        arguments = {
262            {
263                { "box", "integer" },
264                { "command" },
265                { "method" },
266                { "nested", "boolean" },
267            }
268        },
269        actions   = function(specification)
270            local list   = checkedlist(specification.box)
271            local action = methods[specification.method or "char"]
272            if list and action then
273                action(list,context[specification.command or "ruledhbox"],context,specification.nested)
274            end
275         end
276    }
277
278    local split_char = lpeg.Ct(lpeg.C(1)^0)
279    local split_word = lpeg.tsplitat(lpeg.patterns.space)
280    local split_line = lpeg.tsplitat(lpeg.patterns.eol)
281
282    local function processsplit(specification)
283        local str     = specification.data    or ""
284        local command = specification.command or "ruledhbox"
285        local method  = specification.method  or "word"
286        local spaced  = specification.spaced
287        if command then
288            command = context[command]
289        end
290        if method == "char" or method == "character" then
291            if spaced then
292                spaced = context.space
293            end
294            local words = lpegmatch(split_char,str)
295            for i=1,#words do
296                local word = words[i]
297                if word == " " then
298                    if spaced then
299                        spaced()
300                    end
301                elseif command then
302                    command(word)
303                else
304                    context(word)
305                end
306            end
307        elseif method == "word" then
308            if spaced then
309                spaced = context.space
310            end
311            local words = lpegmatch(split_word,str)
312            for i=1,#words do
313                local word = words[i]
314                if spaced and i > 1 then
315                    spaced()
316                end
317                if command then
318                    command(word)
319                else
320                    context(word)
321                end
322            end
323        elseif method == "line" then
324            if spaced then
325                spaced = context.par
326            end
327            local words = lpegmatch(split_line,str)
328            for i=1,#words do
329                local word = words[i]
330                if spaced and i > 1 then
331                    spaced()
332                end
333                if command then
334                    command(word)
335                else
336                    context(word)
337                end
338            end
339        else
340            context(str)
341        end
342    end
343
344    implement {
345        name      = "processsplit",
346        actions   = processsplit,
347        arguments = {
348            {
349                { "data" },
350                { "command" },
351                { "method" },
352                { "spaced", "boolean" },
353            }
354        }
355    }
356
357end
358
359do
360
361    local a_vboxtohboxseparator <const> = attributes.private("vboxtohboxseparator")
362
363    implement {
364        name      = "vboxlisttohbox",
365        arguments = { "integer", "integer", "dimension" },
366        actions   = function(original,target,inbetween)
367            local current = getlist(getbox(original))
368            local head = nil
369            local tail = nil
370            while current do
371                local id   = getid(current)
372                local next = getnext(current)
373                if id == hlist_code then
374                    local list = getlist(current)
375                    if head then
376                        if inbetween > 0 then
377                            local n = new_glue(0,0,inbetween)
378                            setlink(tail,n)
379                            tail = n
380                        end
381                        setlink(tail,list)
382                    else
383                        head = list
384                    end
385                    tail = findtail(list)
386                    -- remove last separator
387                    if getid(tail) == hlist_code and getattribute(tail,a_vboxtohboxseparator) == 1 then
388                        local temp = tail
389                        local prev = getprev(tail)
390                        if next then
391                            local list = getlist(tail)
392                            setlink(prev,list)
393                            setlist(tail)
394                            tail = findtail(list)
395                        else
396                            tail = prev
397                        end
398                        flushnode(temp)
399                    end
400                    -- done
401                    setnext(tail)
402                    setlist(current)
403                end
404                current = next
405            end
406            local result = new_hlist()
407            setlist(result,head)
408            setbox(target,result)
409         -- setbox(target,new_hlist(head))
410        end
411    }
412
413    implement {
414        name      = "hboxtovbox",
415        arguments = "integer",
416        actions   = function(n)
417            local b = getbox(n)
418            local factor = texget("baselineskip",false) / texget("hsize")
419            setdepth(b,0)
420            setheight(b,getwidth(b) * factor)
421        end
422    }
423
424end
425
426implement {
427    name      = "boxtostring",
428    arguments = "integer",
429    public    = true,
430    actions   = function(n)
431        local b = texgetbox(n)
432        local l = b and b.list
433        if l then
434            -- helper is defined later
435            local s = nodes.toutf(l)
436            if s then
437                context.verbatim(s)
438            end
439        end
440    end
441}
442
443do
444
445    -- we can now move this to the tex end
446
447    local function getnaturaldimensions(n)
448        local w = 0
449        local h = 0
450        local d = 0
451        local l = getlist(getbox(n))
452        if l then
453            w, h, d = getdimensions(l)
454        end
455        texsetdimen(d_lastnaturalboxwd,w)
456        texsetdimen(d_lastnaturalboxht,h)
457        texsetdimen(d_lastnaturalboxdp,d)
458        return w, h, d
459    end
460
461    local function setboxtonaturalwd(n)
462        local old = takebox(n)
463        local new = hpack(getlist(old))
464        setlist(old,nil)
465        flushnode(old)
466        setbox(n,new)
467    end
468
469    implement {
470        name      = "getnaturaldimensions",
471        arguments = "integer",
472        public    = true,
473        protected = true,
474        untraced  = true,
475        actions   = getnaturaldimensions
476    }
477
478    implement {
479        name      = "naturalwd",
480        arguments = "integer",
481        usage     = "value",
482        public    = true,
483        protected = true,
484        actions   = function(n)
485            return dimension_value, (getnaturaldimensions(n))
486        end
487    }
488
489    implement {
490        name      = "getnaturalwd",
491        arguments = "integer",
492        usage     = "value",
493        public    = true,
494        protected = true,
495        actions   = function(n)
496            local l = getlist(getbox(n))
497            return dimension_value, l and getdimensions(l) or 0
498        end
499    }
500
501    implement {
502        name      = "setnaturalwd",
503        arguments = "integer",
504        public    = true,
505        protected = true,
506        untraced  = true,
507        actions   = setboxtonaturalwd
508    }
509
510    nodes.setboxtonaturalwd = setboxtonaturalwd
511
512end
513
514do
515
516    local lefttoright_code <const> = tex.directioncodes.lefttoright
517    local righttoleft_code <const> = tex.directioncodes.righttoleft
518
519    local function firstdirinbox(n)
520        local b = getbox(n)
521        if b then
522            local l = getlist(b)
523            if l then
524                for d in nextdir, l do
525                    return getdirection(d)
526                end
527                for h in nexthlist, l do
528                    return getdirection(h)
529                end
530            end
531        end
532        return lefttoright_code
533    end
534
535    nodes.firstdirinbox = firstdirinbox
536
537    implement {
538        name      = "doifelserighttoleftinbox",
539        arguments = "integer",
540        actions   = function(n)
541            ctx_doifelse(firstdirinbox(n) == righttoleft_code)
542        end
543    }
544
545end
546
547-- new (handy for mp) .. might move to its own module
548
549do
550
551    ----- tonut           = nodes.tonut
552    ----- takebox         = nuts.takebox
553    ----- flushlist       = nuts.flushlist
554    ----- copylist        = nuts.copylist
555    ----- getwhd          = nuts.getwhd
556    ----- setbox          = nuts.setbox
557    ----- new_hlist       = nuts.pool.hlist
558
559    local boxes           = { }
560    nodes.boxes           = boxes
561    local cache           = table.setmetatableindex("table")
562    local report          = logs.reporter("boxes","cache")
563    local trace           = false
564
565    trackers.register("nodes.boxes",function(v) trace = v end)
566
567    function boxes.save(category,name,b)
568        name = tonumber(name) or name
569        local b = takebox(b)
570        if trace then
571            report("category %a, name %a, %s (%s)",category,name,"save",b and "content" or "empty")
572        end
573        if name == "+" then
574            name = #cache[category] + 1
575        end
576        cache[category][name] = b or false
577    end
578
579    function boxes.savenode(category,name,n)
580        name = tonumber(name) or name
581        if trace then
582            report("category %a, name %a, %s (%s)",category,name,"save",n and "content" or "empty")
583        end
584        cache[category][name] = tonut(n) or false
585    end
586
587    function boxes.found(category,name)
588        name = tonumber(name) or name
589        return cache[category][name] and true or false
590    end
591
592    function boxes.count(category)
593        return #cache[category]
594    end
595
596    function boxes.direct(category,name,copy)
597        name = tonumber(name) or name
598        local c = cache[category]
599        local b = c[name]
600-- if name == "+" then
601--         b = remove(c, 1)
602-- else
603--         b = c[name]
604-- end
605        if not b then
606            -- do nothing, maybe trace
607        elseif copy then
608            b = copylist(b)
609        else
610            c[name] = false
611        end
612        if trace then
613            report("category %a, name %a, %s (%s)",category,name,"direct",b and "content" or "empty")
614        end
615        if b then
616            return tonode(b)
617        end
618    end
619
620    function boxes.restore(category,name,box,copy)
621        name = tonumber(name) or name
622        local c = cache[category]
623        local b = takebox(box)
624        if b then
625            flushlist(b)
626        end
627        local b = c[name]
628        if not b then
629            -- do nothing, maybe trace
630        elseif copy then
631            b = copylist(b)
632        else
633            c[name] = false
634        end
635        if trace then
636            report("category %a, name %a, %s (%s)",category,name,"restore",b and "content" or "empty")
637        end
638        setbox(box,b or nil)
639    end
640
641    function boxes.prune(category)
642        -- this one assumes an indexed list
643        local c = cache[category]
644        local t = { }
645        local n = 0
646        for i=1,#c do
647            local ci = c[i]
648            if ci then
649                n = n + 1 ; t[n] = ci
650            end
651        end
652        cache[category] = t
653    end
654
655    local function dimensions(category,name)
656        name = tonumber(name) or name
657        local b = cache[category][name]
658        if b then
659            return getwhd(b)
660        else
661            return 0, 0, 0
662        end
663    end
664
665    boxes.dimensions = dimensions
666
667    function boxes.reset(category,name)
668        name = tonumber(name) or name
669        local c = cache[category]
670        if name and name ~= "" then
671            local b = c[name]
672            if b then
673                flushlist(b)
674                c[name] = false
675            end
676            if trace then
677                report("category %a, name %a, reset",category,name)
678            end
679        else
680            for k, b in next, c do
681                if b then
682                    flushlist(b)
683                end
684            end
685            cache[category] = { }
686            if trace then
687                report("category %a, reset",category)
688            end
689        end
690    end
691
692    function boxes.dispose(category)
693        boxes.reset(category)
694        cache[category] = nil -- or false (for tracing)
695    end
696
697    implement {
698        name      = "putboxincache",
699        public    = true,
700        protected = true,
701        arguments = { "string", "string", "integer" },
702        actions   = boxes.save,
703    }
704
705    implement {
706        name      = "getboxfromcache",
707        public    = true,
708        protected = true,
709        arguments = { "string", "string", "integer" },
710        actions   = boxes.restore,
711    }
712
713    implement {
714        name      = "directboxfromcache",
715        public    = true,
716        protected = true,
717        arguments = "2 strings",
718        actions   = { boxes.direct, context },
719     -- actions   = function(category,name) local b = boxes.direct(category,name) if b then context(b) end end,
720    }
721
722    implement {
723        name      = "directcopyboxfromcache",
724        public    = true,
725        protected = true,
726        arguments = { "string", "string", true },
727        actions   = { boxes.direct, context },
728     -- actions   = function(category,name) local b = boxes.direct(category,name,true) if b then context(b) end end,
729    }
730
731    implement {
732        name      = "copyboxfromcache",
733        public    = true,
734        protected = true,
735        arguments = { "string", "string", "integer", true },
736        actions   = boxes.restore,
737    }
738
739    implement {
740        name      = "doifelseboxincache",
741        public    = true,
742        protected = true,
743        arguments = "2 strings",
744        actions   = { boxes.found, ctx_doifelse },
745    }
746
747    implement {
748        name      = "resetboxesincache",
749        public    = true,
750        protected = true,
751        arguments = "string",
752        actions   = boxes.reset,
753    }
754
755    implement {
756        name      = "pruneboxesincache",
757        public    = true,
758        protected = true,
759        arguments = "string",
760        actions   = boxes.prune,
761    }
762
763    implement {
764        name      = "disposeboxesincache",
765        public    = true,
766        protected = true,
767        arguments = "string",
768        actions   = boxes.dispose,
769    }
770
771    -- we can share this ...
772
773    implement {
774        name      = "getboxcountfromcache",
775        public    = true,
776        protected = true,
777        usage     = "value",
778        arguments = "string",
779        actions   = function(category)
780            return cardinal_value, #cache[category]
781        end,
782    }
783
784    implement {
785        name      = "getboxwdfromcache",
786        public    = true,
787        protected = true,
788        usage     = "value",
789        arguments = "2 strings",
790        actions   = function(category,name)
791            local w, h, d = dimensions(category,name)
792            return dimension_value, w
793        end,
794    }
795
796    implement {
797        name      = "getboxhtfromcache",
798        arguments = "2 strings",
799        public    = true,
800        protected = true,
801        usage     = "value",
802        actions   = function(category,name)
803            local w, h, d = dimensions(category,name)
804            return dimension_value, h
805        end,
806    }
807
808    implement {
809        name      = "getboxdpfromcache",
810        arguments = "2 strings",
811        public    = true,
812        protected = true,
813        usage     = "value",
814        actions   = function(category,name)
815            local w, h, d = dimensions(category,name)
816            return dimension_value, d
817        end,
818    }
819
820    implement {
821        name      = "getboxhtdpfromcache",
822        arguments = "2 strings",
823        public    = true,
824        protected = true,
825        usage     = "value",
826        actions   = function(category,name)
827            local w, h, d = dimensions(category,name)
828            return dimension_value, h + d
829        end,
830    }
831
832    implement {
833        name      = "putnextboxincache",
834        public    = true,
835        protected = true,
836        arguments = { "string", "string", "box" },
837        actions   = function(category,name,b)
838            name = tonumber(name) or name
839            b = tonut(b)
840            if trace then
841                report("category %a, name %a, %s (%s)",category,name,"save",b and "content" or "empty")
842            end
843            cache[category][name] = b or false
844        end
845    }
846
847end
848
849-- implement {
850--     name    = "lastlinewidth",
851--     actions = function()
852--     local getnest = tex.getnest
853        local head = tex.lists.pagehead
854--         -- list dimensions returns 3 value but we take the first
855--         context(head and getdimensions(getlist(findtail(tonut(head)))) or 0)
856--     end
857-- }
858
859implement {
860    name      = "lastlinewidth",
861    usage     = "value",
862    public    = true,
863    protected = true,
864    actions   = function()
865        local n = texgetnest()
866        local l = n and getlist(tonut(n.tail))
867        -- todo: a variant that backtracks over penalties and glue
868        return dimension_value, l and getdimensions(l) or 0
869    end
870}
871
872-- implement {
873--     name      = "shiftbox",
874--     arguments = { "integer", "dimension" },
875--     actions   = function(n,d)
876--         setshift(getbox(n),d)
877--     end,
878-- }
879
880implement { name = "vpackbox", arguments = "integer", actions = function(n) setbox(n,(vpack(takebox(n)))) end }
881implement { name = "hpackbox", arguments = "integer", actions = function(n) setbox(n,(hpack(takebox(n)))) end }
882
883implement { name = "vpackedbox", arguments = "integer", actions = function(n) context(vpack(takebox(n))) end }
884implement { name = "hpackedbox", arguments = "integer", actions = function(n) context(hpack(takebox(n))) end }
885
886implement {
887    name      = "scangivendimensions",
888    public    = true,
889    protected = true,
890    arguments = {
891        {
892            { "width",  "dimension" },
893            { "height", "dimension" },
894            { "depth",  "dimension" },
895        },
896    },
897    actions   = function(t)
898        texsetdimen(d_givenwidth, t.width  or 0)
899        texsetdimen(d_givenheight,t.height or 0)
900        texsetdimen(d_givendepth, t.depth  or 0)
901    end,
902}
903
904local function stripglue(list)
905    local done  = false
906    local first = list
907    while first do
908        local id = getid(first)
909        if id == glue_code or id == penalty_code then
910            first = getnext(first)
911        else
912            break
913        end
914    end
915    if first and first ~= list then
916        -- we have discardables
917        setsplit(getprev(first),first)
918        flushlist(list)
919        list = first
920        done = true
921    end
922    if list then
923        local tail = findtail(list)
924        local last = tail
925        while last do
926            local id = getid(last)
927            if id == glue_code or id == penalty_code then
928                last = getprev(last)
929            else
930                break
931            end
932        end
933        if last ~= tail then
934            -- we have discardables
935            flushlist(getnext(last))
936            setnext(last)
937            done = true
938        end
939    end
940    return list, done
941end
942
943local function limitate(t) -- don't pack the result !
944    local text = t.text
945    if text then
946        text = tonut(text)
947    else
948        return
949    end
950    local sentinel = t.sentinel
951    if sentinel then
952        sentinel = tonut(sentinel)
953        local s = getlist(sentinel)
954        setlist(sentinel)
955        free(sentinel)
956        sentinel = s
957    else
958        return tonode(text)
959    end
960    local width = getwidth(text)
961    local list  = getlist(text)
962    local done  = false
963    if t.strip then
964        list, done = stripglue(list)
965        if not list then
966            setlist(text)
967            setwidth(text,0)
968            return text
969        elseif done then
970            width = getdimensions(list)
971            setlist(text,list)
972        end
973    end
974    local left  = t.left or 0
975    local right = t.right or 0
976    local total = left + right
977    if total < width then
978        local last     = nil
979        local first    = nil
980        local maxleft  = left
981        local maxright = right
982        local swidth   = getwidth(sentinel)
983        if maxright > 0 then
984            maxleft  = maxleft  - swidth/2
985            maxright = maxright - swidth/2
986        else
987            maxleft  = maxleft  - swidth
988        end
989        for n in traverseid(glue_code,list) do
990            local width = getdimensions(list,n)
991            if width > maxleft then
992                if not last then
993                    last = n
994                end
995                break
996            else
997                last = n
998            end
999        end
1000        if last and maxright > 0 then
1001            for n in traverseid(glue_code,last) do
1002                local width = getdimensions(n)
1003                if width < maxright then
1004                    first = n
1005                    break
1006                else
1007                    first = n
1008                end
1009            end
1010        end
1011        if last then
1012            local rest = getnext(last)
1013            if rest then
1014                local tail = findtail(sentinel)
1015                if first and getid(first) == glue_code and getid(tail) == glue_code then
1016                    setwidth(first,0)
1017                end
1018                if last and getid(last) == glue_code and getid(sentinel) == glue_code then
1019                    setwidth(last,0)
1020                end
1021                if first and first ~= last then
1022                    local prev = getprev(first)
1023                    if prev then
1024                        setnext(prev)
1025                    end
1026                    setlink(tail,first)
1027                end
1028                setlink(last,sentinel)
1029                setprev(rest)
1030                flushlist(rest)
1031            end
1032        end
1033    end
1034    setlist(text)
1035    free(text)
1036
1037    if t.freeze then
1038        local l = hpack(list,total,"exactly")
1039        for n in traverseid(glue_code,list) do
1040            setglue(n,(effectiveglue(n,l)))
1041        end
1042        setlist(l)
1043        flushnode(l)
1044    end
1045
1046    return tonode(list)
1047end
1048
1049implement {
1050    name      = "limitated",
1051    public    = true,
1052    protected = true,
1053    arguments = {
1054        {
1055            { "left",     "dimension" },
1056            { "right",    "dimension" },
1057            { "text",     "hbox" },
1058            { "sentinel", "hbox" },
1059            { "strip",    "boolean" },
1060            { "freeze",   "boolean" },
1061        }
1062    },
1063    actions   = function(t)
1064        context.dontleavehmode()
1065        context(limitate(t))
1066    end,
1067}
1068
1069-- only in lmtx:
1070
1071do
1072
1073    local naturalhsize = nuts.naturalhsize
1074
1075    implement {
1076        name      = "widthuptohere",
1077        public    = true,
1078        usage     = "value",
1079        actions   = function()
1080            local n = texgetnest()
1081            return dimension_value, n.mode == hmode_code and naturalhsize(getnext(tonut(n.head))) or 0
1082        end,
1083    }
1084
1085end
1086
1087implement {
1088    name      = "doifelseindented",
1089    public    = true,
1090    protected = true,
1091    actions   = function()
1092        local n = texgetnest()
1093        local b = false
1094        -- can be a helper !
1095        if n.mode == hmode_code then
1096            n = tonut(n.head)
1097            while n do
1098                n = getnext(n)
1099                if n then
1100                    local id, subtype = getidsubtype(n)
1101                    if id == glue_code then
1102                        if subtype == indentskip_code then
1103                            b = getwidth(n) > 0
1104                            break
1105                        end
1106                    elseif id == hlist_code then
1107                        if subtype == indentlist_code then
1108                            b = getwidth(n) > 0
1109                        end
1110                        break
1111                    elseif id == par_code then
1112                        -- okay
1113                    else
1114                        break
1115                    end
1116                end
1117            end
1118        end
1119        ctx_doifelse(b)
1120    end,
1121}
1122
1123implement {
1124    name      = "noflinesinbox",
1125    public    = true,
1126    usage     = "value",
1127    arguments = "integer",
1128    actions   = function(n)
1129        local c = 0
1130        local b = getbox(n)
1131        if b then
1132            b = getlist(b)
1133            if b then
1134                c = countnodes(hlist_code,b) + countnodes(vlist_code,b)
1135            end
1136        end
1137        return cardinal_value, c
1138    end,
1139}
1140
1141implement {
1142    name      = "gluetolinesinbox",
1143    public    = true,
1144    protected = false,
1145    arguments = { "integer", "dimension", "dimension", "dimension" },
1146    actions   = function(n,wd,ht,dp)
1147        -- maybe only inbetween
1148        local box = getbox(n)
1149        if box then
1150            local head = getlist(box)
1151            local current = head
1152            while current do
1153                local id, subtype = getidsubtype(current)
1154                if id == glue_code and subtype == userskip_code then
1155                 -- local line = new_hlist()
1156                 -- setwhd(line,wd,ht,dp) -- width for tracing
1157                 -- setsubtype(line,spacing_code)
1158                    local line = new_spacing(wd,ht,dp) -- rule
1159-- nuts.setoptions(line,16) -- discardable
1160                    setattrlist(line,current)
1161                    head          = insertbefore(head,current,new_glue(baselineskip_code))
1162                    head, current = replacenode(head,current,line)
1163                    head, current = insertafter(head,current,new_glue(baselineskip_code))
1164                end
1165                current = getnext(current)
1166            end
1167            setlist(box,head)
1168        end
1169    end,
1170}
1171
1172implement {
1173    name      = "ifgluelineinbox",
1174    public    = true,
1175    usage     = "condition",
1176    arguments = "integer",
1177    actions   = function(n)
1178        local box   = getbox(n)
1179        local found = false
1180        if box and getsubtype(box) == balanceslot_code then
1181            local current = getlist(box)
1182            while current do
1183                local id, subtype = getidsubtype(current)
1184             -- if id == hlist_code and subtype == spacing_code then
1185                if id == rule_code and subtype == spacing_code then
1186                    found = true
1187                    break
1188                end
1189                current = getnext(current)
1190            end
1191        end
1192        return boolean_value, found
1193    end,
1194}
1195
1196do
1197
1198    local takebox = tex.takebox
1199
1200    implement {
1201        name      = "thebox",
1202        public    = true,
1203        arguments = "integer",
1204        actions   = function(n)
1205            context(takebox(n))
1206        end
1207    }
1208
1209end
1210
1211-- only in lmtx
1212
1213implement {
1214    name      = "reversevboxcontent",
1215    protected = true,
1216    public    = true,
1217    arguments = "integer",
1218    actions   = function(n)
1219        local b = getbox(n)
1220        local l = b and getid(b) == vlist_code and getlist(b)
1221        if l and getnext(l) then
1222            setlist(b,reverse(l)) -- no re-vpack here!
1223        end
1224    end
1225}
1226
1227-- only in lmtx
1228
1229do
1230
1231    local scaninteger        = tokens.scanners.integer
1232    local scanbox            = tokens.scanners.box
1233    local scandimension      = tokens.scanners.dimension
1234
1235    ----- setsubtype         = nuts.setsubtype
1236    local removenode         = nuts.remove
1237    local getnormalizedline  = nuts.getnormalizedline -- we can optimize this
1238    local getdimensions      = nuts.dimensions
1239    local getrangedimensions = nuts.rangedimensions
1240    local getwidth           = nuts.getwidth
1241    local getheight          = nuts.getheight
1242    local getdepth           = nuts.getdepth
1243
1244    local setprop            = nuts.setprop
1245    local getprop            = nuts.getprop
1246
1247    local line_code          <const> = nodes.listcodes.line
1248    local alignment_code     <const> = nodes.listcodes.alignment
1249    local unknown_code       <const> = nodes.listcodes.unknown
1250
1251    -- todo: make helper that formats
1252
1253    local reporterror = logs.texerrormessage
1254
1255    -- The first variant did a linear lookup but for large boxes and lots of
1256    -- analysis that is not nice. Okay, in practice performance is quite ok
1257    -- (milliseconds for thousands of access) but still ... the next is nicer
1258    -- and it's part of the experimental fun stuff anyway.
1259
1260    local function countlines(box)
1261        local prop = getprop(box,"boxlines")
1262        if not prop then
1263            local line = 0
1264            local list = getlist(box)
1265-- node.show(tonode(box),100000,100000)
1266            prop = { }
1267            if list then
1268                for n, subtype in nexthlist, list do
1269                    if subtype == line_code or subtype == alignment_code then
1270                        line = line + 1
1271                        prop[line] = n
1272-- local h, f, l = nuts.migrate(n,true)
1273-- if f then
1274--     nuts.setpost(n,f)
1275-- end
1276                    end
1277                end
1278            end
1279            setprop(box,"boxlines",prop)
1280        end
1281        return prop
1282    end
1283
1284    local function boxlinecount(what)
1285        local n   = scaninteger()
1286        local box = getbox(n)
1287        if what == "value" then
1288            return cardinal_value, box and #countlines(box) or 0
1289        end
1290    end
1291
1292    local function boxlinemaxwhd(what,get)
1293        local n   = scaninteger()
1294        local box = getbox(n)
1295        if box and what == "value" then
1296            local prop = getprop(box,"boxlines")
1297            if not prop then
1298                prop = countlines(box)
1299            end
1300            local v = 0
1301            if prop then
1302                for i=1,#prop do
1303                    local w = get(prop[i])
1304                    if w > v then
1305                        v = w
1306                    end
1307                end
1308            end
1309            return dimension_value, v
1310        end
1311    end
1312
1313    local function boxlinemaxwd(what) return boxlinemaxwhd(what,getwidth)  end
1314    local function boxlinemaxht(what) return boxlinemaxwhd(what,getheight) end
1315    local function boxlinemaxdp(what) return boxlinemaxwhd(what,getdepth)  end
1316
1317    local function findline()
1318        local n    = scaninteger()
1319        local line = scaninteger()
1320        local box  = getbox(n)
1321        if box then
1322            local prop = getprop(box,"boxlines")
1323            if not prop then
1324                prop = countlines(box)
1325            end
1326            local found = prop[line]
1327            if found then
1328                local props = getprop(found,"lineproperties") or { } -- alignment has no properties (yet)
1329                if not props then
1330                    props = getnormalizedline(found)
1331                    props.width, props.height, props.depth = getwhd(found)
1332                    setprop(found,"lineproperties",props)
1333                end
1334                return props, line, found, box
1335            end
1336        end
1337        reporterror("no line %i in box %i",line,n)
1338    end
1339
1340    local function findrange()
1341        local n     = scaninteger()
1342        local first = scaninteger()
1343        local last  = scaninteger()
1344        local box   = getbox(n)
1345        if box then
1346            local prop = getprop(box,"boxlines")
1347            if not prop then
1348                prop = countlines(box)
1349            end
1350            if first > 0 and last <= #prop then
1351                for i = first, last do
1352                    local found = prop[i]
1353                    local props = getprop(found,"lineproperties")
1354                    if not props then
1355                        props = getnormalizedline(found) or { } -- alignment has no properties (yet)
1356                        props.width, props.height, props.depth = getwhd(found)
1357                        setprop(found,"lineproperties",props)
1358                    end
1359                end
1360                return prop, first, last, box
1361            end
1362        end
1363        reporterror("no lines %i - %i in box %i",first,last,n)
1364    end
1365
1366    local function getline(props,line,found,box,value)
1367        local p, n = getboth(found)
1368        local temp = new_hlist()
1369        setsubtype(temp,getsubtype(found))
1370        setwhd(temp,getwhd(found))
1371        if found == getlist(box) then
1372            setlink(temp,n)
1373            setlist(box,temp)
1374        else
1375            setlink(p,temp,n)
1376        end
1377        getprop(box,"boxlines")[line] = temp
1378        setboth(found)
1379        setsubtype(found, unknown_code)
1380        if value then
1381            return direct_value, found
1382        else
1383            context(tonode(found))
1384        end
1385    end
1386
1387    local function copyline(props,line,found,box,value)
1388        found = copynode(found)
1389        setsubtype(found, unknown_code)
1390        if value then
1391            return direct_value, found
1392        else
1393            context(tonode(found))
1394        end
1395    end
1396
1397    local function setline(props,line,found,box)
1398        local p, n = getboth(found)
1399        local temp = scanbox()
1400        if temp then
1401            temp = tonut(temp)
1402            if found == getlist(box) then
1403                setlink(temp,n)
1404                setlist(box,temp)
1405            else
1406                setlink(p,temp,n)
1407            end
1408            flushnode(found)
1409            getprop(box,"boxlines")[line] = temp
1410       end
1411    end
1412
1413    local function naturaldimensions(p,l,found)
1414        if not p.naturalwidth then
1415            p.naturalwidth, p.naturalheight, p.naturaldepth = getdimensions(getlist(found))
1416        end
1417        return p
1418    end
1419
1420    local function rangedimensions(p,f,l,box)
1421        local w, h, d = getrangedimensions(box,p[f],getnext(p[l]),true)
1422        return { width = w, height = h, depth = d }
1423    end
1424
1425    local getters_one = {
1426
1427        ["wd"]  = function(p,l,found) return dimension_value, p.width end,
1428        ["ht"]  = function(p,l,found) return dimension_value, p.height end,
1429        ["dp"]  = function(p,l,found) return dimension_value, p.depth end,
1430        ["ls"]  = function(p,l,found) return dimension_value, p.leftskip end,
1431        ["rs"]  = function(p,l,found) return dimension_value, p.rightskip end,
1432        ["lh"]  = function(p,l,found) return dimension_value, p.lefthangskip end,
1433        ["rh"]  = function(p,l,found) return dimension_value, p.righthangskip end,
1434        ["lp"]  = function(p,l,found) return dimension_value, p.parfillleftskip end,
1435        ["rp"]  = function(p,l,found) return dimension_value, p.parfillrightskip end,
1436        ["il"]  = function(p,l,found) return dimension_value, p.parinitleftskip end,
1437        ["ir"]  = function(p,l,found) return dimension_value, p.parinitrightskip end,
1438        ["in"]  = function(p,l,found) return dimension_value, p.indent end,
1439
1440        ["nw"]  = function(p,l,found) return dimension_value, naturaldimensions(p,l,found).naturalwidth end,
1441        ["nh"]  = function(p,l,found) return dimension_value, naturaldimensions(p,l,found).naturalheigth end,
1442        ["nd"]  = function(p,l,found) return dimension_value, naturaldimensions(p,l,found).naturaldepth end,
1443
1444        ["get"] = function(p,l,found,box) return getline(p,l,found,box,true) end,
1445    }
1446
1447    local getters_two = {
1448        ["wd"] = function(p,f,l,box) return dimension_value, rangedimensions(p,f,l,box).width  end,
1449        ["ht"] = function(p,f,l,box) return dimension_value, rangedimensions(p,f,l,box).height end,
1450        ["dp"] = function(p,f,l,box) return dimension_value, rangedimensions(p,f,l,box).depth  end,
1451    }
1452
1453    local setters_one = {
1454        ["wd"]   = function(p,l,found) return setwidth (found,scandimension(false,false,true)) end,
1455        ["ht"]   = function(p,l,found) return setheight(found,scandimension(false,false,true)) end,
1456        ["dp"]   = function(p,l,found) return setdepth (found,scandimension(false,false,true)) end,
1457        ["set"]  = setline,
1458        ["get"]  = getline,
1459        ["copy"] = copyline,
1460    }
1461
1462    local function boxline(name,what)
1463        local props, line, found, box = findline()
1464        if not found then
1465            --
1466        elseif what == "value" then
1467            local getter = getters_one[name]
1468            if getter then
1469                return getter(props,line,found,box)
1470            end
1471        else
1472            local setter = setters_one[name]
1473            if setter then
1474                return setter(props,line,found,box)
1475            end
1476        end
1477    end
1478
1479    --
1480    local function boxrange(name,what)
1481        local prop, first, last, box = findrange()
1482        if not prop then
1483            --
1484        elseif what == "value" then
1485            local getter = getters_two[name]
1486            if getter then
1487                return getter(prop,first,last,box)
1488            end
1489        else
1490            local setter = setters_two[name]
1491            if setter then
1492                return setter(prop,first,last,box)
1493            end
1494        end
1495    end
1496
1497    local function define_one(name,action)
1498        implement {
1499            name    = name,
1500            public  = true,
1501            usage   = "value",
1502            actions = function(what) return boxline(action,what) end,
1503        }
1504    end
1505
1506    local function define_two(name,action)
1507        implement {
1508            name    = name,
1509            public  = true,
1510            usage   = "value",
1511            actions = function(what) return boxrange(action,what) end,
1512        }
1513    end
1514
1515    implement {
1516        name    = "boxlines",
1517        public  = true,
1518        usage   = "value",
1519        actions = boxlinecount,
1520    }
1521
1522    implement { name = "boxlinemaxwd", public = true, usage = "value", actions = boxlinemaxwd }
1523    implement { name = "boxlinemaxht", public = true, usage = "value", actions = boxlinemaxht }
1524    implement { name = "boxlinemaxdp", public = true, usage = "value", actions = boxlinemaxdp }
1525
1526    -- todoL: just inline so that s-system-macros can find them
1527
1528    define_one("boxline",     "get")
1529    define_one("setboxline",  "set")
1530    define_one("copyboxline", "copy")
1531    define_one("boxlineht",   "ht")
1532    define_one("boxlinedp",   "dp")
1533    define_one("boxlinewd",   "wd")
1534    define_one("boxlinels",   "ls")
1535    define_one("boxliners",   "rs")
1536    define_one("boxlinelh",   "lh")
1537    define_one("boxlinerh",   "rh")
1538    define_one("boxlinelp",   "lp")
1539    define_one("boxlinerp",   "rp")
1540    define_one("boxlineil",   "il")
1541    define_one("boxlineir",   "ir")
1542    define_one("boxlinein",   "in")
1543    define_one("boxlinenw",   "nw")
1544    define_one("boxlinenh",   "nh")
1545    define_one("boxlinend",   "nd")
1546
1547    define_two("boxrangewd",  "wd")
1548    define_two("boxrangeht",  "ht")
1549    define_two("boxrangedp",  "dp")
1550
1551end
1552
1553do
1554
1555    local getbox   = tex.getbox
1556    local setfield = nodes.setfield
1557    local getfield = nodes.getfield
1558    local flush    = nodes.flushnode
1559    local copynode = nodes.copy
1560
1561    local function get(n,field,copy)
1562        local b = getbox(n)
1563        if b then
1564            local p = getfield(b,field)
1565            if copy then
1566                p = copynode(p)
1567            else
1568                setfield(b,field)
1569            end
1570            context(p) -- todo: proper return
1571        end
1572    end
1573
1574    local function set(n,l,field)
1575        local b = getbox(n)
1576        if b then
1577            setfield(b,field,l)
1578        else
1579            flush(l)
1580        end
1581    end
1582
1583    implement {
1584        name      = "prelistbox",
1585        public    = true,
1586        usage     = "value",
1587        arguments = { "integer", '"pre"' },
1588        actions   = get,
1589    }
1590
1591    implement {
1592        name      = "postlistbox",
1593        public    = true,
1594        usage     = "value",
1595        arguments = { "integer", '"post"' },
1596        actions   = get,
1597    }
1598
1599    implement {
1600        name      = "prelistcopy",
1601        public    = true,
1602        usage     = "value",
1603        arguments = { "integer", '"pre"', true },
1604        actions   = get,
1605    }
1606
1607    implement {
1608        name      = "postlistcopy",
1609        public    = true,
1610        usage     = "value",
1611        arguments = { "integer", '"post"', true },
1612        actions   = get,
1613    }
1614
1615    implement {
1616        name      = "setprelistbox",
1617        public    = true,
1618        usage     = "value",
1619        arguments = { "integer", "box", '"pre"' },
1620        actions   = set,
1621    }
1622
1623    implement {
1624        name      = "setpostlistbox",
1625        public    = true,
1626        usage     = "value",
1627        arguments = { "integer", "box", '"post"' },
1628        actions   = set,
1629    }
1630
1631end
1632
1633do
1634
1635    local function setsplitlisthtdp(n,ht,dp)
1636        local box = getbox(n)
1637        if box then
1638            local head = getlist(box)
1639            if head then
1640                local tail = findtail(head)
1641                local id = getid(head)
1642                if id == glue_code and getsubtype(head) == topskip_code then
1643                    head = getnext(head)
1644                    id   = head and getid(head)
1645                    ht   = ht - getwidth(head)
1646                end
1647                if id == hlist_code and getsubtype(head) == line_code and getheight(head) < ht then
1648-- print("set height",ht)
1649                    setheight(head,ht)
1650                end
1651                local id = getid(tail)
1652                if id == glue_code then
1653                    tail = getprev(tail)
1654                    id   = tail and getid(tail)
1655                    dp   = dp - getwidth(tail)
1656                end
1657                if id == hlist_code and getsubtype(tail) == line_code and getdepth(tail) < dp then
1658                    setdepth(dept,dp)
1659-- print("set depth",dp)
1660                end
1661            end
1662        end
1663    end
1664
1665    implement {
1666        name      = "setsplitlisthtdp",
1667        public    = true,
1668        protected = true,
1669        arguments = { "integer", "dimension", "dimension" },
1670        actions   = setsplitlisthtdp,
1671    }
1672end
1673
1674do
1675
1676    local ctx_validtext = context.core.validtext
1677
1678    implement {
1679        name    = "iftext",
1680        public  = true,
1681        usage   = "condition",
1682        actions = function()
1683            -- this is kind of weird, the valid_text does the iftrue / iffalse
1684            ctx_validtext()
1685            return conditional_value
1686        end
1687    }
1688
1689    implement {
1690        name      = "ifboxhaslist",
1691        public    = true,
1692        usage     = "condition",
1693        arguments = "integer",
1694        actions   = function(n)
1695            local b = texgetbox(n)
1696            return boolean_value, b and b.list and true or false
1697        end
1698    }
1699
1700end
1701