font-otc.lua /size: 36 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['font-otc'] = {
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
9local insert, sortedkeys, sortedhash, tohash = table.insert, table.sortedkeys, table.sortedhash, table.tohash
10local type, next, tonumber = type, next, tonumber
11local lpegmatch = lpeg.match
12local utfbyte, utflen = utf.byte, utf.len
13local sortedhash = table.sortedhash
14
15-- we assume that the other otf stuff is loaded already
16
17local trace_loading       = false  trackers.register("otf.loading", function(v) trace_loading = v end)
18local report_otf          = logs.reporter("fonts","otf loading")
19
20local fonts               = fonts
21local otf                 = fonts.handlers.otf
22local registerotffeature  = otf.features.register
23local setmetatableindex   = table.setmetatableindex
24
25local fonthelpers         = fonts.helpers
26local checkmerge          = fonthelpers.checkmerge
27local checkflags          = fonthelpers.checkflags
28local checksteps          = fonthelpers.checksteps
29
30local normalized = {
31    substitution      = "substitution",
32    single            = "substitution",
33    ligature          = "ligature",
34    alternate         = "alternate",
35    multiple          = "multiple",
36    kern              = "kern",
37    pair              = "pair",
38    single            = "single",
39    chainsubstitution = "chainsubstitution",
40    chainposition     = "chainposition",
41}
42
43local types = {
44    substitution      = "gsub_single",
45    ligature          = "gsub_ligature",
46    alternate         = "gsub_alternate",
47    multiple          = "gsub_multiple",
48    kern              = "gpos_pair",
49    pair              = "gpos_pair",
50    single            = "gpos_single",
51    chainsubstitution = "gsub_contextchain",
52    chainposition     = "gpos_contextchain",
53}
54
55local names = {
56    gsub_single              = "gsub",
57    gsub_multiple            = "gsub",
58    gsub_alternate           = "gsub",
59    gsub_ligature            = "gsub",
60    gsub_context             = "gsub",
61    gsub_contextchain        = "gsub",
62    gsub_reversecontextchain = "gsub",
63    gpos_single              = "gpos",
64    gpos_pair                = "gpos",
65    gpos_cursive             = "gpos",
66    gpos_mark2base           = "gpos",
67    gpos_mark2ligature       = "gpos",
68    gpos_mark2mark           = "gpos",
69    gpos_context             = "gpos",
70    gpos_contextchain        = "gpos",
71}
72
73setmetatableindex(types, function(t,k) t[k] = k return k end) -- "key"
74
75local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
76local noflags    = { false, false, false, false }
77
78-- beware: shared, maybe we should copy the sequence
79
80local function getrange(sequences,category)
81    local count = #sequences
82    local first = nil
83    local last  = nil
84    for i=1,count do
85        local t = sequences[i].type
86        if t and names[t] == category then
87            if not first then
88                first = i
89            end
90            last  = i
91        end
92    end
93    return first or 1, last or count
94end
95
96local function validspecification(specification,name)
97    local dataset = specification.dataset
98    if dataset then
99        -- okay
100    elseif specification[1] then
101        dataset = specification
102        specification = { dataset = dataset }
103    else
104        dataset = { { data = specification.data } }
105        specification.data     = nil
106        specification.coverage = dataset
107        specification.dataset  = dataset
108    end
109    local first = dataset[1]
110    if first then
111        first = first.data
112    end
113    if not first then
114        report_otf("invalid feature specification, no dataset")
115        return
116    end
117    if type(name) ~= "string" then
118        name = specification.name or first.name
119    end
120    if type(name) ~= "string" then
121        report_otf("invalid feature specification, no name")
122        return
123    end
124    local n = #dataset
125    if n > 0 then
126        for i=1,n do
127            setmetatableindex(dataset[i],specification)
128        end
129        return specification, name
130    end
131end
132
133local function addfeature(data,feature,specifications,prepareonly,filename)
134
135    -- todo: add some validator / check code so that we're more tolerant to
136    -- user errors
137
138    if not specifications then
139        report_otf("missing specification")
140        return
141    end
142
143    local descriptions = data.descriptions
144    local resources    = data.resources
145
146    if not descriptions or not resources then
147        report_otf("missing specification")
148        return
149    end
150
151    local features  = resources.features
152    local sequences = resources.sequences
153
154    if not features or not sequences then
155        report_otf("missing specification")
156        return
157    end
158
159    local alreadydone = resources.alreadydone
160    if not alreadydone then
161        alreadydone = { }
162        resources.alreadydone = alreadydone
163    end
164    if alreadydone[specifications] then
165        return
166    else
167        alreadydone[specifications] = true
168    end
169
170    -- feature has to be unique but the name entry wins eventually
171
172    local fontfeatures = resources.features or everywhere
173    local unicodes     = resources.unicodes
174    local splitter     = lpeg.splitter(" ",unicodes)
175    local done         = 0
176    local skip         = 0
177    local aglunicodes  = false
178    local privateslot  = fonthelpers.privateslot
179
180    local specifications = validspecification(specifications,feature)
181    if not specifications then
182     -- report_otf("invalid specification")
183        return
184    end
185
186    local p = lpeg.P("P")
187            * (lpeg.patterns.hexdigit^1/function(s) return tonumber(s,16) end)
188            * lpeg.P(-1)
189
190    local function tounicode(code)
191        if not code then
192            return
193        end
194        if type(code) == "number" then
195            return code
196        end
197        local u = unicodes[code]
198        if u then
199         -- unicodes[code] = u
200            return u
201        end
202        if utflen(code) == 1 then
203            u = utfbyte(code)
204            if u then
205                return u
206            end
207        end
208        if privateslot then
209            u = privateslot(code) -- no creation !
210            if u then
211             -- unicodes[code] = u
212                return u
213            end
214        end
215        local u = lpegmatch(p,code)
216        if u then
217         -- unicodes[code] = u
218            return u
219        end
220        if not aglunicodes then
221            aglunicodes = fonts.encodings.agl.unicodes -- delayed
222        end
223        local u = aglunicodes[code]
224        if u then
225         -- unicodes[code] = u
226            return u
227        end
228    end
229
230    local coverup      = otf.coverup
231    local coveractions = coverup.actions
232    local stepkey      = coverup.stepkey
233    local register     = coverup.register
234
235    -- todo: directly pass a coverage i.e. for privates that later will be
236    -- set
237
238    local function prepare_substitution(list,featuretype,nocheck)
239        local coverage = { }
240        local cover    = coveractions[featuretype]
241        for code, replacement in next, list do
242            local unicode     = tounicode(code)
243            local description = descriptions[unicode]
244            if not nocheck and not description then
245                -- todo: trace !
246                skip = skip + 1
247            else
248                if type(replacement) == "table" then
249                    replacement = replacement[1]
250                end
251                replacement = tounicode(replacement)
252                if replacement and (nocheck or descriptions[replacement]) then
253                    cover(coverage,unicode,replacement)
254                    done = done + 1
255                else
256                    skip = skip + 1
257                end
258            end
259        end
260        return coverage
261    end
262
263    local function prepare_alternate(list,featuretype,nocheck)
264        local coverage = { }
265        local cover    = coveractions[featuretype]
266        for code, replacement in next, list do
267            local unicode     = tounicode(code)
268            local description = descriptions[unicode]
269            if not nocheck and not description then
270                skip = skip + 1
271            elseif type(replacement) == "table" then
272                local r = { }
273                for i=1,#replacement do
274                    local u = tounicode(replacement[i])
275                    r[i] = (nocheck or descriptions[u]) and u or unicode
276                end
277                cover(coverage,unicode,r)
278                done = done + 1
279            else
280                local u = tounicode(replacement)
281                if u then
282                    cover(coverage,unicode,{ u })
283                    done = done + 1
284                else
285                    skip = skip + 1
286                end
287            end
288        end
289        return coverage
290    end
291
292    local function prepare_multiple(list,featuretype,nocheck)
293        local coverage = { }
294        local cover    = coveractions[featuretype]
295        for code, replacement in next, list do
296            local unicode     = tounicode(code)
297            local description = descriptions[unicode]
298            if not nocheck and not description then
299                skip = skip + 1
300            elseif type(replacement) == "table" then
301                local r = { }
302                local n = 0
303                for i=1,#replacement do
304                    local u = tounicode(replacement[i])
305                    if nocheck or descriptions[u] then
306                        n = n + 1
307                        r[n] = u
308                    end
309                end
310                if n > 0 then
311                    cover(coverage,unicode,r)
312                    done = done + 1
313                else
314                    skip = skip + 1
315                end
316            else
317                local u = tounicode(replacement)
318                if u then
319                    cover(coverage,unicode,{ u })
320                    done = done + 1
321                else
322                    skip = skip + 1
323                end
324            end
325        end
326        return coverage
327    end
328
329    local function prepare_ligature(list,featuretype,nocheck)
330        local coverage = { }
331        local cover    = coveractions[featuretype]
332        for code, ligature in next, list do
333            local unicode     = tounicode(code)
334            local description = descriptions[unicode]
335            if not nocheck and not description then
336                skip = skip + 1
337            else
338                if type(ligature) == "string" then
339                    ligature = { lpegmatch(splitter,ligature) }
340                end
341                local present = true
342                for i=1,#ligature do
343                    local l = ligature[i]
344                    local u = tounicode(l)
345                    if nocheck or descriptions[u] then
346                        ligature[i] = u
347                    else
348                        present = false
349                        break
350                    end
351                end
352                if present then
353                    cover(coverage,unicode,ligature)
354                    done = done + 1
355                else
356                    skip = skip + 1
357                end
358            end
359        end
360        return coverage
361    end
362
363    local function resetspacekerns()
364        -- a bit of a hack, this nil setting but it forces a
365        -- rehash of the resources needed .. the feature itself
366        -- should be a kern (at least for now)
367        data.properties.hasspacekerns = true
368        data.resources .spacekerns    = nil
369    end
370
371    local function prepare_kern(list,featuretype,nocheck)
372        local coverage = { }
373        local cover    = coveractions[featuretype]
374        local isspace  = false
375        for code, replacement in next, list do
376            local unicode     = tounicode(code)
377            local description = descriptions[unicode]
378            if not nocheck and not description then
379                skip = skip + 1
380            elseif type(replacement) == "table" then
381                local r = { }
382                for k, v in next, replacement do
383                    local u = tounicode(k)
384                    if u then
385                        r[u] = v
386                        if u == 32 then
387                            isspace = true
388                        end
389                    end
390                end
391                if next(r) then
392                    cover(coverage,unicode,r)
393                    done = done + 1
394                    if unicode == 32 then
395                        isspace = true
396                    end
397                else
398                    skip = skip + 1
399                end
400            else
401                skip = skip + 1
402            end
403        end
404        if isspace then
405            resetspacekerns()
406        end
407        return coverage
408    end
409
410    local function prepare_pair(list,featuretype,nocheck)
411        local coverage = { }
412        local cover    = coveractions[featuretype]
413        if cover then
414            for code, replacement in next, list do
415                local unicode     = tounicode(code)
416                local description = descriptions[unicode]
417                if not nocheck and not description then
418                    skip = skip + 1
419                elseif type(replacement) == "table" then
420                    local r = { }
421                    for k, v in next, replacement do
422                        local u = tounicode(k)
423                        if u then
424                            r[u] = v
425                            if u == 32 then
426                                isspace = true
427                            end
428                        end
429                    end
430                    if next(r) then
431                        cover(coverage,unicode,r)
432                        done = done + 1
433                        if unicode == 32 then
434                            isspace = true
435                        end
436                    else
437                        skip = skip + 1
438                    end
439                else
440                    skip = skip + 1
441                end
442            end
443            if isspace then
444                resetspacekerns()
445            end
446        else
447            report_otf("unknown cover type %a",featuretype)
448        end
449        return coverage
450    end
451
452    local prepare_single = prepare_pair -- we could have a better test on the spec
453
454    local function hassteps(lookups)
455        if lookups then
456            for i=1,#lookups do
457                local l = lookups[i]
458                if l then
459                    for j=1,#l do
460                        local l = l[j]
461                        if l then
462                            local n = l.nofsteps
463                            if not n then
464                                -- gsub_remove
465                                return true
466                            elseif n > 0 then
467                                return true
468                            end
469                        end
470                    end
471                end
472            end
473        end
474        return false
475    end
476
477    -- 0 == remove, false = ignore (remove is default)
478
479    local function prepare_chain(list,featuretype,sublookups,nocheck)
480        -- todo: coveractions
481        local rules    = list.rules
482        local coverage = { }
483        if rules then
484            local lookuptype = types[featuretype]
485            for nofrules=1,#rules do
486                local rule         = rules[nofrules]
487                local current      = rule.current
488                local before       = rule.before
489                local after        = rule.after
490                local replacements = rule.replacements or false
491                local sequence     = { }
492                local nofsequences = 0
493                if before then
494                    for n=1,#before do
495                        nofsequences = nofsequences + 1
496                        sequence[nofsequences] = before[n]
497                    end
498                end
499                local start = nofsequences + 1
500                for n=1,#current do
501                    nofsequences = nofsequences + 1
502                    sequence[nofsequences] = current[n]
503                end
504                local stop = nofsequences
505                if after then
506                    for n=1,#after do
507                        nofsequences = nofsequences + 1
508                        sequence[nofsequences] = after[n]
509                    end
510                end
511                local lookups = rule.lookups or false
512                local subtype = nil
513                if lookups and sublookups then
514                    -- inspect(lookups)
515                    if #lookups > 0 then
516                        local ns = stop - start + 1
517                        for i=1,ns do
518                            if lookups[i] == nil then
519                                lookups[i] = 0
520                            end
521                        end
522                    end
523                    local l = { }
524                    for k, v in sortedhash(lookups) do
525                        local t = type(v)
526                        if t == "table" then
527                            -- already ok
528                            for i=1,#v do
529                                local vi = v[i]
530                                if type(vi) ~= "table" then
531                                    v[i] = { vi }
532                                end
533                            end
534                            l[k] = v
535                        elseif t == "number" then
536                            local lookup = sublookups[v]
537                            if lookup then
538                                l[k] = { lookup }
539                                if not subtype then
540                                    subtype = lookup.type
541                                end
542                            elseif v == 0 then
543                                l[k] = { { type = "gsub_remove", nosteps = true } }
544                            else
545                                l[k] = false -- { false } -- new
546                            end
547                        else
548                            l[k] = false -- { false } -- new
549                        end
550                    end
551                    if nocheck then
552                        -- fragile
553                        rule.lookups = l --no, because checking can spoil it
554                    end
555                    lookups = l
556                end
557                if nofsequences > 0 then -- we merge coverage into one
558                    -- we copy as we can have different fonts
559                    if hassteps(lookups) then
560                        -- sequence is the before|current|after match list
561                        local hashed = { }
562                        for i=1,nofsequences do
563                            local t = { }
564                            local s = sequence[i]
565                            for i=1,#s do
566                                local u = tounicode(s[i])
567                                if u then
568                                    t[u] = true
569                                end
570                            end
571                            hashed[i] = t
572                        end
573                        -- hashed is the before|current|after match hash
574                        sequence = hashed
575                        local ruleset = {
576                            nofrules,     -- 1
577                            lookuptype,   -- 2
578                            sequence,     -- 3
579                            start,        -- 4
580                            stop,         -- 5
581                            lookups,      -- 6 (6/7 also signal of what to do)
582                            replacements, -- 7
583                            subtype,      -- 8
584                        }
585                        for unic in sortedhash(sequence[start]) do
586                            local cu = coverage[unic]
587                            if cu then
588                                local n = cu.n + 1
589                                cu[n] = ruleset
590                                cu.n = n
591                            else
592                                coverage[unic] = {
593                                    ruleset,
594                                    n = 1,
595                                }
596                            end
597                        end
598                        sequence.n = nofsequences
599                    else
600                     -- report_otf("no steps for %a",lookuptype) -- e.g. in primes feature
601                    end
602                end
603            end
604        end
605        return coverage
606    end
607
608    local dataset = specifications.dataset
609
610    local function report(name,category,position,first,last,sequences)
611        report_otf("injecting name %a of category %a at position %i in [%i,%i] of [%i,%i]",
612            name,category,position,first,last,1,#sequences)
613    end
614
615    local function inject(specification,sequences,sequence,first,last,category,name)
616        local position = specification.position or false
617        if not position then
618            position = specification.prepend
619            if position == true then
620                if trace_loading then
621                    report(name,category,first,first,last,sequences)
622                end
623                insert(sequences,first,sequence)
624                return
625            end
626        end
627        if not position then
628            position = specification.append
629            if position == true then
630                if trace_loading then
631                    report(name,category,last+1,first,last,sequences)
632                end
633                insert(sequences,last+1,sequence)
634                return
635            end
636        end
637        local kind = type(position)
638        if kind == "string" then
639            local index = false
640            for i=first,last do
641                local s = sequences[i]
642                local f = s.features
643                if f then
644                    for k in sortedhash(f) do -- next, f do
645                        if k == position then
646                            index = i
647                            break
648                        end
649                    end
650                    if index then
651                        break
652                    end
653                end
654            end
655            if index then
656                position = index
657            else
658                position = last + 1
659            end
660        elseif kind == "number" then
661            if position < 0 then
662                position = last - position + 1
663            end
664            if position > last then
665                position = last + 1
666            elseif position < first then
667                position = first
668            end
669        else
670            position = last + 1
671        end
672        if trace_loading then
673            report(name,category,position,first,last,sequences)
674        end
675        insert(sequences,position,sequence)
676    end
677
678    for s=1,#dataset do
679        local specification = dataset[s]
680        local valid = specification.valid -- nowhere used
681        local files = specification.files
682        if files and filename then
683            local name = string.lower(file.basename(filename))
684            -- hash test
685            local okay = files[name]
686            -- list test
687            if not okay then
688                for i=1,#files do
689                    if name == files[i] then
690                        okay = true
691                        break
692                    end
693                end
694            end
695            if okay then
696             -- report_otf("feature applied to file %a",name)
697            else
698             -- report_otf("feature skipped for file %a",name)
699                return
700            end
701        end
702        --
703        local feature = specification.name or feature
704        if not feature or feature == "" then
705            report_otf("no valid name given for extra feature")
706        elseif not valid or valid(data,specification,feature) then -- anum uses this
707            local initialize = specification.initialize
708            if initialize then
709                -- when false is returned we initialize only once
710                specification.initialize = initialize(specification,data) and initialize or nil
711            end
712            local askedfeatures = specification.features or everywhere
713            local askedsteps    = specification.steps or specification.subtables or { specification.data } or { }
714            local featuretype   = specification.type or "substitution"
715            local featureaction = false
716            local featureflags  = specification.flags or noflags
717            local nocheck       = specification.nocheck
718            local mapping       = specification.mapping
719            local featureorder  = specification.order or { feature }
720            local featurechain  = (featuretype == "chainsubstitution" or featuretype == "chainposition") and 1 or 0
721            local nofsteps      = 0
722            local steps         = { }
723            local sublookups    = specification.lookups
724            local category      = nil
725            local steptype      = nil
726            local sequence      = nil
727            --
728            if fonts.handlers.otf.handlers[featuretype] then
729                featureaction = true -- function based
730            else
731                featuretype = normalized[specification.type or "substitution"] or "substitution"
732            end
733            --
734            checkflags(specification,resources)
735            --
736            for k, v in next, askedfeatures do
737                if v[1] then
738                    askedfeatures[k] = tohash(v)
739                end
740            end
741            --
742            if featureflags[1] then featureflags[1] = "mark" end
743            if featureflags[2] then featureflags[2] = "ligature" end
744            if featureflags[3] then featureflags[3] = "base" end
745            --
746            if featureaction then
747
748                category = "gsub"
749                sequence = {
750                    features  = { [feature] = askedfeatures },
751                    flags     = featureflags,
752                    name      = feature, -- redundant
753                    order     = featureorder,
754                    type      = featuretype,
755                 -- steps     = { },
756                    nofsteps  = 0, -- just in case we test for that
757                }
758
759            else
760
761                if sublookups then
762                    local s = { }
763                    for i=1,#sublookups do
764                        local specification = sublookups[i]
765                        local askedsteps    = specification.steps or specification.subtables or { specification.data } or { }
766                        local featuretype   = normalized[specification.type or "substitution"] or "substitution"
767                        local featureflags  = specification.flags or noflags
768                        local nofsteps      = 0
769                        local steps         = { }
770                        for i=1,#askedsteps do
771                            local list     = askedsteps[i]
772                            local coverage = nil
773                            local format   = nil
774                            if featuretype == "substitution" then
775                                coverage = prepare_substitution(list,featuretype,nocheck)
776                            elseif featuretype == "ligature" then
777                                coverage = prepare_ligature(list,featuretype,nocheck)
778                            elseif featuretype == "alternate" then
779                                coverage = prepare_alternate(list,featuretype,nocheck)
780                            elseif featuretype == "multiple" then
781                                coverage = prepare_multiple(list,featuretype,nocheck)
782                            elseif featuretype == "kern" or featuretype == "move" then
783                                format   = featuretype
784                                coverage = prepare_kern(list,featuretype,nocheck)
785                            elseif featuretype == "pair" then
786                                format   = "pair"
787                                coverage = prepare_pair(list,featuretype,nocheck)
788                            elseif featuretype == "single" then
789                                format   = "single"
790                                coverage = prepare_single(list,featuretype,nocheck)
791                            end
792                            if coverage and next(coverage) then
793                                nofsteps = nofsteps + 1
794                                steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
795                            end
796                        end
797                        --
798                        checkmerge(specification)
799                        checksteps(specification)
800                        --
801                        s[i] = {
802                            [stepkey] = steps,
803                            nofsteps  = nofsteps,
804                            flags     = featureflags,
805                            type      = types[featuretype],
806                        }
807                    end
808                    sublookups = s
809                end
810
811                for i=1,#askedsteps do
812                    local list     = askedsteps[i]
813                    local coverage = nil
814                    local format   = nil
815                    if type(list) == "function" then
816                        list = list(data,specification,list,i) -- why pass list instead if askedsteps
817                    end
818                    if not list then
819                        -- see ebgaramond hack
820                    elseif featuretype == "substitution" then
821                        -- see font-imp-tweaks: we directly pass a mapping so no checks done
822                        category = "gsub"
823                        coverage = (mapping and list) or prepare_substitution(list,featuretype,nocheck)
824                    elseif featuretype == "ligature" then
825                        category = "gsub"
826                        coverage = prepare_ligature(list,featuretype,nocheck)
827                    elseif featuretype == "alternate" then
828                        category = "gsub"
829                        coverage = prepare_alternate(list,featuretype,nocheck)
830                    elseif featuretype == "multiple" then
831                        category = "gsub"
832                        coverage = prepare_multiple(list,featuretype,nocheck)
833                    elseif featuretype == "kern" or featuretype == "move" then
834                        category = "gpos"
835                        format   = featuretype
836                        coverage = prepare_kern(list,featuretype,nocheck)
837                    elseif featuretype == "pair" then
838                        category = "gpos"
839                        format   = "pair"
840                        coverage = prepare_pair(list,featuretype,nocheck)
841                    elseif featuretype == "single" then
842                        category = "gpos"
843                        format   = "single"
844                        coverage = prepare_single(list,featuretype,nocheck)
845                    elseif featuretype == "chainsubstitution" then
846                        category = "gsub"
847                        coverage = prepare_chain(list,featuretype,sublookups,nocheck)
848                    elseif featuretype == "chainposition" then
849                        category = "gpos"
850                        coverage = prepare_chain(list,featuretype,sublookups,nocheck)
851                    else
852                        report_otf("not registering feature %a, unknown category",feature)
853                        return
854                    end
855                    if coverage and next(coverage) then
856                        nofsteps = nofsteps + 1
857                        steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
858                    end
859                end
860
861                if nofsteps > 0 then
862                    sequence = {
863                        chain     = featurechain,
864                        features  = { [feature] = askedfeatures },
865                        flags     = featureflags,
866                        name      = feature, -- redundant
867                        order     = featureorder,
868                        [stepkey] = steps,
869                        nofsteps  = nofsteps,
870                        type      = specification.handler or types[featuretype],
871                    }
872                    if prepareonly then
873                        return sequence
874                    end
875                end
876            end
877
878            if sequence then
879                -- script = { lang1, lang2, lang3 } or script = { lang1 = true, ... }
880                checkflags(sequence,resources)
881                checkmerge(sequence)
882                checksteps(sequence)
883                -- position | prepend | append
884                local first, last = getrange(sequences,category)
885                inject(specification,sequences,sequence,first,last,category,feature)
886                -- register in metadata (merge as there can be a few)
887                local features = fontfeatures[category]
888                if not features then
889                    features  = { }
890                    fontfeatures[category] = features
891                end
892                local k = features[feature]
893                if not k then
894                    k = { }
895                    features[feature] = k
896                end
897                --
898                for script, languages in next, askedfeatures do
899                    local kk = k[script]
900                    if not kk then
901                        kk = { }
902                        k[script] = kk
903                    end
904                    for language, value in next, languages do
905                        kk[language] = value
906                    end
907                end
908            end
909
910        end
911    end
912    if trace_loading then
913        report_otf("registering feature %a, affected glyphs %a, skipped glyphs %a",feature,done,skip)
914    end
915
916end
917
918otf.enhancers.addfeature = addfeature
919
920local extrafeatures = { }
921local knownfeatures = { }
922
923function otf.addfeature(name,specification)
924    if type(name) == "table" then
925        specification = name
926    end
927    if type(specification) ~= "table" then
928        report_otf("invalid feature specification, no valid table")
929        return
930    end
931    specification, name = validspecification(specification,name)
932    if name and specification then
933        local slot = knownfeatures[name]
934        if not slot then
935            -- we have a new one
936            slot = #extrafeatures + 1
937            knownfeatures[name] = slot
938        elseif specification.overload == false then
939            -- we add an extre one
940            slot = #extrafeatures + 1
941            knownfeatures[name] = slot
942        else
943            -- we overload a previous one
944        end
945        specification.name  = name -- to be sure
946        extrafeatures[slot] = specification
947     -- report_otf("adding feature %a @ %i",name,slot)
948    end
949end
950
951-- for feature, specification in next, extrafeatures do
952--     addfeature(data,feature,specification)
953-- end
954
955local function enhance(data,filename,raw)
956    for slot=1,#extrafeatures do
957        local specification = extrafeatures[slot]
958        addfeature(data,specification.name,specification,nil,filename)
959    end
960end
961
962-- local function enhance(data,filename,raw)
963--     local first = 1
964--     local last  = #extrafeatures
965--     while true do
966--         for slot=first,last do
967--             local specification = extrafeatures[slot]
968--             addfeature(data,specification.name,specification)
969--         end
970--         if #extrafeatures > last then
971--             first = last + 1
972--             last  = #extrafeatures
973--         else
974--             break
975--         end
976--     end
977-- end
978
979otf.enhancers.enhance = enhance
980
981otf.enhancers.register("check extra features",enhance)
982
983-- fonts.handlers.otf.features.register {
984--     name        = 'hangulfix',
985--     description = 'fixes for hangul',
986-- }
987
988-- fonts.handlers.otf.addfeature {
989--     name = "stest",
990--     type = "substitution",
991--     data = {
992--         a = "X",
993--         b = "P",
994--     }
995-- }
996-- fonts.handlers.otf.addfeature {
997--     name = "atest",
998--     type = "alternate",
999--     data = {
1000--         a = { "X", "Y" },
1001--         b = { "P", "Q" },
1002--     }
1003-- }
1004-- fonts.handlers.otf.addfeature {
1005--     name = "mtest",
1006--     type = "multiple",
1007--     data = {
1008--         a = { "X", "Y" },
1009--         b = { "P", "Q" },
1010--     }
1011-- }
1012-- fonts.handlers.otf.addfeature {
1013--     name = "ltest",
1014--     type = "ligature",
1015--     data = {
1016--         X = { "a", "b" },
1017--         Y = { "d", "a" },
1018--     }
1019-- }
1020-- fonts.handlers.otf.addfeature {
1021--     name = "ktest",
1022--     type = "kern",
1023--     data = {
1024--         a = { b = -500 },
1025--     }
1026-- }
1027