@h-ai/ui 0.1.0-alpha.29 → 0.1.0-alpha.31

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.
Files changed (194) hide show
  1. package/README.md +50 -3
  2. package/dist/components/compounds/Accordion.svelte +6 -3
  3. package/dist/components/compounds/Accordion.svelte.d.ts +3 -1
  4. package/dist/components/compounds/ActionSheet.svelte +11 -8
  5. package/dist/components/compounds/ActionSheet.svelte.d.ts +3 -1
  6. package/dist/components/compounds/Alert.svelte +6 -4
  7. package/dist/components/compounds/Alert.svelte.d.ts +3 -2
  8. package/dist/components/compounds/AppBar.svelte +13 -9
  9. package/dist/components/compounds/AppBar.svelte.d.ts +3 -1
  10. package/dist/components/compounds/BottomNav.svelte +13 -10
  11. package/dist/components/compounds/BottomNav.svelte.d.ts +3 -1
  12. package/dist/components/compounds/Breadcrumb.svelte +6 -4
  13. package/dist/components/compounds/Breadcrumb.svelte.d.ts +3 -2
  14. package/dist/components/compounds/Calendar.svelte +7 -2
  15. package/dist/components/compounds/Calendar.svelte.d.ts +3 -1
  16. package/dist/components/compounds/Card.svelte +6 -4
  17. package/dist/components/compounds/Card.svelte.d.ts +3 -2
  18. package/dist/components/compounds/Combobox.svelte +6 -2
  19. package/dist/components/compounds/Combobox.svelte.d.ts +3 -1
  20. package/dist/components/compounds/Confirm.svelte +9 -7
  21. package/dist/components/compounds/Confirm.svelte.d.ts +3 -2
  22. package/dist/components/compounds/DataTable.svelte +6 -2
  23. package/dist/components/compounds/DataTable.svelte.d.ts +2 -1
  24. package/dist/components/compounds/DatePicker.svelte +7 -2
  25. package/dist/components/compounds/DatePicker.svelte.d.ts +3 -1
  26. package/dist/components/compounds/Drawer.svelte +6 -4
  27. package/dist/components/compounds/Drawer.svelte.d.ts +3 -2
  28. package/dist/components/compounds/Dropdown.svelte +6 -4
  29. package/dist/components/compounds/Dropdown.svelte.d.ts +3 -2
  30. package/dist/components/compounds/Empty.svelte +6 -4
  31. package/dist/components/compounds/Empty.svelte.d.ts +3 -2
  32. package/dist/components/compounds/Form.svelte +11 -9
  33. package/dist/components/compounds/Form.svelte.d.ts +3 -2
  34. package/dist/components/compounds/FormField.svelte +6 -4
  35. package/dist/components/compounds/FormField.svelte.d.ts +3 -2
  36. package/dist/components/compounds/InfiniteScroll.svelte +10 -7
  37. package/dist/components/compounds/InfiniteScroll.svelte.d.ts +3 -1
  38. package/dist/components/compounds/MessageBoxContainer.svelte +6 -1
  39. package/dist/components/compounds/MessageBoxContainer.svelte.d.ts +3 -17
  40. package/dist/components/compounds/MessageBoxDialog.svelte +9 -7
  41. package/dist/components/compounds/MessageBoxDialog.svelte.d.ts +2 -1
  42. package/dist/components/compounds/MessageContainer.svelte +43 -66
  43. package/dist/components/compounds/MessageContainer.svelte.d.ts +2 -1
  44. package/dist/components/compounds/Modal.svelte +29 -43
  45. package/dist/components/compounds/Modal.svelte.d.ts +3 -2
  46. package/dist/components/compounds/PageHeader.svelte +6 -2
  47. package/dist/components/compounds/PageHeader.svelte.d.ts +3 -1
  48. package/dist/components/compounds/Pagination.svelte +6 -4
  49. package/dist/components/compounds/Pagination.svelte.d.ts +3 -2
  50. package/dist/components/compounds/Popover.svelte +10 -8
  51. package/dist/components/compounds/Popover.svelte.d.ts +3 -2
  52. package/dist/components/compounds/PullRefresh.svelte +11 -8
  53. package/dist/components/compounds/PullRefresh.svelte.d.ts +3 -1
  54. package/dist/components/compounds/Result.svelte +6 -4
  55. package/dist/components/compounds/Result.svelte.d.ts +3 -2
  56. package/dist/components/compounds/SafeArea.svelte +6 -3
  57. package/dist/components/compounds/SafeArea.svelte.d.ts +3 -1
  58. package/dist/components/compounds/Skeleton.svelte +6 -4
  59. package/dist/components/compounds/Skeleton.svelte.d.ts +3 -2
  60. package/dist/components/compounds/Steps.svelte +6 -4
  61. package/dist/components/compounds/Steps.svelte.d.ts +3 -2
  62. package/dist/components/compounds/SwipeCell.svelte +6 -3
  63. package/dist/components/compounds/SwipeCell.svelte.d.ts +3 -1
  64. package/dist/components/compounds/Tabs.svelte +6 -4
  65. package/dist/components/compounds/Tabs.svelte.d.ts +3 -2
  66. package/dist/components/compounds/TagInput.svelte +11 -9
  67. package/dist/components/compounds/TagInput.svelte.d.ts +3 -2
  68. package/dist/components/compounds/Timeline.svelte +6 -3
  69. package/dist/components/compounds/Timeline.svelte.d.ts +3 -1
  70. package/dist/components/compounds/ToastContainer.svelte +6 -2
  71. package/dist/components/compounds/ToastContainer.svelte.d.ts +2 -1
  72. package/dist/components/compounds/Tooltip.svelte +6 -4
  73. package/dist/components/compounds/Tooltip.svelte.d.ts +3 -2
  74. package/dist/components/primitives/Avatar.svelte +6 -4
  75. package/dist/components/primitives/Avatar.svelte.d.ts +3 -2
  76. package/dist/components/primitives/Badge.svelte +6 -4
  77. package/dist/components/primitives/Badge.svelte.d.ts +3 -2
  78. package/dist/components/primitives/BareButton.svelte +15 -12
  79. package/dist/components/primitives/BareButton.svelte.d.ts +3 -2
  80. package/dist/components/primitives/BareInput.svelte +37 -34
  81. package/dist/components/primitives/BareInput.svelte.d.ts +3 -2
  82. package/dist/components/primitives/Button.svelte +12 -10
  83. package/dist/components/primitives/Button.svelte.d.ts +3 -2
  84. package/dist/components/primitives/Checkbox.svelte +6 -4
  85. package/dist/components/primitives/Checkbox.svelte.d.ts +3 -2
  86. package/dist/components/primitives/IconButton.svelte +6 -4
  87. package/dist/components/primitives/IconButton.svelte.d.ts +3 -2
  88. package/dist/components/primitives/Input.svelte +6 -4
  89. package/dist/components/primitives/Input.svelte.d.ts +3 -2
  90. package/dist/components/primitives/Progress.svelte +6 -4
  91. package/dist/components/primitives/Progress.svelte.d.ts +3 -2
  92. package/dist/components/primitives/Radio.svelte +6 -4
  93. package/dist/components/primitives/Radio.svelte.d.ts +2 -2
  94. package/dist/components/primitives/Range.svelte +6 -3
  95. package/dist/components/primitives/Range.svelte.d.ts +3 -1
  96. package/dist/components/primitives/Rating.svelte +6 -3
  97. package/dist/components/primitives/Rating.svelte.d.ts +3 -1
  98. package/dist/components/primitives/Select.svelte +255 -48
  99. package/dist/components/primitives/Select.svelte.d.ts +2 -2
  100. package/dist/components/primitives/Spinner.svelte +6 -4
  101. package/dist/components/primitives/Spinner.svelte.d.ts +3 -2
  102. package/dist/components/primitives/Switch.svelte +6 -4
  103. package/dist/components/primitives/Switch.svelte.d.ts +3 -2
  104. package/dist/components/primitives/Tag.svelte +6 -4
  105. package/dist/components/primitives/Tag.svelte.d.ts +3 -2
  106. package/dist/components/primitives/Textarea.svelte +34 -19
  107. package/dist/components/primitives/Textarea.svelte.d.ts +3 -2
  108. package/dist/components/primitives/ToggleCheckbox.svelte +13 -10
  109. package/dist/components/primitives/ToggleCheckbox.svelte.d.ts +3 -2
  110. package/dist/components/primitives/ToggleInput.svelte +13 -10
  111. package/dist/components/primitives/ToggleInput.svelte.d.ts +3 -2
  112. package/dist/components/primitives/ToggleRadio.svelte +13 -10
  113. package/dist/components/primitives/ToggleRadio.svelte.d.ts +3 -2
  114. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +8 -5
  115. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte.d.ts +3 -1
  116. package/dist/components/scenes/ai/AiDocumentEditor.svelte +40 -10
  117. package/dist/components/scenes/ai/AiDocumentEditor.svelte.d.ts +3 -1
  118. package/dist/components/scenes/ai/MarkdownRenderer.svelte +49 -7
  119. package/dist/components/scenes/ai/MarkdownRenderer.svelte.d.ts +3 -1
  120. package/dist/components/scenes/ai/document-parse.d.ts +2 -0
  121. package/dist/components/scenes/ai/document-parse.js +16 -17
  122. package/dist/components/scenes/ai/document-types.d.ts +6 -1
  123. package/dist/components/scenes/ai/markdown-parse.d.ts +2 -0
  124. package/dist/components/scenes/ai/markdown-parse.js +17 -20
  125. package/dist/components/scenes/ai/safe-html.d.ts +16 -0
  126. package/dist/components/scenes/ai/safe-html.js +284 -0
  127. package/dist/components/scenes/ai/table/AiTableEditor.svelte +6 -3
  128. package/dist/components/scenes/ai/table/AiTableEditor.svelte.d.ts +3 -1
  129. package/dist/components/scenes/ai/table/table-types.d.ts +2 -1
  130. package/dist/components/scenes/app/FeedbackModal.svelte +5 -2
  131. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +3 -1
  132. package/dist/components/scenes/app/LanguageSwitch.svelte +6 -2
  133. package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +3 -1
  134. package/dist/components/scenes/app/SettingsLayout.svelte +6 -3
  135. package/dist/components/scenes/app/SettingsLayout.svelte.d.ts +2 -1
  136. package/dist/components/scenes/app/SettingsModal.svelte +6 -3
  137. package/dist/components/scenes/app/SettingsModal.svelte.d.ts +3 -1
  138. package/dist/components/scenes/app/ThemeColorPicker.svelte +6 -3
  139. package/dist/components/scenes/app/ThemeColorPicker.svelte.d.ts +3 -1
  140. package/dist/components/scenes/app/ThemeSelector.svelte +6 -3
  141. package/dist/components/scenes/app/ThemeSelector.svelte.d.ts +3 -1
  142. package/dist/components/scenes/app/ThemeToggle.svelte +6 -2
  143. package/dist/components/scenes/app/ThemeToggle.svelte.d.ts +3 -1
  144. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte +16 -11
  145. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte.d.ts +2 -1
  146. package/dist/components/scenes/crud/CrudDetailDrawer.svelte +6 -3
  147. package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +2 -2
  148. package/dist/components/scenes/crud/CrudDetailPanel.svelte +6 -3
  149. package/dist/components/scenes/crud/CrudDetailPanel.svelte.d.ts +2 -2
  150. package/dist/components/scenes/crud/CrudEditPanel.svelte +6 -3
  151. package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +2 -2
  152. package/dist/components/scenes/crud/CrudFilterBar.svelte +6 -2
  153. package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +2 -1
  154. package/dist/components/scenes/crud/CrudPage.svelte +6 -2
  155. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +2 -1
  156. package/dist/components/scenes/crud/crud-types.d.ts +2 -2
  157. package/dist/components/scenes/crypto/EncryptedInput.svelte +6 -3
  158. package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +3 -2
  159. package/dist/components/scenes/crypto/HashDisplay.svelte +6 -3
  160. package/dist/components/scenes/crypto/HashDisplay.svelte.d.ts +3 -1
  161. package/dist/components/scenes/crypto/SignatureDisplay.svelte +6 -3
  162. package/dist/components/scenes/crypto/SignatureDisplay.svelte.d.ts +3 -1
  163. package/dist/components/scenes/error/ErrorPage.svelte +6 -3
  164. package/dist/components/scenes/error/ErrorPage.svelte.d.ts +2 -1
  165. package/dist/components/scenes/iam/AuthShell.svelte +6 -3
  166. package/dist/components/scenes/iam/AuthShell.svelte.d.ts +2 -1
  167. package/dist/components/scenes/iam/ChangePasswordForm.svelte +6 -3
  168. package/dist/components/scenes/iam/ChangePasswordForm.svelte.d.ts +3 -1
  169. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +6 -3
  170. package/dist/components/scenes/iam/ForgotPasswordForm.svelte.d.ts +3 -1
  171. package/dist/components/scenes/iam/LoginForm.svelte +6 -3
  172. package/dist/components/scenes/iam/LoginForm.svelte.d.ts +3 -1
  173. package/dist/components/scenes/iam/PasswordInput.svelte +6 -4
  174. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +3 -2
  175. package/dist/components/scenes/iam/RegisterForm.svelte +6 -4
  176. package/dist/components/scenes/iam/RegisterForm.svelte.d.ts +3 -1
  177. package/dist/components/scenes/iam/ResetPasswordForm.svelte +6 -3
  178. package/dist/components/scenes/iam/ResetPasswordForm.svelte.d.ts +3 -1
  179. package/dist/components/scenes/iam/UserProfile.svelte +6 -3
  180. package/dist/components/scenes/iam/UserProfile.svelte.d.ts +3 -1
  181. package/dist/components/scenes/storage/AvatarUpload.svelte +11 -8
  182. package/dist/components/scenes/storage/AvatarUpload.svelte.d.ts +3 -1
  183. package/dist/components/scenes/storage/FileList.svelte +6 -3
  184. package/dist/components/scenes/storage/FileList.svelte.d.ts +3 -1
  185. package/dist/components/scenes/storage/FileUpload.svelte +6 -3
  186. package/dist/components/scenes/storage/FileUpload.svelte.d.ts +3 -1
  187. package/dist/components/scenes/storage/ImageUpload.svelte +12 -9
  188. package/dist/components/scenes/storage/ImageUpload.svelte.d.ts +3 -1
  189. package/dist/components/scenes/types.d.ts +20 -16
  190. package/dist/styles/global.css +5 -0
  191. package/dist/types.d.ts +48 -41
  192. package/dist/utils.d.ts +6 -1
  193. package/dist/utils.js +16 -1
  194. package/package.json +2 -2
@@ -8,21 +8,14 @@
8
8
  import { Marked } from 'marked';
9
9
  import { createEditorMarkdownExtensions } from './editor-markdown-extensions.js';
10
10
  import { highlightCode, isLanguageSupported } from './highlight.js';
11
- const SAFE_LINK_HREF_REGEX = /^(?:https?:\/\/|\/|#|mailto:)/i;
12
- const SAFE_IMAGE_SRC_REGEX = /^(?:https?:\/\/|\/|data:image\/)/i;
13
- /**
14
- * 对 HTML 特殊字符进行转义
15
- *
16
- * @param text - 原始文本
17
- * @returns 转义后的安全文本
18
- */
19
- function escapeHtml(text) {
20
- return text
21
- .replace(/&/g, '&')
22
- .replace(/</g, '&lt;')
23
- .replace(/>/g, '&gt;')
24
- .replace(/"/g, '&quot;')
25
- .replace(/'/g, '&#39;');
11
+ import { escapeHtml, isExternalLinkHref, sanitizeImageSrc, sanitizeLinkHref, sanitizeMarkdownHtml, } from './safe-html.js';
12
+ function readHtmlTokenSource(token) {
13
+ if ('raw' in token && typeof token.raw === 'string') {
14
+ return token.raw;
15
+ }
16
+ return 'text' in token && typeof token.text === 'string'
17
+ ? token.text
18
+ : '';
26
19
  }
27
20
  function createTableCellAttrs(cell) {
28
21
  const attrs = [];
@@ -69,14 +62,17 @@ function createRendererObject(options) {
69
62
  },
70
63
  // 原始 HTML 块:转义以防止 XSS
71
64
  html(token) {
72
- return escapeHtml('text' in token ? token.text : '');
65
+ const source = readHtmlTokenSource(token);
66
+ return options.allowHtmlTags
67
+ ? sanitizeMarkdownHtml(source)
68
+ : escapeHtml(source);
73
69
  },
74
70
  // 链接:外部链接自动添加 target="_blank",阻止危险协议
75
71
  link({ href, title, tokens }) {
76
72
  const text = this.parser.parseInline(tokens);
77
73
  // 安全检查:仅允许安全协议
78
- const safeHref = href && SAFE_LINK_HREF_REGEX.test(href) ? href : '';
79
- const isExternal = safeHref && (safeHref.startsWith('http://') || safeHref.startsWith('https://'));
74
+ const safeHref = sanitizeLinkHref(href);
75
+ const isExternal = safeHref && isExternalLinkHref(safeHref);
80
76
  const attrs = [
81
77
  `href="${escapeHtml(safeHref)}"`,
82
78
  title ? `title="${escapeHtml(title)}"` : '',
@@ -87,7 +83,7 @@ function createRendererObject(options) {
87
83
  // 图片:添加 loading="lazy",验证 src 协议
88
84
  image({ href, title, text }) {
89
85
  // 安全检查:仅允许安全协议
90
- const safeSrc = href && SAFE_IMAGE_SRC_REGEX.test(href) ? href : '';
86
+ const safeSrc = sanitizeImageSrc(href);
91
87
  const attrs = [
92
88
  `src="${escapeHtml(safeSrc)}"`,
93
89
  `alt="${escapeHtml(text || '')}"`,
@@ -136,6 +132,7 @@ const DEFAULT_OPTIONS = {
136
132
  enableHighlight: true,
137
133
  showCopyButton: true,
138
134
  breaks: true,
135
+ allowHtmlTags: false,
139
136
  };
140
137
  /** 缓存 Marked 实例,避免重复创建 */
141
138
  let cachedInstance = null;
@@ -144,7 +141,7 @@ let cachedOptionsKey = '';
144
141
  * 获取配置签名,用于实例缓存比较
145
142
  */
146
143
  function getOptionsKey(options) {
147
- return `${options.enableHighlight}-${options.showCopyButton}-${options.breaks}`;
144
+ return `${options.enableHighlight}-${options.showCopyButton}-${options.breaks}-${options.allowHtmlTags}`;
148
145
  }
149
146
  /**
150
147
  * 将 Markdown 文本解析为 HTML
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @h-ai/ui — Safe HTML helpers for Markdown rendering.
3
+ *
4
+ * The Markdown pipeline keeps raw HTML escaped by default.
5
+ * When callers opt in, only a controlled allowlist of safe tags and
6
+ * attributes is reconstructed; unsupported or dangerous markup remains escaped.
7
+ */
8
+ export declare function escapeHtml(text: string): string;
9
+ export declare function sanitizeLinkHref(href: string | null | undefined): string;
10
+ export declare function isExternalLinkHref(href: string): boolean;
11
+ export declare function sanitizeImageSrc(src: string | null | undefined): string;
12
+ /**
13
+ * Sanitize raw HTML fragments embedded inside Markdown.
14
+ * Safe allowlisted tags are reconstructed, everything else remains escaped.
15
+ */
16
+ export declare function sanitizeMarkdownHtml(fragment: string): string;
@@ -0,0 +1,284 @@
1
+ /**
2
+ * @h-ai/ui — Safe HTML helpers for Markdown rendering.
3
+ *
4
+ * The Markdown pipeline keeps raw HTML escaped by default.
5
+ * When callers opt in, only a controlled allowlist of safe tags and
6
+ * attributes is reconstructed; unsupported or dangerous markup remains escaped.
7
+ */
8
+ const SAFE_LINK_HREF_REGEX = /^(?:https?:\/\/|\/|#|mailto:)/i;
9
+ const SAFE_IMAGE_SRC_REGEX = /^(?:https?:\/\/|\/|data:image\/)/i;
10
+ const SAFE_ALIGN_VALUE_REGEX = /^(?:left|center|right|justify)$/i;
11
+ const SAFE_TABLE_SPAN_REGEX = /^[1-9]\d{0,2}$/;
12
+ const HTML_ATTR_REGEX = /([^\s"'=<>`/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
13
+ const ALLOWED_HTML_TAGS = new Set([
14
+ 'a',
15
+ 'b',
16
+ 'blockquote',
17
+ 'br',
18
+ 'code',
19
+ 'del',
20
+ 'div',
21
+ 'em',
22
+ 'h1',
23
+ 'h2',
24
+ 'h3',
25
+ 'h4',
26
+ 'h5',
27
+ 'h6',
28
+ 'hr',
29
+ 'i',
30
+ 'img',
31
+ 'kbd',
32
+ 'li',
33
+ 'mark',
34
+ 'ol',
35
+ 'p',
36
+ 'pre',
37
+ 's',
38
+ 'span',
39
+ 'strong',
40
+ 'sub',
41
+ 'sup',
42
+ 'table',
43
+ 'tbody',
44
+ 'td',
45
+ 'th',
46
+ 'thead',
47
+ 'tr',
48
+ 'u',
49
+ 'ul',
50
+ ]);
51
+ const VOID_HTML_TAGS = new Set(['br', 'hr', 'img']);
52
+ const ALIGNABLE_HTML_TAGS = new Set([
53
+ 'blockquote',
54
+ 'div',
55
+ 'h1',
56
+ 'h2',
57
+ 'h3',
58
+ 'h4',
59
+ 'h5',
60
+ 'h6',
61
+ 'li',
62
+ 'p',
63
+ 'td',
64
+ 'th',
65
+ ]);
66
+ export function escapeHtml(text) {
67
+ return text
68
+ .replace(/&/g, '&amp;')
69
+ .replace(/</g, '&lt;')
70
+ .replace(/>/g, '&gt;')
71
+ .replace(/"/g, '&quot;')
72
+ .replace(/'/g, '&#39;');
73
+ }
74
+ export function sanitizeLinkHref(href) {
75
+ const normalized = href?.trim() ?? '';
76
+ return SAFE_LINK_HREF_REGEX.test(normalized) ? normalized : '';
77
+ }
78
+ export function isExternalLinkHref(href) {
79
+ return /^https?:\/\//i.test(href);
80
+ }
81
+ export function sanitizeImageSrc(src) {
82
+ const normalized = src?.trim() ?? '';
83
+ return SAFE_IMAGE_SRC_REGEX.test(normalized) ? normalized : '';
84
+ }
85
+ function sanitizeAlignValue(value) {
86
+ const normalized = value?.trim().toLowerCase();
87
+ return normalized && SAFE_ALIGN_VALUE_REGEX.test(normalized)
88
+ ? normalized
89
+ : undefined;
90
+ }
91
+ function parseHtmlAttributes(rawAttributes) {
92
+ const attributes = [];
93
+ for (const match of rawAttributes.matchAll(HTML_ATTR_REGEX)) {
94
+ const [, rawName, quotedDouble, quotedSingle, unquoted] = match;
95
+ const attrName = rawName?.trim().toLowerCase();
96
+ if (!attrName) {
97
+ continue;
98
+ }
99
+ const attrValue = quotedDouble ?? quotedSingle ?? unquoted ?? '';
100
+ attributes.push([attrName, attrValue]);
101
+ }
102
+ return attributes;
103
+ }
104
+ function isHtmlTagNameChar(char) {
105
+ return typeof char === 'string' && /^[\w:-]$/.test(char);
106
+ }
107
+ function skipWhitespace(source, startIndex) {
108
+ let cursor = startIndex;
109
+ while (cursor < source.length && /\s/.test(source[cursor] ?? '')) {
110
+ cursor += 1;
111
+ }
112
+ return cursor;
113
+ }
114
+ function findHtmlTagEnd(source, startIndex) {
115
+ let quote = null;
116
+ for (let cursor = startIndex; cursor < source.length; cursor += 1) {
117
+ const char = source[cursor];
118
+ if (quote) {
119
+ if (char === quote) {
120
+ quote = null;
121
+ }
122
+ continue;
123
+ }
124
+ if (char === '"' || char === '\'') {
125
+ quote = char;
126
+ continue;
127
+ }
128
+ if (char === '>') {
129
+ return cursor;
130
+ }
131
+ }
132
+ return -1;
133
+ }
134
+ function readNextHtmlTag(source, startIndex) {
135
+ let cursor = source.indexOf('<', startIndex);
136
+ while (cursor >= 0) {
137
+ const nextChar = source[cursor + 1];
138
+ if (!nextChar || (nextChar !== '/' && !/[a-z]/i.test(nextChar))) {
139
+ cursor = source.indexOf('<', cursor + 1);
140
+ continue;
141
+ }
142
+ const tagEnd = findHtmlTagEnd(source, cursor + 1);
143
+ if (tagEnd < 0) {
144
+ return null;
145
+ }
146
+ return {
147
+ rawTag: source.slice(cursor, tagEnd + 1),
148
+ start: cursor,
149
+ end: tagEnd + 1,
150
+ };
151
+ }
152
+ return null;
153
+ }
154
+ function readSanitizedAttributes(tagName, rawAttributes) {
155
+ const sanitized = {};
156
+ for (const [attrName, attrValue] of parseHtmlAttributes(rawAttributes)) {
157
+ if (tagName === 'a') {
158
+ if (attrName === 'href') {
159
+ sanitized.href = sanitizeLinkHref(attrValue);
160
+ }
161
+ if (attrName === 'title' && attrValue.trim()) {
162
+ sanitized.title = attrValue.trim();
163
+ }
164
+ continue;
165
+ }
166
+ if (tagName === 'img') {
167
+ if (attrName === 'src') {
168
+ sanitized.src = sanitizeImageSrc(attrValue);
169
+ }
170
+ if (attrName === 'alt') {
171
+ sanitized.alt = attrValue;
172
+ }
173
+ if (attrName === 'title' && attrValue.trim()) {
174
+ sanitized.title = attrValue.trim();
175
+ }
176
+ continue;
177
+ }
178
+ if (ALIGNABLE_HTML_TAGS.has(tagName) && attrName === 'align') {
179
+ sanitized.align = sanitizeAlignValue(attrValue);
180
+ continue;
181
+ }
182
+ if ((tagName === 'td' || tagName === 'th') && attrName === 'colspan') {
183
+ const normalized = attrValue.trim();
184
+ if (SAFE_TABLE_SPAN_REGEX.test(normalized)) {
185
+ sanitized.colspan = normalized;
186
+ }
187
+ continue;
188
+ }
189
+ if ((tagName === 'td' || tagName === 'th') && attrName === 'rowspan') {
190
+ const normalized = attrValue.trim();
191
+ if (SAFE_TABLE_SPAN_REGEX.test(normalized)) {
192
+ sanitized.rowspan = normalized;
193
+ }
194
+ }
195
+ }
196
+ return sanitized;
197
+ }
198
+ function sanitizeHtmlTag(rawTag) {
199
+ if (!rawTag.startsWith('<') || !rawTag.endsWith('>')) {
200
+ return escapeHtml(rawTag);
201
+ }
202
+ let cursor = 1;
203
+ cursor = skipWhitespace(rawTag, cursor);
204
+ let isClosingTag = false;
205
+ if (rawTag[cursor] === '/') {
206
+ isClosingTag = true;
207
+ cursor += 1;
208
+ cursor = skipWhitespace(rawTag, cursor);
209
+ }
210
+ const tagNameStart = cursor;
211
+ while (isHtmlTagNameChar(rawTag[cursor])) {
212
+ cursor += 1;
213
+ }
214
+ const tagName = rawTag.slice(tagNameStart, cursor).toLowerCase();
215
+ if (!tagName) {
216
+ return escapeHtml(rawTag);
217
+ }
218
+ let rawAttributes = rawTag.slice(cursor, -1);
219
+ let selfClosing = false;
220
+ const trimmedAttributes = rawAttributes.trimEnd();
221
+ if (trimmedAttributes.endsWith('/')) {
222
+ selfClosing = true;
223
+ rawAttributes = trimmedAttributes.slice(0, -1);
224
+ }
225
+ if (!ALLOWED_HTML_TAGS.has(tagName)) {
226
+ return escapeHtml(rawTag);
227
+ }
228
+ if (isClosingTag) {
229
+ return VOID_HTML_TAGS.has(tagName) ? '' : `</${tagName}>`;
230
+ }
231
+ const attributes = readSanitizedAttributes(tagName, rawAttributes);
232
+ const renderedAttributes = [];
233
+ if (attributes.href !== undefined) {
234
+ renderedAttributes.push(`href="${escapeHtml(attributes.href)}"`);
235
+ if (attributes.href && isExternalLinkHref(attributes.href)) {
236
+ renderedAttributes.push('target="_blank"');
237
+ renderedAttributes.push('rel="noopener noreferrer"');
238
+ }
239
+ }
240
+ if (attributes.title) {
241
+ renderedAttributes.push(`title="${escapeHtml(attributes.title)}"`);
242
+ }
243
+ if (attributes.src !== undefined) {
244
+ renderedAttributes.push(`src="${escapeHtml(attributes.src)}"`);
245
+ renderedAttributes.push('loading="lazy"');
246
+ renderedAttributes.push('class="hai-md-img"');
247
+ }
248
+ if (attributes.alt !== undefined) {
249
+ renderedAttributes.push(`alt="${escapeHtml(attributes.alt)}"`);
250
+ }
251
+ if (attributes.align) {
252
+ renderedAttributes.push(`style="text-align:${escapeHtml(attributes.align)}"`);
253
+ }
254
+ if (attributes.colspan) {
255
+ renderedAttributes.push(`colspan="${escapeHtml(attributes.colspan)}"`);
256
+ }
257
+ if (attributes.rowspan) {
258
+ renderedAttributes.push(`rowspan="${escapeHtml(attributes.rowspan)}"`);
259
+ }
260
+ const attributeText = renderedAttributes.length > 0
261
+ ? ` ${renderedAttributes.join(' ')}`
262
+ : '';
263
+ return VOID_HTML_TAGS.has(tagName) || selfClosing
264
+ ? `<${tagName}${attributeText} />`
265
+ : `<${tagName}${attributeText}>`;
266
+ }
267
+ /**
268
+ * Sanitize raw HTML fragments embedded inside Markdown.
269
+ * Safe allowlisted tags are reconstructed, everything else remains escaped.
270
+ */
271
+ export function sanitizeMarkdownHtml(fragment) {
272
+ if (!fragment) {
273
+ return '';
274
+ }
275
+ let sanitized = '';
276
+ let lastIndex = 0;
277
+ for (let match = readNextHtmlTag(fragment, lastIndex); match; match = readNextHtmlTag(fragment, lastIndex)) {
278
+ sanitized += escapeHtml(fragment.slice(lastIndex, match.start));
279
+ sanitized += sanitizeHtmlTag(match.rawTag);
280
+ lastIndex = match.end;
281
+ }
282
+ sanitized += escapeHtml(fragment.slice(lastIndex));
283
+ return sanitized;
284
+ }
@@ -12,6 +12,7 @@
12
12
  =============================================================================
13
13
  -->
14
14
  <script lang='ts'>
15
+ import type { DataAttributes } from '../../../../types.js'
15
16
  import type { MarkdownToolbarDownloadAction } from '../document-types.js'
16
17
  import type {
17
18
  AiTableColumn,
@@ -23,7 +24,7 @@
23
24
  } from './table-types.js'
24
25
  import { SvelteSet } from 'svelte/reactivity'
25
26
  import { uiM } from '../../../../messages.js'
26
- import { cn } from '../../../../utils.js'
27
+ import { cn, getDataAttributes } from '../../../../utils.js'
27
28
  import AiDocumentDownloadMenu from '../AiDocumentDownloadMenu.svelte'
28
29
 
29
30
  interface ExtractedArrayChunk {
@@ -92,8 +93,10 @@
92
93
  oncopytable,
93
94
  // 表格下载回调。
94
95
  ondownloadtable,
95
- }: AiTableEditorProps = $props()
96
+ ...restProps
97
+ }: AiTableEditorProps & DataAttributes = $props()
96
98
 
99
+ const dataAttributes = $derived(getDataAttributes(restProps))
97
100
  // 复制反馈只影响右上角复制按钮,不和业务数据混用。
98
101
  let copied = $state(false)
99
102
  // 连续复制时按最后一次点击重新计时。
@@ -1029,7 +1032,7 @@
1029
1032
  }
1030
1033
  </script>
1031
1034
 
1032
- <section class={tablePaneClass}>
1035
+ <section {...dataAttributes} class={tablePaneClass}>
1033
1036
  <header class='hai-ai-table-header'>
1034
1037
  <div class='hai-ai-table-heading'>
1035
1038
  <div class='hai-ai-table-eyebrow'>
@@ -1,5 +1,7 @@
1
+ import type { DataAttributes } from '../../../../types.js';
1
2
  import type { AiTableEditorProps } from './table-types.js';
2
- declare const AiTableEditor: import("svelte").Component<AiTableEditorProps, {
3
+ type $$ComponentProps = AiTableEditorProps & DataAttributes;
4
+ declare const AiTableEditor: import("svelte").Component<$$ComponentProps, {
3
5
  flushPendingTableChange: () => void;
4
6
  }, "editorScrollHost">;
5
7
  type AiTableEditor = ReturnType<typeof AiTableEditor>;
@@ -1,3 +1,4 @@
1
+ import type { DataAttributes } from '../../../../types.js';
1
2
  /**
2
3
  * 表格列支持的数据类型。
3
4
  * - `text`: 普通文本输入
@@ -67,7 +68,7 @@ export interface AiTableDownloadPayload {
67
68
  /** 推荐文件名(已带 `.csv` 后缀)。 */
68
69
  filename: string;
69
70
  }
70
- export interface AiTableEditorProps {
71
+ export interface AiTableEditorProps extends DataAttributes {
71
72
  /** 原始表格内容字符串,通常是 JSON 文本,支持流式残缺片段。 */
72
73
  content?: string;
73
74
  /** 结构化表格数据;有值时优先于 `content`。 */
@@ -47,7 +47,9 @@
47
47
  }
48
48
  }
49
49
 
50
+ import type { DataAttributes } from '../../../types.js'
50
51
  import { uiM } from '../../../messages.js'
52
+ import { getDataAttributes } from '../../../utils.js'
51
53
 
52
54
  interface Props {
53
55
  open?: boolean
@@ -55,8 +57,9 @@
55
57
  onsubmit?: (data: FeedbackData) => Promise<void>
56
58
  }
57
59
 
58
- let { open = $bindable(false), labels = {}, onsubmit }: Props = $props()
60
+ let { open = $bindable(false), labels = {}, onsubmit, ...restProps }: Props & DataAttributes = $props()
59
61
 
62
+ const dataAttributes = $derived(getDataAttributes(restProps))
60
63
  // 文案优先使用传入的 labels,缺省回退到内置消息 uiM(...)
61
64
 
62
65
  let feedbackType = $state<FeedbackType>('bug')
@@ -106,7 +109,7 @@
106
109
  }
107
110
  </script>
108
111
 
109
- <Modal bind:open title={labels.title ?? uiM('feedback_title')}>
112
+ <Modal {...dataAttributes} bind:open title={labels.title ?? uiM('feedback_title')}>
110
113
  <p class='text-base-content/70 mb-4'>{labels.description ?? uiM('feedback_description')}</p>
111
114
 
112
115
  {#if error}
@@ -1,3 +1,4 @@
1
+ import type { DataAttributes } from '../../../types.js';
1
2
  type FeedbackType = 'bug' | 'feature' | 'question' | 'other';
2
3
  interface FeedbackData {
3
4
  type: FeedbackType;
@@ -29,6 +30,7 @@ interface Props {
29
30
  labels?: FeedbackLabels;
30
31
  onsubmit?: (data: FeedbackData) => Promise<void>;
31
32
  }
33
+ type $$ComponentProps = Props & DataAttributes;
32
34
  /**
33
35
  * FeedbackModal
34
36
  * 用户反馈模态框,支持 Bug 报告、功能建议等多种反馈类型。
@@ -42,6 +44,6 @@ interface Props {
42
44
  * onsubmit={async (data) => { await submitFeedback(data) }}
43
45
  * />
44
46
  */
45
- declare const FeedbackModal: import("svelte").Component<Props, {}, "open">;
47
+ declare const FeedbackModal: import("svelte").Component<$$ComponentProps, {}, "open">;
46
48
  type FeedbackModal = ReturnType<typeof FeedbackModal>;
47
49
  export default FeedbackModal;
@@ -29,7 +29,9 @@
29
29
  ```
30
30
  -->
31
31
  <script lang='ts'>
32
+ import type { DataAttributes } from '../../../types.js'
32
33
  import { uiM } from '../../../messages.js'
34
+ import { getDataAttributes } from '../../../utils.js'
33
35
  import BareButton from '../../primitives/BareButton.svelte'
34
36
 
35
37
  interface Language {
@@ -63,8 +65,10 @@
63
65
  align = 'end',
64
66
  compact = false,
65
67
  class: className = '',
66
- }: Props = $props()
68
+ ...restProps
69
+ }: Props & DataAttributes = $props()
67
70
 
71
+ const dataAttributes = $derived(getDataAttributes(restProps))
68
72
  let open = $state(false)
69
73
  let containerRef = $state<HTMLDivElement | null>(null)
70
74
 
@@ -94,7 +98,7 @@
94
98
  })
95
99
  </script>
96
100
 
97
- <div bind:this={containerRef} class='dropdown {align === 'end' ? 'dropdown-end' : ''} {open ? 'dropdown-open' : ''} {className}'>
101
+ <div {...dataAttributes} bind:this={containerRef} class='dropdown {align === 'end' ? 'dropdown-end' : ''} {open ? 'dropdown-open' : ''} {className}'>
98
102
  <BareButton
99
103
  type='button'
100
104
  class='btn btn-ghost {compact ? 'btn-sm btn-square' : 'btn-sm gap-2 min-w-fit'}'
@@ -1,3 +1,4 @@
1
+ import type { DataAttributes } from '../../../types.js';
1
2
  interface Language {
2
3
  value: string;
3
4
  label: string;
@@ -17,6 +18,7 @@ interface Props {
17
18
  /** 自定义类名 */
18
19
  class?: string;
19
20
  }
21
+ type $$ComponentProps = Props & DataAttributes;
20
22
  /**
21
23
  * LanguageSwitch
22
24
  * 语言切换下拉组件,支持多语言切换。
@@ -47,6 +49,6 @@ interface Props {
47
49
  * />
48
50
  * ```
49
51
  */
50
- declare const LanguageSwitch: import("svelte").Component<Props, {}, "">;
52
+ declare const LanguageSwitch: import("svelte").Component<$$ComponentProps, {}, "">;
51
53
  type LanguageSwitch = ReturnType<typeof LanguageSwitch>;
52
54
  export default LanguageSwitch;
@@ -20,7 +20,8 @@
20
20
  -->
21
21
  <script lang='ts'>
22
22
  import type { Snippet } from 'svelte'
23
- import { cn } from '../../../utils.js'
23
+ import type { DataAttributes } from '../../../types.js'
24
+ import { cn, getDataAttributes } from '../../../utils.js'
24
25
  import BareButton from '../../primitives/BareButton.svelte'
25
26
 
26
27
  type SettingsSection = {
@@ -40,6 +41,7 @@
40
41
  children,
41
42
  headerActions,
42
43
  class: className = '',
44
+ ...restProps
43
45
  }: {
44
46
  title?: string
45
47
  description?: string
@@ -49,8 +51,9 @@
49
51
  children?: Snippet
50
52
  headerActions?: Snippet
51
53
  class?: string
52
- } = $props()
54
+ } & DataAttributes = $props()
53
55
 
56
+ const dataAttributes = $derived(getDataAttributes(restProps))
54
57
  function navItemClass(id: string): string {
55
58
  return cn(
56
59
  'flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
@@ -61,7 +64,7 @@
61
64
  }
62
65
  </script>
63
66
 
64
- <div class={cn('space-y-5', className)}>
67
+ <div {...dataAttributes} class={cn('space-y-5', className)}>
65
68
  <!-- 标题区 -->
66
69
  {#if title || description || headerActions}
67
70
  <div class='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { DataAttributes } from '../../../types.js';
2
3
  type SettingsSection = {
3
4
  id: string;
4
5
  label: string;
@@ -15,7 +16,7 @@ type $$ComponentProps = {
15
16
  children?: Snippet;
16
17
  headerActions?: Snippet;
17
18
  class?: string;
18
- };
19
+ } & DataAttributes;
19
20
  /**
20
21
  * SettingsLayout
21
22
  * 设置页布局场景组件
@@ -10,7 +10,9 @@
10
10
  @prop {function} onthemechange - 主题变更回调
11
11
  -->
12
12
  <script lang='ts'>
13
+ import type { DataAttributes } from '../../../types.js'
13
14
  import { uiM } from '../../../messages.js'
15
+ import { getDataAttributes } from '../../../utils.js'
14
16
  import BareButton from '../../primitives/BareButton.svelte'
15
17
  import IconButton from '../../primitives/IconButton.svelte'
16
18
 
@@ -43,8 +45,10 @@
43
45
  onlanguagechange,
44
46
  onthemechange,
45
47
  onclose,
46
- }: Props = $props()
48
+ ...restProps
49
+ }: Props & DataAttributes = $props()
47
50
 
51
+ const dataAttributes = $derived(getDataAttributes(restProps))
48
52
  function handleClose() {
49
53
  open = false
50
54
  onclose?.()
@@ -66,9 +70,8 @@
66
70
  <svelte:window onkeydown={handleKeydown} />
67
71
 
68
72
  {#if open}
69
- <!-- svelte-ignore a11y_click_events_have_key_events -->
70
- <!-- svelte-ignore a11y_no_static_element_interactions -->
71
73
  <div
74
+ {...dataAttributes}
72
75
  class='fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm transition-opacity'
73
76
  onclick={handleBackdropClick}
74
77
  >
@@ -1,3 +1,4 @@
1
+ import type { DataAttributes } from '../../../types.js';
1
2
  interface Props {
2
3
  open?: boolean;
3
4
  currentLanguage?: string;
@@ -15,6 +16,7 @@ interface Props {
15
16
  onthemechange?: (theme: string) => void;
16
17
  onclose?: () => void;
17
18
  }
19
+ type $$ComponentProps = Props & DataAttributes;
18
20
  /**
19
21
  * SettingsModal
20
22
  * 应用设置模态框,包含语言和主题切换功能。
@@ -26,6 +28,6 @@ interface Props {
26
28
  * @prop {function} onlanguagechange - 语言变更回调
27
29
  * @prop {function} onthemechange - 主题变更回调
28
30
  */
29
- declare const SettingsModal: import("svelte").Component<Props, {}, "open">;
31
+ declare const SettingsModal: import("svelte").Component<$$ComponentProps, {}, "open">;
30
32
  type SettingsModal = ReturnType<typeof SettingsModal>;
31
33
  export default SettingsModal;