typo-spa.lmt /size: 10 Kb    last modification: 2024-01-16 09:03
1if not modules then modules = { } end modules ['typo-spa'] = {
2    version   = 1.001,
3    comment   = "companion to typo-spa.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
9local next, type = next, type
10
11local trace_spacing = false  trackers.register("typesetters.spacing", function(v) trace_spacing = v end)
12
13local report_spacing = logs.reporter("typesetting","spacing")
14
15local nodes, fonts, node = nodes, fonts, node
16
17local fonthashes         = fonts.hashes
18local chardata           = fonthashes.characters
19local quaddata           = fonthashes.quads
20
21local texsetattribute    = tex.setattribute
22local unsetvalue         = attributes.unsetvalue
23
24local xscaled            = font.xscaled
25
26local v_reset            = interfaces.variables.reset
27
28local nuts               = nodes.nuts
29
30local getnext            = nuts.getnext
31local getprev            = nuts.getprev
32local getattr            = nuts.getattr
33local isglyph            = nuts.isglyph
34local getlanguage        = nuts.getlanguage
35
36local insertnodebefore   = nuts.insertbefore
37local insertnodeafter    = nuts.insertafter
38local remove_node        = nuts.remove
39local endofmath          = nuts.endofmath
40local unsetattributes    = nuts.unsetattributes
41local findattribute      = nuts.findattribute
42
43local nodepool           = nuts.pool
44local new_penalty        = nodepool.penalty
45local new_glue           = nodepool.glue
46
47local nodecodes          = nodes.nodecodes
48local math_code          = nodecodes.math
49
50local somespace          = nodes.somespace
51local somepenalty        = nodes.somepenalty
52
53local enableaction       = nodes.tasks.enableaction
54
55typesetters              = typesetters or { }
56local typesetters        = typesetters
57
58typesetters.spacings     = typesetters.spacings or { }
59local spacings           = typesetters.spacings
60
61spacings.mapping         = spacings.mapping or { }
62spacings.numbers         = spacings.numbers or { }
63
64local a_spacings         = attributes.private("spacing")
65
66storage.register("typesetters/spacings/mapping", spacings.mapping, "typesetters.spacings.mapping")
67
68local mapping = spacings.mapping
69local numbers = spacings.numbers
70
71for i=1,#mapping do
72    local m = mapping[i]
73    numbers[m.name] = m
74end
75
76-- todo cache lastattr
77
78function spacings.handler(head)
79    local _, start = findattribute(head, a_spacings)
80    if start then
81        -- head is always begin of par (whatsit), so we have at least two prev nodes
82        -- penalty followed by glue
83        while start do
84            local char, id = isglyph(start)
85            if char then
86                local attr = getattr(start,a_spacings)
87                if attr and attr > 0 then
88                    local data = mapping[attr]
89                    if data then
90                        local map = data.characters[char]
91                        if map then
92                            local language = data.language or 0
93--                             if language == 0 or languages.registered[language].number == getlanguage(start) then
94                            if language == 0 or languages.numbers[language] == getlanguage(start) then
95                                local font = id
96                                local left = map.left
97                                local right = map.right
98                                local alternative = map.alternative
99                                local quad = xscaled(quaddata[font])
100                                local prev = getprev(start)
101                                if not chardata[font][char] then
102                                    report_spacing("missing character %C in font %i",char,font)
103                                end
104                                if left and left ~= 0 and prev then
105                                    local ok = false
106                                    local prevprev = getprev(prev)
107                                    if alternative == 1 then
108                                        if somespace(prev,true) then
109                                            if somepenalty(prevprev,10000) then
110                                                if trace_spacing then
111                                                    report_spacing("removing penalty and space before %C (left)",char)
112                                                end
113                                                head = remove_node(head,prev,true)
114                                                head = remove_node(head,prevprev,true)
115                                            else
116                                                if trace_spacing then
117                                                    report_spacing("removing space before %C (left)",char)
118                                                end
119                                                head = remove_node(head,prev,true)
120                                            end
121                                        end
122                                        ok = true
123                                    else
124                                        ok = not (somespace(prev,true) and somepenalty(prevprev,true)) or somespace(prev,true)
125                                    end
126                                    if ok then
127                                        if trace_spacing then
128                                            report_spacing("inserting penalty and space before %C (left)",char)
129                                        end
130                                        insertnodebefore(head,start,new_penalty(10000))
131                                        insertnodebefore(head,start,new_glue(left*quad))
132                                    end
133                                end
134                                local next = getnext(start)
135                                if right and right ~= 0 and next then
136                                    local ok = false
137                                    local nextnext = getnext(next)
138                                    if alternative == 1 then
139                                        if somepenalty(next,10000) then
140                                            if somespace(nextnext,true) then
141                                                if trace_spacing then
142                                                    report_spacing("removing penalty and space after %C right",char)
143                                                end
144                                                head = remove_node(head,next,true)
145                                                head = remove_node(head,nextnext,true)
146                                            end
147                                        elseif somespace(next,true) then
148                                            if trace_spacing then
149                                                report_spacing("removing space after %C (right)", char)
150                                            end
151                                            head = remove_node(head,next,true)
152                                        end
153                                        ok = true
154                                    else
155                                        ok = not (somepenalty(next,10000) and somespace(nextnext,true)) or somespace(next,true)
156                                    end
157                                    if ok then
158                                        if trace_spacing then
159                                            report_spacing("inserting penalty and space after %C (right)",char)
160                                        end
161                                        insertnodeafter(head,start,new_glue(right*quad))
162                                        insertnodeafter(head,start,new_penalty(10000))
163                                    end
164                                end
165                            end
166                        end
167                    end
168                end
169            elseif id == math_code then
170                start = endofmath(start) -- weird, can return nil .. no math end?
171            end
172            if start then
173                start = getnext(start)
174            end
175        end
176    end
177    return head
178end
179
180local enabled = false
181
182function spacings.define(name,parent)
183    local data = numbers[name]
184    if data then
185        -- error, already defined
186    else
187        local number = #mapping + 1
188        local data = {
189            name       = name,
190            number     = number,
191            characters = { },
192        }
193        mapping[number] = data
194        numbers[name]   = data
195        if parent and parent ~= "" then
196            -- could be dynamic i.e. metatable inheritance but maybe only as option
197            table.merge(data.characters,unpack(utilities.parsers.settings_to_array(parent)))
198        end
199    end
200end
201
202function spacings.setup(name,char,settings)
203    local data = numbers[name]
204    if not data then
205        -- error
206    elseif char > 0 then
207        data.characters[char] = settings
208    else
209        for k, v in next, settings do
210            data[k] = v
211        end
212    end
213end
214
215function spacings.set(name)
216    local n = unsetvalue
217    if name ~= v_reset then
218        local data = numbers[name]
219        if data then
220            if not enabled then
221                enableaction("processors","typesetters.spacings.handler")
222                enabled = true
223            end
224            n = data.number or unsetvalue
225        end
226    end
227    texsetattribute(a_spacings,n)
228end
229
230function spacings.reset()
231    texsetattribute(a_spacings,unsetvalue)
232end
233
234-- interface
235
236local implement = interfaces.implement
237
238implement {
239    name      = "definecharacterspacing",
240    actions   = spacings.define,
241    arguments = "string"
242}
243
244implement {
245    name      = "setupcharacterspacing",
246    actions   = spacings.setup,
247    arguments = {
248        "string",
249        "integer",
250        {
251            { "left",        "number" },
252            { "right",       "number" },
253            { "alternative", "integer" },
254            { "language",    "string" },
255        }
256    }
257}
258
259implement {
260    name      = "setcharacterspacing",
261    actions   = spacings.set,
262    arguments = "string"
263}
264