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
|
#!/usr/bin/env texlua
kpse.set_program_name "luatex"
-- TeXBlend -- Compile Segments of LaTeX Documents
-- Copyright: Michal Hoftich <michal.h21@gmail.com> (2023)
--
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License, either version 1.3
-- of this license or (at your option) any later version.
-- The latest version of this license is in
-- http://www.latex-project.org/lppl.txt
-- and version 1.3 or later is part of all distributions of LaTeX
-- version 2005/12/01 or later.
--
-- This work has the LPPL maintenance status `maintained'.
--
-- The Current Maintainer of this work is Michal Hoftich
local lapp = require "lapp-mk4"
-- mkutils needs the logging library
logging = require "make4ht-logging"
local mkutils = require "mkutils"
local log = logging.new("TeXBlend")
local version_info = "TeXBlend version v0.1"
local cmd_options = [[TeXBlend: Compile Segments of LaTeX Documents
This tool compiles individual files that are included as parts of larger documents.
It utilizes the preamble of the main document but disregards all other included files.
Usage:
$ texblend [options] filename
Available options:
-e,--expand Expand variables in the template
-h,--help Print the help message
-H,--HTML Compile to HTML using TeX4ht
-l,--lualatex Select lualatex as compiler explicitly
-o,--output (default "") Set the output file name
-O,--texoptions (default "") Extra options that should be passed to the LaTeX compiler
-p,--pdflatex Select pdflatex as compiler explicitly
-P,--print Don't compile the document, print the expanded template instead
-s,--shell-escape Enable execution of external commands in LaTeX
-t,--template (default "") Use explicit template instead of the one provided in the input file
-v,--version Print version info
-x,--xelatex Select xelatex as compiler explicitly
<filename> (string) Use "-" if you want to pass document from the standard input
Documentation: https://www.kodymirus.cz/texblend/
Bug reports and development: https://github.com/michal-h21/texblend
]]
local default_template = [[\documentclass{article}
\begin{document}
\end{document}
]]
local function exit_on_error(status, msg)
-- function that tests function results
-- it prints error messages and exits if there was an error
if not status then
log:error( msg)
os.exit(1)
end
end
local function get_preamble(text)
-- try to get the document preamble
local preamble = {}
local found_begin = false
for line in text:gmatch("([^\n]*)") do
-- \begin{document} must be on a separate line, preceded only by whitespace
if line:match("^%s*\\begin{document}") then
found_begin = true
break
end
preamble[#preamble + 1] = line
end
if found_begin then
return table.concat(preamble, "\n")
end
return nil, "Cannot find \\begin{document} in the template. It must be placed on a separate line."
end
--
local function prepare_template(document, template)
-- try to read document preamble from the template
local preamble, msg = get_preamble(template)
if not preamble then return nil, msg end
-- fill template with the document
return string.format("%s\n\\begin{document}\n%s\n\\end{document}\n", preamble, document)
end
local function get_metadata(content)
-- for now, we support only TeXShop's style of metadata:
-- % !TeX program = program
-- % !TeX root = root
-- also variant with % !TEX TS-var is supported
-- program is used for the compilation and root is the template document
local metadata = {}
for key, value in content:gmatch("%%%s*![Tt][Ee][Xx] T?S?%-?(.-)%s*=%s*([^\n]+)") do
metadata[key] = value
end
metadata.program = metadata.program or "pdflatex"
-- remove possible white space at the end of root filename
if metadata.root then metadata.root = metadata.root:gsub("%s*$", "") end
return metadata
end
local function use_arguments(metadata, arguments)
-- this function modifies metadata using command line arguments
-- remove empty strings from arguments
for k, v in pairs(arguments) do
if v == '""' then arguments[k] = nil end
end
-- handle output file name
if arguments.output then
metadata.output = mkutils.remove_extension(arguments.output)
else
-- base output filename on the input filename if we don't get explicit filename
metadata.output = mkutils.remove_extension(arguments.filename)
end
-- handle program
metadata.program = arguments.pdflatex and "pdflatex" or metadata.program
metadata.program = arguments.lualatex and "lualatex" or metadata.program
metadata.program = arguments.xelatex and "xelatex" or metadata.program
-- handle the template
metadata.root = arguments.template or metadata.root
-- pass other useful flags
metadata.shell_escape = arguments["shell-escape"]
metadata.html = arguments.HTML
metadata.options = arguments.texoptions
return metadata
end
local function expand(metadata, template)
-- expand {{variablename}} tags in the template. The variablename key must exitst
-- in the metadata table. If the variable doesn't exist, nothing is rendered
local expanded = template:gsub("{{([%a%d}]-)}}", function(key)
return metadata[key] or ""
end)
return expanded
end
local function load_file(filename)
-- universal function to read files
if not filename then return nil, "No filename passed" end
local f = io.open(filename, "r")
if not f then return nil, string.format("Cannot open file: %s", filename) end
local content = f:read("*all")
f:close()
return content
end
local function load_template(filename)
-- with kpse, we can use root files in the local TEXMF tree, like ~/tex/latex/tpl/template.tex
local filename = kpse.find_file(filename, "tex")
return load_file(filename)
end
local function load_document(filename)
-- load the input file and its metadata
local content, msg = load_file(filename)
if not content then return content, msg end
return get_metadata(content), content
end
local function prepare_command(metadata,arguments)
-- prepare CLI program to be executed
local options = {}
local command
-- we support HTML creation thanks to make4ht
if metadata.html then
command = "make4ht"
options[#options + 1] = "-j " .. metadata.output
options[#options + 1] = metadata.shell_escape and "-s" or nil
if metadata.program == "lualatex" then
options[#options + 1] = "-l"
elseif metadata.program == "xelatex" then
options[#options + 1] = "-x"
end
local texoptions = metadata.options
options[#options+1] = texoptions
texoptions = texoptions or ""
-- if texoptions already contain the - character, don't add it
if not texoptions:match("%-%s") and not texoptions:match("%-$") then
options[#options+1] = "-"
end
else
command = metadata.program
options[#options + 1] = "-jobname=" .. metadata.output
options[#options + 1] = metadata.shell_escape and "-shell-escape" or nil
options[#options+1] = metadata.options
end
return string.format("%s %s", command, table.concat(options, " "))
end
local function compile(command, document)
-- execute the command
local cmd = io.popen(command, "w")
if not cmd then
exit_on_error(nil, "Cannot run command")
end
cmd:write(document)
cmd:close()
end
local arguments
-- you can set the _G.test=true if you want to load texblend as a library for testing
if not __test__ then
-- process command line options
arguments = lapp(cmd_options)
-- handle non file actions
-- arguments.help is handled automatically by lapp
if arguments.version then
print(version_info)
os.exit()
end
local metadata, document
local filename = arguments.filename
if filename == "-" then
-- read from the standard input if the filename is -
document = io.read("*all")
metadata = get_metadata(document)
else
-- otherwise, load the input file and get metadata
metadata, document = load_document(filename)
end
exit_on_error(metadata, document)
-- enrich metadata with command line arguments
metadata = use_arguments(metadata, arguments)
-- user needs to add explicit output filename when dealing with the standard input
if metadata.output == "-" then
exit_on_error(nil, "Empty output filename. Use the -o option")
end
metadata.template, msg = metadata.root and load_template(metadata.root) or default_template
exit_on_error(metadata.template, msg)
if arguments.expand then
metadata.template = expand(metadata,metadata.template)
end
local full_document, msg = prepare_template(document, metadata.template)
exit_on_error(full_document, msg)
if arguments.print then
print(full_document)
os.exit()
end
local command = prepare_command(metadata, arguments)
log:info(command)
compile(command, full_document)
else
return {
prepare_command = prepare_command
,prepare_template = prepare_template
,get_metadata = get_metadata
,use_arguments = use_arguments
,get_preamble = get_preamble
,expand = expand
}
end
|