summaryrefslogtreecommitdiff
path: root/support/lyluatex/lyluatex-options.lua
blob: 62ec72a3c3c9606dd6fe55d53a25503f5aabb49d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
-- luacheck: ignore ly warn info log self luatexbase internalversion font fonts tex token kpse status
local err, warn, info, log = luatexbase.provides_module({
    name               = "lyluatex-options",
    version            = '1.0f',  --LYLUATEX_VERSION
    date               = "2019/05/27",  --LYLUATEX_DATE
    description        = "Module lyluatex-options.",
    author             = "The Gregorio Project  − (see Contributors.md)",
    copyright          = "2015-2019 - jperon and others",
    license            = "MIT",
})

--[[
    This module provides functionality to handle package options and make them
    configurable in a fine-grained fashion as
    - package options
    - local options (for individual instances of commands/environments)
    - changed “from here on” within a document.

-- ]]

local lib = require(kpse.find_file("lyluatex-lib.lua") or "lyluatex-lib.lua")
local optlib = {}  -- namespace for the returned table
local Opts = {options = {}}  -- Options class
Opts.__index = function (self, k) return self.options[k] or rawget(Opts, k) end
setmetatable(Opts, Opts)


function Opts:new(opt_prefix, declarations)
--[[
    Declare package options along with their default and
    accepted values. To *some* extent also provide type checking.
    - opt_prefix: the prefix/name by which the lyluatex-options module is
        referenced in the parent LaTeX document (preamble or package).
        This is required to write the code calling optlib.set_option into
        the option declaration.
    - declarations: a definition table stored in the calling module (see below)
    Each entry in the 'declarations' table represents one package option, with each
    value being an array (table with integer indexes instead of keys). For
    details please refer to the manual.
--]]
    local o = setmetatable(
        {
            declarations = declarations,
            options = {}
        },
        self
    )
    local exopt = ''
    for k, v in pairs(declarations) do
        o.options[k] = v[1] or ''
        tex.sprint(string.format([[
\DeclareOptionX{%s}{\directlua{
    %s:set_option('%s', '\luatexluaescapestring{#1}')
}}%%
]],
            k, opt_prefix, k
        ))
        exopt = exopt..k..'='..(v[1] or '')..','
    end
    tex.sprint([[\ExecuteOptionsX{]]..exopt..[[}%%]], [[\ProcessOptionsX]])
    return o
end

function Opts:check_local_options(opts, ignore_declarations)
--[[
    Parse the given options (options passed to a command/environment),
    sanitize them against the global package options and return a table
    with the local options that should then supersede the global options.
    If ignore_declaration is given any non-false value the sanitization
    step is skipped (i.e. local options are only parsed and duplicates
    rejected).
--]]
    local options = {}
    local next_opt = opts:gmatch('([^,]+)')  -- iterator over options
    for opt in next_opt do
        local k, v = opt:match('([^=]+)=?(.*)')
        if k then
            if v and v:sub(1, 1) == '{' then  -- handle keys with {multiple, values}
                while select(2, v:gsub('{', '')) ~= select(2, v:gsub('}', '')) do v = v..','..next_opt() end
                v = v:sub(2, -2)  -- remove { }
            end
            if not ignore_declarations then
                k, v = self:sanitize_option(k:gsub('^%s', ''), v:gsub('^%s', ''))
            end
            if k then
                if options[k] then err('Option %s is set two times for the same score.', k)
                else options[k] = v
                end
            end
        end
    end
    return options
end

function Opts:is_neg(k)
--[[
    Type check for a 'negative' option. This is an existing option
    name prefixed with 'no' (e.g. 'noalign')
--]]
    local _, i = k:find('^no')
    return i and lib.contains_key(self.declarations, k:sub(i + 1))
end

function Opts:sanitize_option(k, v)
--[[
    Check and (if necessary) adjust the value of a given option.
    Reject undefined options
    Check 'negative' options
    Handle boolean options (empty strings or 'false'), set them to real booleans
--]]
    local declarations = self.declarations
    if k == '' or k == 'noarg' then return end
    if not lib.contains_key(declarations, k) then err('Unknown option: '..k) end
    -- aliases
    if declarations[k] and declarations[k][2] == optlib.is_alias then
        if declarations[k][1] == v then return
        else k = declarations[k] end
    end
    -- boolean
    if v == 'false' then v = false end
    -- negation (for example, noindent is the negation of indent)
    if self:is_neg(k) then
        if v ~= nil and v ~= 'default' then
            k = k:gsub('^no(.*)', '%1')
            v = not v
        else return
        end
    end
    return k, v
end

function Opts:set_option(k, v)
--[[
    Set an option for the given prefix to be in effect from this point on.
    Raises an error if the option is not declared or does not meet the
    declared expectations. (TODO: The latter has to be integrated by extracting
    optlib.validate_option from optlib.validate_options and call it in
    sanitize_option).
--]]
    k, v = self:sanitize_option(k, v)
    if k then
        self.options[k] = v
        self:validate_option(k)
    end
end

function Opts:validate_option(key, options_obj)
--[[
    Validate an (already sanitized) option against its expected values.
    With options_obj a local options table can be provided,
    otherwise the global options stored in OPTIONS are checked.
--]]
    local package_opts = self.declarations
    local options = options_obj or self.options
    local unexpected
    if options[key] == 'default' then
        -- Replace 'default' with an actual value
        options[key] = package_opts[key][1]
        unexpected = options[key] == nil
    end
    if not lib.contains(package_opts[key], options[key]) and package_opts[key][2] then
        -- option value is not in the array of accepted values
        if type(package_opts[key][2]) == 'function' then package_opts[key][2](key, options[key])
        else unexpected = true
        end
    end
    if unexpected then
        err([[
    Unexpected value "%s" for option %s:
    authorized values are "%s"
    ]],
            options[key], key, table.concat(package_opts[key], ', ')
        )
    end
end

function Opts:validate_options(options_obj)
--[[
    Validate the given set of options against the option declaration
    table for the given prefix.
    With options_obj a local options table can be provided,
    otherwise the global options stored in OPTIONS are checked.
--]]
    for k, _ in lib.orderedpairs(self.declarations) do
        self:validate_option(k, options_obj)
    end
end


function optlib.is_alias()
--[[
    This function doesn't do anything, but if an option is defined
    as an alias, its second parameter will be this function, so the
    test declarations[k][2] == optlib.is_alias in Opts:sanitize_options
    will return true.
--]]
end


function optlib.is_dim(k, v)
--[[
    Type checking for options that accept a LaTeX dimension.
    This can be
    - a number (integer or float)
    - a number with unit
    - a (multiplied) TeX length
    (see error message in code for examples)
--]]
    if v == '' or v == false or tonumber(v) then return true end
    local n, sl, u = v:match('^%d*%.?%d*'), v:match('\\'), v:match('%a+')
    -- a value of number - backslash - length is a dimension
    -- invalid input will be prevented in by the LaTeX parser already
    if n and sl and u then return true end
    if n and lib.contains(lib.TEX_UNITS, u) then return true end
    err([[
Unexpected value "%s" for dimension %s:
should be either a number (for example "12"),
a number with unit, without space ("12pt"),
or a (multiplied) TeX length (".8\linewidth")
]],
        v, k
    )
end


function optlib.is_neg(k, _)
--[[
    Type check for a 'negative' option. At this stage,
    we only check that it begins with 'no'.
--]]
    return k:find('^no')
end


function optlib.is_num(_, v)
--[[
    Type check for number options
--]]
    return v == '' or tonumber(v)
end


function optlib.is_str(_, v)
--[[
    Type check for string options
--]]
    return type(v) == 'string'
end


function optlib.merge_options(base_opt, super_opt)
--[[
    Merge two tables.
    Create a new table as a copy of base_opt, then merge with
    super_opt. Entries in super_opt supersede (i.e. overwrite)
    colliding entries in base_opt.
--]]
    local result = {}
    for k, v in pairs(base_opt) do result[k] = v end
    for k, v in pairs(super_opt) do result[k] = v end
    return result
end


optlib.Opts = Opts
return optlib