@yh-ui/components 1.0.9 → 1.0.16
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/dist/ai-artifacts/src/ai-artifacts.css +34 -0
- package/dist/ai-artifacts/src/ai-artifacts.vue +41 -3
- package/dist/ai-bubble/src/ai-bubble.vue +41 -16
- package/dist/ai-code-block/src/ai-code-block.css +40 -0
- package/dist/ai-code-block/src/ai-code-block.vue +48 -2
- package/dist/ai-mermaid/src/ai-mermaid.cjs +9 -0
- package/dist/ai-mermaid/src/ai-mermaid.css +34 -0
- package/dist/ai-mermaid/src/ai-mermaid.d.ts +1 -0
- package/dist/ai-mermaid/src/ai-mermaid.mjs +8 -0
- package/dist/ai-mermaid/src/ai-mermaid.vue +43 -9
- package/dist/ai-thought-chain/src/ai-thought-chain.css +28 -0
- package/dist/ai-thought-chain/src/ai-thought-chain.vue +30 -1
- package/dist/sanitize.cjs +117 -0
- package/dist/sanitize.d.ts +13 -0
- package/dist/sanitize.mjs +240 -0
- package/package.json +6 -5
|
@@ -461,6 +461,8 @@ html.dark {
|
|
|
461
461
|
flex-direction: column;
|
|
462
462
|
z-index: 1001;
|
|
463
463
|
overflow: hidden;
|
|
464
|
+
container-type: inline-size;
|
|
465
|
+
container-name: ai-artifacts;
|
|
464
466
|
/* Theme adjustments */
|
|
465
467
|
background: var(--yh-ai-artifacts-bg-color, var(--yh-bg-color-overlay));
|
|
466
468
|
/* Inline mode */
|
|
@@ -626,6 +628,38 @@ html.dark {
|
|
|
626
628
|
justify-content: space-between;
|
|
627
629
|
}
|
|
628
630
|
|
|
631
|
+
@container ai-artifacts (max-width: 720px) {
|
|
632
|
+
.yh-ai-artifacts__header {
|
|
633
|
+
flex-wrap: wrap;
|
|
634
|
+
gap: 12px;
|
|
635
|
+
align-items: flex-start;
|
|
636
|
+
}
|
|
637
|
+
.yh-ai-artifacts__actions {
|
|
638
|
+
width: 100%;
|
|
639
|
+
justify-content: space-between;
|
|
640
|
+
flex-wrap: wrap;
|
|
641
|
+
}
|
|
642
|
+
.yh-ai-artifacts__tabs {
|
|
643
|
+
flex: 1 1 auto;
|
|
644
|
+
min-width: 0;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
@container ai-artifacts (max-width: 520px) {
|
|
648
|
+
.yh-ai-artifacts__tab {
|
|
649
|
+
flex: 1 1 0;
|
|
650
|
+
text-align: center;
|
|
651
|
+
}
|
|
652
|
+
.yh-ai-artifacts__version-bar {
|
|
653
|
+
flex-direction: column;
|
|
654
|
+
align-items: flex-start;
|
|
655
|
+
}
|
|
656
|
+
.yh-ai-artifacts__footer {
|
|
657
|
+
flex-direction: column;
|
|
658
|
+
gap: 4px;
|
|
659
|
+
align-items: flex-start;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
629
663
|
.yh-slide-right-enter-active,
|
|
630
664
|
.yh-slide-right-leave-active {
|
|
631
665
|
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
@@ -11,6 +11,7 @@ import { YhSpin } from "../../spin";
|
|
|
11
11
|
import { useComponentTheme } from "@yh-ui/theme";
|
|
12
12
|
import hljs from "../../highlight";
|
|
13
13
|
import "highlight.js/styles/atom-one-dark.css";
|
|
14
|
+
import { sanitizeHighlightedHtml } from "../../sanitize";
|
|
14
15
|
defineOptions({
|
|
15
16
|
name: "YhAiArtifacts"
|
|
16
17
|
});
|
|
@@ -24,6 +25,7 @@ const { themeStyle } = useComponentTheme(
|
|
|
24
25
|
);
|
|
25
26
|
const internalMode = ref(props.mode);
|
|
26
27
|
const currentVersionState = ref(props.data?.currentVersion || "");
|
|
28
|
+
const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
27
29
|
const echartsRef = ref(null);
|
|
28
30
|
const echartsInstance = shallowRef(null);
|
|
29
31
|
const echartsLoading = ref(false);
|
|
@@ -94,12 +96,14 @@ const highlightedCode = computed(() => {
|
|
|
94
96
|
if (type === "echarts") lang = "json";
|
|
95
97
|
if (lang && hljs.getLanguage(lang)) {
|
|
96
98
|
try {
|
|
97
|
-
return
|
|
99
|
+
return sanitizeHighlightedHtml(
|
|
100
|
+
hljs.highlight(content, { language: lang, ignoreIllegals: true }).value
|
|
101
|
+
);
|
|
98
102
|
} catch {
|
|
99
|
-
return content;
|
|
103
|
+
return sanitizeHighlightedHtml(escapeHtml(content));
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
|
-
return hljs.highlightAuto(content).value;
|
|
106
|
+
return sanitizeHighlightedHtml(hljs.highlightAuto(content).value);
|
|
103
107
|
});
|
|
104
108
|
const initECharts = async () => {
|
|
105
109
|
const chartType = props.data?.type;
|
|
@@ -803,6 +807,8 @@ html.dark {
|
|
|
803
807
|
flex-direction: column;
|
|
804
808
|
z-index: 1001;
|
|
805
809
|
overflow: hidden;
|
|
810
|
+
container-type: inline-size;
|
|
811
|
+
container-name: ai-artifacts;
|
|
806
812
|
/* Theme adjustments */
|
|
807
813
|
background: var(--yh-ai-artifacts-bg-color, var(--yh-bg-color-overlay));
|
|
808
814
|
/* Inline mode */
|
|
@@ -968,6 +974,38 @@ html.dark {
|
|
|
968
974
|
justify-content: space-between;
|
|
969
975
|
}
|
|
970
976
|
|
|
977
|
+
@container ai-artifacts (max-width: 720px) {
|
|
978
|
+
.yh-ai-artifacts__header {
|
|
979
|
+
flex-wrap: wrap;
|
|
980
|
+
gap: 12px;
|
|
981
|
+
align-items: flex-start;
|
|
982
|
+
}
|
|
983
|
+
.yh-ai-artifacts__actions {
|
|
984
|
+
width: 100%;
|
|
985
|
+
justify-content: space-between;
|
|
986
|
+
flex-wrap: wrap;
|
|
987
|
+
}
|
|
988
|
+
.yh-ai-artifacts__tabs {
|
|
989
|
+
flex: 1 1 auto;
|
|
990
|
+
min-width: 0;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
@container ai-artifacts (max-width: 520px) {
|
|
994
|
+
.yh-ai-artifacts__tab {
|
|
995
|
+
flex: 1 1 0;
|
|
996
|
+
text-align: center;
|
|
997
|
+
}
|
|
998
|
+
.yh-ai-artifacts__version-bar {
|
|
999
|
+
flex-direction: column;
|
|
1000
|
+
align-items: flex-start;
|
|
1001
|
+
}
|
|
1002
|
+
.yh-ai-artifacts__footer {
|
|
1003
|
+
flex-direction: column;
|
|
1004
|
+
gap: 4px;
|
|
1005
|
+
align-items: flex-start;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
971
1009
|
.yh-slide-right-enter-active,
|
|
972
1010
|
.yh-slide-right-leave-active {
|
|
973
1011
|
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
@@ -21,6 +21,7 @@ import { YhAiThoughtChain } from "../../ai-thought-chain";
|
|
|
21
21
|
import MarkdownIt from "../../markdown-it";
|
|
22
22
|
import hljs from "../../highlight";
|
|
23
23
|
import "highlight.js/styles/atom-one-dark.css";
|
|
24
|
+
import { sanitizeHighlightedHtml, sanitizeMarkup, sanitizeSvgMarkup } from "../../sanitize";
|
|
24
25
|
defineOptions({
|
|
25
26
|
name: "YhAiBubble"
|
|
26
27
|
});
|
|
@@ -65,6 +66,28 @@ const escapeHtml = (str) => {
|
|
|
65
66
|
};
|
|
66
67
|
return str.replace(/[&<>"']/g, (char) => htmlEntities[char]);
|
|
67
68
|
};
|
|
69
|
+
const sanitizeBubbleHtml = (html) => {
|
|
70
|
+
if (!props.enableSanitizer) {
|
|
71
|
+
return html;
|
|
72
|
+
}
|
|
73
|
+
return sanitizeMarkup(html, {
|
|
74
|
+
allowedTags: [.../* @__PURE__ */ new Set([...props.allowedTags, "button", "sub", "sup"])],
|
|
75
|
+
allowedAttributes: [
|
|
76
|
+
.../* @__PURE__ */ new Set([
|
|
77
|
+
...props.allowedAttributes,
|
|
78
|
+
"aria-label",
|
|
79
|
+
"data-code",
|
|
80
|
+
"data-id",
|
|
81
|
+
"data-lang",
|
|
82
|
+
"data-latex",
|
|
83
|
+
"role",
|
|
84
|
+
"tabindex"
|
|
85
|
+
])
|
|
86
|
+
],
|
|
87
|
+
allowedSchemes: props.allowedSchemes,
|
|
88
|
+
sanitizer: props.sanitizer
|
|
89
|
+
});
|
|
90
|
+
};
|
|
68
91
|
const getFileIcon = (url = "") => {
|
|
69
92
|
const ext = url.split(".").pop()?.toLowerCase() || "";
|
|
70
93
|
switch (ext) {
|
|
@@ -103,7 +126,7 @@ const initMermaid = async () => {
|
|
|
103
126
|
mermaidModule.default.initialize({
|
|
104
127
|
startOnLoad: false,
|
|
105
128
|
theme: "default",
|
|
106
|
-
securityLevel: "
|
|
129
|
+
securityLevel: "strict",
|
|
107
130
|
flowchart: { curve: "basis", padding: 15 },
|
|
108
131
|
sequence: { actorMargin: 50, boxMargin: 10 }
|
|
109
132
|
});
|
|
@@ -117,18 +140,18 @@ const _renderMermaid = async (code) => {
|
|
|
117
140
|
if (!mermaidModule) {
|
|
118
141
|
await initMermaid();
|
|
119
142
|
}
|
|
120
|
-
if (!mermaidModule) return `<pre class="mermaid-error">${code}</pre>`;
|
|
143
|
+
if (!mermaidModule) return `<pre class="mermaid-error">${escapeHtml(code)}</pre>`;
|
|
121
144
|
mermaidLoading.value = true;
|
|
122
145
|
mermaidError.value = null;
|
|
123
146
|
try {
|
|
124
147
|
const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
125
148
|
const { svg } = await mermaidModule.default.render(id, code);
|
|
126
149
|
mermaidLoading.value = false;
|
|
127
|
-
return svg;
|
|
150
|
+
return sanitizeSvgMarkup(svg);
|
|
128
151
|
} catch (e) {
|
|
129
152
|
mermaidLoading.value = false;
|
|
130
153
|
mermaidError.value = e instanceof Error ? e.message : "Failed to render mermaid diagram";
|
|
131
|
-
return `<pre class="mermaid-error">${code}</pre>`;
|
|
154
|
+
return `<pre class="mermaid-error">${escapeHtml(code)}</pre>`;
|
|
132
155
|
}
|
|
133
156
|
};
|
|
134
157
|
const expandedCodeBlocks = ref(/* @__PURE__ */ new Set());
|
|
@@ -445,7 +468,7 @@ const runCode = async (code, lang, id) => {
|
|
|
445
468
|
}
|
|
446
469
|
const triggerRender = () => {
|
|
447
470
|
if (props.markdown && mdi.value && props.content) {
|
|
448
|
-
parsedContent.value = mdi.value.render(props.content);
|
|
471
|
+
parsedContent.value = sanitizeBubbleHtml(mdi.value.render(props.content));
|
|
449
472
|
}
|
|
450
473
|
};
|
|
451
474
|
triggerRender();
|
|
@@ -462,7 +485,7 @@ const explainCode = async (code, lang) => {
|
|
|
462
485
|
}
|
|
463
486
|
return "";
|
|
464
487
|
};
|
|
465
|
-
const parsedContent = ref(
|
|
488
|
+
const parsedContent = ref("");
|
|
466
489
|
let renderRafId = null;
|
|
467
490
|
let streamTimer = null;
|
|
468
491
|
let streamPosition = 0;
|
|
@@ -567,12 +590,14 @@ const jsonHtml = computed(() => {
|
|
|
567
590
|
const rawData = props.structuredData.data;
|
|
568
591
|
const jsonString = typeof rawData === "string" ? rawData : JSON.stringify(rawData, null, 2);
|
|
569
592
|
if (hljs.getLanguage("json")) {
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
593
|
+
return sanitizeHighlightedHtml(
|
|
594
|
+
hljs.highlight(jsonString, {
|
|
595
|
+
language: "json",
|
|
596
|
+
ignoreIllegals: true
|
|
597
|
+
}).value
|
|
598
|
+
);
|
|
574
599
|
}
|
|
575
|
-
return jsonString;
|
|
600
|
+
return sanitizeHighlightedHtml(escapeHtml(jsonString));
|
|
576
601
|
} catch (e) {
|
|
577
602
|
console.warn("Failed to render JSON structured data:", e);
|
|
578
603
|
return "";
|
|
@@ -831,13 +856,13 @@ const streamRender = (fullContent, mode, speed, interval) => {
|
|
|
831
856
|
if (streamPosition < chunks.length) {
|
|
832
857
|
streamBuffer += chunks[streamPosition];
|
|
833
858
|
streamPosition++;
|
|
834
|
-
parsedContent.value = props.markdown && mdi.value ? mdi.value.render(streamBuffer) : streamBuffer;
|
|
859
|
+
parsedContent.value = props.markdown && mdi.value ? sanitizeBubbleHtml(mdi.value.render(streamBuffer)) : streamBuffer;
|
|
835
860
|
} else {
|
|
836
861
|
if (streamTimer) {
|
|
837
862
|
clearInterval(streamTimer);
|
|
838
863
|
streamTimer = null;
|
|
839
864
|
}
|
|
840
|
-
parsedContent.value = props.markdown && mdi.value ? mdi.value.render(fullContent) : fullContent;
|
|
865
|
+
parsedContent.value = props.markdown && mdi.value ? sanitizeBubbleHtml(mdi.value.render(fullContent)) : fullContent;
|
|
841
866
|
if (props.onStreamComplete) {
|
|
842
867
|
props.onStreamComplete();
|
|
843
868
|
}
|
|
@@ -856,7 +881,7 @@ watch(
|
|
|
856
881
|
cancelAnimationFrame(renderRafId);
|
|
857
882
|
renderRafId = null;
|
|
858
883
|
}
|
|
859
|
-
parsedContent.value = props.markdown && mdi.value ? mdi.value.render(newContent || "") : newContent;
|
|
884
|
+
parsedContent.value = props.markdown && mdi.value ? sanitizeBubbleHtml(mdi.value.render(newContent || "")) : newContent;
|
|
860
885
|
return;
|
|
861
886
|
}
|
|
862
887
|
if (props.streaming && props.typing) {
|
|
@@ -864,12 +889,12 @@ watch(
|
|
|
864
889
|
return;
|
|
865
890
|
}
|
|
866
891
|
if (typeof requestAnimationFrame === "undefined") {
|
|
867
|
-
parsedContent.value = mdi.value ? mdi.value.render(newContent || "") : newContent;
|
|
892
|
+
parsedContent.value = mdi.value ? sanitizeBubbleHtml(mdi.value.render(newContent || "")) : newContent;
|
|
868
893
|
return;
|
|
869
894
|
}
|
|
870
895
|
if (renderRafId) return;
|
|
871
896
|
renderRafId = requestAnimationFrame(() => {
|
|
872
|
-
parsedContent.value = mdi.value ? mdi.value.render(newContent || "") : newContent;
|
|
897
|
+
parsedContent.value = mdi.value ? sanitizeBubbleHtml(mdi.value.render(newContent || "")) : newContent;
|
|
873
898
|
renderRafId = null;
|
|
874
899
|
});
|
|
875
900
|
},
|
|
@@ -465,6 +465,8 @@ html.dark {
|
|
|
465
465
|
box-shadow: var(--yh-ai-code-block-shadow);
|
|
466
466
|
font-family: var(--yh-font-family-mono);
|
|
467
467
|
font-size: 14px;
|
|
468
|
+
container-type: inline-size;
|
|
469
|
+
container-name: ai-code-block;
|
|
468
470
|
}
|
|
469
471
|
.yh-ai-code-block__header {
|
|
470
472
|
display: flex;
|
|
@@ -609,4 +611,42 @@ html.dark {
|
|
|
609
611
|
|
|
610
612
|
.yh-ai-code-block.is-editing .yh-ai-code-block__header {
|
|
611
613
|
background-color: #2d2d2d;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
@container ai-code-block (max-width: 560px) {
|
|
617
|
+
.yh-ai-code-block__header {
|
|
618
|
+
flex-wrap: wrap;
|
|
619
|
+
gap: 8px;
|
|
620
|
+
align-items: flex-start;
|
|
621
|
+
}
|
|
622
|
+
.yh-ai-code-block__actions {
|
|
623
|
+
width: 100%;
|
|
624
|
+
justify-content: flex-end;
|
|
625
|
+
flex-wrap: wrap;
|
|
626
|
+
row-gap: 4px;
|
|
627
|
+
}
|
|
628
|
+
.yh-ai-code-block__line-numbers {
|
|
629
|
+
min-width: 32px;
|
|
630
|
+
padding: 12px 0;
|
|
631
|
+
}
|
|
632
|
+
.yh-ai-code-block__content {
|
|
633
|
+
padding: 12px 0;
|
|
634
|
+
}
|
|
635
|
+
.yh-ai-code-block__line {
|
|
636
|
+
padding: 0 12px;
|
|
637
|
+
}
|
|
638
|
+
.yh-ai-code-block__line.is-active {
|
|
639
|
+
padding-left: 10px;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
@container ai-code-block (max-width: 420px) {
|
|
643
|
+
.yh-ai-code-block__header {
|
|
644
|
+
padding: 8px 12px;
|
|
645
|
+
}
|
|
646
|
+
.yh-ai-code-block__body {
|
|
647
|
+
font-size: 13px;
|
|
648
|
+
}
|
|
649
|
+
.yh-ai-code-block__editor-wrapper {
|
|
650
|
+
padding: 8px;
|
|
651
|
+
}
|
|
612
652
|
}
|
|
@@ -8,6 +8,7 @@ import "highlight.js/styles/atom-one-dark.css";
|
|
|
8
8
|
import { aiCodeBlockProps, aiCodeBlockEmits } from "./ai-code-block";
|
|
9
9
|
import { useComponentTheme } from "@yh-ui/theme";
|
|
10
10
|
import YhAiCodeEditor from "../../ai-code-editor";
|
|
11
|
+
import { sanitizeHighlightedHtml } from "../../sanitize";
|
|
11
12
|
defineOptions({
|
|
12
13
|
name: "YhAiCodeBlock"
|
|
13
14
|
});
|
|
@@ -21,6 +22,9 @@ function normalizeCodeLines(code) {
|
|
|
21
22
|
if (!code) return "";
|
|
22
23
|
return code.replace(/\\n/g, "\n").trim();
|
|
23
24
|
}
|
|
25
|
+
function escapeHtml(value) {
|
|
26
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
27
|
+
}
|
|
24
28
|
const copied = ref(false);
|
|
25
29
|
const collapsed = ref(props.defaultCollapsed);
|
|
26
30
|
const isEditing = ref(false);
|
|
@@ -50,7 +54,7 @@ const lineCount = computed(() => {
|
|
|
50
54
|
});
|
|
51
55
|
const highlightedCodeLines = computed(() => {
|
|
52
56
|
if (!normalizedCode.value) return [];
|
|
53
|
-
let html = normalizedCode.value;
|
|
57
|
+
let html = escapeHtml(normalizedCode.value);
|
|
54
58
|
try {
|
|
55
59
|
if (props.highlight) {
|
|
56
60
|
if (props.language && hljs.getLanguage(props.language)) {
|
|
@@ -67,7 +71,9 @@ const highlightedCodeLines = computed(() => {
|
|
|
67
71
|
}
|
|
68
72
|
return html.split("\n").map((line, idx) => {
|
|
69
73
|
const isHighlighted = props.highlightLines.includes(idx + 1);
|
|
70
|
-
return
|
|
74
|
+
return sanitizeHighlightedHtml(
|
|
75
|
+
`<div class="${ns.e("line")} ${isHighlighted ? "is-active" : ""}">${line === "" ? " " : line}</div>`
|
|
76
|
+
);
|
|
71
77
|
});
|
|
72
78
|
});
|
|
73
79
|
const handleCopy = async () => {
|
|
@@ -663,6 +669,8 @@ html.dark {
|
|
|
663
669
|
box-shadow: var(--yh-ai-code-block-shadow);
|
|
664
670
|
font-family: var(--yh-font-family-mono);
|
|
665
671
|
font-size: 14px;
|
|
672
|
+
container-type: inline-size;
|
|
673
|
+
container-name: ai-code-block;
|
|
666
674
|
}
|
|
667
675
|
.yh-ai-code-block__header {
|
|
668
676
|
display: flex;
|
|
@@ -808,4 +816,42 @@ html.dark {
|
|
|
808
816
|
.yh-ai-code-block.is-editing .yh-ai-code-block__header {
|
|
809
817
|
background-color: #2d2d2d;
|
|
810
818
|
}
|
|
819
|
+
|
|
820
|
+
@container ai-code-block (max-width: 560px) {
|
|
821
|
+
.yh-ai-code-block__header {
|
|
822
|
+
flex-wrap: wrap;
|
|
823
|
+
gap: 8px;
|
|
824
|
+
align-items: flex-start;
|
|
825
|
+
}
|
|
826
|
+
.yh-ai-code-block__actions {
|
|
827
|
+
width: 100%;
|
|
828
|
+
justify-content: flex-end;
|
|
829
|
+
flex-wrap: wrap;
|
|
830
|
+
row-gap: 4px;
|
|
831
|
+
}
|
|
832
|
+
.yh-ai-code-block__line-numbers {
|
|
833
|
+
min-width: 32px;
|
|
834
|
+
padding: 12px 0;
|
|
835
|
+
}
|
|
836
|
+
.yh-ai-code-block__content {
|
|
837
|
+
padding: 12px 0;
|
|
838
|
+
}
|
|
839
|
+
.yh-ai-code-block__line {
|
|
840
|
+
padding: 0 12px;
|
|
841
|
+
}
|
|
842
|
+
.yh-ai-code-block__line.is-active {
|
|
843
|
+
padding-left: 10px;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
@container ai-code-block (max-width: 420px) {
|
|
847
|
+
.yh-ai-code-block__header {
|
|
848
|
+
padding: 8px 12px;
|
|
849
|
+
}
|
|
850
|
+
.yh-ai-code-block__body {
|
|
851
|
+
font-size: 13px;
|
|
852
|
+
}
|
|
853
|
+
.yh-ai-code-block__editor-wrapper {
|
|
854
|
+
padding: 8px;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
811
857
|
</style>
|
|
@@ -4,6 +4,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.aiMermaidProps = exports.aiMermaidEmits = void 0;
|
|
7
|
+
exports.buildMermaidRenderSource = buildMermaidRenderSource;
|
|
8
|
+
function buildMermaidRenderSource(code, config) {
|
|
9
|
+
const configStr = JSON.stringify({
|
|
10
|
+
...(config || {}),
|
|
11
|
+
securityLevel: "strict"
|
|
12
|
+
});
|
|
13
|
+
return `%%{init: ${configStr}}%%
|
|
14
|
+
${code}`;
|
|
15
|
+
}
|
|
7
16
|
const aiMermaidProps = exports.aiMermaidProps = {
|
|
8
17
|
/** Mermaid 代码内容 */
|
|
9
18
|
code: {
|
|
@@ -466,6 +466,8 @@ html.dark {
|
|
|
466
466
|
border-radius: 8px;
|
|
467
467
|
overflow: hidden;
|
|
468
468
|
box-sizing: border-box;
|
|
469
|
+
container-type: inline-size;
|
|
470
|
+
container-name: ai-mermaid;
|
|
469
471
|
}
|
|
470
472
|
.yh-ai-mermaid__header {
|
|
471
473
|
display: flex;
|
|
@@ -590,6 +592,38 @@ html.dark {
|
|
|
590
592
|
color: #f56c6c;
|
|
591
593
|
font-size: 14px;
|
|
592
594
|
}
|
|
595
|
+
@container ai-mermaid (max-width: 640px) {
|
|
596
|
+
.yh-ai-mermaid__header, .yh-ai-mermaid__actions {
|
|
597
|
+
flex-wrap: wrap;
|
|
598
|
+
gap: 8px;
|
|
599
|
+
align-items: flex-start;
|
|
600
|
+
}
|
|
601
|
+
.yh-ai-mermaid__actions {
|
|
602
|
+
padding: 10px 12px;
|
|
603
|
+
}
|
|
604
|
+
.yh-ai-mermaid__render-tabs, .yh-ai-mermaid__action-buttons {
|
|
605
|
+
width: 100%;
|
|
606
|
+
flex-wrap: wrap;
|
|
607
|
+
}
|
|
608
|
+
.yh-ai-mermaid__content {
|
|
609
|
+
padding: 12px;
|
|
610
|
+
}
|
|
611
|
+
.yh-ai-mermaid__code {
|
|
612
|
+
padding: 12px;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
@container ai-mermaid (max-width: 420px) {
|
|
616
|
+
.yh-ai-mermaid__render-tab {
|
|
617
|
+
flex: 1 1 0;
|
|
618
|
+
justify-content: center;
|
|
619
|
+
}
|
|
620
|
+
.yh-ai-mermaid__action-buttons {
|
|
621
|
+
justify-content: flex-end;
|
|
622
|
+
}
|
|
623
|
+
.yh-ai-mermaid__graph {
|
|
624
|
+
text-align: left;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
593
627
|
|
|
594
628
|
@keyframes rotate {
|
|
595
629
|
from {
|
|
@@ -43,6 +43,7 @@ export interface MermaidActions {
|
|
|
43
43
|
}
|
|
44
44
|
/** 渲染类型 */
|
|
45
45
|
export type RenderType = 'image' | 'code';
|
|
46
|
+
export declare function buildMermaidRenderSource(code: string, config?: MermaidConfig): string;
|
|
46
47
|
export declare const aiMermaidProps: {
|
|
47
48
|
/** Mermaid 代码内容 */
|
|
48
49
|
readonly code: {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export function buildMermaidRenderSource(code, config) {
|
|
2
|
+
const configStr = JSON.stringify({
|
|
3
|
+
...config || {},
|
|
4
|
+
securityLevel: "strict"
|
|
5
|
+
});
|
|
6
|
+
return `%%{init: ${configStr}}%%
|
|
7
|
+
${code}`;
|
|
8
|
+
}
|
|
1
9
|
export const aiMermaidProps = {
|
|
2
10
|
/** Mermaid 代码内容 */
|
|
3
11
|
code: {
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import { ref, computed, watch, shallowRef } from "vue";
|
|
3
3
|
import { useNamespace, useLocale } from "@yh-ui/hooks";
|
|
4
4
|
import { useComponentTheme } from "@yh-ui/theme";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
aiMermaidProps,
|
|
7
|
+
aiMermaidEmits,
|
|
8
|
+
buildMermaidRenderSource
|
|
9
|
+
} from "./ai-mermaid";
|
|
6
10
|
import { YhIcon } from "../../icon";
|
|
7
11
|
import { YhSpin } from "../../spin";
|
|
12
|
+
import { sanitizeSvgMarkup } from "../../sanitize";
|
|
8
13
|
defineOptions({
|
|
9
14
|
name: "YhAiMermaid"
|
|
10
15
|
});
|
|
@@ -57,17 +62,12 @@ const renderGraph = async () => {
|
|
|
57
62
|
}
|
|
58
63
|
module.initialize({
|
|
59
64
|
startOnLoad: false,
|
|
60
|
-
securityLevel: "
|
|
65
|
+
securityLevel: "strict"
|
|
61
66
|
});
|
|
62
|
-
|
|
63
|
-
if (props.config && Object.keys(props.config).length > 0) {
|
|
64
|
-
const configStr = JSON.stringify(props.config);
|
|
65
|
-
processedCode = `%%{init: ${configStr}}%%
|
|
66
|
-
${processedCode}`;
|
|
67
|
-
}
|
|
67
|
+
const processedCode = buildMermaidRenderSource(props.code, props.config);
|
|
68
68
|
const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
69
69
|
const { svg } = await module.render(id, processedCode);
|
|
70
|
-
svgContent.value = svg;
|
|
70
|
+
svgContent.value = sanitizeSvgMarkup(svg);
|
|
71
71
|
emit("ready");
|
|
72
72
|
} catch (error) {
|
|
73
73
|
console.error("Failed to render mermaid:", error);
|
|
@@ -716,6 +716,8 @@ html.dark {
|
|
|
716
716
|
border-radius: 8px;
|
|
717
717
|
overflow: hidden;
|
|
718
718
|
box-sizing: border-box;
|
|
719
|
+
container-type: inline-size;
|
|
720
|
+
container-name: ai-mermaid;
|
|
719
721
|
}
|
|
720
722
|
.yh-ai-mermaid__header {
|
|
721
723
|
display: flex;
|
|
@@ -840,6 +842,38 @@ html.dark {
|
|
|
840
842
|
color: #f56c6c;
|
|
841
843
|
font-size: 14px;
|
|
842
844
|
}
|
|
845
|
+
@container ai-mermaid (max-width: 640px) {
|
|
846
|
+
.yh-ai-mermaid__header, .yh-ai-mermaid__actions {
|
|
847
|
+
flex-wrap: wrap;
|
|
848
|
+
gap: 8px;
|
|
849
|
+
align-items: flex-start;
|
|
850
|
+
}
|
|
851
|
+
.yh-ai-mermaid__actions {
|
|
852
|
+
padding: 10px 12px;
|
|
853
|
+
}
|
|
854
|
+
.yh-ai-mermaid__render-tabs, .yh-ai-mermaid__action-buttons {
|
|
855
|
+
width: 100%;
|
|
856
|
+
flex-wrap: wrap;
|
|
857
|
+
}
|
|
858
|
+
.yh-ai-mermaid__content {
|
|
859
|
+
padding: 12px;
|
|
860
|
+
}
|
|
861
|
+
.yh-ai-mermaid__code {
|
|
862
|
+
padding: 12px;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
@container ai-mermaid (max-width: 420px) {
|
|
866
|
+
.yh-ai-mermaid__render-tab {
|
|
867
|
+
flex: 1 1 0;
|
|
868
|
+
justify-content: center;
|
|
869
|
+
}
|
|
870
|
+
.yh-ai-mermaid__action-buttons {
|
|
871
|
+
justify-content: flex-end;
|
|
872
|
+
}
|
|
873
|
+
.yh-ai-mermaid__graph {
|
|
874
|
+
text-align: left;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
843
877
|
|
|
844
878
|
@keyframes rotate {
|
|
845
879
|
from {
|
|
@@ -455,6 +455,8 @@ html.dark {
|
|
|
455
455
|
padding-left: 12px;
|
|
456
456
|
max-width: 100%;
|
|
457
457
|
transition: all 0.3s ease;
|
|
458
|
+
container-type: inline-size;
|
|
459
|
+
container-name: ai-thought-chain;
|
|
458
460
|
}
|
|
459
461
|
.yh-ai-thought-chain__progress-bar {
|
|
460
462
|
height: 4px;
|
|
@@ -762,6 +764,32 @@ html.dark {
|
|
|
762
764
|
white-space: pre-wrap;
|
|
763
765
|
}
|
|
764
766
|
|
|
767
|
+
@container ai-thought-chain (max-width: 560px) {
|
|
768
|
+
.yh-ai-thought-chain__add-node {
|
|
769
|
+
margin-left: 0;
|
|
770
|
+
}
|
|
771
|
+
.yh-ai-thought-chain__item-header {
|
|
772
|
+
flex-wrap: wrap;
|
|
773
|
+
height: auto;
|
|
774
|
+
row-gap: 6px;
|
|
775
|
+
}
|
|
776
|
+
.yh-ai-thought-chain__item-actions {
|
|
777
|
+
opacity: 1;
|
|
778
|
+
margin-left: 0;
|
|
779
|
+
}
|
|
780
|
+
.yh-ai-thought-chain__item-progress {
|
|
781
|
+
margin-left: 0;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
@container ai-thought-chain (max-width: 420px) {
|
|
785
|
+
.yh-ai-thought-chain__content {
|
|
786
|
+
padding: 10px;
|
|
787
|
+
}
|
|
788
|
+
.yh-ai-thought-chain__item-content {
|
|
789
|
+
padding: 8px;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
765
793
|
@keyframes yh-spin {
|
|
766
794
|
100% {
|
|
767
795
|
transform: rotate(360deg);
|
|
@@ -3,6 +3,7 @@ import { useNamespace, useLocale } from "@yh-ui/hooks";
|
|
|
3
3
|
import { ref, computed, watch } from "vue";
|
|
4
4
|
import { YhIcon } from "../../icon";
|
|
5
5
|
import MarkdownIt from "../../markdown-it";
|
|
6
|
+
import { sanitizeMarkup } from "../../sanitize";
|
|
6
7
|
import { aiThoughtChainProps, aiThoughtChainEmits } from "./ai-thought-chain";
|
|
7
8
|
import { useComponentTheme } from "@yh-ui/theme";
|
|
8
9
|
defineOptions({
|
|
@@ -64,7 +65,7 @@ const displayTitle = computed(() => {
|
|
|
64
65
|
});
|
|
65
66
|
const renderMarkdown = (content) => {
|
|
66
67
|
if (!props.markdown || !content) return content;
|
|
67
|
-
return md.render(content);
|
|
68
|
+
return sanitizeMarkup(md.render(content));
|
|
68
69
|
};
|
|
69
70
|
const getStatusIcon = (status = "none") => {
|
|
70
71
|
switch (status) {
|
|
@@ -758,6 +759,8 @@ html.dark {
|
|
|
758
759
|
padding-left: 12px;
|
|
759
760
|
max-width: 100%;
|
|
760
761
|
transition: all 0.3s ease;
|
|
762
|
+
container-type: inline-size;
|
|
763
|
+
container-name: ai-thought-chain;
|
|
761
764
|
}
|
|
762
765
|
.yh-ai-thought-chain__progress-bar {
|
|
763
766
|
height: 4px;
|
|
@@ -1065,6 +1068,32 @@ html.dark {
|
|
|
1065
1068
|
white-space: pre-wrap;
|
|
1066
1069
|
}
|
|
1067
1070
|
|
|
1071
|
+
@container ai-thought-chain (max-width: 560px) {
|
|
1072
|
+
.yh-ai-thought-chain__add-node {
|
|
1073
|
+
margin-left: 0;
|
|
1074
|
+
}
|
|
1075
|
+
.yh-ai-thought-chain__item-header {
|
|
1076
|
+
flex-wrap: wrap;
|
|
1077
|
+
height: auto;
|
|
1078
|
+
row-gap: 6px;
|
|
1079
|
+
}
|
|
1080
|
+
.yh-ai-thought-chain__item-actions {
|
|
1081
|
+
opacity: 1;
|
|
1082
|
+
margin-left: 0;
|
|
1083
|
+
}
|
|
1084
|
+
.yh-ai-thought-chain__item-progress {
|
|
1085
|
+
margin-left: 0;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
@container ai-thought-chain (max-width: 420px) {
|
|
1089
|
+
.yh-ai-thought-chain__content {
|
|
1090
|
+
padding: 10px;
|
|
1091
|
+
}
|
|
1092
|
+
.yh-ai-thought-chain__item-content {
|
|
1093
|
+
padding: 8px;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1068
1097
|
@keyframes yh-spin {
|
|
1069
1098
|
100% {
|
|
1070
1099
|
transform: rotate(360deg);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.sanitizeHighlightedHtml = sanitizeHighlightedHtml;
|
|
7
|
+
exports.sanitizeMarkup = sanitizeMarkup;
|
|
8
|
+
exports.sanitizeSvgMarkup = sanitizeSvgMarkup;
|
|
9
|
+
var _dompurify = _interopRequireDefault(require("dompurify"));
|
|
10
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
const DEFAULT_ALLOWED_SCHEMES = ["http", "https", "mailto", "tel"];
|
|
12
|
+
const DEFAULT_HTML_TAGS = ["a", "blockquote", "br", "button", "code", "div", "em", "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "img", "li", "ol", "p", "pre", "span", "strong", "sup", "sub", "table", "tbody", "td", "th", "thead", "tr", "ul"];
|
|
13
|
+
const DEFAULT_HTML_ATTRIBUTES = ["alt", "aria-label", "class", "data-code", "data-id", "data-lang", "data-latex", "height", "href", "id", "rel", "role", "src", "style", "tabindex", "target", "title", "width"];
|
|
14
|
+
const DEFAULT_SVG_TAGS = ["a", "br", "circle", "clipPath", "defs", "div", "ellipse", "foreignObject", "g", "line", "marker", "mask", "path", "pattern", "polygon", "polyline", "rect", "span", "stop", "style", "svg", "symbol", "text", "tspan", "use"];
|
|
15
|
+
const DEFAULT_SVG_ATTRIBUTES = [...DEFAULT_HTML_ATTRIBUTES, "clip-path", "cx", "cy", "d", "dominant-baseline", "fill", "fill-opacity", "filter", "font-family", "font-size", "font-weight", "gradientTransform", "gradientUnits", "letter-spacing", "marker-end", "marker-mid", "marker-start", "mask", "offset", "opacity", "patternContentUnits", "patternTransform", "patternUnits", "points", "preserveAspectRatio", "r", "rx", "ry", "stroke", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-opacity", "stroke-width", "text-anchor", "transform", "viewBox", "x", "x1", "x2", "xlink:href", "xmlns", "xmlns:xlink", "xml:space", "y", "y1", "y2"];
|
|
16
|
+
let purifier = null;
|
|
17
|
+
let purifierWindow = null;
|
|
18
|
+
function getPurifier() {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (!purifier || purifierWindow !== window) {
|
|
23
|
+
purifier = (0, _dompurify.default)(window);
|
|
24
|
+
purifierWindow = window;
|
|
25
|
+
}
|
|
26
|
+
return purifier;
|
|
27
|
+
}
|
|
28
|
+
function normalizeSchemes(allowedSchemes) {
|
|
29
|
+
return (allowedSchemes?.length ? allowedSchemes : DEFAULT_ALLOWED_SCHEMES).map(scheme => scheme.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
function isAllowedUrl(value, allowedSchemes) {
|
|
32
|
+
const normalized = value.trim();
|
|
33
|
+
if (!normalized) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (normalized.startsWith("#") || normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../")) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (normalized.startsWith("//")) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const schemeMatch = normalized.match(/^([a-z][a-z0-9+.-]*):/i);
|
|
43
|
+
if (!schemeMatch) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return allowedSchemes.includes(schemeMatch[1].toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
function postProcessSanitizedMarkup(html, allowedSchemes) {
|
|
49
|
+
if (typeof document === "undefined") {
|
|
50
|
+
return html;
|
|
51
|
+
}
|
|
52
|
+
const template = document.createElement("template");
|
|
53
|
+
template.innerHTML = html;
|
|
54
|
+
for (const el of template.content.querySelectorAll("[href], [src], [xlink\\:href]")) {
|
|
55
|
+
for (const attributeName of ["href", "src", "xlink:href"]) {
|
|
56
|
+
const rawValue = el.getAttribute(attributeName);
|
|
57
|
+
if (!rawValue) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!isAllowedUrl(rawValue, allowedSchemes)) {
|
|
61
|
+
el.removeAttribute(attributeName);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (el instanceof HTMLAnchorElement && el.getAttribute("target") === "_blank") {
|
|
65
|
+
const relTokens = new Set((el.getAttribute("rel") || "").split(/\s+/).filter(Boolean));
|
|
66
|
+
relTokens.add("noopener");
|
|
67
|
+
relTokens.add("noreferrer");
|
|
68
|
+
el.setAttribute("rel", [...relTokens].join(" "));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return template.innerHTML;
|
|
72
|
+
}
|
|
73
|
+
function serverSideSanitize(html, allowedSchemes) {
|
|
74
|
+
let output = html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/<iframe[\s\S]*?>[\s\S]*?<\/iframe>/gi, "").replace(/<object[\s\S]*?>[\s\S]*?<\/object>/gi, "").replace(/<embed[\s\S]*?>/gi, "").replace(/\son[a-z-]+\s*=\s*(['"]).*?\1/gi, "").replace(/\son[a-z-]+\s*=\s*[^\s>]+/gi, "");
|
|
75
|
+
output = output.replace(/\s(href|src|xlink:href)\s*=\s*(['"])(.*?)\2/gi, (full, attrName, quote, rawValue) => isAllowedUrl(rawValue, allowedSchemes) ? full : ` ${attrName}=${quote}${quote}`);
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
78
|
+
function sanitizeMarkup(html, options = {}) {
|
|
79
|
+
const allowedSchemes = normalizeSchemes(options.allowedSchemes);
|
|
80
|
+
const source = options.sanitizer ? options.sanitizer(html) : html;
|
|
81
|
+
if (!source) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
const allowedTags = options.allowedTags ?? [...(options.profile === "svg" ? DEFAULT_SVG_TAGS : DEFAULT_HTML_TAGS)];
|
|
85
|
+
const allowedAttributes = options.allowedAttributes ?? [...(options.profile === "svg" ? DEFAULT_SVG_ATTRIBUTES : DEFAULT_HTML_ATTRIBUTES)];
|
|
86
|
+
const purifierInstance = getPurifier();
|
|
87
|
+
if (!purifierInstance) {
|
|
88
|
+
return serverSideSanitize(source, allowedSchemes);
|
|
89
|
+
}
|
|
90
|
+
const sanitized = purifierInstance.sanitize(source, {
|
|
91
|
+
ALLOWED_TAGS: allowedTags,
|
|
92
|
+
ALLOWED_ATTR: allowedAttributes,
|
|
93
|
+
ALLOW_DATA_ATTR: options.allowDataAttributes ?? true,
|
|
94
|
+
USE_PROFILES: options.profile === "svg" ? {
|
|
95
|
+
html: true,
|
|
96
|
+
svg: true,
|
|
97
|
+
svgFilters: true
|
|
98
|
+
} : {
|
|
99
|
+
html: true
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return postProcessSanitizedMarkup(String(sanitized), allowedSchemes);
|
|
103
|
+
}
|
|
104
|
+
function sanitizeHighlightedHtml(html, options = {}) {
|
|
105
|
+
return sanitizeMarkup(html, {
|
|
106
|
+
...options,
|
|
107
|
+
allowedTags: ["code", "div", "pre", "span"],
|
|
108
|
+
allowedAttributes: ["class"],
|
|
109
|
+
profile: "html"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function sanitizeSvgMarkup(svg, options = {}) {
|
|
113
|
+
return sanitizeMarkup(svg, {
|
|
114
|
+
...options,
|
|
115
|
+
profile: "svg"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type MarkupProfile = 'html' | 'svg';
|
|
2
|
+
export interface SanitizeMarkupOptions {
|
|
3
|
+
allowedTags?: string[];
|
|
4
|
+
allowedAttributes?: string[];
|
|
5
|
+
allowedSchemes?: string[];
|
|
6
|
+
allowDataAttributes?: boolean;
|
|
7
|
+
profile?: MarkupProfile;
|
|
8
|
+
sanitizer?: ((html: string) => string) | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function sanitizeMarkup(html: string, options?: SanitizeMarkupOptions): string;
|
|
11
|
+
export declare function sanitizeHighlightedHtml(html: string, options?: Omit<SanitizeMarkupOptions, 'allowedTags' | 'allowedAttributes' | 'profile'>): string;
|
|
12
|
+
export declare function sanitizeSvgMarkup(svg: string, options?: Omit<SanitizeMarkupOptions, 'profile'>): string;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import createDOMPurify from "dompurify";
|
|
2
|
+
const DEFAULT_ALLOWED_SCHEMES = ["http", "https", "mailto", "tel"];
|
|
3
|
+
const DEFAULT_HTML_TAGS = [
|
|
4
|
+
"a",
|
|
5
|
+
"blockquote",
|
|
6
|
+
"br",
|
|
7
|
+
"button",
|
|
8
|
+
"code",
|
|
9
|
+
"div",
|
|
10
|
+
"em",
|
|
11
|
+
"figcaption",
|
|
12
|
+
"figure",
|
|
13
|
+
"h1",
|
|
14
|
+
"h2",
|
|
15
|
+
"h3",
|
|
16
|
+
"h4",
|
|
17
|
+
"h5",
|
|
18
|
+
"h6",
|
|
19
|
+
"hr",
|
|
20
|
+
"img",
|
|
21
|
+
"li",
|
|
22
|
+
"ol",
|
|
23
|
+
"p",
|
|
24
|
+
"pre",
|
|
25
|
+
"span",
|
|
26
|
+
"strong",
|
|
27
|
+
"sup",
|
|
28
|
+
"sub",
|
|
29
|
+
"table",
|
|
30
|
+
"tbody",
|
|
31
|
+
"td",
|
|
32
|
+
"th",
|
|
33
|
+
"thead",
|
|
34
|
+
"tr",
|
|
35
|
+
"ul"
|
|
36
|
+
];
|
|
37
|
+
const DEFAULT_HTML_ATTRIBUTES = [
|
|
38
|
+
"alt",
|
|
39
|
+
"aria-label",
|
|
40
|
+
"class",
|
|
41
|
+
"data-code",
|
|
42
|
+
"data-id",
|
|
43
|
+
"data-lang",
|
|
44
|
+
"data-latex",
|
|
45
|
+
"height",
|
|
46
|
+
"href",
|
|
47
|
+
"id",
|
|
48
|
+
"rel",
|
|
49
|
+
"role",
|
|
50
|
+
"src",
|
|
51
|
+
"style",
|
|
52
|
+
"tabindex",
|
|
53
|
+
"target",
|
|
54
|
+
"title",
|
|
55
|
+
"width"
|
|
56
|
+
];
|
|
57
|
+
const DEFAULT_SVG_TAGS = [
|
|
58
|
+
"a",
|
|
59
|
+
"br",
|
|
60
|
+
"circle",
|
|
61
|
+
"clipPath",
|
|
62
|
+
"defs",
|
|
63
|
+
"div",
|
|
64
|
+
"ellipse",
|
|
65
|
+
"foreignObject",
|
|
66
|
+
"g",
|
|
67
|
+
"line",
|
|
68
|
+
"marker",
|
|
69
|
+
"mask",
|
|
70
|
+
"path",
|
|
71
|
+
"pattern",
|
|
72
|
+
"polygon",
|
|
73
|
+
"polyline",
|
|
74
|
+
"rect",
|
|
75
|
+
"span",
|
|
76
|
+
"stop",
|
|
77
|
+
"style",
|
|
78
|
+
"svg",
|
|
79
|
+
"symbol",
|
|
80
|
+
"text",
|
|
81
|
+
"tspan",
|
|
82
|
+
"use"
|
|
83
|
+
];
|
|
84
|
+
const DEFAULT_SVG_ATTRIBUTES = [
|
|
85
|
+
...DEFAULT_HTML_ATTRIBUTES,
|
|
86
|
+
"clip-path",
|
|
87
|
+
"cx",
|
|
88
|
+
"cy",
|
|
89
|
+
"d",
|
|
90
|
+
"dominant-baseline",
|
|
91
|
+
"fill",
|
|
92
|
+
"fill-opacity",
|
|
93
|
+
"filter",
|
|
94
|
+
"font-family",
|
|
95
|
+
"font-size",
|
|
96
|
+
"font-weight",
|
|
97
|
+
"gradientTransform",
|
|
98
|
+
"gradientUnits",
|
|
99
|
+
"letter-spacing",
|
|
100
|
+
"marker-end",
|
|
101
|
+
"marker-mid",
|
|
102
|
+
"marker-start",
|
|
103
|
+
"mask",
|
|
104
|
+
"offset",
|
|
105
|
+
"opacity",
|
|
106
|
+
"patternContentUnits",
|
|
107
|
+
"patternTransform",
|
|
108
|
+
"patternUnits",
|
|
109
|
+
"points",
|
|
110
|
+
"preserveAspectRatio",
|
|
111
|
+
"r",
|
|
112
|
+
"rx",
|
|
113
|
+
"ry",
|
|
114
|
+
"stroke",
|
|
115
|
+
"stroke-dasharray",
|
|
116
|
+
"stroke-linecap",
|
|
117
|
+
"stroke-linejoin",
|
|
118
|
+
"stroke-opacity",
|
|
119
|
+
"stroke-width",
|
|
120
|
+
"text-anchor",
|
|
121
|
+
"transform",
|
|
122
|
+
"viewBox",
|
|
123
|
+
"x",
|
|
124
|
+
"x1",
|
|
125
|
+
"x2",
|
|
126
|
+
"xlink:href",
|
|
127
|
+
"xmlns",
|
|
128
|
+
"xmlns:xlink",
|
|
129
|
+
"xml:space",
|
|
130
|
+
"y",
|
|
131
|
+
"y1",
|
|
132
|
+
"y2"
|
|
133
|
+
];
|
|
134
|
+
let purifier = null;
|
|
135
|
+
let purifierWindow = null;
|
|
136
|
+
function getPurifier() {
|
|
137
|
+
if (typeof window === "undefined") {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (!purifier || purifierWindow !== window) {
|
|
141
|
+
purifier = createDOMPurify(window);
|
|
142
|
+
purifierWindow = window;
|
|
143
|
+
}
|
|
144
|
+
return purifier;
|
|
145
|
+
}
|
|
146
|
+
function normalizeSchemes(allowedSchemes) {
|
|
147
|
+
return (allowedSchemes?.length ? allowedSchemes : DEFAULT_ALLOWED_SCHEMES).map(
|
|
148
|
+
(scheme) => scheme.toLowerCase()
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
function isAllowedUrl(value, allowedSchemes) {
|
|
152
|
+
const normalized = value.trim();
|
|
153
|
+
if (!normalized) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (normalized.startsWith("#") || normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../")) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (normalized.startsWith("//")) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const schemeMatch = normalized.match(/^([a-z][a-z0-9+.-]*):/i);
|
|
163
|
+
if (!schemeMatch) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return allowedSchemes.includes(schemeMatch[1].toLowerCase());
|
|
167
|
+
}
|
|
168
|
+
function postProcessSanitizedMarkup(html, allowedSchemes) {
|
|
169
|
+
if (typeof document === "undefined") {
|
|
170
|
+
return html;
|
|
171
|
+
}
|
|
172
|
+
const template = document.createElement("template");
|
|
173
|
+
template.innerHTML = html;
|
|
174
|
+
for (const el of template.content.querySelectorAll(
|
|
175
|
+
"[href], [src], [xlink\\:href]"
|
|
176
|
+
)) {
|
|
177
|
+
for (const attributeName of ["href", "src", "xlink:href"]) {
|
|
178
|
+
const rawValue = el.getAttribute(attributeName);
|
|
179
|
+
if (!rawValue) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!isAllowedUrl(rawValue, allowedSchemes)) {
|
|
183
|
+
el.removeAttribute(attributeName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (el instanceof HTMLAnchorElement && el.getAttribute("target") === "_blank") {
|
|
187
|
+
const relTokens = new Set((el.getAttribute("rel") || "").split(/\s+/).filter(Boolean));
|
|
188
|
+
relTokens.add("noopener");
|
|
189
|
+
relTokens.add("noreferrer");
|
|
190
|
+
el.setAttribute("rel", [...relTokens].join(" "));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return template.innerHTML;
|
|
194
|
+
}
|
|
195
|
+
function serverSideSanitize(html, allowedSchemes) {
|
|
196
|
+
let output = html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/<iframe[\s\S]*?>[\s\S]*?<\/iframe>/gi, "").replace(/<object[\s\S]*?>[\s\S]*?<\/object>/gi, "").replace(/<embed[\s\S]*?>/gi, "").replace(/\son[a-z-]+\s*=\s*(['"]).*?\1/gi, "").replace(/\son[a-z-]+\s*=\s*[^\s>]+/gi, "");
|
|
197
|
+
output = output.replace(
|
|
198
|
+
/\s(href|src|xlink:href)\s*=\s*(['"])(.*?)\2/gi,
|
|
199
|
+
(full, attrName, quote, rawValue) => isAllowedUrl(rawValue, allowedSchemes) ? full : ` ${attrName}=${quote}${quote}`
|
|
200
|
+
);
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
export function sanitizeMarkup(html, options = {}) {
|
|
204
|
+
const allowedSchemes = normalizeSchemes(options.allowedSchemes);
|
|
205
|
+
const source = options.sanitizer ? options.sanitizer(html) : html;
|
|
206
|
+
if (!source) {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
const allowedTags = options.allowedTags ?? [
|
|
210
|
+
...options.profile === "svg" ? DEFAULT_SVG_TAGS : DEFAULT_HTML_TAGS
|
|
211
|
+
];
|
|
212
|
+
const allowedAttributes = options.allowedAttributes ?? [
|
|
213
|
+
...options.profile === "svg" ? DEFAULT_SVG_ATTRIBUTES : DEFAULT_HTML_ATTRIBUTES
|
|
214
|
+
];
|
|
215
|
+
const purifierInstance = getPurifier();
|
|
216
|
+
if (!purifierInstance) {
|
|
217
|
+
return serverSideSanitize(source, allowedSchemes);
|
|
218
|
+
}
|
|
219
|
+
const sanitized = purifierInstance.sanitize(source, {
|
|
220
|
+
ALLOWED_TAGS: allowedTags,
|
|
221
|
+
ALLOWED_ATTR: allowedAttributes,
|
|
222
|
+
ALLOW_DATA_ATTR: options.allowDataAttributes ?? true,
|
|
223
|
+
USE_PROFILES: options.profile === "svg" ? { html: true, svg: true, svgFilters: true } : { html: true }
|
|
224
|
+
});
|
|
225
|
+
return postProcessSanitizedMarkup(String(sanitized), allowedSchemes);
|
|
226
|
+
}
|
|
227
|
+
export function sanitizeHighlightedHtml(html, options = {}) {
|
|
228
|
+
return sanitizeMarkup(html, {
|
|
229
|
+
...options,
|
|
230
|
+
allowedTags: ["code", "div", "pre", "span"],
|
|
231
|
+
allowedAttributes: ["class"],
|
|
232
|
+
profile: "html"
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
export function sanitizeSvgMarkup(svg, options = {}) {
|
|
236
|
+
return sanitizeMarkup(svg, {
|
|
237
|
+
...options,
|
|
238
|
+
profile: "svg"
|
|
239
|
+
});
|
|
240
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yh-ui/components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "YH-UI Vue 3 Components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -37,12 +37,13 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@webcontainer/api": "^1.6.1",
|
|
39
39
|
"@floating-ui/dom": "^1.7.4",
|
|
40
|
-
"@yh-ui/hooks": "^1.0.
|
|
41
|
-
"@yh-ui/locale": "^1.0.
|
|
42
|
-
"@yh-ui/theme": "^1.0.
|
|
43
|
-
"@yh-ui/utils": "^1.0.
|
|
40
|
+
"@yh-ui/hooks": "^1.0.16",
|
|
41
|
+
"@yh-ui/locale": "^1.0.16",
|
|
42
|
+
"@yh-ui/theme": "^1.0.16",
|
|
43
|
+
"@yh-ui/utils": "^1.0.16",
|
|
44
44
|
"async-validator": "^4.2.5",
|
|
45
45
|
"dayjs": "^1.11.19",
|
|
46
|
+
"dompurify": "^3.3.3",
|
|
46
47
|
"markdown-it": "^14.1.1",
|
|
47
48
|
"monaco-editor": "^0.55.1",
|
|
48
49
|
"xlsx": "^0.18.5",
|