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

sile-typesetter / sile / 4724920295

pending completion
4724920295

push

github

GitHub
Merge 9ac8a069b into 5c9805b8b

4 of 4 new or added lines in 3 files covered. (100.0%)

11714 of 15687 relevant lines covered (74.67%)

7016.48 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
}
17

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

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

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

102
function package:_init (options)
×
103
  base._init(self)
×
104
  self:loadPackage("inputfilter")
×
105
  self:loadPackage("rules")
×
106
  if options then pl.tablex.update(theme, options) end
×
107
  if not SILE.scratch.autodoc then
×
108
    SILE.scratch.autodoc = {
×
109
      theme = theme
×
110
    }
111
  end
112
end
113

114
function package.declareSettings (_)
×
115
  SILE.settings:declare({
×
116
    parameter = "autodoc.highlighting",
117
    default = false,
118
    type = "boolean",
119
    help = "Whether audodoc enables syntax highlighting"
×
120
  })
121
end
122

123
function package:registerRawHandlers ()
×
124

125
  self:registerRawHandler("autodoc:codeblock", function(options, content)
×
126
    SILE.call("autodoc:codeblock", options, { content[1] }) -- Still issues with SU.contentToString() witb raw content
×
127
  end)
128

129
end
130

131
function package:registerCommands ()
×
132

133
  -- Documenting a setting with good line-breaks
134
  local settingFilter = function (node, content)
135
    if type(node) == "table" then return node end
×
136
    local result = {}
×
137
    for token in SU.gtoke(node, "[%.]") do
×
138
      if token.string then
×
139
        result[#result+1] = token.string
×
140
      else
141
        result[#result+1] = token.separator
×
142
        result[#result+1] = self.class.packages.inputfilter:createCommand(
×
143
        content.pos, content.col, content.lno,
×
144
        "penalty", { penalty = 100 }
×
145
        )
146
      end
147
    end
148
    return result
×
149
  end
150

151
  self:registerCommand("package-documentation", function (_, content)
×
152
    local packname = content[1]
×
153
    SU.debug("autodoc", packname)
×
154
    local pkg = require("packages."..packname)
×
155
    if type(pkg) ~= "table" or not pkg.documentation then
×
156
      SU.error("Undocumented package " .. packname)
×
157
    end
158
    if type(pkg.registerCommands) == "function" then
×
159
      -- faking an uninstantiated package
160
      pkg.class = self.class
×
161
      pkg.registerCommands(pkg)
×
162
    end
163
    SILE.processString(pkg.documentation)
×
164
  end)
165

166
  self:registerCommand("autodoc:package:style", function (_, content)
×
167
    SILE.call("font", { weight = 700 }, function()
×
168
      colorWrapper("package", content)
×
169
    end)
170
  end)
171

172
  self:registerCommand("autodoc:code:style", function (options, content)
×
173
    -- options.type is used to distinguish the type of code element and style
174
    -- it accordingly: "ast", "setting", "environment" shall select the font
175
    -- (by default, using \code) and color, the other (lower-level in an AST)
176
    -- shall select only the color.
177
    if options.type == "ast" then
×
178
      SILE.call("code", {}, content)
×
179
    elseif options.type == "setting" then
×
180
      SILE.call("code", {}, function()
×
181
        colorWrapper(options.type, content)
×
182
      end)
183
    elseif options.type == "environment" then
×
184
      SILE.call("code", {}, function()
×
185
        colorWrapper("command", content)
×
186
      end)
187
    else
188
      colorWrapper(options.type, content)
×
189
    end
190
  end)
191

192
  self:registerCommand("autodoc:setting", function (options, content)
×
193
    if type(content) ~= "table" then SU.error("Expected a table content") end
×
194
    if #content ~= 1 then SU.error("Expected a single element") end
×
195
    local name = type(content[1] == "string") and content[1]
×
196
    if not name then SU.error("Unexpected setting") end
×
197
    -- Conditional existence check (can be disable is passing check=false), e.g.
198
    -- for settings that would be define in another context.
199
    if SU.boolean(options.check, true) then
×
200
      SILE.settings:get(name) -- will issue an error if unknown
×
201
    end
202
    -- Inserts breakpoints after dots
203
    local nameWithBreaks = self.class.packages.inputfilter:transformContent(content, settingFilter)
×
204

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

208
  self:registerCommand("autodoc:internal:ast", function (options, content)
×
209
    if type(content) ~= "table" then SU.error("Expected a table content") end
×
210
    SILE.call("autodoc:code:style", { type = "ast" }, function ()
×
211
      typesetAST(options, content)
×
212
    end)
213
  end, "Outputs a nicely typeset AST (low-level command).")
×
214

215
  self:registerCommand("autodoc:internal:bracketed", function (_, content)
×
216
    SILE.typesetter:typeset("⟨")
×
217
    SILE.call("autodoc:code:style", { type = "bracketed" }, function()
×
218
      SILE.call("em", {}, content)
×
219
    end)
220
    SILE.call("kern", { width = "0.1em" }) -- fake italic correction.
×
221
    SILE.typesetter:typeset("⟩")
×
222
  end, "Outputs a nicely formatted user-given value within <brackets>.")
×
223

224
  self:registerCommand("autodoc:value", function (_, content)
×
225
    local value = type(content) == "table" and content[1] or content
×
226
    if type(value) ~= "string" then SU.error("Expected a string") end
×
227

228
    if value:sub(1, 1) == "<" and value:sub(-1) == ">" then
×
229
      SILE.call("autodoc:internal:bracketed", {}, { value:sub(2, -2) })
×
230
    else
231
      if value:match("[,=]") or value:match("^ ") or value:match(" $") then
×
232
        value = ([["%s"]]):format(value)
×
233
      end
234
      SILE.call("autodoc:code:style", { type = "value" }, { value })
×
235
    end
236
  end, "Outputs a nicely formatted argument within <brackets>.")
×
237

238
  -- Documenting a command, benefiting from AST parsing
239

240
  self:registerCommand("autodoc:command", function (options, content)
×
241
    if type(content) ~= "table" then SU.error("Expected a table content") end
×
242
    if type(content[1]) ~= "table" then SU.error("Expected a command, got "..type(content[1]).." '"..content[1].."'") end
×
243

244
    SILE.call("autodoc:internal:ast", options, content)
×
245
  end, "Outputs a formatted command, possibly checking its validity.")
×
246

247
  -- Documenting a parameter
248

249
  self:registerCommand("autodoc:parameter", function (_, content)
×
250
    if type(content) ~= "table" then SU.error("Expected a table content") end
×
251
    if #content ~= 1 then SU.error("Expected a single element") end
×
252
    local param = type(content[1] == "string") and content[1]
×
253

254
    local parts = {}
×
255
    for v in string.gmatch(param, "[^=]+") do
×
256
      parts[#parts+1] = v
×
257
    end
258
    SILE.call("autodoc:code:style", { type = "ast" }, function ()
×
259
      if #parts < 1 or #parts > 2 then SU.error("Unexpected parameter '"..param.."'") end
×
260
      SILE.call("autodoc:code:style", { type = "parameter" }, { parts[1] })
×
261
      if #parts == 2 then
×
262
        SILE.typesetter:typeset("=")
×
263

264
        SILE.call("penalty", { penalty = 100 }, nil) -- Quite decent to break here if need be.
×
265
        SILE.call("autodoc:value", {}, { parts[2] })
×
266
      end
267
    end)
268
  end, "Outputs a nicely presented parameter, possibly with a value.")
×
269

270
  -- Documenting an environment
271

272
  self:registerCommand("autodoc:environment", function (options, content)
×
273
    if type(content) ~= "table" then SU.error("Expected a table content") end
×
274
    if #content ~= 1 then SU.error("Expected a single element") end
×
275
    local name = type(content[1] == "string") and content[1]
×
276
    if not name then SU.error("Unexpected environment") end
×
277
    -- Conditional existence check
278
    if SU.boolean(options.check, true) then
×
279
      if not SILE.Commands[name] then SU.error("Unknown command "..name) end
×
280
    end
281

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

285
  -- Documenting a package name
286

287
  self:registerCommand("autodoc:package", function (_, content)
×
288
    if type(content) ~= "table" then SU.error("Expected a table content") end
×
289
    if #content ~= 1 then SU.error("Expected a single element") end
×
290
    local name = type(content[1] == "string") and content[1]
×
291
    if not name then SU.error("Unexpected package name") end
×
292
    -- We cannot really check package name to exist!
293

294
    SILE.call("autodoc:package:style", {}, { name })
×
295
  end, "Outputs a package name in code, checking its validity.")
×
296

297
  -- Homogenizing the appearance of blocks of code
298

299
  self:registerCommand("autodoc:codeblock", function(_, content)
×
300
    SILE.call("verbatim", {}, function ()
×
301
      SILE.call("autodoc:line")
×
302
      SILE.call("novbreak")
×
303
      SILE.process(content)
×
304
      SILE.call("autodoc:line")
×
305
    end)
306
  end, "Outputs its content as a standardized block of code")
×
307

308
  self:registerCommand("autodoc:line", function(_, _)
×
309
    -- Loosely derived from the \line command from the original SILE manual...
310
    SILE.call("novbreak")
×
311
    SILE.call("skip", { height = "5pt" })
×
312
    SILE.call("novbreak")
×
313
    SILE.call("noindent")
×
314
    SILE.call("hrule", { width = "100%lw", height = "0.3pt"})
×
315
    SILE.call("par")
×
316
    SILE.call("novbreak")
×
317
    SILE.call("skip", { height = "5pt" })
×
318
  end, "Ouputs a line used for surrounding code blocks")
×
319

320
end
321

322
package.documentation = [[
323
\begin{document}
324
This package extracts documentation information from other packages.
325
It’s used to construct the SILE manual.
326
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.
327

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

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

332
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.
333

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

338
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.
339
This relies on SILE’s AST (abstract syntax tree) parsing, so you benefit from typing simplicity, syntax check, and even more—such as styling.%
340
\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.}
341
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.
342
Just type the command as it would appear in code, and it will be nicely typeset.
343
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.
344
Also, it is not adapted to math-related commands.
345
So it comes with many benefits, but also at a cost.
346

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

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

353
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.
354

355
The \autodoc:environment{autodoc:codeblock} environment allows typesetting a block of code in a consistent way.
356
This is not a true verbatim environment, and you still have to escape SILE’s special characters within it
357
(unless calling commands is what you really intend doing there, obviously).
358
For convenience, the package also provides a \code{raw} handler going by the same name, where you do not
359
have to escape the special characters (backslashes, braces, percents).
360
\end{document}
361
]]
×
362

363
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