@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
@@ -1,395 +0,0 @@
1
- import limax from "limax";
2
- //#region \0rolldown/runtime.js
3
- var __defProp = Object.defineProperty;
4
- var __exportAll = (all, no_symbols) => {
5
- let target = {};
6
- for (var name in all) __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true
9
- });
10
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
11
- return target;
12
- };
13
- //#endregion
14
- //#region src/lib/url.ts
15
- /**
16
- * URL Utilities
17
- */ var url_exports = /* @__PURE__ */ __exportAll({
18
- buildSiteUrl: () => buildSiteUrl,
19
- extractDisplayDomain: () => extractDisplayDomain,
20
- extractDomain: () => extractDomain,
21
- getSiteOrigin: () => getSiteOrigin,
22
- getSitePathPrefix: () => getSitePathPrefix,
23
- isFullUrl: () => isFullUrl,
24
- isSafeAbsoluteUrl: () => isSafeAbsoluteUrl,
25
- isSafeInternalRedirect: () => isSafeInternalRedirect,
26
- normalizePath: () => normalizePath,
27
- normalizeSitePathPrefix: () => normalizeSitePathPrefix,
28
- normalizeSiteUrl: () => normalizeSiteUrl,
29
- sanitizeUrl: () => sanitizeUrl,
30
- slugify: () => slugify,
31
- stripSitePathPrefix: () => stripSitePathPrefix,
32
- toAbsoluteAssetUrl: () => toAbsoluteAssetUrl,
33
- toAbsoluteSiteUrl: () => toAbsoluteSiteUrl,
34
- toPublicHref: () => toPublicHref,
35
- toPublicPath: () => toPublicPath,
36
- toSameSitePath: () => toSameSitePath
37
- });
38
- function normalizeSitePathname(pathname) {
39
- if (pathname === "/" || pathname === "") return "";
40
- const trimmed = pathname.replace(/\/+$/, "");
41
- return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
42
- }
43
- /**
44
- * Normalize a configured public site path prefix.
45
- *
46
- * @param sitePathPrefix - Prefix such as `/blog` or `blog`
47
- * @returns Normalized prefix like `/blog`, or an empty string when rooted
48
- */ function normalizeSitePathPrefix(sitePathPrefix) {
49
- return normalizeSitePathname(sitePathPrefix);
50
- }
51
- /**
52
- * Extracts the hostname (domain) from a URL string.
53
- *
54
- * Parses a full URL and returns just the hostname portion (e.g., "example.com" from
55
- * "https://example.com/path"). Returns `null` if the URL is malformed or cannot be parsed.
56
- *
57
- * @param url - The full URL string to extract the domain from
58
- * @returns The hostname/domain if valid, or `null` if parsing fails
59
- *
60
- * @example
61
- * ```ts
62
- * const domain = extractDomain("https://www.example.com/path");
63
- * // Returns: "www.example.com"
64
- *
65
- * const invalid = extractDomain("not-a-url");
66
- * // Returns: null
67
- * ```
68
- */ function extractDomain(url) {
69
- try {
70
- return new URL(url).hostname;
71
- } catch {
72
- return null;
73
- }
74
- }
75
- /**
76
- * Normalizes a path by removing slashes and converting to lowercase.
77
- *
78
- * Trims whitespace, converts to lowercase, removes leading and trailing slashes,
79
- * and collapses multiple consecutive slashes into single slashes. Used to create
80
- * consistent path representations for routing and storage.
81
- *
82
- * @param path - The path string to normalize
83
- * @returns The normalized path string
84
- *
85
- * @example
86
- * ```ts
87
- * const normalized = normalizePath(" /About/Contact// ");
88
- * // Returns: "about/contact"
89
- * ```
90
- */ function normalizePath(path) {
91
- return path.trim().toLowerCase().replace(/^\/+|\/+$/g, "").replace(/\/+/g, "/");
92
- }
93
- /**
94
- * Checks if a string is a full URL with HTTP or HTTPS protocol.
95
- *
96
- * Validates whether a string starts with "http://" or "https://", indicating it's
97
- * a full URL rather than a relative path. Useful for distinguishing between internal
98
- * paths and external URLs.
99
- *
100
- * @param str - The string to check
101
- * @returns `true` if the string starts with http:// or https://, `false` otherwise
102
- *
103
- * @example
104
- * ```ts
105
- * isFullUrl("https://example.com"); // Returns: true
106
- * isFullUrl("/about"); // Returns: false
107
- * isFullUrl("example.com"); // Returns: false
108
- * ```
109
- */ function isFullUrl(str) {
110
- return str.startsWith("http://") || str.startsWith("https://");
111
- }
112
- /**
113
- * If a full URL points at the site's own host, return its same-site path
114
- * (`pathname` + `search` + `hash`). Returns `null` when the input is not a full
115
- * URL, is unparseable, or points at a different host.
116
- *
117
- * Self-referential absolute links — e.g. a nav item set to
118
- * `https://example.com/about` on `example.com` — should behave like the
119
- * internal path `/about`: no external-link icon, no `target="_blank"`.
120
- *
121
- * Matching is by **hostname**, not full origin: scheme and port differences are
122
- * treated as same-site. This keeps the check intuitive ("same domain") and
123
- * robust in dev, where the site is often served over `http://host:<port>` while
124
- * a nav link stores the canonical `https://host` URL. In production (canonical
125
- * https + default port) hostname match is equivalent to origin match.
126
- *
127
- * @param url - Candidate URL (full URL or relative path)
128
- * @param siteOrigin - The site's own origin, e.g. `https://example.com`
129
- * @returns The same-site path, or `null` when the URL is external/non-absolute
130
- *
131
- * @example
132
- * ```ts
133
- * toSameSitePath("https://example.com/about", "https://example.com"); // "/about"
134
- * toSameSitePath("https://example.com/about", "http://example.com:8787"); // "/about"
135
- * toSameSitePath("https://other.com/about", "https://example.com"); // null
136
- * toSameSitePath("/about", "https://example.com"); // null
137
- * ```
138
- */ function toSameSitePath(url, siteOrigin) {
139
- if (!siteOrigin || !isFullUrl(url)) return null;
140
- let parsed;
141
- let reference;
142
- try {
143
- parsed = new URL(url);
144
- reference = new URL(siteOrigin);
145
- } catch {
146
- return null;
147
- }
148
- if (parsed.hostname !== reference.hostname) return null;
149
- return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
150
- }
151
- /**
152
- * Converts text to a URL-friendly slug.
153
- *
154
- * Transforms text into a lowercase, hyphen-separated slug using limax for
155
- * i18n-aware transliteration (CJK → Pinyin, Japanese → Romaji, accented → ASCII).
156
- *
157
- * @param text - The text to convert to a slug
158
- * @returns The slugified string
159
- *
160
- * @example
161
- * ```ts
162
- * slugify("Hello World! This is a Test.");
163
- * // Returns: "hello-world-this-is-a-test"
164
- *
165
- * slugify("书评");
166
- * // Returns: "shu-ping"
167
- * ```
168
- */ function slugify(text) {
169
- return limax(text, { tone: false }).replace(/_/g, "-");
170
- }
171
- /**
172
- * Extracts a human-friendly domain name from a URL for display purposes.
173
- *
174
- * Parses the URL, strips common prefixes (`www.`, `m.`, `mobile.`), and returns
175
- * a clean domain. Returns `null` if the URL is malformed.
176
- *
177
- * @param url - The full URL string
178
- * @returns A display-friendly domain string, or `null` if parsing fails
179
- *
180
- * @example
181
- * ```ts
182
- * extractDisplayDomain("https://www.example.com/path");
183
- * // Returns: "example.com"
184
- *
185
- * extractDisplayDomain("https://m.wikipedia.org/wiki/Test");
186
- * // Returns: "wikipedia.org"
187
- *
188
- * extractDisplayDomain("https://blog.example.com");
189
- * // Returns: "blog.example.com"
190
- * ```
191
- */ function extractDisplayDomain(url) {
192
- const hostname = extractDomain(url);
193
- if (!hostname) return null;
194
- return hostname.replace(/^(?:www|m|mobile)\./, "");
195
- }
196
- var SAFE_URL_PROTOCOLS = new Set([
197
- "http:",
198
- "https:",
199
- "mailto:"
200
- ]);
201
- /**
202
- * Checks whether a string is a safe absolute URL.
203
- *
204
- * Accepts absolute URLs that use `http:`, `https:`, or `mailto:`. Relative
205
- * paths are rejected because external post/source URLs must be fully qualified.
206
- *
207
- * @param url - The URL string to validate
208
- * @returns `true` when the URL is absolute and uses an allowed protocol
209
- *
210
- * @example
211
- * ```ts
212
- * isSafeAbsoluteUrl("https://example.com"); // true
213
- * isSafeAbsoluteUrl("mailto:hello@example.com"); // true
214
- * isSafeAbsoluteUrl("example.com"); // false
215
- * ```
216
- */ function isSafeAbsoluteUrl(url) {
217
- const trimmed = url.trim();
218
- if (!trimmed) return false;
219
- try {
220
- const parsed = new URL(trimmed);
221
- return SAFE_URL_PROTOCOLS.has(parsed.protocol);
222
- } catch {
223
- return false;
224
- }
225
- }
226
- /**
227
- * Sanitizes a URL by ensuring it uses a safe protocol.
228
- *
229
- * Returns the URL unchanged if it uses an allowed protocol (http:, https:, mailto:)
230
- * or is a relative path. Returns an empty string for dangerous protocols like
231
- * `javascript:`, `data:`, or `vbscript:`.
232
- *
233
- * @param url - The URL string to sanitize
234
- * @returns The original URL if safe, or an empty string if the protocol is disallowed
235
- *
236
- * @example
237
- * ```ts
238
- * sanitizeUrl("https://example.com"); // "https://example.com"
239
- * sanitizeUrl("/about"); // "/about"
240
- * sanitizeUrl("javascript:alert(1)"); // ""
241
- * sanitizeUrl("data:text/html,<h1>Hi</h1>"); // ""
242
- * ```
243
- */ function sanitizeUrl(url) {
244
- try {
245
- const parsed = new URL(url, "https://placeholder.invalid");
246
- if (SAFE_URL_PROTOCOLS.has(parsed.protocol)) return url;
247
- return "";
248
- } catch {
249
- return "";
250
- }
251
- }
252
- /**
253
- * Normalize a public site URL by stripping any trailing slash from the path.
254
- *
255
- * @param siteUrl - Full site URL, optionally with a path prefix
256
- * @returns Normalized site URL, or an empty string when not configured
257
- */ function normalizeSiteUrl(siteUrl) {
258
- const trimmed = siteUrl.trim();
259
- if (!trimmed) return "";
260
- const parsed = new URL(trimmed);
261
- parsed.pathname = normalizeSitePathname(parsed.pathname) || "/";
262
- if (parsed.pathname !== "/") parsed.pathname = parsed.pathname.replace(/\/+$/, "");
263
- return parsed.toString().replace(/\/$/, parsed.pathname === "/" ? "/" : "");
264
- }
265
- /**
266
- * Extract the public path prefix from a full site URL.
267
- *
268
- * @param siteUrl - Full site URL
269
- * @returns Prefix like `/blog`, or an empty string when the site is rooted
270
- */ function getSitePathPrefix(siteUrl) {
271
- if (!siteUrl.trim()) return "";
272
- return normalizeSitePathname(new URL(siteUrl).pathname);
273
- }
274
- /**
275
- * Extract the origin from a full site URL.
276
- *
277
- * @param siteUrl - Full site URL
278
- * @returns Origin like `https://example.com`, or an empty string when missing
279
- */ function getSiteOrigin(siteUrl) {
280
- if (!siteUrl.trim()) return "";
281
- return new URL(siteUrl).origin;
282
- }
283
- /**
284
- * Build a normalized public site URL from an origin and optional path prefix.
285
- *
286
- * @param siteOrigin - Public site origin, such as `https://example.com`
287
- * @param sitePathPrefix - Public site path prefix, such as `/blog`
288
- * @returns Normalized site URL, or an empty string when no origin is configured
289
- */ function buildSiteUrl(siteOrigin, sitePathPrefix = "") {
290
- const trimmedOrigin = siteOrigin.trim();
291
- if (!trimmedOrigin) return "";
292
- const origin = new URL(trimmedOrigin).origin;
293
- return normalizeSiteUrl(`${origin}${normalizeSitePathPrefix(sitePathPrefix) || "/"}`);
294
- }
295
- /**
296
- * Prefix an internal app path with the public site path prefix.
297
- *
298
- * Internal paths are always rooted at `/` and never include the deployment
299
- * prefix. This helper converts them to public-facing paths.
300
- *
301
- * @param path - Internal app path, such as `/settings`
302
- * @param sitePathPrefix - Public site path prefix, such as `/blog`
303
- * @returns Public path
304
- */ function toPublicPath(path, sitePathPrefix = "") {
305
- if (!path) return sitePathPrefix || "/";
306
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
307
- if (!sitePathPrefix) return normalizedPath;
308
- if (normalizedPath === sitePathPrefix || normalizedPath.startsWith(`${sitePathPrefix}/`)) return normalizedPath;
309
- if (normalizedPath === "/") return sitePathPrefix;
310
- return `${sitePathPrefix}${normalizedPath}`;
311
- }
312
- /**
313
- * Convert an app-local href to its public path while leaving external URLs
314
- * unchanged.
315
- *
316
- * @param href - Internal app path or external URL
317
- * @param sitePathPrefix - Public site path prefix
318
- * @returns Public-facing href
319
- */ function toPublicHref(href, sitePathPrefix = "") {
320
- if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("#")) return href;
321
- return toPublicPath(href, sitePathPrefix);
322
- }
323
- /**
324
- * Check whether a path is a safe same-origin redirect target.
325
- *
326
- * Accepts only paths that start with a single `/` (no protocol-relative
327
- * `//host`, no scheme, no control characters). Callers should use this to
328
- * validate user-supplied `redirect` query parameters before issuing a
329
- * `Location` header.
330
- *
331
- * @param path - Candidate redirect path
332
- * @returns `true` when the path is safe to use as an internal redirect
333
- *
334
- * @example
335
- * ```ts
336
- * isSafeInternalRedirect("/settings") // true
337
- * isSafeInternalRedirect("//evil.example") // false
338
- * isSafeInternalRedirect("https://evil.example") // false
339
- * ```
340
- */ function isSafeInternalRedirect(path) {
341
- if (typeof path !== "string") return false;
342
- if (!path.startsWith("/")) return false;
343
- if (path.startsWith("//")) return false;
344
- if (path.startsWith("/\\")) return false;
345
- for (let i = 0; i < path.length; i += 1) {
346
- const code = path.charCodeAt(i);
347
- if (code < 32 || code === 127) return false;
348
- }
349
- return true;
350
- }
351
- /**
352
- * Remove the site path prefix from a public request pathname.
353
- *
354
- * @param pathname - Public request pathname
355
- * @param sitePathPrefix - Configured public site path prefix
356
- * @returns Internal pathname, or `null` when the request is outside the site
357
- */ function stripSitePathPrefix(pathname, sitePathPrefix) {
358
- if (!sitePathPrefix) return pathname || "/";
359
- if (pathname === sitePathPrefix) return "/";
360
- if (pathname.startsWith(`${sitePathPrefix}/`)) return pathname.slice(sitePathPrefix.length) || "/";
361
- return null;
362
- }
363
- /**
364
- * Convert an internal or public path into an absolute site URL.
365
- *
366
- * @param path - Internal app path or already-public path
367
- * @param siteUrl - Normalized site URL
368
- * @param sitePathPrefix - Public site path prefix
369
- * @returns Absolute URL when siteUrl is configured, otherwise the original path
370
- */ function toAbsoluteSiteUrl(path, siteUrl, sitePathPrefix = "") {
371
- if (!siteUrl) return toPublicPath(path, sitePathPrefix);
372
- return new URL(toPublicPath(path, sitePathPrefix), siteUrl).toString();
373
- }
374
- /**
375
- * Resolve a possibly-relative asset URL to an absolute URL, leaving
376
- * already-absolute (`http(s):`) and protocol-relative (`//host`) URLs
377
- * untouched. Use for assets — like media — whose stored URL may be either an
378
- * app-local path or a full CDN URL.
379
- *
380
- * @param url - Asset URL: an internal path or an already-absolute URL
381
- * @param siteUrl - Normalized site URL
382
- * @param sitePathPrefix - Public site path prefix
383
- * @returns Absolute URL, or the original value when it is already absolute
384
- *
385
- * @example
386
- * ```ts
387
- * toAbsoluteAssetUrl("/m/a.png", "https://site.com"); // "https://site.com/m/a.png"
388
- * toAbsoluteAssetUrl("https://cdn.example/a.png", "x"); // "https://cdn.example/a.png"
389
- * ```
390
- */ function toAbsoluteAssetUrl(url, siteUrl, sitePathPrefix = "") {
391
- if (isFullUrl(url) || url.startsWith("//")) return url;
392
- return toAbsoluteSiteUrl(url, siteUrl, sitePathPrefix);
393
- }
394
- //#endregion
395
- export { toPublicPath as _, getSitePathPrefix as a, __exportAll as b, normalizePath as c, sanitizeUrl as d, slugify as f, toPublicHref as g, toAbsoluteSiteUrl as h, getSiteOrigin as i, normalizeSitePathPrefix as l, toAbsoluteAssetUrl as m, extractDisplayDomain as n, isFullUrl as o, stripSitePathPrefix as p, extractDomain as r, isSafeInternalRedirect as s, buildSiteUrl as t, normalizeSiteUrl as u, toSameSitePath as v, url_exports as y };
@@ -1,36 +0,0 @@
1
- // @vitest-environment happy-dom
2
-
3
- import { beforeEach, describe, expect, it } from "vitest";
4
- import {
5
- consumeCollectionCreatedNotice,
6
- PENDING_COLLECTION_CREATED_STORAGE_KEY,
7
- queueCollectionCreatedNotice,
8
- } from "../collection-created-notice.js";
9
-
10
- describe("collection created notice", () => {
11
- beforeEach(() => {
12
- globalThis.sessionStorage.clear();
13
- });
14
-
15
- it("queues and consumes the newly created Collection ID once", () => {
16
- queueCollectionCreatedNotice("col_books");
17
-
18
- expect(
19
- globalThis.sessionStorage.getItem(PENDING_COLLECTION_CREATED_STORAGE_KEY),
20
- ).toBe('{"collectionId":"col_books"}');
21
- expect(consumeCollectionCreatedNotice()).toBe("col_books");
22
- expect(consumeCollectionCreatedNotice()).toBeNull();
23
- });
24
-
25
- it("clears malformed pending state", () => {
26
- globalThis.sessionStorage.setItem(
27
- PENDING_COLLECTION_CREATED_STORAGE_KEY,
28
- '{"collectionId":false}',
29
- );
30
-
31
- expect(consumeCollectionCreatedNotice()).toBeNull();
32
- expect(
33
- globalThis.sessionStorage.getItem(PENDING_COLLECTION_CREATED_STORAGE_KEY),
34
- ).toBeNull();
35
- });
36
- });
@@ -1,216 +0,0 @@
1
- // @vitest-environment happy-dom
2
-
3
- import { beforeEach, describe, expect, it, vi } from "vitest";
4
-
5
- vi.mock("../toast.js", () => ({
6
- showToast: vi.fn(),
7
- }));
8
-
9
- vi.mock("../collection-created-notice.js", () => ({
10
- queueCollectionCreatedNotice: vi.fn(),
11
- }));
12
-
13
- import "../collection-form-bridge.js";
14
-
15
- type CollectionFormHarness = HTMLElement & {
16
- cancelHref?: string;
17
- initial?: {
18
- slug?: string;
19
- };
20
- loading?: boolean;
21
- };
22
-
23
- function createPageForm(
24
- cancelHref = "/collections",
25
- initial: CollectionFormHarness["initial"] = { slug: "books" },
26
- ): CollectionFormHarness {
27
- document.body.innerHTML = `
28
- <div
29
- data-collection-editor-page
30
- data-collection-editor-save-failed="Couldn't save. Try again in a moment."
31
- >
32
- <jant-collection-form></jant-collection-form>
33
- </div>
34
- `;
35
-
36
- const formEl = document.querySelector(
37
- "jant-collection-form",
38
- ) as CollectionFormHarness | null;
39
-
40
- if (!formEl) {
41
- throw new Error("Expected collection form element");
42
- }
43
-
44
- formEl.cancelHref = cancelHref;
45
- formEl.initial = initial;
46
- return formEl;
47
- }
48
-
49
- async function flushAsyncWork() {
50
- await new Promise((resolve) => setTimeout(resolve, 0));
51
- await new Promise((resolve) => setTimeout(resolve, 0));
52
- }
53
-
54
- describe("collection form bridge", () => {
55
- beforeEach(() => {
56
- document.body.innerHTML = "";
57
- vi.clearAllMocks();
58
- vi.restoreAllMocks();
59
- window.location.href = "http://localhost/collections/new";
60
- });
61
-
62
- it("redirects new collections back to the configured return URL", async () => {
63
- const formEl = createPageForm("/collections");
64
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
65
- new Response(JSON.stringify({ id: "col_books", slug: "books" }), {
66
- status: 201,
67
- headers: { "Content-Type": "application/json" },
68
- }),
69
- );
70
-
71
- formEl.dispatchEvent(
72
- new CustomEvent("jant:collection-submit", {
73
- bubbles: true,
74
- detail: {
75
- endpoint: "/api/collections",
76
- isEdit: false,
77
- data: {
78
- title: "Books",
79
- slug: "books",
80
- },
81
- },
82
- }),
83
- );
84
-
85
- await flushAsyncWork();
86
-
87
- const { queueCollectionCreatedNotice } =
88
- await import("../collection-created-notice.js");
89
-
90
- expect(fetchSpy).toHaveBeenCalledWith("/api/collections", {
91
- method: "POST",
92
- headers: {
93
- "Content-Type": "application/json",
94
- Accept: "application/json",
95
- },
96
- body: JSON.stringify({
97
- title: "Books",
98
- slug: "books",
99
- }),
100
- });
101
- expect(window.location.pathname).toBe("/collections");
102
- expect(queueCollectionCreatedNotice).toHaveBeenCalledWith("col_books");
103
- });
104
-
105
- it("redirects edited collections back to the collections page when launched from the list", async () => {
106
- const formEl = createPageForm("/collections");
107
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
108
- new Response(JSON.stringify({ slug: "books-renamed" }), {
109
- status: 200,
110
- headers: { "Content-Type": "application/json" },
111
- }),
112
- );
113
-
114
- formEl.dispatchEvent(
115
- new CustomEvent("jant:collection-submit", {
116
- bubbles: true,
117
- detail: {
118
- endpoint: "/api/collections/collection-1",
119
- isEdit: true,
120
- data: {
121
- title: "Books renamed",
122
- slug: "books-renamed",
123
- },
124
- },
125
- }),
126
- );
127
-
128
- await flushAsyncWork();
129
-
130
- expect(fetchSpy).toHaveBeenCalledWith("/api/collections/collection-1", {
131
- method: "PUT",
132
- headers: {
133
- "Content-Type": "application/json",
134
- Accept: "application/json",
135
- },
136
- body: JSON.stringify({
137
- title: "Books renamed",
138
- slug: "books-renamed",
139
- }),
140
- });
141
- expect(window.location.pathname).toBe("/collections");
142
- });
143
-
144
- it("redirects edited collections back to the updated detail page when launched from detail", async () => {
145
- window.location.href =
146
- "http://localhost/collections/books/edit?returnTo=%2Fbooks";
147
- const formEl = createPageForm("/books");
148
- vi.spyOn(globalThis, "fetch").mockResolvedValue(
149
- new Response(JSON.stringify({ slug: "books-renamed" }), {
150
- status: 200,
151
- headers: { "Content-Type": "application/json" },
152
- }),
153
- );
154
-
155
- formEl.dispatchEvent(
156
- new CustomEvent("jant:collection-submit", {
157
- bubbles: true,
158
- detail: {
159
- endpoint: "/api/collections/collection-1",
160
- isEdit: true,
161
- data: {
162
- title: "Books renamed",
163
- slug: "books-renamed",
164
- },
165
- },
166
- }),
167
- );
168
-
169
- await flushAsyncWork();
170
-
171
- expect(window.location.pathname).toBe("/books-renamed");
172
- });
173
-
174
- it("rewrites aggregate return paths when a collection slug changes", async () => {
175
- window.location.href =
176
- "http://localhost/collections/books/edit?returnTo=%2Fcollections%2Fbooks%2Bessays";
177
- const formEl = createPageForm("/collections/books+essays", {
178
- slug: "books",
179
- });
180
- const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
181
- new Response(JSON.stringify({ slug: "books-renamed" }), {
182
- status: 200,
183
- headers: { "Content-Type": "application/json" },
184
- }),
185
- );
186
-
187
- formEl.dispatchEvent(
188
- new CustomEvent("jant:collection-submit", {
189
- bubbles: true,
190
- detail: {
191
- endpoint: "/api/collections/collection-1",
192
- isEdit: true,
193
- data: {
194
- title: "Books renamed",
195
- slug: "books-renamed",
196
- },
197
- },
198
- }),
199
- );
200
-
201
- await flushAsyncWork();
202
-
203
- expect(fetchSpy).toHaveBeenCalledWith("/api/collections/collection-1", {
204
- method: "PUT",
205
- headers: {
206
- "Content-Type": "application/json",
207
- Accept: "application/json",
208
- },
209
- body: JSON.stringify({
210
- title: "Books renamed",
211
- slug: "books-renamed",
212
- }),
213
- });
214
- expect(window.location.pathname).toBe("/collections/books-renamed+essays");
215
- });
216
- });