asciidoctor-pdf-mathjax 0.6.0 → 0.8.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/render.js +122 -40
  3. data/lib/asciidoctor-pdf-mathjax.rb +36 -28
  4. metadata +18 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fc66d0f0c7026c39d6363d44464ad9c5f430aed3202b344de302604679d3166
4
- data.tar.gz: 62009314bb1e5e72e759e2251ce7eaa7ffc14bb123a365a9c0f88dcc58d04bac
3
+ metadata.gz: 7a4ff18612f669474f7731d46e59103e23488c5dcdde291a5aa6635249e28d88
4
+ data.tar.gz: 6fea7b703e6de28edfe0b9615e3132cabf704bdfdc1ca14b2750a1cb5b610b49
5
5
  SHA512:
6
- metadata.gz: 5234f95491bf7ec6320942e21c009f77575567a4312b1651e13736f19b1cb85d8eeee8cd9b3e443e483628c0b1e7d18847384cb98e9b4cab5efafb539973bcc8
7
- data.tar.gz: 7e89244f2911bc889e4f2a3cdd2d34fc31fde201bacac64a0d995ddcbfbd14afe367e98c20922c828ce80bed93e0e05877cd2dbfb81fd655adda92e5c396a6e0
6
+ metadata.gz: ddf278d42765f9cd09394a73b62c6dd34d88059dae04265d5d15106a2834918d51464cab444ea1731e8747ca2fc9254815f41aac9bbfcc0ee84f829279ceefd8
7
+ data.tar.gz: 1c6066f9e7df67a6c1c9f9df9a584dd8b99161ed3f7fc8b37966db09a206ff0542275deb7b794e729b5a6390b64eb48f2aea3485e4c2e1bc3976ef44a6f39f33
data/bin/render.js CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const mj = require('mathjax-node');
4
-
5
3
  // Two modes:
6
4
  // render.js <latex> <format> <pixels_per_ex> <font>
7
5
  // renders one formula, prints the SVG and exits.
@@ -11,48 +9,132 @@ const mj = require('mathjax-node');
11
9
  // per formula, {"svg": "..."} or {"error": "..."}.
12
10
  // Starting MathJax takes seconds; the server mode pays that once per font
13
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.
15
+
14
16
  const server = process.argv[2] === '--server';
15
- const font = server ? process.argv[3] : process.argv[5];
17
+ const fontArg = server ? process.argv[3] : process.argv[5];
16
18
 
17
- mj.config({
18
- MathJax: {
19
- SVG: {
20
- font: font
21
- }
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'] };
22
35
  }
23
- });
24
- mj.start();
25
-
26
- async function convertToSvg(latex, format, pixels_per_ex) {
27
- const data = await mj.typeset({
28
- ex: pixels_per_ex,
29
- math: latex,
30
- format: format,
31
- svg: true,
32
- });
33
- return data.svg;
36
+ return { font: font, fontExtensions: [] };
34
37
  }
35
38
 
36
- if (server) {
37
- let queue = Promise.resolve();
38
- require('readline').createInterface({ input: process.stdin }).on('line', line => {
39
- queue = queue.then(async () => {
40
- let answer;
41
- try {
42
- const request = JSON.parse(line);
43
- answer = { svg: await convertToSvg(request.math, request.format, request.ex) };
44
- } catch (err) {
45
- answer = { error: String(err) };
46
- }
47
- process.stdout.write(JSON.stringify(answer) + '\n');
48
- });
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;
49
85
  });
50
- } else {
51
- const latex = process.argv[2];
52
- const format = process.argv[3];
53
- const pixels_per_ex = parseInt(process.argv[4]);
54
-
55
- convertToSvg(latex, format, pixels_per_ex).then(svg => {
56
- console.log(svg);
57
- }).catch(err => console.error(err));
58
86
  }
87
+
88
+ MathJax.init({
89
+ loader: {
90
+ require: require,
91
+ load: ['input/tex', 'output/svg'],
92
+ // Font packages can report an older component version than MathJax itself.
93
+ versionWarnings: false,
94
+ },
95
+ output: {
96
+ font: font,
97
+ fontExtensions: fontExtensions,
98
+ displayOverflow: 'overflow',
99
+ linebreaks: { inline: false },
100
+ },
101
+ svg: {
102
+ fontCache: 'local',
103
+ },
104
+ startup: {
105
+ typeset: false,
106
+ },
107
+ }).then(() => {
108
+ if (server) {
109
+ let queue = Promise.resolve();
110
+ const input = require('readline').createInterface({ input: process.stdin });
111
+ input.on('line', (line) => {
112
+ queue = queue.then(async () => {
113
+ let answer;
114
+ try {
115
+ const request = JSON.parse(line);
116
+ answer = { svg: await convertToSvg(request.math, request.format, request.ex) };
117
+ } catch (err) {
118
+ answer = { error: String(err && err.message ? err.message : err) };
119
+ }
120
+ process.stdout.write(JSON.stringify(answer) + '\n');
121
+ });
122
+ });
123
+ input.on('close', () => {
124
+ queue.then(() => {
125
+ if (MathJax.done) MathJax.done();
126
+ });
127
+ });
128
+ } else {
129
+ const latex = process.argv[2];
130
+ const format = process.argv[3];
131
+ const pixelsPerEx = parseInt(process.argv[4], 10);
132
+ return convertToSvg(latex, format, pixelsPerEx).then((svg) => {
133
+ process.stdout.write(svg + '\n');
134
+ if (MathJax.done) MathJax.done();
135
+ });
136
+ }
137
+ }).catch((err) => {
138
+ console.error(err && err.message ? err.message : err);
139
+ process.exit(1);
140
+ });
@@ -199,32 +199,33 @@ class AsciidoctorPDFExtensions < (Asciidoctor::Converter.for 'pdf')
199
199
  svg_width = svg_width * scaling_factor
200
200
  svg_height = svg_height * scaling_factor
201
201
 
202
- svg_height_difference = font.font_full_height - svg_height
203
- svg_relative_height_difference = font.font_full_height / svg_height
204
- 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
205
218
 
206
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)}"
207
- if svg_height_difference < 0
208
- svg_relative_portion_extending_embedding_text_below = (1 - svg_relative_height_difference) / 2
209
- svg_relative_baseline_height = embedding_text_relative_baseline_height * svg_relative_height_difference
210
- svg_inner_relative_offset = svg_relative_baseline_height + svg_relative_portion_extending_embedding_text_below - 1
211
-
212
- svg_inner_offset_new = svg_inner_relative_offset * svg_inner_height
213
- svg_inner_height_padding = (svg_inner_offset - svg_inner_offset_new) * 0.25 # 25% padding to handle fractions
214
- svg_inner_height_difference = 2 * svg_inner_height_padding
215
- svg_inner_height_new = svg_inner_height + svg_inner_height_difference
216
- svg_inner_height_relative_difference = svg_inner_height_new / svg_inner_height
217
-
218
- 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}")
219
- 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}")
220
-
221
- svg_height = svg_height * svg_inner_height_relative_difference
222
- svg_inner_height = svg_inner_height_new
223
- 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
224
225
  else
225
- svg_height = font.font_full_height
226
- svg_inner_height = svg_relative_height_difference * svg_inner_height
227
- 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
228
229
  end
229
230
 
230
231
  view_box[1] = svg_inner_offset
@@ -280,12 +281,19 @@ class AsciidoctorPDFExtensions < (Asciidoctor::Converter.for 'pdf')
280
281
  raise "No font context found for node #{node}"
281
282
  end
282
283
 
283
- # noinspection RubyResolve
284
- font_catalog = theme.font_catalog
285
- font_file = font_catalog[font_family][font_style.to_s]
284
+ # asciidoctor-pdf has already resolved the font catalog - against pdf-fontsdir, the theme
285
+ # directory and its bundled fonts - and registered the files with Prawn. Use that file, so a
286
+ # path relative to the theme directory is not looked up in the working directory.
287
+ font_file = node.document.converter.font_families.dig(font_family, font_style.to_sym) if node.document.converter.respond_to?(:font_families)
286
288
 
287
- if font_file && !font_file.include?(File::SEPARATOR)
288
- font_file = File.join(node.document.attributes['pdf-fontsdir'], font_file)
289
+ unless font_file && File.file?(font_file)
290
+ # noinspection RubyResolve
291
+ font_catalog = theme.font_catalog
292
+ font_file = font_catalog[font_family][font_style.to_s]
293
+
294
+ if font_file && !font_file.include?(File::SEPARATOR)
295
+ font_file = File.join(node.document.attributes['pdf-fontsdir'], font_file)
296
+ end
289
297
  end
290
298
 
291
299
  font = TTFunk::File.open(font_file)
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.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Crown0815
@@ -98,63 +98,63 @@ dependencies:
98
98
  - !ruby/object:Gem::Version
99
99
  version: '3.0'
100
100
  - !ruby/object:Gem::Dependency
101
- name: bundler
101
+ name: rake
102
102
  requirement: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - "~>"
105
105
  - !ruby/object:Gem::Version
106
- version: '2.0'
106
+ version: '13.0'
107
107
  type: :development
108
108
  prerelease: false
109
109
  version_requirements: !ruby/object:Gem::Requirement
110
110
  requirements:
111
111
  - - "~>"
112
112
  - !ruby/object:Gem::Version
113
- version: '2.0'
113
+ version: '13.0'
114
114
  - !ruby/object:Gem::Dependency
115
- name: rake
115
+ name: minitest
116
116
  requirement: !ruby/object:Gem::Requirement
117
117
  requirements:
118
118
  - - "~>"
119
119
  - !ruby/object:Gem::Version
120
- version: '13.0'
120
+ version: '5.0'
121
121
  type: :development
122
122
  prerelease: false
123
123
  version_requirements: !ruby/object:Gem::Requirement
124
124
  requirements:
125
125
  - - "~>"
126
126
  - !ruby/object:Gem::Version
127
- version: '13.0'
127
+ version: '5.0'
128
128
  - !ruby/object:Gem::Dependency
129
- name: minitest
129
+ name: logger
130
130
  requirement: !ruby/object:Gem::Requirement
131
131
  requirements:
132
132
  - - "~>"
133
133
  - !ruby/object:Gem::Version
134
- version: '5.0'
134
+ version: '1.4'
135
135
  type: :development
136
136
  prerelease: false
137
137
  version_requirements: !ruby/object:Gem::Requirement
138
138
  requirements:
139
139
  - - "~>"
140
140
  - !ruby/object:Gem::Version
141
- version: '5.0'
141
+ version: '1.4'
142
142
  - !ruby/object:Gem::Dependency
143
- name: logger
143
+ name: pdf-reader
144
144
  requirement: !ruby/object:Gem::Requirement
145
145
  requirements:
146
146
  - - "~>"
147
147
  - !ruby/object:Gem::Version
148
- version: '1.4'
148
+ version: '2.0'
149
149
  type: :development
150
150
  prerelease: false
151
151
  version_requirements: !ruby/object:Gem::Requirement
152
152
  requirements:
153
153
  - - "~>"
154
154
  - !ruby/object:Gem::Version
155
- version: '1.4'
156
- description: Converts STEM blocks and inline macros to SVG images using MathJax-node
157
- for AsciiDoctor PDF.
155
+ version: '2.0'
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