@refraction-ui/astro 0.4.2 → 0.4.5

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 (334) hide show
  1. package/dist/accordion/index.ts +1 -0
  2. package/dist/ai/ai-manager.ts +121 -0
  3. package/dist/ai/index.ts +23 -0
  4. package/dist/ai/mock-providers.ts +68 -0
  5. package/dist/ai/sanitizer.ts +174 -0
  6. package/dist/ai/tts-manager.ts +80 -0
  7. package/dist/ai/types.ts +72 -0
  8. package/dist/animated-text/animated-text.styles.ts +28 -0
  9. package/dist/animated-text/animated-text.ts +83 -0
  10. package/dist/animated-text/index.ts +15 -0
  11. package/dist/app-shell/app-shell.ts +252 -0
  12. package/dist/app-shell/auth-shell.ts +84 -0
  13. package/dist/app-shell/index.ts +22 -0
  14. package/dist/app-shell/page-shell.ts +125 -0
  15. package/dist/astro-accordion/Button.astro +1 -1
  16. package/dist/astro-ai/index.ts +1 -1
  17. package/dist/astro-animated-text/AnimatedText.astro +1 -1
  18. package/dist/astro-animated-text/TypewriterText.astro +1 -1
  19. package/dist/astro-app-shell/AppShell.astro +1 -1
  20. package/dist/astro-app-shell/AuthShell.astro +1 -1
  21. package/dist/astro-app-shell/AuthShellCard.astro +1 -1
  22. package/dist/astro-app-shell/PageShell.astro +1 -1
  23. package/dist/astro-app-shell/PageShellSection.astro +1 -1
  24. package/dist/astro-app-shell/index.ts +1 -1
  25. package/dist/astro-auth/index.ts +2 -2
  26. package/dist/astro-avatar/Avatar.astro +1 -1
  27. package/dist/astro-avatar/AvatarFallback.astro +1 -1
  28. package/dist/astro-avatar/AvatarImage.astro +1 -1
  29. package/dist/astro-avatar-group/AvatarGroup.astro +1 -1
  30. package/dist/astro-badge/Badge.astro +1 -1
  31. package/dist/astro-bottom-nav/BottomNav.astro +1 -1
  32. package/dist/astro-breadcrumbs/Breadcrumbs.astro +1 -1
  33. package/dist/astro-button/Button.astro +1 -1
  34. package/dist/astro-calendar/Calendar.astro +1 -1
  35. package/dist/astro-card/Card.astro +1 -1
  36. package/dist/astro-card/CardContent.astro +1 -1
  37. package/dist/astro-card/CardDescription.astro +1 -1
  38. package/dist/astro-card/CardFooter.astro +1 -1
  39. package/dist/astro-card/CardHeader.astro +1 -1
  40. package/dist/astro-card/CardTitle.astro +1 -1
  41. package/dist/astro-card-grid/CardGrid.astro +1 -1
  42. package/dist/astro-carousel/Button.astro +1 -1
  43. package/dist/astro-charts/Bars.astro +1 -1
  44. package/dist/astro-charts/Chart.astro +1 -1
  45. package/dist/astro-charts/Circles.astro +1 -1
  46. package/dist/astro-charts/Histogram.astro +1 -1
  47. package/dist/astro-charts/Line.astro +1 -1
  48. package/dist/astro-charts/PieChart.astro +1 -1
  49. package/dist/astro-charts/ScatterPlot.astro +1 -1
  50. package/dist/astro-charts/index.ts +1 -1
  51. package/dist/astro-checkbox/Checkbox.astro +1 -1
  52. package/dist/astro-code-editor/CodeEditor.astro +1 -1
  53. package/dist/astro-collapsible/Collapsible.astro +1 -1
  54. package/dist/astro-collapsible/CollapsibleContent.astro +1 -1
  55. package/dist/astro-command/Command.astro +1 -1
  56. package/dist/astro-command/CommandGroup.astro +1 -1
  57. package/dist/astro-command/CommandInput.astro +1 -1
  58. package/dist/astro-command/CommandItem.astro +1 -1
  59. package/dist/astro-command-input/CommandInput.astro +1 -1
  60. package/dist/astro-content-protection/ContentProtection.astro +1 -1
  61. package/dist/astro-data-table/DataTable.astro +1 -1
  62. package/dist/astro-date-picker/DatePicker.astro +1 -1
  63. package/dist/astro-device-frame/DeviceFrame.astro +1 -1
  64. package/dist/astro-dialog/DialogContent.astro +1 -1
  65. package/dist/astro-dialog/DialogOverlay.astro +1 -1
  66. package/dist/astro-diff-viewer/DiffViewer.astro +1 -1
  67. package/dist/astro-dropdown-menu/DropdownMenuContent.astro +1 -1
  68. package/dist/astro-dropdown-menu/DropdownMenuItem.astro +1 -1
  69. package/dist/astro-emoji-picker/EmojiPicker.astro +1 -1
  70. package/dist/astro-feedback-dialog/FeedbackDialog.astro +1 -1
  71. package/dist/astro-file-upload/FileUpload.astro +1 -1
  72. package/dist/astro-footer/Footer.astro +1 -1
  73. package/dist/astro-inline-editor/InlineEditor.astro +1 -1
  74. package/dist/astro-input/Input.astro +1 -1
  75. package/dist/astro-input-group/InputGroup.astro +1 -1
  76. package/dist/astro-input-group/InputGroupAddon.astro +1 -1
  77. package/dist/astro-input-group/InputGroupButton.astro +1 -1
  78. package/dist/astro-install-prompt/InstallPrompt.astro +1 -1
  79. package/dist/astro-keyboard-shortcut/ShortcutBadge.astro +1 -1
  80. package/dist/astro-language-selector/LanguageSelector.astro +1 -1
  81. package/dist/astro-link-card/LinkCard.astro +1 -1
  82. package/dist/astro-markdown-renderer/MarkdownRenderer.astro +1 -1
  83. package/dist/astro-mobile-nav/MobileNav.astro +1 -1
  84. package/dist/astro-mobile-nav/MobileNavLink.astro +1 -1
  85. package/dist/astro-mobile-nav/MobileNavTrigger.astro +1 -1
  86. package/dist/astro-navbar/Navbar.astro +1 -1
  87. package/dist/astro-otp-input/OtpInput.astro +1 -1
  88. package/dist/astro-payment/Payment.astro +1 -1
  89. package/dist/astro-popover/PopoverContent.astro +1 -1
  90. package/dist/astro-presence-indicator/PresenceIndicator.astro +1 -1
  91. package/dist/astro-progress-display/BadgeDisplay.astro +1 -1
  92. package/dist/astro-progress-display/ProgressBar.astro +1 -1
  93. package/dist/astro-progress-display/StatsGrid.astro +1 -1
  94. package/dist/astro-radio/RadioGroup.astro +1 -1
  95. package/dist/astro-radio/RadioItem.astro +1 -1
  96. package/dist/astro-reaction-bar/ReactionBar.astro +1 -1
  97. package/dist/astro-resizable-layout/ResizableDivider.astro +1 -1
  98. package/dist/astro-resizable-layout/ResizableLayout.astro +1 -1
  99. package/dist/astro-resizable-layout/ResizablePane.astro +1 -1
  100. package/dist/astro-resizable-layout/index.ts +1 -1
  101. package/dist/astro-rich-editor/index.ts +2 -2
  102. package/dist/astro-search-bar/SearchBar.astro +1 -1
  103. package/dist/astro-search-bar/SearchResults.astro +1 -1
  104. package/dist/astro-select/Select.astro +1 -1
  105. package/dist/astro-select/SelectContent.astro +1 -1
  106. package/dist/astro-select/SelectItem.astro +1 -1
  107. package/dist/astro-select/SelectTrigger.astro +1 -1
  108. package/dist/astro-sidebar/Sidebar.astro +1 -1
  109. package/dist/astro-skeleton/Skeleton.astro +1 -1
  110. package/dist/astro-slide-viewer/SlideViewer.astro +1 -1
  111. package/dist/astro-status-indicator/StatusIndicator.astro +1 -1
  112. package/dist/astro-switch/Switch.astro +1 -1
  113. package/dist/astro-table-of-contents/TableOfContents.astro +1 -1
  114. package/dist/astro-tabs/Tabs.astro +1 -1
  115. package/dist/astro-tabs/TabsList.astro +1 -1
  116. package/dist/astro-tabs/TabsTrigger.astro +1 -1
  117. package/dist/astro-textarea/Textarea.astro +1 -1
  118. package/dist/astro-theme/ThemeScript.astro +1 -1
  119. package/dist/astro-thread-view/ThreadView.astro +1 -1
  120. package/dist/astro-toast/Toast.astro +1 -1
  121. package/dist/astro-tooltip/TooltipContent.astro +1 -1
  122. package/dist/astro-version-selector/VersionSelector.astro +1 -1
  123. package/dist/astro-video-player/VideoPlayer.astro +1 -1
  124. package/dist/auth/adapters/mock.ts +74 -0
  125. package/dist/auth/auth-machine.ts +120 -0
  126. package/dist/auth/index.ts +22 -0
  127. package/dist/auth/rbac.ts +44 -0
  128. package/dist/auth/types.ts +44 -0
  129. package/dist/avatar/avatar.styles.ts +47 -0
  130. package/dist/avatar/avatar.ts +73 -0
  131. package/dist/avatar/index.ts +15 -0
  132. package/dist/avatar-group/avatar-group.styles.ts +63 -0
  133. package/dist/avatar-group/avatar-group.ts +106 -0
  134. package/dist/avatar-group/index.ts +18 -0
  135. package/dist/badge/badge.styles.ts +24 -0
  136. package/dist/badge/badge.ts +48 -0
  137. package/dist/badge/index.ts +10 -0
  138. package/dist/bottom-nav/bottom-nav.styles.ts +18 -0
  139. package/dist/bottom-nav/bottom-nav.ts +33 -0
  140. package/dist/bottom-nav/index.ts +2 -0
  141. package/dist/breadcrumbs/breadcrumbs.styles.ts +20 -0
  142. package/dist/breadcrumbs/breadcrumbs.ts +72 -0
  143. package/dist/breadcrumbs/index.ts +8 -0
  144. package/dist/button/button.styles.ts +38 -0
  145. package/dist/button/button.ts +64 -0
  146. package/dist/button/index.ts +10 -0
  147. package/dist/calendar/calendar.styles.ts +21 -0
  148. package/dist/calendar/calendar.ts +202 -0
  149. package/dist/calendar/index.ts +9 -0
  150. package/dist/callout/callout.ts +1 -0
  151. package/dist/callout/index.ts +1 -0
  152. package/dist/card/card.styles.ts +55 -0
  153. package/dist/card/card.ts +45 -0
  154. package/dist/card/index.ts +20 -0
  155. package/dist/card-grid/card-grid.ts +9 -0
  156. package/dist/card-grid/index.ts +1 -0
  157. package/dist/carousel/index.ts +1 -0
  158. package/dist/charts/axis.ts +50 -0
  159. package/dist/charts/chart.ts +73 -0
  160. package/dist/charts/colorblind.ts +100 -0
  161. package/dist/charts/data.ts +72 -0
  162. package/dist/charts/geometry.ts +54 -0
  163. package/dist/charts/index.ts +61 -0
  164. package/dist/charts/scales.ts +151 -0
  165. package/dist/checkbox/checkbox.styles.ts +33 -0
  166. package/dist/checkbox/checkbox.ts +73 -0
  167. package/dist/checkbox/index.ts +10 -0
  168. package/dist/code-block/code-block.ts +1 -0
  169. package/dist/code-block/index.ts +1 -0
  170. package/dist/code-editor/code-editor.styles.ts +32 -0
  171. package/dist/code-editor/code-editor.ts +140 -0
  172. package/dist/code-editor/index.ts +9 -0
  173. package/dist/collapsible/collapsible.styles.ts +14 -0
  174. package/dist/collapsible/collapsible.ts +93 -0
  175. package/dist/collapsible/index.ts +7 -0
  176. package/dist/command/command.styles.ts +27 -0
  177. package/dist/command/command.ts +179 -0
  178. package/dist/command/index.ts +14 -0
  179. package/dist/command-input/command-input.ts +176 -0
  180. package/dist/command-input/index.ts +1 -0
  181. package/dist/content-protection/content-protection.styles.ts +9 -0
  182. package/dist/content-protection/content-protection.ts +68 -0
  183. package/dist/content-protection/index.ts +8 -0
  184. package/dist/data-table/data-table.styles.ts +55 -0
  185. package/dist/data-table/data-table.ts +183 -0
  186. package/dist/data-table/index.ts +15 -0
  187. package/dist/date-picker/date-picker.styles.ts +31 -0
  188. package/dist/date-picker/date-picker.ts +333 -0
  189. package/dist/date-picker/index.ts +18 -0
  190. package/dist/device-frame/device-frame.styles.ts +21 -0
  191. package/dist/device-frame/device-frame.ts +112 -0
  192. package/dist/device-frame/index.ts +10 -0
  193. package/dist/dialog/dialog.styles.ts +8 -0
  194. package/dist/dialog/dialog.ts +133 -0
  195. package/dist/dialog/index.ts +8 -0
  196. package/dist/diff-viewer/diff-viewer.styles.ts +124 -0
  197. package/dist/diff-viewer/diff-viewer.ts +255 -0
  198. package/dist/diff-viewer/index.ts +20 -0
  199. package/dist/dropdown-menu/dropdown-menu.styles.ts +9 -0
  200. package/dist/dropdown-menu/dropdown-menu.ts +139 -0
  201. package/dist/dropdown-menu/index.ts +9 -0
  202. package/dist/emoji-picker/emoji-data.ts +282 -0
  203. package/dist/emoji-picker/emoji-picker.styles.ts +32 -0
  204. package/dist/emoji-picker/emoji-picker.ts +165 -0
  205. package/dist/emoji-picker/index.ts +25 -0
  206. package/dist/feedback-dialog/feedback-dialog.styles.ts +15 -0
  207. package/dist/feedback-dialog/feedback-dialog.ts +147 -0
  208. package/dist/feedback-dialog/index.ts +11 -0
  209. package/dist/file-upload/file-upload.styles.ts +30 -0
  210. package/dist/file-upload/file-upload.ts +260 -0
  211. package/dist/file-upload/index.ts +19 -0
  212. package/dist/footer/footer.styles.ts +5 -0
  213. package/dist/footer/footer.ts +32 -0
  214. package/dist/footer/index.ts +9 -0
  215. package/dist/index.ts +1 -1
  216. package/dist/inline-editor/index.ts +13 -0
  217. package/dist/inline-editor/inline-editor.styles.ts +42 -0
  218. package/dist/inline-editor/inline-editor.ts +96 -0
  219. package/dist/input/index.ts +9 -0
  220. package/dist/input/input.styles.ts +15 -0
  221. package/dist/input/input.ts +84 -0
  222. package/dist/input-group/index.ts +13 -0
  223. package/dist/input-group/input-group.styles.ts +50 -0
  224. package/dist/input-group/input-group.ts +44 -0
  225. package/dist/install-prompt/index.ts +9 -0
  226. package/dist/install-prompt/install-prompt.styles.ts +5 -0
  227. package/dist/install-prompt/install-prompt.ts +68 -0
  228. package/dist/keyboard-shortcut/index.ts +12 -0
  229. package/dist/keyboard-shortcut/keyboard-shortcut.styles.ts +8 -0
  230. package/dist/keyboard-shortcut/keyboard-shortcut.ts +153 -0
  231. package/dist/language-selector/index.ts +8 -0
  232. package/dist/language-selector/language-selector.styles.ts +28 -0
  233. package/dist/language-selector/language-selector.ts +149 -0
  234. package/dist/link-card/index.ts +1 -0
  235. package/dist/link-card/link-card.ts +9 -0
  236. package/dist/markdown-renderer/index.ts +8 -0
  237. package/dist/markdown-renderer/markdown-renderer.styles.ts +31 -0
  238. package/dist/markdown-renderer/markdown-renderer.ts +251 -0
  239. package/dist/mobile-nav/index.ts +14 -0
  240. package/dist/mobile-nav/mobile-nav.styles.ts +39 -0
  241. package/dist/mobile-nav/mobile-nav.ts +74 -0
  242. package/dist/navbar/index.ts +9 -0
  243. package/dist/navbar/navbar.styles.ts +29 -0
  244. package/dist/navbar/navbar.ts +39 -0
  245. package/dist/otp-input/index.ts +13 -0
  246. package/dist/otp-input/otp-input.styles.ts +51 -0
  247. package/dist/otp-input/otp-input.ts +198 -0
  248. package/dist/pagination/index.ts +1 -0
  249. package/dist/payment/index.ts +1 -0
  250. package/dist/payment/payment.ts +16 -0
  251. package/dist/popover/index.ts +7 -0
  252. package/dist/popover/popover.styles.ts +16 -0
  253. package/dist/popover/popover.ts +89 -0
  254. package/dist/presence-indicator/index.ts +14 -0
  255. package/dist/presence-indicator/presence-indicator.styles.ts +29 -0
  256. package/dist/presence-indicator/presence-indicator.ts +63 -0
  257. package/dist/progress-display/index.ts +15 -0
  258. package/dist/progress-display/progress-display.styles.ts +72 -0
  259. package/dist/progress-display/progress-display.ts +60 -0
  260. package/dist/radio/index.ts +8 -0
  261. package/dist/radio/radio.styles.ts +34 -0
  262. package/dist/radio/radio.ts +70 -0
  263. package/dist/reaction-bar/index.ts +14 -0
  264. package/dist/reaction-bar/reaction-bar.styles.ts +23 -0
  265. package/dist/reaction-bar/reaction-bar.ts +87 -0
  266. package/dist/resizable-layout/index.ts +12 -0
  267. package/dist/resizable-layout/resizable-layout.styles.ts +50 -0
  268. package/dist/resizable-layout/resizable-layout.ts +139 -0
  269. package/dist/rich-editor/editor.ts +342 -0
  270. package/dist/rich-editor/emoji.ts +175 -0
  271. package/dist/rich-editor/history.ts +79 -0
  272. package/dist/rich-editor/index.ts +106 -0
  273. package/dist/rich-editor/markdown-shortcuts.ts +248 -0
  274. package/dist/rich-editor/mentions.ts +162 -0
  275. package/dist/rich-editor/model.ts +206 -0
  276. package/dist/rich-editor/operations.ts +772 -0
  277. package/dist/rich-editor/selection.ts +157 -0
  278. package/dist/rich-editor/serialization.ts +711 -0
  279. package/dist/rich-editor/slash-commands.ts +212 -0
  280. package/dist/search-bar/index.ts +7 -0
  281. package/dist/search-bar/search-bar.styles.ts +29 -0
  282. package/dist/search-bar/search-bar.ts +130 -0
  283. package/dist/select/index.ts +13 -0
  284. package/dist/select/select.styles.ts +52 -0
  285. package/dist/select/select.ts +152 -0
  286. package/dist/sidebar/index.ts +9 -0
  287. package/dist/sidebar/sidebar.styles.ts +27 -0
  288. package/dist/sidebar/sidebar.ts +59 -0
  289. package/dist/skeleton/index.ts +8 -0
  290. package/dist/skeleton/skeleton.styles.ts +16 -0
  291. package/dist/skeleton/skeleton.ts +39 -0
  292. package/dist/skip-to-content/index.ts +1 -0
  293. package/dist/skip-to-content/skip-to-content.ts +1 -0
  294. package/dist/slide-viewer/index.ts +16 -0
  295. package/dist/slide-viewer/slide-viewer.styles.ts +57 -0
  296. package/dist/slide-viewer/slide-viewer.ts +152 -0
  297. package/dist/slider/index.ts +1 -0
  298. package/dist/slider/slider.ts +2 -0
  299. package/dist/status-indicator/index.ts +15 -0
  300. package/dist/status-indicator/status-indicator.styles.ts +41 -0
  301. package/dist/status-indicator/status-indicator.ts +66 -0
  302. package/dist/steps/index.ts +1 -0
  303. package/dist/steps/steps.ts +1 -0
  304. package/dist/switch/index.ts +7 -0
  305. package/dist/switch/switch.styles.ts +55 -0
  306. package/dist/switch/switch.ts +63 -0
  307. package/dist/table-of-contents/index.ts +1 -0
  308. package/dist/table-of-contents/table-of-contents.ts +37 -0
  309. package/dist/tabs/index.ts +8 -0
  310. package/dist/tabs/tabs.styles.ts +9 -0
  311. package/dist/tabs/tabs.ts +133 -0
  312. package/dist/textarea/index.ts +7 -0
  313. package/dist/textarea/textarea.styles.ts +15 -0
  314. package/dist/textarea/textarea.ts +59 -0
  315. package/dist/theme/dom-adapters.ts +60 -0
  316. package/dist/theme/index.ts +18 -0
  317. package/dist/theme/theme-machine.ts +130 -0
  318. package/dist/theme/theme-script.ts +18 -0
  319. package/dist/thread-view/index.ts +26 -0
  320. package/dist/thread-view/thread-view.styles.ts +35 -0
  321. package/dist/thread-view/thread-view.ts +176 -0
  322. package/dist/toast/index.ts +13 -0
  323. package/dist/toast/toast.styles.ts +19 -0
  324. package/dist/toast/toast.ts +203 -0
  325. package/dist/tooltip/index.ts +7 -0
  326. package/dist/tooltip/tooltip.styles.ts +16 -0
  327. package/dist/tooltip/tooltip.ts +109 -0
  328. package/dist/version-selector/index.ts +12 -0
  329. package/dist/version-selector/version-selector.styles.ts +41 -0
  330. package/dist/version-selector/version-selector.ts +136 -0
  331. package/dist/video-player/index.ts +12 -0
  332. package/dist/video-player/video-player.styles.ts +42 -0
  333. package/dist/video-player/video-player.ts +100 -0
  334. package/package.json +1 -1
@@ -0,0 +1,711 @@
1
+ /**
2
+ * Serialization — convert documents to/from Markdown, HTML, and plain text.
3
+ */
4
+
5
+ import type {
6
+ Document,
7
+ Block,
8
+ InlineContent,
9
+ TextSegment,
10
+ Mark,
11
+ MarkType,
12
+ BlockType,
13
+ } from './model.js'
14
+ import {
15
+ createDocument,
16
+ createBlock,
17
+ createTextSegment,
18
+ createMentionSegment,
19
+ createEmojiSegment,
20
+ normalizeContent,
21
+ } from './model.js'
22
+ import { EMOJI_MAP } from './emoji.js'
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // To Markdown
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function marksToMarkdown(text: string, marks: Mark[]): string {
29
+ let result = text
30
+
31
+ // Sort marks so that wrapping is consistent: link outermost, then bold, italic, etc.
32
+ const sorted = [...marks].sort((a, b) => {
33
+ const order: MarkType[] = ['link', 'bold', 'italic', 'strikethrough', 'code', 'underline']
34
+ return order.indexOf(a.type) - order.indexOf(b.type)
35
+ })
36
+
37
+ for (const mark of sorted) {
38
+ switch (mark.type) {
39
+ case 'bold':
40
+ result = `**${result}**`
41
+ break
42
+ case 'italic':
43
+ result = `*${result}*`
44
+ break
45
+ case 'strikethrough':
46
+ result = `~~${result}~~`
47
+ break
48
+ case 'code':
49
+ result = `\`${result}\``
50
+ break
51
+ case 'underline':
52
+ // No standard markdown for underline; use HTML
53
+ result = `<u>${result}</u>`
54
+ break
55
+ case 'link':
56
+ result = `[${result}](${mark.attrs?.href ?? ''})`
57
+ break
58
+ }
59
+ }
60
+
61
+ return result
62
+ }
63
+
64
+ function inlineContentToMarkdown(content: InlineContent[]): string {
65
+ return content
66
+ .map((seg) => {
67
+ if (seg.type === 'text') {
68
+ if (seg.text === '' && seg.marks.length === 0) return ''
69
+ return marksToMarkdown(seg.text, seg.marks)
70
+ }
71
+ if (seg.type === 'mention') {
72
+ return seg.label
73
+ }
74
+ if (seg.type === 'emoji') {
75
+ return seg.shortcode
76
+ }
77
+ return ''
78
+ })
79
+ .join('')
80
+ }
81
+
82
+ function blockToMarkdown(block: Block): string {
83
+ const content = inlineContentToMarkdown(block.content)
84
+
85
+ switch (block.type) {
86
+ case 'paragraph':
87
+ return content
88
+ case 'heading': {
89
+ const level = (block.meta?.level as number) ?? 1
90
+ const prefix = '#'.repeat(level)
91
+ return `${prefix} ${content}`
92
+ }
93
+ case 'blockquote':
94
+ return `> ${content}`
95
+ case 'code-block': {
96
+ const lang = (block.meta?.language as string) ?? ''
97
+ return `\`\`\`${lang}\n${content}\n\`\`\``
98
+ }
99
+ case 'list-item': {
100
+ const listType = block.meta?.listType as string
101
+ const indent = ' '.repeat(block.indent ?? 0)
102
+ if (listType === 'ordered') {
103
+ return `${indent}1. ${content}`
104
+ }
105
+ return `${indent}- ${content}`
106
+ }
107
+ case 'divider':
108
+ return '---'
109
+ case 'image': {
110
+ const src = (block.meta?.src as string) ?? ''
111
+ const alt = (block.meta?.alt as string) ?? ''
112
+ return `![${alt}](${src})`
113
+ }
114
+ default:
115
+ return content
116
+ }
117
+ }
118
+
119
+ export function toMarkdown(doc: Document): string {
120
+ return doc.blocks.map(blockToMarkdown).join('\n\n')
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // From Markdown
125
+ // ---------------------------------------------------------------------------
126
+
127
+ function parseInlineMarkdown(text: string): InlineContent[] {
128
+ const segments: InlineContent[] = []
129
+ let remaining = text
130
+
131
+ // Process inline patterns
132
+ // We use a priority-based approach: longer/more complex patterns first
133
+ while (remaining.length > 0) {
134
+ let matched = false
135
+
136
+ // Bold: **text** or __text__
137
+ const boldMatch = remaining.match(/^\*\*(.+?)\*\*/) || remaining.match(/^__(.+?)__/)
138
+ if (boldMatch) {
139
+ segments.push(createTextSegment(boldMatch[1], [{ type: 'bold' }]))
140
+ remaining = remaining.slice(boldMatch[0].length)
141
+ matched = true
142
+ continue
143
+ }
144
+
145
+ // Strikethrough: ~~text~~
146
+ const strikeMatch = remaining.match(/^~~(.+?)~~/)
147
+ if (strikeMatch) {
148
+ segments.push(createTextSegment(strikeMatch[1], [{ type: 'strikethrough' }]))
149
+ remaining = remaining.slice(strikeMatch[0].length)
150
+ matched = true
151
+ continue
152
+ }
153
+
154
+ // Italic: *text* or _text_
155
+ const italicMatch = remaining.match(/^\*(.+?)\*/) || remaining.match(/^_(.+?)_/)
156
+ if (italicMatch) {
157
+ segments.push(createTextSegment(italicMatch[1], [{ type: 'italic' }]))
158
+ remaining = remaining.slice(italicMatch[0].length)
159
+ matched = true
160
+ continue
161
+ }
162
+
163
+ // Inline code: `text`
164
+ const codeMatch = remaining.match(/^`(.+?)`/)
165
+ if (codeMatch) {
166
+ segments.push(createTextSegment(codeMatch[1], [{ type: 'code' }]))
167
+ remaining = remaining.slice(codeMatch[0].length)
168
+ matched = true
169
+ continue
170
+ }
171
+
172
+ // Link: [text](url)
173
+ const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/)
174
+ if (linkMatch) {
175
+ segments.push(
176
+ createTextSegment(linkMatch[1], [{ type: 'link', attrs: { href: linkMatch[2] } }]),
177
+ )
178
+ remaining = remaining.slice(linkMatch[0].length)
179
+ matched = true
180
+ continue
181
+ }
182
+
183
+ // Mention: @username (word characters after @)
184
+ const mentionMatch = remaining.match(/^@(\w+)/)
185
+ if (mentionMatch) {
186
+ segments.push(createMentionSegment(mentionMatch[1], `@${mentionMatch[1]}`))
187
+ remaining = remaining.slice(mentionMatch[0].length)
188
+ matched = true
189
+ continue
190
+ }
191
+
192
+ // Emoji shortcode: :name:
193
+ const emojiMatch = remaining.match(/^:([a-z0-9_]+):/)
194
+ if (emojiMatch) {
195
+ const shortcode = `:${emojiMatch[1]}:`
196
+ const unicode = EMOJI_MAP[shortcode]
197
+ if (unicode) {
198
+ segments.push(createEmojiSegment(shortcode, unicode))
199
+ remaining = remaining.slice(emojiMatch[0].length)
200
+ matched = true
201
+ continue
202
+ }
203
+ }
204
+
205
+ // Underline (HTML): <u>text</u>
206
+ const underlineMatch = remaining.match(/^<u>(.+?)<\/u>/)
207
+ if (underlineMatch) {
208
+ segments.push(createTextSegment(underlineMatch[1], [{ type: 'underline' }]))
209
+ remaining = remaining.slice(underlineMatch[0].length)
210
+ matched = true
211
+ continue
212
+ }
213
+
214
+ if (!matched) {
215
+ // Consume one character as plain text
216
+ // Aggregate consecutive plain characters
217
+ let endIdx = 1
218
+ while (endIdx < remaining.length) {
219
+ const ch = remaining[endIdx]
220
+ if (ch === '*' || ch === '~' || ch === '`' || ch === '[' || ch === '@' || ch === ':' || ch === '_' || ch === '<') break
221
+ endIdx++
222
+ }
223
+ segments.push(createTextSegment(remaining.slice(0, endIdx)))
224
+ remaining = remaining.slice(endIdx)
225
+ }
226
+ }
227
+
228
+ return segments.length > 0 ? normalizeContent(segments) : [createTextSegment('')]
229
+ }
230
+
231
+ function parseMarkdownLine(line: string, isInCodeBlock: boolean): Block | null {
232
+ if (isInCodeBlock) return null // handled by caller
233
+
234
+ // Heading
235
+ const headingMatch = line.match(/^(#{1,3}) (.*)/)
236
+ if (headingMatch) {
237
+ const level = headingMatch[1].length as 1 | 2 | 3
238
+ return createBlock('heading', parseInlineMarkdown(headingMatch[2]), { level })
239
+ }
240
+
241
+ // Blockquote
242
+ const quoteMatch = line.match(/^> (.*)/)
243
+ if (quoteMatch) {
244
+ return createBlock('blockquote', parseInlineMarkdown(quoteMatch[1]))
245
+ }
246
+
247
+ // Divider
248
+ if (/^---+$/.test(line.trim())) {
249
+ return createBlock('divider')
250
+ }
251
+
252
+ // Ordered list
253
+ const orderedMatch = line.match(/^(\s*)(\d+)\. (.*)/)
254
+ if (orderedMatch) {
255
+ const indent = Math.floor(orderedMatch[1].length / 2)
256
+ const block = createBlock('list-item', parseInlineMarkdown(orderedMatch[3]), { listType: 'ordered' })
257
+ if (indent > 0) block.indent = indent
258
+ return block
259
+ }
260
+
261
+ // Unordered list
262
+ const unorderedMatch = line.match(/^(\s*)[-*] (.*)/)
263
+ if (unorderedMatch) {
264
+ const indent = Math.floor(unorderedMatch[1].length / 2)
265
+ const block = createBlock('list-item', parseInlineMarkdown(unorderedMatch[2]), { listType: 'bullet' })
266
+ if (indent > 0) block.indent = indent
267
+ return block
268
+ }
269
+
270
+ // Image
271
+ const imageMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)/)
272
+ if (imageMatch) {
273
+ return createBlock('image', [createTextSegment('')], { alt: imageMatch[1], src: imageMatch[2] })
274
+ }
275
+
276
+ // Empty line
277
+ if (line.trim() === '') {
278
+ return null
279
+ }
280
+
281
+ // Paragraph (default)
282
+ return createBlock('paragraph', parseInlineMarkdown(line))
283
+ }
284
+
285
+ export function fromMarkdown(markdown: string): Document {
286
+ const lines = markdown.split('\n')
287
+ const blocks: Block[] = []
288
+ let i = 0
289
+
290
+ while (i < lines.length) {
291
+ const line = lines[i]
292
+
293
+ // Code block
294
+ if (line.startsWith('```')) {
295
+ const lang = line.slice(3).trim()
296
+ const codeLines: string[] = []
297
+ i++
298
+ while (i < lines.length && !lines[i].startsWith('```')) {
299
+ codeLines.push(lines[i])
300
+ i++
301
+ }
302
+ i++ // skip closing ```
303
+ blocks.push(
304
+ createBlock('code-block', [createTextSegment(codeLines.join('\n'))], lang ? { language: lang } : undefined),
305
+ )
306
+ continue
307
+ }
308
+
309
+ const block = parseMarkdownLine(line, false)
310
+ if (block) {
311
+ blocks.push(block)
312
+ }
313
+ i++
314
+ }
315
+
316
+ return createDocument(blocks.length > 0 ? blocks : undefined)
317
+ }
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // To HTML
321
+ // ---------------------------------------------------------------------------
322
+
323
+ function escapeHtml(text: string): string {
324
+ return text
325
+ .replace(/&/g, '&amp;')
326
+ .replace(/</g, '&lt;')
327
+ .replace(/>/g, '&gt;')
328
+ .replace(/"/g, '&quot;')
329
+ }
330
+
331
+ function marksToHtml(text: string, marks: Mark[]): string {
332
+ let result = escapeHtml(text)
333
+
334
+ for (const mark of marks) {
335
+ switch (mark.type) {
336
+ case 'bold':
337
+ result = `<strong>${result}</strong>`
338
+ break
339
+ case 'italic':
340
+ result = `<em>${result}</em>`
341
+ break
342
+ case 'strikethrough':
343
+ result = `<del>${result}</del>`
344
+ break
345
+ case 'code':
346
+ result = `<code>${result}</code>`
347
+ break
348
+ case 'underline':
349
+ result = `<u>${result}</u>`
350
+ break
351
+ case 'link':
352
+ result = `<a href="${escapeHtml(mark.attrs?.href ?? '')}">${result}</a>`
353
+ break
354
+ }
355
+ }
356
+
357
+ return result
358
+ }
359
+
360
+ function inlineContentToHtml(content: InlineContent[]): string {
361
+ return content
362
+ .map((seg) => {
363
+ if (seg.type === 'text') {
364
+ return marksToHtml(seg.text, seg.marks)
365
+ }
366
+ if (seg.type === 'mention') {
367
+ return `<span class="mention" data-mention-id="${escapeHtml(seg.id)}">${escapeHtml(seg.label)}</span>`
368
+ }
369
+ if (seg.type === 'emoji') {
370
+ return `<span class="emoji" data-shortcode="${escapeHtml(seg.shortcode)}">${seg.unicode}</span>`
371
+ }
372
+ return ''
373
+ })
374
+ .join('')
375
+ }
376
+
377
+ function blockToHtml(block: Block): string {
378
+ const content = inlineContentToHtml(block.content)
379
+
380
+ switch (block.type) {
381
+ case 'paragraph':
382
+ return `<p>${content}</p>`
383
+ case 'heading': {
384
+ const level = (block.meta?.level as number) ?? 1
385
+ return `<h${level}>${content}</h${level}>`
386
+ }
387
+ case 'blockquote':
388
+ return `<blockquote>${content}</blockquote>`
389
+ case 'code-block': {
390
+ const lang = block.meta?.language as string
391
+ const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : ''
392
+ return `<pre><code${langAttr}>${content}</code></pre>`
393
+ }
394
+ case 'list-item':
395
+ return `<li>${content}</li>`
396
+ case 'divider':
397
+ return '<hr>'
398
+ case 'image': {
399
+ const src = (block.meta?.src as string) ?? ''
400
+ const alt = (block.meta?.alt as string) ?? ''
401
+ return `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}">`
402
+ }
403
+ default:
404
+ return `<p>${content}</p>`
405
+ }
406
+ }
407
+
408
+ export function toHTML(doc: Document): string {
409
+ // Group list items into <ul> or <ol>
410
+ const parts: string[] = []
411
+ let i = 0
412
+
413
+ while (i < doc.blocks.length) {
414
+ const block = doc.blocks[i]
415
+
416
+ if (block.type === 'list-item') {
417
+ const listType = block.meta?.listType === 'ordered' ? 'ol' : 'ul'
418
+ const items: string[] = []
419
+ while (i < doc.blocks.length && doc.blocks[i].type === 'list-item') {
420
+ items.push(blockToHtml(doc.blocks[i]))
421
+ i++
422
+ }
423
+ parts.push(`<${listType}>${items.join('')}</${listType}>`)
424
+ } else {
425
+ parts.push(blockToHtml(block))
426
+ i++
427
+ }
428
+ }
429
+
430
+ return parts.join('')
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // From HTML (basic)
435
+ // ---------------------------------------------------------------------------
436
+
437
+ function parseHtmlInlineContent(html: string): InlineContent[] {
438
+ const segments: InlineContent[] = []
439
+ let remaining = html
440
+
441
+ while (remaining.length > 0) {
442
+ let matched = false
443
+
444
+ // <strong>text</strong>
445
+ const boldMatch = remaining.match(/^<strong>(.*?)<\/strong>/)
446
+ if (boldMatch) {
447
+ segments.push(createTextSegment(unescapeHtml(boldMatch[1]), [{ type: 'bold' }]))
448
+ remaining = remaining.slice(boldMatch[0].length)
449
+ matched = true
450
+ continue
451
+ }
452
+
453
+ // <em>text</em>
454
+ const emMatch = remaining.match(/^<em>(.*?)<\/em>/)
455
+ if (emMatch) {
456
+ segments.push(createTextSegment(unescapeHtml(emMatch[1]), [{ type: 'italic' }]))
457
+ remaining = remaining.slice(emMatch[0].length)
458
+ matched = true
459
+ continue
460
+ }
461
+
462
+ // <del>text</del>
463
+ const delMatch = remaining.match(/^<del>(.*?)<\/del>/)
464
+ if (delMatch) {
465
+ segments.push(createTextSegment(unescapeHtml(delMatch[1]), [{ type: 'strikethrough' }]))
466
+ remaining = remaining.slice(delMatch[0].length)
467
+ matched = true
468
+ continue
469
+ }
470
+
471
+ // <code>text</code>
472
+ const codeMatch = remaining.match(/^<code>(.*?)<\/code>/)
473
+ if (codeMatch) {
474
+ segments.push(createTextSegment(unescapeHtml(codeMatch[1]), [{ type: 'code' }]))
475
+ remaining = remaining.slice(codeMatch[0].length)
476
+ matched = true
477
+ continue
478
+ }
479
+
480
+ // <u>text</u>
481
+ const uMatch = remaining.match(/^<u>(.*?)<\/u>/)
482
+ if (uMatch) {
483
+ segments.push(createTextSegment(unescapeHtml(uMatch[1]), [{ type: 'underline' }]))
484
+ remaining = remaining.slice(uMatch[0].length)
485
+ matched = true
486
+ continue
487
+ }
488
+
489
+ // <a href="url">text</a>
490
+ const linkMatch = remaining.match(/^<a href="([^"]*)">(.*?)<\/a>/)
491
+ if (linkMatch) {
492
+ segments.push(
493
+ createTextSegment(unescapeHtml(linkMatch[2]), [{ type: 'link', attrs: { href: unescapeHtml(linkMatch[1]) } }]),
494
+ )
495
+ remaining = remaining.slice(linkMatch[0].length)
496
+ matched = true
497
+ continue
498
+ }
499
+
500
+ // <span class="mention" data-mention-id="id">label</span>
501
+ const mentionMatch = remaining.match(
502
+ /^<span class="mention" data-mention-id="([^"]*)">(.*?)<\/span>/,
503
+ )
504
+ if (mentionMatch) {
505
+ segments.push(createMentionSegment(unescapeHtml(mentionMatch[1]), unescapeHtml(mentionMatch[2])))
506
+ remaining = remaining.slice(mentionMatch[0].length)
507
+ matched = true
508
+ continue
509
+ }
510
+
511
+ // <span class="emoji" data-shortcode=":name:">unicode</span>
512
+ const emojiMatch = remaining.match(
513
+ /^<span class="emoji" data-shortcode="([^"]*)">(.*?)<\/span>/,
514
+ )
515
+ if (emojiMatch) {
516
+ segments.push(createEmojiSegment(unescapeHtml(emojiMatch[1]), emojiMatch[2]))
517
+ remaining = remaining.slice(emojiMatch[0].length)
518
+ matched = true
519
+ continue
520
+ }
521
+
522
+ if (!matched) {
523
+ // Plain text until next tag
524
+ const nextTagIdx = remaining.indexOf('<', 1)
525
+ if (nextTagIdx === -1) {
526
+ segments.push(createTextSegment(unescapeHtml(remaining)))
527
+ break
528
+ }
529
+ // Check if the first char is '<' (unknown tag)
530
+ if (remaining[0] === '<') {
531
+ // Skip unknown tag
532
+ const closeIdx = remaining.indexOf('>')
533
+ if (closeIdx !== -1) {
534
+ remaining = remaining.slice(closeIdx + 1)
535
+ } else {
536
+ remaining = remaining.slice(1)
537
+ }
538
+ } else {
539
+ segments.push(createTextSegment(unescapeHtml(remaining.slice(0, nextTagIdx))))
540
+ remaining = remaining.slice(nextTagIdx)
541
+ }
542
+ }
543
+ }
544
+
545
+ return segments.length > 0 ? normalizeContent(segments) : [createTextSegment('')]
546
+ }
547
+
548
+ function unescapeHtml(text: string): string {
549
+ return text
550
+ .replace(/&amp;/g, '&')
551
+ .replace(/&lt;/g, '<')
552
+ .replace(/&gt;/g, '>')
553
+ .replace(/&quot;/g, '"')
554
+ }
555
+
556
+ export function fromHTML(html: string): Document {
557
+ const blocks: Block[] = []
558
+ let remaining = html
559
+
560
+ while (remaining.length > 0) {
561
+ let matched = false
562
+
563
+ // <h1>...</h1>, <h2>...</h2>, <h3>...</h3>
564
+ const headingMatch = remaining.match(/^<h([123])>(.*?)<\/h\1>/)
565
+ if (headingMatch) {
566
+ const level = parseInt(headingMatch[1]) as 1 | 2 | 3
567
+ blocks.push(createBlock('heading', parseHtmlInlineContent(headingMatch[2]), { level }))
568
+ remaining = remaining.slice(headingMatch[0].length)
569
+ matched = true
570
+ continue
571
+ }
572
+
573
+ // <p>...</p>
574
+ const pMatch = remaining.match(/^<p>(.*?)<\/p>/)
575
+ if (pMatch) {
576
+ blocks.push(createBlock('paragraph', parseHtmlInlineContent(pMatch[1])))
577
+ remaining = remaining.slice(pMatch[0].length)
578
+ matched = true
579
+ continue
580
+ }
581
+
582
+ // <blockquote>...</blockquote>
583
+ const bqMatch = remaining.match(/^<blockquote>(.*?)<\/blockquote>/)
584
+ if (bqMatch) {
585
+ blocks.push(createBlock('blockquote', parseHtmlInlineContent(bqMatch[1])))
586
+ remaining = remaining.slice(bqMatch[0].length)
587
+ matched = true
588
+ continue
589
+ }
590
+
591
+ // <pre><code...>...</code></pre>
592
+ const preMatch = remaining.match(/^<pre><code(?: class="language-([^"]*)")?>(.*?)<\/code><\/pre>/)
593
+ if (preMatch) {
594
+ const lang = preMatch[1]
595
+ blocks.push(
596
+ createBlock('code-block', [createTextSegment(unescapeHtml(preMatch[2]))], lang ? { language: lang } : undefined),
597
+ )
598
+ remaining = remaining.slice(preMatch[0].length)
599
+ matched = true
600
+ continue
601
+ }
602
+
603
+ // <ul>...</ul>
604
+ const ulMatch = remaining.match(/^<ul>(.*?)<\/ul>/)
605
+ if (ulMatch) {
606
+ const liMatches = ulMatch[1].matchAll(/<li>(.*?)<\/li>/g)
607
+ for (const li of liMatches) {
608
+ blocks.push(createBlock('list-item', parseHtmlInlineContent(li[1]), { listType: 'bullet' }))
609
+ }
610
+ remaining = remaining.slice(ulMatch[0].length)
611
+ matched = true
612
+ continue
613
+ }
614
+
615
+ // <ol>...</ol>
616
+ const olMatch = remaining.match(/^<ol>(.*?)<\/ol>/)
617
+ if (olMatch) {
618
+ const liMatches = olMatch[1].matchAll(/<li>(.*?)<\/li>/g)
619
+ for (const li of liMatches) {
620
+ blocks.push(createBlock('list-item', parseHtmlInlineContent(li[1]), { listType: 'ordered' }))
621
+ }
622
+ remaining = remaining.slice(olMatch[0].length)
623
+ matched = true
624
+ continue
625
+ }
626
+
627
+ // <hr> or <hr/>
628
+ const hrMatch = remaining.match(/^<hr\s*\/?>/)
629
+ if (hrMatch) {
630
+ blocks.push(createBlock('divider'))
631
+ remaining = remaining.slice(hrMatch[0].length)
632
+ matched = true
633
+ continue
634
+ }
635
+
636
+ // <img src="..." alt="...">
637
+ const imgMatch = remaining.match(/^<img src="([^"]*)" alt="([^"]*)"\s*\/?>/)
638
+ if (imgMatch) {
639
+ blocks.push(createBlock('image', [createTextSegment('')], { src: unescapeHtml(imgMatch[1]), alt: unescapeHtml(imgMatch[2]) }))
640
+ remaining = remaining.slice(imgMatch[0].length)
641
+ matched = true
642
+ continue
643
+ }
644
+
645
+ if (!matched) {
646
+ // Skip whitespace or unknown content
647
+ const nextTagIdx = remaining.indexOf('<', 1)
648
+ if (nextTagIdx === -1) {
649
+ // Remaining text as paragraph
650
+ const trimmed = remaining.trim()
651
+ if (trimmed) {
652
+ blocks.push(createBlock('paragraph', [createTextSegment(trimmed)]))
653
+ }
654
+ break
655
+ }
656
+ if (remaining[0] !== '<') {
657
+ const text = remaining.slice(0, nextTagIdx).trim()
658
+ if (text) {
659
+ blocks.push(createBlock('paragraph', [createTextSegment(text)]))
660
+ }
661
+ remaining = remaining.slice(nextTagIdx)
662
+ } else {
663
+ // Unknown tag, skip
664
+ const closeIdx = remaining.indexOf('>')
665
+ if (closeIdx !== -1) {
666
+ remaining = remaining.slice(closeIdx + 1)
667
+ } else {
668
+ break
669
+ }
670
+ }
671
+ }
672
+ }
673
+
674
+ return createDocument(blocks.length > 0 ? blocks : undefined)
675
+ }
676
+
677
+ // ---------------------------------------------------------------------------
678
+ // To Plain Text
679
+ // ---------------------------------------------------------------------------
680
+
681
+ function blockToPlainText(block: Block): string {
682
+ const content = block.content
683
+ .map((seg) => {
684
+ if (seg.type === 'text') return seg.text
685
+ if (seg.type === 'mention') return seg.label
686
+ if (seg.type === 'emoji') return seg.unicode
687
+ return ''
688
+ })
689
+ .join('')
690
+
691
+ switch (block.type) {
692
+ case 'heading':
693
+ return content
694
+ case 'blockquote':
695
+ return content
696
+ case 'list-item': {
697
+ const indent = ' '.repeat(block.indent ?? 0)
698
+ const listType = block.meta?.listType
699
+ const prefix = listType === 'ordered' ? '1. ' : '- '
700
+ return `${indent}${prefix}${content}`
701
+ }
702
+ case 'divider':
703
+ return '---'
704
+ default:
705
+ return content
706
+ }
707
+ }
708
+
709
+ export function toPlainText(doc: Document): string {
710
+ return doc.blocks.map(blockToPlainText).join('\n')
711
+ }