@h-ai/ui 0.1.0-alpha5

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 (234) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +488 -0
  3. package/auto-import.d.ts +96 -0
  4. package/auto-import.js +259 -0
  5. package/dist/components/compounds/Accordion.svelte +142 -0
  6. package/dist/components/compounds/Accordion.svelte.d.ts +25 -0
  7. package/dist/components/compounds/ActionSheet.svelte +116 -0
  8. package/dist/components/compounds/ActionSheet.svelte.d.ts +22 -0
  9. package/dist/components/compounds/Alert.svelte +67 -0
  10. package/dist/components/compounds/Alert.svelte.d.ts +8 -0
  11. package/dist/components/compounds/AppBar.svelte +77 -0
  12. package/dist/components/compounds/AppBar.svelte.d.ts +20 -0
  13. package/dist/components/compounds/BottomNav.svelte +75 -0
  14. package/dist/components/compounds/BottomNav.svelte.d.ts +18 -0
  15. package/dist/components/compounds/Breadcrumb.svelte +49 -0
  16. package/dist/components/compounds/Breadcrumb.svelte.d.ts +4 -0
  17. package/dist/components/compounds/Calendar.svelte +116 -0
  18. package/dist/components/compounds/Calendar.svelte.d.ts +47 -0
  19. package/dist/components/compounds/Card.svelte +77 -0
  20. package/dist/components/compounds/Card.svelte.d.ts +8 -0
  21. package/dist/components/compounds/Combobox.svelte +327 -0
  22. package/dist/components/compounds/Combobox.svelte.d.ts +69 -0
  23. package/dist/components/compounds/Confirm.svelte +90 -0
  24. package/dist/components/compounds/Confirm.svelte.d.ts +4 -0
  25. package/dist/components/compounds/DataTable.svelte +133 -0
  26. package/dist/components/compounds/DataTable.svelte.d.ts +69 -0
  27. package/dist/components/compounds/DatePicker.svelte +135 -0
  28. package/dist/components/compounds/DatePicker.svelte.d.ts +46 -0
  29. package/dist/components/compounds/Drawer.svelte +113 -0
  30. package/dist/components/compounds/Drawer.svelte.d.ts +4 -0
  31. package/dist/components/compounds/Dropdown.svelte +91 -0
  32. package/dist/components/compounds/Dropdown.svelte.d.ts +8 -0
  33. package/dist/components/compounds/Empty.svelte +83 -0
  34. package/dist/components/compounds/Empty.svelte.d.ts +4 -0
  35. package/dist/components/compounds/Form.svelte +96 -0
  36. package/dist/components/compounds/Form.svelte.d.ts +7 -0
  37. package/dist/components/compounds/FormField.svelte +56 -0
  38. package/dist/components/compounds/FormField.svelte.d.ts +4 -0
  39. package/dist/components/compounds/InfiniteScroll.svelte +82 -0
  40. package/dist/components/compounds/InfiniteScroll.svelte.d.ts +22 -0
  41. package/dist/components/compounds/Modal.svelte +123 -0
  42. package/dist/components/compounds/Modal.svelte.d.ts +8 -0
  43. package/dist/components/compounds/PageHeader.svelte +44 -0
  44. package/dist/components/compounds/PageHeader.svelte.d.ts +28 -0
  45. package/dist/components/compounds/Pagination.svelte +157 -0
  46. package/dist/components/compounds/Pagination.svelte.d.ts +4 -0
  47. package/dist/components/compounds/Popover.svelte +137 -0
  48. package/dist/components/compounds/Popover.svelte.d.ts +4 -0
  49. package/dist/components/compounds/PullRefresh.svelte +98 -0
  50. package/dist/components/compounds/PullRefresh.svelte.d.ts +16 -0
  51. package/dist/components/compounds/Result.svelte +84 -0
  52. package/dist/components/compounds/Result.svelte.d.ts +4 -0
  53. package/dist/components/compounds/SafeArea.svelte +37 -0
  54. package/dist/components/compounds/SafeArea.svelte.d.ts +12 -0
  55. package/dist/components/compounds/Skeleton.svelte +75 -0
  56. package/dist/components/compounds/Skeleton.svelte.d.ts +8 -0
  57. package/dist/components/compounds/Steps.svelte +111 -0
  58. package/dist/components/compounds/Steps.svelte.d.ts +4 -0
  59. package/dist/components/compounds/SwipeCell.svelte +115 -0
  60. package/dist/components/compounds/SwipeCell.svelte.d.ts +19 -0
  61. package/dist/components/compounds/Tabs.svelte +79 -0
  62. package/dist/components/compounds/Tabs.svelte.d.ts +8 -0
  63. package/dist/components/compounds/TagInput.svelte +108 -0
  64. package/dist/components/compounds/TagInput.svelte.d.ts +4 -0
  65. package/dist/components/compounds/Timeline.svelte +167 -0
  66. package/dist/components/compounds/Timeline.svelte.d.ts +25 -0
  67. package/dist/components/compounds/ToastContainer.svelte +59 -0
  68. package/dist/components/compounds/ToastContainer.svelte.d.ts +3 -0
  69. package/dist/components/compounds/Tooltip.svelte +41 -0
  70. package/dist/components/compounds/Tooltip.svelte.d.ts +4 -0
  71. package/dist/components/compounds/accordion-types.d.ts +16 -0
  72. package/dist/components/compounds/accordion-types.js +5 -0
  73. package/dist/components/compounds/action-sheet-types.d.ts +14 -0
  74. package/dist/components/compounds/action-sheet-types.js +5 -0
  75. package/dist/components/compounds/bottom-nav-types.d.ts +17 -0
  76. package/dist/components/compounds/bottom-nav-types.js +5 -0
  77. package/dist/components/compounds/index.d.ts +44 -0
  78. package/dist/components/compounds/index.js +54 -0
  79. package/dist/components/compounds/swipe-cell-types.d.ts +14 -0
  80. package/dist/components/compounds/swipe-cell-types.js +5 -0
  81. package/dist/components/compounds/timeline-types.d.ts +20 -0
  82. package/dist/components/compounds/timeline-types.js +5 -0
  83. package/dist/components/index.d.ts +9 -0
  84. package/dist/components/index.js +12 -0
  85. package/dist/components/primitives/Avatar.svelte +102 -0
  86. package/dist/components/primitives/Avatar.svelte.d.ts +8 -0
  87. package/dist/components/primitives/Badge.svelte +33 -0
  88. package/dist/components/primitives/Badge.svelte.d.ts +8 -0
  89. package/dist/components/primitives/BareButton.svelte +45 -0
  90. package/dist/components/primitives/BareButton.svelte.d.ts +4 -0
  91. package/dist/components/primitives/BareInput.svelte +101 -0
  92. package/dist/components/primitives/BareInput.svelte.d.ts +4 -0
  93. package/dist/components/primitives/Button.svelte +60 -0
  94. package/dist/components/primitives/Button.svelte.d.ts +8 -0
  95. package/dist/components/primitives/Checkbox.svelte +58 -0
  96. package/dist/components/primitives/Checkbox.svelte.d.ts +4 -0
  97. package/dist/components/primitives/IconButton.svelte +105 -0
  98. package/dist/components/primitives/IconButton.svelte.d.ts +4 -0
  99. package/dist/components/primitives/Input.svelte +130 -0
  100. package/dist/components/primitives/Input.svelte.d.ts +8 -0
  101. package/dist/components/primitives/Progress.svelte +58 -0
  102. package/dist/components/primitives/Progress.svelte.d.ts +4 -0
  103. package/dist/components/primitives/Radio.svelte +60 -0
  104. package/dist/components/primitives/Radio.svelte.d.ts +25 -0
  105. package/dist/components/primitives/Range.svelte +117 -0
  106. package/dist/components/primitives/Range.svelte.d.ts +27 -0
  107. package/dist/components/primitives/Rating.svelte +148 -0
  108. package/dist/components/primitives/Rating.svelte.d.ts +27 -0
  109. package/dist/components/primitives/Select.svelte +95 -0
  110. package/dist/components/primitives/Select.svelte.d.ts +25 -0
  111. package/dist/components/primitives/Spinner.svelte +54 -0
  112. package/dist/components/primitives/Spinner.svelte.d.ts +4 -0
  113. package/dist/components/primitives/Switch.svelte +53 -0
  114. package/dist/components/primitives/Switch.svelte.d.ts +4 -0
  115. package/dist/components/primitives/Tag.svelte +64 -0
  116. package/dist/components/primitives/Tag.svelte.d.ts +4 -0
  117. package/dist/components/primitives/Textarea.svelte +92 -0
  118. package/dist/components/primitives/Textarea.svelte.d.ts +4 -0
  119. package/dist/components/primitives/ToggleCheckbox.svelte +36 -0
  120. package/dist/components/primitives/ToggleCheckbox.svelte.d.ts +4 -0
  121. package/dist/components/primitives/ToggleInput.svelte +36 -0
  122. package/dist/components/primitives/ToggleInput.svelte.d.ts +4 -0
  123. package/dist/components/primitives/ToggleRadio.svelte +36 -0
  124. package/dist/components/primitives/ToggleRadio.svelte.d.ts +4 -0
  125. package/dist/components/primitives/index.d.ts +26 -0
  126. package/dist/components/primitives/index.js +30 -0
  127. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +387 -0
  128. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte.d.ts +38 -0
  129. package/dist/components/scenes/ai/AiDocumentEditor.svelte +2259 -0
  130. package/dist/components/scenes/ai/AiDocumentEditor.svelte.d.ts +4 -0
  131. package/dist/components/scenes/ai/MarkdownRenderer.svelte +460 -0
  132. package/dist/components/scenes/ai/MarkdownRenderer.svelte.d.ts +4 -0
  133. package/dist/components/scenes/ai/document-download.d.ts +40 -0
  134. package/dist/components/scenes/ai/document-download.js +300 -0
  135. package/dist/components/scenes/ai/document-parse.d.ts +28 -0
  136. package/dist/components/scenes/ai/document-parse.js +243 -0
  137. package/dist/components/scenes/ai/document-types.d.ts +164 -0
  138. package/dist/components/scenes/ai/document-types.js +1 -0
  139. package/dist/components/scenes/ai/index.d.ts +17 -0
  140. package/dist/components/scenes/ai/index.js +13 -0
  141. package/dist/components/scenes/ai/markdown-parse.d.ts +32 -0
  142. package/dist/components/scenes/ai/markdown-parse.js +161 -0
  143. package/dist/components/scenes/app/FeedbackModal.svelte +160 -0
  144. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +47 -0
  145. package/dist/components/scenes/app/LanguageSwitch.svelte +136 -0
  146. package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +50 -0
  147. package/dist/components/scenes/app/SettingsModal.svelte +174 -0
  148. package/dist/components/scenes/app/SettingsModal.svelte.d.ts +31 -0
  149. package/dist/components/scenes/app/ThemeSelector.svelte +176 -0
  150. package/dist/components/scenes/app/ThemeSelector.svelte.d.ts +31 -0
  151. package/dist/components/scenes/app/ThemeToggle.svelte +76 -0
  152. package/dist/components/scenes/app/ThemeToggle.svelte.d.ts +29 -0
  153. package/dist/components/scenes/app/index.d.ts +11 -0
  154. package/dist/components/scenes/app/index.js +11 -0
  155. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte +39 -0
  156. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte.d.ts +18 -0
  157. package/dist/components/scenes/crud/CrudDetailDrawer.svelte +155 -0
  158. package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +38 -0
  159. package/dist/components/scenes/crud/CrudEditDrawer.svelte +222 -0
  160. package/dist/components/scenes/crud/CrudEditDrawer.svelte.d.ts +48 -0
  161. package/dist/components/scenes/crud/CrudFilterBar.svelte +96 -0
  162. package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +31 -0
  163. package/dist/components/scenes/crud/CrudPage.svelte +499 -0
  164. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +114 -0
  165. package/dist/components/scenes/crud/crud-types.d.ts +135 -0
  166. package/dist/components/scenes/crud/crud-types.js +7 -0
  167. package/dist/components/scenes/crud/index.d.ts +12 -0
  168. package/dist/components/scenes/crud/index.js +11 -0
  169. package/dist/components/scenes/crypto/EncryptedInput.svelte +156 -0
  170. package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +32 -0
  171. package/dist/components/scenes/crypto/HashDisplay.svelte +97 -0
  172. package/dist/components/scenes/crypto/HashDisplay.svelte.d.ts +4 -0
  173. package/dist/components/scenes/crypto/SignatureDisplay.svelte +161 -0
  174. package/dist/components/scenes/crypto/SignatureDisplay.svelte.d.ts +4 -0
  175. package/dist/components/scenes/crypto/index.d.ts +9 -0
  176. package/dist/components/scenes/crypto/index.js +11 -0
  177. package/dist/components/scenes/iam/ChangePasswordForm.svelte +134 -0
  178. package/dist/components/scenes/iam/ChangePasswordForm.svelte.d.ts +8 -0
  179. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +147 -0
  180. package/dist/components/scenes/iam/ForgotPasswordForm.svelte.d.ts +4 -0
  181. package/dist/components/scenes/iam/LoginForm.svelte +191 -0
  182. package/dist/components/scenes/iam/LoginForm.svelte.d.ts +8 -0
  183. package/dist/components/scenes/iam/PasswordInput.svelte +203 -0
  184. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +49 -0
  185. package/dist/components/scenes/iam/PermGuard.svelte +69 -0
  186. package/dist/components/scenes/iam/PermGuard.svelte.d.ts +16 -0
  187. package/dist/components/scenes/iam/RegisterForm.svelte +269 -0
  188. package/dist/components/scenes/iam/RegisterForm.svelte.d.ts +8 -0
  189. package/dist/components/scenes/iam/ResetPasswordForm.svelte +153 -0
  190. package/dist/components/scenes/iam/ResetPasswordForm.svelte.d.ts +8 -0
  191. package/dist/components/scenes/iam/UserProfile.svelte +251 -0
  192. package/dist/components/scenes/iam/UserProfile.svelte.d.ts +4 -0
  193. package/dist/components/scenes/iam/index.d.ts +16 -0
  194. package/dist/components/scenes/iam/index.js +18 -0
  195. package/dist/components/scenes/iam/password-utils.d.ts +15 -0
  196. package/dist/components/scenes/iam/password-utils.js +19 -0
  197. package/dist/components/scenes/iam/permission-context.svelte.d.ts +90 -0
  198. package/dist/components/scenes/iam/permission-context.svelte.js +114 -0
  199. package/dist/components/scenes/index.d.ts +13 -0
  200. package/dist/components/scenes/index.js +20 -0
  201. package/dist/components/scenes/storage/AvatarUpload.svelte +218 -0
  202. package/dist/components/scenes/storage/AvatarUpload.svelte.d.ts +4 -0
  203. package/dist/components/scenes/storage/FileList.svelte +260 -0
  204. package/dist/components/scenes/storage/FileList.svelte.d.ts +4 -0
  205. package/dist/components/scenes/storage/FileUpload.svelte +400 -0
  206. package/dist/components/scenes/storage/FileUpload.svelte.d.ts +7 -0
  207. package/dist/components/scenes/storage/ImageUpload.svelte +285 -0
  208. package/dist/components/scenes/storage/ImageUpload.svelte.d.ts +4 -0
  209. package/dist/components/scenes/storage/index.d.ts +10 -0
  210. package/dist/components/scenes/storage/index.js +12 -0
  211. package/dist/components/scenes/types.d.ts +583 -0
  212. package/dist/components/scenes/types.js +7 -0
  213. package/dist/i18n.svelte.d.ts +44 -0
  214. package/dist/i18n.svelte.js +108 -0
  215. package/dist/index.d.ts +13 -0
  216. package/dist/index.js +19 -0
  217. package/dist/messages/en-US.json +242 -0
  218. package/dist/messages/zh-CN.json +242 -0
  219. package/dist/messages.d.ts +29 -0
  220. package/dist/messages.js +42 -0
  221. package/dist/styles/design-tokens.css +58 -0
  222. package/dist/styles/global.css +218 -0
  223. package/dist/styles/mobile.css +105 -0
  224. package/dist/theme-config.d.ts +92 -0
  225. package/dist/theme-config.js +122 -0
  226. package/dist/toast.svelte.d.ts +45 -0
  227. package/dist/toast.svelte.js +77 -0
  228. package/dist/types.d.ts +944 -0
  229. package/dist/types.js +7 -0
  230. package/dist/utils/platform.svelte.d.ts +54 -0
  231. package/dist/utils/platform.svelte.js +87 -0
  232. package/dist/utils.d.ts +46 -0
  233. package/dist/utils.js +100 -0
  234. package/package.json +69 -0
@@ -0,0 +1,300 @@
1
+ import { uiM } from '../../../messages.js';
2
+ import { renderMarkdownDocument } from './document-parse.js';
3
+ /**
4
+ * code 类型产物通常只有裸代码正文。
5
+ * 下载和预览都通过同一份规范化逻辑包装成 fenced markdown,避免两边导出的结构不一致。
6
+ */
7
+ export function resolveDocumentMarkdownContent(value, kind = 'markdown', language) {
8
+ if (kind !== 'code') {
9
+ return value;
10
+ }
11
+ const fenceLength = Math.max(3, ...Array.from(value.matchAll(/`{3,}/g), match => match[0].length + 1));
12
+ const fence = '`'.repeat(fenceLength);
13
+ const normalizedLanguage = language?.trim() ?? '';
14
+ const suffix = value.endsWith('\n') ? '' : '\n';
15
+ return `${fence}${normalizedLanguage}\n${value}${suffix}${fence}`;
16
+ }
17
+ /**
18
+ * 把自定义动作和内置动作统一归一成统一的菜单项,保证左右两侧下载菜单展示一致。
19
+ */
20
+ export function resolveDocumentDownloadActions(actions = []) {
21
+ if (actions.length === 0) {
22
+ return [
23
+ createBuiltInDownloadAction('word'),
24
+ createBuiltInDownloadAction('pdf'),
25
+ createBuiltInDownloadAction('markdown'),
26
+ ];
27
+ }
28
+ return actions.map((action) => {
29
+ const builtIn = tryResolveBuiltInDownloadAction(action.id);
30
+ return {
31
+ id: action.id,
32
+ label: action.label || builtIn?.label || action.id,
33
+ badgeLabel: action.badgeLabel
34
+ || builtIn?.badgeLabel
35
+ || action.id.slice(0, 4).toUpperCase(),
36
+ };
37
+ });
38
+ }
39
+ /**
40
+ * 执行内置下载动作;未知动作直接跳过,方便外层保留自定义回调扩展口。
41
+ */
42
+ export async function downloadAiDocument(request) {
43
+ if (typeof document === 'undefined') {
44
+ return;
45
+ }
46
+ const builtIn = tryResolveBuiltInDownloadAction(request.actionId);
47
+ if (!builtIn) {
48
+ return;
49
+ }
50
+ const markdownContent = resolveDocumentMarkdownContent(request.content, request.sourceKind ?? 'markdown', request.codeLanguage);
51
+ const baseName = resolveDocumentBaseName(request.title, request.filename);
52
+ if (builtIn.id === 'markdown') {
53
+ downloadBlob(new Blob([markdownContent], {
54
+ type: 'text/markdown;charset=utf-8',
55
+ }), `${baseName}.md`);
56
+ return;
57
+ }
58
+ const exportTitle = request.title?.trim() || baseName;
59
+ const rendered = renderMarkdownDocument(markdownContent, {
60
+ enableHighlight: false,
61
+ showCopyButton: false,
62
+ showRunButton: false,
63
+ breaks: true,
64
+ });
65
+ const exportHtml = buildDocumentExportHtml(exportTitle, rendered.html);
66
+ if (builtIn.id === 'word') {
67
+ downloadBlob(new Blob([`\uFEFF${exportHtml}`], {
68
+ type: 'application/msword;charset=utf-8',
69
+ }), `${baseName}.doc`);
70
+ return;
71
+ }
72
+ await printDocumentAsPdf(htmlToPdfTitle(exportTitle), exportHtml);
73
+ }
74
+ function createBuiltInDownloadAction(format) {
75
+ switch (format) {
76
+ case 'word':
77
+ return {
78
+ id: 'word',
79
+ label: uiM('markdown_download_word_menu'),
80
+ badgeLabel: 'DOC',
81
+ };
82
+ case 'markdown':
83
+ return {
84
+ id: 'markdown',
85
+ label: uiM('markdown_download_markdown_menu'),
86
+ badgeLabel: 'MD',
87
+ };
88
+ case 'pdf':
89
+ return {
90
+ id: 'pdf',
91
+ label: uiM('markdown_download_pdf_menu'),
92
+ badgeLabel: 'PDF',
93
+ };
94
+ }
95
+ }
96
+ function tryResolveBuiltInDownloadAction(value) {
97
+ if (value === 'word' || value === 'markdown' || value === 'pdf') {
98
+ return createBuiltInDownloadAction(value);
99
+ }
100
+ return undefined;
101
+ }
102
+ function stripControlCharacters(value) {
103
+ return Array.from(value).filter(char => char >= ' ').join('');
104
+ }
105
+ function resolveDocumentBaseName(title, filename) {
106
+ const preferredName = filename?.trim() || title?.trim() || 'ai-document';
107
+ const sanitizedName = stripControlCharacters(preferredName)
108
+ .replace(/[<>:"/\\|?*]+/g, ' ')
109
+ .replace(/\s+/g, ' ')
110
+ .replace(/[. ]+$/g, '')
111
+ .trim();
112
+ return sanitizedName || 'ai-document';
113
+ }
114
+ function downloadBlob(blob, filename) {
115
+ const url = URL.createObjectURL(blob);
116
+ const anchor = document.createElement('a');
117
+ anchor.href = url;
118
+ anchor.download = filename;
119
+ anchor.rel = 'noopener';
120
+ anchor.click();
121
+ window.setTimeout(() => {
122
+ URL.revokeObjectURL(url);
123
+ }, 1000);
124
+ }
125
+ function escapeHtml(value) {
126
+ return value
127
+ .replace(/&/g, '&amp;')
128
+ .replace(/</g, '&lt;')
129
+ .replace(/>/g, '&gt;')
130
+ .replace(/"/g, '&quot;')
131
+ .replace(/'/g, '&#39;');
132
+ }
133
+ function buildDocumentExportHtml(title, bodyHtml) {
134
+ return `<!doctype html>
135
+ <html lang="zh-CN">
136
+ <head>
137
+ <meta charset="utf-8" />
138
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
139
+ <title>${escapeHtml(title)}</title>
140
+ <style>
141
+ :root {
142
+ color-scheme: light;
143
+ }
144
+
145
+ body {
146
+ margin: 0;
147
+ padding: 0;
148
+ font-family:
149
+ "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
150
+ "Segoe UI", sans-serif;
151
+ color: #18212f;
152
+ background: #ffffff;
153
+ }
154
+
155
+ main {
156
+ max-width: 860px;
157
+ margin: 0 auto;
158
+ padding: 40px 32px 72px;
159
+ }
160
+
161
+ h1,
162
+ h2,
163
+ h3,
164
+ h4,
165
+ h5,
166
+ h6 {
167
+ margin: 1.5em 0 0.7em;
168
+ line-height: 1.28;
169
+ color: #101827;
170
+ }
171
+
172
+ h1 {
173
+ font-size: 30px;
174
+ padding-bottom: 0.42em;
175
+ border-bottom: 1px solid #dbe4f0;
176
+ }
177
+
178
+ h2 {
179
+ font-size: 24px;
180
+ }
181
+
182
+ h3 {
183
+ font-size: 20px;
184
+ }
185
+
186
+ p,
187
+ li,
188
+ blockquote,
189
+ td,
190
+ th {
191
+ font-size: 15px;
192
+ line-height: 1.82;
193
+ }
194
+
195
+ a {
196
+ color: #2259d1;
197
+ text-decoration: none;
198
+ }
199
+
200
+ code,
201
+ pre,
202
+ kbd {
203
+ font-family:
204
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
205
+ "Liberation Mono", monospace;
206
+ }
207
+
208
+ pre {
209
+ padding: 16px;
210
+ border-radius: 18px;
211
+ overflow: auto;
212
+ background: #111827;
213
+ color: #f8fafc;
214
+ }
215
+
216
+ code {
217
+ white-space: pre-wrap;
218
+ }
219
+
220
+ :not(pre) > code {
221
+ padding: 0.18em 0.42em;
222
+ border-radius: 7px;
223
+ background: #eef2f8;
224
+ color: #132238;
225
+ }
226
+
227
+ blockquote {
228
+ margin: 1.1em 0;
229
+ padding: 0.9em 1.1em;
230
+ border-left: 4px solid #7fa4ff;
231
+ background: #f6f8fd;
232
+ }
233
+
234
+ table {
235
+ width: 100%;
236
+ border-collapse: collapse;
237
+ }
238
+
239
+ th,
240
+ td {
241
+ padding: 12px 14px;
242
+ border: 1px solid #dbe4f0;
243
+ }
244
+
245
+ thead {
246
+ background: #f5f7fb;
247
+ }
248
+
249
+ img {
250
+ max-width: 100%;
251
+ height: auto;
252
+ }
253
+
254
+ @page {
255
+ size: auto;
256
+ margin: 14mm 12mm;
257
+ }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <main>
262
+ ${bodyHtml}
263
+ </main>
264
+ </body>
265
+ </html>`;
266
+ }
267
+ function htmlToPdfTitle(title) {
268
+ return title.endsWith('.pdf') ? title : `${title}.pdf`;
269
+ }
270
+ async function printDocumentAsPdf(pdfTitle, html) {
271
+ const iframe = document.createElement('iframe');
272
+ iframe.setAttribute('aria-hidden', 'true');
273
+ iframe.style.position = 'fixed';
274
+ iframe.style.right = '0';
275
+ iframe.style.bottom = '0';
276
+ iframe.style.width = '0';
277
+ iframe.style.height = '0';
278
+ iframe.style.border = '0';
279
+ document.body.append(iframe);
280
+ await new Promise((resolve) => {
281
+ iframe.onload = () => resolve();
282
+ iframe.srcdoc = html;
283
+ });
284
+ const previewWindow = iframe.contentWindow;
285
+ if (!previewWindow) {
286
+ iframe.remove();
287
+ return;
288
+ }
289
+ previewWindow.document.title = pdfTitle;
290
+ previewWindow.focus();
291
+ // PDF 导出当前走浏览器打印能力,让用户在不额外引入 PDF 库的前提下使用系统“另存为 PDF”。
292
+ const cleanup = () => {
293
+ window.setTimeout(() => {
294
+ iframe.remove();
295
+ }, 240);
296
+ };
297
+ previewWindow.addEventListener('afterprint', cleanup, { once: true });
298
+ previewWindow.print();
299
+ window.setTimeout(cleanup, 1200);
300
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @h-ai/ui — Markdown document parsing helpers
3
+ *
4
+ * Extracts HTML, outline, and code block metadata for document-mode rendering.
5
+ */
6
+ import type { MarkdownCodeBlockItem, MarkdownOutlineItem } from './document-types.js';
7
+ export interface MarkdownDocumentParseOptions {
8
+ /** Whether to enable syntax highlighting for code blocks. */
9
+ enableHighlight?: boolean;
10
+ /** Whether to render the copy button in code blocks. */
11
+ showCopyButton?: boolean;
12
+ /** Whether to render the run button and preview slot. */
13
+ showRunButton?: boolean;
14
+ /** Whether soft line breaks are converted to <br>. */
15
+ breaks?: boolean;
16
+ }
17
+ export interface MarkdownRenderResult {
18
+ /** Rendered HTML output. */
19
+ html: string;
20
+ /** Extracted outline items for navigation. */
21
+ outline: MarkdownOutlineItem[];
22
+ /** Extracted code blocks for run/copy features. */
23
+ codeBlocks: MarkdownCodeBlockItem[];
24
+ }
25
+ /**
26
+ * Render markdown into HTML and extract outline + code block metadata.
27
+ */
28
+ export declare function renderMarkdownDocument(content: string, options?: MarkdownDocumentParseOptions): MarkdownRenderResult;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @h-ai/ui — Markdown document parsing helpers
3
+ *
4
+ * Extracts HTML, outline, and code block metadata for document-mode rendering.
5
+ */
6
+ import hljs from 'highlight.js/lib/common';
7
+ import { Marked } from 'marked';
8
+ /** Default parsing options for document mode. */
9
+ const DEFAULT_OPTIONS = {
10
+ enableHighlight: true,
11
+ showCopyButton: true,
12
+ showRunButton: false,
13
+ breaks: true,
14
+ };
15
+ /**
16
+ * Escape HTML entities to prevent raw HTML injection.
17
+ */
18
+ function escapeHtml(text) {
19
+ return text
20
+ .replace(/&/g, '&amp;')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;')
24
+ .replace(/'/g, '&#39;');
25
+ }
26
+ /**
27
+ * Create a URL-safe heading slug while keeping letters and digits.
28
+ */
29
+ function slugifyHeading(text) {
30
+ // normalized slug used as the base key for headings.
31
+ const normalized = text
32
+ .trim()
33
+ .toLocaleLowerCase()
34
+ .replace(/[^\p{L}\p{N}\s-]/gu, '')
35
+ .replace(/\s+/g, '-')
36
+ .replace(/-+/g, '-')
37
+ .replace(/^-|-$/g, '');
38
+ return normalized || 'section';
39
+ }
40
+ /**
41
+ * Generate a stable heading id, de-duplicating repeated titles.
42
+ */
43
+ function createHeadingId(state, text) {
44
+ // base slug before de-duplication.
45
+ const base = slugifyHeading(text);
46
+ // next count per slug ensures stable suffixing.
47
+ const nextCount = (state.headingIds.get(base) ?? 0) + 1;
48
+ state.headingIds.set(base, nextCount);
49
+ return nextCount === 1 ? base : `${base}-${nextCount}`;
50
+ }
51
+ /**
52
+ * Add numeric prefixes (1, 1.1, 1.1.1) to outline items.
53
+ */
54
+ function createNumberedOutline(outline) {
55
+ // counters track the current numbering state per heading depth.
56
+ const counters = [0, 0, 0, 0, 0, 0];
57
+ return outline.map((item) => {
58
+ // levelIndex caps the depth within 1-6.
59
+ const levelIndex = Math.max(0, Math.min(5, item.level - 1));
60
+ counters[levelIndex] += 1;
61
+ for (let index = levelIndex + 1; index < counters.length; index += 1) {
62
+ counters[index] = 0;
63
+ }
64
+ // prefix is the dotted numbering prefix for the current heading.
65
+ const prefix = counters
66
+ .slice(0, levelIndex + 1)
67
+ .filter(count => count > 0)
68
+ .join('.');
69
+ return {
70
+ ...item,
71
+ numberedTitle: prefix ? `${prefix} ${item.text}` : item.text,
72
+ };
73
+ });
74
+ }
75
+ /**
76
+ * Build the marked renderer with outline extraction and code metadata.
77
+ */
78
+ function createRendererObject(options, state) {
79
+ return {
80
+ heading(token) {
81
+ // depth is clamped to the valid heading range.
82
+ const depth = Math.max(1, Math.min(6, token.depth));
83
+ // text is the plain heading text used for outline labels.
84
+ const text = token.text.trim();
85
+ // id is the stable anchor for outline navigation.
86
+ const id = createHeadingId(state, text);
87
+ state.outline.push({
88
+ id,
89
+ text: text || `Section ${state.outline.length + 1}`,
90
+ level: depth,
91
+ });
92
+ // headingHtml preserves inline markdown in headings.
93
+ const headingHtml = this.parser.parseInline(token.tokens);
94
+ return `<h${depth} id="${escapeHtml(id)}" data-heading-id="${escapeHtml(id)}">${headingHtml}</h${depth}>`;
95
+ },
96
+ code({ text, lang }) {
97
+ // rawLanguage is the original info string from the fence.
98
+ const rawLanguage = lang?.trim() || '';
99
+ // highlightLanguage is validated against highlight.js languages.
100
+ const highlightLanguage = rawLanguage && hljs.getLanguage(rawLanguage)
101
+ ? rawLanguage
102
+ : '';
103
+ // highlighted is the final HTML for the code content.
104
+ const highlighted = options.enableHighlight
105
+ ? highlightLanguage
106
+ ? hljs.highlight(text, { language: highlightLanguage }).value
107
+ : hljs.highlightAuto(text).value
108
+ : escapeHtml(text);
109
+ // codeBlockId is used by run/copy hooks and DOM bindings.
110
+ const codeBlockId = `hai-md-code-${state.codeBlocks.length + 1}`;
111
+ state.codeBlocks.push({
112
+ id: codeBlockId,
113
+ code: text,
114
+ language: rawLanguage || undefined,
115
+ });
116
+ // langLabel keeps a consistent header layout even when language is empty.
117
+ const langLabel = rawLanguage
118
+ ? `<span class="hai-md-code-lang">${escapeHtml(rawLanguage)}</span>`
119
+ : '<span class="hai-md-code-lang hai-md-code-lang-empty">Plain Text</span>';
120
+ // copyBtn is optional based on the options.
121
+ const copyBtn = options.showCopyButton
122
+ ? `<button type="button" class="hai-md-code-action hai-md-copy-btn" data-copy-code aria-label="Copy code"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>`
123
+ : '';
124
+ // runBtn exposes the run action when enabled.
125
+ const runBtn = options.showRunButton
126
+ ? `<button type="button" class="hai-md-code-action hai-md-run-btn" data-run-code data-code-block-id="${escapeHtml(codeBlockId)}" aria-label="Run code"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.14v14l11-7z"/></svg></button>`
127
+ : '';
128
+ // previewHost reserves the DOM slot for run previews.
129
+ const previewHost = options.showRunButton
130
+ ? `<div class="hai-md-code-preview-slot" data-code-preview-host="${escapeHtml(codeBlockId)}"></div>`
131
+ : '';
132
+ return `<div class="hai-md-code-block" data-code-block-id="${escapeHtml(codeBlockId)}">`
133
+ + `<div class="hai-md-code-header">`
134
+ + `<div class="hai-md-code-header-main">${langLabel}</div>`
135
+ + `<div class="hai-md-code-actions">${runBtn}${copyBtn}</div>`
136
+ + `</div>`
137
+ + `<pre><code class="hljs${highlightLanguage ? ` language-${escapeHtml(highlightLanguage)}` : ''}">${highlighted}</code></pre>`
138
+ + `${previewHost}`
139
+ + `</div>`;
140
+ },
141
+ html(token) {
142
+ return escapeHtml('text' in token ? token.text : '');
143
+ },
144
+ link({ href, title, tokens }) {
145
+ // text preserves inline markdown within the link label.
146
+ const text = this.parser.parseInline(tokens);
147
+ // safeHref strips potentially unsafe protocols.
148
+ const safeHref = href && /^(?:https?:\/\/|\/|#|mailto:)/i.test(href) ? href : '';
149
+ // isExternal controls whether target/_blank is added.
150
+ const isExternal = safeHref
151
+ && (safeHref.startsWith('http://') || safeHref.startsWith('https://'));
152
+ // attrs is the flattened HTML attribute string.
153
+ const attrs = [
154
+ `href="${escapeHtml(safeHref)}"`,
155
+ title ? `title="${escapeHtml(title)}"` : '',
156
+ isExternal ? 'target="_blank" rel="noopener noreferrer"' : '',
157
+ ].filter(Boolean).join(' ');
158
+ return `<a ${attrs}>${text}</a>`;
159
+ },
160
+ image({ href, title, text }) {
161
+ // safeSrc prevents dangerous image protocols.
162
+ const safeSrc = href && /^(?:https?:\/\/|\/|data:image\/)/i.test(href) ? href : '';
163
+ // attrs is the flattened HTML attribute string.
164
+ const attrs = [
165
+ `src="${escapeHtml(safeSrc)}"`,
166
+ `alt="${escapeHtml(text || '')}"`,
167
+ title ? `title="${escapeHtml(title)}"` : '',
168
+ 'loading="lazy"',
169
+ 'class="hai-md-img"',
170
+ ].filter(Boolean).join(' ');
171
+ return `<img ${attrs} />`;
172
+ },
173
+ table(token) {
174
+ // headerHtml accumulates the header row content.
175
+ let headerHtml = '';
176
+ for (const cell of token.header) {
177
+ // align adds text alignment for each header cell.
178
+ const align = cell.align ? ` style="text-align:${cell.align}"` : '';
179
+ headerHtml += `<th${align}>${this.parser.parseInline(cell.tokens)}</th>`;
180
+ }
181
+ // bodyHtml accumulates all body rows.
182
+ let bodyHtml = '';
183
+ for (const row of token.rows) {
184
+ // rowHtml accumulates cells for a single row.
185
+ let rowHtml = '';
186
+ for (const cell of row) {
187
+ // align adds text alignment for each body cell.
188
+ const align = cell.align ? ` style="text-align:${cell.align}"` : '';
189
+ rowHtml += `<td${align}>${this.parser.parseInline(cell.tokens)}</td>`;
190
+ }
191
+ bodyHtml += `<tr>${rowHtml}</tr>`;
192
+ }
193
+ return `<div class="hai-md-table-wrap">`
194
+ + `<table>`
195
+ + `<thead><tr>${headerHtml}</tr></thead>`
196
+ + `<tbody>${bodyHtml}</tbody>`
197
+ + `</table>`
198
+ + `</div>`;
199
+ },
200
+ };
201
+ }
202
+ /**
203
+ * Create a marked instance tied to a specific render state.
204
+ */
205
+ function createMarkedInstance(options, state) {
206
+ return new Marked({
207
+ renderer: createRendererObject(options, state),
208
+ gfm: true,
209
+ breaks: options.breaks,
210
+ });
211
+ }
212
+ /**
213
+ * Render markdown into HTML and extract outline + code block metadata.
214
+ */
215
+ export function renderMarkdownDocument(content, options) {
216
+ if (!content) {
217
+ return {
218
+ html: '',
219
+ outline: [],
220
+ codeBlocks: [],
221
+ };
222
+ }
223
+ // mergedOptions ensures defaults are always applied.
224
+ const mergedOptions = {
225
+ ...DEFAULT_OPTIONS,
226
+ ...options,
227
+ };
228
+ // state captures outline, code blocks, and heading ids per render call.
229
+ const state = {
230
+ outline: [],
231
+ codeBlocks: [],
232
+ headingIds: new Map(),
233
+ };
234
+ // marked instance is scoped to this render call.
235
+ const marked = createMarkedInstance(mergedOptions, state);
236
+ // result can be sync string or async promise; only sync string is used.
237
+ const result = marked.parse(content);
238
+ return {
239
+ html: typeof result === 'string' ? result : '',
240
+ outline: createNumberedOutline(state.outline),
241
+ codeBlocks: state.codeBlocks,
242
+ };
243
+ }