• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 6713098919

31 Oct 2023 10:21PM UTC coverage: 52.831% (-21.8%) from 74.636%
6713098919

push

github

web-flow
Merge d0a2a1ee9 into b185d4972

45 of 45 new or added lines in 3 files covered. (100.0%)

8173 of 15470 relevant lines covered (52.83%)

6562.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/packages/features/init.lua
1
local base = require("packages.base")
×
2

3
local package = pl.class(base)
×
4
package._name = "features"
×
5

6
local lpeg = require("lpeg")
×
7

8
local R, S, P, C = lpeg.R, lpeg.S, lpeg.P, lpeg.C
×
9
local Cf, Ct = lpeg.Cf, lpeg.Ct
×
10

11
local otFeatureMap = {
×
12
  Ligatures = {
×
13
    Required = "rlig",
14
    Common = "liga",
15
    Contextual = "clig",
16
    Rare = "dlig",
17
    Discretionary = "dlig",
18
    Historic = "hlig"
×
19
  },
20
  Fractions = {
×
21
    On = "frac",
22
    Alternate = "afrc"
×
23
  },
24
  StylisticSet = function (i)
25
    return string.format("ss%02i", tonumber(i))
×
26
  end,
27
  CharacterVariant = function (i)
28
    return string.format("cv%02i", tonumber(i))
×
29
  end,
30
  Letters = {
×
31
    Uppercase = "case",
32
    SmallCaps = "smcp",
33
    PetiteCaps = "pcap",
34
    UppercaseSmallCaps = "c2sc",
35
    UppercasePetiteCaps = "c2pc",
36
    Unicase = "unic"
×
37
  },
38
  Numbers = {
×
39
    Uppercase = "lnum",
40
    Lining = "lnum",
41
    LowerCase = "onum",
42
    OldStyle = "onum",
43
    Proportional = "pnum",
44
    monospaced = "tnum",
45
    SlashedZero = "zero",
46
    Arabic = "anum"
×
47
  },
48
  Contextuals = {
×
49
    Swash = "cswh",
50
    Alternate = "calt",
51
    WordInitial = "init",
52
    WordFinal = "fina",
53
    LineFinal = "falt",
54
    Inner = "medi"
×
55
  },
56
  VerticalPosition = {
×
57
    Superior = "sups",
58
    Inferior = "subs",
59
    Numerator = "numr",
60
    Denominator = "dnom",
61
    ScientificInferior = "sinf",
62
    Ordinal = "ordn"
×
63
  },
64
  Style = {
×
65
    Alternate = "salt",
66
    Italic = "ital",
67
    Ruby = "ruby",
68
    Swash = "swsh",
69
    Historic = "hist",
70
    TitlingCaps = "titl",
71
    HorizontalKana = "hkna",
72
    VerticalKana = "vkna"
×
73
  },
74
  Diacritics = {
×
75
    MarkToBase = "mark",
76
    MarkToMark = "mkmk",
77
    AboveBase = "abvm",
78
    BelowBase = "blwm"
×
79
  },
80
  Kerning = {
×
81
    Uppercase = "cpsp",
82
    On = "kern"
×
83
  },
84
  CJKShape = {
×
85
    Traditional = "trad",
86
    Simplified = "smpl",
87
    JIS1978 = "jp78",
88
    JIS1983 = "jp83",
89
    JIS1990 = "jp90",
90
    Expert = "expt",
91
    NLC = "nlck"
×
92
  },
93
  CharacterWidth = {
×
94
    Proportional = "pwid",
95
    Full = "fwid",
96
    Half = "hwid",
97
    Third = "twid",
98
    Quarter = "qwid",
99
    AlternateProportional = "palt",
100
    AlternateHalf = "halt"
×
101
  }
102
}
103

104
local function tagpos (pos, k, v)
105
  return k, { posneg = pos, value = v }
×
106
end
107

108
-- Parser for feature strings
109
local featurename = C((1 - S",;:=")^1)
×
110
local value = C(SILE.parserBits.integer)
×
111
local tag = C(S"+-") * featurename * (P"=" * value)^0 * S",;:"^-1 / tagpos
×
112
local featurestring = Cf(Ct"" * tag^0, rawset)
×
113

114
-- Parser for fontspec strings
115
-- Refer to fontspec.pdf (see doc), Chapter 3, Table 4 (p. 37)
116
local fontspecsafe = R("AZ", "az", "09") + P":"
×
117
local ws = SILE.parserBits.ws
×
118
local fontspecsep = P"," * ws
×
119
local fontspecname = C(fontspecsafe^1)
×
120
local fontspeclist = ws * P"{" *
×
121
                     Ct(ws * fontspecname *
×
122
                        (fontspecsep * fontspecname * ws)^0) *
×
123
                     P"}" * ws
×
124

125
local otFeatures = pl.class(pl.Map)
×
126

127
function otFeatures:_init ()
×
128
  self:super()
×
129
  local str = SILE.settings:get("font.features")
×
130
  local tbl = featurestring:match(str)
×
131
  if not tbl then
×
132
    SU.error("Unparsable Opentype feature string '"..str.."'")
×
133
  end
134
  for feat, flag in pairs(tbl) do
×
135
    self:set(feat, flag.posneg == "+")
×
136
  end
137
end
138

139
function otFeatures:__tostring ()
×
140
  local ret = {}
×
141
  for _, f in ipairs(self:items()) do
×
142
    ret[#ret+1] = (f[2] and "+" or "-") .. f[1]
×
143
  end
144
  return table.concat(ret, ";")
×
145
end
146

147
function otFeatures:loadOption (name, val, invert)
×
148
  local posneg = not invert
×
149
  local key = otFeatureMap[name]
×
150
  if not key then
×
151
    SU.warn("Unknown OpenType feature " .. name)
×
152
  else
153
    local matches = lpeg.match(fontspeclist, val)
×
154
    for _, v in ipairs(matches or { val }) do
×
155
      v = v:gsub("^No", function () posneg = false; return "" end)
×
156
      local feat = type(key) == "function" and key(v) or key[v]
×
157
      if not feat then
×
158
        SU.warn("Bad OpenType value " .. v .. " for feature " .. name)
×
159
      else
160
        self:set(feat, posneg)
×
161
      end
162
    end
163
  end
164
end
165

166
-- Input like {Ligatures = Historic} or {Ligatures = "{Historic, Discretionary}"}
167
--
168
-- Most real-world use should be single value, but multiple value use is not
169
-- that odd.  Junicode, for example, a common font among medievalists, has many
170
-- Stylistic Sets and Character Variations, many of which make sense to enable
171
-- simultaneously.
172
function otFeatures:loadOptions (options, invert)
×
173
  SU.debug("features", "Features was", self)
×
174
  for k, v in pairs(options) do
×
175
    self:loadOption(k, v, invert)
×
176
  end
177
  SU.debug("features", "Features interpreted as", self)
×
178
end
179

180
function otFeatures:unloadOptions (options)
×
181
  self:loadOptions(options, true)
×
182
end
183

184
local fontfn = SILE.Commands.font
×
185

186
function package:registerCommands ()
×
187

188
  self:registerCommand("add-font-feature", function (options, _)
×
189
    local otfeatures = otFeatures()
×
190
    otfeatures:loadOptions(options)
×
191
    SILE.settings:set("font.features", tostring(otfeatures))
×
192
  end)
193

194
  self:registerCommand("remove-font-feature", function(options, _)
×
195
    local otfeatures = otFeatures()
×
196
    otfeatures:unloadOptions(options)
×
197
    SILE.settings:set("font.features", tostring(otfeatures))
×
198
  end)
199

200
  self:registerCommand("font", function (options, content)
×
201
    local otfeatures = otFeatures()
×
202
    -- It is guaranteed that future releases of SILE will not implement non-OT \font
203
    -- features with capital letters.
204
    -- Cf. https://github.com/sile-typesetter/sile/issues/992#issuecomment-665575353
205
    -- So, we reserve 'em all. ⍩⃝
206
    for k, v in pairs(options) do
×
207
      if k:match('^[A-Z]') then
×
208
        otfeatures:loadOption(k, v)
×
209
        options[k] = nil
×
210
      end
211
    end
212
    SU.debug("features", "Font features parsed as:", otfeatures)
×
213
    options.features = (options.features and options.features .. ";" or "") .. tostring(otfeatures)
×
214
    return fontfn(options, content)
×
215
  end, tostring(SILE.Help.font) .. " (overridden)")
×
216

217
end
218

219
package.documentation = [[
220
\begin{document}
221
SILE automatically applies ligatures defined by the fonts that you use.
222
These ligatures are defined by tables of \em{features} within the font file.
223
As well as ligatures (multiple glyphs displayed as a single glyph), the features tables also declare other glyph substitutions.
224

225
The standard \autodoc:command{\font} command provides an interface to selecting the features that you want SILE to apply to a font.
226
The features available will be specific to the font file; some fonts come with documentation explaining their supported features.
227
Discussion of OpenType features is beyond the scope of this manual.
228

229
These features can be turned on and off by passing “raw” feature names to the \autodoc:command{\font} command like so:
230

231
\begin[type=autodoc:codeblock]{raw}
232
\font[features="+dlig,+hlig"]{...} % turn on discretionary and historic ligatures
233
\end{raw}
234

235
However, this is unwieldy and requires memorizing the feature codes.
236

237
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.
238
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{\code{http://texdoc.net/texmf-dist/doc/latex/fontspec/fontspec.pdf}}
239

240
Here is how you would turn on discretionary and historic ligatures with the \autodoc:package{features} package:
241

242
\begin[type=autodoc:codeblock]{raw}
243
\add-font-feature[Ligatures=Rare]\add-font-feature[Ligatures=Discretionary]
244
...
245
\remove-font-feature[Ligatures=Rare]\remove-font-feature[Ligatures=Discretionary]
246
\end{raw}
247
\end{document}
248
]]
×
249

250
return package
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc