typo-rub.lmt /size: 14 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['typo-rub'] = {
2    version   = 1.001,
3    comment   = "companion to typo-rub.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- todo: recycle slots better
10-- todo: hoffset
11-- todo: auto-increase line height
12-- todo: only hpack when start <> stop
13
14-- A typical bit of afternoon hackery ... with some breaks for watching
15-- Ghost-Note on youtube (Robert Searight and Nate Werth) ... which expands
16-- my to-be-had cd/dvd list again.
17
18local lpegmatch         = lpeg.match
19local utfcharacters     = utf.characters
20local setmetatableindex = table.setmetatableindex
21
22local variables       = interfaces.variables
23local implement       = interfaces.implement
24
25local texsetattribute = tex.setattribute
26
27local v_flushleft     = variables.flushleft
28local v_middle        = variables.middle
29local v_flushright    = variables.flushright
30local v_yes           = variables.yes
31local v_no            = variables.no
32local v_auto          = variables.auto
33
34local nuts            = nodes.nuts
35
36local getid           = nuts.getid
37local getsubtype      = nuts.getsubtype
38local getattr         = nuts.getattr
39local setattr         = nuts.setattr
40local getnext         = nuts.getnext
41local setnext         = nuts.setnext
42local getprev         = nuts.getprev
43local setprev         = nuts.setprev
44local setlink         = nuts.setlink
45local getlist         = nuts.getlist
46local setlist         = nuts.setlist
47local setshift        = nuts.setshift
48local getwidth        = nuts.getwidth
49local setwidth        = nuts.setwidth
50
51local hpack           = nuts.hpack
52local takebox         = nuts.takebox
53
54local nextlist        = nuts.traversers.list
55
56local nodecodes       = nodes.nodecodes
57local glyph_code      = nodecodes.glyph
58local disc_code       = nodecodes.disc
59local kern_code       = nodecodes.kern
60local glue_code       = nodecodes.glue
61local penalty_code    = nodecodes.penalty
62local hlist_code      = nodecodes.hlist
63local vlist_code      = nodecodes.vlist
64local par_code        = nodecodes.par
65local dir_code        = nodecodes.dir
66
67local kerncodes       = nodes.kerncodes
68local fontkern_code   = kerncodes.font
69
70local nodepool        = nuts.pool
71local new_kern        = nodepool.kern
72
73local setprop         = nuts.setprop
74local getprop         = nuts.getprop
75
76local findattribute   = nuts.findattribute
77
78local enableaction    = nodes.tasks.enableaction
79
80local nofrubies       = 0
81local rubylist        = { }
82
83local a_ruby          = attributes.private("ruby")
84
85local rubies          = { }
86typesetters.rubies    = rubies
87
88local trace_rubies    = false  trackers.register("typesetters.rubies",function(v) trace_rubies = v end)
89local report_rubies   = logs.reporter("rubies")
90
91-- todo: use the more modern lmtx storage model
92
93local registervalue   = attributes.registervalue
94local getvalue        = attributes.getvalue
95local texsetattribute = tex.setattribute
96
97do
98
99    local shared   = nil
100    local splitter = lpeg.tsplitat("|")
101
102    local function enable()
103        enableaction("processors","typesetters.rubies.check")
104        enableaction("shipouts",  "typesetters.rubies.attach")
105        enable = false
106    end
107
108    local ctx_setruby = context.core.setruby
109
110    local function ruby(settings)
111        local base    = settings.base
112        local comment = settings.comment
113        shared = settings
114        local c = lpegmatch(splitter,comment)
115        if #c == 1 then
116            ctx_setruby(base,comment)
117            if trace_rubies then
118                report_rubies("- %s -> %s",base,comment)
119            end
120        else
121            local i = 0
122            for b in utfcharacters(base) do
123                i = i + 1
124                local r = c[i]
125                if r then
126                    ctx_setruby(b,r)
127                    if trace_rubies then
128                        report_rubies("%i: %s -> %s",i,b,r)
129                    end
130                else
131                    ctx_setruby(b,"")
132                    if trace_rubies then
133                        report_rubies("%i: %s",i,b)
134                    end
135                end
136            end
137        end
138        if enable then
139            enable()
140        end
141    end
142
143    local function startruby(settings)
144        shared = settings
145        if enable then
146            enable()
147        end
148    end
149
150    implement {
151        name      = "ruby",
152        actions   = ruby,
153        arguments = {
154            {
155                { "align" },
156                { "stretch" },
157                { "hoffset", "dimension" },
158                { "voffset", "dimension" },
159                { "comment" },
160                { "base" },
161            }
162        },
163    }
164
165    implement {
166        name      = "startruby",
167        actions   = startruby,
168        arguments = {
169            {
170                { "align" },
171                { "stretch" },
172                { "hoffset", "dimension" },
173                { "voffset", "dimension" },
174            }
175        },
176    }
177
178    local function setruby(n,m)
179        nofrubies = nofrubies + 1
180        local r = takebox(n)
181        local t = {
182            text      = r,
183            width     = getwidth(r),
184            basewidth = 0,
185            start     = false,
186            stop      = false,
187        }
188     -- rubylist[nofrubies] = setmetatableindex(t,shared)
189     -- texsetattribute(a_ruby,nofrubies)
190        texsetattribute(a_ruby,registervalue(a_ruby,setmetatableindex(t,shared)))
191    end
192
193    implement {
194        name      = "setruby",
195        actions   = setruby,
196        arguments = "integer",
197    }
198
199end
200
201-- function rubies.check(head)
202--     local current = head
203--     local start   = nil
204--     local stop    = nil
205--     local found   = nil
206--
207--     local function flush(where)
208--         local r = rubylist[found]
209--         if r then
210--             local prev = getprev(start)
211--             local next = getnext(stop)
212--             setprev(start)
213--             setnext(stop)
214--             local h = hpack(start)
215--             if start == head then
216--                 head = h
217--             else
218--                 setlink(prev,h)
219--             end
220--             setlink(h,next)
221--             local bwidth = getwidth(h)
222--             local rwidth = r.width
223--             r.basewidth  = bwidth
224--             r.start      = start
225--             r.stop       = stop
226--             setprop(h,"ruby",found)
227--             if rwidth > bwidth then
228--                 -- ruby is wider
229--                 setwidth(h,rwidth)
230--             end
231--         end
232--     end
233--
234--     while current do
235--         local nx = getnext(current)
236--         local id = getid(current)
237--         if id == glyph_code then
238--             local a  = getattr(current,a_ruby)
239--             if not a then
240--                 if found then
241--                     flush("flush 1")
242--                     found = nil
243--                 end
244--             elseif a == found then
245--                 stop = current
246--             else
247--                 if found then
248--                     flush("flush 2")
249--                 end
250--                 found = a
251--                 start = current
252--                 stop  = current
253--             end
254--             -- go on
255--         elseif id == kern_code and getsubtype(current,fontkern_code) then
256--             -- go on
257--         elseif found and id == disc_code then
258--             -- go on (todo: look into disc)
259--         elseif found then
260--             flush("flush 3")
261--             found = nil
262--         end
263--         current = nx
264--     end
265--
266--     if found then
267--         flush("flush 4")
268--     end
269--     return head, true -- no need for true
270-- end
271
272function rubies.check(head)
273    local _, current = findattribute(head,a_ruby)
274    if current then
275
276        local start   = nil
277        local stop    = nil
278        local found   = nil
279
280        local function flush(where)
281--            local r = rubylist[found]
282local r = getvalue(a_ruby,found)
283            if r then
284                -- can be an option
285                while start ~= stop and getid(start) == glue_code do
286                    start = getnext(start)
287                end
288                while stop ~= start and getid(stop) == glue_code do
289                    stop = getprev(stop)
290                end
291                --
292                local prev = getprev(start)
293                local next = getnext(stop)
294                setprev(start)
295                setnext(stop)
296                local h = hpack(start)
297                if start == head then
298                    head = h
299                else
300                    setlink(prev,h)
301                end
302                setlink(h,next)
303                local bwidth = getwidth(h)
304                local rwidth = r.width
305                r.basewidth  = bwidth
306                r.start      = start
307                r.stop       = stop
308                setprop(h,"ruby",found)
309                if rwidth > bwidth then
310                    -- ruby is wider
311                    setwidth(h,rwidth)
312                end
313            end
314        end
315
316--         while current do
317--             local nx = getnext(current)
318--             local a  = getattr(current,a_ruby)
319--             if not a then
320--                 if found then
321--                     flush("flush 1")
322--                     found = nil
323--                 end
324--             elseif a == found then
325--                 stop = current
326--             else
327--                 if found then
328--                     flush("flush 2")
329--                 end
330--                 found = a
331--                 start = current
332--                 stop  = current
333--             end
334--             current = nx
335--         end
336
337        -- todo: we can avoid a lookup
338
339        while current do
340            local nx = getnext(current)
341            local a  = getattr(current,a_ruby)
342            if not a then
343                if found then
344                    flush("flush 1")
345                    found = nil
346                end
347_, current = findattribute(nx,a_ruby)
348            elseif a == found then
349                stop = current
350current = nx
351            else
352                if found then
353                    flush("flush 2")
354                end
355                found = a
356                start = current
357                stop  = current
358current = nx
359            end
360        end
361
362    end
363
364    if found then
365        flush("flush 4")
366    end
367    return head, true -- no need for true
368end
369
370
371local attach
372
373local function whatever(current,list)
374    local a = getprop(current,"ruby")
375    if a then
376--         local ruby    = rubylist[a]
377local ruby = getvalue(a_ruby,a)
378        local align   = ruby.align   or v_middle
379        local stretch = ruby.stretch or v_no
380        local hoffset = ruby.hoffset or 0
381        local voffset = ruby.voffset or 0
382        local start   = ruby.start
383        local stop    = ruby.stop
384        local text    = ruby.text
385        local rwidth  = ruby.width
386        local bwidth  = ruby.basewidth
387        local delta   = rwidth - bwidth
388        setwidth(text,0)
389        if voffset ~= 0 then
390            setshift(text,voffset)
391        end
392        -- center them
393        if delta > 0 then
394            -- ruby is wider
395            if stretch == v_yes then
396                setlink(text,start)
397                while start and start ~= stop do
398                    local s = nodepool.stretch()
399                    local n = getnext(start)
400                    setlink(start,s,n)
401                    start = n
402                end
403                text = hpack(text,rwidth,"exactly")
404            else
405                local left  = new_kern(delta/2)
406                local right = new_kern(delta/2)
407                setlink(text,left,start)
408                setlink(stop,right)
409            end
410            setlist(current,text)
411        elseif delta < 0 then
412            -- ruby is narrower
413            if align == v_auto then
414                local l = true
415                local c = getprev(current)
416                while c do
417                    local id = getid(c)
418                    if id == glue_code or id == penalty_code or id == kern_code then
419                        -- go on
420                    elseif id == hlist_code and getwidth(c) == 0 then
421                        -- go on
422                    elseif id == whatsit_code or id == par_code or id == dir_code then
423                        -- go on
424                    else
425                        l = false
426                        break
427                    end
428                    c = getprev(c)
429                end
430                local r = true
431                local c = getnext(current)
432                while c do
433                    local id = getid(c)
434                    if id == glue_code or id == penalty_code or id == kern_code then
435                        -- go on
436                    elseif id == hlist_code and getwidth(c) == 0 then
437                        -- go on
438                    else
439                        r = false
440                        break
441                    end
442                    c = getnext(c)
443                end
444                if l and not r then
445                    align = v_flushleft
446                elseif r and not l then
447                    align = v_flushright
448                else
449                    align = v_middle
450                end
451            end
452            if align == v_flushleft then
453                setlink(text,start)
454                setlist(current,text)
455            elseif align == v_flushright then
456                local left  = new_kern(-delta)
457                local right = new_kern(delta)
458                setlink(left,text,right,start)
459                setlist(current,left)
460            else
461                local left  = new_kern(-delta/2)
462                local right = new_kern(delta/2)
463                setlink(left,text,right,start)
464                setlist(current,left)
465            end
466        else
467            setlink(text,start)
468            setlist(current,text)
469        end
470        setprop(current,"ruby",false)
471--         rubylist[a] = nil
472    elseif list then
473        attach(list)
474    end
475end
476
477attach = function(head)
478    for current, id, subtype, list in nextlist, head do
479        if id == hlist_code or id == vlist_code then
480            whatever(current,list)
481        end
482    end
483    return head
484end
485
486rubies.attach = attach
487
488-- for now there is no need to be compact
489
490-- local data          = { }
491-- rubies.data         = data
492--
493-- function rubies.define(settings)
494--     data[#data+1] = settings
495--     return #data
496-- end
497--
498-- implement {
499--     name      = "defineruby",
500--     actions   = { rubies.define, context },
501--     arguments = {
502--         {
503--             { "align" },
504--             { "stretch" },
505--         }
506--     }
507-- }
508