pygpt-net 2.6.44__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.
- pygpt_net/CHANGELOG.txt +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +0 -5
- pygpt_net/controller/ctx/ctx.py +6 -0
- pygpt_net/controller/debug/debug.py +11 -9
- pygpt_net/controller/debug/fixtures.py +1 -1
- pygpt_net/controller/dialogs/debug.py +40 -29
- pygpt_net/core/debug/agent.py +19 -14
- pygpt_net/core/debug/assistants.py +22 -24
- pygpt_net/core/debug/attachments.py +11 -7
- pygpt_net/core/debug/config.py +22 -23
- pygpt_net/core/debug/console/console.py +2 -1
- pygpt_net/core/debug/context.py +63 -63
- pygpt_net/core/debug/db.py +1 -4
- pygpt_net/core/debug/debug.py +1 -1
- pygpt_net/core/debug/events.py +14 -11
- pygpt_net/core/debug/indexes.py +41 -76
- pygpt_net/core/debug/kernel.py +11 -8
- pygpt_net/core/debug/models.py +20 -15
- pygpt_net/core/debug/plugins.py +9 -6
- pygpt_net/core/debug/presets.py +16 -11
- pygpt_net/core/debug/tabs.py +28 -22
- pygpt_net/core/debug/ui.py +25 -22
- pygpt_net/core/fixtures/stream/generator.py +1 -2
- pygpt_net/core/render/web/body.py +290 -23
- pygpt_net/core/render/web/helpers.py +26 -0
- pygpt_net/core/render/web/renderer.py +459 -705
- pygpt_net/core/tabs/tab.py +14 -1
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +15 -17
- pygpt_net/data/css/style.dark.css +6 -0
- pygpt_net/data/css/web-blocks.css +4 -0
- pygpt_net/data/css/web-blocks.light.css +1 -1
- pygpt_net/data/css/web-chatgpt.css +4 -0
- pygpt_net/data/css/web-chatgpt.light.css +1 -1
- pygpt_net/data/css/web-chatgpt_wide.css +4 -0
- pygpt_net/data/css/web-chatgpt_wide.light.css +1 -1
- pygpt_net/data/fixtures/fake_stream.txt +5733 -0
- pygpt_net/data/js/app.js +1921 -901
- pygpt_net/data/locale/locale.de.ini +1 -1
- pygpt_net/data/locale/locale.en.ini +5 -5
- pygpt_net/data/locale/locale.es.ini +1 -1
- pygpt_net/data/locale/locale.fr.ini +1 -1
- pygpt_net/data/locale/locale.it.ini +1 -1
- pygpt_net/data/locale/locale.pl.ini +2 -2
- pygpt_net/data/locale/locale.uk.ini +1 -1
- pygpt_net/data/locale/locale.zh.ini +1 -1
- pygpt_net/item/model.py +4 -1
- pygpt_net/js_rc.py +13076 -10198
- pygpt_net/provider/api/anthropic/__init__.py +3 -1
- pygpt_net/provider/api/anthropic/tools.py +1 -1
- pygpt_net/provider/api/google/__init__.py +7 -1
- pygpt_net/provider/api/x_ai/__init__.py +5 -1
- pygpt_net/provider/core/config/patch.py +14 -1
- pygpt_net/provider/llms/anthropic.py +37 -5
- pygpt_net/provider/llms/azure_openai.py +3 -1
- pygpt_net/provider/llms/base.py +13 -1
- pygpt_net/provider/llms/deepseek_api.py +13 -3
- pygpt_net/provider/llms/google.py +14 -1
- pygpt_net/provider/llms/hugging_face_api.py +105 -24
- pygpt_net/provider/llms/hugging_face_embedding.py +88 -0
- pygpt_net/provider/llms/hugging_face_router.py +28 -16
- pygpt_net/provider/llms/local.py +2 -0
- pygpt_net/provider/llms/mistral.py +60 -3
- pygpt_net/provider/llms/open_router.py +4 -2
- pygpt_net/provider/llms/openai.py +4 -1
- pygpt_net/provider/llms/perplexity.py +66 -5
- pygpt_net/provider/llms/utils.py +39 -0
- pygpt_net/provider/llms/voyage.py +50 -0
- pygpt_net/provider/llms/x_ai.py +70 -10
- pygpt_net/ui/layout/chat/output.py +1 -1
- pygpt_net/ui/widget/lists/db.py +1 -0
- pygpt_net/ui/widget/lists/debug.py +1 -0
- pygpt_net/ui/widget/tabs/body.py +12 -1
- pygpt_net/ui/widget/textarea/web.py +4 -4
- pygpt_net/utils.py +3 -2
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/METADATA +73 -16
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/RECORD +82 -78
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.44.dist-info → pygpt_net-2.6.46.dist-info}/entry_points.txt +0 -0
pygpt_net/data/js/app.js
CHANGED
|
@@ -599,9 +599,19 @@
|
|
|
599
599
|
|
|
600
600
|
// Custom markup rules for simple tags in text.
|
|
601
601
|
this.CUSTOM_MARKUP_RULES = Utils.g('CUSTOM_MARKUP_RULES', [
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
602
|
+
{ name: 'cmd', open: '[!cmd]', close: '[/!cmd]', tag: 'div', className: 'cmd', innerMode: 'text' },
|
|
603
|
+
{ name: 'think_md', open: '[!think]', close: '[/!think]', tag: 'think', className: '', innerMode: 'text' },
|
|
604
|
+
{ name: 'think_html', open: '<think>', close: '</think>', tag: 'think', className: '', innerMode: 'text', stream: true },
|
|
605
|
+
{ name: 'tool', open: '<tool>', close: '</tool>', tag: 'div', className: 'cmd', innerMode: 'text', stream: true },
|
|
606
|
+
|
|
607
|
+
// Streams+final: convert [!exec]... into fenced python code BEFORE markdown-it
|
|
608
|
+
{ name: 'exec_md', open: '[!exec]', close: '[/!exec]', innerMode: 'text', stream: true,
|
|
609
|
+
openReplace: '```python\n', closeReplace: '\n```', phase: 'source' },
|
|
610
|
+
|
|
611
|
+
// Streams+final: convert <execute>...</execute> into fenced python code BEFORE markdown-it
|
|
612
|
+
{ name: 'exec_html', open: '<execute>', close: '</execute>', innerMode: 'text', stream: true,
|
|
613
|
+
openReplace: '```python\n', closeReplace: '\n```', phase: 'source' }
|
|
614
|
+
]);
|
|
605
615
|
}
|
|
606
616
|
}
|
|
607
617
|
|
|
@@ -911,6 +921,20 @@
|
|
|
911
921
|
const hint = (cfg && cfg.RAF && cfg.RAF.FLUSH_BUDGET_MS) ? cfg.RAF.FLUSH_BUDGET_MS : 7;
|
|
912
922
|
this.SCAN_STEP_BUDGET_MS = Math.max(3, Math.min(12, hint));
|
|
913
923
|
}
|
|
924
|
+
_decodeEntitiesDeep(text, maxPasses = 2) {
|
|
925
|
+
if (!text || text.indexOf('&') === -1) return text || '';
|
|
926
|
+
const ta = Highlighter._decTA || (Highlighter._decTA = document.createElement('textarea'));
|
|
927
|
+
const decodeOnce = (s) => { ta.innerHTML = s; return ta.value; };
|
|
928
|
+
let prev = String(text);
|
|
929
|
+
let cur = decodeOnce(prev);
|
|
930
|
+
let passes = 1;
|
|
931
|
+
while (passes < maxPasses && cur !== prev) {
|
|
932
|
+
prev = cur;
|
|
933
|
+
cur = decodeOnce(prev);
|
|
934
|
+
passes++;
|
|
935
|
+
}
|
|
936
|
+
return cur;
|
|
937
|
+
}
|
|
914
938
|
// Global switch to skip all highlighting.
|
|
915
939
|
isDisabled() { return !!this.cfg.HL.DISABLE_ALL; }
|
|
916
940
|
// Configure hljs once (safe if hljs not present).
|
|
@@ -966,43 +990,44 @@
|
|
|
966
990
|
}
|
|
967
991
|
}
|
|
968
992
|
// Highlight a single code block with safety checks and scroll preservation.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (activeCode && codeEl === activeCode.codeEl) return;
|
|
993
|
+
_needsDeepDecode(text) {
|
|
994
|
+
if (!text) return false;
|
|
995
|
+
const s = String(text);
|
|
996
|
+
return (s.indexOf('&') !== -1) || (s.indexOf('&#') !== -1);
|
|
997
|
+
}
|
|
975
998
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
// Prefer wrapper meta if available to avoid .textContent on huge nodes.
|
|
983
|
-
let lines = NaN, chars = NaN;
|
|
984
|
-
if (wrap) {
|
|
985
|
-
const nlAttr = wrap.getAttribute('data-code-nl');
|
|
986
|
-
const lenAttr = wrap.getAttribute('data-code-len');
|
|
987
|
-
if (nlAttr) lines = parseInt(nlAttr, 10);
|
|
988
|
-
if (lenAttr) chars = parseInt(lenAttr, 10);
|
|
989
|
-
}
|
|
999
|
+
safeHighlight(codeEl, activeCode) {
|
|
1000
|
+
if (this.isDisabled()) return;
|
|
1001
|
+
if (!window.hljs || !codeEl || !codeEl.isConnected) return;
|
|
1002
|
+
if (!codeEl.closest('.msg-box.msg-bot')) return;
|
|
1003
|
+
if (codeEl.getAttribute('data-highlighted') === 'yes') return;
|
|
1004
|
+
if (activeCode && codeEl === activeCode.codeEl) return;
|
|
990
1005
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
codeEl.
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1006
|
+
// fast-skip final highlight for gigantic blocks using precomputed meta.
|
|
1007
|
+
try {
|
|
1008
|
+
const wrap = codeEl.closest('.code-wrapper');
|
|
1009
|
+
const maxLines = this.cfg.PROFILE_CODE.finalHighlightMaxLines | 0;
|
|
1010
|
+
const maxChars = this.cfg.PROFILE_CODE.finalHighlightMaxChars | 0;
|
|
1011
|
+
|
|
1012
|
+
let lines = NaN, chars = NaN;
|
|
1013
|
+
if (wrap) {
|
|
1014
|
+
const nlAttr = wrap.getAttribute('data-code-nl');
|
|
1015
|
+
const lenAttr = wrap.getAttribute('data-code-len');
|
|
1016
|
+
if (nlAttr) lines = parseInt(nlAttr, 10);
|
|
1017
|
+
if (lenAttr) chars = parseInt(lenAttr, 10);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if ((Number.isFinite(lines) && maxLines > 0 && lines > maxLines) ||
|
|
1021
|
+
(Number.isFinite(chars) && maxChars > 0 && chars > maxChars)) {
|
|
1022
|
+
// NEW: normalize entities for readability even if we skip final highlight
|
|
1023
|
+
try {
|
|
1024
|
+
const raw = codeEl.textContent || '';
|
|
1025
|
+
if (this._needsDeepDecode(raw)) {
|
|
1026
|
+
const dec = this._decodeEntitiesDeep(raw);
|
|
1027
|
+
if (dec !== raw) codeEl.textContent = dec;
|
|
1028
|
+
}
|
|
1029
|
+
} catch (_) {}
|
|
1000
1030
|
|
|
1001
|
-
// Fallback to reading actual text only if wrapper meta is missing.
|
|
1002
|
-
if (!Number.isFinite(lines) || !Number.isFinite(chars)) {
|
|
1003
|
-
const txt0 = codeEl.textContent || '';
|
|
1004
|
-
const ln0 = Utils.countNewlines(txt0);
|
|
1005
|
-
if ((maxLines > 0 && ln0 > maxLines) || (maxChars > 0 && txt0.length > maxChars)) {
|
|
1006
1031
|
codeEl.classList.add('hljs');
|
|
1007
1032
|
codeEl.setAttribute('data-highlighted', 'yes');
|
|
1008
1033
|
codeEl.dataset.finalHlSkip = '1';
|
|
@@ -1010,32 +1035,59 @@
|
|
|
1010
1035
|
this.codeScroll.scheduleScroll(codeEl, false, false);
|
|
1011
1036
|
return;
|
|
1012
1037
|
}
|
|
1013
|
-
}
|
|
1014
|
-
} catch (_) { /* safe fallback */ }
|
|
1015
1038
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1039
|
+
// Fallback to reading actual text only if wrapper meta is missing.
|
|
1040
|
+
if (!Number.isFinite(lines) || !Number.isFinite(chars)) {
|
|
1041
|
+
const txt0 = codeEl.textContent || '';
|
|
1042
|
+
const ln0 = Utils.countNewlines(txt0);
|
|
1043
|
+
if ((maxLines > 0 && ln0 > maxLines) || (maxChars > 0 && txt0.length > maxChars)) {
|
|
1044
|
+
// NEW: normalize entities here as well
|
|
1045
|
+
try {
|
|
1046
|
+
if (this._needsDeepDecode(txt0)) {
|
|
1047
|
+
const dec = this._decodeEntitiesDeep(txt0);
|
|
1048
|
+
if (dec !== txt0) codeEl.textContent = dec;
|
|
1049
|
+
}
|
|
1050
|
+
} catch (_) {}
|
|
1019
1051
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1052
|
+
codeEl.classList.add('hljs');
|
|
1053
|
+
codeEl.setAttribute('data-highlighted', 'yes');
|
|
1054
|
+
codeEl.dataset.finalHlSkip = '1';
|
|
1055
|
+
try { this.codeScroll.attachHandlers(codeEl); } catch (_) {}
|
|
1056
|
+
this.codeScroll.scheduleScroll(codeEl, false, false);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
} catch (_) { /* safe fallback */ }
|
|
1061
|
+
|
|
1062
|
+
const wasNearBottom = this.codeScroll.isNearBottomEl(codeEl, 16);
|
|
1063
|
+
const st = this.codeScroll.state(codeEl);
|
|
1064
|
+
const shouldAutoScrollAfter = (st.autoFollow === true) || wasNearBottom;
|
|
1065
|
+
|
|
1066
|
+
try {
|
|
1067
|
+
try { codeEl.classList.remove('hljs'); codeEl.removeAttribute('data-highlighted'); } catch (_) {}
|
|
1068
|
+
|
|
1069
|
+
// NEW: deep-decode text before highlighting (fixes &#x27; → ' etc.)
|
|
1070
|
+
let txt = codeEl.textContent || '';
|
|
1071
|
+
if (this._needsDeepDecode(txt)) {
|
|
1072
|
+
try { txt = this._decodeEntitiesDeep(txt); } catch (_) {}
|
|
1073
|
+
}
|
|
1074
|
+
codeEl.textContent = txt; // ensure no stale spans remain and normalized text provided
|
|
1075
|
+
|
|
1076
|
+
hljs.highlightElement(codeEl);
|
|
1077
|
+
codeEl.setAttribute('data-highlighted', 'yes');
|
|
1078
|
+
} catch (_) {
|
|
1079
|
+
if (!codeEl.classList.contains('hljs')) codeEl.classList.add('hljs');
|
|
1080
|
+
} finally {
|
|
1081
|
+
try { this.codeScroll.attachHandlers(codeEl); } catch (_) {}
|
|
1082
|
+
const needInitForce = (codeEl.dataset && (codeEl.dataset.csInitBtm === '1' || codeEl.dataset.justFinalized === '1'));
|
|
1083
|
+
const mustScroll = shouldAutoScrollAfter || needInitForce;
|
|
1084
|
+
if (mustScroll) this.codeScroll.scheduleScroll(codeEl, false, !!needInitForce);
|
|
1085
|
+
if (codeEl.dataset) {
|
|
1086
|
+
if (codeEl.dataset.csInitBtm === '1') codeEl.dataset.csInitBtm = '0';
|
|
1087
|
+
if (codeEl.dataset.justFinalized === '1') codeEl.dataset.justFinalized = '0';
|
|
1088
|
+
}
|
|
1036
1089
|
}
|
|
1037
1090
|
}
|
|
1038
|
-
}
|
|
1039
1091
|
|
|
1040
1092
|
// Start a budgeted global scan – split across frames to avoid long blocking.
|
|
1041
1093
|
_startGlobalScan(activeCode) {
|
|
@@ -1154,913 +1206,1384 @@
|
|
|
1154
1206
|
this.hlQueueSet.clear(); this.hlQueue.length = 0;
|
|
1155
1207
|
}
|
|
1156
1208
|
}
|
|
1209
|
+
// ==========================================================================
|
|
1210
|
+
// 4) Custom Markup Processor
|
|
1211
|
+
// ==========================================================================
|
|
1157
1212
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
+
}
|
|
1161
1231
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
this.__compiled = null;
|
|
1167
|
-
}
|
|
1168
|
-
_d(line, ctx) { try { this.logger.debug('CM', line, ctx); } catch (_) {} }
|
|
1169
|
-
|
|
1170
|
-
// Decode HTML entities once (safe)
|
|
1171
|
-
// This addresses cases when linkify/full markdown path leaves literal """ etc. in text nodes.
|
|
1172
|
-
// We decode only for rules that explicitly opt-in (see compile()) to avoid changing semantics globally.
|
|
1173
|
-
decodeEntitiesOnce(s) {
|
|
1174
|
-
if (!s || s.indexOf('&') === -1) return String(s || '');
|
|
1175
|
-
// Using a shared <textarea> avoids DOM parsing side-effects and is fast enough for small JSON payloads.
|
|
1176
|
-
const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
|
|
1177
|
-
ta.innerHTML = s;
|
|
1178
|
-
return ta.value;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Compile rules once; also precompile strict and whitespace-tolerant "full match" regexes.
|
|
1182
|
-
compile(rules) {
|
|
1183
|
-
const src = Array.isArray(rules) ? rules : (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
|
|
1184
|
-
const compiled = [];
|
|
1185
|
-
for (const r of src) {
|
|
1186
|
-
if (!r || typeof r.open !== 'string' || typeof r.close !== 'string') continue;
|
|
1187
|
-
const tag = (r.tag || 'span').toLowerCase();
|
|
1188
|
-
const className = (r.className || r.class || '').trim();
|
|
1189
|
-
const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
|
|
1190
|
-
|
|
1191
|
-
// Opt-in decoding: default to true for "cmd" to fix JSON quotes (") in static full-render path,
|
|
1192
|
-
// leave false for other tags unless explicitly requested by rule author.
|
|
1193
|
-
const decodeEntities = (typeof r.decodeEntities === 'boolean')
|
|
1194
|
-
? r.decodeEntities
|
|
1195
|
-
: ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
|
|
1196
|
-
|
|
1197
|
-
const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
|
|
1198
|
-
const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
|
|
1199
|
-
const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
|
|
1200
|
-
|
|
1201
|
-
const item = {
|
|
1202
|
-
name: r.name || tag,
|
|
1203
|
-
tag,
|
|
1204
|
-
className,
|
|
1205
|
-
innerMode,
|
|
1206
|
-
open: r.open,
|
|
1207
|
-
close: r.close,
|
|
1208
|
-
decodeEntities, // per-rule decode switch
|
|
1209
|
-
re, reFull, reFullTrim
|
|
1210
|
-
};
|
|
1211
|
-
compiled.push(item);
|
|
1212
|
-
this._d('COMPILE_RULE', { name: item.name, tag: item.tag, innerMode: item.innerMode, open: item.open, close: item.close });
|
|
1213
|
-
}
|
|
1214
|
-
if (compiled.length === 0) {
|
|
1215
|
-
const open = '[!cmd]', close = '[/!cmd]';
|
|
1216
|
-
const item = {
|
|
1217
|
-
name: 'cmd', tag: 'p', className: 'cmd', innerMode: 'text', open, close,
|
|
1218
|
-
decodeEntities: true, // Fallback rule for cmd also opts-in to decoding
|
|
1219
|
-
re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
|
|
1220
|
-
reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
|
|
1221
|
-
reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$')
|
|
1222
|
-
};
|
|
1223
|
-
compiled.push(item);
|
|
1224
|
-
this._d('COMPILE_RULE_FALLBACK', { name: item.name });
|
|
1225
|
-
}
|
|
1226
|
-
return compiled;
|
|
1227
|
-
}
|
|
1228
|
-
// Ensure rules are compiled and cached.
|
|
1229
|
-
ensureCompiled() {
|
|
1230
|
-
if (!this.__compiled) {
|
|
1231
|
-
this.__compiled = this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
|
|
1232
|
-
this._d('ENSURE_COMPILED', { count: this.__compiled.length });
|
|
1233
|
-
}
|
|
1234
|
-
return this.__compiled;
|
|
1235
|
-
}
|
|
1236
|
-
// Replace rules set (also exposes rules on window).
|
|
1237
|
-
setRules(rules) {
|
|
1238
|
-
this.__compiled = this.compile(rules);
|
|
1239
|
-
window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() : (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
|
|
1240
|
-
this._d('SET_RULES', { count: this.__compiled.length });
|
|
1241
|
-
}
|
|
1242
|
-
// Return current rules as array.
|
|
1243
|
-
getRules() {
|
|
1244
|
-
const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice()
|
|
1245
|
-
: (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
|
|
1246
|
-
this._d('GET_RULES', { count: list.length });
|
|
1247
|
-
return list;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
// Context guards
|
|
1251
|
-
isInsideForbiddenContext(node) {
|
|
1252
|
-
const p = node.parentElement; if (!p) return true;
|
|
1253
|
-
return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper');
|
|
1254
|
-
}
|
|
1255
|
-
isInsideForbiddenElement(el) {
|
|
1256
|
-
if (!el) return true;
|
|
1257
|
-
return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper');
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Global finder on a single text blob (original per-text-node logic).
|
|
1261
|
-
findNextMatch(text, from, rules) {
|
|
1262
|
-
let best = null;
|
|
1263
|
-
for (const rule of rules) {
|
|
1264
|
-
rule.re.lastIndex = from;
|
|
1265
|
-
const m = rule.re.exec(text);
|
|
1266
|
-
if (m) {
|
|
1267
|
-
const start = m.index, end = rule.re.lastIndex;
|
|
1268
|
-
if (!best || start < best.start) best = { rule, start, end, inner: m[1] || '' };
|
|
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
1269
1236
|
}
|
|
1270
1237
|
}
|
|
1271
|
-
return best;
|
|
1272
|
-
}
|
|
1273
1238
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1279
|
-
if (
|
|
1280
|
-
|
|
1281
|
-
// Legacy safety net (should not normally execute).
|
|
1282
|
-
rule.re.lastIndex = 0;
|
|
1283
|
-
const m = rule.re.exec(text);
|
|
1284
|
-
if (m && m.index === 0 && (rule.re.lastIndex === text.length)) {
|
|
1285
|
-
const m2 = rule.re.exec(text);
|
|
1286
|
-
if (!m2) return { rule, inner: m[1] || '' };
|
|
1287
|
-
}
|
|
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;
|
|
1288
1246
|
}
|
|
1247
|
+
return false;
|
|
1289
1248
|
}
|
|
1290
|
-
return null;
|
|
1291
|
-
}
|
|
1292
1249
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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);
|
|
1299
1260
|
}
|
|
1300
1261
|
|
|
1301
|
-
|
|
1262
|
+
// Make a DOM Fragment from HTML string (robust across contexts).
|
|
1263
|
+
_fragmentFromHTML(html, ctxNode) {
|
|
1264
|
+
let frag = null;
|
|
1302
1265
|
try {
|
|
1303
|
-
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
+
}
|
|
1307
1278
|
}
|
|
1308
|
-
el.textContent = payload;
|
|
1309
|
-
}
|
|
1310
1279
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
this.
|
|
1316
|
-
|
|
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
|
+
}
|
|
1317
1296
|
}
|
|
1318
|
-
const t = el.textContent || '';
|
|
1319
|
-
if (t.indexOf('[!') === -1) return false;
|
|
1320
|
-
|
|
1321
|
-
for (const rule of rules) {
|
|
1322
|
-
if (!rule) continue;
|
|
1323
1297
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
+
}
|
|
1339
1349
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const
|
|
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 });
|
|
1343
1364
|
}
|
|
1344
1365
|
|
|
1345
|
-
this.
|
|
1346
|
-
return
|
|
1366
|
+
this.__hasStreamRules = hasStream;
|
|
1367
|
+
return compiled;
|
|
1347
1368
|
}
|
|
1348
|
-
this._d('P_NO_FULL_MATCH', { preview: this.logger.pv(t, 160) });
|
|
1349
|
-
return false;
|
|
1350
|
-
}
|
|
1351
1369
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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;
|
|
1359
1387
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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);
|
|
1373
1403
|
}
|
|
1404
|
+
out += s.slice(a, b); // pass fenced code verbatim
|
|
1405
|
+
last = b;
|
|
1374
1406
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1407
|
+
if (last < s.length) {
|
|
1408
|
+
const tail = s.slice(last);
|
|
1409
|
+
out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
|
|
1410
|
+
}
|
|
1411
|
+
return out;
|
|
1377
1412
|
}
|
|
1378
1413
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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 });
|
|
1384
1425
|
}
|
|
1385
|
-
|
|
1426
|
+
return out;
|
|
1427
|
+
}
|
|
1386
1428
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
const parent = node.parentElement;
|
|
1393
|
-
|
|
1394
|
-
// Entire text node equals one full match and parent is <p>.
|
|
1395
|
-
if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
|
|
1396
|
-
const fm = this.findFullMatch(text, rules);
|
|
1397
|
-
if (fm && fm.rule.tag === 'p') {
|
|
1398
|
-
const out = document.createElement('p');
|
|
1399
|
-
if (fm.rule.className) out.className = fm.rule.className;
|
|
1400
|
-
out.setAttribute('data-cm', fm.rule.name);
|
|
1401
|
-
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities);
|
|
1402
|
-
try { parent.replaceWith(out); } catch (_) {
|
|
1403
|
-
const par = parent.parentNode; if (par) par.replaceChild(out, parent);
|
|
1404
|
-
}
|
|
1405
|
-
this._d('WALKER_FULL_REPLACE', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
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 });
|
|
1408
1434
|
}
|
|
1435
|
+
return this.__compiled;
|
|
1436
|
+
}
|
|
1409
1437
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
+
}
|
|
1414
1444
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
+
}
|
|
1418
1452
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1453
|
+
// Fast switch: do we have any rules that want streaming parsing?
|
|
1454
|
+
hasStreamRules() {
|
|
1455
|
+
this.ensureCompiled();
|
|
1456
|
+
return !!this.__hasStreamRules;
|
|
1457
|
+
}
|
|
1422
1458
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
+
}
|
|
1428
1470
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
+
}
|
|
1433
1481
|
}
|
|
1482
|
+
return best;
|
|
1483
|
+
}
|
|
1434
1484
|
|
|
1435
|
-
|
|
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
|
+
}
|
|
1436
1502
|
|
|
1437
|
-
|
|
1438
|
-
|
|
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 (_) {}
|
|
1439
1508
|
}
|
|
1440
1509
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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 (_) {}
|
|
1445
1516
|
}
|
|
1517
|
+
el.textContent = payload;
|
|
1446
1518
|
}
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
1519
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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;
|
|
1453
1529
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
// Cooperative async utilities available in renderer for heavy decode/render paths
|
|
1459
|
-
this.asyncer = asyncer || new AsyncRunner(cfg, raf);
|
|
1460
|
-
this.raf = raf || null;
|
|
1530
|
+
for (const rule of rules) {
|
|
1531
|
+
if (!rule) continue;
|
|
1532
|
+
const m = rule.reFullTrim ? rule.reFullTrim.exec(t) : null;
|
|
1533
|
+
if (!m) continue;
|
|
1461
1534
|
|
|
1462
|
-
|
|
1463
|
-
this.MD_STREAM = null;
|
|
1535
|
+
const innerText = m[1] || '';
|
|
1464
1536
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
init() {
|
|
1474
|
-
if (!window.markdownit) { this.logger.log('[MD] markdown-it not found – rendering skipped.'); return; }
|
|
1475
|
-
// Full renderer (used for non-hot paths, final results)
|
|
1476
|
-
this.MD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
|
|
1477
|
-
// Streaming renderer (no linkify) – hot path
|
|
1478
|
-
this.MD_STREAM = window.markdownit({ html: false, linkify: false, breaks: true, highlight: () => '' });
|
|
1479
|
-
|
|
1480
|
-
// SAFETY: disable CommonMark "indented code blocks" unless explicitly enabled.
|
|
1481
|
-
if (!this.cfg.MD || this.cfg.MD.ALLOW_INDENTED_CODE !== true) {
|
|
1482
|
-
try { this.MD.block.ruler.disable('code'); } catch (_) {}
|
|
1483
|
-
try { this.MD_STREAM.block.ruler.disable('code'); } catch (_) {}
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
const escapeHtml = Utils.escapeHtml;
|
|
1487
|
-
|
|
1488
|
-
// Dollar and bracket math placeholder plugins: generate lightweight placeholders to be picked up by KaTeX later.
|
|
1489
|
-
const mathDollarPlaceholderPlugin = (md) => {
|
|
1490
|
-
function notEscaped(src, pos) { let back = 0; while (pos - back - 1 >= 0 && src.charCodeAt(pos - back - 1) === 0x5C) back++; return (back % 2) === 0; }
|
|
1491
|
-
function math_block_dollar(state, startLine, endLine, silent) {
|
|
1492
|
-
const pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
1493
|
-
const max = state.eMarks[startLine];
|
|
1494
|
-
if (pos + 1 >= max) return false;
|
|
1495
|
-
if (state.src.charCodeAt(pos) !== 0x24 || state.src.charCodeAt(pos + 1) !== 0x24) return false;
|
|
1496
|
-
let nextLine = startLine + 1, found = false;
|
|
1497
|
-
for (; nextLine < endLine; nextLine++) {
|
|
1498
|
-
let p = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
1499
|
-
const pe = state.eMarks[nextLine];
|
|
1500
|
-
if (p + 1 < pe && state.src.charCodeAt(p) === 0x24 && state.src.charCodeAt(p + 1) === 0x24) { found = true; break; }
|
|
1501
|
-
}
|
|
1502
|
-
if (!found) return false;
|
|
1503
|
-
if (silent) return true;
|
|
1504
|
-
|
|
1505
|
-
const contentStart = state.bMarks[startLine] + state.tShift[startLine] + 2;
|
|
1506
|
-
const contentEndLine = nextLine - 1;
|
|
1507
|
-
let content = '';
|
|
1508
|
-
if (contentEndLine >= startLine + 1) {
|
|
1509
|
-
const startIdx = state.bMarks[startLine + 1];
|
|
1510
|
-
const endIdx = state.eMarks[contentEndLine];
|
|
1511
|
-
content = state.src.slice(startIdx, endIdx);
|
|
1512
|
-
} else content = '';
|
|
1513
|
-
|
|
1514
|
-
const token = state.push('math_block_dollar', '', 0);
|
|
1515
|
-
token.block = true; token.content = content; state.line = nextLine + 1; return true;
|
|
1516
|
-
}
|
|
1517
|
-
function math_inline_dollar(state, silent) {
|
|
1518
|
-
const pos = state.pos, src = state.src, max = state.posMax;
|
|
1519
|
-
if (pos >= max) return false;
|
|
1520
|
-
if (src.charCodeAt(pos) !== 0x24) return false;
|
|
1521
|
-
if (pos + 1 < max && src.charCodeAt(pos + 1) === 0x24) return false;
|
|
1522
|
-
const after = pos + 1 < max ? src.charCodeAt(pos + 1) : 0;
|
|
1523
|
-
if (after === 0x20 || after === 0x0A || after === 0x0D) return false;
|
|
1524
|
-
let i = pos + 1;
|
|
1525
|
-
while (i < max) {
|
|
1526
|
-
const ch = src.charCodeAt(i);
|
|
1527
|
-
if (ch === 0x24 && notEscaped(src, i)) {
|
|
1528
|
-
const before = i - 1 >= 0 ? src.charCodeAt(i - 1) : 0;
|
|
1529
|
-
if (before === 0x20 || before === 0x0A || before === 0x0D) { i++; continue; }
|
|
1530
|
-
break;
|
|
1531
|
-
}
|
|
1532
|
-
i++;
|
|
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;
|
|
1533
1545
|
}
|
|
1534
|
-
if (i >= max || src.charCodeAt(i) !== 0x24) return false;
|
|
1535
1546
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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);
|
|
1539
1555
|
}
|
|
1540
|
-
|
|
1556
|
+
this._d('P_REPLACED', { rule: rule.name, asTag: outTag });
|
|
1557
|
+
return true;
|
|
1541
1558
|
}
|
|
1559
|
+
this._d('P_NO_FULL_MATCH', { preview: this.logger.pv(t, 160) });
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1542
1562
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1563
|
+
// Core implementation shared by static and streaming passes.
|
|
1564
|
+
applyRules(root, MD, rules) {
|
|
1565
|
+
if (!root || !rules || !rules.length) return;
|
|
1545
1566
|
|
|
1546
|
-
|
|
1547
|
-
const tex = tokens[idx].content || '';
|
|
1548
|
-
return `<span class="math-pending" data-display="0"><span class="math-fallback">$${escapeHtml(tex)}$</span><script type="math/tex">${escapeHtml(tex)}</script></span>`;
|
|
1549
|
-
};
|
|
1550
|
-
md.renderer.rules.math_block_dollar = (tokens, idx) => {
|
|
1551
|
-
const tex = tokens[idx].content || '';
|
|
1552
|
-
return `<div class="math-pending" data-display="1"><div class="math-fallback">$$${escapeHtml(tex)}$$</div><script type="math/tex; mode=display">${escapeHtml(tex)}</script></div>`;
|
|
1553
|
-
};
|
|
1554
|
-
};
|
|
1567
|
+
const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
|
|
1555
1568
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
if (
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
+
}
|
|
1569
1584
|
}
|
|
1570
|
-
|
|
1585
|
+
} catch (e) {
|
|
1586
|
+
this._d('P_TOLERANT_SCAN_ERR', String(e));
|
|
1571
1587
|
}
|
|
1572
|
-
md.inline.ruler.before('escape', 'math_brackets', math_brackets);
|
|
1573
|
-
md.renderer.rules.math_inline_bracket = (tokens, idx) => {
|
|
1574
|
-
const tex = tokens[idx].content || '';
|
|
1575
|
-
return `<span class="math-pending" data-display="0"><span class="math-fallback">\\(${escapeHtml(tex)}\\)</span><script type="math/tex">${escapeHtml(tex)}</script></span>`;
|
|
1576
|
-
};
|
|
1577
|
-
md.renderer.rules.math_block_bracket = (tokens, idx) => {
|
|
1578
|
-
const tex = tokens[idx].content || '';
|
|
1579
|
-
return `<div class="math-pending" data-display="1"><div class="math-fallback">\\[${escapeHtml(tex)}\\]</div><script type="math/tex; mode=display">${escapeHtml(tex)}</script></div>`;
|
|
1580
|
-
};
|
|
1581
|
-
};
|
|
1582
|
-
|
|
1583
|
-
this.MD.use(mathDollarPlaceholderPlugin);
|
|
1584
|
-
this.MD.use(mathBracketsPlaceholderPlugin);
|
|
1585
|
-
this.MD_STREAM.use(mathDollarPlaceholderPlugin);
|
|
1586
|
-
this.MD_STREAM.use(mathBracketsPlaceholderPlugin);
|
|
1587
|
-
|
|
1588
|
-
const cfg = this.cfg; const logger = this.logger;
|
|
1589
|
-
(function codeWrapperPlugin(md, logger) {
|
|
1590
|
-
let CODE_IDX = 1;
|
|
1591
|
-
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
1592
|
-
|
|
1593
|
-
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
1594
|
-
const seenFP = new Set();
|
|
1595
|
-
const makeFP = (info, raw) => {
|
|
1596
|
-
const head = (raw || '').slice(0, 96);
|
|
1597
|
-
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
1598
|
-
};
|
|
1599
1588
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
1609
|
-
docker: 'dockerfile'
|
|
1610
|
-
};
|
|
1611
|
-
function normLang(s) { if (!s) return ''; const v = String(s).trim().toLowerCase(); return ALIAS[v] || v; }
|
|
1612
|
-
function isSupportedByHLJS(lang) { try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; } }
|
|
1613
|
-
function classForHighlight(lang) { if (!lang) return 'plaintext'; return isSupportedByHLJS(lang) ? lang : 'plaintext'; }
|
|
1614
|
-
function stripBOM(s) { return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s; }
|
|
1615
|
-
|
|
1616
|
-
function detectFromFirstLine(raw, rid) {
|
|
1617
|
-
if (!raw) return { lang: '', content: raw, isOutput: false };
|
|
1618
|
-
const lines = raw.split(/\r?\n/);
|
|
1619
|
-
if (!lines.length) return { lang: '', content: raw, isOutput: false };
|
|
1620
|
-
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
1621
|
-
if (i >= lines.length) { log(`#${rid} first-line: only whitespace`); return { lang: '', content: raw, isOutput: false }; }
|
|
1622
|
-
let first = stripBOM(lines[i]).trim();
|
|
1623
|
-
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
1624
|
-
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
1625
|
-
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) { log(`#${rid} first-line: no token match`, { first }); return { lang: '', content: raw, isOutput: false }; }
|
|
1626
|
-
let cand = normLang(token);
|
|
1627
|
-
if (cand === 'output') {
|
|
1628
|
-
const content = lines.slice(i + 1).join('\n');
|
|
1629
|
-
log(`#${rid} first-line: output header`);
|
|
1630
|
-
return { lang: 'python', headerLabel: 'output', content, isOutput: true };
|
|
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;
|
|
1631
1597
|
}
|
|
1632
|
-
|
|
1633
|
-
if (!rest.trim()) { log(`#${rid} first-line: directive but no content after, ignore`, { cand }); return { lang: '', content: raw, isOutput: false }; }
|
|
1634
|
-
log(`#${rid} first-line: directive accepted`, { cand, restLen: rest.length, hljs: isSupportedByHLJS(cand) });
|
|
1635
|
-
return { lang: cand, headerLabel: cand, content: rest, isOutput: false };
|
|
1636
|
-
}
|
|
1598
|
+
});
|
|
1637
1599
|
|
|
1638
|
-
|
|
1639
|
-
|
|
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
|
+
}
|
|
1640
1618
|
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
+
}
|
|
1647
1632
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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;
|
|
1651
1676
|
}
|
|
1652
|
-
const det = detectFromFirstLine(raw, rid);
|
|
1653
|
-
if (det && (det.lang || det.isOutput)) return det;
|
|
1654
|
-
log(`#${rid} resolve: fallback`);
|
|
1655
|
-
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
1656
|
-
}
|
|
1657
1677
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
const canLog = !DEDUP || !seenFP.has(fp);
|
|
1663
|
-
if (canLog) log(`FENCE_ENTER #${rid}`, { info: (token.info || ''), rawHead: logger.pv(raw) });
|
|
1664
|
-
|
|
1665
|
-
const res = resolveLanguageAndContent(token.info || '', raw, rid);
|
|
1666
|
-
const isOutput = !!res.isOutput;
|
|
1667
|
-
const headerLabel = isOutput ? 'output' : (res.headerLabel || (res.lang || 'code'));
|
|
1668
|
-
const langClass = isOutput ? 'python' : classForHighlight(res.lang);
|
|
1669
|
-
|
|
1670
|
-
if (canLog) {
|
|
1671
|
-
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
1672
|
-
if (DEDUP) seenFP.add(fp);
|
|
1678
|
+
if (!didReplace) continue;
|
|
1679
|
+
|
|
1680
|
+
if (i < text.length) {
|
|
1681
|
+
frag.appendChild(document.createTextNode(text.slice(i)));
|
|
1673
1682
|
}
|
|
1674
1683
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const tail = content.slice(-64);
|
|
1680
|
-
const headEsc = Utils.escapeHtml(head);
|
|
1681
|
-
const tailEsc = Utils.escapeHtml(tail);
|
|
1682
|
-
// Note: for full renderer we will also persist data-code-nl (see below).
|
|
1683
|
-
|
|
1684
|
-
const inner = Utils.escapeHtml(content);
|
|
1685
|
-
const idxLocal = CODE_IDX++;
|
|
1686
|
-
|
|
1687
|
-
let actions = '';
|
|
1688
|
-
if (langClass === 'html') {
|
|
1689
|
-
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-preview"><img src="${cfg.ICONS.CODE_PREVIEW}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.PREVIEW)}</span></a>`;
|
|
1690
|
-
} else if (langClass === 'python' && headerLabel !== 'output') {
|
|
1691
|
-
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-run"><img src="${cfg.ICONS.CODE_RUN}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.RUN)}</span></a>`;
|
|
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) });
|
|
1692
1688
|
}
|
|
1693
|
-
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-collapse"><img src="${cfg.ICONS.CODE_MENU}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}</span></a>`;
|
|
1694
|
-
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-copy"><img src="${cfg.ICONS.CODE_COPY}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.COPY)}</span></a>`;
|
|
1695
|
-
|
|
1696
|
-
// attach precomputed meta (len/head/tail) on wrapper for downstream optimizations
|
|
1697
|
-
return (
|
|
1698
|
-
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
1699
|
-
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
1700
|
-
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}"` + // meta (no nl here – only in full renderer)
|
|
1701
|
-
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
1702
|
-
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
1703
|
-
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
1704
|
-
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
1705
|
-
`</div>`
|
|
1706
|
-
);
|
|
1707
1689
|
}
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// Apply wrapper plugin to full renderer with extra meta (includes number of lines).
|
|
1711
|
-
(function codeWrapperPlugin(md, logger) {
|
|
1712
|
-
// identical core logic – augmented with data-code-nl for full renderer
|
|
1713
|
-
let CODE_IDX = 1;
|
|
1714
|
-
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
1715
|
-
|
|
1716
|
-
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
1717
|
-
const seenFP = new Set();
|
|
1718
|
-
const makeFP = (info, raw) => {
|
|
1719
|
-
const head = (raw || '').slice(0, 96);
|
|
1720
|
-
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
1721
|
-
};
|
|
1690
|
+
}
|
|
1722
1691
|
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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;
|
|
1754
1740
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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;
|
|
1760
1757
|
|
|
1761
|
-
|
|
1762
|
-
|
|
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;
|
|
1763
1762
|
|
|
1764
|
-
|
|
1765
|
-
const infoLangRaw = (info || '').trim().split(/\s+/)[0] || '';
|
|
1766
|
-
let cand = normLang(infoLangRaw);
|
|
1767
|
-
if (cand === 'output') {
|
|
1768
|
-
log(`#${rid} info: output header`);
|
|
1769
|
-
return { lang: 'python', headerLabel: 'output', content: raw, isOutput: true };
|
|
1763
|
+
if (!best || idx > best.start) best = { rule: r, start: idx };
|
|
1770
1764
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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.
|
|
1774
1793
|
}
|
|
1775
|
-
const det = detectFromFirstLine(raw, rid);
|
|
1776
|
-
if (det && (det.lang || det.isOutput)) return det;
|
|
1777
|
-
log(`#${rid} resolve: fallback`);
|
|
1778
|
-
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
1779
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
|
+
}
|
|
1780
1819
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
const isOutput = !!res.isOutput;
|
|
1790
|
-
const headerLabel = isOutput ? 'output' : (res.headerLabel || (res.lang || 'code'));
|
|
1791
|
-
const langClass = isOutput ? 'python' : classForHighlight(res.lang);
|
|
1792
|
-
|
|
1793
|
-
if (canLog) {
|
|
1794
|
-
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
1795
|
-
if (DEDUP) seenFP.add(fp);
|
|
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;
|
|
1796
1828
|
}
|
|
1797
1829
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
+
}
|
|
1815
1865
|
}
|
|
1816
|
-
|
|
1817
|
-
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-copy"><img src="${cfg.ICONS.CODE_COPY}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.COPY)}</span></a>`;
|
|
1818
|
-
|
|
1819
|
-
return (
|
|
1820
|
-
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
1821
|
-
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
1822
|
-
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}" data-code-nl="${String(nl)}"` + // include nl for full renderer
|
|
1823
|
-
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
1824
|
-
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
1825
|
-
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
1826
|
-
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
1827
|
-
`</div>`
|
|
1828
|
-
);
|
|
1866
|
+
i = lineEnd + nl;
|
|
1829
1867
|
}
|
|
1830
|
-
})(this.MD, this.logger);
|
|
1831
|
-
}
|
|
1832
|
-
// Replace "sandbox:" links with file:// in markdown source (host policy).
|
|
1833
|
-
preprocessMD(s) { return (s || '').replace(/\]\(sandbox:/g, '](file://'); }
|
|
1834
|
-
// Decode base64 UTF-8 to string (shared TextDecoder).
|
|
1835
|
-
b64ToUtf8(b64) {
|
|
1836
|
-
const bin = atob(b64);
|
|
1837
|
-
const bytes = new Uint8Array(bin.length);
|
|
1838
|
-
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1839
|
-
return Utils.utf8Decode(bytes);
|
|
1840
|
-
}
|
|
1841
1868
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
const scope = root || document;
|
|
1847
|
-
const targets = [];
|
|
1869
|
+
// If EOF while still in fence, mark until end of string.
|
|
1870
|
+
if (inFence) ranges.push([startLineStart, n]);
|
|
1871
|
+
return ranges;
|
|
1872
|
+
}
|
|
1848
1873
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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--;
|
|
1853
1884
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
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;
|
|
1859
1894
|
}
|
|
1895
|
+
if (indent > 3) return false;
|
|
1896
|
+
const rest = prefix.slice(i);
|
|
1860
1897
|
|
|
1861
|
-
//
|
|
1862
|
-
if (
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
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;
|
|
1866
1902
|
|
|
1867
|
-
//
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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 */ }
|
|
1873
1926
|
}
|
|
1874
|
-
|
|
1875
|
-
// Keep render path resilient
|
|
1927
|
+
return t;
|
|
1876
1928
|
}
|
|
1877
1929
|
}
|
|
1878
1930
|
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
}
|
|
1931
|
+
// ==========================================================================
|
|
1932
|
+
// 5) Markdown runtime (markdown-it + code wrapper + math placeholders)
|
|
1933
|
+
// ==========================================================================
|
|
1883
1934
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1935
|
+
class MarkdownRenderer {
|
|
1936
|
+
constructor(cfg, customMarkup, logger, asyncer, raf) {
|
|
1937
|
+
this.cfg = cfg; this.customMarkup = customMarkup; this.MD = null;
|
|
1938
|
+
this.logger = logger || new Logger(cfg);
|
|
1939
|
+
// Cooperative async utilities available in renderer for heavy decode/render paths
|
|
1940
|
+
this.asyncer = asyncer || new AsyncRunner(cfg, raf);
|
|
1941
|
+
this.raf = raf || null;
|
|
1942
|
+
|
|
1943
|
+
// Fast-path streaming renderer without linkify to reduce regex work on hot path.
|
|
1944
|
+
this.MD_STREAM = null;
|
|
1945
|
+
|
|
1946
|
+
this.hooks = {
|
|
1947
|
+
observeNewCode: () => {},
|
|
1948
|
+
observeMsgBoxes: () => {},
|
|
1949
|
+
scheduleMathRender: () => {},
|
|
1950
|
+
codeScrollInit: () => {}
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
// Initialize markdown-it instances and plugins.
|
|
1954
|
+
init() {
|
|
1955
|
+
if (!window.markdownit) { this.logger.log('[MD] markdown-it not found – rendering skipped.'); return; }
|
|
1956
|
+
// Full renderer (used for non-hot paths, final results)
|
|
1957
|
+
this.MD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
|
|
1958
|
+
// Streaming renderer (no linkify) – hot path
|
|
1959
|
+
this.MD_STREAM = window.markdownit({ html: false, linkify: false, breaks: true, highlight: () => '' });
|
|
1960
|
+
|
|
1961
|
+
// SAFETY: disable CommonMark "indented code blocks" unless explicitly enabled.
|
|
1962
|
+
if (!this.cfg.MD || this.cfg.MD.ALLOW_INDENTED_CODE !== true) {
|
|
1963
|
+
try { this.MD.block.ruler.disable('code'); } catch (_) {}
|
|
1964
|
+
try { this.MD_STREAM.block.ruler.disable('code'); } catch (_) {}
|
|
1965
|
+
}
|
|
1889
1966
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1967
|
+
const escapeHtml = Utils.escapeHtml;
|
|
1968
|
+
|
|
1969
|
+
// Dollar and bracket math placeholder plugins: generate lightweight placeholders to be picked up by KaTeX later.
|
|
1970
|
+
const mathDollarPlaceholderPlugin = (md) => {
|
|
1971
|
+
function notEscaped(src, pos) { let back = 0; while (pos - back - 1 >= 0 && src.charCodeAt(pos - back - 1) === 0x5C) back++; return (back % 2) === 0; }
|
|
1972
|
+
function math_block_dollar(state, startLine, endLine, silent) {
|
|
1973
|
+
const pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
1974
|
+
const max = state.eMarks[startLine];
|
|
1975
|
+
if (pos + 1 >= max) return false;
|
|
1976
|
+
if (state.src.charCodeAt(pos) !== 0x24 || state.src.charCodeAt(pos + 1) !== 0x24) return false;
|
|
1977
|
+
let nextLine = startLine + 1, found = false;
|
|
1978
|
+
for (; nextLine < endLine; nextLine++) {
|
|
1979
|
+
let p = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
1980
|
+
const pe = state.eMarks[nextLine];
|
|
1981
|
+
if (p + 1 < pe && state.src.charCodeAt(p) === 0x24 && state.src.charCodeAt(p + 1) === 0x24) { found = true; break; }
|
|
1982
|
+
}
|
|
1983
|
+
if (!found) return false;
|
|
1984
|
+
if (silent) return true;
|
|
1985
|
+
|
|
1986
|
+
const contentStart = state.bMarks[startLine] + state.tShift[startLine] + 2;
|
|
1987
|
+
const contentEndLine = nextLine - 1;
|
|
1988
|
+
let content = '';
|
|
1989
|
+
if (contentEndLine >= startLine + 1) {
|
|
1990
|
+
const startIdx = state.bMarks[startLine + 1];
|
|
1991
|
+
const endIdx = state.eMarks[contentEndLine];
|
|
1992
|
+
content = state.src.slice(startIdx, endIdx);
|
|
1993
|
+
} else content = '';
|
|
1994
|
+
|
|
1995
|
+
const token = state.push('math_block_dollar', '', 0);
|
|
1996
|
+
token.block = true; token.content = content; state.line = nextLine + 1; return true;
|
|
1997
|
+
}
|
|
1998
|
+
function math_inline_dollar(state, silent) {
|
|
1999
|
+
const pos = state.pos, src = state.src, max = state.posMax;
|
|
2000
|
+
if (pos >= max) return false;
|
|
2001
|
+
if (src.charCodeAt(pos) !== 0x24) return false;
|
|
2002
|
+
if (pos + 1 < max && src.charCodeAt(pos + 1) === 0x24) return false;
|
|
2003
|
+
const after = pos + 1 < max ? src.charCodeAt(pos + 1) : 0;
|
|
2004
|
+
if (after === 0x20 || after === 0x0A || after === 0x0D) return false;
|
|
2005
|
+
let i = pos + 1;
|
|
2006
|
+
while (i < max) {
|
|
2007
|
+
const ch = src.charCodeAt(i);
|
|
2008
|
+
if (ch === 0x24 && notEscaped(src, i)) {
|
|
2009
|
+
const before = i - 1 >= 0 ? src.charCodeAt(i - 1) : 0;
|
|
2010
|
+
if (before === 0x20 || before === 0x0A || before === 0x0D) { i++; continue; }
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
i++;
|
|
2014
|
+
}
|
|
2015
|
+
if (i >= max || src.charCodeAt(i) !== 0x24) return false;
|
|
1901
2016
|
|
|
1902
|
-
|
|
1903
|
-
|
|
2017
|
+
if (!silent) {
|
|
2018
|
+
const token = state.push('math_inline_dollar', '', 0);
|
|
2019
|
+
token.block = false; token.content = src.slice(pos + 1, i);
|
|
2020
|
+
}
|
|
2021
|
+
state.pos = i + 1; return true;
|
|
2022
|
+
}
|
|
1904
2023
|
|
|
1905
|
-
|
|
1906
|
-
|
|
2024
|
+
md.block.ruler.before('fence', 'math_block_dollar', math_block_dollar, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
2025
|
+
md.inline.ruler.before('escape', 'math_inline_dollar', math_inline_dollar);
|
|
1907
2026
|
|
|
1908
|
-
|
|
1909
|
-
|
|
2027
|
+
md.renderer.rules.math_inline_dollar = (tokens, idx) => {
|
|
2028
|
+
const tex = tokens[idx].content || '';
|
|
2029
|
+
return `<span class="math-pending" data-display="0"><span class="math-fallback">$${escapeHtml(tex)}$</span><script type="math/tex">${escapeHtml(tex)}</script></span>`;
|
|
2030
|
+
};
|
|
2031
|
+
md.renderer.rules.math_block_dollar = (tokens, idx) => {
|
|
2032
|
+
const tex = tokens[idx].content || '';
|
|
2033
|
+
return `<div class="math-pending" data-display="1"><div class="math-fallback">$$${escapeHtml(tex)}$$</div><script type="math/tex; mode=display">${escapeHtml(tex)}</script></div>`;
|
|
2034
|
+
};
|
|
2035
|
+
};
|
|
1910
2036
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
if (
|
|
1920
|
-
|
|
2037
|
+
const mathBracketsPlaceholderPlugin = (md) => {
|
|
2038
|
+
function math_brackets(state, silent) {
|
|
2039
|
+
const src = state.src, pos = state.pos, max = state.posMax;
|
|
2040
|
+
if (pos + 1 >= max || src.charCodeAt(pos) !== 0x5C) return false;
|
|
2041
|
+
const next = src.charCodeAt(pos + 1);
|
|
2042
|
+
if (next !== 0x28 && next !== 0x5B) return false;
|
|
2043
|
+
const isInline = (next === 0x28); const close = isInline ? '\\)' : '\\]';
|
|
2044
|
+
const start = pos + 2; const end = src.indexOf(close, start);
|
|
2045
|
+
if (end < 0) return false;
|
|
2046
|
+
const content = src.slice(start, end);
|
|
2047
|
+
if (!silent) {
|
|
2048
|
+
const t = state.push(isInline ? 'math_inline_bracket' : 'math_block_bracket', '', 0);
|
|
2049
|
+
t.content = content; t.block = !isInline;
|
|
1921
2050
|
}
|
|
2051
|
+
state.pos = end + 2; return true;
|
|
1922
2052
|
}
|
|
2053
|
+
md.inline.ruler.before('escape', 'math_brackets', math_brackets);
|
|
2054
|
+
md.renderer.rules.math_inline_bracket = (tokens, idx) => {
|
|
2055
|
+
const tex = tokens[idx].content || '';
|
|
2056
|
+
return `<span class="math-pending" data-display="0"><span class="math-fallback">\\(${escapeHtml(tex)}\\)</span><script type="math/tex">${escapeHtml(tex)}</script></span>`;
|
|
2057
|
+
};
|
|
2058
|
+
md.renderer.rules.math_block_bracket = (tokens, idx) => {
|
|
2059
|
+
const tex = tokens[idx].content || '';
|
|
2060
|
+
return `<div class="math-pending" data-display="1"><div class="math-fallback">\\[${escapeHtml(tex)}\\]</div><script type="math/tex; mode=display">${escapeHtml(tex)}</script></div>`;
|
|
2061
|
+
};
|
|
2062
|
+
};
|
|
1923
2063
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2064
|
+
this.MD.use(mathDollarPlaceholderPlugin);
|
|
2065
|
+
this.MD.use(mathBracketsPlaceholderPlugin);
|
|
2066
|
+
this.MD_STREAM.use(mathDollarPlaceholderPlugin);
|
|
2067
|
+
this.MD_STREAM.use(mathBracketsPlaceholderPlugin);
|
|
1927
2068
|
|
|
1928
|
-
|
|
2069
|
+
const cfg = this.cfg; const logger = this.logger;
|
|
1929
2070
|
|
|
1930
|
-
|
|
2071
|
+
// STREAMING wrapper plugin (modified header label guard)
|
|
2072
|
+
(function codeWrapperPlugin(md, logger) {
|
|
2073
|
+
let CODE_IDX = 1;
|
|
2074
|
+
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
2075
|
+
|
|
2076
|
+
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
2077
|
+
const seenFP = new Set();
|
|
2078
|
+
const makeFP = (info, raw) => {
|
|
2079
|
+
const head = (raw || '').slice(0, 96);
|
|
2080
|
+
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
2081
|
+
};
|
|
2082
|
+
|
|
2083
|
+
const ALIAS = {
|
|
2084
|
+
txt: 'plaintext', text: 'plaintext', plaintext: 'plaintext',
|
|
2085
|
+
sh: 'bash', shell: 'bash', zsh: 'bash', 'shell-session': 'bash',
|
|
2086
|
+
py: 'python', python3: 'python', py3: 'python',
|
|
2087
|
+
js: 'javascript', node: 'javascript', nodejs: 'javascript',
|
|
2088
|
+
ts: 'typescript', 'ts-node': 'typescript',
|
|
2089
|
+
yml: 'yaml', kt: 'kotlin', rs: 'rust',
|
|
2090
|
+
csharp: 'csharp', 'c#': 'csharp', 'c++': 'cpp',
|
|
2091
|
+
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
2092
|
+
docker: 'dockerfile'
|
|
2093
|
+
};
|
|
2094
|
+
function normLang(s) { if (!s) return ''; const v = String(s).trim().toLowerCase(); return ALIAS[v] || v; }
|
|
2095
|
+
function isSupportedByHLJS(lang) { try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; } }
|
|
2096
|
+
function classForHighlight(lang) { if (!lang) return 'plaintext'; return isSupportedByHLJS(lang) ? lang : 'plaintext'; }
|
|
2097
|
+
function stripBOM(s) { return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s; }
|
|
2098
|
+
|
|
2099
|
+
function detectFromFirstLine(raw, rid) {
|
|
2100
|
+
if (!raw) return { lang: '', content: raw, isOutput: false };
|
|
2101
|
+
const lines = raw.split(/\r?\n/);
|
|
2102
|
+
if (!lines.length) return { lang: '', content: raw, isOutput: false };
|
|
2103
|
+
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
2104
|
+
if (i >= lines.length) { log(`#${rid} first-line: only whitespace`); return { lang: '', content: raw, isOutput: false }; }
|
|
2105
|
+
let first = stripBOM(lines[i]).trim();
|
|
2106
|
+
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
2107
|
+
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
2108
|
+
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) { log(`#${rid} first-line: no token match`, { first }); return { lang: '', content: raw, isOutput: false }; }
|
|
2109
|
+
let cand = normLang(token);
|
|
2110
|
+
if (cand === 'output') {
|
|
2111
|
+
const content = lines.slice(i + 1).join('\n');
|
|
2112
|
+
log(`#${rid} first-line: output header`);
|
|
2113
|
+
return { lang: 'python', headerLabel: 'output', content, isOutput: true };
|
|
2114
|
+
}
|
|
2115
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
2116
|
+
if (!rest.trim()) { log(`#${rid} first-line: directive but no content after, ignore`, { cand }); return { lang: '', content: raw, isOutput: false }; }
|
|
2117
|
+
log(`#${rid} first-line: directive accepted`, { cand, restLen: rest.length, hljs: isSupportedByHLJS(cand) });
|
|
2118
|
+
return { lang: cand, headerLabel: cand, content: rest, isOutput: false };
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
md.renderer.rules.fence = (tokens, idx) => renderFence(tokens[idx]);
|
|
2122
|
+
md.renderer.rules.code_block = (tokens, idx) => renderFence({ info: '', content: tokens[idx].content || '' });
|
|
2123
|
+
|
|
2124
|
+
function resolveLanguageAndContent(info, raw, rid) {
|
|
2125
|
+
const infoLangRaw = (info || '').trim().split(/\s+/)[0] || '';
|
|
2126
|
+
let cand = normLang(infoLangRaw);
|
|
2127
|
+
if (cand === 'output') {
|
|
2128
|
+
log(`#${rid} info: output header`);
|
|
2129
|
+
return { lang: 'python', headerLabel: 'output', content: raw, isOutput: true };
|
|
2130
|
+
}
|
|
2131
|
+
if (cand) {
|
|
2132
|
+
log(`#${rid} info: token`, { infoLangRaw, cand, hljs: isSupportedByHLJS(cand) });
|
|
2133
|
+
return { lang: cand, headerLabel: cand, content: raw, isOutput: false };
|
|
2134
|
+
}
|
|
2135
|
+
const det = detectFromFirstLine(raw, rid);
|
|
2136
|
+
if (det && (det.lang || det.isOutput)) return det;
|
|
2137
|
+
log(`#${rid} resolve: fallback`);
|
|
2138
|
+
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function renderFence(token) {
|
|
2142
|
+
const raw = token.content || '';
|
|
2143
|
+
const rid = String(CODE_IDX + '');
|
|
2144
|
+
const fp = makeFP(token.info || '', raw);
|
|
2145
|
+
const canLog = !DEDUP || !seenFP.has(fp);
|
|
2146
|
+
if (canLog) log(`FENCE_ENTER #${rid}`, { info: (token.info || ''), rawHead: logger.pv(raw) });
|
|
2147
|
+
|
|
2148
|
+
const res = resolveLanguageAndContent(token.info || '', raw, rid);
|
|
2149
|
+
const isOutput = !!res.isOutput;
|
|
2150
|
+
|
|
2151
|
+
// Choose class and a safe header label (avoid 'on', 'ml', 's' etc.)
|
|
2152
|
+
const rawToken = (res.lang || '').trim();
|
|
2153
|
+
const langClass = isOutput ? 'python' : classForHighlight(rawToken);
|
|
2154
|
+
|
|
2155
|
+
// Guard against tiny unsupported tokens – show temporary 'code' instead of partial suffix.
|
|
2156
|
+
let headerLabel = isOutput ? 'output' : (res.headerLabel || (rawToken || 'code'));
|
|
2157
|
+
if (!isOutput) {
|
|
2158
|
+
if (rawToken && !isSupportedByHLJS(rawToken) && rawToken.length < 3) {
|
|
2159
|
+
headerLabel = 'code';
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
if (canLog) {
|
|
2164
|
+
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
2165
|
+
if (DEDUP) seenFP.add(fp);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// precompute code meta to avoid expensive .textContent on next phases
|
|
2169
|
+
const content = res.content || '';
|
|
2170
|
+
const len = content.length;
|
|
2171
|
+
const head = content.slice(0, 64);
|
|
2172
|
+
const tail = content.slice(-64);
|
|
2173
|
+
const headEsc = Utils.escapeHtml(head);
|
|
2174
|
+
const tailEsc = Utils.escapeHtml(tail);
|
|
2175
|
+
// Note: for full renderer we will also persist data-code-nl (see below).
|
|
2176
|
+
|
|
2177
|
+
const inner = Utils.escapeHtml(content);
|
|
2178
|
+
const idxLocal = CODE_IDX++;
|
|
2179
|
+
|
|
2180
|
+
let actions = '';
|
|
2181
|
+
if (langClass === 'html') {
|
|
2182
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-preview"><img src="${cfg.ICONS.CODE_PREVIEW}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.PREVIEW)}</span></a>`;
|
|
2183
|
+
} else if (langClass === 'python' && headerLabel !== 'output') {
|
|
2184
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-run"><img src="${cfg.ICONS.CODE_RUN}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.RUN)}</span></a>`;
|
|
2185
|
+
}
|
|
2186
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-collapse"><img src="${cfg.ICONS.CODE_MENU}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}</span></a>`;
|
|
2187
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-copy"><img src="${cfg.ICONS.CODE_COPY}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.COPY)}</span></a>`;
|
|
2188
|
+
|
|
2189
|
+
// attach precomputed meta (len/head/tail) on wrapper for downstream optimizations
|
|
2190
|
+
return (
|
|
2191
|
+
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
2192
|
+
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
2193
|
+
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}"` + // meta (no nl here – only in full renderer)
|
|
2194
|
+
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
2195
|
+
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
2196
|
+
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
2197
|
+
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
2198
|
+
`</div>`
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
})(this.MD_STREAM, this.logger);
|
|
2202
|
+
|
|
2203
|
+
// FULL renderer wrapper plugin (modified header label guard)
|
|
2204
|
+
(function codeWrapperPlugin(md, logger) {
|
|
2205
|
+
// identical core logic – augmented with data-code-nl for full renderer
|
|
2206
|
+
let CODE_IDX = 1;
|
|
2207
|
+
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
2208
|
+
|
|
2209
|
+
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
2210
|
+
const seenFP = new Set();
|
|
2211
|
+
const makeFP = (info, raw) => {
|
|
2212
|
+
const head = (raw || '').slice(0, 96);
|
|
2213
|
+
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
const ALIAS = {
|
|
2217
|
+
txt: 'plaintext', text: 'plaintext', plaintext: 'plaintext',
|
|
2218
|
+
sh: 'bash', shell: 'bash', zsh: 'bash', 'shell-session': 'bash',
|
|
2219
|
+
py: 'python', python3: 'python', py3: 'python',
|
|
2220
|
+
js: 'javascript', node: 'javascript', nodejs: 'javascript',
|
|
2221
|
+
ts: 'typescript', 'ts-node': 'typescript',
|
|
2222
|
+
yml: 'yaml', kt: 'kotlin', rs: 'rust',
|
|
2223
|
+
csharp: 'csharp', 'c#': 'csharp', 'c++': 'cpp',
|
|
2224
|
+
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
2225
|
+
docker: 'dockerfile'
|
|
2226
|
+
};
|
|
2227
|
+
function normLang(s) { if (!s) return ''; const v = String(s).trim().toLowerCase(); return ALIAS[v] || v; }
|
|
2228
|
+
function isSupportedByHLJS(lang) { try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; } }
|
|
2229
|
+
function classForHighlight(lang) { if (!lang) return 'plaintext'; return isSupportedByHLJS(lang) ? lang : 'plaintext'; }
|
|
2230
|
+
function stripBOM(s) { return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s; }
|
|
2231
|
+
|
|
2232
|
+
function detectFromFirstLine(raw, rid) {
|
|
2233
|
+
if (!raw) return { lang: '', content: raw, isOutput: false };
|
|
2234
|
+
const lines = raw.split(/\r?\n/);
|
|
2235
|
+
if (!lines.length) return { lang: '', content: raw, isOutput: false };
|
|
2236
|
+
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
2237
|
+
if (i >= lines.length) { log(`#${rid} first-line: only whitespace`); return { lang: '', content: raw, isOutput: false }; }
|
|
2238
|
+
let first = stripBOM(lines[i]).trim();
|
|
2239
|
+
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
2240
|
+
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
2241
|
+
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) { log(`#${rid} first-line: no token match`, { first }); return { lang: '', content: raw, isOutput: false }; }
|
|
2242
|
+
let cand = normLang(token);
|
|
2243
|
+
if (cand === 'output') {
|
|
2244
|
+
const content = lines.slice(i + 1).join('\n');
|
|
2245
|
+
log(`#${rid} first-line: output header`);
|
|
2246
|
+
return { lang: 'python', headerLabel: 'output', content, isOutput: true };
|
|
2247
|
+
}
|
|
2248
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
2249
|
+
if (!rest.trim()) { log(`#${rid} first-line: directive but no content after, ignore`, { cand }); return { lang: '', content: raw, isOutput: false }; }
|
|
2250
|
+
log(`#${rid} first-line: directive accepted`, { cand, restLen: rest.length, hljs: isSupportedByHLJS(cand) });
|
|
2251
|
+
return { lang: cand, headerLabel: cand, content: rest, isOutput: false };
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
md.renderer.rules.fence = (tokens, idx) => renderFence(tokens[idx]);
|
|
2255
|
+
md.renderer.rules.code_block = (tokens, idx) => renderFence({ info: '', content: tokens[idx].content || '' });
|
|
2256
|
+
|
|
2257
|
+
function resolveLanguageAndContent(info, raw, rid) {
|
|
2258
|
+
const infoLangRaw = (info || '').trim().split(/\s+/)[0] || '';
|
|
2259
|
+
let cand = normLang(infoLangRaw);
|
|
2260
|
+
if (cand === 'output') {
|
|
2261
|
+
log(`#${rid} info: output header`);
|
|
2262
|
+
return { lang: 'python', headerLabel: 'output', content: raw, isOutput: true };
|
|
2263
|
+
}
|
|
2264
|
+
if (cand) {
|
|
2265
|
+
log(`#${rid} info: token`, { infoLangRaw, cand, hljs: isSupportedByHLJS(cand) });
|
|
2266
|
+
return { lang: cand, headerLabel: cand, content: raw, isOutput: false };
|
|
2267
|
+
}
|
|
2268
|
+
const det = detectFromFirstLine(raw, rid);
|
|
2269
|
+
if (det && (det.lang || det.isOutput)) return det;
|
|
2270
|
+
log(`#${rid} resolve: fallback`);
|
|
2271
|
+
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function renderFence(token) {
|
|
2275
|
+
const raw = token.content || '';
|
|
2276
|
+
const rid = String(CODE_IDX + '');
|
|
2277
|
+
const fp = makeFP(token.info || '', raw);
|
|
2278
|
+
const canLog = !DEDUP || !seenFP.has(fp);
|
|
2279
|
+
if (canLog) log(`FENCE_ENTER #${rid}`, { info: (token.info || ''), rawHead: logger.pv(raw) });
|
|
2280
|
+
|
|
2281
|
+
const res = resolveLanguageAndContent(token.info || '', raw, rid);
|
|
2282
|
+
const isOutput = !!res.isOutput;
|
|
2283
|
+
|
|
2284
|
+
// Choose class and a safe header label (avoid 'on', 'ml', 's' etc.)
|
|
2285
|
+
const rawToken = (res.lang || '').trim();
|
|
2286
|
+
const langClass = isOutput ? 'python' : classForHighlight(rawToken);
|
|
2287
|
+
|
|
2288
|
+
// Guard against tiny unsupported tokens – show temporary 'code' instead of partial suffix.
|
|
2289
|
+
let headerLabel = isOutput ? 'output' : (res.headerLabel || (rawToken || 'code'));
|
|
2290
|
+
if (!isOutput) {
|
|
2291
|
+
if (rawToken && !isSupportedByHLJS(rawToken) && rawToken.length < 3) {
|
|
2292
|
+
headerLabel = 'code';
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
if (canLog) {
|
|
2297
|
+
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
2298
|
+
if (DEDUP) seenFP.add(fp);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
// precompute code meta
|
|
2302
|
+
const content = res.content || '';
|
|
2303
|
+
const len = content.length;
|
|
2304
|
+
const head = content.slice(0, 64);
|
|
2305
|
+
const tail = content.slice(-64);
|
|
2306
|
+
const headEsc = Utils.escapeHtml(head);
|
|
2307
|
+
const tailEsc = Utils.escapeHtml(tail);
|
|
2308
|
+
const nl = Utils.countNewlines(content);
|
|
2309
|
+
|
|
2310
|
+
const inner = Utils.escapeHtml(content);
|
|
2311
|
+
const idxLocal = CODE_IDX++;
|
|
2312
|
+
|
|
2313
|
+
let actions = '';
|
|
2314
|
+
if (langClass === 'html') {
|
|
2315
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-preview"><img src="${cfg.ICONS.CODE_PREVIEW}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.PREVIEW)}</span></a>`;
|
|
2316
|
+
} else if (langClass === 'python' && headerLabel !== 'output') {
|
|
2317
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-run"><img src="${cfg.ICONS.CODE_RUN}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.RUN)}</span></a>`;
|
|
2318
|
+
}
|
|
2319
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-collapse"><img src="${cfg.ICONS.CODE_MENU}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}</span></a>`;
|
|
2320
|
+
actions += `<a href="empty:${idxLocal}" class="code-header-action code-header-copy"><img src="${cfg.ICONS.CODE_COPY}" class="action-img" data-id="${idxLocal}"><span>${Utils.escapeHtml(cfg.LOCALE.COPY)}</span></a>`;
|
|
2321
|
+
|
|
2322
|
+
return (
|
|
2323
|
+
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
2324
|
+
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
2325
|
+
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}" data-code-nl="${String(nl)}"` +
|
|
2326
|
+
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
2327
|
+
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
2328
|
+
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
2329
|
+
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
2330
|
+
`</div>`
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
})(this.MD, this.logger);
|
|
2334
|
+
}
|
|
2335
|
+
// Replace "sandbox:" links with file:// in markdown source (host policy).
|
|
2336
|
+
preprocessMD(s) { return (s || '').replace(/\]\(sandbox:/g, '](file://'); }
|
|
2337
|
+
// Decode base64 UTF-8 to string (shared TextDecoder).
|
|
2338
|
+
b64ToUtf8(b64) {
|
|
2339
|
+
const bin = atob(b64);
|
|
2340
|
+
const bytes = new Uint8Array(bin.length);
|
|
2341
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
2342
|
+
return Utils.utf8Decode(bytes);
|
|
1931
2343
|
}
|
|
1932
2344
|
|
|
1933
|
-
//
|
|
1934
|
-
|
|
2345
|
+
// Apply custom markup for bot messages only (method name kept for API).
|
|
2346
|
+
applyCustomMarkupForBots(root) {
|
|
2347
|
+
const MD = this.MD;
|
|
2348
|
+
try {
|
|
2349
|
+
const scope = root || document;
|
|
2350
|
+
const targets = [];
|
|
1935
2351
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2352
|
+
// If scope itself is a bot message box
|
|
2353
|
+
if (scope && scope.nodeType === 1 && scope.classList && scope.classList.contains('msg-box') &&
|
|
2354
|
+
scope.classList.contains('msg-bot')) {
|
|
2355
|
+
targets.push(scope);
|
|
2356
|
+
}
|
|
1940
2357
|
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2358
|
+
// Collect bot message boxes within the scope
|
|
2359
|
+
if (scope && typeof scope.querySelectorAll === 'function') {
|
|
2360
|
+
const list = scope.querySelectorAll('.msg-box.msg-bot');
|
|
2361
|
+
for (let i = 0; i < list.length; i++) targets.push(list[i]);
|
|
2362
|
+
}
|
|
1944
2363
|
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2364
|
+
// If scope is inside a bot message, include the closest ancestor as well
|
|
2365
|
+
if (scope && scope.nodeType === 1 && typeof scope.closest === 'function') {
|
|
2366
|
+
const closestMsg = scope.closest('.msg-box.msg-bot');
|
|
2367
|
+
if (closestMsg) targets.push(closestMsg);
|
|
2368
|
+
}
|
|
1950
2369
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2370
|
+
// Deduplicate and apply rules only to bot messages
|
|
2371
|
+
const seen = new Set();
|
|
2372
|
+
for (const el of targets) {
|
|
2373
|
+
if (!el || !el.isConnected || seen.has(el)) continue;
|
|
2374
|
+
seen.add(el);
|
|
2375
|
+
this.customMarkup.apply(el, MD);
|
|
2376
|
+
}
|
|
2377
|
+
} catch (_) {
|
|
2378
|
+
// Keep render path resilient
|
|
1960
2379
|
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// Helper: choose renderer (hot vs full) for snapshot use.
|
|
2383
|
+
_md(streamingHint) {
|
|
2384
|
+
return streamingHint ? (this.MD_STREAM || this.MD) : (this.MD || this.MD_STREAM);
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// Async, batched processing of [data-md64] / [md-block-markdown] to keep UI responsive on heavy loads.
|
|
2388
|
+
// Note: user messages are rendered as plain text (no markdown-it, no custom markup, no KaTeX).
|
|
2389
|
+
async renderPendingMarkdown(root) {
|
|
2390
|
+
const MD = this.MD; if (!MD) return;
|
|
2391
|
+
const scope = root || document;
|
|
1961
2392
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
// Intentionally do NOT add to touchedBoxes; no Custom Markup for user.
|
|
1968
|
-
} else if (isBotMsg) {
|
|
1969
|
-
// Bot message: full markdown-it render with Custom Markup.
|
|
1970
|
-
let html = '';
|
|
1971
|
-
try { html = MD.render(md); } catch (_) { html = Utils.escapeHtml(md); }
|
|
1972
|
-
|
|
1973
|
-
// build fragment directly (avoid intermediate container allocations).
|
|
1974
|
-
let frag = null;
|
|
2393
|
+
// Collect both legacy base64 holders and new native Markdown holders
|
|
2394
|
+
const nodes = Array.from(scope.querySelectorAll('[data-md64], [md-block-markdown]'));
|
|
2395
|
+
if (nodes.length === 0) {
|
|
2396
|
+
// Nothing to materialize right now. Avoid arming rAF work unless there is
|
|
2397
|
+
// actually something present that needs highlight/scroll/math.
|
|
1975
2398
|
try {
|
|
1976
|
-
const
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
const tmp = document.createElement('div');
|
|
1982
|
-
tmp.innerHTML = html;
|
|
1983
|
-
frag = document.createDocumentFragment();
|
|
1984
|
-
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
1985
|
-
}
|
|
2399
|
+
const hasBots = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot'));
|
|
2400
|
+
const hasWrappers = !!(scope && scope.querySelector && scope.querySelector('.code-wrapper'));
|
|
2401
|
+
const hasCodes = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot pre code'));
|
|
2402
|
+
const hasUnhighlighted = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot pre code:not([data-highlighted="yes"])'));
|
|
2403
|
+
const hasMath = !!(scope && scope.querySelector && scope.querySelector('script[type^="math/tex"]'));
|
|
1986
2404
|
|
|
1987
|
-
|
|
1988
|
-
|
|
2405
|
+
// Apply Custom Markup only if bot messages are present.
|
|
2406
|
+
if (hasBots) { this.applyCustomMarkupForBots(scope); }
|
|
1989
2407
|
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2408
|
+
// Restore collapsed state only if we can actually find wrappers.
|
|
2409
|
+
if (hasWrappers) { this.restoreCollapsedCode(scope); }
|
|
2410
|
+
|
|
2411
|
+
// Initialize code scroll helpers for current root.
|
|
2412
|
+
this.hooks.codeScrollInit(scope);
|
|
2413
|
+
|
|
2414
|
+
// Init code-scroll/highlight observers only when there are codes in DOM.
|
|
2415
|
+
if (hasCodes) {
|
|
2416
|
+
this.hooks.observeMsgBoxes(scope);
|
|
2417
|
+
this.hooks.observeNewCode(scope, {
|
|
2418
|
+
deferLastIfStreaming: true,
|
|
2419
|
+
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
2420
|
+
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
2421
|
+
});
|
|
2422
|
+
if (hasUnhighlighted && typeof runtime !== 'undefined' && runtime.highlighter) {
|
|
2423
|
+
runtime.highlighter.scanVisibleCodesInRoot(scope, runtime.stream.activeCode || null);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// Schedule KaTeX render only if there are math scripts present.
|
|
2428
|
+
if (hasMath) { this.hooks.scheduleMathRender(scope); }
|
|
2429
|
+
this.hooks.codeScrollInit(scope);
|
|
2430
|
+
|
|
2431
|
+
} catch (_) { /* swallow: keep idle path safe */ }
|
|
2432
|
+
|
|
2433
|
+
return;
|
|
1997
2434
|
}
|
|
1998
2435
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2436
|
+
// Track which bot message boxes actually changed to avoid a heavy global Custom Markup pass.
|
|
2437
|
+
const touchedBoxes = new Set();
|
|
2438
|
+
|
|
2439
|
+
// Budgeted, cooperative loop: process nodes one-by-one with per-frame yield when needed.
|
|
2440
|
+
const perSlice = (this.cfg.ASYNC && this.cfg.ASYNC.MD_NODES_PER_SLICE) || 12; // upper bound per frame
|
|
2441
|
+
let sliceCount = 0;
|
|
2442
|
+
let startedAt = Utils.now();
|
|
2443
|
+
|
|
2444
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
2445
|
+
const el = nodes[j];
|
|
2446
|
+
if (!el || !el.isConnected) continue;
|
|
2447
|
+
|
|
2448
|
+
let md = '';
|
|
2449
|
+
const isNative = el.hasAttribute('md-block-markdown');
|
|
2450
|
+
const msgBox = (el.closest && el.closest('.msg-box.msg-bot, .msg-box.msg-user')) || null;
|
|
2451
|
+
const isUserMsg = !!(msgBox && msgBox.classList.contains('msg-user'));
|
|
2452
|
+
const isBotMsg = !!(msgBox && msgBox.classList.contains('msg-bot'));
|
|
2453
|
+
|
|
2454
|
+
// Read source text (do not preprocess for user messages to keep it raw)
|
|
2455
|
+
if (isNative) {
|
|
2456
|
+
try { md = isUserMsg ? (el.textContent || '') : this.preprocessMD(el.textContent || ''); } catch (_) { md = ''; }
|
|
2457
|
+
try { el.removeAttribute('md-block-markdown'); } catch (_) {}
|
|
2458
|
+
} else {
|
|
2459
|
+
const b64 = el.getAttribute('data-md64'); if (!b64) continue;
|
|
2460
|
+
try { md = this.b64ToUtf8(b64); } catch (_) { md = ''; }
|
|
2461
|
+
el.removeAttribute('data-md64');
|
|
2462
|
+
if (!isUserMsg) { try { md = this.preprocessMD(md); } catch (_) {} }
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (isUserMsg) {
|
|
2466
|
+
// User message: replace placeholder with raw plain text only.
|
|
2467
|
+
const span = document.createElement('span');
|
|
2468
|
+
span.textContent = md;
|
|
2469
|
+
el.replaceWith(span);
|
|
2470
|
+
// Intentionally do NOT add to touchedBoxes; no Custom Markup for user.
|
|
2471
|
+
} else if (isBotMsg) {
|
|
2472
|
+
// Bot message: full markdown-it render with Custom Markup.
|
|
2473
|
+
let html = '';
|
|
2474
|
+
try {
|
|
2475
|
+
let src = md;
|
|
2476
|
+
// Pre-md transforms for source-phase rules
|
|
2477
|
+
if (this.customMarkup && typeof this.customMarkup.transformSource === 'function') {
|
|
2478
|
+
src = this.customMarkup.transformSource(src, { streaming: false });
|
|
2479
|
+
}
|
|
2480
|
+
html = MD.render(src);
|
|
2481
|
+
} catch (_) { html = Utils.escapeHtml(md); }
|
|
2482
|
+
|
|
2483
|
+
// build fragment directly (avoid intermediate container allocations).
|
|
2484
|
+
let frag = null;
|
|
2485
|
+
try {
|
|
2486
|
+
const range = document.createRange();
|
|
2487
|
+
const ctx = el.parentNode || document.body || document.documentElement;
|
|
2488
|
+
range.selectNode(ctx);
|
|
2489
|
+
frag = range.createContextualFragment(html);
|
|
2490
|
+
} catch (_) {
|
|
2491
|
+
const tmp = document.createElement('div');
|
|
2492
|
+
tmp.innerHTML = html;
|
|
2493
|
+
frag = document.createDocumentFragment();
|
|
2494
|
+
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// Apply Custom Markup on a lightweight DocumentFragment
|
|
2498
|
+
try { this.customMarkup.apply(frag, MD); } catch (_) {}
|
|
2499
|
+
|
|
2500
|
+
el.replaceWith(frag);
|
|
2501
|
+
touchedBoxes.add(msgBox);
|
|
2502
|
+
} else {
|
|
2503
|
+
// Outside of any message box: materialize as plain text.
|
|
2504
|
+
const span = document.createElement('span');
|
|
2505
|
+
span.textContent = md;
|
|
2506
|
+
el.replaceWith(span);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
sliceCount++;
|
|
2510
|
+
// Yield by time budget or by count to keep frame short and reactive.
|
|
2511
|
+
if (sliceCount >= perSlice || this.asyncer.shouldYield(startedAt)) {
|
|
2512
|
+
await this.asyncer.yield();
|
|
2513
|
+
startedAt = Utils.now();
|
|
2514
|
+
sliceCount = 0;
|
|
2515
|
+
}
|
|
2005
2516
|
}
|
|
2006
|
-
}
|
|
2007
2517
|
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2518
|
+
// Apply Custom Markup only to actually modified BOT messages (keeps this pass light).
|
|
2519
|
+
try {
|
|
2520
|
+
touchedBoxes.forEach(box => { try { this.customMarkup.apply(box, MD); } catch (_) {} });
|
|
2521
|
+
} catch (_) {}
|
|
2012
2522
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2523
|
+
// Same post-processing as before (idempotent with external calls).
|
|
2524
|
+
this.restoreCollapsedCode(scope);
|
|
2525
|
+
this.hooks.observeNewCode(scope, {
|
|
2526
|
+
deferLastIfStreaming: true,
|
|
2527
|
+
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
2528
|
+
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
2529
|
+
});
|
|
2530
|
+
this.hooks.observeMsgBoxes(scope);
|
|
2531
|
+
this.hooks.scheduleMathRender(scope);
|
|
2532
|
+
this.hooks.codeScrollInit(scope);
|
|
2023
2533
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2534
|
+
if (typeof runtime !== 'undefined' && runtime.highlighter) {
|
|
2535
|
+
runtime.highlighter.scanVisibleCodesInRoot(scope, runtime.stream.activeCode || null);
|
|
2536
|
+
}
|
|
2026
2537
|
}
|
|
2027
|
-
}
|
|
2028
2538
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2539
|
+
// Render streaming snapshot.
|
|
2540
|
+
renderStreamingSnapshot(src) {
|
|
2541
|
+
const md = this._md(true);
|
|
2542
|
+
if (!md) return '';
|
|
2543
|
+
try {
|
|
2544
|
+
let s = String(src || '');
|
|
2545
|
+
// Pre-markdown custom transforms (e.g. [!exec]/<execute> => ```python fences)
|
|
2546
|
+
if (this.customMarkup && typeof this.customMarkup.transformSource === 'function') {
|
|
2547
|
+
s = this.customMarkup.transformSource(s, { streaming: true });
|
|
2548
|
+
}
|
|
2549
|
+
return md.render(s);
|
|
2550
|
+
} catch (_) { return Utils.escapeHtml(src); }
|
|
2551
|
+
}
|
|
2041
2552
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2553
|
+
renderFinalSnapshot(src) {
|
|
2554
|
+
const md = this._md(false);
|
|
2555
|
+
if (!md) return '';
|
|
2556
|
+
try {
|
|
2557
|
+
let s = String(src || '');
|
|
2558
|
+
if (this.customMarkup && typeof this.customMarkup.transformSource === 'function') {
|
|
2559
|
+
s = this.customMarkup.transformSource(s, { streaming: false });
|
|
2560
|
+
}
|
|
2561
|
+
return md.render(s);
|
|
2562
|
+
} catch (_) { return Utils.escapeHtml(src); }
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// Restore collapse/expand state of code blocks after DOM updates.
|
|
2566
|
+
restoreCollapsedCode(root) {
|
|
2567
|
+
const scope = root || document;
|
|
2568
|
+
const wrappers = scope.querySelectorAll('.code-wrapper');
|
|
2569
|
+
wrappers.forEach((wrapper) => {
|
|
2570
|
+
const index = wrapper.getAttribute('data-index');
|
|
2571
|
+
const localeCollapse = wrapper.getAttribute('data-locale-collapse');
|
|
2572
|
+
const localeExpand = wrapper.getAttribute('data-locale-expand');
|
|
2573
|
+
const source = wrapper.querySelector('code');
|
|
2574
|
+
const isCollapsed = (window.__collapsed_idx || []).includes(index);
|
|
2575
|
+
if (!source) return;
|
|
2576
|
+
const btn = wrapper.querySelector('.code-header-collapse');
|
|
2577
|
+
if (isCollapsed) {
|
|
2578
|
+
source.style.display = 'none';
|
|
2579
|
+
if (btn) { const span = btn.querySelector('span'); if (span) span.textContent = localeExpand; }
|
|
2580
|
+
} else {
|
|
2581
|
+
source.style.display = 'block';
|
|
2582
|
+
if (btn) { const span = btn.querySelector('span'); if (span) span.textContent = localeCollapse; }
|
|
2583
|
+
}
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2062
2586
|
}
|
|
2063
|
-
}
|
|
2064
2587
|
window.__collapsed_idx = window.__collapsed_idx || [];
|
|
2065
2588
|
|
|
2066
2589
|
// ==========================================================================
|
|
@@ -2596,6 +3119,11 @@
|
|
|
2596
3119
|
img.className = 'uc-toggle-icon';
|
|
2597
3120
|
img.alt = labels.expand;
|
|
2598
3121
|
img.src = icons.expand;
|
|
3122
|
+
|
|
3123
|
+
// Provide a sane default size even if CSS did not load yet (CSS will override when present).
|
|
3124
|
+
img.width = 26; // keep in sync with CSS fallback var(--uc-toggle-icon-size, 26px)
|
|
3125
|
+
img.height = 26; // ensures a consistent, non-tiny control from the first paint
|
|
3126
|
+
|
|
2599
3127
|
toggle.appendChild(img);
|
|
2600
3128
|
|
|
2601
3129
|
// Attach local listeners (no global handler change; production-safe).
|
|
@@ -2863,14 +3391,32 @@
|
|
|
2863
3391
|
} catch (_) {}
|
|
2864
3392
|
}
|
|
2865
3393
|
|
|
2866
|
-
// Append HTML into message input container.
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3394
|
+
// Append HTML/text into the message input container.
|
|
3395
|
+
// If plain text is provided, wrap it into a minimal msg-user box to keep layout consistent.
|
|
3396
|
+
appendToInput(content) {
|
|
3397
|
+
const el = this.dom.get('_append_input_'); if (!el) return;
|
|
3398
|
+
|
|
3399
|
+
let html = String(content || '');
|
|
3400
|
+
const trimmed = html.trim();
|
|
3401
|
+
|
|
3402
|
+
// If already a full msg-user wrapper, append as-is; otherwise wrap the plain text.
|
|
3403
|
+
const isWrapped = (trimmed.startsWith('<div') && /class=["']msg-box msg-user["']/.test(trimmed));
|
|
3404
|
+
if (!isWrapped) {
|
|
3405
|
+
// Treat incoming payload as plain text (escape + convert newlines to <br>).
|
|
3406
|
+
const safe = (typeof Utils !== 'undefined' && Utils.escapeHtml)
|
|
3407
|
+
? Utils.escapeHtml(html)
|
|
3408
|
+
: String(html).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
3409
|
+
const body = safe.replace(/\r?\n/g, '<br>');
|
|
3410
|
+
// Minimal, margin-less user message (no empty msg-extra to avoid extra spacing).
|
|
3411
|
+
html = `<div class="msg-box msg-user"><div class="msg"><p style="margin:0">${body}</p></div></div>`;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
// Synchronous DOM update.
|
|
3415
|
+
el.insertAdjacentHTML('beforeend', html);
|
|
3416
|
+
|
|
3417
|
+
// Apply collapse to any user messages in input area (now or later).
|
|
3418
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
3419
|
+
}
|
|
2874
3420
|
|
|
2875
3421
|
// Append nodes into messages list and perform post-processing (markdown, code, math).
|
|
2876
3422
|
appendNode(content, scrollMgr) {
|
|
@@ -3050,6 +3596,333 @@
|
|
|
3050
3596
|
}
|
|
3051
3597
|
}
|
|
3052
3598
|
|
|
3599
|
+
// ==========================================================================
|
|
3600
|
+
// 9a) Template engine for JSON nodes
|
|
3601
|
+
// ==========================================================================
|
|
3602
|
+
|
|
3603
|
+
class NodeTemplateEngine {
|
|
3604
|
+
// JS-side templates for nodes rendered from JSON payload (RenderBlock).
|
|
3605
|
+
constructor(cfg, logger) {
|
|
3606
|
+
this.cfg = cfg || {};
|
|
3607
|
+
this.logger = logger || { debug: () => {} };
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
_esc(s) { return (s == null) ? '' : String(s); }
|
|
3611
|
+
_escapeHtml(s) { return (typeof Utils !== 'undefined') ? Utils.escapeHtml(s) : String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); }
|
|
3612
|
+
|
|
3613
|
+
// Render name header given role
|
|
3614
|
+
_nameHeader(role, name, avatarUrl) {
|
|
3615
|
+
if (!name && !avatarUrl) return '';
|
|
3616
|
+
const cls = (role === 'user') ? 'name-user' : 'name-bot';
|
|
3617
|
+
const img = avatarUrl ? `<img src="${this._esc(avatarUrl)}" class="avatar"> ` : '';
|
|
3618
|
+
return `<div class="name-header ${cls}">${img}${this._esc(name || '')}</div>`;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// Render user message block
|
|
3622
|
+
_renderUser(block) {
|
|
3623
|
+
const id = block.id;
|
|
3624
|
+
const inp = block.input || {};
|
|
3625
|
+
const msgId = `msg-user-${id}`;
|
|
3626
|
+
|
|
3627
|
+
// NOTE: timestamps intentionally disabled on frontend
|
|
3628
|
+
// let ts = '';
|
|
3629
|
+
// if (inp.timestamp) { ... }
|
|
3630
|
+
|
|
3631
|
+
const personalize = !!(block && block.extra && block.extra.personalize === true);
|
|
3632
|
+
const nameHeader = personalize ? this._nameHeader('user', inp.name || '', inp.avatar_img || null) : '';
|
|
3633
|
+
|
|
3634
|
+
const content = this._escapeHtml(inp.text || '').replace(/\r?\n/g, '<br>');
|
|
3635
|
+
return `<div class="msg-box msg-user" id="${msgId}">${nameHeader}<div class="msg"><p style="margin:0">${content}</p></div></div>`;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// Render extra blocks (images/files/urls/docs/tool-extra)
|
|
3639
|
+
_renderExtras(block) {
|
|
3640
|
+
const parts = [];
|
|
3641
|
+
|
|
3642
|
+
// images
|
|
3643
|
+
const images = block.images || {};
|
|
3644
|
+
const keysI = Object.keys(images);
|
|
3645
|
+
if (keysI.length) {
|
|
3646
|
+
keysI.forEach((k) => {
|
|
3647
|
+
const it = images[k];
|
|
3648
|
+
if (!it) return;
|
|
3649
|
+
const url = this._esc(it.url); const path = this._esc(it.path); const bn = this._esc(it.basename || '');
|
|
3650
|
+
if (it.is_video) {
|
|
3651
|
+
const src = (it.ext === '.webm' || !it.webm_path) ? path : this._esc(it.webm_path);
|
|
3652
|
+
const ext = (src.endsWith('.webm') ? 'webm' : (path.split('.').pop() || 'mp4'));
|
|
3653
|
+
parts.push(
|
|
3654
|
+
`<div class="extra-src-video-box" title="${url}">` +
|
|
3655
|
+
`<video class="video-player" controls>` +
|
|
3656
|
+
`<source src="${src}" type="video/${ext}">` +
|
|
3657
|
+
`</video>` +
|
|
3658
|
+
`<p><a href="bridge://play_video/${url}" class="title">${this._escapeHtml(bn)}</a></p>` +
|
|
3659
|
+
`</div>`
|
|
3660
|
+
);
|
|
3661
|
+
} else {
|
|
3662
|
+
parts.push(
|
|
3663
|
+
`<div class="extra-src-img-box" title="${url}">` +
|
|
3664
|
+
`<div class="img-outer"><div class="img-wrapper"><a href="${url}"><img src="${path}" class="image"></a></div>` +
|
|
3665
|
+
`<a href="${url}" class="title">${this._escapeHtml(bn)}</a></div>` +
|
|
3666
|
+
`</div><br/>`
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// files
|
|
3673
|
+
const files = block.files || {};
|
|
3674
|
+
const kF = Object.keys(files);
|
|
3675
|
+
if (kF.length) {
|
|
3676
|
+
const rows = [];
|
|
3677
|
+
kF.forEach((k) => {
|
|
3678
|
+
const it = files[k]; if (!it) return;
|
|
3679
|
+
const url = this._esc(it.url); const path = this._esc(it.path);
|
|
3680
|
+
const icon = (typeof window !== 'undefined' && window.ICON_ATTACHMENTS) ? `<img src="${window.ICON_ATTACHMENTS}" class="extra-src-icon">` : '';
|
|
3681
|
+
rows.push(`${icon} <b> [${k}] </b> <a href="${url}">${path}</a>`);
|
|
3682
|
+
});
|
|
3683
|
+
if (rows.length) parts.push(`<div>${rows.join("<br/><br/>")}</div>`);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
// urls
|
|
3687
|
+
const urls = block.urls || {};
|
|
3688
|
+
const kU = Object.keys(urls);
|
|
3689
|
+
if (kU.length) {
|
|
3690
|
+
const rows = [];
|
|
3691
|
+
kU.forEach((k) => {
|
|
3692
|
+
const it = urls[k]; if (!it) return;
|
|
3693
|
+
const url = this._esc(it.url);
|
|
3694
|
+
const icon = (typeof window !== 'undefined' && window.ICON_URL) ? `<img src="${window.ICON_URL}" class="extra-src-icon">` : '';
|
|
3695
|
+
rows.push(`${icon}<a href="${url}" title="${url}">${url}</a> <small> [${k}] </small>`);
|
|
3696
|
+
});
|
|
3697
|
+
if (rows.length) parts.push(`<div>${rows.join("<br/><br/>")}</div>`);
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
// docs (render on JS) or fallback to docs_html
|
|
3701
|
+
const extra = block.extra || {};
|
|
3702
|
+
const docsRaw = Array.isArray(extra.docs) ? extra.docs : null;
|
|
3703
|
+
|
|
3704
|
+
if (docsRaw && docsRaw.length) {
|
|
3705
|
+
const icon = (typeof window !== 'undefined' && window.ICON_DB) ? `<img src="${window.ICON_DB}" class="extra-src-icon">` : '';
|
|
3706
|
+
const prefix = (typeof window !== 'undefined' && window.LOCALE_DOC_PREFIX) ? String(window.LOCALE_DOC_PREFIX) : 'Doc:';
|
|
3707
|
+
const limit = 3;
|
|
3708
|
+
|
|
3709
|
+
// normalize: [{uuid, meta}] OR [{ uuid: {...} }]
|
|
3710
|
+
const normalized = [];
|
|
3711
|
+
docsRaw.forEach((it) => {
|
|
3712
|
+
if (!it || typeof it !== 'object') return;
|
|
3713
|
+
if ('uuid' in it && 'meta' in it && typeof it.meta === 'object') {
|
|
3714
|
+
normalized.push({ uuid: String(it.uuid), meta: it.meta || {} });
|
|
3715
|
+
} else {
|
|
3716
|
+
const keys = Object.keys(it);
|
|
3717
|
+
if (keys.length === 1) {
|
|
3718
|
+
const uuid = keys[0];
|
|
3719
|
+
const meta = it[uuid];
|
|
3720
|
+
if (meta && typeof meta === 'object') {
|
|
3721
|
+
normalized.push({ uuid: String(uuid), meta });
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
});
|
|
3726
|
+
|
|
3727
|
+
const rows = [];
|
|
3728
|
+
for (let i = 0; i < Math.min(limit, normalized.length); i++) {
|
|
3729
|
+
const d = normalized[i];
|
|
3730
|
+
const meta = d.meta || {};
|
|
3731
|
+
const entries = Object.keys(meta).map(k => `<b>${this._escapeHtml(k)}:</b> ${this._escapeHtml(String(meta[k]))}`).join(', ');
|
|
3732
|
+
rows.push(`<p><small>[${i + 1}] ${this._escapeHtml(d.uuid)}: ${entries}</small></p>`);
|
|
3733
|
+
}
|
|
3734
|
+
if (rows.length) {
|
|
3735
|
+
parts.push(`<p>${icon}<small><b>${this._escapeHtml(prefix)}:</b></small></p>`);
|
|
3736
|
+
parts.push(`<div class="cmd"><p>${rows.join('')}</p></div>`);
|
|
3737
|
+
}
|
|
3738
|
+
} else {
|
|
3739
|
+
// backward compat
|
|
3740
|
+
const docs_html = extra && extra.docs_html ? String(extra.docs_html) : '';
|
|
3741
|
+
if (docs_html) parts.push(docs_html);
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
// plugin-driven tool extra HTML
|
|
3745
|
+
const tool_extra_html = extra && extra.tool_extra_html ? String(extra.tool_extra_html) : '';
|
|
3746
|
+
if (tool_extra_html) parts.push(`<div class="msg-extra">${tool_extra_html}</div>`);
|
|
3747
|
+
|
|
3748
|
+
return parts.join('');
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
// Render message-level actions
|
|
3752
|
+
_renderActions(block) {
|
|
3753
|
+
const extra = block.extra || {};
|
|
3754
|
+
const actions = extra.actions || [];
|
|
3755
|
+
if (!actions || !actions.length) return '';
|
|
3756
|
+
const parts = actions.map((a) => {
|
|
3757
|
+
const href = this._esc(a.href || '#');
|
|
3758
|
+
const title = this._esc(a.title || '');
|
|
3759
|
+
const icon = this._esc(a.icon || '');
|
|
3760
|
+
const id = this._esc(a.id || block.id);
|
|
3761
|
+
return `<a href="${href}" class="action-icon" data-id="${id}" role="button"><span class="cmd"><img src="${icon}" class="action-img" title="${title}" alt="${title}" data-id="${id}"></span></a>`;
|
|
3762
|
+
});
|
|
3763
|
+
return `<div class="action-icons" data-id="${this._esc(block.id)}">${parts.join('')}</div>`;
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
// Render tool output wrapper (always collapsed by default; wrapper visibility depends on flag)
|
|
3767
|
+
// Inside class NodeTemplateEngine
|
|
3768
|
+
_renderToolOutputWrapper(block) {
|
|
3769
|
+
const extra = block.extra || {};
|
|
3770
|
+
|
|
3771
|
+
// IMPORTANT: keep initial tool output verbatim (HTML-ready).
|
|
3772
|
+
// Do NOT HTML-escape here – the host already provides a safe/HTML-ready string.
|
|
3773
|
+
// Escaping again would double-encode entities (e.g. " -> "), which
|
|
3774
|
+
// caused visible """ in the UI instead of quotes.
|
|
3775
|
+
const tool_output_html = (extra.tool_output != null) ? String(extra.tool_output) : '';
|
|
3776
|
+
|
|
3777
|
+
// Wrapper visibility: show/hide based on tool_output_visible...
|
|
3778
|
+
const wrapperDisplay = (extra.tool_output_visible === true) ? '' : 'display:none';
|
|
3779
|
+
|
|
3780
|
+
const toggleTitle = (typeof trans !== 'undefined' && trans) ? trans('action.cmd.expand') : 'Expand';
|
|
3781
|
+
const expIcon = (typeof window !== 'undefined' && window.ICON_EXPAND) ? window.ICON_EXPAND : '';
|
|
3782
|
+
|
|
3783
|
+
return (
|
|
3784
|
+
`<div class='tool-output' style='${wrapperDisplay}'>` +
|
|
3785
|
+
`<span class='toggle-cmd-output' onclick='toggleToolOutput(${this._esc(block.id)});' ` +
|
|
3786
|
+
`title='${this._escapeHtml(toggleTitle)}' role='button'>` +
|
|
3787
|
+
`<img src='${this._esc(expIcon)}' width='25' height='25' valign='middle'>` +
|
|
3788
|
+
`</span>` +
|
|
3789
|
+
// Content is initially collapsed. We intentionally do NOT escape here,
|
|
3790
|
+
// to keep behavior consistent with ToolOutput.append/update (HTML-in).
|
|
3791
|
+
`<div class='content' style='display:none' data-trusted='1'>${tool_output_html}</div>` +
|
|
3792
|
+
`</div>`
|
|
3793
|
+
);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
// Render bot message block (md-block-markdown)
|
|
3797
|
+
_renderBot(block) {
|
|
3798
|
+
const id = block.id;
|
|
3799
|
+
const out = block.output || {};
|
|
3800
|
+
const msgId = `msg-bot-${id}`;
|
|
3801
|
+
|
|
3802
|
+
// NOTE: timestamps intentionally disabled on frontend
|
|
3803
|
+
// let ts = '';
|
|
3804
|
+
// if (out.timestamp) { ... }
|
|
3805
|
+
|
|
3806
|
+
const personalize = !!(block && block.extra && block.extra.personalize === true);
|
|
3807
|
+
const nameHeader = personalize ? this._nameHeader('bot', out.name || '', out.avatar_img || null) : '';
|
|
3808
|
+
|
|
3809
|
+
const mdText = this._escapeHtml(out.text || '');
|
|
3810
|
+
const toolWrap = this._renderToolOutputWrapper(block);
|
|
3811
|
+
const extras = this._renderExtras(block);
|
|
3812
|
+
const actions = (block.extra && block.extra.footer_icons) ? this._renderActions(block) : '';
|
|
3813
|
+
const debug = (block.extra && block.extra.debug_html) ? String(block.extra.debug_html) : '';
|
|
3814
|
+
|
|
3815
|
+
return (
|
|
3816
|
+
`<div class='msg-box msg-bot' id='${msgId}'>` +
|
|
3817
|
+
`${nameHeader}` +
|
|
3818
|
+
`<div class='msg'>` +
|
|
3819
|
+
`<div class='md-block' md-block-markdown='1'>${mdText}</div>` +
|
|
3820
|
+
`<div class='msg-tool-extra'></div>` +
|
|
3821
|
+
`${toolWrap}` +
|
|
3822
|
+
`<div class='msg-extra'>${extras}</div>` +
|
|
3823
|
+
`${actions}${debug}` +
|
|
3824
|
+
`</div>` +
|
|
3825
|
+
`</div>`
|
|
3826
|
+
);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// Render one RenderBlock into HTML (may produce 1 or 2 messages – input and/or output)
|
|
3830
|
+
renderNode(block) {
|
|
3831
|
+
const parts = [];
|
|
3832
|
+
if (block && block.input && block.input.text) parts.push(this._renderUser(block));
|
|
3833
|
+
if (block && block.output && block.output.text) parts.push(this._renderBot(block));
|
|
3834
|
+
return parts.join('');
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
// Render array of blocks
|
|
3838
|
+
renderNodes(blocks) {
|
|
3839
|
+
if (!Array.isArray(blocks)) return '';
|
|
3840
|
+
const out = [];
|
|
3841
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
3842
|
+
const b = blocks[i] || null;
|
|
3843
|
+
if (!b) continue;
|
|
3844
|
+
out.push(this.renderNode(b));
|
|
3845
|
+
}
|
|
3846
|
+
return out.join('');
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
// ==========================================================================
|
|
3851
|
+
// 9b) Data receiver for append/replace nodes
|
|
3852
|
+
// ==========================================================================
|
|
3853
|
+
|
|
3854
|
+
class DataReceiver {
|
|
3855
|
+
// Normalizes payload (HTML string or JSON) and delegates to NodesManager.
|
|
3856
|
+
constructor(cfg, templates, nodes, scrollMgr) {
|
|
3857
|
+
this.cfg = cfg || {};
|
|
3858
|
+
this.templates = templates;
|
|
3859
|
+
this.nodes = nodes;
|
|
3860
|
+
this.scrollMgr = scrollMgr;
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
_tryParseJSON(s) {
|
|
3864
|
+
if (typeof s !== 'string') return s;
|
|
3865
|
+
const t = s.trim();
|
|
3866
|
+
if (!t) return null;
|
|
3867
|
+
// If it's like HTML, don't parse as JSON
|
|
3868
|
+
if (t[0] === '<') return null;
|
|
3869
|
+
try { return JSON.parse(t); } catch (_) { return null; }
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
_normalizeToBlocks(obj) {
|
|
3873
|
+
if (!obj) return [];
|
|
3874
|
+
if (Array.isArray(obj)) return obj;
|
|
3875
|
+
if (obj.node) return [obj.node];
|
|
3876
|
+
if (obj.nodes) return (Array.isArray(obj.nodes) ? obj.nodes : []);
|
|
3877
|
+
// single node-like object
|
|
3878
|
+
if (typeof obj === 'object' && (obj.input || obj.output || obj.id)) return [obj];
|
|
3879
|
+
return [];
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
append(payload) {
|
|
3883
|
+
// Legacy HTML string?
|
|
3884
|
+
if (typeof payload === 'string' && payload.trim().startsWith('<')) {
|
|
3885
|
+
this.nodes.appendNode(payload, this.scrollMgr);
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
// Try JSON
|
|
3889
|
+
const obj = this._tryParseJSON(payload);
|
|
3890
|
+
if (!obj) {
|
|
3891
|
+
// Not JSON – pass through
|
|
3892
|
+
this.nodes.appendNode(String(payload), this.scrollMgr);
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
const blocks = this._normalizeToBlocks(obj);
|
|
3896
|
+
if (!blocks.length) {
|
|
3897
|
+
this.nodes.appendNode('', this.scrollMgr);
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
const html = this.templates.renderNodes(blocks);
|
|
3901
|
+
this.nodes.appendNode(html, this.scrollMgr);
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
replace(payload) {
|
|
3905
|
+
// Legacy HTML string?
|
|
3906
|
+
if (typeof payload === 'string' && payload.trim().startsWith('<')) {
|
|
3907
|
+
this.nodes.replaceNodes(payload, this.scrollMgr);
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
// Try JSON
|
|
3911
|
+
const obj = this._tryParseJSON(payload);
|
|
3912
|
+
if (!obj) {
|
|
3913
|
+
this.nodes.replaceNodes(String(payload), this.scrollMgr);
|
|
3914
|
+
return;
|
|
3915
|
+
}
|
|
3916
|
+
const blocks = this._normalizeToBlocks(obj);
|
|
3917
|
+
if (!blocks.length) {
|
|
3918
|
+
this.nodes.replaceNodes('', this.scrollMgr);
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
const html = this.templates.renderNodes(blocks);
|
|
3922
|
+
this.nodes.replaceNodes(html, this.scrollMgr);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3053
3926
|
// ==========================================================================
|
|
3054
3927
|
// 10) UI manager
|
|
3055
3928
|
// ==========================================================================
|
|
@@ -3081,7 +3954,10 @@
|
|
|
3081
3954
|
'.msg-box.msg-user .msg > .uc-content.uc-collapsed { max-height: 1000px; overflow: hidden; }',
|
|
3082
3955
|
'.msg-box.msg-user .msg > .uc-toggle { display: none; margin-top: 8px; text-align: center; cursor: pointer; user-select: none; }',
|
|
3083
3956
|
'.msg-box.msg-user .msg > .uc-toggle.visible { display: block; }',
|
|
3084
|
-
|
|
3957
|
+
|
|
3958
|
+
/* Increased toggle icon size to a comfortable/default size.
|
|
3959
|
+
Overridable via CSS var --uc-toggle-icon-size to keep host-level control. */
|
|
3960
|
+
'.msg-box.msg-user .msg > .uc-toggle img { width: var(--uc-toggle-icon-size, 26px); height: var(--uc-toggle-icon-size, 26px); opacity: .8; }',
|
|
3085
3961
|
'.msg-box.msg-user .msg > .uc-toggle:hover img { opacity: 1; }'
|
|
3086
3962
|
].join('\n');
|
|
3087
3963
|
document.head.appendChild(style);
|
|
@@ -3133,9 +4009,16 @@
|
|
|
3133
4009
|
// Tracks whether renderSnapshot injected a one-off synthetic EOL for parsing an open fence
|
|
3134
4010
|
// (used to strip it from the initial streaming tail to avoid "#\n foo" on first line).
|
|
3135
4011
|
this._lastInjectedEOL = false;
|
|
4012
|
+
|
|
4013
|
+
this._customFenceSpecs = []; // [{ open, close }, ...]
|
|
4014
|
+
this._fenceCustom = null; // currently active custom fence spec or null
|
|
3136
4015
|
}
|
|
3137
4016
|
_d(tag, data) { this.logger.debug('STREAM', tag, data); }
|
|
3138
4017
|
|
|
4018
|
+
setCustomFenceSpecs(specs) {
|
|
4019
|
+
this._customFenceSpecs = Array.isArray(specs) ? specs.slice() : [];
|
|
4020
|
+
}
|
|
4021
|
+
|
|
3139
4022
|
// --- Rope buffer helpers (internal) ---
|
|
3140
4023
|
|
|
3141
4024
|
// Append a chunk into the rope without immediately touching the large string.
|
|
@@ -3180,6 +4063,7 @@
|
|
|
3180
4063
|
|
|
3181
4064
|
// Clear any previous synthetic EOL marker.
|
|
3182
4065
|
this._lastInjectedEOL = false;
|
|
4066
|
+
this._fenceCustom = null;
|
|
3183
4067
|
|
|
3184
4068
|
this._d('RESET', { });
|
|
3185
4069
|
}
|
|
@@ -3289,11 +4173,13 @@
|
|
|
3289
4173
|
if (!atLineStart) { i++; continue; }
|
|
3290
4174
|
atLineStart = false;
|
|
3291
4175
|
|
|
4176
|
+
// Skip list/blockquote/indent normalization (existing logic)
|
|
3292
4177
|
let j = i;
|
|
3293
4178
|
while (j < n) {
|
|
3294
4179
|
let localSpaces = 0;
|
|
3295
4180
|
while (j < n && (s[j] === ' ' || s[j] === '\t')) { localSpaces += (s[j] === '\t') ? 4 : 1; j++; if (localSpaces > 3) break; }
|
|
3296
4181
|
if (j < n && s[j] === '>') { j++; if (j < n && s[j] === ' ') j++; continue; }
|
|
4182
|
+
|
|
3297
4183
|
let saved = j;
|
|
3298
4184
|
if (j < n && (s[j] === '-' || s[j] === '*' || s[j] === '+')) {
|
|
3299
4185
|
let jj = j + 1; if (jj < n && s[jj] === ' ') { j = jj + 1; } else { j = saved; }
|
|
@@ -3313,6 +4199,47 @@
|
|
|
3313
4199
|
}
|
|
3314
4200
|
if (indent > 3) { i = j; continue; }
|
|
3315
4201
|
|
|
4202
|
+
// 1) Custom fences first (e.g. [!exec] ... [/!exec], <execute>...</execute>)
|
|
4203
|
+
if (!this.fenceOpen && this._customFenceSpecs && this._customFenceSpecs.length) {
|
|
4204
|
+
for (let ci = 0; ci < this._customFenceSpecs.length; ci++) {
|
|
4205
|
+
const spec = this._customFenceSpecs[ci];
|
|
4206
|
+
const open = spec && spec.open ? spec.open : '';
|
|
4207
|
+
if (!open) continue;
|
|
4208
|
+
const k = j + open.length;
|
|
4209
|
+
if (k <= n && s.slice(j, k) === open) {
|
|
4210
|
+
if (!inNewOrCrosses(j, k)) { /* seen fully in previous prefix */ }
|
|
4211
|
+
else {
|
|
4212
|
+
this.fenceOpen = true; this._fenceCustom = spec; opened = true; i = k;
|
|
4213
|
+
this._d('FENCE_OPEN_DETECTED_CUSTOM', { open, idxStart: j, idxEnd: k, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4214
|
+
continue; // main while
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
} else if (this.fenceOpen && this._fenceCustom && this._fenceCustom.close) {
|
|
4219
|
+
const close = this._fenceCustom.close;
|
|
4220
|
+
const k = j + close.length;
|
|
4221
|
+
if (k <= n && s.slice(j, k) === close) {
|
|
4222
|
+
// Require only trailing whitespace on the line (consistent with ``` logic)
|
|
4223
|
+
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
4224
|
+
const onlyWS = this.onlyTrailingWhitespace(s, k, eol);
|
|
4225
|
+
if (onlyWS) {
|
|
4226
|
+
if (!inNewOrCrosses(j, k)) { /* seen in previous prefix */ }
|
|
4227
|
+
else {
|
|
4228
|
+
this.fenceOpen = false; this._fenceCustom = null; closed = true;
|
|
4229
|
+
const endInS = k;
|
|
4230
|
+
const rel = endInS - preLen;
|
|
4231
|
+
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4232
|
+
i = k;
|
|
4233
|
+
this._d('FENCE_CLOSE_DETECTED_CUSTOM', { close, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4234
|
+
continue; // main while
|
|
4235
|
+
}
|
|
4236
|
+
} else {
|
|
4237
|
+
this._d('FENCE_CLOSE_REJECTED_CUSTOM_NON_WS_AFTER', { close, idxStart: j, idxEnd: k });
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
// 2) Standard markdown-it fences (``` or ~~~) – leave your original logic intact
|
|
3316
4243
|
if (j < n && (s[j] === '`' || s[j] === '~')) {
|
|
3317
4244
|
const mark = s[j]; let k = j; while (k < n && s[k] === mark) k++; const run = k - j;
|
|
3318
4245
|
|
|
@@ -3320,10 +4247,10 @@
|
|
|
3320
4247
|
if (run >= 3) {
|
|
3321
4248
|
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
3322
4249
|
this.fenceOpen = true; this.fenceMark = mark; this.fenceLen = run; opened = true; i = k;
|
|
3323
|
-
this._d('FENCE_OPEN_DETECTED', { mark, run, idxStart: j, idxEnd: k,
|
|
4250
|
+
this._d('FENCE_OPEN_DETECTED', { mark, run, idxStart: j, idxEnd: k, region: (j >= preLen) ? 'new' : 'cross' });
|
|
3324
4251
|
continue;
|
|
3325
4252
|
}
|
|
3326
|
-
} else {
|
|
4253
|
+
} else if (!this._fenceCustom) {
|
|
3327
4254
|
if (mark === this.fenceMark && run >= this.fenceLen) {
|
|
3328
4255
|
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
3329
4256
|
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
@@ -3331,16 +4258,17 @@
|
|
|
3331
4258
|
this.fenceOpen = false; closed = true;
|
|
3332
4259
|
const endInS = k;
|
|
3333
4260
|
const rel = endInS - preLen;
|
|
3334
|
-
|
|
3335
|
-
|
|
4261
|
+
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4262
|
+
i = k;
|
|
3336
4263
|
this._d('FENCE_CLOSE_DETECTED', { mark, run, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
3337
4264
|
continue;
|
|
3338
4265
|
} else {
|
|
3339
|
-
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k
|
|
4266
|
+
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k });
|
|
3340
4267
|
}
|
|
3341
4268
|
}
|
|
3342
4269
|
}
|
|
3343
4270
|
}
|
|
4271
|
+
|
|
3344
4272
|
i = j + 1;
|
|
3345
4273
|
}
|
|
3346
4274
|
|
|
@@ -3349,6 +4277,7 @@
|
|
|
3349
4277
|
this.fenceTail = s.slice(-3);
|
|
3350
4278
|
return { opened, closed, splitAt };
|
|
3351
4279
|
}
|
|
4280
|
+
|
|
3352
4281
|
// Ensure message snapshot container exists.
|
|
3353
4282
|
getMsgSnapshotRoot(msg) {
|
|
3354
4283
|
if (!msg) return null;
|
|
@@ -3788,6 +4717,48 @@
|
|
|
3788
4717
|
this.schedulePromoteTail(true);
|
|
3789
4718
|
}
|
|
3790
4719
|
}
|
|
4720
|
+
// Keep language header stable across snapshots for the active streaming code.
|
|
4721
|
+
// If the current snapshot produced a tiny/unsupported token (e.g. 'on', 'ml', 's'),
|
|
4722
|
+
// reuse the last known good language (from previous active state or sticky attribute).
|
|
4723
|
+
stabilizeHeaderLabel(prevAC, newAC) {
|
|
4724
|
+
try {
|
|
4725
|
+
if (!newAC || !newAC.codeEl || !newAC.codeEl.isConnected) return;
|
|
4726
|
+
|
|
4727
|
+
const wrap = newAC.codeEl.closest('.code-wrapper');
|
|
4728
|
+
if (!wrap) return;
|
|
4729
|
+
|
|
4730
|
+
const span = wrap.querySelector('.code-header-lang');
|
|
4731
|
+
const curLabel = (span && span.textContent ? span.textContent.trim() : '').toLowerCase();
|
|
4732
|
+
|
|
4733
|
+
// Do not touch tool/output blocks
|
|
4734
|
+
if (curLabel === 'output') return;
|
|
4735
|
+
|
|
4736
|
+
const tokNow = (wrap.getAttribute('data-code-lang') || '').trim().toLowerCase();
|
|
4737
|
+
const sticky = (wrap.getAttribute('data-lang-sticky') || '').trim().toLowerCase();
|
|
4738
|
+
const prev = (prevAC && prevAC.lang && prevAC.lang !== 'plaintext') ? prevAC.lang.toLowerCase() : '';
|
|
4739
|
+
|
|
4740
|
+
const valid = (t) => !!t && t !== 'plaintext' && this._isHLJSSupported(t);
|
|
4741
|
+
|
|
4742
|
+
let finalTok = '';
|
|
4743
|
+
if (valid(tokNow)) finalTok = tokNow;
|
|
4744
|
+
else if (valid(prev)) finalTok = prev;
|
|
4745
|
+
else if (valid(sticky)) finalTok = sticky;
|
|
4746
|
+
|
|
4747
|
+
if (finalTok) {
|
|
4748
|
+
// Update code class and header label consistently
|
|
4749
|
+
this._updateCodeLangClass(newAC.codeEl, finalTok);
|
|
4750
|
+
this._updateCodeHeaderLabel(newAC.codeEl, finalTok, finalTok);
|
|
4751
|
+
try { wrap.setAttribute('data-code-lang', finalTok); } catch (_) {}
|
|
4752
|
+
try { wrap.setAttribute('data-lang-sticky', finalTok); } catch (_) {}
|
|
4753
|
+
newAC.lang = finalTok; // keep AC state in sync
|
|
4754
|
+
} else {
|
|
4755
|
+
// If current label looks like a tiny/incomplete token, normalize to 'code'
|
|
4756
|
+
if (span && curLabel && curLabel.length < 3) {
|
|
4757
|
+
span.textContent = 'code';
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
} catch (_) { /* defensive: never break streaming path */ }
|
|
4761
|
+
}
|
|
3791
4762
|
// Render a snapshot of current stream buffer into the DOM.
|
|
3792
4763
|
renderSnapshot(msg) {
|
|
3793
4764
|
const streaming = !!this.isStreaming;
|
|
@@ -3822,13 +4793,21 @@
|
|
|
3822
4793
|
range.selectNodeContents(snap);
|
|
3823
4794
|
frag = range.createContextualFragment(html);
|
|
3824
4795
|
} catch (_) {
|
|
3825
|
-
// Fallback: safe temporary container
|
|
3826
4796
|
const tmp = document.createElement('div');
|
|
3827
4797
|
tmp.innerHTML = html;
|
|
3828
4798
|
frag = document.createDocumentFragment();
|
|
3829
4799
|
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
3830
4800
|
}
|
|
3831
4801
|
|
|
4802
|
+
// (stream-aware custom markup):
|
|
4803
|
+
// Apply Custom Markup on the fragment only if at least one rule opted-in for stream.
|
|
4804
|
+
try {
|
|
4805
|
+
if (this.renderer && this.renderer.customMarkup && this.renderer.customMarkup.hasStreamRules()) {
|
|
4806
|
+
const MDinline = this.renderer.MD_STREAM || this.renderer.MD || null;
|
|
4807
|
+
this.renderer.customMarkup.applyStream(frag, MDinline);
|
|
4808
|
+
}
|
|
4809
|
+
} catch (_) { /* keep snapshot path resilient */ }
|
|
4810
|
+
|
|
3832
4811
|
// Reuse closed, stable code blocks from previous snapshot to avoid re-highlighting
|
|
3833
4812
|
this.preserveStableClosedCodes(snap, frag, this.fenceOpen === true);
|
|
3834
4813
|
|
|
@@ -3840,12 +4819,21 @@
|
|
|
3840
4819
|
this._ensureBottomForJustFinalized(snap);
|
|
3841
4820
|
|
|
3842
4821
|
// Setup active streaming code if fence is open, otherwise clear active state
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
this.
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
4822
|
+
const prevAC = this.activeCode; // remember previous active streaming state (if any)
|
|
4823
|
+
|
|
4824
|
+
if (this.fenceOpen) {
|
|
4825
|
+
const newAC = this.setupActiveCodeFromSnapshot(snap);
|
|
4826
|
+
|
|
4827
|
+
// preserve previous frozen/tail state and stable lang/header across snapshots
|
|
4828
|
+
if (prevAC && newAC) {
|
|
4829
|
+
this.rehydrateActiveCode(prevAC, newAC);
|
|
4830
|
+
this.stabilizeHeaderLabel(prevAC, newAC);
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4833
|
+
this.activeCode = newAC || null;
|
|
4834
|
+
} else {
|
|
4835
|
+
this.activeCode = null;
|
|
4836
|
+
}
|
|
3849
4837
|
|
|
3850
4838
|
// Attach scroll/highlight observers (viewport aware)
|
|
3851
4839
|
if (!this.fenceOpen) {
|
|
@@ -3902,6 +4890,7 @@
|
|
|
3902
4890
|
const dt = Utils.now() - t0;
|
|
3903
4891
|
this._d('SNAPSHOT', { fenceOpen: this.fenceOpen, activeCode: !!this.activeCode, bufLen: this.getStreamLength(), timeMs: Math.round(dt), streaming });
|
|
3904
4892
|
}
|
|
4893
|
+
|
|
3905
4894
|
// Get current message container (.msg) or create if allowed.
|
|
3906
4895
|
getMsg(create, name_header) { return this.dom.getStreamMsg(create, name_header); }
|
|
3907
4896
|
// Start a new streaming session (clear state and display loader, if any).
|
|
@@ -4153,20 +5142,17 @@
|
|
|
4153
5142
|
this.cfg = cfg; this.logger = logger || new Logger(cfg);
|
|
4154
5143
|
this.bridge = null; this.connected = false;
|
|
4155
5144
|
}
|
|
4156
|
-
// Low-level log via bridge if available.
|
|
4157
5145
|
log(text) { try { if (this.bridge && this.bridge.log) this.bridge.log(text); } catch (_) {} }
|
|
4158
|
-
// Wire JS callbacks to QWebChannel signals.
|
|
4159
5146
|
connect(onChunk, onNode, onNodeReplace, onNodeInput) {
|
|
4160
5147
|
if (!this.bridge) return false; if (this.connected) return true;
|
|
4161
5148
|
try {
|
|
4162
|
-
if (this.bridge.chunk) this.bridge.chunk.connect(onChunk);
|
|
5149
|
+
if (this.bridge.chunk) this.bridge.chunk.connect((name, chunk, type) => onChunk(name, chunk, type));
|
|
4163
5150
|
if (this.bridge.node) this.bridge.node.connect(onNode);
|
|
4164
5151
|
if (this.bridge.nodeReplace) this.bridge.nodeReplace.connect(onNodeReplace);
|
|
4165
5152
|
if (this.bridge.nodeInput) this.bridge.nodeInput.connect(onNodeInput);
|
|
4166
5153
|
this.connected = true; return true;
|
|
4167
5154
|
} catch (e) { this.log(e); return false; }
|
|
4168
5155
|
}
|
|
4169
|
-
// Detach callbacks.
|
|
4170
5156
|
disconnect() {
|
|
4171
5157
|
if (!this.bridge) return false; if (!this.connected) return true;
|
|
4172
5158
|
try {
|
|
@@ -4177,7 +5163,6 @@
|
|
|
4177
5163
|
} catch (_) {}
|
|
4178
5164
|
this.connected = false; return true;
|
|
4179
5165
|
}
|
|
4180
|
-
// Initialize QWebChannel and notify Python side that JS is ready.
|
|
4181
5166
|
initQWebChannel(pid, onReady) {
|
|
4182
5167
|
try {
|
|
4183
5168
|
new QWebChannel(qt.webChannelTransport, (channel) => {
|
|
@@ -4186,9 +5171,8 @@
|
|
|
4186
5171
|
onReady && onReady(this.bridge);
|
|
4187
5172
|
if (this.bridge && this.bridge.js_ready) this.bridge.js_ready(pid);
|
|
4188
5173
|
});
|
|
4189
|
-
} catch (e) { /* swallow
|
|
5174
|
+
} catch (e) { /* swallow */ }
|
|
4190
5175
|
}
|
|
4191
|
-
// Convenience wrappers for host actions.
|
|
4192
5176
|
copyCode(text) { if (this.bridge && this.bridge.copy_text) this.bridge.copy_text(text); }
|
|
4193
5177
|
previewCode(text) { if (this.bridge && this.bridge.preview_text) this.bridge.preview_text(text); }
|
|
4194
5178
|
runCode(text) { if (this.bridge && this.bridge.run_text) this.bridge.run_text(text); }
|
|
@@ -4417,10 +5401,16 @@
|
|
|
4417
5401
|
this.streamQ = new StreamQueue(this.cfg, this.stream, this.scrollMgr, this.raf);
|
|
4418
5402
|
this.events = new EventManager(this.cfg, this.dom, this.scrollMgr, this.highlighter, this.codeScroll, this.toolOutput, this.bridge);
|
|
4419
5403
|
|
|
5404
|
+
try {
|
|
5405
|
+
this.stream.setCustomFenceSpecs(this.customMarkup.getSourceFenceSpecs());
|
|
5406
|
+
} catch (_) {}
|
|
5407
|
+
|
|
5408
|
+
this.templates = new NodeTemplateEngine(this.cfg, this.logger);
|
|
5409
|
+
this.data = new DataReceiver(this.cfg, this.templates, this.nodes, this.scrollMgr);
|
|
5410
|
+
|
|
4420
5411
|
this.tips = null;
|
|
4421
5412
|
this._lastHeavyResetMs = 0;
|
|
4422
5413
|
|
|
4423
|
-
// Bridge hooks between renderer and other subsystems.
|
|
4424
5414
|
this.renderer.hooks.observeNewCode = (root, opts) => this.highlighter.observeNewCode(root, opts, this.stream.activeCode);
|
|
4425
5415
|
this.renderer.hooks.observeMsgBoxes = (root) => this.highlighter.observeMsgBoxes(root, (box) => {
|
|
4426
5416
|
this.highlighter.observeNewCode(box, {
|
|
@@ -4437,6 +5427,7 @@
|
|
|
4437
5427
|
};
|
|
4438
5428
|
this.renderer.hooks.codeScrollInit = (root) => this.codeScroll.initScrollableBlocks(root);
|
|
4439
5429
|
}
|
|
5430
|
+
|
|
4440
5431
|
// Reset stream state and optionally perform a heavy reset of schedulers and observers.
|
|
4441
5432
|
resetStreamState(origin, opts) {
|
|
4442
5433
|
try { this.streamQ.clear(); } catch (_) {}
|
|
@@ -4471,6 +5462,17 @@
|
|
|
4471
5462
|
|
|
4472
5463
|
try { this.tips && this.tips.hide(); } catch (_) {}
|
|
4473
5464
|
}
|
|
5465
|
+
// API: handle incoming chunk (from bridge).
|
|
5466
|
+
api_onChunk = (name, chunk, type) => {
|
|
5467
|
+
const t = String(type || 'text_delta');
|
|
5468
|
+
if (t === 'text_delta') {
|
|
5469
|
+
this.api_appendStream(name, chunk);
|
|
5470
|
+
return;
|
|
5471
|
+
}
|
|
5472
|
+
// Future-proof: add other chunk types here (attachments, status, etc.)
|
|
5473
|
+
// No-op for unknown types to keep current behavior.
|
|
5474
|
+
this.logger.debug('STREAM', 'IGNORED_NON_TEXT_CHUNK', { type: t, len: (chunk ? String(chunk).length : 0) });
|
|
5475
|
+
};
|
|
4474
5476
|
// API: begin stream.
|
|
4475
5477
|
api_beginStream = (chunk = false) => { this.tips && this.tips.hide(); this.resetStreamState('beginStream', { clearMsg: true, finalizeActive: false, forceHeavy: true }); this.stream.beginStream(chunk); };
|
|
4476
5478
|
// API: end stream.
|
|
@@ -4494,12 +5496,27 @@
|
|
|
4494
5496
|
// API: clear streaming output area entirely.
|
|
4495
5497
|
api_clearStream = () => { this.tips && this.tips.hide(); this.resetStreamState('clearStream', { clearMsg: true, forceHeavy: true }); const el = this.dom.getStreamContainer(); if (!el) return; el.replaceChildren(); };
|
|
4496
5498
|
|
|
4497
|
-
// API: append
|
|
4498
|
-
api_appendNode = (
|
|
4499
|
-
|
|
4500
|
-
|
|
5499
|
+
// API: append/replace messages (non-streaming).
|
|
5500
|
+
api_appendNode = (payload) => { this.resetStreamState('appendNode'); this.data.append(payload); };
|
|
5501
|
+
api_replaceNodes = (payload) => { this.resetStreamState('replaceNodes', { clearMsg: true, forceHeavy: true }); this.dom.clearNodes(); this.data.replace(payload); };
|
|
5502
|
+
|
|
4501
5503
|
// API: append to input area.
|
|
4502
|
-
|
|
5504
|
+
api_appendToInput = (payload) => {
|
|
5505
|
+
this.nodes.appendToInput(payload);
|
|
5506
|
+
|
|
5507
|
+
// Ensure initial auto-follow is ON for the next stream that will start right after user input.
|
|
5508
|
+
// Rationale: previously, if the user had scrolled up, autoFollow could remain false and the
|
|
5509
|
+
// live stream would not follow even though we just sent a new input.
|
|
5510
|
+
this.scrollMgr.autoFollow = true; // explicitly re-enable page auto-follow
|
|
5511
|
+
this.scrollMgr.userInteracted = false; // Reset interaction so live scroll is allowed
|
|
5512
|
+
|
|
5513
|
+
// Keep lastScrollTop in sync to avoid misclassification in the next onscroll handler.
|
|
5514
|
+
try { this.scrollMgr.lastScrollTop = Utils.SE.scrollTop | 0; } catch (_) {}
|
|
5515
|
+
|
|
5516
|
+
// Non-live scroll to bottom right away, independent of autoFollow state.
|
|
5517
|
+
this.scrollMgr.scheduleScroll();
|
|
5518
|
+
// NOTE: No resetStreamState() here to avoid flicker/reflow issues while previewing user input.
|
|
5519
|
+
};
|
|
4503
5520
|
|
|
4504
5521
|
// API: clear messages list.
|
|
4505
5522
|
api_clearNodes = () => { this.dom.clearNodes(); this.resetStreamState('clearNodes', { clearMsg: true, forceHeavy: true }); };
|
|
@@ -4597,6 +5614,7 @@
|
|
|
4597
5614
|
|
|
4598
5615
|
// API: restore collapsed state of codes in a given root.
|
|
4599
5616
|
api_restoreCollapsedCode = (root) => this.renderer.restoreCollapsedCode(root);
|
|
5617
|
+
|
|
4600
5618
|
// API: user-triggered page scroll.
|
|
4601
5619
|
api_scrollToTopUser = () => this.scrollMgr.scrollToTopUser();
|
|
4602
5620
|
api_scrollToBottomUser = () => this.scrollMgr.scrollToBottomUser();
|
|
@@ -4607,7 +5625,11 @@
|
|
|
4607
5625
|
|
|
4608
5626
|
// API: custom markup rules control.
|
|
4609
5627
|
api_getCustomMarkupRules = () => this.customMarkup.getRules();
|
|
4610
|
-
api_setCustomMarkupRules = (rules) => {
|
|
5628
|
+
api_setCustomMarkupRules = (rules) => {
|
|
5629
|
+
this.customMarkup.setRules(rules);
|
|
5630
|
+
// Keep StreamEngine in sync with rules producing fenced code
|
|
5631
|
+
try { this.stream.setCustomFenceSpecs(this.customMarkup.getSourceFenceSpecs()); } catch (_) {}
|
|
5632
|
+
};
|
|
4611
5633
|
|
|
4612
5634
|
// Initialize runtime (called on DOMContentLoaded).
|
|
4613
5635
|
init() {
|
|
@@ -4615,15 +5637,13 @@
|
|
|
4615
5637
|
this.dom.init();
|
|
4616
5638
|
this.ui.ensureStickyHeaderStyle();
|
|
4617
5639
|
|
|
4618
|
-
// Tips manager with rAF-based centering and rotation
|
|
4619
5640
|
this.tips = new TipsManager(this.dom);
|
|
4620
|
-
|
|
4621
5641
|
this.events.install();
|
|
4622
5642
|
|
|
4623
5643
|
this.bridge.initQWebChannel(this.cfg.PID, (bridge) => {
|
|
4624
|
-
const onChunk = (name, chunk) => this.
|
|
4625
|
-
const onNode = (
|
|
4626
|
-
const onNodeReplace = (
|
|
5644
|
+
const onChunk = (name, chunk, type) => this.api_onChunk(name, chunk, type);
|
|
5645
|
+
const onNode = (payload) => this.api_appendNode(payload);
|
|
5646
|
+
const onNodeReplace = (payload) => this.api_replaceNodes(payload);
|
|
4627
5647
|
const onNodeInput = (html) => this.api_appendToInput(html);
|
|
4628
5648
|
this.bridge.connect(onChunk, onNode, onNodeReplace, onNodeInput);
|
|
4629
5649
|
try { this.logger.bindBridge(this.bridge.bridge || this.bridge); } catch (_) {}
|
|
@@ -4647,7 +5667,6 @@
|
|
|
4647
5667
|
}, this.stream.activeCode);
|
|
4648
5668
|
this.highlighter.scheduleScanVisibleCodes(this.stream.activeCode);
|
|
4649
5669
|
|
|
4650
|
-
// Start tips rotation; internal delay matches legacy timing (TIPS_INIT_DELAY_MS)
|
|
4651
5670
|
this.tips.cycle();
|
|
4652
5671
|
this.scrollMgr.updateScrollFab(true);
|
|
4653
5672
|
}
|
|
@@ -4665,17 +5684,17 @@
|
|
|
4665
5684
|
}
|
|
4666
5685
|
|
|
4667
5686
|
// Ensure RafManager.cancel uses the correct group key cleanup.
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
5687
|
+
if (typeof RafManager !== 'undefined' && RafManager.prototype && typeof RafManager.prototype.cancel === 'function') {
|
|
5688
|
+
RafManager.prototype.cancel = function(key) {
|
|
5689
|
+
const t = this.tasks.get(key);
|
|
5690
|
+
if (!t) return;
|
|
5691
|
+
this.tasks.delete(key);
|
|
5692
|
+
if (t.group) {
|
|
5693
|
+
const set = this.groups.get(t.group);
|
|
5694
|
+
if (set) { set.delete(key); if (set.size === 0) this.groups.delete(t.group); }
|
|
5695
|
+
}
|
|
5696
|
+
};
|
|
4676
5697
|
}
|
|
4677
|
-
};
|
|
4678
|
-
}
|
|
4679
5698
|
|
|
4680
5699
|
const runtime = new Runtime();
|
|
4681
5700
|
|
|
@@ -4687,11 +5706,12 @@
|
|
|
4687
5706
|
window.endStream = () => runtime.api_endStream();
|
|
4688
5707
|
window.applyStream = (name, chunk) => runtime.api_applyStream(name, chunk);
|
|
4689
5708
|
window.appendStream = (name, chunk) => runtime.api_appendStream(name, chunk);
|
|
5709
|
+
window.appendStreamTyped = (type, name, chunk) => runtime.api_onChunk(name, chunk, type);
|
|
4690
5710
|
window.nextStream = () => runtime.api_nextStream();
|
|
4691
5711
|
window.clearStream = () => runtime.api_clearStream();
|
|
4692
5712
|
|
|
4693
|
-
window.appendNode = (
|
|
4694
|
-
window.replaceNodes = (
|
|
5713
|
+
window.appendNode = (payload) => runtime.api_appendNode(payload);
|
|
5714
|
+
window.replaceNodes = (payload) => runtime.api_replaceNodes(payload);
|
|
4695
5715
|
window.appendToInput = (html) => runtime.api_appendToInput(html);
|
|
4696
5716
|
|
|
4697
5717
|
window.clearNodes = () => runtime.api_clearNodes();
|