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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 953f3afce27ff482bfcae64ac333e262c1282f6c50d0bdb8317064f3d60b888d
4
- data.tar.gz: ea179ece644755ef9834f2ed4da39b21008917004b2d6ff7f2cbbca82495e6c8
3
+ metadata.gz: 3ef66936477b46fb7aceae1c15a22cb42ebcea5d227ab19be302493e55efc9d2
4
+ data.tar.gz: df675d9c39be83650834c20b818c437c75bf842a117be0ba5fa4c4ce0e23eddb
5
5
  SHA512:
6
- metadata.gz: c671beb46e4bd786b0deceb6af5108430b9417ff1ea4a585b83efb3c77910bffe6b3b1384a47fa7e876708be13485ae7521b43b50dcf64155c748b4cf814a28b
7
- data.tar.gz: 2d76424a66dc155b0078f1b91ba327e2b19f89a41de049c834dc90bf0af5f5609420a193492433d043b1f92e0fd57cc2d0b3c155e18e13808150e4cfba27d9e3
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
- const mj = require('mathjax-node');
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 latex = process.argv[2];
6
- const format = process.argv[3];
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
- mj.config({
11
- MathJax: {
12
- SVG: {
13
- font: font
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
- mj.start();
18
-
19
- async function convertToSvg(latex, format, pixels_per_ex) {
20
- const data = await mj.typeset({
21
- ex: pixels_per_ex,
22
- math: latex,
23
- format: format,
24
- svg: true,
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
- convertToSvg(latex, format, pixels_per_ex).then(svg => {
30
- console.log(svg);
31
- }).catch(err => console.error(err));
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('"', '&quot;')}\">"
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
- begin
131
- Open3.popen3('node', js_script, latex_content, format, POINTS_PER_EX.to_s, math_font) do |_, stdout, stderr, wait_thr|
132
- svg_output = stdout.read
133
- error = stderr.read unless wait_thr.value.success?
134
- end
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
- error = "Node.js executable 'node' was not found. Please install Node.js and ensure 'node' is available on your PATH. Original error: #{e.message}"
137
- svg_output = nil
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
- svg_height_difference = font.font_full_height - svg_height
159
- svg_relative_height_difference = font.font_full_height / svg_height
160
- embedding_text_relative_baseline_height = font.font_baseline / font.font_full_height
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
- if svg_height_difference < 0
164
- svg_relative_portion_extending_embedding_text_below = (1 - svg_relative_height_difference) / 2
165
- svg_relative_baseline_height = embedding_text_relative_baseline_height * svg_relative_height_difference
166
- svg_inner_relative_offset = svg_relative_baseline_height + svg_relative_portion_extending_embedding_text_below - 1
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
- svg_height = font.font_full_height
182
- svg_inner_height = svg_relative_height_difference * svg_inner_height
183
- svg_inner_offset = (embedding_text_relative_baseline_height - 1) * svg_inner_height
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.5.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-node
157
- for AsciiDoctor PDF.
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-Node for full functionality (e.g., LaTeX rendering).
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-node
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