Skip to content

Commit

Permalink
refactor(packages): Cleanup font feature package coding
Browse files Browse the repository at this point in the history
  • Loading branch information
alerque committed Feb 24, 2021
1 parent 8b3760e commit 79c484b
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 112 deletions.
171 changes: 64 additions & 107 deletions packages/features.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ local lpeg = require("lpeg")
local R, S, P, C = lpeg.R, lpeg.S, lpeg.P, lpeg.C
local Cf, Ct = lpeg.Cf, lpeg.Ct

local opentype = { -- Mapping of opentype features to friendly names
local otFeatureMap = {
Ligatures = {
Required = "rlig",
Common = "liga",
Expand Down Expand Up @@ -56,7 +56,6 @@ local opentype = { -- Mapping of opentype features to friendly names
ScientificInferior = "sinf",
Ordinal = "ordn"
},
-- XXX Character variant support not implemented yet
Style = {
Alternate = "salt",
Italic = "ital",
Expand Down Expand Up @@ -118,136 +117,94 @@ local fontspeclist = fontspecws * P"{" *
(fontspecsep * fontspecname * fontspecws)^0) *
P"}" * fontspecws

local dumpTable = function (self)
return pl.pretty.write(self, "")
end
local otFeatures = pl.class(pl.Map)

local featurestring2table = function (str)
local ret = featurestring:match(str)
setmetatable(ret, { __tostring = dumpTable })
return ret or SU.error("Unparsable Opentype feature string '"..str.."'")
function otFeatures:_init ()
self:super()
local str = SILE.settings.get("font.features")
local tbl = featurestring:match(str)
if not tbl then
SU.error("Unparsable Opentype feature string '"..str.."'")
end
for feat, flag in pairs(tbl) do
self:set(feat, flag.posneg == "+")
end
end

local table2featurestring = function (tbl)
local t2 = {}
for k, v in pl.tablex.sort(tbl) do t2[#t2+1] = v.posneg..k..(v.value and "="..v.value or "") end
return table.concat(t2, ";")
function otFeatures:__tostring ()
local ret = {}
for _, f in ipairs(self:items()) do
ret[#ret+1] = (f[2] and "+" or "-") .. f[1]
end
return table.concat(ret, ";")
end

local _parsefontfeaturevalue = function (k, v)
local posneg = "+"
v = v:gsub("^No", function () posneg= "-"; return "" end)
local res
if type(opentype[k]) == "function" then
res = opentype[k](v)
else
res = opentype[k][v]
end
if not res then SU.error("Bad OpenType value "..v.." for feature "..k) end
if type(res) == "string" then
return res, { posneg = posneg }
else
return res.key, { posneg = posneg, value = res.value }
function otFeatures:loadOption (name, val, invert)
local posneg = not invert
local key = otFeatureMap[name]
if not key then
SU.warn("Unknown OpenType feature " .. name)
else
local matches = lpeg.match(fontspeclist, val)
for _, v in ipairs(matches or { val }) do
v = v:gsub("^No", function () posneg = false; return "" end)
local feat = type(key) == "function" and key(v) or key[v]
if not feat then
SU.warn("Bad OpenType value " .. v .. " for feature " .. name)
else
self:set(feat, posneg)
end
end
end
end

-- Input like {Ligatures = Historic} or {Ligatures = "{Historic, Discretionary}"}
--
-- Build intermediary table using an lpeg "fontspec" parser based on
-- fontspec.pdf, and:
-- * If multiple values, run each one through _parsefontfeaturevalue
-- * Else, run only one through _parsefontfeaturevalue
--
-- Output like { dlig = { posneg = "+" }, hlig = { posneg = "+" } }
--
-- Most real-world use should be single value, but multiple value use is not
-- that odd. Junicode, for example, a common font among medievalists, has many
-- Stylistic Sets and Character Variations, many of which make sense to enable
-- simultaneously.
local parsefontfeatures = function (options)
local otfeatures = featurestring2table(SILE.settings.get("font.features"))
function otFeatures:loadOptions (options, invert)
SU.debug("features", "Features was", self)
for k, v in pairs(options) do
if not opentype[k] then SU.warn("Unknown Opentype feature "..k)
else
local features = {}
setmetatable(features, { __tostring = dumpTable })
local m = lpeg.match(fontspeclist, v)
if m then
for i, match in pairs(m) do
features[i] = match
end
else
features[k] = v
end
SU.debug("features", "Parsed features:", features)
for _, vv in pairs(features) do
local pk, pv = _parsefontfeaturevalue(k, vv)
otfeatures[pk] = pv
end
end
self:loadOption(k, v, invert)
end
SU.debug("features", "Interpreted features as:", otfeatures)
return otfeatures
SU.debug("features", "Features interpreted as", self)
end

function otFeatures:unloadOptions (options)
self:loadOptions(options, true)
end

-- We do it this way so that we can use a SILE.temporarily in \font,
-- instead of calling these user-facing functions in it.
SILE.registerCommand("add-font-feature", function (options, _)
local otfeatures = parsefontfeatures(options)
local features_s = table2featurestring(otfeatures)
SILE.settings.set("font.features", features_s)
SU.debug("features", "Added features:", features_s)
local otfeatures = otFeatures()
otfeatures:loadOptions(options)
SILE.settings.set("font.features", tostring(otfeatures))
end)

local removefontfeatures = function (options, _)
local t_cur = featurestring2table(SILE.settings.get("font.features"))
local t_rm = parsefontfeatures(options)
local otfeatures = pl.tablex.deepcopy(t_cur)
for k, v in pairs(t_rm) do
-- \remove-font-features{Ligatures=NoHistoric} should not remove Historic
if otfeatures[k] and otfeatures[k].posneg == v.posneg then
otfeatures[k] = nil
end
end
SILE.settings.set("font.features", table2featurestring(otfeatures))
SU.debug("features", "Features were:", t_cur)
SU.debug("features", "Removed features: ", t_rm)
SU.debug("features", "Features are now:", otfeatures)
end
SILE.registerCommand("remove-font-feature", removefontfeatures)
SILE.registerCommand("remove-font-feature", function(options, _)
local otfeatures = otFeatures()
otfeatures:unloadOptions(options)
SILE.settings.set("font.features", tostring(otfeatures))
end)

local fontfn = SILE.Commands.font
SILE.registerCommand("font", function (options, content)
-- It is guaranteed that future releases of SILE will not implement non-OT \font
-- features with capital letters.
-- Cf. https://github.com/sile-typesetter/sile/issues/992#issuecomment-665575353
-- So, we reserve 'em all. ⍩⃝
local features = {}
local nfeatures = 0
for k, v in pairs(options) do
-- Does key begin with capital?
if k:sub(1, 1):match('^[A-Z]$') then
-- OK, possible feature.
-- We allow \add-font-feature to give the warning if invalid.
-- This is so user's font still considered.
features[k] = v
nfeatures = nfeatures + 1
end
end
local feats = {}
if nfeatures > 0 then
feats = parsefontfeatures(features)
local otfeatures = otFeatures()
-- It is guaranteed that future releases of SILE will not implement non-OT \font
-- features with capital letters.
-- Cf. https://github.com/sile-typesetter/sile/issues/992#issuecomment-665575353
-- So, we reserve 'em all. ⍩⃝
for k, v in pairs(options) do
if k:match('^[A-Z]') then
otfeatures:loadOption(k, v)
options[k] = nil
end
local features_s = SILE.settings.get("font.features")
local features_temp = ''
if string.len(features_s) > 0 then features_temp = features_s .. ';' end
features_temp = features_temp .. table2featurestring(feats)
SILE.settings.set("font.features", features_temp)
SU.debug("features", "Font features temporarily set to:", features_temp)
fontfn(options, content)
SILE.settings.set("font.features", features_s)
end, "Set current font family, size, weight, style, variant, script, direction, language,\
and features (overridden)")
end
SU.debug("features", "Font features parsed as:", otfeatures)
options.features = (options.features and options.features .. ";" or "") .. tostring(otfeatures)
return fontfn(options, content)
end, SILE.Help.font .. " (overridden)")

return { documentation = [[\begin{document}
As mentioned in Chapter 3, SILE automatically applies ligatures defined by the fonts
Expand Down
6 changes: 3 additions & 3 deletions tests/font-features-cvXX.expected
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ Mx 22.7674
T 1307 w=5.0293 (ồ)
Mx 14.8819
My 54.4442
Set font Gentium Plus;10;400;;normal;+cv75;+cv81;+cv75;-cv81;LTR
Set font Gentium Plus;10;400;;normal;+cv75;-cv81;LTR
T 858 w=5.5176 (һ)
Mx 23.0701
Set font Gentium Plus;10;400;;normal;+cv75;+cv81;-cv75;+cv81;LTR
Set font Gentium Plus;10;400;;normal;-cv75;+cv81;LTR
T 1306 w=5.0293 (ồ)
Mx 14.8819
My 66.4442
Set font Gentium Plus;10;400;;normal;+cv75;+cv81;-cv75;-cv81;LTR
Set font Gentium Plus;10;400;;normal;-cv75;-cv81;LTR
T 858 w=5.5176 (һ)
Mx 23.0701
T 1306 w=5.0293 (ồ)
Expand Down
4 changes: 2 additions & 2 deletions tests/font-features.expected
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ T 54 3871 3490 3850 3850 38 3490 3964 4007 w=47.2266 (SmallCaps)
My 39.2752
T 54 3871 3490 3850 3850 38 3490 3964 4007 w=47.2266 (SmallCaps)
My 51.2752
Set font Gentium Plus;10;400;;normal;+dlig;+hlig;LTR
Set font Gentium Plus;10;400;;normal;+dlig;+hlig;-smcp;LTR
Mx 4.7607
Mx 2.7100
Mx 4.8779
Expand All @@ -32,7 +32,7 @@ Mx 3.8623
T 47 a=4.7607 76 a=2.7100 74 a=4.8779 68 a=4.5898 87 a=3.4424 88 a=5.2979 85 a=3.9600 72 a=4.6191 86 a=3.8623 (Ligatures)
Mx 14.8819
My 75.2752
Set font Gentium Plus;10;400;;normal;;LTR
Set font Gentium Plus;10;400;;normal;-dlig;-hlig;-smcp;LTR
T 49 82 85 80 68 79 w=30.8105 (Normal)
End page
Finish

0 comments on commit 79c484b

Please sign in to comment.