nodebb-plugin-katex2 1.0.5 → 1.0.7
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/package.json +1 -1
- package/static/js/render.js +240 -79
package/package.json
CHANGED
package/static/js/render.js
CHANGED
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ленивая загрузка и рендеринг KaTeX
|
|
3
|
-
*
|
|
2
|
+
* Ленивая загрузка и рендеринг KaTeX для NodeBB
|
|
3
|
+
* Версия 2.0 - исправлены проблемы с SPA-навигацией
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
(function () {
|
|
7
7
|
"use strict";
|
|
8
8
|
|
|
9
|
-
console.log("
|
|
9
|
+
console.log("[KaTeX] Plugin initialized");
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// === Состояние загрузки ===
|
|
12
12
|
let katexLoaded = false;
|
|
13
13
|
let katexLoading = false;
|
|
14
14
|
let loadCallbacks = [];
|
|
15
15
|
|
|
16
|
+
// === Debounce таймеры ===
|
|
17
|
+
let renderTimer = null;
|
|
18
|
+
const RENDER_DELAY = 100; // Задержка перед рендерингом (мс)
|
|
19
|
+
|
|
20
|
+
// === Регулярное выражение для быстрой проверки ===
|
|
21
|
+
const MATH_PATTERN = /\$\$|\\\[|\\\(/;
|
|
22
|
+
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
24
|
+
* Debounce функция - откладывает выполнение
|
|
25
|
+
* @param {Function} func - Функция для выполнения
|
|
26
|
+
* @param {number} wait - Задержка в мс
|
|
19
27
|
*/
|
|
20
|
-
|
|
28
|
+
function debounce(func, wait) {
|
|
29
|
+
return function executedFunction() {
|
|
30
|
+
const context = this;
|
|
31
|
+
const args = arguments;
|
|
32
|
+
|
|
33
|
+
clearTimeout(renderTimer);
|
|
34
|
+
renderTimer = setTimeout(function () {
|
|
35
|
+
func.apply(context, args);
|
|
36
|
+
}, wait);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Проверка наличия формул в элементе
|
|
42
|
+
* @param {HTMLElement} element
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
function hasFormulas(element) {
|
|
46
|
+
if (!element) return false;
|
|
47
|
+
|
|
48
|
+
// Быстрая проверка через textContent
|
|
49
|
+
return MATH_PATTERN.test(element.textContent);
|
|
50
|
+
}
|
|
21
51
|
|
|
22
52
|
/**
|
|
23
53
|
* Проверка наличия формул на странице
|
|
@@ -25,24 +55,25 @@
|
|
|
25
55
|
*/
|
|
26
56
|
function hasMathContent() {
|
|
27
57
|
// Проверяем в постах
|
|
28
|
-
const posts = document.querySelectorAll(
|
|
29
|
-
|
|
58
|
+
const posts = document.querySelectorAll(
|
|
59
|
+
'.posts-container [component="post/content"]',
|
|
60
|
+
);
|
|
30
61
|
for (let i = 0; i < posts.length; i++) {
|
|
31
|
-
if (
|
|
62
|
+
if (hasFormulas(posts[i])) {
|
|
32
63
|
return true;
|
|
33
64
|
}
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
// Проверяем в превью редактора
|
|
37
68
|
const preview = document.querySelector(".preview-container");
|
|
38
|
-
if (
|
|
69
|
+
if (hasFormulas(preview)) {
|
|
39
70
|
return true;
|
|
40
71
|
}
|
|
41
72
|
|
|
42
|
-
// Проверяем в заголовках
|
|
73
|
+
// Проверяем в заголовках тем
|
|
43
74
|
const titles = document.querySelectorAll('[component="topic/title"]');
|
|
44
75
|
for (let i = 0; i < titles.length; i++) {
|
|
45
|
-
if (
|
|
76
|
+
if (hasFormulas(titles[i])) {
|
|
46
77
|
return true;
|
|
47
78
|
}
|
|
48
79
|
}
|
|
@@ -51,8 +82,8 @@
|
|
|
51
82
|
}
|
|
52
83
|
|
|
53
84
|
/**
|
|
54
|
-
*
|
|
55
|
-
* @param {string} href - Путь к CSS
|
|
85
|
+
* Загрузка CSS файла
|
|
86
|
+
* @param {string} href - Путь к CSS
|
|
56
87
|
* @returns {Promise}
|
|
57
88
|
*/
|
|
58
89
|
function loadCSS(href) {
|
|
@@ -74,16 +105,24 @@
|
|
|
74
105
|
}
|
|
75
106
|
|
|
76
107
|
/**
|
|
77
|
-
*
|
|
78
|
-
* @param {string} src - Путь к JS
|
|
108
|
+
* Загрузка JavaScript файла
|
|
109
|
+
* @param {string} src - Путь к JS
|
|
79
110
|
* @returns {Promise}
|
|
80
111
|
*/
|
|
81
112
|
function loadScript(src) {
|
|
82
113
|
return new Promise(function (resolve, reject) {
|
|
83
|
-
//
|
|
114
|
+
// Проверяем глобальный объект
|
|
115
|
+
if (window.renderMathInElement && window.katex) {
|
|
116
|
+
resolve();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Проверяем, не загружен ли уже скрипт
|
|
84
121
|
const existing = document.querySelector('script[src="' + src + '"]');
|
|
85
122
|
if (existing) {
|
|
86
|
-
|
|
123
|
+
// Ждем, пока загрузится
|
|
124
|
+
existing.onload = resolve;
|
|
125
|
+
existing.onerror = reject;
|
|
87
126
|
return;
|
|
88
127
|
}
|
|
89
128
|
|
|
@@ -96,12 +135,38 @@
|
|
|
96
135
|
}
|
|
97
136
|
|
|
98
137
|
/**
|
|
99
|
-
*
|
|
138
|
+
* Ожидание готовности KaTeX с таймаутом
|
|
139
|
+
*/
|
|
140
|
+
function waitForKatex(maxAttempts = 50, interval = 50) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
let attempts = 0;
|
|
143
|
+
|
|
144
|
+
const check = () => {
|
|
145
|
+
if (isKatexReady()) {
|
|
146
|
+
resolve();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
attempts++;
|
|
151
|
+
if (attempts >= maxAttempts) {
|
|
152
|
+
reject(new Error("KaTeX did not initialize in time"));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setTimeout(check, interval);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
check();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Загрузка библиотеки KaTeX
|
|
100
165
|
* @returns {Promise}
|
|
101
166
|
*/
|
|
102
167
|
async function loadKaTeX() {
|
|
103
168
|
// Если уже загружено
|
|
104
|
-
if (katexLoaded) {
|
|
169
|
+
if (katexLoaded && window.renderMathInElement && window.katex) {
|
|
105
170
|
return;
|
|
106
171
|
}
|
|
107
172
|
|
|
@@ -116,19 +181,25 @@
|
|
|
116
181
|
katexLoading = true;
|
|
117
182
|
|
|
118
183
|
try {
|
|
119
|
-
//
|
|
120
|
-
// const basePath = "/plugins/nodebb-plugin-katex/node_modules/katex/dist/";
|
|
121
|
-
// Находим правильный путь
|
|
122
|
-
// Путь к файлам через modules
|
|
184
|
+
// Правильный путь согласно plugin.json staticDirs
|
|
123
185
|
const basePath = "/assets/plugins/nodebb-plugin-katex2/katex/";
|
|
124
186
|
|
|
125
|
-
|
|
187
|
+
await loadScript(basePath + "katex.min.js");
|
|
188
|
+
|
|
189
|
+
// 3. Ждем инициализации katex
|
|
190
|
+
await waitForKatex();
|
|
191
|
+
|
|
192
|
+
// Параллельная загрузка всех ресурсов
|
|
126
193
|
await Promise.all([
|
|
127
194
|
loadCSS(basePath + "katex.min.css"),
|
|
128
|
-
loadScript(basePath + "katex.min.js"),
|
|
129
195
|
loadScript(basePath + "contrib/auto-render.min.js"),
|
|
130
196
|
]);
|
|
131
197
|
|
|
198
|
+
// Проверяем, что библиотека действительно загрузилась
|
|
199
|
+
if (!window.renderMathInElement || !window.katex) {
|
|
200
|
+
throw new Error("KaTeX library not available after loading");
|
|
201
|
+
}
|
|
202
|
+
|
|
132
203
|
katexLoaded = true;
|
|
133
204
|
katexLoading = false;
|
|
134
205
|
console.log("[KaTeX] Library loaded successfully");
|
|
@@ -140,13 +211,14 @@
|
|
|
140
211
|
loadCallbacks = [];
|
|
141
212
|
} catch (err) {
|
|
142
213
|
katexLoading = false;
|
|
214
|
+
katexLoaded = false;
|
|
143
215
|
console.error("[KaTeX] Failed to load library:", err);
|
|
144
216
|
throw err;
|
|
145
217
|
}
|
|
146
218
|
}
|
|
147
219
|
|
|
148
220
|
/**
|
|
149
|
-
* Конфигурация KaTeX
|
|
221
|
+
* Конфигурация рендеринга KaTeX
|
|
150
222
|
*/
|
|
151
223
|
const KATEX_CONFIG = {
|
|
152
224
|
delimiters: [
|
|
@@ -158,14 +230,18 @@
|
|
|
158
230
|
errorColor: "#cc0000",
|
|
159
231
|
strict: false,
|
|
160
232
|
trust: false,
|
|
233
|
+
// Игнорируем уже отрендеренные элементы
|
|
234
|
+
ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code"],
|
|
235
|
+
ignoredClasses: ["katex", "katex-display", "katex-rendered"],
|
|
161
236
|
};
|
|
162
237
|
|
|
163
238
|
/**
|
|
164
239
|
* Очистка HTML-тегов внутри формул
|
|
165
|
-
* Markdown может добавить <br>, <p> и другие теги
|
|
166
240
|
* @param {HTMLElement} element
|
|
167
241
|
*/
|
|
168
242
|
function cleanMathElements(element) {
|
|
243
|
+
if (!element) return;
|
|
244
|
+
|
|
169
245
|
const walker = document.createTreeWalker(
|
|
170
246
|
element,
|
|
171
247
|
NodeFilter.SHOW_TEXT,
|
|
@@ -176,7 +252,7 @@
|
|
|
176
252
|
const nodesToProcess = [];
|
|
177
253
|
let node;
|
|
178
254
|
|
|
179
|
-
// Собираем
|
|
255
|
+
// Собираем текстовые узлы с формулами
|
|
180
256
|
while ((node = walker.nextNode())) {
|
|
181
257
|
const text = node.textContent;
|
|
182
258
|
if (MATH_PATTERN.test(text)) {
|
|
@@ -184,13 +260,20 @@
|
|
|
184
260
|
}
|
|
185
261
|
}
|
|
186
262
|
|
|
187
|
-
// Очищаем
|
|
263
|
+
// Очищаем от лишних HTML-тегов
|
|
188
264
|
nodesToProcess.forEach(function (textNode) {
|
|
189
265
|
let parent = textNode.parentNode;
|
|
190
266
|
|
|
267
|
+
// Пропускаем уже отрендеренные элементы
|
|
268
|
+
if (parent && parent.classList && parent.classList.contains("katex")) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
191
272
|
if (
|
|
192
273
|
parent &&
|
|
193
|
-
(parent.innerHTML.includes("<br") ||
|
|
274
|
+
(parent.innerHTML.includes("<br") ||
|
|
275
|
+
parent.innerHTML.includes("<p") ||
|
|
276
|
+
parent.innerHTML.includes("<span"))
|
|
194
277
|
) {
|
|
195
278
|
const cleanText = parent.textContent;
|
|
196
279
|
|
|
@@ -204,29 +287,63 @@
|
|
|
204
287
|
/**
|
|
205
288
|
* Рендеринг формул в элементе
|
|
206
289
|
* @param {HTMLElement} element
|
|
290
|
+
* @returns {boolean} - true если что-то отрендерено
|
|
207
291
|
*/
|
|
208
292
|
function renderMath(element) {
|
|
209
|
-
if (!element)
|
|
293
|
+
if (!element || !window.renderMathInElement) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Проверяем наличие формул
|
|
298
|
+
if (!hasFormulas(element)) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Проверяем, не рендерили ли уже этот элемент
|
|
303
|
+
if (element.hasAttribute("data-katex-rendered")) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
210
306
|
|
|
211
307
|
try {
|
|
212
|
-
//
|
|
308
|
+
// Очищаем от HTML-тегов
|
|
213
309
|
cleanMathElements(element);
|
|
214
310
|
|
|
215
|
-
//
|
|
216
|
-
renderMathInElement(element, KATEX_CONFIG);
|
|
311
|
+
// Рендерим формулы
|
|
312
|
+
window.renderMathInElement(element, KATEX_CONFIG);
|
|
313
|
+
|
|
314
|
+
// Помечаем как отрендеренный
|
|
315
|
+
element.setAttribute("data-katex-rendered", "true");
|
|
316
|
+
|
|
317
|
+
return true;
|
|
217
318
|
} catch (err) {
|
|
218
319
|
console.error("[KaTeX] Render error:", err);
|
|
320
|
+
return false;
|
|
219
321
|
}
|
|
220
322
|
}
|
|
221
323
|
|
|
222
324
|
/**
|
|
223
|
-
*
|
|
325
|
+
* Сброс флага рендеринга для обновленного контента
|
|
326
|
+
* @param {HTMLElement} element
|
|
327
|
+
*/
|
|
328
|
+
function markForRerender(element) {
|
|
329
|
+
if (element && element.hasAttribute("data-katex-rendered")) {
|
|
330
|
+
element.removeAttribute("data-katex-rendered");
|
|
331
|
+
|
|
332
|
+
// Также сбрасываем для вложенных элементов
|
|
333
|
+
const rendered = element.querySelectorAll("[data-katex-rendered]");
|
|
334
|
+
rendered.forEach(function (el) {
|
|
335
|
+
el.removeAttribute("data-katex-rendered");
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Рендеринг всех постов на странице
|
|
224
342
|
*/
|
|
225
343
|
async function renderAllPosts() {
|
|
226
|
-
console.log("[KaTeX] Try rendering");
|
|
227
344
|
// Проверяем наличие формул
|
|
228
345
|
if (!hasMathContent()) {
|
|
229
|
-
console.log("[KaTeX] No math content found
|
|
346
|
+
console.log("[KaTeX] No math content found");
|
|
230
347
|
return;
|
|
231
348
|
}
|
|
232
349
|
|
|
@@ -234,82 +351,126 @@
|
|
|
234
351
|
// Загружаем KaTeX если нужно
|
|
235
352
|
await loadKaTeX();
|
|
236
353
|
|
|
354
|
+
let renderCount = 0;
|
|
355
|
+
|
|
237
356
|
// Рендерим посты
|
|
238
357
|
const posts = document.querySelectorAll('[component="post/content"]');
|
|
239
358
|
posts.forEach(function (post) {
|
|
240
|
-
renderMath(post)
|
|
359
|
+
if (renderMath(post)) {
|
|
360
|
+
renderCount++;
|
|
361
|
+
}
|
|
241
362
|
});
|
|
242
363
|
|
|
243
|
-
// Рендерим превью
|
|
364
|
+
// Рендерим превью редактора
|
|
244
365
|
const preview = document.querySelector(".preview-container");
|
|
245
366
|
if (preview) {
|
|
246
367
|
renderMath(preview);
|
|
247
368
|
}
|
|
248
369
|
|
|
249
|
-
// Рендерим заголовки
|
|
370
|
+
// Рендерим заголовки тем
|
|
250
371
|
const titles = document.querySelectorAll('[component="topic/title"]');
|
|
251
372
|
titles.forEach(function (title) {
|
|
252
373
|
renderMath(title);
|
|
253
374
|
});
|
|
254
375
|
|
|
255
|
-
|
|
376
|
+
if (renderCount > 0) {
|
|
377
|
+
console.log("[KaTeX] Rendered " + renderCount + " new posts");
|
|
378
|
+
}
|
|
256
379
|
} catch (err) {
|
|
257
|
-
console.error("[KaTeX] Failed to render:", err);
|
|
380
|
+
console.error("[KaTeX] Failed to render posts:", err);
|
|
258
381
|
}
|
|
259
382
|
}
|
|
260
383
|
|
|
261
384
|
/**
|
|
262
|
-
*
|
|
385
|
+
* Debounced версия рендеринга
|
|
386
|
+
*/
|
|
387
|
+
const debouncedRender = debounce(renderAllPosts, RENDER_DELAY);
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Обработчик для событий навигации
|
|
391
|
+
* Критично для SPA-режима NodeBB
|
|
392
|
+
*/
|
|
393
|
+
function handleNavigation() {
|
|
394
|
+
// Сбрасываем все флаги рендеринга при навигации
|
|
395
|
+
const allPosts = document.querySelectorAll("[data-katex-rendered]");
|
|
396
|
+
allPosts.forEach(function (post) {
|
|
397
|
+
post.removeAttribute("data-katex-rendered");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Запускаем рендеринг с задержкой
|
|
401
|
+
debouncedRender();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Инициализация плагина
|
|
263
406
|
*/
|
|
264
407
|
function init() {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// События NodeBB
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
window.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}, 500);
|
|
408
|
+
console.log("[KaTeX] Initializing plugin");
|
|
409
|
+
|
|
410
|
+
// Первоначальный рендеринг
|
|
411
|
+
debouncedRender();
|
|
412
|
+
|
|
413
|
+
// === События NodeBB ===
|
|
414
|
+
|
|
415
|
+
// Навигация в SPA (КРИТИЧНО!)
|
|
416
|
+
$(window).on("action:ajaxify.end", function (event, data) {
|
|
417
|
+
console.log("[KaTeX] Page navigation detected:", data.url);
|
|
418
|
+
handleNavigation();
|
|
277
419
|
});
|
|
278
420
|
|
|
279
|
-
//
|
|
280
|
-
window.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}, 500);
|
|
421
|
+
// Загрузка новых постов (скролл, пагинация)
|
|
422
|
+
$(window).on("action:posts.loaded", function (event, data) {
|
|
423
|
+
console.log("[KaTeX] New posts loaded:", data.posts.length);
|
|
424
|
+
debouncedRender();
|
|
284
425
|
});
|
|
285
426
|
|
|
286
|
-
//
|
|
287
|
-
window.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}, 800);
|
|
427
|
+
// Загрузка темы
|
|
428
|
+
$(window).on("action:topic.loaded", function (event, data) {
|
|
429
|
+
console.log("[KaTeX] Topic loaded:", data.tid);
|
|
430
|
+
debouncedRender();
|
|
291
431
|
});
|
|
292
432
|
|
|
293
|
-
//
|
|
294
|
-
window.
|
|
433
|
+
// Обновление превью в редакторе
|
|
434
|
+
$(window).on("action:composer.preview", function () {
|
|
295
435
|
if (hasMathContent()) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
436
|
+
loadKaTeX()
|
|
437
|
+
.then(function () {
|
|
438
|
+
const preview = document.querySelector(".preview-container");
|
|
439
|
+
if (preview) {
|
|
440
|
+
markForRerender(preview);
|
|
441
|
+
renderMath(preview);
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
.catch(function (err) {
|
|
445
|
+
console.error("[KaTeX] Preview render error:", err);
|
|
446
|
+
});
|
|
305
447
|
}
|
|
306
448
|
});
|
|
449
|
+
|
|
450
|
+
// Редактирование поста
|
|
451
|
+
$(window).on("action:posts.edited", function (event, data) {
|
|
452
|
+
console.log("[KaTeX] Post edited:", data.post.pid);
|
|
453
|
+
debouncedRender();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Создание нового поста
|
|
457
|
+
$(window).on("action:posts.loaded", function () {
|
|
458
|
+
debouncedRender();
|
|
459
|
+
});
|
|
307
460
|
}
|
|
308
461
|
|
|
309
|
-
//
|
|
462
|
+
// === Точка входа ===
|
|
463
|
+
|
|
464
|
+
// NodeBB использует jQuery, поверяем его наличие
|
|
465
|
+
// if (typeof $ === "undefined" || typeof jQuery === "undefined") {
|
|
466
|
+
// console.error("[KaTeX] jQuery not found! Plugin may not work correctly.");
|
|
467
|
+
// }
|
|
468
|
+
|
|
469
|
+
// Запуск после полной загрузки
|
|
310
470
|
if (document.readyState === "loading") {
|
|
311
471
|
document.addEventListener("DOMContentLoaded", init);
|
|
312
472
|
} else {
|
|
473
|
+
// DOM уже загружен
|
|
313
474
|
init();
|
|
314
475
|
}
|
|
315
476
|
})();
|