typo-sus.lmt /size: 11 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['typo-sus'] = {
2    version   = 1.001,
3    comment   = "companion to typo-sus.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 = next
10
11local punctuation = {
12    po = true,
13}
14
15local openquote = {
16    ps = true,
17    pi = true,
18}
19
20local closequote = {
21    pe = true,
22    pf = true,
23}
24
25local weird = {
26    lm = true,
27    no = true,
28}
29
30local categories      = characters.categories
31
32local nodecodes       = nodes.nodecodes
33
34local glyph_code      <const> = nodecodes.glyph
35local kern_code       <const> = nodecodes.kern
36local penalty_code    <const> = nodecodes.penalty
37local glue_code       <const> = nodecodes.glue
38local math_code       <const> = nodecodes.math
39local hlist_code      <const> = nodecodes.hlist
40local vlist_code      <const> = nodecodes.vlist
41
42local nuts            = nodes.nuts
43
44local nextnode        = nuts.traversers.node
45
46local getid           = nuts.getid
47local getprev         = nuts.getprev
48local getnext         = nuts.getnext
49local getattr         = nuts.getattr
50local getfont         = nuts.getfont
51local getchar         = nuts.getchar
52local getlist         = nuts.getlist
53local getkern         = nuts.getkern
54local getpenalty      = nuts.getpenalty
55local getwidth        = nuts.getwidth
56local getwhd          = nuts.getwhd
57local isglyph         = nuts.isglyph
58
59local setattr         = nuts.setattr
60local setlist         = nuts.setlist
61
62local setcolor        = nodes.tracers.colors.set
63local insertbefore    = nuts.insertbefore
64local insertafter     = nuts.insertafter
65local endofmath       = nuts.endofmath
66
67local nodepool        = nuts.pool
68
69local new_rule        = nodepool.rule
70local new_kern        = nodepool.kern
71local new_hlist       = nodepool.hlist
72----- new_penalty     = nodepool.penalty
73
74local a_characters    <const> = attributes.private("characters")
75local a_suspecting    <const> = attributes.private('suspecting')
76local a_suspect       <const> = attributes.private('suspect')
77local a_invisibles    <const> = attributes.private('invisibles')
78
79local unsetvalue      <const> = attributes.unsetvalue
80
81local texsetattribute = tex.setattribute
82local enabled         = false
83
84local enableaction    = nodes.tasks.enableaction
85
86local threshold       = 65536 / 4
87
88local function special(n)
89    if n then
90        local id = getid(n)
91        if id == kern_code then
92            return getkern(n) < threshold
93        elseif id == penalty_code then
94            return true
95        elseif id == glue_code then
96            return getwidth(n) < threshold
97        elseif id == hlist_code then
98            return getwidth(n) < threshold
99        end
100    else
101        return false
102    end
103end
104
105local function goback(current)
106    local prev = getprev(current)
107    while prev and special(prev) do
108        prev = getprev(prev)
109    end
110    if prev then
111        return prev, getid(prev)
112    end
113end
114
115local function goforward(current)
116    local next = getnext(current)
117    while next and special(next) do
118        next = getnext(next)
119    end
120    if next then
121        return next, getid(next)
122    end
123end
124
125-- We can cache rules if needed but there are not that many.
126
127local function mark(head,current,id,color)
128    if id == glue_code then
129        -- the glue can have stretch and/or shrink so the rule can overlap with the
130        -- following glyph .. no big deal as that one then sits on top of the rule
131        local width = getwidth(current)
132        local rule  = new_rule(width)
133        local kern  = new_kern(-width)
134        head = insertbefore(head,current,rule)
135        head = insertbefore(head,current,kern)
136        setcolor(rule,color)
137 -- elseif id == kern_code then
138 --     local width = getkern(current)
139 --     local rule  = new_rule(width)
140 --     local kern  = new_kern(-width)
141 --     head = insertbefore(head,current,rule)
142 --     head = insertbefore(head,current,kern)
143 --     setcolor(rule,color)
144    else
145        local width, height, depth = getwhd(current)
146        local extra = fonts.hashes.xheights[getfont(current)] / 2
147        local rule  = new_rule(width,height+extra,depth+extra)
148        local hlist = new_hlist(rule)
149        head = insertbefore(head,current,hlist)
150        setcolor(rule,color)
151        setcolor(current,"white")
152    end
153    return head, current
154end
155
156-- we can cache the font and skip ahead to next but it doesn't
157-- save enough time and it makes the code looks bad too ... after
158-- all, we seldom use this
159
160local colors = {
161    "darkred",
162    "darkgreen",
163    "darkblue",
164    "darkcyan",
165    "darkmagenta",
166    "darkyellow",
167    "darkgray",
168    "orange",
169}
170
171local found = 0
172
173function typesetters.marksuspects(head)
174    local current  = head
175    local lastdone = nil
176    while current do
177        if getattr(current,a_suspecting) then
178            local char, id = isglyph(current)
179            if char then
180                local code = categories[char]
181                local done = false
182                if punctuation[code] then
183                    local prev, pid = goback(current)
184                    if prev and pid == glue_code then
185                        done = 3 -- darkblue
186                    elseif prev and pid == math_code then
187                        done = 3 -- darkblue
188                    else
189                        local next, nid = goforward(current)
190                        if next and nid ~= glue_code then
191                            done = 3 -- darkblue
192                        end
193                    end
194                elseif openquote[code] then
195                    local next, nid = goforward(current)
196                    if next and nid == glue_code then
197                        done = 1 -- darkred
198                    end
199                elseif closequote[code] then
200                    local prev, pid = goback(current)
201                    if prev and pid == glue_code then
202                        done = 1 -- darkred
203                    end
204                elseif weird[code] then
205                    done = 2 -- darkgreen
206                else
207                    local prev, pid = goback(current)
208                    if prev then
209                        if pid == math_code then
210                            done = 7-- darkgray
211                        elseif pid == glyph_code and getfont(current) ~= getfont(prev) then
212                            if lastdone ~= prev then
213                                done = 2 -- darkgreen
214                            end
215                        end
216                    end
217                    if not done then
218                        local next, nid = goforward(current)
219                        if next then
220                            if nid == math_code then
221                                done = 7 -- darkgray
222                            elseif nid == glyph_code and getfont(current) ~= getfont(next) then
223                                if lastdone ~= prev then
224                                    done = 2 -- darkgreen
225                                end
226                            end
227                        end
228                    end
229                end
230                if done then
231                    setattr(current,a_suspect,done)
232                    lastdone = current
233                    found = found + 1
234                end
235                current = getnext(current)
236            elseif id == math_code then
237                current = getnext(endofmath(current))
238            elseif id == glue_code then
239                local a = getattr(current,a_characters)
240                if a then
241                    local prev = getprev(current)
242                    local prid = prev and getid(prev)
243                    local done = false
244                    if prid == penalty_code and getpenalty(prev) == 10000 then
245                        done = 8 -- orange
246                    else
247                        done = 5 -- darkmagenta
248                    end
249                    if done then
250                        setattr(current,a_suspect,done)
251                     -- lastdone = current
252                        found = found + 1
253                    end
254                end
255                current = getnext(current)
256            else
257                current = getnext(current)
258            end
259        else
260            current = getnext(current)
261        end
262    end
263    return head
264end
265
266local function showsuspects(head)
267    -- we inject before so we're okay with a loop
268    local pickup = false
269    for current, id, subtype in nextnode, head do
270        if current == pickup then
271            pickup = false
272        elseif id == glyph_code then
273            local a = getattr(current,a_suspect)
274            if a then
275                head = mark(head,current,id,colors[a])
276            end
277        elseif id == glue_code then
278            local a = getattr(current,a_suspect)
279            if a then
280                head = mark(head,current,id,colors[a])
281            end
282        elseif id == math_code then
283            pickup = endofmath(current)
284            if pickup == current then
285                pickup = false
286            end
287        elseif id == hlist_code or id == vlist_code then
288            local list = getlist(current)
289            if list then
290                local l = showsuspects(list)
291                if l ~= list then
292                    setlist(current,l)
293                end
294            end
295        end
296    end
297    return head
298end
299
300function typesetters.showsuspects(head)
301    if found > 0 then
302        return showsuspects(head)
303    else
304        return head
305    end
306end
307
308-- or maybe a directive
309
310trackers.register("typesetters.suspects",function(v)
311    texsetattribute(a_suspecting,v and 1 or unsetvalue)
312    if v and not enabled then
313        enableaction("processors","typesetters.marksuspects")
314        enableaction("shipouts",  "typesetters.showsuspects")
315        enabled = true
316    end
317end)
318
319local enabled    = false
320local invisibles = false
321
322local invisible = {
323    [0x200B] = "zwsp",
324    [0x200C] = "zwnj",
325    [0x200D] = "zwj",
326    [0x2061] = "apply",
327    [0x2062] = "times",
328    [0x2063] = "separator",
329    [0x2064] = "plus",
330}
331
332local function showinvisibles(head)
333    -- we inject before so we're okay with a loop
334    for current, id, subtype in nextnode, head do
335        -- get...
336        if id == glyph_code then
337--            local a = getattr(current,a_suspect)
338--            if a then
339                local c = getchar(current)
340                local i = invisible[c]
341                if i then
342                    if not invisibles then
343                        invisibles = nodes.visualizers.register("invisible")
344                    end
345                    local info = invisibles(i)
346                    head = insertbefore(head,current,info)
347                end
348--            end
349        elseif id == hlist_code or id == vlist_code then
350            local list = getlist(current)
351            if list then
352                local l = showinvisibles(list)
353                if l ~= list then
354                    setlist(current,l)
355                end
356            end
357        end
358    end
359    return head
360end
361
362function typesetters.showinvisibles(head)
363    return showinvisibles(head)
364end
365
366trackers.register("typesetters.invisibles",function(v)
367--     texsetattribute(a_invisibles,v and 1 or unsetvalue)
368    if v and not enabled then
369        enableaction("shipouts","typesetters.showinvisibles")
370        enabled = true
371    end
372end)
373