pygpt-net 2.6.45__py3-none-any.whl → 2.6.46__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.
Files changed (71) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +0 -5
  4. pygpt_net/controller/debug/debug.py +11 -9
  5. pygpt_net/controller/dialogs/debug.py +40 -29
  6. pygpt_net/core/debug/agent.py +19 -14
  7. pygpt_net/core/debug/assistants.py +22 -24
  8. pygpt_net/core/debug/attachments.py +11 -7
  9. pygpt_net/core/debug/config.py +22 -23
  10. pygpt_net/core/debug/context.py +63 -63
  11. pygpt_net/core/debug/db.py +1 -4
  12. pygpt_net/core/debug/events.py +14 -11
  13. pygpt_net/core/debug/indexes.py +41 -76
  14. pygpt_net/core/debug/kernel.py +11 -8
  15. pygpt_net/core/debug/models.py +20 -15
  16. pygpt_net/core/debug/plugins.py +9 -6
  17. pygpt_net/core/debug/presets.py +16 -11
  18. pygpt_net/core/debug/tabs.py +28 -22
  19. pygpt_net/core/debug/ui.py +25 -22
  20. pygpt_net/core/render/web/renderer.py +3 -2
  21. pygpt_net/core/tabs/tab.py +14 -1
  22. pygpt_net/data/config/config.json +3 -3
  23. pygpt_net/data/config/models.json +3 -3
  24. pygpt_net/data/config/settings.json +15 -17
  25. pygpt_net/data/css/style.dark.css +6 -0
  26. pygpt_net/data/css/web-blocks.css +4 -0
  27. pygpt_net/data/css/web-blocks.light.css +1 -1
  28. pygpt_net/data/css/web-chatgpt.css +4 -0
  29. pygpt_net/data/css/web-chatgpt.light.css +1 -1
  30. pygpt_net/data/css/web-chatgpt_wide.css +4 -0
  31. pygpt_net/data/css/web-chatgpt_wide.light.css +1 -1
  32. pygpt_net/data/js/app.js +720 -636
  33. pygpt_net/data/locale/locale.de.ini +1 -1
  34. pygpt_net/data/locale/locale.en.ini +1 -1
  35. pygpt_net/data/locale/locale.es.ini +1 -1
  36. pygpt_net/data/locale/locale.fr.ini +1 -1
  37. pygpt_net/data/locale/locale.it.ini +1 -1
  38. pygpt_net/data/locale/locale.pl.ini +2 -2
  39. pygpt_net/data/locale/locale.uk.ini +1 -1
  40. pygpt_net/data/locale/locale.zh.ini +1 -1
  41. pygpt_net/item/model.py +4 -1
  42. pygpt_net/js_rc.py +12824 -12612
  43. pygpt_net/provider/api/anthropic/__init__.py +3 -1
  44. pygpt_net/provider/api/anthropic/tools.py +1 -1
  45. pygpt_net/provider/api/google/__init__.py +7 -1
  46. pygpt_net/provider/api/x_ai/__init__.py +5 -1
  47. pygpt_net/provider/core/config/patch.py +14 -1
  48. pygpt_net/provider/llms/anthropic.py +37 -5
  49. pygpt_net/provider/llms/azure_openai.py +3 -1
  50. pygpt_net/provider/llms/base.py +13 -1
  51. pygpt_net/provider/llms/deepseek_api.py +13 -3
  52. pygpt_net/provider/llms/google.py +14 -1
  53. pygpt_net/provider/llms/hugging_face_api.py +105 -24
  54. pygpt_net/provider/llms/hugging_face_embedding.py +88 -0
  55. pygpt_net/provider/llms/hugging_face_router.py +28 -16
  56. pygpt_net/provider/llms/local.py +2 -0
  57. pygpt_net/provider/llms/mistral.py +60 -3
  58. pygpt_net/provider/llms/open_router.py +4 -2
  59. pygpt_net/provider/llms/openai.py +4 -1
  60. pygpt_net/provider/llms/perplexity.py +66 -5
  61. pygpt_net/provider/llms/utils.py +39 -0
  62. pygpt_net/provider/llms/voyage.py +50 -0
  63. pygpt_net/provider/llms/x_ai.py +70 -10
  64. pygpt_net/ui/widget/lists/db.py +1 -0
  65. pygpt_net/ui/widget/lists/debug.py +1 -0
  66. pygpt_net/ui/widget/tabs/body.py +12 -1
  67. {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.46.dist-info}/METADATA +11 -4
  68. {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.46.dist-info}/RECORD +71 -68
  69. {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.46.dist-info}/LICENSE +0 -0
  70. {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.46.dist-info}/WHEEL +0 -0
  71. {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.46.dist-info}/entry_points.txt +0 -0
pygpt_net/data/js/app.js CHANGED
@@ -1206,643 +1206,727 @@
1206
1206
  this.hlQueueSet.clear(); this.hlQueue.length = 0;
1207
1207
  }
1208
1208
  }
1209
+ // ==========================================================================
1210
+ // 4) Custom Markup Processor
1211
+ // ==========================================================================
1212
+
1213
+ class CustomMarkup {
1214
+ constructor(cfg, logger) {
1215
+ this.cfg = cfg || { CUSTOM_MARKUP_RULES: [] };
1216
+ this.logger = logger || new Logger(cfg);
1217
+ this.__compiled = null;
1218
+ this.__hasStreamRules = false; // Fast flag to skip stream work if not needed
1219
+ }
1220
+ _d(line, ctx) { try { this.logger.debug('CM', line, ctx); } catch (_) {} }
1221
+
1222
+ // Decode HTML entities once (safe)
1223
+ // This addresses cases when linkify/full markdown path leaves literal """ etc. in text nodes.
1224
+ // We decode only for rules that explicitly opt-in (see compile()) to avoid changing semantics globally.
1225
+ decodeEntitiesOnce(s) {
1226
+ if (!s || s.indexOf('&') === -1) return String(s || '');
1227
+ const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
1228
+ ta.innerHTML = s;
1229
+ return ta.value;
1230
+ }
1231
+
1232
+ // Small helper: escape text to safe HTML (shared Utils or fallback)
1233
+ _escHtml(s) {
1234
+ try { return Utils.escapeHtml(s); } catch (_) {
1235
+ return String(s || '').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]));
1236
+ }
1237
+ }
1238
+
1239
+ // quick check if any rule's open token is present in text (used to skip expensive work early)
1240
+ hasAnyOpenToken(text, rules) {
1241
+ if (!text || !rules || !rules.length) return false;
1242
+ for (let i = 0; i < rules.length; i++) {
1243
+ const r = rules[i];
1244
+ if (!r || !r.open) continue;
1245
+ if (text.indexOf(r.open) !== -1) return true;
1246
+ }
1247
+ return false;
1248
+ }
1249
+
1250
+ // Build inner HTML from text according to rule's mode (markdown-inline | text) with optional entity decode.
1251
+ _materializeInnerHTML(rule, text, MD) {
1252
+ let payload = String(text || '');
1253
+ if (rule && rule.decodeEntities && payload && payload.indexOf('&') !== -1) {
1254
+ try { payload = this.decodeEntitiesOnce(payload); } catch (_) { /* keep original */ }
1255
+ }
1256
+ if (rule && rule.innerMode === 'markdown-inline' && MD && typeof MD.renderInline === 'function') {
1257
+ try { return MD.renderInline(payload); } catch (_) { return this._escHtml(payload); }
1258
+ }
1259
+ return this._escHtml(payload);
1260
+ }
1261
+
1262
+ // Make a DOM Fragment from HTML string (robust across contexts).
1263
+ _fragmentFromHTML(html, ctxNode) {
1264
+ let frag = null;
1265
+ try {
1266
+ const range = document.createRange();
1267
+ const ctx = (ctxNode && ctxNode.parentNode) ? ctxNode.parentNode : (document.body || document.documentElement);
1268
+ range.selectNode(ctx);
1269
+ frag = range.createContextualFragment(String(html || ''));
1270
+ return frag;
1271
+ } catch (_) {
1272
+ const tmp = document.createElement('div');
1273
+ tmp.innerHTML = String(html || '');
1274
+ frag = document.createDocumentFragment();
1275
+ while (tmp.firstChild) frag.appendChild(tmp.firstChild);
1276
+ return frag;
1277
+ }
1278
+ }
1209
1279
 
1210
- // ==========================================================================
1211
- // 4) Custom Markup Processor
1212
- // ==========================================================================
1213
-
1214
- class CustomMarkup {
1215
- constructor(cfg, logger) {
1216
- this.cfg = cfg || { CUSTOM_MARKUP_RULES: [] };
1217
- this.logger = logger || new Logger(cfg);
1218
- this.__compiled = null;
1219
- this.__hasStreamRules = false; // Fast flag to skip stream work if not needed
1220
- }
1221
- _d(line, ctx) { try { this.logger.debug('CM', line, ctx); } catch (_) {} }
1222
-
1223
- // Decode HTML entities once (safe)
1224
- // This addresses cases when linkify/full markdown path leaves literal "&quot;" etc. in text nodes.
1225
- // We decode only for rules that explicitly opt-in (see compile()) to avoid changing semantics globally.
1226
- decodeEntitiesOnce(s) {
1227
- if (!s || s.indexOf('&') === -1) return String(s || '');
1228
- const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
1229
- ta.innerHTML = s;
1230
- return ta.value;
1231
- }
1232
-
1233
- // Small helper: escape text to safe HTML (shared Utils or fallback)
1234
- _escHtml(s) {
1235
- try { return Utils.escapeHtml(s); } catch (_) {
1236
- return String(s || '').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]));
1237
- }
1238
- }
1239
-
1240
- // quick check if any rule's open token is present in text (used to skip expensive work early)
1241
- hasAnyOpenToken(text, rules) {
1242
- if (!text || !rules || !rules.length) return false;
1243
- for (let i = 0; i < rules.length; i++) {
1244
- const r = rules[i];
1245
- if (!r || !r.open) continue;
1246
- if (text.indexOf(r.open) !== -1) return true;
1247
- }
1248
- return false;
1249
- }
1250
-
1251
- // Build inner HTML from text according to rule's mode (markdown-inline | text) with optional entity decode.
1252
- _materializeInnerHTML(rule, text, MD) {
1253
- let payload = String(text || '');
1254
- if (rule && rule.decodeEntities && payload && payload.indexOf('&') !== -1) {
1255
- try { payload = this.decodeEntitiesOnce(payload); } catch (_) { /* keep original */ }
1256
- }
1257
- if (rule && rule.innerMode === 'markdown-inline' && MD && typeof MD.renderInline === 'function') {
1258
- try { return MD.renderInline(payload); } catch (_) { return this._escHtml(payload); }
1259
- }
1260
- return this._escHtml(payload);
1261
- }
1262
-
1263
- // Make a DOM Fragment from HTML string (robust across contexts).
1264
- _fragmentFromHTML(html, ctxNode) {
1265
- let frag = null;
1266
- try {
1267
- const range = document.createRange();
1268
- const ctx = (ctxNode && ctxNode.parentNode) ? ctxNode.parentNode : (document.body || document.documentElement);
1269
- range.selectNode(ctx);
1270
- frag = range.createContextualFragment(String(html || ''));
1271
- return frag;
1272
- } catch (_) {
1273
- const tmp = document.createElement('div');
1274
- tmp.innerHTML = String(html || '');
1275
- frag = document.createDocumentFragment();
1276
- while (tmp.firstChild) frag.appendChild(tmp.firstChild);
1277
- return frag;
1278
- }
1279
- }
1280
-
1281
- // Replace one element in DOM with HTML string (keeps siblings intact).
1282
- _replaceElementWithHTML(el, html) {
1283
- if (!el || !el.parentNode) return;
1284
- const parent = el.parentNode;
1285
- const frag = this._fragmentFromHTML(html, el);
1286
- try {
1287
- // Insert new nodes before the old element, then remove the old element (widely supported).
1288
- parent.insertBefore(frag, el);
1289
- parent.removeChild(el);
1290
- } catch (_) {
1291
- // Conservative fallback: wrap in a span if direct fragment insertion failed for some reason.
1292
- const tmp = document.createElement('span');
1293
- tmp.innerHTML = String(html || '');
1294
- while (tmp.firstChild) parent.insertBefore(tmp.firstChild, el);
1295
- parent.removeChild(el);
1296
- }
1297
- }
1298
-
1299
- // Compile rules once; also precompile strict and whitespace-tolerant "full match" regexes.
1300
- compile(rules) {
1301
- const src = Array.isArray(rules) ? rules : (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
1302
- const compiled = [];
1303
- let hasStream = false;
1304
-
1305
- for (const r of src) {
1306
- if (!r || typeof r.open !== 'string' || typeof r.close !== 'string') continue;
1307
-
1308
- const tag = (r.tag || 'span').toLowerCase();
1309
- const className = (r.className || r.class || '').trim();
1310
- const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
1311
-
1312
- const stream = !!(r.stream === true);
1313
- const openReplace = String((r.openReplace != null ? r.openReplace : (r.openReplace || '')) || '');
1314
- const closeReplace = String((r.closeReplace != null ? r.closeReplace : (r.closeReplace || '')) || '');
1315
-
1316
- // Back-compat: decode entities default true for cmd-like
1317
- const decodeEntities = (typeof r.decodeEntities === 'boolean')
1318
- ? r.decodeEntities
1319
- : ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
1320
-
1321
- // Optional application phase (where replacement should happen)
1322
- // - 'source' => before markdown-it
1323
- // - 'html' => after markdown-it (DOM fragment)
1324
- // - 'both'
1325
- let phaseRaw = (typeof r.phase === 'string') ? r.phase.toLowerCase() : '';
1326
- if (phaseRaw !== 'source' && phaseRaw !== 'html' && phaseRaw !== 'both') phaseRaw = '';
1327
- // Heuristic: if replacement contains fenced code backticks, default to 'source'
1328
- const looksLikeFence = (openReplace.indexOf('```') !== -1) || (closeReplace.indexOf('```') !== -1);
1329
- const phase = phaseRaw || (looksLikeFence ? 'source' : 'html');
1330
-
1331
- const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
1332
- const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
1333
- const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
1334
-
1335
- const item = {
1336
- name: r.name || tag,
1337
- tag, className, innerMode,
1338
- open: r.open, close: r.close,
1339
- decodeEntities,
1340
- re, reFull, reFullTrim,
1341
- stream,
1342
- openReplace, closeReplace,
1343
- phase, // NEW: where this rule should be applied
1344
- isSourceFence: looksLikeFence // NEW: hints StreamEngine to treat as custom fence
1345
- };
1346
- compiled.push(item);
1347
- if (stream) hasStream = true;
1348
- this._d('COMPILE_RULE', { name: item.name, phase: item.phase, stream: item.stream });
1349
- }
1350
-
1351
- if (compiled.length === 0) {
1352
- const open = '[!cmd]', close = '[/!cmd]';
1353
- const item = {
1354
- name: 'cmd', tag: 'p', className: 'cmd', innerMode: 'text', open, close,
1355
- decodeEntities: true,
1356
- re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
1357
- reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
1358
- reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$'),
1359
- stream: false,
1360
- openReplace: '', closeReplace: '',
1361
- phase: 'html', isSourceFence: false
1362
- };
1363
- compiled.push(item);
1364
- this._d('COMPILE_RULE_FALLBACK', { name: item.name });
1365
- }
1366
-
1367
- this.__hasStreamRules = hasStream;
1368
- return compiled;
1369
- }
1370
-
1371
- // pre-markdown source transformer – applies only rules for 'source'/'both' with replacements
1372
- // IMPORTANT CHANGE:
1373
- // - Skips replacements inside fenced code blocks (``` / ~~~).
1374
- // - Applies only when the rule opener is at top-level of the line (no list markers/blockquote).
1375
- transformSource(src, opts) {
1376
- let s = String(src || '');
1377
- this.ensureCompiled();
1378
- const rules = this.__compiled;
1379
- if (!rules || !rules.length) return s;
1380
-
1381
- // Pick only source-phase rules with explicit replacements
1382
- const candidates = [];
1383
- for (let i = 0; i < rules.length; i++) {
1384
- const r = rules[i];
1385
- if (!r) continue;
1386
- if ((r.phase === 'source' || r.phase === 'both') && (r.openReplace || r.closeReplace)) candidates.push(r);
1387
- }
1388
- if (!candidates.length) return s;
1389
-
1390
- // Compute fenced-code ranges once to exclude them from replacements (production-safe).
1391
- const fences = this._findFenceRanges(s);
1392
- if (!fences.length) {
1393
- // No code fences in source; apply top-level guarded replacements globally.
1394
- return this._applySourceReplacementsInChunk(s, s, 0, candidates);
1395
- }
1396
-
1397
- // Apply replacements only in segments outside fenced code.
1398
- let out = '';
1399
- let last = 0;
1400
- for (let k = 0; k < fences.length; k++) {
1401
- const [a, b] = fences[k];
1402
- if (a > last) {
1403
- const chunk = s.slice(last, a);
1404
- out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
1405
- }
1406
- out += s.slice(a, b); // pass fenced code verbatim
1407
- last = b;
1408
- }
1409
- if (last < s.length) {
1410
- const tail = s.slice(last);
1411
- out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
1412
- }
1413
- return out;
1414
- }
1415
-
1416
- // expose custom fence specs (to StreamEngine)
1417
- getSourceFenceSpecs() {
1418
- this.ensureCompiled();
1419
- const rules = this.__compiled || [];
1420
- const out = [];
1421
- for (let i = 0; i < rules.length; i++) {
1422
- const r = rules[i];
1423
- if (!r || !r.isSourceFence) continue;
1424
- // Only expose when they actually look like fences in source phase
1425
- if (r.phase !== 'source' && r.phase !== 'both') continue;
1426
- out.push({ open: r.open, close: r.close });
1427
- }
1428
- return out;
1429
- }
1430
-
1431
- // Ensure rules are compiled and cached.
1432
- ensureCompiled() {
1433
- if (!this.__compiled) {
1434
- this.__compiled = this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
1435
- this._d('ENSURE_COMPILED', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
1436
- }
1437
- return this.__compiled;
1438
- }
1439
-
1440
- // Replace rules set (also exposes rules on window).
1441
- setRules(rules) {
1442
- this.__compiled = this.compile(rules);
1443
- window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() : (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
1444
- this._d('SET_RULES', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
1445
- }
1446
-
1447
- // Return current rules as array.
1448
- getRules() {
1449
- const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice()
1450
- : (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
1451
- this._d('GET_RULES', { count: list.length });
1452
- return list;
1453
- }
1454
-
1455
- // Fast switch: do we have any rules that want streaming parsing?
1456
- hasStreamRules() {
1457
- this.ensureCompiled();
1458
- return !!this.__hasStreamRules;
1459
- }
1460
-
1461
- // Context guards
1462
- isInsideForbiddenContext(node) {
1463
- const p = node.parentElement; if (!p) return true;
1464
- // IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
1465
- return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
1466
- }
1467
- isInsideForbiddenElement(el) {
1468
- if (!el) return true;
1469
- // IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
1470
- return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
1471
- }
1472
-
1473
- // Global finder on a single text blob (original per-text-node logic).
1474
- findNextMatch(text, from, rules) {
1475
- let best = null;
1476
- for (const rule of rules) {
1477
- rule.re.lastIndex = from;
1478
- const m = rule.re.exec(text);
1479
- if (m) {
1480
- const start = m.index, end = rule.re.lastIndex;
1481
- if (!best || start < best.start) best = { rule, start, end, inner: m[1] || '' };
1482
- }
1483
- }
1484
- return best;
1485
- }
1486
-
1487
- // Strict full match of a pure text node (legacy path).
1488
- findFullMatch(text, rules) {
1489
- for (const rule of rules) {
1490
- if (rule.reFull) {
1491
- const m = rule.reFull.exec(text);
1492
- if (m) return { rule, inner: m[1] || '' };
1493
- } else {
1494
- rule.re.lastIndex = 0;
1495
- const m = rule.re.exec(text);
1496
- if (m && m.index === 0 && (rule.re.lastIndex === text.length)) {
1497
- const m2 = rule.re.exec(text);
1498
- if (!m2) return { rule, inner: m[1] || '' };
1499
- }
1500
- }
1501
- }
1502
- return null;
1503
- }
1504
-
1505
- // Set inner content according to the rule's mode, with optional entity decode (element mode).
1506
- setInnerByMode(el, mode, text, MD, decodeEntities = false) {
1507
- let payload = String(text || '');
1508
- if (decodeEntities && payload && payload.indexOf('&') !== -1) {
1509
- try { payload = this.decodeEntitiesOnce(payload); } catch (_) {}
1510
- }
1511
-
1512
- if (mode === 'markdown-inline' && typeof window.markdownit !== 'undefined') {
1513
- try {
1514
- if (MD && typeof MD.renderInline === 'function') { el.innerHTML = MD.renderInline(payload); return; }
1515
- const tempMD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
1516
- el.innerHTML = tempMD.renderInline(payload); return;
1517
- } catch (_) {}
1518
- }
1519
- el.textContent = payload;
1520
- }
1521
-
1522
- // Try to replace an entire <p> that is a full custom markup match.
1523
- _tryReplaceFullParagraph(el, rules, MD) {
1524
- if (!el || el.tagName !== 'P') return false;
1525
- if (this.isInsideForbiddenElement(el)) {
1526
- this._d('P_SKIP_FORBIDDEN', { tag: el.tagName });
1527
- return false;
1528
- }
1529
- const t = el.textContent || '';
1530
- if (!this.hasAnyOpenToken(t, rules)) return false;
1531
-
1532
- for (const rule of rules) {
1533
- if (!rule) continue;
1534
- const m = rule.reFullTrim ? rule.reFullTrim.exec(t) : null;
1535
- if (!m) continue;
1536
-
1537
- const innerText = m[1] || '';
1538
-
1539
- if (rule.phase !== 'html' && rule.phase !== 'both') continue; // element materialization is html-phase only
1540
-
1541
- if (rule.openReplace || rule.closeReplace) {
1542
- const innerHTML = this._materializeInnerHTML(rule, innerText, MD);
1543
- const html = String(rule.openReplace || '') + innerHTML + String(rule.closeReplace || '');
1544
- this._replaceElementWithHTML(el, html);
1545
- this._d('P_REPLACED_AS_HTML', { rule: rule.name });
1546
- return true;
1547
- }
1548
-
1549
- const outTag = (rule.tag && typeof rule.tag === 'string') ? rule.tag.toLowerCase() : 'span';
1550
- const out = document.createElement(outTag === 'p' ? 'p' : outTag);
1551
- if (rule.className) out.className = rule.className;
1552
- out.setAttribute('data-cm', rule.name);
1553
- this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities);
1554
-
1555
- try { el.replaceWith(out); } catch (_) {
1556
- const par = el.parentNode; if (par) par.replaceChild(out, el);
1557
- }
1558
- this._d('P_REPLACED', { rule: rule.name, asTag: outTag });
1559
- return true;
1560
- }
1561
- this._d('P_NO_FULL_MATCH', { preview: this.logger.pv(t, 160) });
1562
- return false;
1563
- }
1564
-
1565
- // Core implementation shared by static and streaming passes.
1566
- applyRules(root, MD, rules) {
1567
- if (!root || !rules || !rules.length) return;
1568
-
1569
- const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
1570
-
1571
- // Phase 1: tolerant <p> replacements
1572
- try {
1573
- const paragraphs = (typeof scope.querySelectorAll === 'function') ? scope.querySelectorAll('p') : [];
1574
- this._d('P_TOLERANT_SCAN_START', { count: paragraphs.length });
1575
-
1576
- if (paragraphs && paragraphs.length) {
1577
- for (let i = 0; i < paragraphs.length; i++) {
1578
- const p = paragraphs[i];
1579
- if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
1580
- const tc = p && (p.textContent || '');
1581
- if (!tc || !this.hasAnyOpenToken(tc, rules)) continue;
1582
- // Skip paragraphs inside forbidden contexts (includes lists now)
1583
- if (this.isInsideForbiddenElement(p)) continue;
1584
- this._tryReplaceFullParagraph(p, rules, MD);
1585
- }
1586
- }
1587
- } catch (e) {
1588
- this._d('P_TOLERANT_SCAN_ERR', String(e));
1589
- }
1590
-
1591
- // Phase 2: legacy per-text-node pass for partial inline cases.
1592
- const self = this;
1593
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
1594
- acceptNode: (node) => {
1595
- const val = node && node.nodeValue ? node.nodeValue : '';
1596
- if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
1597
- if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
1598
- return NodeFilter.FILTER_ACCEPT;
1599
- }
1600
- });
1601
-
1602
- let node;
1603
- while ((node = walker.nextNode())) {
1604
- const text = node.nodeValue;
1605
- if (!text || !this.hasAnyOpenToken(text, rules)) continue; // quick skip
1606
- const parent = node.parentElement;
1607
-
1608
- // Entire text node equals one full match and parent is <p>.
1609
- if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
1610
- const fm = this.findFullMatch(text, rules);
1611
- if (fm) {
1612
- // If explicit HTML replacements are provided, swap <p> for exact HTML (only for html/both phase).
1613
- if ((fm.rule.phase === 'html' || fm.rule.phase === 'both') && (fm.rule.openReplace || fm.rule.closeReplace)) {
1614
- const innerHTML = this._materializeInnerHTML(fm.rule, fm.inner, MD);
1615
- const html = String(fm.rule.openReplace || '') + innerHTML + String(fm.rule.closeReplace || '');
1616
- this._replaceElementWithHTML(parent, html);
1617
- this._d('WALKER_FULL_REPLACE_HTML', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
1618
- continue;
1619
- }
1620
-
1621
- // Backward-compatible: only replace as <p> when rule tag is 'p'
1622
- if (fm.rule.tag === 'p') {
1623
- const out = document.createElement('p');
1624
- if (fm.rule.className) out.className = fm.rule.className;
1625
- out.setAttribute('data-cm', fm.rule.name);
1626
- this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities);
1627
- try { parent.replaceWith(out); } catch (_) {
1628
- const par = parent.parentNode; if (par) par.replaceChild(out, parent);
1629
- }
1630
- this._d('WALKER_FULL_REPLACE', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
1631
- continue;
1632
- }
1633
- }
1634
- }
1635
-
1636
- // General inline replacement inside the text node (span-like or HTML-replace).
1637
- let i = 0;
1638
- let didReplace = false;
1639
- const frag = document.createDocumentFragment();
1640
-
1641
- while (i < text.length) {
1642
- const m = this.findNextMatch(text, i, rules);
1643
- if (!m) break;
1644
-
1645
- if (m.start > i) {
1646
- frag.appendChild(document.createTextNode(text.slice(i, m.start)));
1647
- }
1648
-
1649
- // If HTML replacements are provided, build exact HTML around processed inner – only for html/both phase.
1650
- if ((m.rule.openReplace || m.rule.closeReplace) && (m.rule.phase === 'html' || m.rule.phase === 'both')) {
1651
- const innerHTML = this._materializeInnerHTML(m.rule, m.inner, MD);
1652
- const html = String(m.rule.openReplace || '') + innerHTML + String(m.rule.closeReplace || '');
1653
- const part = this._fragmentFromHTML(html, node);
1654
- frag.appendChild(part);
1655
- this._d('WALKER_INLINE_MATCH_HTML', { rule: m.rule.name, start: m.start, end: m.end });
1656
- i = m.end; didReplace = true; continue;
1657
- }
1658
-
1659
- // If rule is not html-phase, do NOT inject open/close replacements here (source-only rules are handled pre-md).
1660
- if (m.rule.openReplace || m.rule.closeReplace) {
1661
- // Source-only replacement met in DOM pass keep original text verbatim for this match.
1662
- frag.appendChild(document.createTextNode(text.slice(m.start, m.end)));
1663
- this._d('WALKER_INLINE_SKIP_SOURCE_PHASE_HTML', { rule: m.rule.name, start: m.start, end: m.end });
1664
- i = m.end; didReplace = true; continue;
1665
- }
1666
-
1667
- // Element-based inline replacement (original behavior).
1668
- const tag = (m.rule.tag === 'p') ? 'span' : m.rule.tag;
1669
- const el = document.createElement(tag);
1670
- if (m.rule.className) el.className = m.rule.className;
1671
- el.setAttribute('data-cm', m.rule.name);
1672
- this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities);
1673
- frag.appendChild(el);
1674
- this._d('WALKER_INLINE_MATCH', { rule: m.rule.name, start: m.start, end: m.end });
1675
-
1676
- i = m.end;
1677
- didReplace = true;
1678
- }
1679
-
1680
- if (!didReplace) continue;
1681
-
1682
- if (i < text.length) {
1683
- frag.appendChild(document.createTextNode(text.slice(i)));
1684
- }
1685
-
1686
- const parentNode = node.parentNode;
1687
- if (parentNode) {
1688
- parentNode.replaceChild(frag, node);
1689
- this._d('WALKER_INLINE_DONE', { preview: this.logger.pv(text, 120) });
1690
- }
1691
- }
1692
- }
1693
-
1694
- // Public API: apply custom markup for full (static) paths – unchanged behavior.
1695
- apply(root, MD) {
1696
- this.ensureCompiled();
1697
- this.applyRules(root, MD, this.__compiled);
1698
- }
1699
-
1700
- // Public API: apply only stream-enabled rules (used in snapshots).
1701
- applyStream(root, MD) {
1702
- this.ensureCompiled();
1703
- if (!this.__hasStreamRules) return;
1704
- const rules = this.__compiled.filter(r => !!r.stream);
1705
- if (!rules.length) return;
1706
- this.applyRules(root, MD, rules);
1707
- }
1708
-
1709
- // -----------------------------
1710
- // INTERNAL HELPERS (NEW)
1711
- // -----------------------------
1712
-
1713
- // Scan source and return ranges [start, end) of fenced code blocks (``` or ~~~).
1714
- // Matches Markdown fences at line-start with up to 3 spaces/tabs indentation.
1715
- _findFenceRanges(s) {
1716
- const ranges = [];
1717
- const n = s.length;
1718
- let i = 0;
1719
- let inFence = false;
1720
- let fenceMark = '';
1721
- let fenceLen = 0;
1722
- let startLineStart = 0;
1723
-
1724
- while (i < n) {
1725
- const lineStart = i;
1726
- // Find line end and newline length
1727
- let j = lineStart;
1728
- while (j < n && s.charCodeAt(j) !== 10 && s.charCodeAt(j) !== 13) j++;
1729
- const lineEnd = j;
1730
- let nl = 0;
1731
- if (j < n) {
1732
- if (s.charCodeAt(j) === 13 && j + 1 < n && s.charCodeAt(j + 1) === 10) nl = 2;
1733
- else nl = 1;
1734
- }
1735
-
1736
- // Compute indentation up to 3 "spaces" (tabs count as 1 here – safe heuristic)
1737
- let k = lineStart;
1738
- let indent = 0;
1739
- while (k < lineEnd) {
1740
- const c = s.charCodeAt(k);
1741
- if (c === 32 /* space */) { indent++; if (indent > 3) break; k++; }
1742
- else if (c === 9 /* tab */) { indent++; if (indent > 3) break; k++; }
1743
- else break;
1744
- }
1745
-
1746
- if (!inFence) {
1747
- if (indent <= 3 && k < lineEnd) {
1748
- const ch = s.charCodeAt(k);
1749
- if (ch === 0x60 /* ` */ || ch === 0x7E /* ~ */) {
1750
- const mark = String.fromCharCode(ch);
1751
- let m = k;
1752
- while (m < lineEnd && s.charCodeAt(m) === ch) m++;
1753
- const run = m - k;
1754
- if (run >= 3) {
1755
- inFence = true;
1756
- fenceMark = mark;
1757
- fenceLen = run;
1758
- startLineStart = lineStart;
1759
- }
1760
- }
1761
- }
1762
- } else {
1763
- if (indent <= 3 && k < lineEnd && s.charCodeAt(k) === fenceMark.charCodeAt(0)) {
1764
- let m = k;
1765
- while (m < lineEnd && s.charCodeAt(m) === fenceMark.charCodeAt(0)) m++;
1766
- const run = m - k;
1767
- if (run >= fenceLen) {
1768
- // Only whitespace is allowed after closing fence on the same line
1769
- let onlyWS = true;
1770
- for (let t = m; t < lineEnd; t++) {
1771
- const cc = s.charCodeAt(t);
1772
- if (cc !== 32 && cc !== 9) { onlyWS = false; break; }
1773
- }
1774
- if (onlyWS) {
1775
- const endIdx = lineEnd + nl; // include trailing newline if present
1776
- ranges.push([startLineStart, endIdx]);
1777
- inFence = false; fenceMark = ''; fenceLen = 0; startLineStart = 0;
1778
- }
1779
- }
1780
- }
1781
- }
1782
- i = lineEnd + nl;
1783
- }
1784
-
1785
- // If EOF while still in fence, mark until end of string.
1786
- if (inFence) ranges.push([startLineStart, n]);
1787
- return ranges;
1788
- }
1789
-
1790
- // Check if match starts at "top-level" of a line:
1791
- // - up to 3 leading spaces/tabs allowed
1792
- // - not a list item marker ("- ", "+ ", "* ", "1. ", "1) ") and not a blockquote ("> ")
1793
- // - nothing else precedes the token on the same line
1794
- _isTopLevelLineInSource(s, absIdx) {
1795
- let ls = absIdx;
1796
- while (ls > 0) {
1797
- const ch = s.charCodeAt(ls - 1);
1798
- if (ch === 10 /* \n */ || ch === 13 /* \r */) break;
1799
- ls--;
1800
- }
1801
- const prefix = s.slice(ls, absIdx);
1802
-
1803
- // Strip up to 3 leading "spaces" (tabs treated as 1 – acceptable heuristic)
1804
- let i = 0, indent = 0;
1805
- while (i < prefix.length) {
1806
- const c = prefix.charCodeAt(i);
1807
- if (c === 32) { indent++; if (indent > 3) break; i++; }
1808
- else if (c === 9) { indent++; if (indent > 3) break; i++; }
1809
- else break;
1810
- }
1811
- if (indent > 3) return false;
1812
- const rest = prefix.slice(i);
1813
-
1814
- // Reject lists/blockquote
1815
- if (/^>\s?/.test(rest)) return false;
1816
- if (/^[-+*]\s/.test(rest)) return false;
1817
- if (/^\d+[.)]\s/.test(rest)) return false;
1818
-
1819
- // If any other non-whitespace text precedes the token on this line – not top-level
1820
- if (rest.trim().length > 0) return false;
1821
-
1822
- return true;
1823
- }
1824
-
1825
- // Apply source-phase replacements to one outside-of-fence chunk with top-level guard.
1826
- _applySourceReplacementsInChunk(full, chunk, baseOffset, rules) {
1827
- let t = chunk;
1828
- for (let i = 0; i < rules.length; i++) {
1829
- const r = rules[i];
1830
- if (!r || !(r.openReplace || r.closeReplace)) continue;
1831
- try {
1832
- r.re.lastIndex = 0;
1833
- t = t.replace(r.re, (match, inner, offset /*, ...rest*/) => {
1834
- const abs = baseOffset + (offset | 0);
1835
- // Only apply when opener is at top-level on that line (not in lists/blockquote)
1836
- if (!this._isTopLevelLineInSource(full, abs)) return match;
1837
- const open = r.openReplace || '';
1838
- const close = r.closeReplace || '';
1839
- return open + (inner || '') + close;
1840
- });
1841
- } catch (_) { /* keep chunk as is on any error */ }
1842
- }
1843
- return t;
1844
- }
1845
- }
1280
+ // Replace one element in DOM with HTML string (keeps siblings intact).
1281
+ _replaceElementWithHTML(el, html) {
1282
+ if (!el || !el.parentNode) return;
1283
+ const parent = el.parentNode;
1284
+ const frag = this._fragmentFromHTML(html, el);
1285
+ try {
1286
+ // Insert new nodes before the old element, then remove the old element (widely supported).
1287
+ parent.insertBefore(frag, el);
1288
+ parent.removeChild(el);
1289
+ } catch (_) {
1290
+ // Conservative fallback: wrap in a span if direct fragment insertion failed for some reason.
1291
+ const tmp = document.createElement('span');
1292
+ tmp.innerHTML = String(html || '');
1293
+ while (tmp.firstChild) parent.insertBefore(tmp.firstChild, el);
1294
+ parent.removeChild(el);
1295
+ }
1296
+ }
1297
+
1298
+ // Compile rules once; also precompile strict and whitespace-tolerant "full match" regexes.
1299
+ compile(rules) {
1300
+ const src = Array.isArray(rules) ? rules : (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
1301
+ const compiled = [];
1302
+ let hasStream = false;
1303
+
1304
+ for (const r of src) {
1305
+ if (!r || typeof r.open !== 'string' || typeof r.close !== 'string') continue;
1306
+
1307
+ const tag = (r.tag || 'span').toLowerCase();
1308
+ const className = (r.className || r.class || '').trim();
1309
+ const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
1310
+
1311
+ const stream = !!(r.stream === true);
1312
+ const openReplace = String((r.openReplace != null ? r.openReplace : (r.openReplace || '')) || '');
1313
+ const closeReplace = String((r.closeReplace != null ? r.closeReplace : (r.closeReplace || '')) || '');
1314
+
1315
+ // Back-compat: decode entities default true for cmd-like
1316
+ const decodeEntities = (typeof r.decodeEntities === 'boolean')
1317
+ ? r.decodeEntities
1318
+ : ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
1319
+
1320
+ // Optional application phase (where replacement should happen)
1321
+ // - 'source' => before markdown-it
1322
+ // - 'html' => after markdown-it (DOM fragment)
1323
+ // - 'both'
1324
+ let phaseRaw = (typeof r.phase === 'string') ? r.phase.toLowerCase() : '';
1325
+ if (phaseRaw !== 'source' && phaseRaw !== 'html' && phaseRaw !== 'both') phaseRaw = '';
1326
+ // Heuristic: if replacement contains fenced code backticks, default to 'source'
1327
+ const looksLikeFence = (openReplace.indexOf('```') !== -1) || (closeReplace.indexOf('```') !== -1);
1328
+ const phase = phaseRaw || (looksLikeFence ? 'source' : 'html');
1329
+
1330
+ const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
1331
+ const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
1332
+ const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
1333
+
1334
+ const item = {
1335
+ name: r.name || tag,
1336
+ tag, className, innerMode,
1337
+ open: r.open, close: r.close,
1338
+ decodeEntities,
1339
+ re, reFull, reFullTrim,
1340
+ stream,
1341
+ openReplace, closeReplace,
1342
+ phase, // where this rule should be applied
1343
+ isSourceFence: looksLikeFence // hints StreamEngine to treat as custom fence
1344
+ };
1345
+ compiled.push(item);
1346
+ if (stream) hasStream = true;
1347
+ this._d('COMPILE_RULE', { name: item.name, phase: item.phase, stream: item.stream });
1348
+ }
1349
+
1350
+ if (compiled.length === 0) {
1351
+ const open = '[!cmd]', close = '[/!cmd]';
1352
+ const item = {
1353
+ name: 'cmd', tag: 'p', className: 'cmd', innerMode: 'text', open, close,
1354
+ decodeEntities: true,
1355
+ re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
1356
+ reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
1357
+ reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$'),
1358
+ stream: false,
1359
+ openReplace: '', closeReplace: '',
1360
+ phase: 'html', isSourceFence: false
1361
+ };
1362
+ compiled.push(item);
1363
+ this._d('COMPILE_RULE_FALLBACK', { name: item.name });
1364
+ }
1365
+
1366
+ this.__hasStreamRules = hasStream;
1367
+ return compiled;
1368
+ }
1369
+
1370
+ // pre-markdown source transformer – applies only rules for 'source'/'both' with replacements
1371
+ // - Skips replacements inside fenced code blocks (``` / ~~~).
1372
+ // - Applies only when the rule opener is at top-level of the line (no list markers/blockquote).
1373
+ transformSource(src, opts) {
1374
+ let s = String(src || '');
1375
+ this.ensureCompiled();
1376
+ const rules = this.__compiled;
1377
+ if (!rules || !rules.length) return s;
1378
+
1379
+ // Pick only source-phase rules with explicit replacements
1380
+ const candidates = [];
1381
+ for (let i = 0; i < rules.length; i++) {
1382
+ const r = rules[i];
1383
+ if (!r) continue;
1384
+ if ((r.phase === 'source' || r.phase === 'both') && (r.openReplace || r.closeReplace)) candidates.push(r);
1385
+ }
1386
+ if (!candidates.length) return s;
1387
+
1388
+ // Compute fenced-code ranges once to exclude them from replacements (production-safe).
1389
+ const fences = this._findFenceRanges(s);
1390
+ if (!fences.length) {
1391
+ // No code fences in source; apply top-level guarded replacements globally.
1392
+ return this._applySourceReplacementsInChunk(s, s, 0, candidates);
1393
+ }
1394
+
1395
+ // Apply replacements only in segments outside fenced code.
1396
+ let out = '';
1397
+ let last = 0;
1398
+ for (let k = 0; k < fences.length; k++) {
1399
+ const [a, b] = fences[k];
1400
+ if (a > last) {
1401
+ const chunk = s.slice(last, a);
1402
+ out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
1403
+ }
1404
+ out += s.slice(a, b); // pass fenced code verbatim
1405
+ last = b;
1406
+ }
1407
+ if (last < s.length) {
1408
+ const tail = s.slice(last);
1409
+ out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
1410
+ }
1411
+ return out;
1412
+ }
1413
+
1414
+ // expose custom fence specs (to StreamEngine)
1415
+ getSourceFenceSpecs() {
1416
+ this.ensureCompiled();
1417
+ const rules = this.__compiled || [];
1418
+ const out = [];
1419
+ for (let i = 0; i < rules.length; i++) {
1420
+ const r = rules[i];
1421
+ if (!r || !r.isSourceFence) continue;
1422
+ // Only expose when they actually look like fences in source phase
1423
+ if (r.phase !== 'source' && r.phase !== 'both') continue;
1424
+ out.push({ open: r.open, close: r.close });
1425
+ }
1426
+ return out;
1427
+ }
1428
+
1429
+ // Ensure rules are compiled and cached.
1430
+ ensureCompiled() {
1431
+ if (!this.__compiled) {
1432
+ this.__compiled = this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
1433
+ this._d('ENSURE_COMPILED', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
1434
+ }
1435
+ return this.__compiled;
1436
+ }
1437
+
1438
+ // Replace rules set (also exposes rules on window).
1439
+ setRules(rules) {
1440
+ this.__compiled = this.compile(rules);
1441
+ window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() : (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
1442
+ this._d('SET_RULES', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
1443
+ }
1444
+
1445
+ // Return current rules as array.
1446
+ getRules() {
1447
+ const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice()
1448
+ : (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
1449
+ this._d('GET_RULES', { count: list.length });
1450
+ return list;
1451
+ }
1452
+
1453
+ // Fast switch: do we have any rules that want streaming parsing?
1454
+ hasStreamRules() {
1455
+ this.ensureCompiled();
1456
+ return !!this.__hasStreamRules;
1457
+ }
1458
+
1459
+ // Context guards
1460
+ isInsideForbiddenContext(node) {
1461
+ const p = node.parentElement; if (!p) return true;
1462
+ // IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
1463
+ return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
1464
+ }
1465
+ isInsideForbiddenElement(el) {
1466
+ if (!el) return true;
1467
+ // IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
1468
+ return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
1469
+ }
1470
+
1471
+ // Global finder on a single text blob (original per-text-node logic).
1472
+ findNextMatch(text, from, rules) {
1473
+ let best = null;
1474
+ for (const rule of rules) {
1475
+ rule.re.lastIndex = from;
1476
+ const m = rule.re.exec(text);
1477
+ if (m) {
1478
+ const start = m.index, end = rule.re.lastIndex;
1479
+ if (!best || start < best.start) best = { rule, start, end, inner: m[1] || '' };
1480
+ }
1481
+ }
1482
+ return best;
1483
+ }
1484
+
1485
+ // Strict full match of a pure text node (legacy path).
1486
+ findFullMatch(text, rules) {
1487
+ for (const rule of rules) {
1488
+ if (rule.reFull) {
1489
+ const m = rule.reFull.exec(text);
1490
+ if (m) return { rule, inner: m[1] || '' };
1491
+ } else {
1492
+ rule.re.lastIndex = 0;
1493
+ const m = rule.re.exec(text);
1494
+ if (m && m.index === 0 && (rule.re.lastIndex === text.length)) {
1495
+ const m2 = rule.re.exec(text);
1496
+ if (!m2) return { rule: rule, inner: m[1] || '' };
1497
+ }
1498
+ }
1499
+ }
1500
+ return null;
1501
+ }
1502
+
1503
+ // Set inner content according to the rule's mode, with optional entity decode (element mode).
1504
+ setInnerByMode(el, mode, text, MD, decodeEntities = false) {
1505
+ let payload = String(text || '');
1506
+ if (decodeEntities && payload && payload.indexOf('&') !== -1) {
1507
+ try { payload = this.decodeEntitiesOnce(payload); } catch (_) {}
1508
+ }
1509
+
1510
+ if (mode === 'markdown-inline' && typeof window.markdownit !== 'undefined') {
1511
+ try {
1512
+ if (MD && typeof MD.renderInline === 'function') { el.innerHTML = MD.renderInline(payload); return; }
1513
+ const tempMD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
1514
+ el.innerHTML = tempMD.renderInline(payload); return;
1515
+ } catch (_) {}
1516
+ }
1517
+ el.textContent = payload;
1518
+ }
1519
+
1520
+ // Try to replace an entire <p> that is a full custom markup match.
1521
+ _tryReplaceFullParagraph(el, rules, MD) {
1522
+ if (!el || el.tagName !== 'P') return false;
1523
+ if (this.isInsideForbiddenElement(el)) {
1524
+ this._d('P_SKIP_FORBIDDEN', { tag: el.tagName });
1525
+ return false;
1526
+ }
1527
+ const t = el.textContent || '';
1528
+ if (!this.hasAnyOpenToken(t, rules)) return false;
1529
+
1530
+ for (const rule of rules) {
1531
+ if (!rule) continue;
1532
+ const m = rule.reFullTrim ? rule.reFullTrim.exec(t) : null;
1533
+ if (!m) continue;
1534
+
1535
+ const innerText = m[1] || '';
1536
+
1537
+ if (rule.phase !== 'html' && rule.phase !== 'both') continue; // element materialization is html-phase only
1538
+
1539
+ if (rule.openReplace || rule.closeReplace) {
1540
+ const innerHTML = this._materializeInnerHTML(rule, innerText, MD);
1541
+ const html = String(rule.openReplace || '') + innerHTML + String(rule.closeReplace || '');
1542
+ this._replaceElementWithHTML(el, html);
1543
+ this._d('P_REPLACED_AS_HTML', { rule: rule.name });
1544
+ return true;
1545
+ }
1546
+
1547
+ const outTag = (rule.tag && typeof rule.tag === 'string') ? rule.tag.toLowerCase() : 'span';
1548
+ const out = document.createElement(outTag === 'p' ? 'p' : outTag);
1549
+ if (rule.className) out.className = rule.className;
1550
+ out.setAttribute('data-cm', rule.name);
1551
+ this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities);
1552
+
1553
+ try { el.replaceWith(out); } catch (_) {
1554
+ const par = el.parentNode; if (par) par.replaceChild(out, el);
1555
+ }
1556
+ this._d('P_REPLACED', { rule: rule.name, asTag: outTag });
1557
+ return true;
1558
+ }
1559
+ this._d('P_NO_FULL_MATCH', { preview: this.logger.pv(t, 160) });
1560
+ return false;
1561
+ }
1562
+
1563
+ // Core implementation shared by static and streaming passes.
1564
+ applyRules(root, MD, rules) {
1565
+ if (!root || !rules || !rules.length) return;
1566
+
1567
+ const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
1568
+
1569
+ // Phase 1: tolerant <p> replacements
1570
+ try {
1571
+ const paragraphs = (typeof scope.querySelectorAll === 'function') ? scope.querySelectorAll('p') : [];
1572
+ this._d('P_TOLERANT_SCAN_START', { count: paragraphs.length });
1573
+
1574
+ if (paragraphs && paragraphs.length) {
1575
+ for (let i = 0; i < paragraphs.length; i++) {
1576
+ const p = paragraphs[i];
1577
+ if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
1578
+ const tc = p && (p.textContent || '');
1579
+ if (!tc || !this.hasAnyOpenToken(tc, rules)) continue;
1580
+ // Skip paragraphs inside forbidden contexts (includes lists now)
1581
+ if (this.isInsideForbiddenElement(p)) continue;
1582
+ this._tryReplaceFullParagraph(p, rules, MD);
1583
+ }
1584
+ }
1585
+ } catch (e) {
1586
+ this._d('P_TOLERANT_SCAN_ERR', String(e));
1587
+ }
1588
+
1589
+ // Phase 2: legacy per-text-node pass for partial inline cases.
1590
+ const self = this;
1591
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
1592
+ acceptNode: (node) => {
1593
+ const val = node && node.nodeValue ? node.nodeValue : '';
1594
+ if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
1595
+ if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
1596
+ return NodeFilter.FILTER_ACCEPT;
1597
+ }
1598
+ });
1599
+
1600
+ let node;
1601
+ while ((node = walker.nextNode())) {
1602
+ const text = node.nodeValue;
1603
+ if (!text || !this.hasAnyOpenToken(text, rules)) continue; // quick skip
1604
+ const parent = node.parentElement;
1605
+
1606
+ // Entire text node equals one full match and parent is <p>.
1607
+ if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
1608
+ const fm = this.findFullMatch(text, rules);
1609
+ if (fm) {
1610
+ // If explicit HTML replacements are provided, swap <p> for exact HTML (only for html/both phase).
1611
+ if ((fm.rule.phase === 'html' || fm.rule.phase === 'both') && (fm.rule.openReplace || fm.rule.closeReplace)) {
1612
+ const innerHTML = this._materializeInnerHTML(fm.rule, fm.inner, MD);
1613
+ const html = String(fm.rule.openReplace || '') + innerHTML + String(fm.rule.closeReplace || '');
1614
+ this._replaceElementWithHTML(parent, html);
1615
+ this._d('WALKER_FULL_REPLACE_HTML', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
1616
+ continue;
1617
+ }
1618
+
1619
+ // Backward-compatible: only replace as <p> when rule tag is 'p'
1620
+ if (fm.rule.tag === 'p') {
1621
+ const out = document.createElement('p');
1622
+ if (fm.rule.className) out.className = fm.rule.className;
1623
+ out.setAttribute('data-cm', fm.rule.name);
1624
+ this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities);
1625
+ try { parent.replaceWith(out); } catch (_) {
1626
+ const par = parent.parentNode; if (par) par.replaceChild(out, parent);
1627
+ }
1628
+ this._d('WALKER_FULL_REPLACE', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
1629
+ continue;
1630
+ }
1631
+ }
1632
+ }
1633
+
1634
+ // General inline replacement inside the text node (span-like or HTML-replace).
1635
+ let i = 0;
1636
+ let didReplace = false;
1637
+ const frag = document.createDocumentFragment();
1638
+
1639
+ while (i < text.length) {
1640
+ const m = this.findNextMatch(text, i, rules);
1641
+ if (!m) break;
1642
+
1643
+ if (m.start > i) {
1644
+ frag.appendChild(document.createTextNode(text.slice(i, m.start)));
1645
+ }
1646
+
1647
+ // If HTML replacements are provided, build exact HTML around processed inner – only for html/both phase.
1648
+ if ((m.rule.openReplace || m.rule.closeReplace) && (m.rule.phase === 'html' || m.rule.phase === 'both')) {
1649
+ const innerHTML = this._materializeInnerHTML(m.rule, m.inner, MD);
1650
+ const html = String(m.rule.openReplace || '') + innerHTML + String(m.rule.closeReplace || '');
1651
+ const part = this._fragmentFromHTML(html, node);
1652
+ frag.appendChild(part);
1653
+ this._d('WALKER_INLINE_MATCH_HTML', { rule: m.rule.name, start: m.start, end: m.end });
1654
+ i = m.end; didReplace = true; continue;
1655
+ }
1656
+
1657
+ // If rule is not html-phase, do NOT inject open/close replacements here (source-only rules are handled pre-md).
1658
+ if (m.rule.openReplace || m.rule.closeReplace) {
1659
+ // Source-only replacement met in DOM pass – keep original text verbatim for this match.
1660
+ frag.appendChild(document.createTextNode(text.slice(m.start, m.end)));
1661
+ this._d('WALKER_INLINE_SKIP_SOURCE_PHASE_HTML', { rule: m.rule.name, start: m.start, end: m.end });
1662
+ i = m.end; didReplace = true; continue;
1663
+ }
1664
+
1665
+ // Element-based inline replacement (original behavior).
1666
+ const tag = (m.rule.tag === 'p') ? 'span' : m.rule.tag;
1667
+ const el = document.createElement(tag);
1668
+ if (m.rule.className) el.className = m.rule.className;
1669
+ el.setAttribute('data-cm', m.rule.name);
1670
+ this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities);
1671
+ frag.appendChild(el);
1672
+ this._d('WALKER_INLINE_MATCH', { rule: m.rule.name, start: m.start, end: m.end });
1673
+
1674
+ i = m.end;
1675
+ didReplace = true;
1676
+ }
1677
+
1678
+ if (!didReplace) continue;
1679
+
1680
+ if (i < text.length) {
1681
+ frag.appendChild(document.createTextNode(text.slice(i)));
1682
+ }
1683
+
1684
+ const parentNode = node.parentNode;
1685
+ if (parentNode) {
1686
+ parentNode.replaceChild(frag, node);
1687
+ this._d('WALKER_INLINE_DONE', { preview: this.logger.pv(text, 120) });
1688
+ }
1689
+ }
1690
+ }
1691
+
1692
+ // Public API: apply custom markup for full (static) paths – unchanged behavior.
1693
+ apply(root, MD) {
1694
+ this.ensureCompiled();
1695
+ this.applyRules(root, MD, this.__compiled);
1696
+ }
1697
+
1698
+ // Public API: apply only stream-enabled rules (used in snapshots).
1699
+ applyStream(root, MD) {
1700
+ this.ensureCompiled();
1701
+ if (!this.__hasStreamRules) return;
1702
+ const rules = this.__compiled.filter(r => !!r.stream);
1703
+ if (!rules.length) return;
1704
+
1705
+ // Existing full open+close pass
1706
+ this.applyRules(root, MD, rules);
1707
+
1708
+ // streaming-only partial opener fallback (begin materialization on open without close)
1709
+ try { this.applyStreamPartialOpeners(root, MD, rules); } catch (_) {}
1710
+ }
1711
+
1712
+ // -----------------------------
1713
+ // INTERNAL HELPERS
1714
+ // -----------------------------
1715
+
1716
+ // Streaming-only: begin replacement when an opening token is present without a closing token
1717
+ // in the SAME text node. We wrap the tail after the opener into the target element and mark
1718
+ // it as pending (data-cm-pending="1"). On subsequent snapshots (when a close arrives),
1719
+ // the standard full-pass (applyRules) will supersede this.
1720
+ applyStreamPartialOpeners(root, MD, rulesAll) {
1721
+ if (!root) return;
1722
+
1723
+ // Consider only DOM-phase element rules (html/both) without explicit HTML replacements.
1724
+ // Source-phase rules (e.g. exec fences) are intentionally excluded here to avoid changing
1725
+ // code streaming semantics (handled by transformSource/StreamEngine).
1726
+ const rules = (rulesAll || []).filter(r =>
1727
+ (r && (r.phase === 'html' || r.phase === 'both') && !(r.openReplace || r.closeReplace))
1728
+ );
1729
+ if (!rules.length) return;
1730
+
1731
+ const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
1732
+ const self = this;
1733
+
1734
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT, {
1735
+ acceptNode(node) {
1736
+ const val = node && node.nodeValue ? node.nodeValue : '';
1737
+ if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
1738
+ if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
1739
+ return NodeFilter.FILTER_ACCEPT;
1740
+ }
1741
+ });
1742
+
1743
+ let node;
1744
+ while ((node = walker.nextNode())) {
1745
+ const text = node.nodeValue || '';
1746
+ if (!text) continue;
1747
+
1748
+ // Find the last unmatched opener among eligible rules in this node.
1749
+ let best = null; // { rule, start }
1750
+ for (let i = 0; i < rules.length; i++) {
1751
+ const r = rules[i];
1752
+ if (!r || !r.open || !r.close) continue;
1753
+
1754
+ // Find last occurrence for better UX on multiple openers; keeps earlier content intact.
1755
+ const idx = text.lastIndexOf(r.open);
1756
+ if (idx === -1) continue;
1757
+
1758
+ // If a closing token for this rule exists after this opener within the same node,
1759
+ // let the standard pass handle it (we only want truly unmatched opens).
1760
+ const after = text.indexOf(r.close, idx + r.open.length);
1761
+ if (after !== -1) continue;
1762
+
1763
+ if (!best || idx > best.start) best = { rule: r, start: idx };
1764
+ }
1765
+
1766
+ if (!best) continue;
1767
+
1768
+ // Build fragment: keep prefix as text, wrap the tail into the rule element and mark as pending.
1769
+ const r = best.rule;
1770
+ const start = best.start;
1771
+
1772
+ const prefix = text.slice(0, start);
1773
+ const tail = text.slice(start + r.open.length);
1774
+ const frag = document.createDocumentFragment();
1775
+
1776
+ if (prefix) frag.appendChild(document.createTextNode(prefix));
1777
+
1778
+ const outTag = (r.tag && typeof r.tag === 'string') ? r.tag.toLowerCase() : 'span';
1779
+ const el = document.createElement(outTag === 'p' ? 'span' : outTag); // never create <p> inline
1780
+ if (r.className) el.className = r.className;
1781
+ el.setAttribute('data-cm', r.name);
1782
+ el.setAttribute('data-cm-pending', '1'); // allows styling/debug on "open-but-not-closed"
1783
+
1784
+ // Populate inner according to innerMode and decode policy.
1785
+ this.setInnerByMode(el, r.innerMode, tail, MD, !!r.decodeEntities);
1786
+ frag.appendChild(el);
1787
+
1788
+ try {
1789
+ node.parentNode.replaceChild(frag, node);
1790
+ this._d('STREAM_PENDING_OPEN_WRAP', { rule: r.name, start, open: r.open, preview: this.logger.pv(text, 160) });
1791
+ } catch (_) {
1792
+ // In the worst case, do nothing – keep original text node untouched.
1793
+ }
1794
+ }
1795
+ }
1796
+
1797
+ // Scan source and return ranges [start, end) of fenced code blocks (``` or ~~~).
1798
+ // Matches Markdown fences at line-start with up to 3 spaces/tabs indentation.
1799
+ _findFenceRanges(s) {
1800
+ const ranges = [];
1801
+ const n = s.length;
1802
+ let i = 0;
1803
+ let inFence = false;
1804
+ let fenceMark = '';
1805
+ let fenceLen = 0;
1806
+ let startLineStart = 0;
1807
+
1808
+ while (i < n) {
1809
+ const lineStart = i;
1810
+ // Find line end and newline length
1811
+ let j = lineStart;
1812
+ while (j < n && s.charCodeAt(j) !== 10 && s.charCodeAt(j) !== 13) j++;
1813
+ const lineEnd = j;
1814
+ let nl = 0;
1815
+ if (j < n) {
1816
+ if (s.charCodeAt(j) === 13 && j + 1 < n && s.charCodeAt(j + 1) === 10) nl = 2;
1817
+ else nl = 1;
1818
+ }
1819
+
1820
+ // Compute indentation up to 3 "spaces" (tabs count as 1 here – safe heuristic)
1821
+ let k = lineStart;
1822
+ let indent = 0;
1823
+ while (k < lineEnd) {
1824
+ const c = s.charCodeAt(k);
1825
+ if (c === 32 /* space */) { indent++; if (indent > 3) break; k++; }
1826
+ else if (c === 9 /* tab */) { indent++; if (indent > 3) break; k++; }
1827
+ else break;
1828
+ }
1829
+
1830
+ if (!inFence) {
1831
+ if (indent <= 3 && k < lineEnd) {
1832
+ const ch = s.charCodeAt(k);
1833
+ if (ch === 0x60 /* ` */ || ch === 0x7E /* ~ */) {
1834
+ const mark = String.fromCharCode(ch);
1835
+ let m = k;
1836
+ while (m < lineEnd && s.charCodeAt(m) === ch) m++;
1837
+ const run = m - k;
1838
+ if (run >= 3) {
1839
+ inFence = true;
1840
+ fenceMark = mark;
1841
+ fenceLen = run;
1842
+ startLineStart = lineStart;
1843
+ }
1844
+ }
1845
+ }
1846
+ } else {
1847
+ if (indent <= 3 && k < lineEnd && s.charCodeAt(k) === fenceMark.charCodeAt(0)) {
1848
+ let m = k;
1849
+ while (m < lineEnd && s.charCodeAt(m) === fenceMark.charCodeAt(0)) m++;
1850
+ const run = m - k;
1851
+ if (run >= fenceLen) {
1852
+ // Only whitespace is allowed after closing fence on the same line
1853
+ let onlyWS = true;
1854
+ for (let t = m; t < lineEnd; t++) {
1855
+ const cc = s.charCodeAt(t);
1856
+ if (cc !== 32 && cc !== 9) { onlyWS = false; break; }
1857
+ }
1858
+ if (onlyWS) {
1859
+ const endIdx = lineEnd + nl; // include trailing newline if present
1860
+ ranges.push([startLineStart, endIdx]);
1861
+ inFence = false; fenceMark = ''; fenceLen = 0; startLineStart = 0;
1862
+ }
1863
+ }
1864
+ }
1865
+ }
1866
+ i = lineEnd + nl;
1867
+ }
1868
+
1869
+ // If EOF while still in fence, mark until end of string.
1870
+ if (inFence) ranges.push([startLineStart, n]);
1871
+ return ranges;
1872
+ }
1873
+
1874
+ // Check if match starts at "top-level" of a line:
1875
+ // - up to 3 leading spaces/tabs allowed
1876
+ // - not a list item marker ("- ", "+ ", "* ", "1. ", "1) ") and not a blockquote ("> ")
1877
+ // - nothing else precedes the token on the same line
1878
+ _isTopLevelLineInSource(s, absIdx) {
1879
+ let ls = absIdx;
1880
+ while (ls > 0) {
1881
+ const ch = s.charCodeAt(ls - 1);
1882
+ if (ch === 10 /* \n */ || ch === 13 /* \r */) break;
1883
+ ls--;
1884
+ }
1885
+ const prefix = s.slice(ls, absIdx);
1886
+
1887
+ // Strip up to 3 leading "spaces" (tabs treated as 1 – acceptable heuristic)
1888
+ let i = 0, indent = 0;
1889
+ while (i < prefix.length) {
1890
+ const c = prefix.charCodeAt(i);
1891
+ if (c === 32) { indent++; if (indent > 3) break; i++; }
1892
+ else if (c === 9) { indent++; if (indent > 3) break; i++; }
1893
+ else break;
1894
+ }
1895
+ if (indent > 3) return false;
1896
+ const rest = prefix.slice(i);
1897
+
1898
+ // Reject lists/blockquote
1899
+ if (/^>\s?/.test(rest)) return false;
1900
+ if (/^[-+*]\s/.test(rest)) return false;
1901
+ if (/^\d+[.)]\s/.test(rest)) return false;
1902
+
1903
+ // If any other non-whitespace text precedes the token on this line – not top-level
1904
+ if (rest.trim().length > 0) return false;
1905
+
1906
+ return true;
1907
+ }
1908
+
1909
+ // Apply source-phase replacements to one outside-of-fence chunk with top-level guard.
1910
+ _applySourceReplacementsInChunk(full, chunk, baseOffset, rules) {
1911
+ let t = chunk;
1912
+ for (let i = 0; i < rules.length; i++) {
1913
+ const r = rules[i];
1914
+ if (!r || !(r.openReplace || r.closeReplace)) continue;
1915
+ try {
1916
+ r.re.lastIndex = 0;
1917
+ t = t.replace(r.re, (match, inner, offset /*, ...rest*/) => {
1918
+ const abs = baseOffset + (offset | 0);
1919
+ // Only apply when opener is at top-level on that line (not in lists/blockquote)
1920
+ if (!this._isTopLevelLineInSource(full, abs)) return match;
1921
+ const open = r.openReplace || '';
1922
+ const close = r.closeReplace || '';
1923
+ return open + (inner || '') + close;
1924
+ });
1925
+ } catch (_) { /* keep chunk as is on any error */ }
1926
+ }
1927
+ return t;
1928
+ }
1929
+ }
1846
1930
 
1847
1931
  // ==========================================================================
1848
1932
  // 5) Markdown runtime (markdown-it + code wrapper + math placeholders)