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