@jant/core 0.6.16 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { sendDiscoverPing } from "../discover-ping.js";
3
+
4
+ const NOW = 1_800_000_000;
5
+
6
+ /** No real waiting between attempts; the retry itself is what is under test. */
7
+ const options = {
8
+ endpoint: "https://directory.example/api/discover/ping",
9
+ feedUrl: "https://example.com/latest/feed",
10
+ now: () => NOW,
11
+ sleep: async () => {},
12
+ };
13
+
14
+ describe("sendDiscoverPing", () => {
15
+ let fetchMock: ReturnType<typeof vi.fn>;
16
+
17
+ beforeEach(() => {
18
+ fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 202 }));
19
+ vi.stubGlobal("fetch", fetchMock);
20
+ });
21
+
22
+ afterEach(() => {
23
+ vi.unstubAllGlobals();
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ it("posts the feed address and nothing else", async () => {
28
+ const outcome = await sendDiscoverPing(options);
29
+
30
+ expect(fetchMock).toHaveBeenCalledTimes(1);
31
+ const [url, init] = fetchMock.mock.calls[0] as [
32
+ string,
33
+ globalThis.RequestInit,
34
+ ];
35
+ expect(url).toBe("https://directory.example/api/discover/ping");
36
+ expect(init.method).toBe("POST");
37
+ expect(JSON.parse(String(init.body))).toEqual({
38
+ feed: "https://example.com/latest/feed",
39
+ });
40
+ expect(outcome).toMatchObject({
41
+ at: NOW,
42
+ ok: true,
43
+ feedUrl: "https://example.com/latest/feed",
44
+ });
45
+ });
46
+
47
+ it("does not retry an announcement that got through", async () => {
48
+ await sendDiscoverPing(options);
49
+
50
+ expect(fetchMock).toHaveBeenCalledTimes(1);
51
+ });
52
+
53
+ // The failure worth retrying automatically: a directory mid-deploy, or one
54
+ // answering a transient 5xx. One more attempt costs nothing and fixes it.
55
+ it("retries once, and reports success when the second attempt lands", async () => {
56
+ fetchMock
57
+ .mockResolvedValueOnce(new Response(null, { status: 503 }))
58
+ .mockResolvedValueOnce(new Response(null, { status: 202 }));
59
+
60
+ const outcome = await sendDiscoverPing(options);
61
+
62
+ expect(fetchMock).toHaveBeenCalledTimes(2);
63
+ expect(outcome.ok).toBe(true);
64
+ expect(outcome.error).toBeUndefined();
65
+ });
66
+
67
+ // The whole point of the rewrite: a lost announcement leaves evidence the
68
+ // owner can read, instead of a line in a log nobody sees.
69
+ it("reports the status a refusing directory answered", async () => {
70
+ fetchMock.mockResolvedValue(new Response(null, { status: 403 }));
71
+
72
+ const outcome = await sendDiscoverPing(options);
73
+
74
+ expect(fetchMock).toHaveBeenCalledTimes(2);
75
+ expect(outcome).toMatchObject({ at: NOW, ok: false, status: 403 });
76
+ expect(outcome.error).toContain("403");
77
+ });
78
+
79
+ it("reports a network failure rather than throwing", async () => {
80
+ fetchMock.mockRejectedValue(new Error("connect ECONNREFUSED"));
81
+
82
+ const outcome = await sendDiscoverPing(options);
83
+
84
+ expect(outcome.ok).toBe(false);
85
+ expect(outcome.error).toContain("ECONNREFUSED");
86
+ // No `status`: nothing answered, so claiming one would be a fabrication.
87
+ expect(outcome.status).toBeUndefined();
88
+ });
89
+ });
@@ -0,0 +1,410 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ discoverIntroRuns,
4
+ getDiscoverFeedPath,
5
+ getDiscoverPageUrls,
6
+ getDiscoverSubmitUrl,
7
+ linkTerms,
8
+ measureDiscoverMaturity,
9
+ parseDiscoverSetting,
10
+ resolveDiscoverMode,
11
+ } from "../discover.js";
12
+ import {
13
+ DEFAULT_DISCOVER_PING_URL,
14
+ getDiscoverDirectoryBaseUrl,
15
+ getDiscoverPingUrl,
16
+ } from "../env.js";
17
+
18
+ function resolve(
19
+ overrides: Partial<Parameters<typeof resolveDiscoverMode>[0]>,
20
+ ) {
21
+ return resolveDiscoverMode({
22
+ storedValue: null,
23
+ defaultValue: null,
24
+ demoMode: false,
25
+ noindex: false,
26
+ rssFeedsEnabled: true,
27
+ ...overrides,
28
+ });
29
+ }
30
+
31
+ describe("parseDiscoverSetting", () => {
32
+ it("accepts the three stored values", () => {
33
+ expect(parseDiscoverSetting("latest")).toBe("latest");
34
+ expect(parseDiscoverSetting("featured")).toBe("featured");
35
+ expect(parseDiscoverSetting("off")).toBe("off");
36
+ });
37
+
38
+ it("treats absent, blank, and unrecognized values as unset", () => {
39
+ expect(parseDiscoverSetting(undefined)).toBeNull();
40
+ expect(parseDiscoverSetting(null)).toBeNull();
41
+ expect(parseDiscoverSetting("")).toBeNull();
42
+ expect(parseDiscoverSetting(" ")).toBeNull();
43
+ expect(parseDiscoverSetting("true")).toBeNull();
44
+ });
45
+ });
46
+
47
+ describe("resolveDiscoverMode", () => {
48
+ // The declaration is the consent record. A self-hosted site nobody
49
+ // configured has said nothing, and a directory must read that as a no.
50
+ it("keeps an untouched self-hosted site out", () => {
51
+ expect(resolve({})).toBe("none");
52
+ });
53
+
54
+ it("honours the owner's stored choice", () => {
55
+ expect(resolve({ storedValue: "featured" })).toBe("featured");
56
+ expect(resolve({ storedValue: "latest" })).toBe("latest");
57
+ expect(resolve({ storedValue: "off" })).toBe("none");
58
+ });
59
+
60
+ // How hosted Jant lists its fleet without asking every owner.
61
+ it("takes the deployment default when nothing is stored", () => {
62
+ expect(resolve({ defaultValue: "latest" })).toBe("latest");
63
+ expect(resolve({ defaultValue: "featured" })).toBe("featured");
64
+ expect(resolve({ defaultValue: "off" })).toBe("none");
65
+ });
66
+
67
+ it("lets the owner overrule the deployment default", () => {
68
+ expect(resolve({ defaultValue: "latest", storedValue: "off" })).toBe(
69
+ "none",
70
+ );
71
+ expect(resolve({ defaultValue: "off", storedValue: "latest" })).toBe(
72
+ "latest",
73
+ );
74
+ });
75
+
76
+ // Hiding from search engines and being surfaced by a directory contradict
77
+ // each other, and the quieter reading is the safe one.
78
+ it("reads noindex as none while the setting is unset", () => {
79
+ expect(resolve({ noindex: true })).toBe("none");
80
+ });
81
+
82
+ // The whole reason the two are separate: a deployment-wide "list my blogs"
83
+ // is a default, not the answer of the owner who hid this one from search.
84
+ it("puts noindex above the deployment default", () => {
85
+ expect(resolve({ noindex: true, defaultValue: "latest" })).toBe("none");
86
+ });
87
+
88
+ it("lets a stored choice override noindex", () => {
89
+ expect(resolve({ noindex: true, storedValue: "latest" })).toBe("latest");
90
+ expect(resolve({ noindex: true, storedValue: "featured" })).toBe(
91
+ "featured",
92
+ );
93
+ });
94
+
95
+ // Demos exist to be thrown away; nothing they publish belongs in a directory.
96
+ it("locks demo sites out even when they ask to be listed", () => {
97
+ expect(resolve({ demoMode: true })).toBe("none");
98
+ expect(resolve({ demoMode: true, storedValue: "featured" })).toBe("none");
99
+ expect(resolve({ demoMode: true, defaultValue: "latest" })).toBe("none");
100
+ });
101
+
102
+ // Every feed path 404s with feeds off, so there would be nothing to poll.
103
+ it("declares none when the site publishes no feeds", () => {
104
+ expect(resolve({ rssFeedsEnabled: false })).toBe("none");
105
+ expect(resolve({ rssFeedsEnabled: false, storedValue: "latest" })).toBe(
106
+ "none",
107
+ );
108
+ });
109
+
110
+ it("ignores values it does not recognize, on either side", () => {
111
+ expect(resolve({ storedValue: "sometimes", defaultValue: "latest" })).toBe(
112
+ "latest",
113
+ );
114
+ expect(resolve({ defaultValue: "sometimes" })).toBe("none");
115
+ });
116
+ });
117
+
118
+ describe("getDiscoverFeedPath", () => {
119
+ it("names the feed each mode draws from", () => {
120
+ expect(getDiscoverFeedPath("latest")).toBe("/latest/feed");
121
+ expect(getDiscoverFeedPath("featured")).toBe("/featured/feed");
122
+ expect(getDiscoverFeedPath("none")).toBeNull();
123
+ });
124
+ });
125
+
126
+ /**
127
+ * The two states that matter are spelled differently, and collapsing them is
128
+ * how "switch the ping off" would quietly become "switch it on".
129
+ */
130
+ describe("getDiscoverPingUrl", () => {
131
+ it("uses Jant's directory when nothing is configured", () => {
132
+ expect(getDiscoverPingUrl({})).toBe(DEFAULT_DISCOVER_PING_URL);
133
+ expect(getDiscoverPingUrl(undefined)).toBe(DEFAULT_DISCOVER_PING_URL);
134
+ });
135
+
136
+ it("announces nowhere when the binding is set but empty", () => {
137
+ expect(getDiscoverPingUrl({ DISCOVER_PING_URL: "" })).toBeUndefined();
138
+ expect(getDiscoverPingUrl({ DISCOVER_PING_URL: " " })).toBeUndefined();
139
+ });
140
+
141
+ it("uses a directory of the operator's own", () => {
142
+ expect(
143
+ getDiscoverPingUrl({
144
+ DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
145
+ }),
146
+ ).toBe("https://directory.example/api/discover/ping");
147
+ });
148
+
149
+ // Naming the directory twice is what let a hosted deployment announce its
150
+ // blogs to jant.me, which had never heard of them.
151
+ it("announces to the control plane that hosts this deployment", () => {
152
+ expect(
153
+ getDiscoverPingUrl({
154
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
155
+ }),
156
+ ).toBe("https://cloud.example/api/discover/ping");
157
+ });
158
+
159
+ // Server-to-server, like every other core to control-plane call.
160
+ it("prefers the control plane's internal address", () => {
161
+ expect(
162
+ getDiscoverPingUrl({
163
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
164
+ HOSTED_CONTROL_PLANE_INTERNAL_BASE_URL: "http://127.0.0.1:3300",
165
+ }),
166
+ ).toBe("http://127.0.0.1:3300/api/discover/ping");
167
+ });
168
+
169
+ it("lets an explicit directory override the control plane", () => {
170
+ expect(
171
+ getDiscoverPingUrl({
172
+ DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
173
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
174
+ }),
175
+ ).toBe("https://directory.example/api/discover/ping");
176
+ });
177
+
178
+ it("still announces nowhere when a hosted deployment empties the binding", () => {
179
+ expect(
180
+ getDiscoverPingUrl({
181
+ DISCOVER_PING_URL: "",
182
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
183
+ }),
184
+ ).toBeUndefined();
185
+ });
186
+
187
+ it("falls back to Jant's directory when the control plane URL is unusable", () => {
188
+ expect(
189
+ getDiscoverPingUrl({ HOSTED_CONTROL_PLANE_BASE_URL: "not a url" }),
190
+ ).toBe(DEFAULT_DISCOVER_PING_URL);
191
+ });
192
+ });
193
+
194
+ /**
195
+ * The address a browser opens, for the same directory the ping goes to. The
196
+ * two must never name different directories, which is why they share a source.
197
+ */
198
+ describe("getDiscoverDirectoryBaseUrl", () => {
199
+ it("uses Jant's directory when nothing is configured", () => {
200
+ expect(getDiscoverDirectoryBaseUrl({})).toBe("https://jant.me/");
201
+ });
202
+
203
+ // The public address, not the internal one the ping uses: this ends up in an
204
+ // href, and `127.0.0.1` is not a directory anybody can visit.
205
+ it("links to the control plane's public address", () => {
206
+ expect(
207
+ getDiscoverDirectoryBaseUrl({
208
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
209
+ HOSTED_CONTROL_PLANE_INTERNAL_BASE_URL: "http://127.0.0.1:3300",
210
+ }),
211
+ ).toBe("https://cloud.example/");
212
+ });
213
+
214
+ it("follows an explicitly configured directory", () => {
215
+ expect(
216
+ getDiscoverDirectoryBaseUrl({
217
+ DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
218
+ }),
219
+ ).toBe("https://directory.example/");
220
+ });
221
+
222
+ it("has nothing to link to when announcing is off", () => {
223
+ expect(
224
+ getDiscoverDirectoryBaseUrl({ DISCOVER_PING_URL: "" }),
225
+ ).toBeUndefined();
226
+ });
227
+ });
228
+
229
+ describe("measureDiscoverMaturity", () => {
230
+ it("passes a blog that has published something", () => {
231
+ expect(measureDiscoverMaturity({ publicPostCount: 5 })).toEqual({
232
+ publicPostCount: 5,
233
+ established: true,
234
+ });
235
+ });
236
+
237
+ // One post is the whole threshold; what keeps throwaways out is the opt-in.
238
+ it("passes a blog with a single public post", () => {
239
+ expect(measureDiscoverMaturity({ publicPostCount: 1 })).toMatchObject({
240
+ established: true,
241
+ });
242
+ });
243
+
244
+ it("holds back a blog with nothing public in it", () => {
245
+ expect(measureDiscoverMaturity({ publicPostCount: 0 })).toEqual({
246
+ publicPostCount: 0,
247
+ established: false,
248
+ });
249
+ });
250
+ });
251
+
252
+ describe("getDiscoverSubmitUrl", () => {
253
+ it("finds the form beside the directory's own ping endpoint", () => {
254
+ expect(getDiscoverSubmitUrl("https://jant.me/api/discover/ping")).toBe(
255
+ "https://jant.me/discover/submit",
256
+ );
257
+ });
258
+
259
+ // Provider-neutral on purpose: a site announcing to its own directory must
260
+ // not be sent to somebody else's form.
261
+ it("follows a directory of your own", () => {
262
+ expect(
263
+ getDiscoverSubmitUrl("https://directory.example/api/discover/ping"),
264
+ ).toBe("https://directory.example/discover/submit");
265
+ });
266
+
267
+ it("has nothing to offer when no directory is configured", () => {
268
+ expect(getDiscoverSubmitUrl(undefined)).toBeNull();
269
+ expect(getDiscoverSubmitUrl("")).toBeNull();
270
+ expect(getDiscoverSubmitUrl("not a url")).toBeNull();
271
+ });
272
+ });
273
+
274
+ describe("getDiscoverPageUrls", () => {
275
+ // The settings page links the directory's name to the directory itself, so
276
+ // "what is Discover" is answered by the list rather than by a page about it.
277
+ // The three lists sit flat on jant.me; only the rules sit under /discover.
278
+ it("finds the directory's pages behind its own ping endpoint", () => {
279
+ expect(getDiscoverPageUrls("https://jant.me/api/discover/ping")).toEqual({
280
+ home: "https://jant.me/discover",
281
+ links: "https://jant.me/links",
282
+ quotes: "https://jant.me/quotes",
283
+ rules: "https://jant.me/discover/about",
284
+ });
285
+ });
286
+
287
+ it("follows a directory of your own", () => {
288
+ expect(
289
+ getDiscoverPageUrls("https://directory.example/api/discover/ping"),
290
+ ).toEqual({
291
+ home: "https://directory.example/discover",
292
+ links: "https://directory.example/links",
293
+ quotes: "https://directory.example/quotes",
294
+ rules: "https://directory.example/discover/about",
295
+ });
296
+ });
297
+
298
+ it("has nothing to link to when no directory is configured", () => {
299
+ expect(getDiscoverPageUrls(undefined)).toBeNull();
300
+ expect(getDiscoverPageUrls("")).toBeNull();
301
+ expect(getDiscoverPageUrls("not a url")).toBeNull();
302
+ });
303
+ });
304
+
305
+ describe("linkTerms", () => {
306
+ const links = [
307
+ { term: "Jant Discover", href: "/discover" },
308
+ { term: "Links", href: "/links" },
309
+ { term: "Quotes", href: "/quotes" },
310
+ { term: "Discover community rules", href: "/discover/about" },
311
+ ];
312
+
313
+ it("links every term where the sentence puts it", () => {
314
+ expect(
315
+ linkTerms(
316
+ "Jant Discover lists link and quote posts on Links and Quotes. See the Discover community rules.",
317
+ links,
318
+ ),
319
+ ).toEqual([
320
+ { text: "Jant Discover", href: "/discover" },
321
+ { text: " lists link and quote posts on " },
322
+ { text: "Links", href: "/links" },
323
+ { text: " and " },
324
+ { text: "Quotes", href: "/quotes" },
325
+ { text: ". See the " },
326
+ { text: "Discover community rules", href: "/discover/about" },
327
+ { text: "." },
328
+ ]);
329
+ });
330
+
331
+ // A translation owns its word order; the links follow the words, not the
332
+ // order they were given in.
333
+ it("follows the translation's word order", () => {
334
+ expect(
335
+ linkTerms("详见 Discover 社区规则。Jant Discover 是一个目录。", [
336
+ { term: "Jant Discover", href: "/discover" },
337
+ { term: "Discover 社区规则", href: "/discover/about" },
338
+ ]),
339
+ ).toEqual([
340
+ { text: "详见 " },
341
+ { text: "Discover 社区规则", href: "/discover/about" },
342
+ { text: "。" },
343
+ { text: "Jant Discover", href: "/discover" },
344
+ { text: " 是一个目录。" },
345
+ ]);
346
+ });
347
+
348
+ // "Discover" alone sits inside "Jant Discover"; whichever the sentence
349
+ // mentions first, the longer name keeps its whole run.
350
+ it("never lets a term inside another take the longer one's place", () => {
351
+ expect(
352
+ linkTerms("Jant Discover reads your feed. Discover shows it.", [
353
+ { term: "Discover", href: "/about" },
354
+ { term: "Jant Discover", href: "/discover" },
355
+ ]),
356
+ ).toEqual([
357
+ { text: "Jant Discover", href: "/discover" },
358
+ { text: " reads your feed. " },
359
+ { text: "Discover", href: "/about" },
360
+ { text: " shows it." },
361
+ ]);
362
+ });
363
+
364
+ it("leaves a term the translation dropped as plain text", () => {
365
+ expect(
366
+ linkTerms("A directory of Jant blogs.", [
367
+ { term: "Jant Discover", href: "/discover" },
368
+ ]),
369
+ ).toEqual([{ text: "A directory of Jant blogs." }]);
370
+ });
371
+
372
+ it("links nothing for an empty term or address", () => {
373
+ expect(
374
+ linkTerms("See Links.", [
375
+ { term: "", href: "/links" },
376
+ { term: "Links", href: "" },
377
+ ]),
378
+ ).toEqual([{ text: "See Links." }]);
379
+ });
380
+ });
381
+
382
+ describe("discoverIntroRuns", () => {
383
+ const terms = { name: "Jant Discover", rules: "Discover community rules" };
384
+ const intro =
385
+ "Jant Discover lists link and quote posts on Links and Quotes. See the Discover community rules.";
386
+
387
+ // The settings page and the setup screen both render the line through here,
388
+ // so this is the one place that says which words go where.
389
+ it("links the name, both lists and the rules to their pages", () => {
390
+ expect(
391
+ discoverIntroRuns(intro, terms, getDiscoverPageUrls("https://jant.me/")),
392
+ ).toEqual([
393
+ { text: "Jant Discover", href: "https://jant.me/discover" },
394
+ { text: " lists link and quote posts on " },
395
+ { text: "Links", href: "https://jant.me/links" },
396
+ { text: " and " },
397
+ { text: "Quotes", href: "https://jant.me/quotes" },
398
+ { text: ". See the " },
399
+ {
400
+ text: "Discover community rules",
401
+ href: "https://jant.me/discover/about",
402
+ },
403
+ { text: "." },
404
+ ]);
405
+ });
406
+
407
+ it("leaves the whole line plain when there is no directory", () => {
408
+ expect(discoverIntroRuns(intro, terms, null)).toEqual([{ text: intro }]);
409
+ });
410
+ });
@@ -1,9 +1,35 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
+ FEED_LIMIT_MAX,
3
4
  getFeedEntryUpdatedAt,
4
5
  getRssPublishedBefore,
6
+ parseFeedLimit,
5
7
  } from "../feed-policy.js";
6
8
 
9
+ describe("parseFeedLimit", () => {
10
+ it("keeps the site's length when the parameter is absent", () => {
11
+ expect(parseFeedLimit(undefined, 50)).toBe(50);
12
+ });
13
+
14
+ it("honours a request above or below the site's length", () => {
15
+ expect(parseFeedLimit("200", 50)).toBe(200);
16
+ expect(parseFeedLimit("5", 50)).toBe(5);
17
+ expect(parseFeedLimit("050", 50)).toBe(50);
18
+ });
19
+
20
+ it("gives the most there is to a request past the ceiling", () => {
21
+ expect(parseFeedLimit(String(FEED_LIMIT_MAX), 50)).toBe(FEED_LIMIT_MAX);
22
+ expect(parseFeedLimit(String(FEED_LIMIT_MAX + 1), 50)).toBe(FEED_LIMIT_MAX);
23
+ expect(parseFeedLimit("99999999999999999999", 50)).toBe(FEED_LIMIT_MAX);
24
+ });
25
+
26
+ it("ignores anything that is not a positive whole number", () => {
27
+ for (const raw of ["", "0", "-3", "1.5", "abc", "10abc", " 10", "1e3"]) {
28
+ expect(parseFeedLimit(raw, 50)).toBe(50);
29
+ }
30
+ });
31
+ });
32
+
7
33
  describe("feed policy", () => {
8
34
  it("makes the exact delay boundary eligible", () => {
9
35
  expect(getRssPublishedBefore(300, 1_000)).toBe(701);