@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -141,4 +141,74 @@ describe("generatePostSlug", () => {
141
141
  ).rejects.toThrow("Could not generate a unique slug");
142
142
  });
143
143
  });
144
+
145
+ describe("language suffix", () => {
146
+ /** Every slug is taken except the ones listed. */
147
+ const onlyAvailable = (...free: string[]) => {
148
+ const allowed = new Set(free);
149
+ return async (slug: string) => allowed.has(slug);
150
+ };
151
+
152
+ it("is not used when the base slug is free", async () => {
153
+ const slug = await generatePostSlug({
154
+ title: "Book Review",
155
+ idLength: 5,
156
+ languageSuffix: "en",
157
+ isAvailable: alwaysAvailable,
158
+ });
159
+ expect(slug).toBe("book-review");
160
+ });
161
+
162
+ it("is tried before a random suffix when the base collides", async () => {
163
+ const slug = await generatePostSlug({
164
+ title: "Book Review",
165
+ idLength: 5,
166
+ languageSuffix: "en",
167
+ isAvailable: onlyAvailable("book-review-en"),
168
+ });
169
+ expect(slug).toBe("book-review-en");
170
+ });
171
+
172
+ it("keeps the multi-part tag intact", async () => {
173
+ const slug = await generatePostSlug({
174
+ title: "Book Review",
175
+ idLength: 5,
176
+ languageSuffix: "zh-hant",
177
+ isAvailable: onlyAvailable("book-review-zh-hant"),
178
+ });
179
+ expect(slug).toBe("book-review-zh-hant");
180
+ });
181
+
182
+ it("falls back to a random suffix when the language slug is taken too", async () => {
183
+ const slug = await generatePostSlug({
184
+ title: "Book Review",
185
+ idLength: 5,
186
+ languageSuffix: "en",
187
+ isAvailable: async (candidate: string) =>
188
+ candidate !== "book-review" && candidate !== "book-review-en",
189
+ });
190
+ expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
191
+ });
192
+
193
+ it("goes straight to a random suffix without one", async () => {
194
+ const slug = await generatePostSlug({
195
+ title: "Book Review",
196
+ idLength: 5,
197
+ isAvailable: async (candidate: string) => candidate !== "book-review",
198
+ });
199
+ expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
200
+ });
201
+
202
+ it("skips a language candidate that lands on a reserved path", async () => {
203
+ const slug = await generatePostSlug({
204
+ title: "feed",
205
+ idLength: 5,
206
+ languageSuffix: "en",
207
+ isAvailable: alwaysAvailable,
208
+ });
209
+ // "feed" is reserved, so the base is never offered and the language
210
+ // candidate is never reached — the random fallback owns this case.
211
+ expect(slug).toMatch(/^feed-[a-z0-9]{5}$/);
212
+ });
213
+ });
144
214
  });
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { slugify } from "../slugify.js";
3
+
4
+ describe("slugify", () => {
5
+ it("converts text to lowercase hyphenated slug", () => {
6
+ expect(slugify("Hello World")).toBe("hello-world");
7
+ });
8
+
9
+ it("removes special characters", () => {
10
+ expect(slugify("Hello World! This is a Test.")).toBe(
11
+ "hello-world-this-is-a-test",
12
+ );
13
+ });
14
+
15
+ it("collapses multiple spaces", () => {
16
+ expect(slugify("Multiple Spaces")).toBe("multiple-spaces");
17
+ });
18
+
19
+ it("trims leading/trailing whitespace and hyphens", () => {
20
+ expect(slugify(" Multiple Spaces ")).toBe("multiple-spaces");
21
+ });
22
+
23
+ it("replaces underscores with hyphens", () => {
24
+ expect(slugify("hello_world")).toBe("hello-world");
25
+ });
26
+
27
+ it("handles already-slugified text", () => {
28
+ expect(slugify("already-a-slug")).toBe("already-a-slug");
29
+ });
30
+
31
+ it("handles empty string", () => {
32
+ expect(slugify("")).toBe("");
33
+ });
34
+
35
+ it("transliterates accented characters", () => {
36
+ expect(slugify("café & résumé")).toBe("cafe-and-resume");
37
+ });
38
+
39
+ it("converts Chinese characters to pinyin", () => {
40
+ expect(slugify("书评")).toBe("shu-ping");
41
+ });
42
+
43
+ it("handles mixed Chinese and English text", () => {
44
+ expect(slugify("我的 Blog")).toBe("wo-de-blog");
45
+ });
46
+
47
+ it("handles CJK characters with spaces", () => {
48
+ expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
49
+ });
50
+
51
+ it("romanizes Korean titles", () => {
52
+ expect(slugify("안녕하세요 세계")).toBe("annyeonghaseyo-segye");
53
+ expect(slugify("개발 일지 3일차")).toBe("gaebal-ilji-3ilcha");
54
+ });
55
+
56
+ it("romanizes kana-only Japanese titles", () => {
57
+ expect(slugify("カタカナタイトル")).toBe("katakanataitoru");
58
+ expect(slugify("ハンバーガー")).toBe("hanbaga");
59
+ });
60
+
61
+ it("keeps Latin words in mostly-kana Japanese titles", () => {
62
+ expect(slugify("React入門ガイド")).toBe("react-gaido");
63
+ });
64
+
65
+ it("returns empty for kanji-heavy Japanese titles instead of garbling them", () => {
66
+ // Kanji carries the meaning; without a reading dictionary any output
67
+ // would be gibberish, so callers should fall back to a random ID.
68
+ expect(slugify("日本語のタイトルです")).toBe("");
69
+ expect(slugify("東京タワーに行った")).toBe("");
70
+ expect(slugify("こんにちは世界")).toBe("");
71
+ });
72
+
73
+ it("returns empty for scripts with no usable transliteration", () => {
74
+ expect(slugify("שלום עולם")).toBe("");
75
+ expect(slugify("สวัสดีชาวโลก")).toBe("");
76
+ });
77
+
78
+ it("returns empty when most of the title is untransliterable", () => {
79
+ expect(slugify("שלום world")).toBe("");
80
+ });
81
+
82
+ it("returns empty for emoji-only titles", () => {
83
+ expect(slugify("🎉🎉")).toBe("");
84
+ });
85
+ });
@@ -210,6 +210,38 @@ describe("createS3Driver", () => {
210
210
  "x-amz-checksum-sha256": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
211
211
  });
212
212
  });
213
+
214
+ it("omits a request checksum when the caller did not supply one", async () => {
215
+ const driver = createS3Driver({
216
+ endpoint: "https://s3.example.com",
217
+ bucket: "jant-media",
218
+ accessKeyId: "access-key",
219
+ secretAccessKey: "secret-key",
220
+ region: "auto",
221
+ });
222
+
223
+ const presigned = await driver.presignPut?.("media/test.webp", {
224
+ contentType: "image/webp",
225
+ contentDisposition: "inline",
226
+ cacheControl: "public, max-age=31536000, immutable",
227
+ expiresInSeconds: 900,
228
+ });
229
+
230
+ if (!presigned) {
231
+ throw new Error("Expected presigned PUT target");
232
+ }
233
+
234
+ // Presigning runs with an empty body, so a default request checksum would
235
+ // pin CRC32-of-nothing to the URL and every real upload would 400.
236
+ const params = new URL(presigned.url).searchParams;
237
+ expect(params.has("x-amz-checksum-crc32")).toBe(false);
238
+ expect(params.has("x-amz-sdk-checksum-algorithm")).toBe(false);
239
+ expect(presigned.headers).toEqual({
240
+ "Content-Type": "image/webp",
241
+ "Content-Disposition": "inline",
242
+ "Cache-Control": "public, max-age=31536000, immutable",
243
+ });
244
+ });
213
245
  });
214
246
 
215
247
  describe("createR2Driver", () => {
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ foldThreadReplies,
4
+ getThreadHiddenCount,
5
+ THREAD_LEADING_REPLIES,
6
+ THREAD_TRAILING_REPLIES,
7
+ } from "../thread-fold.js";
8
+
9
+ const reply = (n: number) => ({ id: `reply-${n}` });
10
+ const replies = (count: number) =>
11
+ Array.from({ length: count }, (_unused, index) => reply(index + 1));
12
+
13
+ const ids = (list: { id: string }[]) => list.map((item) => item.id);
14
+
15
+ describe("foldThreadReplies", () => {
16
+ it("has no fold for a thread with no replies", () => {
17
+ expect(foldThreadReplies([])).toBeNull();
18
+ });
19
+
20
+ // The window is the first two plus the last three, so anything up to six
21
+ // posts arrives whole and there is nothing for a gap link to stand for.
22
+ it.each([1, 2, 3, 4, 5])("hides nothing with %i replies", (count) => {
23
+ const fold = foldThreadReplies(replies(count));
24
+
25
+ expect(fold?.hiddenCount).toBe(0);
26
+ expect(fold?.firstHiddenReply).toBeNull();
27
+ // Every reply is shown exactly once, the hero included.
28
+ const shown = [
29
+ ...ids(fold?.leadingReplies ?? []),
30
+ ...ids(fold?.trailingReplies ?? []),
31
+ fold?.latestReply.id,
32
+ ];
33
+ expect(new Set(shown).size).toBe(shown.length);
34
+ expect(shown.length).toBe(count);
35
+ });
36
+
37
+ // The shortest thread puts one reply in every role at once. It must still
38
+ // be rendered once, not three times.
39
+ it("shows a lone reply once, as the hero", () => {
40
+ const fold = foldThreadReplies(replies(1));
41
+
42
+ expect(fold?.latestReply.id).toBe("reply-1");
43
+ expect(fold?.leadingReplies).toEqual([]);
44
+ expect(fold?.trailingReplies).toEqual([]);
45
+ });
46
+
47
+ it("folds the middle out of a long thread", () => {
48
+ const fold = foldThreadReplies(replies(7));
49
+
50
+ expect(ids(fold?.leadingReplies ?? [])).toEqual(["reply-1", "reply-2"]);
51
+ expect(ids(fold?.trailingReplies ?? [])).toEqual(["reply-5", "reply-6"]);
52
+ expect(fold?.latestReply.id).toBe("reply-7");
53
+ expect(fold?.hiddenCount).toBe(2);
54
+ });
55
+
56
+ // The gap stands for a run of posts and opens the first of them, so a reader
57
+ // following it lands where the missing stretch starts.
58
+ it("points the gap at the earliest post it hides", () => {
59
+ expect(foldThreadReplies(replies(20))?.firstHiddenReply?.id).toBe(
60
+ "reply-3",
61
+ );
62
+ });
63
+
64
+ it("keeps the window fixed however long the thread runs", () => {
65
+ const fold = foldThreadReplies(replies(50));
66
+ const shown =
67
+ (fold?.leadingReplies.length ?? 0) +
68
+ (fold?.trailingReplies.length ?? 0) +
69
+ 1;
70
+
71
+ expect(shown).toBe(THREAD_LEADING_REPLIES + THREAD_TRAILING_REPLIES);
72
+ expect(fold?.hiddenCount).toBe(50 - shown);
73
+ });
74
+ });
75
+
76
+ describe("getThreadHiddenCount", () => {
77
+ // The site selects its buckets in SQL and they overlap on short threads, so
78
+ // the count has to dedupe rather than subtract two lengths.
79
+ it("counts a reply in two buckets once", () => {
80
+ expect(
81
+ getThreadHiddenCount({
82
+ leadingReplies: [reply(1), reply(2)],
83
+ trailingReplies: [reply(1)],
84
+ latestReply: reply(2),
85
+ totalReplyCount: 2,
86
+ }),
87
+ ).toBe(0);
88
+ });
89
+
90
+ it("counts what a long thread leaves out", () => {
91
+ expect(
92
+ getThreadHiddenCount({
93
+ leadingReplies: [reply(1), reply(2)],
94
+ trailingReplies: [reply(6), reply(7)],
95
+ latestReply: reply(8),
96
+ totalReplyCount: 8,
97
+ }),
98
+ ).toBe(3);
99
+ });
100
+
101
+ // A reply published between the count and the selection would otherwise
102
+ // report a negative number of hidden posts.
103
+ it("never goes below zero", () => {
104
+ expect(
105
+ getThreadHiddenCount({
106
+ leadingReplies: [reply(1), reply(2)],
107
+ trailingReplies: [],
108
+ latestReply: reply(3),
109
+ totalReplyCount: 1,
110
+ }),
111
+ ).toBe(0);
112
+ });
113
+ });
@@ -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
  });