@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -733,6 +733,93 @@ describe("Timeline data assembly", () => {
733
733
  ]);
734
734
  });
735
735
 
736
+ // A reply can be older than its root (a post moved into the Thread keeps
737
+ // its creation time; an export without `created` dates posts by
738
+ // publication). The Thread still opens on its root.
739
+ async function createThreadWithOlderReplies() {
740
+ const root = await postService.create({
741
+ format: "note",
742
+ bodyMarkdown: "Root",
743
+ featured: true,
744
+ publishedAt: 3000,
745
+ });
746
+ const first = await postService.create({
747
+ format: "note",
748
+ bodyMarkdown: "First reply",
749
+ replyToId: root.id,
750
+ publishedAt: 1000,
751
+ });
752
+ const second = await postService.create({
753
+ format: "note",
754
+ bodyMarkdown: "Second reply",
755
+ replyToId: first.id,
756
+ publishedAt: 2000,
757
+ });
758
+ for (const [id, createdAt] of [
759
+ [root.id, 3000],
760
+ [first.id, 1000],
761
+ [second.id, 2000],
762
+ ] as const) {
763
+ await db.update(postTable).set({ createdAt }).where(eq(postTable.id, id));
764
+ }
765
+ return { root, first, second };
766
+ }
767
+
768
+ it("keeps a Featured Thread whose replies are older than its root", async () => {
769
+ const { root, second } = await createThreadWithOlderReplies();
770
+
771
+ const result = await assembleFeaturedTimeline(createTimelineContext(), {
772
+ isAuthenticated: true,
773
+ });
774
+
775
+ expect(result.items).toHaveLength(1);
776
+ expect(result.items[0]?.post.id).toBe(root.id);
777
+ expect(result.items[0]?.curatedThread?.segments).toEqual([
778
+ expect.objectContaining({
779
+ post: expect.objectContaining({ id: root.id }),
780
+ hiddenBeforeCount: 0,
781
+ highlighted: true,
782
+ }),
783
+ expect.objectContaining({
784
+ post: expect.objectContaining({ id: second.id }),
785
+ hiddenBeforeCount: 1,
786
+ highlighted: false,
787
+ }),
788
+ ]);
789
+ });
790
+
791
+ it("renders a collected Thread whose replies are older than its root", async () => {
792
+ const collection = await collectionService.create({
793
+ slug: "older-replies",
794
+ title: "Older replies",
795
+ });
796
+ const { root, first, second } = await createThreadWithOlderReplies();
797
+ await collectionService.addThread(collection.id, root.id);
798
+
799
+ const result = await assembleCollectionTimeline(createTimelineContext(), {
800
+ collectionIds: [collection.id],
801
+ isAuthenticated: true,
802
+ sortOrder: "newest",
803
+ });
804
+
805
+ expect(result.items).toHaveLength(1);
806
+ expect(result.items[0]?.post.id).toBe(root.id);
807
+ expect(result.items[0]?.curatedThread?.segments).toEqual([
808
+ expect.objectContaining({
809
+ post: expect.objectContaining({ id: root.id }),
810
+ highlighted: false,
811
+ }),
812
+ expect.objectContaining({
813
+ post: expect.objectContaining({ id: first.id }),
814
+ highlighted: false,
815
+ }),
816
+ expect.objectContaining({
817
+ post: expect.objectContaining({ id: second.id }),
818
+ highlighted: true,
819
+ }),
820
+ ]);
821
+ });
822
+
736
823
  it("deduplicates Threads selected through multiple collections", async () => {
737
824
  const smart = await collectionService.create({
738
825
  slug: "smart",
@@ -361,6 +361,154 @@ describe("tiptapJsonToMarkdown", () => {
361
361
  );
362
362
  expect(tiptapJsonToMarkdown(json)).toBe("Line 1 \nLine 2");
363
363
  });
364
+
365
+ // Trailing spaces on a line with nothing else on it make a blank line,
366
+ // which ends the paragraph (or leaves a list item empty).
367
+ it("keeps two hard breaks in a row inside one paragraph", () => {
368
+ const br = { type: "hardBreak" };
369
+ const json = doc(p(text("A"), br, br, text("B")));
370
+ const markdown = tiptapJsonToMarkdown(json);
371
+ expect(markdown).toBe("A \n\\\nB");
372
+ expect(JSON.parse(markdownToTiptapJson(markdown))).toEqual(
373
+ JSON.parse(json),
374
+ );
375
+ });
376
+
377
+ it("keeps a list item that opens with a hard break in the list", () => {
378
+ const json = doc({
379
+ type: "bulletList",
380
+ content: [
381
+ {
382
+ type: "listItem",
383
+ content: [p({ type: "hardBreak" }, text("Ships in 3–7 days"))],
384
+ },
385
+ { type: "listItem", content: [p(text("Next"))] },
386
+ ],
387
+ });
388
+ const markdown = tiptapJsonToMarkdown(json);
389
+ expect(JSON.parse(markdownToTiptapJson(markdown))).toEqual(
390
+ JSON.parse(json),
391
+ );
392
+ });
393
+ });
394
+
395
+ // CommonMark only closes `**` after punctuation when a space or another
396
+ // punctuation mark follows, so `**句子。**后文` stays literal asterisks in
397
+ // Hugo and in Jant. Those runs use HTML tags, which both read.
398
+ describe("emphasis a delimiter can't open or close", () => {
399
+ const bold = [{ type: "bold" }];
400
+
401
+ it.each([
402
+ [
403
+ "bold ending in CJK punctuation before a letter",
404
+ [text("而是:"), text("固定时间,一群人。", bold), text("来的人")],
405
+ "而是:<strong>固定时间,一群人。</strong>来的人",
406
+ ],
407
+ [
408
+ "bold opening on CJK punctuation after a letter",
409
+ [text("说"), text("「引用」", bold), text("后")],
410
+ "说<strong>「引用」</strong>后",
411
+ ],
412
+ [
413
+ "italic ending in punctuation before a letter",
414
+ [text("这是"), text("强调。", [{ type: "italic" }]), text("后面")],
415
+ "这是<em>强调。</em>后面",
416
+ ],
417
+ [
418
+ "strike ending in punctuation before a letter",
419
+ [text("价格"), text("100元。", [{ type: "strike" }]), text("现在")],
420
+ "价格<s>100元。</s>现在",
421
+ ],
422
+ [
423
+ "English bold ending in a period before a letter",
424
+ [text("Say "), text("hello.", bold), text("World")],
425
+ "Say <strong>hello.</strong>World",
426
+ ],
427
+ ])("writes %s as HTML", (_label, content, expected) => {
428
+ const json = doc(p(...content));
429
+ expect(tiptapJsonToMarkdown(json)).toBe(expected);
430
+ expect(JSON.parse(markdownToTiptapJson(expected))).toEqual(
431
+ JSON.parse(json),
432
+ );
433
+ });
434
+
435
+ it.each([
436
+ [
437
+ "between spaces",
438
+ [text("a "), text("bold", bold), text(" b")],
439
+ "a **bold** b",
440
+ ],
441
+ [
442
+ "inside CJK text",
443
+ [text("中"), text("加粗", bold), text("文")],
444
+ "中**加粗**文",
445
+ ],
446
+ [
447
+ "ending in punctuation before punctuation",
448
+ [text("说"), text("好。", bold), text("」")],
449
+ "说**好。**」",
450
+ ],
451
+ ])("keeps Markdown delimiters %s", (_label, content, expected) => {
452
+ const json = doc(p(...content));
453
+ expect(tiptapJsonToMarkdown(json)).toBe(expected);
454
+ expect(JSON.parse(markdownToTiptapJson(expected))).toEqual(
455
+ JSON.parse(json),
456
+ );
457
+ });
458
+ });
459
+
460
+ // Text that happens to open a line with block syntax must come back as the
461
+ // same paragraph, not as a list, heading, or setext underline.
462
+ describe("block markers at the start of a line", () => {
463
+ const br = { type: "hardBreak" };
464
+
465
+ it.each([
466
+ ["a numbered line", [text("1986. A good year")], "1986\\. A good year"],
467
+ ["a parenthesized number", [text("2) Second")], "2\\) Second"],
468
+ ["a dash", [text("- not a list")], "\\- not a list"],
469
+ ["a plus", [text("+ not a list")], "\\+ not a list"],
470
+ ["a hash", [text("# not a heading")], "\\# not a heading"],
471
+ [
472
+ "numbered lines after hard breaks",
473
+ [text("Update:"), br, text("1. Pony"), br, text("2. Next")],
474
+ "Update: \n1\\. Pony \n2\\. Next",
475
+ ],
476
+ [
477
+ "a dash rule after a hard break",
478
+ [text("Title"), br, text("---")],
479
+ "Title \n\\---",
480
+ ],
481
+ [
482
+ "an equals rule after a hard break",
483
+ [text("Title"), br, text("===")],
484
+ "Title \n\\===",
485
+ ],
486
+ ])("escapes %s", (_label, content, expected) => {
487
+ const json = doc(p(...content));
488
+ expect(tiptapJsonToMarkdown(json)).toBe(expected);
489
+ expect(JSON.parse(markdownToTiptapJson(expected))).toEqual(
490
+ JSON.parse(json),
491
+ );
492
+ });
493
+
494
+ it("leaves markers in the middle of a line alone", () => {
495
+ expect(tiptapJsonToMarkdown(doc(p(text("Step 1. then 2."))))).toBe(
496
+ "Step 1. then 2.",
497
+ );
498
+ });
499
+
500
+ it("escapes a numbered line inside a list item", () => {
501
+ const json = doc({
502
+ type: "orderedList",
503
+ content: [
504
+ { type: "listItem", content: [p(text("1. nested-looking"))] },
505
+ ],
506
+ });
507
+ expect(tiptapJsonToMarkdown(json)).toBe("1. 1\\. nested-looking");
508
+ expect(
509
+ JSON.parse(markdownToTiptapJson("1. 1\\. nested-looking")),
510
+ ).toEqual(JSON.parse(json));
511
+ });
364
512
  });
365
513
 
366
514
  describe("round-trip", () => {
@@ -411,12 +559,32 @@ describe("tiptapJsonToMarkdown", () => {
411
559
  });
412
560
 
413
561
  describe("edge cases", () => {
414
- it("returns empty string for invalid JSON", () => {
415
- expect(tiptapJsonToMarkdown("not json")).toBe("");
562
+ it("throws for invalid JSON instead of returning nothing", () => {
563
+ expect(() => tiptapJsonToMarkdown("not json")).toThrow();
416
564
  });
417
565
 
418
- it("returns empty string for non-doc node", () => {
419
- expect(tiptapJsonToMarkdown('{"type":"paragraph"}')).toBe("");
566
+ it("throws for a root that is not a doc", () => {
567
+ expect(() => tiptapJsonToMarkdown('{"type":"paragraph"}')).toThrow(/doc/);
568
+ });
569
+
570
+ // A stored body with an empty list item made the serializer throw, and
571
+ // the catch-all turned the whole post into "" in the export.
572
+ it("fills an empty list item instead of dropping the document", () => {
573
+ const json = doc(
574
+ { type: "heading", attrs: { level: 2 }, content: [text("Setup")] },
575
+ {
576
+ type: "orderedList",
577
+ content: [
578
+ { type: "listItem", content: [] },
579
+ { type: "listItem", content: [p(text("Install"))] },
580
+ ],
581
+ },
582
+ { type: "codeBlock", content: [text("nix run")] },
583
+ );
584
+ const markdown = tiptapJsonToMarkdown(json);
585
+ expect(markdown).toContain("## Setup");
586
+ expect(markdown).toContain("Install");
587
+ expect(markdown).toContain("nix run");
420
588
  });
421
589
 
422
590
  it("handles empty doc", () => {
@@ -64,6 +64,20 @@ describe("sniffImageMimeType", () => {
64
64
  expect(sniffImageMimeType(bytes)).toBe("image/avif");
65
65
  });
66
66
 
67
+ it.each(["heic", "heix", "hevc", "hevx", "mif1", "msf1"])(
68
+ "detects HEIC/HEIF via the %s ftyp brand",
69
+ (brand) => {
70
+ const bytes = new Uint8Array(16);
71
+ bytes.set([...new TextEncoder().encode("ftyp")], 4);
72
+ bytes.set([...new TextEncoder().encode(brand)], 8);
73
+ expect(sniffImageMimeType(bytes)).toBe("image/heic");
74
+ },
75
+ );
76
+
77
+ it("keeps HEIC out of the sideload formats", () => {
78
+ expect(isAllowedSideloadImageType("image/heic")).toBe(false);
79
+ });
80
+
67
81
  it("detects SVG from an <svg> root", () => {
68
82
  const svg = new TextEncoder().encode(
69
83
  '<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg"></svg>',
@@ -83,8 +83,7 @@ describe("fetchImageBytes", () => {
83
83
  );
84
84
 
85
85
  const headers = fetchMock.mock.calls[0]?.[1]?.headers as
86
- | Record<string, string>
87
- | undefined;
86
+ Record<string, string> | undefined;
88
87
  expect(headers?.Referer).toBe("https://img9.doubanio.com/");
89
88
  });
90
89
 
@@ -4,11 +4,12 @@ import {
4
4
  extractDisplayDomain,
5
5
  isFullUrl,
6
6
  isSafeAbsoluteUrl,
7
+ looksLikeAddress,
7
8
  normalizePath,
8
9
  sanitizeRichTextHref,
9
10
  sanitizeUrl,
10
11
  stripSitePathPrefix,
11
- slugify,
12
+ toInternalPath,
12
13
  toSameSitePath,
13
14
  } from "../url.js";
14
15
 
@@ -229,97 +230,102 @@ describe("URL sanitizers", () => {
229
230
  });
230
231
  });
231
232
 
232
- describe("slugify", () => {
233
- it("converts text to lowercase hyphenated slug", () => {
234
- expect(slugify("Hello World")).toBe("hello-world");
235
- });
236
-
237
- it("removes special characters", () => {
238
- expect(slugify("Hello World! This is a Test.")).toBe(
239
- "hello-world-this-is-a-test",
233
+ describe("stripSitePathPrefix", () => {
234
+ it("rewrites prefixed asset paths into the internal asset namespace", () => {
235
+ expect(stripSitePathPrefix("/blog/_assets/client.css", "/blog")).toBe(
236
+ "/_assets/client.css",
240
237
  );
241
238
  });
242
239
 
243
- it("collapses multiple spaces", () => {
244
- expect(slugify("Multiple Spaces")).toBe("multiple-spaces");
245
- });
246
-
247
- it("trims leading/trailing whitespace and hyphens", () => {
248
- expect(slugify(" Multiple Spaces ")).toBe("multiple-spaces");
249
- });
250
-
251
- it("replaces underscores with hyphens", () => {
252
- expect(slugify("hello_world")).toBe("hello-world");
253
- });
254
-
255
- it("handles already-slugified text", () => {
256
- expect(slugify("already-a-slug")).toBe("already-a-slug");
257
- });
258
-
259
- it("handles empty string", () => {
260
- expect(slugify("")).toBe("");
261
- });
262
-
263
- it("transliterates accented characters", () => {
264
- expect(slugify("café & résumé")).toBe("cafe-and-resume");
265
- });
266
-
267
- it("converts Chinese characters to pinyin", () => {
268
- expect(slugify("书评")).toBe("shu-ping");
269
- });
270
-
271
- it("handles mixed Chinese and English text", () => {
272
- expect(slugify("我的 Blog")).toBe("wo-de-blog");
273
- });
274
-
275
- it("handles CJK characters with spaces", () => {
276
- expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
240
+ it("rejects paths outside the configured site prefix", () => {
241
+ expect(stripSitePathPrefix("/_assets/client.css", "/blog")).toBe(null);
277
242
  });
243
+ });
278
244
 
279
- it("romanizes Korean titles", () => {
280
- expect(slugify("안녕하세요 세계")).toBe("annyeonghaseyo-segye");
281
- expect(slugify("개발 일지 3일차")).toBe("gaebal-ilji-3ilcha");
245
+ describe("looksLikeAddress", () => {
246
+ it("recognizes what a browser would have produced", () => {
247
+ expect(looksLikeAddress("/about")).toBe(true);
248
+ expect(looksLikeAddress(" https://example.com/about ")).toBe(true);
282
249
  });
283
250
 
284
- it("romanizes kana-only Japanese titles", () => {
285
- expect(slugify("カタカナタイトル")).toBe("katakanataitoru");
286
- expect(slugify("ハンバーガー")).toBe("hanbaga");
251
+ it("leaves search words alone", () => {
252
+ // The line that keeps a picker's search box a search box: "about" is a
253
+ // word someone is looking for, not the page /about.
254
+ expect(looksLikeAddress("about")).toBe(false);
255
+ expect(looksLikeAddress("coffee notes")).toBe(false);
256
+ expect(looksLikeAddress("example.com")).toBe(false);
287
257
  });
258
+ });
288
259
 
289
- it("keeps Latin words in mostly-kana Japanese titles", () => {
290
- expect(slugify("React入門ガイド")).toBe("react-gaido");
260
+ describe("toInternalPath", () => {
261
+ const SITE = {
262
+ siteOrigins: ["https://example.com"],
263
+ sitePathPrefix: "/blog",
264
+ languagePrefixes: ["zh-hans", "en"],
265
+ };
266
+
267
+ it("reads a path, a full URL, and a prefixed URL as the same page", () => {
268
+ expect(toInternalPath("/about", SITE)).toBe("/about");
269
+ expect(toInternalPath("about", SITE)).toBe("/about");
270
+ expect(toInternalPath("https://example.com/blog/about", SITE)).toBe(
271
+ "/about",
272
+ );
273
+ expect(toInternalPath("/blog/about", SITE)).toBe("/about");
274
+ // A typed path without the deployment prefix still means this site.
275
+ expect(toInternalPath("/about", SITE)).toBe("/about");
291
276
  });
292
277
 
293
- it("returns empty for kanji-heavy Japanese titles instead of garbling them", () => {
294
- // Kanji carries the meaning; without a reading dictionary any output
295
- // would be gibberish, so callers should fall back to a random ID.
296
- expect(slugify("日本語のタイトルです")).toBe("");
297
- expect(slugify("東京タワーに行った")).toBe("");
298
- expect(slugify("こんにちは世界")).toBe("");
278
+ it("strips a language prefix, because a view is not another page", () => {
279
+ expect(toInternalPath("/en/about", SITE)).toBe("/about");
280
+ expect(toInternalPath("https://example.com/blog/zh-hans/about", SITE)).toBe(
281
+ "/about",
282
+ );
283
+ // The prefix alone is that view's home.
284
+ expect(toInternalPath("/en", SITE)).toBe("/");
299
285
  });
300
286
 
301
- it("returns empty for scripts with no usable transliteration", () => {
302
- expect(slugify("שלום עולם")).toBe("");
303
- expect(slugify("สวัสดีชาวโลก")).toBe("");
287
+ it("keeps a first segment that only looks like a language", () => {
288
+ // A single-language site is free to have a post at /en — nothing is
289
+ // serving a view there.
290
+ expect(toInternalPath("/en/about", { sitePathPrefix: "" })).toBe(
291
+ "/en/about",
292
+ );
304
293
  });
305
294
 
306
- it("returns empty when most of the title is untransliterable", () => {
307
- expect(slugify("שלום world")).toBe("");
295
+ it("matches any host the site answers on", () => {
296
+ const origins = {
297
+ siteOrigins: ["https://example.com", "https://alias.dev"],
298
+ };
299
+ expect(toInternalPath("https://alias.dev/about", origins)).toBe("/about");
300
+ // Scheme and port differ in dev; the host is what decides.
301
+ expect(toInternalPath("http://example.com:8787/about", origins)).toBe(
302
+ "/about",
303
+ );
308
304
  });
309
305
 
310
- it("returns empty for emoji-only titles", () => {
311
- expect(slugify("🎉🎉")).toBe("");
306
+ it("refuses everything that is not a page on this site", () => {
307
+ expect(toInternalPath("https://other.com/about", SITE)).toBeNull();
308
+ expect(toInternalPath("mailto:hi@example.com", SITE)).toBeNull();
309
+ expect(toInternalPath("tel:+15551234567", SITE)).toBeNull();
310
+ expect(toInternalPath("//other.com/about", SITE)).toBeNull();
311
+ expect(toInternalPath("#section", SITE)).toBeNull();
312
+ expect(toInternalPath(" ", SITE)).toBeNull();
313
+ // A full URL inside our host but outside the deployment.
314
+ expect(
315
+ toInternalPath("https://example.com/other-app/about", SITE),
316
+ ).toBeNull();
312
317
  });
313
- });
314
318
 
315
- describe("stripSitePathPrefix", () => {
316
- it("rewrites prefixed asset paths into the internal asset namespace", () => {
317
- expect(stripSitePathPrefix("/blog/_assets/client.css", "/blog")).toBe(
318
- "/_assets/client.css",
319
- );
319
+ it("drops the query unless the caller is navigating", () => {
320
+ expect(toInternalPath("/about?ref=x#top", SITE)).toBe("/about");
321
+ expect(
322
+ toInternalPath("/about?ref=x#top", { ...SITE, keepQuery: true }),
323
+ ).toBe("/about?ref=x#top");
320
324
  });
321
325
 
322
- it("rejects paths outside the configured site prefix", () => {
323
- expect(stripSitePathPrefix("/_assets/client.css", "/blog")).toBe(null);
326
+ it("normalizes trailing slashes and relative steps", () => {
327
+ expect(toInternalPath("/about/", SITE)).toBe("/about");
328
+ expect(toInternalPath("/collections/../about", SITE)).toBe("/about");
329
+ expect(toInternalPath("/", SITE)).toBe("/");
324
330
  });
325
331
  });