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

sile-typesetter / sile / 9322841962

31 May 2024 06:41PM UTC coverage: 47.763% (-8.9%) from 56.674%
9322841962

push

github

web-flow
Merge 71aa6f2a1 into bb89a7e79

8081 of 16919 relevant lines covered (47.76%)

246.83 hits per line

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

78.13
/core/settings.lua
1
--- core settings instance
2
--- @module SILE.settings
3

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

8
--- @type settings
9
local settings = pl.class()
7✔
10

11
function settings:_init ()
7✔
12
   self.state = {}
7✔
13
   self.declarations = {}
7✔
14
   self.stateQueue = {}
7✔
15
   self.defaults = {}
7✔
16
   self.hooks = {}
7✔
17

18
   self:declare({
7✔
19
      parameter = "document.language",
20
      type = "string",
21
      default = "en",
22
      help = "Locale for localized language support",
23
   })
24

25
   self:declare({
14✔
26
      parameter = "document.parindent",
27
      type = "glue",
28
      default = SILE.types.node.glue("1bs"),
21✔
29
      help = "Glue at start of paragraph",
30
   })
31

32
   self:declare({
14✔
33
      parameter = "document.baselineskip",
34
      type = "vglue",
35
      default = SILE.types.node.vglue("1.2em plus 1pt"),
14✔
36
      help = "Leading",
37
   })
38

39
   self:declare({
14✔
40
      parameter = "document.lineskip",
41
      type = "vglue",
42
      default = SILE.types.node.vglue("1pt"),
14✔
43
      help = "Leading",
44
   })
45

46
   self:declare({
14✔
47
      parameter = "document.parskip",
48
      type = "vglue",
49
      default = SILE.types.node.vglue("0pt plus 1pt"),
14✔
50
      help = "Leading",
51
   })
52

53
   self:declare({
7✔
54
      parameter = "document.spaceskip",
55
      type = "length or nil",
56
      default = nil,
57
      help = "The length of a space (if nil, then measured from the font)",
58
   })
59

60
   self:declare({
7✔
61
      parameter = "document.rskip",
62
      type = "glue or nil",
63
      default = nil,
64
      help = "Skip to be added to right side of line",
65
   })
66

67
   self:declare({
7✔
68
      parameter = "document.lskip",
69
      type = "glue or nil",
70
      default = nil,
71
      help = "Skip to be added to left side of line",
72
   })
73

74
   self:declare({
7✔
75
      parameter = "document.zenkakuchar",
76
      default = "あ",
77
      type = "string",
78
      help = "The character measured to determine the length of a zenkaku width (全角幅)",
79
   })
80

81
   SILE.registerCommand(
14✔
82
      "set",
7✔
83
      function (options, content)
84
         local makedefault = SU.boolean(options.makedefault, false)
3✔
85
         local reset = SU.boolean(options.reset, false)
3✔
86
         local value = options.value
3✔
87
         if content and (type(content) == "function" or content[1]) then
3✔
88
            if makedefault then
2✔
89
               SU.warn(
×
90
                  "Are you sure meant to set default settings *and* pass content to ostensibly apply them to temporarily?"
91
               )
92
            end
93
            self:temporarily(function ()
4✔
94
               if options.parameter then
2✔
95
                  local parameter = SU.required(options, "parameter", "\\set command")
2✔
96
                  self:set(parameter, value, makedefault, reset)
2✔
97
               end
98
               SILE.process(content)
2✔
99
            end)
100
         else
101
            local parameter = SU.required(options, "parameter", "\\set command")
1✔
102
            self:set(parameter, value, makedefault, reset)
1✔
103
         end
104
      end,
105
      "Set a SILE parameter <parameter> to value <value> (restoring the value afterwards if <content> is provided)",
7✔
106
      nil,
7✔
107
      true
108
   )
7✔
109
end
110

111
--- Stash the current values of all settings in a stack to be returned to later
112
function settings:pushState ()
7✔
113
   if not self then
34✔
114
      return deprecator()
×
115
   end
116
   table.insert(self.stateQueue, self.state)
34✔
117
   self.state = pl.tablex.copy(self.state)
68✔
118
end
119

120
--- Return the most recently pushed set of values in the setting stack
121
function settings:popState ()
7✔
122
   if not self then
34✔
123
      return deprecator()
×
124
   end
125
   local previous = self.state
34✔
126
   self.state = table.remove(self.stateQueue)
68✔
127
   for parameter, oldvalue in pairs(previous) do
1,860✔
128
      if self.hooks[parameter] then
1,826✔
129
         local newvalue = self.state[parameter]
1,826✔
130
         if oldvalue ~= newvalue then
1,826✔
131
            self:runHooks(parameter, newvalue)
76✔
132
         end
133
      end
134
   end
135
end
136

137
--- Declare a new setting
138
--- @tparam table specs { parameter, type, default, help, hook, ... } declaration specification
139
function settings:declare (spec)
7✔
140
   if not spec then
436✔
141
      return deprecator()
×
142
   end
143
   if spec.name then
436✔
144
      SU.deprecated(
×
145
         "'name' argument of SILE.settings:declare",
146
         "'parameter' argument of SILE.settings:declare",
147
         "0.10.10",
148
         "0.11.0"
149
      )
150
   end
151
   if self.declarations[spec.parameter] then
436✔
152
      SU.debug("settings", "Attempt to re-declare setting:", spec.parameter)
52✔
153
      return
52✔
154
   end
155
   self.declarations[spec.parameter] = spec
384✔
156
   self.hooks[spec.parameter] = {}
384✔
157
   if spec.hook then
384✔
158
      self:registerHook(spec.parameter, spec.hook)
×
159
   end
160
   self:set(spec.parameter, spec.default, true)
384✔
161
end
162

163
--- Reset all settings to their registered default values.
164
function settings:reset ()
7✔
165
   if not self then
×
166
      return deprecator()
×
167
   end
168
   for k, _ in pairs(self.state) do
×
169
      self:set(k, self.defaults[k])
×
170
   end
171
end
172

173
--- Restore all settings to the value they had in the top-level state,
174
-- that is at the tap of the settings stack (normally the document level).
175
function settings:toplevelState ()
7✔
176
   if not self then
4✔
177
      return deprecator()
×
178
   end
179
   if #self.stateQueue ~= 0 then
4✔
180
      for parameter, _ in pairs(self.state) do
214✔
181
         -- Bypass self:set() as the latter performs some tests and a cast,
182
         -- but the setting might not have been defined in the top level state
183
         -- (in which case, assume the default value).
184
         self.state[parameter] = self.stateQueue[1][parameter] or self.defaults[parameter]
210✔
185
      end
186
   end
187
end
188

189
--- Get the value of a setting
190
-- @tparam string parameter The full name of the setting to fetch.
191
-- @return Value of setting
192
function settings:get (parameter)
7✔
193
   -- HACK FIXME https://github.com/sile-typesetter/sile/issues/1699
194
   -- See comment on set() below.
195
   if parameter == "current.parindent" then
11,823✔
196
      return SILE.typesetter and SILE.typesetter.state.parindent
26✔
197
   end
198
   if not parameter then
11,797✔
199
      return deprecator()
×
200
   end
201
   if not self.declarations[parameter] then
11,797✔
202
      SU.error("Undefined setting '" .. parameter .. "'")
×
203
   end
204
   if type(self.state[parameter]) ~= "nil" then
11,797✔
205
      return self.state[parameter]
9,861✔
206
   else
207
      return self.defaults[parameter]
1,936✔
208
   end
209
end
210

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

262
--- Register a callback hook to be run when a setting changes.
263
-- @tparam string parameter Name of the setting to add a hook to.
264
-- @tparam function func Callback function accepting one argument (the new value).
265
function settings:registerHook (parameter, func)
7✔
266
   table.insert(self.hooks[parameter], func)
×
267
end
268

269
--- Trigger execution of callback hooks for a given setting.
270
-- @tparam string parameter The name of the parameter changes.
271
-- @param value The new value of the setting, passed as the first argument to the hook function.
272
function settings:runHooks (parameter, value)
7✔
273
   if self.hooks[parameter] then
556✔
274
      for _, func in ipairs(self.hooks[parameter]) do
556✔
275
         SU.debug("classhooks", "Running seting hook for", parameter)
×
276
         func(value)
×
277
      end
278
   end
279
end
280

281
--- Isolate a block of processing so that setting changes made during the block don't last past the block.
282
-- (Under the hood this just uses `:pushState()`, the processes the function, then runs `:popState()`)
283
-- @tparam function func A function wrapping the actions to take without affecting settings for future use.
284
function settings:temporarily (func)
7✔
285
   if not func then
25✔
286
      return deprecator()
×
287
   end
288
   self:pushState()
25✔
289
   func()
25✔
290
   self:popState()
25✔
291
end
292

293
--- Create a settings wrapper function that applies current settings to later content processing.
294
--- @treturn function a closure fuction accepting one argument (content) to process using
295
--- typesetter settings as they are at the time of closure creation.
296
function settings:wrap ()
7✔
297
   if not self then
×
298
      return deprecator()
×
299
   end
300
   local clSettings = pl.tablex.copy(self.state)
×
301
   return function (content)
302
      table.insert(self.stateQueue, self.state)
×
303
      self.state = clSettings
×
304
      SILE.process(content)
×
305
      self:popState()
×
306
   end
307
end
308

309
return settings
7✔
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