asciidoctor-latexmath 1.0.0.beta.2 → 2.0.0.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +119 -36
- data/lib/asciidoctor/latexmath/attribute_resolver.rb +485 -0
- data/lib/asciidoctor/latexmath/cache/cache_entry.rb +31 -0
- data/lib/asciidoctor/latexmath/cache/cache_key.rb +33 -0
- data/lib/asciidoctor/latexmath/cache/disk_cache.rb +151 -0
- data/lib/asciidoctor/latexmath/command_runner.rb +116 -0
- data/lib/asciidoctor/latexmath/converters/html5.rb +72 -0
- data/lib/asciidoctor/latexmath/errors.rb +134 -0
- data/lib/asciidoctor/latexmath/html_builder.rb +103 -0
- data/lib/asciidoctor/latexmath/math_expression.rb +22 -0
- data/lib/asciidoctor/latexmath/path_utils.rb +46 -0
- data/lib/asciidoctor/latexmath/processors/block_processor.rb +74 -0
- data/lib/asciidoctor/latexmath/processors/inline_macro_processor.rb +70 -0
- data/lib/asciidoctor/latexmath/processors/statistics_postprocessor.rb +18 -0
- data/lib/asciidoctor/latexmath/render_request.rb +32 -0
- data/lib/asciidoctor/latexmath/renderer_service.rb +741 -0
- data/lib/asciidoctor/latexmath/rendering/pdf_to_png_renderer.rb +97 -0
- data/lib/asciidoctor/latexmath/rendering/pdf_to_svg_renderer.rb +84 -0
- data/lib/asciidoctor/latexmath/rendering/pdflatex_renderer.rb +166 -0
- data/lib/asciidoctor/latexmath/rendering/pipeline.rb +58 -0
- data/lib/asciidoctor/latexmath/rendering/renderer.rb +33 -0
- data/lib/asciidoctor/latexmath/rendering/tool_detector.rb +320 -0
- data/lib/asciidoctor/latexmath/rendering/toolchain_record.rb +21 -0
- data/lib/asciidoctor/latexmath/statistics/collector.rb +47 -0
- data/lib/asciidoctor/latexmath/support/conflict_registry.rb +75 -0
- data/lib/{asciidoctor-latexmath → asciidoctor/latexmath}/version.rb +1 -1
- data/lib/asciidoctor-latexmath.rb +93 -3
- metadata +34 -12
- data/lib/asciidoctor-latexmath/renderer.rb +0 -526
- data/lib/asciidoctor-latexmath/treeprocessor.rb +0 -369
|
@@ -1,526 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# SPDX-FileCopyrightText: 2025 Shuai Zhang
|
|
4
|
-
#
|
|
5
|
-
# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception
|
|
6
|
-
|
|
7
|
-
require "tmpdir"
|
|
8
|
-
require "fileutils"
|
|
9
|
-
require "open3"
|
|
10
|
-
require "shellwords"
|
|
11
|
-
require "digest"
|
|
12
|
-
require "pathname"
|
|
13
|
-
require "json"
|
|
14
|
-
require "cgi"
|
|
15
|
-
require "asciidoctor"
|
|
16
|
-
|
|
17
|
-
module Asciidoctor
|
|
18
|
-
module Latexmath
|
|
19
|
-
class RenderingError < StandardError; end
|
|
20
|
-
|
|
21
|
-
RenderResult = Struct.new(
|
|
22
|
-
:format,
|
|
23
|
-
:data,
|
|
24
|
-
:extension,
|
|
25
|
-
:width,
|
|
26
|
-
:height,
|
|
27
|
-
:inline_markup,
|
|
28
|
-
:basename,
|
|
29
|
-
keyword_init: true
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
class Renderer
|
|
33
|
-
DEFAULT_FORMAT = :svg
|
|
34
|
-
CACHE_VERSION = 1
|
|
35
|
-
LATEX_TEMPLATE = <<~'LATEX'
|
|
36
|
-
\documentclass[preview,border=2bp]{standalone}
|
|
37
|
-
\usepackage{amsmath}
|
|
38
|
-
\usepackage{amssymb}
|
|
39
|
-
%<PREAMBLE>
|
|
40
|
-
\begin{document}
|
|
41
|
-
%<BODY>
|
|
42
|
-
\end{document}
|
|
43
|
-
LATEX
|
|
44
|
-
|
|
45
|
-
def initialize(document)
|
|
46
|
-
@document = document
|
|
47
|
-
@format = resolve_format
|
|
48
|
-
@inline_attribute = document.attr? "latexmath-inline"
|
|
49
|
-
@ppi = resolve_ppi
|
|
50
|
-
@keep_artifacts = truthy_attr?("latexmath-keep-artifacts")
|
|
51
|
-
@pdf_engine = resolve_command(document.attr("pdflatex") || "pdflatex")
|
|
52
|
-
@pdf2svg = resolve_command(document.attr("latexmath-pdf2svg") || "pdf2svg") if @format == :svg || @inline_attribute
|
|
53
|
-
@png_tool = resolve_png_tool if @format == :png
|
|
54
|
-
@preamble = document.attr("latexmath-preamble")
|
|
55
|
-
@cache_enabled = cache_enabled?
|
|
56
|
-
@cache_dir = resolve_cache_dir if @cache_enabled
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def render(equation:, display:, inline: false, id: nil, asciidoc_source: nil, source_location: nil)
|
|
60
|
-
normalized_equation = normalize_equation(equation)
|
|
61
|
-
basename = sanitize_basename(id) || auto_basename(normalized_equation)
|
|
62
|
-
inline_embed = inline && @inline_attribute
|
|
63
|
-
signature = cache_signature(
|
|
64
|
-
equation: normalized_equation,
|
|
65
|
-
display: display,
|
|
66
|
-
inline: inline,
|
|
67
|
-
inline_embed: inline_embed
|
|
68
|
-
)
|
|
69
|
-
equation_digest = Digest::SHA256.hexdigest(normalized_equation)
|
|
70
|
-
|
|
71
|
-
if @cache_enabled
|
|
72
|
-
if (cached = load_cached_render(basename, signature, equation_digest, inline_embed))
|
|
73
|
-
copy_cached_artifacts(signature, basename) if @keep_artifacts
|
|
74
|
-
return cached
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
result = nil
|
|
79
|
-
Dir.mktmpdir("asciidoctor-latexmath-") do |dir|
|
|
80
|
-
tex_path = File.join(dir, "#{basename}.tex")
|
|
81
|
-
pdf_path = File.join(dir, "#{basename}.pdf")
|
|
82
|
-
|
|
83
|
-
latex_source = build_document(normalized_equation, display)
|
|
84
|
-
File.write(tex_path, latex_source)
|
|
85
|
-
begin
|
|
86
|
-
run_pdflatex(
|
|
87
|
-
tex_path,
|
|
88
|
-
dir,
|
|
89
|
-
latex_source: latex_source,
|
|
90
|
-
asciidoc_source: asciidoc_source,
|
|
91
|
-
source_location: source_location
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
unless File.exist?(pdf_path)
|
|
95
|
-
raise RenderingError, "pdflatex did not produce #{basename}.pdf"
|
|
96
|
-
end
|
|
97
|
-
rescue RenderingError
|
|
98
|
-
copy_artifacts(dir, basename)
|
|
99
|
-
raise
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
result = case @format
|
|
103
|
-
when :pdf
|
|
104
|
-
handle_pdf(pdf_path, dir, basename, inline_embed)
|
|
105
|
-
when :svg
|
|
106
|
-
handle_svg(pdf_path, dir, basename, inline_embed)
|
|
107
|
-
when :png
|
|
108
|
-
handle_png(pdf_path, dir, basename, inline_embed)
|
|
109
|
-
else
|
|
110
|
-
raise RenderingError, "Unsupported format: #{@format}"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
if @cache_enabled && result
|
|
114
|
-
persist_cached_render(signature, equation_digest, inline_embed, result, dir, basename)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
result
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
def resolve_format
|
|
123
|
-
raw = (@document.attr("latexmath-format") || DEFAULT_FORMAT).to_s.strip
|
|
124
|
-
raw = DEFAULT_FORMAT if raw.empty?
|
|
125
|
-
fmt = raw.downcase.to_sym
|
|
126
|
-
return fmt if %i[pdf svg png].include?(fmt)
|
|
127
|
-
|
|
128
|
-
warn %(Unknown latexmath-format '#{raw}', falling back to #{DEFAULT_FORMAT})
|
|
129
|
-
DEFAULT_FORMAT
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def resolve_ppi
|
|
133
|
-
value = (@document.attr("latexmath-ppi") || "300").to_f
|
|
134
|
-
value.positive? ? value : 300.0
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def resolve_png_tool
|
|
138
|
-
tool = @document.attr("latexmath-png-tool")
|
|
139
|
-
candidates = [tool, "magick", "convert", "pdftoppm"]
|
|
140
|
-
candidates.compact.each do |candidate|
|
|
141
|
-
resolved = resolve_command(candidate, silent: true)
|
|
142
|
-
return resolved if resolved
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
raise RenderingError, "No PNG conversion tool found; set latexmath-png-tool to a valid command (magick, convert, or pdftoppm)."
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def build_document(equation, display)
|
|
149
|
-
body = if latex_environment?(equation)
|
|
150
|
-
equation
|
|
151
|
-
elsif display
|
|
152
|
-
"\\[#{equation}\\]"
|
|
153
|
-
else
|
|
154
|
-
"\\(#{equation}\\)"
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
LATEX_TEMPLATE
|
|
158
|
-
.sub("%<PREAMBLE>") { @preamble ? "\n#{@preamble}\n" : "" }
|
|
159
|
-
.sub("%<BODY>") { body }
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def run_pdflatex(tex_path, dir, latex_source:, asciidoc_source: nil, source_location: nil)
|
|
163
|
-
command = [
|
|
164
|
-
@pdf_engine,
|
|
165
|
-
"-interaction=nonstopmode",
|
|
166
|
-
"-halt-on-error",
|
|
167
|
-
"-file-line-error",
|
|
168
|
-
"-output-directory",
|
|
169
|
-
dir,
|
|
170
|
-
tex_path
|
|
171
|
-
]
|
|
172
|
-
|
|
173
|
-
execute(command, work_dir: dir)
|
|
174
|
-
rescue RenderingError => e
|
|
175
|
-
latex_source ||= File.read(tex_path, mode: "r:UTF-8")
|
|
176
|
-
message_parts = [e.message.rstrip]
|
|
177
|
-
|
|
178
|
-
message_parts << "LaTeX source (#{tex_path}):\n#{latex_source}"
|
|
179
|
-
|
|
180
|
-
if asciidoc_source
|
|
181
|
-
location_hint = format_source_location(source_location)
|
|
182
|
-
header = "Asciidoc source#{location_hint}:"
|
|
183
|
-
message_parts << "#{header}\n#{asciidoc_source}"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
raise RenderingError, message_parts.join("\n\n")
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def handle_pdf(pdf_path, dir, basename, inline_embed)
|
|
190
|
-
copy_artifacts(dir, basename)
|
|
191
|
-
warn "latexmath-inline is ignored for pdf format." if inline_embed
|
|
192
|
-
RenderResult.new(format: :pdf, data: File.read(pdf_path, mode: "rb"), extension: "pdf", basename: basename)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def handle_svg(pdf_path, dir, basename, inline_embed)
|
|
196
|
-
svg_path = File.join(dir, "#{basename}.svg")
|
|
197
|
-
execute([@pdf2svg, pdf_path, svg_path], work_dir: dir)
|
|
198
|
-
svg_data = sanitize_svg(File.read(svg_path))
|
|
199
|
-
width, height = svg_dimensions(svg_data)
|
|
200
|
-
copy_artifacts(dir, basename)
|
|
201
|
-
|
|
202
|
-
if inline_embed
|
|
203
|
-
inline_markup = %(<span class="latexmath-inline">#{svg_data}</span>)
|
|
204
|
-
RenderResult.new(format: :svg, inline_markup: inline_markup, width: width, height: height, data: svg_data, extension: "svg", basename: basename)
|
|
205
|
-
else
|
|
206
|
-
RenderResult.new(format: :svg, data: File.read(svg_path, mode: "rb"), width: width, height: height, extension: "svg", basename: basename)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def handle_png(pdf_path, dir, basename, inline_embed)
|
|
211
|
-
png_path = File.join(dir, "#{basename}.png")
|
|
212
|
-
convert_pdf_to_png(pdf_path, png_path)
|
|
213
|
-
png_data = File.read(png_path, mode: "rb")
|
|
214
|
-
width, height = png_dimensions(png_path)
|
|
215
|
-
copy_artifacts(dir, basename)
|
|
216
|
-
|
|
217
|
-
if inline_embed
|
|
218
|
-
encoded = [png_data].pack("m0")
|
|
219
|
-
inline_markup = %(<span class="latexmath-inline"><img src="data:image/png;base64,#{encoded}" alt="latexmath"/></span>)
|
|
220
|
-
RenderResult.new(format: :png, inline_markup: inline_markup, width: width, height: height, data: png_data, extension: "png", basename: basename)
|
|
221
|
-
else
|
|
222
|
-
RenderResult.new(format: :png, data: png_data, width: width, height: height, extension: "png", basename: basename)
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def convert_pdf_to_png(pdf_path, png_path)
|
|
227
|
-
if File.basename(@png_tool) == "pdftoppm"
|
|
228
|
-
base = png_path.sub(/\.png\z/, "")
|
|
229
|
-
command = [@png_tool, "-png", "-r", @ppi.to_i.to_s, pdf_path, base]
|
|
230
|
-
execute(command, work_dir: File.dirname(pdf_path))
|
|
231
|
-
generated = Dir["#{base}*.png"].first
|
|
232
|
-
raise RenderingError, "pdftoppm did not produce a PNG file" unless generated
|
|
233
|
-
FileUtils.mv(generated, png_path)
|
|
234
|
-
else
|
|
235
|
-
density = @ppi.to_i
|
|
236
|
-
command = [@png_tool, "-density", density.to_s, pdf_path, "-quality", "100", png_path]
|
|
237
|
-
execute(command, work_dir: File.dirname(pdf_path))
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def svg_dimensions(svg_data)
|
|
242
|
-
if (match = svg_data.match(/viewBox="\s*0\s+0\s+([0-9.]+)\s+([0-9.]+)/))
|
|
243
|
-
[match[1].to_f, match[2].to_f]
|
|
244
|
-
elsif (match = svg_data.match(/width="([0-9.]+)(px)?"\s+height="([0-9.]+)(px)?"/))
|
|
245
|
-
[match[1].to_f, match[3].to_f]
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def png_dimensions(png_path)
|
|
250
|
-
IO.popen(["identify", "-format", "%w %h", png_path]) do |io|
|
|
251
|
-
output = io.read
|
|
252
|
-
return output.split.map!(&:to_i) if output
|
|
253
|
-
end
|
|
254
|
-
rescue Errno::ENOENT
|
|
255
|
-
nil
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def execute(command, work_dir: nil)
|
|
259
|
-
stdout_str, stderr_str, status = Open3.capture3(*command, chdir: work_dir)
|
|
260
|
-
return if status.success?
|
|
261
|
-
|
|
262
|
-
raise RenderingError, <<~MSG
|
|
263
|
-
Command failed: #{Shellwords.join(command)}
|
|
264
|
-
stdout: #{stdout_str}
|
|
265
|
-
stderr: #{stderr_str}
|
|
266
|
-
MSG
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def sanitize_svg(svg_data)
|
|
270
|
-
svg_data.sub(/\A<\?xml.*?\?>\s*/m, "").sub(/<!DOCTYPE.*?>\s*/m, "")
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def cache_enabled?
|
|
274
|
-
value = @document.attr("latexmath-cache")
|
|
275
|
-
return true if value.nil?
|
|
276
|
-
!%w[false off no 0].include?(value.to_s.downcase)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def resolve_cache_dir
|
|
280
|
-
attr = @document.attr("latexmath-cache-dir")
|
|
281
|
-
docdir = @document.attr("docdir")
|
|
282
|
-
if attr && !attr.to_s.strip.empty?
|
|
283
|
-
@document.normalize_system_path(attr, docdir)
|
|
284
|
-
else
|
|
285
|
-
base_attr = @document.attr("outdir") || docdir || Dir.pwd
|
|
286
|
-
base = @document.normalize_system_path(base_attr, docdir)
|
|
287
|
-
File.join(base, ".asciidoctor", "latexmath")
|
|
288
|
-
end
|
|
289
|
-
rescue
|
|
290
|
-
File.join(Dir.pwd, ".asciidoctor", "latexmath")
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def cache_signature(equation:, display:, inline:, inline_embed:)
|
|
294
|
-
components = [
|
|
295
|
-
@format,
|
|
296
|
-
equation,
|
|
297
|
-
display,
|
|
298
|
-
inline,
|
|
299
|
-
inline_embed,
|
|
300
|
-
@preamble.to_s,
|
|
301
|
-
@ppi,
|
|
302
|
-
@pdf_engine,
|
|
303
|
-
@pdf2svg,
|
|
304
|
-
@png_tool
|
|
305
|
-
]
|
|
306
|
-
Digest::SHA256.hexdigest(components.join("\u0000"))
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def cache_entry_dir(basename, signature)
|
|
310
|
-
File.join(@cache_dir, basename, signature)
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def cache_metadata_path(basename, signature)
|
|
314
|
-
File.join(cache_entry_dir(basename, signature), "metadata.json")
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def load_cached_render(basename, signature, equation_digest, inline_embed)
|
|
318
|
-
return unless @cache_dir
|
|
319
|
-
metadata_path = cache_metadata_path(basename, signature)
|
|
320
|
-
return unless File.file?(metadata_path)
|
|
321
|
-
|
|
322
|
-
metadata = JSON.parse(File.read(metadata_path), symbolize_names: true)
|
|
323
|
-
return unless metadata[:version] == CACHE_VERSION
|
|
324
|
-
return unless metadata[:signature] == signature
|
|
325
|
-
return unless metadata[:source_digest] == equation_digest
|
|
326
|
-
return unless metadata[:inline_embed] == inline_embed
|
|
327
|
-
|
|
328
|
-
data_path = metadata[:data_path]
|
|
329
|
-
return unless data_path
|
|
330
|
-
|
|
331
|
-
data_file = File.join(cache_entry_dir(basename, signature), data_path)
|
|
332
|
-
return unless File.file?(data_file)
|
|
333
|
-
|
|
334
|
-
data = File.binread(data_file)
|
|
335
|
-
if (encoding = metadata[:encoding])
|
|
336
|
-
begin
|
|
337
|
-
data.force_encoding(Encoding.find(encoding))
|
|
338
|
-
rescue
|
|
339
|
-
data.force_encoding(Encoding::BINARY)
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
RenderResult.new(
|
|
344
|
-
format: metadata[:format]&.to_sym,
|
|
345
|
-
data: data,
|
|
346
|
-
extension: metadata[:extension],
|
|
347
|
-
width: metadata[:width],
|
|
348
|
-
height: metadata[:height],
|
|
349
|
-
inline_markup: metadata[:inline_markup],
|
|
350
|
-
basename: basename
|
|
351
|
-
)
|
|
352
|
-
rescue
|
|
353
|
-
nil
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
def persist_cached_render(signature, equation_digest, inline_embed, result, dir, basename)
|
|
357
|
-
return unless @cache_dir
|
|
358
|
-
|
|
359
|
-
entry_dir = cache_entry_dir(basename, signature)
|
|
360
|
-
FileUtils.rm_rf(entry_dir)
|
|
361
|
-
FileUtils.mkdir_p(entry_dir)
|
|
362
|
-
|
|
363
|
-
data_filename = if result.extension
|
|
364
|
-
"result.#{result.extension}"
|
|
365
|
-
else
|
|
366
|
-
"result.bin"
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
if result.data
|
|
370
|
-
File.binwrite(File.join(entry_dir, data_filename), result.data)
|
|
371
|
-
else
|
|
372
|
-
data_filename = nil
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
metadata = {
|
|
376
|
-
version: CACHE_VERSION,
|
|
377
|
-
signature: signature,
|
|
378
|
-
format: result.format.to_s,
|
|
379
|
-
extension: result.extension,
|
|
380
|
-
width: result.width && result.width.to_f,
|
|
381
|
-
height: result.height && result.height.to_f,
|
|
382
|
-
inline_embed: inline_embed,
|
|
383
|
-
encoding: result.data&.encoding&.name,
|
|
384
|
-
data_path: data_filename,
|
|
385
|
-
source_digest: equation_digest,
|
|
386
|
-
inline_markup: result.inline_markup
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
artifacts = store_cache_artifacts(entry_dir, dir, basename)
|
|
390
|
-
metadata[:artifacts] = artifacts unless artifacts.empty?
|
|
391
|
-
metadata.delete(:data_path) unless data_filename
|
|
392
|
-
|
|
393
|
-
File.write(cache_metadata_path(basename, signature), JSON.pretty_generate(stringify_keys(metadata)))
|
|
394
|
-
rescue
|
|
395
|
-
nil
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def store_cache_artifacts(entry_dir, dir, basename)
|
|
399
|
-
return [] unless @keep_artifacts
|
|
400
|
-
|
|
401
|
-
artifacts = Dir.glob(File.join(dir, "#{basename}.*"))
|
|
402
|
-
return [] if artifacts.empty?
|
|
403
|
-
|
|
404
|
-
target_dir = File.join(entry_dir, "artifacts")
|
|
405
|
-
FileUtils.mkdir_p(target_dir)
|
|
406
|
-
|
|
407
|
-
artifacts.map do |path|
|
|
408
|
-
filename = File.basename(path)
|
|
409
|
-
FileUtils.cp(path, File.join(target_dir, filename))
|
|
410
|
-
filename
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def copy_cached_artifacts(signature, basename)
|
|
415
|
-
return unless @cache_dir
|
|
416
|
-
|
|
417
|
-
artifacts_dir = File.join(cache_entry_dir(basename, signature), "artifacts")
|
|
418
|
-
return unless Dir.exist?(artifacts_dir)
|
|
419
|
-
|
|
420
|
-
copy_artifacts(artifacts_dir, basename)
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def stringify_keys(hash)
|
|
424
|
-
hash.each_with_object({}) do |(key, value), memo|
|
|
425
|
-
memo[key.to_s] = value
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def copy_artifacts(dir, basename)
|
|
430
|
-
return unless @keep_artifacts
|
|
431
|
-
|
|
432
|
-
out_dir = artifacts_output_dir
|
|
433
|
-
FileUtils.mkdir_p(out_dir)
|
|
434
|
-
Dir.glob(File.join(dir, "#{basename}.*")).each do |path|
|
|
435
|
-
FileUtils.cp(path, File.join(out_dir, File.basename(path)))
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def artifacts_output_dir
|
|
440
|
-
attr = @document.attr("latexmath-artifacts-dir")
|
|
441
|
-
if attr && !attr.strip.empty?
|
|
442
|
-
@document.normalize_system_path(attr, @document.attr("docdir"))
|
|
443
|
-
else
|
|
444
|
-
@document.attr("imagesoutdir") || @document.attr("docdir")
|
|
445
|
-
end
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def auto_basename(equation)
|
|
449
|
-
"latexmath-#{Digest::MD5.hexdigest(equation)}"
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def sanitize_basename(id)
|
|
453
|
-
return unless id
|
|
454
|
-
id.to_s.gsub(/[^a-zA-Z0-9_.-]/, "-")
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
def resolve_command(cmd, silent: false)
|
|
458
|
-
return cmd if Pathname.new(cmd).absolute? && File.executable?(cmd)
|
|
459
|
-
|
|
460
|
-
resolved = which(cmd)
|
|
461
|
-
return resolved if resolved
|
|
462
|
-
|
|
463
|
-
return nil if silent
|
|
464
|
-
|
|
465
|
-
raise RenderingError, "Command '#{cmd}' could not be found in PATH"
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
def which(cmd)
|
|
469
|
-
path_ext = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
|
470
|
-
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
|
|
471
|
-
path_ext.each do |ext|
|
|
472
|
-
candidate = File.join(path, "#{cmd}#{ext}")
|
|
473
|
-
return candidate if File.executable?(candidate) && !File.directory?(candidate)
|
|
474
|
-
end
|
|
475
|
-
end
|
|
476
|
-
nil
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
def format_source_location(source_location)
|
|
480
|
-
return "" unless source_location
|
|
481
|
-
|
|
482
|
-
file = if source_location.respond_to?(:file)
|
|
483
|
-
source_location.file
|
|
484
|
-
elsif source_location.respond_to?(:path)
|
|
485
|
-
source_location.path
|
|
486
|
-
elsif source_location.respond_to?(:filename)
|
|
487
|
-
source_location.filename
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
line = if source_location.respond_to?(:lineno)
|
|
491
|
-
source_location.lineno
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
parts = []
|
|
495
|
-
parts << file if file && !file.to_s.empty?
|
|
496
|
-
parts << line if line
|
|
497
|
-
parts.empty? ? "" : " (#{parts.join(":")})"
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
def truthy_attr?(name)
|
|
501
|
-
value = @document.attr(name)
|
|
502
|
-
case value
|
|
503
|
-
when nil
|
|
504
|
-
false
|
|
505
|
-
when true
|
|
506
|
-
true
|
|
507
|
-
else
|
|
508
|
-
!%w[false off no 0].include?(value.to_s.downcase)
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
def latex_environment?(equation)
|
|
513
|
-
equation.match?(/\\begin\s*\{[^}]+\}/) && equation.match?(/\\end\s*\{[^}]+\}/)
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
def normalize_equation(equation)
|
|
517
|
-
return "" if equation.nil?
|
|
518
|
-
|
|
519
|
-
stripped = equation.to_s.strip
|
|
520
|
-
return "" if stripped.empty?
|
|
521
|
-
|
|
522
|
-
CGI.unescapeHTML(stripped).tr("\u00A0", " ")
|
|
523
|
-
end
|
|
524
|
-
end
|
|
525
|
-
end
|
|
526
|
-
end
|