typo-syn.lmt /size: 20 Kb    last modification: 2024-01-16 10:22
1if not modules then modules = { } end modules ['typo-syn'] = {
2    version   = 1.000,
3    optimize  = true,
4    comment   = "companion to typo-syn.mkxl",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- For the moment we have the splitter here but it actually belongs
11-- in the builders namespace.
12
13local nodes             = nodes
14
15local tasks             = nodes.tasks
16local enableaction      = tasks.enableaction
17----- disableaction     = tasks.disableaction
18
19local nuts              = nodes.nuts
20local tonut             = nodes.tonut
21
22local getattr           = nuts.getattr
23local getattrlist       = nuts.getattrlist
24local getdepth          = nuts.getdepth
25local getdirection      = nuts.getdirection
26local getdisc           = nuts.getdisc
27local getglue           = nuts.getglue
28local getheight         = nuts.getheight
29local getid             = nuts.getid
30local getlist           = nuts.getlist
31local getnext           = nuts.getnext
32local getprev           = nuts.getprev
33local getprop           = nuts.getprop
34local getsubtype        = nuts.getsubtype
35local gettotal          = nuts.gettotal
36local getwhd            = nuts.getwhd
37local getwidth          = nuts.getwidth
38local setattrlist       = nuts.setattrlist
39local setdepth          = nuts.setdepth
40local setdisc           = nuts.setdisc
41local setheight         = nuts.setheight
42local setlink           = nuts.setlink
43local setlist           = nuts.setlist
44local setnext           = nuts.setnext
45local setoffsets        = nuts.setoffsets
46local setprev           = nuts.setprev
47local setprop           = nuts.setprop
48local setwidth          = nuts.setwidth
49
50local hpack             = nuts.hpack
51local rangedimensions   = nuts.rangedimensions
52local insertbefore      = nuts.insertbefore
53local insertafter       = nuts.insertafter
54local removenode        = nuts.remove
55local flushnode         = nuts.flush
56
57local traverselist      = nuts.traverselist
58
59local nodecodes         = nodes.nodecodes
60local glyph_code        = nodecodes.glyph
61local rule_code         = nodecodes.rule
62local dir_code          = nodecodes.dir
63local disc_code         = nodecodes.disc
64local hlist_code        = nodecodes.hlist
65local vlist_code        = nodecodes.vlist
66local math_code         = nodecodes.math
67local glue_code         = nodecodes.glue
68local kern_code         = nodecodes.kern
69local penalty_code      = nodecodes.penalty
70
71local line_code         = nodes.listcodes.line
72local fontkern_code     = nodes.kerncodes.fontkern
73local parfillskip_code  = nodes.gluecodes.parfillrightskip
74local baselineskip_code = nodes.gluecodes.baselineskip
75
76local new_direction     = nuts.pool.direction
77
78local runningrule       = tex.magicconstants.runningrule
79
80-----------------
81
82local synchronize       = typesetters.synchronize or { }
83typesetters.synchronize = synchronize or { }
84
85local a_synchronize     = attributes.private("synchronize")
86local registervalue     = attributes.registervalue
87local getvalue          = attributes.getvalue
88local hasvalues         = attributes.hasvalues
89
90local trace  = false
91-- local trace  = true
92local report = logs.reporter("parallel")
93
94local pushsavelevel = tex.pushsavelevel -- token.expandmacro("bgroup")
95local popsavelevel  = tex.popsavelevel  -- token.expandmacro("egroup")
96local dontcomplain  = tex.dontcomplain
97
98local index    = 0
99local lastattr = nil
100local lastline = nil
101
102interfaces.implement {
103    name      = "registersynchronize",
104    arguments = { "dimen", "dimen", "dimen", "box" },
105    actions   = function(ht,dp,slack,box)
106        index = index + 1
107        box   = tonut(box)
108        local t = {
109            index      = index,
110            lineheight = ht,
111            linedepth  = dp,
112            slack      = slack,
113            height     = getheight(height),
114            depth      = getheight(depth),
115            box        = box,
116        }
117        local v = registervalue(a_synchronize,t)
118        tex.setattribute(a_synchronize,v)
119        if index > 0 then
120            enableaction("vboxbuilders", "typesetters.synchronize.handler")
121            enableaction("mvlbuilders",  "typesetters.synchronize.handler")
122        end
123    end,
124}
125
126-- When this is stable it can become a proper helper and primitive.
127
128local function hsplit(box,targetwidth,targetheight,targetdepth,mcriterium,pcriterium,upto)
129    local first     = getlist(box)
130    local last      = first
131    local current   = first
132    local previous  = current
133    local lastdisc  = nil
134    local lastglyph = nil
135 -- local stretch   = 0
136 -- local shrink    = 0
137    local width     = 0
138    local height    = 0
139    local depth     = 0
140    local dirstack  = { } -- can move to outer
141    local dirtop    = 0
142    local minwidth  = targetwidth
143    local maxwidth  = targetwidth
144    local usedwidth = targetwidth
145    while true do
146        previous = current
147        local id = getid(current)
148        if id == glyph_code then
149            local wd, ht, dp = getwhd(current)
150            -- find next break first
151            local newwidth = width + wd
152            if newwidth >= usedwidth then
153                if not lastdisc and lastglyph then
154                    last = getprev(lastglyph)
155                end
156                break
157            else
158                width = newwidth
159                if ht > height then
160                    height = ht
161                end
162                if dp > depth then
163                    depth = dp
164                end
165            end
166            if not lastglyph then
167                lastglyph = current
168            end
169        elseif id == kern_code then
170            local wd = getwidth(current)
171            local newwidth = width + wd
172            if getsubtype(current) == fontkern_code then
173                -- assume sane kerns
174                width = newwidth
175            else
176                last = previous
177                if newwidth >= usedwidth then
178                    break
179                else
180                    width = newwidth
181                end
182                lastdisc  = nil
183                lastglyph = nil
184            end
185        elseif id == disc_code then
186            local pre, post, replace = getdisc(current)
187            local wd = replace and rangedimensions(box,replace) or 0
188            local newwidth = width + wd
189            if newwidth >= usedwidth then
190                break
191            end
192            local wd = pre and rangedimensions(box,pre) or 0
193            local prewidth = width + wd
194            if prewidth >= usedwidth then
195                break
196            end
197            width    = newwidth
198            lastdisc = current
199        else
200            -- common code at the end
201            if id == glue_code or id == math_code then -- refactor : common code
202                -- leaders
203                last = previous
204                local wd, more, less = getglue(current)
205                local newwidth = width + wd
206                if newwidth >= usedwidth then
207                    break
208                else
209                    width   = newwidth
210                 -- stretch = stretch + more
211                 -- shrink  = shrink  + less
212                    -- also for statistics
213                    maxwidth = maxwidth + more
214                    minwidth = minwidth + less
215                    -- can become an option:
216                    usedwidth = minwidth
217                end
218            elseif id == hlist_code or id == vlist_code then
219                last = previous
220                local wd, ht, dp = getwhd(current)
221                local newwidth = width + wd
222                if newwidth >= usedwidth then
223                    break
224                else
225                    width = newwidth
226                    if ht > height then
227                        height = ht
228                    end
229                    if dp > depth then
230                        depth = dp
231                    end
232                end
233            elseif id == rule_code then
234                last = previous
235                local wd, ht, dp = getwhd(current)
236                local newwidth = width + wd
237                if newwidth >= usedwidth then
238                    break
239                else
240                    width = newwidth
241                    if ht ~= runningrule and ht > height then
242                        height = ht
243                    end
244                    if dp ~= runningrule and dp > depth then
245                        depth = dp
246                    end
247                end
248            elseif id == dir_code then
249                local dir, cancel = getdirection(current)
250                if cancel then
251                    if dirtop > 0 then
252                        dirtop = dirtop - 1
253                    end
254                else
255                    dirtop = dirtop + 1
256                    dirstack[dirtop] = dir
257                end
258            end
259            lastdisc  = nil
260            lastglyph = nil
261        end
262        local next = getnext(current)
263        if next then
264            current = next
265        else
266            last = previous
267            break
268        end
269    end
270    local next
271    if lastdisc then
272        local pre, post, replace, pretail, posttail, replacetail = getdisc(lastdisc,true)
273        last = getprev(lastdisc)
274        --
275        next = getnext(lastdisc)
276        if next then
277            setprev(next)
278        end
279        --
280        setlink(last,pre)
281        last = pretail
282        if post then
283            setlink(posttail,next)
284            next = post
285        end
286        setdisc(lastdisc,nil,nil,replace)
287        flushnode(lastdisc)
288    else
289        next = getnext(last)
290        if next then
291            setprev(next)
292        end
293        setnext(last)
294    end
295    while last do
296        local id = getid(last)
297        if id == glue_code or id == penalty_code then
298         -- if id == glue_code then
299         --     local wd, more, less = getglue(last)
300         --  -- stretch = stretch - more
301         --  -- shrink  = shrink  - less
302         --     width   = width   - wd
303         --     -- also for statistics
304         --     maxwidth = maxwidth - more
305         --     minwidth = minwidth - less
306         --     -- can become an option:
307         --     usedwidth = minwidth
308         -- end
309            first, last = removenode(first,last,true)
310        else
311            break
312        end
313    end
314    if dirtop > 0 then
315        for i=dirtop,1,-1 do
316            local d = new_direction(dirstack[i],true)
317            first, last = insertafter(first,last,d)
318        end
319        for i=1,dirtop do
320            local d = new_direction(dirstack[i],false)
321            next = insertbefore(next,next,d)
322        end
323    end
324    if first then
325     -- pushsavelevel()
326     -- dontcomplain()
327        local result
328        if upto then
329            result = hpack(first)
330        else
331            local badness, overshoot
332            result, badness, overshoot = hpack(first,targetwidth,"exactly")
333            local pdone = pcriterium and badness > pcriterium
334            local mdone = mcriterium and badness > mcriterium
335            if overshoot > 0 then
336                if pdone then
337                    result = hpack(first)
338                end
339            elseif overshoot < 0 then
340                if mdone then
341                    result = hpack(first)
342                end
343            else
344                if pdone or mdone then
345                    result = hpack(first)
346                end
347            end
348        end
349     -- popsavelevel()
350        setattrlist(result,getattrlist(box)) -- useattrlist(result,box)
351        setheight(result,targetheight or height)
352        setdepth(result,targetdepth or depth)
353        setlist(box,next)
354        setwidth(box,rangedimensions(box,next))
355        return result
356    end
357end
358
359do
360
361    local scanners     = tokens.scanners
362    local scanword     = scanners.word
363    local scaninteger  = scanners.integer
364    local scandimen    = scanners.dimen
365
366    local tonode       = nuts.tonode
367    local getbox       = nuts.getbox
368    local setbox       = nuts.setbox
369
370    local direct_value = tokens.values.direct
371    local none_value   = tokens.values.none
372
373    interfaces.implement {
374        name      = "hsplit",
375        protected = true,
376        public    = true,
377        usage     = "value",
378        actions   = function(what)
379            local n = scaninteger()
380            local w = 0
381            local h = false
382            local d = false
383            local pcriterium = false
384            local mcriterium = false
385            local upto = false
386            while true do
387                local key = scanword()
388                if key == "to" then
389                    upto = false
390                    w = scandimen()
391                elseif key == "upto" then
392                    upto = true
393                    w = scandimen()
394                elseif key == "width" then
395                    w = scandimen()
396                elseif key == "height" then
397                    h = scandimen()
398                elseif key == "depth" then
399                    d = scandimen()
400                elseif key == "criterium" then
401                    pcriterium = scaninteger()
402                    mcriterium = scaninteger()
403                elseif key == "shrinkcriterium" then
404                    mcriterium = scaninteger()
405                elseif key == "stretchcriterium" then
406                    pcriterium = scaninteger()
407                else
408                    break
409                end
410            end
411            pushsavelevel()
412            dontcomplain()
413            local r = hsplit(getbox(n),w,h,d,mcriterium,pcriterium,upto)
414            popsavelevel()
415            if r then
416                if what == "value" then
417                    return direct_value, r
418                else
419                    context(tonode(r))
420                end
421            else
422                setbox(n)
423                if what == "value" then
424                    return none_value, nil
425                end
426            end
427        end,
428    }
429
430end
431
432local function getproperties(parent)
433    local props = getprop(parent,"parallel")
434    if not props then
435        local w, h, d = getwhd(parent)
436        props = {
437            width  = w,
438            height = h,
439            depth  = d,
440        }
441        setprop(parent,"parallel",props)
442    end
443    return props
444end
445
446local function setproperties(parent,data,result,level,ctotal)
447    local props  = getproperties(parent)
448    local depth  = props.depth
449    local height = props.height
450    local delta  = data.linedepth - depth
451    if delta > 0 then
452        depth = data.linedepth
453        setdepth(parent,depth)
454        props.depth = depth
455        local n = getnext(parent)
456        if n and getid(n) == glue_code and getsubtype(n) == baselineskip_code then
457            setwidth(n,getwidth(n) - delta)
458        end
459    end
460--     if height < data.lineheight then
461--         height = data.lineheight
462--         setheight(parent,height)
463--         props.height = height
464--     end
465    local offset = level * ctotal
466    if props.depth + offset > depth then
467        setdepth(parent,props.depth+offset)
468    end
469    setoffsets(result,0,-offset)
470    setwidth(result,0)
471end
472
473local function flush(head,first,last,a,parent,nesting)
474    if first and nesting == 0 then
475        local data = getvalue(a_synchronize,a)
476        local upto = getnext(last)
477        if upto and getid(upto) == penalty_code then
478            upto = getnext(upto)
479        end
480        if upto and getid(upto) == glue_code and getsubtype(upto) == parfillskip_code then
481            upto = getnext(upto)
482        end
483        local props = getproperties(parent)
484        local width = rangedimensions(parent,first,upto)
485        if width > props.width then
486            width = props.width
487        end
488        local content = data.box
489        local index   = data.index
490        if not content then
491            if trace then
492                report("index %i, verdict %a",index,"done")
493            end
494        else
495            local result = nil
496            local cwidth = getwidth(content)
497            local ctotal = gettotal(content)
498            if cwidth <= width then
499                if trace then
500                    report("index %i, available %p, content %p, verdict %a",index,width,cwidth,"fit")
501                end
502                result   = content
503                data.box = nil
504            elseif cwidth > width then
505                if trace then
506                    report("index %i, available %p, content %p, verdict %a",index,width,cwidth,"overflow")
507                end
508                result   = hsplit(content,width-(data.slack or 0),nil,nil,200)
509                lastattr = a
510                lastline = parent
511            else
512                report("index %i, verdict %a",index,"weird")
513            end
514            if result then
515                setproperties(parent,data,result,1,ctotal)
516                head = insertbefore(head,first,result)
517            end
518        end
519    end
520    return head
521end
522
523local function lastflush(lastline,lastattr)
524    local data = getvalue(a_synchronize,lastattr)
525    if not data then
526        return
527    end
528    local content = data.box
529    if not content or getwidth(content) == 0 then
530        return
531    end
532    local head = getlist(lastline)
533    if not head then
534        return
535    end
536    local first  = head
537    local last   = nil
538    local props  = getproperties(lastline)
539    local width  = props.width
540    local height = props.height
541    local depth  = props.depth
542    local level  = 1
543    if depth < data.linedepth then
544        depth = data.linedepth
545        setdepth(lastline,depth)
546    end
547    if height < data.lineheight then
548        height = data.lineheight
549        setheight(lastline,height)
550    end
551    while true do
552        local content = data.box
553        local index   = data.index
554        if content then
555            local result = nil
556            local total  = 0
557            local cwidth = getwidth(content)
558            local ctotal = gettotal(content)
559            if cwidth <= width then
560                if trace then
561                    report("index %i, available %p, content %p, verdict %a",index,width,cwidth,"fit")
562                end
563                result = content
564                data.box  = nil
565            elseif cwidth > width then
566                if trace then
567                    report("index %i, available %p, content %p, verdict %a",index,width,cwidth,"overflow")
568                end
569                result = hsplit(content,width-(data.slack or 0),nil,nil,200)
570            else
571                report("index %i, verdict %a",index,"weird")
572            end
573            if result then
574                level = level + 1
575                setproperties(lastline,data,result,level,ctotal)
576                head = insertbefore(head,first,result)
577                setlist(lastline,head)
578            else
579                break
580            end
581        else
582            break
583        end
584    end
585end
586
587local processranges = nuts.processranges
588
589function synchronize.handler(head,where)
590    if where == "hmodepar" and hasvalues(a_synchronize) then
591        lastattr = nil
592        lastline = nil
593        for n, id, subtype in traverselist(head) do
594            if subtype == line_code then
595                lastattr = nil
596                local list = getlist(n)
597                local head = processranges(a_synchronize,flush,list,n)
598                if head ~= list then
599                    setlist(n,head)
600                end
601            end
602        end
603        if lastattr and lastline then
604            lastflush(lastline,lastattr)
605        end
606    end
607    return head
608end
609
610--
611
612local settings_to_array  = utilities.parsers.settings_to_array
613local get_buffer_content = buffers.getcontent
614local splitlines         = string.splitlines
615
616interfaces.implement {
617    name      = "synchronizesteps",
618    arguments = { {
619        { "list" },
620        { "split" },
621        { "buffer" },
622        { "text" },
623    } },
624    actions   = function(t)
625        local split  = t.split -- not used yet
626        local list   = t.list
627        local buffer = t.buffer
628        local text   = t.text
629        local data   = false
630        if buffer and buffer ~= "" then
631            data = settings_to_array(buffer)
632            if #data == 2 then
633                for i=1,#data do
634                    data[i] = splitlines(get_buffer_content(data[i]) or "")
635                end
636            else
637                return
638            end
639        elseif text and text ~= "" then
640            data = settings_to_array(text)
641            if #data == 2 then
642                for i=1,#data do
643                    data[i] = settings_to_array(data[i])
644                end
645            else
646                return
647            end
648        else
649            return
650        end
651        if list and list ~= "" then
652            list = settings_to_array(list)
653        else
654            list = { }
655        end
656        local done = data[1]
657        local dtwo = data[2]
658        if #done == #dtwo then
659            local lone = list[1] or ""
660            local ltwo = list[2] or ""
661            for i=1,#done do
662                context.dosplitsynchronize(lone,ltwo,done[i],dtwo[i])
663            end
664        else
665            context.type("[different sizes in synchronize]")
666        end
667    end,
668}
669