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