typo-rub.lua /size: 11 Kb    last modification: 2021-10-28 13:50
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 nexthlist       = nuts.traversers.hlist
55local nextvlist       = nuts.traversers.vlist
56
57local nodecodes       = nodes.nodecodes
58local glyph_code      = nodecodes.glyph
59local disc_code       = nodecodes.disc
60local kern_code       = nodecodes.kern
61local glue_code       = nodecodes.glue
62local penalty_code    = nodecodes.penalty
63local hlist_code      = nodecodes.hlist
64local vlist_code      = nodecodes.vlist
65local par_code        = nodecodes.par
66local dir_code        = nodecodes.dir
67
68local kerncodes       = nodes.kerncodes
69local fontkern_code   = kerncodes.font
70
71local nodepool        = nuts.pool
72local new_kern        = nodepool.kern
73
74local setprop         = nuts.setprop
75local getprop         = nuts.getprop
76
77local enableaction    = nodes.tasks.enableaction
78
79local nofrubies       = 0
80local rubylist        = { }
81
82local a_ruby          = attributes.private("ruby")
83
84local rubies          = { }
85typesetters.rubies    = rubies
86
87local trace_rubies    = false  trackers.register("typesetters.rubies",function(v) trace_rubies = v end)
88local report_rubies   = logs.reporter("rubies")
89
90do
91
92    local shared   = nil
93    local splitter = lpeg.tsplitat("|")
94
95    local function enable()
96        enableaction("processors","typesetters.rubies.check")
97        enableaction("shipouts",  "typesetters.rubies.attach")
98        enable = false
99    end
100
101    local ctx_setruby = context.core.setruby
102
103    local function ruby(settings)
104        local base    = settings.base
105        local comment = settings.comment
106        shared = settings
107        local c = lpegmatch(splitter,comment)
108        if #c == 1 then
109            ctx_setruby(base,comment)
110            if trace_rubies then
111                report_rubies("- %s -> %s",base,comment)
112            end
113        else
114            local i = 0
115            for b in utfcharacters(base) do
116                i = i + 1
117                local r = c[i]
118                if r then
119                    ctx_setruby(b,r)
120                    if trace_rubies then
121                        report_rubies("%i: %s -> %s",i,b,r)
122                    end
123                else
124                    ctx_setruby(b,"")
125                    if trace_rubies then
126                        report_rubies("%i: %s",i,b)
127                    end
128                end
129            end
130        end
131        if enable then
132            enable()
133        end
134    end
135
136    local function startruby(settings)
137        shared = settings
138        if enable then
139            enable()
140        end
141    end
142
143    implement {
144        name      = "ruby",
145        actions   = ruby,
146        arguments = {
147            {
148                { "align" },
149                { "stretch" },
150                { "hoffset", "dimension" },
151                { "voffset", "dimension" },
152                { "comment" },
153                { "base" },
154            }
155        },
156    }
157
158    implement {
159        name      = "startruby",
160        actions   = startruby,
161        arguments = {
162            {
163                { "align" },
164                { "stretch" },
165                { "hoffset", "dimension" },
166                { "voffset", "dimension" },
167            }
168        },
169    }
170
171    local function setruby(n,m)
172        nofrubies = nofrubies + 1
173        local r = takebox(n)
174        rubylist[nofrubies] = setmetatableindex({
175            text      = r,
176            width     = getwidth(r),
177            basewidth = 0,
178            start     = false,
179            stop      = false,
180        }, shared)
181        texsetattribute(a_ruby,nofrubies)
182    end
183
184    implement {
185        name      = "setruby",
186        actions   = setruby,
187        arguments = "integer",
188    }
189
190end
191
192function rubies.check(head)
193    local current = head
194    local start   = nil
195    local stop    = nil
196    local found   = nil
197
198    local function flush(where)
199        local r = rubylist[found]
200        if r then
201            local prev = getprev(start)
202            local next = getnext(stop)
203            setprev(start)
204            setnext(stop)
205            local h = hpack(start)
206            if start == head then
207                head = h
208            else
209                setlink(prev,h)
210            end
211            setlink(h,next)
212            local bwidth = getwidth(h)
213            local rwidth = r.width
214            r.basewidth  = bwidth
215            r.start      = start
216            r.stop       = stop
217            setprop(h,"ruby",found)
218            if rwidth > bwidth then
219                -- ruby is wider
220                setwidth(h,rwidth)
221            end
222        end
223    end
224
225    while current do
226        local nx = getnext(current)
227        local id = getid(current)
228        if id == glyph_code then
229            local a  = getattr(current,a_ruby)
230            if not a then
231                if found then
232                    flush("flush 1")
233                    found = nil
234                end
235            elseif a == found then
236                stop = current
237            else
238                if found then
239                    flush("flush 2")
240                end
241                found = a
242                start = current
243                stop  = current
244            end
245            -- go on
246        elseif id == kern_code and getsubtype(current,fontkern_code) then
247            -- go on
248        elseif found and id == disc_code then
249            -- go on (todo: look into disc)
250        elseif found then
251            flush("flush 3")
252            found = nil
253        end
254        current = nx
255    end
256    if found then
257        flush("flush 4")
258    end
259    return head, true -- no need for true
260end
261
262local attach
263
264local function whatever(current)
265    local a = getprop(current,"ruby")
266    if a then
267        local ruby    = rubylist[a]
268        local align   = ruby.align   or v_middle
269        local stretch = ruby.stretch or v_no
270        local hoffset = ruby.hoffset or 0
271        local voffset = ruby.voffset or 0
272        local start   = ruby.start
273        local stop    = ruby.stop
274        local text    = ruby.text
275        local rwidth  = ruby.width
276        local bwidth  = ruby.basewidth
277        local delta   = rwidth - bwidth
278        setwidth(text,0)
279        if voffset ~= 0 then
280            setshift(text,voffset)
281        end
282        -- center them
283        if delta > 0 then
284            -- ruby is wider
285            if stretch == v_yes then
286                setlink(text,start)
287                while start and start ~= stop do
288                    local s = nodepool.stretch()
289                    local n = getnext(start)
290                    setlink(start,s,n)
291                    start = n
292                end
293                text = hpack(text,rwidth,"exactly")
294            else
295                local left  = new_kern(delta/2)
296                local right = new_kern(delta/2)
297                setlink(text,left,start)
298                setlink(stop,right)
299            end
300            setlist(current,text)
301        elseif delta < 0 then
302            -- ruby is narrower
303            if align == v_auto then
304                local l = true
305                local c = getprev(current)
306                while c do
307                    local id = getid(c)
308                    if id == glue_code or id == penalty_code or id == kern_code then
309                        -- go on
310                    elseif id == hlist_code and getwidth(c) == 0 then
311                        -- go on
312                    elseif id == whatsit_code or id == par_code or id == dir_code then
313                        -- go on
314                    else
315                        l = false
316                        break
317                    end
318                    c = getprev(c)
319                end
320                local r = true
321                local c = getnext(current)
322                while c do
323                    local id = getid(c)
324                    if id == glue_code or id == penalty_code or id == kern_code then
325                        -- go on
326                    elseif id == hlist_code and getwidth(c) == 0 then
327                        -- go on
328                    else
329                        r = false
330                        break
331                    end
332                    c = getnext(c)
333                end
334                if l and not r then
335                    align = v_flushleft
336                elseif r and not l then
337                    align = v_flushright
338                else
339                    align = v_middle
340                end
341            end
342            if align == v_flushleft then
343                setlink(text,start)
344                setlist(current,text)
345            elseif align == v_flushright then
346                local left  = new_kern(-delta)
347                local right = new_kern(delta)
348                setlink(left,text,right,start)
349                setlist(current,left)
350            else
351                local left  = new_kern(-delta/2)
352                local right = new_kern(delta/2)
353                setlink(left,text,right,start)
354                setlist(current,left)
355            end
356        else
357            setlink(text,start)
358            setlist(current,text)
359        end
360        setprop(current,"ruby",false)
361        rubylist[a] = nil
362    else
363        local list = getlist(current)
364        if list then
365            attach(list)
366        end
367    end
368end
369
370attach = function(head)
371    for current in nexthlist, head do
372        whatever(current)
373    end
374    for current in nextvlist, head do
375        whatever(current)
376    end
377    return head
378end
379
380rubies.attach = attach
381
382-- for now there is no need to be compact
383
384-- local data          = { }
385-- rubies.data         = data
386--
387-- function rubies.define(settings)
388--     data[#data+1] = settings
389--     return #data
390-- end
391--
392-- implement {
393--     name      = "defineruby",
394--     actions   = { rubies.define, context },
395--     arguments = {
396--         {
397--             { "align" },
398--             { "stretch" },
399--         }
400--     }
401-- }
402