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