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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module Asciidoctor
|
|
6
|
+
module Latexmath
|
|
7
|
+
class CommandRunner
|
|
8
|
+
Result = Struct.new(:stdout, :stderr, :exit_status, :duration, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_writer :backend
|
|
12
|
+
|
|
13
|
+
def backend
|
|
14
|
+
@backend ||= SystemRunner.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run(command, timeout:, chdir:, env: {}, stdin: nil)
|
|
18
|
+
backend.run(command, timeout: timeout, chdir: chdir, env: env, stdin: stdin)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def with_backend(temp_backend)
|
|
22
|
+
previous = backend
|
|
23
|
+
self.backend = temp_backend
|
|
24
|
+
yield
|
|
25
|
+
ensure
|
|
26
|
+
self.backend = previous
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class NullRunner
|
|
31
|
+
def run(_command, timeout:, **_rest)
|
|
32
|
+
raise ArgumentError, "timeout must be positive" unless timeout&.positive?
|
|
33
|
+
|
|
34
|
+
Result.new(stdout: "", stderr: "", exit_status: 0, duration: 0.0)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class SystemRunner
|
|
39
|
+
DEFAULT_WAIT_INTERVAL = 0.05
|
|
40
|
+
KILL_GRACE_SECONDS = 2.0
|
|
41
|
+
|
|
42
|
+
def run(command, timeout:, chdir:, env: {}, stdin: nil)
|
|
43
|
+
raise ArgumentError, "timeout must be positive" unless timeout&.positive?
|
|
44
|
+
|
|
45
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
46
|
+
stdout_read, stdout_write = IO.pipe
|
|
47
|
+
stderr_read, stderr_write = IO.pipe
|
|
48
|
+
|
|
49
|
+
spawn_options = {
|
|
50
|
+
chdir: chdir,
|
|
51
|
+
out: stdout_write,
|
|
52
|
+
err: stderr_write,
|
|
53
|
+
pgroup: true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pid = Process.spawn(env, *command, spawn_options)
|
|
57
|
+
stdout_write.close
|
|
58
|
+
stderr_write.close
|
|
59
|
+
|
|
60
|
+
stdout_thread = Thread.new { stdout_read.read }
|
|
61
|
+
stderr_thread = Thread.new { stderr_read.read }
|
|
62
|
+
|
|
63
|
+
wait_with_timeout(pid, timeout)
|
|
64
|
+
|
|
65
|
+
exit_status = $?.exitstatus
|
|
66
|
+
stdout_output = stdout_thread.value
|
|
67
|
+
stderr_output = stderr_thread.value
|
|
68
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
69
|
+
|
|
70
|
+
Result.new(stdout: stdout_output, stderr: stderr_output, exit_status: exit_status, duration: duration)
|
|
71
|
+
rescue Errno::ENOENT => error
|
|
72
|
+
raise StageFailureError, "Executable not found: #{error.message.split.last}"
|
|
73
|
+
ensure
|
|
74
|
+
stdout_read&.close unless stdout_read&.closed?
|
|
75
|
+
stderr_read&.close unless stderr_read&.closed?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def wait_with_timeout(pid, timeout)
|
|
81
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
82
|
+
|
|
83
|
+
loop do
|
|
84
|
+
result = Process.wait(pid, Process::WNOHANG)
|
|
85
|
+
return result if result
|
|
86
|
+
|
|
87
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
88
|
+
terminate_process_group(pid)
|
|
89
|
+
raise RenderTimeoutError, "command timed out after #{timeout}s"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sleep(DEFAULT_WAIT_INTERVAL)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def terminate_process_group(pid)
|
|
97
|
+
Process.kill("TERM", -pid)
|
|
98
|
+
grace_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + KILL_GRACE_SECONDS
|
|
99
|
+
|
|
100
|
+
loop do
|
|
101
|
+
result = Process.wait(pid, Process::WNOHANG)
|
|
102
|
+
return result if result
|
|
103
|
+
|
|
104
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= grace_deadline
|
|
105
|
+
|
|
106
|
+
sleep(DEFAULT_WAIT_INTERVAL)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
Process.kill("KILL", -pid)
|
|
110
|
+
Process.wait(pid)
|
|
111
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "asciidoctor/converter/html5"
|
|
4
|
+
|
|
5
|
+
require_relative "../html_builder"
|
|
6
|
+
require_relative "../renderer_service"
|
|
7
|
+
|
|
8
|
+
module Asciidoctor
|
|
9
|
+
module Latexmath
|
|
10
|
+
module Converters
|
|
11
|
+
module Html5
|
|
12
|
+
def convert_stem(node)
|
|
13
|
+
if latexmath_block?(node)
|
|
14
|
+
convert_latexmath_block(node)
|
|
15
|
+
else
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def convert_inline_quoted(node)
|
|
21
|
+
if latexmath_inline?(node)
|
|
22
|
+
convert_latexmath_inline(node)
|
|
23
|
+
else
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def latexmath_block?(node)
|
|
31
|
+
node.style == "latexmath"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def latexmath_inline?(node)
|
|
35
|
+
node.type == :latexmath
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def convert_latexmath_block(node)
|
|
39
|
+
result = latexmath_renderer_service(node.document)
|
|
40
|
+
.render_block_content(node.parent || node.document, node.source, safe_attributes(node.attributes))
|
|
41
|
+
|
|
42
|
+
return result.placeholder_html if result.type == :placeholder
|
|
43
|
+
|
|
44
|
+
HtmlBuilder.block_html(node, result)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def convert_latexmath_inline(node)
|
|
48
|
+
result = latexmath_renderer_service(node.document)
|
|
49
|
+
.render_inline_content(node.parent || node.document, node.text, safe_attributes(node.attributes))
|
|
50
|
+
|
|
51
|
+
return result.placeholder_html if result.type == :placeholder
|
|
52
|
+
|
|
53
|
+
HtmlBuilder.inline_html(node, result)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def latexmath_renderer_service(document)
|
|
57
|
+
document.instance_variable_get(:@latexmath_renderer_service) || begin
|
|
58
|
+
service = RendererService.new(document)
|
|
59
|
+
document.instance_variable_set(:@latexmath_renderer_service, service)
|
|
60
|
+
service
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def safe_attributes(attrs)
|
|
65
|
+
attrs ? attrs.dup : {}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
Asciidoctor::Converter::Html5Converter.prepend(Asciidoctor::Latexmath::Converters::Html5)
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
class LatexmathError < StandardError; end
|
|
10
|
+
|
|
11
|
+
class UnsupportedFormatError < LatexmathError; end
|
|
12
|
+
|
|
13
|
+
class MissingToolError < LatexmathError
|
|
14
|
+
attr_reader :tool
|
|
15
|
+
|
|
16
|
+
def initialize(tool, message = nil)
|
|
17
|
+
@tool = tool
|
|
18
|
+
super(message || "Missing required tool: #{tool}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class InvalidAttributeError < LatexmathError; end
|
|
23
|
+
|
|
24
|
+
class UnsupportedValueError < InvalidAttributeError
|
|
25
|
+
attr_reader :category, :subject, :value, :supported, :hints
|
|
26
|
+
|
|
27
|
+
def initialize(category:, subject:, value:, supported:, hint: nil)
|
|
28
|
+
@category = category
|
|
29
|
+
@subject = subject
|
|
30
|
+
@value = value
|
|
31
|
+
@supported = supported
|
|
32
|
+
@hints = Array(hint).compact
|
|
33
|
+
super(build_message)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_message
|
|
39
|
+
descriptor = subject ? "#{subject}=#{present_value}" : present_value
|
|
40
|
+
base = "unsupported #{category}: '#{descriptor}' (supported: #{formatted_supported})"
|
|
41
|
+
hints.empty? ? base : "#{base}\nhint: #{formatted_hints}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def present_value
|
|
45
|
+
(value.nil? || value.to_s.empty?) ? "<empty>" : value.to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def formatted_supported
|
|
49
|
+
case supported
|
|
50
|
+
when Array
|
|
51
|
+
values = supported.map { |entry| entry.to_s.strip }.reject(&:empty?)
|
|
52
|
+
values.sort_by { |entry| entry.downcase }.join(", ")
|
|
53
|
+
else
|
|
54
|
+
supported.to_s
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def formatted_hints
|
|
59
|
+
hints.map(&:to_s).reject(&:empty?).join("; ")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class TargetConflictError < LatexmathError; end
|
|
64
|
+
|
|
65
|
+
class RenderTimeoutError < LatexmathError; end
|
|
66
|
+
|
|
67
|
+
class StageFailureError < LatexmathError; end
|
|
68
|
+
|
|
69
|
+
module ErrorHandling
|
|
70
|
+
class Policy
|
|
71
|
+
attr_reader :mode
|
|
72
|
+
|
|
73
|
+
def initialize(mode)
|
|
74
|
+
@mode = mode
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def abort?
|
|
78
|
+
mode == :abort
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def log?
|
|
82
|
+
mode == :log
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
alias_method :abort, :abort?
|
|
86
|
+
alias_method :log, :log?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
SUPPORTED_POLICIES = {
|
|
90
|
+
abort: Policy.new(:abort),
|
|
91
|
+
log: Policy.new(:log)
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
def self.policy(name)
|
|
95
|
+
SUPPORTED_POLICIES.fetch(name.to_sym) do
|
|
96
|
+
raise ArgumentError, "Unknown on-error policy: #{name}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
module Placeholder
|
|
101
|
+
HTML_HEADER = %(<pre class="highlight latexmath-error" role="note" data-latex-error="1">)
|
|
102
|
+
HTML_FOOTER = "</pre>"
|
|
103
|
+
|
|
104
|
+
def self.render(message:, command:, stdout:, stderr:, source:, latex_source:)
|
|
105
|
+
body = [
|
|
106
|
+
"Error: #{message}",
|
|
107
|
+
"Command: #{command}",
|
|
108
|
+
"Stdout: #{present_or_placeholder(stdout)}",
|
|
109
|
+
"Stderr: #{present_or_placeholder(stderr)}",
|
|
110
|
+
"Source (AsciiDoc): #{source}",
|
|
111
|
+
"Source (LaTeX): #{latex_source}"
|
|
112
|
+
].join("\n")
|
|
113
|
+
|
|
114
|
+
<<~HTML.rstrip
|
|
115
|
+
#{HTML_HEADER}
|
|
116
|
+
#{escape_html(body)}
|
|
117
|
+
#{HTML_FOOTER}
|
|
118
|
+
HTML
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.present_or_placeholder(value)
|
|
122
|
+
return "<empty>" if value.nil? || value.to_s.empty?
|
|
123
|
+
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.escape_html(text)
|
|
128
|
+
text.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
129
|
+
end
|
|
130
|
+
private_class_method :escape_html, :present_or_placeholder
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module Asciidoctor
|
|
6
|
+
module Latexmath
|
|
7
|
+
module HtmlBuilder
|
|
8
|
+
LINE_FEED = "\n"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def block_html(node, result)
|
|
13
|
+
attrs = result.attributes.dup
|
|
14
|
+
target = attrs.delete("target") || result.target
|
|
15
|
+
alt_text = attrs.delete("alt") || result.alt_text || ""
|
|
16
|
+
original = attrs.delete("data-latex-original") || result.alt_text || ""
|
|
17
|
+
role_attr = attrs.delete("role") || "math"
|
|
18
|
+
width = attrs.delete("width")
|
|
19
|
+
height = attrs.delete("height")
|
|
20
|
+
|
|
21
|
+
residual_attrs = attrs.except("format")
|
|
22
|
+
|
|
23
|
+
classes = ["imageblock"]
|
|
24
|
+
classes.concat(node.roles) if node.respond_to?(:roles)
|
|
25
|
+
classes << "math" unless classes.include?("math")
|
|
26
|
+
class_value = classes.reject(&:empty?).uniq.join(" ")
|
|
27
|
+
|
|
28
|
+
div_attrs = []
|
|
29
|
+
div_attrs << %(id="#{escape_html_attribute(node.id)}") if node.id
|
|
30
|
+
div_attrs << %(class="#{escape_html_attribute(class_value)}") unless class_value.empty?
|
|
31
|
+
div_attrs << %(role="#{escape_html_attribute(role_attr)}") if role_attr
|
|
32
|
+
div_attrs << %(data-latex-original="#{escape_html_attribute(original)}") unless original.to_s.empty?
|
|
33
|
+
if node.respond_to?(:attr) && (align = node.attr("align"))
|
|
34
|
+
div_attrs << %(style="text-align: #{escape_html_attribute(align)};")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
img_attrs = []
|
|
38
|
+
img_attrs << %(src="#{escape_html_attribute(target)}") if target
|
|
39
|
+
img_attrs << %(alt="#{escape_html_attribute(alt_text)}") unless alt_text.to_s.empty?
|
|
40
|
+
img_attrs << %(role="#{escape_html_attribute(role_attr)}") if role_attr
|
|
41
|
+
img_attrs << %(data-latex-original="#{escape_html_attribute(original)}") unless original.to_s.empty?
|
|
42
|
+
img_attrs << %(width="#{escape_html_attribute(width)}") if width
|
|
43
|
+
img_attrs << %(height="#{escape_html_attribute(height)}") if height
|
|
44
|
+
residual_attrs.each do |key, value|
|
|
45
|
+
next if value.nil?
|
|
46
|
+
|
|
47
|
+
img_attrs << %(#{key}="#{escape_html_attribute(value)}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
div_attr_string = div_attrs.empty? ? "" : " " + div_attrs.join(" ")
|
|
51
|
+
lines = []
|
|
52
|
+
lines << %(<div#{div_attr_string}>)
|
|
53
|
+
if node.respond_to?(:title?) && node.title?
|
|
54
|
+
caption = node.respond_to?(:captioned_title) ? node.captioned_title : node.title
|
|
55
|
+
lines << %(<div class="title">#{caption}</div>)
|
|
56
|
+
end
|
|
57
|
+
lines << %(<div class="content">)
|
|
58
|
+
lines << %(<img #{img_attrs.join(" ")}>)
|
|
59
|
+
lines << %(</div>)
|
|
60
|
+
lines << %(</div>)
|
|
61
|
+
lines.join(LINE_FEED)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inline_html(node, result)
|
|
65
|
+
attrs = result.attributes.dup
|
|
66
|
+
target = attrs.delete("target") || result.target
|
|
67
|
+
alt_text = attrs.delete("alt") || result.alt_text || ""
|
|
68
|
+
role_attr = attrs.delete("role") || "math"
|
|
69
|
+
original = attrs.delete("data-latex-original") || result.alt_text || ""
|
|
70
|
+
width = attrs.delete("width")
|
|
71
|
+
height = attrs.delete("height")
|
|
72
|
+
|
|
73
|
+
classes = ["image"]
|
|
74
|
+
classes.concat(role_attr.to_s.split) if role_attr
|
|
75
|
+
if node.respond_to?(:role) && node.role
|
|
76
|
+
classes.concat(node.role.to_s.split)
|
|
77
|
+
end
|
|
78
|
+
classes << "math" unless classes.include?("math")
|
|
79
|
+
span_class = classes.reject(&:empty?).uniq.join(" ")
|
|
80
|
+
|
|
81
|
+
img_attrs = []
|
|
82
|
+
img_attrs << %(src="#{escape_html_attribute(target)}") if target
|
|
83
|
+
img_attrs << %(alt="#{escape_html_attribute(alt_text)}") unless alt_text.to_s.empty?
|
|
84
|
+
img_attrs << %(role="#{escape_html_attribute(role_attr)}") if role_attr
|
|
85
|
+
img_attrs << %(data-latex-original="#{escape_html_attribute(original)}") unless original.to_s.empty?
|
|
86
|
+
img_attrs << %(width="#{escape_html_attribute(width)}") if width
|
|
87
|
+
img_attrs << %(height="#{escape_html_attribute(height)}") if height
|
|
88
|
+
attrs.each do |key, value|
|
|
89
|
+
next if key == "format" || value.nil?
|
|
90
|
+
|
|
91
|
+
img_attrs << %(#{key}="#{escape_html_attribute(value)}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
span_attr = span_class.empty? ? "" : %( class="#{escape_html_attribute(span_class)}")
|
|
95
|
+
%(<span#{span_attr}><img #{img_attrs.join(" ")}></span>)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def escape_html_attribute(value)
|
|
99
|
+
::CGI.escapeHTML(value.to_s)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
class MathExpression
|
|
10
|
+
attr_reader :content, :entry_type, :target_basename, :attributes, :options, :location
|
|
11
|
+
|
|
12
|
+
def initialize(content:, entry_type:, target_basename: nil, attributes: {}, options: [], location: nil)
|
|
13
|
+
@content = content
|
|
14
|
+
@entry_type = entry_type
|
|
15
|
+
@target_basename = target_basename
|
|
16
|
+
@attributes = attributes
|
|
17
|
+
@options = options
|
|
18
|
+
@location = location
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Asciidoctor
|
|
6
|
+
module Latexmath
|
|
7
|
+
module PathUtils
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def normalize_separators(path)
|
|
11
|
+
return path unless path.is_a?(String) || path.is_a?(Symbol)
|
|
12
|
+
|
|
13
|
+
path.to_s.tr("\\", "/")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def absolute_path?(path)
|
|
17
|
+
return false if path.nil?
|
|
18
|
+
|
|
19
|
+
normalized = normalize_separators(path).to_s
|
|
20
|
+
return false if normalized.empty?
|
|
21
|
+
|
|
22
|
+
normalized.start_with?("/", "~") ||
|
|
23
|
+
normalized.match?(/\A[A-Za-z]:\//) ||
|
|
24
|
+
normalized.start_with?("//")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def expand_path(path, base_dir)
|
|
28
|
+
return nil if path.nil?
|
|
29
|
+
|
|
30
|
+
normalized_path = normalize_separators(path)
|
|
31
|
+
normalized_base = normalize_separators(base_dir || Dir.pwd)
|
|
32
|
+
|
|
33
|
+
if absolute_path?(normalized_path)
|
|
34
|
+
File.expand_path(normalized_path)
|
|
35
|
+
else
|
|
36
|
+
File.expand_path(normalized_path, normalized_base)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clean_join(*parts)
|
|
41
|
+
normalized_parts = parts.compact.map { |part| normalize_separators(part) }
|
|
42
|
+
File.join(*normalized_parts)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
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 "asciidoctor/extensions"
|
|
9
|
+
|
|
10
|
+
require_relative "../renderer_service"
|
|
11
|
+
|
|
12
|
+
module Asciidoctor
|
|
13
|
+
module Latexmath
|
|
14
|
+
module Processors
|
|
15
|
+
class BlockProcessor < ::Asciidoctor::Extensions::BlockProcessor
|
|
16
|
+
use_dsl
|
|
17
|
+
|
|
18
|
+
named :latexmath
|
|
19
|
+
on_context :listing, :literal, :open, :pass, :paragraph, :stem
|
|
20
|
+
parse_content_as :raw
|
|
21
|
+
|
|
22
|
+
def process(parent, reader, attrs)
|
|
23
|
+
result = renderer_service(parent).render_block(parent, reader, attrs)
|
|
24
|
+
return create_block(parent, :pass, result.placeholder_html, {}) if result.type == :placeholder
|
|
25
|
+
|
|
26
|
+
create_image_block(parent, build_block_attributes(result, attrs))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def renderer_service(parent)
|
|
32
|
+
document = parent.document
|
|
33
|
+
document.instance_variable_get(:@latexmath_renderer_service) || begin
|
|
34
|
+
service = RendererService.new(document)
|
|
35
|
+
document.instance_variable_set(:@latexmath_renderer_service, service)
|
|
36
|
+
service
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_block_attributes(result, original_attrs)
|
|
41
|
+
attributes = result.attributes.dup
|
|
42
|
+
attributes["target"] ||= result.target
|
|
43
|
+
attributes["alt"] = sanitize_alt(fetch_attr(original_attrs, :alt), result.alt_text)
|
|
44
|
+
attributes["data-latex-original"] = result.alt_text
|
|
45
|
+
attributes["role"] = merge_roles(attributes["role"], fetch_attr(original_attrs, :role))
|
|
46
|
+
if (align = fetch_attr(original_attrs, :align))
|
|
47
|
+
attributes["align"] = align
|
|
48
|
+
end
|
|
49
|
+
attributes
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fetch_attr(attrs, name)
|
|
53
|
+
return nil unless attrs
|
|
54
|
+
|
|
55
|
+
attrs[name.to_s] || attrs[name]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sanitize_alt(user_value, fallback)
|
|
59
|
+
return fallback if user_value.nil?
|
|
60
|
+
|
|
61
|
+
value = user_value.to_s
|
|
62
|
+
value.empty? ? fallback : value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def merge_roles(existing_role, additional_role)
|
|
66
|
+
existing = existing_role.to_s.split
|
|
67
|
+
additional = additional_role.to_s.split
|
|
68
|
+
combined = (existing + additional).uniq
|
|
69
|
+
combined.empty? ? nil : combined.join(" ")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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 "asciidoctor/extensions"
|
|
9
|
+
|
|
10
|
+
require_relative "../renderer_service"
|
|
11
|
+
|
|
12
|
+
module Asciidoctor
|
|
13
|
+
module Latexmath
|
|
14
|
+
module Processors
|
|
15
|
+
class InlineMacroProcessor < ::Asciidoctor::Extensions::InlineMacroProcessor
|
|
16
|
+
use_dsl
|
|
17
|
+
|
|
18
|
+
named :latexmath
|
|
19
|
+
|
|
20
|
+
def process(parent, target, attrs)
|
|
21
|
+
content = attrs["text"] || target
|
|
22
|
+
result = renderer_service(parent).render_inline_content(parent, content, attrs)
|
|
23
|
+
return create_inline(parent, :quoted, result.placeholder_html) if result.type == :placeholder
|
|
24
|
+
|
|
25
|
+
create_inline(parent, :image, nil, build_inline_attributes(result, attrs))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def renderer_service(parent)
|
|
31
|
+
document = parent.document
|
|
32
|
+
document.instance_variable_get(:@latexmath_renderer_service) || begin
|
|
33
|
+
service = RendererService.new(document)
|
|
34
|
+
document.instance_variable_set(:@latexmath_renderer_service, service)
|
|
35
|
+
service
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_inline_attributes(result, original_attrs)
|
|
40
|
+
attributes = result.attributes.dup
|
|
41
|
+
attributes["target"] ||= result.target
|
|
42
|
+
attributes["alt"] = sanitize_alt(fetch_attr(original_attrs, :alt), result.alt_text)
|
|
43
|
+
attributes["data-latex-original"] = result.alt_text
|
|
44
|
+
attributes["role"] = merge_roles(attributes["role"], fetch_attr(original_attrs, :role))
|
|
45
|
+
attributes
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch_attr(attrs, name)
|
|
49
|
+
return nil unless attrs
|
|
50
|
+
|
|
51
|
+
attrs[name.to_s] || attrs[name]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sanitize_alt(user_value, fallback)
|
|
55
|
+
return fallback if user_value.nil?
|
|
56
|
+
|
|
57
|
+
value = user_value.to_s
|
|
58
|
+
value.empty? ? fallback : value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def merge_roles(existing_role, additional_role)
|
|
62
|
+
existing = existing_role.to_s.split
|
|
63
|
+
additional = additional_role.to_s.split
|
|
64
|
+
combined = (existing + additional).uniq
|
|
65
|
+
combined.empty? ? nil : combined.join(" ")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "asciidoctor"
|
|
4
|
+
require "asciidoctor/extensions"
|
|
5
|
+
|
|
6
|
+
module Asciidoctor
|
|
7
|
+
module Latexmath
|
|
8
|
+
module Processors
|
|
9
|
+
class StatisticsPostprocessor < ::Asciidoctor::Extensions::Postprocessor
|
|
10
|
+
def process(document, output)
|
|
11
|
+
service = document.instance_variable_get(:@latexmath_renderer_service)
|
|
12
|
+
service&.send(:flush_statistics)
|
|
13
|
+
output
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
class RenderRequest
|
|
10
|
+
attr_reader :expression, :format, :engine, :preamble, :fontsize, :ppi, :timeout, :keep_artifacts,
|
|
11
|
+
:nocache, :cachedir, :artifacts_dir, :tool_overrides, :content_hash, :preamble_hash, :fontsize_hash
|
|
12
|
+
|
|
13
|
+
def initialize(expression:, format:, engine:, preamble:, fontsize:, ppi:, timeout:, keep_artifacts:, nocache:, cachedir:, artifacts_dir:, tool_overrides:, content_hash:, preamble_hash:, fontsize_hash:)
|
|
14
|
+
@expression = expression
|
|
15
|
+
@format = format
|
|
16
|
+
@engine = engine
|
|
17
|
+
@preamble = preamble
|
|
18
|
+
@fontsize = fontsize
|
|
19
|
+
@ppi = ppi
|
|
20
|
+
@timeout = timeout
|
|
21
|
+
@keep_artifacts = keep_artifacts
|
|
22
|
+
@nocache = nocache
|
|
23
|
+
@cachedir = cachedir
|
|
24
|
+
@artifacts_dir = artifacts_dir
|
|
25
|
+
@tool_overrides = tool_overrides
|
|
26
|
+
@content_hash = content_hash
|
|
27
|
+
@preamble_hash = preamble_hash
|
|
28
|
+
@fontsize_hash = fontsize_hash
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|