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

sile-typesetter / sile / 9307175333

30 May 2024 06:08PM UTC coverage: 70.644% (-3.5%) from 74.124%
9307175333

push

github

web-flow
Merge b18390e74 into 70ff5c335

1910 of 2592 new or added lines in 108 files covered. (73.69%)

901 existing lines in 52 files now uncovered.

12203 of 17274 relevant lines covered (70.64%)

6112.16 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
NEW
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")
×
121
   if options then
×
122
      pl.tablex.update(theme, options)
×
123
   end
124
   if not SILE.scratch.autodoc then
×
125
      SILE.scratch.autodoc = {
×
126
         theme = theme,
127
      }
128
   end
129
end
130

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

286
   -- Documenting a parameter
287

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

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

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

315
   -- Documenting an environment
316

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

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

338
   -- Documenting a package name
339

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

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

356
   -- Documenting a class name
357

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

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

374
   -- Homogenizing the appearance of blocks of code
375

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

411
   self:registerCommand("autodoc:line", function (_, _)
×
412
      SILE.call("novbreak")
×
413
      SILE.call("fullrule", { thickness = "0.5pt" })
×
414
      SILE.call("novbreak")
×
415
   end, "Outputs a line used for surrounding code blocks (somewhat internal)")
×
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...
NEW
424
      local linedimen = SILE.types.length("0.75em")
×
NEW
425
      local linethickness = SILE.types.length("0.3pt")
×
NEW
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
×
NEW
429
      local innerindent = SILE.types.measurement("1em"):absolute()
×
430
      SILE.settings:temporarily(function ()
×
431
         SILE.settings:set("document.lskip", leftindent)
×
432
         SILE.settings:set("document.rskip", leftindent)
×
433

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 ()
×
NEW
445
               SILE.settings:set("document.lskip", SILE.types.node.glue(leftindent + innerindent))
×
NEW
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")
×
449
            end)
450

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

464
package.documentation = [[
465
\begin{document}
466
The \autodoc:package{autodoc} package extracts documentation information from other packages.
467
It’s used to construct the SILE manual.
468
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.
469

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

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

474
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.
475

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

480
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.
481
This relies on SILE’s AST (abstract syntax tree) parsing, so you benefit from typing simplicity, syntax check, and even more—such as styling.%
482
\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.}
483
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.
484
Just type the command as it would appear in code, and it will be nicely typeset.
485
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.
486
Also, it is not adapted to math-related commands.
487
So it comes with many benefits, but also at a cost.
488

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

491
The \autodoc:command{\autodoc:setting}, \autodoc:command{\autodoc:command}, and \autodoc:command{\autodoc:environment} commands all check the validity and existence of their inputs.
492
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}.
493
Note, however, that for commands, it is applied recursively to the parsed AST—so it is a all-or-none trade-off.
494

495
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.
496

497
The \autodoc:environment{autodoc:codeblock} environment allows typesetting a block of code in a consistent way.
498
This is not a true verbatim environment, and you still have to escape SILE’s special characters within it
499
(unless calling commands is what you really intend doing there, obviously).
500
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).
501

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

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

509
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