@yh-ui/components 1.0.8 → 1.0.15

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.
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 hljs.highlight(content, { language: lang, ignoreIllegals: true }).value;
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: "loose",
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(props.content);
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 hljs.highlight(jsonString, {
571
- language: "json",
572
- ignoreIllegals: true
573
- }).value;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 `<div class="${ns.e("line")} ${isHighlighted ? "is-active" : ""}">${line === "" ? " " : line}</div>`;
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 { aiMermaidProps, aiMermaidEmits } from "./ai-mermaid";
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: "loose"
65
+ securityLevel: "strict"
61
66
  });
62
- let processedCode = props.code;
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.8",
3
+ "version": "1.0.15",
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.8",
41
- "@yh-ui/locale": "^1.0.8",
42
- "@yh-ui/theme": "^1.0.8",
43
- "@yh-ui/utils": "^1.0.8",
40
+ "@yh-ui/hooks": "^1.0.15",
41
+ "@yh-ui/locale": "^1.0.15",
42
+ "@yh-ui/theme": "^1.0.15",
43
+ "@yh-ui/utils": "^1.0.15",
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",