asciidoctor-pdf-mathjax 0.1.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 +7 -0
- data/bin/render.js +26 -0
- data/lib/asciidoctor-pdf-mathjax.rb +225 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5463b0301cbd7e4d75a4f567a9e0b97c19ae4ec607ee167403630cbb4a7f65da
|
4
|
+
data.tar.gz: dfcce64d1431856ae431c300d91c1bf0d5739fb95325d38c58931aadda1d4a21
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b3dd0ed36a5e2fa4d2157a5dbc189d7c84a0fbb7ab15c873bf863e03c4121b0c37637457cfc55af98d1e3a07f4f8e1d3ce284f16a7bd0a3b020fd90953ac769
|
7
|
+
data.tar.gz: 2b9a8082c4795438457733cd4d90d8b386e7c46be9e0775dedda29bed809db10f1837b06194bd10caed9a7ea1f0df2f4f38b8d3c5892b3c78e853299480d83c5
|
data/bin/render.js
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
const mj = require('mathjax-node');
|
4
|
+
mj.config({
|
5
|
+
MathJax: {
|
6
|
+
// MathJax configuration
|
7
|
+
}
|
8
|
+
});
|
9
|
+
mj.start();
|
10
|
+
|
11
|
+
async function convertToSvg(latex, format, pixels_per_ex) {
|
12
|
+
const data = await mj.typeset({
|
13
|
+
ex: pixels_per_ex,
|
14
|
+
math: latex,
|
15
|
+
format: format,
|
16
|
+
svg: true,
|
17
|
+
});
|
18
|
+
return data.svg;
|
19
|
+
}
|
20
|
+
|
21
|
+
const latex = process.argv[2];
|
22
|
+
const format = process.argv[3];
|
23
|
+
const pixels_per_ex = parseInt(process.argv[4]);
|
24
|
+
convertToSvg(latex, format, pixels_per_ex).then(svg => {
|
25
|
+
console.log(svg);
|
26
|
+
}).catch(err => console.error(err));
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'asciidoctor-pdf' unless Asciidoctor::Converter.for 'pdf'
|
2
|
+
require 'open3'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'ttfunk'
|
6
|
+
require 'asciimath'
|
7
|
+
|
8
|
+
POINTS_PER_EX = 6
|
9
|
+
MATHJAX_DEFAULT_COLOR_STRING = "currentColor"
|
10
|
+
|
11
|
+
FALLBACK_FONT_SIZE = 12
|
12
|
+
FALLBACK_FONT_STYLE = 'normal'
|
13
|
+
FALLBACK_FONT_FAMILY = 'Arial'
|
14
|
+
FALLBACK_FONT_COLOR = '#000000'
|
15
|
+
|
16
|
+
class AsciidoctorPDFExtensions < (Asciidoctor::Converter.for 'pdf')
|
17
|
+
register_for 'pdf'
|
18
|
+
|
19
|
+
@tempfiles = []
|
20
|
+
class << self
|
21
|
+
attr_reader :tempfiles
|
22
|
+
end
|
23
|
+
|
24
|
+
def convert_stem node
|
25
|
+
arrange_block node do |_|
|
26
|
+
add_dest_for_block node if node.id
|
27
|
+
|
28
|
+
latex_content = extract_latex_content(node.content, node.style.to_sym)
|
29
|
+
|
30
|
+
svg_output, error = stem_to_svg(latex_content, false)
|
31
|
+
|
32
|
+
if svg_output.nil? || svg_output.empty?
|
33
|
+
logger.warn "Failed to convert STEM to SVG: #{error} (Fallback to code block)"
|
34
|
+
pad_box @theme.code_padding, node do
|
35
|
+
theme_font :code do
|
36
|
+
typeset_formatted_text [{ text: (guard_indentation latex_content), color: @font_color }],
|
37
|
+
(calc_line_metrics @base_line_height),
|
38
|
+
bottom_gutter: @bottom_gutters[-1][node]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
else
|
42
|
+
svg_output = adjust_svg_color(svg_output, @font_color)
|
43
|
+
svg_file = Tempfile.new(['stem', '.svg'])
|
44
|
+
begin
|
45
|
+
svg_file.write(svg_output)
|
46
|
+
svg_file.close
|
47
|
+
|
48
|
+
pad_box @theme.code_padding, node do
|
49
|
+
begin
|
50
|
+
image_obj = image svg_file.path, position: :center
|
51
|
+
logger.debug "Successfully embedded stem block (as latex) #{latex_content} as SVG image" if image_obj
|
52
|
+
rescue Prawn::Errors::UnsupportedImageType => e
|
53
|
+
logger.warn "Unsupported image type error: #{e.message}"
|
54
|
+
rescue StandardError => e
|
55
|
+
logger.warn "Failed embedding SVG: #{e.message}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
ensure
|
59
|
+
svg_file.unlink
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
theme_margin :block, :bottom, (next_enclosed_block node)
|
64
|
+
end
|
65
|
+
|
66
|
+
def convert_inline_quoted node
|
67
|
+
latex_content = extract_latex_content(node.text, node.type)
|
68
|
+
return super if latex_content.nil?
|
69
|
+
|
70
|
+
theme = (load_theme node.document)
|
71
|
+
|
72
|
+
svg_output, error = stem_to_svg(latex_content, true)
|
73
|
+
adjusted_svg, svg_width = adjust_svg_to_match_text(svg_output, node, theme)
|
74
|
+
if adjusted_svg.nil? || adjusted_svg.empty?
|
75
|
+
logger.warn "Error processing stem: #{error || 'No SVG output'}"
|
76
|
+
return super
|
77
|
+
end
|
78
|
+
|
79
|
+
tmp_svg = Tempfile.new(['stem-', '.svg'])
|
80
|
+
self.class.tempfiles << tmp_svg
|
81
|
+
begin
|
82
|
+
tmp_svg.write(adjusted_svg)
|
83
|
+
tmp_svg.close
|
84
|
+
|
85
|
+
logger.debug "Successfully embedded stem inline #{node.text} as SVG image"
|
86
|
+
quoted_text = "<img src=\"#{tmp_svg.path}\" format=\"svg\" width=\"#{svg_width}\" alt=\"#{node.text}\">"
|
87
|
+
node.id ? %(<a id="#{node.id}">#{DummyText}</a>#{quoted_text}) : quoted_text
|
88
|
+
rescue => e
|
89
|
+
logger.warn "Failed to process SVG: #{e.message}"
|
90
|
+
super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def extract_latex_content(content, type)
|
97
|
+
content = content.strip.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
98
|
+
case type
|
99
|
+
when :latexmath
|
100
|
+
return content
|
101
|
+
when :asciimath
|
102
|
+
return AsciiMath.parse(content).to_latex
|
103
|
+
else
|
104
|
+
return nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def adjust_svg_color(svg_output, font_color)
|
109
|
+
svg_output.gsub(MATHJAX_DEFAULT_COLOR_STRING, "##{font_color}")
|
110
|
+
end
|
111
|
+
|
112
|
+
def stem_to_svg(latex_content, is_inline)
|
113
|
+
js_script = File.join(File.dirname(__FILE__), '../bin/render.js')
|
114
|
+
svg_output, error = nil, nil
|
115
|
+
format = is_inline ? 'inline-TeX' : 'TeX'
|
116
|
+
Open3.popen3('node', js_script, latex_content, format, POINTS_PER_EX.to_s) do |_, stdout, stderr, wait_thr|
|
117
|
+
svg_output = stdout.read
|
118
|
+
error = stderr.read unless wait_thr.value.success?
|
119
|
+
end
|
120
|
+
[svg_output, error]
|
121
|
+
end
|
122
|
+
|
123
|
+
def adjust_svg_to_match_text(svg_content, node, theme)
|
124
|
+
node_context = find_font_context(node)
|
125
|
+
logger.debug "Found font context #{node_context} for node #{node}"
|
126
|
+
|
127
|
+
if node_context.is_a?(Asciidoctor::Section)
|
128
|
+
level = node_context.level.next
|
129
|
+
theme_key = "heading_h#{level}"
|
130
|
+
if node_context.sectname == 'abstract'
|
131
|
+
theme_key = 'abstract_title'
|
132
|
+
end
|
133
|
+
|
134
|
+
font_family = theme["#{theme_key}_font_family"] || theme['heading_font_family'] || theme['base_font_family'] || FALLBACK_FONT_FAMILY
|
135
|
+
font_style = theme["#{theme_key}_font_style"] || theme['heading_font_style'] || theme['base_font_style'] || FALLBACK_FONT_STYLE
|
136
|
+
font_size = theme["#{theme_key}_font_size"] || theme['heading_font_size'] || theme['base_font_size'] || FALLBACK_FONT_SIZE
|
137
|
+
font_color = theme["#{theme_key}_font_color"] || theme['heading_font_color'] || theme['base_font_color'] || FALLBACK_FONT_COLOR
|
138
|
+
else
|
139
|
+
if node_context.parent.is_a?(Asciidoctor::Section) && node_context.parent.sectname == 'abstract'
|
140
|
+
theme_key = :abstract
|
141
|
+
else
|
142
|
+
theme_key = :base
|
143
|
+
end
|
144
|
+
|
145
|
+
font_family = nil
|
146
|
+
font_style = nil
|
147
|
+
font_size = nil
|
148
|
+
font_color = nil
|
149
|
+
converter = node_context.converter
|
150
|
+
converter.theme_font theme_key do
|
151
|
+
font_family = converter.font_family || FALLBACK_FONT_FAMILY
|
152
|
+
font_style = converter.font_style || FALLBACK_FONT_STYLE
|
153
|
+
font_size = converter.font_size || FALLBACK_FONT_SIZE
|
154
|
+
font_color = converter.font_color || FALLBACK_FONT_COLOR
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
font_catalog = theme.font_catalog
|
159
|
+
font_file = font_catalog[font_family][font_style.to_s]
|
160
|
+
|
161
|
+
font = TTFunk::File.open(font_file)
|
162
|
+
descender_height = font.horizontal_header.descent.abs
|
163
|
+
ascender_height = font.horizontal_header.ascent.abs
|
164
|
+
|
165
|
+
units_per_em = font.header.units_per_em.to_f
|
166
|
+
total_height = (descender_height.to_f + ascender_height.to_f)
|
167
|
+
|
168
|
+
embedding_text_height = total_height / units_per_em * font_size
|
169
|
+
embedding_text_baseline_height = descender_height / units_per_em * font_size
|
170
|
+
|
171
|
+
svg_doc = REXML::Document.new(svg_content)
|
172
|
+
svg_width = svg_doc.root.attributes['width'].to_f * POINTS_PER_EX || raise("No width found in SVG")
|
173
|
+
svg_height = svg_doc.root.attributes['height'].to_f * POINTS_PER_EX || raise("No height found in SVG")
|
174
|
+
view_box = svg_doc.root.attributes['viewBox']&.split(/\s+/)&.map(&:to_f) || raise("No viewBox found in SVG")
|
175
|
+
svg_inner_offset = view_box[1]
|
176
|
+
svg_inner_height = view_box[3]
|
177
|
+
|
178
|
+
svg_default_font_size = FALLBACK_FONT_SIZE
|
179
|
+
|
180
|
+
# Adjust SVG height and width so that math font matches embedding text
|
181
|
+
scaling_factor = font_size.to_f / svg_default_font_size
|
182
|
+
svg_width = svg_width * scaling_factor
|
183
|
+
svg_height = svg_height * scaling_factor
|
184
|
+
|
185
|
+
svg_height_difference = embedding_text_height - svg_height
|
186
|
+
svg_relative_height_difference = embedding_text_height / svg_height
|
187
|
+
embedding_text_relative_baseline_height = embedding_text_baseline_height / embedding_text_height
|
188
|
+
|
189
|
+
logger.debug "Original SVG height: #{svg_height.round(2)}, width: #{svg_width.round(2)}, inner height: #{svg_inner_height.round(2)}, inner offset: #{svg_inner_offset.round(2)}"
|
190
|
+
if svg_height_difference < 0
|
191
|
+
svg_relative_portion_extending_embedding_text_below = (1 - svg_relative_height_difference) / 2
|
192
|
+
svg_relative_baseline_height = embedding_text_relative_baseline_height * svg_relative_height_difference
|
193
|
+
svg_inner_relative_offset = svg_relative_baseline_height + svg_relative_portion_extending_embedding_text_below - 1
|
194
|
+
|
195
|
+
svg_inner_offset = svg_inner_relative_offset * svg_inner_height
|
196
|
+
else
|
197
|
+
svg_height = embedding_text_height
|
198
|
+
svg_inner_height = svg_relative_height_difference * svg_inner_height
|
199
|
+
svg_inner_offset = (embedding_text_relative_baseline_height - 1) * svg_inner_height
|
200
|
+
end
|
201
|
+
|
202
|
+
view_box[1] = svg_inner_offset
|
203
|
+
view_box[3] = svg_inner_height
|
204
|
+
svg_doc.root.attributes['viewBox'] = view_box.join(' ')
|
205
|
+
svg_doc.root.attributes['height'] = "#{svg_height / POINTS_PER_EX}ex"
|
206
|
+
svg_doc.root.attributes['width'] = "#{svg_width / POINTS_PER_EX}ex"
|
207
|
+
svg_doc.root.attributes.delete('style')
|
208
|
+
|
209
|
+
logger.debug "Adjusted SVG height: #{svg_height.round(2)}, width: #{svg_width.round(2)}, inner height: #{svg_inner_height.round(2)}, inner offset: #{svg_inner_offset.round(2)}"
|
210
|
+
svg_output = adjust_svg_color(svg_doc.to_s, font_color)
|
211
|
+
|
212
|
+
[svg_output, svg_width]
|
213
|
+
rescue => e
|
214
|
+
logger.warn "Failed to adjust SVG baseline: #{e.full_message}"
|
215
|
+
nil # Fallback to original if adjustment fails
|
216
|
+
end
|
217
|
+
|
218
|
+
def find_font_context(node)
|
219
|
+
while node
|
220
|
+
return node unless node.is_a?(Asciidoctor::Inline)
|
221
|
+
node = node.parent
|
222
|
+
end
|
223
|
+
node
|
224
|
+
end
|
225
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: asciidoctor-pdf-mathjax
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Crown0815
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-03-10 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: asciidoctor
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: asciidoctor-pdf
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.3'
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 2.3.19
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '2.3'
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.3.19
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: nokogiri
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - "~>"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '1.18'
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 1.18.3
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '1.18'
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.18.3
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: asciimath
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - "~>"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '2.0'
|
73
|
+
type: :runtime
|
74
|
+
prerelease: false
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '2.0'
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: bigdecimal
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - "~>"
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '3.0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - "~>"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '3.0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: bundler
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - "~>"
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '2.0'
|
101
|
+
type: :development
|
102
|
+
prerelease: false
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - "~>"
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '2.0'
|
108
|
+
- !ruby/object:Gem::Dependency
|
109
|
+
name: rake
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '13.0'
|
115
|
+
type: :development
|
116
|
+
prerelease: false
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - "~>"
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '13.0'
|
122
|
+
- !ruby/object:Gem::Dependency
|
123
|
+
name: minitest
|
124
|
+
requirement: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - "~>"
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '5.0'
|
129
|
+
type: :development
|
130
|
+
prerelease: false
|
131
|
+
version_requirements: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - "~>"
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '5.0'
|
136
|
+
- !ruby/object:Gem::Dependency
|
137
|
+
name: logger
|
138
|
+
requirement: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - "~>"
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '1.4'
|
143
|
+
type: :development
|
144
|
+
prerelease: false
|
145
|
+
version_requirements: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - "~>"
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '1.4'
|
150
|
+
description: Converts STEM blocks and inline macros to SVG images using MathJax-node
|
151
|
+
for AsciiDoctor PDF.
|
152
|
+
executables:
|
153
|
+
- render.js
|
154
|
+
extensions: []
|
155
|
+
extra_rdoc_files: []
|
156
|
+
files:
|
157
|
+
- bin/render.js
|
158
|
+
- lib/asciidoctor-pdf-mathjax.rb
|
159
|
+
homepage: https://github.com/Crown0815/asciidoctor-pdf-mathjax
|
160
|
+
licenses:
|
161
|
+
- MIT
|
162
|
+
metadata: {}
|
163
|
+
post_install_message: |
|
164
|
+
Thank you for installing asciidoctor-pdf-mathjax!
|
165
|
+
Note: This gem requires MathJax-Node for full functionality (e.g., LaTeX rendering).
|
166
|
+
If you haven't installed it, run:
|
167
|
+
npm install -g mathjax-node
|
168
|
+
See the README for details.
|
169
|
+
rdoc_options: []
|
170
|
+
require_paths:
|
171
|
+
- lib
|
172
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '2.7'
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
requirements: []
|
183
|
+
rubygems_version: 3.6.2
|
184
|
+
specification_version: 4
|
185
|
+
summary: AsciiDoctor extension to render STEM fields using MathJax SVG
|
186
|
+
test_files: []
|