if not modules then modules = { } end modules ['font-otc'] = {
    version   = 1.001,
    comment   = "companion to font-ini.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

local insert, sortedkeys, sortedhash, tohash = table.insert, table.sortedkeys, table.sortedhash, table.tohash
local type, next, tonumber = type, next, tonumber
local lpegmatch = lpeg.match
local utfbyte, utflen = utf.byte, utf.len
local sortedhash = table.sortedhash

-- we assume that the other otf stuff is loaded already

local trace_loading       = false  trackers.register("otf.loading", function(v) trace_loading = v end)
local report_otf          = logs.reporter("fonts","otf loading")

local fonts               = fonts
local otf                 = fonts.handlers.otf
local registerotffeature  = otf.features.register
local setmetatableindex   = table.setmetatableindex

local fonthelpers         = fonts.helpers
local checkmerge          = fonthelpers.checkmerge
local checkflags          = fonthelpers.checkflags
local checksteps          = fonthelpers.checksteps

local normalized = {
    substitution      = "substitution",
    single            = "substitution",
    ligature          = "ligature",
    alternate         = "alternate",
    multiple          = "multiple",
    kern              = "kern",
    pair              = "pair",
    single            = "single",
    chainsubstitution = "chainsubstitution",
    chainposition     = "chainposition",
}

local types = {
    substitution      = "gsub_single",
    ligature          = "gsub_ligature",
    alternate         = "gsub_alternate",
    multiple          = "gsub_multiple",
    kern              = "gpos_pair",
    pair              = "gpos_pair",
    single            = "gpos_single",
    chainsubstitution = "gsub_contextchain",
    chainposition     = "gpos_contextchain",
}

local names = {
    gsub_single              = "gsub",
    gsub_multiple            = "gsub",
    gsub_alternate           = "gsub",
    gsub_ligature            = "gsub",
    gsub_context             = "gsub",
    gsub_contextchain        = "gsub",
    gsub_reversecontextchain = "gsub",
    gpos_single              = "gpos",
    gpos_pair                = "gpos",
    gpos_cursive             = "gpos",
    gpos_mark2base           = "gpos",
    gpos_mark2ligature       = "gpos",
    gpos_mark2mark           = "gpos",
    gpos_context             = "gpos",
    gpos_contextchain        = "gpos",
}

setmetatableindex(types, function(t,k) t[k] = k return k end) -- "key"

local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
local noflags    = { false, false, false, false }

-- beware: shared, maybe we should copy the sequence

local function getrange(sequences,category)
    local count = #sequences
    local first = nil
    local last  = nil
    for i=1,count do
        local t = sequences[i].type
        if t and names[t] == category then
            if not first then
                first = i
            end
            last  = i
        end
    end
    return first or 1, last or count
end

local function validspecification(specification,name)
    local dataset = specification.dataset
    if dataset then
        -- okay
    elseif specification[1] then
        dataset = specification
        specification = { dataset = dataset }
    else
        dataset = { { data = specification.data } }
        specification.data     = nil
        specification.coverage = dataset
        specification.dataset  = dataset
    end
    local first = dataset[1]
    if first then
        first = first.data
    end
    if not first then
        report_otf("invalid feature specification, no dataset")
        return
    end
    if type(name) ~= "string" then
        name = specification.name or first.name
    end
    if type(name) ~= "string" then
        report_otf("invalid feature specification, no name")
        return
    end
    local n = #dataset
    if n > 0 then
        for i=1,n do
            setmetatableindex(dataset[i],specification)
        end
        return specification, name
    end
end

local function addfeature(data,feature,specifications,prepareonly)

    -- todo: add some validator / check code so that we're more tolerant to
    -- user errors

    if not specifications then
        report_otf("missing specification")
        return
    end

    local descriptions = data.descriptions
    local resources    = data.resources

    if not descriptions or not resources then
        report_otf("missing specification")
        return
    end

    local features     = resources.features
    local sequences    = resources.sequences

    if not features or not sequences then
        report_otf("missing specification")
        return
    end

    local alreadydone = resources.alreadydone
    if not alreadydone then
        alreadydone = { }
        resources.alreadydone = alreadydone
    end
    if alreadydone[specifications] then
        return
    else
        alreadydone[specifications] = true
    end

    -- feature has to be unique but the name entry wins eventually

    local fontfeatures = resources.features or everywhere
    local unicodes     = resources.unicodes
    local splitter     = lpeg.splitter(" ",unicodes)
    local done         = 0
    local skip         = 0
    local aglunicodes  = false
    local privateslot  = fonthelpers.privateslot

    local specifications = validspecification(specifications,feature)
    if not specifications then
     -- report_otf("invalid specification")
        return
    end

    local p = lpeg.P("P")
            * (lpeg.patterns.hexdigit^1/function(s) return tonumber(s,16) end)
            * lpeg.P(-1)

    local function tounicode(code)
        if not code then
            return
        end
        if type(code) == "number" then
            return code
        end
        local u = unicodes[code]
        if u then
         -- unicodes[code] = u
            return u
        end
        if utflen(code) == 1 then
            u = utfbyte(code)
            if u then
                return u
            end
        end
        if privateslot then
            u = privateslot(code) -- no creation !
            if u then
             -- unicodes[code] = u
                return u
            end
        end
        local u = lpegmatch(p,code)
        if u then
         -- unicodes[code] = u
            return u
        end
        if not aglunicodes then
            aglunicodes = fonts.encodings.agl.unicodes -- delayed
        end
        local u = aglunicodes[code]
        if u then
         -- unicodes[code] = u
            return u
        end
    end

    local coverup      = otf.coverup
    local coveractions = coverup.actions
    local stepkey      = coverup.stepkey
    local register     = coverup.register

    -- todo: directly pass a coverage i.e. for privates that later will be
    -- set

    local function prepare_substitution(list,featuretype,nocheck)
        local coverage = { }
        local cover    = coveractions[featuretype]
        for code, replacement in next, list do
            local unicode     = tounicode(code)
            local description = descriptions[unicode]
            if not nocheck and not description then
                -- todo: trace !
                skip = skip + 1
            else
                if type(replacement) == "table" then
                    replacement = replacement[1]
                end
                replacement = tounicode(replacement)
                if replacement and (nocheck or descriptions[replacement]) then
                    cover(coverage,unicode,replacement)
                    done = done + 1
                else
                    skip = skip + 1
                end
            end
        end
        return coverage
    end

    local function prepare_alternate(list,featuretype,nocheck)
        local coverage = { }
        local cover    = coveractions[featuretype]
        for code, replacement in next, list do
            local unicode     = tounicode(code)
            local description = descriptions[unicode]
            if not nocheck and not description then
                skip = skip + 1
            elseif type(replacement) == "table" then
                local r = { }
                for i=1,#replacement do
                    local u = tounicode(replacement[i])
                    r[i] = (nocheck or descriptions[u]) and u or unicode
                end
                cover(coverage,unicode,r)
                done = done + 1
            else
                local u = tounicode(replacement)
                if u then
                    cover(coverage,unicode,{ u })
                    done = done + 1
                else
                    skip = skip + 1
                end
            end
        end
        return coverage
    end

    local function prepare_multiple(list,featuretype,nocheck)
        local coverage = { }
        local cover    = coveractions[featuretype]
        for code, replacement in next, list do
            local unicode     = tounicode(code)
            local description = descriptions[unicode]
            if not nocheck and not description then
                skip = skip + 1
            elseif type(replacement) == "table" then
                local r = { }
                local n = 0
                for i=1,#replacement do
                    local u = tounicode(replacement[i])
                    if nocheck or descriptions[u] then
                        n = n + 1
                        r[n] = u
                    end
                end
                if n > 0 then
                    cover(coverage,unicode,r)
                    done = done + 1
                else
                    skip = skip + 1
                end
            else
                local u = tounicode(replacement)
                if u then
                    cover(coverage,unicode,{ u })
                    done = done + 1
                else
                    skip = skip + 1
                end
            end
        end
        return coverage
    end

    local function prepare_ligature(list,featuretype,nocheck)
        local coverage = { }
        local cover    = coveractions[featuretype]
        for code, ligature in next, list do
            local unicode     = tounicode(code)
            local description = descriptions[unicode]
            if not nocheck and not description then
                skip = skip + 1
            else
                if type(ligature) == "string" then
                    ligature = { lpegmatch(splitter,ligature) }
                end
                local present = true
                for i=1,#ligature do
                    local l = ligature[i]
                    local u = tounicode(l)
                    if nocheck or descriptions[u] then
                        ligature[i] = u
                    else
                        present = false
                        break
                    end
                end
                if present then
                    cover(coverage,unicode,ligature)
                    done = done + 1
                else
                    skip = skip + 1
                end
            end
        end
        return coverage
    end

    local function resetspacekerns()
        -- a bit of a hack, this nil setting but it forces a
        -- rehash of the resources needed .. the feature itself
        -- should be a kern (at least for now)
        data.properties.hasspacekerns = true
        data.resources .spacekerns    = nil
    end

    local function prepare_kern(list,featuretype)
        local coverage = { }
        local cover    = coveractions[featuretype]
        local isspace  = false
        for code, replacement in next, list do
            local unicode     = tounicode(code)
            local description = descriptions[unicode]
            if description and type(replacement) == "table" then
                local r = { }
                for k, v in next, replacement do
                    local u = tounicode(k)
                    if u then
                        r[u] = v
                        if u == 32 then
                            isspace = true
                        end
                    end
                end
                if next(r) then
                    cover(coverage,unicode,r)
                    done = done + 1
                    if unicode == 32 then
                        isspace = true
                    end
                else
                    skip = skip + 1
                end
            else
                skip = skip + 1
            end
        end
        if isspace then
            resetspacekerns()
        end
        return coverage
    end

    local function prepare_pair(list,featuretype)
        local coverage = { }
        local cover    = coveractions[featuretype]
        if cover then
            for code, replacement in next, list do
                local unicode     = tounicode(code)
                local description = descriptions[unicode]
                if description and type(replacement) == "table" then
                    local r = { }
                    for k, v in next, replacement do
                        local u = tounicode(k)
                        if u then
                            r[u] = v
                            if u == 32 then
                                isspace = true
                            end
                        end
                    end
                    if next(r) then
                        cover(coverage,unicode,r)
                        done = done + 1
                        if unicode == 32 then
                            isspace = true
                        end
                    else
                        skip = skip + 1
                    end
                else
                    skip = skip + 1
                end
            end
            if isspace then
                resetspacekerns()
            end
        else
            report_otf("unknown cover type %a",featuretype)
        end
        return coverage
    end

    local prepare_single = prepare_pair -- we could have a better test on the spec

    local function hassteps(lookups)
        if lookups then
            for i=1,#lookups do
                local l = lookups[i]
                if l then
                    for j=1,#l do
                        local l = l[j]
                        if l then
                            local n = l.nofsteps
                            if not n then
                                -- gsub_remove
                                return true
                            elseif n > 0 then
                                return true
                            end
                        end
                    end
                end
            end
        end
        return false
    end

    -- 0 == remove, false = ignore (remove is default)

    local function prepare_chain(list,featuretype,sublookups,nocheck)
        -- todo: coveractions
        local rules    = list.rules
        local coverage = { }
        if rules then
            local lookuptype = types[featuretype]
            for nofrules=1,#rules do
                local rule         = rules[nofrules]
                local current      = rule.current
                local before       = rule.before
                local after        = rule.after
                local replacements = rule.replacements or false
                local sequence     = { }
                local nofsequences = 0
                if before then
                    for n=1,#before do
                        nofsequences = nofsequences + 1
                        sequence[nofsequences] = before[n]
                    end
                end
                local start = nofsequences + 1
                for n=1,#current do
                    nofsequences = nofsequences + 1
                    sequence[nofsequences] = current[n]
                end
                local stop = nofsequences
                if after then
                    for n=1,#after do
                        nofsequences = nofsequences + 1
                        sequence[nofsequences] = after[n]
                    end
                end
                local lookups = rule.lookups or false
                local subtype = nil
                if lookups and sublookups then
                    -- inspect(lookups)
                    if #lookups > 0 then
                        local ns = stop - start + 1
                        for i=1,ns do
                            if lookups[i] == nil then
                                lookups[i] = 0
                            end
                        end
                    end
                    local l = { }
                    for k, v in sortedhash(lookups) do
                        local t = type(v)
                        if t == "table" then
                            -- already ok
                            for i=1,#v do
                                local vi = v[i]
                                if type(vi) ~= "table" then
                                    v[i] = { vi }
                                end
                            end
                            l[k] = v
                        elseif t == "number" then
                            local lookup = sublookups[v]
                            if lookup then
                                l[k] = { lookup }
                                if not subtype then
                                    subtype = lookup.type
                                end
                            elseif v == 0 then
                                l[k] = { { type = "gsub_remove", nosteps = true } }
                            else
                                l[k] = false -- { false } -- new
                            end
                        else
                            l[k] = false -- { false } -- new
                        end
                    end
                    if nocheck then
                        -- fragile
                        rule.lookups = l --no, because checking can spoil it
                    end
                    lookups = l
                end
                if nofsequences > 0 then -- we merge coverage into one
                    -- we copy as we can have different fonts
                    if hassteps(lookups) then
                        -- sequence is the before|current|after match list
                        local hashed = { }
                        for i=1,nofsequences do
                            local t = { }
                            local s = sequence[i]
                            for i=1,#s do
                                local u = tounicode(s[i])
                                if u then
                                    t[u] = true
                                end
                            end
                            hashed[i] = t
                        end
                        -- hashed is the before|current|after match hash
                        sequence = hashed
                        local ruleset = {
                            nofrules,     -- 1
                            lookuptype,   -- 2
                            sequence,     -- 3
                            start,        -- 4
                            stop,         -- 5
                            lookups,      -- 6 (6/7 also signal of what to do)
                            replacements, -- 7
                            subtype,      -- 8
                        }
                        for unic in sortedhash(sequence[start]) do
                            local cu = coverage[unic]
                            if cu then
                                local n = cu.n + 1
                                cu[n] = ruleset
                                cu.n = n
                            else
                                coverage[unic] = {
                                    ruleset,
                                    n = 1,
                                }
                            end
                        end
                        sequence.n = nofsequences
                    else
                     -- report_otf("no steps for %a",lookuptype) -- e.g. in primes feature
                    end
                end
            end
        end
        return coverage
    end

    local dataset = specifications.dataset

    local function report(name,category,position,first,last,sequences)
        report_otf("injecting name %a of category %a at position %i in [%i,%i] of [%i,%i]",
            name,category,position,first,last,1,#sequences)
    end

    local function inject(specification,sequences,sequence,first,last,category,name)
        local position = specification.position or false
        if not position then
            position = specification.prepend
            if position == true then
                if trace_loading then
                    report(name,category,first,first,last,sequences)
                end
                insert(sequences,first,sequence)
                return
            end
        end
        if not position then
            position = specification.append
            if position == true then
                if trace_loading then
                    report(name,category,last+1,first,last,sequences)
                end
                insert(sequences,last+1,sequence)
                return
            end
        end
        local kind = type(position)
        if kind == "string" then
            local index = false
            for i=first,last do
                local s = sequences[i]
                local f = s.features
                if f then
                    for k in sortedhash(f) do -- next, f do
                        if k == position then
                            index = i
                            break
                        end
                    end
                    if index then
                        break
                    end
                end
            end
            if index then
                position = index
            else
                position = last + 1
            end
        elseif kind == "number" then
            if position < 0 then
                position = last - position + 1
            end
            if position > last then
                position = last + 1
            elseif position < first then
                position = first
            end
        else
            position = last + 1
        end
        if trace_loading then
            report(name,category,position,first,last,sequences)
        end
        insert(sequences,position,sequence)
    end

    for s=1,#dataset do
        local specification = dataset[s]
        local valid = specification.valid -- nowhere used
        local feature = specification.name or feature
        if not feature or feature == "" then
            report_otf("no valid name given for extra feature")
        elseif not valid or valid(data,specification,feature) then -- anum uses this
            local initialize = specification.initialize
            if initialize then
                -- when false is returned we initialize only once
                specification.initialize = initialize(specification,data) and initialize or nil
            end
            local askedfeatures = specification.features or everywhere
            local askedsteps    = specification.steps or specification.subtables or { specification.data } or { }
            local featuretype   = specification.type or "substitution"
            local featureaction = false
            local featureflags  = specification.flags or noflags
            local nocheck       = specification.nocheck
            local mapping       = specification.mapping
            local featureorder  = specification.order or { feature }
            local featurechain  = (featuretype == "chainsubstitution" or featuretype == "chainposition") and 1 or 0
            local nofsteps      = 0
            local steps         = { }
            local sublookups    = specification.lookups
            local category      = nil
            local steptype      = nil
            local sequence      = nil
            --
            if fonts.handlers.otf.handlers[featuretype] then
                featureaction = true -- function based
            else
                featuretype = normalized[specification.type or "substitution"] or "substitution"
            end
            --
            checkflags(specification,resources)
            --
            for k, v in next, askedfeatures do
                if v[1] then
                    askedfeatures[k] = tohash(v)
                end
            end
            --
            if featureflags[1] then featureflags[1] = "mark" end
            if featureflags[2] then featureflags[2] = "ligature" end
            if featureflags[3] then featureflags[3] = "base" end
            --
            if featureaction then

                category = "gsub"
                sequence = {
                    features  = { [feature] = askedfeatures },
                    flags     = featureflags,
                    name      = feature, -- redundant
                    order     = featureorder,
                    type      = featuretype,
                 -- steps     = { },
                    nofsteps  = 0, -- just in case we test for that
                }

            else

                if sublookups then
                    local s = { }
                    for i=1,#sublookups do
                        local specification = sublookups[i]
                        local askedsteps    = specification.steps or specification.subtables or { specification.data } or { }
                        local featuretype   = normalized[specification.type or "substitution"] or "substitution"
                        local featureflags  = specification.flags or noflags
                        local nofsteps      = 0
                        local steps         = { }
                        for i=1,#askedsteps do
                            local list     = askedsteps[i]
                            local coverage = nil
                            local format   = nil
                            if featuretype == "substitution" then
                                coverage = prepare_substitution(list,featuretype,nocheck)
                            elseif featuretype == "ligature" then
                                coverage = prepare_ligature(list,featuretype,nocheck)
                            elseif featuretype == "alternate" then
                                coverage = prepare_alternate(list,featuretype,nocheck)
                            elseif featuretype == "multiple" then
                                coverage = prepare_multiple(list,featuretype,nocheck)
                            elseif featuretype == "kern" or featuretype == "move" then
                                format   = featuretype
                                coverage = prepare_kern(list,featuretype)
                            elseif featuretype == "pair" then
                                format   = "pair"
                                coverage = prepare_pair(list,featuretype)
                            elseif featuretype == "single" then
                                format   = "single"
                                coverage = prepare_single(list,featuretype)
                            end
                            if coverage and next(coverage) then
                                nofsteps = nofsteps + 1
                                steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
                            end
                        end
                        --
                        checkmerge(specification)
                        checksteps(specification)
                        --
                        s[i] = {
                            [stepkey] = steps,
                            nofsteps  = nofsteps,
                            flags     = featureflags,
                            type      = types[featuretype],
                        }
                    end
                    sublookups = s
                end

                for i=1,#askedsteps do
                    local list     = askedsteps[i]
                    local coverage = nil
                    local format   = nil
                    if featuretype == "substitution" then
                        -- see font-imp-tweaks: we directly pass a mapping so no checks done
                        category = "gsub"
                        coverage = (mapping and list) or prepare_substitution(list,featuretype,nocheck)
                    elseif featuretype == "ligature" then
                        category = "gsub"
                        coverage = prepare_ligature(list,featuretype,nocheck)
                    elseif featuretype == "alternate" then
                        category = "gsub"
                        coverage = prepare_alternate(list,featuretype,nocheck)
                    elseif featuretype == "multiple" then
                        category = "gsub"
                        coverage = prepare_multiple(list,featuretype,nocheck)
                    elseif featuretype == "kern" or featuretype == "move" then
                        category = "gpos"
                        format   = featuretype
                        coverage = prepare_kern(list,featuretype)
                    elseif featuretype == "pair" then
                        category = "gpos"
                        format   = "pair"
                        coverage = prepare_pair(list,featuretype)
                    elseif featuretype == "single" then
                        category = "gpos"
                        format   = "single"
                        coverage = prepare_single(list,featuretype)
                    elseif featuretype == "chainsubstitution" then
                        category = "gsub"
                        coverage = prepare_chain(list,featuretype,sublookups,nocheck)
                    elseif featuretype == "chainposition" then
                        category = "gpos"
                        coverage = prepare_chain(list,featuretype,sublookups,nocheck)
                    else
                        report_otf("not registering feature %a, unknown category",feature)
                        return
                    end
                    if coverage and next(coverage) then
                        nofsteps = nofsteps + 1
                        steps[nofsteps] = register(coverage,featuretype,format,feature,nofsteps,descriptions,resources)
                    end
                end

                if nofsteps > 0 then
                    sequence = {
                        chain     = featurechain,
                        features  = { [feature] = askedfeatures },
                        flags     = featureflags,
                        name      = feature, -- redundant
                        order     = featureorder,
                        [stepkey] = steps,
                        nofsteps  = nofsteps,
                        type      = specification.handler or types[featuretype],
                    }
                    if prepareonly then
                        return sequence
                    end
                end
            end

            if sequence then
                -- script = { lang1, lang2, lang3 } or script = { lang1 = true, ... }
                checkflags(sequence,resources)
                checkmerge(sequence)
                checksteps(sequence)
                -- position | prepend | append
                local first, last = getrange(sequences,category)
                inject(specification,sequences,sequence,first,last,category,feature)
                -- register in metadata (merge as there can be a few)
                local features = fontfeatures[category]
                if not features then
                    features  = { }
                    fontfeatures[category] = features
                end
                local k = features[feature]
                if not k then
                    k = { }
                    features[feature] = k
                end
                --
                for script, languages in next, askedfeatures do
                    local kk = k[script]
                    if not kk then
                        kk = { }
                        k[script] = kk
                    end
                    for language, value in next, languages do
                        kk[language] = value
                    end
                end
            end

        end
    end
    if trace_loading then
        report_otf("registering feature %a, affected glyphs %a, skipped glyphs %a",feature,done,skip)
    end

end

otf.enhancers.addfeature = addfeature

local extrafeatures = { }
local knownfeatures = { }

function otf.addfeature(name,specification)
    if type(name) == "table" then
        specification = name
    end
    if type(specification) ~= "table" then
        report_otf("invalid feature specification, no valid table")
        return
    end
    specification, name = validspecification(specification,name)
    if name and specification then
        local slot = knownfeatures[name]
        if not slot then
            -- we have a new one
            slot = #extrafeatures + 1
            knownfeatures[name] = slot
        elseif specification.overload == false then
            -- we add an extre one
            slot = #extrafeatures + 1
            knownfeatures[name] = slot
        else
            -- we overload a previous one
        end
        specification.name  = name -- to be sure
        extrafeatures[slot] = specification
     -- report_otf("adding feature %a @ %i",name,slot)
    end
end

-- for feature, specification in next, extrafeatures do
--     addfeature(data,feature,specification)
-- end

local function enhance(data,filename,raw)
    for slot=1,#extrafeatures do
        local specification = extrafeatures[slot]
        addfeature(data,specification.name,specification)
    end
end

-- local function enhance(data,filename,raw)
--     local first = 1
--     local last  = #extrafeatures
--     while true do
--         for slot=first,last do
--             local specification = extrafeatures[slot]
--             addfeature(data,specification.name,specification)
--         end
--         if #extrafeatures > last then
--             first = last + 1
--             last  = #extrafeatures
--         else
--             break
--         end
--     end
-- end

otf.enhancers.enhance = enhance

otf.enhancers.register("check extra features",enhance)

-- fonts.handlers.otf.features.register {
--     name        = 'hangulfix',
--     description = 'fixes for hangul',
-- }

-- fonts.handlers.otf.addfeature {
--     name = "stest",
--     type = "substitution",
--     data = {
--         a = "X",
--         b = "P",
--     }
-- }
-- fonts.handlers.otf.addfeature {
--     name = "atest",
--     type = "alternate",
--     data = {
--         a = { "X", "Y" },
--         b = { "P", "Q" },
--     }
-- }
-- fonts.handlers.otf.addfeature {
--     name = "mtest",
--     type = "multiple",
--     data = {
--         a = { "X", "Y" },
--         b = { "P", "Q" },
--     }
-- }
-- fonts.handlers.otf.addfeature {
--     name = "ltest",
--     type = "ligature",
--     data = {
--         X = { "a", "b" },
--         Y = { "d", "a" },
--     }
-- }
-- fonts.handlers.otf.addfeature {
--     name = "ktest",
--     type = "kern",
--     data = {
--         a = { b = -500 },
--     }
-- }
