node-fnt.lmt /size: 30 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['node-fnt'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.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
9if not context then os.exit() end -- generic function in node-dum
10
11local next, type = next, type
12local concat, keys = table.concat, table.keys
13
14local nodes, node, fonts = nodes, node, fonts
15
16local trace_characters  = false  trackers.register("nodes.characters", function(v) trace_characters = v end)
17local trace_fontrun     = false  trackers.register("nodes.fontrun",    function(v) trace_fontrun    = v end)
18local trace_variants    = false  trackers.register("nodes.variants",   function(v) trace_variants   = v end)
19
20-- bad namespace for directives
21
22local force_discrun     = true   directives.register("nodes.discrun",      function(v) force_discrun     = v end)
23local force_boundaryrun = true   directives.register("nodes.boundaryrun",  function(v) force_boundaryrun = v end)
24----- force_basepass    = true   directives.register("nodes.basepass",     function(v) force_basepass    = v end)
25local keep_redundant    = false  directives.register("nodes.keepredundant",function(v) keep_redundant    = v end)
26
27local report_fonts      = logs.reporter("fonts","processing")
28
29local fonthashes        = fonts.hashes
30local fontdata          = fonthashes.identifiers
31local fontvariants      = fonthashes.variants
32local fontmodes         = fonthashes.modes
33
34local otf               = fonts.handlers.otf
35
36local starttiming       = statistics.starttiming
37local stoptiming        = statistics.stoptiming
38
39local nodecodes         = nodes.nodecodes
40local boundarycodes     = nodes.boundarycodes
41
42local handlers          = nodes.handlers
43
44local nuts              = nodes.nuts
45
46local getid             = nuts.getid
47local getsubtype        = nuts.getsubtype
48local getreplace        = nuts.getreplace
49local getnext           = nuts.getnext
50local getprev           = nuts.getprev
51local getboth           = nuts.getboth
52local getdata           = nuts.getdata
53local getglyphdata      = nuts.getglyphdata
54
55local setchar           = nuts.setchar
56local setlink           = nuts.setlink
57local setnext           = nuts.setnext
58local setprev           = nuts.setprev
59
60local isglyph           = nuts.isglyph -- unchecked
61local ischar            = nuts.ischar  -- checked
62
63local nextboundary      = nuts.traversers.boundary
64local nextdisc          = nuts.traversers.disc
65local nextchar          = nuts.traversers.char
66
67local flushnode         = nuts.flush
68local removefromlist    = nuts.removefromlist
69
70local disc_code         = nodecodes.disc
71local boundary_code     = nodecodes.boundary
72
73local wordboundary_code = boundarycodes.word
74
75local protectglyphs     = nuts.protectglyphs
76local unprotectglyphs   = nuts.unprotectglyphs
77local protectglyphsnone = nuts.protectglyphsnone
78
79local setmetatableindex = table.setmetatableindex
80
81-- some tests with using an array of dynamics[id] and processes[id] demonstrated
82-- that there was nothing to gain (unless we also optimize other parts)
83--
84-- maybe getting rid of the intermediate shared can save some time
85
86local run = 0
87
88local setfontdynamics = { }
89local fontprocesses   = { }
90
91setmetatableindex(setfontdynamics, function(t,font)
92    local tfmdata = fontdata[font]
93    local shared = tfmdata.shared
94    local f = shared and shared.dynamics and otf.setdynamics or false
95    if f then
96        local v = { }
97        t[font] = v
98        setmetatableindex(v,function(t,k)
99            local v = f(font,k)
100            t[k] = v
101            return v
102        end)
103        return v
104    else
105        t[font] = false
106        return false
107    end
108end)
109
110setmetatableindex(fontprocesses, function(t,font)
111    local tfmdata = fontdata[font]
112    local shared = tfmdata.shared -- we need to check shared, only when same features
113    local processes = shared and shared.processes
114    if processes and #processes > 0 then
115        t[font] = processes
116        return processes
117    else
118        t[font] = false
119        return false
120    end
121end)
122
123fonts.hashes.setdynamics = setfontdynamics
124fonts.hashes.processes   = fontprocesses
125
126-- if we forget about basemode we don't need to test too much here and we can consider running
127-- over sub-ranges .. this involves a bit more initializations but who cares .. in that case we
128-- also need to use the stop criterium (we already use head too) ... we cannot use traverse
129-- then, so i'll test it on some local clone first ... the only pitfall is changed directions
130-- inside a run which means that we need to keep track of this which in turn complicates matters
131-- in a way i don't like
132
133-- we need to deal with the basemode fonts here and can only run over ranges as we otherwise get
134-- luatex craches due to all kind of asserts in the disc/lig builder
135
136-- there is no gain in merging used (dynamic 0) and dynamics apart from a bit less code
137
138local ligaturing = nuts.ligaturing
139local kerning    = nuts.kerning
140
141-- local function start_trace(head)
142--     run = run + 1
143--     report_fonts()
144--     report_fonts("checking node list, run %s",run)
145--     report_fonts()
146--     local n = head
147--     while n do
148--         local char, id = isglyph(n)
149--         if char then
150--             local font = id
151--             local dynamic = getglyphdata(n) or 0
152--             report_fonts("font %03i, dynamic %03i, glyph %C",font,dynamic,char)
153--         elseif id == disc_code then
154--             report_fonts("[disc] %s",nodes.listtoutf(n,true,false,n))
155--         elseif id == boundary_code then
156--             report_fonts("[boundary] %i:%i",getsubtype(n),getdata(n))
157--         else
158--             report_fonts("[%s]",nodecodes[id])
159--         end
160--         n = getnext(n)
161--     end
162-- end
163
164-- local function stop_trace(u,usedfonts,d,dynamicfonts,b,basefonts,r,redundant)
165--     report_fonts()
166--     report_fonts("statics : %s",u > 0 and concat(keys(usedfonts)," ") or "none")
167--     report_fonts("dynamics: %s",d > 0 and concat(keys(dynamicfonts)," ") or "none")
168--     report_fonts("built-in: %s",b > 0 and b or "none")
169--     report_fonts("removed : %s",r > 0 and r or "none")
170--     report_fonts()
171-- end
172
173-- This is the original handler and we keep it around as reference. It served us
174-- well for quite a while.
175
176-- do
177--
178--     local usedfonts
179--     local dynamicfonts
180--     local basefonts  -- could be reused
181--     local basefont
182--     local prevfont
183--     local prevdynamic
184--     local variants
185--     local redundant  -- could be reused
186--     local firstnone
187--     local lastfont
188--     local lastproc
189--     local lastnone
190--
191--     local d, u, b, r
192--
193--     local function protectnone()
194--         protectglyphs(firstnone,lastnone)
195--         firstnone = nil
196--     end
197--
198--     local function setnone(n)
199--         if firstnone then
200--             protectnone()
201--         end
202--         if basefont then
203--             basefont[2] = getprev(n)
204--             basefont = false
205--         end
206--         if not firstnone then
207--             firstnone = n
208--         end
209--         lastnone = n
210--     end
211--
212--     local function setbase(n)
213--         if firstnone then
214--             protectnone()
215--         end
216--         if force_basepass then
217--             if basefont then
218--                 basefont[2] = getprev(n)
219--             end
220--             b = b + 1
221--             basefont = { n, false }
222--             basefonts[b] = basefont
223--         end
224--     end
225--
226--     local function setnode(n,font,dynamic) -- we could use prevfont and prevdynamic when we set then first
227--         if firstnone then
228--             protectnone()
229--         end
230--         if basefont then
231--             basefont[2] = getprev(n)
232--             basefont = false
233--         end
234--         if dynamic > 0 then
235--             local used = dynamicfonts[font]
236--             if not used then
237--                 used = { }
238--                 dynamicfonts[font] = used
239--             end
240--             if not used[dynamic] then
241--                 local fd = setfontdynamics[font]
242--                 if fd then
243--                     used[dynamic] = fd[dynamic]
244--                     d = d + 1
245--                 end
246--             end
247--         else
248--             local used = usedfonts[font]
249--             if not used then
250--                 lastfont = font
251--                 lastproc = fontprocesses[font]
252--                 if lastproc then
253--                     usedfonts[font] = lastproc
254--                     u = u + 1
255--                 end
256--             end
257--         end
258--     end
259--
260--     function handlers.characters(head,groupcode,direction)
261--         -- either next or not, but definitely no already processed list
262--         starttiming(nodes)
263--
264--         usedfonts    = { }
265--         dynamicfonts = { }
266--         basefonts    = { }
267--         basefont     = nil
268--         prevfont     = nil
269--         prevdynamic  = 0
270--         variants     = nil
271--         redundant    = nil
272--         firstnone    = nil
273--         lastfont     = nil
274--         lastproc     = nil
275--         lastnone     = nil
276--
277--         local fontmode = nil -- base none or other
278--
279--         d, u, b, r = 0, 0, 0, 0
280--
281--         if trace_fontrun then
282--             start_trace(head)
283--         end
284--
285--         -- There is no gain in checking for a single glyph and then having a fast path. On the
286--         -- metafun manual (with some 2500 single char lists) the difference is just noise.
287--
288--         for n, char, font, dynamic in nextchar, head do
289--
290--             if font ~= prevfont then
291--                 prevfont = font
292--                 fontmode = fontmodes[font]
293--                 if fontmode == "none" then
294--                     prevdynamic = 0
295--                     variants    = false
296--                     setnone(n)
297--                 elseif fontmode == "base" then
298--                     prevdynamic = 0
299--                     variants    = false
300--                     setbase(n)
301--                 else
302--                  -- local dynamic = getglyphdata(n) or 0 -- zero dynamic is reserved for fonts in context
303--                     prevdynamic = dynamic
304--                     variants    = fontvariants[font]
305--                     setnode(n,font,dynamic)
306--                 end
307--             elseif fontmode == "node" then
308--                 local dynamic = getglyphdata(n) or 0 -- zero dynamic is reserved for fonts in context
309--                 if dynamic ~= prevdynamic then
310--                     prevdynamic = dynamic
311--                     variants    = fontvariants[font]
312--                     setnode(n,font,dynamic)
313--                 end
314--             elseif firstnone then
315--                 lastnone = n
316--             end
317--
318--             if variants then
319--                 if (char >= 0xFE00 and char <= 0xFE0F) or (char >= 0xE0100 and char <= 0xE01EF) then
320--          -- if variants and char >= 0xFE00 then
321--          --     if char < 0xFE0F or (char >= 0xE0100 and char <= 0xE01EF) then
322--                     local hash = variants[char]
323--                     if hash then
324--                         local p = getprev(n)
325--                         if p then
326--                             local char    = ischar(p) -- checked
327--                             local variant = hash[char]
328--                             if variant then
329--                                 if trace_variants then
330--                                     report_fonts("replacing %C by %C",char,variant)
331--                                 end
332--                                 setchar(p,variant)
333--                                 if redundant then
334--                                     r = r + 1
335--                                     redundant[r] = n
336--                                 else
337--                                     r = 1
338--                                     redundant = { n }
339--                                 end
340--                             end
341--                         end
342--                     elseif keep_redundant then
343--                         -- go on, can be used for tracing
344--                     elseif redundant then
345--                         r = r + 1
346--                         redundant[r] = n
347--                     else
348--                         r = 1
349--                         redundant = { n }
350--                     end
351--                 end
352--             end
353--
354--         end
355--
356--         if firstnone then
357--             protectnone()
358--         end
359--
360--         if force_boundaryrun then
361--
362--             -- we can inject wordboundaries and then let the hyphenator do its work
363--             -- but we need to get rid of those nodes in order to build ligatures
364--             -- and kern (a rather context thing)
365--
366--             for b, subtype in nextboundary, head do
367--                 if subtype == wordboundary_code then
368--                     if redundant then
369--                         r = r + 1
370--                         redundant[r] = b
371--                     else
372--                         r = 1
373--                         redundant = { b }
374--                     end
375--                 end
376--             end
377--
378--         end
379--
380--         if redundant then
381--             for i=1,r do
382--                 local r = redundant[i]
383--                 local p, n = getboth(r)
384--                 if r == head then
385--                     head = n
386--                     setprev(n)
387--                 else
388--                     setlink(p,n)
389--                 end
390--                 if b > 0 then
391--                     for i=1,b do
392--                         local bi = basefonts[i]
393--                         local b1 = bi[1]
394--                         local b2 = bi[2]
395--                         if b1 == b2 then
396--                             if b1 == r then
397--                                 bi[1] = false
398--                                 bi[2] = false
399--                             end
400--                         elseif b1 == r then
401--                             bi[1] = n
402--                         elseif b2 == r then
403--                             bi[2] = p
404--                         end
405--                     end
406--                 end
407--                 flushnode(r)
408--             end
409--         end
410--
411--         if force_discrun then
412--             -- basefont is not supported in disc only runs ... it would mean a lot of
413--             -- ranges .. we could try to run basemode as a separate processor run but not
414--             -- for now (we can consider it when the new node code is tested
415--             for disc in nextdisc, head do
416--                 -- doing only replace is good enough because pre and post are normally used
417--                 -- for hyphens and these come from fonts that part of the hyphenated word
418--                 local r = getreplace(disc)
419--                 if r then
420--                     local prevfont    = nil
421--                     local prevdynamic = nil
422--                     local none        = false
423--                           firstnone   = nil
424--                           basefont    = nil
425--                     for n, char, font, dynamic in nextchar, r do
426--                      -- local dynamic = getglyphdata(n) or 0 -- zero dynamic is reserved for fonts in context
427--                         if font ~= prevfont or dynamic ~= prevdynamic then
428--                             prevfont    = font
429--                             prevdynamic = dynamic
430--                             local fontmode = fontmodes[font]
431--                             if fontmode == "none" then
432--                                 setnone(n)
433--                             elseif fontmode == "base" then
434--                                 -- so the replace gets an extra treatment ... so be it
435--                                 setbase(n)
436--                             else
437--                                 setnode(n,font,dynamic)
438--                             end
439--                         elseif firstnone then
440--                          -- lastnone = n
441--                             lastnone = nil
442--                         end
443--                         -- we assume one font for now (and if there are more and we get into issues then
444--                         -- we can always remove the break)
445--                         break
446--                     end
447--                     if firstnone then
448--                         protectnone()
449--                     end
450--                 end
451--             end
452--
453--         end
454--
455--         if trace_fontrun then
456--             stop_trace(u,usedfonts,d,dynamicfonts,b,basefonts,r,redundant)
457--         end
458--
459--         -- in context we always have at least 2 processors
460--         if u == 0 then
461--             -- skip
462--         elseif u == 1 then
463--             for i=1,#lastproc do
464--                 head = lastproc[i](head,lastfont,0,direction)
465--             end
466--         else
467--             for font, processors in next, usedfonts do -- unordered
468--                 for i=1,#processors do
469--                     head = processors[i](head,font,0,direction,u) -- u triggers disc optimizer
470--                 end
471--             end
472--         end
473--
474--         if d == 0 then
475--             -- skip
476--         elseif d == 1 then
477--             local font, dynamics = next(dynamicfonts)
478--             for dynamic, processors in next, dynamics do -- unordered, dynamic can switch in between
479--                 for i=1,#processors do
480--                     head = processors[i](head,font,dynamic,direction)
481--                 end
482--             end
483--         else
484--             for font, dynamics in next, dynamicfonts do
485--                 for dynamic, processors in next, dynamics do -- unordered, dynamic can switch in between
486--                     for i=1,#processors do
487--                         head = processors[i](head,font,dynamic,direction,d) -- d triggers disc optimizer
488--                     end
489--                 end
490--             end
491--         end
492--         if b == 0 then
493--             -- skip
494--         elseif b == 1 then
495--             -- only one font
496--             local range = basefonts[1]
497--             local start = range[1]
498--             local stop  = range[2]
499--             if (start or stop) and (start ~= stop) then
500--                 local front = head == start
501--                 if stop then
502--                     start = ligaturing(start,stop)
503--                     start = kerning(start,stop)
504--                 elseif start then -- safeguard
505--                     start = ligaturing(start)
506--                     start = kerning(start)
507--                 end
508--                 if front and head ~= start then
509--                     head = start
510--                 end
511--             end
512--         else
513--             -- multiple fonts
514--             for i=1,b do
515--                 local range = basefonts[i]
516--                 local start = range[1]
517--                 local stop  = range[2]
518--                 if start then -- and start ~= stop but that seldom happens
519--                     local front = head == start
520--                     local prev  = getprev(start)
521--                     local next  = getnext(stop)
522--                     if stop then
523--                         start, stop = ligaturing(start,stop)
524--                         start, stop = kerning(start,stop)
525--                     else
526--                         start = ligaturing(start)
527--                         start = kerning(start)
528--                     end
529--                     -- is done automatically
530--                     if prev then
531--                         setlink(prev,start)
532--                     end
533--                     if next then
534--                         setlink(stop,next)
535--                     end
536--                     -- till here
537--                     if front and head ~= start then
538--                         head = start
539--                     end
540--                 end
541--             end
542--         end
543--
544--         stoptiming(nodes)
545--
546--         if trace_characters then
547--             nodes.report(head)
548--         end
549--
550--         return head
551--     end
552--
553-- end
554
555
556-- This variant uses less code but relies on the engine checking the textcontrol
557-- flags:
558--
559-- baseligatures : 0x02
560-- basekerns     : 0x04
561-- noneprotected : 0x08
562--
563-- This permits one 'base' pass instead of multiple over ranges which is kind of
564-- tricky because we then can have clashes when we process replace fields
565-- independently. We can also protect 'none' in one go. It is actually not that
566-- much faster (and in some cases it might even be slower). We can make the code
567-- a bit leaner (no setbase and setnone).
568
569do
570
571    local usedfonts
572    local dynamicfonts
573    local prevfont
574    local prevdynamic
575    local variants
576    local redundant  -- could be reused
577    local lastfont
578    local lastproc
579 -- local basedone
580 -- local nonedone
581
582 -- local d, u, b, r
583    local d, u, r
584
585 -- local function setnone()
586 --     nonedone = true
587 -- end
588
589 -- local function setbase()
590 --     if force_basepass then
591 --         basedone = true
592 --     end
593 -- end
594
595--     local function setnode(font,dynamic) -- we could use prevfont and prevdynamic when we set them first
596--         if dynamic > 0 then
597--             local used = dynamicfonts[font]
598--             if not used then
599--                 used = { }
600--                 dynamicfonts[font] = used
601--             end
602--             if not used[dynamic] then
603--                 local fd = setfontdynamics[font]
604--                 if fd then
605--                     used[dynamic] = fd[dynamic]
606--                     d = d + 1
607--                 end
608--             end
609--         else
610--             local used = usedfonts[font]
611--             if not used then
612--                 lastfont = font
613--                 lastproc = fontprocesses[font]
614--                 if lastproc then
615--                     usedfonts[font] = lastproc
616--                     u = u + 1
617--                 end
618--             end
619--         end
620--     end
621
622    local function setnode() -- we could use prevfont and prevdynamic when we set them first
623        if prevdynamic > 0 then
624            local used = dynamicfonts[prevfont]
625            if not used then
626                used = { }
627                dynamicfonts[prevfont] = used
628            end
629            if not used[prevdynamic] then
630                local fd = setfontdynamics[prevfont]
631                if fd then
632                    used[prevdynamic] = fd[prevdynamic]
633                    d = d + 1
634                end
635            end
636        else
637            local used = usedfonts[prevfont]
638            if not used then
639                lastfont = prevfont
640                lastproc = fontprocesses[prevfont]
641                if lastproc then
642                    usedfonts[prevfont] = lastproc
643                    u = u + 1
644                end
645            end
646        end
647    end
648
649    -- local hasglyph = nuts.hasglyph
650
651    function handlers.characters(head,groupcode,direction)
652
653    -- no gain:
654
655    -- local h = hasglyph(head)
656    -- if not h then
657    --     return head
658    -- end
659
660        starttiming(nodes)
661
662        usedfonts    = { }
663        dynamicfonts = { }
664        prevfont     = nil -- local
665        prevdynamic  = 0   -- local
666        variants     = nil -- local
667        redundant    = nil -- local
668        lastfont     = nil
669        lastproc     = nil
670     -- nonedone     = nil
671     -- basedone     = nil
672
673        local nonedone = nil
674        local basedone = nil
675
676        local fontmode = nil -- base none or other
677
678     -- d, u, b, r = 0, 0, 0, 0
679        d, u, r = 0, 0, 0
680
681     -- if trace_fontrun then
682     --     start_trace(head)
683     -- end
684
685        -- There is no gain in checking for a single glyph and then having a fast path. On the
686        -- metafun manual (with some 2500 single char lists) the difference is just noise.
687
688        for n, char, font, dynamic in nextchar, head do
689
690            if font ~= prevfont then
691                prevfont = font
692                fontmode = fontmodes[font]
693                if fontmode == "none" then
694                    prevdynamic = 0
695                    variants    = false
696                 -- setnone()
697                    nonedone = true
698                elseif fontmode == "base" then
699                    prevdynamic = 0
700                    variants    = false
701                 -- setbase()
702                    basedone = true
703                else
704                    prevdynamic = dynamic
705                    variants    = fontvariants[font]
706                 -- setnode(font,dynamic)
707                    setnode()
708                end
709            elseif fontmode == "node" then
710                if dynamic ~= prevdynamic then
711                    prevdynamic = dynamic
712                    variants    = fontvariants[font]
713                 -- setnode(font,dynamic)
714                    setnode()
715                end
716            end
717
718            -- we could just mark them and then have a separate pass .. happens seldom
719
720            if variants then
721                -- We need a proper test for this!
722                if (char >= 0xFE00 and char <= 0xFE0F) or (char >= 0xE0100 and char <= 0xE01EF) then
723                    local hash = variants[char]
724                    if hash then
725                     -- local p, _, char = isprevchar(n)
726                     -- if char then
727                     --     local variant = hash[char]
728                        local p = getprev(n)
729                        if p then
730                            local char    = ischar(p) -- checked
731                            local variant = hash[char]
732                            if variant then
733                                if trace_variants then
734                                    report_fonts("replacing %C by %C",char,variant)
735                                end
736                                setchar(p,variant)
737                                if redundant then
738                                    r = r + 1
739                                    redundant[r] = n
740                                else
741                                    r = 1
742                                    redundant = { n }
743                                end
744                            end
745                        end
746                    elseif keep_redundant then
747                        -- go on, can be used for tracing
748                    elseif redundant then
749                        r = r + 1
750                        redundant[r] = n
751                    else
752                        r = 1
753                        redundant = { n }
754                    end
755                end
756            end
757
758        end
759
760        if force_boundaryrun then
761            -- we can inject wordboundaries and then let the hyphenator do its work
762            -- but we need to get rid of those nodes in order to build ligatures
763            -- and kern (a rather context thing)
764
765         -- for b, subtype in nextboundary, head do
766         --     if subtype == wordboundary_code then
767         --         if redundant then
768         --             r = r + 1
769         --             redundant[r] = b
770         --         else
771         --             r = 1
772         --             redundant = { b }
773         --         end
774         --     end
775         -- end
776
777            head = removefromlist(head,boundary_code,wordboundary_code)
778
779        end
780
781        if redundant then
782            for i=1,r do
783                local r = redundant[i]
784                local p, n = getboth(r)
785                if r == head then
786                    head = n
787                    setprev(n)
788                else
789                    setlink(p,n)
790                end
791                flushnode(r)
792            end
793        end
794
795        -- todo: make this more clever
796
797        if force_discrun then
798            for disc in nextdisc, head do
799                -- doing only replace is good enough because pre and post are normally used
800                -- for hyphens and these come from fonts that part of the hyphenated word
801                local r = getreplace(disc)
802                if r then
803                    prevfont    = nil
804                    prevdynamic = nil
805                 -- fontmode    = nil
806                    for n, char, font, dynamic in nextchar, r do
807                        if font ~= prevfont or dynamic ~= prevdynamic then
808                            prevfont    = font
809                            prevdynamic = dynamic
810                            fontmode    = fontmodes[font]
811                            if fontmode == "none" then
812                             -- setnone()
813                                nonedone = true
814                            elseif fontmode == "base" then
815                             -- setbase()
816                                basedone = true
817                            else
818                                setnode() -- (font,dynamic)
819                            end
820                        end
821                        -- we assume one font for now (and if there are more and we get into issues then
822                        -- we can always remove the break)
823                        break
824                    end
825                end
826            end
827
828        end
829
830     -- if trace_fontrun then
831     --     stop_trace(u,usedfonts,d,dynamicfonts,b,basefonts,r,redundant)
832     -- end
833
834        if nonedone then
835            protectglyphsnone(head)
836        end
837
838        -- in context we always have at least 2 processors
839        if u == 0 then
840            -- skip
841        elseif u == 1 then
842            for i=1,#lastproc do
843                head = lastproc[i](head,lastfont,0,direction)
844            end
845        else
846            for font, processors in next, usedfonts do -- unordered
847                for i=1,#processors do
848                    head = processors[i](head,font,0,direction,u) -- u triggers disc optimizer
849                end
850            end
851        end
852
853        if d == 0 then
854            -- skip
855        elseif d == 1 then
856            local font, dynamics = next(dynamicfonts)
857            for dynamic, processors in next, dynamics do -- unordered, dynamic can switch in between
858                for i=1,#processors do
859                    head = processors[i](head,font,dynamic,direction)
860                end
861            end
862        else
863            for font, dynamics in next, dynamicfonts do
864                for dynamic, processors in next, dynamics do -- unordered, dynamic can switch in between
865                    for i=1,#processors do
866                        head = processors[i](head,font,dynamic,direction,d) -- d triggers disc optimizer
867                    end
868                end
869            end
870        end
871
872        if basedone then
873            local start = head
874            start = ligaturing(start)
875            start = kerning(start)
876            if head ~= start then
877                head = start
878            end
879        end
880
881        stoptiming(nodes)
882
883        if trace_characters then
884            nodes.report(head)
885        end
886
887        return head
888    end
889
890end
891
892handlers.protectglyphs   = protectglyphs
893handlers.unprotectglyphs = unprotectglyphs
894