asciidoctor-latexmath 1.0.0.pre.dev.1 → 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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +119 -36
  3. data/lib/asciidoctor/latexmath/attribute_resolver.rb +485 -0
  4. data/lib/asciidoctor/latexmath/cache/cache_entry.rb +31 -0
  5. data/lib/asciidoctor/latexmath/cache/cache_key.rb +33 -0
  6. data/lib/asciidoctor/latexmath/cache/disk_cache.rb +151 -0
  7. data/lib/asciidoctor/latexmath/command_runner.rb +116 -0
  8. data/lib/asciidoctor/latexmath/converters/html5.rb +72 -0
  9. data/lib/asciidoctor/latexmath/errors.rb +134 -0
  10. data/lib/asciidoctor/latexmath/html_builder.rb +103 -0
  11. data/lib/asciidoctor/latexmath/math_expression.rb +22 -0
  12. data/lib/asciidoctor/latexmath/path_utils.rb +46 -0
  13. data/lib/asciidoctor/latexmath/processors/block_processor.rb +74 -0
  14. data/lib/asciidoctor/latexmath/processors/inline_macro_processor.rb +70 -0
  15. data/lib/asciidoctor/latexmath/processors/statistics_postprocessor.rb +18 -0
  16. data/lib/asciidoctor/latexmath/render_request.rb +32 -0
  17. data/lib/asciidoctor/latexmath/renderer_service.rb +741 -0
  18. data/lib/asciidoctor/latexmath/rendering/pdf_to_png_renderer.rb +97 -0
  19. data/lib/asciidoctor/latexmath/rendering/pdf_to_svg_renderer.rb +84 -0
  20. data/lib/asciidoctor/latexmath/rendering/pdflatex_renderer.rb +166 -0
  21. data/lib/asciidoctor/latexmath/rendering/pipeline.rb +58 -0
  22. data/lib/asciidoctor/latexmath/rendering/renderer.rb +33 -0
  23. data/lib/asciidoctor/latexmath/rendering/tool_detector.rb +320 -0
  24. data/lib/asciidoctor/latexmath/rendering/toolchain_record.rb +21 -0
  25. data/lib/asciidoctor/latexmath/statistics/collector.rb +47 -0
  26. data/lib/asciidoctor/latexmath/support/conflict_registry.rb +75 -0
  27. data/lib/{asciidoctor-latexmath → asciidoctor/latexmath}/version.rb +1 -1
  28. data/lib/asciidoctor-latexmath.rb +93 -3
  29. metadata +34 -12
  30. data/lib/asciidoctor-latexmath/renderer.rb +0 -515
  31. 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