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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/static/js/render.js +240 -79
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-katex2",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "KaTeX math rendering plugin for NodeBB",
5
5
  "main": "lib/index.js",
6
6
  "repository": {
@@ -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("Katex render");
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
- const MATH_PATTERN = /\$\$|\\\[|\\\(/;
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('[component="post/content"]');
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 (MATH_PATTERN.test(posts[i].textContent)) {
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 (preview && MATH_PATTERN.test(preview.textContent)) {
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 (MATH_PATTERN.test(titles[i].textContent)) {
76
+ if (hasFormulas(titles[i])) {
46
77
  return true;
47
78
  }
48
79
  }
@@ -51,8 +82,8 @@
51
82
  }
52
83
 
53
84
  /**
54
- * Динамическая загрузка CSS файла
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
- * Динамическая загрузка JavaScript файла
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
- resolve();
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
- * Загрузка библиотеки KaTeX (с async/await и параллельной загрузкой)
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
- // Путь к библиотеке в node_modules
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
- // Очищаем найденные узлы от HTML
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") || parent.innerHTML.includes("<p"))
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) return;
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
- // Сначала очищаем от HTML-тегов
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
- * Рендеринг всех постов (async)
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, skipping");
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
- console.log("[KaTeX] Rendered " + posts.length + " posts");
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
- setTimeout(function () {
267
- renderAllPosts();
268
- }, 500);
269
-
270
- // События NodeBB для динамического контента
271
-
272
- // Когда загружаются новые посты (скролл, пагинация)
273
- window.addEventListener("action:posts.loaded", function () {
274
- setTimeout(function () {
275
- renderAllPosts();
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.addEventListener("action:topic.loaded", function () {
281
- setTimeout(function () {
282
- renderAllPosts();
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.addEventListener("action:ajaxify.end", function () {
288
- setTimeout(function () {
289
- renderAllPosts();
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.addEventListener("action:composer.preview", async function () {
433
+ // Обновление превью в редакторе
434
+ $(window).on("action:composer.preview", function () {
295
435
  if (hasMathContent()) {
296
- try {
297
- await loadKaTeX();
298
- const preview = document.querySelector(".preview-container");
299
- if (preview) {
300
- renderMath(preview);
301
- }
302
- } catch (err) {
303
- console.error("[KaTeX] Preview render error:", err);
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
- // Запуск после загрузки DOM
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
  })();