font-sol.lua /size: 32 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['font-sol'] = { -- this was: node-spl
2    version   = 1.001,
3    comment   = "companion to font-sol.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
9-- We can speed this up.
10
11-- This module is dedicated to the oriental tex project and for
12-- the moment is too experimental to be publicly supported.
13--
14-- We could cache solutions: say that we store the featureset and
15-- all 'words' -> replacement ... so we create a large solution
16-- database (per font)
17--
18-- This module can be optimized by using a dedicated dynamics handler
19-- but I'll only do that when the rest of the code is stable.
20--
21-- Todo: bind setups to paragraph.
22
23local gmatch, concat, format, remove = string.gmatch, table.concat, string.format, table.remove
24local next, tostring, tonumber = next, tostring, tonumber
25local insert, remove = table.insert, table.remove
26local getrandom = utilities.randomizer.get
27
28local utilities, logs, statistics, fonts, trackers = utilities, logs, statistics, fonts, trackers
29local interfaces, commands, attributes = interfaces, commands, attributes
30local nodes, node, tex = nodes, node, tex
31
32local trace_split        = false  trackers.register("builders.paragraphs.solutions.splitters.splitter",  function(v) trace_split    = v end)
33local trace_optimize     = false  trackers.register("builders.paragraphs.solutions.splitters.optimizer", function(v) trace_optimize = v end)
34local trace_colors       = false  trackers.register("builders.paragraphs.solutions.splitters.colors",    function(v) trace_colors   = v end)
35local trace_goodies      = false  trackers.register("fonts.goodies",                                     function(v) trace_goodies  = v end)
36
37local report_solutions   = logs.reporter("fonts","solutions")
38local report_splitters   = logs.reporter("fonts","splitters")
39local report_optimizers  = logs.reporter("fonts","optimizers")
40
41local variables          = interfaces.variables
42
43local v_normal           = variables.normal
44local v_reverse          = variables.reverse
45local v_preroll          = variables.preroll
46local v_random           = variables.random
47local v_split            = variables.split
48
49local implement          = interfaces.implement
50
51local settings_to_array  = utilities.parsers.settings_to_array
52local settings_to_hash   = utilities.parsers.settings_to_hash
53
54local tasks              = nodes.tasks
55
56local nuts               = nodes.nuts
57
58local getfield           = nuts.getfield
59local getnext            = nuts.getnext
60local getprev            = nuts.getprev
61local getid              = nuts.getid
62local getattr            = nuts.getattr
63local getfont            = nuts.getfont
64local getsubtype         = nuts.getsubtype
65local getlist            = nuts.getlist
66local getdirection       = nuts.getdirection
67local getwidth           = nuts.getwidth
68local getdata            = nuts.getdata
69
70local getboxglue         = nuts.getboxglue
71
72local setattr            = nuts.setattr
73local setlink            = nuts.setlink
74local setnext            = nuts.setnext
75local setlist            = nuts.setlist
76
77local find_node_tail     = nuts.tail
78local flushnode          = nuts.flushnode
79local flushnodelist      = nuts.flushlist
80local copy_node_list     = nuts.copylist
81local hpack_nodes        = nuts.hpack
82local insertnodebefore   = nuts.insertbefore
83local insertnodeafter    = nuts.insertafter
84local protectglyphs      = nuts.protectglyphs
85local startofpar         = nuts.startofpar
86
87local nextnode           = nuts.traversers.next
88local nexthlist          = nuts.traversers.hlist
89local nextwhatsit        = nuts.traversers.whatsit
90
91local repack_hlist       = nuts.repackhlist
92
93local nodes_to_utf       = nodes.listtoutf
94
95----- protectglyphs      = nodes.handlers.protectglyphs
96
97local setnodecolor       = nodes.tracers.colors.set
98
99local nodecodes          = nodes.nodecodes
100local whatsitcodes       = nodes.whatsitcodes
101local kerncodes          = nodes.kerncodes
102
103local glyph_code         = nodecodes.glyph
104local disc_code          = nodecodes.disc
105local kern_code          = nodecodes.kern
106local hlist_code         = nodecodes.hlist
107local dir_code           = nodecodes.dir
108local par_code           = nodecodes.par
109
110local whatsit_code       = nodecodes.whatsit
111
112local fontkern_code      = kerncodes.fontkern
113
114local righttoleft_code   = (tex.directioncodes and tex.directioncodes.righttoleft) or nodes.dirvalues.righttoleft -- LMTX
115
116local userdefinedwhatsit_code = whatsitcodes.userdefined
117
118local nodeproperties     = nodes.properties.data
119
120local nodepool           = nuts.pool
121local usernodeids        = nodepool.userids
122
123local new_direction      = nodepool.direction
124local new_usernode       = nodepool.usernode
125local new_glue           = nodepool.glue
126local new_leftskip       = nodepool.leftskip
127
128local starttiming        = statistics.starttiming
129local stoptiming         = statistics.stoptiming
130----- process_characters = nodes.handlers.characters
131local inject_kerns       = nodes.injections.handler
132
133local fonthashes         = fonts.hashes
134local setfontdynamics    = fonthashes.setdynamics
135
136local texsetattribute    = tex.setattribute
137local unsetvalue         = attributes.unsetvalue
138
139local parbuilders        = builders.paragraphs
140parbuilders.solutions    = parbuilders.solutions or { }
141local parsolutions       = parbuilders.solutions
142parsolutions.splitters   = parsolutions.splitters or { }
143local splitters          = parsolutions.splitters
144
145local solutions          = { } -- attribute sets
146local registered         = { } -- backmapping
147splitters.registered     = registered
148
149local a_split            = attributes.private('splitter')
150
151local preroll            = true
152local criterium          = 0
153local randomseed         = nil
154local optimize           = nil -- set later
155local variant            = v_normal
156local splitwords         = true
157
158local cache              = { }
159local variants           = { }
160local max_less           = 0
161local max_more           = 0
162
163local stack              = { }
164
165local dummy              = {
166    attribute  = unsetvalue,
167    randomseed = 0,
168    criterium  = 0,
169    preroll    = false,
170    optimize   = nil,
171    splitwords = false,
172    variant    = v_normal,
173}
174
175local function checksettings(r,settings)
176    local s = r.settings
177    local method = settings_to_array(settings.method or "")
178    local optimize, preroll, splitwords
179    for i=1,#method do
180        local k = method[i]
181        if k == v_preroll then
182            preroll = true
183        elseif k == v_split then
184            splitwords = true
185        elseif variants[k] then
186            variant = k
187            optimize = variants[k] -- last one wins
188        end
189    end
190    r.randomseed = tonumber(settings.randomseed) or s.randomseed or r.randomseed or 0
191    r.criterium  = tonumber(settings.criterium ) or s.criterium  or r.criterium  or 0
192    r.preroll    = preroll or false
193    r.splitwords = splitwords or false
194    r.optimize   = optimize or s.optimize or r.optimize or variants[v_normal]
195end
196
197local function pushsplitter(name,settings)
198    local r = name and registered[name]
199    if r then
200        if settings then
201            checksettings(r,settings)
202        end
203    else
204        r = dummy
205    end
206    insert(stack,r)
207    -- brr
208    randomseed = r.randomseed or 0
209    criterium  = r.criterium  or 0
210    preroll    = r.preroll    or false
211    optimize   = r.optimize   or nil
212    splitwords = r.splitwords or nil
213    --
214    texsetattribute(a_split,r.attribute)
215    return #stack
216end
217
218local function popsplitter()
219    remove(stack)
220    local n = #stack
221    local r = stack[n] or dummy
222    --
223    randomseed = r.randomseed or 0
224    criterium  = r.criterium  or 0
225    preroll    = r.preroll    or false
226    optimize   = r.optimize   or nil
227    --
228    texsetattribute(a_split,r.attribute)
229    return n
230end
231
232local contextsetups = fonts.specifiers.contextsetups
233
234local function convert(featuresets,name,list)
235    if list then
236        local numbers = { }
237        local nofnumbers = 0
238        for i=1,#list do
239            local feature = list[i]
240            local fs = featuresets[feature]
241            local fn = fs and fs.number
242            if not fn then
243                -- fall back on global features
244                fs = contextsetups[feature]
245                fn = fs and fs.number
246            end
247            if fn then
248                nofnumbers = nofnumbers + 1
249                numbers[nofnumbers] = fn
250                if trace_goodies or trace_optimize then
251                    report_solutions("solution %a of %a uses feature %a with number %s",i,name,feature,fn)
252                end
253            else
254                report_solutions("solution %a of %a has an invalid feature reference %a",i,name,feature)
255            end
256        end
257        return nofnumbers > 0 and numbers
258    end
259end
260
261local function initialize(goodies)
262    local solutions = goodies.solutions
263    if solutions then
264        local featuresets = goodies.featuresets
265        local goodiesname = goodies.name
266        if trace_goodies or trace_optimize then
267            report_solutions("checking solutions in %a",goodiesname)
268        end
269        for name, set in next, solutions do
270            set.less = convert(featuresets,name,set.less)
271            set.more = convert(featuresets,name,set.more)
272        end
273    end
274end
275
276fonts.goodies.register("solutions",initialize)
277
278function splitters.define(name,settings)
279    local goodies  = settings.goodies
280    local solution = settings.solution
281    local less     = settings.less
282    local more     = settings.more
283    local less_set, more_set
284    local l = less and settings_to_array(less)
285    local m = more and settings_to_array(more)
286    if goodies then
287        goodies = fonts.goodies.load(goodies) -- also in tfmdata
288        if goodies then
289            local featuresets = goodies.featuresets
290            local solution = solution and goodies.solutions[solution]
291            if l and #l > 0 then
292                less_set = convert(featuresets,name,less) -- take from settings
293            else
294                less_set = solution and solution.less -- take from goodies
295            end
296            if m and #m > 0 then
297                more_set = convert(featuresets,name,more) -- take from settings
298            else
299                more_set = solution and solution.more -- take from goodies
300            end
301        end
302    else
303        if l then
304            local n = #less_set
305            for i=1,#l do
306                local ss = contextsetups[l[i]]
307                if ss then
308                    n = n + 1
309                    less_set[n] = ss.number
310                end
311            end
312        end
313        if m then
314            local n = #more_set
315            for i=1,#m do
316                local ss = contextsetups[m[i]]
317                if ss then
318                    n = n + 1
319                    more_set[n] = ss.number
320                end
321            end
322        end
323    end
324    if trace_optimize then
325        report_solutions("defining solutions %a, less %a, more %a",name,concat(less_set or {}," "),concat(more_set or {}," "))
326    end
327    local nofsolutions = #solutions + 1
328    local t = {
329        solution  = solution,
330        less      = less_set or { },
331        more      = more_set or { },
332        settings  = settings, -- for tracing
333        attribute = nofsolutions,
334    }
335    solutions[nofsolutions] = t
336    registered[name] = t
337    return nofsolutions
338end
339
340local nofwords, noftries, nofadapted, nofkept, nofparagraphs = 0, 0, 0, 0, 0
341
342local splitter_one = usernodeids["splitters.one"]
343local splitter_two = usernodeids["splitters.two"]
344
345local a_word       = attributes.private('word')
346
347local encapsulate  = false
348
349directives.register("builders.paragraphs.solutions.splitters.encapsulate", function(v)
350    encapsulate = v
351end)
352
353function splitters.split(head) -- best also pass the direction
354    local current   = head
355    local r2l       = false
356    local start     = nil
357    local stop      = nil
358    local attribute = 0
359    cache           = { }
360    max_less        = 0
361    max_more        = 0
362    local function flush() -- we can move this
363        local font = getfont(start)
364        local last = getnext(stop)
365        local list = last and copy_node_list(start,last) or copy_node_list(start)
366        local n    = #cache + 1
367        if encapsulate then
368            local user_one = new_usernode(splitter_one,n)
369            local user_two = new_usernode(splitter_two,n)
370            head, start = insertnodebefore(head,start,user_one)
371            insertnodeafter(head,stop,user_two)
372        else
373            local current = start
374            while true do
375                setattr(current,a_word,n)
376                if current == stop then
377                    break
378                else
379                    current = getnext(current)
380                end
381            end
382        end
383        if r2l then
384            local dirnode = new_direction(righttoleft_code) -- brrr, we don't pop ... to be done (when used at all)
385            setlink(dirnode,list)
386            list = dirnode
387        end
388        local c = {
389            original  = list,
390            attribute = attribute,
391         -- direction = rlmode,
392            font      = font
393        }
394        if trace_split then
395            report_splitters("cached %4i: font %a, attribute %a, direction %a, word %a",
396                n, font, attribute, nodes_to_utf(list,true), r2l and "r2l" or "l2r")
397        end
398        cache[n] = c
399        local solution = solutions[attribute]
400        local l = #solution.less
401        local m = #solution.more
402        if l > max_less then max_less = l end
403        if m > max_more then max_more = m end
404        start, stop = nil, nil
405    end
406    while current do -- also ischar
407        local next = getnext(current)
408        local id = getid(current)
409        if id == glyph_code then
410            if getsubtype(current) < 256 then
411                local a = getattr(current,a_split)
412                if not a then
413                    start, stop = nil, nil
414                elseif not start then
415                    start, stop, attribute = current, current, a
416                elseif a ~= attribute then
417                    start, stop = nil, nil
418                else
419                    stop = current
420                end
421            end
422        elseif id == disc_code then
423            if splitwords then
424                if start then
425                    flush()
426                end
427            elseif start and next and getid(next) == glyph_code and getsubtype(next) < 256 then
428                -- beware: we can cross future lines
429                stop = next
430            else
431                start, stop = nil, nil
432            end
433        elseif id == dir_code then
434            -- not tested (to be done by idris when font is ready)
435            if start then
436                flush()
437            end
438            local direction, pop = getdirection(current)
439            r2l = not pop and direction == righttoleft_code
440        elseif id == par_code and startofpar(current) then
441            if start then
442                flush() -- very unlikely as this starts a paragraph
443            end
444            local direction = getdirection(current)
445            r2l = direction == righttoleft_code
446        else
447            if start then
448                flush()
449            end
450        end
451        current = next
452    end
453    if start then
454        flush()
455    end
456    nofparagraphs = nofparagraphs + 1
457    nofwords      = nofwords + #cache
458    return head
459end
460
461local function collect_words(list) -- can be made faster for attributes
462    local words = { }
463    local w     = 0
464    local word  = nil
465    if encapsulate then
466        for current, subtype in nextwhatsit, list do
467            if subtype == userdefinedwhatsit_code then -- hm
468                local p = nodeproperties[current]
469                if p then
470                    local user_id = p.id
471                    if user_id == splitter_one then
472                        word = { p.data, current, current }
473                        w = w + 1
474                        words[w] = word
475                    elseif user_id == splitter_two then
476                        if word then
477                            word[3] = current
478                        else
479                            -- something is wrong
480                        end
481                    end
482                end
483            end
484        end
485    else
486        local first, last, index
487        local current = list
488        while current do
489            -- todo: disc and kern
490            local id = getid(current)
491            if id == glyph_code or id == disc_code then
492                local a = getattr(current,a_word)
493                if a then
494                    if a == index then
495                        -- same word
496                        last = current
497                    elseif index then
498                        w = w + 1
499                        words[w] = { index, first, last }
500                        first = current
501                        last  = current
502                        index = a
503                    elseif first then
504                        last  = current
505                        index = a
506                    else
507                        first = current
508                        last  = current
509                        index = a
510                    end
511                elseif index then
512                    if first then
513                        w = w + 1
514                        words[w] = { index, first, last }
515                    end
516                    index = nil
517                    first = nil
518                elseif trace_split then
519                    if id == disc_code then
520                        report_splitters("skipped: disc node")
521                    else
522                        report_splitters("skipped: %C",current.char)
523                    end
524                end
525            elseif id == kern_code and getsubtype(current) == fontkern_code then
526                if first then
527                    last = current
528                else
529                    first = current
530                    last = current
531                end
532            elseif index then
533                w = w + 1
534                words[w] = { index, first, last }
535                index = nil
536                first = nil
537                if id == disc_code then
538                    if trace_split then
539                        report_splitters("skipped: disc node")
540                    end
541                end
542            end
543            current = getnext(current)
544        end
545        if index then
546            w = w + 1
547            words[w] = { index, first, last }
548        end
549        if trace_split then
550            for i=1,#words do
551                local w = words[i]
552                local n = w[1]
553                local f = w[2]
554                local l = w[3]
555                local c = cache[n]
556                if c then
557                    report_splitters("found %4i: word %a, cached %a",n,nodes_to_utf(f,true,true,l),nodes_to_utf(c.original,true))
558                else
559                    report_splitters("found %4i: word %a, not in cache",n,nodes_to_utf(f,true,true,l))
560                end
561            end
562        end
563    end
564    return words, list  -- check for empty (elsewhere)
565end
566
567-- we could avoid a hpack but hpack is not that slow
568
569local function doit(word,list,best,width,badness,line,set,listdir)
570    local changed = 0
571    local n = word[1]
572    local found = cache[n]
573    if found then
574        local h, t
575        if encapsulate then
576            h = getnext(word[2]) -- head of current word
577            t = getprev(word[3]) -- tail of current word
578        else
579            h = word[2]
580            t = word[3]
581        end
582        if splitwords then
583            -- there are no lines crossed in a word
584        else
585            local ok = false
586            local c = h
587            while c do
588                if c == t then
589                    ok = true
590                    break
591                else
592                    c = getnext(c)
593                end
594            end
595            if not ok then
596                report_solutions("skipping hyphenated word (for now)")
597                -- todo: mark in words as skipped, saves a bit runtime
598                return false, changed
599            end
600        end
601        local original  = found.original
602        local attribute = found.attribute
603        local solution  = solutions[attribute]
604        local features  = solution and solution[set]
605        if features then
606            local featurenumber = features[best] -- not ok probably
607            if featurenumber then
608                noftries = noftries + 1
609                local first = copy_node_list(original)
610                if not trace_colors then
611                    for n in nextnode, first do -- maybe fast force so no attr needed
612                        setattr(n,0,featurenumber) -- this forces dynamics
613                    end
614                elseif set == "less" then
615                    for n in nextnode, first do
616                        setnodecolor(n,"font:isol") -- yellow
617                        setattr(n,0,featurenumber)
618                    end
619                else
620                    for n in nextnode, first do
621                        setnodecolor(n,"font:medi") -- green
622                        setattr(n,0,featurenumber)
623                    end
624                end
625                local font = found.font
626                local setdynamics = setfontdynamics[font]
627                if setdynamics then
628                    local processes = setdynamics[featurenumber]
629                    for i=1,#processes do -- often more than 1
630                        first = processes[i](first,font,featurenumber)
631                    end
632                else
633                    report_solutions("fatal error, no dynamics for font %a",font)
634                end
635                first = inject_kerns(first)
636                if getid(first) == whatsit_code then
637                    local temp = first
638                    first = getnext(first)
639                    flushnode(temp)
640                end
641                local last = find_node_tail(first)
642                -- replace [u]h->t by [u]first->last
643                local prev = getprev(h)
644                local next = getnext(t)
645                setlink(prev,first)
646                if next then
647                    setlink(last,next)
648                end
649                -- check new pack
650                local temp, b = repack_hlist(list,width,'exactly',listdir)
651                if b > badness then
652                    if trace_optimize then
653                        report_optimizers("line %a, set %a, badness before %a, after %a, criterium %a, verdict %a",line,set or "?",badness,b,criterium,"quit")
654                    end
655                    -- remove last insert
656                    setlink(prev,h)
657                    if next then
658                        setlink(t,next)
659                    else
660                        setnext(t)
661                    end
662                    setnext(last)
663                    flushnodelist(first)
664                else
665                    if trace_optimize then
666                        report_optimizers("line %a, set %a, badness before: %a, after %a, criterium %a, verdict %a",line,set or "?",badness,b,criterium,"continue")
667                    end
668                    -- free old h->t
669                    setnext(t)
670                    flushnodelist(h) -- somehow fails
671                    if not encapsulate then
672                        word[2] = first
673                        word[3] = last
674                    end
675                    changed, badness = changed + 1, b
676                end
677                if b <= criterium then
678                    return true, changed
679                end
680            end
681        end
682    end
683    return false, changed
684end
685
686-- We repeat some code but adding yet another layer of indirectness is not
687-- making things better.
688
689variants[v_normal] = function(words,list,best,width,badness,line,set,listdir)
690    local changed = 0
691    for i=1,#words do
692        local done, c = doit(words[i],list,best,width,badness,line,set,listdir)
693        changed = changed + c
694        if done then
695            break
696        end
697    end
698    if changed > 0 then
699        nofadapted = nofadapted + 1
700        -- todo: get rid of pack when ok because we already have packed and we only need the last b
701        local list, b = repack_hlist(list,width,'exactly',listdir)
702        return list, true, changed, b -- badness
703    else
704        nofkept = nofkept + 1
705        return list, false, 0, badness
706    end
707end
708
709variants[v_reverse] = function(words,list,best,width,badness,line,set,listdir)
710    local changed = 0
711    for i=#words,1,-1 do
712        local done, c = doit(words[i],list,best,width,badness,line,set,listdir)
713        changed = changed + c
714        if done then
715            break
716        end
717    end
718    if changed > 0 then
719        nofadapted = nofadapted + 1
720        -- todo: get rid of pack when ok because we already have packed and we only need the last b
721        local list, b = repack_hlist(list,width,'exactly',listdir)
722        return list, true, changed, b -- badness
723    else
724        nofkept = nofkept + 1
725        return list, false, 0, badness
726    end
727end
728
729variants[v_random] = function(words,list,best,width,badness,line,set,listdir)
730    local changed = 0
731    while #words > 0 do
732        local done, c = doit(remove(words,getrandom("solution",1,#words)),list,best,width,badness,line,set,listdir)
733        changed = changed + c
734        if done then
735            break
736        end
737    end
738    if changed > 0 then
739        nofadapted = nofadapted + 1
740        -- todo: get rid of pack when ok because we already have packed and we only need the last b
741        local list, b = repack_hlist(list,width,'exactly',listdir)
742        return list, true, changed, b -- badness
743    else
744        nofkept = nofkept + 1
745        return list, false, 0, badness
746    end
747end
748
749local function show_quality(current,what,line)
750    local set, order, sign = getboxglue(current)
751    local amount = set * ((sign == 2 and -1) or 1)
752    report_optimizers("line %a, category %a, amount %a, set %a, sign %a, how %a, order %a",line,what,amount,set,sign,how,order)
753end
754
755function splitters.optimize(head)
756    if not optimize then
757        report_optimizers("no optimizer set")
758        return
759    end
760    local nc = #cache
761    if nc == 0 then
762        return
763    end
764    starttiming(splitters)
765    local listdir = nil -- todo ! ! !
766    if randomseed then
767        math.setrandomseedi(randomseed)
768        randomseed = nil
769    end
770    local line         = 0
771    local tex_hbadness = tex.hbadness
772    local tex_hfuzz    = tex.hfuzz
773    tex.hbadness       = 10000
774    tex.hfuzz          = number.maxdimen
775    if trace_optimize then
776        report_optimizers("preroll %a, variant %a, criterium %a, cache size %a",preroll,variant,criterium,nc)
777    end
778    for current in nexthlist, head do
779        line = line + 1
780        local sign      = getfield(current,"glue_sign")
781        local direction = getdirection(current)
782        local width     = getwidth(current)
783        local list      = getlist(current)
784        if not encapsulate and getid(list) == glyph_code then
785            -- nasty .. we always assume a prev being there .. future luatex will always have a leftskip set
786            -- is this assignment ok ? .. needs checking
787            list = insertnodebefore(list,list,new_leftskip(0)) -- new_glue(0)
788            setlist(current,list)
789        end
790        local temp, badness = repack_hlist(list,width,"exactly",direction) -- it would be nice if the badness was stored in the node
791        if badness > 0 then
792            if sign == 0 then
793                if trace_optimize then
794                    report_optimizers("line %a, badness %a, outcome %a, verdict %a",line,badness,"okay","okay")
795                end
796            else
797                local set, max
798                if sign == 1 then
799                    if trace_optimize then
800                        report_optimizers("line %a, badness %a, outcome %a, verdict %a",line,badness,"underfull","trying more")
801                    end
802                    set, max = "more", max_more
803                else
804                    if trace_optimize then
805                        report_optimizers("line %a, badness %a, outcome %a, verdict %a",line,badness,"overfull","trying less")
806                    end
807                    set, max = "less", max_less
808                end
809                -- we can keep the best variants
810                local lastbest    = nil
811                local lastbadness = badness
812                if preroll then
813                    local bb, base
814                    for i=1,max do
815                        if base then
816                            flushnodelist(base)
817                        end
818                        base = copy_node_list(list)
819                        local words = collect_words(base) -- beware: words is adapted
820                        for j=i,max do
821                            local temp, done, changes, b = optimize(words,base,j,width,badness,line,set,dir)
822                            base = temp
823                            if trace_optimize then
824                                report_optimizers("line %a, alternative %a.%a, changes %a, badness %a",line,i,j,changes,b)
825                            end
826                            bb = b
827                            if b <= criterium then
828                                break
829                            end
830                         -- if done then
831                         --     break
832                         -- end
833                        end
834                        if bb and bb > criterium then -- needs checking
835                            if not lastbest then
836                                lastbest, lastbadness = i, bb
837                            elseif bb > lastbadness then
838                                lastbest, lastbadness = i, bb
839                            end
840                        else
841                            break
842                        end
843                    end
844                    flushnodelist(base)
845                end
846                local words = collect_words(list)
847                for best=lastbest or 1,max do
848                    local temp, done, changes, b = optimize(words,list,best,width,badness,line,set,dir)
849                    setlist(current,temp)
850                    if trace_optimize then
851                        report_optimizers("line %a, alternative %a, changes %a, badness %a",line,best,changes,b)
852                    end
853                    if done then
854                        if b <= criterium then -- was == 0
855                            protectglyphs(list)
856                            break
857                        end
858                    end
859                end
860            end
861        else
862            if trace_optimize then
863                report_optimizers("line %a, verdict %a",line,"not bad enough")
864            end
865        end
866        -- we pack inside the outer hpack and that way keep the original wd/ht/dp as bonus
867        local list = hpack_nodes(getlist(current),width,'exactly',listdir)
868        setlist(current,list)
869    end
870    for i=1,nc do
871        local ci = cache[i]
872        flushnodelist(ci.original)
873    end
874    cache = { }
875    tex.hbadness = tex_hbadness
876    tex.hfuzz    = tex_hfuzz
877    stoptiming(splitters)
878end
879
880statistics.register("optimizer statistics", function()
881    if nofwords > 0 then
882        local elapsed = statistics.elapsedtime(splitters)
883        local average = noftries/elapsed
884        return format("%s words identified in %s paragraphs, %s words retried, %s lines tried, %s seconds used, %s adapted, %0.1f lines per second",
885            nofwords,nofparagraphs,noftries,nofadapted+nofkept,elapsed,nofadapted,average)
886    end
887end)
888
889-- we could use a stack
890
891local enableaction  = tasks.enableaction
892local disableaction = tasks.disableaction
893
894local function enable()
895    enableaction("processors", "builders.paragraphs.solutions.splitters.split")
896    enableaction("finalizers", "builders.paragraphs.solutions.splitters.optimize")
897end
898
899local function disable()
900    disableaction("processors", "builders.paragraphs.solutions.splitters.split")
901    disableaction("finalizers", "builders.paragraphs.solutions.splitters.optimize")
902end
903
904function splitters.start(name,settings)
905    if pushsplitter(name,settings) == 1 then
906        enable()
907    end
908end
909
910function splitters.stop()
911    if popsplitter() == 0 then
912        disable()
913    end
914end
915
916function splitters.set(name,settings)
917    if #stack > 0 then
918        stack = { }
919    else
920        enable()
921    end
922    pushsplitter(name,settings) -- sets attribute etc
923end
924
925function splitters.reset()
926    if #stack > 0 then
927        stack = { }
928        popsplitter() -- resets attribute etc
929        disable()
930    end
931end
932
933-- interface
934
935implement {
936    name      = "definefontsolution",
937    actions   = splitters.define,
938    arguments = {
939        "string",
940        {
941            { "goodies" },
942            { "solution" },
943            { "less" },
944            { "more" },
945        }
946    }
947}
948
949implement {
950    name      = "startfontsolution",
951    actions   = splitters.start,
952    arguments = {
953        "string",
954        {
955            { "method" },
956            { "criterium" },
957            { "randomseed" },
958        }
959    }
960}
961
962implement {
963    name      = "stopfontsolution",
964    actions   = splitters.stop
965}
966
967implement {
968    name      = "setfontsolution",
969    actions   = splitters.set,
970    arguments = {
971        "string",
972        {
973            { "method" },
974            { "criterium" },
975            { "randomseed" },
976        }
977    }
978}
979
980implement {
981    name      = "resetfontsolution",
982    actions   = splitters.reset
983}
984