nodebb-plugin-katex2 1.0.4 → 1.0.6

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 +207 -73
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-katex2",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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,12 @@
96
135
  }
97
136
 
98
137
  /**
99
- * Загрузка библиотеки KaTeX (с async/await и параллельной загрузкой)
138
+ * Загрузка библиотеки KaTeX
100
139
  * @returns {Promise}
101
140
  */
102
141
  async function loadKaTeX() {
103
142
  // Если уже загружено
104
- if (katexLoaded) {
143
+ if (katexLoaded && window.renderMathInElement && window.katex) {
105
144
  return;
106
145
  }
107
146
 
@@ -116,19 +155,21 @@
116
155
  katexLoading = true;
117
156
 
118
157
  try {
119
- // Путь к библиотеке в node_modules
120
- // const basePath = "/plugins/nodebb-plugin-katex/node_modules/katex/dist/";
121
- // Находим правильный путь
122
- // Путь к файлам через modules
158
+ // Правильный путь согласно plugin.json staticDirs
123
159
  const basePath = "/assets/plugins/nodebb-plugin-katex2/katex/";
124
160
 
125
- // Параллельная загрузка всех файлов
161
+ // Параллельная загрузка всех ресурсов
126
162
  await Promise.all([
127
163
  loadCSS(basePath + "katex.min.css"),
128
164
  loadScript(basePath + "katex.min.js"),
129
165
  loadScript(basePath + "contrib/auto-render.min.js"),
130
166
  ]);
131
167
 
168
+ // Проверяем, что библиотека действительно загрузилась
169
+ if (!window.renderMathInElement || !window.katex) {
170
+ throw new Error("KaTeX library not available after loading");
171
+ }
172
+
132
173
  katexLoaded = true;
133
174
  katexLoading = false;
134
175
  console.log("[KaTeX] Library loaded successfully");
@@ -140,13 +181,14 @@
140
181
  loadCallbacks = [];
141
182
  } catch (err) {
142
183
  katexLoading = false;
184
+ katexLoaded = false;
143
185
  console.error("[KaTeX] Failed to load library:", err);
144
186
  throw err;
145
187
  }
146
188
  }
147
189
 
148
190
  /**
149
- * Конфигурация KaTeX
191
+ * Конфигурация рендеринга KaTeX
150
192
  */
151
193
  const KATEX_CONFIG = {
152
194
  delimiters: [
@@ -158,14 +200,18 @@
158
200
  errorColor: "#cc0000",
159
201
  strict: false,
160
202
  trust: false,
203
+ // Игнорируем уже отрендеренные элементы
204
+ ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code"],
205
+ ignoredClasses: ["katex", "katex-display", "katex-rendered"],
161
206
  };
162
207
 
163
208
  /**
164
209
  * Очистка HTML-тегов внутри формул
165
- * Markdown может добавить <br>, <p> и другие теги
166
210
  * @param {HTMLElement} element
167
211
  */
168
212
  function cleanMathElements(element) {
213
+ if (!element) return;
214
+
169
215
  const walker = document.createTreeWalker(
170
216
  element,
171
217
  NodeFilter.SHOW_TEXT,
@@ -176,7 +222,7 @@
176
222
  const nodesToProcess = [];
177
223
  let node;
178
224
 
179
- // Собираем все текстовые узлы с формулами
225
+ // Собираем текстовые узлы с формулами
180
226
  while ((node = walker.nextNode())) {
181
227
  const text = node.textContent;
182
228
  if (MATH_PATTERN.test(text)) {
@@ -184,13 +230,20 @@
184
230
  }
185
231
  }
186
232
 
187
- // Очищаем найденные узлы от HTML
233
+ // Очищаем от лишних HTML-тегов
188
234
  nodesToProcess.forEach(function (textNode) {
189
235
  let parent = textNode.parentNode;
190
236
 
237
+ // Пропускаем уже отрендеренные элементы
238
+ if (parent && parent.classList && parent.classList.contains("katex")) {
239
+ return;
240
+ }
241
+
191
242
  if (
192
243
  parent &&
193
- (parent.innerHTML.includes("<br") || parent.innerHTML.includes("<p"))
244
+ (parent.innerHTML.includes("<br") ||
245
+ parent.innerHTML.includes("<p") ||
246
+ parent.innerHTML.includes("<span"))
194
247
  ) {
195
248
  const cleanText = parent.textContent;
196
249
 
@@ -204,28 +257,63 @@
204
257
  /**
205
258
  * Рендеринг формул в элементе
206
259
  * @param {HTMLElement} element
260
+ * @returns {boolean} - true если что-то отрендерено
207
261
  */
208
262
  function renderMath(element) {
209
- if (!element) return;
263
+ if (!element || !window.renderMathInElement) {
264
+ return false;
265
+ }
266
+
267
+ // Проверяем наличие формул
268
+ if (!hasFormulas(element)) {
269
+ return false;
270
+ }
271
+
272
+ // Проверяем, не рендерили ли уже этот элемент
273
+ if (element.hasAttribute("data-katex-rendered")) {
274
+ return false;
275
+ }
210
276
 
211
277
  try {
212
- // Сначала очищаем от HTML-тегов
278
+ // Очищаем от HTML-тегов
213
279
  cleanMathElements(element);
214
280
 
215
- // Затем рендерим формулы
216
- renderMathInElement(element, KATEX_CONFIG);
281
+ // Рендерим формулы
282
+ window.renderMathInElement(element, KATEX_CONFIG);
283
+
284
+ // Помечаем как отрендеренный
285
+ element.setAttribute("data-katex-rendered", "true");
286
+
287
+ return true;
217
288
  } catch (err) {
218
289
  console.error("[KaTeX] Render error:", err);
290
+ return false;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Сброс флага рендеринга для обновленного контента
296
+ * @param {HTMLElement} element
297
+ */
298
+ function markForRerender(element) {
299
+ if (element && element.hasAttribute("data-katex-rendered")) {
300
+ element.removeAttribute("data-katex-rendered");
301
+
302
+ // Также сбрасываем для вложенных элементов
303
+ const rendered = element.querySelectorAll("[data-katex-rendered]");
304
+ rendered.forEach(function (el) {
305
+ el.removeAttribute("data-katex-rendered");
306
+ });
219
307
  }
220
308
  }
221
309
 
222
310
  /**
223
- * Рендеринг всех постов (async)
311
+ * Рендеринг всех постов на странице
224
312
  */
225
313
  async function renderAllPosts() {
226
314
  // Проверяем наличие формул
227
315
  if (!hasMathContent()) {
228
- console.log("[KaTeX] No math content found, skipping");
316
+ console.log("[KaTeX] No math content found");
229
317
  return;
230
318
  }
231
319
 
@@ -233,80 +321,126 @@
233
321
  // Загружаем KaTeX если нужно
234
322
  await loadKaTeX();
235
323
 
324
+ let renderCount = 0;
325
+
236
326
  // Рендерим посты
237
327
  const posts = document.querySelectorAll('[component="post/content"]');
238
328
  posts.forEach(function (post) {
239
- renderMath(post);
329
+ if (renderMath(post)) {
330
+ renderCount++;
331
+ }
240
332
  });
241
333
 
242
- // Рендерим превью
334
+ // Рендерим превью редактора
243
335
  const preview = document.querySelector(".preview-container");
244
336
  if (preview) {
245
337
  renderMath(preview);
246
338
  }
247
339
 
248
- // Рендерим заголовки
340
+ // Рендерим заголовки тем
249
341
  const titles = document.querySelectorAll('[component="topic/title"]');
250
342
  titles.forEach(function (title) {
251
343
  renderMath(title);
252
344
  });
253
345
 
254
- console.log("[KaTeX] Rendered " + posts.length + " posts");
346
+ if (renderCount > 0) {
347
+ console.log("[KaTeX] Rendered " + renderCount + " new posts");
348
+ }
255
349
  } catch (err) {
256
- console.error("[KaTeX] Failed to render:", err);
350
+ console.error("[KaTeX] Failed to render posts:", err);
257
351
  }
258
352
  }
259
353
 
260
354
  /**
261
- * Инициализация
355
+ * Debounced версия рендеринга
356
+ */
357
+ const debouncedRender = debounce(renderAllPosts, RENDER_DELAY);
358
+
359
+ /**
360
+ * Обработчик для событий навигации
361
+ * Критично для SPA-режима NodeBB
362
+ */
363
+ function handleNavigation() {
364
+ // Сбрасываем все флаги рендеринга при навигации
365
+ const allPosts = document.querySelectorAll("[data-katex-rendered]");
366
+ allPosts.forEach(function (post) {
367
+ post.removeAttribute("data-katex-rendered");
368
+ });
369
+
370
+ // Запускаем рендеринг с задержкой
371
+ debouncedRender();
372
+ }
373
+
374
+ /**
375
+ * Инициализация плагина
262
376
  */
263
377
  function init() {
264
- // Первый рендеринг
265
- renderAllPosts();
378
+ console.log("[KaTeX] Initializing plugin");
266
379
 
267
- // События NodeBB для динамического контента
380
+ // Первоначальный рендеринг
381
+ debouncedRender();
268
382
 
269
- // Когда загружаются новые посты (скролл, пагинация)
270
- window.addEventListener("action:posts.loaded", function () {
271
- setTimeout(function () {
272
- renderAllPosts();
273
- }, 50);
383
+ // === События NodeBB ===
384
+
385
+ // Навигация в SPA (КРИТИЧНО!)
386
+ $(window).on("action:ajaxify.end", function (event, data) {
387
+ console.log("[KaTeX] Page navigation detected:", data.url);
388
+ handleNavigation();
274
389
  });
275
390
 
276
- // Когда открывается тема
277
- window.addEventListener("action:topic.loaded", function () {
278
- setTimeout(function () {
279
- renderAllPosts();
280
- }, 50);
391
+ // Загрузка новых постов (скролл, пагинация)
392
+ $(window).on("action:posts.loaded", function (event, data) {
393
+ console.log("[KaTeX] New posts loaded:", data.posts.length);
394
+ debouncedRender();
281
395
  });
282
396
 
283
- // Когда происходит навигация
284
- window.addEventListener("action:ajaxify.end", function () {
285
- setTimeout(function () {
286
- renderAllPosts();
287
- }, 100);
397
+ // Загрузка темы
398
+ $(window).on("action:topic.loaded", function (event, data) {
399
+ console.log("[KaTeX] Topic loaded:", data.tid);
400
+ debouncedRender();
288
401
  });
289
402
 
290
- // Когда обновляется превью в редакторе
291
- window.addEventListener("action:composer.preview", async function () {
403
+ // Обновление превью в редакторе
404
+ $(window).on("action:composer.preview", function () {
292
405
  if (hasMathContent()) {
293
- try {
294
- await loadKaTeX();
295
- const preview = document.querySelector(".preview-container");
296
- if (preview) {
297
- renderMath(preview);
298
- }
299
- } catch (err) {
300
- console.error("[KaTeX] Preview render error:", err);
301
- }
406
+ loadKaTeX()
407
+ .then(function () {
408
+ const preview = document.querySelector(".preview-container");
409
+ if (preview) {
410
+ markForRerender(preview);
411
+ renderMath(preview);
412
+ }
413
+ })
414
+ .catch(function (err) {
415
+ console.error("[KaTeX] Preview render error:", err);
416
+ });
302
417
  }
303
418
  });
419
+
420
+ // Редактирование поста
421
+ $(window).on("action:posts.edited", function (event, data) {
422
+ console.log("[KaTeX] Post edited:", data.post.pid);
423
+ debouncedRender();
424
+ });
425
+
426
+ // Создание нового поста
427
+ $(window).on("action:posts.loaded", function () {
428
+ debouncedRender();
429
+ });
304
430
  }
305
431
 
306
- // Запуск после загрузки DOM
432
+ // === Точка входа ===
433
+
434
+ // NodeBB использует jQuery, поверяем его наличие
435
+ // if (typeof $ === "undefined" || typeof jQuery === "undefined") {
436
+ // console.error("[KaTeX] jQuery not found! Plugin may not work correctly.");
437
+ // }
438
+
439
+ // Запуск после полной загрузки
307
440
  if (document.readyState === "loading") {
308
441
  document.addEventListener("DOMContentLoaded", init);
309
442
  } else {
443
+ // DOM уже загружен
310
444
  init();
311
445
  }
312
446
  })();