@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
@@ -1,12 +1,20 @@
1
1
  import {
2
2
  Extension,
3
3
  Node,
4
+ getSchema,
4
5
  type AnyExtension,
5
6
  type Extensions,
6
7
  type JSONContent,
7
8
  } from "@tiptap/core";
9
+ import { Fragment, type Schema } from "@tiptap/pm/model";
8
10
  import { MarkdownManager } from "@tiptap/markdown";
9
11
  import CodeBlock from "@tiptap/extension-code-block";
12
+ import { OrderedList } from "@tiptap/extension-list";
13
+ import HardBreak from "@tiptap/extension-hard-break";
14
+ import Paragraph from "@tiptap/extension-paragraph";
15
+ import Bold from "@tiptap/extension-bold";
16
+ import Italic from "@tiptap/extension-italic";
17
+ import Strike from "@tiptap/extension-strike";
10
18
  import Link from "@tiptap/extension-link";
11
19
  import StarterKit from "@tiptap/starter-kit";
12
20
  import {
@@ -224,6 +232,333 @@ const SemanticLink = Link.extend({
224
232
  clearable: false,
225
233
  });
226
234
 
235
+ const LINE_START_BLOCK_SYNTAX: ReadonlyArray<[RegExp, string]> = [
236
+ // `1986. A good year`, `2) Second`: an ordered-list marker
237
+ [/^([ \t]{0,3})(\d{1,9})([.)])(?=[ \t]|$)/, "$1$2\\$3"],
238
+ // `- note`, `+ note`: a bullet-list marker (`*` is escaped inline already)
239
+ [/^([ \t]{0,3})([-+])(?=[ \t]|$)/, "$1\\$2"],
240
+ // `# note`: an ATX heading
241
+ [/^([ \t]{0,3})(#{1,6})(?=[ \t]|$)/, "$1\\$2"],
242
+ // `---`, `===`: a thematic break or a setext underline for the line above
243
+ [/^([ \t]{0,3})([-=])(?=[-= \t]*$)/, "$1\\$2"],
244
+ ];
245
+
246
+ /**
247
+ * Backslash-escape block syntax that a paragraph line happens to open with.
248
+ *
249
+ * Text nodes only get inline escaping, so a paragraph reading `1. Pony`, or a
250
+ * hard break followed by `- note`, came back from the Markdown as a list.
251
+ *
252
+ * @param markdown - A paragraph's rendered Markdown
253
+ * @returns The same Markdown with each line's leading block syntax escaped
254
+ * @example
255
+ * escapeLineStartBlockSyntax("Update: \n1. Pony"); // "Update: \n1\\. Pony"
256
+ */
257
+ function escapeLineStartBlockSyntax(markdown: string): string {
258
+ return markdown
259
+ .split("\n")
260
+ .map((line) =>
261
+ LINE_START_BLOCK_SYNTAX.reduce(
262
+ (escaped, [pattern, replacement]) =>
263
+ escaped.replace(pattern, replacement),
264
+ line,
265
+ ),
266
+ )
267
+ .join("\n");
268
+ }
269
+
270
+ /** The HTML tag each emphasis mark falls back to. */
271
+ const HTML_EMPHASIS_TAGS = {
272
+ bold: "strong",
273
+ italic: "em",
274
+ strike: "s",
275
+ } as const;
276
+
277
+ type EmphasisMarkType = keyof typeof HTML_EMPHASIS_TAGS;
278
+
279
+ const EMPHASIS_MARK_TYPES = Object.keys(
280
+ HTML_EMPHASIS_TAGS,
281
+ ) as EmphasisMarkType[];
282
+
283
+ /** Marks that write their own delimiter characters around the text. */
284
+ const DELIMITED_MARK_TYPES = new Set<string>([
285
+ ...EMPHASIS_MARK_TYPES,
286
+ "code",
287
+ "link",
288
+ ]);
289
+
290
+ /**
291
+ * Serialization-only mark attribute: write this run with HTML tags. It is
292
+ * set by `markEmphasisDelimiters` on a copy of the document and never stored.
293
+ */
294
+ const HTML_EMPHASIS_ATTR = "markdownAsHtml";
295
+
296
+ type FlankingClass = "space" | "punctuation" | "other";
297
+
298
+ function classifyFlankingChar(char: string | undefined): FlankingClass {
299
+ if (char === undefined || /\s/u.test(char)) return "space";
300
+ return /[\p{P}\p{S}]/u.test(char) ? "punctuation" : "other";
301
+ }
302
+
303
+ function hasMark(node: JSONContent | undefined, type: string): boolean {
304
+ return node?.marks?.some((mark) => mark.type === type) ?? false;
305
+ }
306
+
307
+ /**
308
+ * Whether a `**`, `*`, or `~~` at one end of a run can open or close it.
309
+ *
310
+ * CommonMark's flanking rule, which GFM strikethrough shares: the character
311
+ * on the text side must not be whitespace, and when it is punctuation, the
312
+ * character on the outside must be whitespace or punctuation too. The rule is
313
+ * symmetric, so one check covers the opening and the closing delimiter.
314
+ *
315
+ * @param nodes - The inline nodes of one block
316
+ * @param index - The run's first node (`side: "start"`) or last (`"end"`)
317
+ * @param side - Which end of the run
318
+ * @param markType - The run's mark
319
+ * @returns True when Markdown delimiters work at this end
320
+ */
321
+ function delimiterFlanks(
322
+ nodes: JSONContent[],
323
+ index: number,
324
+ side: "start" | "end",
325
+ markType: string,
326
+ ): boolean {
327
+ const node = nodes[index];
328
+ const neighbor = nodes[side === "start" ? index - 1 : index + 1];
329
+ const chars = [...(node?.text ?? "")];
330
+ const edge = side === "start" ? chars : chars.slice().reverse();
331
+ const innerChar = edge.find((char) => !/\s/u.test(char));
332
+
333
+ // Another mark opening or closing at the same edge writes its delimiter
334
+ // between ours and the text; it counts as punctuation.
335
+ const sharesEdge =
336
+ node?.marks?.some(
337
+ (mark) =>
338
+ mark.type !== markType &&
339
+ DELIMITED_MARK_TYPES.has(mark.type) &&
340
+ !hasMark(neighbor, mark.type),
341
+ ) ?? false;
342
+ const inner = sharesEdge ? "punctuation" : classifyFlankingChar(innerChar);
343
+
344
+ // The serializer moves whitespace at a run's edge outside the delimiter.
345
+ let outer: FlankingClass;
346
+ if (edge[0] !== undefined && /\s/u.test(edge[0])) outer = "space";
347
+ else if (!neighbor || neighbor.type === "hardBreak") outer = "space";
348
+ else if (neighbor.type === "text") {
349
+ const neighborChars = [...(neighbor.text ?? "")];
350
+ outer = classifyFlankingChar(
351
+ side === "start" ? neighborChars.at(-1) : neighborChars[0],
352
+ );
353
+ } else outer = "punctuation"; // `![…](…)`, `[^1]`
354
+
355
+ return inner !== "space" && (inner !== "punctuation" || outer !== "other");
356
+ }
357
+
358
+ function markEmphasisRuns(nodes: JSONContent[]): JSONContent[] {
359
+ const result = nodes.map((node) => ({ ...node }));
360
+
361
+ for (const markType of EMPHASIS_MARK_TYPES) {
362
+ let start = 0;
363
+ while (start < result.length) {
364
+ if (result[start]?.type !== "text" || !hasMark(result[start], markType)) {
365
+ start += 1;
366
+ continue;
367
+ }
368
+
369
+ // The serializer closes marks around any non-text node, so a run is
370
+ // consecutive text nodes.
371
+ let end = start;
372
+ while (
373
+ result[end + 1]?.type === "text" &&
374
+ hasMark(result[end + 1], markType)
375
+ ) {
376
+ end += 1;
377
+ }
378
+
379
+ if (
380
+ !delimiterFlanks(result, start, "start", markType) ||
381
+ !delimiterFlanks(result, end, "end", markType)
382
+ ) {
383
+ for (let index = start; index <= end; index += 1) {
384
+ const node = result[index] as JSONContent;
385
+ node.marks = node.marks?.map((mark) =>
386
+ mark.type === markType
387
+ ? {
388
+ ...mark,
389
+ attrs: { ...mark.attrs, [HTML_EMPHASIS_ATTR]: true },
390
+ }
391
+ : mark,
392
+ );
393
+ }
394
+ }
395
+
396
+ start = end + 1;
397
+ }
398
+ }
399
+
400
+ return result;
401
+ }
402
+
403
+ /**
404
+ * Flag the emphasis runs whose Markdown delimiters a CommonMark parser would
405
+ * leave as literal characters, so they serialize as HTML tags instead.
406
+ *
407
+ * Chinese and Japanese put no space around punctuation, so `**说话。**来的人`
408
+ * is common, and neither Hugo (goldmark) nor Jant's own parser reads the
409
+ * closing `**` after `。` followed by `来`. `<strong>…</strong>` reads the
410
+ * same in both, and the rest of the Markdown stays as it was.
411
+ *
412
+ * @param node - A TipTap document or descendant
413
+ * @returns A copy with unflankable runs flagged
414
+ * @example
415
+ * markEmphasisDelimiters(doc); // bold "说话。" before "来" gets the flag
416
+ */
417
+ function markEmphasisDelimiters(node: JSONContent): JSONContent {
418
+ if (!node.content || node.type === "codeBlock") return node;
419
+
420
+ const content = node.content.map(markEmphasisDelimiters);
421
+ const hasInline = content.some(
422
+ (child) => child.type === "text" || child.type === "hardBreak",
423
+ );
424
+ return { ...node, content: hasInline ? markEmphasisRuns(content) : content };
425
+ }
426
+
427
+ function renderEmphasis(
428
+ node: JSONContent,
429
+ content: string,
430
+ markType: EmphasisMarkType,
431
+ delimiter: string,
432
+ ): string {
433
+ if (!node.attrs?.[HTML_EMPHASIS_ATTR]) {
434
+ return `${delimiter}${content}${delimiter}`;
435
+ }
436
+ const tag = HTML_EMPHASIS_TAGS[markType];
437
+ return `<${tag}>${content}</${tag}>`;
438
+ }
439
+
440
+ const MarkdownBold = Bold.extend({
441
+ renderMarkdown(node, helpers) {
442
+ return renderEmphasis(node, helpers.renderChildren(node), "bold", "**");
443
+ },
444
+ });
445
+
446
+ const MarkdownItalic = Italic.extend({
447
+ renderMarkdown(node, helpers) {
448
+ return renderEmphasis(node, helpers.renderChildren(node), "italic", "*");
449
+ },
450
+ });
451
+
452
+ const MarkdownStrike = Strike.extend({
453
+ renderMarkdown(node, helpers) {
454
+ return renderEmphasis(node, helpers.renderChildren(node), "strike", "~~");
455
+ },
456
+ });
457
+
458
+ /** Marked token for each HTML emphasis tag the parser accepts. */
459
+ const HTML_EMPHASIS_TOKEN_TYPES: Record<string, "strong" | "em" | "del"> = {
460
+ strong: "strong",
461
+ b: "strong",
462
+ em: "em",
463
+ i: "em",
464
+ s: "del",
465
+ del: "del",
466
+ };
467
+
468
+ const HTML_EMPHASIS_OPEN_PATTERN = /<(?:strong|b|em|i|s|del)>/i;
469
+ const HTML_EMPHASIS_PATTERN = /^<(strong|b|em|i|s|del)>([\s\S]*?)<\/\1>/i;
470
+
471
+ /**
472
+ * Reads the HTML tags `markEmphasisDelimiters` writes back as marks.
473
+ *
474
+ * Only bare tags: `<strong onclick=…>` and every other tag stay text, as
475
+ * inline HTML always has. The content between the tags is Markdown.
476
+ */
477
+ const MarkdownHtmlEmphasis = Extension.create({
478
+ name: "markdownHtmlEmphasis",
479
+
480
+ markdownTokenizer: {
481
+ name: "htmlEmphasis",
482
+ level: "inline",
483
+ start(src: string) {
484
+ return src.search(HTML_EMPHASIS_OPEN_PATTERN);
485
+ },
486
+ tokenize(src: string, _tokens, helpers) {
487
+ const match = HTML_EMPHASIS_PATTERN.exec(src);
488
+ if (!match) return undefined;
489
+
490
+ const [raw, tag = "", text = ""] = match;
491
+ const type = HTML_EMPHASIS_TOKEN_TYPES[tag.toLowerCase()];
492
+ if (!type) return undefined;
493
+ return { type, raw, text, tokens: helpers.inlineTokens(text) };
494
+ },
495
+ },
496
+ });
497
+
498
+ /**
499
+ * Hard breaks as two trailing spaces, except where that line would be blank.
500
+ *
501
+ * A break at the start of a paragraph, or right after another break, puts
502
+ * the spaces on a line of their own. A line of spaces is blank in Markdown:
503
+ * it ended the paragraph, and `- ` left a list item empty with the text
504
+ * after it outside the list. The backslash form keeps something on the line.
505
+ */
506
+ const MarkdownHardBreak = HardBreak.extend({
507
+ renderMarkdown(_node, _helpers, context) {
508
+ const previous = context?.previousNode;
509
+ return !previous || previous.type === "hardBreak" ? "\\\n" : " \n";
510
+ },
511
+ });
512
+
513
+ const renderParagraphMarkdown = Paragraph.config.renderMarkdown;
514
+
515
+ const MarkdownParagraph = Paragraph.extend({
516
+ renderMarkdown(node, helpers, context) {
517
+ const rendered =
518
+ renderParagraphMarkdown?.call(this, node, helpers, context) ?? "";
519
+ return escapeLineStartBlockSyntax(rendered);
520
+ },
521
+ });
522
+
523
+ /**
524
+ * Ordered lists with CommonMark markers: digits only.
525
+ *
526
+ * Tiptap's ordered list also reads letters and roman numerals as markers
527
+ * (`a.`, `IV.`, anything of one or two letters), so a line such as
528
+ * `PS. 补充一句` or `Mr. Smith went` became a list item and lost its first
529
+ * word. Its tokenizer also measured a marker's width without the `.`, which
530
+ * left one stray space on every line of a code block inside a list item.
531
+ *
532
+ * Jant's Markdown is CommonMark plus GFM (docs/internal/markdown-contract.md),
533
+ * the same dialect Hugo reads in an export, so list tokenizing goes back to
534
+ * marked: a tokenizer that never matches leaves the built-in one in charge,
535
+ * and marked's list items go to `listItem` the way bullet lists' do. Tiptap's
536
+ * plain-text paste plugin goes too; it applied the same markers, and the
537
+ * editors already parse pasted plain text as Markdown (`MarkdownClipboard`).
538
+ */
539
+ const CommonMarkOrderedList = OrderedList.extend({
540
+ markdownTokenizer: {
541
+ name: "orderedList",
542
+ level: "block",
543
+ start: () => -1,
544
+ tokenize: () => undefined,
545
+ },
546
+
547
+ parseMarkdown: (token, helpers) => {
548
+ if (token.type !== "list" || !token.ordered) return [];
549
+ const start = typeof token.start === "number" ? token.start : 1;
550
+ return {
551
+ type: "orderedList",
552
+ ...(start === 1 ? {} : { attrs: { start } }),
553
+ content: token.items ? helpers.parseChildren(token.items) : [],
554
+ };
555
+ },
556
+
557
+ addProseMirrorPlugins() {
558
+ return [];
559
+ },
560
+ });
561
+
227
562
  const MarkdownFigureImageSupport = Extension.create({
228
563
  name: "markdownFigureImageSupport",
229
564
 
@@ -824,8 +1159,21 @@ export function createMarkdownContentExtensions(
824
1159
  heading: { levels: [1, 2, 3] },
825
1160
  link: false,
826
1161
  codeBlock: false,
1162
+ orderedList: false,
1163
+ paragraph: false,
1164
+ hardBreak: false,
1165
+ bold: false,
1166
+ italic: false,
1167
+ strike: false,
827
1168
  trailingNode: { notAfter: ["footnoteDefinition"] },
828
1169
  }),
1170
+ MarkdownParagraph,
1171
+ MarkdownHardBreak,
1172
+ MarkdownBold,
1173
+ MarkdownItalic,
1174
+ MarkdownStrike,
1175
+ MarkdownHtmlEmphasis,
1176
+ CommonMarkOrderedList,
829
1177
  SemanticLink.configure({
830
1178
  openOnClick: false,
831
1179
  autolink: false,
@@ -1050,12 +1398,54 @@ export function getMarkdownManager(): MarkdownManager {
1050
1398
  return sharedMarkdownManager;
1051
1399
  }
1052
1400
 
1401
+ let sharedContentSchema: Schema | null = null;
1402
+
1403
+ function getContentSchema(): Schema {
1404
+ sharedContentSchema ??= getSchema(createMarkdownContentExtensions());
1405
+ return sharedContentSchema;
1406
+ }
1407
+
1408
+ /**
1409
+ * Fill in the children a node's schema requires but the document left out.
1410
+ *
1411
+ * A list item must hold a paragraph, a blockquote a block, a doc a block.
1412
+ * Documents that skipped one (an empty `1. ` item from an older Markdown
1413
+ * parser, or JSON posted through the API) made the Markdown serializer throw.
1414
+ * Each empty node that cannot be empty gets what `createAndFill` would give it.
1415
+ *
1416
+ * @param node - A TipTap document or descendant
1417
+ * @returns A copy whose empty required containers are filled
1418
+ * @example
1419
+ * fillRequiredContent({ type: "listItem", content: [] });
1420
+ * // { type: "listItem", content: [{ type: "paragraph" }] }
1421
+ */
1422
+ export function fillRequiredContent(node: JSONContent): JSONContent {
1423
+ const nodeType = node.type ? getContentSchema().nodes[node.type] : undefined;
1424
+ if (!nodeType || nodeType.isLeaf) return node;
1425
+
1426
+ const content = node.content?.map(fillRequiredContent);
1427
+ if ((content?.length ?? 0) === 0 && !nodeType.contentMatch.validEnd) {
1428
+ const filled = nodeType.contentMatch.fillBefore(Fragment.empty, true);
1429
+ if (filled) {
1430
+ return { ...node, content: filled.toJSON() as JSONContent[] };
1431
+ }
1432
+ }
1433
+
1434
+ return content ? { ...node, content } : node;
1435
+ }
1436
+
1053
1437
  export function parseMarkdownDocument(markdown: string): JSONContent {
1054
- return normalizeMarkdownDocument(getMarkdownManager().parse(markdown));
1438
+ return fillRequiredContent(
1439
+ normalizeMarkdownDocument(getMarkdownManager().parse(markdown)),
1440
+ );
1055
1441
  }
1056
1442
 
1057
1443
  export function serializeMarkdownDocument(doc: JSONContent): string {
1058
1444
  return expandCodeBlockFences(
1059
- getMarkdownManager().serialize(normalizeFootnoteArtifacts(doc)),
1445
+ getMarkdownManager().serialize(
1446
+ normalizeFootnoteArtifacts(
1447
+ markEmphasisDelimiters(fillRequiredContent(doc)),
1448
+ ),
1449
+ ),
1060
1450
  );
1061
1451
  }
@@ -58,9 +58,21 @@ export function buildMediaMap(
58
58
  fit: "scale-down",
59
59
  })
60
60
  : mediaUrl;
61
- const posterUrl = m.posterKey
61
+ // A poster is a still frame at whatever resolution the source video
62
+ // had, so it needs the same transform as any other image — without it
63
+ // a 4K clip puts a multi-megabyte PNG in the feed. Matches the limits
64
+ // `toMediaView` uses for the same field.
65
+ const posterRawUrl = m.posterKey
62
66
  ? getMediaUrl(m.posterKey, publicUrl, sitePathPrefix)
63
67
  : null;
68
+ const posterUrl = posterRawUrl
69
+ ? getImageUrl(posterRawUrl, imageTransformUrl, {
70
+ width: 640,
71
+ quality: 80,
72
+ format: "auto",
73
+ fit: "scale-down",
74
+ })
75
+ : null;
64
76
  return {
65
77
  id: m.id,
66
78
  url: mediaUrl,
@@ -6,7 +6,9 @@
6
6
 
7
7
  import type { Context } from "hono";
8
8
  import type { Collection, NavItem, NavItemView } from "../types.js";
9
+ import { isFeedNavKey } from "../types/constants.js";
9
10
  import { toNavItemViews } from "./view.js";
11
+ import { languageScopeBasePath, viewBasePath } from "./view-language.js";
10
12
  import { render as renderMarkdown, toPlainText } from "./markdown.js";
11
13
 
12
14
  /**
@@ -15,7 +17,15 @@ import { render as renderMarkdown, toPlainText } from "./markdown.js";
15
17
  export interface NavigationData {
16
18
  links: NavItemView[];
17
19
  currentPath: string;
20
+ /** Deployment path prefix, such as `/blog`. */
18
21
  sitePathPrefix: string;
22
+ /**
23
+ * Public path prefix for reader-facing surfaces. Equals `sitePathPrefix`
24
+ * outside a language view, and carries the language prefix inside one
25
+ * (`/blog/en`). Pass this — not `sitePathPrefix` — to anything building
26
+ * links to the home page, archive, search, feeds or collections.
27
+ */
28
+ basePath: string;
19
29
  siteName: string;
20
30
  /** Plain-text description for meta tags and RSS/Atom feeds */
21
31
  siteDescription: string;
@@ -23,11 +33,37 @@ export interface NavigationData {
23
33
  siteDescriptionHtml?: string;
24
34
  isAuthenticated: boolean;
25
35
  collections: Collection[];
26
- siteAvatarUrl?: string;
36
+ /** Header/drawer mark, already sized down — not the canonical asset. */
37
+ siteAvatarThumbUrl?: string;
27
38
  showHeaderAvatar?: boolean;
28
39
  siteFooterHtml?: string;
29
40
  }
30
41
 
42
+ /**
43
+ * Collect the collections and smart collections already placed in navigation.
44
+ *
45
+ * The directory offers both kinds the same "Add to Navigation" action, so it
46
+ * needs one list to check against; TypeID prefixes (`col_`, `smc_`) keep the
47
+ * two apart without a second field.
48
+ *
49
+ * @param links - Navigation items as rendered for this request
50
+ * @returns TypeIDs of every collection and smart collection in navigation
51
+ * @example
52
+ * const placed = collectNavigationCollectionIds(navData.links);
53
+ * // ["col_01abc", "smc_01xyz"]
54
+ */
55
+ export function collectNavigationCollectionIds(links: NavItemView[]): string[] {
56
+ return links.flatMap((item) => {
57
+ if (item.type === "collection" && item.collectionId) {
58
+ return [item.collectionId];
59
+ }
60
+ if (item.type === "smart_collection" && item.smartCollectionId) {
61
+ return [item.smartCollectionId];
62
+ }
63
+ return [];
64
+ });
65
+ }
66
+
31
67
  /**
32
68
  * Fetch navigation data for public pages.
33
69
  *
@@ -49,20 +85,45 @@ export interface NavigationData {
49
85
  */
50
86
  export async function getNavigationData(
51
87
  c: Context,
52
- options?: { preloadedItems?: NavItem[]; includeCollections?: boolean },
88
+ options?: {
89
+ preloadedItems?: NavItem[];
90
+ includeCollections?: boolean;
91
+ /**
92
+ * Content language whose view the page's chrome should live in. Post
93
+ * pages pass their post's language here: their URLs carry no language
94
+ * prefix, so `viewLang` is never set on them, yet a Japanese post's
95
+ * logo and nav should lead to `/ja`, not to the primary view.
96
+ */
97
+ languageScope?: string | null;
98
+ },
53
99
  ): Promise<NavigationData> {
100
+ const appConfig = c.var.appConfig;
101
+ const langBase =
102
+ options?.languageScope !== undefined
103
+ ? languageScopeBasePath(c, options.languageScope)
104
+ : viewBasePath(c);
105
+ // An empty prefix means the primary language's view, whose nav items already
106
+ // point where they should — so the whole primary-language site, multilingual
107
+ // or not, never pays for the translation lookup.
108
+ const scopeLanguage = langBase
109
+ ? (options?.languageScope ?? c.var.viewLang ?? null)
110
+ : null;
111
+
54
112
  // Callers that already fetched nav items can pass them in to avoid a
55
113
  // redundant DB round-trip.
56
114
  const savedItems =
57
- options?.preloadedItems ?? (await c.var.services.navItems.list());
115
+ options?.preloadedItems ??
116
+ (await c.var.services.navItems.list({ language: scopeLanguage }));
58
117
  const currentPath = c.var.publicPath;
59
- const appConfig = c.var.appConfig;
60
- // Keep the saved RSS item untouched so its placement and custom label come
61
- // back when feeds are re-enabled. Only the rendered projection is filtered.
118
+ // Keep the saved feed items untouched so their placement and custom labels
119
+ // come back when feeds are re-enabled. Only the rendered projection is
120
+ // filtered. Both entries go: `rss` addresses a feed that is off, and
121
+ // `subscribe` a page that 404s without one.
62
122
  const items = appConfig.rssFeedsEnabled
63
123
  ? savedItems
64
124
  : savedItems.filter(
65
- (item: NavItem) => item.type !== "system" || item.systemKey !== "rss",
125
+ (item: NavItem) =>
126
+ item.type !== "system" || !isFeedNavKey(item.systemKey),
66
127
  );
67
128
 
68
129
  const siteName = appConfig.siteName;
@@ -80,8 +141,10 @@ export async function getNavigationData(
80
141
  })
81
142
  : undefined;
82
143
 
83
- // Avatar URL and display flag come from appConfig
84
- const siteAvatarUrl = appConfig.siteAvatarUrl || undefined;
144
+ // Avatar URL and display flag come from appConfig. The chrome takes the
145
+ // sized variant: the full-resolution asset belongs to the social card and
146
+ // the settings editor, not to a 32px mark on every page.
147
+ const siteAvatarThumbUrl = appConfig.siteAvatarThumbUrl || undefined;
85
148
  const showHeaderAvatar = appConfig.showHeaderAvatar;
86
149
 
87
150
  // Render footer markdown
@@ -107,6 +170,8 @@ export async function getNavigationData(
107
170
  ? await c.var.services.navItems.getCollectionFreshness(collectionNavIds)
108
171
  : undefined;
109
172
 
173
+ const basePath = `${appConfig.sitePathPrefix}${langBase}`;
174
+
110
175
  const links = toNavItemViews(
111
176
  items,
112
177
  currentPath,
@@ -114,6 +179,7 @@ export async function getNavigationData(
114
179
  appConfig.sitePathPrefix,
115
180
  collectionFreshness,
116
181
  appConfig.siteOrigin,
182
+ basePath,
117
183
  );
118
184
 
119
185
  // Only load collections when authenticated (for compose dialog)
@@ -125,13 +191,14 @@ export async function getNavigationData(
125
191
  links,
126
192
  currentPath,
127
193
  sitePathPrefix: appConfig.sitePathPrefix,
194
+ basePath,
128
195
  siteName,
129
196
  siteDescription,
130
197
  siteDescriptionHtml,
131
198
  isAuthenticated,
132
199
  collections,
133
- siteAvatarUrl,
134
- showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
200
+ siteAvatarThumbUrl,
201
+ showHeaderAvatar: showHeaderAvatar && !!siteAvatarThumbUrl,
135
202
  siteFooterHtml,
136
203
  };
137
204
  }
@@ -38,10 +38,18 @@ export function formatPageLabel(
38
38
  return `Page ${String(currentPage)}`;
39
39
  }
40
40
 
41
+ /** Slots a numbered pagination control fills once there are more pages. */
42
+ const PAGE_SLOT_COUNT = 7;
43
+
41
44
  /**
42
45
  * Computes which page numbers to display in a numbered pagination control.
43
- * Always includes: first page, last page, current page, and 1 page on each side of current.
44
- * Gaps between non-consecutive pages are represented by 0 (ellipsis marker).
46
+ *
47
+ * Seven or fewer pages are all shown. Past that, the control always fills
48
+ * exactly seven slots — the first page, the last page, and a three-page
49
+ * window around the current page — so it keeps the same width, and the
50
+ * Previous/Next links stay put, as the reader moves through pages. Skipped
51
+ * pages are represented by 0 (ellipsis marker). An ellipsis always stands in
52
+ * for at least two pages; where it would hide just one, that page is shown.
45
53
  *
46
54
  * @param currentPage - The current active page (1-indexed)
47
55
  * @param totalPages - Total number of pages
@@ -50,36 +58,32 @@ export function formatPageLabel(
50
58
  * @example
51
59
  * ```ts
52
60
  * getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
53
- * getPageNumbers(1, 20) // [1, 2, 0, 20]
54
- * getPageNumbers(10, 20) // [1, 0, 9, 10, 11, 0, 20]
61
+ * getPageNumbers(1, 19) // [1, 2, 3, 4, 5, 0, 19]
62
+ * getPageNumbers(10, 19) // [1, 0, 9, 10, 11, 0, 19]
63
+ * getPageNumbers(19, 19) // [1, 0, 15, 16, 17, 18, 19]
55
64
  * ```
56
65
  */
57
66
  export function getPageNumbers(
58
67
  currentPage: number,
59
68
  totalPages: number,
60
69
  ): number[] {
61
- if (totalPages <= 7) {
70
+ if (totalPages <= PAGE_SLOT_COUNT) {
62
71
  return Array.from({ length: totalPages }, (_, i) => i + 1);
63
72
  }
64
73
 
65
- const pages = new Set<number>();
66
- pages.add(1);
67
- pages.add(totalPages);
68
- pages.add(currentPage);
69
- if (currentPage > 1) pages.add(currentPage - 1);
70
- if (currentPage < totalPages) pages.add(currentPage + 1);
71
-
72
- const sorted = [...pages].sort((a, b) => a - b);
73
-
74
- // Insert 0 for gaps
75
- const result: number[] = [];
76
- for (let i = 0; i < sorted.length; i++) {
77
- const current = sorted[i] as number;
78
- if (i > 0 && current - (sorted[i - 1] as number) > 1) {
79
- result.push(0); // ellipsis marker
80
- }
81
- result.push(current);
82
- }
74
+ // The second slot holds page 2 or an ellipsis, and the second-to-last slot
75
+ // holds page totalPages - 1 or an ellipsis, so the window around the current
76
+ // page starts no earlier than page 3 and ends no later than totalPages - 2.
77
+ const windowStart = Math.min(Math.max(currentPage - 1, 3), totalPages - 4);
78
+ const windowEnd = windowStart + 2;
83
79
 
84
- return result;
80
+ return [
81
+ 1,
82
+ windowStart === 3 ? 2 : 0,
83
+ windowStart,
84
+ windowStart + 1,
85
+ windowEnd,
86
+ windowEnd === totalPages - 2 ? totalPages - 1 : 0,
87
+ totalPages,
88
+ ];
85
89
  }