render-math 0.1.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.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # render-math
2
+
3
+ Render MathJax formula inside html fiies from `--src-dir` to SVG and
4
+ write the result html files to `--dest-dir`.
5
+
6
+ ```console
7
+ render-math --help
8
+ Usage: render-math [options]
9
+
10
+ MathJax renderer. Read HTML and render math formula to SVG.
11
+
12
+ Options:
13
+ --src-dir <path> source directory (default: "...")
14
+ --dest-dir <path> destination directory (default: "...")
15
+ -f, --force force render
16
+ -q, --quite print render message only
17
+ --quieter do not print per file message
18
+ -h, --help display help for command
19
+ ```
20
+
21
+ The program uses default source/destination directories
22
+ if you don't specify `--src-dir` and `--dest-dir` options.
23
+
24
+ ```console
25
+ $BLOG_BASE_DIR/public # src
26
+ $BLOG_BASE_DIR/rendered_public # dest
27
+ ```
@@ -0,0 +1,7 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import { defineConfig } from "eslint/config";
4
+
5
+ export default defineConfig([
6
+ { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
7
+ ]);
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "render-math",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool that render maths with MathJax",
5
+ "keywords": [
6
+ "math",
7
+ "mathjax",
8
+ "latex",
9
+ "tex",
10
+ "formula"
11
+ ],
12
+ "homepage": "https://github.com/ntalbs/render-math/#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ntalbs/render-math/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ntalbs/render-math.git"
19
+ },
20
+ "license": "ISC",
21
+ "author": "ntalbs",
22
+ "type": "module",
23
+ "main": "src/index.js",
24
+ "bin": {
25
+ "render-math": "./src/index.js"
26
+ },
27
+ "scripts": {
28
+ "lint": "eslint .",
29
+ "test": "echo \"Error: no test specified\" && exit 1"
30
+ },
31
+ "dependencies": {
32
+ "chalk": "^5.6.2",
33
+ "commander": "^14.0.2",
34
+ "cripto": "^1.1.4",
35
+ "glob": "^13.0.0",
36
+ "jsdom": "^27.4.0",
37
+ "mathjax-full": "^3.2.1"
38
+ },
39
+ "devDependencies": {
40
+ "eslint": "^9.39.2"
41
+ }
42
+ }
package/src/index.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { render } from './math-renderer.js';
5
+
6
+ program
7
+ .description('MathJax renderer. Read HTML and render math formula to SVG.')
8
+ .option('--src-dir <path>', 'source directory', `${process.env.BLOG_BASE_DIR}/public`)
9
+ .option('--dest-dir <path>', 'destination directory', `${process.env.BLOG_BASE_DIR}/rendered-public`)
10
+ .option('-f, --force', 'force render')
11
+ .option('-q, --quite', 'print render message only')
12
+ .option('--quieter', 'do not print per file message')
13
+ .action((options) => {
14
+ render(options.srcDir, options.destDir, options);
15
+ })
16
+ .parse(process.argv);
@@ -0,0 +1,207 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { glob } from 'glob';
5
+ import chalk from 'chalk';
6
+ import { JSDOM } from 'jsdom';
7
+
8
+ // MathJax imports
9
+ import { mathjax } from 'mathjax-full/js/mathjax.js';
10
+ import { TeX } from 'mathjax-full/js/input/tex.js';
11
+ import { SVG } from 'mathjax-full/js/output/svg.js';
12
+ import { liteAdaptor } from 'mathjax-full/js/adaptors/liteAdaptor.js';
13
+ import { RegisterHTMLHandler } from 'mathjax-full/js/handlers/html.js';
14
+ import { AllPackages } from 'mathjax-full/js/input/tex/AllPackages.js';
15
+
16
+ export function render(src, target, options) {
17
+ // --- MathJax Initialization ---
18
+ const adaptor = liteAdaptor();
19
+ RegisterHTMLHandler(adaptor);
20
+ const tex = new TeX({ packages: AllPackages });
21
+ const svg = new SVG({ fontCache: 'local' });
22
+ const mjPage = mathjax.document('', { InputJax: tex, OutputJax: svg });
23
+
24
+ const cacheFile = path.join(target, 'math-cache.json');
25
+ let cache = readCacheFile(cacheFile);
26
+
27
+ const stat = {
28
+ directories: 0,
29
+ rendered: 0,
30
+ copied: 0,
31
+ skipped: 0,
32
+ total: 0
33
+ };
34
+
35
+
36
+ const files = glob.sync(`${src}/**/*`);
37
+
38
+ console.log(chalk.yellow.bold('> Start processing:'), `Found ${files.length} files ...`);
39
+ files.forEach(f => process(f));
40
+ console.log(chalk.green.bold('> Completed.'), stat);
41
+
42
+ writeCacheFile(cacheFile);
43
+
44
+
45
+ // -- internal functions --
46
+
47
+ function readCacheFile(cacheFile) {
48
+ if (fs.existsSync(cacheFile)) {
49
+ return JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
50
+ } else {
51
+ return {}
52
+ }
53
+ }
54
+
55
+ function writeCacheFile(cacheFile) {
56
+ fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
57
+ }
58
+
59
+ function process(sourcePath) {
60
+ let targetPath = getTargetPathFrom(sourcePath);
61
+
62
+ let sourcePathStat = fs.statSync(sourcePath);
63
+ stat.total++;
64
+ if (sourcePathStat.isDirectory()) {
65
+ ensureDir(targetPath);
66
+ stat.directories++;
67
+ } else {
68
+ if (isSrcNotChanged(sourcePath) && !options.force) {
69
+ stat.skipped ++;
70
+ if (!options.quite) {
71
+ log('skip');
72
+ }
73
+ return;
74
+ }
75
+
76
+ ensureDir(path.dirname(targetPath));
77
+ if (sourcePath.endsWith('.html')) {
78
+ if (processHtml(sourcePath, targetPath)) {
79
+ log('render');
80
+ } else {
81
+ log('copy');
82
+ }
83
+ } else {
84
+ stat.copied++;
85
+ if (!options.quite) {
86
+ log('copy');
87
+ }
88
+ fs.copyFileSync(sourcePath, targetPath);
89
+ }
90
+ }
91
+
92
+ function log(action) {
93
+ if (options.quieter) return;
94
+
95
+ switch (action) {
96
+ case 'render':
97
+ console.log(chalk.red.bold('RENDER:'), sourcePath);
98
+ break;
99
+ case 'copy':
100
+ if (options.quiet) return;
101
+ console.log(chalk.yellow.bold('COPY:'), sourcePath);
102
+ break;
103
+ default:
104
+ if (options.quiet) return;
105
+ console.log(chalk.green.bold('SKIP:'), sourcePath);
106
+ break;
107
+ }
108
+ }
109
+ }
110
+
111
+ function isSrcNotChanged(src) {
112
+ let newMd5 = md5(src);
113
+ let same = cache[src] === newMd5;
114
+ if (!same) {
115
+ cache[src] = newMd5;
116
+ }
117
+ return same;
118
+ }
119
+
120
+ function md5(src) {
121
+ let content = fs.readFileSync(src)
122
+ return crypto.createHash('md5').update(content).digest('hex');
123
+ }
124
+
125
+ function processHtml(sourcePath, targetPath) {
126
+ const html = fs.readFileSync(sourcePath, 'utf8');
127
+ const dom = new JSDOM(html);
128
+ const document = dom.window.document;
129
+ const body = document.body;
130
+ let needsUpdate = false;
131
+
132
+ processNode(body);
133
+
134
+ if (needsUpdate) {
135
+ // Add the required MathJax CSS to the <head>
136
+ const styleTag = document.createElement('style');
137
+ styleTag.setAttribute('id', 'MJX-SVG-styles');
138
+ styleTag.innerHTML = adaptor.innerHTML(svg.styleSheet(mjPage));
139
+ document.head.appendChild(styleTag);
140
+ fs.writeFileSync(targetPath, dom.serialize());
141
+ stat.rendered++;
142
+ return true; // rendered
143
+ } else {
144
+ fs.copyFileSync(sourcePath, targetPath);
145
+ stat.copied++;
146
+ return false; // no math, copied
147
+ }
148
+
149
+ function processNode(node) {
150
+ // Regex for $$...$$ and $...$
151
+ const displayRegex = /\$\$(.*?)\$\$/gs;
152
+ const inlineRegex = /(?<!\\)\$([^$]+?)\$/g;
153
+
154
+ if (node.nodeType === 3) { // Text node
155
+ let text = node.textContent;
156
+ if (displayRegex.test(text) || inlineRegex.test(text)) {
157
+ needsUpdate = true;
158
+
159
+ // Render Display Math
160
+ text = text.replace(displayRegex, (_, texStr) => {
161
+ const output = mjPage.convert(texStr, { display: true });
162
+ const svg = adaptor.innerHTML(output);
163
+ return `<mjx-container display="true" style="display: block; text-align: center; margin: 1em 0;">${svg}</mjx-container>`;
164
+ });
165
+
166
+ // Render Inline Math
167
+ text = text.replace(inlineRegex, (_, texStr) => {
168
+ const output = mjPage.convert(texStr, { display: false });
169
+ return adaptor.innerHTML(output);
170
+ });
171
+
172
+ // Create a temporary container to hold the new HTML
173
+ const wrapper = document.createElement('div');
174
+ wrapper.innerHTML = text;
175
+ node.replaceWith(...wrapper.childNodes);
176
+ }
177
+ } else if (node.className === 'latex-block') { // div.latex-block by org-mode
178
+ needsUpdate = true;
179
+
180
+ let texStr = node.textContent;
181
+
182
+ // Render Display Math
183
+ const output = mjPage.convert(texStr, { display: true });
184
+ const svg = adaptor.innerHTML(output);
185
+ let text = `<mjx-container display="true" style="display: block; text-align: center; margin: 1em 0;">${svg}</mjx-container>`;
186
+
187
+ // Create a temporary container to hold the new HTML
188
+ const wrapper = document.createElement('div');
189
+ wrapper.innerHTML = text;
190
+ node.replaceWith(...wrapper.childNodes);
191
+ } else if (node.nodeName !== 'SCRIPT' && node.nodeName !== 'CODE' && node.nodeName !== 'PRE') {
192
+ // Recursively check children, skipping code blocks
193
+ Array.from(node.childNodes).forEach(processNode);
194
+ }
195
+ }
196
+ }
197
+
198
+ function getTargetPathFrom(sourcePath) {
199
+ return sourcePath.replace(src, target);
200
+ }
201
+
202
+ function ensureDir(dir) {
203
+ if (!fs.existsSync(dir)) {
204
+ fs.mkdirSync(dir, { recursive: true });
205
+ }
206
+ }
207
+ }