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

sile-typesetter / sile / 12272864087

11 Dec 2024 08:55AM UTC coverage: 29.607% (-41.0%) from 70.614%
12272864087

push

github

web-flow
Merge 95cccf286 into f394f608c

5834 of 19705 relevant lines covered (29.61%)

429.05 hits per line

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

77.78
/core/settings.lua
1
local setenv = require("rusile").setenv
20✔
2

3
--- core settings instance
4
--- @module SILE.settings
5

6
local deprecator = function ()
7
   SU.deprecated("SILE.settings.*", "SILE.settings:*", "0.13.0", "0.15.0")
×
8
end
9

10
--- @type settings
11
local settings = pl.class()
20✔
12

13
function settings:_init ()
20✔
14
   self.state = {}
20✔
15
   self.declarations = {}
20✔
16
   self.stateQueue = {}
20✔
17
   self.defaults = {}
20✔
18
   self.hooks = {}
20✔
19

20
   self:declare({
40✔
21
      parameter = "document.language",
22
      type = "string",
23
      default = "en",
24
      hook = function (language)
25
         if SILE.scratch.loaded_languages and not SILE.scratch.loaded_languages[language] then
32✔
26
            SU.warn(([[
2✔
27
               Setting document.language to '%s', but support for '%s' has not been loaded
28

29
               Consider invoking \language[main=%s] which loads language support before
30
               setting it or manually calling SILE.languageSupport.loadLanguage("%s").
31
            ]]):format(language, language, language, language))
1✔
32
         end
33
         fluent:set_locale(language)
32✔
34
         os.setlocale(language)
32✔
35
         setenv("LANG", language)
32✔
36
      end,
37
      help = "Locale for localized language support",
38
   })
39

40
   self:declare({
40✔
41
      parameter = "document.parindent",
42
      type = "glue",
43
      default = SILE.types.node.glue("1bs"),
60✔
44
      help = "Glue at start of paragraph",
45
   })
46

47
   self:declare({
40✔
48
      parameter = "document.baselineskip",
49
      type = "vglue",
50
      default = SILE.types.node.vglue("1.2em plus 1pt"),
40✔
51
      help = "Leading",
52
   })
53

54
   self:declare({
40✔
55
      parameter = "document.lineskip",
56
      type = "vglue",
57
      default = SILE.types.node.vglue("1pt"),
40✔
58
      help = "Leading",
59
   })
60

61
   self:declare({
40✔
62
      parameter = "document.parskip",
63
      type = "vglue",
64
      default = SILE.types.node.vglue("0pt plus 1pt"),
40✔
65
      help = "Leading",
66
   })
67

68
   self:declare({
20✔
69
      parameter = "document.spaceskip",
70
      type = "length or nil",
71
      default = nil,
72
      help = "The length of a space (if nil, then measured from the font)",
73
   })
74

75
   self:declare({
20✔
76
      parameter = "document.rskip",
77
      type = "glue or nil",
78
      default = nil,
79
      help = "Skip to be added to right side of line",
80
   })
81

82
   self:declare({
20✔
83
      parameter = "document.lskip",
84
      type = "glue or nil",
85
      default = nil,
86
      help = "Skip to be added to left side of line",
87
   })
88

89
   self:declare({
20✔
90
      parameter = "document.zenkakuchar",
91
      default = "あ",
92
      type = "string",
93
      help = "The character measured to determine the length of a zenkaku width (全角幅)",
94
   })
95

96
   SILE.registerCommand(
40✔
97
      "set",
20✔
98
      function (options, content)
99
         local makedefault = SU.boolean(options.makedefault, false)
13✔
100
         local reset = SU.boolean(options.reset, false)
13✔
101
         local value = options.value
13✔
102
         if content and (type(content) == "function" or content[1]) then
13✔
103
            if makedefault then
×
104
               SU.warn(
×
105
                  "Are you sure meant to set default settings *and* pass content to ostensibly apply them to temporarily?"
106
               )
107
            end
108
            self:temporarily(function ()
×
109
               if options.parameter then
×
110
                  local parameter = SU.required(options, "parameter", "\\set command")
×
111
                  self:set(parameter, value, makedefault, reset)
×
112
               end
113
               SILE.process(content)
×
114
            end)
115
         else
116
            local parameter = SU.required(options, "parameter", "\\set command")
13✔
117
            self:set(parameter, value, makedefault, reset)
13✔
118
         end
119
      end,
120
      "Set a SILE parameter <parameter> to value <value> (restoring the value afterwards if <content> is provided)",
20✔
121
      nil,
20✔
122
      true
123
   )
20✔
124
end
125

126
--- Stash the current values of all settings in a stack to be returned to later
127
function settings:pushState ()
20✔
128
   if not self then
64✔
129
      return deprecator()
×
130
   end
131
   table.insert(self.stateQueue, self.state)
64✔
132
   self.state = pl.tablex.copy(self.state)
128✔
133
end
134

135
--- Return the most recently pushed set of values in the setting stack
136
function settings:popState ()
20✔
137
   if not self then
64✔
138
      return deprecator()
×
139
   end
140
   local previous = self.state
64✔
141
   self.state = table.remove(self.stateQueue)
128✔
142
   for parameter, oldvalue in pairs(previous) do
3,331✔
143
      if self.hooks[parameter] then
3,267✔
144
         local newvalue = self.state[parameter]
3,267✔
145
         if oldvalue ~= newvalue then
3,267✔
146
            self:runHooks(parameter, newvalue)
112✔
147
         end
148
      end
149
   end
150
end
151

152
--- Declare a new setting
153
--- @tparam table specs { parameter, type, default, help, hook, ... } declaration specification
154
function settings:declare (spec)
20✔
155
   if not spec then
1,193✔
156
      return deprecator()
×
157
   end
158
   if spec.name then
1,193✔
159
      SU.deprecated(
×
160
         "'name' argument of SILE.settings:declare",
161
         "'parameter' argument of SILE.settings:declare",
162
         "0.10.10",
163
         "0.11.0"
164
      )
165
   end
166
   if self.declarations[spec.parameter] then
1,193✔
167
      SU.debug("settings", "Attempt to re-declare setting:", spec.parameter)
208✔
168
      return
208✔
169
   end
170
   self.declarations[spec.parameter] = spec
985✔
171
   self.hooks[spec.parameter] = {}
985✔
172
   if spec.hook then
985✔
173
      self:registerHook(spec.parameter, spec.hook)
20✔
174
   end
175
   self:set(spec.parameter, spec.default, true)
985✔
176
end
177

178
--- Reset all settings to their registered default values.
179
function settings:reset ()
20✔
180
   if not self then
×
181
      return deprecator()
×
182
   end
183
   for k, _ in pairs(self.state) do
×
184
      self:set(k, self.defaults[k])
×
185
   end
186
end
187

188
--- Restore all settings to the value they had in the top-level state,
189
-- that is at the tap of the settings stack (normally the document level).
190
function settings:toplevelState ()
20✔
191
   if not self then
15✔
192
      return deprecator()
×
193
   end
194
   if #self.stateQueue ~= 0 then
15✔
195
      for parameter, _ in pairs(self.state) do
763✔
196
         -- Bypass self:set() as the latter performs some tests and a cast,
197
         -- but the setting might not have been defined in the top level state
198
         -- (in which case, assume the default value).
199
         self.state[parameter] = self.stateQueue[1][parameter] or self.defaults[parameter]
748✔
200
      end
201
   end
202
end
203

204
--- Get the value of a setting
205
-- @tparam string parameter The full name of the setting to fetch.
206
-- @return Value of setting
207
function settings:get (parameter)
20✔
208
   -- HACK FIXME https://github.com/sile-typesetter/sile/issues/1699
209
   -- See comment on set() below.
210
   if parameter == "current.parindent" then
12,318✔
211
      return SILE.typesetter and SILE.typesetter.state.parindent
109✔
212
   end
213
   if not parameter then
12,209✔
214
      return deprecator()
×
215
   end
216
   if not self.declarations[parameter] then
12,209✔
217
      SU.error("Undefined setting '" .. parameter .. "'")
×
218
   end
219
   if type(self.state[parameter]) ~= "nil" then
12,209✔
220
      return self.state[parameter]
6,236✔
221
   else
222
      return self.defaults[parameter]
5,973✔
223
   end
224
end
225

226
--- Set the value of a setting
227
-- @tparam string parameter The full name of the setting to change.
228
-- @param value The new value to change it to.
229
-- @tparam[opt=false] boolean makedefault Whether to make this the new default value.
230
-- @tparam[opt=false] boolean reset Whether to reset the value to the current default value.
231
function settings:set (parameter, value, makedefault, reset)
20✔
232
   -- HACK FIXME https://github.com/sile-typesetter/sile/issues/1699
233
   -- Anything dubbed current.xxx should likely NOT be a "setting" (subject
234
   -- to being pushed/popped via temporary stacking) and actually has its
235
   -- own lifecycle (e.g. reset for the next paragraph).
236
   -- These should be rather typesetter states, or something to that extent
237
   -- yet to clarify. Notably, current.parindent falls in that category,
238
   -- BUT probably current.hangAfter and current.hangIndent too.
239
   -- To avoid breaking too much code yet without being sure of the solution,
240
   -- we implement a hack of sorts for current.parindent only.
241
   -- Note moreover that current.parindent is currently probably a bad concept
242
   -- anyway:
243
   --   - It can be nil (= document.parindent applies)
244
   --   - It can be a zero-glue (\noindent, ragged environments, etc.)
245
   --   - It can be a valued glue set to document.parindent
246
   --     (e.g. from \indent, and document.parindent thus applies)
247
   --   - It could be another valued glue (uh, use case to ascertain)
248
   -- What we would _likely_ only need to track is whether document.parindent
249
   -- applies or not on the paragraph just composed afterwards...
250
   if parameter == "current.parindent" then
1,381✔
251
      if SILE.typesetter and not SILE.typesetter.state.hmodeOnly then
156✔
252
         SILE.typesetter.state.parindent = SU.cast("glue or nil", value)
290✔
253
      end
254
      return
156✔
255
   end
256
   if type(self) ~= "table" then
1,225✔
257
      return deprecator()
×
258
   end
259
   if not self.declarations[parameter] then
1,225✔
260
      SU.error("Undefined setting '" .. parameter .. "'")
×
261
   end
262
   if reset then
1,225✔
263
      if makedefault then
4✔
264
         SU.error("Can't set a new default and revert to and old default setting at the same time")
×
265
      end
266
      value = self.defaults[parameter]
4✔
267
   else
268
      value = SU.cast(self.declarations[parameter].type, value)
2,442✔
269
   end
270
   self.state[parameter] = value
1,225✔
271
   if makedefault then
1,225✔
272
      self.defaults[parameter] = value
991✔
273
   end
274
   self:runHooks(parameter, value)
1,225✔
275
end
276

277
--- Register a callback hook to be run when a setting changes.
278
-- @tparam string parameter Name of the setting to add a hook to.
279
-- @tparam function func Callback function accepting one argument (the new value).
280
function settings:registerHook (parameter, func)
20✔
281
   table.insert(self.hooks[parameter], func)
20✔
282
end
283

284
--- Trigger execution of callback hooks for a given setting.
285
-- @tparam string parameter The name of the parameter changes.
286
-- @param value The new value of the setting, passed as the first argument to the hook function.
287
function settings:runHooks (parameter, value)
20✔
288
   if self.hooks[parameter] then
1,337✔
289
      for _, func in ipairs(self.hooks[parameter]) do
1,369✔
290
         SU.debug("classhooks", "Running setting hook for", parameter)
32✔
291
         func(value)
32✔
292
      end
293
   end
294
end
295

296
--- Isolate a block of processing so that setting changes made during the block don't last past the block.
297
-- (Under the hood this just uses `:pushState()`, the processes the function, then runs `:popState()`)
298
-- @tparam function func A function wrapping the actions to take without affecting settings for future use.
299
function settings:temporarily (func)
20✔
300
   if not func then
42✔
301
      return deprecator()
×
302
   end
303
   self:pushState()
42✔
304
   func()
42✔
305
   self:popState()
42✔
306
end
307

308
--- Create a settings wrapper function that applies current settings to later content processing.
309
--- @treturn function a closure function accepting one argument (content) to process using
310
--- typesetter settings as they are at the time of closure creation.
311
function settings:wrap ()
20✔
312
   if not self then
×
313
      return deprecator()
×
314
   end
315
   local clSettings = pl.tablex.copy(self.state)
×
316
   return function (content)
317
      table.insert(self.stateQueue, self.state)
×
318
      self.state = clSettings
×
319
      SILE.process(content)
×
320
      self:popState()
×
321
   end
322
end
323

324
return settings
20✔
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

© 2026 Coveralls, Inc