summaryrefslogtreecommitdiff
path: root/support/texblend/texblend
blob: bb5c9d066104f9b678255fc43da698ad1d442e2c (plain)
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