You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
615 lines
20 KiB
615 lines
20 KiB
-----------------------------------------------------------------------
|
|
-- FILE: luaotfload-features.lua
|
|
-- DESCRIPTION: part of luaotfload / font features
|
|
-----------------------------------------------------------------------
|
|
|
|
local ProvidesLuaModule = {
|
|
name = "luaotfload-features",
|
|
version = "3.14", --TAGVERSION
|
|
date = "2020-05-06", --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",
|
|
}
|
|
|
|
if luatexbase and luatexbase.provides_module then
|
|
luatexbase.provides_module (ProvidesLuaModule)
|
|
end
|
|
|
|
|
|
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 = 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 function cmp_by_idx (a, b) return a.idx < b.idx end
|
|
|
|
local defined_combos = 0
|
|
|
|
local function handle_combination (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 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
|
|
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 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",
|
|
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
|
|
|
|
--- spec -> spec
|
|
local function handle_request (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) or sub
|
|
specification.name = fullpath
|
|
else
|
|
specification.name = specification.specification
|
|
end
|
|
specification.lookup = "path"
|
|
return specification
|
|
end
|
|
|
|
local lookup, name = request.lookup, request.name
|
|
if lookup == "combo" then
|
|
return handle_combination (name, specification)
|
|
end
|
|
|
|
local features = specification.features
|
|
if not features then
|
|
features = { }
|
|
specification.features = features
|
|
end
|
|
|
|
features.raw = request.features or {}
|
|
request.features = apply_default_features(features.raw)
|
|
|
|
if name then
|
|
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.
|
|
features.normal = normalize (request.features)
|
|
specification.sub = request.sub or specification.sub or false
|
|
|
|
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.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
|
|
end
|
|
|
|
-- MK: Added
|
|
function fonts.definers.analyze (spec_string, size)
|
|
return handle_request {
|
|
size = size,
|
|
specification = spec_string,
|
|
}
|
|
end
|
|
-- /MK
|
|
|
|
-- 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 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
|
|
|
|
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
|
|
|