cli-jaw 1.5.0 → 1.6.2
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.
- package/README.ko.md +26 -19
- package/README.md +50 -22
- package/README.zh-CN.md +26 -19
- package/dist/bin/cli-jaw.js +5 -1
- package/dist/bin/cli-jaw.js.map +1 -1
- package/dist/bin/commands/dispatch.js +57 -0
- package/dist/bin/commands/dispatch.js.map +1 -0
- package/dist/lib/mcp-sync.js +1 -1
- package/dist/lib/mcp-sync.js.map +1 -1
- package/dist/lib/upload.js +14 -1
- package/dist/lib/upload.js.map +1 -1
- package/dist/server.js +57 -42
- package/dist/server.js.map +1 -1
- package/dist/src/agent/events.js +72 -13
- package/dist/src/agent/events.js.map +1 -1
- package/dist/src/agent/spawn.js +71 -29
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/acp-client.js +4 -2
- package/dist/src/cli/acp-client.js.map +1 -1
- package/dist/src/cli/commands.js +6 -6
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/handlers.js +11 -9
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/cli/registry.js +7 -1
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/core/config.js +15 -8
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/db.js +18 -3
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/core/employees.js +34 -0
- package/dist/src/core/employees.js.map +1 -0
- package/dist/src/discord/bot.js +72 -9
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/commands.js +12 -8
- package/dist/src/discord/commands.js.map +1 -1
- package/dist/src/orchestrator/distribute.js +57 -35
- package/dist/src/orchestrator/distribute.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +3 -1
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +120 -27
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/research.js +5 -5
- package/dist/src/orchestrator/research.js.map +1 -1
- package/dist/src/orchestrator/scope.js +55 -0
- package/dist/src/orchestrator/scope.js.map +1 -0
- package/dist/src/orchestrator/state-machine.js +23 -21
- package/dist/src/orchestrator/state-machine.js.map +1 -1
- package/dist/src/orchestrator/worker-registry.js +9 -2
- package/dist/src/orchestrator/worker-registry.js.map +1 -1
- package/dist/src/prompt/builder.js +76 -37
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +40 -0
- package/dist/src/prompt/templates/employee.md +12 -4
- package/dist/src/prompt/templates/orchestration.md +17 -1
- package/dist/src/telegram/bot.js +7 -1
- package/dist/src/telegram/bot.js.map +1 -1
- package/package.json +4 -2
- package/public/assets/fonts/GeistVF.woff2 +0 -0
- package/public/assets/fonts/JetBrainsMono-Variable.woff2 +0 -0
- package/public/assets/providers/claude-color.svg +1 -0
- package/public/assets/providers/claude.svg +1 -0
- package/public/assets/providers/copilot-color.svg +1 -0
- package/public/assets/providers/copilot.svg +1 -0
- package/public/assets/providers/discord.svg +1 -0
- package/public/assets/providers/gemini-color.svg +1 -0
- package/public/assets/providers/gemini.svg +1 -0
- package/public/assets/providers/openai.svg +1 -0
- package/public/assets/providers/opencode.svg +1 -0
- package/public/assets/providers/telegram.svg +1 -0
- package/public/css/chat.css +79 -51
- package/public/css/diagram.css +348 -0
- package/public/css/layout.css +38 -11
- package/public/css/markdown.css +64 -18
- package/public/css/modals.css +3 -3
- package/public/css/orc-state.css +9 -9
- package/public/css/sidebar.css +37 -3
- package/public/css/tool-ui.css +46 -11
- package/public/css/variables.css +63 -63
- package/public/dist/assets/api-Bbmmo0o1.js +1 -0
- package/public/dist/assets/architecture-YZFGNWBL-BxiHqtOH.js +1 -0
- package/public/dist/assets/architectureDiagram-Q4EWVU46-CJTGDw5p.js +1 -0
- package/public/dist/assets/blockDiagram-DXYQGD6D-Btl5Y-Er.js +1 -0
- package/public/dist/assets/c4Diagram-AHTNJAMY-sDfjW4ZF.js +1 -0
- package/public/dist/assets/classDiagram-6PBFFD2Q-ByCgggL2.js +1 -0
- package/public/dist/assets/classDiagram-v2-HSJHXN6E-t1amaXkU.js +1 -0
- package/public/dist/assets/constants-C8_0OtK2.js +1 -0
- package/public/dist/assets/cose-bilkent-S5V4N54A-eOSFGdaE.js +1 -0
- package/public/dist/assets/dagre-KV5264BT-OCB5j8Q6.js +1 -0
- package/public/dist/assets/diagram-5BDNPKRD-C-4fgK5P.js +1 -0
- package/public/dist/assets/diagram-G4DWMVQ6-C6vmHKDV.js +1 -0
- package/public/dist/assets/diagram-MMDJMWI5-BMCo_wmt.js +1 -0
- package/public/dist/assets/diagram-TYMM5635-DaCLdttJ.js +1 -0
- package/public/dist/assets/employees-D5n7mX5v.js +39 -0
- package/public/dist/assets/erDiagram-SMLLAGMA-BiTRdm5s.js +1 -0
- package/public/dist/assets/flowDiagram-DWJPFMVM-sv6jVi0d.js +1 -0
- package/public/dist/assets/ganttDiagram-T4ZO3ILL-jCP3OW23.js +1 -0
- package/public/dist/assets/gitGraph-7Q5UKJZL-BIeKLxpm.js +1 -0
- package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CokylnbW.js +1 -0
- package/public/dist/assets/index-CLd0BsAu.js +49 -0
- package/public/dist/assets/index-D6ci1wCN.css +1 -0
- package/public/dist/assets/info-OMHHGYJF-CIEl5dWF.js +1 -0
- package/public/dist/assets/infoDiagram-42DDH7IO-BBnK1Bh2.js +1 -0
- package/public/dist/assets/ishikawaDiagram-UXIWVN3A-qXIz0VhS.js +1 -0
- package/public/dist/assets/journeyDiagram-VCZTEJTY-BdrOLof3.js +1 -0
- package/public/dist/assets/kanban-definition-6JOO6SKY-CcX7CE04.js +1 -0
- package/public/dist/assets/mermaid.core-BoxIvw7E.js +1 -0
- package/public/dist/assets/mindmap-definition-QFDTVHPH-CdXARskX.js +1 -0
- package/public/dist/assets/packet-4T2RLAQJ-Bz4ZwYZ3.js +1 -0
- package/public/dist/assets/pie-ZZUOXDRM-AtGL3wZ2.js +1 -0
- package/public/dist/assets/pieDiagram-DEJITSTG-B5SOq9Yr.js +1 -0
- package/public/dist/assets/quadrantDiagram-34T5L4WZ-D0uNWgpI.js +1 -0
- package/public/dist/assets/radar-PYXPWWZC-AbJSfeqB.js +1 -0
- package/public/dist/assets/render-C8N0rp4L.js +25 -0
- package/public/dist/assets/requirementDiagram-MS252O5E-62td6IQ-.js +1 -0
- package/public/dist/assets/sankeyDiagram-XADWPNL6-Crg_02iC.js +1 -0
- package/public/dist/assets/sequenceDiagram-FGHM5R23-fbCocZHj.js +1 -0
- package/public/dist/assets/settings-BJR-IGM5.js +41 -0
- package/public/dist/assets/settings-Dm6OnPmY.js +1 -0
- package/public/dist/assets/skills-D6jv9AIs.js +1 -0
- package/public/dist/assets/skills-uywskdFh.js +12 -0
- package/public/dist/assets/slash-commands-CCDonT40.js +1 -0
- package/public/dist/assets/{slash-commands-DMsE88bu.js → slash-commands-DWvL-VDU.js} +1 -1
- package/public/dist/assets/stateDiagram-FHFEXIEX-Gy3DhXxQ.js +1 -0
- package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-DJc-FW9O.js +1 -0
- package/public/dist/assets/timeline-definition-GMOUNBTQ-B3cA9DgY.js +1 -0
- package/public/dist/assets/treeView-SZITEDCU-Cn58DIbB.js +1 -0
- package/public/dist/assets/treemap-W4RFUUIX-DPcYjDzH.js +1 -0
- package/public/dist/assets/ui-C1daR00l.js +1 -0
- package/public/dist/assets/ui-D-oFkXed.js +131 -0
- package/public/dist/assets/vendor-icons-1Ec7ZWcT.js +1 -0
- package/public/dist/assets/{vendor-mermaid-COidH9HB.js → vendor-mermaid-lvHqQdfg.js} +4 -4
- package/public/dist/assets/vennDiagram-DHZGUBPP-DkBfxilo.js +1 -0
- package/public/dist/assets/wardley-RL74JXVD-6Dg0PJ4H.js +1 -0
- package/public/dist/assets/wardleyDiagram-NUSXRM2D-Dsntl_vy.js +1 -0
- package/public/dist/assets/ws-Dnn8HG8B.js +2 -0
- package/public/dist/assets/xychartDiagram-5P7HB3ND-B_McB5GE.js +1 -0
- package/public/dist/index.html +63 -55
- package/public/index.html +62 -53
- package/public/js/constants.ts +8 -2
- package/public/js/diagram/iframe-renderer.ts +559 -0
- package/public/js/diagram/types.ts +129 -0
- package/public/js/diagram/widget-validator.ts +82 -0
- package/public/js/features/chat.ts +24 -12
- package/public/js/features/employees.ts +3 -2
- package/public/js/features/heartbeat.ts +4 -3
- package/public/js/features/memory.ts +4 -3
- package/public/js/features/process-block.ts +12 -11
- package/public/js/features/settings-cli-status.ts +10 -7
- package/public/js/features/settings-core.ts +13 -3
- package/public/js/features/settings-discord.ts +9 -0
- package/public/js/features/settings-mcp.ts +8 -7
- package/public/js/features/settings-stt.ts +4 -4
- package/public/js/features/settings-templates.ts +10 -8
- package/public/js/features/settings-types.ts +1 -1
- package/public/js/features/settings.ts +1 -1
- package/public/js/features/sidebar.ts +37 -7
- package/public/js/features/skills.ts +4 -3
- package/public/js/features/theme.ts +4 -0
- package/public/js/features/tool-ui.ts +6 -5
- package/public/js/features/voice-recorder.ts +2 -1
- package/public/js/icons.ts +257 -0
- package/public/js/main.ts +9 -3
- package/public/js/provider-icons.ts +88 -0
- package/public/js/render.ts +493 -30
- package/public/js/streaming-render.ts +3 -3
- package/public/js/ui.ts +38 -18
- package/public/js/ws.ts +17 -10
- package/public/locales/en.json +89 -88
- package/public/locales/ko.json +89 -88
- package/scripts/release-1.6.0.sh +69 -0
- package/scripts/release-preview.sh +1 -1
- package/dist/lib/token-keepalive.js +0 -34
- package/dist/lib/token-keepalive.js.map +0 -1
- package/public/dist/assets/api-BlPw3bUI.js +0 -1
- package/public/dist/assets/architecture-YZFGNWBL-BkS7SZQi.js +0 -1
- package/public/dist/assets/architectureDiagram-Q4EWVU46-Dl3iBKeR.js +0 -1
- package/public/dist/assets/blockDiagram-DXYQGD6D-DPr4D17p.js +0 -1
- package/public/dist/assets/c4Diagram-AHTNJAMY-CfoxJtDk.js +0 -1
- package/public/dist/assets/classDiagram-6PBFFD2Q-BOAdHnnB.js +0 -1
- package/public/dist/assets/classDiagram-v2-HSJHXN6E-B2QCJXWC.js +0 -1
- package/public/dist/assets/constants-DshMUJbo.js +0 -1
- package/public/dist/assets/cose-bilkent-S5V4N54A-CA14jk7w.js +0 -1
- package/public/dist/assets/dagre-KV5264BT-BVwNaSwC.js +0 -1
- package/public/dist/assets/diagram-5BDNPKRD-CHqIAvdc.js +0 -1
- package/public/dist/assets/diagram-G4DWMVQ6-DxvsCvTP.js +0 -1
- package/public/dist/assets/diagram-MMDJMWI5-DqOPO7dl.js +0 -1
- package/public/dist/assets/diagram-TYMM5635-C9xMWPQn.js +0 -1
- package/public/dist/assets/employees-CFRlsbHm.js +0 -39
- package/public/dist/assets/erDiagram-SMLLAGMA-BTmpfRkm.js +0 -1
- package/public/dist/assets/flowDiagram-DWJPFMVM-DZBSQAKA.js +0 -1
- package/public/dist/assets/ganttDiagram-T4ZO3ILL-TAEMCRbT.js +0 -1
- package/public/dist/assets/gitGraph-7Q5UKJZL-bbxndRNA.js +0 -1
- package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CBxtx0MB.js +0 -1
- package/public/dist/assets/index-1Gg-6jeC.css +0 -1
- package/public/dist/assets/index-CQHqXjrK.js +0 -49
- package/public/dist/assets/info-OMHHGYJF-DRXq_Ywr.js +0 -1
- package/public/dist/assets/infoDiagram-42DDH7IO-C7FFwX4m.js +0 -1
- package/public/dist/assets/ishikawaDiagram-UXIWVN3A-DEZJE79j.js +0 -1
- package/public/dist/assets/journeyDiagram-VCZTEJTY-ZHLiY6GV.js +0 -1
- package/public/dist/assets/kanban-definition-6JOO6SKY-NP7pY7ML.js +0 -1
- package/public/dist/assets/mermaid.core-CHuluSlD.js +0 -1
- package/public/dist/assets/mindmap-definition-QFDTVHPH-Bd1OgfN_.js +0 -1
- package/public/dist/assets/packet-4T2RLAQJ-CGu8ST97.js +0 -1
- package/public/dist/assets/pie-ZZUOXDRM-CDG65mhS.js +0 -1
- package/public/dist/assets/pieDiagram-DEJITSTG-BbMBHLDM.js +0 -1
- package/public/dist/assets/quadrantDiagram-34T5L4WZ-CDCDgI1e.js +0 -1
- package/public/dist/assets/radar-PYXPWWZC-1gS-6ylu.js +0 -1
- package/public/dist/assets/render-YHvL5VM_.js +0 -6
- package/public/dist/assets/requirementDiagram-MS252O5E-DkmvOtYz.js +0 -1
- package/public/dist/assets/sankeyDiagram-XADWPNL6-GNcXIYsL.js +0 -1
- package/public/dist/assets/sequenceDiagram-FGHM5R23-BFrw2n6x.js +0 -1
- package/public/dist/assets/settings-BQF_u5px.js +0 -37
- package/public/dist/assets/settings-C5Q9IPjJ.js +0 -1
- package/public/dist/assets/skills-9Pd2rOw-.js +0 -1
- package/public/dist/assets/skills-BI7sQAdk.js +0 -12
- package/public/dist/assets/slash-commands-FMpJzlDB.js +0 -1
- package/public/dist/assets/stateDiagram-FHFEXIEX-BLgKnllT.js +0 -1
- package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-CbzuWsSa.js +0 -1
- package/public/dist/assets/timeline-definition-GMOUNBTQ-CcZTJ3gL.js +0 -1
- package/public/dist/assets/treeView-SZITEDCU-BDqBsaH1.js +0 -1
- package/public/dist/assets/treemap-W4RFUUIX-BaWHA3Di.js +0 -1
- package/public/dist/assets/ui-BukgLHuh.js +0 -29
- package/public/dist/assets/ui-Bvz1JfTE.js +0 -1
- package/public/dist/assets/vennDiagram-DHZGUBPP-BWV_j1bh.js +0 -1
- package/public/dist/assets/wardley-RL74JXVD-C7gKA3d7.js +0 -1
- package/public/dist/assets/wardleyDiagram-NUSXRM2D-BUoq_wug.js +0 -1
- package/public/dist/assets/ws-S_AZgx7L.js +0 -2
- package/public/dist/assets/xychartDiagram-5P7HB3ND-PvT5Ec82.js +0 -1
- /package/public/dist/assets/{api-B8XKQ4OT.js → api-Ci-lgwRp.js} +0 -0
- /package/public/dist/assets/{locale-BjoAcbis.js → locale-DIXc-_34.js} +0 -0
package/public/js/render.ts
CHANGED
|
@@ -24,7 +24,12 @@ import plaintext from 'highlight.js/lib/languages/plaintext';
|
|
|
24
24
|
import katex from 'katex';
|
|
25
25
|
import DOMPurify from 'dompurify';
|
|
26
26
|
import { t } from './features/i18n.js';
|
|
27
|
+
import { ICONS } from './icons.js';
|
|
27
28
|
import { fixCjkPunctuationBoundary } from './cjk-fix.js';
|
|
29
|
+
import {
|
|
30
|
+
SvgBlock, shieldCodeFenceSvg, unshieldCodeFenceSvg,
|
|
31
|
+
extractTopLevelSvg,
|
|
32
|
+
} from './diagram/types.js';
|
|
28
33
|
|
|
29
34
|
// Register hljs languages (core-only import: ~25KB vs ~1MB full)
|
|
30
35
|
hljs.registerLanguage('javascript', javascript);
|
|
@@ -57,35 +62,167 @@ hljs.registerLanguage('text', plaintext);
|
|
|
57
62
|
|
|
58
63
|
// Lazy mermaid: loaded on first diagram encounter
|
|
59
64
|
let mermaidModule: typeof import('mermaid') | null = null;
|
|
65
|
+
let mermaidTheme: string | null = null;
|
|
66
|
+
|
|
67
|
+
function getMermaidThemeVars() {
|
|
68
|
+
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
69
|
+
return isLight ? {
|
|
70
|
+
primaryColor: '#e2e8f0',
|
|
71
|
+
primaryTextColor: '#1a202c',
|
|
72
|
+
primaryBorderColor: '#a0aec0',
|
|
73
|
+
lineColor: '#718096',
|
|
74
|
+
secondaryColor: '#ebf8ff',
|
|
75
|
+
tertiaryColor: '#f7fafc',
|
|
76
|
+
background: 'transparent',
|
|
77
|
+
mainBkg: '#e2e8f0',
|
|
78
|
+
nodeBorder: '#a0aec0',
|
|
79
|
+
clusterBkg: '#f7fafc',
|
|
80
|
+
clusterBorder: '#cbd5e0',
|
|
81
|
+
titleColor: '#1a202c',
|
|
82
|
+
edgeLabelBackground: '#f7fafc',
|
|
83
|
+
} : {
|
|
84
|
+
primaryColor: '#2d3748',
|
|
85
|
+
primaryTextColor: '#e2e8f0',
|
|
86
|
+
primaryBorderColor: '#4a5568',
|
|
87
|
+
lineColor: '#718096',
|
|
88
|
+
secondaryColor: '#1a365d',
|
|
89
|
+
tertiaryColor: '#1a202c',
|
|
90
|
+
background: 'transparent',
|
|
91
|
+
mainBkg: '#2d3748',
|
|
92
|
+
nodeBorder: '#4a5568',
|
|
93
|
+
clusterBkg: '#1a202c',
|
|
94
|
+
clusterBorder: '#2d3748',
|
|
95
|
+
titleColor: '#e2e8f0',
|
|
96
|
+
edgeLabelBackground: '#1a202c',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
60
99
|
|
|
61
100
|
async function getMermaid() {
|
|
101
|
+
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
62
102
|
if (!mermaidModule) {
|
|
63
103
|
mermaidModule = await import('mermaid');
|
|
104
|
+
}
|
|
105
|
+
if (mermaidTheme !== currentTheme) {
|
|
106
|
+
mermaidTheme = currentTheme;
|
|
64
107
|
mermaidModule.default.initialize({
|
|
65
108
|
startOnLoad: false,
|
|
66
|
-
theme:
|
|
109
|
+
theme: 'base',
|
|
110
|
+
themeVariables: getMermaidThemeVars(),
|
|
67
111
|
securityLevel: 'strict',
|
|
68
112
|
});
|
|
69
113
|
}
|
|
70
114
|
return mermaidModule.default;
|
|
71
115
|
}
|
|
72
116
|
|
|
117
|
+
// Mermaid SVG sanitizer — allows <style> (required for Mermaid theming)
|
|
118
|
+
// Separate from sanitizeHtml() which blocks <style> for user-supplied SVGs.
|
|
119
|
+
// Mermaid is configured with htmlLabels:false so labels use SVG <text>,
|
|
120
|
+
// not <foreignObject> + HTML. This avoids DOMPurify namespace issues.
|
|
121
|
+
function sanitizeMermaidSvg(svg: string): string {
|
|
122
|
+
const clean = DOMPurify.sanitize(svg, {
|
|
123
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
124
|
+
FORBID_TAGS: [
|
|
125
|
+
'script', 'iframe', 'object', 'embed', 'form', 'input',
|
|
126
|
+
'foreignObject', 'animate', 'set', 'animateTransform', 'animateMotion',
|
|
127
|
+
],
|
|
128
|
+
FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover', 'onfocus', 'onblur',
|
|
129
|
+
'background'],
|
|
130
|
+
});
|
|
131
|
+
// Sanitize CSS inside <style> blocks: strip @import, @font-face, external url()
|
|
132
|
+
const div = document.createElement('div');
|
|
133
|
+
div.innerHTML = clean;
|
|
134
|
+
for (const style of div.querySelectorAll('style')) {
|
|
135
|
+
let css = style.textContent || '';
|
|
136
|
+
css = css.replace(/@import\b[^;]*;?/gi, '/* stripped */');
|
|
137
|
+
css = css.replace(/@font-face\s*\{[^}]*\}/gi, '/* stripped */');
|
|
138
|
+
css = css.replace(/url\s*\(\s*(?!['"]?#)[^)]*\)/gi, 'none');
|
|
139
|
+
style.textContent = css;
|
|
140
|
+
}
|
|
141
|
+
return div.innerHTML;
|
|
142
|
+
}
|
|
143
|
+
|
|
73
144
|
export function escapeHtml(str: string): string {
|
|
74
145
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
75
146
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
76
147
|
}
|
|
77
148
|
|
|
78
|
-
// ── XSS sanitization ──
|
|
149
|
+
// ── XSS sanitization (hardened for inline SVG — Phase 1) ──
|
|
79
150
|
export function sanitizeHtml(html: string): string {
|
|
80
151
|
return DOMPurify.sanitize(html, {
|
|
81
152
|
USE_PROFILES: { html: true, svg: true, svgFilters: true },
|
|
82
|
-
FORBID_TAGS: [
|
|
83
|
-
|
|
153
|
+
FORBID_TAGS: [
|
|
154
|
+
'script', 'style', 'iframe', 'object', 'embed', 'form', 'input',
|
|
155
|
+
// SVG security: block animation + foreignObject (script injection vectors)
|
|
156
|
+
'foreignObject', 'animate', 'set', 'animateTransform', 'animateMotion',
|
|
157
|
+
],
|
|
158
|
+
FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover', 'onfocus', 'onblur',
|
|
159
|
+
'background'], // legacy HTML attr that triggers remote fetch
|
|
84
160
|
ADD_TAGS: ['use'],
|
|
85
|
-
ADD_ATTR: ['aria-hidden', 'xmlns', 'viewBox'
|
|
161
|
+
ADD_ATTR: ['aria-hidden', 'xmlns', 'viewBox', 'role', 'aria-label',
|
|
162
|
+
'data-jaw-svg', 'data-jaw-kind'],
|
|
86
163
|
});
|
|
87
164
|
}
|
|
88
165
|
|
|
166
|
+
// Hook: strip external href/xlink:href on <use> and <image>
|
|
167
|
+
// Only fragment references (#id) allowed — blocks external resource loading.
|
|
168
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
169
|
+
const HTML_HREF_ALLOWED = new Set(['a', 'area', 'link']);
|
|
170
|
+
|
|
171
|
+
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
|
172
|
+
const tag = node.tagName.toLowerCase();
|
|
173
|
+
|
|
174
|
+
// ── href / xlink:href: deny-by-default ──
|
|
175
|
+
// Only standard HTML (non-SVG) elements in the allow-set may carry external href.
|
|
176
|
+
// SVG <a> shares tagName 'a' with HTML <a>, so we also check namespaceURI
|
|
177
|
+
// to distinguish them — SVG elements always get fragment-only.
|
|
178
|
+
const isSvgElement = node.namespaceURI === SVG_NS;
|
|
179
|
+
if (isSvgElement || !HTML_HREF_ALLOWED.has(tag)) {
|
|
180
|
+
const href = node.getAttribute('href') || '';
|
|
181
|
+
if (href && !href.startsWith('#')) {
|
|
182
|
+
node.removeAttribute('href');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// xlink:href is SVG-only — always enforce fragment-only on ALL elements
|
|
186
|
+
const xlinkHref = node.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
|
|
187
|
+
|| node.getAttribute('xlink:href') || '';
|
|
188
|
+
if (xlinkHref && !xlinkHref.startsWith('#')) {
|
|
189
|
+
node.removeAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
|
190
|
+
node.removeAttribute('xlink:href');
|
|
191
|
+
}
|
|
192
|
+
// SVG <image>/<feimage> may carry src in HTML parser context (belt-and-suspenders)
|
|
193
|
+
if (tag === 'image' || tag === 'feimage') {
|
|
194
|
+
const src = node.getAttribute('src') || '';
|
|
195
|
+
if (src && !src.startsWith('#')) {
|
|
196
|
+
node.removeAttribute('src');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Strip external url() from style and SVG presentation attributes
|
|
200
|
+
// Prevents outbound requests / beaconing via CSS or SVG attrs like
|
|
201
|
+
// filter="url(https://evil)", fill="url(https://evil)", mask, clip-path, marker-*
|
|
202
|
+
const URL_CAPABLE_ATTRS = [
|
|
203
|
+
'fill', 'stroke', 'filter', 'mask', 'clip-path',
|
|
204
|
+
'marker-start', 'marker-mid', 'marker-end', 'cursor',
|
|
205
|
+
];
|
|
206
|
+
// For style: use cssText (browser-parsed) to defeat CSS hex-escape bypass (\75\72\6c = url)
|
|
207
|
+
if (node.hasAttribute('style')) {
|
|
208
|
+
const cssText = (node as HTMLElement).style?.cssText || '';
|
|
209
|
+
if (/url\s*\(/i.test(cssText)) {
|
|
210
|
+
const cleaned = cssText.replace(/url\s*\(\s*(?!['"]?#)[^)]*\)/gi, 'none');
|
|
211
|
+
(node as HTMLElement).style.cssText = cleaned;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const attr of URL_CAPABLE_ATTRS) {
|
|
215
|
+
if (node.hasAttribute(attr)) {
|
|
216
|
+
const val = node.getAttribute(attr) || '';
|
|
217
|
+
if (/url\s*\(/i.test(val)) {
|
|
218
|
+
// Keep fragment-only url(#id), strip external url()
|
|
219
|
+
const cleaned = val.replace(/url\s*\(\s*(?!['"]?#)[^)]*\)/gi, 'none');
|
|
220
|
+
node.setAttribute(attr, cleaned);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
89
226
|
// ── Orchestration JSON stripping ──
|
|
90
227
|
// Only strip JSON blocks that contain orchestration-specific keys, not all JSON blocks
|
|
91
228
|
// Require keys unique to orchestration payloads (avoid generic words like "phase")
|
|
@@ -167,29 +304,92 @@ export function unshieldMath(html: string, blocks: MathBlock[]): string {
|
|
|
167
304
|
// ── Mermaid deferred rendering (lazy-loaded) ──
|
|
168
305
|
let mermaidId = 0;
|
|
169
306
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
307
|
+
/** Re-render all existing Mermaid diagrams (call on theme toggle). */
|
|
308
|
+
export async function rerenderMermaidDiagrams(): Promise<void> {
|
|
309
|
+
mermaidTheme = null; // force re-init with new theme vars
|
|
310
|
+
const rendered = document.querySelectorAll('.mermaid-rendered');
|
|
311
|
+
if (!rendered.length) return;
|
|
312
|
+
const mm = await getMermaid();
|
|
313
|
+
for (const el of rendered) {
|
|
314
|
+
const code = (el as HTMLElement).dataset.mermaidCode;
|
|
315
|
+
if (!code) continue;
|
|
176
316
|
const id = `mermaid-${++mermaidId}`;
|
|
177
317
|
try {
|
|
178
|
-
const mm = await getMermaid();
|
|
179
318
|
const { svg } = await mm.render(id, code);
|
|
180
|
-
el.innerHTML =
|
|
181
|
-
el
|
|
182
|
-
} catch
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
319
|
+
el.innerHTML = svg;
|
|
320
|
+
appendMermaidActionBtns(el as HTMLElement);
|
|
321
|
+
} catch { /* keep existing render on failure */ }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Lazy Mermaid rendering — only render blocks near the viewport
|
|
326
|
+
let mermaidObserver: IntersectionObserver | null = null;
|
|
327
|
+
|
|
328
|
+
function ensureMermaidObserver(): void {
|
|
329
|
+
if (mermaidObserver) return;
|
|
330
|
+
mermaidObserver = new IntersectionObserver((entries) => {
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (!entry.isIntersecting) continue;
|
|
333
|
+
const el = entry.target as HTMLElement;
|
|
334
|
+
if (!el.classList.contains('mermaid-pending')) continue;
|
|
335
|
+
mermaidObserver!.unobserve(el);
|
|
336
|
+
renderSingleMermaid(el);
|
|
191
337
|
}
|
|
192
|
-
});
|
|
338
|
+
}, { rootMargin: '200px' }); // pre-render 200px before visible
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function appendMermaidActionBtns(el: HTMLElement): void {
|
|
342
|
+
// Remove existing buttons if present (e.g. re-render)
|
|
343
|
+
el.querySelector('.mermaid-copy-btn')?.remove();
|
|
344
|
+
el.querySelector('.mermaid-save-btn')?.remove();
|
|
345
|
+
|
|
346
|
+
const saveBtn = document.createElement('button');
|
|
347
|
+
saveBtn.className = 'mermaid-save-btn';
|
|
348
|
+
saveBtn.type = 'button';
|
|
349
|
+
saveBtn.ariaLabel = 'Save as image';
|
|
350
|
+
saveBtn.title = 'Save';
|
|
351
|
+
saveBtn.innerHTML = ICONS.download;
|
|
352
|
+
el.appendChild(saveBtn);
|
|
353
|
+
|
|
354
|
+
const copyBtn = document.createElement('button');
|
|
355
|
+
copyBtn.className = 'mermaid-copy-btn';
|
|
356
|
+
copyBtn.type = 'button';
|
|
357
|
+
copyBtn.ariaLabel = 'Copy source';
|
|
358
|
+
copyBtn.title = 'Copy';
|
|
359
|
+
copyBtn.innerHTML = ICONS.copy;
|
|
360
|
+
el.appendChild(copyBtn);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function renderSingleMermaid(el: HTMLElement): Promise<void> {
|
|
364
|
+
el.classList.remove('mermaid-pending');
|
|
365
|
+
const code = el.textContent || '';
|
|
366
|
+
el.dataset.mermaidCode = code;
|
|
367
|
+
const id = `mermaid-${++mermaidId}`;
|
|
368
|
+
try {
|
|
369
|
+
const mm = await getMermaid();
|
|
370
|
+
const { svg } = await mm.render(id, code);
|
|
371
|
+
el.innerHTML = svg;
|
|
372
|
+
el.classList.add('mermaid-rendered');
|
|
373
|
+
appendMermaidActionBtns(el);
|
|
374
|
+
} catch (err: unknown) {
|
|
375
|
+
const errMsg = (err as { message?: string; str?: string })?.message
|
|
376
|
+
|| (err as { str?: string })?.str || 'Unknown error';
|
|
377
|
+
el.innerHTML = `
|
|
378
|
+
<div class="mermaid-error">
|
|
379
|
+
<div class="mermaid-error-title">${ICONS.warning} ${escapeHtml(t('mermaid.renderFail') || 'Mermaid render failed')}</div>
|
|
380
|
+
<div class="mermaid-error-msg">${escapeHtml(errMsg.slice(0, 200))}</div>
|
|
381
|
+
<pre class="mermaid-error-code"><code>${escapeHtml(code)}</code></pre>
|
|
382
|
+
</div>`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function renderMermaidBlocks(): Promise<void> {
|
|
387
|
+
const pending = document.querySelectorAll('.mermaid-pending');
|
|
388
|
+
if (!pending.length) return;
|
|
389
|
+
ensureMermaidObserver();
|
|
390
|
+
for (const el of pending) {
|
|
391
|
+
mermaidObserver!.observe(el);
|
|
392
|
+
}
|
|
193
393
|
}
|
|
194
394
|
|
|
195
395
|
// ── marked.js configuration (ES module — always available) ──
|
|
@@ -200,11 +400,19 @@ function ensureMarked(): boolean {
|
|
|
200
400
|
|
|
201
401
|
const renderer = new Renderer();
|
|
202
402
|
|
|
203
|
-
// Code blocks: highlight.js + mermaid detection
|
|
403
|
+
// Code blocks: highlight.js + mermaid + diagram-html detection
|
|
204
404
|
renderer.code = function ({ text, lang }: { text: string; lang?: string }) {
|
|
205
405
|
if (lang === 'mermaid') {
|
|
206
406
|
return `<div class="mermaid-container mermaid-pending">${escapeHtml(text)}</div>`;
|
|
207
407
|
}
|
|
408
|
+
// diagram-html: encode as base64, Phase 2 activateWidgets() inflates to sandboxed iframe
|
|
409
|
+
if (lang?.trim().toLowerCase() === 'diagram-html') {
|
|
410
|
+
const encoded = btoa(unescape(encodeURIComponent(text)));
|
|
411
|
+
return `<div class="diagram-widget-pending" data-diagram-html="${encoded}"
|
|
412
|
+
role="status" aria-label="Interactive widget loading">
|
|
413
|
+
<div class="diagram-spinner"></div>
|
|
414
|
+
</div>`;
|
|
415
|
+
}
|
|
208
416
|
let highlighted = escapeHtml(text);
|
|
209
417
|
if (lang && hljs.getLanguage(lang)) {
|
|
210
418
|
try {
|
|
@@ -223,7 +431,7 @@ function ensureMarked(): boolean {
|
|
|
223
431
|
marked.setOptions({
|
|
224
432
|
renderer,
|
|
225
433
|
gfm: true,
|
|
226
|
-
breaks:
|
|
434
|
+
breaks: false,
|
|
227
435
|
});
|
|
228
436
|
|
|
229
437
|
markedReady = true;
|
|
@@ -292,27 +500,282 @@ function ensureCopyDelegation(): void {
|
|
|
292
500
|
});
|
|
293
501
|
}
|
|
294
502
|
|
|
503
|
+
// ── Diagram action button event delegation (copy + save) ──
|
|
504
|
+
let diagramActionsReady = false;
|
|
505
|
+
|
|
506
|
+
function ensureDiagramActionDelegation(): void {
|
|
507
|
+
if (diagramActionsReady) return;
|
|
508
|
+
diagramActionsReady = true;
|
|
509
|
+
|
|
510
|
+
document.addEventListener('click', (e: MouseEvent) => {
|
|
511
|
+
const target = e.target as HTMLElement;
|
|
512
|
+
|
|
513
|
+
// ── Copy buttons ──
|
|
514
|
+
const diagCopyBtn = target?.closest('.diagram-copy-btn') as HTMLElement | null;
|
|
515
|
+
if (diagCopyBtn) {
|
|
516
|
+
const container = diagCopyBtn.closest('.diagram-container') as HTMLElement | null;
|
|
517
|
+
if (!container) return;
|
|
518
|
+
let text = '';
|
|
519
|
+
if (container.dataset.widgetHtml) {
|
|
520
|
+
try { text = decodeURIComponent(escape(atob(container.dataset.widgetHtml))); }
|
|
521
|
+
catch { return; }
|
|
522
|
+
} else {
|
|
523
|
+
const svgEl = container.querySelector('svg');
|
|
524
|
+
if (svgEl) text = svgEl.outerHTML;
|
|
525
|
+
}
|
|
526
|
+
if (text) btnFeedback(diagCopyBtn, text, 'copy');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const mermaidCopyBtn = target?.closest('.mermaid-copy-btn') as HTMLElement | null;
|
|
531
|
+
if (mermaidCopyBtn) {
|
|
532
|
+
const container = mermaidCopyBtn.closest('.mermaid-container') as HTMLElement | null;
|
|
533
|
+
if (!container) return;
|
|
534
|
+
const code = container.dataset.mermaidCode || '';
|
|
535
|
+
if (code) btnFeedback(mermaidCopyBtn, code, 'copy');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── Save buttons ──
|
|
540
|
+
const diagSaveBtn = target?.closest('.diagram-save-btn') as HTMLElement | null;
|
|
541
|
+
if (diagSaveBtn) {
|
|
542
|
+
const container = diagSaveBtn.closest('.diagram-container') as HTMLElement | null;
|
|
543
|
+
if (!container) return;
|
|
544
|
+
// Widget: request screenshot via bridge
|
|
545
|
+
if (container.dataset.widgetHtml) {
|
|
546
|
+
const iframe = container.querySelector('iframe') as HTMLIFrameElement | null;
|
|
547
|
+
if (iframe?.contentWindow) {
|
|
548
|
+
iframe.contentWindow.postMessage({ type: 'jaw-request-screenshot' }, '*');
|
|
549
|
+
btnFeedback(diagSaveBtn, '', 'save');
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// SVG: convert to PNG
|
|
554
|
+
const svgEl = container.querySelector('svg');
|
|
555
|
+
if (svgEl) saveSvgAsPng(svgEl, diagSaveBtn);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const mermaidSaveBtn = target?.closest('.mermaid-save-btn') as HTMLElement | null;
|
|
560
|
+
if (mermaidSaveBtn) {
|
|
561
|
+
const container = mermaidSaveBtn.closest('.mermaid-container') as HTMLElement | null;
|
|
562
|
+
if (!container) return;
|
|
563
|
+
const svgEl = container.querySelector('svg');
|
|
564
|
+
if (svgEl) saveSvgAsPng(svgEl, mermaidSaveBtn);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function btnFeedback(btn: HTMLElement, text: string, action: 'copy' | 'save'): void {
|
|
571
|
+
const doFeedback = () => {
|
|
572
|
+
const orig = btn.innerHTML;
|
|
573
|
+
btn.innerHTML = ICONS.checkSimple;
|
|
574
|
+
btn.classList.add('copied');
|
|
575
|
+
setTimeout(() => { btn.innerHTML = orig; btn.classList.remove('copied'); }, 1500);
|
|
576
|
+
};
|
|
577
|
+
if (action === 'copy') {
|
|
578
|
+
navigator.clipboard.writeText(text).then(doFeedback).catch(() => {});
|
|
579
|
+
} else {
|
|
580
|
+
doFeedback();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function saveSvgAsPng(svgEl: SVGElement, btn: HTMLElement): void {
|
|
585
|
+
const clone = svgEl.cloneNode(true) as SVGElement;
|
|
586
|
+
// Ensure width/height attributes for canvas rendering
|
|
587
|
+
const bbox = svgEl.getBoundingClientRect();
|
|
588
|
+
if (!clone.getAttribute('width')) clone.setAttribute('width', String(bbox.width));
|
|
589
|
+
if (!clone.getAttribute('height')) clone.setAttribute('height', String(bbox.height));
|
|
590
|
+
|
|
591
|
+
const svgData = new XMLSerializer().serializeToString(clone);
|
|
592
|
+
// Data URL avoids tainted canvas (blob URL treated as cross-origin)
|
|
593
|
+
const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
|
|
594
|
+
const img = new Image();
|
|
595
|
+
img.onload = () => {
|
|
596
|
+
const scale = 2; // retina
|
|
597
|
+
const canvas = document.createElement('canvas');
|
|
598
|
+
canvas.width = img.naturalWidth * scale;
|
|
599
|
+
canvas.height = img.naturalHeight * scale;
|
|
600
|
+
const ctx = canvas.getContext('2d')!;
|
|
601
|
+
ctx.scale(scale, scale);
|
|
602
|
+
ctx.drawImage(img, 0, 0);
|
|
603
|
+
canvas.toBlob(blob => {
|
|
604
|
+
if (!blob) return;
|
|
605
|
+
downloadBlob(blob, `diagram-${Date.now()}.png`);
|
|
606
|
+
btnFeedback(btn, '', 'save');
|
|
607
|
+
}, 'image/png');
|
|
608
|
+
};
|
|
609
|
+
img.onerror = () => {
|
|
610
|
+
// Fallback: download as SVG
|
|
611
|
+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
|
612
|
+
downloadBlob(svgBlob, `diagram-${Date.now()}.svg`);
|
|
613
|
+
btnFeedback(btn, '', 'save');
|
|
614
|
+
};
|
|
615
|
+
img.src = dataUrl;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function downloadBlob(blob: Blob, filename: string): void {
|
|
619
|
+
const url = URL.createObjectURL(blob);
|
|
620
|
+
const a = document.createElement('a');
|
|
621
|
+
a.href = url;
|
|
622
|
+
a.download = filename;
|
|
623
|
+
a.click();
|
|
624
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ── SVG Block Rendering (Phase 1) ──
|
|
628
|
+
|
|
629
|
+
function renderSvgBlock(block: SvgBlock): string {
|
|
630
|
+
if (block.kind === 'partial') {
|
|
631
|
+
return `<div class="diagram-container diagram-loading" role="status"
|
|
632
|
+
aria-label="Diagram loading"><div class="diagram-spinner"></div></div>`;
|
|
633
|
+
}
|
|
634
|
+
if (block.kind === 'error') {
|
|
635
|
+
return `<div class="diagram-container diagram-error" role="alert">
|
|
636
|
+
Malformed SVG: unclosed element</div>`;
|
|
637
|
+
}
|
|
638
|
+
// Complete SVG — sanitize individually (extracted SVGs bypass main pipeline)
|
|
639
|
+
const sanitized = sanitizeHtml(block.svg);
|
|
640
|
+
return `<div class="diagram-container diagram-svg" tabindex="0"
|
|
641
|
+
role="figure" aria-label="SVG diagram">
|
|
642
|
+
${sanitized}
|
|
643
|
+
<button class="diagram-save-btn" type="button"
|
|
644
|
+
aria-label="Save as image" title="Save">${ICONS.download}</button>
|
|
645
|
+
<button class="diagram-copy-btn" type="button"
|
|
646
|
+
aria-label="Copy source" title="Copy">${ICONS.copy}</button>
|
|
647
|
+
<button class="diagram-zoom-btn" type="button"
|
|
648
|
+
aria-label="Expand diagram" title="Expand">⤢</button>
|
|
649
|
+
</div>`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function unshieldSvgBlocks(html: string, blocks: SvgBlock[]): string {
|
|
653
|
+
for (const block of blocks) {
|
|
654
|
+
const pattern = `<div\\b[^>]*?\\bdata-jaw-svg="${block.id}"[^>]*></div>`;
|
|
655
|
+
const re = new RegExp(pattern, 'g');
|
|
656
|
+
const rendered = renderSvgBlock(block);
|
|
657
|
+
// Use function replacement to avoid $& $' $` special patterns in SVG content
|
|
658
|
+
html = html.replace(re, () => rendered);
|
|
659
|
+
}
|
|
660
|
+
return html;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ── Diagram Zoom Overlay ──
|
|
664
|
+
|
|
665
|
+
export function bindDiagramZoom(): void {
|
|
666
|
+
document.querySelectorAll('.diagram-zoom-btn').forEach(btn => {
|
|
667
|
+
if ((btn as HTMLElement).dataset.bound) return;
|
|
668
|
+
(btn as HTMLElement).dataset.bound = '1';
|
|
669
|
+
btn.addEventListener('click', () => {
|
|
670
|
+
const container = btn.closest('.diagram-container');
|
|
671
|
+
if (!container) return;
|
|
672
|
+
// Clone without zoom button to prevent nesting
|
|
673
|
+
const clone = container.cloneNode(true) as HTMLElement;
|
|
674
|
+
clone.querySelectorAll('.diagram-zoom-btn, .diagram-copy-btn, .diagram-save-btn').forEach(b => b.remove());
|
|
675
|
+
openDiagramOverlay(clone.innerHTML);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function openDiagramOverlay(innerHtml: string): void {
|
|
681
|
+
const previousFocus = document.activeElement as HTMLElement | null;
|
|
682
|
+
const overlay = document.createElement('div');
|
|
683
|
+
overlay.className = 'diagram-overlay';
|
|
684
|
+
overlay.setAttribute('role', 'dialog');
|
|
685
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
686
|
+
overlay.setAttribute('aria-label', 'Expanded diagram');
|
|
687
|
+
// Re-sanitize to prevent mXSS from double HTML parsing
|
|
688
|
+
const safeHtml = sanitizeHtml(innerHtml);
|
|
689
|
+
overlay.innerHTML = `
|
|
690
|
+
<div class="diagram-overlay-content">${safeHtml}</div>
|
|
691
|
+
<button class="diagram-overlay-close" type="button" aria-label="Close">✕</button>
|
|
692
|
+
`;
|
|
693
|
+
|
|
694
|
+
// Ensure SVGs scale inside overlay: add viewBox if missing, remove fixed dimensions
|
|
695
|
+
overlay.querySelectorAll<SVGSVGElement>('.diagram-overlay-content svg').forEach(svg => {
|
|
696
|
+
if (!svg.getAttribute('viewBox')) {
|
|
697
|
+
const w = svg.getAttribute('width') || svg.getBBox?.()?.width;
|
|
698
|
+
const h = svg.getAttribute('height') || svg.getBBox?.()?.height;
|
|
699
|
+
if (w && h) svg.setAttribute('viewBox', `0 0 ${parseFloat(String(w))} ${parseFloat(String(h))}`);
|
|
700
|
+
}
|
|
701
|
+
svg.removeAttribute('width');
|
|
702
|
+
svg.removeAttribute('height');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const closeBtn = overlay.querySelector('.diagram-overlay-close') as HTMLElement;
|
|
706
|
+
|
|
707
|
+
const close = () => {
|
|
708
|
+
overlay.remove();
|
|
709
|
+
document.removeEventListener('keydown', onKey);
|
|
710
|
+
// Restore focus to the element that opened the overlay
|
|
711
|
+
if (previousFocus && previousFocus.isConnected) previousFocus.focus();
|
|
712
|
+
};
|
|
713
|
+
const onKey = (e: KeyboardEvent) => {
|
|
714
|
+
if (e.key === 'Escape') { close(); return; }
|
|
715
|
+
// Focus trap: Tab cycles within overlay
|
|
716
|
+
if (e.key === 'Tab') {
|
|
717
|
+
const focusable = overlay.querySelectorAll<HTMLElement>(
|
|
718
|
+
'button, [href], [tabindex]:not([tabindex="-1"])');
|
|
719
|
+
if (focusable.length === 0) return;
|
|
720
|
+
const first = focusable[0];
|
|
721
|
+
const last = focusable[focusable.length - 1];
|
|
722
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
723
|
+
e.preventDefault(); last.focus();
|
|
724
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
725
|
+
e.preventDefault(); first.focus();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
closeBtn.addEventListener('click', close);
|
|
731
|
+
document.addEventListener('keydown', onKey);
|
|
732
|
+
document.body.appendChild(overlay);
|
|
733
|
+
closeBtn.focus();
|
|
734
|
+
}
|
|
735
|
+
|
|
295
736
|
// ── Main export ──
|
|
296
|
-
export function renderMarkdown(text: string): string {
|
|
297
|
-
const
|
|
298
|
-
if (!
|
|
737
|
+
export function renderMarkdown(text: string, isStreaming = false): string {
|
|
738
|
+
const rawCleaned = stripOrchestration(text);
|
|
739
|
+
if (!rawCleaned) return `<em class="text-dim orchestrate-placeholder">${escapeHtml(t('orchestrator.dispatching'))}</em>`;
|
|
740
|
+
// Collapse 3+ consecutive newlines → double newline (prevents excessive paragraph breaks)
|
|
741
|
+
const cleaned = rawCleaned.replace(/\n{3,}/g, '\n\n');
|
|
742
|
+
|
|
743
|
+
// 1. Shield code fences (protect SVG in code blocks)
|
|
744
|
+
const { text: fenceShielded, fences } = shieldCodeFenceSvg(cleaned);
|
|
745
|
+
|
|
746
|
+
// 2. Extract top-level SVGs
|
|
747
|
+
const { text: svgShielded, blocks: svgBlocks } = extractTopLevelSvg(fenceShielded, isStreaming);
|
|
299
748
|
|
|
300
|
-
|
|
749
|
+
// 3. Unshield code fences (restore for marked processing)
|
|
750
|
+
const restored = unshieldCodeFenceSvg(svgShielded, fences);
|
|
301
751
|
|
|
752
|
+
// 4. Shield math
|
|
753
|
+
const { text: shielded, blocks: mathBlocks } = shieldMath(restored);
|
|
754
|
+
|
|
755
|
+
// 5. Marked parse
|
|
302
756
|
ensureMarked();
|
|
303
757
|
const fixed = fixCjkPunctuationBoundary(shielded);
|
|
304
758
|
let html = marked.parse(fixed) as string;
|
|
305
759
|
html = html.replace(/<table/g, '<div class="table-wrapper"><table').replace(/<\/table>/g, '</table></div>');
|
|
306
760
|
|
|
761
|
+
// 6. Unshield math
|
|
307
762
|
html = unshieldMath(html, mathBlocks);
|
|
763
|
+
|
|
764
|
+
// 7. Sanitize
|
|
308
765
|
html = sanitizeHtml(html);
|
|
309
766
|
|
|
767
|
+
// 8. Unshield SVGs (after sanitize — SVGs sanitized individually in renderSvgBlock)
|
|
768
|
+
html = unshieldSvgBlocks(html, svgBlocks);
|
|
769
|
+
|
|
770
|
+
// 9. Post-render async tasks
|
|
310
771
|
requestAnimationFrame(() => {
|
|
311
772
|
renderMermaidBlocks();
|
|
312
773
|
rehighlightAll();
|
|
774
|
+
bindDiagramZoom();
|
|
313
775
|
});
|
|
314
776
|
|
|
315
777
|
ensureCopyDelegation();
|
|
778
|
+
ensureDiagramActionDelegation();
|
|
316
779
|
|
|
317
780
|
return html;
|
|
318
781
|
}
|
|
@@ -13,7 +13,7 @@ export interface StreamState {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const FULL_RENDER_THRESHOLD = 2000;
|
|
16
|
-
const THROTTLE_MS =
|
|
16
|
+
const THROTTLE_MS = 80; // ~12fps — was 32ms (30fps), reduced to avoid blocking input
|
|
17
17
|
|
|
18
18
|
export function createStreamRenderer(el: HTMLElement): StreamState {
|
|
19
19
|
return { fullText: '', element: el, pendingRAF: null, isFinalized: false, lastRenderTime: 0 };
|
|
@@ -27,7 +27,7 @@ export function appendChunk(ss: StreamState, chunk: string): void {
|
|
|
27
27
|
if (ss.isFinalized) return;
|
|
28
28
|
const now = performance.now();
|
|
29
29
|
if (ss.fullText.length < FULL_RENDER_THRESHOLD || now - ss.lastRenderTime > THROTTLE_MS) {
|
|
30
|
-
ss.element.innerHTML = renderMarkdown(ss.fullText) +
|
|
30
|
+
ss.element.innerHTML = renderMarkdown(ss.fullText, true) +
|
|
31
31
|
'<span class="stream-cursor" aria-hidden="true"></span>';
|
|
32
32
|
ss.lastRenderTime = now;
|
|
33
33
|
} else {
|
|
@@ -35,7 +35,7 @@ export function appendChunk(ss: StreamState, chunk: string): void {
|
|
|
35
35
|
ss.pendingRAF = requestAnimationFrame(() => {
|
|
36
36
|
ss.pendingRAF = null;
|
|
37
37
|
if (ss.isFinalized) return;
|
|
38
|
-
ss.element.innerHTML = renderMarkdown(ss.fullText) +
|
|
38
|
+
ss.element.innerHTML = renderMarkdown(ss.fullText, true) +
|
|
39
39
|
'<span class="stream-cursor" aria-hidden="true"></span>';
|
|
40
40
|
ss.lastRenderTime = performance.now();
|
|
41
41
|
});
|