@jant/core 0.6.15 → 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 (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  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__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Dictionary-free transliteration helpers for slug generation.
3
+ *
4
+ * These converters cover the scripts limax handles poorly:
5
+ *
6
+ * - Korean hangul → Revised Romanization (limax strips hangul entirely)
7
+ * - Japanese kana → Hepburn romaji (limax mangles long vowels and digraphs)
8
+ *
9
+ * Both are pure algorithms over Unicode code points — no dictionaries — so
10
+ * they stay small enough for the Workers bundle and the lazy client chunk.
11
+ * Japanese kanji is intentionally out of scope: readings are context-dependent
12
+ * and need a morphological dictionary, which is why `slugify` falls back to a
13
+ * random ID for kanji-heavy Japanese titles instead of guessing.
14
+ */
15
+
16
+ /** Matches hiragana, katakana, and the chōonpu (ー) long-vowel mark. */
17
+ export const KANA_RE = /[ぁ-ゖァ-ヺー]/u;
18
+
19
+ /** Matches precomposed hangul syllables and compatibility jamo. */
20
+ export const HANGUL_RE = /[가-힣ㄱ-ㆎ]/u;
21
+
22
+ /** Matches Han (CJK ideograph) characters. */
23
+ export const HAN_RE = /\p{Script=Han}/u;
24
+
25
+ // --- Korean: Revised Romanization ---------------------------------------
26
+
27
+ const HANGUL_SYLLABLE_BASE = 0xac00;
28
+ const HANGUL_SYLLABLE_END = 0xd7a3;
29
+ const VOWEL_COUNT = 21;
30
+ const FINAL_COUNT = 28;
31
+
32
+ // prettier-ignore
33
+ const RR_INITIALS = [
34
+ "g", "kk", "n", "d", "tt", "r", "m", "b", "pp", "s",
35
+ "ss", "", "j", "jj", "ch", "k", "t", "p", "h",
36
+ ];
37
+
38
+ // prettier-ignore
39
+ const RR_VOWELS = [
40
+ "a", "ae", "ya", "yae", "eo", "e", "yeo", "ye", "o", "wa",
41
+ "wae", "oe", "yo", "u", "wo", "we", "wi", "yu", "eu", "ui", "i",
42
+ ];
43
+
44
+ // Finals use their phonetic (syllable-final) value, e.g. ㅅ → "t".
45
+ // prettier-ignore
46
+ const RR_FINALS = [
47
+ "", "k", "k", "k", "n", "n", "n", "t", "l", "k", "m", "l", "l", "l",
48
+ "p", "l", "m", "p", "p", "t", "t", "ng", "t", "t", "k", "t", "p", "t",
49
+ ];
50
+
51
+ // Liaison: when the next syllable starts with the silent initial ㅇ, the
52
+ // final consonant is pronounced as that syllable's initial instead
53
+ // (한국어 → han-gu-geo, not han-guk-eo). Each entry is
54
+ // [what remains as the final, what carries over as the next initial].
55
+ // ㅎ is dropped when carried (좋아 → joa); final ㅇ (ng) never carries.
56
+ // prettier-ignore
57
+ const RR_LIAISON: readonly (readonly [string, string])[] = [
58
+ ["", ""], ["", "g"], ["", "kk"], ["k", "s"], ["", "n"], ["n", "j"],
59
+ ["n", ""], ["", "d"], ["", "r"], ["l", "g"], ["l", "m"], ["l", "b"],
60
+ ["l", "s"], ["l", "t"], ["l", "p"], ["l", ""], ["", "m"], ["", "b"],
61
+ ["p", "s"], ["", "s"], ["", "ss"], ["ng", ""], ["", "j"], ["", "ch"],
62
+ ["", "k"], ["", "t"], ["", "p"], ["", ""],
63
+ ];
64
+
65
+ // Standalone compatibility jamo (ㅋㅋㅋ, ㅠㅠ) — common in informal titles.
66
+ const COMPAT_JAMO: Record<string, string> = {
67
+ ㄱ: "g",
68
+ ㄲ: "kk",
69
+ ㄴ: "n",
70
+ ㄷ: "d",
71
+ ㄸ: "tt",
72
+ ㄹ: "r",
73
+ ㅁ: "m",
74
+ ㅂ: "b",
75
+ ㅃ: "pp",
76
+ ㅅ: "s",
77
+ ㅆ: "ss",
78
+ ㅇ: "ng",
79
+ ㅈ: "j",
80
+ ㅉ: "jj",
81
+ ㅊ: "ch",
82
+ ㅋ: "k",
83
+ ㅌ: "t",
84
+ ㅍ: "p",
85
+ ㅎ: "h",
86
+ ㅏ: "a",
87
+ ㅐ: "ae",
88
+ ㅑ: "ya",
89
+ ㅒ: "yae",
90
+ ㅓ: "eo",
91
+ ㅔ: "e",
92
+ ㅕ: "yeo",
93
+ ㅖ: "ye",
94
+ ㅗ: "o",
95
+ ㅘ: "wa",
96
+ ㅙ: "wae",
97
+ ㅚ: "oe",
98
+ ㅛ: "yo",
99
+ ㅜ: "u",
100
+ ㅝ: "wo",
101
+ ㅞ: "we",
102
+ ㅟ: "wi",
103
+ ㅠ: "yu",
104
+ ㅡ: "eu",
105
+ ㅢ: "ui",
106
+ ㅣ: "i",
107
+ };
108
+
109
+ interface HangulSyllable {
110
+ initial: number;
111
+ vowel: number;
112
+ final: number;
113
+ }
114
+
115
+ function decomposeHangul(codePoint: number): HangulSyllable | null {
116
+ if (codePoint < HANGUL_SYLLABLE_BASE || codePoint > HANGUL_SYLLABLE_END) {
117
+ return null;
118
+ }
119
+ const index = codePoint - HANGUL_SYLLABLE_BASE;
120
+ return {
121
+ initial: Math.floor(index / (VOWEL_COUNT * FINAL_COUNT)),
122
+ vowel: Math.floor(index / FINAL_COUNT) % VOWEL_COUNT,
123
+ final: index % FINAL_COUNT,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Transliterates Korean hangul to Latin using Revised Romanization.
129
+ *
130
+ * Handles precomposed syllables with basic liaison (a final consonant before
131
+ * a silent-ㅇ syllable carries over as its initial) and standalone
132
+ * compatibility jamo. Assimilation sound rules (e.g. 종로 → "Jongno") are
133
+ * intentionally skipped — slugs need recognizable output, not phonetic
134
+ * perfection. Non-hangul characters pass through unchanged.
135
+ *
136
+ * @param text - Input text, possibly mixing hangul with other characters
137
+ * @returns Text with hangul replaced by its romanization
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * hangulToLatin("안녕하세요"); // "annyeonghaseyo"
142
+ * hangulToLatin("한국어"); // "hangugeo" (liaison)
143
+ * hangulToLatin("개발 일지"); // "gaebal ilji"
144
+ * ```
145
+ */
146
+ export function hangulToLatin(text: string): string {
147
+ const chars = [...text];
148
+ let out = "";
149
+ let carried: string | null = null;
150
+
151
+ for (const [i, char] of chars.entries()) {
152
+ const syllable = decomposeHangul(char.codePointAt(0) ?? 0);
153
+ if (!syllable) {
154
+ carried = null;
155
+ out += COMPAT_JAMO[char] ?? char;
156
+ continue;
157
+ }
158
+
159
+ const initial = carried ?? RR_INITIALS[syllable.initial] ?? "";
160
+ carried = null;
161
+ const next = decomposeHangul(chars[i + 1]?.codePointAt(0) ?? 0);
162
+ let final = RR_FINALS[syllable.final] ?? "";
163
+ if (syllable.final !== 0 && next && RR_INITIALS[next.initial] === "") {
164
+ const [remains, carry] = RR_LIAISON[syllable.final] ?? ["", ""];
165
+ final = remains;
166
+ carried = carry;
167
+ }
168
+ out += initial + (RR_VOWELS[syllable.vowel] ?? "") + final;
169
+ }
170
+ return out;
171
+ }
172
+
173
+ // --- Japanese: Hepburn romaji -------------------------------------------
174
+
175
+ // prettier-ignore
176
+ const KANA_BASE: Record<string, string> = {
177
+ あ: "a", い: "i", う: "u", え: "e", お: "o",
178
+ か: "ka", き: "ki", く: "ku", け: "ke", こ: "ko",
179
+ さ: "sa", し: "shi", す: "su", せ: "se", そ: "so",
180
+ た: "ta", ち: "chi", つ: "tsu", て: "te", と: "to",
181
+ な: "na", に: "ni", ぬ: "nu", ね: "ne", の: "no",
182
+ は: "ha", ひ: "hi", ふ: "fu", へ: "he", ほ: "ho",
183
+ ま: "ma", み: "mi", む: "mu", め: "me", も: "mo",
184
+ や: "ya", ゆ: "yu", よ: "yo",
185
+ ら: "ra", り: "ri", る: "ru", れ: "re", ろ: "ro",
186
+ わ: "wa", ゐ: "i", ゑ: "e", を: "o", ん: "n",
187
+ が: "ga", ぎ: "gi", ぐ: "gu", げ: "ge", ご: "go",
188
+ ざ: "za", じ: "ji", ず: "zu", ぜ: "ze", ぞ: "zo",
189
+ だ: "da", ぢ: "ji", づ: "zu", で: "de", ど: "do",
190
+ ば: "ba", び: "bi", ぶ: "bu", べ: "be", ぼ: "bo",
191
+ ぱ: "pa", ぴ: "pi", ぷ: "pu", ぺ: "pe", ぽ: "po",
192
+ ゔ: "vu",
193
+ ぁ: "a", ぃ: "i", ぅ: "u", ぇ: "e", ぉ: "o",
194
+ ゃ: "ya", ゅ: "yu", ょ: "yo", ゎ: "wa",
195
+ };
196
+
197
+ // Digraphs and extended katakana combos (ファ, ティ, チェ, …). Keys are the
198
+ // hiragana-normalized two-character sequences.
199
+ // prettier-ignore
200
+ const KANA_COMBO: Record<string, string> = {
201
+ きゃ: "kya", きゅ: "kyu", きょ: "kyo",
202
+ ぎゃ: "gya", ぎゅ: "gyu", ぎょ: "gyo",
203
+ しゃ: "sha", しゅ: "shu", しょ: "sho",
204
+ じゃ: "ja", じゅ: "ju", じょ: "jo",
205
+ ちゃ: "cha", ちゅ: "chu", ちょ: "cho",
206
+ ぢゃ: "ja", ぢゅ: "ju", ぢょ: "jo",
207
+ にゃ: "nya", にゅ: "nyu", にょ: "nyo",
208
+ ひゃ: "hya", ひゅ: "hyu", ひょ: "hyo",
209
+ びゃ: "bya", びゅ: "byu", びょ: "byo",
210
+ ぴゃ: "pya", ぴゅ: "pyu", ぴょ: "pyo",
211
+ みゃ: "mya", みゅ: "myu", みょ: "myo",
212
+ りゃ: "rya", りゅ: "ryu", りょ: "ryo",
213
+ ふぁ: "fa", ふぃ: "fi", ふぇ: "fe", ふぉ: "fo",
214
+ てぃ: "ti", でぃ: "di", とぅ: "tu", どぅ: "du",
215
+ うぃ: "wi", うぇ: "we", うぉ: "wo",
216
+ ゔぁ: "va", ゔぃ: "vi", ゔぇ: "ve", ゔぉ: "vo",
217
+ ちぇ: "che", しぇ: "she", じぇ: "je",
218
+ つぁ: "tsa", つぃ: "tsi", つぇ: "tse", つぉ: "tso",
219
+ いぇ: "ye",
220
+ };
221
+
222
+ const KATAKANA_START = 0x30a1;
223
+ const KATAKANA_END = 0x30f6;
224
+ const KATAKANA_TO_HIRAGANA_SHIFT = 0x60;
225
+ const SOKUON = "っ";
226
+ const CHOONPU = "ー";
227
+
228
+ function normalizeToHiragana(char: string): string {
229
+ const codePoint = char.codePointAt(0) ?? 0;
230
+ if (codePoint >= KATAKANA_START && codePoint <= KATAKANA_END) {
231
+ return String.fromCodePoint(codePoint - KATAKANA_TO_HIRAGANA_SHIFT);
232
+ }
233
+ return char;
234
+ }
235
+
236
+ /**
237
+ * Transliterates Japanese kana to macron-less Hepburn romaji.
238
+ *
239
+ * Handles hiragana and katakana, digraphs (きゃ → "kya"), extended katakana
240
+ * loan combos (ファ → "fa"), sokuon gemination (まっちゃ → "matcha"), and
241
+ * drops the chōonpu long-vowel mark (ハンバーガー → "hanbaga"), matching how
242
+ * macrons are conventionally stripped in URLs. Kanji and other non-kana
243
+ * characters pass through unchanged.
244
+ *
245
+ * @param text - Input text, possibly mixing kana with other characters
246
+ * @returns Text with kana replaced by romaji
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * kanaToRomaji("カタカナタイトル"); // "katakanataitoru"
251
+ * kanaToRomaji("ハンバーガー"); // "hanbaga"
252
+ * kanaToRomaji("まっちゃ"); // "matcha"
253
+ * ```
254
+ */
255
+ export function kanaToRomaji(text: string): string {
256
+ const chars = [...text].map(normalizeToHiragana);
257
+ let out = "";
258
+ let geminate = false;
259
+
260
+ for (let i = 0; i < chars.length; i++) {
261
+ const char = chars[i] ?? "";
262
+ if (char === SOKUON) {
263
+ geminate = true;
264
+ continue;
265
+ }
266
+ if (char === CHOONPU) {
267
+ geminate = false;
268
+ continue;
269
+ }
270
+
271
+ const combo = KANA_COMBO[char + (chars[i + 1] ?? "")];
272
+ const romaji = combo ?? KANA_BASE[char];
273
+ if (combo) i++;
274
+ if (romaji === undefined) {
275
+ geminate = false;
276
+ out += char;
277
+ continue;
278
+ }
279
+ if (geminate) {
280
+ geminate = false;
281
+ if (/^[^aeiou]/.test(romaji)) {
282
+ out += romaji.startsWith("ch") ? "t" : (romaji[0] ?? "");
283
+ }
284
+ }
285
+ out += romaji;
286
+ }
287
+ return out;
288
+ }
package/src/lib/upload.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Shared file validation and storage key generation for upload routes.
5
5
  */
6
6
 
7
- import type { MediaKind } from "../types/constants.js";
7
+ import type { ContentDisposition, MediaKind } from "../types/constants.js";
8
8
  import { createEntityId } from "./ids.js";
9
9
 
10
10
  const MEDIA_ROOT_PREFIX = "media";
@@ -161,16 +161,6 @@ const MIME_CATEGORY_MAP = new Map<string, MediaCategory>([
161
161
  ...CODE_MIME_TYPES.map((t) => [t, "code" as const] as const),
162
162
  ]);
163
163
 
164
- /**
165
- * Accept string for file inputs. Accepts all file types.
166
- *
167
- * @example
168
- * ```ts
169
- * <input type="file" accept={UPLOAD_ACCEPT} />
170
- * ```
171
- */
172
- export const UPLOAD_ACCEPT = "*/*";
173
-
174
164
  export type MediaCategory =
175
165
  | "image"
176
166
  | "video"
@@ -183,7 +173,7 @@ export type MediaCategory =
183
173
  | "3d"
184
174
  | "code";
185
175
 
186
- export type UploadContentDisposition = "inline" | "attachment";
176
+ export type UploadContentDisposition = ContentDisposition;
187
177
 
188
178
  export interface StoredUploadPolicy {
189
179
  contentDisposition: UploadContentDisposition;
@@ -326,10 +316,22 @@ export function imageExtensionForMimeType(contentType: string): string | null {
326
316
  return IMAGE_MIME_EXTENSIONS[contentType] ?? null;
327
317
  }
328
318
 
319
+ /** ISO BMFF `ftyp` major brands that mean a HEIC/HEIF still image. */
320
+ const HEIC_FTYP_BRANDS = new Set([
321
+ "heic",
322
+ "heix",
323
+ "hevc",
324
+ "hevx",
325
+ "mif1",
326
+ "msf1",
327
+ ]);
328
+
329
329
  /**
330
330
  * Identify an image format from its leading bytes (magic numbers), so a remote
331
331
  * sideload can trust the actual content rather than the server's content-type
332
- * header. Recognizes every {@link isAllowedSideloadImageType} format.
332
+ * header. Recognizes every {@link isAllowedSideloadImageType} format, plus
333
+ * HEIC/HEIF — which no browser decodes natively and no sideload accepts, but
334
+ * which the composer converts before upload once it knows what it holds.
333
335
  *
334
336
  * @param bytes - The leading bytes of the file (≥ ~256 bytes recommended)
335
337
  * @returns The detected image MIME type, or null if unrecognized
@@ -367,6 +369,7 @@ export function sniffImageMimeType(bytes: Uint8Array): string | null {
367
369
  if (bytes.length >= 12 && readAscii(bytes, 4, 4) === "ftyp") {
368
370
  const brand = readAscii(bytes, 8, 4);
369
371
  if (brand === "avif" || brand === "avis") return "image/avif";
372
+ if (HEIC_FTYP_BRANDS.has(brand)) return "image/heic";
370
373
  }
371
374
  if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) {
372
375
  return "image/bmp";
package/src/lib/url.ts CHANGED
@@ -2,8 +2,6 @@
2
2
  * URL Utilities
3
3
  */
4
4
 
5
- import limax from "limax";
6
-
7
5
  function normalizeSitePathname(pathname: string): string {
8
6
  if (pathname === "/" || pathname === "") return "";
9
7
  const trimmed = pathname.replace(/\/+$/, "");
@@ -134,25 +132,159 @@ export function toSameSitePath(url: string, siteOrigin: string): string | null {
134
132
  }
135
133
 
136
134
  /**
137
- * Converts text to a URL-friendly slug.
138
- *
139
- * Transforms text into a lowercase, hyphen-separated slug using limax for
140
- * i18n-aware transliteration (CJK → Pinyin, Japanese → Romaji, accented → ASCII).
135
+ * Any `scheme:` at the start. `mailto:` and `tel:` address something real but
136
+ * never a page here, and an unrecognized scheme must not be read as a path.
137
+ */
138
+ const ANY_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
139
+
140
+ export interface InternalPathOptions {
141
+ /**
142
+ * Origins this site answers on. Pass the configured one *and* the origin of
143
+ * the current request: a hosted site is often reached on a custom domain the
144
+ * canonical URL knows nothing about.
145
+ */
146
+ siteOrigins?: readonly string[];
147
+ /** Public site path prefix, such as `/blog`. */
148
+ sitePathPrefix?: string;
149
+ /**
150
+ * Language URL prefixes the site serves, without slashes (`["en", "ja"]`).
151
+ * A leading one is removed, because `/en/about` and `/about` are the same
152
+ * page seen from two views.
153
+ */
154
+ languagePrefixes?: readonly string[];
155
+ /** Keep `?query` and `#hash`. Off by default: they name a spot, not a page. */
156
+ keepQuery?: boolean;
157
+ }
158
+
159
+ /**
160
+ * Whether a string is meant as an address rather than as search words.
141
161
  *
142
- * @param text - The text to convert to a slug
143
- * @returns The slugified string
162
+ * Deliberately narrow — a leading slash or a full `http(s)` URL. Anything
163
+ * looser would swallow real queries: a search for "about" is not a search for
164
+ * `/about`.
144
165
  *
166
+ * @param input - What the author typed or pasted
167
+ * @returns True when the input should be resolved instead of searched
145
168
  * @example
146
- * ```ts
147
- * slugify("Hello World! This is a Test.");
148
- * // Returns: "hello-world-this-is-a-test"
169
+ * looksLikeAddress("/about"); // true
170
+ * looksLikeAddress("https://example.com/about"); // true
171
+ * looksLikeAddress("about"); // false
172
+ */
173
+ export function looksLikeAddress(input: string): boolean {
174
+ const trimmed = input.trim();
175
+ return trimmed.startsWith("/") || isFullUrl(trimmed);
176
+ }
177
+
178
+ /**
179
+ * Convert an address the author pasted into an internal app path.
149
180
  *
150
- * slugify("书评");
151
- * // Returns: "shu-ping"
152
- * ```
181
+ * Accepts what a browser address bar would produce — a full URL on one of the
182
+ * site's own origins, or a path — and returns the path the router resolves
183
+ * against: no site path prefix, no language prefix, rooted at `/`. Returns
184
+ * `null` for anything that is not a page on this site, so callers can tell an
185
+ * external link apart from a bad one.
186
+ *
187
+ * Case is left alone; storage-level normalization belongs to the path
188
+ * registry, which lowercases on its own terms.
189
+ *
190
+ * A full URL has to sit inside the deployment to count. A typed path does not:
191
+ * `/about` on a site served from `/blog` is the About page, because that is
192
+ * what someone typing it means.
193
+ *
194
+ * @param input - A full URL, or a path
195
+ * @param options - The site's origins, prefixes, and what to keep
196
+ * @returns Internal path rooted at `/`, or `null` when the address is not ours
197
+ * @example
198
+ * toInternalPath("https://example.com/blog/en/about", {
199
+ * siteOrigins: ["https://example.com"],
200
+ * sitePathPrefix: "/blog",
201
+ * languagePrefixes: ["en"],
202
+ * }); // "/about"
203
+ * toInternalPath("https://other.com/about", { siteOrigins: ["https://example.com"] }); // null
153
204
  */
154
- export function slugify(text: string): string {
155
- return limax(text, { tone: false }).replace(/_/g, "-");
205
+ export function toInternalPath(
206
+ input: string,
207
+ options: InternalPathOptions = {},
208
+ ): string | null {
209
+ const value = input.trim();
210
+ if (!value) return null;
211
+
212
+ const {
213
+ siteOrigins = [],
214
+ sitePathPrefix = "",
215
+ languagePrefixes = [],
216
+ keepQuery = false,
217
+ } = options;
218
+
219
+ const isFull = isFullUrl(value);
220
+ let candidate: string | null = null;
221
+ if (isFull) {
222
+ for (const origin of siteOrigins) {
223
+ const path = toSameSitePath(value, origin);
224
+ if (path !== null) {
225
+ candidate = path;
226
+ break;
227
+ }
228
+ }
229
+ // A full URL on a host we do not serve is somebody else's page.
230
+ if (candidate === null) return null;
231
+ } else if (
232
+ value.startsWith("//") ||
233
+ ANY_SCHEME_RE.test(value) ||
234
+ value.startsWith("#")
235
+ ) {
236
+ return null;
237
+ } else {
238
+ candidate = value;
239
+ }
240
+
241
+ let url: URL;
242
+ try {
243
+ // A fixed base resolves `..`, percent-encoding, and stray backslashes the
244
+ // same way the browser that produced the address did.
245
+ url = new URL(candidate, "https://jant.invalid");
246
+ } catch {
247
+ return null;
248
+ }
249
+
250
+ const withoutSite = stripSitePathPrefix(
251
+ url.pathname,
252
+ normalizeSitePathPrefix(sitePathPrefix),
253
+ );
254
+ if (withoutSite === null && isFull) {
255
+ // A URL on our host but outside the deployment: another app, not a page.
256
+ return null;
257
+ }
258
+
259
+ // A typed path that does not carry the prefix is already an internal one —
260
+ // an author writing `/about` on a site served from `/blog` means the About
261
+ // page, not a different site.
262
+ const internal = stripLanguagePrefix(
263
+ withoutSite ?? url.pathname,
264
+ languagePrefixes,
265
+ );
266
+ const trimmed =
267
+ internal.length > 1 ? internal.replace(/\/+$/, "") || "/" : internal;
268
+
269
+ return keepQuery ? `${trimmed}${url.search}${url.hash}` : trimmed;
270
+ }
271
+
272
+ /** Drop a leading `/en`-style segment when the site serves that language. */
273
+ function stripLanguagePrefix(
274
+ pathname: string,
275
+ languagePrefixes: readonly string[],
276
+ ): string {
277
+ if (languagePrefixes.length === 0) return pathname;
278
+
279
+ const rest = pathname.startsWith("/") ? pathname.slice(1) : pathname;
280
+ const slash = rest.indexOf("/");
281
+ const first = (slash === -1 ? rest : rest.slice(0, slash)).toLowerCase();
282
+ if (!first) return pathname;
283
+ if (!languagePrefixes.some((prefix) => prefix.toLowerCase() === first)) {
284
+ return pathname;
285
+ }
286
+
287
+ return slash === -1 ? "/" : rest.slice(slash);
156
288
  }
157
289
 
158
290
  /**
@@ -183,6 +315,25 @@ export function extractDisplayDomain(url: string): string | null {
183
315
  }
184
316
 
185
317
  const SAFE_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
318
+ const SAFE_RICH_TEXT_HREF_PROTOCOLS = new Set([
319
+ ...SAFE_URL_PROTOCOLS,
320
+ "tel:",
321
+ "sms:",
322
+ ]);
323
+
324
+ function sanitizeUrlWithProtocols(
325
+ url: string,
326
+ protocols: ReadonlySet<string>,
327
+ ): string {
328
+ try {
329
+ const parsed = new URL(url, "https://placeholder.invalid");
330
+ // Relative URLs resolve against the placeholder and get https: — allow them
331
+ if (protocols.has(parsed.protocol)) return url;
332
+ return "";
333
+ } catch {
334
+ return "";
335
+ }
336
+ }
186
337
 
187
338
  /**
188
339
  * Checks whether a string is a safe absolute URL.
@@ -231,14 +382,27 @@ export function isSafeAbsoluteUrl(url: string): boolean {
231
382
  * ```
232
383
  */
233
384
  export function sanitizeUrl(url: string): string {
234
- try {
235
- const parsed = new URL(url, "https://placeholder.invalid");
236
- // Relative URLs resolve against the placeholder and get https: — allow them
237
- if (SAFE_URL_PROTOCOLS.has(parsed.protocol)) return url;
238
- return "";
239
- } catch {
240
- return "";
241
- }
385
+ return sanitizeUrlWithProtocols(url, SAFE_URL_PROTOCOLS);
386
+ }
387
+
388
+ /**
389
+ * Sanitizes a rich-text link destination.
390
+ *
391
+ * In addition to safe web and email URLs, rich-text links may launch the
392
+ * device's phone or messaging app through `tel:` and `sms:`. Relative links
393
+ * remain valid, while executable and resource-only protocols are rejected.
394
+ *
395
+ * @param url - The link destination to sanitize
396
+ * @returns The original URL if safe for an anchor, or an empty string otherwise
397
+ *
398
+ * @example
399
+ * ```ts
400
+ * sanitizeRichTextHref("sms:+15551234567"); // "sms:+15551234567"
401
+ * sanitizeRichTextHref("javascript:alert(1)"); // ""
402
+ * ```
403
+ */
404
+ export function sanitizeRichTextHref(url: string): string {
405
+ return sanitizeUrlWithProtocols(url, SAFE_RICH_TEXT_HREF_PROTOCOLS);
242
406
  }
243
407
 
244
408
  /**
@@ -1,33 +1,35 @@
1
1
  /**
2
- * Version and environment detection
3
- *
4
- * In Vite dev, `__JANT_DEV__` is replaced with `true` via Vite's `define` config.
5
- * In production (wrangler/esbuild), the typeof check evaluates to false safely.
2
+ * Version and build asset paths
6
3
  *
7
4
  * `__JANT_VERSION__` is replaced by Vite's `define` during both dev and lib build.
8
5
  *
9
6
  * `__CLIENT_JS_FILE__` and `__CLIENT_AUTH_JS_FILE__` are content-addressed
10
7
  * internal paths (e.g. `/_assets/client-HASH.js`) embedded by the Worker build
11
8
  * from the Vite client manifest. Used only in production (IS_VITE_DEV=false).
9
+ * `__CLIENT_COMPOSE_PRELOAD__` lists every file the composer entry needs —
10
+ * the entry and the chunks it imports — for the layout's modulepreload links.
11
+ *
12
+ * The dev flag itself lives in `build-env.ts`, which client bundles can import
13
+ * without these Worker-only globals.
12
14
  */
13
15
 
14
- declare const __JANT_DEV__: boolean | undefined;
15
16
  declare const __JANT_VERSION__: string;
16
17
  declare const __CLIENT_JS_FILE__: string;
17
18
  declare const __CLIENT_AUTH_JS_FILE__: string;
19
+ declare const __CLIENT_COMPOSE_PRELOAD__: readonly string[];
18
20
  declare const __CLIENT_CSS_FILE__: string;
21
+ declare const __CLIENT_AUTHOR_CSS_FILE__: string;
19
22
  declare const __CLIENT_CJK_CSS_FILE__: string;
20
23
  declare const __CLIENT_CJK_TC_CSS_FILE__: string;
21
24
  declare const __CLIENT_CJK_JP_CSS_FILE__: string;
22
25
  declare const __CLIENT_CJK_KR_CSS_FILE__: string;
23
26
 
24
- export const IS_VITE_DEV =
25
- typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
26
-
27
27
  export const CORE_VERSION = __JANT_VERSION__;
28
28
  export const CLIENT_JS_FILE = __CLIENT_JS_FILE__;
29
29
  export const CLIENT_AUTH_JS_FILE = __CLIENT_AUTH_JS_FILE__;
30
+ export const CLIENT_COMPOSE_PRELOAD = __CLIENT_COMPOSE_PRELOAD__;
30
31
  export const CLIENT_CSS_FILE = __CLIENT_CSS_FILE__;
32
+ export const CLIENT_AUTHOR_CSS_FILE = __CLIENT_AUTHOR_CSS_FILE__;
31
33
  export const CLIENT_CJK_CSS_FILE = __CLIENT_CJK_CSS_FILE__;
32
34
  export const CLIENT_CJK_TC_CSS_FILE = __CLIENT_CJK_TC_CSS_FILE__;
33
35
  export const CLIENT_CJK_JP_CSS_FILE = __CLIENT_CJK_JP_CSS_FILE__;