asciidoctor-pdf-mathjax 0.5.0 → 0.7.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/bin/render.js +131 -23
- data/lib/asciidoctor-pdf-mathjax.rb +79 -34
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ef66936477b46fb7aceae1c15a22cb42ebcea5d227ab19be302493e55efc9d2
|
|
4
|
+
data.tar.gz: df675d9c39be83650834c20b818c437c75bf842a117be0ba5fa4c4ce0e23eddb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 204c9d8cb2f5fc184ca1e968d4666bcb971fe1e44dba2306d6230a0a2800a22178bfd417fc032a64e9e989d43ac4f89a027de8c99ba3e244c49c683986ad725f
|
|
7
|
+
data.tar.gz: e908b87d74694eddce9063863755e4119bbe9ad6adc7959c8373da173b3ca75bcb6aaf1cecf8a3559448762af696928091df1a53f3e727fae3055888a7b55a8a
|
data/bin/render.js
CHANGED
|
@@ -1,31 +1,139 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Two modes:
|
|
4
|
+
// render.js <latex> <format> <pixels_per_ex> <font>
|
|
5
|
+
// renders one formula, prints the SVG and exits.
|
|
6
|
+
// render.js --server <font>
|
|
7
|
+
// starts MathJax once and renders one formula per line of stdin,
|
|
8
|
+
// {"math": "...", "format": "TeX", "ex": 6}, answering with one line
|
|
9
|
+
// per formula, {"svg": "..."} or {"error": "..."}.
|
|
10
|
+
// Starting MathJax takes seconds; the server mode pays that once per font
|
|
11
|
+
// instead of once per formula.
|
|
12
|
+
//
|
|
13
|
+
// Font names are MathJax 4 names (mathjax-tex, mathjax-stix2, ...).
|
|
14
|
+
// The MathJax 2 names previously passed to mathjax-node are still accepted.
|
|
4
15
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const pixels_per_ex = parseInt(process.argv[4]);
|
|
8
|
-
const font = process.argv[5];
|
|
16
|
+
const server = process.argv[2] === '--server';
|
|
17
|
+
const fontArg = server ? process.argv[3] : process.argv[5];
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
const LEGACY_FONTS = {
|
|
20
|
+
'TeX': 'mathjax-tex',
|
|
21
|
+
'STIX-Web': 'mathjax-stix2',
|
|
22
|
+
'Asana-Math': 'mathjax-asana',
|
|
23
|
+
'Neo-Euler': 'mathjax-euler',
|
|
24
|
+
'Gyre-Pagella': 'mathjax-pagella',
|
|
25
|
+
'Gyre-Termes': 'mathjax-termes',
|
|
26
|
+
'Latin-Modern': 'mathjax-modern',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resolveFont(name) {
|
|
30
|
+
const requested = (name && String(name).trim()) || 'TeX';
|
|
31
|
+
const font = LEGACY_FONTS[requested] || requested;
|
|
32
|
+
// Neo Euler ships as a font extension in MathJax 4, not a standalone font.
|
|
33
|
+
if (font === 'mathjax-euler') {
|
|
34
|
+
return { font: 'mathjax-tex', fontExtensions: ['mathjax-euler'] };
|
|
15
35
|
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
return { font: font, fontExtensions: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertInstalled(packageName) {
|
|
40
|
+
try {
|
|
41
|
+
// Font packages export no main entry, so resolve a file they do export.
|
|
42
|
+
require.resolve(packageName + '/package.json');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Cannot load ${packageName}. Install MathJax 4 with npm, for example: npm install ${packageName}@4`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { font, fontExtensions } = resolveFont(fontArg);
|
|
51
|
+
|
|
52
|
+
let MathJax;
|
|
53
|
+
try {
|
|
54
|
+
assertInstalled('@mathjax/src');
|
|
55
|
+
assertInstalled(`@mathjax/${font}-font`);
|
|
56
|
+
fontExtensions.forEach((extension) => assertInstalled(`@mathjax/${extension}-font-extension`));
|
|
57
|
+
MathJax = require('@mathjax/src');
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function convertToSvg(latex, format, pixelsPerEx) {
|
|
64
|
+
const display = format !== 'inline-TeX';
|
|
65
|
+
const ex = Number(pixelsPerEx) || 6;
|
|
66
|
+
return MathJax.tex2svgPromise(latex, {
|
|
67
|
+
display: display,
|
|
68
|
+
em: ex * 2,
|
|
69
|
+
ex: ex,
|
|
70
|
+
// Formulas are embedded at their natural width. A wide container keeps
|
|
71
|
+
// MathJax from inserting line breaks when a width is still consulted.
|
|
72
|
+
containerWidth: 100000,
|
|
73
|
+
}).then((node) => {
|
|
74
|
+
const adaptor = MathJax.startup.adaptor;
|
|
75
|
+
const svg = adaptor.tags(node, 'svg')[0];
|
|
76
|
+
if (!svg) {
|
|
77
|
+
throw new Error('MathJax produced no SVG');
|
|
78
|
+
}
|
|
79
|
+
const xml = adaptor.serializeXML(svg);
|
|
80
|
+
const mathError = xml.match(/data-mjx-error="([^"]*)"/);
|
|
81
|
+
if (mathError) {
|
|
82
|
+
throw new Error(mathError[1]);
|
|
83
|
+
}
|
|
84
|
+
return xml;
|
|
25
85
|
});
|
|
26
|
-
return data.svg;
|
|
27
86
|
}
|
|
28
87
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
88
|
+
MathJax.init({
|
|
89
|
+
loader: {
|
|
90
|
+
load: ['input/tex', 'output/svg'],
|
|
91
|
+
// Font packages can report an older component version than MathJax itself.
|
|
92
|
+
versionWarnings: false,
|
|
93
|
+
},
|
|
94
|
+
output: {
|
|
95
|
+
font: font,
|
|
96
|
+
fontExtensions: fontExtensions,
|
|
97
|
+
displayOverflow: 'overflow',
|
|
98
|
+
linebreaks: { inline: false },
|
|
99
|
+
},
|
|
100
|
+
svg: {
|
|
101
|
+
fontCache: 'local',
|
|
102
|
+
},
|
|
103
|
+
startup: {
|
|
104
|
+
typeset: false,
|
|
105
|
+
},
|
|
106
|
+
}).then(() => {
|
|
107
|
+
if (server) {
|
|
108
|
+
let queue = Promise.resolve();
|
|
109
|
+
const input = require('readline').createInterface({ input: process.stdin });
|
|
110
|
+
input.on('line', (line) => {
|
|
111
|
+
queue = queue.then(async () => {
|
|
112
|
+
let answer;
|
|
113
|
+
try {
|
|
114
|
+
const request = JSON.parse(line);
|
|
115
|
+
answer = { svg: await convertToSvg(request.math, request.format, request.ex) };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
answer = { error: String(err && err.message ? err.message : err) };
|
|
118
|
+
}
|
|
119
|
+
process.stdout.write(JSON.stringify(answer) + '\n');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
input.on('close', () => {
|
|
123
|
+
queue.then(() => {
|
|
124
|
+
if (MathJax.done) MathJax.done();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
const latex = process.argv[2];
|
|
129
|
+
const format = process.argv[3];
|
|
130
|
+
const pixelsPerEx = parseInt(process.argv[4], 10);
|
|
131
|
+
return convertToSvg(latex, format, pixelsPerEx).then((svg) => {
|
|
132
|
+
process.stdout.write(svg + '\n');
|
|
133
|
+
if (MathJax.done) MathJax.done();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}).catch((err) => {
|
|
137
|
+
console.error(err && err.message ? err.message : err);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'asciidoctor-pdf' unless Asciidoctor::Converter.for 'pdf'
|
|
2
2
|
require 'open3'
|
|
3
|
+
require 'json'
|
|
3
4
|
require 'tempfile'
|
|
4
5
|
require 'rexml/document'
|
|
5
6
|
require 'ttfunk'
|
|
@@ -97,7 +98,7 @@ class AsciidoctorPDFExtensions < (Asciidoctor::Converter.for 'pdf')
|
|
|
97
98
|
tmp_svg.close
|
|
98
99
|
|
|
99
100
|
logger.debug "Successfully embedded stem inline #{node.text} with font #{math_font} as SVG image"
|
|
100
|
-
quoted_text = "<img src=\"#{tmp_svg.path}\" format=\"svg\" width=\"#{svg_width}\" alt=\"#{node.text}\">"
|
|
101
|
+
quoted_text = "<img src=\"#{tmp_svg.path}\" format=\"svg\" width=\"#{svg_width}\" alt=\"#{node.text.gsub('"', '"')}\">"
|
|
101
102
|
node.id ? %(<a id="#{node.id}">#{DummyText}</a>#{quoted_text}) : quoted_text
|
|
102
103
|
rescue => e
|
|
103
104
|
logger.warn "Failed to process SVG: #{e.message}"
|
|
@@ -124,19 +125,62 @@ class AsciidoctorPDFExtensions < (Asciidoctor::Converter.for 'pdf')
|
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
def stem_to_svg(latex_content, is_inline, math_font)
|
|
127
|
-
js_script = File.join(File.dirname(__FILE__), '../bin/render.js')
|
|
128
|
-
svg_output, error = nil, nil
|
|
129
128
|
format = is_inline ? 'inline-TeX' : 'TeX'
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
MathJaxServer.for(math_font).render(latex_content, format)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# One long-running render.js process per math font. Starting node and MathJax takes
|
|
133
|
+
# seconds, rendering a formula takes milliseconds, so a document pays the start once.
|
|
134
|
+
class MathJaxServer
|
|
135
|
+
JS_SCRIPT = File.join(File.dirname(__FILE__), '../bin/render.js')
|
|
136
|
+
|
|
137
|
+
@servers = {}
|
|
138
|
+
|
|
139
|
+
def self.for(math_font)
|
|
140
|
+
@servers[math_font] ||= new(math_font)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def initialize(math_font)
|
|
144
|
+
@math_font = math_font
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def render(latex_content, format)
|
|
148
|
+
start unless @stdin
|
|
149
|
+
@stdin.puts JSON.generate('math' => latex_content, 'format' => format, 'ex' => POINTS_PER_EX)
|
|
150
|
+
@stdin.flush
|
|
151
|
+
answer = read_answer
|
|
152
|
+
[answer['svg'], answer['error']]
|
|
135
153
|
rescue Errno::ENOENT => e
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
[nil, "Node.js executable 'node' was not found. Please install Node.js and ensure 'node' is available on your PATH. Original error: #{e.message}"]
|
|
155
|
+
rescue IOError, SystemCallError => e
|
|
156
|
+
stop
|
|
157
|
+
[nil, "MathJax process for font #{@math_font} failed: #{e.message}"]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def start
|
|
163
|
+
@stdin, @stdout, @wait_thr = Open3.popen2('node', JS_SCRIPT, '--server', @math_font)
|
|
164
|
+
at_exit { stop }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Skips anything else a dependency may print to stdout.
|
|
168
|
+
def read_answer
|
|
169
|
+
while (line = @stdout.gets)
|
|
170
|
+
answer = JSON.parse(line) rescue nil
|
|
171
|
+
return answer if answer.is_a?(Hash) && (answer.key?('svg') || answer.key?('error'))
|
|
172
|
+
end
|
|
173
|
+
raise IOError, 'render.js ended without an answer'
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def stop
|
|
177
|
+
return unless @stdin
|
|
178
|
+
@stdin.close unless @stdin.closed?
|
|
179
|
+
Process.kill('KILL', @wait_thr.pid) unless @wait_thr.join(5)
|
|
180
|
+
@stdin = @stdout = @wait_thr = nil
|
|
181
|
+
rescue SystemCallError
|
|
182
|
+
@stdin = @stdout = @wait_thr = nil
|
|
138
183
|
end
|
|
139
|
-
[svg_output, error]
|
|
140
184
|
end
|
|
141
185
|
|
|
142
186
|
def adjust_svg_to_match_text(svg_content, font)
|
|
@@ -155,32 +199,33 @@ class AsciidoctorPDFExtensions < (Asciidoctor::Converter.for 'pdf')
|
|
|
155
199
|
svg_width = svg_width * scaling_factor
|
|
156
200
|
svg_height = svg_height * scaling_factor
|
|
157
201
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
202
|
+
# Points per viewBox unit. Held constant so the glyphs keep the size MathJax produced.
|
|
203
|
+
pt_per_unit = svg_height / svg_inner_height
|
|
204
|
+
orig_top = svg_inner_offset
|
|
205
|
+
orig_bottom = svg_inner_offset + svg_inner_height
|
|
206
|
+
|
|
207
|
+
# Line-sized window with the math baseline (y = 0) on the text baseline.
|
|
208
|
+
# Asciidoctor centers an inline image in the line when it is at most 1.5 line
|
|
209
|
+
# heights tall, and bottom-aligns it otherwise. A window exactly the line height
|
|
210
|
+
# crops any ink past the font ascender or descender (subscripts, denominators).
|
|
211
|
+
line_units = font.font_full_height / pt_per_unit
|
|
212
|
+
line_bottom = font.font_baseline / pt_per_unit
|
|
213
|
+
line_top = line_bottom - line_units
|
|
214
|
+
overflow_above = [line_top - orig_top, 0].max
|
|
215
|
+
overflow_below = [orig_bottom - line_bottom, 0].max
|
|
216
|
+
# Pad both sides equally so centering still leaves the baseline on the text baseline.
|
|
217
|
+
extra = [overflow_above, overflow_below].max
|
|
161
218
|
|
|
162
219
|
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)}"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
svg_inner_offset_new = svg_inner_relative_offset * svg_inner_height
|
|
169
|
-
svg_inner_height_padding = (svg_inner_offset - svg_inner_offset_new) * 0.25 # 25% padding to handle fractions
|
|
170
|
-
svg_inner_height_difference = 2 * svg_inner_height_padding
|
|
171
|
-
svg_inner_height_new = svg_inner_height + svg_inner_height_difference
|
|
172
|
-
svg_inner_height_relative_difference = svg_inner_height_new / svg_inner_height
|
|
173
|
-
|
|
174
|
-
logger.debug("svg_inner_offset = #{svg_inner_offset}, svg_inner_height = #{svg_inner_height}, svg_inner_offset_new = #{svg_inner_offset_new}, svg_inner_height_new = #{svg_inner_height_new}")
|
|
175
|
-
logger.debug("svg_inner_offset_diff = #{svg_inner_offset - svg_inner_offset_new}, svg_inner_offset_diff_relative = #{(svg_inner_offset - svg_inner_offset_new) / svg_inner_height}")
|
|
176
|
-
|
|
177
|
-
svg_height = svg_height * svg_inner_height_relative_difference
|
|
178
|
-
svg_inner_height = svg_inner_height_new
|
|
179
|
-
svg_inner_offset = svg_inner_offset_new - svg_inner_height_padding
|
|
220
|
+
fitted_height = (line_units + 2 * extra) * pt_per_unit
|
|
221
|
+
if fitted_height <= font.font_full_height * 1.5
|
|
222
|
+
svg_inner_offset = line_top - extra
|
|
223
|
+
svg_inner_height = line_units + 2 * extra
|
|
224
|
+
svg_height = fitted_height
|
|
180
225
|
else
|
|
181
|
-
|
|
182
|
-
svg_inner_height =
|
|
183
|
-
|
|
226
|
+
svg_inner_offset = orig_top
|
|
227
|
+
svg_inner_height = orig_bottom - orig_top
|
|
228
|
+
svg_height = svg_inner_height * pt_per_unit
|
|
184
229
|
end
|
|
185
230
|
|
|
186
231
|
view_box[1] = svg_inner_offset
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asciidoctor-pdf-mathjax
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Crown0815
|
|
@@ -153,8 +153,8 @@ dependencies:
|
|
|
153
153
|
- - "~>"
|
|
154
154
|
- !ruby/object:Gem::Version
|
|
155
155
|
version: '1.4'
|
|
156
|
-
description: Converts STEM blocks and inline macros to SVG images using MathJax
|
|
157
|
-
|
|
156
|
+
description: Converts STEM blocks and inline macros to SVG images using MathJax for
|
|
157
|
+
AsciiDoctor PDF.
|
|
158
158
|
executables:
|
|
159
159
|
- render.js
|
|
160
160
|
extensions: []
|
|
@@ -168,10 +168,10 @@ licenses:
|
|
|
168
168
|
metadata: {}
|
|
169
169
|
post_install_message: |
|
|
170
170
|
Thank you for installing asciidoctor-pdf-mathjax!
|
|
171
|
-
Note: This gem requires MathJax
|
|
171
|
+
Note: This gem requires MathJax 4 for full functionality (e.g., LaTeX rendering).
|
|
172
172
|
If you haven't installed it, run:
|
|
173
|
-
npm install -g mathjax-
|
|
174
|
-
See the README for details.
|
|
173
|
+
npm install -g @mathjax/src@4 @mathjax/mathjax-tex-font@4
|
|
174
|
+
See the README for details and for the other math fonts.
|
|
175
175
|
rdoc_options: []
|
|
176
176
|
require_paths:
|
|
177
177
|
- lib
|