@jant/core 0.6.14 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -13,9 +13,10 @@ import type { PasteMediaOptions } from "./paste-media.js";
13
13
  import { BubbleMenu } from "./bubble-menu.js";
14
14
  import { LinkToolbar } from "./link-toolbar.js";
15
15
  import { ExitableMarks } from "./exitable-marks.js";
16
- import { TabIndent } from "./tab-indent.js";
16
+ import { CodeBlockIndent } from "./tab-indent.js";
17
+ import { StructuralKeymap } from "./structural-keymap.js";
18
+ import { ContinuousLists } from "./continuous-lists.js";
17
19
  import { LinkInputRules } from "./link-input-rules.js";
18
- import { WrappingInputRules } from "./wrapping-input-rules.js";
19
20
  import { InsertParagraphAround } from "./insert-paragraph-around.js";
20
21
  import { Footnotes } from "./footnotes.js";
21
22
  import type { FormattingToolbarMode } from "./toolbar-mode.js";
@@ -28,6 +29,8 @@ import { RehostImages } from "./rehost-images.js";
28
29
  import type { RehostImagesOptions } from "./rehost-images.js";
29
30
  import { MarkdownClipboard } from "./markdown-clipboard.js";
30
31
  import { ImageInputRules } from "./image-input-rules.js";
32
+ import { TableControls } from "./table-controls.js";
33
+ import type { TableControlLabels } from "./table-control-labels.js";
31
34
  import {
32
35
  MARKDOWN_MARKED_OPTIONS,
33
36
  createMarkdownContentExtensions,
@@ -39,6 +42,7 @@ export interface EditorExtensionOptions {
39
42
  pasteMedia?: PasteMediaOptions;
40
43
  rehostImages?: RehostImagesOptions;
41
44
  imageNodeLabels?: Partial<ImageNodeLabels>;
45
+ tableControlLabels?: TableControlLabels;
42
46
  }
43
47
 
44
48
  /**
@@ -83,10 +87,11 @@ export function createSettingsEditorExtensions(
83
87
  }),
84
88
  ReclaimModEnter,
85
89
  LinkInputRules,
86
- WrappingInputRules,
87
90
  MarkdownClipboard,
88
91
  ExitableMarks,
89
92
  InsertParagraphAround,
93
+ StructuralKeymap,
94
+ ContinuousLists,
90
95
  BubbleMenu.configure({
91
96
  toolbarMode: "compose",
92
97
  }),
@@ -116,9 +121,11 @@ export function createEditorExtensions(
116
121
  Footnotes,
117
122
  ImageInputRules,
118
123
  LinkInputRules,
119
- WrappingInputRules,
120
124
  MarkdownClipboard,
121
125
  SlashCommands,
126
+ ...(options.tableControlLabels
127
+ ? [TableControls.configure({ labels: options.tableControlLabels })]
128
+ : []),
122
129
  EmbedPaste,
123
130
  PasteMedia.configure(options.pasteMedia ?? {}),
124
131
  RehostImages.configure(options.rehostImages ?? {}),
@@ -128,6 +135,8 @@ export function createEditorExtensions(
128
135
  LinkToolbar,
129
136
  ExitableMarks,
130
137
  InsertParagraphAround,
131
- TabIndent,
138
+ StructuralKeymap,
139
+ ContinuousLists,
140
+ CodeBlockIndent,
132
141
  ];
133
142
  }
@@ -1,7 +1,38 @@
1
- import { createNodeFromContent, Extension } from "@tiptap/core";
2
- import { Fragment, Slice } from "@tiptap/pm/model";
3
- import { Plugin } from "@tiptap/pm/state";
4
- import { normalizeMarkdownDocument } from "../../lib/markdown-manager.js";
1
+ import {
2
+ createNodeFromContent,
3
+ Extension,
4
+ getTextBetween,
5
+ getTextSerializersFromSchema,
6
+ type Editor,
7
+ } from "@tiptap/core";
8
+ import {
9
+ Fragment,
10
+ Slice,
11
+ type Node as ProseMirrorNode,
12
+ } from "@tiptap/pm/model";
13
+ import { AllSelection, Plugin } from "@tiptap/pm/state";
14
+ import {
15
+ getFootnoteLabelKey,
16
+ getFootnoteReferenceText,
17
+ normalizeFootnoteLabel,
18
+ } from "../../lib/footnotes.js";
19
+ import {
20
+ normalizeMarkdownDocument,
21
+ serializeMarkdownDocument,
22
+ } from "../../lib/markdown-manager.js";
23
+
24
+ const OBSIDIAN_CLIPBOARD_MARKER = /<!--\s*obsidian\s*-->/i;
25
+ const FOOTNOTE_CONTAINER_SELECTOR =
26
+ 'section.footnotes, [data-footnotes], [role~="doc-endnotes"]';
27
+ const FOOTNOTE_BACKLINK_SELECTOR =
28
+ 'a.footnote-backref, a.footnote-back, a[data-footnote-backref], a[role~="doc-backlink"]';
29
+
30
+ interface HtmlFootnoteDefinition {
31
+ container: globalThis.Element | null;
32
+ element: globalThis.Element;
33
+ label: string;
34
+ targetId: string;
35
+ }
5
36
 
6
37
  function toFragment(
7
38
  content: ReturnType<typeof createNodeFromContent>,
@@ -9,6 +40,52 @@ function toFragment(
9
40
  return content instanceof Fragment ? content : content.content;
10
41
  }
11
42
 
43
+ function selectionCoversDocument(editor: Editor): boolean {
44
+ const { doc, selection } = editor.state;
45
+
46
+ return (
47
+ selection instanceof AllSelection ||
48
+ (selection.from === 0 && selection.to === doc.content.size)
49
+ );
50
+ }
51
+
52
+ function serializeReadableSelection(editor: Editor): string {
53
+ const { doc, schema, selection } = editor.state;
54
+ const textSerializers = getTextSerializersFromSchema(schema);
55
+ const sortedRanges = [...selection.ranges].sort(
56
+ (left, right) => left.$from.pos - right.$from.pos,
57
+ );
58
+
59
+ return sortedRanges
60
+ .map(({ $from, $to }) =>
61
+ getTextBetween(
62
+ doc,
63
+ { from: $from.pos, to: $to.pos },
64
+ { textSerializers },
65
+ ),
66
+ )
67
+ .join("\n\n");
68
+ }
69
+
70
+ /**
71
+ * Serializes clipboard plain text without changing the editor document.
72
+ *
73
+ * A complete document selection expresses an export-like intent, so it uses
74
+ * Jant's canonical Markdown contract. Partial selections retain Tiptap's
75
+ * readable plain-text behavior; rich HTML is serialized separately by
76
+ * ProseMirror and is unaffected by this hook.
77
+ *
78
+ * @param editor - Active Tiptap editor
79
+ * @returns Text to write to the clipboard's `text/plain` flavor
80
+ */
81
+ function serializeClipboardText(editor: Editor): string {
82
+ if (selectionCoversDocument(editor)) {
83
+ return serializeMarkdownDocument(editor.getJSON()).trimEnd();
84
+ }
85
+
86
+ return serializeReadableSelection(editor);
87
+ }
88
+
12
89
  /**
13
90
  * Detects whether pasted HTML originates from a code editor (VS Code,
14
91
  * JetBrains, etc.) rather than a rich-text source. These editors copy
@@ -43,24 +120,544 @@ export function isCodeEditorHtml(html: string): boolean {
43
120
  return false;
44
121
  }
45
122
 
123
+ /**
124
+ * Detects the marker emitted by Obsidian 1.12+ for editor-authored HTML.
125
+ *
126
+ * Obsidian writes the original Markdown to `text/plain` and `text/markdown`,
127
+ * while this comment marks the rendered `text/html` flavor. The marker can be
128
+ * preceded by browser- or OS-inserted metadata, so detection is not anchored.
129
+ *
130
+ * @param html - Clipboard HTML
131
+ * @returns Whether the HTML carries Obsidian's editor clipboard marker
132
+ */
133
+ export function isObsidianHtml(html: string): boolean {
134
+ return OBSIDIAN_CLIPBOARD_MARKER.test(html);
135
+ }
136
+
137
+ function isBareObsidianSpacer(element: globalThis.Element): boolean {
138
+ return (
139
+ element.tagName === "P" &&
140
+ element.attributes.length === 0 &&
141
+ Array.from(element.childNodes).every(
142
+ (child) =>
143
+ child.nodeType === globalThis.Node.TEXT_NODE &&
144
+ /^[\t\n\f\r ]*$/.test(child.textContent ?? ""),
145
+ )
146
+ );
147
+ }
148
+
149
+ function isClipboardMetadataElement(element: globalThis.Element): boolean {
150
+ return element.matches("meta, link, style, title");
151
+ }
152
+
153
+ /**
154
+ * Removes bare top-level spacer paragraphs emitted by Obsidian's HTML flavor.
155
+ *
156
+ * Markdown blank lines separate blocks; they are not authored empty
157
+ * paragraphs. Obsidian's clipboard HTML can materialize those separators as
158
+ * bare `<p></p>` siblings, which ProseMirror would otherwise preserve. The
159
+ * cleanup is deliberately marker-gated and narrow: attributed, nested,
160
+ * media-bearing, `<br>`, non-breaking-space, and edge paragraphs are retained.
161
+ *
162
+ * @param html - Clipboard HTML fragment
163
+ * @returns Normalized HTML, or the original string when no artifact was found
164
+ */
165
+ export function normalizeObsidianClipboardArtifacts(html: string): string {
166
+ if (
167
+ !html.trim() ||
168
+ !isObsidianHtml(html) ||
169
+ typeof document === "undefined"
170
+ ) {
171
+ return html;
172
+ }
173
+
174
+ const template = document.createElement("template");
175
+ template.innerHTML = html;
176
+ const elements = Array.from(template.content.children);
177
+ const spacers = new Set(elements.filter(isBareObsidianSpacer));
178
+ if (spacers.size === 0) return html;
179
+
180
+ const contentElements = elements.filter(
181
+ (element) => !spacers.has(element) && !isClipboardMetadataElement(element),
182
+ );
183
+ const contentIndexes = contentElements.map((element) =>
184
+ elements.indexOf(element),
185
+ );
186
+ let changed = false;
187
+
188
+ for (const spacer of spacers) {
189
+ const index = elements.indexOf(spacer);
190
+ const hasContentBefore = contentIndexes.some(
191
+ (contentIndex) => contentIndex < index,
192
+ );
193
+ const hasContentAfter = contentIndexes.some(
194
+ (contentIndex) => contentIndex > index,
195
+ );
196
+
197
+ if (hasContentBefore && hasContentAfter) {
198
+ spacer.remove();
199
+ changed = true;
200
+ }
201
+ }
202
+
203
+ return changed ? template.innerHTML : html;
204
+ }
205
+
206
+ function getMarkdownClipboardText(clipboardData: DataTransfer): string | null {
207
+ const html = clipboardData.getData("text/html");
208
+ const markdown = clipboardData.getData("text/markdown");
209
+ if (markdown.trim()) return markdown;
210
+
211
+ const plain = clipboardData.getData("text/plain");
212
+ if (!plain.trim()) return null;
213
+
214
+ return isObsidianHtml(html) || isCodeEditorHtml(html) ? plain : null;
215
+ }
216
+
217
+ function hasRole(element: globalThis.Element, role: string): boolean {
218
+ return (element.getAttribute("role") ?? "").split(/\s+/).includes(role);
219
+ }
220
+
221
+ function getHashTarget(anchor: HTMLAnchorElement): string | null {
222
+ const href = anchor.getAttribute("href") ?? "";
223
+ if (!href.startsWith("#") || href.length === 1) return null;
224
+
225
+ const target = href.slice(1);
226
+ try {
227
+ return decodeURIComponent(target);
228
+ } catch {
229
+ return target;
230
+ }
231
+ }
232
+
233
+ function getDefinitionTargetId(element: globalThis.Element): string {
234
+ return (
235
+ element.getAttribute("id") ?? element.getAttribute("data-footnote-id") ?? ""
236
+ );
237
+ }
238
+
239
+ function isNestedList(
240
+ list: globalThis.Element,
241
+ container: globalThis.Element,
242
+ ): boolean {
243
+ let ancestor = list.parentElement;
244
+
245
+ while (ancestor && ancestor !== container) {
246
+ if (ancestor.tagName === "LI") return true;
247
+ ancestor = ancestor.parentElement;
248
+ }
249
+
250
+ return false;
251
+ }
252
+
253
+ function collectContainerDefinitionElements(
254
+ container: globalThis.Element,
255
+ ): globalThis.Element[] {
256
+ const definitions: globalThis.Element[] = [];
257
+
258
+ for (const list of container.querySelectorAll("ol, ul")) {
259
+ if (
260
+ list.closest(FOOTNOTE_CONTAINER_SELECTOR) !== container ||
261
+ isNestedList(list, container)
262
+ ) {
263
+ continue;
264
+ }
265
+
266
+ for (const child of list.children) {
267
+ if (child.tagName === "LI") definitions.push(child);
268
+ }
269
+ }
270
+
271
+ return definitions;
272
+ }
273
+
274
+ function isFootnoteReferenceAnchor(anchor: HTMLAnchorElement): boolean {
275
+ if (anchor.matches(FOOTNOTE_BACKLINK_SELECTOR)) return false;
276
+
277
+ return (
278
+ anchor.hasAttribute("data-footnote-ref") ||
279
+ anchor.classList.contains("footnote-ref") ||
280
+ hasRole(anchor, "doc-noteref") ||
281
+ anchor.closest("sup.footnote-ref") !== null
282
+ );
283
+ }
284
+
285
+ function deriveLabelFromReference(anchor: HTMLAnchorElement): string {
286
+ const sourceLabel =
287
+ anchor.getAttribute("data-footref") ??
288
+ anchor.getAttribute("data-footnote-label");
289
+ if (normalizeFootnoteLabel(sourceLabel)) {
290
+ return normalizeFootnoteLabel(sourceLabel);
291
+ }
292
+
293
+ return normalizeFootnoteLabel(anchor.textContent)
294
+ .replace(/^\[\^?/, "")
295
+ .replace(/\]$/, "");
296
+ }
297
+
298
+ function deriveLabelFromTargetId(targetId: string): string {
299
+ const footnoteSuffix = targetId.match(/(?:^|[-_:])fn[-_:]?(.+)$/i)?.[1];
300
+ return normalizeFootnoteLabel(footnoteSuffix ?? targetId);
301
+ }
302
+
303
+ function allocateUniqueFootnoteLabel(
304
+ candidate: string,
305
+ fallbackIndex: number,
306
+ usedLabels: Set<string>,
307
+ ): string {
308
+ const base = normalizeFootnoteLabel(candidate) || String(fallbackIndex + 1);
309
+ let label = base;
310
+ let suffix = 2;
311
+
312
+ while (usedLabels.has(getFootnoteLabelKey(label))) {
313
+ label = `${base}-${suffix}`;
314
+ suffix += 1;
315
+ }
316
+
317
+ usedLabels.add(getFootnoteLabelKey(label));
318
+ return label;
319
+ }
320
+
321
+ function createCanonicalFootnoteReference(
322
+ ownerDocument: globalThis.Document,
323
+ label: string,
324
+ ): HTMLElement {
325
+ const reference = ownerDocument.createElement("sup");
326
+ reference.setAttribute("data-footnote-reference", "");
327
+ reference.setAttribute("data-footnote-label", label);
328
+ reference.className = "tiptap-footnote-reference";
329
+ reference.textContent = getFootnoteReferenceText(label);
330
+ return reference;
331
+ }
332
+
333
+ function createCanonicalFootnoteDefinition(
334
+ definition: HtmlFootnoteDefinition,
335
+ ): HTMLElement {
336
+ const ownerDocument = definition.element.ownerDocument;
337
+ const source = definition.element.cloneNode(true) as globalThis.Element;
338
+
339
+ for (const backlink of source.querySelectorAll(FOOTNOTE_BACKLINK_SELECTOR)) {
340
+ backlink.remove();
341
+ }
342
+
343
+ for (const wrapper of Array.from(
344
+ source.querySelectorAll("p, span"),
345
+ ).reverse()) {
346
+ if (
347
+ !wrapper.textContent?.trim() &&
348
+ !wrapper.querySelector(
349
+ "img, video, audio, iframe, svg, math, br, hr, input, table",
350
+ )
351
+ ) {
352
+ wrapper.remove();
353
+ }
354
+ }
355
+
356
+ const canonical = ownerDocument.createElement("div");
357
+ canonical.setAttribute("data-footnote-definition", "");
358
+ canonical.setAttribute("data-footnote-label", definition.label);
359
+ canonical.className = "tiptap-footnote-definition";
360
+
361
+ while (source.firstChild) {
362
+ canonical.appendChild(source.firstChild);
363
+ }
364
+
365
+ if (!canonical.hasChildNodes()) {
366
+ canonical.appendChild(ownerDocument.createElement("p"));
367
+ }
368
+
369
+ return canonical;
370
+ }
371
+
372
+ /**
373
+ * Normalizes common rendered-footnote HTML into Jant's structural editor DOM.
374
+ *
375
+ * Supported inputs include Obsidian/markdown-it (`.footnote-ref` and
376
+ * `.footnotes`), GitHub/unified (`data-footnote-*`), and DPUB-ARIA
377
+ * (`doc-noteref`, `doc-footnote`, and `doc-endnotes`). Conversion requires a
378
+ * recognized definition container or role, so ordinary superscript links and
379
+ * ordered lists are left untouched.
380
+ *
381
+ * @param html - Clipboard HTML fragment
382
+ * @returns The normalized fragment, or the original string when no footnotes
383
+ * were recognized
384
+ */
385
+ export function normalizePastedFootnoteHtml(html: string): string {
386
+ if (!html.trim() || typeof document === "undefined") return html;
387
+
388
+ const template = document.createElement("template");
389
+ template.innerHTML = html;
390
+ const root = template.content;
391
+ const containers = Array.from(
392
+ root.querySelectorAll(FOOTNOTE_CONTAINER_SELECTOR),
393
+ ).filter(
394
+ (container) =>
395
+ !container.parentElement?.closest(FOOTNOTE_CONTAINER_SELECTOR),
396
+ );
397
+ const containerDefinitions = new Map<
398
+ globalThis.Element,
399
+ globalThis.Element[]
400
+ >();
401
+ const rawDefinitions: Array<{
402
+ container: globalThis.Element | null;
403
+ element: globalThis.Element;
404
+ targetId: string;
405
+ }> = [];
406
+
407
+ for (const container of containers) {
408
+ const elements = collectContainerDefinitionElements(container);
409
+ if (elements.length === 0) continue;
410
+
411
+ containerDefinitions.set(container, elements);
412
+ for (const element of elements) {
413
+ rawDefinitions.push({
414
+ container,
415
+ element,
416
+ targetId: getDefinitionTargetId(element),
417
+ });
418
+ }
419
+ }
420
+
421
+ for (const element of root.querySelectorAll('[role~="doc-footnote"]')) {
422
+ if (element.closest(FOOTNOTE_CONTAINER_SELECTOR)) continue;
423
+ rawDefinitions.push({
424
+ container: null,
425
+ element,
426
+ targetId: getDefinitionTargetId(element),
427
+ });
428
+ }
429
+
430
+ if (rawDefinitions.length === 0) return html;
431
+
432
+ const definitionsByTarget = new Map<
433
+ string,
434
+ (typeof rawDefinitions)[number]
435
+ >();
436
+ for (const definition of rawDefinitions) {
437
+ if (definition.targetId && !definitionsByTarget.has(definition.targetId)) {
438
+ definitionsByTarget.set(definition.targetId, definition);
439
+ }
440
+ }
441
+
442
+ const referencesByTarget = new Map<string, HTMLAnchorElement[]>();
443
+ for (const anchor of root.querySelectorAll<HTMLAnchorElement>("a[href]")) {
444
+ if (!isFootnoteReferenceAnchor(anchor)) continue;
445
+ const targetId = getHashTarget(anchor);
446
+ if (!targetId || !definitionsByTarget.has(targetId)) continue;
447
+
448
+ const references = referencesByTarget.get(targetId) ?? [];
449
+ references.push(anchor);
450
+ referencesByTarget.set(targetId, references);
451
+ }
452
+
453
+ const usedLabels = new Set<string>();
454
+ const definitions: HtmlFootnoteDefinition[] = rawDefinitions.map(
455
+ (definition, index) => {
456
+ const reference = definition.targetId
457
+ ? referencesByTarget.get(definition.targetId)?.[0]
458
+ : undefined;
459
+ const candidate = reference
460
+ ? deriveLabelFromReference(reference)
461
+ : deriveLabelFromTargetId(definition.targetId);
462
+
463
+ return {
464
+ ...definition,
465
+ label: allocateUniqueFootnoteLabel(candidate, index, usedLabels),
466
+ };
467
+ },
468
+ );
469
+ const labelsByTarget = new Map(
470
+ definitions
471
+ .filter((definition) => definition.targetId)
472
+ .map((definition) => [definition.targetId, definition.label] as const),
473
+ );
474
+
475
+ let changed = false;
476
+ for (const [targetId, anchors] of referencesByTarget) {
477
+ const label = labelsByTarget.get(targetId);
478
+ if (!label) continue;
479
+
480
+ for (const anchor of anchors) {
481
+ const wrapper = anchor.closest("sup");
482
+ const replaceTarget = wrapper?.contains(anchor) ? wrapper : anchor;
483
+ if (!replaceTarget.parentNode) continue;
484
+
485
+ replaceTarget.replaceWith(
486
+ createCanonicalFootnoteReference(anchor.ownerDocument, label),
487
+ );
488
+ changed = true;
489
+ }
490
+ }
491
+
492
+ const normalizedByElement = new Map(
493
+ definitions.map(
494
+ (definition) =>
495
+ [
496
+ definition.element,
497
+ createCanonicalFootnoteDefinition(definition),
498
+ ] as const,
499
+ ),
500
+ );
501
+
502
+ for (const [container, elements] of containerDefinitions) {
503
+ if (!container.parentNode) continue;
504
+
505
+ const replacement = container.ownerDocument.createDocumentFragment();
506
+ for (const element of elements) {
507
+ const definition = normalizedByElement.get(element);
508
+ if (definition) replacement.appendChild(definition);
509
+ }
510
+
511
+ const separator = container.previousElementSibling;
512
+ if (separator?.matches("hr.footnotes-sep, hr[data-footnotes-separator]")) {
513
+ separator.remove();
514
+ }
515
+
516
+ container.replaceWith(replacement);
517
+ changed = true;
518
+ }
519
+
520
+ for (const definition of definitions) {
521
+ if (definition.container || !definition.element.parentNode) continue;
522
+ const normalized = normalizedByElement.get(definition.element);
523
+ if (!normalized) continue;
524
+
525
+ definition.element.replaceWith(normalized);
526
+ changed = true;
527
+ }
528
+
529
+ return changed ? template.innerHTML : html;
530
+ }
531
+
532
+ function normalizePastedHtml(html: string): string {
533
+ return normalizePastedFootnoteHtml(normalizeObsidianClipboardArtifacts(html));
534
+ }
535
+
536
+ interface NormalizedPasteNodes {
537
+ nodes: ProseMirrorNode[];
538
+ changed: boolean;
539
+ }
540
+
541
+ /**
542
+ * Split a paragraph where rich-text producers used consecutive `<br>` nodes
543
+ * to imitate paragraph spacing. A single hard break remains an intentional
544
+ * line break; a run of two or more becomes a semantic block boundary.
545
+ */
546
+ function splitPastedParagraph(node: ProseMirrorNode): NormalizedPasteNodes {
547
+ const children: ProseMirrorNode[] = [];
548
+ node.forEach((child) => children.push(child));
549
+
550
+ const paragraphs: ProseMirrorNode[][] = [];
551
+ let current: ProseMirrorNode[] = [];
552
+ let changed = false;
553
+
554
+ for (let index = 0; index < children.length; index += 1) {
555
+ const child = children[index];
556
+ if (child?.type.name !== "hardBreak") {
557
+ if (child) current.push(child);
558
+ continue;
559
+ }
560
+
561
+ let runEnd = index + 1;
562
+ while (children[runEnd]?.type.name === "hardBreak") runEnd += 1;
563
+
564
+ if (runEnd - index === 1) {
565
+ current.push(child);
566
+ continue;
567
+ }
568
+
569
+ changed = true;
570
+ if (current.length > 0) paragraphs.push(current);
571
+ current = [];
572
+ index = runEnd - 1;
573
+ }
574
+
575
+ if (!changed) return { nodes: [node], changed: false };
576
+ if (current.length > 0) paragraphs.push(current);
577
+
578
+ // Keep a valid empty paragraph when the pasted block contained only visual
579
+ // spacing (for example `<p><br><br></p>`).
580
+ if (paragraphs.length === 0) paragraphs.push([]);
581
+
582
+ return {
583
+ nodes: paragraphs.map((content) => node.copy(Fragment.fromArray(content))),
584
+ changed: true,
585
+ };
586
+ }
587
+
588
+ /**
589
+ * Normalize paragraph-like breaks inside a pasted rich-text tree while
590
+ * retaining the surrounding list, quote, table, and inline-mark structure.
591
+ */
592
+ function normalizePastedNode(node: ProseMirrorNode): NormalizedPasteNodes {
593
+ if (node.type.name === "paragraph") return splitPastedParagraph(node);
594
+ if (node.isLeaf || node.childCount === 0) {
595
+ return { nodes: [node], changed: false };
596
+ }
597
+
598
+ const children: ProseMirrorNode[] = [];
599
+ let changed = false;
600
+ node.forEach((child) => {
601
+ const normalized = normalizePastedNode(child);
602
+ children.push(...normalized.nodes);
603
+ changed ||= normalized.changed;
604
+ });
605
+
606
+ if (!changed) return { nodes: [node], changed: false };
607
+
608
+ const content = Fragment.fromArray(children);
609
+ if (!node.type.validContent(content)) {
610
+ return { nodes: [node], changed: false };
611
+ }
612
+
613
+ return { nodes: [node.copy(content)], changed: true };
614
+ }
615
+
616
+ function normalizePastedSlice(slice: Slice, plain: boolean): Slice {
617
+ if (plain) return slice;
618
+
619
+ const nodes: ProseMirrorNode[] = [];
620
+ let changed = false;
621
+ slice.content.forEach((node) => {
622
+ const normalized = normalizePastedNode(node);
623
+ nodes.push(...normalized.nodes);
624
+ changed ||= normalized.changed;
625
+ });
626
+
627
+ return changed
628
+ ? new Slice(Fragment.fromArray(nodes), slice.openStart, slice.openEnd)
629
+ : slice;
630
+ }
631
+
46
632
  export const MarkdownClipboard = Extension.create({
47
633
  name: "markdownClipboard",
48
634
 
635
+ // Run before Tiptap's built-in plain-text serializer. Paste handling is also
636
+ // intentionally resolved before generic clipboard parsing.
637
+ priority: 1000,
638
+
49
639
  addProseMirrorPlugins() {
50
640
  return [
51
641
  new Plugin({
52
642
  props: {
643
+ clipboardTextSerializer: () => serializeClipboardText(this.editor),
644
+
645
+ transformPastedHTML: normalizePastedHtml,
646
+ transformPasted: (slice, _view, plain) =>
647
+ normalizePastedSlice(slice, plain),
648
+
53
649
  /**
54
- * When pasting from a code editor, discard the HTML and re-insert
55
- * the plain text so it flows through `clipboardTextParser` below
56
- * and gets parsed as markdown.
650
+ * Prefer an explicit Markdown flavor. Marked Obsidian and detected
651
+ * code-editor HTML use their plain flavor as Markdown; other rich-text
652
+ * producers keep the normal HTML path.
57
653
  */
58
654
  handlePaste: (view, event) => {
59
- const html = event.clipboardData?.getData("text/html") ?? "";
60
- if (!html || !isCodeEditorHtml(html)) return false;
655
+ const clipboardData = event.clipboardData;
656
+ if (!clipboardData || typeof clipboardData.getData !== "function") {
657
+ return false;
658
+ }
61
659
 
62
- const text =
63
- event.clipboardData?.getData("text/plain")?.trim() ?? "";
660
+ const text = getMarkdownClipboardText(clipboardData);
64
661
  if (!text || !this.editor.markdown) return false;
65
662
 
66
663
  const parsed = normalizeMarkdownDocument(