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
|
@@ -0,0 +1,320 @@
|
|
|
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"
|
|
8
|
+
require "shellwords"
|
|
9
|
+
|
|
10
|
+
require_relative "toolchain_record"
|
|
11
|
+
require_relative "../../latexmath/errors"
|
|
12
|
+
|
|
13
|
+
module Asciidoctor
|
|
14
|
+
module Latexmath
|
|
15
|
+
module Rendering
|
|
16
|
+
class ToolDetector
|
|
17
|
+
SVG_PRIORITY = %i[dvisvgm pdf2svg].freeze
|
|
18
|
+
PNG_PRIORITY = %i[pdftoppm magick gs].freeze
|
|
19
|
+
SUMMARY_IDENTIFIERS = (SVG_PRIORITY + %i[pdflatex xelatex lualatex tectonic] + PNG_PRIORITY).freeze
|
|
20
|
+
|
|
21
|
+
def initialize(request, raw_attributes)
|
|
22
|
+
@request = request
|
|
23
|
+
@raw_attributes = raw_attributes || {}
|
|
24
|
+
@tool_presence_map = {}
|
|
25
|
+
@svg_log_emitted = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def emit_tool_summary
|
|
29
|
+
self.class.emit_summary_once do
|
|
30
|
+
summary = SUMMARY_IDENTIFIERS.map do |identifier|
|
|
31
|
+
record = summary_record(identifier)
|
|
32
|
+
"#{identifier}=#{record.available ? "ok" : "missing"}"
|
|
33
|
+
end.join(" ")
|
|
34
|
+
Asciidoctor::LoggerManager.logger&.info { "latexmath.tools: #{summary}" }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ensure_svg_tool!
|
|
39
|
+
return nil unless request.format == :svg
|
|
40
|
+
|
|
41
|
+
record = detect_svg_tool
|
|
42
|
+
log_svg_tool_selection(record)
|
|
43
|
+
return record if record.available
|
|
44
|
+
|
|
45
|
+
message = missing_tool_message(
|
|
46
|
+
"SVG",
|
|
47
|
+
record,
|
|
48
|
+
SVG_PRIORITY,
|
|
49
|
+
fallback_formats: "pdf|png",
|
|
50
|
+
attribute: ":latexmath-pdf2svg:"
|
|
51
|
+
)
|
|
52
|
+
raise MissingToolError.new(record.id, message)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ensure_png_tool!
|
|
56
|
+
return nil unless request.format == :png
|
|
57
|
+
|
|
58
|
+
record = detect_png_tool
|
|
59
|
+
return record if record.available
|
|
60
|
+
|
|
61
|
+
message = missing_tool_message(
|
|
62
|
+
"PNG",
|
|
63
|
+
record,
|
|
64
|
+
PNG_PRIORITY,
|
|
65
|
+
fallback_formats: "svg|pdf",
|
|
66
|
+
attribute: ":latexmath-pdftoppm:"
|
|
67
|
+
)
|
|
68
|
+
raise MissingToolError.new(record.id, message)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def record_engine(command)
|
|
72
|
+
return if command.nil?
|
|
73
|
+
|
|
74
|
+
id = canonical_engine_id(command)
|
|
75
|
+
tool_presence_map[id] = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def tool_presence
|
|
79
|
+
tool_presence_map.transform_keys(&:to_s)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
attr_reader :request
|
|
85
|
+
attr_reader :raw_attributes
|
|
86
|
+
attr_reader :tool_presence_map
|
|
87
|
+
|
|
88
|
+
def log_svg_tool_selection(record)
|
|
89
|
+
return if @svg_log_emitted
|
|
90
|
+
|
|
91
|
+
selected = record&.available ? record.id.to_s : "missing"
|
|
92
|
+
Asciidoctor::LoggerManager.logger&.info { "latexmath.svg.tool=#{selected}" }
|
|
93
|
+
@svg_log_emitted = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def detect_svg_tool
|
|
97
|
+
if (explicit = explicit_svg_path)
|
|
98
|
+
id = infer_identifier(request.tool_overrides[:svg] || :pdf2svg, SVG_PRIORITY)
|
|
99
|
+
available = executable_file?(explicit)
|
|
100
|
+
return remember_record(ToolchainRecord.new(id: id, available: available, path: explicit))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
override = request.tool_overrides[:svg]
|
|
104
|
+
if override
|
|
105
|
+
record = resolve_override(override, SVG_PRIORITY)
|
|
106
|
+
return remember_record(record)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
records = SVG_PRIORITY.map { |candidate| remember_record(memoized_lookup(candidate)) }
|
|
110
|
+
records.find(&:available) || records.first
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def detect_png_tool
|
|
114
|
+
if (explicit = explicit_png_path)
|
|
115
|
+
id = infer_identifier(request.tool_overrides[:png] || :pdftoppm, PNG_PRIORITY)
|
|
116
|
+
available = executable_file?(explicit)
|
|
117
|
+
return remember_record(ToolchainRecord.new(id: id, available: available, path: explicit))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
override = request.tool_overrides[:png]
|
|
121
|
+
if override
|
|
122
|
+
record = resolve_override(override, PNG_PRIORITY)
|
|
123
|
+
return remember_record(record)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
records = PNG_PRIORITY.map { |candidate| remember_record(memoized_lookup(candidate)) }
|
|
127
|
+
records.find(&:available) || records.first
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def detect_with_priority(priority)
|
|
131
|
+
priority.each do |candidate|
|
|
132
|
+
record = memoized_lookup(candidate)
|
|
133
|
+
return record if record.available
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
memoized_lookup(priority.first)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def resolve_override(value, priority)
|
|
140
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
141
|
+
|
|
142
|
+
candidate = value.to_s.strip
|
|
143
|
+
id = infer_identifier(candidate, priority)
|
|
144
|
+
|
|
145
|
+
if File.exist?(candidate)
|
|
146
|
+
available = File.executable?(candidate)
|
|
147
|
+
return remember_record(ToolchainRecord.new(id: id, available: available, path: candidate))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
record = memoized_lookup(id, candidate)
|
|
151
|
+
return remember_record(record) if record.available
|
|
152
|
+
|
|
153
|
+
remember_record(ToolchainRecord.new(id: id, available: false, path: candidate))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def infer_identifier(candidate, priority)
|
|
157
|
+
symbol = candidate.respond_to?(:to_sym) ? candidate.to_sym : candidate
|
|
158
|
+
normalized = symbol.to_s.downcase
|
|
159
|
+
explicit = normalized.gsub(/[^a-z0-9]+/, "_").to_sym
|
|
160
|
+
return explicit if priority.include?(explicit)
|
|
161
|
+
|
|
162
|
+
if File.exist?(candidate)
|
|
163
|
+
basename = File.basename(candidate)
|
|
164
|
+
inferred = basename.downcase.gsub(/[^a-z0-9]+/, "_").to_sym
|
|
165
|
+
return inferred if priority.include?(inferred)
|
|
166
|
+
return inferred
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
explicit
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def memoized_lookup(identifier, override_command = nil)
|
|
173
|
+
command = override_command || identifier.to_s
|
|
174
|
+
self.class.lookup(identifier, command) do
|
|
175
|
+
path = find_executable(command)
|
|
176
|
+
if override_command
|
|
177
|
+
ToolchainRecord.new(id: identifier, available: !path.nil?, path: path)
|
|
178
|
+
else
|
|
179
|
+
ToolchainRecord.new(id: identifier, available: true, path: path || command)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def find_executable(command)
|
|
185
|
+
return command if executable_file?(command)
|
|
186
|
+
|
|
187
|
+
path_extensions = if Gem.win_platform?
|
|
188
|
+
ENV.fetch("PATHEXT", ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC").split(";")
|
|
189
|
+
else
|
|
190
|
+
[""]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
194
|
+
path_extensions.each do |ext|
|
|
195
|
+
candidate = File.join(dir, ext.empty? ? command : "#{command}#{ext}")
|
|
196
|
+
return candidate if executable_file?(candidate)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def executable_file?(path)
|
|
204
|
+
File.exist?(path) && File.executable?(path) && !File.directory?(path)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def explicit_svg_path
|
|
208
|
+
override = request.tool_overrides[:svg_path]
|
|
209
|
+
return override if override && !override.to_s.strip.empty?
|
|
210
|
+
|
|
211
|
+
extract_path("latexmath-pdf2svg")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def explicit_png_path
|
|
215
|
+
override = request.tool_overrides[:png_path]
|
|
216
|
+
return override if override && !override.to_s.strip.empty?
|
|
217
|
+
|
|
218
|
+
extract_path("latexmath-pdftoppm")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def extract_path(key)
|
|
222
|
+
value = raw_attributes[key]
|
|
223
|
+
return nil if value.nil?
|
|
224
|
+
|
|
225
|
+
stripped = value.to_s.strip
|
|
226
|
+
stripped.empty? ? nil : stripped
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def svg_candidates
|
|
230
|
+
override = request.tool_overrides[:svg]
|
|
231
|
+
return [override.to_s.strip].reject(&:empty?) unless override.nil? || override.to_s.strip.empty?
|
|
232
|
+
|
|
233
|
+
SVG_PRIORITY.map(&:to_s)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def png_candidates
|
|
237
|
+
override = request.tool_overrides[:png]
|
|
238
|
+
return [override.to_s.strip].reject(&:empty?) unless override.nil? || override.to_s.strip.empty?
|
|
239
|
+
|
|
240
|
+
PNG_PRIORITY.map(&:to_s)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def missing_tool_message(kind, record, priority, fallback_formats:, attribute:)
|
|
244
|
+
tried = if priority.include?(record.id)
|
|
245
|
+
priority.map(&:to_s)
|
|
246
|
+
else
|
|
247
|
+
([record.id.to_s] + priority.map(&:to_s)).uniq
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
base = "Required #{kind} conversion tool not available. Tried: #{tried.join(", ")}. Configure #{attribute} with an executable path or install one of the supported tools."
|
|
251
|
+
|
|
252
|
+
preferred = record.id.to_s
|
|
253
|
+
alternates = (priority.map(&:to_s) - [preferred]).uniq
|
|
254
|
+
alt_hint = if alternates.empty?
|
|
255
|
+
"install #{preferred} (preferred)"
|
|
256
|
+
else
|
|
257
|
+
available_alternates = alternates.select { |name| tool_presence_map[name.to_sym] }
|
|
258
|
+
if available_alternates.any?
|
|
259
|
+
"install #{preferred} (preferred) or keep using #{available_alternates.join("/")}"
|
|
260
|
+
else
|
|
261
|
+
"install #{preferred} (preferred) or install #{alternates.join("/")}"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
hints = [alt_hint, "set :latexmath-format: #{fallback_formats}"]
|
|
266
|
+
"#{base}\nhint: #{hints.join("; ")}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def remember_record(record)
|
|
270
|
+
return record unless record
|
|
271
|
+
|
|
272
|
+
tool_presence_map[record.id] = record.available
|
|
273
|
+
record
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def summary_record(identifier)
|
|
277
|
+
command = identifier.to_s
|
|
278
|
+
self.class.lookup(identifier, command) do
|
|
279
|
+
path = find_executable(command)
|
|
280
|
+
ToolchainRecord.new(id: identifier, available: !path.nil?, path: path || command)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def canonical_engine_id(command)
|
|
285
|
+
first = Shellwords.shellsplit(command.to_s).first
|
|
286
|
+
return :pdflatex unless first
|
|
287
|
+
|
|
288
|
+
first.downcase.gsub(/[^a-z0-9]+/, "_").to_sym
|
|
289
|
+
rescue ArgumentError
|
|
290
|
+
:pdflatex
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
class << self
|
|
294
|
+
def lookup(identifier, command)
|
|
295
|
+
@cache ||= {}
|
|
296
|
+
key = [identifier, command]
|
|
297
|
+
cached = @cache[key]
|
|
298
|
+
return cached if cached
|
|
299
|
+
|
|
300
|
+
record = yield
|
|
301
|
+
@cache[key] = record
|
|
302
|
+
record
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def reset!
|
|
306
|
+
@cache = {}
|
|
307
|
+
@summary_emitted = false
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def emit_summary_once
|
|
311
|
+
return if @summary_emitted
|
|
312
|
+
|
|
313
|
+
yield
|
|
314
|
+
@summary_emitted = true
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
module Asciidoctor
|
|
8
|
+
module Latexmath
|
|
9
|
+
module Rendering
|
|
10
|
+
class ToolchainRecord
|
|
11
|
+
attr_reader :id, :available, :path
|
|
12
|
+
|
|
13
|
+
def initialize(id:, available:, path: nil)
|
|
14
|
+
@id = id
|
|
15
|
+
@available = available
|
|
16
|
+
@path = path
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
module Asciidoctor
|
|
8
|
+
module Latexmath
|
|
9
|
+
module Statistics
|
|
10
|
+
class Collector
|
|
11
|
+
def initialize
|
|
12
|
+
@render_durations = []
|
|
13
|
+
@hit_durations = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def record_render(duration_ms)
|
|
17
|
+
@render_durations << duration_ms
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def record_hit(duration_ms)
|
|
21
|
+
@hit_durations << duration_ms
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_line
|
|
25
|
+
return nil if @render_durations.empty? && @hit_durations.empty?
|
|
26
|
+
|
|
27
|
+
format(
|
|
28
|
+
"latexmath stats: renders=%<renders>d cache_hits=%<hits>d avg_render_ms=%<avg_render>d avg_hit_ms=%<avg_hit>d",
|
|
29
|
+
renders: @render_durations.size,
|
|
30
|
+
hits: @hit_durations.size,
|
|
31
|
+
avg_render: average(@render_durations),
|
|
32
|
+
avg_hit: average(@hit_durations)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def average(values)
|
|
39
|
+
return 0 if values.empty?
|
|
40
|
+
|
|
41
|
+
sum = values.sum
|
|
42
|
+
(sum.to_f / values.size).round(0, half: :up)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
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_relative "../errors"
|
|
8
|
+
|
|
9
|
+
module Asciidoctor
|
|
10
|
+
module Latexmath
|
|
11
|
+
module Support
|
|
12
|
+
class ConflictRegistry
|
|
13
|
+
Entry = Struct.new(:signature, :details)
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@entries = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register!(basename, signature, details)
|
|
20
|
+
normalized = normalize_details(details)
|
|
21
|
+
entry = entries[basename]
|
|
22
|
+
if entry
|
|
23
|
+
return if entry.signature == signature
|
|
24
|
+
|
|
25
|
+
raise TargetConflictError, conflict_message(basename, entry, normalized, signature)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
entries[basename] = Entry.new(signature, normalized)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :entries
|
|
34
|
+
|
|
35
|
+
def normalize_details(details)
|
|
36
|
+
{
|
|
37
|
+
location: details[:location] || "unknown location",
|
|
38
|
+
format: (details[:format] || "unknown").to_s,
|
|
39
|
+
content_hash: (details[:content_hash] || "").to_s,
|
|
40
|
+
preamble_hash: (details[:preamble_hash] || "").to_s,
|
|
41
|
+
fontsize_hash: (details[:fontsize_hash] || "").to_s,
|
|
42
|
+
entry_type: (details[:entry_type] || "unknown").to_s
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def conflict_message(basename, existing_entry, incoming_details, incoming_signature)
|
|
47
|
+
existing_details = existing_entry.details
|
|
48
|
+
<<~MSG.strip
|
|
49
|
+
conflicting target '#{basename}' already defined at #{existing_details[:location]} (signature #{signature_summary(existing_entry.signature, existing_details)}).
|
|
50
|
+
new definition from #{incoming_details[:location]} would produce signature #{signature_summary(incoming_signature, incoming_details)}.
|
|
51
|
+
hint: choose a unique target basename or remove the explicit target attribute.
|
|
52
|
+
MSG
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def signature_summary(signature, details)
|
|
56
|
+
parts = []
|
|
57
|
+
parts << "key=#{signature.to_s[0, 12]}" if signature
|
|
58
|
+
parts << "format=#{details[:format]}"
|
|
59
|
+
parts << "content=#{truncate(details[:content_hash])}"
|
|
60
|
+
parts << "preamble=#{truncate(details[:preamble_hash])}"
|
|
61
|
+
parts << "fontsize=#{truncate(details[:fontsize_hash])}"
|
|
62
|
+
parts << "entry=#{details[:entry_type]}"
|
|
63
|
+
parts.join(" ")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def truncate(value)
|
|
67
|
+
text = value.to_s
|
|
68
|
+
return "-" if text.empty?
|
|
69
|
+
|
|
70
|
+
text[0, 12]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -4,9 +4,99 @@
|
|
|
4
4
|
#
|
|
5
5
|
# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
require "asciidoctor"
|
|
8
|
+
require "asciidoctor/extensions"
|
|
9
|
+
require_relative "asciidoctor/latexmath/version"
|
|
10
|
+
require_relative "asciidoctor/latexmath/processors/block_processor"
|
|
11
|
+
require_relative "asciidoctor/latexmath/processors/inline_macro_processor"
|
|
12
|
+
require_relative "asciidoctor/latexmath/processors/statistics_postprocessor"
|
|
13
|
+
require_relative "asciidoctor/latexmath/converters/html5"
|
|
14
|
+
|
|
15
|
+
module Asciidoctor
|
|
16
|
+
module Latexmath
|
|
17
|
+
class << self
|
|
18
|
+
def register(registry = ::Asciidoctor::Extensions)
|
|
19
|
+
block_extension = registry.block Processors::BlockProcessor
|
|
20
|
+
inline_extension = registry.inline_macro Processors::InlineMacroProcessor
|
|
21
|
+
ensure_processor_collection!(registry, :@block_processors, :latexmath, block_extension)
|
|
22
|
+
ensure_processor_collection!(registry, :@inline_macros, :latexmath, inline_extension)
|
|
23
|
+
registry.postprocessor Processors::StatisticsPostprocessor
|
|
24
|
+
|
|
25
|
+
ensure_aliases!(registry)
|
|
26
|
+
ensure_empty_collection!(registry, :@block_macros, {})
|
|
27
|
+
ensure_empty_collection!(registry, :@tree_processors, [])
|
|
28
|
+
ensure_empty_collection!(registry, :@tree_processor_extensions, [])
|
|
29
|
+
|
|
30
|
+
registry
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reset_render_counters!
|
|
34
|
+
@render_invocations = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def record_render_invocation!
|
|
38
|
+
@render_invocations = render_invocations + 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_invocations
|
|
42
|
+
@render_invocations ||= 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def ensure_processor_collection!(registry, ivar, canonical_name, extension)
|
|
48
|
+
collection = registry.instance_variable_get(ivar)
|
|
49
|
+
unless collection
|
|
50
|
+
collection = {}
|
|
51
|
+
registry.instance_variable_set(ivar, collection)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
canonical_name = canonical_name.to_sym
|
|
55
|
+
collection[canonical_name] ||= []
|
|
56
|
+
unless collection[canonical_name].include?(extension)
|
|
57
|
+
collection[canonical_name] << extension
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
collection
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def ensure_aliases!(registry)
|
|
64
|
+
ensure_collection_alias!(registry.instance_variable_get(:@block_extensions), :latexmath, :stem)
|
|
65
|
+
ensure_collection_alias!(registry.instance_variable_get(:@inline_macro_extensions), :latexmath, :stem)
|
|
66
|
+
ensure_collection_alias!(registry.instance_variable_get(:@block_processors), :latexmath, :stem)
|
|
67
|
+
ensure_collection_alias!(registry.instance_variable_get(:@inline_macros), :latexmath, :stem)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ensure_collection_alias!(collection, canonical_name, alias_name)
|
|
71
|
+
return unless collection
|
|
72
|
+
canonical_name = canonical_name.to_sym
|
|
73
|
+
alias_name = alias_name&.to_sym
|
|
74
|
+
return unless alias_name
|
|
75
|
+
|
|
76
|
+
existing_default = collection.default_proc
|
|
77
|
+
|
|
78
|
+
collection.default_proc = lambda do |hash, key|
|
|
79
|
+
key_sym = key.to_sym
|
|
80
|
+
if key_sym == alias_name
|
|
81
|
+
hash[canonical_name]
|
|
82
|
+
elsif existing_default
|
|
83
|
+
existing_default.call(hash, key)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ensure_empty_collection!(registry, ivar, empty_value)
|
|
89
|
+
value = registry.instance_variable_get(ivar)
|
|
90
|
+
if value.nil?
|
|
91
|
+
registry.instance_variable_set(ivar, empty_value.dup)
|
|
92
|
+
else
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
9
99
|
|
|
10
100
|
Asciidoctor::Extensions.register do
|
|
11
|
-
|
|
101
|
+
Asciidoctor::Latexmath.register(self)
|
|
12
102
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asciidoctor-latexmath
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shuai Zhang
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: asciidoctor
|
|
@@ -83,17 +83,39 @@ files:
|
|
|
83
83
|
- LICENSE
|
|
84
84
|
- README.md
|
|
85
85
|
- lib/asciidoctor-latexmath.rb
|
|
86
|
-
- lib/asciidoctor
|
|
87
|
-
- lib/asciidoctor
|
|
88
|
-
- lib/asciidoctor
|
|
89
|
-
|
|
86
|
+
- lib/asciidoctor/latexmath/attribute_resolver.rb
|
|
87
|
+
- lib/asciidoctor/latexmath/cache/cache_entry.rb
|
|
88
|
+
- lib/asciidoctor/latexmath/cache/cache_key.rb
|
|
89
|
+
- lib/asciidoctor/latexmath/cache/disk_cache.rb
|
|
90
|
+
- lib/asciidoctor/latexmath/command_runner.rb
|
|
91
|
+
- lib/asciidoctor/latexmath/converters/html5.rb
|
|
92
|
+
- lib/asciidoctor/latexmath/errors.rb
|
|
93
|
+
- lib/asciidoctor/latexmath/html_builder.rb
|
|
94
|
+
- lib/asciidoctor/latexmath/math_expression.rb
|
|
95
|
+
- lib/asciidoctor/latexmath/path_utils.rb
|
|
96
|
+
- lib/asciidoctor/latexmath/processors/block_processor.rb
|
|
97
|
+
- lib/asciidoctor/latexmath/processors/inline_macro_processor.rb
|
|
98
|
+
- lib/asciidoctor/latexmath/processors/statistics_postprocessor.rb
|
|
99
|
+
- lib/asciidoctor/latexmath/render_request.rb
|
|
100
|
+
- lib/asciidoctor/latexmath/renderer_service.rb
|
|
101
|
+
- lib/asciidoctor/latexmath/rendering/pdf_to_png_renderer.rb
|
|
102
|
+
- lib/asciidoctor/latexmath/rendering/pdf_to_svg_renderer.rb
|
|
103
|
+
- lib/asciidoctor/latexmath/rendering/pdflatex_renderer.rb
|
|
104
|
+
- lib/asciidoctor/latexmath/rendering/pipeline.rb
|
|
105
|
+
- lib/asciidoctor/latexmath/rendering/renderer.rb
|
|
106
|
+
- lib/asciidoctor/latexmath/rendering/tool_detector.rb
|
|
107
|
+
- lib/asciidoctor/latexmath/rendering/toolchain_record.rb
|
|
108
|
+
- lib/asciidoctor/latexmath/statistics/collector.rb
|
|
109
|
+
- lib/asciidoctor/latexmath/support/conflict_registry.rb
|
|
110
|
+
- lib/asciidoctor/latexmath/version.rb
|
|
111
|
+
homepage: https://github.com/hcoona/asciidoctor-latexmath#readme
|
|
90
112
|
licenses:
|
|
91
113
|
- LGPL-3.0-or-later WITH LGPL-3.0-linking-exception
|
|
92
114
|
metadata:
|
|
93
|
-
homepage_uri: https://github.com/hcoona/asciidoctor-latexmath
|
|
115
|
+
homepage_uri: https://github.com/hcoona/asciidoctor-latexmath#readme
|
|
94
116
|
source_code_uri: https://github.com/hcoona/asciidoctor-latexmath
|
|
95
117
|
bug_tracker_uri: https://github.com/hcoona/asciidoctor-latexmath/issues
|
|
96
|
-
documentation_uri: https://github.com/hcoona/asciidoctor-latexmath
|
|
118
|
+
documentation_uri: https://github.com/hcoona/asciidoctor-latexmath/blob/main/README.md
|
|
97
119
|
post_install_message:
|
|
98
120
|
rdoc_options: []
|
|
99
121
|
require_paths:
|
|
@@ -102,14 +124,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
102
124
|
requirements:
|
|
103
125
|
- - ">="
|
|
104
126
|
- !ruby/object:Gem::Version
|
|
105
|
-
version: '
|
|
127
|
+
version: '3.2'
|
|
106
128
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
129
|
requirements:
|
|
108
|
-
- - "
|
|
130
|
+
- - ">="
|
|
109
131
|
- !ruby/object:Gem::Version
|
|
110
|
-
version:
|
|
132
|
+
version: '0'
|
|
111
133
|
requirements: []
|
|
112
|
-
rubygems_version: 3.
|
|
134
|
+
rubygems_version: 3.5.22
|
|
113
135
|
signing_key:
|
|
114
136
|
specification_version: 4
|
|
115
137
|
summary: Offline latexmath rendering for Asciidoctor.
|