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

sile-typesetter / sile / 9428435077

08 Jun 2024 11:35AM UTC coverage: 64.56% (-9.9%) from 74.46%
9428435077

push

github

web-flow
Merge pull request #2047 from alerque/end-pars

23 of 46 new or added lines in 5 files covered. (50.0%)

1684 existing lines in 60 files now uncovered.

11145 of 17263 relevant lines covered (64.56%)

4562.45 hits per line

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

0.0
/packages/autodoc/init.lua
1
--
2
-- Documentation tooling for package designers.
3
--
4

5
local base = require("packages.base")
×
6

7
local package = pl.class(base)
×
8
package._name = "autodoc"
×
9

10
local theme = {
×
11
   command = "#1d4851", -- oil blue
12
   parameter = "#3f5218", -- some sort of dark green
13
   setting = "#42280e", -- some kind of dark brown
14
   bracketed = "#656565", -- some grey
15
   package = "#172557", -- saturated space blue
16
   note = "#525257", -- some asphalt grey hue
17
   class = "#6a2c54", -- some dark shaded magenta
18
   codeblock = "#303040", -- dark grey with a hint of blue
19
}
20

21
local colorWrapper = function (ctype, content)
22
   local color = SILE.scratch.autodoc.theme[ctype]
×
23
   if color and SILE.settings:get("autodoc.highlighting") and SILE.Commands["color"] then
×
24
      SILE.call("color", { color = color }, content)
×
25
   else
26
      SILE.process(content)
×
27
   end
28
end
29

30
local function optionSorter (o1, o2)
31
   -- options are in an associative table and Lua doesn't guarantee a fixed order.
32
   -- To ensure we get a consistent and stable output, we make with some wild guesses here
33
   -- (Quick'n dirty, could be improved!), and rely on alphabetical order otherwise.
34
   if o1 == "src" then
×
35
      return true
×
36
   end
37
   if o2 == "src" then
×
38
      return false
×
39
   end
40
   if o1 == "name" then
×
41
      return true
×
42
   end
43
   if o2 == "name" then
×
44
      return false
×
45
   end
46
   return o1 < o2
×
47
end
48

49
local function typesetAST (options, content)
50
   if not content then
×
51
      return
×
52
   end
53
   local seenCommandWithoutArg = false
×
54
   for i = 1, #content do
×
55
      local ast = content[i]
×
56
      if type(ast) == "string" then
×
57
         if seenCommandWithoutArg and ast:sub(1, 1) ~= " " and ast:sub(1, 1) ~= "{" then
×
58
            -- Touchy:
59
            -- There might have been a space or a {} here in the original code. The AST does
60
            -- not remember it, we only know we have to separate somehow the string from
61
            -- the previous command...
62
            SILE.typesetter:typeset(" ")
×
63
            seenCommandWithoutArg = false
×
64
         end
65
         if ast:sub(1, 1) == "<" and ast:sub(-1) == ">" then
×
66
            SILE.call("autodoc:internal:bracketed", {}, { ast:sub(2, -2) })
×
67
         else
68
            SILE.typesetter:typeset(ast)
×
69
         end
70
      elseif ast.command then
×
71
         local cmd = SILE.Commands[ast.command]
×
72
         if not cmd and SU.boolean(options.check, true) then
×
73
            SU.error("Unexpected command '" .. ast.command .. "'")
×
74
         end
75
         SILE.typesetter:typeset("\\")
×
76
         SILE.call("autodoc:code:style", { type = "command" }, { ast.command })
×
77
         local sortedOpts = {}
×
78
         for k, _ in pairs(ast.options) do
×
79
            table.insert(sortedOpts, k)
×
80
         end
81
         table.sort(sortedOpts, optionSorter)
×
82
         if #sortedOpts > 0 then
×
83
            SILE.typesetter:typeset("[")
×
84
            for iOpt, option in ipairs(sortedOpts) do
×
85
               SILE.call("autodoc:code:style", { type = "parameter" }, { option })
×
86
               SILE.typesetter:typeset("=")
×
87
               SILE.call("penalty", { penalty = 100 }) -- Quite decent to break here if need be.
×
88
               SILE.call("autodoc:value", {}, { ast.options[option] })
×
89
               if iOpt == #sortedOpts then
×
90
                  SILE.typesetter:typeset("]")
×
91
               else
92
                  SILE.typesetter:typeset(", ")
×
93
               end
94
            end
95
         end
96
         if #ast >= 1 then
×
97
            SILE.call("penalty", { penalty = 200 }) -- Less than optimal break.
×
98
            SILE.typesetter:typeset("{")
×
99
            typesetAST(options, ast)
×
100
            SILE.typesetter:typeset("}")
×
101
         else
102
            seenCommandWithoutArg = true
×
103
         end
104
      elseif ast.id == "content" or (not ast.command and not ast.id) then
×
105
         -- Due to the way it is implemented, the SILE-inputter may generate such
106
         -- nodes in the AST. It's poorly documented, so it's not clear why they
107
         -- are even kept there (esp. the "content" nodes), but anyhow, as
108
         -- far as autodoc is concerned for presentation purposes, just
109
         -- recurse into them.
110
         typesetAST(options, ast)
×
111
      else
112
         SU.error("Unrecognized AST element, type " .. type(ast))
×
113
      end
114
   end
115
end
116

117
function package:_init (options)
×
118
   base._init(self)
×
119
   self:loadPackage("inputfilter")
×
120
   self:loadPackage("rules")
×
NEW
121
   self:loadPackage("raiselower")
×
122
   if options then
×
123
      pl.tablex.update(theme, options)
×
124
   end
125
   if not SILE.scratch.autodoc then
×
126
      SILE.scratch.autodoc = {
×
127
         theme = theme,
128
      }
129
   end
130
end
131

132
function package.declareSettings (_)
×
133
   SILE.settings:declare({
×
134
      parameter = "autodoc.highlighting",
135
      default = false,
136
      type = "boolean",
137
      help = "Whether audodoc enables syntax highlighting",
138
   })
139
end
140

141
function package:registerRawHandlers ()
×
142
   self:registerRawHandler("autodoc:codeblock", function (options, content)
×
143
      SILE.call("autodoc:codeblock", options, { content[1] }) -- Still issues with SU.ast.contentToString() witb raw content
×
144
   end)
145
end
146

147
function package:registerCommands ()
×
148
   -- Documenting a setting with good line-breaks
149
   local settingFilter = function (node, content)
150
      if type(node) == "table" then
×
151
         return node
×
152
      end
153
      local result = {}
×
154
      for token in SU.gtoke(node, "[%.]") do
×
155
         if token.string then
×
156
            result[#result + 1] = token.string
×
157
         else
158
            result[#result + 1] = token.separator
×
159
            result[#result + 1] = self.class.packages.inputfilter:createCommand(
×
160
               content.pos,
×
161
               content.col,
×
162
               content.lno,
×
163
               "penalty",
164
               { penalty = 100 }
×
165
            )
166
         end
167
      end
168
      return result
×
169
   end
170

171
   self:registerCommand("package-documentation", function (_, content)
×
172
      local packname = content[1]
×
173
      SU.debug("autodoc", packname)
×
174
      local pkg = require("packages." .. packname)
×
175
      if type(pkg) ~= "table" or not pkg.documentation then
×
176
         SU.error("Undocumented package " .. packname)
×
177
      end
178
      if type(pkg.registerCommands) == "function" then
×
179
         -- faking an uninstantiated package
180
         pkg.class = self.class
×
181
         pkg.registerCommands(pkg)
×
182
      end
183
      SILE.processString(pkg.documentation)
×
184
   end)
185

186
   self:registerCommand("autodoc:package:style", function (_, content)
×
187
      SILE.call("font", { weight = 700 }, function ()
×
188
         colorWrapper("package", content)
×
189
      end)
190
   end)
191

192
   self:registerCommand("autodoc:class:style", function (_, content)
×
193
      SILE.call("font", { weight = 700 }, function ()
×
194
         colorWrapper("class", content)
×
195
      end)
196
   end)
197

198
   self:registerCommand("autodoc:code:style", function (options, content)
×
199
      -- options.type is used to distinguish the type of code element and style
200
      -- it accordingly: "ast", "setting", "environment" shall select the font
201
      -- (by default, using \code) and color, the other (lower-level in an AST)
202
      -- shall select only the color.
203
      if options.type == "ast" then
×
204
         SILE.call("code", {}, content)
×
205
      elseif options.type == "setting" then
×
206
         SILE.call("code", {}, function ()
×
207
            colorWrapper(options.type, content)
×
208
         end)
209
      elseif options.type == "environment" then
×
210
         SILE.call("code", {}, function ()
×
211
            colorWrapper("command", content)
×
212
         end)
213
      else
214
         colorWrapper(options.type, content)
×
215
      end
216
   end)
217

218
   self:registerCommand("autodoc:setting", function (options, content)
×
219
      if type(content) ~= "table" then
×
220
         SU.error("Expected a table content")
×
221
      end
222
      if #content ~= 1 then
×
223
         SU.error("Expected a single element")
×
224
      end
225
      local name = type(content[1] == "string") and content[1]
×
226
      if not name then
×
227
         SU.error("Unexpected setting")
×
228
      end
229
      -- Conditional existence check (can be disable is passing check=false), e.g.
230
      -- for settings that would be define in another context.
231
      if SU.boolean(options.check, true) then
×
232
         SILE.settings:get(name) -- will issue an error if unknown
×
233
      end
234
      -- Inserts breakpoints after dots
235
      local nameWithBreaks = self.class.packages.inputfilter:transformContent(content, settingFilter)
×
236

237
      SILE.call("autodoc:code:style", { type = "setting" }, nameWithBreaks)
×
238
   end, "Outputs a settings name in code, ensuring good line breaks and possibly checking their existence.")
×
239

240
   self:registerCommand("autodoc:internal:ast", function (options, content)
×
241
      if type(content) ~= "table" then
×
242
         SU.error("Expected a table content")
×
243
      end
244
      SILE.call("autodoc:code:style", { type = "ast" }, function ()
×
245
         typesetAST(options, content)
×
246
      end)
247
   end, "Outputs a nicely typeset AST (low-level command).")
×
248

249
   self:registerCommand("autodoc:internal:bracketed", function (_, content)
×
250
      SILE.typesetter:typeset("⟨")
×
251
      SILE.call("autodoc:code:style", { type = "bracketed" }, function ()
×
252
         SILE.call("em", {}, content)
×
253
      end)
254
      SILE.call("kern", { width = "0.1em" }) -- fake italic correction.
×
255
      SILE.typesetter:typeset("⟩")
×
256
   end, "Outputs a nicely formatted user-given value within <brackets>.")
×
257

258
   self:registerCommand("autodoc:value", function (_, content)
×
259
      local value = type(content) == "table" and content[1] or content
×
260
      if type(value) ~= "string" then
×
261
         SU.error("Expected a string")
×
262
      end
263

264
      if value:sub(1, 1) == "<" and value:sub(-1) == ">" then
×
265
         SILE.call("autodoc:internal:bracketed", {}, { value:sub(2, -2) })
×
266
      else
267
         if value:match("[,=]") or value:match("^ ") or value:match(" $") then
×
268
            value = ([["%s"]]):format(value)
×
269
         end
270
         SILE.call("autodoc:code:style", { type = "value" }, { value })
×
271
      end
272
   end, "Outputs a nicely formatted argument within <brackets>.")
×
273

274
   -- Documenting a command, benefiting from AST parsing
275

276
   self:registerCommand("autodoc:command", function (options, content)
×
277
      if type(content) ~= "table" then
×
278
         SU.error("Expected a table content")
×
279
      end
280
      if type(content[1]) ~= "table" then
×
281
         SU.error("Expected a command, got " .. type(content[1]) .. " '" .. content[1] .. "'")
×
282
      end
283

284
      SILE.call("autodoc:internal:ast", options, content)
×
285
   end, "Outputs a formatted command, possibly checking its validity.")
×
286

287
   -- Documenting a parameter
288

289
   self:registerCommand("autodoc:parameter", function (_, content)
×
290
      if type(content) ~= "table" then
×
291
         SU.error("Expected a table content")
×
292
      end
293
      if #content ~= 1 then
×
294
         SU.error("Expected a single element")
×
295
      end
296
      local param = type(content[1] == "string") and content[1]
×
297

298
      local parts = {}
×
299
      for v in string.gmatch(param, "[^=]+") do
×
300
         parts[#parts + 1] = v
×
301
      end
302
      SILE.call("autodoc:code:style", { type = "ast" }, function ()
×
303
         if #parts < 1 or #parts > 2 then
×
304
            SU.error("Unexpected parameter '" .. param .. "'")
×
305
         end
306
         SILE.call("autodoc:code:style", { type = "parameter" }, { parts[1] })
×
307
         if #parts == 2 then
×
308
            SILE.typesetter:typeset("=")
×
309

310
            SILE.call("penalty", { penalty = 100 }, nil) -- Quite decent to break here if need be.
×
311
            SILE.call("autodoc:value", {}, { parts[2] })
×
312
         end
313
      end)
314
   end, "Outputs a nicely presented parameter, possibly with a value.")
×
315

316
   -- Documenting an environment
317

318
   self:registerCommand("autodoc:environment", function (options, content)
×
319
      if type(content) ~= "table" then
×
320
         SU.error("Expected a table content")
×
321
      end
322
      if #content ~= 1 then
×
323
         SU.error("Expected a single element")
×
324
      end
325
      local name = type(content[1] == "string") and content[1]
×
326
      if not name then
×
327
         SU.error("Unexpected environment")
×
328
      end
329
      -- Conditional existence check
330
      if SU.boolean(options.check, true) then
×
331
         if not SILE.Commands[name] then
×
332
            SU.error("Unknown command " .. name)
×
333
         end
334
      end
335

336
      SILE.call("autodoc:code:style", { type = "environment" }, { name })
×
337
   end, "Outputs a command name in code, checking its validity.")
×
338

339
   -- Documenting a package name
340

341
   self:registerCommand("autodoc:package", function (_, content)
×
342
      if type(content) ~= "table" then
×
343
         SU.error("Expected a table content")
×
344
      end
345
      if #content ~= 1 then
×
346
         SU.error("Expected a single element")
×
347
      end
348
      local name = type(content[1] == "string") and content[1]
×
349
      if not name then
×
350
         SU.error("Unexpected package name")
×
351
      end
352
      -- We cannot really check package name to exist!
353

354
      SILE.call("autodoc:package:style", {}, { name })
×
355
   end, "Outputs a package name.")
×
356

357
   -- Documenting a class name
358

359
   self:registerCommand("autodoc:class", function (_, content)
×
360
      if type(content) ~= "table" then
×
361
         SU.error("Expected a table content")
×
362
      end
363
      if #content ~= 1 then
×
364
         SU.error("Expected a single element")
×
365
      end
366
      local name = type(content[1] == "string") and content[1]
×
367
      if not name then
×
368
         SU.error("Unexpected class name")
×
369
      end
370
      -- We cannot really check class name to exist!
371

372
      SILE.call("autodoc:class:style", {}, { name })
×
373
   end, "Outputs a class name.")
×
374

375
   -- Homogenizing the appearance of blocks of code
UNCOV
376
   self:registerCommand("autodoc:codeblock", function (_, content)
×
NEW
377
      SILE.typesetter:leaveHmode()
×
UNCOV
378
      SILE.settings:temporarily(function ()
×
379
         -- Note: We avoid using the verbatim environment and simplify things a bit
380
         -- (and try to better enforce novbreak points of insertion)
381
         SILE.call("verbatim:font")
×
382
         -- Rather than absolutizing 4 different values, just do it once and cache it
383
         local ex = SILE.types.measurement("1ex"):absolute()
×
384
         local pushline = function ()
NEW
385
            colorWrapper("note", function ()
×
NEW
386
               SILE.call("novbreak")
×
NEW
387
               SILE.typesetter:pushVglue(ex)
×
NEW
388
               SILE.call("novbreak")
×
NEW
389
               SILE.call("fullrule", { thickness = "0.5pt" })
×
NEW
390
               SILE.call("novbreak")
×
NEW
391
               SILE.typesetter:pushVglue(-ex)
×
NEW
392
               SILE.call("novbreak")
×
393
            end)
394
         end
395
         SILE.settings:set("typesetter.parseppattern", "\n")
×
396
         SILE.settings:set("typesetter.obeyspaces", true)
×
397
         SILE.settings:set("document.parindent", SILE.types.node.glue())
×
398
         SILE.settings:set("document.parskip", SILE.types.node.vglue(0.3 * ex))
×
399
         SILE.settings:set("document.baselineskip", SILE.types.node.glue(2.3 * ex))
×
400
         SILE.settings:set("document.spaceskip", SILE.types.length("1spc"))
×
401
         SILE.settings:set("shaper.variablespaces", false)
×
402
         SILE.settings:set("document.language", "und")
×
403
         colorWrapper("codeblock", function ()
×
NEW
404
            SILE.call("skip", { height = ex })
×
NEW
405
            pushline()
×
NEW
406
            SILE.typesetter:pushVglue(SILE.settings:get("document.parskip"))
×
407
            SILE.call("novbreak")
×
408
            SILE.process(content)
×
409
            SILE.call("novbreak")
×
NEW
410
            SILE.typesetter:pushVglue(SILE.settings:get("document.parskip"))
×
NEW
411
            pushline()
×
412
         end)
NEW
413
         SILE.typesetter:leaveHmode()
×
414
      end)
415
   end, "Outputs its content as a standardized block of code")
×
416

417
   self:registerCommand("autodoc:example", function (_, content)
×
418
      -- Loosely derived from the \examplefont command from the original SILE manual...
419
      SILE.call("font", { family = "Cormorant Infant", size = "1.1em" }, content)
×
420
   end, "Marks content as an example (possibly typeset in a distinct font, etc.)")
×
421

422
   self:registerCommand("autodoc:note", function (_, content)
×
423
      -- Replacing the \note command from the original SILE manual...
424
      local linedimen = SILE.types.length("0.75em")
×
425
      local linethickness = SILE.types.length("0.3pt")
×
426
      local ls = SILE.settings:get("document.lskip") or SILE.types.node.glue()
×
427
      local p = SILE.settings:get("document.parindent")
×
428
      local leftindent = (p.width:absolute() + ls.width:absolute()).length -- fixed part
×
429
      local innerindent = SILE.types.measurement("1em"):absolute()
×
430
      SILE.settings:temporarily(function ()
×
NEW
431
         SILE.typesetter:leaveHmode()
×
432
         SILE.settings:set("document.lskip", leftindent)
×
433
         SILE.settings:set("document.rskip", leftindent)
×
UNCOV
434
         SILE.call("noindent")
×
435
         colorWrapper("note", function ()
×
436
            SILE.call("hrule", { width = linethickness, height = linethickness, depth = linedimen })
×
437
            SILE.call("hrule", { width = 3 * linedimen, height = linethickness })
×
438
            SILE.call("hfill")
×
439
            SILE.call("hrule", { width = 3 * linedimen, height = linethickness })
×
440
            SILE.call("hrule", { width = linethickness, height = linethickness, depth = linedimen })
×
441

442
            SILE.call("noindent")
×
443
            SILE.call("novbreak")
×
444
            SILE.settings:temporarily(function ()
×
445
               SILE.settings:set("document.lskip", SILE.types.node.glue(leftindent + innerindent))
×
446
               SILE.settings:set("document.rskip", SILE.types.node.glue(leftindent + innerindent))
×
447
               SILE.call("font", { size = "0.95em", style = "italic " }, content)
×
448
               SILE.call("novbreak")
×
NEW
449
               SILE.typesetter:pushVglue(SILE.types.node.vglue(-0.5 * linedimen))
×
NEW
450
               SILE.call("novbreak")
×
451
            end)
452

453
            SILE.call("noindent")
×
454
            SILE.call("hrule", { width = linethickness, depth = linethickness, height = linedimen })
×
455
            SILE.call("hrule", { width = 3 * linedimen, depth = linethickness })
×
456
            SILE.call("hfill")
×
457
            SILE.call("hrule", { width = 3 * linedimen, depth = linethickness })
×
458
            SILE.call("hrule", { width = linethickness, depth = linethickness, height = linedimen })
×
459
         end)
NEW
460
         SILE.typesetter:leaveHmode()
×
461
      end)
462
   end, "Outputs its content as a note in a specific boxed and indented block")
×
463
end
464

465
package.documentation = [[
466
\begin{document}
467
The \autodoc:package{autodoc} package extracts documentation information from other packages.
468
It’s used to construct the SILE manual.
469
Keeping package documentation in the package itself keeps the documentation near the implementation, which (in theory) makes it easy for documentation and implementation to be in sync.
470

471
For that purpose, it provides the \autodoc:command{\package-documentation{<package>}} command.
472

473
Properly documented packages should export a \code{documentation} string containing their documentation, as a SILE document.
474

475
For documenters and package authors, \autodoc:package{autodoc} also provides commands that can be used in their package documentation to present various pieces of information in a consistent way.
476

477
Setting names can be fairly long (e.g., \code{namespace.area.some-stuff}).
478
The \autodoc:command{\autodoc:setting} command helps line-breaking them automatically at appropriate points, so that package authors do not have to do so
479
manually.
480

481
With the \autodoc:command{\autodoc:command} command, one can pass a simple command, or even an extended command with parameters and arguments, without the need for escaping special characters.
482
This relies on SILE’s AST (abstract syntax tree) parsing, so you benefit from typing simplicity, syntax check, and even more—such as styling.%
483
\footnote{If the \autodoc:package{color} package is loaded and the \autodoc:setting{autodoc.highlighting} setting is set to \code{true}, you get syntax highlighting.}
484
Moreover, for text content in parameter values or command arguments, if they are enclosed between angle brackets, they will be presented in a distinguishable style.
485
Just type the command as it would appear in code, and it will be nicely typeset.
486
It comes with a few caveats, though: parameters are not guaranteed to appear in the order you entered them, and some purely syntactic sequences are simply skipped and not reconstructed.
487
Also, it is not adapted to math-related commands.
488
So it comes with many benefits, but also at a cost.
489

490
The \autodoc:command{\autodoc:environment} command takes an environment name or a command, but displays it without a leading backslash.
491

492
The \autodoc:command{\autodoc:setting}, \autodoc:command{\autodoc:command}, and \autodoc:command{\autodoc:environment} commands all check the validity and existence of their inputs.
493
If you want to disable this feature (e.g., to refer to a setting or command defined in another package or module that might not yet be loaded), you can set the optional parameter \autodoc:parameter{check} to \code{false}.
494
Note, however, that for commands, it is applied recursively to the parsed AST—so it is a all-or-none trade-off.
495

496
The \autodoc:command{\autodoc:parameter} commands takes either a parameter name, possibly with a value (which as above, may be bracketed) and typesets it in the same fashion.
497

498
The \autodoc:environment{autodoc:codeblock} environment allows typesetting a block of code in a consistent way.
499
This is not a true verbatim environment, and you still have to escape SILE’s special characters within it
500
(unless calling commands is what you really intend doing there, obviously).
501
For convenience, the package also provides a \code{raw} handler going by the same name, where you do not have to escape the special characters (backslashes, braces, percents).
502

503
The \autodoc:command{\autodoc:example} marks its content as an example, possibly typeset in a different choice of font.
504

505
The \autodoc:command{\autodoc:note} outputs its content as a note, in a dedicated framed and indented block.
506
The \autodoc:command{\autodoc:package} and \autodoc:command{\autodoc:class} commands are used to format a package and class name.
507
\end{document}
508
]]
×
509

510
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

© 2026 Coveralls, Inc