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