if not modules then modules = { } end modules ["features"] = {
    version   = "2.8",
    comment   = "companion to luaotfload-main.lua",
    author    = "Hans Hagen, Khaled Hosny, Elie Roux, Philipp Gesang",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

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 C                 = lpeg.C

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         = true
local normalize         = function () end

if config.luaotfload.run.live ~= false then
    normalize = otf.features.normalize
    as_script = false
end

--[[HH (font-xtx) --
    tricky: we sort of bypass the parser and directly feed all into
    the sub parser
--HH]]--

function definers.getspecification(str)
    return "", str, "", ":", str
end

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

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

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

local defined_combos = 0

local handle_combination = function (combo, spec)
    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.name
    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 pickchr = function (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
    spec.lookup     = "combo"
    spec.file       = basefnt.filename
    spec.name       = stringformat ("luaotfload<%d>", defined_combos)
    spec.features   = { normal = { spec.specification } }
    spec.forced     = "evl"
    spec.eval       = function () return basefnt end
    return spec
end

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

local scripts = {
    ["arab"] = "arabic",
    ["armn"] = "armenian",
    ["bali"] = "balinese",
    ["beng"] = "bengali",
    ["bopo"] = "bopomofo",
    ["brai"] = "braille",
    ["bugi"] = "buginese",
    ["buhd"] = "buhid",
    ["byzm"] = "byzantine music",
    ["cans"] = "canadian syllabics",
    ["cher"] = "cherokee",
    ["copt"] = "coptic",
    ["cprt"] = "cypriot syllabary",
    ["cyrl"] = "cyrillic",
    ["deva"] = "devanagari",
    ["dsrt"] = "deseret",
    ["ethi"] = "ethiopic",
    ["geor"] = "georgian",
    ["glag"] = "glagolitic",
    ["goth"] = "gothic",
    ["grek"] = "greek",
    ["gujr"] = "gujarati",
    ["guru"] = "gurmukhi",
    ["hang"] = "hangul",
    ["hani"] = "cjk ideographic",
    ["hano"] = "hanunoo",
    ["hebr"] = "hebrew",
    ["ital"] = "old italic",
    ["jamo"] = "hangul jamo",
    ["java"] = "javanese",
    ["kana"] = "hiragana and katakana",
    ["khar"] = "kharosthi",
    ["khmr"] = "khmer",
    ["knda"] = "kannada",
    ["lao" ] = "lao",
    ["latn"] = "latin",
    ["limb"] = "limbu",
    ["linb"] = "linear b",
    ["math"] = "mathematical alphanumeric symbols",
    ["mlym"] = "malayalam",
    ["mong"] = "mongolian",
    ["musc"] = "musical symbols",
    ["mymr"] = "myanmar",
    ["nko" ] = "n\"ko",
    ["ogam"] = "ogham",
    ["orya"] = "oriya",
    ["osma"] = "osmanya",
    ["phag"] = "phags-pa",
    ["phnx"] = "phoenician",
    ["runr"] = "runic",
    ["shaw"] = "shavian",
    ["sinh"] = "sinhala",
    ["sylo"] = "syloti nagri",
    ["syrc"] = "syriac",
    ["tagb"] = "tagbanwa",
    ["tale"] = "tai le",
    ["talu"] = "tai lu",
    ["taml"] = "tamil",
    ["telu"] = "telugu",
    ["tfng"] = "tifinagh",
    ["tglg"] = "tagalog",
    ["thaa"] = "thaana",
    ["thai"] = "thai",
    ["tibt"] = "tibetan",
    ["ugar"] = "ugaritic cuneiform",
    ["xpeo"] = "old persian cuneiform",
    ["xsux"] = "sumero-akkadian cuneiform",
    ["yi"  ] = "yi",
} -- [[ [scripts] ]]

local languages = {
    ["aba" ] = "abaza",
    ["abk" ] = "abkhazian",
    ["ach" ] = "acholi",
    ["acr" ] = "achi",
    ["ady" ] = "adyghe",
    ["afk" ] = "afrikaans",
    ["afr" ] = "afar",
    ["agw" ] = "agaw",
    ["aio" ] = "aiton",
    ["aka" ] = "akan",
    ["als" ] = "alsatian",
    ["alt" ] = "altai",
    ["amh" ] = "amharic",
    ["ang" ] = "anglo-saxon",
    ["apph"] = "phonetic transcription—americanist conventions",
    ["ara" ] = "arabic",
    ["arg" ] = "aragonese",
    ["ari" ] = "aari",
    ["ark" ] = "rakhine",
    ["asm" ] = "assamese",
    ["ast" ] = "asturian",
    ["ath" ] = "athapaskan",
    ["avr" ] = "avar",
    ["awa" ] = "awadhi",
    ["aym" ] = "aymara",
    ["azb" ] = "torki",
    ["aze" ] = "azerbaijani",
    ["bad" ] = "badaga",
    ["bad0"] = "banda",
    ["bag" ] = "baghelkhandi",
    ["bal" ] = "balkar",
    ["ban" ] = "balinese",
    ["bar" ] = "bavarian",
    ["bau" ] = "baulé",
    ["bbc" ] = "batak toba",
    ["bbr" ] = "berber",
    ["bch" ] = "bench",
    ["bcr" ] = "bible cree",
    ["bdy" ] = "bandjalang",
    ["bel" ] = "belarussian",
    ["bem" ] = "bemba",
    ["ben" ] = "bengali",
    ["bgc" ] = "haryanvi",
    ["bgq" ] = "bagri",
    ["bgr" ] = "bulgarian",
    ["bhi" ] = "bhili",
    ["bho" ] = "bhojpuri",
    ["bik" ] = "bikol",
    ["bil" ] = "bilen",
    ["bis" ] = "bislama",
    ["bjj" ] = "kanauji",
    ["bkf" ] = "blackfoot",
    ["bli" ] = "baluchi",
    ["blk" ] = "pa'o karen",
    ["bln" ] = "balante",
    ["blt" ] = "balti",
    ["bmb" ] = "bambara (bamanankan)",
    ["bml" ] = "bamileke",
    ["bos" ] = "bosnian",
    ["bpy" ] = "bishnupriya manipuri",
    ["bre" ] = "breton",
    ["brh" ] = "brahui",
    ["bri" ] = "braj bhasha",
    ["brm" ] = "burmese",
    ["brx" ] = "bodo",
    ["bsh" ] = "bashkir",
    ["bti" ] = "beti",
    ["bts" ] = "batak simalungun",
    ["bug" ] = "bugis",
    ["cak" ] = "kaqchikel",
    ["cat" ] = "catalan",
    ["cbk" ] = "zamboanga chavacano",
    ["ceb" ] = "cebuano",
    ["cgg" ] = "chiga",
    ["cha" ] = "chamorro",
    ["che" ] = "chechen",
    ["chg" ] = "chaha gurage",
    ["chh" ] = "chattisgarhi",
    ["chi" ] = "chichewa (chewa, nyanja)",
    ["chk" ] = "chukchi",
    ["chk0"] = "chuukese",
    ["cho" ] = "choctaw",
    ["chp" ] = "chipewyan",
    ["chr" ] = "cherokee",
    ["chu" ] = "chuvash",
    ["chy" ] = "cheyenne",
    ["cmr" ] = "comorian",
    ["cop" ] = "coptic",
    ["cor" ] = "cornish",
    ["cos" ] = "corsican",
    ["cpp" ] = "creoles",
    ["cre" ] = "cree",
    ["crr" ] = "carrier",
    ["crt" ] = "crimean tatar",
    ["csb" ] = "kashubian",
    ["csl" ] = "church slavonic",
    ["csy" ] = "czech",
    ["ctg" ] = "chittagonian",
    ["cuk" ] = "san blas kuna",
    ["dan" ] = "danish",
    ["dar" ] = "dargwa",
    ["dax" ] = "dayi",
    ["dcr" ] = "woods cree",
    ["deu" ] = "german",
    ["dgo" ] = "dogri",
    ["dgr" ] = "dogri",
    ["dhg" ] = "dhangu",
    ["dhv" ] = "divehi (dhivehi, maldivian)",
    ["diq" ] = "dimli",
    ["div" ] = "divehi (dhivehi, maldivian)",
    ["djr" ] = "zarma",
    ["djr0"] = "djambarrpuyngu",
    ["dng" ] = "dangme",
    ["dnj" ] = "dan",
    ["dnk" ] = "dinka",
    ["dri" ] = "dari",
    ["duj" ] = "dhuwal",
    ["dun" ] = "dungan",
    ["dzn" ] = "dzongkha",
    ["ebi" ] = "ebira",
    ["ecr" ] = "eastern cree",
    ["edo" ] = "edo",
    ["efi" ] = "efik",
    ["ell" ] = "greek",
    ["emk" ] = "eastern maninkakan",
    ["eng" ] = "english",
    ["erz" ] = "erzya",
    ["esp" ] = "spanish",
    ["esu" ] = "central yupik",
    ["eti" ] = "estonian",
    ["euq" ] = "basque",
    ["evk" ] = "evenki",
    ["evn" ] = "even",
    ["ewe" ] = "ewe",
    ["fan" ] = "french antillean",
    ["fan0"] = " fang",
    ["far" ] = "persian",
    ["fat" ] = "fanti",
    ["fin" ] = "finnish",
    ["fji" ] = "fijian",
    ["fle" ] = "dutch (flemish)",
    ["fne" ] = "forest nenets",
    ["fon" ] = "fon",
    ["fos" ] = "faroese",
    ["fra" ] = "french",
    ["frc" ] = "cajun french",
    ["fri" ] = "frisian",
    ["frl" ] = "friulian",
    ["frp" ] = "arpitan",
    ["fta" ] = "futa",
    ["ful" ] = "fulah",
    ["fuv" ] = "nigerian fulfulde",
    ["gad" ] = "ga",
    ["gae" ] = "scottish gaelic (gaelic)",
    ["gag" ] = "gagauz",
    ["gal" ] = "galician",
    ["gar" ] = "garshuni",
    ["gaw" ] = "garhwali",
    ["gez" ] = "ge'ez",
    ["gih" ] = "githabul",
    ["gil" ] = "gilyak",
    ["gil0"] = " kiribati (gilbertese)",
    ["gkp" ] = "kpelle (guinea)",
    ["glk" ] = "gilaki",
    ["gmz" ] = "gumuz",
    ["gnn" ] = "gumatj",
    ["gog" ] = "gogo",
    ["gon" ] = "gondi",
    ["grn" ] = "greenlandic",
    ["gro" ] = "garo",
    ["gua" ] = "guarani",
    ["guc" ] = "wayuu",
    ["guf" ] = "gupapuyngu",
    ["guj" ] = "gujarati",
    ["guz" ] = "gusii",
    ["hai" ] = "haitian (haitian creole)",
    ["hal" ] = "halam",
    ["har" ] = "harauti",
    ["hau" ] = "hausa",
    ["haw" ] = "hawaiian",
    ["hay" ] = "haya",
    ["haz" ] = "hazaragi",
    ["hbn" ] = "hammer-banna",
    ["her" ] = "herero",
    ["hil" ] = "hiligaynon",
    ["hin" ] = "hindi",
    ["hma" ] = "high mari",
    ["hmn" ] = "hmong",
    ["hmo" ] = "hiri motu",
    ["hnd" ] = "hindko",
    ["ho"  ] = "ho",
    ["hri" ] = "harari",
    ["hrv" ] = "croatian",
    ["hun" ] = "hungarian",
    ["hye" ] = "armenian",
    ["hye0"] = "armenian east",
    ["iba" ] = "iban",
    ["ibb" ] = "ibibio",
    ["ibo" ] = "igbo",
    ["ido" ] = "ido",
    ["ijo" ] = "ijo languages",
    ["ile" ] = "interlingue",
    ["ilo" ] = "ilokano",
    ["ina" ] = "interlingua",
    ["ind" ] = "indonesian",
    ["ing" ] = "ingush",
    ["inu" ] = "inuktitut",
    ["ipk" ] = "inupiat",
    ["ipph"] = "phonetic transcription—ipa conventions",
    ["iri" ] = "irish",
    ["irt" ] = "irish traditional",
    ["isl" ] = "icelandic",
    ["ism" ] = "inari sami",
    ["ita" ] = "italian",
    ["iwr" ] = "hebrew",
    ["jam" ] = "jamaican creole",
    ["jan" ] = "japanese",
    ["jav" ] = "javanese",
    ["jbo" ] = "lojban",
    ["jii" ] = "yiddish",
    ["jud" ] = "ladino",
    ["jul" ] = "jula",
    ["kab" ] = "kabardian",
    ["kab0"] = "kabyle",
    ["kac" ] = "kachchi",
    ["kal" ] = "kalenjin",
    ["kan" ] = "kannada",
    ["kar" ] = "karachay",
    ["kat" ] = "georgian",
    ["kaz" ] = "kazakh",
    ["kde" ] = "makonde",
    ["kea" ] = "kabuverdianu (crioulo)",
    ["keb" ] = "kebena",
    ["kek" ] = "kekchi",
    ["kge" ] = "khutsuri georgian",
    ["kha" ] = "khakass",
    ["khk" ] = "khanty-kazim",
    ["khm" ] = "khmer",
    ["khs" ] = "khanty-shurishkar",
    ["kht" ] = "khamti shan",
    ["khv" ] = "khanty-vakhi",
    ["khw" ] = "khowar",
    ["kik" ] = "kikuyu (gikuyu)",
    ["kir" ] = "kirghiz (kyrgyz)",
    ["kis" ] = "kisii",
    ["kiu" ] = "kirmanjki",
    ["kjd" ] = "southern kiwai",
    ["kjp" ] = "eastern pwo karen",
    ["kkn" ] = "kokni",
    ["klm" ] = "kalmyk",
    ["kmb" ] = "kamba",
    ["kmn" ] = "kumaoni",
    ["kmo" ] = "komo",
    ["kms" ] = "komso",
    ["knr" ] = "kanuri",
    ["kod" ] = "kodagu",
    ["koh" ] = "korean old hangul",
    ["kok" ] = "konkani",
    ["kom" ] = "komi",
    ["kon" ] = "kikongo",
    ["kon0"] = "kongo",
    ["kop" ] = "komi-permyak",
    ["kor" ] = "korean",
    ["kos" ] = "kosraean",
    ["koz" ] = "komi-zyrian",
    ["kpl" ] = "kpelle",
    ["kri" ] = "krio",
    ["krk" ] = "karakalpak",
    ["krl" ] = "karelian",
    ["krm" ] = "karaim",
    ["krn" ] = "karen",
    ["krt" ] = "koorete",
    ["ksh" ] = "kashmiri",
    ["ksh0"] = "ripuarian",
    ["ksi" ] = "khasi",
    ["ksm" ] = "kildin sami",
    ["ksw" ] = "s’gaw karen",
    ["kua" ] = "kuanyama",
    ["kui" ] = "kui",
    ["kul" ] = "kulvi",
    ["kum" ] = "kumyk",
    ["kur" ] = "kurdish",
    ["kuu" ] = "kurukh",
    ["kuy" ] = "kuy",
    ["kyk" ] = "koryak",
    ["kyu" ] = "western kayah",
    ["lad" ] = "ladin",
    ["lah" ] = "lahuli",
    ["lak" ] = "lak",
    ["lam" ] = "lambani",
    ["lao" ] = "lao",
    ["lat" ] = "latin",
    ["laz" ] = "laz",
    ["lcr" ] = "l-cree",
    ["ldk" ] = "ladakhi",
    ["lez" ] = "lezgi",
    ["lij" ] = "ligurian",
    ["lim" ] = "limburgish",
    ["lin" ] = "lingala",
    ["lis" ] = "lisu",
    ["ljp" ] = "lampung",
    ["lki" ] = "laki",
    ["lma" ] = "low mari",
    ["lmb" ] = "limbu",
    ["lmo" ] = "lombard",
    ["lmw" ] = "lomwe",
    ["lom" ] = "loma",
    ["lrc" ] = "luri",
    ["lsb" ] = "lower sorbian",
    ["lsm" ] = "lule sami",
    ["lth" ] = "lithuanian",
    ["ltz" ] = "luxembourgish",
    ["lua" ] = "luba-lulua",
    ["lub" ] = "luba-katanga",
    ["lug" ] = "ganda",
    ["luh" ] = "luyia",
    ["luo" ] = "luo",
    ["lvi" ] = "latvian",
    ["mad" ] = "madura",
    ["mag" ] = "magahi",
    ["mah" ] = "marshallese",
    ["maj" ] = "majang",
    ["mak" ] = "makhuwa",
    ["mal" ] = "malayalam reformed",
    ["mam" ] = "mam",
    ["man" ] = "mansi",
    ["map" ] = "mapudungun",
    ["mar" ] = "marathi",
    ["maw" ] = "marwari",
    ["mbn" ] = "mbundu",
    ["mch" ] = "manchu",
    ["mcr" ] = "moose cree",
    ["mde" ] = "mende",
    ["mdr" ] = "mandar",
    ["men" ] = "me'en",
    ["mer" ] = "meru",
    ["mfe" ] = "morisyen",
    ["min" ] = "minangkabau",
    ["miz" ] = "mizo",
    ["mkd" ] = "macedonian",
    ["mkr" ] = "makasar",
    ["mkw" ] = "kituba",
    ["mle" ] = "male",
    ["mlg" ] = "malagasy",
    ["mln" ] = "malinke",
    ["mly" ] = "malay",
    ["mnd" ] = "mandinka",
    ["mng" ] = "mongolian",
    ["mni" ] = "manipuri",
    ["mnk" ] = "maninka",
    ["mnx" ] = "manx",
    ["moh" ] = "mohawk",
    ["mok" ] = "moksha",
    ["mol" ] = "moldavian",
    ["mon" ] = "mon",
    ["mor" ] = "moroccan",
    ["mos" ] = "mossi",
    ["mri" ] = "maori",
    ["mth" ] = "maithili",
    ["mts" ] = "maltese",
    ["mun" ] = "mundari",
    ["mus" ] = "muscogee",
    ["mwl" ] = "mirandese",
    ["mww" ] = "hmong daw",
    ["myn" ] = "mayan",
    ["mzn" ] = "mazanderani",
    ["nag" ] = "naga-assamese",
    ["nah" ] = "nahuatl",
    ["nan" ] = "nanai",
    ["nap" ] = "neapolitan",
    ["nas" ] = "naskapi",
    ["nau" ] = "nauruan",
    ["nav" ] = "navajo",
    ["ncr" ] = "n-cree",
    ["ndb" ] = "ndebele",
    ["ndc" ] = "ndau",
    ["ndg" ] = "ndonga",
    ["nds" ] = "low saxon",
    ["nep" ] = "nepali",
    ["new" ] = "newari",
    ["nga" ] = "ngbaka",
    ["ngr" ] = "nagari",
    ["nhc" ] = "norway house cree",
    ["nis" ] = "nisi",
    ["niu" ] = "niuean",
    ["nkl" ] = "nyankole",
    ["nko" ] = "n'ko",
    ["nld" ] = "dutch",
    ["noe" ] = "nimadi",
    ["nog" ] = "nogai",
    ["nor" ] = "norwegian",
    ["nov" ] = "novial",
    ["nsm" ] = "northern sami",
    ["nso" ] = "sotho, northern",
    ["nta" ] = "northern tai",
    ["nto" ] = "esperanto",
    ["nym" ] = "nyamwezi",
    ["nyn" ] = "norwegian nynorsk",
    ["oci" ] = "occitan",
    ["ocr" ] = "oji-cree",
    ["ojb" ] = "ojibway",
    ["ori" ] = "odia",
    ["oro" ] = "oromo",
    ["oss" ] = "ossetian",
    ["paa" ] = "palestinian aramaic",
    ["pag" ] = "pangasinan",
    ["pal" ] = "pali",
    ["pam" ] = "pampangan",
    ["pan" ] = "punjabi",
    ["pap" ] = "palpa",
    ["pap0"] = "papiamentu",
    ["pas" ] = "pashto",
    ["pau" ] = "palauan",
    ["pcc" ] = "bouyei",
    ["pcd" ] = "picard",
    ["pdc" ] = "pennsylvania german",
    ["pgr" ] = "polytonic greek",
    ["phk" ] = "phake",
    ["pih" ] = "norfolk",
    ["pil" ] = "filipino",
    ["plg" ] = "palaung",
    ["plk" ] = "polish",
    ["pms" ] = "piemontese",
    ["pnb" ] = "western panjabi",
    ["poh" ] = "pocomchi",
    ["pon" ] = "pohnpeian",
    ["pro" ] = "provencal",
    ["ptg" ] = "portuguese",
    ["pwo" ] = "western pwo karen",
    ["qin" ] = "chin",
    ["quc" ] = "k’iche’",
    ["quh" ] = "quechua (bolivia)",
    ["quz" ] = "quechua",
    ["qvi" ] = "quechua (ecuador)",
    ["qwh" ] = "quechua (peru)",
    ["raj" ] = "rajasthani",
    ["rar" ] = "rarotongan",
    ["rbu" ] = "russian buriat",
    ["rcr" ] = "r-cree",
    ["rej" ] = "rejang",
    ["ria" ] = "riang",
    ["rif" ] = "tarifit",
    ["rit" ] = "ritarungo",
    ["rkw" ] = "arakwal",
    ["rms" ] = "romansh",
    ["rmy" ] = "vlax romani",
    ["rom" ] = "romanian",
    ["roy" ] = "romany",
    ["rsy" ] = "rusyn",
    ["rtm" ] = "rotuman",
    ["rua" ] = "kinyarwanda",
    ["run" ] = "rundi",
    ["rup" ] = "aromanian",
    ["rus" ] = "russian",
    ["sad" ] = "sadri",
    ["san" ] = "sanskrit",
    ["sas" ] = "sasak",
    ["sat" ] = "santali",
    ["say" ] = "sayisi",
    ["scn" ] = "sicilian",
    ["sco" ] = "scots",
    ["sek" ] = "sekota",
    ["sel" ] = "selkup",
    ["sga" ] = "old irish",
    ["sgo" ] = "sango",
    ["sgs" ] = "samogitian",
    ["shi" ] = "tachelhit",
    ["shn" ] = "shan",
    ["sib" ] = "sibe",
    ["sid" ] = "sidamo",
    ["sig" ] = "silte gurage",
    ["sks" ] = "skolt sami",
    ["sky" ] = "slovak",
    ["sla" ] = "slavey",
    ["slv" ] = "slovenian",
    ["sml" ] = "somali",
    ["smo" ] = "samoan",
    ["sna" ] = "sena",
    ["sna0"] = "shona",
    ["snd" ] = "sindhi",
    ["snh" ] = "sinhala (sinhalese)",
    ["snk" ] = "soninke",
    ["sog" ] = "sodo gurage",
    ["sop" ] = "songe",
    ["sot" ] = "sotho, southern",
    ["sqi" ] = "albanian",
    ["srb" ] = "serbian",
    ["srd" ] = "sardinian",
    ["srk" ] = "saraiki",
    ["srr" ] = "serer",
    ["ssl" ] = "south slavey",
    ["ssm" ] = "southern sami",
    ["stq" ] = "saterland frisian",
    ["suk" ] = "sukuma",
    ["sun" ] = "sundanese",
    ["sur" ] = "suri",
    ["sva" ] = "svan",
    ["sve" ] = "swedish",
    ["swa" ] = "swadaya aramaic",
    ["swk" ] = "swahili",
    ["swz" ] = "swati",
    ["sxt" ] = "sutu",
    ["sxu" ] = "upper saxon",
    ["syl" ] = "sylheti",
    ["syr" ] = "syriac",
    ["szl" ] = "silesian",
    ["tab" ] = "tabasaran",
    ["taj" ] = "tajiki",
    ["tam" ] = "tamil",
    ["tat" ] = "tatar",
    ["tcr" ] = "th-cree",
    ["tdd" ] = "dehong dai",
    ["tel" ] = "telugu",
    ["tet" ] = "tetum",
    ["tgl" ] = "tagalog",
    ["tgn" ] = "tongan",
    ["tgr" ] = "tigre",
    ["tgy" ] = "tigrinya",
    ["tha" ] = "thai",
    ["tht" ] = "tahitian",
    ["tib" ] = "tibetan",
    ["tiv" ] = "tiv",
    ["tkm" ] = "turkmen",
    ["tmh" ] = "tamashek",
    ["tmn" ] = "temne",
    ["tna" ] = "tswana",
    ["tne" ] = "tundra nenets",
    ["tng" ] = "tonga",
    ["tod" ] = "todo",
    ["tod0"] = "toma",
    ["tpi" ] = "tok pisin",
    ["trk" ] = "turkish",
    ["tsg" ] = "tsonga",
    ["tua" ] = "turoyo aramaic",
    ["tul" ] = "tulu",
    ["tuv" ] = "tuvin",
    ["tvl" ] = "tuvalu",
    ["twi" ] = "twi",
    ["tyz" ] = "tày",
    ["tzm" ] = "tamazight",
    ["tzo" ] = "tzotzil",
    ["udm" ] = "udmurt",
    ["ukr" ] = "ukrainian",
    ["umb" ] = "umbundu",
    ["urd" ] = "urdu",
    ["usb" ] = "upper sorbian",
    ["uyg" ] = "uyghur",
    ["uzb" ] = "uzbek",
    ["vec" ] = "venetian",
    ["ven" ] = "venda",
    ["vit" ] = "vietnamese",
    ["vol" ] = "volapük",
    ["vro" ] = "võro",
    ["wa"  ] = "wa",
    ["wag" ] = "wagdi",
    ["war" ] = "waray-waray",
    ["wcr" ] = "west-cree",
    ["wel" ] = "welsh",
    ["wlf" ] = "wolof",
    ["wln" ] = "walloon",
    ["xbd" ] = "lü",
    ["xhs" ] = "xhosa",
    ["xjb" ] = "minjangbal",
    ["xog" ] = "soga",
    ["xpe" ] = "kpelle (liberia)",
    ["yak" ] = "sakha",
    ["yao" ] = "yao",
    ["yap" ] = "yapese",
    ["yba" ] = "yoruba",
    ["ycr" ] = "y-cree",
    ["yic" ] = "yi classic",
    ["yim" ] = "yi modern",
    ["zea" ] = "zealandic",
    ["zgh" ] = "standard morrocan tamazigh",
    ["zha" ] = "zhuang",
    ["zhh" ] = "chinese, hong kong sar",
    ["zhp" ] = "chinese phonetic",
    ["zhs" ] = "chinese simplified",
    ["zht" ] = "chinese traditional",
    ["znd" ] = "zande",
    ["zul" ] = "zulu",
    ["zza" ] = "zazaki",
} --[[ [languages] ]]

local features = {
    ["aalt"] = "access all alternates",
    ["abvf"] = "above-base forms",
    ["abvm"] = "above-base mark positioning",
    ["abvs"] = "above-base substitutions",
    ["afrc"] = "alternative fractions",
    ["akhn"] = "akhands",
    ["blwf"] = "below-base forms",
    ["blwm"] = "below-base mark positioning",
    ["blws"] = "below-base substitutions",
    ["c2pc"] = "petite capitals from capitals",
    ["c2sc"] = "small capitals from capitals",
    ["calt"] = "contextual alternates",
    ["case"] = "case-sensitive forms",
    ["ccmp"] = "glyph composition/decomposition",
    ["cfar"] = "conjunct form after ro",
    ["cjct"] = "conjunct forms",
    ["clig"] = "contextual ligatures",
    ["cpct"] = "centered cjk punctuation",
    ["cpsp"] = "capital spacing",
    ["cswh"] = "contextual swash",
    ["curs"] = "cursive positioning",
    ["dflt"] = "default processing",
    ["dist"] = "distances",
    ["dlig"] = "discretionary ligatures",
    ["dnom"] = "denominators",
    ["dtls"] = "dotless forms", -- math
    ["expt"] = "expert forms",
    ["falt"] = "final glyph alternates",
    ["fin2"] = "terminal forms #2",
    ["fin3"] = "terminal forms #3",
    ["fina"] = "terminal forms",
    ["flac"] = "flattened accents over capitals", -- math
    ["frac"] = "fractions",
    ["fwid"] = "full width",
    ["half"] = "half forms",
    ["haln"] = "halant forms",
    ["halt"] = "alternate half width",
    ["hist"] = "historical forms",
    ["hkna"] = "horizontal kana alternates",
    ["hlig"] = "historical ligatures",
    ["hngl"] = "hangul",
    ["hojo"] = "hojo kanji forms",
    ["hwid"] = "half width",
    ["init"] = "initial forms",
    ["isol"] = "isolated forms",
    ["ital"] = "italics",
    ["jalt"] = "justification alternatives",
    ["jp04"] = "jis2004 forms",
    ["jp78"] = "jis78 forms",
    ["jp83"] = "jis83 forms",
    ["jp90"] = "jis90 forms",
    ["kern"] = "kerning",
    ["lfbd"] = "left bounds",
    ["liga"] = "standard ligatures",
    ["ljmo"] = "leading jamo forms",
    ["lnum"] = "lining figures",
    ["locl"] = "localized forms",
    ["ltra"] = "left-to-right alternates",
    ["ltrm"] = "left-to-right mirrored forms",
    ["mark"] = "mark positioning",
    ["med2"] = "medial forms #2",
    ["medi"] = "medial forms",
    ["mgrk"] = "mathematical greek",
    ["mkmk"] = "mark to mark positioning",
    ["mset"] = "mark positioning via substitution",
    ["nalt"] = "alternate annotation forms",
    ["nlck"] = "nlc kanji forms",
    ["nukt"] = "nukta forms",
    ["numr"] = "numerators",
    ["onum"] = "old style figures",
    ["opbd"] = "optical bounds",
    ["ordn"] = "ordinals",
    ["ornm"] = "ornaments",
    ["palt"] = "proportional alternate width",
    ["pcap"] = "petite capitals",
    ["pkna"] = "proportional kana",
    ["pnum"] = "proportional figures",
    ["pref"] = "pre-base forms",
    ["pres"] = "pre-base substitutions",
    ["pstf"] = "post-base forms",
    ["psts"] = "post-base substitutions",
    ["pwid"] = "proportional widths",
    ["qwid"] = "quarter widths",
    ["rand"] = "randomize",
    ["rclt"] = "required contextual alternates",
    ["rkrf"] = "rakar forms",
    ["rlig"] = "required ligatures",
    ["rphf"] = "reph form",
    ["rtbd"] = "right bounds",
    ["rtla"] = "right-to-left alternates",
    ["rtlm"] = "right to left math", -- math
    ["ruby"] = "ruby notation forms",
    ["salt"] = "stylistic alternates",
    ["sinf"] = "scientific inferiors",
    ["size"] = "optical size",
    ["smcp"] = "small capitals",
    ["smpl"] = "simplified forms",
 -- ["ss01"] = "stylistic set 1",
 -- ["ss02"] = "stylistic set 2",
 -- ["ss03"] = "stylistic set 3",
 -- ["ss04"] = "stylistic set 4",
 -- ["ss05"] = "stylistic set 5",
 -- ["ss06"] = "stylistic set 6",
 -- ["ss07"] = "stylistic set 7",
 -- ["ss08"] = "stylistic set 8",
 -- ["ss09"] = "stylistic set 9",
 -- ["ss10"] = "stylistic set 10",
 -- ["ss11"] = "stylistic set 11",
 -- ["ss12"] = "stylistic set 12",
 -- ["ss13"] = "stylistic set 13",
 -- ["ss14"] = "stylistic set 14",
 -- ["ss15"] = "stylistic set 15",
 -- ["ss16"] = "stylistic set 16",
 -- ["ss17"] = "stylistic set 17",
 -- ["ss18"] = "stylistic set 18",
 -- ["ss19"] = "stylistic set 19",
 -- ["ss20"] = "stylistic set 20",
    ["ssty"] = "script style", -- math
    ["stch"] = "stretching glyph decomposition",
    ["subs"] = "subscript",
    ["sups"] = "superscript",
    ["swsh"] = "swash",
    ["titl"] = "titling",
    ["tjmo"] = "trailing jamo forms",
    ["tnam"] = "traditional name forms",
    ["tnum"] = "tabular figures",
    ["trad"] = "traditional forms",
    ["twid"] = "third widths",
    ["unic"] = "unicase",
    ["valt"] = "alternate vertical metrics",
    ["vatu"] = "vattu variants",
    ["vert"] = "vertical writing",
    ["vhal"] = "alternate vertical half metrics",
    ["vjmo"] = "vowel jamo forms",
    ["vkna"] = "vertical kana alternates",
    ["vkrn"] = "vertical kerning",
    ["vpal"] = "proportional alternate vertical metrics",
    ["vrt2"] = "vertical rotation",
    ["zero"] = "slashed zero",

    ["trep"] = "traditional tex replacements",
    ["tlig"] = "traditional tex ligatures",

    ["ss.."] = "stylistic set ..",
    ["cv.."] = "character variant ..",
    ["js.."] = "justification ..",

    ["dv.."] = "devanagari ..",
    ["ml.."] = "malayalam ..",
} --[[ [features] ]]

local baselines = {
    ["hang"] = "hanging baseline",
    ["icfb"] = "ideographic character face bottom edge baseline",
    ["icft"] = "ideographic character face tope edige baseline",
    ["ideo"] = "ideographic em-box bottom edge baseline",
    ["idtp"] = "ideographic em-box top edge baseline",
    ["math"] = "mathematical centered baseline",
    ["romn"] = "roman baseline"
} --[[ [baselines] ]]

local swapped = function (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 verbosescripts   = swapped(scripts  )
local verboselanguages = swapped(languages)
local verbosefeatures  = swapped(features )
local verbosebaselines = swapped(baselines)

---[[ 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 apply_default_features = function (speclist)
    local default_features = luaotfload.features

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

    --- handle language tag
    local language = speclist.language
    if language then --- already lowercase at this point
        language = stringgsub(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(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 lookup_types = { "anon"  , "file", "kpse"
                     , "my"    , "name", "path"
                     , "combo"
                     }

local select_lookup = function (request)
    for i=1, #lookup_types do
        local lookup = lookup_types[i]
        local value  = request[lookup]
        if value then
            return lookup, value
        end
    end
end

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

--- (string | (string * string) | bool) list -> (string * number)
local handle_slashed = function (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)
    local digit       = R"09"
    --- Theoretically a valid subfont address can be up to ten
    --- digits long.
    local sub_expr    = P"(" * C(digit^1) * P")" * eof
    local full_path   = C(P(1 - sub_expr)^1)
    extract_subfont   = full_path * sub_expr
end

--- spec -> spec
local handle_request = function (specification)
    local request = lpegmatch(luaotfload.parsers.font_request,
                              specification.specification)
----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",
            specification.specification)
        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,
                                        specification.specification)
        if fullpath and sub then
            specification.sub  = tonumber(sub)
            specification.name = fullpath
        else
            specification.name = specification.specification
        end
        specification.lookup = "path"
        return specification
    end

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

    request.features = apply_default_features(request.features)

    if name then
        if lookup == "file" then
            local suffix = file.suffix (name)
            specification.forcedname = name
            specification.forced     = suffix
            name                     = file.removesuffix (name)
        end
        specification.name     = name
        specification.lookup   = lookup or specification.lookup
    end

    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   = request.features[feat]
        if newvalue then
            specification[feat] = request.features[feat]
            if not keep then
                request.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.
    local features = specification.features
    if not features then
        features = { }
        specification.features = features
    end
    features.normal = normalize (request.features)
    local subfont = tonumber (request.sub)
    if subfont and subfont >= 0 then
        specification.sub = subfont + 1
    else
        specification.sub = false
    end
    return specification
end

fonts.names.handle_request = handle_request


if as_script == true then --- skip the remainder of the file
    report ("log", 5, "features",
            "Exiting early from luaotfload-features.lua.")
    return
else
    local registersplit = definers.registersplit
    registersplit (":", handle_request, "common")
    registersplit ("",  handle_request, "xetex path style") -- catches \font\text=[names]
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,
    },
}

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 add_auto_features = function ()
    local nfeats = #autofeatures
    logreport ("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

return {
    init = function ()

        logreport = luaotfload.log.report

        if not fonts and fonts.handlers then
            logreport ("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
