pygpt-net 2.6.43__py3-none-any.whl → 2.6.45__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 +13 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +1 -1
- pygpt_net/controller/ctx/ctx.py +6 -0
- pygpt_net/controller/debug/debug.py +7 -19
- pygpt_net/controller/debug/fixtures.py +103 -0
- pygpt_net/core/debug/console/console.py +2 -1
- pygpt_net/core/debug/debug.py +1 -1
- pygpt_net/core/fixtures/stream/__init__.py +0 -0
- pygpt_net/{provider/api/fake → core/fixtures/stream}/generator.py +2 -3
- pygpt_net/core/render/web/body.py +294 -23
- pygpt_net/core/render/web/helpers.py +26 -0
- pygpt_net/core/render/web/renderer.py +457 -704
- pygpt_net/data/config/config.json +10 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +59 -19
- pygpt_net/data/fixtures/fake_stream.txt +5733 -0
- pygpt_net/data/js/app.js +2617 -1315
- pygpt_net/data/locale/locale.en.ini +12 -5
- pygpt_net/js_rc.py +14272 -10602
- pygpt_net/provider/api/openai/__init__.py +4 -12
- pygpt_net/provider/core/config/patch.py +14 -1
- pygpt_net/ui/base/context_menu.py +3 -2
- pygpt_net/ui/layout/chat/output.py +1 -1
- pygpt_net/ui/layout/ctx/ctx_list.py +3 -3
- pygpt_net/ui/menu/debug.py +36 -23
- pygpt_net/ui/widget/lists/context.py +233 -51
- pygpt_net/ui/widget/textarea/web.py +4 -4
- pygpt_net/utils.py +3 -2
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/METADATA +72 -14
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/RECORD +35 -32
- /pygpt_net/{provider/api/fake/__init__.py → core/fixtures/__init__} +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.43.dist-info → pygpt_net-2.6.45.dist-info}/entry_points.txt +0 -0
pygpt_net/data/js/app.js
CHANGED
|
@@ -553,8 +553,9 @@
|
|
|
553
553
|
growth: 2.6, // Ramp step quickly if adaptive is enabled
|
|
554
554
|
minInterval: 500, // Minimum time between snapshots (ms) to avoid churn
|
|
555
555
|
softLatency: 1200, // Force snapshot only after a noticeable idle (ms)
|
|
556
|
-
minLinesForHL:
|
|
557
|
-
|
|
556
|
+
minLinesForHL: Utils.g('PROFILE_CODE_HL_N_LINE', 25),
|
|
557
|
+
minCharsForHL: Utils.g('PROFILE_CODE_HL_N_CHARS', 5000),
|
|
558
|
+
promoteMinInterval: 300, promoteMaxLatency: 800, promoteMinLines: Utils.g('PROFILE_CODE_HL_N_LINE', 25),
|
|
558
559
|
adaptiveStep: Utils.g('PROFILE_CODE_ADAPTIVE_STEP', true),
|
|
559
560
|
// Hard switches to plain streaming (no incremental hljs, minimal DOM churn)
|
|
560
561
|
stopAfterLines: Utils.g('PROFILE_CODE_STOP_HL_AFTER_LINES', 300), // Stop incremental hljs after this many lines
|
|
@@ -598,9 +599,19 @@
|
|
|
598
599
|
|
|
599
600
|
// Custom markup rules for simple tags in text.
|
|
600
601
|
this.CUSTOM_MARKUP_RULES = Utils.g('CUSTOM_MARKUP_RULES', [
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
+
]);
|
|
604
615
|
}
|
|
605
616
|
}
|
|
606
617
|
|
|
@@ -736,175 +747,159 @@
|
|
|
736
747
|
}
|
|
737
748
|
}
|
|
738
749
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
// Get or create per-code element state.
|
|
753
|
-
state(el) {
|
|
754
|
-
let s = this.map.get(el);
|
|
755
|
-
if (!s) { s = { autoFollow: false, lastScrollTop: 0, userInteracted: false, freezeUntil: 0 }; this.map.set(el, s); }
|
|
756
|
-
return s;
|
|
757
|
-
}
|
|
758
|
-
// Check if code block is already finalized (not streaming).
|
|
759
|
-
isFinalizedCode(el) {
|
|
760
|
-
if (!el || el.tagName !== 'CODE') return false;
|
|
761
|
-
if (el.dataset && el.dataset._active_stream === '1') return false;
|
|
762
|
-
const highlighted = (el.getAttribute('data-highlighted') === 'yes') || el.classList.contains('hljs');
|
|
763
|
-
return highlighted;
|
|
764
|
-
}
|
|
765
|
-
// Is element scrolled close to the bottom by a margin?
|
|
766
|
-
isNearBottomEl(el, margin = 100) {
|
|
767
|
-
if (!el) return true;
|
|
768
|
-
const distance = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
769
|
-
return distance <= margin;
|
|
770
|
-
}
|
|
771
|
-
// Scroll code element to the bottom respecting interaction state.
|
|
772
|
-
scrollToBottom(el, live = false, force = false) {
|
|
773
|
-
if (!el || !el.isConnected) return;
|
|
774
|
-
if (!force && this.isFinalizedCode(el)) return;
|
|
775
|
-
|
|
776
|
-
const st = this.state(el);
|
|
777
|
-
const now = Utils.now();
|
|
778
|
-
if (!force && st.freezeUntil && now < st.freezeUntil) return;
|
|
779
|
-
|
|
780
|
-
const distNow = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
781
|
-
if (!force && distNow <= 1) { st.lastScrollTop = el.scrollTop; return; }
|
|
782
|
-
|
|
783
|
-
const marginPx = live ? 96 : this.cfg.CODE_SCROLL.NEAR_MARGIN_PX;
|
|
784
|
-
const behavior = 'instant';
|
|
785
|
-
|
|
786
|
-
if (!force) {
|
|
787
|
-
if (live && st.autoFollow !== true) return;
|
|
788
|
-
if (!live && !(st.autoFollow === true || this.isNearBottomEl(el, marginPx) || !st.userInteracted)) return;
|
|
750
|
+
// ==========================================================================
|
|
751
|
+
// 2) Code scroll state manager
|
|
752
|
+
// ==========================================================================
|
|
753
|
+
|
|
754
|
+
class CodeScrollState {
|
|
755
|
+
constructor(cfg, raf) {
|
|
756
|
+
this.cfg = cfg;
|
|
757
|
+
this.raf = raf;
|
|
758
|
+
this.map = new WeakMap();
|
|
759
|
+
this.rafMap = new WeakMap();
|
|
760
|
+
this.rafIds = new Set(); // legacy
|
|
761
|
+
this.rafKeyMap = new WeakMap();
|
|
789
762
|
}
|
|
763
|
+
// Get or create per-code element state.
|
|
764
|
+
state(el) {
|
|
765
|
+
let s = this.map.get(el);
|
|
766
|
+
if (!s) { s = { autoFollow: false, lastScrollTop: 0, userInteracted: false, freezeUntil: 0 }; this.map.set(el, s); }
|
|
767
|
+
return s;
|
|
768
|
+
}
|
|
769
|
+
// Check if code block is already finalized (not streaming).
|
|
770
|
+
isFinalizedCode(el) {
|
|
771
|
+
if (!el || el.tagName !== 'CODE') return false;
|
|
772
|
+
if (el.dataset && el.dataset._active_stream === '1') return false;
|
|
773
|
+
const highlighted = (el.getAttribute('data-highlighted') === 'yes') || el.classList.contains('hljs');
|
|
774
|
+
return highlighted;
|
|
775
|
+
}
|
|
776
|
+
// Is element scrolled close to the bottom by a margin?
|
|
777
|
+
isNearBottomEl(el, margin = 100) {
|
|
778
|
+
if (!el) return true;
|
|
779
|
+
const distance = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
780
|
+
return distance <= margin;
|
|
781
|
+
}
|
|
782
|
+
// Scroll code element to the bottom respecting interaction state.
|
|
783
|
+
scrollToBottom(el, live = false, force = false) {
|
|
784
|
+
if (!el || !el.isConnected) return;
|
|
785
|
+
if (!force && this.isFinalizedCode(el)) return;
|
|
790
786
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
// Schedule bottom scroll in rAF (coalesces multiple calls).
|
|
795
|
-
scheduleScroll(el, live = false, force = false) {
|
|
796
|
-
if (!el || !el.isConnected) return;
|
|
797
|
-
if (!force && this.isFinalizedCode(el)) return;
|
|
798
|
-
if (this.rafMap.get(el)) return;
|
|
799
|
-
this.rafMap.set(el, true);
|
|
800
|
-
|
|
801
|
-
let key = this.rafKeyMap.get(el);
|
|
802
|
-
if (!key) { key = { t: 'codeScroll', el }; this.rafKeyMap.set(el, key); }
|
|
787
|
+
const st = this.state(el);
|
|
788
|
+
const now = Utils.now();
|
|
789
|
+
if (!force && st.freezeUntil && now < st.freezeUntil) return;
|
|
803
790
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
this.scrollToBottom(el, live, force);
|
|
807
|
-
}, 'CodeScroll', 0);
|
|
808
|
-
}
|
|
809
|
-
// Attach scroll/wheel/touch handlers to manage auto-follow state.
|
|
810
|
-
attachHandlers(codeEl) {
|
|
811
|
-
if (!codeEl || codeEl.dataset.csListeners === '1') return;
|
|
812
|
-
codeEl.dataset.csListeners = '1';
|
|
813
|
-
const st = this.state(codeEl);
|
|
791
|
+
const distNow = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
792
|
+
if (!force && distNow <= 1) { st.lastScrollTop = el.scrollTop; return; }
|
|
814
793
|
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
const isUser = !!(ev && ev.isTrusted === true);
|
|
818
|
-
const now = Utils.now();
|
|
794
|
+
const marginPx = live ? 96 : this.cfg.CODE_SCROLL.NEAR_MARGIN_PX;
|
|
795
|
+
const behavior = 'instant';
|
|
819
796
|
|
|
820
|
-
if (
|
|
821
|
-
if (
|
|
822
|
-
st.autoFollow
|
|
823
|
-
st.lastScrollTop = top;
|
|
824
|
-
return;
|
|
797
|
+
if (!force) {
|
|
798
|
+
if (live && st.autoFollow !== true) return;
|
|
799
|
+
if (!live && !(st.autoFollow === true || this.isNearBottomEl(el, marginPx) || !st.userInteracted)) return;
|
|
825
800
|
}
|
|
826
801
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
802
|
+
try { el.scrollTo({ top: el.scrollHeight, behavior }); } catch (_) { el.scrollTop = el.scrollHeight; }
|
|
803
|
+
st.lastScrollTop = el.scrollTop;
|
|
804
|
+
}
|
|
805
|
+
// Schedule bottom scroll in rAF (coalesces multiple calls).
|
|
806
|
+
scheduleScroll(el, live = false, force = false) {
|
|
807
|
+
if (!el || !el.isConnected) return;
|
|
808
|
+
if (!force && this.isFinalizedCode(el)) return;
|
|
809
|
+
if (this.rafMap.get(el)) return;
|
|
810
|
+
this.rafMap.set(el, true);
|
|
811
|
+
|
|
812
|
+
let key = this.rafKeyMap.get(el);
|
|
813
|
+
if (!key) { key = { t: 'codeScroll', el }; this.rafKeyMap.set(el, key); }
|
|
814
|
+
|
|
815
|
+
this.raf.schedule(key, () => {
|
|
816
|
+
this.rafMap.delete(el);
|
|
817
|
+
this.scrollToBottom(el, live, force);
|
|
818
|
+
}, 'CodeScroll', 0);
|
|
819
|
+
}
|
|
820
|
+
// Attach scroll/wheel/touch handlers to manage auto-follow state.
|
|
821
|
+
attachHandlers(codeEl) {
|
|
822
|
+
if (!codeEl || codeEl.dataset.csListeners === '1') return;
|
|
823
|
+
codeEl.dataset.csListeners = '1';
|
|
824
|
+
const st = this.state(codeEl);
|
|
825
|
+
|
|
826
|
+
const onScroll = (ev) => {
|
|
827
|
+
const top = codeEl.scrollTop;
|
|
828
|
+
const isUser = !!(ev && ev.isTrusted === true);
|
|
829
|
+
const now = Utils.now();
|
|
830
|
+
|
|
831
|
+
if (this.isFinalizedCode(codeEl)) {
|
|
832
|
+
if (isUser) st.userInteracted = true;
|
|
833
|
+
st.autoFollow = false;
|
|
834
|
+
st.lastScrollTop = top;
|
|
835
|
+
return;
|
|
832
836
|
}
|
|
833
|
-
} else {
|
|
834
|
-
if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) st.autoFollow = true;
|
|
835
|
-
}
|
|
836
|
-
st.lastScrollTop = top;
|
|
837
|
-
};
|
|
838
837
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
838
|
+
if (isUser) {
|
|
839
|
+
if (top + 1 < st.lastScrollTop) {
|
|
840
|
+
st.autoFollow = false; st.userInteracted = true; st.freezeUntil = now + 1000;
|
|
841
|
+
} else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) {
|
|
842
|
+
st.autoFollow = true;
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) st.autoFollow = true;
|
|
846
|
+
}
|
|
847
|
+
st.lastScrollTop = top;
|
|
848
|
+
};
|
|
842
849
|
|
|
843
|
-
|
|
850
|
+
const onWheel = (ev) => {
|
|
851
|
+
st.userInteracted = true;
|
|
852
|
+
const now = Utils.now();
|
|
844
853
|
|
|
845
|
-
|
|
846
|
-
else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) { st.autoFollow = true; }
|
|
847
|
-
};
|
|
854
|
+
if (this.isFinalizedCode(codeEl)) { st.autoFollow = false; return; }
|
|
848
855
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
// Ensure code starts scrolled to bottom once after insert.
|
|
854
|
-
initCodeBottomOnce(codeEl) {
|
|
855
|
-
if (!codeEl || !codeEl.isConnected) return;
|
|
856
|
-
if (codeEl.dataset && codeEl.dataset._active_stream === '1') return;
|
|
857
|
-
if (codeEl.dataset && codeEl.dataset.csInitBtm === '1') return;
|
|
858
|
-
const wrapper = codeEl.closest('.code-wrapper');
|
|
859
|
-
if (!wrapper) return;
|
|
856
|
+
if (ev.deltaY < 0) { st.autoFollow = false; st.freezeUntil = now + 1000; }
|
|
857
|
+
else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) { st.autoFollow = true; }
|
|
858
|
+
};
|
|
860
859
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (!codeEl.isConnected) return;
|
|
865
|
-
try {
|
|
866
|
-
this.scrollToBottom(codeEl, false, true);
|
|
867
|
-
const st = this.state(codeEl);
|
|
868
|
-
st.autoFollow = false;
|
|
869
|
-
st.lastScrollTop = codeEl.scrollTop;
|
|
870
|
-
} catch (_) {}
|
|
871
|
-
}, 'CodeScroll', 0);
|
|
872
|
-
}
|
|
873
|
-
// Attach handlers to all bot code blocks under root (or document).
|
|
874
|
-
initScrollableBlocks(root) {
|
|
875
|
-
const scope = root || document;
|
|
876
|
-
let nodes = [];
|
|
877
|
-
if (scope.nodeType === 1 && scope.closest && scope.closest('.msg-box.msg-bot')) {
|
|
878
|
-
nodes = scope.querySelectorAll('pre code');
|
|
879
|
-
} else {
|
|
880
|
-
nodes = document.querySelectorAll('.msg-box.msg-bot pre code');
|
|
860
|
+
codeEl.addEventListener('scroll', onScroll, { passive: true });
|
|
861
|
+
codeEl.addEventListener('wheel', onWheel, { passive: true });
|
|
862
|
+
codeEl.addEventListener('touchstart', function () { st.userInteracted = true; }, { passive: true });
|
|
881
863
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
864
|
+
// Attach handlers to all bot code blocks under root (or document).
|
|
865
|
+
// IMPORTANT: We intentionally do NOT auto-scroll finalized/static code blocks to the bottom.
|
|
866
|
+
// Only actively streaming code blocks (data-_active_stream="1") are auto-followed live.
|
|
867
|
+
initScrollableBlocks(root) {
|
|
868
|
+
const scope = root || document;
|
|
869
|
+
let nodes = [];
|
|
870
|
+
if (scope.nodeType === 1 && scope.closest && scope.closest('.msg-box.msg-bot')) {
|
|
871
|
+
nodes = scope.querySelectorAll('pre code');
|
|
889
872
|
} else {
|
|
890
|
-
|
|
873
|
+
nodes = document.querySelectorAll('.msg-box.msg-bot pre code');
|
|
891
874
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
875
|
+
if (!nodes.length) return;
|
|
876
|
+
|
|
877
|
+
nodes.forEach((code) => {
|
|
878
|
+
this.attachHandlers(code);
|
|
879
|
+
// Live streaming blocks: enable auto-follow and keep them glued to bottom.
|
|
880
|
+
if (code.dataset._active_stream === '1') {
|
|
881
|
+
const st = this.state(code);
|
|
882
|
+
st.autoFollow = true;
|
|
883
|
+
this.scheduleScroll(code, true, false);
|
|
884
|
+
}
|
|
885
|
+
// Finalized/static blocks: do nothing (no initial scroll-to-bottom).
|
|
886
|
+
// This avoids surprising jumps when static content is rendered.
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
// Transfer stored scroll state between elements (after replace).
|
|
890
|
+
transfer(oldEl, newEl) {
|
|
891
|
+
if (!oldEl || !newEl || oldEl === newEl) return;
|
|
892
|
+
const oldState = this.map.get(oldEl);
|
|
893
|
+
if (oldState) this.map.set(newEl, { ...oldState });
|
|
894
|
+
this.attachHandlers(newEl);
|
|
895
|
+
}
|
|
896
|
+
// Cancel any scheduled scroll tasks for code blocks.
|
|
897
|
+
cancelAllScrolls() {
|
|
898
|
+
try { this.raf.cancelGroup('CodeScroll'); } catch (_) {}
|
|
899
|
+
this.rafMap = new WeakMap();
|
|
900
|
+
this.rafIds.clear();
|
|
901
|
+
}
|
|
906
902
|
}
|
|
907
|
-
}
|
|
908
903
|
|
|
909
904
|
// ==========================================================================
|
|
910
905
|
// 3) Highlighter (hljs) + rAF viewport scan
|
|
@@ -926,6 +921,20 @@
|
|
|
926
921
|
const hint = (cfg && cfg.RAF && cfg.RAF.FLUSH_BUDGET_MS) ? cfg.RAF.FLUSH_BUDGET_MS : 7;
|
|
927
922
|
this.SCAN_STEP_BUDGET_MS = Math.max(3, Math.min(12, hint));
|
|
928
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
|
+
}
|
|
929
938
|
// Global switch to skip all highlighting.
|
|
930
939
|
isDisabled() { return !!this.cfg.HL.DISABLE_ALL; }
|
|
931
940
|
// Configure hljs once (safe if hljs not present).
|
|
@@ -981,43 +990,44 @@
|
|
|
981
990
|
}
|
|
982
991
|
}
|
|
983
992
|
// Highlight a single code block with safety checks and scroll preservation.
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
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
|
+
}
|
|
990
998
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
// Prefer wrapper meta if available to avoid .textContent on huge nodes.
|
|
998
|
-
let lines = NaN, chars = NaN;
|
|
999
|
-
if (wrap) {
|
|
1000
|
-
const nlAttr = wrap.getAttribute('data-code-nl');
|
|
1001
|
-
const lenAttr = wrap.getAttribute('data-code-len');
|
|
1002
|
-
if (nlAttr) lines = parseInt(nlAttr, 10);
|
|
1003
|
-
if (lenAttr) chars = parseInt(lenAttr, 10);
|
|
1004
|
-
}
|
|
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;
|
|
1005
1005
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
codeEl.
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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 (_) {}
|
|
1015
1030
|
|
|
1016
|
-
// Fallback to reading actual text only if wrapper meta is missing.
|
|
1017
|
-
if (!Number.isFinite(lines) || !Number.isFinite(chars)) {
|
|
1018
|
-
const txt0 = codeEl.textContent || '';
|
|
1019
|
-
const ln0 = Utils.countNewlines(txt0);
|
|
1020
|
-
if ((maxLines > 0 && ln0 > maxLines) || (maxChars > 0 && txt0.length > maxChars)) {
|
|
1021
1031
|
codeEl.classList.add('hljs');
|
|
1022
1032
|
codeEl.setAttribute('data-highlighted', 'yes');
|
|
1023
1033
|
codeEl.dataset.finalHlSkip = '1';
|
|
@@ -1025,32 +1035,59 @@
|
|
|
1025
1035
|
this.codeScroll.scheduleScroll(codeEl, false, false);
|
|
1026
1036
|
return;
|
|
1027
1037
|
}
|
|
1028
|
-
}
|
|
1029
|
-
} catch (_) { /* safe fallback */ }
|
|
1030
1038
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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 (_) {}
|
|
1034
1051
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
+
}
|
|
1051
1089
|
}
|
|
1052
1090
|
}
|
|
1053
|
-
}
|
|
1054
1091
|
|
|
1055
1092
|
// Start a budgeted global scan – split across frames to avoid long blocking.
|
|
1056
1093
|
_startGlobalScan(activeCode) {
|
|
@@ -1170,880 +1207,1299 @@
|
|
|
1170
1207
|
}
|
|
1171
1208
|
}
|
|
1172
1209
|
|
|
1210
|
+
// ==========================================================================
|
|
1211
|
+
// 4) Custom Markup Processor
|
|
1212
|
+
// ==========================================================================
|
|
1213
|
+
|
|
1214
|
+
class CustomMarkup {
|
|
1215
|
+
constructor(cfg, logger) {
|
|
1216
|
+
this.cfg = cfg || { CUSTOM_MARKUP_RULES: [] };
|
|
1217
|
+
this.logger = logger || new Logger(cfg);
|
|
1218
|
+
this.__compiled = null;
|
|
1219
|
+
this.__hasStreamRules = false; // Fast flag to skip stream work if not needed
|
|
1220
|
+
}
|
|
1221
|
+
_d(line, ctx) { try { this.logger.debug('CM', line, ctx); } catch (_) {} }
|
|
1222
|
+
|
|
1223
|
+
// Decode HTML entities once (safe)
|
|
1224
|
+
// This addresses cases when linkify/full markdown path leaves literal """ etc. in text nodes.
|
|
1225
|
+
// We decode only for rules that explicitly opt-in (see compile()) to avoid changing semantics globally.
|
|
1226
|
+
decodeEntitiesOnce(s) {
|
|
1227
|
+
if (!s || s.indexOf('&') === -1) return String(s || '');
|
|
1228
|
+
const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
|
|
1229
|
+
ta.innerHTML = s;
|
|
1230
|
+
return ta.value;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Small helper: escape text to safe HTML (shared Utils or fallback)
|
|
1234
|
+
_escHtml(s) {
|
|
1235
|
+
try { return Utils.escapeHtml(s); } catch (_) {
|
|
1236
|
+
return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// quick check if any rule's open token is present in text (used to skip expensive work early)
|
|
1241
|
+
hasAnyOpenToken(text, rules) {
|
|
1242
|
+
if (!text || !rules || !rules.length) return false;
|
|
1243
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1244
|
+
const r = rules[i];
|
|
1245
|
+
if (!r || !r.open) continue;
|
|
1246
|
+
if (text.indexOf(r.open) !== -1) return true;
|
|
1247
|
+
}
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Build inner HTML from text according to rule's mode (markdown-inline | text) with optional entity decode.
|
|
1252
|
+
_materializeInnerHTML(rule, text, MD) {
|
|
1253
|
+
let payload = String(text || '');
|
|
1254
|
+
if (rule && rule.decodeEntities && payload && payload.indexOf('&') !== -1) {
|
|
1255
|
+
try { payload = this.decodeEntitiesOnce(payload); } catch (_) { /* keep original */ }
|
|
1256
|
+
}
|
|
1257
|
+
if (rule && rule.innerMode === 'markdown-inline' && MD && typeof MD.renderInline === 'function') {
|
|
1258
|
+
try { return MD.renderInline(payload); } catch (_) { return this._escHtml(payload); }
|
|
1259
|
+
}
|
|
1260
|
+
return this._escHtml(payload);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Make a DOM Fragment from HTML string (robust across contexts).
|
|
1264
|
+
_fragmentFromHTML(html, ctxNode) {
|
|
1265
|
+
let frag = null;
|
|
1266
|
+
try {
|
|
1267
|
+
const range = document.createRange();
|
|
1268
|
+
const ctx = (ctxNode && ctxNode.parentNode) ? ctxNode.parentNode : (document.body || document.documentElement);
|
|
1269
|
+
range.selectNode(ctx);
|
|
1270
|
+
frag = range.createContextualFragment(String(html || ''));
|
|
1271
|
+
return frag;
|
|
1272
|
+
} catch (_) {
|
|
1273
|
+
const tmp = document.createElement('div');
|
|
1274
|
+
tmp.innerHTML = String(html || '');
|
|
1275
|
+
frag = document.createDocumentFragment();
|
|
1276
|
+
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
1277
|
+
return frag;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Replace one element in DOM with HTML string (keeps siblings intact).
|
|
1282
|
+
_replaceElementWithHTML(el, html) {
|
|
1283
|
+
if (!el || !el.parentNode) return;
|
|
1284
|
+
const parent = el.parentNode;
|
|
1285
|
+
const frag = this._fragmentFromHTML(html, el);
|
|
1286
|
+
try {
|
|
1287
|
+
// Insert new nodes before the old element, then remove the old element (widely supported).
|
|
1288
|
+
parent.insertBefore(frag, el);
|
|
1289
|
+
parent.removeChild(el);
|
|
1290
|
+
} catch (_) {
|
|
1291
|
+
// Conservative fallback: wrap in a span if direct fragment insertion failed for some reason.
|
|
1292
|
+
const tmp = document.createElement('span');
|
|
1293
|
+
tmp.innerHTML = String(html || '');
|
|
1294
|
+
while (tmp.firstChild) parent.insertBefore(tmp.firstChild, el);
|
|
1295
|
+
parent.removeChild(el);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Compile rules once; also precompile strict and whitespace-tolerant "full match" regexes.
|
|
1300
|
+
compile(rules) {
|
|
1301
|
+
const src = Array.isArray(rules) ? rules : (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
|
|
1302
|
+
const compiled = [];
|
|
1303
|
+
let hasStream = false;
|
|
1304
|
+
|
|
1305
|
+
for (const r of src) {
|
|
1306
|
+
if (!r || typeof r.open !== 'string' || typeof r.close !== 'string') continue;
|
|
1307
|
+
|
|
1308
|
+
const tag = (r.tag || 'span').toLowerCase();
|
|
1309
|
+
const className = (r.className || r.class || '').trim();
|
|
1310
|
+
const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
|
|
1311
|
+
|
|
1312
|
+
const stream = !!(r.stream === true);
|
|
1313
|
+
const openReplace = String((r.openReplace != null ? r.openReplace : (r.openReplace || '')) || '');
|
|
1314
|
+
const closeReplace = String((r.closeReplace != null ? r.closeReplace : (r.closeReplace || '')) || '');
|
|
1315
|
+
|
|
1316
|
+
// Back-compat: decode entities default true for cmd-like
|
|
1317
|
+
const decodeEntities = (typeof r.decodeEntities === 'boolean')
|
|
1318
|
+
? r.decodeEntities
|
|
1319
|
+
: ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
|
|
1320
|
+
|
|
1321
|
+
// Optional application phase (where replacement should happen)
|
|
1322
|
+
// - 'source' => before markdown-it
|
|
1323
|
+
// - 'html' => after markdown-it (DOM fragment)
|
|
1324
|
+
// - 'both'
|
|
1325
|
+
let phaseRaw = (typeof r.phase === 'string') ? r.phase.toLowerCase() : '';
|
|
1326
|
+
if (phaseRaw !== 'source' && phaseRaw !== 'html' && phaseRaw !== 'both') phaseRaw = '';
|
|
1327
|
+
// Heuristic: if replacement contains fenced code backticks, default to 'source'
|
|
1328
|
+
const looksLikeFence = (openReplace.indexOf('```') !== -1) || (closeReplace.indexOf('```') !== -1);
|
|
1329
|
+
const phase = phaseRaw || (looksLikeFence ? 'source' : 'html');
|
|
1330
|
+
|
|
1331
|
+
const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
|
|
1332
|
+
const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
|
|
1333
|
+
const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
|
|
1334
|
+
|
|
1335
|
+
const item = {
|
|
1336
|
+
name: r.name || tag,
|
|
1337
|
+
tag, className, innerMode,
|
|
1338
|
+
open: r.open, close: r.close,
|
|
1339
|
+
decodeEntities,
|
|
1340
|
+
re, reFull, reFullTrim,
|
|
1341
|
+
stream,
|
|
1342
|
+
openReplace, closeReplace,
|
|
1343
|
+
phase, // NEW: where this rule should be applied
|
|
1344
|
+
isSourceFence: looksLikeFence // NEW: hints StreamEngine to treat as custom fence
|
|
1345
|
+
};
|
|
1346
|
+
compiled.push(item);
|
|
1347
|
+
if (stream) hasStream = true;
|
|
1348
|
+
this._d('COMPILE_RULE', { name: item.name, phase: item.phase, stream: item.stream });
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (compiled.length === 0) {
|
|
1352
|
+
const open = '[!cmd]', close = '[/!cmd]';
|
|
1353
|
+
const item = {
|
|
1354
|
+
name: 'cmd', tag: 'p', className: 'cmd', innerMode: 'text', open, close,
|
|
1355
|
+
decodeEntities: true,
|
|
1356
|
+
re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
|
|
1357
|
+
reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
|
|
1358
|
+
reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$'),
|
|
1359
|
+
stream: false,
|
|
1360
|
+
openReplace: '', closeReplace: '',
|
|
1361
|
+
phase: 'html', isSourceFence: false
|
|
1362
|
+
};
|
|
1363
|
+
compiled.push(item);
|
|
1364
|
+
this._d('COMPILE_RULE_FALLBACK', { name: item.name });
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
this.__hasStreamRules = hasStream;
|
|
1368
|
+
return compiled;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// pre-markdown source transformer – applies only rules for 'source'/'both' with replacements
|
|
1372
|
+
// IMPORTANT CHANGE:
|
|
1373
|
+
// - Skips replacements inside fenced code blocks (``` / ~~~).
|
|
1374
|
+
// - Applies only when the rule opener is at top-level of the line (no list markers/blockquote).
|
|
1375
|
+
transformSource(src, opts) {
|
|
1376
|
+
let s = String(src || '');
|
|
1377
|
+
this.ensureCompiled();
|
|
1378
|
+
const rules = this.__compiled;
|
|
1379
|
+
if (!rules || !rules.length) return s;
|
|
1380
|
+
|
|
1381
|
+
// Pick only source-phase rules with explicit replacements
|
|
1382
|
+
const candidates = [];
|
|
1383
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1384
|
+
const r = rules[i];
|
|
1385
|
+
if (!r) continue;
|
|
1386
|
+
if ((r.phase === 'source' || r.phase === 'both') && (r.openReplace || r.closeReplace)) candidates.push(r);
|
|
1387
|
+
}
|
|
1388
|
+
if (!candidates.length) return s;
|
|
1389
|
+
|
|
1390
|
+
// Compute fenced-code ranges once to exclude them from replacements (production-safe).
|
|
1391
|
+
const fences = this._findFenceRanges(s);
|
|
1392
|
+
if (!fences.length) {
|
|
1393
|
+
// No code fences in source; apply top-level guarded replacements globally.
|
|
1394
|
+
return this._applySourceReplacementsInChunk(s, s, 0, candidates);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Apply replacements only in segments outside fenced code.
|
|
1398
|
+
let out = '';
|
|
1399
|
+
let last = 0;
|
|
1400
|
+
for (let k = 0; k < fences.length; k++) {
|
|
1401
|
+
const [a, b] = fences[k];
|
|
1402
|
+
if (a > last) {
|
|
1403
|
+
const chunk = s.slice(last, a);
|
|
1404
|
+
out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
|
|
1405
|
+
}
|
|
1406
|
+
out += s.slice(a, b); // pass fenced code verbatim
|
|
1407
|
+
last = b;
|
|
1408
|
+
}
|
|
1409
|
+
if (last < s.length) {
|
|
1410
|
+
const tail = s.slice(last);
|
|
1411
|
+
out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
|
|
1412
|
+
}
|
|
1413
|
+
return out;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// expose custom fence specs (to StreamEngine)
|
|
1417
|
+
getSourceFenceSpecs() {
|
|
1418
|
+
this.ensureCompiled();
|
|
1419
|
+
const rules = this.__compiled || [];
|
|
1420
|
+
const out = [];
|
|
1421
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1422
|
+
const r = rules[i];
|
|
1423
|
+
if (!r || !r.isSourceFence) continue;
|
|
1424
|
+
// Only expose when they actually look like fences in source phase
|
|
1425
|
+
if (r.phase !== 'source' && r.phase !== 'both') continue;
|
|
1426
|
+
out.push({ open: r.open, close: r.close });
|
|
1427
|
+
}
|
|
1428
|
+
return out;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Ensure rules are compiled and cached.
|
|
1432
|
+
ensureCompiled() {
|
|
1433
|
+
if (!this.__compiled) {
|
|
1434
|
+
this.__compiled = this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
|
|
1435
|
+
this._d('ENSURE_COMPILED', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
|
|
1436
|
+
}
|
|
1437
|
+
return this.__compiled;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Replace rules set (also exposes rules on window).
|
|
1441
|
+
setRules(rules) {
|
|
1442
|
+
this.__compiled = this.compile(rules);
|
|
1443
|
+
window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() : (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
|
|
1444
|
+
this._d('SET_RULES', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Return current rules as array.
|
|
1448
|
+
getRules() {
|
|
1449
|
+
const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice()
|
|
1450
|
+
: (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
|
|
1451
|
+
this._d('GET_RULES', { count: list.length });
|
|
1452
|
+
return list;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Fast switch: do we have any rules that want streaming parsing?
|
|
1456
|
+
hasStreamRules() {
|
|
1457
|
+
this.ensureCompiled();
|
|
1458
|
+
return !!this.__hasStreamRules;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Context guards
|
|
1462
|
+
isInsideForbiddenContext(node) {
|
|
1463
|
+
const p = node.parentElement; if (!p) return true;
|
|
1464
|
+
// IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
|
|
1465
|
+
return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
|
|
1466
|
+
}
|
|
1467
|
+
isInsideForbiddenElement(el) {
|
|
1468
|
+
if (!el) return true;
|
|
1469
|
+
// IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
|
|
1470
|
+
return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Global finder on a single text blob (original per-text-node logic).
|
|
1474
|
+
findNextMatch(text, from, rules) {
|
|
1475
|
+
let best = null;
|
|
1476
|
+
for (const rule of rules) {
|
|
1477
|
+
rule.re.lastIndex = from;
|
|
1478
|
+
const m = rule.re.exec(text);
|
|
1479
|
+
if (m) {
|
|
1480
|
+
const start = m.index, end = rule.re.lastIndex;
|
|
1481
|
+
if (!best || start < best.start) best = { rule, start, end, inner: m[1] || '' };
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return best;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Strict full match of a pure text node (legacy path).
|
|
1488
|
+
findFullMatch(text, rules) {
|
|
1489
|
+
for (const rule of rules) {
|
|
1490
|
+
if (rule.reFull) {
|
|
1491
|
+
const m = rule.reFull.exec(text);
|
|
1492
|
+
if (m) return { rule, inner: m[1] || '' };
|
|
1493
|
+
} else {
|
|
1494
|
+
rule.re.lastIndex = 0;
|
|
1495
|
+
const m = rule.re.exec(text);
|
|
1496
|
+
if (m && m.index === 0 && (rule.re.lastIndex === text.length)) {
|
|
1497
|
+
const m2 = rule.re.exec(text);
|
|
1498
|
+
if (!m2) return { rule, inner: m[1] || '' };
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Set inner content according to the rule's mode, with optional entity decode (element mode).
|
|
1506
|
+
setInnerByMode(el, mode, text, MD, decodeEntities = false) {
|
|
1507
|
+
let payload = String(text || '');
|
|
1508
|
+
if (decodeEntities && payload && payload.indexOf('&') !== -1) {
|
|
1509
|
+
try { payload = this.decodeEntitiesOnce(payload); } catch (_) {}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (mode === 'markdown-inline' && typeof window.markdownit !== 'undefined') {
|
|
1513
|
+
try {
|
|
1514
|
+
if (MD && typeof MD.renderInline === 'function') { el.innerHTML = MD.renderInline(payload); return; }
|
|
1515
|
+
const tempMD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
|
|
1516
|
+
el.innerHTML = tempMD.renderInline(payload); return;
|
|
1517
|
+
} catch (_) {}
|
|
1518
|
+
}
|
|
1519
|
+
el.textContent = payload;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Try to replace an entire <p> that is a full custom markup match.
|
|
1523
|
+
_tryReplaceFullParagraph(el, rules, MD) {
|
|
1524
|
+
if (!el || el.tagName !== 'P') return false;
|
|
1525
|
+
if (this.isInsideForbiddenElement(el)) {
|
|
1526
|
+
this._d('P_SKIP_FORBIDDEN', { tag: el.tagName });
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
const t = el.textContent || '';
|
|
1530
|
+
if (!this.hasAnyOpenToken(t, rules)) return false;
|
|
1531
|
+
|
|
1532
|
+
for (const rule of rules) {
|
|
1533
|
+
if (!rule) continue;
|
|
1534
|
+
const m = rule.reFullTrim ? rule.reFullTrim.exec(t) : null;
|
|
1535
|
+
if (!m) continue;
|
|
1536
|
+
|
|
1537
|
+
const innerText = m[1] || '';
|
|
1538
|
+
|
|
1539
|
+
if (rule.phase !== 'html' && rule.phase !== 'both') continue; // element materialization is html-phase only
|
|
1540
|
+
|
|
1541
|
+
if (rule.openReplace || rule.closeReplace) {
|
|
1542
|
+
const innerHTML = this._materializeInnerHTML(rule, innerText, MD);
|
|
1543
|
+
const html = String(rule.openReplace || '') + innerHTML + String(rule.closeReplace || '');
|
|
1544
|
+
this._replaceElementWithHTML(el, html);
|
|
1545
|
+
this._d('P_REPLACED_AS_HTML', { rule: rule.name });
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const outTag = (rule.tag && typeof rule.tag === 'string') ? rule.tag.toLowerCase() : 'span';
|
|
1550
|
+
const out = document.createElement(outTag === 'p' ? 'p' : outTag);
|
|
1551
|
+
if (rule.className) out.className = rule.className;
|
|
1552
|
+
out.setAttribute('data-cm', rule.name);
|
|
1553
|
+
this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities);
|
|
1554
|
+
|
|
1555
|
+
try { el.replaceWith(out); } catch (_) {
|
|
1556
|
+
const par = el.parentNode; if (par) par.replaceChild(out, el);
|
|
1557
|
+
}
|
|
1558
|
+
this._d('P_REPLACED', { rule: rule.name, asTag: outTag });
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
this._d('P_NO_FULL_MATCH', { preview: this.logger.pv(t, 160) });
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Core implementation shared by static and streaming passes.
|
|
1566
|
+
applyRules(root, MD, rules) {
|
|
1567
|
+
if (!root || !rules || !rules.length) return;
|
|
1568
|
+
|
|
1569
|
+
const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
|
|
1570
|
+
|
|
1571
|
+
// Phase 1: tolerant <p> replacements
|
|
1572
|
+
try {
|
|
1573
|
+
const paragraphs = (typeof scope.querySelectorAll === 'function') ? scope.querySelectorAll('p') : [];
|
|
1574
|
+
this._d('P_TOLERANT_SCAN_START', { count: paragraphs.length });
|
|
1575
|
+
|
|
1576
|
+
if (paragraphs && paragraphs.length) {
|
|
1577
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
1578
|
+
const p = paragraphs[i];
|
|
1579
|
+
if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
|
|
1580
|
+
const tc = p && (p.textContent || '');
|
|
1581
|
+
if (!tc || !this.hasAnyOpenToken(tc, rules)) continue;
|
|
1582
|
+
// Skip paragraphs inside forbidden contexts (includes lists now)
|
|
1583
|
+
if (this.isInsideForbiddenElement(p)) continue;
|
|
1584
|
+
this._tryReplaceFullParagraph(p, rules, MD);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
} catch (e) {
|
|
1588
|
+
this._d('P_TOLERANT_SCAN_ERR', String(e));
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// Phase 2: legacy per-text-node pass for partial inline cases.
|
|
1592
|
+
const self = this;
|
|
1593
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
1594
|
+
acceptNode: (node) => {
|
|
1595
|
+
const val = node && node.nodeValue ? node.nodeValue : '';
|
|
1596
|
+
if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
|
|
1597
|
+
if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
|
|
1598
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
let node;
|
|
1603
|
+
while ((node = walker.nextNode())) {
|
|
1604
|
+
const text = node.nodeValue;
|
|
1605
|
+
if (!text || !this.hasAnyOpenToken(text, rules)) continue; // quick skip
|
|
1606
|
+
const parent = node.parentElement;
|
|
1607
|
+
|
|
1608
|
+
// Entire text node equals one full match and parent is <p>.
|
|
1609
|
+
if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
|
|
1610
|
+
const fm = this.findFullMatch(text, rules);
|
|
1611
|
+
if (fm) {
|
|
1612
|
+
// If explicit HTML replacements are provided, swap <p> for exact HTML (only for html/both phase).
|
|
1613
|
+
if ((fm.rule.phase === 'html' || fm.rule.phase === 'both') && (fm.rule.openReplace || fm.rule.closeReplace)) {
|
|
1614
|
+
const innerHTML = this._materializeInnerHTML(fm.rule, fm.inner, MD);
|
|
1615
|
+
const html = String(fm.rule.openReplace || '') + innerHTML + String(fm.rule.closeReplace || '');
|
|
1616
|
+
this._replaceElementWithHTML(parent, html);
|
|
1617
|
+
this._d('WALKER_FULL_REPLACE_HTML', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Backward-compatible: only replace as <p> when rule tag is 'p'
|
|
1622
|
+
if (fm.rule.tag === 'p') {
|
|
1623
|
+
const out = document.createElement('p');
|
|
1624
|
+
if (fm.rule.className) out.className = fm.rule.className;
|
|
1625
|
+
out.setAttribute('data-cm', fm.rule.name);
|
|
1626
|
+
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities);
|
|
1627
|
+
try { parent.replaceWith(out); } catch (_) {
|
|
1628
|
+
const par = parent.parentNode; if (par) par.replaceChild(out, parent);
|
|
1629
|
+
}
|
|
1630
|
+
this._d('WALKER_FULL_REPLACE', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// General inline replacement inside the text node (span-like or HTML-replace).
|
|
1637
|
+
let i = 0;
|
|
1638
|
+
let didReplace = false;
|
|
1639
|
+
const frag = document.createDocumentFragment();
|
|
1640
|
+
|
|
1641
|
+
while (i < text.length) {
|
|
1642
|
+
const m = this.findNextMatch(text, i, rules);
|
|
1643
|
+
if (!m) break;
|
|
1644
|
+
|
|
1645
|
+
if (m.start > i) {
|
|
1646
|
+
frag.appendChild(document.createTextNode(text.slice(i, m.start)));
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// If HTML replacements are provided, build exact HTML around processed inner – only for html/both phase.
|
|
1650
|
+
if ((m.rule.openReplace || m.rule.closeReplace) && (m.rule.phase === 'html' || m.rule.phase === 'both')) {
|
|
1651
|
+
const innerHTML = this._materializeInnerHTML(m.rule, m.inner, MD);
|
|
1652
|
+
const html = String(m.rule.openReplace || '') + innerHTML + String(m.rule.closeReplace || '');
|
|
1653
|
+
const part = this._fragmentFromHTML(html, node);
|
|
1654
|
+
frag.appendChild(part);
|
|
1655
|
+
this._d('WALKER_INLINE_MATCH_HTML', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1656
|
+
i = m.end; didReplace = true; continue;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// If rule is not html-phase, do NOT inject open/close replacements here (source-only rules are handled pre-md).
|
|
1660
|
+
if (m.rule.openReplace || m.rule.closeReplace) {
|
|
1661
|
+
// Source-only replacement met in DOM pass – keep original text verbatim for this match.
|
|
1662
|
+
frag.appendChild(document.createTextNode(text.slice(m.start, m.end)));
|
|
1663
|
+
this._d('WALKER_INLINE_SKIP_SOURCE_PHASE_HTML', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1664
|
+
i = m.end; didReplace = true; continue;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Element-based inline replacement (original behavior).
|
|
1668
|
+
const tag = (m.rule.tag === 'p') ? 'span' : m.rule.tag;
|
|
1669
|
+
const el = document.createElement(tag);
|
|
1670
|
+
if (m.rule.className) el.className = m.rule.className;
|
|
1671
|
+
el.setAttribute('data-cm', m.rule.name);
|
|
1672
|
+
this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities);
|
|
1673
|
+
frag.appendChild(el);
|
|
1674
|
+
this._d('WALKER_INLINE_MATCH', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1675
|
+
|
|
1676
|
+
i = m.end;
|
|
1677
|
+
didReplace = true;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (!didReplace) continue;
|
|
1681
|
+
|
|
1682
|
+
if (i < text.length) {
|
|
1683
|
+
frag.appendChild(document.createTextNode(text.slice(i)));
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const parentNode = node.parentNode;
|
|
1687
|
+
if (parentNode) {
|
|
1688
|
+
parentNode.replaceChild(frag, node);
|
|
1689
|
+
this._d('WALKER_INLINE_DONE', { preview: this.logger.pv(text, 120) });
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Public API: apply custom markup for full (static) paths – unchanged behavior.
|
|
1695
|
+
apply(root, MD) {
|
|
1696
|
+
this.ensureCompiled();
|
|
1697
|
+
this.applyRules(root, MD, this.__compiled);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// Public API: apply only stream-enabled rules (used in snapshots).
|
|
1701
|
+
applyStream(root, MD) {
|
|
1702
|
+
this.ensureCompiled();
|
|
1703
|
+
if (!this.__hasStreamRules) return;
|
|
1704
|
+
const rules = this.__compiled.filter(r => !!r.stream);
|
|
1705
|
+
if (!rules.length) return;
|
|
1706
|
+
this.applyRules(root, MD, rules);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// -----------------------------
|
|
1710
|
+
// INTERNAL HELPERS (NEW)
|
|
1711
|
+
// -----------------------------
|
|
1712
|
+
|
|
1713
|
+
// Scan source and return ranges [start, end) of fenced code blocks (``` or ~~~).
|
|
1714
|
+
// Matches Markdown fences at line-start with up to 3 spaces/tabs indentation.
|
|
1715
|
+
_findFenceRanges(s) {
|
|
1716
|
+
const ranges = [];
|
|
1717
|
+
const n = s.length;
|
|
1718
|
+
let i = 0;
|
|
1719
|
+
let inFence = false;
|
|
1720
|
+
let fenceMark = '';
|
|
1721
|
+
let fenceLen = 0;
|
|
1722
|
+
let startLineStart = 0;
|
|
1723
|
+
|
|
1724
|
+
while (i < n) {
|
|
1725
|
+
const lineStart = i;
|
|
1726
|
+
// Find line end and newline length
|
|
1727
|
+
let j = lineStart;
|
|
1728
|
+
while (j < n && s.charCodeAt(j) !== 10 && s.charCodeAt(j) !== 13) j++;
|
|
1729
|
+
const lineEnd = j;
|
|
1730
|
+
let nl = 0;
|
|
1731
|
+
if (j < n) {
|
|
1732
|
+
if (s.charCodeAt(j) === 13 && j + 1 < n && s.charCodeAt(j + 1) === 10) nl = 2;
|
|
1733
|
+
else nl = 1;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Compute indentation up to 3 "spaces" (tabs count as 1 here – safe heuristic)
|
|
1737
|
+
let k = lineStart;
|
|
1738
|
+
let indent = 0;
|
|
1739
|
+
while (k < lineEnd) {
|
|
1740
|
+
const c = s.charCodeAt(k);
|
|
1741
|
+
if (c === 32 /* space */) { indent++; if (indent > 3) break; k++; }
|
|
1742
|
+
else if (c === 9 /* tab */) { indent++; if (indent > 3) break; k++; }
|
|
1743
|
+
else break;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (!inFence) {
|
|
1747
|
+
if (indent <= 3 && k < lineEnd) {
|
|
1748
|
+
const ch = s.charCodeAt(k);
|
|
1749
|
+
if (ch === 0x60 /* ` */ || ch === 0x7E /* ~ */) {
|
|
1750
|
+
const mark = String.fromCharCode(ch);
|
|
1751
|
+
let m = k;
|
|
1752
|
+
while (m < lineEnd && s.charCodeAt(m) === ch) m++;
|
|
1753
|
+
const run = m - k;
|
|
1754
|
+
if (run >= 3) {
|
|
1755
|
+
inFence = true;
|
|
1756
|
+
fenceMark = mark;
|
|
1757
|
+
fenceLen = run;
|
|
1758
|
+
startLineStart = lineStart;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
} else {
|
|
1763
|
+
if (indent <= 3 && k < lineEnd && s.charCodeAt(k) === fenceMark.charCodeAt(0)) {
|
|
1764
|
+
let m = k;
|
|
1765
|
+
while (m < lineEnd && s.charCodeAt(m) === fenceMark.charCodeAt(0)) m++;
|
|
1766
|
+
const run = m - k;
|
|
1767
|
+
if (run >= fenceLen) {
|
|
1768
|
+
// Only whitespace is allowed after closing fence on the same line
|
|
1769
|
+
let onlyWS = true;
|
|
1770
|
+
for (let t = m; t < lineEnd; t++) {
|
|
1771
|
+
const cc = s.charCodeAt(t);
|
|
1772
|
+
if (cc !== 32 && cc !== 9) { onlyWS = false; break; }
|
|
1773
|
+
}
|
|
1774
|
+
if (onlyWS) {
|
|
1775
|
+
const endIdx = lineEnd + nl; // include trailing newline if present
|
|
1776
|
+
ranges.push([startLineStart, endIdx]);
|
|
1777
|
+
inFence = false; fenceMark = ''; fenceLen = 0; startLineStart = 0;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
i = lineEnd + nl;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// If EOF while still in fence, mark until end of string.
|
|
1786
|
+
if (inFence) ranges.push([startLineStart, n]);
|
|
1787
|
+
return ranges;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Check if match starts at "top-level" of a line:
|
|
1791
|
+
// - up to 3 leading spaces/tabs allowed
|
|
1792
|
+
// - not a list item marker ("- ", "+ ", "* ", "1. ", "1) ") and not a blockquote ("> ")
|
|
1793
|
+
// - nothing else precedes the token on the same line
|
|
1794
|
+
_isTopLevelLineInSource(s, absIdx) {
|
|
1795
|
+
let ls = absIdx;
|
|
1796
|
+
while (ls > 0) {
|
|
1797
|
+
const ch = s.charCodeAt(ls - 1);
|
|
1798
|
+
if (ch === 10 /* \n */ || ch === 13 /* \r */) break;
|
|
1799
|
+
ls--;
|
|
1800
|
+
}
|
|
1801
|
+
const prefix = s.slice(ls, absIdx);
|
|
1802
|
+
|
|
1803
|
+
// Strip up to 3 leading "spaces" (tabs treated as 1 – acceptable heuristic)
|
|
1804
|
+
let i = 0, indent = 0;
|
|
1805
|
+
while (i < prefix.length) {
|
|
1806
|
+
const c = prefix.charCodeAt(i);
|
|
1807
|
+
if (c === 32) { indent++; if (indent > 3) break; i++; }
|
|
1808
|
+
else if (c === 9) { indent++; if (indent > 3) break; i++; }
|
|
1809
|
+
else break;
|
|
1810
|
+
}
|
|
1811
|
+
if (indent > 3) return false;
|
|
1812
|
+
const rest = prefix.slice(i);
|
|
1813
|
+
|
|
1814
|
+
// Reject lists/blockquote
|
|
1815
|
+
if (/^>\s?/.test(rest)) return false;
|
|
1816
|
+
if (/^[-+*]\s/.test(rest)) return false;
|
|
1817
|
+
if (/^\d+[.)]\s/.test(rest)) return false;
|
|
1818
|
+
|
|
1819
|
+
// If any other non-whitespace text precedes the token on this line – not top-level
|
|
1820
|
+
if (rest.trim().length > 0) return false;
|
|
1821
|
+
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Apply source-phase replacements to one outside-of-fence chunk with top-level guard.
|
|
1826
|
+
_applySourceReplacementsInChunk(full, chunk, baseOffset, rules) {
|
|
1827
|
+
let t = chunk;
|
|
1828
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1829
|
+
const r = rules[i];
|
|
1830
|
+
if (!r || !(r.openReplace || r.closeReplace)) continue;
|
|
1831
|
+
try {
|
|
1832
|
+
r.re.lastIndex = 0;
|
|
1833
|
+
t = t.replace(r.re, (match, inner, offset /*, ...rest*/) => {
|
|
1834
|
+
const abs = baseOffset + (offset | 0);
|
|
1835
|
+
// Only apply when opener is at top-level on that line (not in lists/blockquote)
|
|
1836
|
+
if (!this._isTopLevelLineInSource(full, abs)) return match;
|
|
1837
|
+
const open = r.openReplace || '';
|
|
1838
|
+
const close = r.closeReplace || '';
|
|
1839
|
+
return open + (inner || '') + close;
|
|
1840
|
+
});
|
|
1841
|
+
} catch (_) { /* keep chunk as is on any error */ }
|
|
1842
|
+
}
|
|
1843
|
+
return t;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1173
1847
|
// ==========================================================================
|
|
1174
|
-
//
|
|
1848
|
+
// 5) Markdown runtime (markdown-it + code wrapper + math placeholders)
|
|
1175
1849
|
// ==========================================================================
|
|
1176
1850
|
|
|
1177
|
-
class
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const className = (r.className || r.class || '').trim();
|
|
1194
|
-
const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
|
|
1195
|
-
|
|
1196
|
-
const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
|
|
1197
|
-
const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
|
|
1198
|
-
const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
|
|
1199
|
-
|
|
1200
|
-
const item = { name: r.name || tag, tag, className, innerMode, open: r.open, close: r.close, re, reFull, reFullTrim };
|
|
1201
|
-
compiled.push(item);
|
|
1202
|
-
this._d('COMPILE_RULE', { name: item.name, tag: item.tag, innerMode: item.innerMode, open: item.open, close: item.close });
|
|
1203
|
-
}
|
|
1204
|
-
if (compiled.length === 0) {
|
|
1205
|
-
const open = '[!cmd]', close = '[/!cmd]';
|
|
1206
|
-
const item = {
|
|
1207
|
-
name: 'cmd', tag: 'p', className: 'cmd', innerMode: 'text', open, close,
|
|
1208
|
-
re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
|
|
1209
|
-
reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
|
|
1210
|
-
reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$')
|
|
1851
|
+
class MarkdownRenderer {
|
|
1852
|
+
constructor(cfg, customMarkup, logger, asyncer, raf) {
|
|
1853
|
+
this.cfg = cfg; this.customMarkup = customMarkup; this.MD = null;
|
|
1854
|
+
this.logger = logger || new Logger(cfg);
|
|
1855
|
+
// Cooperative async utilities available in renderer for heavy decode/render paths
|
|
1856
|
+
this.asyncer = asyncer || new AsyncRunner(cfg, raf);
|
|
1857
|
+
this.raf = raf || null;
|
|
1858
|
+
|
|
1859
|
+
// Fast-path streaming renderer without linkify to reduce regex work on hot path.
|
|
1860
|
+
this.MD_STREAM = null;
|
|
1861
|
+
|
|
1862
|
+
this.hooks = {
|
|
1863
|
+
observeNewCode: () => {},
|
|
1864
|
+
observeMsgBoxes: () => {},
|
|
1865
|
+
scheduleMathRender: () => {},
|
|
1866
|
+
codeScrollInit: () => {}
|
|
1211
1867
|
};
|
|
1212
|
-
compiled.push(item);
|
|
1213
|
-
this._d('COMPILE_RULE_FALLBACK', { name: item.name });
|
|
1214
|
-
}
|
|
1215
|
-
return compiled;
|
|
1216
|
-
}
|
|
1217
|
-
// Ensure rules are compiled and cached.
|
|
1218
|
-
ensureCompiled() {
|
|
1219
|
-
if (!this.__compiled) {
|
|
1220
|
-
this.__compiled = this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
|
|
1221
|
-
this._d('ENSURE_COMPILED', { count: this.__compiled.length });
|
|
1222
|
-
}
|
|
1223
|
-
return this.__compiled;
|
|
1224
|
-
}
|
|
1225
|
-
// Replace rules set (also exposes rules on window).
|
|
1226
|
-
setRules(rules) {
|
|
1227
|
-
this.__compiled = this.compile(rules);
|
|
1228
|
-
window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() : (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
|
|
1229
|
-
this._d('SET_RULES', { count: this.__compiled.length });
|
|
1230
|
-
}
|
|
1231
|
-
// Return current rules as array.
|
|
1232
|
-
getRules() {
|
|
1233
|
-
const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice()
|
|
1234
|
-
: (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
|
|
1235
|
-
this._d('GET_RULES', { count: list.length });
|
|
1236
|
-
return list;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// Context guards
|
|
1240
|
-
isInsideForbiddenContext(node) {
|
|
1241
|
-
const p = node.parentElement; if (!p) return true;
|
|
1242
|
-
return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper');
|
|
1243
|
-
}
|
|
1244
|
-
isInsideForbiddenElement(el) {
|
|
1245
|
-
if (!el) return true;
|
|
1246
|
-
return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper');
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Global finder on a single text blob (original per-text-node logic).
|
|
1250
|
-
findNextMatch(text, from, rules) {
|
|
1251
|
-
let best = null;
|
|
1252
|
-
for (const rule of rules) {
|
|
1253
|
-
rule.re.lastIndex = from;
|
|
1254
|
-
const m = rule.re.exec(text);
|
|
1255
|
-
if (m) {
|
|
1256
|
-
const start = m.index, end = rule.re.lastIndex;
|
|
1257
|
-
if (!best || start < best.start) best = { rule, start, end, inner: m[1] || '' };
|
|
1258
|
-
}
|
|
1259
1868
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const m = rule.re.exec(text);
|
|
1273
|
-
if (m && m.index === 0 && (rule.re.lastIndex === text.length)) {
|
|
1274
|
-
const m2 = rule.re.exec(text);
|
|
1275
|
-
if (!m2) return { rule, inner: m[1] || '' };
|
|
1276
|
-
}
|
|
1869
|
+
// Initialize markdown-it instances and plugins.
|
|
1870
|
+
init() {
|
|
1871
|
+
if (!window.markdownit) { this.logger.log('[MD] markdown-it not found – rendering skipped.'); return; }
|
|
1872
|
+
// Full renderer (used for non-hot paths, final results)
|
|
1873
|
+
this.MD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
|
|
1874
|
+
// Streaming renderer (no linkify) – hot path
|
|
1875
|
+
this.MD_STREAM = window.markdownit({ html: false, linkify: false, breaks: true, highlight: () => '' });
|
|
1876
|
+
|
|
1877
|
+
// SAFETY: disable CommonMark "indented code blocks" unless explicitly enabled.
|
|
1878
|
+
if (!this.cfg.MD || this.cfg.MD.ALLOW_INDENTED_CODE !== true) {
|
|
1879
|
+
try { this.MD.block.ruler.disable('code'); } catch (_) {}
|
|
1880
|
+
try { this.MD_STREAM.block.ruler.disable('code'); } catch (_) {}
|
|
1277
1881
|
}
|
|
1278
|
-
}
|
|
1279
|
-
return null;
|
|
1280
|
-
}
|
|
1281
1882
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
// This makes [!cmd] ... [/!cmd] (configured with tag: 'div') work even when linkify
|
|
1313
|
-
// inserted <a> tags inside the paragraph — detection is done on textContent, not DOM nodes.
|
|
1314
|
-
const outTag = (rule.tag && typeof rule.tag === 'string') ? rule.tag.toLowerCase() : 'span';
|
|
1315
|
-
const out = document.createElement(outTag === 'p' ? 'p' : outTag);
|
|
1316
|
-
if (rule.className) out.className = rule.className;
|
|
1317
|
-
out.setAttribute('data-cm', rule.name);
|
|
1318
|
-
|
|
1319
|
-
const innerText = m[1] || '';
|
|
1320
|
-
// Use mode-driven inner content materialization (text or markdown-inline).
|
|
1321
|
-
this.setInnerByMode(out, rule.innerMode, innerText, MD);
|
|
1322
|
-
|
|
1323
|
-
// Replace the original <p> with the desired container (<div>, <think>, <p>, etc.).
|
|
1324
|
-
try { el.replaceWith(out); } catch (_) {
|
|
1325
|
-
const parent = el.parentNode; if (parent) parent.replaceChild(out, el);
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
this._d('P_REPLACED', { rule: rule.name, asTag: outTag, preview: this.logger.pv(t, 160) });
|
|
1329
|
-
return true;
|
|
1330
|
-
}
|
|
1331
|
-
this._d('P_NO_FULL_MATCH', { preview: this.logger.pv(t, 160) });
|
|
1332
|
-
return false;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// Apply custom markup with two-phase strategy:
|
|
1336
|
-
// 1) Full-paragraph tolerant pass (survives linkify splitting).
|
|
1337
|
-
// 2) Legacy per-text-node pass for partial inline cases.
|
|
1338
|
-
apply(root, MD) {
|
|
1339
|
-
this.ensureCompiled();
|
|
1340
|
-
const rules = this.__compiled;
|
|
1341
|
-
if (!root || !rules || !rules.length) return;
|
|
1342
|
-
|
|
1343
|
-
const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
|
|
1344
|
-
try {
|
|
1345
|
-
const paragraphs = (typeof scope.querySelectorAll === 'function') ? scope.querySelectorAll('p') : [];
|
|
1346
|
-
this._d('P_TOLERANT_SCAN_START', { count: paragraphs.length });
|
|
1347
|
-
|
|
1348
|
-
if (paragraphs && paragraphs.length) {
|
|
1349
|
-
for (let i = 0; i < paragraphs.length; i++) {
|
|
1350
|
-
const p = paragraphs[i];
|
|
1351
|
-
if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
|
|
1352
|
-
// Quick check: avoid work if no marker in entire <p>
|
|
1353
|
-
const tc = p && (p.textContent || '');
|
|
1354
|
-
if (!tc || tc.indexOf('[!') === -1) continue;
|
|
1355
|
-
this._tryReplaceFullParagraph(p, rules, MD);
|
|
1883
|
+
const escapeHtml = Utils.escapeHtml;
|
|
1884
|
+
|
|
1885
|
+
// Dollar and bracket math placeholder plugins: generate lightweight placeholders to be picked up by KaTeX later.
|
|
1886
|
+
const mathDollarPlaceholderPlugin = (md) => {
|
|
1887
|
+
function notEscaped(src, pos) { let back = 0; while (pos - back - 1 >= 0 && src.charCodeAt(pos - back - 1) === 0x5C) back++; return (back % 2) === 0; }
|
|
1888
|
+
function math_block_dollar(state, startLine, endLine, silent) {
|
|
1889
|
+
const pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
1890
|
+
const max = state.eMarks[startLine];
|
|
1891
|
+
if (pos + 1 >= max) return false;
|
|
1892
|
+
if (state.src.charCodeAt(pos) !== 0x24 || state.src.charCodeAt(pos + 1) !== 0x24) return false;
|
|
1893
|
+
let nextLine = startLine + 1, found = false;
|
|
1894
|
+
for (; nextLine < endLine; nextLine++) {
|
|
1895
|
+
let p = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
1896
|
+
const pe = state.eMarks[nextLine];
|
|
1897
|
+
if (p + 1 < pe && state.src.charCodeAt(p) === 0x24 && state.src.charCodeAt(p + 1) === 0x24) { found = true; break; }
|
|
1898
|
+
}
|
|
1899
|
+
if (!found) return false;
|
|
1900
|
+
if (silent) return true;
|
|
1901
|
+
|
|
1902
|
+
const contentStart = state.bMarks[startLine] + state.tShift[startLine] + 2;
|
|
1903
|
+
const contentEndLine = nextLine - 1;
|
|
1904
|
+
let content = '';
|
|
1905
|
+
if (contentEndLine >= startLine + 1) {
|
|
1906
|
+
const startIdx = state.bMarks[startLine + 1];
|
|
1907
|
+
const endIdx = state.eMarks[contentEndLine];
|
|
1908
|
+
content = state.src.slice(startIdx, endIdx);
|
|
1909
|
+
} else content = '';
|
|
1910
|
+
|
|
1911
|
+
const token = state.push('math_block_dollar', '', 0);
|
|
1912
|
+
token.block = true; token.content = content; state.line = nextLine + 1; return true;
|
|
1356
1913
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1914
|
+
function math_inline_dollar(state, silent) {
|
|
1915
|
+
const pos = state.pos, src = state.src, max = state.posMax;
|
|
1916
|
+
if (pos >= max) return false;
|
|
1917
|
+
if (src.charCodeAt(pos) !== 0x24) return false;
|
|
1918
|
+
if (pos + 1 < max && src.charCodeAt(pos + 1) === 0x24) return false;
|
|
1919
|
+
const after = pos + 1 < max ? src.charCodeAt(pos + 1) : 0;
|
|
1920
|
+
if (after === 0x20 || after === 0x0A || after === 0x0D) return false;
|
|
1921
|
+
let i = pos + 1;
|
|
1922
|
+
while (i < max) {
|
|
1923
|
+
const ch = src.charCodeAt(i);
|
|
1924
|
+
if (ch === 0x24 && notEscaped(src, i)) {
|
|
1925
|
+
const before = i - 1 >= 0 ? src.charCodeAt(i - 1) : 0;
|
|
1926
|
+
if (before === 0x20 || before === 0x0A || before === 0x0D) { i++; continue; }
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1929
|
+
i++;
|
|
1930
|
+
}
|
|
1931
|
+
if (i >= max || src.charCodeAt(i) !== 0x24) return false;
|
|
1369
1932
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
if (!text || text.indexOf('[!') === -1) continue;
|
|
1374
|
-
|
|
1375
|
-
const parent = node.parentElement;
|
|
1376
|
-
|
|
1377
|
-
// Entire text node equals one full match and parent is <p>.
|
|
1378
|
-
if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
|
|
1379
|
-
const fm = this.findFullMatch(text, rules);
|
|
1380
|
-
if (fm && fm.rule.tag === 'p') {
|
|
1381
|
-
const out = document.createElement('p');
|
|
1382
|
-
if (fm.rule.className) out.className = fm.rule.className;
|
|
1383
|
-
out.setAttribute('data-cm', fm.rule.name);
|
|
1384
|
-
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD);
|
|
1385
|
-
try { parent.replaceWith(out); } catch (_) {
|
|
1386
|
-
const par = parent.parentNode; if (par) par.replaceChild(out, parent);
|
|
1933
|
+
if (!silent) {
|
|
1934
|
+
const token = state.push('math_inline_dollar', '', 0);
|
|
1935
|
+
token.block = false; token.content = src.slice(pos + 1, i);
|
|
1387
1936
|
}
|
|
1388
|
-
|
|
1389
|
-
continue;
|
|
1937
|
+
state.pos = i + 1; return true;
|
|
1390
1938
|
}
|
|
1391
|
-
}
|
|
1392
1939
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1940
|
+
md.block.ruler.before('fence', 'math_block_dollar', math_block_dollar, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
|
|
1941
|
+
md.inline.ruler.before('escape', 'math_inline_dollar', math_inline_dollar);
|
|
1942
|
+
|
|
1943
|
+
md.renderer.rules.math_inline_dollar = (tokens, idx) => {
|
|
1944
|
+
const tex = tokens[idx].content || '';
|
|
1945
|
+
return `<span class="math-pending" data-display="0"><span class="math-fallback">$${escapeHtml(tex)}$</span><script type="math/tex">${escapeHtml(tex)}</script></span>`;
|
|
1946
|
+
};
|
|
1947
|
+
md.renderer.rules.math_block_dollar = (tokens, idx) => {
|
|
1948
|
+
const tex = tokens[idx].content || '';
|
|
1949
|
+
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>`;
|
|
1950
|
+
};
|
|
1951
|
+
};
|
|
1401
1952
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1953
|
+
const mathBracketsPlaceholderPlugin = (md) => {
|
|
1954
|
+
function math_brackets(state, silent) {
|
|
1955
|
+
const src = state.src, pos = state.pos, max = state.posMax;
|
|
1956
|
+
if (pos + 1 >= max || src.charCodeAt(pos) !== 0x5C) return false;
|
|
1957
|
+
const next = src.charCodeAt(pos + 1);
|
|
1958
|
+
if (next !== 0x28 && next !== 0x5B) return false;
|
|
1959
|
+
const isInline = (next === 0x28); const close = isInline ? '\\)' : '\\]';
|
|
1960
|
+
const start = pos + 2; const end = src.indexOf(close, start);
|
|
1961
|
+
if (end < 0) return false;
|
|
1962
|
+
const content = src.slice(start, end);
|
|
1963
|
+
if (!silent) {
|
|
1964
|
+
const t = state.push(isInline ? 'math_inline_bracket' : 'math_block_bracket', '', 0);
|
|
1965
|
+
t.content = content; t.block = !isInline;
|
|
1966
|
+
}
|
|
1967
|
+
state.pos = end + 2; return true;
|
|
1404
1968
|
}
|
|
1969
|
+
md.inline.ruler.before('escape', 'math_brackets', math_brackets);
|
|
1970
|
+
md.renderer.rules.math_inline_bracket = (tokens, idx) => {
|
|
1971
|
+
const tex = tokens[idx].content || '';
|
|
1972
|
+
return `<span class="math-pending" data-display="0"><span class="math-fallback">\\(${escapeHtml(tex)}\\)</span><script type="math/tex">${escapeHtml(tex)}</script></span>`;
|
|
1973
|
+
};
|
|
1974
|
+
md.renderer.rules.math_block_bracket = (tokens, idx) => {
|
|
1975
|
+
const tex = tokens[idx].content || '';
|
|
1976
|
+
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>`;
|
|
1977
|
+
};
|
|
1978
|
+
};
|
|
1405
1979
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1980
|
+
this.MD.use(mathDollarPlaceholderPlugin);
|
|
1981
|
+
this.MD.use(mathBracketsPlaceholderPlugin);
|
|
1982
|
+
this.MD_STREAM.use(mathDollarPlaceholderPlugin);
|
|
1983
|
+
this.MD_STREAM.use(mathBracketsPlaceholderPlugin);
|
|
1984
|
+
|
|
1985
|
+
const cfg = this.cfg; const logger = this.logger;
|
|
1986
|
+
|
|
1987
|
+
// STREAMING wrapper plugin (modified header label guard)
|
|
1988
|
+
(function codeWrapperPlugin(md, logger) {
|
|
1989
|
+
let CODE_IDX = 1;
|
|
1990
|
+
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
1991
|
+
|
|
1992
|
+
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
1993
|
+
const seenFP = new Set();
|
|
1994
|
+
const makeFP = (info, raw) => {
|
|
1995
|
+
const head = (raw || '').slice(0, 96);
|
|
1996
|
+
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
const ALIAS = {
|
|
2000
|
+
txt: 'plaintext', text: 'plaintext', plaintext: 'plaintext',
|
|
2001
|
+
sh: 'bash', shell: 'bash', zsh: 'bash', 'shell-session': 'bash',
|
|
2002
|
+
py: 'python', python3: 'python', py3: 'python',
|
|
2003
|
+
js: 'javascript', node: 'javascript', nodejs: 'javascript',
|
|
2004
|
+
ts: 'typescript', 'ts-node': 'typescript',
|
|
2005
|
+
yml: 'yaml', kt: 'kotlin', rs: 'rust',
|
|
2006
|
+
csharp: 'csharp', 'c#': 'csharp', 'c++': 'cpp',
|
|
2007
|
+
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
2008
|
+
docker: 'dockerfile'
|
|
2009
|
+
};
|
|
2010
|
+
function normLang(s) { if (!s) return ''; const v = String(s).trim().toLowerCase(); return ALIAS[v] || v; }
|
|
2011
|
+
function isSupportedByHLJS(lang) { try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; } }
|
|
2012
|
+
function classForHighlight(lang) { if (!lang) return 'plaintext'; return isSupportedByHLJS(lang) ? lang : 'plaintext'; }
|
|
2013
|
+
function stripBOM(s) { return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s; }
|
|
2014
|
+
|
|
2015
|
+
function detectFromFirstLine(raw, rid) {
|
|
2016
|
+
if (!raw) return { lang: '', content: raw, isOutput: false };
|
|
2017
|
+
const lines = raw.split(/\r?\n/);
|
|
2018
|
+
if (!lines.length) return { lang: '', content: raw, isOutput: false };
|
|
2019
|
+
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
2020
|
+
if (i >= lines.length) { log(`#${rid} first-line: only whitespace`); return { lang: '', content: raw, isOutput: false }; }
|
|
2021
|
+
let first = stripBOM(lines[i]).trim();
|
|
2022
|
+
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
2023
|
+
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
2024
|
+
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) { log(`#${rid} first-line: no token match`, { first }); return { lang: '', content: raw, isOutput: false }; }
|
|
2025
|
+
let cand = normLang(token);
|
|
2026
|
+
if (cand === 'output') {
|
|
2027
|
+
const content = lines.slice(i + 1).join('\n');
|
|
2028
|
+
log(`#${rid} first-line: output header`);
|
|
2029
|
+
return { lang: 'python', headerLabel: 'output', content, isOutput: true };
|
|
2030
|
+
}
|
|
2031
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
2032
|
+
if (!rest.trim()) { log(`#${rid} first-line: directive but no content after, ignore`, { cand }); return { lang: '', content: raw, isOutput: false }; }
|
|
2033
|
+
log(`#${rid} first-line: directive accepted`, { cand, restLen: rest.length, hljs: isSupportedByHLJS(cand) });
|
|
2034
|
+
return { lang: cand, headerLabel: cand, content: rest, isOutput: false };
|
|
2035
|
+
}
|
|
1423
2036
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
parentNode.replaceChild(frag, node);
|
|
1427
|
-
this._d('WALKER_INLINE_DONE', { preview: this.logger.pv(text, 120) });
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
2037
|
+
md.renderer.rules.fence = (tokens, idx) => renderFence(tokens[idx]);
|
|
2038
|
+
md.renderer.rules.code_block = (tokens, idx) => renderFence({ info: '', content: tokens[idx].content || '' });
|
|
1432
2039
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
2040
|
+
function resolveLanguageAndContent(info, raw, rid) {
|
|
2041
|
+
const infoLangRaw = (info || '').trim().split(/\s+/)[0] || '';
|
|
2042
|
+
let cand = normLang(infoLangRaw);
|
|
2043
|
+
if (cand === 'output') {
|
|
2044
|
+
log(`#${rid} info: output header`);
|
|
2045
|
+
return { lang: 'python', headerLabel: 'output', content: raw, isOutput: true };
|
|
2046
|
+
}
|
|
2047
|
+
if (cand) {
|
|
2048
|
+
log(`#${rid} info: token`, { infoLangRaw, cand, hljs: isSupportedByHLJS(cand) });
|
|
2049
|
+
return { lang: cand, headerLabel: cand, content: raw, isOutput: false };
|
|
2050
|
+
}
|
|
2051
|
+
const det = detectFromFirstLine(raw, rid);
|
|
2052
|
+
if (det && (det.lang || det.isOutput)) return det;
|
|
2053
|
+
log(`#${rid} resolve: fallback`);
|
|
2054
|
+
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
2055
|
+
}
|
|
1436
2056
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2057
|
+
function renderFence(token) {
|
|
2058
|
+
const raw = token.content || '';
|
|
2059
|
+
const rid = String(CODE_IDX + '');
|
|
2060
|
+
const fp = makeFP(token.info || '', raw);
|
|
2061
|
+
const canLog = !DEDUP || !seenFP.has(fp);
|
|
2062
|
+
if (canLog) log(`FENCE_ENTER #${rid}`, { info: (token.info || ''), rawHead: logger.pv(raw) });
|
|
2063
|
+
|
|
2064
|
+
const res = resolveLanguageAndContent(token.info || '', raw, rid);
|
|
2065
|
+
const isOutput = !!res.isOutput;
|
|
2066
|
+
|
|
2067
|
+
// Choose class and a safe header label (avoid 'on', 'ml', 's' etc.)
|
|
2068
|
+
const rawToken = (res.lang || '').trim();
|
|
2069
|
+
const langClass = isOutput ? 'python' : classForHighlight(rawToken);
|
|
2070
|
+
|
|
2071
|
+
// Guard against tiny unsupported tokens – show temporary 'code' instead of partial suffix.
|
|
2072
|
+
let headerLabel = isOutput ? 'output' : (res.headerLabel || (rawToken || 'code'));
|
|
2073
|
+
if (!isOutput) {
|
|
2074
|
+
if (rawToken && !isSupportedByHLJS(rawToken) && rawToken.length < 3) {
|
|
2075
|
+
headerLabel = 'code';
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
1444
2078
|
|
|
1445
|
-
|
|
1446
|
-
|
|
2079
|
+
if (canLog) {
|
|
2080
|
+
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
2081
|
+
if (DEDUP) seenFP.add(fp);
|
|
2082
|
+
}
|
|
1447
2083
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
for (; nextLine < endLine; nextLine++) {
|
|
1481
|
-
let p = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
1482
|
-
const pe = state.eMarks[nextLine];
|
|
1483
|
-
if (p + 1 < pe && state.src.charCodeAt(p) === 0x24 && state.src.charCodeAt(p + 1) === 0x24) { found = true; break; }
|
|
2084
|
+
// precompute code meta to avoid expensive .textContent on next phases
|
|
2085
|
+
const content = res.content || '';
|
|
2086
|
+
const len = content.length;
|
|
2087
|
+
const head = content.slice(0, 64);
|
|
2088
|
+
const tail = content.slice(-64);
|
|
2089
|
+
const headEsc = Utils.escapeHtml(head);
|
|
2090
|
+
const tailEsc = Utils.escapeHtml(tail);
|
|
2091
|
+
// Note: for full renderer we will also persist data-code-nl (see below).
|
|
2092
|
+
|
|
2093
|
+
const inner = Utils.escapeHtml(content);
|
|
2094
|
+
const idxLocal = CODE_IDX++;
|
|
2095
|
+
|
|
2096
|
+
let actions = '';
|
|
2097
|
+
if (langClass === 'html') {
|
|
2098
|
+
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>`;
|
|
2099
|
+
} else if (langClass === 'python' && headerLabel !== 'output') {
|
|
2100
|
+
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>`;
|
|
2101
|
+
}
|
|
2102
|
+
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>`;
|
|
2103
|
+
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>`;
|
|
2104
|
+
|
|
2105
|
+
// attach precomputed meta (len/head/tail) on wrapper for downstream optimizations
|
|
2106
|
+
return (
|
|
2107
|
+
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
2108
|
+
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
2109
|
+
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}"` + // meta (no nl here – only in full renderer)
|
|
2110
|
+
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
2111
|
+
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
2112
|
+
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
2113
|
+
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
2114
|
+
`</div>`
|
|
2115
|
+
);
|
|
1484
2116
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
let
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2117
|
+
})(this.MD_STREAM, this.logger);
|
|
2118
|
+
|
|
2119
|
+
// FULL renderer wrapper plugin (modified header label guard)
|
|
2120
|
+
(function codeWrapperPlugin(md, logger) {
|
|
2121
|
+
// identical core logic – augmented with data-code-nl for full renderer
|
|
2122
|
+
let CODE_IDX = 1;
|
|
2123
|
+
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
2124
|
+
|
|
2125
|
+
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
2126
|
+
const seenFP = new Set();
|
|
2127
|
+
const makeFP = (info, raw) => {
|
|
2128
|
+
const head = (raw || '').slice(0, 96);
|
|
2129
|
+
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
2130
|
+
};
|
|
2131
|
+
|
|
2132
|
+
const ALIAS = {
|
|
2133
|
+
txt: 'plaintext', text: 'plaintext', plaintext: 'plaintext',
|
|
2134
|
+
sh: 'bash', shell: 'bash', zsh: 'bash', 'shell-session': 'bash',
|
|
2135
|
+
py: 'python', python3: 'python', py3: 'python',
|
|
2136
|
+
js: 'javascript', node: 'javascript', nodejs: 'javascript',
|
|
2137
|
+
ts: 'typescript', 'ts-node': 'typescript',
|
|
2138
|
+
yml: 'yaml', kt: 'kotlin', rs: 'rust',
|
|
2139
|
+
csharp: 'csharp', 'c#': 'csharp', 'c++': 'cpp',
|
|
2140
|
+
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
2141
|
+
docker: 'dockerfile'
|
|
2142
|
+
};
|
|
2143
|
+
function normLang(s) { if (!s) return ''; const v = String(s).trim().toLowerCase(); return ALIAS[v] || v; }
|
|
2144
|
+
function isSupportedByHLJS(lang) { try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; } }
|
|
2145
|
+
function classForHighlight(lang) { if (!lang) return 'plaintext'; return isSupportedByHLJS(lang) ? lang : 'plaintext'; }
|
|
2146
|
+
function stripBOM(s) { return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s; }
|
|
2147
|
+
|
|
2148
|
+
function detectFromFirstLine(raw, rid) {
|
|
2149
|
+
if (!raw) return { lang: '', content: raw, isOutput: false };
|
|
2150
|
+
const lines = raw.split(/\r?\n/);
|
|
2151
|
+
if (!lines.length) return { lang: '', content: raw, isOutput: false };
|
|
2152
|
+
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
2153
|
+
if (i >= lines.length) { log(`#${rid} first-line: only whitespace`); return { lang: '', content: raw, isOutput: false }; }
|
|
2154
|
+
let first = stripBOM(lines[i]).trim();
|
|
2155
|
+
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
2156
|
+
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
2157
|
+
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) { log(`#${rid} first-line: no token match`, { first }); return { lang: '', content: raw, isOutput: false }; }
|
|
2158
|
+
let cand = normLang(token);
|
|
2159
|
+
if (cand === 'output') {
|
|
2160
|
+
const content = lines.slice(i + 1).join('\n');
|
|
2161
|
+
log(`#${rid} first-line: output header`);
|
|
2162
|
+
return { lang: 'python', headerLabel: 'output', content, isOutput: true };
|
|
1514
2163
|
}
|
|
1515
|
-
i
|
|
2164
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
2165
|
+
if (!rest.trim()) { log(`#${rid} first-line: directive but no content after, ignore`, { cand }); return { lang: '', content: raw, isOutput: false }; }
|
|
2166
|
+
log(`#${rid} first-line: directive accepted`, { cand, restLen: rest.length, hljs: isSupportedByHLJS(cand) });
|
|
2167
|
+
return { lang: cand, headerLabel: cand, content: rest, isOutput: false };
|
|
1516
2168
|
}
|
|
1517
|
-
if (i >= max || src.charCodeAt(i) !== 0x24) return false;
|
|
1518
2169
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2170
|
+
md.renderer.rules.fence = (tokens, idx) => renderFence(tokens[idx]);
|
|
2171
|
+
md.renderer.rules.code_block = (tokens, idx) => renderFence({ info: '', content: tokens[idx].content || '' });
|
|
2172
|
+
|
|
2173
|
+
function resolveLanguageAndContent(info, raw, rid) {
|
|
2174
|
+
const infoLangRaw = (info || '').trim().split(/\s+/)[0] || '';
|
|
2175
|
+
let cand = normLang(infoLangRaw);
|
|
2176
|
+
if (cand === 'output') {
|
|
2177
|
+
log(`#${rid} info: output header`);
|
|
2178
|
+
return { lang: 'python', headerLabel: 'output', content: raw, isOutput: true };
|
|
2179
|
+
}
|
|
2180
|
+
if (cand) {
|
|
2181
|
+
log(`#${rid} info: token`, { infoLangRaw, cand, hljs: isSupportedByHLJS(cand) });
|
|
2182
|
+
return { lang: cand, headerLabel: cand, content: raw, isOutput: false };
|
|
2183
|
+
}
|
|
2184
|
+
const det = detectFromFirstLine(raw, rid);
|
|
2185
|
+
if (det && (det.lang || det.isOutput)) return det;
|
|
2186
|
+
log(`#${rid} resolve: fallback`);
|
|
2187
|
+
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
1522
2188
|
}
|
|
1523
|
-
state.pos = i + 1; return true;
|
|
1524
|
-
}
|
|
1525
2189
|
|
|
1526
|
-
|
|
1527
|
-
|
|
2190
|
+
function renderFence(token) {
|
|
2191
|
+
const raw = token.content || '';
|
|
2192
|
+
const rid = String(CODE_IDX + '');
|
|
2193
|
+
const fp = makeFP(token.info || '', raw);
|
|
2194
|
+
const canLog = !DEDUP || !seenFP.has(fp);
|
|
2195
|
+
if (canLog) log(`FENCE_ENTER #${rid}`, { info: (token.info || ''), rawHead: logger.pv(raw) });
|
|
2196
|
+
|
|
2197
|
+
const res = resolveLanguageAndContent(token.info || '', raw, rid);
|
|
2198
|
+
const isOutput = !!res.isOutput;
|
|
2199
|
+
|
|
2200
|
+
// Choose class and a safe header label (avoid 'on', 'ml', 's' etc.)
|
|
2201
|
+
const rawToken = (res.lang || '').trim();
|
|
2202
|
+
const langClass = isOutput ? 'python' : classForHighlight(rawToken);
|
|
2203
|
+
|
|
2204
|
+
// Guard against tiny unsupported tokens – show temporary 'code' instead of partial suffix.
|
|
2205
|
+
let headerLabel = isOutput ? 'output' : (res.headerLabel || (rawToken || 'code'));
|
|
2206
|
+
if (!isOutput) {
|
|
2207
|
+
if (rawToken && !isSupportedByHLJS(rawToken) && rawToken.length < 3) {
|
|
2208
|
+
headerLabel = 'code';
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
1528
2211
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
md.renderer.rules.math_block_dollar = (tokens, idx) => {
|
|
1534
|
-
const tex = tokens[idx].content || '';
|
|
1535
|
-
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>`;
|
|
1536
|
-
};
|
|
1537
|
-
};
|
|
2212
|
+
if (canLog) {
|
|
2213
|
+
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
2214
|
+
if (DEDUP) seenFP.add(fp);
|
|
2215
|
+
}
|
|
1538
2216
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
2217
|
+
// precompute code meta
|
|
2218
|
+
const content = res.content || '';
|
|
2219
|
+
const len = content.length;
|
|
2220
|
+
const head = content.slice(0, 64);
|
|
2221
|
+
const tail = content.slice(-64);
|
|
2222
|
+
const headEsc = Utils.escapeHtml(head);
|
|
2223
|
+
const tailEsc = Utils.escapeHtml(tail);
|
|
2224
|
+
const nl = Utils.countNewlines(content);
|
|
2225
|
+
|
|
2226
|
+
const inner = Utils.escapeHtml(content);
|
|
2227
|
+
const idxLocal = CODE_IDX++;
|
|
2228
|
+
|
|
2229
|
+
let actions = '';
|
|
2230
|
+
if (langClass === 'html') {
|
|
2231
|
+
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>`;
|
|
2232
|
+
} else if (langClass === 'python' && headerLabel !== 'output') {
|
|
2233
|
+
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>`;
|
|
2234
|
+
}
|
|
2235
|
+
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>`;
|
|
2236
|
+
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>`;
|
|
2237
|
+
|
|
2238
|
+
return (
|
|
2239
|
+
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
2240
|
+
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
2241
|
+
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}" data-code-nl="${String(nl)}"` +
|
|
2242
|
+
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
2243
|
+
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
2244
|
+
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
2245
|
+
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
2246
|
+
`</div>`
|
|
2247
|
+
);
|
|
1552
2248
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
};
|
|
2249
|
+
})(this.MD, this.logger);
|
|
2250
|
+
}
|
|
2251
|
+
// Replace "sandbox:" links with file:// in markdown source (host policy).
|
|
2252
|
+
preprocessMD(s) { return (s || '').replace(/\]\(sandbox:/g, '](file://'); }
|
|
2253
|
+
// Decode base64 UTF-8 to string (shared TextDecoder).
|
|
2254
|
+
b64ToUtf8(b64) {
|
|
2255
|
+
const bin = atob(b64);
|
|
2256
|
+
const bytes = new Uint8Array(bin.length);
|
|
2257
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
2258
|
+
return Utils.utf8Decode(bytes);
|
|
2259
|
+
}
|
|
1565
2260
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
(function codeWrapperPlugin(md, logger) {
|
|
1573
|
-
let CODE_IDX = 1;
|
|
1574
|
-
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
1575
|
-
|
|
1576
|
-
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
1577
|
-
const seenFP = new Set();
|
|
1578
|
-
const makeFP = (info, raw) => {
|
|
1579
|
-
const head = (raw || '').slice(0, 96);
|
|
1580
|
-
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
1581
|
-
};
|
|
2261
|
+
// Apply custom markup for bot messages only (method name kept for API).
|
|
2262
|
+
applyCustomMarkupForBots(root) {
|
|
2263
|
+
const MD = this.MD;
|
|
2264
|
+
try {
|
|
2265
|
+
const scope = root || document;
|
|
2266
|
+
const targets = [];
|
|
1582
2267
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
js: 'javascript', node: 'javascript', nodejs: 'javascript',
|
|
1588
|
-
ts: 'typescript', 'ts-node': 'typescript',
|
|
1589
|
-
yml: 'yaml', kt: 'kotlin', rs: 'rust',
|
|
1590
|
-
csharp: 'csharp', 'c#': 'csharp', 'c++': 'cpp',
|
|
1591
|
-
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
1592
|
-
docker: 'dockerfile'
|
|
1593
|
-
};
|
|
1594
|
-
function normLang(s) { if (!s) return ''; const v = String(s).trim().toLowerCase(); return ALIAS[v] || v; }
|
|
1595
|
-
function isSupportedByHLJS(lang) { try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; } }
|
|
1596
|
-
function classForHighlight(lang) { if (!lang) return 'plaintext'; return isSupportedByHLJS(lang) ? lang : 'plaintext'; }
|
|
1597
|
-
function stripBOM(s) { return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s; }
|
|
1598
|
-
|
|
1599
|
-
function detectFromFirstLine(raw, rid) {
|
|
1600
|
-
if (!raw) return { lang: '', content: raw, isOutput: false };
|
|
1601
|
-
const lines = raw.split(/\r?\n/);
|
|
1602
|
-
if (!lines.length) return { lang: '', content: raw, isOutput: false };
|
|
1603
|
-
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
1604
|
-
if (i >= lines.length) { log(`#${rid} first-line: only whitespace`); return { lang: '', content: raw, isOutput: false }; }
|
|
1605
|
-
let first = stripBOM(lines[i]).trim();
|
|
1606
|
-
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
1607
|
-
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
1608
|
-
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) { log(`#${rid} first-line: no token match`, { first }); return { lang: '', content: raw, isOutput: false }; }
|
|
1609
|
-
let cand = normLang(token);
|
|
1610
|
-
if (cand === 'output') {
|
|
1611
|
-
const content = lines.slice(i + 1).join('\n');
|
|
1612
|
-
log(`#${rid} first-line: output header`);
|
|
1613
|
-
return { lang: 'python', headerLabel: 'output', content, isOutput: true };
|
|
2268
|
+
// If scope itself is a bot message box
|
|
2269
|
+
if (scope && scope.nodeType === 1 && scope.classList && scope.classList.contains('msg-box') &&
|
|
2270
|
+
scope.classList.contains('msg-bot')) {
|
|
2271
|
+
targets.push(scope);
|
|
1614
2272
|
}
|
|
1615
|
-
const rest = lines.slice(i + 1).join('\n');
|
|
1616
|
-
if (!rest.trim()) { log(`#${rid} first-line: directive but no content after, ignore`, { cand }); return { lang: '', content: raw, isOutput: false }; }
|
|
1617
|
-
log(`#${rid} first-line: directive accepted`, { cand, restLen: rest.length, hljs: isSupportedByHLJS(cand) });
|
|
1618
|
-
return { lang: cand, headerLabel: cand, content: rest, isOutput: false };
|
|
1619
|
-
}
|
|
1620
2273
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
const infoLangRaw = (info || '').trim().split(/\s+/)[0] || '';
|
|
1626
|
-
let cand = normLang(infoLangRaw);
|
|
1627
|
-
if (cand === 'output') {
|
|
1628
|
-
log(`#${rid} info: output header`);
|
|
1629
|
-
return { lang: 'python', headerLabel: 'output', content: raw, isOutput: true };
|
|
1630
|
-
}
|
|
1631
|
-
if (cand) {
|
|
1632
|
-
log(`#${rid} info: token`, { infoLangRaw, cand, hljs: isSupportedByHLJS(cand) });
|
|
1633
|
-
return { lang: cand, headerLabel: cand, content: raw, isOutput: false };
|
|
2274
|
+
// Collect bot message boxes within the scope
|
|
2275
|
+
if (scope && typeof scope.querySelectorAll === 'function') {
|
|
2276
|
+
const list = scope.querySelectorAll('.msg-box.msg-bot');
|
|
2277
|
+
for (let i = 0; i < list.length; i++) targets.push(list[i]);
|
|
1634
2278
|
}
|
|
1635
|
-
const det = detectFromFirstLine(raw, rid);
|
|
1636
|
-
if (det && (det.lang || det.isOutput)) return det;
|
|
1637
|
-
log(`#${rid} resolve: fallback`);
|
|
1638
|
-
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
1639
|
-
}
|
|
1640
2279
|
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
const canLog = !DEDUP || !seenFP.has(fp);
|
|
1646
|
-
if (canLog) log(`FENCE_ENTER #${rid}`, { info: (token.info || ''), rawHead: logger.pv(raw) });
|
|
1647
|
-
|
|
1648
|
-
const res = resolveLanguageAndContent(token.info || '', raw, rid);
|
|
1649
|
-
const isOutput = !!res.isOutput;
|
|
1650
|
-
const headerLabel = isOutput ? 'output' : (res.headerLabel || (res.lang || 'code'));
|
|
1651
|
-
const langClass = isOutput ? 'python' : classForHighlight(res.lang);
|
|
1652
|
-
|
|
1653
|
-
if (canLog) {
|
|
1654
|
-
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
1655
|
-
if (DEDUP) seenFP.add(fp);
|
|
2280
|
+
// If scope is inside a bot message, include the closest ancestor as well
|
|
2281
|
+
if (scope && scope.nodeType === 1 && typeof scope.closest === 'function') {
|
|
2282
|
+
const closestMsg = scope.closest('.msg-box.msg-bot');
|
|
2283
|
+
if (closestMsg) targets.push(closestMsg);
|
|
1656
2284
|
}
|
|
1657
2285
|
|
|
1658
|
-
//
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
const tailEsc = Utils.escapeHtml(tail);
|
|
1665
|
-
// Note: for full renderer we will also persist data-code-nl (see below).
|
|
1666
|
-
|
|
1667
|
-
const inner = Utils.escapeHtml(content);
|
|
1668
|
-
const idxLocal = CODE_IDX++;
|
|
1669
|
-
|
|
1670
|
-
let actions = '';
|
|
1671
|
-
if (langClass === 'html') {
|
|
1672
|
-
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>`;
|
|
1673
|
-
} else if (langClass === 'python' && headerLabel !== 'output') {
|
|
1674
|
-
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>`;
|
|
2286
|
+
// Deduplicate and apply rules only to bot messages
|
|
2287
|
+
const seen = new Set();
|
|
2288
|
+
for (const el of targets) {
|
|
2289
|
+
if (!el || !el.isConnected || seen.has(el)) continue;
|
|
2290
|
+
seen.add(el);
|
|
2291
|
+
this.customMarkup.apply(el, MD);
|
|
1675
2292
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
// attach precomputed meta (len/head/tail) on wrapper for downstream optimizations
|
|
1680
|
-
return (
|
|
1681
|
-
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
1682
|
-
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
1683
|
-
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}"` + // meta (no nl here – only in full renderer)
|
|
1684
|
-
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
1685
|
-
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
1686
|
-
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
1687
|
-
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
1688
|
-
`</div>`
|
|
1689
|
-
);
|
|
2293
|
+
} catch (_) {
|
|
2294
|
+
// Keep render path resilient
|
|
1690
2295
|
}
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
// Apply wrapper plugin to full renderer with extra meta (includes number of lines).
|
|
1694
|
-
(function codeWrapperPlugin(md, logger) {
|
|
1695
|
-
// identical core logic – augmented with data-code-nl for full renderer
|
|
1696
|
-
let CODE_IDX = 1;
|
|
1697
|
-
const log = (line, ctx) => logger.debug('MD_LANG', line, ctx);
|
|
1698
|
-
|
|
1699
|
-
const DEDUP = (window.MD_LANG_LOG_DEDUP !== false);
|
|
1700
|
-
const seenFP = new Set();
|
|
1701
|
-
const makeFP = (info, raw) => {
|
|
1702
|
-
const head = (raw || '').slice(0, 96);
|
|
1703
|
-
return String(info || '') + '|' + String((raw || '').length) + '|' + head;
|
|
1704
|
-
};
|
|
2296
|
+
}
|
|
1705
2297
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
js: 'javascript', node: 'javascript', nodejs: 'javascript',
|
|
1711
|
-
ts: 'typescript', 'ts-node': 'typescript',
|
|
1712
|
-
yml: 'yaml', kt: 'kotlin', rs: 'rust',
|
|
1713
|
-
csharp: 'csharp', 'c#': 'csharp', 'c++': 'cpp',
|
|
1714
|
-
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
1715
|
-
docker: 'dockerfile'
|
|
1716
|
-
};
|
|
1717
|
-
function normLang(s) { if (!s) return ''; const v = String(s).trim().toLowerCase(); return ALIAS[v] || v; }
|
|
1718
|
-
function isSupportedByHLJS(lang) { try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; } }
|
|
1719
|
-
function classForHighlight(lang) { if (!lang) return 'plaintext'; return isSupportedByHLJS(lang) ? lang : 'plaintext'; }
|
|
1720
|
-
function stripBOM(s) { return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s; }
|
|
1721
|
-
|
|
1722
|
-
function detectFromFirstLine(raw, rid) {
|
|
1723
|
-
if (!raw) return { lang: '', content: raw, isOutput: false };
|
|
1724
|
-
const lines = raw.split(/\r?\n/);
|
|
1725
|
-
if (!lines.length) return { lang: '', content: raw, isOutput: false };
|
|
1726
|
-
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
1727
|
-
if (i >= lines.length) { log(`#${rid} first-line: only whitespace`); return { lang: '', content: raw, isOutput: false }; }
|
|
1728
|
-
let first = stripBOM(lines[i]).trim();
|
|
1729
|
-
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
1730
|
-
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
1731
|
-
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) { log(`#${rid} first-line: no token match`, { first }); return { lang: '', content: raw, isOutput: false }; }
|
|
1732
|
-
let cand = normLang(token);
|
|
1733
|
-
if (cand === 'output') {
|
|
1734
|
-
const content = lines.slice(i + 1).join('\n');
|
|
1735
|
-
log(`#${rid} first-line: output header`);
|
|
1736
|
-
return { lang: 'python', headerLabel: 'output', content, isOutput: true };
|
|
1737
|
-
}
|
|
1738
|
-
const rest = lines.slice(i + 1).join('\n');
|
|
1739
|
-
if (!rest.trim()) { log(`#${rid} first-line: directive but no content after, ignore`, { cand }); return { lang: '', content: raw, isOutput: false }; }
|
|
1740
|
-
log(`#${rid} first-line: directive accepted`, { cand, restLen: rest.length, hljs: isSupportedByHLJS(cand) });
|
|
1741
|
-
return { lang: cand, headerLabel: cand, content: rest, isOutput: false };
|
|
1742
|
-
}
|
|
2298
|
+
// Helper: choose renderer (hot vs full) for snapshot use.
|
|
2299
|
+
_md(streamingHint) {
|
|
2300
|
+
return streamingHint ? (this.MD_STREAM || this.MD) : (this.MD || this.MD_STREAM);
|
|
2301
|
+
}
|
|
1743
2302
|
|
|
1744
|
-
|
|
1745
|
-
|
|
2303
|
+
// Async, batched processing of [data-md64] / [md-block-markdown] to keep UI responsive on heavy loads.
|
|
2304
|
+
// Note: user messages are rendered as plain text (no markdown-it, no custom markup, no KaTeX).
|
|
2305
|
+
async renderPendingMarkdown(root) {
|
|
2306
|
+
const MD = this.MD; if (!MD) return;
|
|
2307
|
+
const scope = root || document;
|
|
1746
2308
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const det = detectFromFirstLine(raw, rid);
|
|
1759
|
-
if (det && (det.lang || det.isOutput)) return det;
|
|
1760
|
-
log(`#${rid} resolve: fallback`);
|
|
1761
|
-
return { lang: '', headerLabel: 'code', content: raw, isOutput: false };
|
|
1762
|
-
}
|
|
2309
|
+
// Collect both legacy base64 holders and new native Markdown holders
|
|
2310
|
+
const nodes = Array.from(scope.querySelectorAll('[data-md64], [md-block-markdown]'));
|
|
2311
|
+
if (nodes.length === 0) {
|
|
2312
|
+
// Nothing to materialize right now. Avoid arming rAF work unless there is
|
|
2313
|
+
// actually something present that needs highlight/scroll/math.
|
|
2314
|
+
try {
|
|
2315
|
+
const hasBots = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot'));
|
|
2316
|
+
const hasWrappers = !!(scope && scope.querySelector && scope.querySelector('.code-wrapper'));
|
|
2317
|
+
const hasCodes = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot pre code'));
|
|
2318
|
+
const hasUnhighlighted = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot pre code:not([data-highlighted="yes"])'));
|
|
2319
|
+
const hasMath = !!(scope && scope.querySelector && scope.querySelector('script[type^="math/tex"]'));
|
|
1763
2320
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
const rid = String(CODE_IDX + '');
|
|
1767
|
-
const fp = makeFP(token.info || '', raw);
|
|
1768
|
-
const canLog = !DEDUP || !seenFP.has(fp);
|
|
1769
|
-
if (canLog) log(`FENCE_ENTER #${rid}`, { info: (token.info || ''), rawHead: logger.pv(raw) });
|
|
1770
|
-
|
|
1771
|
-
const res = resolveLanguageAndContent(token.info || '', raw, rid);
|
|
1772
|
-
const isOutput = !!res.isOutput;
|
|
1773
|
-
const headerLabel = isOutput ? 'output' : (res.headerLabel || (res.lang || 'code'));
|
|
1774
|
-
const langClass = isOutput ? 'python' : classForHighlight(res.lang);
|
|
1775
|
-
|
|
1776
|
-
if (canLog) {
|
|
1777
|
-
log(`FENCE_RESOLVE #${rid}`, { headerLabel, langToken: (res.lang || ''), langClass, hljsSupported: isSupportedByHLJS(res.lang || ''), contentLen: (res.content || '').length });
|
|
1778
|
-
if (DEDUP) seenFP.add(fp);
|
|
1779
|
-
}
|
|
2321
|
+
// Apply Custom Markup only if bot messages are present.
|
|
2322
|
+
if (hasBots) { this.applyCustomMarkupForBots(scope); }
|
|
1780
2323
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const len = content.length;
|
|
1784
|
-
const head = content.slice(0, 64);
|
|
1785
|
-
const tail = content.slice(-64);
|
|
1786
|
-
const headEsc = Utils.escapeHtml(head);
|
|
1787
|
-
const tailEsc = Utils.escapeHtml(tail);
|
|
1788
|
-
const nl = Utils.countNewlines(content);
|
|
1789
|
-
|
|
1790
|
-
const inner = Utils.escapeHtml(content);
|
|
1791
|
-
const idxLocal = CODE_IDX++;
|
|
1792
|
-
|
|
1793
|
-
let actions = '';
|
|
1794
|
-
if (langClass === 'html') {
|
|
1795
|
-
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>`;
|
|
1796
|
-
} else if (langClass === 'python' && headerLabel !== 'output') {
|
|
1797
|
-
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>`;
|
|
1798
|
-
}
|
|
1799
|
-
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>`;
|
|
1800
|
-
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>`;
|
|
1801
|
-
|
|
1802
|
-
return (
|
|
1803
|
-
`<div class="code-wrapper highlight" data-index="${idxLocal}"` +
|
|
1804
|
-
` data-code-lang="${Utils.escapeHtml(res.lang || '')}"` +
|
|
1805
|
-
` data-code-len="${String(len)}" data-code-head="${headEsc}" data-code-tail="${tailEsc}" data-code-nl="${String(nl)}"` + // include nl for full renderer
|
|
1806
|
-
` data-locale-collapse="${Utils.escapeHtml(cfg.LOCALE.COLLAPSE)}" data-locale-expand="${Utils.escapeHtml(cfg.LOCALE.EXPAND)}"` +
|
|
1807
|
-
` data-locale-copy="${Utils.escapeHtml(cfg.LOCALE.COPY)}" data-locale-copied="${Utils.escapeHtml(cfg.LOCALE.COPIED)}" data-style="${Utils.escapeHtml(cfg.CODE_STYLE)}">` +
|
|
1808
|
-
`<p class="code-header-wrapper"><span><span class="code-header-lang">${Utils.escapeHtml(headerLabel)} </span>${actions}</span></p>` +
|
|
1809
|
-
`<pre><code class="language-${Utils.escapeHtml(langClass)} hljs">${inner}</code></pre>` +
|
|
1810
|
-
`</div>`
|
|
1811
|
-
);
|
|
1812
|
-
}
|
|
1813
|
-
})(this.MD, this.logger);
|
|
1814
|
-
}
|
|
1815
|
-
// Replace "sandbox:" links with file:// in markdown source (host policy).
|
|
1816
|
-
preprocessMD(s) { return (s || '').replace(/\]\(sandbox:/g, '](file://'); }
|
|
1817
|
-
// Decode base64 UTF-8 to string (shared TextDecoder).
|
|
1818
|
-
b64ToUtf8(b64) {
|
|
1819
|
-
const bin = atob(b64);
|
|
1820
|
-
const bytes = new Uint8Array(bin.length);
|
|
1821
|
-
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1822
|
-
return Utils.utf8Decode(bytes);
|
|
1823
|
-
}
|
|
2324
|
+
// Restore collapsed state only if we can actually find wrappers.
|
|
2325
|
+
if (hasWrappers) { this.restoreCollapsedCode(scope); }
|
|
1824
2326
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
const MD = this.MD;
|
|
1828
|
-
try {
|
|
1829
|
-
const scope = root || document;
|
|
1830
|
-
const targets = [];
|
|
2327
|
+
// Initialize code scroll helpers for current root.
|
|
2328
|
+
this.hooks.codeScrollInit(scope);
|
|
1831
2329
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2330
|
+
// Init code-scroll/highlight observers only when there are codes in DOM.
|
|
2331
|
+
if (hasCodes) {
|
|
2332
|
+
this.hooks.observeMsgBoxes(scope);
|
|
2333
|
+
this.hooks.observeNewCode(scope, {
|
|
2334
|
+
deferLastIfStreaming: true,
|
|
2335
|
+
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
2336
|
+
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
2337
|
+
});
|
|
2338
|
+
if (hasUnhighlighted && typeof runtime !== 'undefined' && runtime.highlighter) {
|
|
2339
|
+
runtime.highlighter.scanVisibleCodesInRoot(scope, runtime.stream.activeCode || null);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
1837
2342
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
for (let i = 0; i < list.length; i++) targets.push(list[i]);
|
|
1842
|
-
}
|
|
2343
|
+
// Schedule KaTeX render only if there are math scripts present.
|
|
2344
|
+
if (hasMath) { this.hooks.scheduleMathRender(scope); }
|
|
2345
|
+
this.hooks.codeScrollInit(scope);
|
|
1843
2346
|
|
|
1844
|
-
|
|
1845
|
-
if (scope && scope.nodeType === 1 && typeof scope.closest === 'function') {
|
|
1846
|
-
const closestMsg = scope.closest('.msg-box.msg-bot');
|
|
1847
|
-
if (closestMsg) targets.push(closestMsg);
|
|
1848
|
-
}
|
|
2347
|
+
} catch (_) { /* swallow: keep idle path safe */ }
|
|
1849
2348
|
|
|
1850
|
-
|
|
1851
|
-
const seen = new Set();
|
|
1852
|
-
for (const el of targets) {
|
|
1853
|
-
if (!el || !el.isConnected || seen.has(el)) continue;
|
|
1854
|
-
seen.add(el);
|
|
1855
|
-
this.customMarkup.apply(el, MD);
|
|
2349
|
+
return;
|
|
1856
2350
|
}
|
|
1857
|
-
} catch (_) {
|
|
1858
|
-
// Keep render path resilient
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
// Helper: choose renderer (hot vs full) for snapshot use.
|
|
1863
|
-
_md(streamingHint) {
|
|
1864
|
-
return streamingHint ? (this.MD_STREAM || this.MD) : (this.MD || this.MD_STREAM);
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
// Async, batched processing of [data-md64] / [md-block-markdown] to keep UI responsive on heavy loads.
|
|
1868
|
-
// Note: user messages are rendered as plain text (no markdown-it, no custom markup, no KaTeX).
|
|
1869
|
-
async renderPendingMarkdown(root) {
|
|
1870
|
-
const MD = this.MD; if (!MD) return;
|
|
1871
|
-
const scope = root || document;
|
|
1872
2351
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
if (nodes.length === 0) {
|
|
1876
|
-
// Nothing to materialize right now. Avoid arming rAF work unless there is
|
|
1877
|
-
// actually something present that needs highlight/scroll/math.
|
|
1878
|
-
try {
|
|
1879
|
-
const hasBots = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot'));
|
|
1880
|
-
const hasWrappers = !!(scope && scope.querySelector && scope.querySelector('.code-wrapper'));
|
|
1881
|
-
const hasCodes = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot pre code'));
|
|
1882
|
-
const hasUnhighlighted = !!(scope && scope.querySelector && scope.querySelector('.msg-box.msg-bot pre code:not([data-highlighted="yes"])'));
|
|
1883
|
-
const hasMath = !!(scope && scope.querySelector && scope.querySelector('script[type^="math/tex"]'));
|
|
2352
|
+
// Track which bot message boxes actually changed to avoid a heavy global Custom Markup pass.
|
|
2353
|
+
const touchedBoxes = new Set();
|
|
1884
2354
|
|
|
1885
|
-
|
|
1886
|
-
|
|
2355
|
+
// Budgeted, cooperative loop: process nodes one-by-one with per-frame yield when needed.
|
|
2356
|
+
const perSlice = (this.cfg.ASYNC && this.cfg.ASYNC.MD_NODES_PER_SLICE) || 12; // upper bound per frame
|
|
2357
|
+
let sliceCount = 0;
|
|
2358
|
+
let startedAt = Utils.now();
|
|
1887
2359
|
|
|
1888
|
-
|
|
1889
|
-
|
|
2360
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
2361
|
+
const el = nodes[j];
|
|
2362
|
+
if (!el || !el.isConnected) continue;
|
|
1890
2363
|
|
|
1891
|
-
|
|
1892
|
-
|
|
2364
|
+
let md = '';
|
|
2365
|
+
const isNative = el.hasAttribute('md-block-markdown');
|
|
2366
|
+
const msgBox = (el.closest && el.closest('.msg-box.msg-bot, .msg-box.msg-user')) || null;
|
|
2367
|
+
const isUserMsg = !!(msgBox && msgBox.classList.contains('msg-user'));
|
|
2368
|
+
const isBotMsg = !!(msgBox && msgBox.classList.contains('msg-bot'));
|
|
1893
2369
|
|
|
1894
|
-
//
|
|
1895
|
-
if (
|
|
1896
|
-
this.
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
if (
|
|
1903
|
-
runtime.highlighter.scanVisibleCodesInRoot(scope, runtime.stream.activeCode || null);
|
|
1904
|
-
}
|
|
2370
|
+
// Read source text (do not preprocess for user messages to keep it raw)
|
|
2371
|
+
if (isNative) {
|
|
2372
|
+
try { md = isUserMsg ? (el.textContent || '') : this.preprocessMD(el.textContent || ''); } catch (_) { md = ''; }
|
|
2373
|
+
try { el.removeAttribute('md-block-markdown'); } catch (_) {}
|
|
2374
|
+
} else {
|
|
2375
|
+
const b64 = el.getAttribute('data-md64'); if (!b64) continue;
|
|
2376
|
+
try { md = this.b64ToUtf8(b64); } catch (_) { md = ''; }
|
|
2377
|
+
el.removeAttribute('data-md64');
|
|
2378
|
+
if (!isUserMsg) { try { md = this.preprocessMD(md); } catch (_) {} }
|
|
1905
2379
|
}
|
|
1906
2380
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2381
|
+
if (isUserMsg) {
|
|
2382
|
+
// User message: replace placeholder with raw plain text only.
|
|
2383
|
+
const span = document.createElement('span');
|
|
2384
|
+
span.textContent = md;
|
|
2385
|
+
el.replaceWith(span);
|
|
2386
|
+
// Intentionally do NOT add to touchedBoxes; no Custom Markup for user.
|
|
2387
|
+
} else if (isBotMsg) {
|
|
2388
|
+
// Bot message: full markdown-it render with Custom Markup.
|
|
2389
|
+
let html = '';
|
|
2390
|
+
try {
|
|
2391
|
+
let src = md;
|
|
2392
|
+
// Pre-md transforms for source-phase rules
|
|
2393
|
+
if (this.customMarkup && typeof this.customMarkup.transformSource === 'function') {
|
|
2394
|
+
src = this.customMarkup.transformSource(src, { streaming: false });
|
|
2395
|
+
}
|
|
2396
|
+
html = MD.render(src);
|
|
2397
|
+
} catch (_) { html = Utils.escapeHtml(md); }
|
|
2398
|
+
|
|
2399
|
+
// build fragment directly (avoid intermediate container allocations).
|
|
2400
|
+
let frag = null;
|
|
2401
|
+
try {
|
|
2402
|
+
const range = document.createRange();
|
|
2403
|
+
const ctx = el.parentNode || document.body || document.documentElement;
|
|
2404
|
+
range.selectNode(ctx);
|
|
2405
|
+
frag = range.createContextualFragment(html);
|
|
2406
|
+
} catch (_) {
|
|
2407
|
+
const tmp = document.createElement('div');
|
|
2408
|
+
tmp.innerHTML = html;
|
|
2409
|
+
frag = document.createDocumentFragment();
|
|
2410
|
+
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
2411
|
+
}
|
|
1927
2412
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
const msgBox = (el.closest && el.closest('.msg-box.msg-bot, .msg-box.msg-user')) || null;
|
|
1931
|
-
const isUserMsg = !!(msgBox && msgBox.classList.contains('msg-user'));
|
|
1932
|
-
const isBotMsg = !!(msgBox && msgBox.classList.contains('msg-bot'));
|
|
2413
|
+
// Apply Custom Markup on a lightweight DocumentFragment
|
|
2414
|
+
try { this.customMarkup.apply(frag, MD); } catch (_) {}
|
|
1933
2415
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
if (!isUserMsg) { try { md = this.preprocessMD(md); } catch (_) {} }
|
|
1943
|
-
}
|
|
2416
|
+
el.replaceWith(frag);
|
|
2417
|
+
touchedBoxes.add(msgBox);
|
|
2418
|
+
} else {
|
|
2419
|
+
// Outside of any message box: materialize as plain text.
|
|
2420
|
+
const span = document.createElement('span');
|
|
2421
|
+
span.textContent = md;
|
|
2422
|
+
el.replaceWith(span);
|
|
2423
|
+
}
|
|
1944
2424
|
|
|
1945
|
-
|
|
1946
|
-
//
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
} else if (isBotMsg) {
|
|
1952
|
-
// Bot message: full markdown-it render with Custom Markup.
|
|
1953
|
-
let html = '';
|
|
1954
|
-
try { html = MD.render(md); } catch (_) { html = Utils.escapeHtml(md); }
|
|
1955
|
-
|
|
1956
|
-
// build fragment directly (avoid intermediate container allocations).
|
|
1957
|
-
let frag = null;
|
|
1958
|
-
try {
|
|
1959
|
-
const range = document.createRange();
|
|
1960
|
-
const ctx = el.parentNode || document.body || document.documentElement;
|
|
1961
|
-
range.selectNode(ctx);
|
|
1962
|
-
frag = range.createContextualFragment(html);
|
|
1963
|
-
} catch (_) {
|
|
1964
|
-
const tmp = document.createElement('div');
|
|
1965
|
-
tmp.innerHTML = html;
|
|
1966
|
-
frag = document.createDocumentFragment();
|
|
1967
|
-
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
2425
|
+
sliceCount++;
|
|
2426
|
+
// Yield by time budget or by count to keep frame short and reactive.
|
|
2427
|
+
if (sliceCount >= perSlice || this.asyncer.shouldYield(startedAt)) {
|
|
2428
|
+
await this.asyncer.yield();
|
|
2429
|
+
startedAt = Utils.now();
|
|
2430
|
+
sliceCount = 0;
|
|
1968
2431
|
}
|
|
2432
|
+
}
|
|
1969
2433
|
|
|
1970
|
-
|
|
1971
|
-
|
|
2434
|
+
// Apply Custom Markup only to actually modified BOT messages (keeps this pass light).
|
|
2435
|
+
try {
|
|
2436
|
+
touchedBoxes.forEach(box => { try { this.customMarkup.apply(box, MD); } catch (_) {} });
|
|
2437
|
+
} catch (_) {}
|
|
1972
2438
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2439
|
+
// Same post-processing as before (idempotent with external calls).
|
|
2440
|
+
this.restoreCollapsedCode(scope);
|
|
2441
|
+
this.hooks.observeNewCode(scope, {
|
|
2442
|
+
deferLastIfStreaming: true,
|
|
2443
|
+
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
2444
|
+
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
2445
|
+
});
|
|
2446
|
+
this.hooks.observeMsgBoxes(scope);
|
|
2447
|
+
this.hooks.scheduleMathRender(scope);
|
|
2448
|
+
this.hooks.codeScrollInit(scope);
|
|
1981
2449
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
if (sliceCount >= perSlice || this.asyncer.shouldYield(startedAt)) {
|
|
1985
|
-
await this.asyncer.yield();
|
|
1986
|
-
startedAt = Utils.now();
|
|
1987
|
-
sliceCount = 0;
|
|
2450
|
+
if (typeof runtime !== 'undefined' && runtime.highlighter) {
|
|
2451
|
+
runtime.highlighter.scanVisibleCodesInRoot(scope, runtime.stream.activeCode || null);
|
|
1988
2452
|
}
|
|
1989
2453
|
}
|
|
1990
2454
|
|
|
1991
|
-
//
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
this.hooks.observeMsgBoxes(scope);
|
|
2004
|
-
this.hooks.scheduleMathRender(scope);
|
|
2005
|
-
this.hooks.codeScrollInit(scope);
|
|
2006
|
-
|
|
2007
|
-
if (typeof runtime !== 'undefined' && runtime.highlighter) {
|
|
2008
|
-
runtime.highlighter.scanVisibleCodesInRoot(scope, runtime.stream.activeCode || null);
|
|
2455
|
+
// Render streaming snapshot.
|
|
2456
|
+
renderStreamingSnapshot(src) {
|
|
2457
|
+
const md = this._md(true);
|
|
2458
|
+
if (!md) return '';
|
|
2459
|
+
try {
|
|
2460
|
+
let s = String(src || '');
|
|
2461
|
+
// Pre-markdown custom transforms (e.g. [!exec]/<execute> => ```python fences)
|
|
2462
|
+
if (this.customMarkup && typeof this.customMarkup.transformSource === 'function') {
|
|
2463
|
+
s = this.customMarkup.transformSource(s, { streaming: true });
|
|
2464
|
+
}
|
|
2465
|
+
return md.render(s);
|
|
2466
|
+
} catch (_) { return Utils.escapeHtml(src); }
|
|
2009
2467
|
}
|
|
2010
|
-
}
|
|
2011
2468
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2469
|
+
renderFinalSnapshot(src) {
|
|
2470
|
+
const md = this._md(false);
|
|
2471
|
+
if (!md) return '';
|
|
2472
|
+
try {
|
|
2473
|
+
let s = String(src || '');
|
|
2474
|
+
if (this.customMarkup && typeof this.customMarkup.transformSource === 'function') {
|
|
2475
|
+
s = this.customMarkup.transformSource(s, { streaming: false });
|
|
2476
|
+
}
|
|
2477
|
+
return md.render(s);
|
|
2478
|
+
} catch (_) { return Utils.escapeHtml(src); }
|
|
2479
|
+
}
|
|
2024
2480
|
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2481
|
+
// Restore collapse/expand state of code blocks after DOM updates.
|
|
2482
|
+
restoreCollapsedCode(root) {
|
|
2483
|
+
const scope = root || document;
|
|
2484
|
+
const wrappers = scope.querySelectorAll('.code-wrapper');
|
|
2485
|
+
wrappers.forEach((wrapper) => {
|
|
2486
|
+
const index = wrapper.getAttribute('data-index');
|
|
2487
|
+
const localeCollapse = wrapper.getAttribute('data-locale-collapse');
|
|
2488
|
+
const localeExpand = wrapper.getAttribute('data-locale-expand');
|
|
2489
|
+
const source = wrapper.querySelector('code');
|
|
2490
|
+
const isCollapsed = (window.__collapsed_idx || []).includes(index);
|
|
2491
|
+
if (!source) return;
|
|
2492
|
+
const btn = wrapper.querySelector('.code-header-collapse');
|
|
2493
|
+
if (isCollapsed) {
|
|
2494
|
+
source.style.display = 'none';
|
|
2495
|
+
if (btn) { const span = btn.querySelector('span'); if (span) span.textContent = localeExpand; }
|
|
2496
|
+
} else {
|
|
2497
|
+
source.style.display = 'block';
|
|
2498
|
+
if (btn) { const span = btn.querySelector('span'); if (span) span.textContent = localeCollapse; }
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2045
2502
|
}
|
|
2046
|
-
}
|
|
2047
2503
|
window.__collapsed_idx = window.__collapsed_idx || [];
|
|
2048
2504
|
|
|
2049
2505
|
// ==========================================================================
|
|
@@ -2432,264 +2888,954 @@
|
|
|
2432
2888
|
}, VISIBLE_MS));
|
|
2433
2889
|
}
|
|
2434
2890
|
|
|
2435
|
-
// Start rotation with initial delay.
|
|
2436
|
-
cycle() {
|
|
2437
|
-
const list = this._getList(); if (!list.length || this._running) return;
|
|
2438
|
-
this._running = true; this._idx = 0;
|
|
2439
|
-
this.show(); // make sure the host is visible and centered
|
|
2891
|
+
// Start rotation with initial delay.
|
|
2892
|
+
cycle() {
|
|
2893
|
+
const list = this._getList(); if (!list.length || this._running) return;
|
|
2894
|
+
this._running = true; this._idx = 0;
|
|
2895
|
+
this.show(); // make sure the host is visible and centered
|
|
2896
|
+
|
|
2897
|
+
const INIT_DELAY = (typeof window !== 'undefined' && window.TIPS_INIT_DELAY_MS) ? window.TIPS_INIT_DELAY_MS : 10000;
|
|
2898
|
+
this._timers.push(setTimeout(() => {
|
|
2899
|
+
if (this.hidden) return;
|
|
2900
|
+
this._cycleLoop();
|
|
2901
|
+
}, Math.max(0, INIT_DELAY)));
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// Stop and reset.
|
|
2905
|
+
cleanup() {
|
|
2906
|
+
this.stopTimers();
|
|
2907
|
+
const el = this._host();
|
|
2908
|
+
if (el) el.classList.remove('visible');
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// ==========================================================================
|
|
2913
|
+
// 9) Tool output + Nodes manager
|
|
2914
|
+
// ==========================================================================
|
|
2915
|
+
|
|
2916
|
+
class ToolOutput {
|
|
2917
|
+
// Placeholder for loader show (can be extended by host).
|
|
2918
|
+
showLoader() { return; }
|
|
2919
|
+
// Hide spinner elements in bot messages.
|
|
2920
|
+
hideLoader() {
|
|
2921
|
+
const elements = document.querySelectorAll('.msg-bot');
|
|
2922
|
+
if (elements.length > 0) elements.forEach(el => { const s = el.querySelector('.spinner'); if (s) s.style.display = 'none'; });
|
|
2923
|
+
}
|
|
2924
|
+
begin() { this.showLoader(); }
|
|
2925
|
+
end() { this.hideLoader(); }
|
|
2926
|
+
enable() { const els = document.querySelectorAll('.tool-output'); if (els.length) els[els.length - 1].style.display = 'block'; }
|
|
2927
|
+
disable() { const els = document.querySelectorAll('.tool-output'); if (els.length) els[els.length - 1].style.display = 'none'; }
|
|
2928
|
+
// Append HTML into the latest tool-output content area.
|
|
2929
|
+
append(content) {
|
|
2930
|
+
this.hideLoader(); this.enable();
|
|
2931
|
+
const els = document.querySelectorAll('.tool-output');
|
|
2932
|
+
if (els.length) { const contentEl = els[els.length - 1].querySelector('.content'); if (contentEl) contentEl.insertAdjacentHTML('beforeend', content); }
|
|
2933
|
+
}
|
|
2934
|
+
// Replace inner HTML for the latest tool-output content area.
|
|
2935
|
+
update(content) {
|
|
2936
|
+
this.hideLoader(); this.enable();
|
|
2937
|
+
const els = document.querySelectorAll('.tool-output');
|
|
2938
|
+
if (els.length) { const contentEl = els[els.length - 1].querySelector('.content'); if (contentEl) contentEl.innerHTML = content; }
|
|
2939
|
+
}
|
|
2940
|
+
// Remove children from the latest tool-output content area.
|
|
2941
|
+
clear() {
|
|
2942
|
+
this.hideLoader(); this.enable();
|
|
2943
|
+
const els = document.querySelectorAll('.tool-output');
|
|
2944
|
+
if (els.length) { const contentEl = els[els.length - 1].querySelector('.content'); if (contentEl) contentEl.replaceChildren(); }
|
|
2945
|
+
}
|
|
2946
|
+
// Toggle visibility of a specific tool output block by message id.
|
|
2947
|
+
toggle(id) {
|
|
2948
|
+
const el = document.getElementById('msg-bot-' + id); if (!el) return;
|
|
2949
|
+
const outputEl = el.querySelector('.tool-output'); if (!outputEl) return;
|
|
2950
|
+
const contentEl = outputEl.querySelector('.content');
|
|
2951
|
+
if (contentEl) contentEl.style.display = (contentEl.style.display === 'none') ? 'block' : 'none';
|
|
2952
|
+
const toggleEl = outputEl.querySelector('.toggle-cmd-output img'); if (toggleEl) toggleEl.classList.toggle('toggle-expanded');
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// UserCollapseManager – collapsible user messages (msg-box.msg-user)
|
|
2957
|
+
class UserCollapseManager {
|
|
2958
|
+
constructor(cfg) {
|
|
2959
|
+
this.cfg = cfg || {};
|
|
2960
|
+
// Collapse threshold in pixels (can be overridden via window.USER_MSG_COLLAPSE_HEIGHT_PX).
|
|
2961
|
+
this.threshold = Utils.g('USER_MSG_COLLAPSE_HEIGHT_PX', 1000);
|
|
2962
|
+
// Track processed .msg elements to allow cheap remeasure on resize if needed.
|
|
2963
|
+
this._processed = new Set();
|
|
2964
|
+
|
|
2965
|
+
// Visual indicator attached while collapsed (does not modify original text).
|
|
2966
|
+
this.ellipsisText = ' [...]';
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
_icons() {
|
|
2970
|
+
const I = (this.cfg && this.cfg.ICONS) || {};
|
|
2971
|
+
return { expand: I.EXPAND || '', collapse: I.COLLAPSE || '' };
|
|
2972
|
+
}
|
|
2973
|
+
_labels() {
|
|
2974
|
+
const L = (this.cfg && this.cfg.LOCALE) || {};
|
|
2975
|
+
return { expand: L.EXPAND || 'Expand', collapse: L.COLLAPSE || 'Collapse' };
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// Schedule a function for next frame (ensures layout is up-to-date before scrolling).
|
|
2979
|
+
_afterLayout(fn) {
|
|
2980
|
+
try {
|
|
2981
|
+
if (typeof runtime !== 'undefined' && runtime.raf && typeof runtime.raf.schedule === 'function') {
|
|
2982
|
+
const key = { t: 'UC:afterLayout', i: Math.random() };
|
|
2983
|
+
runtime.raf.schedule(key, () => { try { fn && fn(); } catch (_) {} }, 'UserCollapse', 0);
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
} catch (_) {}
|
|
2987
|
+
try { requestAnimationFrame(() => { try { fn && fn(); } catch (_) {} }); }
|
|
2988
|
+
catch (_) { setTimeout(() => { try { fn && fn(); } catch (__){ } }, 0); }
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// Bring toggle into view with minimal scroll (upwards if it moved above after collapse).
|
|
2992
|
+
_scrollToggleIntoView(toggleEl) {
|
|
2993
|
+
if (!toggleEl || !toggleEl.isConnected) return;
|
|
2994
|
+
try { if (runtime && runtime.scrollMgr) { runtime.scrollMgr.userInteracted = true; runtime.scrollMgr.autoFollow = false; } } catch (_) {}
|
|
2995
|
+
this._afterLayout(() => {
|
|
2996
|
+
try {
|
|
2997
|
+
if (toggleEl.scrollIntoView) {
|
|
2998
|
+
// Prefer minimal movement; keep behavior non-animated and predictable.
|
|
2999
|
+
try { toggleEl.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' }); }
|
|
3000
|
+
catch (_) { toggleEl.scrollIntoView(false); }
|
|
3001
|
+
}
|
|
3002
|
+
} catch (_) {}
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// Ensure wrapper and toggle exist for a given .msg element.
|
|
3007
|
+
_ensureStructure(msg) {
|
|
3008
|
+
if (!msg || !msg.isConnected) return null;
|
|
3009
|
+
|
|
3010
|
+
// Wrap all direct children into a dedicated content container to measure height accurately.
|
|
3011
|
+
let content = msg.querySelector('.uc-content');
|
|
3012
|
+
if (!content) {
|
|
3013
|
+
content = document.createElement('div');
|
|
3014
|
+
content.className = 'uc-content';
|
|
3015
|
+
const frag = document.createDocumentFragment();
|
|
3016
|
+
while (msg.firstChild) frag.appendChild(msg.firstChild);
|
|
3017
|
+
content.appendChild(frag);
|
|
3018
|
+
msg.appendChild(content);
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
// Ensure a single toggle exists (click and keyboard accessible).
|
|
3022
|
+
let toggle = msg.querySelector('.uc-toggle');
|
|
3023
|
+
if (!toggle) {
|
|
3024
|
+
const icons = this._icons();
|
|
3025
|
+
const labels = this._labels();
|
|
3026
|
+
|
|
3027
|
+
toggle = document.createElement('div');
|
|
3028
|
+
toggle.className = 'uc-toggle';
|
|
3029
|
+
toggle.tabIndex = 0;
|
|
3030
|
+
toggle.setAttribute('role', 'button');
|
|
3031
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
3032
|
+
toggle.title = labels.expand;
|
|
3033
|
+
|
|
3034
|
+
const img = document.createElement('img');
|
|
3035
|
+
img.className = 'uc-toggle-icon';
|
|
3036
|
+
img.alt = labels.expand;
|
|
3037
|
+
img.src = icons.expand;
|
|
3038
|
+
|
|
3039
|
+
// Provide a sane default size even if CSS did not load yet (CSS will override when present).
|
|
3040
|
+
img.width = 26; // keep in sync with CSS fallback var(--uc-toggle-icon-size, 26px)
|
|
3041
|
+
img.height = 26; // ensures a consistent, non-tiny control from the first paint
|
|
3042
|
+
|
|
3043
|
+
toggle.appendChild(img);
|
|
3044
|
+
|
|
3045
|
+
// Attach local listeners (no global handler change; production-safe).
|
|
3046
|
+
toggle.addEventListener('click', (ev) => {
|
|
3047
|
+
ev.preventDefault();
|
|
3048
|
+
ev.stopPropagation();
|
|
3049
|
+
this.toggleFromToggle(toggle);
|
|
3050
|
+
});
|
|
3051
|
+
toggle.addEventListener('keydown', (ev) => {
|
|
3052
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
3053
|
+
ev.preventDefault();
|
|
3054
|
+
ev.stopPropagation();
|
|
3055
|
+
this.toggleFromToggle(toggle);
|
|
3056
|
+
}
|
|
3057
|
+
}, { passive: false });
|
|
3058
|
+
|
|
3059
|
+
msg.appendChild(toggle);
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
this._processed.add(msg);
|
|
3063
|
+
msg.dataset.ucInit = '1';
|
|
3064
|
+
return { content, toggle };
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// Create or update the ellipsis indicator inside content (absolute in the bottom-right corner).
|
|
3068
|
+
_ensureEllipsisEl(msg, contentEl) {
|
|
3069
|
+
const content = contentEl || (msg && msg.querySelector('.uc-content'));
|
|
3070
|
+
if (!content) return null;
|
|
3071
|
+
|
|
3072
|
+
// Ensure the content becomes a positioning context only when needed.
|
|
3073
|
+
if (getComputedStyle(content).position === 'static') {
|
|
3074
|
+
content.style.position = 'relative';
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
let dot = content.querySelector('.uc-ellipsis');
|
|
3078
|
+
if (!dot) {
|
|
3079
|
+
dot = document.createElement('span');
|
|
3080
|
+
dot.className = 'uc-ellipsis';
|
|
3081
|
+
dot.textContent = this.ellipsisText;
|
|
3082
|
+
// Inline, theme-agnostic styles; kept minimal and non-interactive.
|
|
3083
|
+
dot.style.position = 'absolute';
|
|
3084
|
+
dot.style.right = '0';
|
|
3085
|
+
dot.style.bottom = '0';
|
|
3086
|
+
dot.style.paddingLeft = '6px';
|
|
3087
|
+
dot.style.pointerEvents = 'none';
|
|
3088
|
+
dot.style.zIndex = '1';
|
|
3089
|
+
dot.style.fontWeight = '500';
|
|
3090
|
+
dot.style.opacity = '0.75';
|
|
3091
|
+
|
|
3092
|
+
content.appendChild(dot);
|
|
3093
|
+
}
|
|
3094
|
+
return dot;
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// Show ellipsis only when there is hidden overflow (collapsed).
|
|
3098
|
+
_showEllipsis(msg, contentEl) {
|
|
3099
|
+
const dot = this._ensureEllipsisEl(msg, contentEl);
|
|
3100
|
+
if (dot) dot.style.display = 'inline';
|
|
3101
|
+
}
|
|
3102
|
+
// Hide and clean ellipsis when not needed (expanded or short content).
|
|
3103
|
+
_hideEllipsis(msg) {
|
|
3104
|
+
const content = msg && msg.querySelector('.uc-content');
|
|
3105
|
+
if (!content) return;
|
|
3106
|
+
const dot = content.querySelector('.uc-ellipsis');
|
|
3107
|
+
if (dot && dot.parentNode) {
|
|
3108
|
+
// Remove the indicator to avoid accidental copy/select and keep DOM lean.
|
|
3109
|
+
dot.parentNode.removeChild(dot);
|
|
3110
|
+
}
|
|
3111
|
+
// Drop positioning context when no indicator is present (keep styles minimal).
|
|
3112
|
+
try {
|
|
3113
|
+
if (content && content.style && content.querySelector('.uc-ellipsis') == null) {
|
|
3114
|
+
content.style.position = '';
|
|
3115
|
+
}
|
|
3116
|
+
} catch (_) {}
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// Apply collapse to all user messages under root.
|
|
3120
|
+
apply(root) {
|
|
3121
|
+
const scope = root || document;
|
|
3122
|
+
let list;
|
|
3123
|
+
if (scope.nodeType === 1) list = scope.querySelectorAll('.msg-box.msg-user .msg');
|
|
3124
|
+
else list = document.querySelectorAll('.msg-box.msg-user .msg');
|
|
3125
|
+
if (!list || !list.length) return;
|
|
3126
|
+
|
|
3127
|
+
for (let i = 0; i < list.length; i++) {
|
|
3128
|
+
const msg = list[i];
|
|
3129
|
+
const st = this._ensureStructure(msg);
|
|
3130
|
+
if (!st) continue;
|
|
3131
|
+
this._update(msg, st.content, st.toggle);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// Update collapsed/expanded state depending on content height.
|
|
3136
|
+
_update(msg, contentEl, toggleEl) {
|
|
3137
|
+
const c = contentEl || (msg && msg.querySelector('.uc-content'));
|
|
3138
|
+
if (!msg || !c) return;
|
|
3139
|
+
|
|
3140
|
+
// Special-case: when threshold = 0 (or '0'), auto-collapse is globally disabled.
|
|
3141
|
+
// We avoid any measurement, force the content to be fully expanded, and ensure the toggle is hidden.
|
|
3142
|
+
// This preserves public API while providing an explicit opt-out, without impacting existing behavior.
|
|
3143
|
+
if (this.threshold === 0 || this.threshold === '0') {
|
|
3144
|
+
const t = toggleEl || msg.querySelector('.uc-toggle');
|
|
3145
|
+
const labels = this._labels();
|
|
3146
|
+
|
|
3147
|
+
// Ensure expanded state and remove any limiting classes.
|
|
3148
|
+
c.classList.remove('uc-collapsed');
|
|
3149
|
+
c.classList.remove('uc-expanded'); // No class => fully expanded by default CSS.
|
|
3150
|
+
msg.dataset.ucState = 'expanded';
|
|
3151
|
+
|
|
3152
|
+
// Hide ellipsis in disabled mode.
|
|
3153
|
+
this._hideEllipsis(msg);
|
|
3154
|
+
|
|
3155
|
+
// Hide toggle in disabled mode to avoid user interaction.
|
|
3156
|
+
if (t) {
|
|
3157
|
+
t.classList.remove('visible');
|
|
3158
|
+
t.setAttribute('aria-expanded', 'false');
|
|
3159
|
+
t.title = labels.expand;
|
|
3160
|
+
const img = t.querySelector('img');
|
|
3161
|
+
if (img) { img.alt = labels.expand; }
|
|
3162
|
+
}
|
|
3163
|
+
return; // Do not proceed with measuring or collapsing.
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// Temporarily remove limiting classes for precise measurement.
|
|
3167
|
+
c.classList.remove('uc-collapsed');
|
|
3168
|
+
c.classList.remove('uc-expanded');
|
|
3169
|
+
|
|
3170
|
+
const fullHeight = Math.ceil(c.scrollHeight);
|
|
3171
|
+
const labels = this._labels();
|
|
3172
|
+
const icons = this._icons();
|
|
3173
|
+
const t = toggleEl || msg.querySelector('.uc-toggle');
|
|
3174
|
+
|
|
3175
|
+
if (fullHeight > this.threshold) {
|
|
3176
|
+
if (t) t.classList.add('visible');
|
|
3177
|
+
const desired = msg.dataset.ucState || 'collapsed';
|
|
3178
|
+
const expand = (desired === 'expanded');
|
|
3179
|
+
|
|
3180
|
+
if (expand) {
|
|
3181
|
+
c.classList.add('uc-expanded');
|
|
3182
|
+
this._hideEllipsis(msg); // Expanded => no ellipsis
|
|
3183
|
+
} else {
|
|
3184
|
+
c.classList.add('uc-collapsed');
|
|
3185
|
+
this._showEllipsis(msg, c); // Collapsed => show ellipsis overlay
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
if (t) {
|
|
3189
|
+
const img = t.querySelector('img');
|
|
3190
|
+
if (img) {
|
|
3191
|
+
if (expand) { img.src = icons.collapse; img.alt = labels.collapse; }
|
|
3192
|
+
else { img.src = icons.expand; img.alt = labels.expand; }
|
|
3193
|
+
}
|
|
3194
|
+
t.setAttribute('aria-expanded', expand ? 'true' : 'false');
|
|
3195
|
+
t.title = expand ? labels.collapse : labels.expand;
|
|
3196
|
+
}
|
|
3197
|
+
} else {
|
|
3198
|
+
// Short content – ensure fully expanded and hide toggle + ellipsis.
|
|
3199
|
+
c.classList.remove('uc-collapsed');
|
|
3200
|
+
c.classList.remove('uc-expanded');
|
|
3201
|
+
msg.dataset.ucState = 'expanded';
|
|
3202
|
+
this._hideEllipsis(msg);
|
|
3203
|
+
if (t) {
|
|
3204
|
+
t.classList.remove('visible');
|
|
3205
|
+
t.setAttribute('aria-expanded', 'false');
|
|
3206
|
+
t.title = labels.expand;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
// Toggle handler via the toggle element (div.uc-toggle).
|
|
3212
|
+
toggleFromToggle(toggleEl) {
|
|
3213
|
+
const msg = toggleEl && toggleEl.closest ? toggleEl.closest('.msg-box.msg-user .msg') : null;
|
|
3214
|
+
if (!msg) return;
|
|
3215
|
+
this.toggle(msg);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// Core toggle logic.
|
|
3219
|
+
toggle(msg) {
|
|
3220
|
+
if (!msg || !msg.isConnected) return;
|
|
3221
|
+
const c = msg.querySelector('.uc-content'); if (!c) return;
|
|
3222
|
+
const t = msg.querySelector('.uc-toggle');
|
|
3223
|
+
const labels = this._labels();
|
|
3224
|
+
const icons = this._icons();
|
|
3225
|
+
|
|
3226
|
+
const isCollapsed = c.classList.contains('uc-collapsed');
|
|
3227
|
+
if (isCollapsed) {
|
|
3228
|
+
// Expand – leave scroll as-is; remove ellipsis.
|
|
3229
|
+
c.classList.remove('uc-collapsed');
|
|
3230
|
+
c.classList.add('uc-expanded');
|
|
3231
|
+
msg.dataset.ucState = 'expanded';
|
|
3232
|
+
this._hideEllipsis(msg);
|
|
3233
|
+
if (t) {
|
|
3234
|
+
t.setAttribute('aria-expanded', 'true');
|
|
3235
|
+
t.title = labels.collapse;
|
|
3236
|
+
const img = t.querySelector('img'); if (img) { img.src = icons.collapse; img.alt = labels.collapse; }
|
|
3237
|
+
}
|
|
3238
|
+
} else {
|
|
3239
|
+
// Collapse – apply classes, show ellipsis, then bring toggle into view (scroll up if needed).
|
|
3240
|
+
c.classList.remove('uc-expanded');
|
|
3241
|
+
c.classList.add('uc-collapsed');
|
|
3242
|
+
msg.dataset.ucState = 'collapsed';
|
|
3243
|
+
this._showEllipsis(msg, c);
|
|
3244
|
+
if (t) {
|
|
3245
|
+
t.setAttribute('aria-expanded', 'false');
|
|
3246
|
+
t.title = labels.expand;
|
|
3247
|
+
const img = t.querySelector('img'); if (img) { img.src = icons.expand; img.alt = labels.expand; }
|
|
3248
|
+
// Follow the collapsing content upward – keep the toggle visible.
|
|
3249
|
+
this._scrollToggleIntoView(t);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
// Optional public method to re-evaluate height after layout/resize.
|
|
3255
|
+
remeasureAll() {
|
|
3256
|
+
const arr = Array.from(this._processed || []);
|
|
3257
|
+
for (let i = 0; i < arr.length; i++) {
|
|
3258
|
+
const msg = arr[i];
|
|
3259
|
+
if (!msg || !msg.isConnected) { this._processed.delete(msg); continue; }
|
|
3260
|
+
this._update(msg);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
class NodesManager {
|
|
3266
|
+
constructor(dom, renderer, highlighter, math) {
|
|
3267
|
+
this.dom = dom;
|
|
3268
|
+
this.renderer = renderer;
|
|
3269
|
+
this.highlighter = highlighter;
|
|
3270
|
+
this.math = math;
|
|
3271
|
+
// User message collapse manager
|
|
3272
|
+
this._userCollapse = new UserCollapseManager(this.renderer.cfg);
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// Check if HTML contains only user messages without any markdown or code features.
|
|
3276
|
+
_isUserOnlyContent(html) {
|
|
3277
|
+
try {
|
|
3278
|
+
const tmp = document.createElement('div');
|
|
3279
|
+
tmp.innerHTML = html;
|
|
3280
|
+
const hasBot = !!tmp.querySelector('.msg-box.msg-bot');
|
|
3281
|
+
const hasUser = !!tmp.querySelector('.msg-box.msg-user');
|
|
3282
|
+
const hasMD64 = !!tmp.querySelector('[data-md64]');
|
|
3283
|
+
const hasMDNative = !!tmp.querySelector('[md-block-markdown]');
|
|
3284
|
+
const hasCode = !!tmp.querySelector('pre code');
|
|
3285
|
+
const hasMath = !!tmp.querySelector('script[type^="math/tex"]');
|
|
3286
|
+
return hasUser && !hasBot && !hasMD64 && !hasMDNative && !hasCode && !hasMath;
|
|
3287
|
+
} catch (_) { return false; }
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
// Convert user markdown placeholders into plain text nodes.
|
|
3291
|
+
_materializeUserMdAsPlainText(scopeEl) {
|
|
3292
|
+
try {
|
|
3293
|
+
const nodes = scopeEl.querySelectorAll('.msg-box.msg-user [data-md64], .msg-box.msg-user [md-block-markdown]');
|
|
3294
|
+
nodes.forEach(el => {
|
|
3295
|
+
let txt = '';
|
|
3296
|
+
if (el.hasAttribute('data-md64')) {
|
|
3297
|
+
const b64 = el.getAttribute('data-md64') || '';
|
|
3298
|
+
el.removeAttribute('data-md64');
|
|
3299
|
+
try { txt = this.renderer.b64ToUtf8(b64); } catch (_) { txt = ''; }
|
|
3300
|
+
} else {
|
|
3301
|
+
// Native Markdown block in user message: keep as plain text (no markdown-it)
|
|
3302
|
+
try { txt = el.textContent || ''; } catch (_) { txt = ''; }
|
|
3303
|
+
try { el.removeAttribute('md-block-markdown'); } catch (_) {}
|
|
3304
|
+
}
|
|
3305
|
+
const span = document.createElement('span'); span.textContent = txt; el.replaceWith(span);
|
|
3306
|
+
});
|
|
3307
|
+
} catch (_) {}
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// Append HTML/text into the message input container.
|
|
3311
|
+
// If plain text is provided, wrap it into a minimal msg-user box to keep layout consistent.
|
|
3312
|
+
appendToInput(content) {
|
|
3313
|
+
const el = this.dom.get('_append_input_'); if (!el) return;
|
|
3314
|
+
|
|
3315
|
+
let html = String(content || '');
|
|
3316
|
+
const trimmed = html.trim();
|
|
3317
|
+
|
|
3318
|
+
// If already a full msg-user wrapper, append as-is; otherwise wrap the plain text.
|
|
3319
|
+
const isWrapped = (trimmed.startsWith('<div') && /class=["']msg-box msg-user["']/.test(trimmed));
|
|
3320
|
+
if (!isWrapped) {
|
|
3321
|
+
// Treat incoming payload as plain text (escape + convert newlines to <br>).
|
|
3322
|
+
const safe = (typeof Utils !== 'undefined' && Utils.escapeHtml)
|
|
3323
|
+
? Utils.escapeHtml(html)
|
|
3324
|
+
: String(html).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
3325
|
+
const body = safe.replace(/\r?\n/g, '<br>');
|
|
3326
|
+
// Minimal, margin-less user message (no empty msg-extra to avoid extra spacing).
|
|
3327
|
+
html = `<div class="msg-box msg-user"><div class="msg"><p style="margin:0">${body}</p></div></div>`;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// Synchronous DOM update.
|
|
3331
|
+
el.insertAdjacentHTML('beforeend', html);
|
|
3332
|
+
|
|
3333
|
+
// Apply collapse to any user messages in input area (now or later).
|
|
3334
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// Append nodes into messages list and perform post-processing (markdown, code, math).
|
|
3338
|
+
appendNode(content, scrollMgr) {
|
|
3339
|
+
// Keep scroll behavior consistent with existing logic
|
|
3340
|
+
scrollMgr.userInteracted = false; scrollMgr.prevScroll = 0;
|
|
3341
|
+
this.dom.clearStreamBefore();
|
|
3342
|
+
|
|
3343
|
+
const el = this.dom.get('_nodes_'); if (!el) return;
|
|
3344
|
+
el.classList.remove('empty_list');
|
|
3345
|
+
|
|
3346
|
+
const userOnly = this._isUserOnlyContent(content);
|
|
3347
|
+
if (userOnly) {
|
|
3348
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
3349
|
+
this._materializeUserMdAsPlainText(el);
|
|
3350
|
+
// Collapse before scrolling to ensure final height is used for scroll computations.
|
|
3351
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
3352
|
+
scrollMgr.scrollToBottom(false);
|
|
3353
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
2440
3358
|
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
3359
|
+
try {
|
|
3360
|
+
// Defer post-processing (highlight/math/collapse) and perform scroll AFTER collapse.
|
|
3361
|
+
const maybePromise = this.renderer.renderPendingMarkdown(el);
|
|
3362
|
+
const post = () => {
|
|
3363
|
+
// Viewport highlight scheduling
|
|
3364
|
+
try { this.highlighter.scheduleScanVisibleCodes(null); } catch (_) {}
|
|
2447
3365
|
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
this.stopTimers();
|
|
2451
|
-
const el = this._host();
|
|
2452
|
-
if (el) el.classList.remove('visible');
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
3366
|
+
// In finalize-only mode we must explicitly schedule KaTeX
|
|
3367
|
+
try { if (getMathMode() === 'finalize-only') this.math.schedule(el, 0, true); } catch (_) {}
|
|
2455
3368
|
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
// ==========================================================================
|
|
3369
|
+
// Collapse user messages now that DOM is materialized (ensures correct height).
|
|
3370
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
2459
3371
|
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
hideLoader() {
|
|
2465
|
-
const elements = document.querySelectorAll('.msg-bot');
|
|
2466
|
-
if (elements.length > 0) elements.forEach(el => { const s = el.querySelector('.spinner'); if (s) s.style.display = 'none'; });
|
|
2467
|
-
}
|
|
2468
|
-
begin() { this.showLoader(); }
|
|
2469
|
-
end() { this.hideLoader(); }
|
|
2470
|
-
enable() { const els = document.querySelectorAll('.tool-output'); if (els.length) els[els.length - 1].style.display = 'block'; }
|
|
2471
|
-
disable() { const els = document.querySelectorAll('.tool-output'); if (els.length) els[els.length - 1].style.display = 'none'; }
|
|
2472
|
-
// Append HTML into the latest tool-output content area.
|
|
2473
|
-
append(content) {
|
|
2474
|
-
this.hideLoader(); this.enable();
|
|
2475
|
-
const els = document.querySelectorAll('.tool-output');
|
|
2476
|
-
if (els.length) { const contentEl = els[els.length - 1].querySelector('.content'); if (contentEl) contentEl.insertAdjacentHTML('beforeend', content); }
|
|
2477
|
-
}
|
|
2478
|
-
// Replace inner HTML for the latest tool-output content area.
|
|
2479
|
-
update(content) {
|
|
2480
|
-
this.hideLoader(); this.enable();
|
|
2481
|
-
const els = document.querySelectorAll('.tool-output');
|
|
2482
|
-
if (els.length) { const contentEl = els[els.length - 1].querySelector('.content'); if (contentEl) contentEl.innerHTML = content; }
|
|
2483
|
-
}
|
|
2484
|
-
// Remove children from the latest tool-output content area.
|
|
2485
|
-
clear() {
|
|
2486
|
-
this.hideLoader(); this.enable();
|
|
2487
|
-
const els = document.querySelectorAll('.tool-output');
|
|
2488
|
-
if (els.length) { const contentEl = els[els.length - 1].querySelector('.content'); if (contentEl) contentEl.replaceChildren(); }
|
|
2489
|
-
}
|
|
2490
|
-
// Toggle visibility of a specific tool output block by message id.
|
|
2491
|
-
toggle(id) {
|
|
2492
|
-
const el = document.getElementById('msg-bot-' + id); if (!el) return;
|
|
2493
|
-
const outputEl = el.querySelector('.tool-output'); if (!outputEl) return;
|
|
2494
|
-
const contentEl = outputEl.querySelector('.content');
|
|
2495
|
-
if (contentEl) contentEl.style.display = (contentEl.style.display === 'none') ? 'block' : 'none';
|
|
2496
|
-
const toggleEl = outputEl.querySelector('.toggle-cmd-output img'); if (toggleEl) toggleEl.classList.toggle('toggle-expanded');
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
3372
|
+
// Only now scroll to bottom and update FAB – uses post-collapse heights.
|
|
3373
|
+
scrollMgr.scrollToBottom(false);
|
|
3374
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
3375
|
+
};
|
|
2499
3376
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
// Check if HTML contains only user messages without any markdown or code features.
|
|
2503
|
-
_isUserOnlyContent(html) {
|
|
2504
|
-
try {
|
|
2505
|
-
const tmp = document.createElement('div');
|
|
2506
|
-
tmp.innerHTML = html;
|
|
2507
|
-
const hasBot = !!tmp.querySelector('.msg-box.msg-bot');
|
|
2508
|
-
const hasUser = !!tmp.querySelector('.msg-box.msg-user');
|
|
2509
|
-
const hasMD64 = !!tmp.querySelector('[data-md64]');
|
|
2510
|
-
const hasMDNative = !!tmp.querySelector('[md-block-markdown]');
|
|
2511
|
-
const hasCode = !!tmp.querySelector('pre code');
|
|
2512
|
-
const hasMath = !!tmp.querySelector('script[type^="math/tex"]');
|
|
2513
|
-
return hasUser && !hasBot && !hasMD64 && !hasMDNative && !hasCode && !hasMath;
|
|
2514
|
-
} catch (_) { return false; }
|
|
2515
|
-
}
|
|
2516
|
-
// Convert user markdown placeholders into plain text nodes.
|
|
2517
|
-
_materializeUserMdAsPlainText(scopeEl) {
|
|
2518
|
-
try {
|
|
2519
|
-
const nodes = scopeEl.querySelectorAll('.msg-box.msg-user [data-md64], .msg-box.msg-user [md-block-markdown]');
|
|
2520
|
-
nodes.forEach(el => {
|
|
2521
|
-
let txt = '';
|
|
2522
|
-
if (el.hasAttribute('data-md64')) {
|
|
2523
|
-
const b64 = el.getAttribute('data-md64') || '';
|
|
2524
|
-
el.removeAttribute('data-md64');
|
|
2525
|
-
try { txt = this.renderer.b64ToUtf8(b64); } catch (_) { txt = ''; }
|
|
3377
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
3378
|
+
maybePromise.then(post);
|
|
2526
3379
|
} else {
|
|
2527
|
-
|
|
2528
|
-
try { txt = el.textContent || ''; } catch (_) { txt = ''; }
|
|
2529
|
-
try { el.removeAttribute('md-block-markdown'); } catch (_) {}
|
|
3380
|
+
post();
|
|
2530
3381
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
// Synchronous DOM update – message input must reflect immediately with no waiting.
|
|
2538
|
-
const el = this.dom.get('_append_input_'); if (!el) return; el.insertAdjacentHTML('beforeend', content);
|
|
2539
|
-
}
|
|
2540
|
-
// Append nodes into messages list and perform post-processing (markdown, code, math).
|
|
2541
|
-
appendNode(content, scrollMgr) {
|
|
2542
|
-
// Keep scroll behavior consistent with existing logic
|
|
2543
|
-
scrollMgr.userInteracted = false; scrollMgr.prevScroll = 0;
|
|
2544
|
-
this.dom.clearStreamBefore();
|
|
3382
|
+
} catch (_) {
|
|
3383
|
+
// In case of error, do a conservative scroll to keep UX responsive.
|
|
3384
|
+
scrollMgr.scrollToBottom(false);
|
|
3385
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
2545
3388
|
|
|
2546
|
-
|
|
2547
|
-
|
|
3389
|
+
// Replace messages list content entirely and re-run post-processing.
|
|
3390
|
+
replaceNodes(content, scrollMgr) {
|
|
3391
|
+
// Same semantics as appendNode, but using a hard clone reset
|
|
3392
|
+
scrollMgr.userInteracted = false; scrollMgr.prevScroll = 0;
|
|
3393
|
+
this.dom.clearStreamBefore();
|
|
3394
|
+
|
|
3395
|
+
const el = this.dom.hardReplaceByClone('_nodes_'); if (!el) return;
|
|
3396
|
+
el.classList.remove('empty_list');
|
|
3397
|
+
|
|
3398
|
+
const userOnly = this._isUserOnlyContent(content);
|
|
3399
|
+
if (userOnly) {
|
|
3400
|
+
el.insertAdjacentHTML('beforeend', content);
|
|
3401
|
+
this._materializeUserMdAsPlainText(el);
|
|
3402
|
+
// Collapse before scrolling to ensure final height is used for scroll computations.
|
|
3403
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
3404
|
+
scrollMgr.scrollToBottom(false, true);
|
|
3405
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
2548
3408
|
|
|
2549
|
-
const userOnly = this._isUserOnlyContent(content);
|
|
2550
|
-
if (userOnly) {
|
|
2551
3409
|
el.insertAdjacentHTML('beforeend', content);
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
3410
|
+
|
|
3411
|
+
try {
|
|
3412
|
+
// Defer KaTeX schedule to post-Markdown to avoid races and collapse before scroll.
|
|
3413
|
+
const maybePromise = this.renderer.renderPendingMarkdown(el);
|
|
3414
|
+
const post = () => {
|
|
3415
|
+
try { this.highlighter.scheduleScanVisibleCodes(null); } catch (_) {}
|
|
3416
|
+
try { if (getMathMode() === 'finalize-only') this.math.schedule(el, 0, true); } catch (_) {}
|
|
3417
|
+
|
|
3418
|
+
// Collapse after materialization to compute final heights correctly.
|
|
3419
|
+
try { this._userCollapse.apply(el); } catch (_) {}
|
|
3420
|
+
|
|
3421
|
+
// Now scroll and update FAB using the collapsed layout.
|
|
3422
|
+
scrollMgr.scrollToBottom(false, true);
|
|
3423
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
3424
|
+
};
|
|
3425
|
+
|
|
3426
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
3427
|
+
maybePromise.then(post);
|
|
3428
|
+
} else {
|
|
3429
|
+
post();
|
|
3430
|
+
}
|
|
3431
|
+
} catch (_) {
|
|
3432
|
+
scrollMgr.scrollToBottom(false, true);
|
|
3433
|
+
scrollMgr.scheduleScrollFabUpdate();
|
|
3434
|
+
}
|
|
2556
3435
|
}
|
|
2557
3436
|
|
|
2558
|
-
|
|
3437
|
+
// Append "extra" content into a specific bot message and post-process locally.
|
|
3438
|
+
appendExtra(id, content, scrollMgr) {
|
|
3439
|
+
const el = document.getElementById('msg-bot-' + id); if (!el) return;
|
|
3440
|
+
const extra = el.querySelector('.msg-extra'); if (!extra) return;
|
|
2559
3441
|
|
|
2560
|
-
|
|
2561
|
-
// Schedule all post-processing strictly after Markdown is materialized.
|
|
2562
|
-
const maybePromise = this.renderer.renderPendingMarkdown(el);
|
|
2563
|
-
const post = () => {
|
|
2564
|
-
try { this.highlighter.scheduleScanVisibleCodes(null); } catch (_) {}
|
|
3442
|
+
extra.insertAdjacentHTML('beforeend', content);
|
|
2565
3443
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
try { if (getMathMode() === 'finalize-only') this.math.schedule(el, 0, true); } catch (_) {}
|
|
2569
|
-
};
|
|
3444
|
+
try {
|
|
3445
|
+
const maybePromise = this.renderer.renderPendingMarkdown(extra);
|
|
2570
3446
|
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
} else {
|
|
2574
|
-
post();
|
|
2575
|
-
}
|
|
2576
|
-
} catch (_) { /* swallow to keep append path resilient */ }
|
|
3447
|
+
const post = () => {
|
|
3448
|
+
const activeCode = (typeof runtime !== 'undefined' && runtime.stream) ? runtime.stream.activeCode : null;
|
|
2577
3449
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
3450
|
+
// Attach observers after Markdown produced the nodes
|
|
3451
|
+
try {
|
|
3452
|
+
this.highlighter.observeNewCode(extra, {
|
|
3453
|
+
deferLastIfStreaming: true,
|
|
3454
|
+
minLinesForLast: this.renderer.cfg.PROFILE_CODE.minLinesForHL,
|
|
3455
|
+
minCharsForLast: this.renderer.cfg.PROFILE_CODE.minCharsForHL
|
|
3456
|
+
}, activeCode);
|
|
3457
|
+
this.highlighter.observeMsgBoxes(extra, (box) => this._onBox(box));
|
|
3458
|
+
} catch (_) {}
|
|
2587
3459
|
|
|
2588
|
-
|
|
2589
|
-
|
|
3460
|
+
// KaTeX: honor stream mode; in finalize-only force immediate schedule
|
|
3461
|
+
try {
|
|
3462
|
+
const mm = getMathMode();
|
|
3463
|
+
if (mm === 'finalize-only') this.math.schedule(extra, 0, true);
|
|
3464
|
+
else this.math.schedule(extra);
|
|
3465
|
+
} catch (_) {}
|
|
3466
|
+
};
|
|
3467
|
+
|
|
3468
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
3469
|
+
maybePromise.then(post);
|
|
3470
|
+
} else {
|
|
3471
|
+
post();
|
|
3472
|
+
}
|
|
3473
|
+
} catch (_) { /* swallow */ }
|
|
2590
3474
|
|
|
2591
|
-
|
|
2592
|
-
if (userOnly) {
|
|
2593
|
-
el.insertAdjacentHTML('beforeend', content);
|
|
2594
|
-
this._materializeUserMdAsPlainText(el);
|
|
2595
|
-
scrollMgr.scrollToBottom(false, true);
|
|
2596
|
-
scrollMgr.scheduleScrollFabUpdate();
|
|
2597
|
-
return;
|
|
3475
|
+
scrollMgr.scheduleScroll(true);
|
|
2598
3476
|
}
|
|
2599
3477
|
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3478
|
+
// When a new message box appears, hook up code/highlight handlers.
|
|
3479
|
+
_onBox(box) {
|
|
3480
|
+
const activeCode = (typeof runtime !== 'undefined' && runtime.stream) ? runtime.stream.activeCode : null;
|
|
3481
|
+
this.highlighter.observeNewCode(box, {
|
|
3482
|
+
deferLastIfStreaming: true,
|
|
3483
|
+
minLinesForLast: this.renderer.cfg.PROFILE_CODE.minLinesForHL,
|
|
3484
|
+
minCharsForLast: this.renderer.cfg.PROFILE_CODE.minCharsForHL
|
|
3485
|
+
}, activeCode);
|
|
3486
|
+
this.renderer.hooks.codeScrollInit(box);
|
|
3487
|
+
}
|
|
2609
3488
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
3489
|
+
// Remove message by id and keep scroll consistent.
|
|
3490
|
+
removeNode(id, scrollMgr) {
|
|
3491
|
+
scrollMgr.prevScroll = 0;
|
|
3492
|
+
let el = document.getElementById('msg-user-' + id); if (el) el.remove();
|
|
3493
|
+
el = document.getElementById('msg-bot-' + id); if (el) el.remove();
|
|
3494
|
+
this.dom.resetEphemeral();
|
|
3495
|
+
try { this.renderer.renderPendingMarkdown(); } catch (_) {}
|
|
3496
|
+
scrollMgr.scheduleScroll(true);
|
|
3497
|
+
}
|
|
2616
3498
|
|
|
2617
|
-
|
|
2618
|
-
scrollMgr
|
|
3499
|
+
// Remove all messages from (and including) a given message id.
|
|
3500
|
+
removeNodesFromId(id, scrollMgr) {
|
|
3501
|
+
scrollMgr.prevScroll = 0;
|
|
3502
|
+
const container = this.dom.get('_nodes_'); if (!container) return;
|
|
3503
|
+
const elements = container.querySelectorAll('.msg-box');
|
|
3504
|
+
let remove = false;
|
|
3505
|
+
elements.forEach((element) => {
|
|
3506
|
+
if (element.id && element.id.endsWith('-' + id)) remove = true;
|
|
3507
|
+
if (remove) element.remove();
|
|
3508
|
+
});
|
|
3509
|
+
this.dom.resetEphemeral();
|
|
3510
|
+
try { this.renderer.renderPendingMarkdown(container); } catch (_) {}
|
|
3511
|
+
scrollMgr.scheduleScroll(true);
|
|
3512
|
+
}
|
|
2619
3513
|
}
|
|
2620
|
-
// Append "extra" content into a specific bot message and post-process locally.
|
|
2621
|
-
appendExtra(id, content, scrollMgr) {
|
|
2622
|
-
const el = document.getElementById('msg-bot-' + id); if (!el) return;
|
|
2623
|
-
const extra = el.querySelector('.msg-extra'); if (!extra) return;
|
|
2624
3514
|
|
|
2625
|
-
|
|
3515
|
+
// ==========================================================================
|
|
3516
|
+
// 9a) Template engine for JSON nodes
|
|
3517
|
+
// ==========================================================================
|
|
2626
3518
|
|
|
2627
|
-
|
|
2628
|
-
|
|
3519
|
+
class NodeTemplateEngine {
|
|
3520
|
+
// JS-side templates for nodes rendered from JSON payload (RenderBlock).
|
|
3521
|
+
constructor(cfg, logger) {
|
|
3522
|
+
this.cfg = cfg || {};
|
|
3523
|
+
this.logger = logger || { debug: () => {} };
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
_esc(s) { return (s == null) ? '' : String(s); }
|
|
3527
|
+
_escapeHtml(s) { return (typeof Utils !== 'undefined') ? Utils.escapeHtml(s) : String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); }
|
|
3528
|
+
|
|
3529
|
+
// Render name header given role
|
|
3530
|
+
_nameHeader(role, name, avatarUrl) {
|
|
3531
|
+
if (!name && !avatarUrl) return '';
|
|
3532
|
+
const cls = (role === 'user') ? 'name-user' : 'name-bot';
|
|
3533
|
+
const img = avatarUrl ? `<img src="${this._esc(avatarUrl)}" class="avatar"> ` : '';
|
|
3534
|
+
return `<div class="name-header ${cls}">${img}${this._esc(name || '')}</div>`;
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
// Render user message block
|
|
3538
|
+
_renderUser(block) {
|
|
3539
|
+
const id = block.id;
|
|
3540
|
+
const inp = block.input || {};
|
|
3541
|
+
const msgId = `msg-user-${id}`;
|
|
3542
|
+
|
|
3543
|
+
// NOTE: timestamps intentionally disabled on frontend
|
|
3544
|
+
// let ts = '';
|
|
3545
|
+
// if (inp.timestamp) { ... }
|
|
3546
|
+
|
|
3547
|
+
const personalize = !!(block && block.extra && block.extra.personalize === true);
|
|
3548
|
+
const nameHeader = personalize ? this._nameHeader('user', inp.name || '', inp.avatar_img || null) : '';
|
|
3549
|
+
|
|
3550
|
+
const content = this._escapeHtml(inp.text || '').replace(/\r?\n/g, '<br>');
|
|
3551
|
+
return `<div class="msg-box msg-user" id="${msgId}">${nameHeader}<div class="msg"><p style="margin:0">${content}</p></div></div>`;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
// Render extra blocks (images/files/urls/docs/tool-extra)
|
|
3555
|
+
_renderExtras(block) {
|
|
3556
|
+
const parts = [];
|
|
3557
|
+
|
|
3558
|
+
// images
|
|
3559
|
+
const images = block.images || {};
|
|
3560
|
+
const keysI = Object.keys(images);
|
|
3561
|
+
if (keysI.length) {
|
|
3562
|
+
keysI.forEach((k) => {
|
|
3563
|
+
const it = images[k];
|
|
3564
|
+
if (!it) return;
|
|
3565
|
+
const url = this._esc(it.url); const path = this._esc(it.path); const bn = this._esc(it.basename || '');
|
|
3566
|
+
if (it.is_video) {
|
|
3567
|
+
const src = (it.ext === '.webm' || !it.webm_path) ? path : this._esc(it.webm_path);
|
|
3568
|
+
const ext = (src.endsWith('.webm') ? 'webm' : (path.split('.').pop() || 'mp4'));
|
|
3569
|
+
parts.push(
|
|
3570
|
+
`<div class="extra-src-video-box" title="${url}">` +
|
|
3571
|
+
`<video class="video-player" controls>` +
|
|
3572
|
+
`<source src="${src}" type="video/${ext}">` +
|
|
3573
|
+
`</video>` +
|
|
3574
|
+
`<p><a href="bridge://play_video/${url}" class="title">${this._escapeHtml(bn)}</a></p>` +
|
|
3575
|
+
`</div>`
|
|
3576
|
+
);
|
|
3577
|
+
} else {
|
|
3578
|
+
parts.push(
|
|
3579
|
+
`<div class="extra-src-img-box" title="${url}">` +
|
|
3580
|
+
`<div class="img-outer"><div class="img-wrapper"><a href="${url}"><img src="${path}" class="image"></a></div>` +
|
|
3581
|
+
`<a href="${url}" class="title">${this._escapeHtml(bn)}</a></div>` +
|
|
3582
|
+
`</div><br/>`
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
2629
3587
|
|
|
2630
|
-
|
|
2631
|
-
|
|
3588
|
+
// files
|
|
3589
|
+
const files = block.files || {};
|
|
3590
|
+
const kF = Object.keys(files);
|
|
3591
|
+
if (kF.length) {
|
|
3592
|
+
const rows = [];
|
|
3593
|
+
kF.forEach((k) => {
|
|
3594
|
+
const it = files[k]; if (!it) return;
|
|
3595
|
+
const url = this._esc(it.url); const path = this._esc(it.path);
|
|
3596
|
+
const icon = (typeof window !== 'undefined' && window.ICON_ATTACHMENTS) ? `<img src="${window.ICON_ATTACHMENTS}" class="extra-src-icon">` : '';
|
|
3597
|
+
rows.push(`${icon} <b> [${k}] </b> <a href="${url}">${path}</a>`);
|
|
3598
|
+
});
|
|
3599
|
+
if (rows.length) parts.push(`<div>${rows.join("<br/><br/>")}</div>`);
|
|
3600
|
+
}
|
|
2632
3601
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
3602
|
+
// urls
|
|
3603
|
+
const urls = block.urls || {};
|
|
3604
|
+
const kU = Object.keys(urls);
|
|
3605
|
+
if (kU.length) {
|
|
3606
|
+
const rows = [];
|
|
3607
|
+
kU.forEach((k) => {
|
|
3608
|
+
const it = urls[k]; if (!it) return;
|
|
3609
|
+
const url = this._esc(it.url);
|
|
3610
|
+
const icon = (typeof window !== 'undefined' && window.ICON_URL) ? `<img src="${window.ICON_URL}" class="extra-src-icon">` : '';
|
|
3611
|
+
rows.push(`${icon}<a href="${url}" title="${url}">${url}</a> <small> [${k}] </small>`);
|
|
3612
|
+
});
|
|
3613
|
+
if (rows.length) parts.push(`<div>${rows.join("<br/><br/>")}</div>`);
|
|
3614
|
+
}
|
|
2642
3615
|
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3616
|
+
// docs (render on JS) or fallback to docs_html
|
|
3617
|
+
const extra = block.extra || {};
|
|
3618
|
+
const docsRaw = Array.isArray(extra.docs) ? extra.docs : null;
|
|
3619
|
+
|
|
3620
|
+
if (docsRaw && docsRaw.length) {
|
|
3621
|
+
const icon = (typeof window !== 'undefined' && window.ICON_DB) ? `<img src="${window.ICON_DB}" class="extra-src-icon">` : '';
|
|
3622
|
+
const prefix = (typeof window !== 'undefined' && window.LOCALE_DOC_PREFIX) ? String(window.LOCALE_DOC_PREFIX) : 'Doc:';
|
|
3623
|
+
const limit = 3;
|
|
3624
|
+
|
|
3625
|
+
// normalize: [{uuid, meta}] OR [{ uuid: {...} }]
|
|
3626
|
+
const normalized = [];
|
|
3627
|
+
docsRaw.forEach((it) => {
|
|
3628
|
+
if (!it || typeof it !== 'object') return;
|
|
3629
|
+
if ('uuid' in it && 'meta' in it && typeof it.meta === 'object') {
|
|
3630
|
+
normalized.push({ uuid: String(it.uuid), meta: it.meta || {} });
|
|
3631
|
+
} else {
|
|
3632
|
+
const keys = Object.keys(it);
|
|
3633
|
+
if (keys.length === 1) {
|
|
3634
|
+
const uuid = keys[0];
|
|
3635
|
+
const meta = it[uuid];
|
|
3636
|
+
if (meta && typeof meta === 'object') {
|
|
3637
|
+
normalized.push({ uuid: String(uuid), meta });
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
});
|
|
2651
3642
|
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3643
|
+
const rows = [];
|
|
3644
|
+
for (let i = 0; i < Math.min(limit, normalized.length); i++) {
|
|
3645
|
+
const d = normalized[i];
|
|
3646
|
+
const meta = d.meta || {};
|
|
3647
|
+
const entries = Object.keys(meta).map(k => `<b>${this._escapeHtml(k)}:</b> ${this._escapeHtml(String(meta[k]))}`).join(', ');
|
|
3648
|
+
rows.push(`<p><small>[${i + 1}] ${this._escapeHtml(d.uuid)}: ${entries}</small></p>`);
|
|
2656
3649
|
}
|
|
2657
|
-
|
|
3650
|
+
if (rows.length) {
|
|
3651
|
+
parts.push(`<p>${icon}<small><b>${this._escapeHtml(prefix)}:</b></small></p>`);
|
|
3652
|
+
parts.push(`<div class="cmd"><p>${rows.join('')}</p></div>`);
|
|
3653
|
+
}
|
|
3654
|
+
} else {
|
|
3655
|
+
// backward compat
|
|
3656
|
+
const docs_html = extra && extra.docs_html ? String(extra.docs_html) : '';
|
|
3657
|
+
if (docs_html) parts.push(docs_html);
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// plugin-driven tool extra HTML
|
|
3661
|
+
const tool_extra_html = extra && extra.tool_extra_html ? String(extra.tool_extra_html) : '';
|
|
3662
|
+
if (tool_extra_html) parts.push(`<div class="msg-extra">${tool_extra_html}</div>`);
|
|
2658
3663
|
|
|
2659
|
-
|
|
3664
|
+
return parts.join('');
|
|
2660
3665
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
scrollMgr.prevScroll = 0;
|
|
2674
|
-
let el = document.getElementById('msg-user-' + id); if (el) el.remove();
|
|
2675
|
-
el = document.getElementById('msg-bot-' + id); if (el) el.remove();
|
|
2676
|
-
this.dom.resetEphemeral();
|
|
2677
|
-
try { this.renderer.renderPendingMarkdown(); } catch (_) {}
|
|
2678
|
-
scrollMgr.scheduleScroll(true);
|
|
2679
|
-
}
|
|
2680
|
-
// Remove all messages from (and including) a given message id.
|
|
2681
|
-
removeNodesFromId(id, scrollMgr) {
|
|
2682
|
-
scrollMgr.prevScroll = 0;
|
|
2683
|
-
const container = this.dom.get('_nodes_'); if (!container) return;
|
|
2684
|
-
const elements = container.querySelectorAll('.msg-box');
|
|
2685
|
-
let remove = false;
|
|
2686
|
-
elements.forEach((element) => {
|
|
2687
|
-
if (element.id && element.id.endsWith('-' + id)) remove = true;
|
|
2688
|
-
if (remove) element.remove();
|
|
3666
|
+
|
|
3667
|
+
// Render message-level actions
|
|
3668
|
+
_renderActions(block) {
|
|
3669
|
+
const extra = block.extra || {};
|
|
3670
|
+
const actions = extra.actions || [];
|
|
3671
|
+
if (!actions || !actions.length) return '';
|
|
3672
|
+
const parts = actions.map((a) => {
|
|
3673
|
+
const href = this._esc(a.href || '#');
|
|
3674
|
+
const title = this._esc(a.title || '');
|
|
3675
|
+
const icon = this._esc(a.icon || '');
|
|
3676
|
+
const id = this._esc(a.id || block.id);
|
|
3677
|
+
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>`;
|
|
2689
3678
|
});
|
|
2690
|
-
this.
|
|
2691
|
-
|
|
2692
|
-
|
|
3679
|
+
return `<div class="action-icons" data-id="${this._esc(block.id)}">${parts.join('')}</div>`;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
// Render tool output wrapper (always collapsed by default; wrapper visibility depends on flag)
|
|
3683
|
+
// Inside class NodeTemplateEngine
|
|
3684
|
+
_renderToolOutputWrapper(block) {
|
|
3685
|
+
const extra = block.extra || {};
|
|
3686
|
+
|
|
3687
|
+
// IMPORTANT: keep initial tool output verbatim (HTML-ready).
|
|
3688
|
+
// Do NOT HTML-escape here – the host already provides a safe/HTML-ready string.
|
|
3689
|
+
// Escaping again would double-encode entities (e.g. " -> "), which
|
|
3690
|
+
// caused visible """ in the UI instead of quotes.
|
|
3691
|
+
const tool_output_html = (extra.tool_output != null) ? String(extra.tool_output) : '';
|
|
3692
|
+
|
|
3693
|
+
// Wrapper visibility: show/hide based on tool_output_visible...
|
|
3694
|
+
const wrapperDisplay = (extra.tool_output_visible === true) ? '' : 'display:none';
|
|
3695
|
+
|
|
3696
|
+
const toggleTitle = (typeof trans !== 'undefined' && trans) ? trans('action.cmd.expand') : 'Expand';
|
|
3697
|
+
const expIcon = (typeof window !== 'undefined' && window.ICON_EXPAND) ? window.ICON_EXPAND : '';
|
|
3698
|
+
|
|
3699
|
+
return (
|
|
3700
|
+
`<div class='tool-output' style='${wrapperDisplay}'>` +
|
|
3701
|
+
`<span class='toggle-cmd-output' onclick='toggleToolOutput(${this._esc(block.id)});' ` +
|
|
3702
|
+
`title='${this._escapeHtml(toggleTitle)}' role='button'>` +
|
|
3703
|
+
`<img src='${this._esc(expIcon)}' width='25' height='25' valign='middle'>` +
|
|
3704
|
+
`</span>` +
|
|
3705
|
+
// Content is initially collapsed. We intentionally do NOT escape here,
|
|
3706
|
+
// to keep behavior consistent with ToolOutput.append/update (HTML-in).
|
|
3707
|
+
`<div class='content' style='display:none' data-trusted='1'>${tool_output_html}</div>` +
|
|
3708
|
+
`</div>`
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
// Render bot message block (md-block-markdown)
|
|
3713
|
+
_renderBot(block) {
|
|
3714
|
+
const id = block.id;
|
|
3715
|
+
const out = block.output || {};
|
|
3716
|
+
const msgId = `msg-bot-${id}`;
|
|
3717
|
+
|
|
3718
|
+
// NOTE: timestamps intentionally disabled on frontend
|
|
3719
|
+
// let ts = '';
|
|
3720
|
+
// if (out.timestamp) { ... }
|
|
3721
|
+
|
|
3722
|
+
const personalize = !!(block && block.extra && block.extra.personalize === true);
|
|
3723
|
+
const nameHeader = personalize ? this._nameHeader('bot', out.name || '', out.avatar_img || null) : '';
|
|
3724
|
+
|
|
3725
|
+
const mdText = this._escapeHtml(out.text || '');
|
|
3726
|
+
const toolWrap = this._renderToolOutputWrapper(block);
|
|
3727
|
+
const extras = this._renderExtras(block);
|
|
3728
|
+
const actions = (block.extra && block.extra.footer_icons) ? this._renderActions(block) : '';
|
|
3729
|
+
const debug = (block.extra && block.extra.debug_html) ? String(block.extra.debug_html) : '';
|
|
3730
|
+
|
|
3731
|
+
return (
|
|
3732
|
+
`<div class='msg-box msg-bot' id='${msgId}'>` +
|
|
3733
|
+
`${nameHeader}` +
|
|
3734
|
+
`<div class='msg'>` +
|
|
3735
|
+
`<div class='md-block' md-block-markdown='1'>${mdText}</div>` +
|
|
3736
|
+
`<div class='msg-tool-extra'></div>` +
|
|
3737
|
+
`${toolWrap}` +
|
|
3738
|
+
`<div class='msg-extra'>${extras}</div>` +
|
|
3739
|
+
`${actions}${debug}` +
|
|
3740
|
+
`</div>` +
|
|
3741
|
+
`</div>`
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
// Render one RenderBlock into HTML (may produce 1 or 2 messages – input and/or output)
|
|
3746
|
+
renderNode(block) {
|
|
3747
|
+
const parts = [];
|
|
3748
|
+
if (block && block.input && block.input.text) parts.push(this._renderUser(block));
|
|
3749
|
+
if (block && block.output && block.output.text) parts.push(this._renderBot(block));
|
|
3750
|
+
return parts.join('');
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// Render array of blocks
|
|
3754
|
+
renderNodes(blocks) {
|
|
3755
|
+
if (!Array.isArray(blocks)) return '';
|
|
3756
|
+
const out = [];
|
|
3757
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
3758
|
+
const b = blocks[i] || null;
|
|
3759
|
+
if (!b) continue;
|
|
3760
|
+
out.push(this.renderNode(b));
|
|
3761
|
+
}
|
|
3762
|
+
return out.join('');
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
// ==========================================================================
|
|
3767
|
+
// 9b) Data receiver for append/replace nodes
|
|
3768
|
+
// ==========================================================================
|
|
3769
|
+
|
|
3770
|
+
class DataReceiver {
|
|
3771
|
+
// Normalizes payload (HTML string or JSON) and delegates to NodesManager.
|
|
3772
|
+
constructor(cfg, templates, nodes, scrollMgr) {
|
|
3773
|
+
this.cfg = cfg || {};
|
|
3774
|
+
this.templates = templates;
|
|
3775
|
+
this.nodes = nodes;
|
|
3776
|
+
this.scrollMgr = scrollMgr;
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
_tryParseJSON(s) {
|
|
3780
|
+
if (typeof s !== 'string') return s;
|
|
3781
|
+
const t = s.trim();
|
|
3782
|
+
if (!t) return null;
|
|
3783
|
+
// If it's like HTML, don't parse as JSON
|
|
3784
|
+
if (t[0] === '<') return null;
|
|
3785
|
+
try { return JSON.parse(t); } catch (_) { return null; }
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
_normalizeToBlocks(obj) {
|
|
3789
|
+
if (!obj) return [];
|
|
3790
|
+
if (Array.isArray(obj)) return obj;
|
|
3791
|
+
if (obj.node) return [obj.node];
|
|
3792
|
+
if (obj.nodes) return (Array.isArray(obj.nodes) ? obj.nodes : []);
|
|
3793
|
+
// single node-like object
|
|
3794
|
+
if (typeof obj === 'object' && (obj.input || obj.output || obj.id)) return [obj];
|
|
3795
|
+
return [];
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
append(payload) {
|
|
3799
|
+
// Legacy HTML string?
|
|
3800
|
+
if (typeof payload === 'string' && payload.trim().startsWith('<')) {
|
|
3801
|
+
this.nodes.appendNode(payload, this.scrollMgr);
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
// Try JSON
|
|
3805
|
+
const obj = this._tryParseJSON(payload);
|
|
3806
|
+
if (!obj) {
|
|
3807
|
+
// Not JSON – pass through
|
|
3808
|
+
this.nodes.appendNode(String(payload), this.scrollMgr);
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
const blocks = this._normalizeToBlocks(obj);
|
|
3812
|
+
if (!blocks.length) {
|
|
3813
|
+
this.nodes.appendNode('', this.scrollMgr);
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
const html = this.templates.renderNodes(blocks);
|
|
3817
|
+
this.nodes.appendNode(html, this.scrollMgr);
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
replace(payload) {
|
|
3821
|
+
// Legacy HTML string?
|
|
3822
|
+
if (typeof payload === 'string' && payload.trim().startsWith('<')) {
|
|
3823
|
+
this.nodes.replaceNodes(payload, this.scrollMgr);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
// Try JSON
|
|
3827
|
+
const obj = this._tryParseJSON(payload);
|
|
3828
|
+
if (!obj) {
|
|
3829
|
+
this.nodes.replaceNodes(String(payload), this.scrollMgr);
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
const blocks = this._normalizeToBlocks(obj);
|
|
3833
|
+
if (!blocks.length) {
|
|
3834
|
+
this.nodes.replaceNodes('', this.scrollMgr);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
const html = this.templates.renderNodes(blocks);
|
|
3838
|
+
this.nodes.replaceNodes(html, this.scrollMgr);
|
|
2693
3839
|
}
|
|
2694
3840
|
}
|
|
2695
3841
|
|
|
@@ -2698,36 +3844,48 @@
|
|
|
2698
3844
|
// ==========================================================================
|
|
2699
3845
|
|
|
2700
3846
|
class UIManager {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
3847
|
+
// Replace or insert app-level CSS in a <style> tag.
|
|
3848
|
+
updateCSS(styles) {
|
|
3849
|
+
let style = document.getElementById('app-style');
|
|
3850
|
+
if (!style) { style = document.createElement('style'); style.id = 'app-style'; document.head.appendChild(style); }
|
|
3851
|
+
style.textContent = styles;
|
|
3852
|
+
}
|
|
3853
|
+
// Ensure base styles for code header sticky behavior exist.
|
|
3854
|
+
ensureStickyHeaderStyle() {
|
|
3855
|
+
let style = document.getElementById('code-sticky-style');
|
|
3856
|
+
if (style) return;
|
|
3857
|
+
style = document.createElement('style'); style.id = 'code-sticky-style';
|
|
3858
|
+
style.textContent = [
|
|
3859
|
+
'.code-wrapper { position: relative; }',
|
|
3860
|
+
'.code-wrapper .code-header-wrapper { position: sticky; top: var(--code-header-sticky-top, 0px); z-index: 2; box-shadow: 0 1px 0 rgba(0,0,0,.06); }',
|
|
3861
|
+
'.code-wrapper pre { overflow: visible; margin-top: 0; }',
|
|
3862
|
+
'.code-wrapper pre code { display: block; white-space: pre; max-height: 100dvh; overflow: auto;',
|
|
3863
|
+
' overscroll-behavior: contain; -webkit-overflow-scrolling: touch; overflow-anchor: none; scrollbar-gutter: stable both-edges; scroll-behavior: auto; }',
|
|
3864
|
+
'#_loader_.hidden { display: none !important; visibility: hidden !important; }',
|
|
3865
|
+
'#_loader_.visible { display: block; visibility: visible; }',
|
|
3866
|
+
|
|
3867
|
+
/* User message collapse (uc-*) */
|
|
3868
|
+
'.msg-box.msg-user .msg { position: relative; }',
|
|
3869
|
+
'.msg-box.msg-user .msg > .uc-content { display: block; overflow: visible; }',
|
|
3870
|
+
'.msg-box.msg-user .msg > .uc-content.uc-collapsed { max-height: 1000px; overflow: hidden; }',
|
|
3871
|
+
'.msg-box.msg-user .msg > .uc-toggle { display: none; margin-top: 8px; text-align: center; cursor: pointer; user-select: none; }',
|
|
3872
|
+
'.msg-box.msg-user .msg > .uc-toggle.visible { display: block; }',
|
|
3873
|
+
|
|
3874
|
+
/* Increased toggle icon size to a comfortable/default size.
|
|
3875
|
+
Overridable via CSS var --uc-toggle-icon-size to keep host-level control. */
|
|
3876
|
+
'.msg-box.msg-user .msg > .uc-toggle img { width: var(--uc-toggle-icon-size, 26px); height: var(--uc-toggle-icon-size, 26px); opacity: .8; }',
|
|
3877
|
+
'.msg-box.msg-user .msg > .uc-toggle:hover img { opacity: 1; }'
|
|
3878
|
+
].join('\n');
|
|
3879
|
+
document.head.appendChild(style);
|
|
3880
|
+
}
|
|
3881
|
+
// Toggle classes controlling optional UI features.
|
|
3882
|
+
enableEditIcons() { document.body && document.body.classList.add('display-edit-icons'); }
|
|
3883
|
+
disableEditIcons() { document.body && document.body.classList.remove('display-edit-icons'); }
|
|
3884
|
+
enableTimestamp() { document.body && document.body.classList.add('display-timestamp'); }
|
|
3885
|
+
disableTimestamp() { document.body && document.body.classList.remove('display-timestamp'); }
|
|
3886
|
+
enableBlocks() { document.body && document.body.classList.add('display-blocks'); }
|
|
3887
|
+
disableBlocks() { document.body && document.body.classList.remove('display-blocks'); }
|
|
3888
|
+
}
|
|
2731
3889
|
|
|
2732
3890
|
// ==========================================================================
|
|
2733
3891
|
// 11) Stream snapshot engine + incremental code streaming
|
|
@@ -2767,9 +3925,16 @@
|
|
|
2767
3925
|
// Tracks whether renderSnapshot injected a one-off synthetic EOL for parsing an open fence
|
|
2768
3926
|
// (used to strip it from the initial streaming tail to avoid "#\n foo" on first line).
|
|
2769
3927
|
this._lastInjectedEOL = false;
|
|
3928
|
+
|
|
3929
|
+
this._customFenceSpecs = []; // [{ open, close }, ...]
|
|
3930
|
+
this._fenceCustom = null; // currently active custom fence spec or null
|
|
2770
3931
|
}
|
|
2771
3932
|
_d(tag, data) { this.logger.debug('STREAM', tag, data); }
|
|
2772
3933
|
|
|
3934
|
+
setCustomFenceSpecs(specs) {
|
|
3935
|
+
this._customFenceSpecs = Array.isArray(specs) ? specs.slice() : [];
|
|
3936
|
+
}
|
|
3937
|
+
|
|
2773
3938
|
// --- Rope buffer helpers (internal) ---
|
|
2774
3939
|
|
|
2775
3940
|
// Append a chunk into the rope without immediately touching the large string.
|
|
@@ -2814,6 +3979,7 @@
|
|
|
2814
3979
|
|
|
2815
3980
|
// Clear any previous synthetic EOL marker.
|
|
2816
3981
|
this._lastInjectedEOL = false;
|
|
3982
|
+
this._fenceCustom = null;
|
|
2817
3983
|
|
|
2818
3984
|
this._d('RESET', { });
|
|
2819
3985
|
}
|
|
@@ -2923,11 +4089,13 @@
|
|
|
2923
4089
|
if (!atLineStart) { i++; continue; }
|
|
2924
4090
|
atLineStart = false;
|
|
2925
4091
|
|
|
4092
|
+
// Skip list/blockquote/indent normalization (existing logic)
|
|
2926
4093
|
let j = i;
|
|
2927
4094
|
while (j < n) {
|
|
2928
4095
|
let localSpaces = 0;
|
|
2929
4096
|
while (j < n && (s[j] === ' ' || s[j] === '\t')) { localSpaces += (s[j] === '\t') ? 4 : 1; j++; if (localSpaces > 3) break; }
|
|
2930
4097
|
if (j < n && s[j] === '>') { j++; if (j < n && s[j] === ' ') j++; continue; }
|
|
4098
|
+
|
|
2931
4099
|
let saved = j;
|
|
2932
4100
|
if (j < n && (s[j] === '-' || s[j] === '*' || s[j] === '+')) {
|
|
2933
4101
|
let jj = j + 1; if (jj < n && s[jj] === ' ') { j = jj + 1; } else { j = saved; }
|
|
@@ -2947,6 +4115,47 @@
|
|
|
2947
4115
|
}
|
|
2948
4116
|
if (indent > 3) { i = j; continue; }
|
|
2949
4117
|
|
|
4118
|
+
// 1) Custom fences first (e.g. [!exec] ... [/!exec], <execute>...</execute>)
|
|
4119
|
+
if (!this.fenceOpen && this._customFenceSpecs && this._customFenceSpecs.length) {
|
|
4120
|
+
for (let ci = 0; ci < this._customFenceSpecs.length; ci++) {
|
|
4121
|
+
const spec = this._customFenceSpecs[ci];
|
|
4122
|
+
const open = spec && spec.open ? spec.open : '';
|
|
4123
|
+
if (!open) continue;
|
|
4124
|
+
const k = j + open.length;
|
|
4125
|
+
if (k <= n && s.slice(j, k) === open) {
|
|
4126
|
+
if (!inNewOrCrosses(j, k)) { /* seen fully in previous prefix */ }
|
|
4127
|
+
else {
|
|
4128
|
+
this.fenceOpen = true; this._fenceCustom = spec; opened = true; i = k;
|
|
4129
|
+
this._d('FENCE_OPEN_DETECTED_CUSTOM', { open, idxStart: j, idxEnd: k, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4130
|
+
continue; // main while
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
} else if (this.fenceOpen && this._fenceCustom && this._fenceCustom.close) {
|
|
4135
|
+
const close = this._fenceCustom.close;
|
|
4136
|
+
const k = j + close.length;
|
|
4137
|
+
if (k <= n && s.slice(j, k) === close) {
|
|
4138
|
+
// Require only trailing whitespace on the line (consistent with ``` logic)
|
|
4139
|
+
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
4140
|
+
const onlyWS = this.onlyTrailingWhitespace(s, k, eol);
|
|
4141
|
+
if (onlyWS) {
|
|
4142
|
+
if (!inNewOrCrosses(j, k)) { /* seen in previous prefix */ }
|
|
4143
|
+
else {
|
|
4144
|
+
this.fenceOpen = false; this._fenceCustom = null; closed = true;
|
|
4145
|
+
const endInS = k;
|
|
4146
|
+
const rel = endInS - preLen;
|
|
4147
|
+
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4148
|
+
i = k;
|
|
4149
|
+
this._d('FENCE_CLOSE_DETECTED_CUSTOM', { close, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4150
|
+
continue; // main while
|
|
4151
|
+
}
|
|
4152
|
+
} else {
|
|
4153
|
+
this._d('FENCE_CLOSE_REJECTED_CUSTOM_NON_WS_AFTER', { close, idxStart: j, idxEnd: k });
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
// 2) Standard markdown-it fences (``` or ~~~) – leave your original logic intact
|
|
2950
4159
|
if (j < n && (s[j] === '`' || s[j] === '~')) {
|
|
2951
4160
|
const mark = s[j]; let k = j; while (k < n && s[k] === mark) k++; const run = k - j;
|
|
2952
4161
|
|
|
@@ -2954,10 +4163,10 @@
|
|
|
2954
4163
|
if (run >= 3) {
|
|
2955
4164
|
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
2956
4165
|
this.fenceOpen = true; this.fenceMark = mark; this.fenceLen = run; opened = true; i = k;
|
|
2957
|
-
this._d('FENCE_OPEN_DETECTED', { mark, run, idxStart: j, idxEnd: k,
|
|
4166
|
+
this._d('FENCE_OPEN_DETECTED', { mark, run, idxStart: j, idxEnd: k, region: (j >= preLen) ? 'new' : 'cross' });
|
|
2958
4167
|
continue;
|
|
2959
4168
|
}
|
|
2960
|
-
} else {
|
|
4169
|
+
} else if (!this._fenceCustom) {
|
|
2961
4170
|
if (mark === this.fenceMark && run >= this.fenceLen) {
|
|
2962
4171
|
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
2963
4172
|
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
@@ -2965,16 +4174,17 @@
|
|
|
2965
4174
|
this.fenceOpen = false; closed = true;
|
|
2966
4175
|
const endInS = k;
|
|
2967
4176
|
const rel = endInS - preLen;
|
|
2968
|
-
|
|
2969
|
-
|
|
4177
|
+
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4178
|
+
i = k;
|
|
2970
4179
|
this._d('FENCE_CLOSE_DETECTED', { mark, run, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
2971
4180
|
continue;
|
|
2972
4181
|
} else {
|
|
2973
|
-
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k
|
|
4182
|
+
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k });
|
|
2974
4183
|
}
|
|
2975
4184
|
}
|
|
2976
4185
|
}
|
|
2977
4186
|
}
|
|
4187
|
+
|
|
2978
4188
|
i = j + 1;
|
|
2979
4189
|
}
|
|
2980
4190
|
|
|
@@ -2983,6 +4193,7 @@
|
|
|
2983
4193
|
this.fenceTail = s.slice(-3);
|
|
2984
4194
|
return { opened, closed, splitAt };
|
|
2985
4195
|
}
|
|
4196
|
+
|
|
2986
4197
|
// Ensure message snapshot container exists.
|
|
2987
4198
|
getMsgSnapshotRoot(msg) {
|
|
2988
4199
|
if (!msg) return null;
|
|
@@ -3422,6 +4633,48 @@
|
|
|
3422
4633
|
this.schedulePromoteTail(true);
|
|
3423
4634
|
}
|
|
3424
4635
|
}
|
|
4636
|
+
// Keep language header stable across snapshots for the active streaming code.
|
|
4637
|
+
// If the current snapshot produced a tiny/unsupported token (e.g. 'on', 'ml', 's'),
|
|
4638
|
+
// reuse the last known good language (from previous active state or sticky attribute).
|
|
4639
|
+
stabilizeHeaderLabel(prevAC, newAC) {
|
|
4640
|
+
try {
|
|
4641
|
+
if (!newAC || !newAC.codeEl || !newAC.codeEl.isConnected) return;
|
|
4642
|
+
|
|
4643
|
+
const wrap = newAC.codeEl.closest('.code-wrapper');
|
|
4644
|
+
if (!wrap) return;
|
|
4645
|
+
|
|
4646
|
+
const span = wrap.querySelector('.code-header-lang');
|
|
4647
|
+
const curLabel = (span && span.textContent ? span.textContent.trim() : '').toLowerCase();
|
|
4648
|
+
|
|
4649
|
+
// Do not touch tool/output blocks
|
|
4650
|
+
if (curLabel === 'output') return;
|
|
4651
|
+
|
|
4652
|
+
const tokNow = (wrap.getAttribute('data-code-lang') || '').trim().toLowerCase();
|
|
4653
|
+
const sticky = (wrap.getAttribute('data-lang-sticky') || '').trim().toLowerCase();
|
|
4654
|
+
const prev = (prevAC && prevAC.lang && prevAC.lang !== 'plaintext') ? prevAC.lang.toLowerCase() : '';
|
|
4655
|
+
|
|
4656
|
+
const valid = (t) => !!t && t !== 'plaintext' && this._isHLJSSupported(t);
|
|
4657
|
+
|
|
4658
|
+
let finalTok = '';
|
|
4659
|
+
if (valid(tokNow)) finalTok = tokNow;
|
|
4660
|
+
else if (valid(prev)) finalTok = prev;
|
|
4661
|
+
else if (valid(sticky)) finalTok = sticky;
|
|
4662
|
+
|
|
4663
|
+
if (finalTok) {
|
|
4664
|
+
// Update code class and header label consistently
|
|
4665
|
+
this._updateCodeLangClass(newAC.codeEl, finalTok);
|
|
4666
|
+
this._updateCodeHeaderLabel(newAC.codeEl, finalTok, finalTok);
|
|
4667
|
+
try { wrap.setAttribute('data-code-lang', finalTok); } catch (_) {}
|
|
4668
|
+
try { wrap.setAttribute('data-lang-sticky', finalTok); } catch (_) {}
|
|
4669
|
+
newAC.lang = finalTok; // keep AC state in sync
|
|
4670
|
+
} else {
|
|
4671
|
+
// If current label looks like a tiny/incomplete token, normalize to 'code'
|
|
4672
|
+
if (span && curLabel && curLabel.length < 3) {
|
|
4673
|
+
span.textContent = 'code';
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
} catch (_) { /* defensive: never break streaming path */ }
|
|
4677
|
+
}
|
|
3425
4678
|
// Render a snapshot of current stream buffer into the DOM.
|
|
3426
4679
|
renderSnapshot(msg) {
|
|
3427
4680
|
const streaming = !!this.isStreaming;
|
|
@@ -3456,13 +4709,21 @@
|
|
|
3456
4709
|
range.selectNodeContents(snap);
|
|
3457
4710
|
frag = range.createContextualFragment(html);
|
|
3458
4711
|
} catch (_) {
|
|
3459
|
-
// Fallback: safe temporary container
|
|
3460
4712
|
const tmp = document.createElement('div');
|
|
3461
4713
|
tmp.innerHTML = html;
|
|
3462
4714
|
frag = document.createDocumentFragment();
|
|
3463
4715
|
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
3464
4716
|
}
|
|
3465
4717
|
|
|
4718
|
+
// (stream-aware custom markup):
|
|
4719
|
+
// Apply Custom Markup on the fragment only if at least one rule opted-in for stream.
|
|
4720
|
+
try {
|
|
4721
|
+
if (this.renderer && this.renderer.customMarkup && this.renderer.customMarkup.hasStreamRules()) {
|
|
4722
|
+
const MDinline = this.renderer.MD_STREAM || this.renderer.MD || null;
|
|
4723
|
+
this.renderer.customMarkup.applyStream(frag, MDinline);
|
|
4724
|
+
}
|
|
4725
|
+
} catch (_) { /* keep snapshot path resilient */ }
|
|
4726
|
+
|
|
3466
4727
|
// Reuse closed, stable code blocks from previous snapshot to avoid re-highlighting
|
|
3467
4728
|
this.preserveStableClosedCodes(snap, frag, this.fenceOpen === true);
|
|
3468
4729
|
|
|
@@ -3474,12 +4735,21 @@
|
|
|
3474
4735
|
this._ensureBottomForJustFinalized(snap);
|
|
3475
4736
|
|
|
3476
4737
|
// Setup active streaming code if fence is open, otherwise clear active state
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
this.
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
4738
|
+
const prevAC = this.activeCode; // remember previous active streaming state (if any)
|
|
4739
|
+
|
|
4740
|
+
if (this.fenceOpen) {
|
|
4741
|
+
const newAC = this.setupActiveCodeFromSnapshot(snap);
|
|
4742
|
+
|
|
4743
|
+
// preserve previous frozen/tail state and stable lang/header across snapshots
|
|
4744
|
+
if (prevAC && newAC) {
|
|
4745
|
+
this.rehydrateActiveCode(prevAC, newAC);
|
|
4746
|
+
this.stabilizeHeaderLabel(prevAC, newAC);
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
this.activeCode = newAC || null;
|
|
4750
|
+
} else {
|
|
4751
|
+
this.activeCode = null;
|
|
4752
|
+
}
|
|
3483
4753
|
|
|
3484
4754
|
// Attach scroll/highlight observers (viewport aware)
|
|
3485
4755
|
if (!this.fenceOpen) {
|
|
@@ -3536,6 +4806,7 @@
|
|
|
3536
4806
|
const dt = Utils.now() - t0;
|
|
3537
4807
|
this._d('SNAPSHOT', { fenceOpen: this.fenceOpen, activeCode: !!this.activeCode, bufLen: this.getStreamLength(), timeMs: Math.round(dt), streaming });
|
|
3538
4808
|
}
|
|
4809
|
+
|
|
3539
4810
|
// Get current message container (.msg) or create if allowed.
|
|
3540
4811
|
getMsg(create, name_header) { return this.dom.getStreamMsg(create, name_header); }
|
|
3541
4812
|
// Start a new streaming session (clear state and display loader, if any).
|
|
@@ -3787,20 +5058,17 @@
|
|
|
3787
5058
|
this.cfg = cfg; this.logger = logger || new Logger(cfg);
|
|
3788
5059
|
this.bridge = null; this.connected = false;
|
|
3789
5060
|
}
|
|
3790
|
-
// Low-level log via bridge if available.
|
|
3791
5061
|
log(text) { try { if (this.bridge && this.bridge.log) this.bridge.log(text); } catch (_) {} }
|
|
3792
|
-
// Wire JS callbacks to QWebChannel signals.
|
|
3793
5062
|
connect(onChunk, onNode, onNodeReplace, onNodeInput) {
|
|
3794
5063
|
if (!this.bridge) return false; if (this.connected) return true;
|
|
3795
5064
|
try {
|
|
3796
|
-
if (this.bridge.chunk) this.bridge.chunk.connect(onChunk);
|
|
5065
|
+
if (this.bridge.chunk) this.bridge.chunk.connect((name, chunk, type) => onChunk(name, chunk, type));
|
|
3797
5066
|
if (this.bridge.node) this.bridge.node.connect(onNode);
|
|
3798
5067
|
if (this.bridge.nodeReplace) this.bridge.nodeReplace.connect(onNodeReplace);
|
|
3799
5068
|
if (this.bridge.nodeInput) this.bridge.nodeInput.connect(onNodeInput);
|
|
3800
5069
|
this.connected = true; return true;
|
|
3801
5070
|
} catch (e) { this.log(e); return false; }
|
|
3802
5071
|
}
|
|
3803
|
-
// Detach callbacks.
|
|
3804
5072
|
disconnect() {
|
|
3805
5073
|
if (!this.bridge) return false; if (!this.connected) return true;
|
|
3806
5074
|
try {
|
|
@@ -3811,7 +5079,6 @@
|
|
|
3811
5079
|
} catch (_) {}
|
|
3812
5080
|
this.connected = false; return true;
|
|
3813
5081
|
}
|
|
3814
|
-
// Initialize QWebChannel and notify Python side that JS is ready.
|
|
3815
5082
|
initQWebChannel(pid, onReady) {
|
|
3816
5083
|
try {
|
|
3817
5084
|
new QWebChannel(qt.webChannelTransport, (channel) => {
|
|
@@ -3820,9 +5087,8 @@
|
|
|
3820
5087
|
onReady && onReady(this.bridge);
|
|
3821
5088
|
if (this.bridge && this.bridge.js_ready) this.bridge.js_ready(pid);
|
|
3822
5089
|
});
|
|
3823
|
-
} catch (e) { /* swallow
|
|
5090
|
+
} catch (e) { /* swallow */ }
|
|
3824
5091
|
}
|
|
3825
|
-
// Convenience wrappers for host actions.
|
|
3826
5092
|
copyCode(text) { if (this.bridge && this.bridge.copy_text) this.bridge.copy_text(text); }
|
|
3827
5093
|
previewCode(text) { if (this.bridge && this.bridge.preview_text) this.bridge.preview_text(text); }
|
|
3828
5094
|
runCode(text) { if (this.bridge && this.bridge.run_text) this.bridge.run_text(text); }
|
|
@@ -4051,10 +5317,16 @@
|
|
|
4051
5317
|
this.streamQ = new StreamQueue(this.cfg, this.stream, this.scrollMgr, this.raf);
|
|
4052
5318
|
this.events = new EventManager(this.cfg, this.dom, this.scrollMgr, this.highlighter, this.codeScroll, this.toolOutput, this.bridge);
|
|
4053
5319
|
|
|
5320
|
+
try {
|
|
5321
|
+
this.stream.setCustomFenceSpecs(this.customMarkup.getSourceFenceSpecs());
|
|
5322
|
+
} catch (_) {}
|
|
5323
|
+
|
|
5324
|
+
this.templates = new NodeTemplateEngine(this.cfg, this.logger);
|
|
5325
|
+
this.data = new DataReceiver(this.cfg, this.templates, this.nodes, this.scrollMgr);
|
|
5326
|
+
|
|
4054
5327
|
this.tips = null;
|
|
4055
5328
|
this._lastHeavyResetMs = 0;
|
|
4056
5329
|
|
|
4057
|
-
// Bridge hooks between renderer and other subsystems.
|
|
4058
5330
|
this.renderer.hooks.observeNewCode = (root, opts) => this.highlighter.observeNewCode(root, opts, this.stream.activeCode);
|
|
4059
5331
|
this.renderer.hooks.observeMsgBoxes = (root) => this.highlighter.observeMsgBoxes(root, (box) => {
|
|
4060
5332
|
this.highlighter.observeNewCode(box, {
|
|
@@ -4071,6 +5343,7 @@
|
|
|
4071
5343
|
};
|
|
4072
5344
|
this.renderer.hooks.codeScrollInit = (root) => this.codeScroll.initScrollableBlocks(root);
|
|
4073
5345
|
}
|
|
5346
|
+
|
|
4074
5347
|
// Reset stream state and optionally perform a heavy reset of schedulers and observers.
|
|
4075
5348
|
resetStreamState(origin, opts) {
|
|
4076
5349
|
try { this.streamQ.clear(); } catch (_) {}
|
|
@@ -4105,6 +5378,17 @@
|
|
|
4105
5378
|
|
|
4106
5379
|
try { this.tips && this.tips.hide(); } catch (_) {}
|
|
4107
5380
|
}
|
|
5381
|
+
// API: handle incoming chunk (from bridge).
|
|
5382
|
+
api_onChunk = (name, chunk, type) => {
|
|
5383
|
+
const t = String(type || 'text_delta');
|
|
5384
|
+
if (t === 'text_delta') {
|
|
5385
|
+
this.api_appendStream(name, chunk);
|
|
5386
|
+
return;
|
|
5387
|
+
}
|
|
5388
|
+
// Future-proof: add other chunk types here (attachments, status, etc.)
|
|
5389
|
+
// No-op for unknown types to keep current behavior.
|
|
5390
|
+
this.logger.debug('STREAM', 'IGNORED_NON_TEXT_CHUNK', { type: t, len: (chunk ? String(chunk).length : 0) });
|
|
5391
|
+
};
|
|
4108
5392
|
// API: begin stream.
|
|
4109
5393
|
api_beginStream = (chunk = false) => { this.tips && this.tips.hide(); this.resetStreamState('beginStream', { clearMsg: true, finalizeActive: false, forceHeavy: true }); this.stream.beginStream(chunk); };
|
|
4110
5394
|
// API: end stream.
|
|
@@ -4128,12 +5412,27 @@
|
|
|
4128
5412
|
// API: clear streaming output area entirely.
|
|
4129
5413
|
api_clearStream = () => { this.tips && this.tips.hide(); this.resetStreamState('clearStream', { clearMsg: true, forceHeavy: true }); const el = this.dom.getStreamContainer(); if (!el) return; el.replaceChildren(); };
|
|
4130
5414
|
|
|
4131
|
-
// API: append
|
|
4132
|
-
api_appendNode = (
|
|
4133
|
-
|
|
4134
|
-
|
|
5415
|
+
// API: append/replace messages (non-streaming).
|
|
5416
|
+
api_appendNode = (payload) => { this.resetStreamState('appendNode'); this.data.append(payload); };
|
|
5417
|
+
api_replaceNodes = (payload) => { this.resetStreamState('replaceNodes', { clearMsg: true, forceHeavy: true }); this.dom.clearNodes(); this.data.replace(payload); };
|
|
5418
|
+
|
|
4135
5419
|
// API: append to input area.
|
|
4136
|
-
|
|
5420
|
+
api_appendToInput = (payload) => {
|
|
5421
|
+
this.nodes.appendToInput(payload);
|
|
5422
|
+
|
|
5423
|
+
// Ensure initial auto-follow is ON for the next stream that will start right after user input.
|
|
5424
|
+
// Rationale: previously, if the user had scrolled up, autoFollow could remain false and the
|
|
5425
|
+
// live stream would not follow even though we just sent a new input.
|
|
5426
|
+
this.scrollMgr.autoFollow = true; // explicitly re-enable page auto-follow
|
|
5427
|
+
this.scrollMgr.userInteracted = false; // Reset interaction so live scroll is allowed
|
|
5428
|
+
|
|
5429
|
+
// Keep lastScrollTop in sync to avoid misclassification in the next onscroll handler.
|
|
5430
|
+
try { this.scrollMgr.lastScrollTop = Utils.SE.scrollTop | 0; } catch (_) {}
|
|
5431
|
+
|
|
5432
|
+
// Non-live scroll to bottom right away, independent of autoFollow state.
|
|
5433
|
+
this.scrollMgr.scheduleScroll();
|
|
5434
|
+
// NOTE: No resetStreamState() here to avoid flicker/reflow issues while previewing user input.
|
|
5435
|
+
};
|
|
4137
5436
|
|
|
4138
5437
|
// API: clear messages list.
|
|
4139
5438
|
api_clearNodes = () => { this.dom.clearNodes(); this.resetStreamState('clearNodes', { clearMsg: true, forceHeavy: true }); };
|
|
@@ -4231,6 +5530,7 @@
|
|
|
4231
5530
|
|
|
4232
5531
|
// API: restore collapsed state of codes in a given root.
|
|
4233
5532
|
api_restoreCollapsedCode = (root) => this.renderer.restoreCollapsedCode(root);
|
|
5533
|
+
|
|
4234
5534
|
// API: user-triggered page scroll.
|
|
4235
5535
|
api_scrollToTopUser = () => this.scrollMgr.scrollToTopUser();
|
|
4236
5536
|
api_scrollToBottomUser = () => this.scrollMgr.scrollToBottomUser();
|
|
@@ -4241,7 +5541,11 @@
|
|
|
4241
5541
|
|
|
4242
5542
|
// API: custom markup rules control.
|
|
4243
5543
|
api_getCustomMarkupRules = () => this.customMarkup.getRules();
|
|
4244
|
-
api_setCustomMarkupRules = (rules) => {
|
|
5544
|
+
api_setCustomMarkupRules = (rules) => {
|
|
5545
|
+
this.customMarkup.setRules(rules);
|
|
5546
|
+
// Keep StreamEngine in sync with rules producing fenced code
|
|
5547
|
+
try { this.stream.setCustomFenceSpecs(this.customMarkup.getSourceFenceSpecs()); } catch (_) {}
|
|
5548
|
+
};
|
|
4245
5549
|
|
|
4246
5550
|
// Initialize runtime (called on DOMContentLoaded).
|
|
4247
5551
|
init() {
|
|
@@ -4249,15 +5553,13 @@
|
|
|
4249
5553
|
this.dom.init();
|
|
4250
5554
|
this.ui.ensureStickyHeaderStyle();
|
|
4251
5555
|
|
|
4252
|
-
// Tips manager with rAF-based centering and rotation
|
|
4253
5556
|
this.tips = new TipsManager(this.dom);
|
|
4254
|
-
|
|
4255
5557
|
this.events.install();
|
|
4256
5558
|
|
|
4257
5559
|
this.bridge.initQWebChannel(this.cfg.PID, (bridge) => {
|
|
4258
|
-
const onChunk = (name, chunk) => this.
|
|
4259
|
-
const onNode = (
|
|
4260
|
-
const onNodeReplace = (
|
|
5560
|
+
const onChunk = (name, chunk, type) => this.api_onChunk(name, chunk, type);
|
|
5561
|
+
const onNode = (payload) => this.api_appendNode(payload);
|
|
5562
|
+
const onNodeReplace = (payload) => this.api_replaceNodes(payload);
|
|
4261
5563
|
const onNodeInput = (html) => this.api_appendToInput(html);
|
|
4262
5564
|
this.bridge.connect(onChunk, onNode, onNodeReplace, onNodeInput);
|
|
4263
5565
|
try { this.logger.bindBridge(this.bridge.bridge || this.bridge); } catch (_) {}
|
|
@@ -4281,7 +5583,6 @@
|
|
|
4281
5583
|
}, this.stream.activeCode);
|
|
4282
5584
|
this.highlighter.scheduleScanVisibleCodes(this.stream.activeCode);
|
|
4283
5585
|
|
|
4284
|
-
// Start tips rotation; internal delay matches legacy timing (TIPS_INIT_DELAY_MS)
|
|
4285
5586
|
this.tips.cycle();
|
|
4286
5587
|
this.scrollMgr.updateScrollFab(true);
|
|
4287
5588
|
}
|
|
@@ -4299,17 +5600,17 @@
|
|
|
4299
5600
|
}
|
|
4300
5601
|
|
|
4301
5602
|
// Ensure RafManager.cancel uses the correct group key cleanup.
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
5603
|
+
if (typeof RafManager !== 'undefined' && RafManager.prototype && typeof RafManager.prototype.cancel === 'function') {
|
|
5604
|
+
RafManager.prototype.cancel = function(key) {
|
|
5605
|
+
const t = this.tasks.get(key);
|
|
5606
|
+
if (!t) return;
|
|
5607
|
+
this.tasks.delete(key);
|
|
5608
|
+
if (t.group) {
|
|
5609
|
+
const set = this.groups.get(t.group);
|
|
5610
|
+
if (set) { set.delete(key); if (set.size === 0) this.groups.delete(t.group); }
|
|
5611
|
+
}
|
|
5612
|
+
};
|
|
4310
5613
|
}
|
|
4311
|
-
};
|
|
4312
|
-
}
|
|
4313
5614
|
|
|
4314
5615
|
const runtime = new Runtime();
|
|
4315
5616
|
|
|
@@ -4321,11 +5622,12 @@
|
|
|
4321
5622
|
window.endStream = () => runtime.api_endStream();
|
|
4322
5623
|
window.applyStream = (name, chunk) => runtime.api_applyStream(name, chunk);
|
|
4323
5624
|
window.appendStream = (name, chunk) => runtime.api_appendStream(name, chunk);
|
|
5625
|
+
window.appendStreamTyped = (type, name, chunk) => runtime.api_onChunk(name, chunk, type);
|
|
4324
5626
|
window.nextStream = () => runtime.api_nextStream();
|
|
4325
5627
|
window.clearStream = () => runtime.api_clearStream();
|
|
4326
5628
|
|
|
4327
|
-
window.appendNode = (
|
|
4328
|
-
window.replaceNodes = (
|
|
5629
|
+
window.appendNode = (payload) => runtime.api_appendNode(payload);
|
|
5630
|
+
window.replaceNodes = (payload) => runtime.api_replaceNodes(payload);
|
|
4329
5631
|
window.appendToInput = (html) => runtime.api_appendToInput(html);
|
|
4330
5632
|
|
|
4331
5633
|
window.clearNodes = () => runtime.api_clearNodes();
|