@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Content language detection.
3
+ *
4
+ * The detector is a suggestion engine, so these tests care about two things:
5
+ * that it is right on the cases it claims to be reliable on (Hangul, kana,
6
+ * Simplified vs Traditional over a sentence), and that it refuses rather than
7
+ * guesses everywhere else — a wrong confident answer costs the author an edit
8
+ * they may never notice they need.
9
+ */
10
+
11
+ import { describe, expect, it } from "vitest";
12
+ import {
13
+ detectContentLanguage,
14
+ detectScript,
15
+ readContentLanguage,
16
+ suggestPostLanguage,
17
+ } from "../lang-detect.js";
18
+
19
+ describe("detectScript", () => {
20
+ it("reads Hangul as Korean", () => {
21
+ expect(detectScript("안녕하세요, 반갑습니다")).toBe("ko");
22
+ });
23
+
24
+ it("reads Korean mixed with hanja as Korean", () => {
25
+ // Korean borrows Han characters but never kana, so Hangul settles it.
26
+ expect(detectScript("韓國語 문법 정리")).toBe("ko");
27
+ });
28
+
29
+ it("reads kana as Japanese", () => {
30
+ expect(detectScript("これはテストです")).toBe("ja");
31
+ expect(detectScript("日本語の文章を書く")).toBe("ja");
32
+ });
33
+
34
+ it("reads Simplified-only characters as Simplified", () => {
35
+ expect(detectScript("国学说这时会对后")).toBe("zh-Hans");
36
+ });
37
+
38
+ it("reads Traditional-only characters as Traditional", () => {
39
+ expect(detectScript("國學說這時會對後")).toBe("zh-Hant");
40
+ });
41
+
42
+ it("decides a mixed passage by majority", () => {
43
+ // A Traditional post that quotes a short Simplified phrase.
44
+ expect(detectScript("這篇文章談的是國學說法,裡頭引了一句「这个」")).toBe(
45
+ "zh-Hant",
46
+ );
47
+ });
48
+
49
+ it("names the script but not the variant when the two sides tie", () => {
50
+ // Three distinctive characters each. Which variant is a coin toss; that
51
+ // the text is Chinese is not in doubt.
52
+ expect(detectScript("這個說这个说")).toBe("han");
53
+ });
54
+
55
+ it("names the script but not the variant for characters shared by both", () => {
56
+ // "我今天很好" is identical in Simplified and Traditional.
57
+ expect(detectScript("我今天很好")).toBe("han");
58
+ // Repetition across paragraphs adds length, never a distinguishing
59
+ // character — the case that first showed this up.
60
+ expect(detectScript("哈哈哈哈哈哈哈啊哈哈哈\n\n".repeat(5))).toBe("han");
61
+ });
62
+
63
+ it("reads Latin script as Latin, accents included", () => {
64
+ expect(detectScript("Hello there")).toBe("latin");
65
+ expect(detectScript("Café crème à Paris")).toBe("latin");
66
+ });
67
+
68
+ it("refuses text with no letters at all", () => {
69
+ expect(detectScript("123 — !!! 🙂")).toBeNull();
70
+ expect(detectScript("")).toBeNull();
71
+ });
72
+
73
+ it("refuses a fragment, whichever script it is in", () => {
74
+ // The first few characters of a post decide nothing — an answer here would
75
+ // change on the next keystroke, which is worse than no answer.
76
+ expect(detectScript("H")).toBeNull();
77
+ expect(detectScript("Hello")).toBeNull();
78
+ expect(detectScript("这会")).toBeNull();
79
+ expect(detectScript("こん")).toBeNull();
80
+ });
81
+
82
+ it("answers once there are a couple of words", () => {
83
+ expect(detectScript("Hello there")).toBe("latin");
84
+ expect(detectScript("这是中文")).toBe("zh-Hans");
85
+ });
86
+
87
+ it("reads a passage by what it is mostly in", () => {
88
+ // A Han character used to speak for the whole post no matter how much
89
+ // English surrounded it.
90
+ expect(
91
+ detectScript(
92
+ "The word 说 comes up in every other sentence of this chapter.",
93
+ ),
94
+ ).toBe("latin");
95
+ // ...and the reverse still holds: a Chinese passage quoting a English
96
+ // word is Chinese.
97
+ expect(detectScript("这篇讲的是国学时间,里头引了 React")).toBe("zh-Hans");
98
+ });
99
+ });
100
+
101
+ describe("detectContentLanguage", () => {
102
+ const zhEn = { languages: ["zh-Hans", "en"], fallback: "zh-Hans" };
103
+
104
+ it("picks the language matching the detected script", () => {
105
+ expect(detectContentLanguage("这是中文", zhEn)).toBe("zh-Hans");
106
+ expect(detectContentLanguage("This is English", zhEn)).toBe("en");
107
+ });
108
+
109
+ it("matches Simplified and Traditional as distinct languages", () => {
110
+ const both = {
111
+ languages: ["zh-Hans", "zh-Hant", "en"],
112
+ fallback: "zh-Hans",
113
+ };
114
+
115
+ expect(detectContentLanguage("國學說這時會對後", both)).toBe("zh-Hant");
116
+ expect(detectContentLanguage("国学说这时会对后", both)).toBe("zh-Hans");
117
+ });
118
+
119
+ it("matches a language tag that only implies its script", () => {
120
+ // The site configured zh-TW, not zh-Hant.
121
+ expect(
122
+ detectContentLanguage("國學說這時", {
123
+ languages: ["en", "zh-TW"],
124
+ fallback: "en",
125
+ }),
126
+ ).toBe("zh-TW");
127
+ });
128
+
129
+ it("will not choose between two non-CJK languages", () => {
130
+ // The Latin alphabet does not distinguish English from French.
131
+ expect(
132
+ detectContentLanguage("Bonjour tout le monde", {
133
+ languages: ["en", "fr", "ja"],
134
+ fallback: "en",
135
+ }),
136
+ ).toBe("en");
137
+ });
138
+
139
+ it("reads shared Han as the site's one Chinese language", () => {
140
+ // Text written the same way in both variants. With one of the two
141
+ // published there is nothing left to be ambiguous about.
142
+ expect(detectContentLanguage("哈哈哈哈哈哈哈啊哈哈哈", zhEn)).toBe(
143
+ "zh-Hans",
144
+ );
145
+ expect(
146
+ detectContentLanguage("我今天很好", {
147
+ languages: ["zh-TW", "en"],
148
+ fallback: "en",
149
+ }),
150
+ ).toBe("zh-TW");
151
+ });
152
+
153
+ it("reads shared Han as Simplified when that is the only language", () => {
154
+ // Nothing to weigh it against: every candidate the site has is Simplified
155
+ // Chinese, so text that is certainly Chinese is certainly that.
156
+ expect(
157
+ readContentLanguage("哈哈哈哈哈哈哈啊哈哈哈", {
158
+ languages: ["zh-Hans"],
159
+ }),
160
+ ).toBe("zh-Hans");
161
+ });
162
+
163
+ it("will not choose between Simplified and Traditional", () => {
164
+ // Both published, and the text distinguishes neither: the fallback is the
165
+ // only honest answer.
166
+ expect(
167
+ readContentLanguage("我今天很好", {
168
+ languages: ["zh-Hans", "zh-Hant", "en"],
169
+ }),
170
+ ).toBeNull();
171
+ });
172
+
173
+ it("falls back when the site does not publish the detected script", () => {
174
+ expect(
175
+ detectContentLanguage("これは日本語です", {
176
+ languages: ["zh-Hans", "en"],
177
+ fallback: "zh-Hans",
178
+ }),
179
+ ).toBe("zh-Hans");
180
+ });
181
+
182
+ it("never returns a language outside the configured set", () => {
183
+ const samples = [
184
+ "안녕하세요",
185
+ "これはテスト",
186
+ "國學說這時",
187
+ "国学说这时",
188
+ "Hello",
189
+ "12345",
190
+ "",
191
+ ];
192
+
193
+ for (const sample of samples) {
194
+ expect(zhEn.languages, sample).toContain(
195
+ detectContentLanguage(sample, zhEn),
196
+ );
197
+ }
198
+ });
199
+ });
200
+
201
+ describe("suggestPostLanguage", () => {
202
+ const site = { languages: ["zh-Hans", "en"], primary: "zh-Hans" };
203
+
204
+ it("reads the language out of the text", () => {
205
+ expect(suggestPostLanguage({ ...site, text: "Hello there" })).toBe("en");
206
+ expect(suggestPostLanguage({ ...site, text: "这是中文" })).toBe("zh-Hans");
207
+ });
208
+
209
+ it("falls back to the primary language for empty text", () => {
210
+ expect(suggestPostLanguage({ ...site, text: " " })).toBe("zh-Hans");
211
+ expect(suggestPostLanguage(site)).toBe("zh-Hans");
212
+ });
213
+
214
+ it("keeps the primary language until the text says otherwise", () => {
215
+ // Half a word in: the composer shows the language of the page the author
216
+ // is writing from, and stays there until there is something to read.
217
+ expect(suggestPostLanguage({ ...site, text: "Hel" })).toBe("zh-Hans");
218
+ expect(suggestPostLanguage({ ...site, text: "Hello there" })).toBe("en");
219
+ });
220
+
221
+ it("returns null on a site that is not multilingual", () => {
222
+ // Nothing is stamped before the author turns the feature on.
223
+ expect(
224
+ suggestPostLanguage({
225
+ languages: [],
226
+ primary: "zh-Hans",
227
+ text: "Hello there",
228
+ }),
229
+ ).toBeNull();
230
+ });
231
+ });
@@ -90,6 +90,111 @@ describe("markdownToTiptapJson", () => {
90
90
  expect(doc.content[0].content).toHaveLength(3);
91
91
  });
92
92
 
93
+ // Tiptap's own tokenizer gave an empty item no content at all, which the
94
+ // schema rejects and the Markdown serializer threw on.
95
+ it("gives an empty list item the paragraph the schema requires", () => {
96
+ const doc = parse("1. First\n2.\n3. Third");
97
+ expect(doc.content[0].type).toBe("orderedList");
98
+ expect(doc.content[0].content).toHaveLength(3);
99
+ expect(doc.content[0].content[1].type).toBe("listItem");
100
+ expect(doc.content[0].content[1].content).toMatchObject([
101
+ { type: "paragraph" },
102
+ ]);
103
+ });
104
+
105
+ it.each([
106
+ ["<strong>粗</strong>", "bold"],
107
+ ["<b>粗</b>", "bold"],
108
+ ["<em>斜</em>", "italic"],
109
+ ["<i>斜</i>", "italic"],
110
+ ["<s>删</s>", "strike"],
111
+ ["<del>删</del>", "strike"],
112
+ ])("reads %s as a %s mark", (html, mark) => {
113
+ const doc = parse(`前${html}后`);
114
+ const marked = doc.content[0].content.find(
115
+ (node: TiptapNode) => node.marks?.length,
116
+ );
117
+ expect(marked.marks).toEqual([{ type: mark }]);
118
+ expect(
119
+ doc.content[0].content.map((n: TiptapNode) => n.text).join(""),
120
+ ).toBe(`前${html.replace(/<[^>]+>/g, "")}后`);
121
+ });
122
+
123
+ it("parses Markdown inside an HTML emphasis tag", () => {
124
+ const doc = parse("<strong>[链接](https://example.com)。</strong>后");
125
+ const first = doc.content[0].content[0];
126
+ expect(first.text).toBe("链接");
127
+ expect(first.marks.map((mark: TiptapMark) => mark.type).sort()).toEqual([
128
+ "bold",
129
+ "link",
130
+ ]);
131
+ });
132
+
133
+ it.each([
134
+ '<strong onclick="alert(1)">x</strong>',
135
+ "<span>x</span>",
136
+ "<script>x</script>",
137
+ ])("keeps %j as text", (html) => {
138
+ const doc = parse(html);
139
+ expect(doc.content[0].content).toEqual([{ type: "text", text: html }]);
140
+ });
141
+
142
+ it("keeps an ordered list's start number", () => {
143
+ const doc = parse("3. Third\n4. Fourth");
144
+ expect(doc.content[0].type).toBe("orderedList");
145
+ expect(doc.content[0].attrs).toEqual({ start: 3 });
146
+ });
147
+
148
+ // CommonMark list markers are digits. Tiptap's own tokenizer also takes
149
+ // letters and roman numerals, which ate the first word of these lines.
150
+ it.each([
151
+ "Ps. 今天发现苹果尼日利亚区的终身会员",
152
+ "PS. 补充一句",
153
+ "Mr. Smith went to Washington.",
154
+ "No. 5 is here.",
155
+ "OK. Fine.",
156
+ "I. Introduction",
157
+ "a. first",
158
+ "mix. of words",
159
+ ])("keeps %j a paragraph", (line) => {
160
+ const doc = parse(line);
161
+ expect(doc.content).toEqual([
162
+ { type: "paragraph", content: [{ type: "text", text: line }] },
163
+ ]);
164
+ });
165
+
166
+ it("keeps a code block inside a list item exactly as written", () => {
167
+ const doc = parse(
168
+ [
169
+ "1. Create the project:",
170
+ " ```bash",
171
+ " ├── .github",
172
+ " │ └── workflows",
173
+ " └── package.json",
174
+ " ```",
175
+ "2. Clone it",
176
+ ].join("\n"),
177
+ );
178
+ const list = doc.content[0];
179
+ expect(list.type).toBe("orderedList");
180
+ expect(list.content).toHaveLength(2);
181
+ const codeBlock = list.content[0].content.find(
182
+ (node: TiptapNode) => node.type === "codeBlock",
183
+ );
184
+ expect(codeBlock.content[0].text).toBe(
185
+ "├── .github\n│ └── workflows\n└── package.json",
186
+ );
187
+ });
188
+
189
+ it("keeps a wide marker's continuation lines intact", () => {
190
+ const doc = parse("10. Run:\n\n ```sh\n ls -la\n ```");
191
+ const codeBlock = doc.content[0].content[0].content.find(
192
+ (node: TiptapNode) => node.type === "codeBlock",
193
+ );
194
+ expect(doc.content[0].attrs).toEqual({ start: 10 });
195
+ expect(codeBlock.content[0].text).toBe("ls -la");
196
+ });
197
+
93
198
  it("converts horizontal rules", () => {
94
199
  const doc = parse("Above\n\n---\n\nBelow");
95
200
  const hr = doc.content.find(
@@ -2,6 +2,16 @@ import { describe, it, expect } from "vitest";
2
2
  import { render, toPlainText, extractTitle } from "../markdown.js";
3
3
 
4
4
  describe("render", () => {
5
+ it("renders the HTML emphasis tags the serializer writes for CJK text", () => {
6
+ expect(render("而是:<strong>固定时间。</strong>来的人")).toBe(
7
+ "<p>而是:<strong>固定时间。</strong>来的人</p>",
8
+ );
9
+ });
10
+
11
+ it("escapes HTML tags outside the emphasis allowlist", () => {
12
+ expect(render("<span>x</span>")).toBe("<p>&lt;span&gt;x&lt;/span&gt;</p>");
13
+ });
14
+
5
15
  it("renders a heading", () => {
6
16
  const html = render("# Hello");
7
17
  expect(html).toContain("<h1>");
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Media } from "../../types.js";
3
+ import { buildMediaMap } from "../media-helpers.js";
4
+
5
+ const R2_PUBLIC_URL = "https://cdn.example.com";
6
+ const IMAGE_TRANSFORM_URL = "https://example.com/cdn-cgi/image";
7
+
8
+ function makeMedia(overrides: Partial<Media> = {}): Media {
9
+ return {
10
+ id: "med_01j0000000000000000000000",
11
+ siteId: "sit_test00000000000000000000000",
12
+ postId: "pst_01j0000000000000000000000",
13
+ filename: "clip.mp4",
14
+ originalName: "clip.mp4",
15
+ mimeType: "video/mp4",
16
+ size: 4_000_000,
17
+ storageKey: "media/clip.mp4",
18
+ provider: "r2",
19
+ width: 1920,
20
+ height: 1080,
21
+ durationSeconds: 12,
22
+ alt: null,
23
+ position: "a0",
24
+ blurhash: null,
25
+ waveform: null,
26
+ posterKey: "posters/clip-poster.png",
27
+ summary: null,
28
+ chars: null,
29
+ mediaKind: "video",
30
+ createdAt: 1706745600,
31
+ updatedAt: 1706745600,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function firstAttachment(media: Media) {
37
+ const map = buildMediaMap(
38
+ new Map([["pst_01j0000000000000000000000", [media]]]),
39
+ R2_PUBLIC_URL,
40
+ IMAGE_TRANSFORM_URL,
41
+ );
42
+ const list = map.get("pst_01j0000000000000000000000");
43
+ expect(list).toHaveLength(1);
44
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked above
45
+ return list![0]!;
46
+ }
47
+
48
+ describe("buildMediaMap", () => {
49
+ it("sizes a video poster through the image transform", () => {
50
+ // A poster is a still frame at the source video's resolution, so an
51
+ // untransformed one puts the full-size original in the timeline. This is
52
+ // the same treatment `toMediaView` gives the field.
53
+ expect(firstAttachment(makeMedia()).posterUrl).toBe(
54
+ "https://example.com/cdn-cgi/image/width=640,quality=80,format=auto,fit=scale-down/https://cdn.example.com/posters/clip-poster.png",
55
+ );
56
+ });
57
+
58
+ it("leaves the poster URL untransformed when no transform is configured", () => {
59
+ const map = buildMediaMap(
60
+ new Map([["pst_01j0000000000000000000000", [makeMedia()]]]),
61
+ R2_PUBLIC_URL,
62
+ );
63
+ expect(map.get("pst_01j0000000000000000000000")?.[0]?.posterUrl).toBe(
64
+ "https://cdn.example.com/posters/clip-poster.png",
65
+ );
66
+ });
67
+
68
+ it("has no poster URL without a poster key", () => {
69
+ expect(
70
+ firstAttachment(makeMedia({ posterKey: null })).posterUrl,
71
+ ).toBeNull();
72
+ });
73
+
74
+ it("leaves a video's own URL alone", () => {
75
+ // Only images go through the transform; asking it to resize an mp4
76
+ // returns nothing useful.
77
+ expect(firstAttachment(makeMedia()).previewUrl).toBe(
78
+ "https://cdn.example.com/media/clip.mp4",
79
+ );
80
+ });
81
+ });
@@ -1,6 +1,10 @@
1
1
  import type { Context } from "hono";
2
2
  import { describe, expect, it } from "vitest";
3
- import { getNavigationData } from "../navigation.js";
3
+ import type { NavItemView } from "../../types.js";
4
+ import {
5
+ collectNavigationCollectionIds,
6
+ getNavigationData,
7
+ } from "../navigation.js";
4
8
 
5
9
  describe("getNavigationData", () => {
6
10
  it("renders site footer markdown through the shared pipeline", async () => {
@@ -99,6 +103,61 @@ describe("getNavigationData", () => {
99
103
  expect(result.links[1]?.url).toBe("/");
100
104
  });
101
105
 
106
+ it("hides the built-in Subscribe item when feeds are disabled", async () => {
107
+ const context = {
108
+ var: {
109
+ publicPath: "/",
110
+ appConfig: {
111
+ siteName: "Jant",
112
+ sitePathPrefix: "",
113
+ siteOrigin: "https://example.com",
114
+ siteDescription: "",
115
+ siteDescriptionExplicit: false,
116
+ siteAvatarUrl: "",
117
+ showHeaderAvatar: false,
118
+ siteFooter: "",
119
+ rssFeedsEnabled: false,
120
+ },
121
+ services: {
122
+ navItems: {
123
+ list: async () => [
124
+ {
125
+ id: "nav_subscribe",
126
+ type: "system",
127
+ systemKey: "subscribe",
128
+ label: "Subscribe",
129
+ url: "/subscribe",
130
+ placement: "header",
131
+ position: "a0",
132
+ createdAt: 1,
133
+ updatedAt: 1,
134
+ },
135
+ {
136
+ id: "nav_archive",
137
+ type: "system",
138
+ systemKey: "archive",
139
+ label: "All",
140
+ url: "/archive",
141
+ placement: "header",
142
+ position: "a1",
143
+ createdAt: 1,
144
+ updatedAt: 1,
145
+ },
146
+ ],
147
+ },
148
+ collections: { list: async () => [] },
149
+ },
150
+ isAuthenticated: false,
151
+ session: null,
152
+ },
153
+ } as unknown as Context;
154
+
155
+ const result = await getNavigationData(context);
156
+
157
+ // The page it addresses 404s without feeds, so the entry has to go too.
158
+ expect(result.links.map((link) => link.url)).toEqual(["/archive"]);
159
+ });
160
+
102
161
  it("hides only the built-in RSS item when feeds are disabled", async () => {
103
162
  const context = {
104
163
  var: {
@@ -153,4 +212,79 @@ describe("getNavigationData", () => {
153
212
 
154
213
  expect(result.links.map((link) => link.id)).toEqual(["nav_custom"]);
155
214
  });
215
+
216
+ // A post page's URL carries no language prefix, so its chrome is scoped by
217
+ // the post's language instead of the request path.
218
+ it("scopes the base path to an explicit language", async () => {
219
+ const makeContext = (languageScope: string) =>
220
+ ({
221
+ var: {
222
+ publicPath: "/some-post",
223
+ appConfig: {
224
+ siteName: "Jant",
225
+ sitePathPrefix: "",
226
+ siteDescription: "",
227
+ siteDescriptionExplicit: false,
228
+ siteAvatarUrl: "",
229
+ showHeaderAvatar: false,
230
+ siteFooter: "",
231
+ siteLanguage: "zh-Hans",
232
+ additionalLanguages: ["ja"],
233
+ multilingualEnabled: true,
234
+ rssFeedsEnabled: true,
235
+ },
236
+ services: {
237
+ navItems: {
238
+ list: async () => [
239
+ {
240
+ id: "nav_latest",
241
+ type: "system",
242
+ systemKey: "latest",
243
+ label: "Latest",
244
+ url: "/",
245
+ placement: "header",
246
+ position: "a0",
247
+ createdAt: 1,
248
+ updatedAt: 1,
249
+ },
250
+ ],
251
+ },
252
+ collections: {
253
+ listByRecentActivity: async () => [],
254
+ },
255
+ },
256
+ isAuthenticated: false,
257
+ session: null,
258
+ },
259
+ }) as unknown as Context;
260
+
261
+ const japanese = await getNavigationData(makeContext("ja"), {
262
+ languageScope: "ja",
263
+ });
264
+ expect(japanese.basePath).toBe("/ja");
265
+ expect(japanese.links[0]?.url).toBe("/ja");
266
+
267
+ const primary = await getNavigationData(makeContext("zh-Hans"), {
268
+ languageScope: "zh-Hans",
269
+ });
270
+ expect(primary.basePath).toBe("");
271
+ expect(primary.links[0]?.url).toBe("/");
272
+ });
273
+ });
274
+
275
+ describe("collectNavigationCollectionIds", () => {
276
+ // The directory used to read only `collection` items, so a smart collection
277
+ // already in the navigation still offered "Add to Navigation".
278
+ it("names both kinds of collection and ignores everything else", () => {
279
+ const links = [
280
+ { id: "nav_1", type: "system", systemKey: "home" },
281
+ { id: "nav_2", type: "collection", collectionId: "col_1" },
282
+ { id: "nav_3", type: "smart_collection", smartCollectionId: "smc_1" },
283
+ { id: "nav_4", type: "link", url: "https://example.com" },
284
+ { id: "nav_5", type: "collection" },
285
+ { id: "nav_6", type: "smart_collection" },
286
+ ] as unknown as NavItemView[];
287
+
288
+ expect(collectNavigationCollectionIds(links)).toEqual(["col_1", "smc_1"]);
289
+ });
156
290
  });