local base = require("packages.base")
local package = pl.class(base)
package._name = "features"
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 otFeatureMap = {
Ligatures = {
Required = "rlig",
Common = "liga",
Contextual = "clig",
Rare = "dlig",
Discretionary = "dlig",
Historic = "hlig",
},
Fractions = {
On = "frac",
Alternate = "afrc",
},
StylisticSet = function (i)
return string.format("ss%02i", tonumber(i))
end,
CharacterVariant = function (i)
return string.format("cv%02i", tonumber(i))
end,
Letters = {
Uppercase = "case",
SmallCaps = "smcp",
PetiteCaps = "pcap",
UppercaseSmallCaps = "c2sc",
UppercasePetiteCaps = "c2pc",
Unicase = "unic",
},
Numbers = {
Uppercase = "lnum",
Lining = "lnum",
LowerCase = "onum",
OldStyle = "onum",
Proportional = "pnum",
monospaced = "tnum",
SlashedZero = "zero",
Arabic = "anum",
},
Contextuals = {
Swash = "cswh",
Alternate = "calt",
WordInitial = "init",
WordFinal = "fina",
LineFinal = "fault",
Inner = "medi",
},
VerticalPosition = {
Superior = "sups",
Inferior = "subs",
Numerator = "numr",
Denominator = "dnom",
ScientificInferior = "sinf",
Ordinal = "ordn",
},
Style = {
Alternate = "salt",
Italic = "ital",
Ruby = "ruby",
Swash = "swsh",
Historic = "hist",
TitlingCaps = "titl",
HorizontalKana = "hkna",
VerticalKana = "vkna",
},
Diacritics = {
MarkToBase = "mark",
MarkToMark = "mkmk",
AboveBase = "abvm",
BelowBase = "blwm",
},
Kerning = {
Uppercase = "cpsp",
On = "kern",
},
CJKShape = {
Traditional = "trad",
Simplified = "smpl",
JIS1978 = "jp78",
JIS1983 = "jp83",
JIS1990 = "jp90",
Expert = "expt",
NLC = "nlck",
},
CharacterWidth = {
Proportional = "pwid",
Full = "fwid",
Half = "hwid",
Third = "twid",
Quarter = "qwid",
AlternateProportional = "palt",
AlternateHalf = "halt",
},
}
local function tagpos (pos, k, v)
return k, { posneg = pos, value = v }
end
local featurename = C((1 - S",;:=")^1)
local value = C(SILE.parserBits.integer)
local tag = C(S"+-") * featurename * (P"=" * value)^0 * S",;:"^-1 / tagpos
local featurestring = Cf(Ct"" * tag^0, rawset)
local fontspecsafe = R("AZ", "az", "09") + P":"
local ws = SILE.parserBits.ws
local fontspecsep = P"," * ws
local fontspecname = C(fontspecsafe^1)
local fontspeclist = ws * P"{" *
Ct(ws * fontspecname *
(fontspecsep * fontspecname * ws)^0) *
P"}" * ws
local otFeatures = pl.class(pl.Map)
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
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
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
function otFeatures:loadOptions (options, invert)
SU.debug("features", "Features was", self)
for k, v in pairs(options) do
self:loadOption(k, v, invert)
end
SU.debug("features", "Features interpreted as", self)
end
function otFeatures:unloadOptions (options)
self:loadOptions(options, true)
end
local fontfn = SILE.Commands.font
function package:registerCommands ()
self:registerCommand("add-font-feature", function (options, _)
local otfeatures = otFeatures()
otfeatures:loadOptions(options)
SILE.settings:set("font.features", tostring(otfeatures))
end)
self:registerCommand("remove-font-feature", function (options, _)
local otfeatures = otFeatures()
otfeatures:unloadOptions(options)
SILE.settings:set("font.features", tostring(otfeatures))
end)
self:registerCommand("font", function (options, content)
local otfeatures = otFeatures()
for k, v in pairs(options) do
if k:match("^[A-Z]") then
otfeatures:loadOption(k, v)
options[k] = nil
end
end
SU.debug("features", "Font features parsed as:", otfeatures)
options.features = (options.features and options.features .. ";" or "") .. tostring(otfeatures)
return fontfn(options, content)
end, tostring(SILE.Help.font) .. " (overridden)")
end
package.documentation = [[
\begin{document}
SILE automatically applies ligatures defined by the fonts that you use.
These ligatures are defined by tables of \em{features} within the font file.
As well as ligatures (multiple glyphs displayed as a single glyph), the features tables also declare other glyph substitutions.
The standard \autodoc:command{\font} command provides an interface to selecting the features that you want SILE to apply to a font.
The features available will be specific to the font file; some fonts come with documentation explaining their supported features.
Discussion of OpenType features is beyond the scope of this manual.
These features can be turned on and off by passing “raw” feature names to the \autodoc:command{\font} command like so:
\begin[type=autodoc:codeblock]{raw}
\font[features="+dlig,+hlig"]{...} % turn on discretionary and historic ligatures
\end{raw}
However, this is unwieldy and requires memorizing the feature codes.
The \autodoc:package{features} package provides two commands, \autodoc:command{\add-font-feature} and \autodoc:command{\remove-font-feature}, which make it easier to access OpenType features.
The interface is patterned on the TeX package \code{fontspec}; for full documentation of the OpenType features supported, see the documentation for that package.\footnote{\href{http://texdoc.net/texmf-dist/doc/latex/fontspec/fontspec.pdf}}
Here is how you would turn on discretionary and historic ligatures with the \autodoc:package{features} package:
\begin[type=autodoc:codeblock]{raw}
\add-font-feature[Ligatures=Rare]\add-font-feature[Ligatures=Discretionary]
...
\remove-font-feature[Ligatures=Rare]\remove-font-feature[Ligatures=Discretionary]
\end{raw}
\end{document}
]]
return package