mkdocs-katex-ssr 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,272 @@
1
+ import os
2
+ import json
3
+ import subprocess
4
+ import threading
5
+ import warnings
6
+ import logging
7
+ import requests
8
+ import shutil
9
+ from mkdocs.plugins import BasePlugin
10
+ from mkdocs.config import config_options
11
+ from mkdocs.utils import get_relative_url
12
+ from bs4 import BeautifulSoup
13
+
14
+ # Extremely aggressive global warning suppression
15
+ warnings.filterwarnings("ignore")
16
+ os.environ["PYTHONWARNINGS"] = "ignore"
17
+
18
+ class WarningFilter(logging.Filter):
19
+ def filter(self, record):
20
+ msg = record.getMessage().lower()
21
+ if "pkg_resources" in msg or "jieba" in msg or "deprecationwarning" in msg or "userwarning" in msg:
22
+ return False
23
+ return True
24
+
25
+ # Apply filter to all potential loggers
26
+ for logger_name in ["mkdocs", "mkdocs.plugins", "py.warnings", ""]:
27
+ logger = logging.getLogger(logger_name)
28
+ logger.addFilter(WarningFilter())
29
+
30
+ logging.captureWarnings(True)
31
+
32
+ class KatexSsrPlugin(BasePlugin):
33
+ config_scheme = (
34
+ ('katex_dist', config_options.Type(str, default='https://cdn.jsdelivr.net/npm/katex@latest/dist/')),
35
+ ('katex_css_filename', config_options.Type(str, default='katex.min.css')),
36
+ ('add_katex_css', config_options.Type(bool, default=True)),
37
+ ('embed_assets', config_options.Type(bool, default=False)),
38
+ ('copy_assets_to', config_options.Type(str, default='assets/katex')),
39
+ ('ssr_contribs', config_options.Type(list, default=[])),
40
+ ('client_scripts', config_options.Type(list, default=[])),
41
+ # Legacy/Alias for ssr_contribs to maintain some compat, though behavior changes
42
+ ('contrib_scripts', config_options.Type(list, default=[])),
43
+ ('katex_options', config_options.Type(dict, default={})),
44
+ )
45
+
46
+ def __init__(self):
47
+ self.process = None
48
+ self.lock = threading.Lock()
49
+ self._asset_cache = {}
50
+ self._local_dist_path = None
51
+
52
+ def _ensure_trailing_slash(self, path):
53
+ if not path.endswith('/') and not path.endswith('\\'):
54
+ return path + '/'
55
+ return path
56
+
57
+ def _resolve_url(self, base, path):
58
+ base = base.replace('\\', '/')
59
+ if base.startswith('http'):
60
+ return base.rstrip('/') + '/' + path.lstrip('/')
61
+ else:
62
+ return os.path.normpath(os.path.join(base, path))
63
+
64
+ def on_config(self, config):
65
+ self.config['katex_dist'] = self._ensure_trailing_slash(self.config['katex_dist'])
66
+
67
+ project_dir = os.path.dirname(config['config_file_path'])
68
+
69
+ # Merge legacy contrib_scripts into ssr_contribs if used
70
+ if self.config['contrib_scripts']:
71
+ # Append unique items
72
+ for script in self.config['contrib_scripts']:
73
+ if script not in self.config['ssr_contribs']:
74
+ self.config['ssr_contribs'].append(script)
75
+
76
+ # Asset resolution logic
77
+ possible_dist = self._resolve_url(project_dir, self.config['katex_dist'])
78
+ if os.path.isdir(possible_dist):
79
+ self._local_dist_path = possible_dist
80
+ else:
81
+ node_modules = os.path.join(project_dir, 'node_modules')
82
+ dist = os.path.join(node_modules, 'katex', 'dist')
83
+ if os.path.isdir(dist):
84
+ self._local_dist_path = dist
85
+
86
+ # Start Node.js process
87
+ renderer_path = os.path.join(os.path.dirname(__file__), 'renderer.js')
88
+
89
+ use_shell = os.name == 'nt'
90
+ cmd = ['node', renderer_path]
91
+ if use_shell:
92
+ cmd = f'node "{renderer_path}"'
93
+
94
+ try:
95
+ env = os.environ.copy()
96
+ node_modules = os.path.join(project_dir, 'node_modules')
97
+ if 'NODE_PATH' in env:
98
+ env['NODE_PATH'] = node_modules + os.pathsep + env['NODE_PATH']
99
+ else:
100
+ env['NODE_PATH'] = node_modules
101
+
102
+ self.process = subprocess.Popen(
103
+ cmd,
104
+ cwd=project_dir,
105
+ stdin=subprocess.PIPE,
106
+ stdout=subprocess.PIPE,
107
+ stderr=subprocess.PIPE,
108
+ shell=use_shell,
109
+ env=env
110
+ )
111
+
112
+ # Send ONLY ssr_contribs to Node
113
+ node_contribs = [c for c in self.config['ssr_contribs'] if '://' not in c]
114
+ setup_payload = {
115
+ 'type': 'setup',
116
+ 'contribs': node_contribs
117
+ }
118
+ try:
119
+ line = (json.dumps(setup_payload) + '\n').encode('utf-8')
120
+ self.process.stdin.write(line)
121
+ self.process.stdin.flush()
122
+ except Exception as e:
123
+ print(f"Error during KaTeX setup: {e}")
124
+
125
+ except Exception as e:
126
+ print(f"Error starting KaTeX renderer: {e}")
127
+ self.process = None
128
+
129
+ return config
130
+
131
+ def _render_latex(self, latex, display_mode=False):
132
+ if not self.process:
133
+ return None
134
+
135
+ with self.lock:
136
+ payload = {
137
+ 'type': 'render',
138
+ 'latex': latex,
139
+ 'displayMode': display_mode,
140
+ 'options': self.config['katex_options']
141
+ }
142
+ try:
143
+ line = (json.dumps(payload) + '\n').encode('utf-8')
144
+ self.process.stdin.write(line)
145
+ self.process.stdin.flush()
146
+
147
+ response_line = self.process.stdout.readline()
148
+ if not response_line:
149
+ return None
150
+
151
+ result = json.loads(response_line.decode('utf-8'))
152
+ if result.get('status') == 'success':
153
+ return result.get('html')
154
+ else:
155
+ print(f"KaTeX error: {result.get('message')}")
156
+ except Exception as e:
157
+ if self.process and self.process.poll() is not None:
158
+ stderr_content = self.process.stderr.read()
159
+ if stderr_content:
160
+ print(f"Renderer died with: {stderr_content.decode('utf-8', errors='replace')}")
161
+ return None
162
+
163
+ def on_post_page(self, output, page, config):
164
+ if not self.process:
165
+ return output
166
+
167
+ soup = BeautifulSoup(output, 'html.parser')
168
+ math_elements = soup.find_all(class_='arithmatex')
169
+ for el in math_elements:
170
+ content = el.get_text().strip()
171
+ display_mode = False
172
+
173
+ if content.startswith('\\(') and content.endswith('\\)'):
174
+ latex = content[2:-2]
175
+ elif content.startswith('\\[') and content.endswith('\\]'):
176
+ latex = content[2:-2]
177
+ display_mode = True
178
+ elif content.startswith('$') and content.endswith('$'):
179
+ latex = content[1:-1]
180
+ elif content.startswith('$$') and content.endswith('$$'):
181
+ latex = content[2:-2]
182
+ display_mode = True
183
+ else:
184
+ latex = content
185
+
186
+ rendered_html = self._render_latex(latex, display_mode)
187
+ if rendered_html:
188
+ new_soup = BeautifulSoup(rendered_html, 'html.parser')
189
+ el.replace_with(new_soup)
190
+
191
+ # Assets Injection
192
+ css_file = self.config['katex_css_filename']
193
+ if self.config['add_katex_css']:
194
+ if self.config['embed_assets'] and self._local_dist_path:
195
+ dest_path = self.config['copy_assets_to']
196
+ css_dest_file = f"{dest_path}/{css_file}"
197
+ css_url = get_relative_url(css_dest_file, page.url)
198
+ css_link = soup.new_tag('link', rel='stylesheet', href=css_url)
199
+ if soup.head:
200
+ soup.head.append(css_link)
201
+ else:
202
+ soup.insert(0, css_link)
203
+ else:
204
+ css_url = self._resolve_url(self.config['katex_dist'], css_file)
205
+ css_link = soup.new_tag('link', rel='stylesheet', href=css_url)
206
+ if soup.head:
207
+ soup.head.append(css_link)
208
+ else:
209
+ soup.insert(0, css_link)
210
+
211
+ # Inject ONLY client_scripts
212
+ for script_name in self.config['client_scripts']:
213
+ if '://' in script_name or script_name.endswith('.js'):
214
+ script_url = script_name
215
+ else:
216
+ if self.config['embed_assets'] and self._local_dist_path:
217
+ dest_path = self.config['copy_assets_to']
218
+ script_dest_file = f"{dest_path}/contrib/{script_name}.min.js"
219
+ script_url = get_relative_url(script_dest_file, page.url)
220
+ else:
221
+ script_url = self._resolve_url(self.config['katex_dist'], f'contrib/{script_name}.min.js')
222
+
223
+ script_tag = soup.new_tag('script', src=script_url)
224
+ if soup.body:
225
+ soup.body.append(script_tag)
226
+ else:
227
+ soup.append(script_tag)
228
+
229
+ return str(soup)
230
+
231
+ def on_post_build(self, config):
232
+ if self.process:
233
+ self.process.terminate()
234
+ self.process.wait()
235
+
236
+ # Copy assets if requested
237
+ if self.config['embed_assets'] and self._local_dist_path:
238
+ dest_dir = os.path.join(config['site_dir'], self.config['copy_assets_to'])
239
+ if not os.path.exists(dest_dir):
240
+ os.makedirs(dest_dir, exist_ok=True)
241
+
242
+ # Copy katex CSS (filename depends on config)
243
+ css_file = self.config['katex_css_filename']
244
+ src_css = os.path.join(self._local_dist_path, css_file)
245
+ if os.path.exists(src_css):
246
+ shutil.copy2(src_css, dest_dir)
247
+ else:
248
+ print(f"Warning: Could not find {css_file} at {src_css}")
249
+
250
+ # Copy fonts
251
+ src_fonts = os.path.join(self._local_dist_path, 'fonts')
252
+ dest_fonts = os.path.join(dest_dir, 'fonts')
253
+ if os.path.exists(src_fonts):
254
+ if os.path.exists(dest_fonts):
255
+ shutil.rmtree(dest_fonts)
256
+ shutil.copytree(src_fonts, dest_fonts)
257
+
258
+ # Copy requested client_scripts
259
+ dest_contrib = os.path.join(dest_dir, 'contrib')
260
+ if not os.path.exists(dest_contrib):
261
+ os.makedirs(dest_contrib, exist_ok=True)
262
+
263
+ # Note: We technically might need to copy items from ssr_contribs IF the user wanted them
264
+ # but we decided they are separate. However, if 'mhchem' is in ssr_contribs only,
265
+ # we don't copy it. If user wants it on client, they MUST put it in client_scripts.
266
+ for script_name in self.config['client_scripts']:
267
+ if '://' not in script_name and not script_name.endswith('.js'):
268
+ src_script = os.path.join(self._local_dist_path, 'contrib', f'{script_name}.min.js')
269
+ if os.path.exists(src_script):
270
+ shutil.copy2(src_script, dest_contrib)
271
+
272
+
@@ -0,0 +1,58 @@
1
+ const katex = require('katex');
2
+ const readline = require('readline');
3
+
4
+ // Store loaded contrib names to avoid reloading
5
+ const loadedContribs = new Set();
6
+
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ terminal: false
11
+ });
12
+
13
+ rl.on('line', (line) => {
14
+ if (!line.trim()) return;
15
+
16
+ try {
17
+ const data = JSON.parse(line);
18
+
19
+ if (data.type === 'setup') {
20
+ if (data.contribs && Array.isArray(data.contribs)) {
21
+ data.contribs.forEach(name => {
22
+ if (!loadedContribs.has(name)) {
23
+ try {
24
+ // Try to load from katex/dist/contrib/
25
+ require(`katex/dist/contrib/${name}.js`);
26
+ loadedContribs.add(name);
27
+ } catch (e) {
28
+ // Also try without .js or direct name if it's external
29
+ try {
30
+ require(name);
31
+ loadedContribs.add(name);
32
+ } catch (e2) {
33
+ console.error(`Failed to load contrib: ${name}`);
34
+ }
35
+ }
36
+ }
37
+ });
38
+ }
39
+ return; // No response needed for setup
40
+ }
41
+
42
+ if (data.type === 'render' || !data.type) {
43
+ const { latex, displayMode, options } = data;
44
+
45
+ const html = katex.renderToString(latex, {
46
+ displayMode: displayMode || false,
47
+ throwOnError: false,
48
+ ...options
49
+ });
50
+
51
+ process.stdout.write(JSON.stringify({ status: 'success', html }) + '\n');
52
+ }
53
+ } catch (err) {
54
+ process.stdout.write(JSON.stringify({ status: 'error', message: err.message }) + '\n');
55
+ }
56
+ });
57
+
58
+ console.error('KaTeX SSR Renderer started');
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: mkdocs-katex-ssr
3
+ Version: 0.1.0
4
+ Summary: A MkDocs plugin for server-side rendering of KaTeX math.
5
+ Author-email: Your Name <your.email@example.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Dist: mkdocs>=1.1.0
9
+ Requires-Dist: beautifulsoup4>=4.9.0
10
+ Dynamic: license-file
@@ -0,0 +1,8 @@
1
+ mkdocs_katex_ssr/plugin.py,sha256=9HcxJv4Q1SfWMmnNvWwpw0oaJCuCFCrbhBM5WMgp4vc,11073
2
+ mkdocs_katex_ssr/renderer.js,sha256=15lIck7GXZHiAza9IpnSbu9HmeklaDp_zubetQQowkU,1967
3
+ mkdocs_katex_ssr-0.1.0.dist-info/licenses/LICENSE,sha256=Bea4m1bAyMhuJT5bCGl76QfpKatJarfMzl9vcrdXRgQ,1064
4
+ mkdocs_katex_ssr-0.1.0.dist-info/METADATA,sha256=UHxjskpQ8Zb0smgBOE0dIOeWvlojZkQ3BlQ0Sz8OS0Q,298
5
+ mkdocs_katex_ssr-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ mkdocs_katex_ssr-0.1.0.dist-info/entry_points.txt,sha256=l3PrOPi_1AqGLse2BTZVz2SB8rn3I4NRVI-wDUbIHVg,68
7
+ mkdocs_katex_ssr-0.1.0.dist-info/top_level.txt,sha256=FdfQyPUx4r9kgiqTW4da0TPUdBbOda_0PXjVkfGmsgs,17
8
+ mkdocs_katex_ssr-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [mkdocs.plugins]
2
+ katex-ssr = mkdocs_katex_ssr.plugin:KatexSsrPlugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RainPPR
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mkdocs_katex_ssr