asciidoctor-latexmath 1.0.0.pre.dev.1 → 2.0.0
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 +748 -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 -515
- data/lib/asciidoctor-latexmath/treeprocessor.rb +0 -369
|
@@ -1,369 +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 "asciidoctor/extensions"
|
|
8
|
-
require "fileutils"
|
|
9
|
-
require "pathname"
|
|
10
|
-
require "cgi"
|
|
11
|
-
require_relative "renderer"
|
|
12
|
-
|
|
13
|
-
module Asciidoctor
|
|
14
|
-
module Latexmath
|
|
15
|
-
class Treeprocessor < Asciidoctor::Extensions::Treeprocessor
|
|
16
|
-
LINE_FEED = %(
|
|
17
|
-
)
|
|
18
|
-
STEM_INLINE_MACRO_RX = /\\?(stem|latexmath):([a-z,]*)\[(.*?[^\\])\]/m
|
|
19
|
-
|
|
20
|
-
def process(document)
|
|
21
|
-
return unless needs_processing?(document)
|
|
22
|
-
|
|
23
|
-
renderer = Renderer.new(document)
|
|
24
|
-
image_output_dir, image_target_dir = image_output_and_target_dir(document)
|
|
25
|
-
context = {renderer: renderer, image_output_dir: image_output_dir, image_target_dir: image_target_dir}
|
|
26
|
-
|
|
27
|
-
(document.find_by(context: :stem, traverse_documents: true) || []).dup.each do |stem|
|
|
28
|
-
handle_stem_block(stem, context)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
(document.find_by(traverse_documents: true) { |node| prose_candidate?(node) } || []).each do |prose|
|
|
32
|
-
handle_prose_block(prose, context)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
(document.find_by(content: :section) || []).each do |section|
|
|
36
|
-
handle_section_title(section, context)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
document.remove_attr "stem"
|
|
40
|
-
begin
|
|
41
|
-
(document.instance_variable_get :@header_attributes)&.delete("stem")
|
|
42
|
-
rescue
|
|
43
|
-
nil
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
nil
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def needs_processing?(document)
|
|
52
|
-
stem_nodes = document.find_by(context: :stem, traverse_documents: true) || []
|
|
53
|
-
return true if stem_nodes.any? { |stem| latexmath_node?(stem) }
|
|
54
|
-
|
|
55
|
-
if (stem_attr = default_stem_style(document))
|
|
56
|
-
return true if stem_attr == "latexmath"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
inline_candidates = document.find_by(traverse_documents: true) { |node| prose_candidate?(node) } || []
|
|
60
|
-
inline_candidates.any? { |node| contains_inline_stem?(node) }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def handle_stem_block(stem, context)
|
|
64
|
-
return unless latexmath_node?(stem)
|
|
65
|
-
|
|
66
|
-
content = extract_block_content(stem)
|
|
67
|
-
result = render_equation(
|
|
68
|
-
content,
|
|
69
|
-
display: true,
|
|
70
|
-
inline: false,
|
|
71
|
-
id: stem.id,
|
|
72
|
-
context: context,
|
|
73
|
-
asciidoc_source: stem.source,
|
|
74
|
-
source_location: stem.source_location
|
|
75
|
-
)
|
|
76
|
-
return unless result
|
|
77
|
-
|
|
78
|
-
parent = stem.parent
|
|
79
|
-
target, width, height = store_result(result, context)
|
|
80
|
-
return unless target
|
|
81
|
-
|
|
82
|
-
alt_text = stem.attr "alt", ((stem.context == :stem) ? stem.source : stem.content).to_s
|
|
83
|
-
attrs = {
|
|
84
|
-
"target" => target,
|
|
85
|
-
"alt" => alt_text,
|
|
86
|
-
"align" => "center"
|
|
87
|
-
}
|
|
88
|
-
if result.format == :png && width && height
|
|
89
|
-
attrs["width"] = width.to_i
|
|
90
|
-
attrs["height"] = height.to_i
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
replacement = create_image_block parent, attrs
|
|
94
|
-
replacement.id = stem.id if stem.id
|
|
95
|
-
if (title = stem.attributes["title"])
|
|
96
|
-
replacement.title = title
|
|
97
|
-
end
|
|
98
|
-
index = parent.blocks.index(stem)
|
|
99
|
-
parent.blocks[index] = replacement if index
|
|
100
|
-
rescue RenderingError => e
|
|
101
|
-
insert_block_error(stem, e)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def handle_prose_block(prose, context)
|
|
105
|
-
use_text_property = %i[list_item table_cell].include?(prose.context)
|
|
106
|
-
text = if use_text_property
|
|
107
|
-
prose.instance_variable_get(:@text)
|
|
108
|
-
else
|
|
109
|
-
(prose.lines || []) * LINE_FEED
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
updated_text, modified = inline_replace(text, prose, prose.document, context)
|
|
113
|
-
return unless modified
|
|
114
|
-
|
|
115
|
-
if use_text_property
|
|
116
|
-
prose.text = updated_text
|
|
117
|
-
else
|
|
118
|
-
prose.lines = updated_text.split(LINE_FEED)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def handle_section_title(section, context)
|
|
123
|
-
text = section.instance_variable_get(:@title)
|
|
124
|
-
updated_text, modified = inline_replace(text, section, section.document, context)
|
|
125
|
-
section.title = updated_text if modified
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def latexmath_node?(node)
|
|
129
|
-
style = node.style
|
|
130
|
-
if style
|
|
131
|
-
style_name = style.to_s
|
|
132
|
-
return true if %w[latexmath tex].include?(style_name)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
default_style = default_stem_style(node.document)
|
|
136
|
-
default_style == "latexmath"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def default_stem_style(document)
|
|
140
|
-
stem_attr = document.attr("stem")
|
|
141
|
-
return unless stem_attr
|
|
142
|
-
value = stem_attr.to_s.split(",").map(&:strip).find { |val| val == "latexmath" || val == "tex" }
|
|
143
|
-
(value == "tex") ? "latexmath" : value
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def extract_block_content(stem)
|
|
147
|
-
case stem.context
|
|
148
|
-
when :stem
|
|
149
|
-
stem.content
|
|
150
|
-
else
|
|
151
|
-
stem.source
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def render_equation(content, display:, inline:, context:, id: nil, asciidoc_source: nil, source_location: nil)
|
|
156
|
-
context[:renderer].render(
|
|
157
|
-
equation: normalize_equation(content),
|
|
158
|
-
display: display,
|
|
159
|
-
inline: inline,
|
|
160
|
-
id: id,
|
|
161
|
-
asciidoc_source: asciidoc_source,
|
|
162
|
-
source_location: source_location
|
|
163
|
-
)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def normalize_equation(content)
|
|
167
|
-
content.strip
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def store_result(result, context)
|
|
171
|
-
return [nil, nil, nil] unless result.data
|
|
172
|
-
|
|
173
|
-
image_output_dir = context[:image_output_dir]
|
|
174
|
-
image_target_dir = context[:image_target_dir]
|
|
175
|
-
|
|
176
|
-
FileUtils.mkdir_p(image_output_dir) unless File.directory?(image_output_dir)
|
|
177
|
-
filename = "#{result.basename}.#{result.extension}"
|
|
178
|
-
output_path = ::File.join(image_output_dir, filename)
|
|
179
|
-
::File.binwrite(output_path, result.data)
|
|
180
|
-
|
|
181
|
-
target = if image_target_dir == "."
|
|
182
|
-
filename
|
|
183
|
-
else
|
|
184
|
-
::File.join(image_target_dir, filename)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
[target, result.width, result.height]
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def inline_replace(text, node, document, context)
|
|
191
|
-
return [text, false] unless text && !text.empty?
|
|
192
|
-
|
|
193
|
-
modified = false
|
|
194
|
-
default_style = default_stem_style(document) || "latexmath"
|
|
195
|
-
|
|
196
|
-
new_text = text.gsub(STEM_INLINE_MACRO_RX) do
|
|
197
|
-
match = Regexp.last_match
|
|
198
|
-
escaped = match[0].start_with?("\\")
|
|
199
|
-
if escaped
|
|
200
|
-
match[0][1..]
|
|
201
|
-
else
|
|
202
|
-
macro = match[1]
|
|
203
|
-
subs = match[2]
|
|
204
|
-
equation = match[3].rstrip
|
|
205
|
-
next "" if equation.empty?
|
|
206
|
-
|
|
207
|
-
style = (macro == "stem") ? default_style : "latexmath"
|
|
208
|
-
if style == "latexmath"
|
|
209
|
-
inline_subs = (subs.nil? || subs.empty?) ? [] : node.resolve_pass_subs(subs)
|
|
210
|
-
equation = node.apply_subs(equation, inline_subs) unless inline_subs.empty?
|
|
211
|
-
begin
|
|
212
|
-
result = render_equation(
|
|
213
|
-
equation,
|
|
214
|
-
display: false,
|
|
215
|
-
inline: true,
|
|
216
|
-
context: context,
|
|
217
|
-
asciidoc_source: match[0],
|
|
218
|
-
source_location: node.source_location
|
|
219
|
-
)
|
|
220
|
-
rescue RenderingError => e
|
|
221
|
-
location = node.source_location
|
|
222
|
-
log_rendering_error(e, location)
|
|
223
|
-
ensure_macros_substitution(node)
|
|
224
|
-
modified = true
|
|
225
|
-
next inline_error_markup(e, location)
|
|
226
|
-
end
|
|
227
|
-
next match[0] unless result
|
|
228
|
-
|
|
229
|
-
modified = true
|
|
230
|
-
|
|
231
|
-
if result.inline_markup
|
|
232
|
-
ensure_macros_substitution(node)
|
|
233
|
-
%(pass:[#{result.inline_markup}])
|
|
234
|
-
else
|
|
235
|
-
target, width, height = store_result(result, context)
|
|
236
|
-
next match[0] unless target
|
|
237
|
-
attrs = []
|
|
238
|
-
attrs << %(width=#{width.to_i}) if width
|
|
239
|
-
attrs << %(height=#{height.to_i}) if height
|
|
240
|
-
attr_text = attrs.join(",")
|
|
241
|
-
ensure_macros_substitution(node)
|
|
242
|
-
%(image:#{target}[#{attr_text}])
|
|
243
|
-
end
|
|
244
|
-
else
|
|
245
|
-
match[0]
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
[new_text, modified]
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def prose_candidate?(node)
|
|
254
|
-
(node.content_model == :simple && node.subs.include?(:macros)) || %i[list_item table_cell].include?(node.context)
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def contains_inline_stem?(node)
|
|
258
|
-
text = if node.context == :list_item || node.context == :table_cell
|
|
259
|
-
node.instance_variable_get(:@text)
|
|
260
|
-
else
|
|
261
|
-
(node.lines || []) * LINE_FEED
|
|
262
|
-
end
|
|
263
|
-
text && text =~ STEM_INLINE_MACRO_RX
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def ensure_macros_substitution(node)
|
|
267
|
-
return unless node.respond_to?(:subs)
|
|
268
|
-
|
|
269
|
-
subs = node.subs
|
|
270
|
-
if subs.nil?
|
|
271
|
-
node.instance_variable_set(:@subs, [:macros])
|
|
272
|
-
elsif subs.include?(:macros)
|
|
273
|
-
# already enabled
|
|
274
|
-
else
|
|
275
|
-
updated = subs.dup
|
|
276
|
-
updated << :macros
|
|
277
|
-
node.instance_variable_set(:@subs, updated)
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def image_output_and_target_dir(doc)
|
|
282
|
-
output_dir = doc.attr("imagesoutdir")
|
|
283
|
-
if output_dir
|
|
284
|
-
if doc.attr("imagesdir").nil_or_empty?
|
|
285
|
-
target_dir = output_dir
|
|
286
|
-
else
|
|
287
|
-
abs_imagesdir = ::Pathname.new doc.normalize_system_path(doc.attr("imagesdir"))
|
|
288
|
-
abs_outdir = ::Pathname.new doc.normalize_system_path(output_dir)
|
|
289
|
-
target_dir = abs_outdir.relative_path_from(abs_imagesdir).to_s
|
|
290
|
-
end
|
|
291
|
-
else
|
|
292
|
-
output_dir = doc.attr("imagesdir") || "."
|
|
293
|
-
target_dir = "."
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
output_dir = doc.normalize_system_path(output_dir, doc.attr("docdir"))
|
|
297
|
-
[output_dir, target_dir]
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def insert_block_error(stem, error)
|
|
301
|
-
log_rendering_error(error, stem.source_location)
|
|
302
|
-
parent = stem.parent
|
|
303
|
-
return unless parent
|
|
304
|
-
|
|
305
|
-
text = block_error_text(error, stem.source_location)
|
|
306
|
-
replacement = Asciidoctor::Block.new(parent, :listing, source: text)
|
|
307
|
-
replacement.add_role("latexmath-error") if replacement.respond_to?(:add_role)
|
|
308
|
-
replacement.id = stem.id if stem.id
|
|
309
|
-
if (title = stem.attributes["title"])
|
|
310
|
-
replacement.title = title
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
index = parent.blocks.index(stem)
|
|
314
|
-
if index
|
|
315
|
-
parent.blocks[index] = replacement
|
|
316
|
-
else
|
|
317
|
-
parent.blocks << replacement
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def block_error_text(error, source_location)
|
|
322
|
-
location_hint = format_error_location(source_location)
|
|
323
|
-
header = if location_hint.empty?
|
|
324
|
-
"Failed to render latexmath"
|
|
325
|
-
else
|
|
326
|
-
"Failed to render latexmath#{location_hint}"
|
|
327
|
-
end
|
|
328
|
-
header = "#{header}:"
|
|
329
|
-
message = error.message.to_s.rstrip
|
|
330
|
-
message.empty? ? header : "#{header}\n#{message}"
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def inline_error_markup(error, source_location = nil)
|
|
334
|
-
location_hint = format_error_location(source_location)
|
|
335
|
-
prefix = "Failed to render latexmath"
|
|
336
|
-
prefix += location_hint unless location_hint.empty?
|
|
337
|
-
message = "#{prefix}: #{error.message}".strip
|
|
338
|
-
escaped = CGI.escapeHTML(message)
|
|
339
|
-
escaped = escaped.gsub(/\r?\n/, "<br>")
|
|
340
|
-
"+++<span class=\"latexmath-error\"><code>#{escaped}</code></span>+++"
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def log_rendering_error(error, source_location)
|
|
344
|
-
location_hint = format_error_location(source_location)
|
|
345
|
-
first_line = error.message.to_s.lines.first&.strip || error.message.to_s
|
|
346
|
-
warn %(asciidoctor-latexmath: #{first_line}#{location_hint})
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
def format_error_location(source_location)
|
|
350
|
-
return "" unless source_location
|
|
351
|
-
|
|
352
|
-
file = if source_location.respond_to?(:file)
|
|
353
|
-
source_location.file
|
|
354
|
-
elsif source_location.respond_to?(:path)
|
|
355
|
-
source_location.path
|
|
356
|
-
elsif source_location.respond_to?(:filename)
|
|
357
|
-
source_location.filename
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
line = source_location.respond_to?(:lineno) ? source_location.lineno : nil
|
|
361
|
-
|
|
362
|
-
parts = []
|
|
363
|
-
parts << file if file && !file.to_s.empty?
|
|
364
|
-
parts << line if line
|
|
365
|
-
parts.empty? ? "" : " (#{parts.join(':')})"
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
end
|