nodebb-plugin-katex2 1.0.5 → 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 +209 -78
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.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,29 +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;
219
291
  }
220
292
  }
221
293
 
222
294
  /**
223
- * Рендеринг всех постов (async)
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
+ });
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Рендеринг всех постов на странице
224
312
  */
225
313
  async function renderAllPosts() {
226
- console.log("[KaTeX] Try rendering");
227
314
  // Проверяем наличие формул
228
315
  if (!hasMathContent()) {
229
- console.log("[KaTeX] No math content found, skipping");
316
+ console.log("[KaTeX] No math content found");
230
317
  return;
231
318
  }
232
319
 
@@ -234,82 +321,126 @@
234
321
  // Загружаем KaTeX если нужно
235
322
  await loadKaTeX();
236
323
 
324
+ let renderCount = 0;
325
+
237
326
  // Рендерим посты
238
327
  const posts = document.querySelectorAll('[component="post/content"]');
239
328
  posts.forEach(function (post) {
240
- renderMath(post);
329
+ if (renderMath(post)) {
330
+ renderCount++;
331
+ }
241
332
  });
242
333
 
243
- // Рендерим превью
334
+ // Рендерим превью редактора
244
335
  const preview = document.querySelector(".preview-container");
245
336
  if (preview) {
246
337
  renderMath(preview);
247
338
  }
248
339
 
249
- // Рендерим заголовки
340
+ // Рендерим заголовки тем
250
341
  const titles = document.querySelectorAll('[component="topic/title"]');
251
342
  titles.forEach(function (title) {
252
343
  renderMath(title);
253
344
  });
254
345
 
255
- console.log("[KaTeX] Rendered " + posts.length + " posts");
346
+ if (renderCount > 0) {
347
+ console.log("[KaTeX] Rendered " + renderCount + " new posts");
348
+ }
256
349
  } catch (err) {
257
- console.error("[KaTeX] Failed to render:", err);
350
+ console.error("[KaTeX] Failed to render posts:", err);
258
351
  }
259
352
  }
260
353
 
261
354
  /**
262
- * Инициализация
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
+ * Инициализация плагина
263
376
  */
264
377
  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);
378
+ console.log("[KaTeX] Initializing plugin");
379
+
380
+ // Первоначальный рендеринг
381
+ debouncedRender();
382
+
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();
277
389
  });
278
390
 
279
- // Когда открывается тема
280
- window.addEventListener("action:topic.loaded", function () {
281
- setTimeout(function () {
282
- renderAllPosts();
283
- }, 500);
391
+ // Загрузка новых постов (скролл, пагинация)
392
+ $(window).on("action:posts.loaded", function (event, data) {
393
+ console.log("[KaTeX] New posts loaded:", data.posts.length);
394
+ debouncedRender();
284
395
  });
285
396
 
286
- // Когда происходит навигация
287
- window.addEventListener("action:ajaxify.end", function () {
288
- setTimeout(function () {
289
- renderAllPosts();
290
- }, 800);
397
+ // Загрузка темы
398
+ $(window).on("action:topic.loaded", function (event, data) {
399
+ console.log("[KaTeX] Topic loaded:", data.tid);
400
+ debouncedRender();
291
401
  });
292
402
 
293
- // Когда обновляется превью в редакторе
294
- window.addEventListener("action:composer.preview", async function () {
403
+ // Обновление превью в редакторе
404
+ $(window).on("action:composer.preview", function () {
295
405
  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
- }
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
+ });
305
417
  }
306
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
+ });
307
430
  }
308
431
 
309
- // Запуск после загрузки 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
+ // Запуск после полной загрузки
310
440
  if (document.readyState === "loading") {
311
441
  document.addEventListener("DOMContentLoaded", init);
312
442
  } else {
443
+ // DOM уже загружен
313
444
  init();
314
445
  }
315
446
  })();