font-otd.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['font-otd'] = {
    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 type = type
local match = string.match
local sequenced = table.sequenced

local trace_dynamics     = false  trackers.register("otf.dynamics", function(v) trace_dynamics = v end)
local trace_applied      = false  trackers.register("otf.applied",  function(v) trace_applied      = v end)

local report_otf         = logs.reporter("fonts","otf loading")
local report_process     = logs.reporter("fonts","otf process")

local allocate           = utilities.storage.allocate

local fonts              = fonts
local otf                = fonts.handlers.otf
local hashes             = fonts.hashes
local definers           = fonts.definers
local constructors       = fonts.constructors
local specifiers         = fonts.specifiers

local fontidentifiers    = hashes.identifiers
local fontresources      = hashes.resources
local fontproperties     = hashes.properties
local fontdynamics       = hashes.dynamics

local contextsetups      = specifiers.contextsetups
local contextnumbers     = specifiers.contextnumbers
local contextmerged      = specifiers.contextmerged

local setmetatableindex  = table.setmetatableindex

local a_to_script        = { }
local a_to_language      = { }

-- we can have a scripts hash in fonts.hashes

function otf.setdynamics(font,attribute)
 -- local features = contextsetups[contextnumbers[attribute]] -- can be moved to caller
    local features = contextsetups[attribute]
    if features then
        local dynamics = fontdynamics[font]
        dynamic = contextmerged[attribute] or 0
        local script, language
        if dynamic == 2 then -- merge
            language  = features.language or fontproperties[font].language or "dflt"
            script    = features.script   or fontproperties[font].script   or "dflt"
        else -- if dynamic == 1 then -- replace
            language  = features.language or "dflt"
            script    = features.script   or "dflt"
        end
        if script == "auto" then
            -- checkedscript and resources are defined later so we cannot shortcut them -- todo: make installer
            script = definers.checkedscript(fontidentifiers[font],fontresources[font],features)
        end
        local ds = dynamics[script] -- can be metatable magic (less testing)
-- or dynamics.dflt
        if not ds then
            ds = { }
            dynamics[script] = ds
        end
        local dsl = ds[language]
-- or ds.dflt
        if not dsl then
            dsl = { }
            ds[language] = dsl
        end
        local dsla = dsl[attribute]
        if not dsla then
            local tfmdata = fontidentifiers[font]
            a_to_script  [attribute] = script
            a_to_language[attribute] = language
            -- we need to save some values .. quite messy
            local properties = tfmdata.properties
            local shared     = tfmdata.shared
            local s_script   = properties.script
            local s_language = properties.language
            local s_mode     = properties.mode
            local s_features = shared.features
            properties.mode     = "node"
            properties.language = language
            properties.script   = script
            properties.dynamics = true -- handy for tracing
            shared.features     = { }
            -- end of save
            local set = constructors.checkedfeatures("otf",features)
            set.mode = "node" -- really needed
            dsla = otf.setfeatures(tfmdata,set)
            if trace_dynamics then
                report_otf("setting dynamics %s: attribute %a, script %a, language %a, set %a",contextnumbers[attribute],attribute,script,language,set)
            end
            -- we need to restore some values
            properties.script   = s_script
            properties.language = s_language
            properties.mode     = s_mode
            shared.features     = s_features
            -- end of restore
            dynamics[script][language][attribute] = dsla -- cache
        elseif trace_dynamics then
         -- report_otf("using dynamics %s: attribute %a, script %a, language %a",contextnumbers[attribute],attribute,script,language)
        end
        return dsla
    end
end

function otf.scriptandlanguage(tfmdata,attr)
    local properties = tfmdata.properties
    if attr and attr > 0 then
        return a_to_script[attr] or properties.script or "dflt", a_to_language[attr] or properties.language or "dflt"
    else
        return properties.script or "dflt", properties.language or "dflt"
    end
end

-- we reimplement the dataset resolver

local autofeatures    = fonts.analyzers.features
local featuretypes    = otf.tables.featuretypes
local defaultscript   = otf.features.checkeddefaultscript
local defaultlanguage = otf.features.checkeddefaultlanguage

local resolved = { } -- we only resolve a font,script,language,attribute pair once
local wildcard = "*"

-- what about analyze in local and not in font

-- needs checking: some added features can pass twice

local P, C, Cc, lpegmatch = lpeg.P, lpeg.C, lpeg.Cc, lpeg.match

local pattern = P("always") * (P(-1) * Cc(true) + P(":") * C((1-P(-1))^1))

local function initialize(sequence,script,language,s_enabled,a_enabled,font,attr,dynamic,ra,autoscript,autolanguage)
    local features = sequence.features
    if features then
        local order = sequence.order
        if order then
            local featuretype = featuretypes[sequence.type or "unknown"]
            local lookupdone  = false
            for i=1,#order do --
                local kind = order[i] --
                local e_e
                local a_e = a_enabled and a_enabled[kind] -- the value (location)
                if a_e ~= nil then
                    e_e = a_e
                else
                    e_e = s_enabled and s_enabled[kind] -- the value (font)
                end
                if e_e then
                    local usedattribute, usedscript, usedlanguage, usedlookup
                    local valid = type(e_e) == "string" and lpegmatch(pattern,e_e)
                    if valid then
                        -- we have hit always
                        usedattribute = autofeatures[kind] or false
                        usedlanguage  = "*"
                        usedscript    = "*"
                        usedlookup    = { valid, usedattribute, sequence, kind }
                    else
                        -- we already checked for e_e
                        local scripts   = features[kind] --
                        local languages = scripts[script] or scripts[wildcard]
                        if not languages and autoscript then
                            langages = defaultscript(featuretype,autoscript,scripts)
                        end
                        if languages then
                            -- we need detailed control over default because we want to trace
                            -- only first attribute match check, so we assume simple fina's
                         -- local valid = false
                            if languages[language] then
                                valid = e_e
                            elseif languages[wildcard] then
                                valid = e_e
                            elseif autolanguage and defaultlanguage(featuretype,autolanguage,languages) then
                                valid = e_e
                            end
                        end
                        if valid then
                            usedattribute = autofeatures[kind] or false
                            usedlanguage  = script
                            usedscript    = language
                            usedlookup    = { valid, usedattribute, sequence, kind }
                        end
                    end
                    if not usedlookup then
                        -- go on
                    elseif lookupdone then
                        if trace_applied then
                            report_process(
                                "font %s, dynamic %a (%a), feature %a, script %a, language %a, lookup %a, value %a, nofsteps %a, lookup already set by %a",
                                    font,attr or 0,dynamic,kind,usedscript,usedlanguage,sequence.name,valid,sequence.nofsteps,ra[#ra][4])
                        end
                    else
                        ra[#ra+1] = usedlookup
                        if trace_applied then
                            report_process(
                                "font %s, dynamic %a (%a), feature %a, script %a, language %a, lookup %a, value %a, nofsteps %a",
                                    font,attr or 0,dynamic,kind,usedscript,usedlanguage,sequence.name,valid,sequence.nofsteps)
                        else
                            return -- no need to look further
                        end
                        lookupdone = true
                    end
                end
            end
        end
    end
end

-- there is some fuzzy language/script state stuff in properties (temporary)

function otf.dataset(tfmdata,font,attr) -- attr only when explicit (as in special parbuilder)

    local script, language, s_enabled, a_enabled, dynamic

    if attr and attr ~= 0 then
        dynamic = contextmerged[attr] or 0
     -- local features = contextsetups[contextnumbers[attr]] -- could be a direct list
        local features = contextsetups[attr]
        a_enabled = features -- location based
        if dynamic == 1 then -- or dynamic == -1 then
            -- replace
            language  = features.language or "dflt"
            script    = features.script   or "dflt"
        elseif dynamic == 2 then -- or dynamic == -2 then
            -- merge
            local properties = tfmdata.properties
            s_enabled = tfmdata.shared.features -- font based
            language  = features.language or properties.language or  "dflt"
            script    = features.script   or properties.script   or  "dflt"
        else
            -- error
            local properties = tfmdata.properties
            language  = properties.language or "dflt"
            script    = properties.script   or "dflt"
        end
    else
        local properties = tfmdata.properties
        language  = properties.language or "dflt"
        script    = properties.script   or "dflt"
        s_enabled = tfmdata.shared.features -- can be made local to the resolver
        dynamic   = 0
    end

    local res = resolved[font]
    if not res then
        res = { }
        resolved[font] = res
    end
    local rs = res[script]
    if not rs then
        rs = { }
        res[script] = rs
    end
    local rl = rs[language]
    if not rl then
        rl = { }
        rs[language] = rl
    end
    local ra = rl[attr]
    if ra == nil then -- attr can be false
        ra = {
            -- indexed but we can also add specific data by key in:
        }
        rl[attr] = ra
        local sequences = tfmdata.shared.reorderedsequences or tfmdata.resources.sequences
        if sequences then
            local autoscript   = (s_enabled and s_enabled.autoscript  ) or (a_enabled and a_enabled.autoscript  )
            local autolanguage = (s_enabled and s_enabled.autolanguage) or (a_enabled and a_enabled.autolanguage)
            for s=1,#sequences do
                -- just return nil or ra step
                initialize(sequences[s],script,language,s_enabled,a_enabled,font,attr,dynamic,ra,autoscript,autolanguage)
            end
        end
    end
    return ra

end