-----------------------------------------------------------------------
--         FILE:  luaotfload-features.lua
--  DESCRIPTION:  part of luaotfload / font features
-----------------------------------------------------------------------

assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") { 
    name          = "luaotfload-features",
    version       = "3.29",       --TAGVERSION
    date          = "2024-12-03", --TAGDATE
    description   = "luaotfload submodule / features",
    license       = "GPL v2.0",
    author        = "Hans Hagen, Khaled Hosny, Elie Roux, Philipp Gesang, Marcel Krüger",
    copyright     = "PRAGMA ADE / ConTeXt Development Team",
}

local type              = type
local next              = next
local tonumber          = tonumber

local lpeg              = require "lpeg"
local lpegmatch         = lpeg.match
local P                 = lpeg.P
local R                 = lpeg.R
local S                 = lpeg.S
local C                 = lpeg.C

local lower             = string.lower

local table             = table
local tabletohash       = table.tohash
local tablesort         = table.sort

--- this appears to be based in part on luatex-fonts-def.lua

local fonts             = fonts
local definers          = fonts.definers
local handlers          = fonts.handlers
local fontidentifiers   = fonts.hashes and fonts.hashes.identifiers
local otf               = handlers.otf

local config            = config or { luaotfload = { run = { } } }

local as_script         = not config.luaotfload.run.live
local normalize

if as_script then
    function normalize(features)
        return {
            axis = features and features.axis,
            instance = features and features.instance,
        }
    end
else
    normalize = otf.features.normalize
end

local log              = luaotfload.log
local report           = log.report

local stringgsub       = string.gsub
local stringformat     = string.format
local stringis_empty   = string.is_empty

local function cmp_by_idx (a, b) return a.idx < b.idx end

local defined_combos = 0

local function handle_combination (combo, spec, size)
    defined_combos = defined_combos + 1
    if not combo [1] then
        report ("both", 0, "features",
                "combo %d: Empty font combination requested.",
                defined_combos)
        return false
    end

    if not fontidentifiers then
        fontidentifiers = fonts.hashes and fonts.hashes.identifiers
    end

    local chain   = { }
    local fontids = { }
    local n       = #combo

    tablesort (combo, cmp_by_idx)

    --- pass 1: skim combo and resolve fonts
    report ("both", 2, "features", "combo %d: combining %d fonts.",
            defined_combos, n)
    for i = 1, n do
        local cur = combo [i]
        local id  = cur.id
        local idx = cur.idx
        local fnt = fontidentifiers [id]
        if fnt then
            local chars = cur.chars
            if chars == true then
                report ("both", 2, "features",
                        " *> %.2d: fallback font %d at rank %d.",
                        i, id, idx)
            else
                report ("both", 2, "features",
                        " *> %.2d: include font %d at rank %d (%d items).",
                        i, id, idx, (chars and #chars or 0))
            end
            chain   [#chain + 1]   = { fnt, chars, idx = idx }
            fontids [#fontids + 1] = { id = id }
        else
            report ("both", 0, "features",
                    " *> %.2d: font %d at rank %d unknown, skipping.",
                    n, id, idx)
            --- TODO might instead attempt to define the font at this point
            ---      but that’d require some modifications to the syntax
        end
    end

    local nc = #chain
    if nc == 0 then
        report ("both", 0, "features",
                " *> no valid font (of %d) in combination.", n)
        return false
    end

    local basefnt = chain [1] [1]
    if nc == 1 then
        report ("both", 0, "features",
                " *> combination boils down to a single font (%s) \z
                 of %d initially specified; not pursuing this any \z
                 further.", basefnt.fullname, n)
        return basefnt
    end

    local basechar       = basefnt.characters
    local baseprop       = basefnt.properties
    baseprop.name        = spec
    baseprop.virtualized = true
    basefnt.fonts        = fontids

    for i = 2, nc do
        local cur = chain [i]
        local fnt = cur [1]
        local def = cur [2]
        local src = fnt.characters
        local cnt = 0

        local function pickchr (uc, unavailable)
            local chr = src [uc]
            if unavailable == true and basechar [uc] then
                --- fallback mode: already known
                return
            end
            if chr then
                chr.commands = { { "slot", i, uc } }
                basechar [uc] = chr
                cnt = cnt + 1
            end
        end

        if def == true then --> fallback; grab all currently unavailable
            for uc, _chr in next, src do pickchr (uc, true) end
        else --> grab only defined range
            for j = 1, #def do
                local this = def [j]
                if type (this) == "number" then
                    report ("both", 2, "features",
                            " *> [%d][%d]: import codepoint U+%.4X",
                            i, j, this)
                    pickchr (this)
                elseif type (this) == "table" then
                    local lo, hi = unpack (this)
                    report ("both", 2, "features",
                            " *> [%d][%d]: import codepoint range U+%.4X--U+%.4X",
                            i, j, lo, hi)
                    for uc = lo, hi do pickchr (uc) end
                else
                    report ("both", 0, "features",
                            " *> item no. %d of combination definition \z
                             %d not processable.", j, i)
                end
            end
        end
        report ("both", 2, "features",
                " *> font %d / %d: imported %d glyphs into combo.",
                i, nc, cnt)
    end
    return {
        lookup        = "combo",
        file          = basefnt.filename,
        name          = stringformat ("luaotfload<%d>", defined_combos),
        specification = spec,
        features      = { normal = { } },
        forced        = "evl",
        eval          = function () return basefnt end,
        size          = size,
    }
end

---[[ begin excerpt from font-ott.lua ]]

local function swapped (h)
    local r = { }
    for k, v in next, h do
        r[stringgsub(v,"[^a-z0-9]","")] = k -- is already lower
    end
    return r
end

local tables           = otf.tables
local scripts          = tables.scripts
local languages        = tables.languages
local verbosescripts   = swapped(scripts  )
local verboselanguages = swapped(languages)

---[[ end excerpt from font-ott.lua ]]

--[[doc--

    As discussed, we will issue a warning because of incomplete support
    when one of the scripts below is requested.

    Reference: https://github.com/lualatex/luaotfload/issues/31

--doc]]--

local support_incomplete = tabletohash({
   -- "deva", 
    "beng", "guru", "gujr",
    "orya", "taml", "telu", "knda",
    "mlym", "sinh",
}, true)

--[[doc--

    Which features are active by default depends on the script
    requested.

--doc]]--

--- (string, string) dict -> (string, string) dict
local function apply_default_features (rawlist)
    local speclist = {}
    for k, v in pairs(rawlist) do
        if type(v) == 'string' then
            v = ({['true'] = true, ['false'] = false})[lower(v)] or v
        end
        speclist[k] = v
    end
    local default_features = luaotfload.features

    speclist = speclist or { }
    speclist[""] = nil --- invalid options stub

    --- handle language tag
    local language = speclist.language
    if language then
        language = stringgsub(lower(language), "[^a-z0-9]", "")
        language = rawget(verboselanguages, language) -- srsly, rawget?
                or (languages[language] and language)
                or "dflt"
    else
        language = "dflt"
    end
    speclist.language = language

    --- handle script tag
    local script = speclist.script
    if script then
        script = stringgsub(lower(script), "[^a-z0-9]","")
        script = rawget(verbosescripts, script)
              or (scripts[script] and script)
              or "dflt"
        if support_incomplete[script] then
            report("log", 0, "features",
                "Support for the requested script: "
                .. "%q may be incomplete.", script)
        end
    else
        script = "dflt"
    end
    speclist.script = script

    report("log", 2, "features",
        "Auto-selecting default features for script: %s.",
        script)

    local requested = default_features.defaults[script]
    if not requested then
        report("log", 2, "features",
            "No default features for script %q, falling back to \"dflt\".",
            script)
        requested = default_features.defaults.dflt
    end

    for feat, state in next, requested do
        if speclist[feat] == nil then speclist[feat] = state end
    end

    for feat, state in next, default_features.global do
        --- This is primarily intended for setting node
        --- mode unless “base” is requested, as stated
        --- in the manual.
        if speclist[feat] == nil then speclist[feat] = state end
    end
    return speclist
end

local import_values = {
    --- That’s what the 1.x parser did, not quite as graciously,
    --- with an array of branch expressions.
    -- "style", "optsize",--> from slashed notation; handled otherwise
    { "lookup", false },
    { "sub",    false },
}

local supported = {
    b    = "b",
    i    = "i",
    bi   = "bi",
    r    = "r",
    aat  = false,
    icu  = false,
    gr   = false,
}

--- (string | (string * string) | bool) list -> (string * number)
local function handle_slashed (modifiers)
    local style, optsize
    for i=1, #modifiers do
        local mod  = modifiers[i]
        if type(mod) == "table" and mod[1] == "optsize" then --> optical size
            optsize = tonumber(mod[2])
        elseif mod == false then
            --- ignore
            report("log", 0, "features", "unsupported font option: %s", v)
        elseif supported[mod] then
            style = supported[mod]
        elseif not stringis_empty(mod) then
            style = stringgsub(mod, "[^%a%d]", "")
        end
    end
    return style, optsize
end

local extract_subfont
do
    local eof         = P(-1)
    --- Theoretically a valid subfont address can be up to ten
    --- digits long. Additionally we allow names
    local sub_expr    = P"(" * C((1 - S"()")^1) * P")" * eof
    local full_path   = C(P(1 - sub_expr)^1)
    extract_subfont   = full_path * sub_expr
end

local function analyze(spec_string, size)
    local request = lpegmatch(luaotfload.parsers.font_request,
                              spec_string)
----inspect(request)
    if not request then
        --- happens when called with an absolute path
        --- in an anonymous lookup;
        --- we try to behave as friendly as possible
        --- just go with it ...
        report("log", 1, "features", "invalid request %q of type anon",
               spec_string)
        report("log", 1, "features",
               "use square bracket syntax or consult the documentation.")
        --- The result of \fontname must be re-feedable into \font
        --- which is expected by the Latex font mechanism. Now this
        --- is complicated with TTC fonts that need to pass the
        --- number of the requested subfont along with the file name.
        --- Thus we test whether the request is a bare path only or
        --- ends in a subfont expression (decimal digits inside
        --- parentheses).
        --- https://github.com/lualatex/luaotfload/issues/57
        local fullpath, sub = lpegmatch(extract_subfont,
                                        spec_string)
        if fullpath and sub then
            return {
                lookup = 'path',
                sub = tonumber(sub) or sub,
                name = fullpath,
                size = size,
            }
        else
            return {
                lookup = 'path',
                name = spec_string,
                size = size,
            }
        end
    end

    local lookup, name = request.lookup, request.name
    if lookup == "combo" then
        return handle_combination (name, spec_string, size)
    end

    local features = {
        raw = request.features or {}
    }

    local specification = {
        specification = spec_string,
        size = size,
        lookup = lookup,
        name = name,
        sub = request.sub or false,
        features = features,
    }

    if lookup == 'id' then
        local original_font = fontidentifiers [request.id]
        if not original_font then return end
        local original_spec = original_font.specification
        if not original_spec then return end
        if size < 0 then
            specification.size = original_spec.size * size // -1000
        end
        specification.lookup = original_spec.lookup
        specification.name = original_spec.name
        specification.sub = specification.sub or original_spec.sub
        specification.style = original_spec.style
        specification.optsize = original_spec.optsize
        specification.forced = original_spec.forced
        if original_spec.features then
            features.raw = table.merged(original_spec.features.raw, features.raw)
        end
    end

    local processed_features = apply_default_features(features.raw)

    if request.modifiers then
        local style, optsize = handle_slashed(request.modifiers)
        specification.style, specification.optsize = style, optsize
    end

    for n=1, #import_values do
        local feat       = import_values[n][1]
        local keep       = import_values[n][2]
        local newvalue   = processed_features[feat]
        if newvalue then
            specification[feat] = newvalue
            if not keep then
                processed_features[feat] = nil
            end
        end
    end

    --- The next line sets the “rand” feature to “random”; I haven’t
    --- investigated it any further (luatex-fonts-ext), so it will
    --- just stay here.
    features.normal = normalize (processed_features)
    if features.normal.instance then
        if features.normal.axis then
            report("term and log", 0, "features", "instance and axis provided, instance will be ignored")
        else
            specification.instance = features.normal.instance
        end
    end

    local forced_mode = request.features and request.features.mode
    if forced_mode then
        forced_mode = lower(forced_mode)
        if fonts.readers[forced_mode] then
            specification.forced = forced_mode
        end
    end

    return specification
end

fonts.definers.analyze = analyze

if as_script == true then --- skip the remainder of the file
    report ("log", 5, "features",
            "Exiting early from luaotfload-features.lua.")
    return
end

do
    local helpers = otf.readers.helpers
    local axistofactors = helpers.axistofactors
    local cleanname = helpers.cleanname
    local getaxisscale = helpers.getaxisscale
    local function search(table, term, key_field, value_field)
        if not table then return end
        term = cleanname(term)
        for i=1, #table do
            local entry = table[i]
            if cleanname(entry[key_field]) == term then
                return entry[value_field]
            end
        end
    end
    function helpers.getfactors(tfmdata, instance) -- `instance` might refer to an `axis` value here
        assert(instance == true or type(instance) == "string", "Fontloader changed interface of helpers.getfactors. This is a bug, please notify the luaotfload maintainers.")
        local variabledata = tfmdata.variabledata
        if not variabledata then return end
        local instances = variabledata.instances
        local axis = variabledata.axis
        local designaxis = variabledata.designaxis
        local segments = variabledata.segments
        if not axis then return end
        local factors = {}
        if instance == true then
            for i=1, #axis do
                local cur = axis[i]
                local default = cur.default
                factors[i] = getaxisscale(segments, cur.minimum, default, cur.maximum, default)
            end
            return factors
        end
        local values = search(instances, instance, "subfamily", "values")
        if values then
            for i=1, #axis do
                local cur = axis[i]
                factors[i] = getaxisscale(segments, cur.minimum, cur.default, cur.maximum, values[i].value)
            end
            return factors
        end
        values = axistofactors(instance)
        for i=1, #axis do
            local cur = axis[i]
            local default = cur.default
            local value = cur.name and values[cur.name] or values[cur.tag]
            value = tonumber(value) or (value and search(search(designaxis, cur.tag, "tag", "variants"), cleanname(value), "name", "value")) or default
            factors[i] = getaxisscale(segments, cur.minimum, default, cur.maximum, value)
        end
        return factors
    end

    -- Additionally we patch trytosharefont to ensure that variable fonts work
    -- with default values whenever no explicit values are passed.
    local original_trytosharefont = fonts.constructors.trytosharefont
    function fonts.constructors.trytosharefont(target, tfmdata)
        original_trytosharefont(target, tfmdata)
        if not target.streamprovider and tfmdata.resources.variabledata then
            local format = tfmdata.properties.format
            target.streamprovider = format == 'opentype' and 1 or format == 'truetype' and 2 or 0
        end
    end
end

-- We assume that the other otf stuff is loaded already; though there’s
-- another check below during the initialization phase.


local tlig_specification = {
    {
        type      = "substitution",
        features  = everywhere,
        data      = {
            --- quotedblright:
            --- " (QUOTATION MARK)   → ” (RIGHT DOUBLE QUOTATION MARK)
            [0x0022] = 0x201D,

            --- quoteleft:
            --- ' (APOSTROPHE)       → ’ (RIGHT SINGLE QUOTATION MARK)
            [0x0027] = 0x2019,

            --- quoteright:
            --- ` (GRAVE ACCENT)     → ‘ (LEFT SINGLE QUOTATION MARK)
            [0x0060] = 0x2018,
        },
        flags     = noflags,
        order     = { "tlig" },
        prepend   = true,
    },
    {
        type     = "ligature",
        features = everywhere,
        data     = {

            --- endash:
            --- [--] (HYPHEN-MINUS, HYPHEN-MINUS)                   → – (EN DASH)
            [0x2013] = {0x002D, 0x002D},

            --- emdash:
            --- [---] (HYPHEN-MINUS, HYPHEN-MINUS, HYPHEN-MINUS)    → — (EM DASH)
            [0x2014] = {0x002D, 0x002D, 0x002D},

            --- quotedblleft:
            --- [''] (GRAVE ACCENT, GRAVE ACCENT)                   → “ (LEFT DOUBLE QUOTATION MARK)
            [0x201C] = {0x0060, 0x0060},

            --- quotedblright:
            --- [``] (APOSTROPHE, APOSTROPHE)                       → ” (RIGHT DOUBLE QUOTATION MARK)
            [0x201D] = {0x0027, 0x0027},

            --- exclamdown:
            --- [!'] (EXCLAMATION MARK, GRAVE ACCENT)               → ¡ (INVERTED EXCLAMATION MARK)
            [0x00A1] = {0x0021, 0x0060},

            --- questiondown:
            --- [?'] (QUESTION MARK, GRAVE ACCENT)                  → ¡ (INVERTED EXCLAMATION MARK)
            [0x00BF] = {0x003F, 0x0060},

            --- next three originate in T1 encoding (Xetex applies them too)
            --- quotedblbase:
            --- [,,] (COMMA, COMMA)                                 → ¡ (DOUBLE LOW-9 QUOTATION MARK)
            [0x201E] = {0x002C, 0x002C},

            --- LEFT-POINTING DOUBLE ANGLE QUOTATION MARK:
            --- [,,] (LESS-THAN SIGN, LESS-THAN SIGN)               → ¡ (LEFT-POINTING ANGLE QUOTATION MARK)
            [0x00AB] = {0x003C, 0x003C},

            --- RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK:
            --- [,,] (GREATER-THAN SIGN, GREATER-THAN SIGN)         → ¡ (RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK)
            [0x00BB] = {0x003E, 0x003E},
        },
        flags    = noflags,
        order    = { "tlig" },
        prepend  = true,
    },
}

-- "substitution" features allow to replace individual characters with other
-- characters. This is often used inside of fonts to e.g. implement stylistic
-- sets.
-- As an example how such features can be added in the fontloader, we have a
-- simple implementation of the ROT13 cipher as a font feature. It can be
-- used by enabling the `rot13` font feature in a font, but we are not aware of
-- any practical usecase for this.
local rot13_specification = {
    type      = "substitution",
    features  = everywhere,
    data      = {
        [65] = 78, [ 97] = 110, [78] = 65, [110] =  97,
        [66] = 79, [ 98] = 111, [79] = 66, [111] =  98,
        [67] = 80, [ 99] = 112, [80] = 67, [112] =  99,
        [68] = 81, [100] = 113, [81] = 68, [113] = 100,
        [69] = 82, [101] = 114, [82] = 69, [114] = 101,
        [70] = 83, [102] = 115, [83] = 70, [115] = 102,
        [71] = 84, [103] = 116, [84] = 71, [116] = 103,
        [72] = 85, [104] = 117, [85] = 72, [117] = 104,
        [73] = 86, [105] = 118, [86] = 73, [118] = 105,
        [74] = 87, [106] = 119, [87] = 74, [119] = 106,
        [75] = 88, [107] = 120, [88] = 75, [120] = 107,
        [76] = 89, [108] = 121, [89] = 76, [121] = 108,
        [77] = 90, [109] = 122, [90] = 77, [122] = 109,
    },
    flags     = noflags,
    order     = { "rot13" },
    prepend   = true,
}

local interrolig_specification = {
    { type     = "ligature", data = { [0x203d] = {0x21, 0x3f}, [0x2e18] = {0xa1, 0xbf}, }, },
    { type     = "ligature", data = { [0x203d] = {0x3f, 0x21}, [0x2e18] = {0xbf, 0xa1}, }, },
}

local autofeatures = {
    --- always present with Luaotfload; anum for Arabic and Persian is
    --- predefined in font-otc.
    { "tlig" , tlig_specification , "tex ligatures and substitutions" },
    { "rot13", rot13_specification, "rot13"                           },
    { "!!??",  interrolig_specification, "interrobang substitutions"  },
}

local function add_auto_features ()
    local nfeats = #autofeatures
    report ("both", 5, "features",
            "auto-installing %d feature definitions", nfeats)
    for i = 1, nfeats do
        local name, spec, desc = unpack (autofeatures [i])
        spec.description = desc
        otf.addfeature (name, spec)
    end
end

luaotfload.apply_default_features = apply_default_features

do
    local function mathparaminitializer(tfmdata, value, features)
        if not next(tfmdata.mathparameters) then return end
        if value == 'auto' then
            if features.script == 'math' then return end
        end
        tfmdata.mathparameters = {}
    end
    fonts.constructors.features.otf.register {
        name = 'nomathparam',
        description = 'Set Math parameters based on this font',
        default = 'auto',
        initializers = {
            base = mathparaminitializer,
            node = mathparaminitializer,
          -- plug = mathparaminitializer,
        },
    }
end

do
    local function mathfontdimen(tfmdata, _, value)
        if not (tfmdata.mathparameters and next(tfmdata.mathparameters)) then return end
        local parameters = tfmdata.parameters
        local mathparameters = tfmdata.mathparameters
        if value == 'xetex' then
            parameters[10] = mathparameters.ScriptPercentScaleDown
            parameters[11] = mathparameters.ScriptScriptPercentScaleDown
            parameters[12] = mathparameters.DelimitedSubFormulaMinHeight
            parameters[13] = mathparameters.DisplayOperatorMinHeight
            parameters[14] = mathparameters.MathLeading
            parameters[15] = mathparameters.AxisHeight
            parameters[16] = mathparameters.AccentBaseHeight
            parameters[17] = mathparameters.FlattenedAccentBaseHeight
            parameters[18] = mathparameters.SubscriptShiftDown
            parameters[19] = mathparameters.SubscriptTopMax
            parameters[20] = mathparameters.SubscriptBaselineDropMin
            parameters[21] = mathparameters.SuperscriptShiftUp
            parameters[22] = mathparameters.SuperscriptShiftUpCramped
            parameters[23] = mathparameters.SuperscriptBottomMin
            parameters[24] = mathparameters.SuperscriptBaselineDropMax
            parameters[25] = mathparameters.SubSuperscriptGapMin
            parameters[26] = mathparameters.SuperscriptBottomMaxWithSubscript
            parameters[27] = mathparameters.SpaceAfterScript
            parameters[28] = mathparameters.UpperLimitGapMin
            parameters[29] = mathparameters.UpperLimitBaselineRiseMin
            parameters[30] = mathparameters.LowerLimitGapMin
            parameters[31] = mathparameters.LowerLimitBaselineDropMin
            parameters[32] = mathparameters.StackTopShiftUp
            parameters[33] = mathparameters.StackTopDisplayStyleShiftUp
            parameters[34] = mathparameters.StackBottomShiftDown
            parameters[35] = mathparameters.StackBottomDisplayStyleShiftDown
            parameters[36] = mathparameters.StackGapMin
            parameters[37] = mathparameters.StackDisplayStyleGapMin
            parameters[38] = mathparameters.StretchStackTopShiftUp
            parameters[39] = mathparameters.StretchStackBottomShiftDown
            parameters[40] = mathparameters.StretchStackGapAboveMin
            parameters[41] = mathparameters.StretchStackGapBelowMin
            parameters[42] = mathparameters.FractionNumeratorShiftUp
            parameters[43] = mathparameters.FractionNumeratorDisplayStyleShiftUp
            parameters[44] = mathparameters.FractionDenominatorShiftDown
            parameters[45] = mathparameters.FractionDenominatorDisplayStyleShiftDown
            parameters[46] = mathparameters.FractionNumeratorGapMin
            parameters[47] = mathparameters.FractionNumeratorDisplayStyleGapMin
            parameters[48] = mathparameters.FractionRuleThickness
            parameters[49] = mathparameters.FractionDenominatorGapMin
            parameters[50] = mathparameters.FractionDenominatorDisplayStyleGapMin
            parameters[51] = mathparameters.SkewedFractionHorizontalGap
            parameters[52] = mathparameters.SkewedFractionVerticalGap
            parameters[53] = mathparameters.OverbarVerticalGap
            parameters[54] = mathparameters.OverbarRuleThickness
            parameters[55] = mathparameters.OverbarExtraAscender
            parameters[56] = mathparameters.UnderbarVerticalGap
            parameters[57] = mathparameters.UnderbarRuleThickness
            parameters[58] = mathparameters.UnderbarExtraDescender
            parameters[59] = mathparameters.RadicalVerticalGap
            parameters[60] = mathparameters.RadicalDisplayStyleVerticalGap
            parameters[61] = mathparameters.RadicalRuleThickness
            parameters[62] = mathparameters.RadicalExtraAscender
            parameters[63] = mathparameters.RadicalKernBeforeDegree
            parameters[64] = mathparameters.RadicalKernAfterDegree
            parameters[65] = mathparameters.RadicalDegreeBottomRaisePercent
            -- parameters[66] = mathparameters.MinConnectorOverlap
            -- parameters[67] = mathparameters.SubscriptShiftDownWithSuperscript
            -- parameters[68] = mathparameters.FractionDelimiterSize
            -- parameters[69] = mathparameters.FractionDelimiterDisplayStyleSize
            -- parameters[70] = mathparameters.NoLimitSubFactor
            -- parameters[71] = mathparameters.NoLimitSupFactor
        elseif value == 'tex2' then
            parameters[8] = mathparameters.FractionNumeratorDisplayStyleShiftUp
            parameters[9] = mathparameters.FractionNumeratorShiftUp
            parameters[10] = mathparameters.StackTopShiftUp
            parameters[11] = mathparameters.FractionDenominatorDisplayStyleShiftDown
            parameters[12] = mathparameters.FractionDenominatorShiftDown
            parameters[13] = mathparameters.SuperscriptShiftUp
            parameters[14] = mathparameters.SuperscriptShiftUp
            parameters[15] = mathparameters.SuperscriptShiftUpCramped
            parameters[16] = mathparameters.SubscriptShiftDown
            parameters[17] = mathparameters.SubscriptShiftDown
            parameters[18] = mathparameters.SuperscriptBaselineDropMax
            parameters[19] = mathparameters.SubscriptBaselineDropMin
            parameters[20] = mathparameters.FractionDelimiterDisplayStyleSize
            parameters[21] = mathparameters.FractionDelimiterSize
            parameters[22] = mathparameters.AxisHeight
        elseif value == 'tex3' then
            parameters[8] = mathparameters.Defa
            parameters[9] = mathparameters.UpperLimitGapMin
            parameters[10] = mathparameters.LowerLimitGapMin
            parameters[11] = mathparameters.UpperLimitBaselineRiseMin
            parameters[12] = mathparameters.LowerLimitBaselineDropMin
            parameters[13] = 0
        end
    end
    fonts.constructors.features.otf.register {
        name = 'mathfontdimen',
        description = 'Set fontdimen values for compatibility with other engines',
        manipulators = {
            base = mathfontdimen,
          -- node = mathfontdimen,
          -- plug = mathfontdimen,
        },
    }
end

do
    local function restore(tfmdata, value, features)
        if not tfmdata.properties.monospaced then return end
        if features.fixedspace then return end -- In this case, 'auto' is true
        local parameters = tfmdata.parameters
        local space = parameters.space
        parameters.space_stretch, parameters.space_shrink = space/2, space/3
    end
    fonts.constructors.features.otf.register {
        name = 'internal__variablespace',
        default = true,
        initializers = {
            base = restore,
            node = restore,
        },
    }
    local function node_fixedspace(tfmdata, value, features)
        if value == 'auto' then return end
        if tfmdata.properties.monospaced then return end -- handled by internal__variablespace
        local parameters = tfmdata.parameters
        parameters.space_stretch, parameters.space_shrink = 0, 0
    end
    local hb = luaotfload.harfbuzz
    local post_tag = hb and hb.Tag.new'post'
    local function harf_fixedspace(tfmdata, value, features)
        if value == 'auto' then
            -- We have to determine if we have a monospace font.
            -- Let's be honest, it would be boring if that were easy.
            local post_table = tfmdata.hb.shared.face:get_table(post_tag):get_data()
            if #post_table < 16 then
                -- Invalid OpenType font... Let's assume that it's not
                -- monospaced:
                return
            end
            local monospaced = string.unpack('>I4', post_table, 13) ~= 0
            if not monospaced then return end -- FIXME: How to determine?
        end
        local parameters = tfmdata.parameters
        parameters.space_stretch, parameters.space_shrink = 0, 0
    end
    fonts.constructors.features.otf.register {
        name = 'fixedspace',
        description = 'Do not stretch or shrink spaces',
        default = 'auto',
        initializers = {
            base = node_fixedspace,
            node = node_fixedspace,
            plug = harf_fixedspace,
        },
    }
end

local uni_normalize = require'lua-uni-normalize'.direct
local normalize_lookup = setmetatable({}, {__index = function(t, f)
    local fontdir = assert(font.getfont(f))
    local normalize_func = t[fontdir]
    local characters = fontdir.characters
    local function result(head)
        return normalize_func(head, f, characters, true)
    end
    t[f] = result
    return result
end})
-- When this is loaded as part of luaotfload-tool, then we can't access nodes
-- and therefore uni_normalize doesn't exists. In that case we don't need it
-- anyway, so just skip it then.
local normalize_funcs = uni_normalize and {
    nfc = uni_normalize.NFC,
    nfd = uni_normalize.NFD,
    nfkd = uni_normalize.NFKD,
}
fonts.constructors.features.otf.register {
    name = 'normalize',
    default = 'nfc',
    description = 'Normalize text to NFC before shaping',
    manipulators = {
        node = function(fonttable, _, value)
            if value == true then
                value = 'nfc'
            end
            local func = normalize_funcs[value]
            if not func then
                report ("report", 0, "features",
                        "Unsupported normalization method replaced by NFC")
                func = normalize_funcs.nfc
            end
            normalize_lookup[fonttable] = func
        end,
    },
    processors = {
      position = 1,
      node = function(head, f, _, _, _)
          return normalize_lookup[f](head)
      end,
    },
}
require'luaotfload-arabic'

-- mathsize feature for compatibility with older fontloader versions
-- Not all that useful in most cases since it leads to messy font sizes,
-- especially reloading a font using this feature based on it's \fontname
-- will load a wrong size.
-- Only supported for base mode since the other modes don't make sense for
-- math fonts.
local calculatescale = fonts.constructors.calculatescale
function fonts.constructors.calculatescale(tfmdata, size, _, spec)
    local parameters = tfmdata.parameters
    local sizepercentage = parameters and parameters.sizepercentage
    if sizepercentage then
        size = size * sizepercentage / 100
    end
    return calculatescale(tfmdata, size, _, spec)
end

fonts.constructors.features.otf.register {
    name = 'mathsize',
    description = 'Scale math fonts based on their scriptpercentage parameters',
    initializers = {
        base = function(tfmdata, mathsize)
            local mathdata = tfmdata.shared.rawdata.metadata.math
            if not mathdata then return end

            if mathsize == 1 then
                tfmdata.parameters.sizepercentage = 100
            elseif mathsize == 2 then
                tfmdata.parameters.sizepercentage = mathdata.ScriptPercentScaleDown
            elseif mathsize == 3 then
                tfmdata.parameters.sizepercentage = mathdata.ScriptScriptPercentScaleDown
            end
        end,
    },
}

local function unset_designsize(tfmdata)
    tfmdata.designsize = 0
end
fonts.constructors.features.otf.register {
    name = 'no_designsize',
    description = 'Set designsize to zero to ensure round-tripping in \\fontsize',
    default = true,
    manipulators = {
        base = unset_designsize,
        node = unset_designsize,
        plug = unset_designsize,
    },
}

return function ()
    if not fonts and fonts.handlers then
        report ("log", 0, "features",
                "OTF mechanisms missing -- did you forget to \z
                load a font loader?")
        return false
    end
    add_auto_features ()
    return true
end
-- vim:tw=79:sw=4:ts=4:expandtab
