typo-cap.lmt /size: 14 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['typo-cap'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to typo-cap.mkiv",
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-- see typo-cap.lua for a word traverser variant
11
12local next, type, tonumber = next, type, tonumber
13local format, insert = string.format, table.insert
14local div, getrandom, random = math.div, utilities.randomizer.get, math.random
15
16local trace_casing = false  trackers.register("typesetters.casing", function(v) trace_casing = v end)
17
18local report_casing = logs.reporter("typesetting","casing")
19
20local nodes, node = nodes, node
21
22local nuts            = nodes.nuts
23
24local getnext         = nuts.getnext
25local getid           = nuts.getid
26local getattr         = nuts.getattr
27local getcharspec     = nuts.getcharspec
28local getsubtype      = nuts.getsubtype
29local getchar         = nuts.getchar
30local isglyph         = nuts.isglyph
31local getdisc         = nuts.getdisc
32
33local setchar         = nuts.setchar
34local setfont         = nuts.setfont
35local setscales       = nuts.setscales
36
37local copy_node       = nuts.copy
38local endofmath       = nuts.endofmath
39local insertafter     = nuts.insertafter
40local findattribute   = nuts.findattribute
41----- unsetattributes = nuts.unsetattributes
42
43local nextglyph       = nuts.traversers.glyph
44
45local nodecodes       = nodes.nodecodes
46local kerncodes       = nodes.kerncodes
47
48local glyph_code      = nodecodes.glyph
49local kern_code       = nodecodes.kern
50local disc_code       = nodecodes.disc
51local math_code       = nodecodes.math
52
53local fontkern_code   = kerncodes.fontkern
54
55local enableaction    = nodes.tasks.enableaction
56
57local newkern         = nuts.pool.kern
58
59local fonthashes      = fonts.hashes
60local fontdata        = fonthashes.identifiers
61local fontchar        = fonthashes.characters
62
63local currentfont     = font.current
64
65local variables       = interfaces.variables
66local v_reset         = variables.reset
67
68local texsetattribute = tex.setattribute
69local texgetattribute = tex.getattribute
70local texgetscales    = tex.getglyphscales
71
72local integer_value   = tokens.values.integer
73
74typesetters           = typesetters or { }
75local typesetters     = typesetters
76
77typesetters.cases     = typesetters.cases or { }
78local cases           = typesetters.cases
79
80cases.actions         = { }
81local actions         = cases.actions
82local a_cases         = attributes.private("case")
83
84local run             = 0 -- a trick to make neighbouring ranges work
85
86local registervalue   = attributes.registervalue
87local getvalue        = attributes.getvalue
88
89local function set(tag,font,category,xscale,yscale)
90    run = run + 1
91    local scales = { texgetscales() }
92    local settings = {
93        font     = font,
94        tag      = tag,
95        run      = run, -- still needed ?
96        category = category or 0,
97        scales   = scales,
98    }
99    if xscale and xscale ~= 1000 then
100        scales[2] = scales[2] * xscale / 1000
101    end
102    if yscale and yscale ~= 1000 then
103        scales[3] = scales[3] * yscale / 1000
104    end
105    texsetattribute(a_cases,registervalue(a_cases,settings))
106end
107
108-- a previous implementation used char(0) as placeholder for the larger font, so we needed
109-- to remove it before it can do further harm ... that was too tricky as we use char 0 for
110-- other cases too
111--
112-- we could do the whole glyph run here (till no more attributes match) but then we end up
113-- with more code .. maybe i will clean this up anyway as the lastfont hack is somewhat ugly
114-- ... on the other hand, we need to deal with cases like:
115--
116-- \WORD {far too \Word{many \WORD{more \word{pushed} in between} useless} words}
117
118local uccodes    = characters.uccodes
119local lccodes    = characters.lccodes
120local categories = characters.categories
121
122-- true false true == mixed
123
124local function replacer(start,codes)
125    local char, fnt = isglyph(start)
126    local dc = codes[char]
127    if dc then
128        local ifc = fontchar[fnt]
129        if type(dc) == "table" then
130            for i=1,#dc do
131                if not ifc[dc[i]] then
132                    return start, false
133                end
134            end
135            for i=#dc,1,-1 do
136                local chr = dc[i]
137                if i == 1 then
138                    setchar(start,chr)
139                else
140                    local g = copy_node(start)
141                    setchar(g,chr)
142                    insertafter(start,start,g)
143                end
144            end
145        elseif ifc[dc] then
146            setchar(start,dc)
147        end
148    end
149    return start
150end
151
152local registered, n = { }, 0
153
154local function register(name,f)
155    if type(f) == "function" then
156        n = n + 1
157        actions[n] = f
158        registered[name] = n
159        return n
160    else
161        local n = registered[f]
162        registered[name] = n
163        return n
164    end
165end
166
167cases.register = register
168
169local function WORD(start,data,lastfont,n,count,where,first)
170    lastfont[n] = false
171    return replacer(first or start,uccodes)
172end
173
174local function word(start,data,lastfont,n,count,where,first)
175    lastfont[n] = false
176    return replacer(first or start,lccodes)
177end
178
179local function Words(start,data,lastfont,n,count,where,first) -- looks quite complex
180    if where == "post" then
181        return
182    end
183    if count == 1 and where ~= "post" then
184        replacer(first or start,uccodes)
185        return start, true
186    else
187        return start, true
188    end
189end
190
191local function Word(start,data,lastfont,n,count,where,first)
192    data.blocked = true
193    return Words(start,data,lastfont,n,count,where,first)
194end
195
196local function camel(start,data,lastfont,n,count,where,first)
197    word(start,data,lastfont,n,count,where,first)
198    Words(start,data,lastfont,n,count,where,first)
199    return start, true
200end
201
202local function mixed(start,data,lastfont,n,count,where,first,keep)
203    if where == "post" then
204        return
205    end
206    local used = first or start
207    local char = getchar(used)
208    local dc   = uccodes[char]
209    if not dc then
210        -- quit
211    elseif dc == char then
212        local lfa = lastfont[n]
213        if lfa then
214            local s = data.scales
215            setfont(used,lfa)
216            if s then
217                setscales(used,s[1],s[2],s[3])
218            end
219        end
220    elseif not keep then
221        replacer(used,uccodes)
222    end
223    return start, true
224end
225
226local function Camel(start,data,lastfont,n,count,where,first)
227    return mixed(start,data,lastfont,n,count,where,first,true)
228end
229
230local function Capital(start,data,lastfont,n,count,where,first,once) -- 3
231    local used = first or start
232    if count == 1 and where ~= "post" then
233        local lfa = lastfont[n]
234        if lfa then
235            local dc = uccodes[getchar(used)]
236            if dc then
237                local s = data.scales
238                setfont(used,lfa)
239                if s then
240                    setscales(used,s[1],s[2],s[3])
241                end
242            end
243        end
244    end
245    local s, c = replacer(first or start,uccodes)
246    if once then
247        lastfont[n] = false -- here
248    end
249    return start, c
250end
251
252local function capital(start,data,lastfont,n,where,count,first,count) -- 4
253    return Capital(start,data,lastfont,n,where,count,first,true)
254end
255
256local function none(start,data,lastfont,n,count,where,first)
257    return start, true
258end
259
260local function randomized(start,data,lastfont,n,count,where,first)
261    local used  = first or start
262    local char,
263          font  = getcharfont(used)
264    local tfm   = fontchar[font]
265    lastfont[n] = false
266    local kind  = categories[char]
267    if kind == "lu" then
268        while true do
269            local n = getrandom("capital lu",0x41,0x5A)
270            if tfm[n] then -- this also intercepts tables
271                setchar(used,n)
272                return start
273            end
274        end
275    elseif kind == "ll" then
276        while true do
277            local n = getrandom("capital ll",0x61,0x7A)
278            if tfm[n] then -- this also intercepts tables
279                setchar(used,n)
280                return start
281            end
282        end
283    end
284    return start
285end
286
287register(variables.WORD,   WORD)              --   1
288register(variables.word,   word)              --   2
289register(variables.Word,   Word)              --   3
290register(variables.Words,  Words)             --   4
291register(variables.capital,capital)           --   5
292register(variables.Capital,Capital)           --   6
293register(variables.none,   none)              --   7 (dummy)
294register(variables.random, randomized)        --   8
295register(variables.mixed,  mixed)             --   9
296register(variables.camel,  camel)             --  10
297register(variables.Camel,  Camel)             --  11
298
299register(variables.cap,    variables.capital) -- clone
300register(variables.Cap,    variables.Capital) -- clone
301
302function cases.handler(head)
303    local _, start = findattribute(head,a_cases)
304    if start then
305        local lastfont = { }
306        local lastattr = nil
307        local count    = 0
308        while start do -- while because start can jump ahead
309            local id = getid(start)
310            if id == glyph_code then
311                local attr = getattr(start,a_cases)
312                if attr and attr > 0 then
313                    local data = getvalue(a_cases,attr)
314                    if data and not data.blocked then
315                        if attr ~= lastattr then
316                            lastattr = attr
317                            count    = 1
318                        else
319                            count    = count + 1
320                        end
321                        local tag    = data.tag
322                        local font   = data.font
323                        local run    = data.run
324                        local action = actions[tag] -- map back to low number
325                        lastfont[tag] = font
326                        if action then
327                            local quit
328                            start, quit = action(start,data,lastfont,tag,count)
329                            if trace_casing then
330                                report_casing("case trigger %a, instance %a, fontid %a, result %a",
331                                    tag,run,font,quit and "-" or "+")
332                            end
333                        elseif trace_casing then
334                            report_casing("unknown case trigger %a",tag)
335                        end
336                    end
337                end
338            elseif id == disc_code then
339                local attr = getattr(start,a_cases)
340                if attr and attr > 0 then
341                    local data = getvalue(a_cases,attr)
342                    if data and not data.blocked then
343                        if attr ~= lastattr then
344                            lastattr = attr
345                            count    = 0
346                        end
347                        local tag    = data.tag
348                        local font   = data.font
349                        local action = actions[tag] -- map back to low number
350                        lastfont[tag] = font
351                        if action then
352                            local pre, post, replace = getdisc(start)
353                            if replace then
354                                local cnt = count
355                                for g in nextglyph, replace do
356                                    cnt = cnt + 1
357                                    getattr(g,a_cases)
358                                    local h, quit = action(start,data,lastfont,tag,cnt,"replace",g)
359                                    if quit then
360                                        break
361                                    end
362                                end
363                            end
364                            if pre then
365                                local cnt = count
366                                for g in nextglyph, pre do
367                                    cnt = cnt + 1
368                                    getattr(g,a_cases)
369                                    local h, quit = action(start,data,lastfont,tag,cnt,"pre",g)
370                                    if quit then
371                                        break
372                                    end
373                                end
374                            end
375                            if post then
376                                local cnt = count
377                                for g in nextglyph, post do
378                                    cnt = cnt + 1
379                                    getattr(g,a_cases)
380                                    local h, quit = action(start,data,lastfont,tag,cnt,"post",g)
381                                    if quit then
382                                        break
383                                    end
384                                end
385                            end
386                        end
387                        count = count + 1
388                    end
389                end
390            else
391                if id == math_code then
392                    start = endofmath(start)
393                end
394                count = 0
395            end
396            if start then
397                start = getnext(start)
398            end
399        end
400    end
401    return head
402end
403
404local enabled = false
405
406local function setcases(n,id,category,xscale,yscale)
407    if n ~= v_reset then
408        n = registered[n] or tonumber(n)
409        if n then
410            if not enabled then
411                enableaction("processors","typesetters.cases.handler")
412                if trace_casing then
413                    report_casing("enabling case handler")
414                end
415                enabled = true
416            end
417            set(n,id or currentfont(),category,xscale,yscale)
418            return
419        end
420    end
421    texsetattribute(a_cases)
422end
423
424cases.set = setcases
425
426-- interface
427
428interfaces.implement {
429    name      = "setcharactercasing",
430    actions   = function(name,category,xscale,yscale)
431        setcases(name,false,category,xscale,yscale)
432    end,
433    arguments = { "argument", "integer", "integer", "integer" },
434}
435
436interfaces.implement {
437    name      = "getcharactercasingcategory",
438    public    = true,
439    usage     = "value",
440    actions   = function()
441        local v = getvalue(a_cases,texgetattribute(a_cases))
442        return integer_value, v and v.category or 0
443    end,
444}
445
446-- An example of a special plug, see type-imp-punk for usage.
447
448cases.register("randomvariant", function(start)
449    local char, fnt = isglyph(start)
450    local data = fontchar[fnt][char]
451    if data then
452        local variants = data.variants
453        if variants then
454            local n = #variants
455            local i = getrandom("variant",1,n+1)
456            if i > n then
457                -- we keep the original
458            else
459                setchar(start,variants[i])
460            end
461            return start, true
462        end
463    end
464    return start, false
465end)
466