-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcomment2tex.lua
More file actions
executable file
·344 lines (315 loc) · 11.3 KB
/
Copy pathcomment2tex.lua
File metadata and controls
executable file
·344 lines (315 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#!/usr/bin/env texlua
--- \subsection{The converter}
--- This file is the engine behind \cs{includebash} and \cs{includelua}. It plays two
--- roles from one source: a command-line program run under \texttt{texlua}, and a Lua
--- library loaded \emph{in process} by the \TeX\ wrappers under Lua\TeX.
---
--- Run as a program it reads a source file and emits \LaTeX\ on stdout (or to
--- \texttt{-o}). Loaded as a
--- library --- \texttt{require"comment2tex"} or \texttt{loadfile(...)("comment2tex")} ---
--- it returns a module table whose \texttt{write} entry converts a file directly, so
--- Lua\LaTeX\ needs neither shell escape nor a separate run.
---
--- The two modes are told apart by the first vararg of the chunk: \texttt{require} (and
--- our explicit loader) pass the module name \texttt{"comment2tex"}, whereas
--- \texttt{texlua} passes the command-line arguments. Per the Lua\TeX\ manual,
--- \texttt{texlua} is a stand-alone Lua~5.3 interpreter that fills the global
--- \texttt{arg} table just like stock \texttt{lua}.
local LIBRARY_MODE = (... == "comment2tex")
local M = {}
--- \subsubsection{Styles and wrappers}
--- A \emph{style} pairs a doc-comment prefix with a listing language; a \emph{wrapper}
--- is the pair of \texttt{begin}/\texttt{end} lines emitted around a code block. Both
--- are presets so that a single \texttt{--style}/\texttt{--wrapper} selects sensible
--- defaults, while \texttt{--comment}, \texttt{--language}, \texttt{--begin} and
--- \texttt{--end} still override any individual field.
M.styles = {
bash = { comment = "##", language = "bash" },
lua = { comment = "---", language = "{[5.3]Lua}" },
yaml = { comment = "##", language = "yaml" },
make = { comment = "##", language = "make" },
}
--- The \texttt{lstlisting} wrapper targets \LaTeX\ (the \texttt{listings} package); the
--- \texttt{plain} wrapper targets plain \TeX, where \cs{ctxlisting} reads its body
--- verbatim up to a line equal to \cs{endctxlisting}. Templates substitute
--- \texttt{@LANG@} with the language and \texttt{@CONT@} with \texttt{firstnumber=last,}
--- on every block after the first (empty on the first) so numbering stays continuous.
--- A wrapper may also carry a \texttt{preamble}, emitted once before the first block; the
--- \texttt{plain} wrapper uses it to announce the code-line count (as \texttt{@WIDTH@}, a
--- run of zeros matching its digit width) so the reader can size the line-number gutter.
M.wrappers = {
lstlisting = {
begin = "\\begin{lstlisting}[language=@LANG@,@CONT@numbers=left]",
finish = "\\end{lstlisting}",
},
plain = {
preamble = "\\ctxlistingtotal{@WIDTH@}",
begin = "\\ctxlisting%",
finish = "\\endctxlisting",
},
}
M.defaults = {
style = "bash",
wrapper = "lstlisting",
comment = nil,
language = nil,
begin = nil,
finish = nil,
preamble = nil,
}
--- \subsubsection{Option handling}
--- \texttt{new\_opts} layers explicit overrides on top of the defaults; \texttt{resolve}
--- then fills any still-empty field from the chosen style and wrapper presets.
function M.new_opts(over)
local o = {}
for k, v in pairs(M.defaults) do o[k] = v end
if over then
for k, v in pairs(over) do
if v ~= nil then o[k] = v end
end
end
return o
end
function M.resolve(o)
local style = M.styles[o.style]
if not style then
error("comment2tex: unknown style: " .. tostring(o.style)
.. " (expected bash, lua, yaml or make)")
end
local wrapper = M.wrappers[o.wrapper]
if not wrapper then
error("comment2tex: unknown wrapper: " .. tostring(o.wrapper)
.. " (expected lstlisting or plain)")
end
o.comment = o.comment or style.comment
o.language = o.language or style.language
o.begin = o.begin or wrapper.begin
o.finish = o.finish or wrapper.finish
o.preamble = o.preamble or wrapper.preamble
return o
end
--- \subsubsection{Conversion}
--- \texttt{expand\_tabs} turns tabs into spaces to the next \texttt{M.tabwidth} stop, so
--- a listing shows clean indentation instead of a raw tab (which a monospace font would
--- typeset as a missing-glyph box). It is applied only when \emph{weaving} code lines;
--- \cs{tangle} leaves the source byte-for-byte, so a \texttt{Makefile}'s recipe tabs
--- survive.
M.tabwidth = 4
local function expand_tabs(line, w)
if not line:find("\t", 1, true) then return line end
local out, col = {}, 0
for i = 1, #line do
local c = line:sub(i, i)
if c == "\t" then
local n = w - (col % w)
out[#out + 1] = string.rep(" ", n)
col = col + n
else
out[#out + 1] = c
col = col + 1
end
end
return table.concat(out)
end
--- The source is walked line by line. A line that starts with the comment prefix is
--- documentation: any open code block is closed and the line is emitted with the prefix
--- (and one optional following space) stripped. Anything else is code: a block is
--- opened if one is not already running and the line is emitted with tabs expanded. A
--- final close guarantees the listing is shut even when the file ends inside code.
function M.convert(o, lines, emit)
local prefix = o.comment
local plen = #prefix
local in_code = false
local block_count = 0
if o.preamble then
local n = 0
for _, line in ipairs(lines) do
if line:sub(1, plen) ~= prefix then n = n + 1 end
end
emit((o.preamble:gsub("@WIDTH@", ("0"):rep(#tostring(n)))))
end
local function open_code()
if not in_code then
block_count = block_count + 1
local cont = block_count == 1 and "" or "firstnumber=last,"
local line = o.begin:gsub("@LANG@", o.language):gsub("@CONT@", cont)
emit(line)
in_code = true
end
end
local function close_code()
if in_code then
emit(o.finish)
in_code = false
end
end
for _, line in ipairs(lines) do
if line:sub(1, plen) == prefix then
close_code()
emit((line:sub(plen + 1):gsub("^ ", "")))
else
open_code()
emit(expand_tabs(line, M.tabwidth))
end
end
close_code()
end
--- \subsubsection{Tangling}
--- The inverse of weaving: \texttt{tangle} drops every doc-comment line and emits the
--- rest of the source unchanged, so an annotated file yields its runnable form without a
--- separate \texttt{sed}/\texttt{grep} pass. The lines it keeps are exactly the lines
--- \texttt{convert} would have numbered, so a tangled file and its typeset listing agree
--- on line numbers.
function M.tangle(o, lines, emit)
local prefix = o.comment
local plen = #prefix
for _, line in ipairs(lines) do
if line:sub(1, plen) ~= prefix then
emit(line)
end
end
end
--- \subsubsection{File helpers}
--- \texttt{read\_lines} slurps a file and splits it into lines, keeping a final
--- unterminated line if present. \texttt{convert\_file} returns the converted \LaTeX\
--- as a string; \texttt{write\_file} sends it to \texttt{outfile}.
function M.read_lines(path)
local fh, err = io.open(path, "r")
if not fh then
error("comment2tex: cannot open input: " .. tostring(err))
end
local data = fh:read("*a")
fh:close()
local lines = {}
for line in (data .. "\n"):gmatch("(.-)\n") do
lines[#lines + 1] = line
end
if data:sub(-1) == "\n" then
lines[#lines] = nil
end
return lines
end
function M.convert_file(path, o)
o = M.resolve(o or M.new_opts())
local out = {}
M.convert(o, M.read_lines(path), function(line) out[#out + 1] = line end)
return table.concat(out, "\n") .. "\n"
end
function M.tangle_file(path, o)
o = M.resolve(o or M.new_opts())
local out = {}
M.tangle(o, M.read_lines(path), function(line) out[#out + 1] = line end)
return table.concat(out, "\n") .. "\n"
end
function M.write_file(infile, outfile, o)
local text = M.convert_file(infile, o)
local fh, err = io.open(outfile, "w")
if not fh then
error("comment2tex: cannot open output: " .. tostring(err))
end
fh:write(text)
fh:close()
return outfile
end
--- \subsubsection{The \TeX-facing entry point}
--- \cs{includebash} and \cs{includelua} call this through \cs{directlua}: it converts
--- \texttt{infile} to \texttt{outfile} for the given style and wrapper, entirely in
--- process. Keeping the signature positional keeps the \TeX\ side trivial.
function M.write(style, wrapper, infile, outfile)
return M.write_file(infile, outfile,
M.new_opts{ style = style, wrapper = wrapper })
end
--- \subsubsection{Command-line interface}
--- Parsing mirrors the documented options; \texttt{die} reports to stderr and exits
--- non-zero. Only reached when the file is executed by \texttt{texlua}, never when it
--- is loaded as a library.
local function usage(stream)
stream:write([[
Usage: comment2tex.lua [options] <input>
Weave a source with embedded LaTeX doc-comments into LaTeX, or with
--tangle strip the doc-comments back to the runnable source.
Options:
-s, --style NAME bash, yaml, make (##) or lua (---) [default: bash]
-w, --wrapper NAME lstlisting or plain [default: lstlisting]
-c, --comment PREFIX doc-comment prefix marking a doc line
-l, --language LANG listing language for code blocks
-b, --begin TEMPLATE listing begin template (@LANG@, @CONT@)
-e, --end TEMPLATE listing end template
-t, --tangle strip doc-comments; emit runnable source
-o, --output FILE write output here instead of stdout
-h, --help show this help
Templates substitute @LANG@ with the language and @CONT@ with
"firstnumber=last," on continuation blocks (empty on the first).
]])
end
local function die(msg)
io.stderr:write("comment2tex: " .. msg .. "\n")
os.exit(1)
end
function M.main(argv)
local over = {}
local tangle = false
local input
local i = 1
local function value(flag)
i = i + 1
local v = argv[i]
if v == nil then die("missing value for " .. flag) end
return v
end
while i <= #argv do
local a = argv[i]
if a == "-h" or a == "--help" then
usage(io.stdout); return 0
elseif a == "-s" or a == "--style" then
over.style = value(a)
elseif a == "-w" or a == "--wrapper" then
over.wrapper = value(a)
elseif a == "-c" or a == "--comment" then
over.comment = value(a)
elseif a == "-l" or a == "--language" then
over.language = value(a)
elseif a == "-b" or a == "--begin" then
over.begin = value(a)
elseif a == "-e" or a == "--end" then
over.finish = value(a)
elseif a == "-t" or a == "--tangle" then
tangle = true
elseif a == "-o" or a == "--output" then
over.output = value(a)
elseif a == "--" then
input = argv[i + 1]; break
elseif a:sub(1, 1) == "-" and a ~= "-" then
die("unknown option: " .. a)
elseif input == nil then
input = a
else
die("unexpected argument: " .. a)
end
i = i + 1
end
if not input then
usage(io.stderr); return 1
end
local ok, err = pcall(function()
local o = M.resolve(M.new_opts(over))
local text = tangle and M.tangle_file(input, o)
or M.convert_file(input, o)
if over.output then
local fh, e = io.open(over.output, "w")
if not fh then
error("comment2tex: cannot open output: " .. tostring(e))
end
fh:write(text)
fh:close()
else
io.stdout:write(text)
end
end)
if not ok then die(tostring(err):gsub("^comment2tex: ", "")) end
return 0
end
if not LIBRARY_MODE then
os.exit(M.main(arg))
end
return M