@jant/core 0.6.13 → 0.6.15

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 (316) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-HnzmsaqU.js → app-C-oi_t8W.js} +7419 -6350
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-3_OaxhTs.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-Dfbq9aot.js → export-CsFx6F_M.js} +1273 -131
  18. package/dist/{github-sync--PSoE-Ne.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-BGTLx8Mf.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
  47. package/src/client/components/__tests__/jant-compose-dialog.test.ts +373 -5
  48. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  49. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  50. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  51. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  52. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  53. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  54. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  55. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  56. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  57. package/src/client/components/collection-manager-types.ts +9 -3
  58. package/src/client/components/compose-types.ts +8 -0
  59. package/src/client/components/jant-collection-directory.ts +165 -30
  60. package/src/client/components/jant-command-palette.ts +102 -71
  61. package/src/client/components/jant-compose-dialog.ts +86 -22
  62. package/src/client/components/jant-compose-editor.ts +13 -7
  63. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  64. package/src/client/components/jant-config-editor.ts +722 -0
  65. package/src/client/components/jant-media-lightbox.ts +100 -18
  66. package/src/client/components/jant-nav-manager.ts +1283 -54
  67. package/src/client/components/jant-post-form.ts +3 -3
  68. package/src/client/components/jant-post-menu.ts +86 -74
  69. package/src/client/components/nav-manager-types.ts +51 -2
  70. package/src/client/components/post-form-types.ts +1 -1
  71. package/src/client/compose-bridge.ts +4 -1
  72. package/src/client/feed-video-player.ts +62 -2
  73. package/src/client/footnote-rail.ts +478 -0
  74. package/src/client/hydrate-partial.ts +2 -0
  75. package/src/client/media-lightbox-events.ts +63 -0
  76. package/src/client/site-header-fragment.ts +38 -0
  77. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  78. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  79. package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
  80. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  81. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +872 -2
  82. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  83. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  84. package/src/client/tiptap/bubble-menu.ts +91 -13
  85. package/src/client/tiptap/continuous-lists.ts +102 -0
  86. package/src/client/tiptap/create-editor.ts +3 -0
  87. package/src/client/tiptap/extensions.ts +14 -5
  88. package/src/client/tiptap/footnotes.ts +156 -0
  89. package/src/client/tiptap/markdown-clipboard.ts +614 -12
  90. package/src/client/tiptap/slash-commands.ts +8 -1
  91. package/src/client/tiptap/structural-keymap.ts +211 -0
  92. package/src/client/tiptap/tab-indent.ts +8 -22
  93. package/src/client/tiptap/table-control-labels.ts +30 -0
  94. package/src/client/tiptap/table-controls.ts +537 -0
  95. package/src/client/tiptap/table-size-picker.ts +254 -0
  96. package/src/client/toast.ts +8 -6
  97. package/src/client-auth.ts +1 -0
  98. package/src/client.ts +1 -0
  99. package/src/db/__tests__/d1-query.test.ts +43 -1
  100. package/src/db/__tests__/migration-runner.test.ts +63 -0
  101. package/src/db/__tests__/migrations.test.ts +469 -0
  102. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  103. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  104. package/src/db/migrations/0028_superb_post.sql +2 -0
  105. package/src/db/migrations/0029_boring_magma.sql +1 -0
  106. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  107. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  108. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  109. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  110. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  111. package/src/db/migrations/meta/_journal.json +28 -0
  112. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  113. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  114. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  115. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  116. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  117. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  118. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  119. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  120. package/src/db/migrations/pg/meta/_journal.json +28 -0
  121. package/src/db/pg/schema.ts +28 -20
  122. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  123. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  124. package/src/db/schema.ts +28 -20
  125. package/src/i18n/__tests__/detect.test.ts +8 -2
  126. package/src/i18n/detect.ts +14 -5
  127. package/src/i18n/locales/public/en.po +133 -30
  128. package/src/i18n/locales/public/en.ts +1 -1
  129. package/src/i18n/locales/public/zh-Hans.po +128 -25
  130. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  131. package/src/i18n/locales/public/zh-Hant.po +128 -25
  132. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  133. package/src/i18n/locales/settings/en.po +513 -10
  134. package/src/i18n/locales/settings/en.ts +1 -1
  135. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  136. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  137. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  138. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  139. package/src/lib/__tests__/api-settings.test.ts +299 -0
  140. package/src/lib/__tests__/constants.test.ts +5 -0
  141. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  142. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  143. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  144. package/src/lib/__tests__/markdown-to-tiptap.test.ts +36 -4
  145. package/src/lib/__tests__/markdown.test.ts +8 -6
  146. package/src/lib/__tests__/navigation.test.ts +59 -2
  147. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  148. package/src/lib/__tests__/post-display.test.ts +64 -0
  149. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  150. package/src/lib/__tests__/summary.test.ts +82 -0
  151. package/src/lib/__tests__/timeline.test.ts +109 -70
  152. package/src/lib/__tests__/timezones.test.ts +31 -3
  153. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  154. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  155. package/src/lib/__tests__/view.test.ts +53 -0
  156. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  157. package/src/lib/api-settings.ts +239 -12
  158. package/src/lib/collection-sort.ts +7 -5
  159. package/src/lib/constants.ts +2 -0
  160. package/src/lib/feed-path.ts +25 -0
  161. package/src/lib/feed-policy.ts +64 -0
  162. package/src/lib/footnotes.ts +264 -0
  163. package/src/lib/github-sync-site-config.ts +2 -0
  164. package/src/lib/hugo-markdown.ts +10 -3
  165. package/src/lib/jant-branding.ts +1 -0
  166. package/src/lib/markdown-manager.ts +202 -10
  167. package/src/lib/markdown.ts +10 -3
  168. package/src/lib/navigation.ts +16 -3
  169. package/src/lib/post-body-html.ts +131 -0
  170. package/src/lib/post-display.ts +7 -1
  171. package/src/lib/render.tsx +9 -2
  172. package/src/lib/resolve-config.ts +100 -16
  173. package/src/lib/schemas.ts +133 -1
  174. package/src/lib/settings-paths.ts +5 -0
  175. package/src/lib/site-skill.ts +78 -0
  176. package/src/lib/summary.ts +16 -5
  177. package/src/lib/timeline.ts +98 -122
  178. package/src/lib/timezones.ts +54 -14
  179. package/src/lib/tiptap-render.ts +355 -81
  180. package/src/lib/view.ts +30 -20
  181. package/src/lib/worker-response-cache.ts +11 -18
  182. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  183. package/src/middleware/config.ts +2 -2
  184. package/src/middleware/public-content-access.ts +87 -0
  185. package/src/node/__tests__/cli-export.test.ts +14 -0
  186. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  187. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  188. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  189. package/src/node/index.ts +2 -1
  190. package/src/preset.css +16 -2
  191. package/src/routes/__tests__/compose.test.ts +51 -0
  192. package/src/routes/__tests__/site-skill.test.ts +112 -0
  193. package/src/routes/api/__tests__/collections.test.ts +91 -19
  194. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  195. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  196. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  197. package/src/routes/api/__tests__/settings.test.ts +217 -0
  198. package/src/routes/api/collections.ts +22 -26
  199. package/src/routes/api/export.ts +2 -0
  200. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  201. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  202. package/src/routes/api/internal/post-body-html.ts +43 -0
  203. package/src/routes/api/internal/sites.ts +24 -0
  204. package/src/routes/api/nav-items.ts +24 -1
  205. package/src/routes/api/posts.ts +2 -2
  206. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  207. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  208. package/src/routes/api/public/archive.ts +4 -0
  209. package/src/routes/api/public/posts.ts +41 -24
  210. package/src/routes/api/search.ts +3 -0
  211. package/src/routes/api/settings.ts +33 -0
  212. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  213. package/src/routes/compose.tsx +2 -1
  214. package/src/routes/dash/settings.tsx +45 -2
  215. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  216. package/src/routes/feed/feed.ts +23 -14
  217. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  218. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  219. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  220. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  221. package/src/routes/pages/__tests__/search.test.ts +69 -0
  222. package/src/routes/pages/archive.tsx +27 -7
  223. package/src/routes/pages/collection.tsx +32 -19
  224. package/src/routes/pages/collections.tsx +6 -1
  225. package/src/routes/pages/page.tsx +83 -11
  226. package/src/routes/site-skill.ts +18 -0
  227. package/src/services/__tests__/auth.test.ts +7 -5
  228. package/src/services/__tests__/collection.test.ts +159 -44
  229. package/src/services/__tests__/navigation.test.ts +229 -21
  230. package/src/services/__tests__/post-timeline.test.ts +109 -30
  231. package/src/services/__tests__/post.test.ts +567 -5
  232. package/src/services/__tests__/search.test.ts +48 -1
  233. package/src/services/__tests__/settings.test.ts +8 -0
  234. package/src/services/__tests__/site-admin.test.ts +121 -1
  235. package/src/services/auth.ts +2 -2
  236. package/src/services/bootstrap.ts +3 -2
  237. package/src/services/collection.ts +226 -167
  238. package/src/services/export-theme/assets/client-site.css +1 -1
  239. package/src/services/export-theme/assets/client-site.js +46 -25
  240. package/src/services/export-theme/layouts/_default/list.html +18 -4
  241. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  242. package/src/services/export-theme/layouts/featured/list.html +5 -4
  243. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  244. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  245. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  246. package/src/services/export-theme/styles/main.css +30 -2
  247. package/src/services/export.ts +91 -80
  248. package/src/services/mcp.ts +37 -27
  249. package/src/services/media.ts +1 -1
  250. package/src/services/navigation.ts +185 -46
  251. package/src/services/post.ts +874 -271
  252. package/src/services/search.ts +8 -2
  253. package/src/services/settings.ts +2 -10
  254. package/src/services/site-admin.ts +63 -43
  255. package/src/style-cjk-jp.css +1 -1
  256. package/src/style-cjk-kr.css +1 -1
  257. package/src/style-cjk-tc.css +1 -1
  258. package/src/style-cjk.css +1 -1
  259. package/src/styles/components.css +540 -0
  260. package/src/styles/site-media.css +111 -19
  261. package/src/styles/tokens.css +14 -9
  262. package/src/styles/ui.css +952 -64
  263. package/src/types/bindings.ts +3 -0
  264. package/src/types/config.ts +243 -12
  265. package/src/types/constants.ts +22 -1
  266. package/src/types/entities.ts +9 -8
  267. package/src/types/operations.ts +18 -10
  268. package/src/types/props.ts +8 -2
  269. package/src/types/raw-assets.d.ts +5 -0
  270. package/src/types/views.ts +16 -4
  271. package/src/ui/__tests__/font-themes.test.ts +42 -30
  272. package/src/ui/compose/ComposeDialog.tsx +92 -2
  273. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  274. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  275. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  276. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  277. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  278. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  279. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  280. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  281. package/src/ui/feed/LinkCard.tsx +2 -12
  282. package/src/ui/feed/NoteCard.tsx +10 -20
  283. package/src/ui/feed/QuoteCard.tsx +2 -12
  284. package/src/ui/feed/ThreadPreview.tsx +26 -34
  285. package/src/ui/feed/TimelineFeed.tsx +2 -2
  286. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  287. package/src/ui/feed/__tests__/timeline-cards.test.ts +186 -6
  288. package/src/ui/feed/thread-preview-state.ts +5 -7
  289. package/src/ui/font-themes.ts +74 -26
  290. package/src/ui/layouts/BaseLayout.tsx +23 -9
  291. package/src/ui/layouts/SiteLayout.tsx +19 -13
  292. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  293. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
  294. package/src/ui/pages/CollectionPage.tsx +101 -22
  295. package/src/ui/pages/CollectionsPage.tsx +3 -1
  296. package/src/ui/pages/PostPage.tsx +39 -12
  297. package/src/ui/pages/SearchPage.tsx +6 -14
  298. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  299. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  300. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  301. package/src/ui/shared/CollectionsManager.tsx +17 -7
  302. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  303. package/src/ui/shared/HomePageBranding.tsx +2 -2
  304. package/src/ui/shared/MediaGallery.tsx +1 -0
  305. package/src/ui/shared/PostFooter.tsx +3 -1
  306. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  307. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  308. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  309. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  310. package/src/ui/shared/collection-management-labels.ts +33 -2
  311. package/src/ui/shared/navigation-labels.ts +2 -4
  312. package/src/ui/shared/post-article-attributes.ts +46 -0
  313. package/dist/app-CWJFPjuR.js +0 -6
  314. package/dist/client/_assets/client-DCrcHVJl.css +0 -2
  315. package/dist/client/_assets/client-auth-TsYq90xm.js +0 -4733
  316. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -30,6 +30,8 @@ export async function buildSyncSiteConfig(c: {
30
30
  siteDescription: appConfig.siteDescription,
31
31
  siteLanguage: appConfig.siteLanguage,
32
32
  showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
33
+ publicApiEnabled: appConfig.publicApiEnabled,
34
+ rssFeedsEnabled: appConfig.rssFeedsEnabled,
33
35
  mainRssFeed: appConfig.mainRssFeed,
34
36
  siteFooter: appConfig.siteFooter,
35
37
  showHeaderAvatar: appConfig.showHeaderAvatar,
@@ -16,8 +16,9 @@
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
18
  /**
19
- * Per-collection membership metadata. Preserved verbatim on round-trip so
20
- * curators keep their collected_at / position / pinned_at ordering.
19
+ * Per-Thread collection membership metadata. Current exports write this
20
+ * only on the Thread root bundle. Import also accepts legacy reply-level
21
+ * entries and folds them into the root without losing metadata.
21
22
  */
22
23
  export interface HugoCollectionRef {
23
24
  slug: string;
@@ -111,10 +112,14 @@ export interface HugoFrontMatter {
111
112
  featured_at?: string | null;
112
113
  pinned_at?: string | null;
113
114
 
115
+ // Derived Featured Thread projection (root bundles only)
116
+ featured_post_ids?: string[];
117
+ featured_sort_at?: string;
118
+
114
119
  // Round-trip bookkeeping
115
120
  root_aliases?: string[];
116
121
 
117
- // Memberships + attachments
122
+ // Thread memberships (root bundles only) + attachments
118
123
  collections?: HugoCollectionRef[];
119
124
  media?: JantMedia[];
120
125
 
@@ -201,6 +206,8 @@ const FRONT_MATTER_KEY_ORDER: readonly string[] = [
201
206
  "quote_text",
202
207
  "rating",
203
208
  "featured_at",
209
+ "featured_post_ids",
210
+ "featured_sort_at",
204
211
  "pinned_at",
205
212
 
206
213
  // Bookkeeping / attachments
@@ -23,6 +23,7 @@ import { toPublicPath } from "./url.js";
23
23
 
24
24
  export { JANT_APP_ICON_CORNER_RADIUS, JANT_LOGO_PATH_DATA, JANT_LOGO_VIEW_BOX };
25
25
 
26
+ export const JANT_HOME_URL = "https://jant.me";
26
27
  export const JANT_REPO_URL = "https://github.com/jant-me/jant";
27
28
  export const HOME_BRANDING_PREFIX = "Build with";
28
29
  export const HOME_BRANDING_LINK_LABEL = "Jant";
@@ -7,6 +7,7 @@ import {
7
7
  } from "@tiptap/core";
8
8
  import { MarkdownManager } from "@tiptap/markdown";
9
9
  import CodeBlock from "@tiptap/extension-code-block";
10
+ import Link from "@tiptap/extension-link";
10
11
  import StarterKit from "@tiptap/starter-kit";
11
12
  import {
12
13
  Table,
@@ -16,6 +17,7 @@ import {
16
17
  } from "@tiptap/extension-table";
17
18
  import {
18
19
  getFootnoteDefinitionLabelText,
20
+ getFootnoteLabelKey,
19
21
  getFootnoteReferenceText,
20
22
  indentFootnoteMarkdown,
21
23
  normalizeFootnoteArtifacts,
@@ -217,6 +219,10 @@ const MarkdownCodeBlock = CodeBlock.extend({
217
219
  },
218
220
  });
219
221
 
222
+ const SemanticLink = Link.extend({
223
+ clearable: false,
224
+ });
225
+
220
226
  const MarkdownFigureImageSupport = Extension.create({
221
227
  name: "markdownFigureImageSupport",
222
228
 
@@ -539,6 +545,100 @@ function createFootnoteReferenceMarkdownToken() {
539
545
  };
540
546
  }
541
547
 
548
+ const INLINE_FOOTNOTE_CONTENT_ATTR = "__jantInlineFootnoteContent";
549
+
550
+ function parseInlineFootnoteSource(
551
+ src: string,
552
+ ): { content: string; raw: string } | null {
553
+ if (!src.startsWith("^[")) return null;
554
+
555
+ let bracketDepth = 1;
556
+ let codeDelimiterLength = 0;
557
+
558
+ for (let index = 2; index < src.length; index += 1) {
559
+ const character = src[index];
560
+
561
+ if (character === "\n" || character === "\r") return null;
562
+
563
+ if (character === "\\" && codeDelimiterLength === 0) {
564
+ index += 1;
565
+ continue;
566
+ }
567
+
568
+ if (character === "`") {
569
+ let delimiterLength = 1;
570
+ while (src[index + delimiterLength] === "`") {
571
+ delimiterLength += 1;
572
+ }
573
+
574
+ if (codeDelimiterLength === 0) {
575
+ codeDelimiterLength = delimiterLength;
576
+ } else if (codeDelimiterLength === delimiterLength) {
577
+ codeDelimiterLength = 0;
578
+ }
579
+
580
+ index += delimiterLength - 1;
581
+ continue;
582
+ }
583
+
584
+ if (codeDelimiterLength > 0) continue;
585
+
586
+ if (character === "[") {
587
+ bracketDepth += 1;
588
+ continue;
589
+ }
590
+
591
+ if (character !== "]") continue;
592
+ bracketDepth -= 1;
593
+ if (bracketDepth !== 0) continue;
594
+
595
+ const content = src.slice(2, index);
596
+ if (!content.trim()) return null;
597
+
598
+ return {
599
+ content,
600
+ raw: src.slice(0, index + 1),
601
+ };
602
+ }
603
+
604
+ return null;
605
+ }
606
+
607
+ const MarkdownInlineFootnote = Extension.create({
608
+ name: "inlineFootnote",
609
+
610
+ parseMarkdown: (token, helpers) =>
611
+ helpers.createNode("footnoteReference", {
612
+ [INLINE_FOOTNOTE_CONTENT_ATTR]: Array.isArray(token.tokens)
613
+ ? helpers.parseInline(token.tokens)
614
+ : [],
615
+ }),
616
+
617
+ markdownTokenizer: {
618
+ name: "inlineFootnote",
619
+ level: "inline",
620
+ start(src: string) {
621
+ return src.indexOf("^[");
622
+ },
623
+ tokenize(src: string, _tokens: unknown[], helpers) {
624
+ const inlineFootnote = parseInlineFootnoteSource(src);
625
+ if (!inlineFootnote) return undefined;
626
+
627
+ return {
628
+ type: "inlineFootnote",
629
+ raw: inlineFootnote.raw,
630
+ tokens: helpers.inlineTokens(inlineFootnote.content),
631
+ };
632
+ },
633
+ },
634
+ });
635
+
636
+ function normalizeFootnoteDataLabel(label: unknown): string {
637
+ const normalized = normalizeFootnoteLabel(label);
638
+ const legacyDisplayLabel = normalized.match(/^\[\^([^\]]+)\]:?$/)?.[1];
639
+ return normalizeFootnoteLabel(legacyDisplayLabel ?? normalized);
640
+ }
641
+
542
642
  export const MarkdownFootnoteReference = Node.create({
543
643
  name: "footnoteReference",
544
644
  group: "inline",
@@ -560,7 +660,7 @@ export const MarkdownFootnoteReference = Node.create({
560
660
  getAttrs(dom) {
561
661
  const element = dom as QueryableElement;
562
662
  return {
563
- label: normalizeFootnoteLabel(
663
+ label: normalizeFootnoteDataLabel(
564
664
  element.getAttribute("data-footnote-label"),
565
665
  ),
566
666
  };
@@ -615,7 +715,7 @@ export const MarkdownFootnoteDefinition = Node.create({
615
715
  getAttrs(dom) {
616
716
  const element = dom as QueryableElement;
617
717
  return {
618
- label: normalizeFootnoteLabel(
718
+ label: normalizeFootnoteDataLabel(
619
719
  element.getAttribute("data-footnote-label"),
620
720
  ),
621
721
  };
@@ -631,7 +731,7 @@ export const MarkdownFootnoteDefinition = Node.create({
631
731
  "div",
632
732
  {
633
733
  "data-footnote-definition": "",
634
- "data-footnote-label": getFootnoteDefinitionLabelText(label),
734
+ "data-footnote-label": label,
635
735
  class: "tiptap-footnote-definition",
636
736
  },
637
737
  0,
@@ -721,9 +821,11 @@ export function createMarkdownContentExtensions(
721
821
  return [
722
822
  StarterKit.configure({
723
823
  heading: { levels: [1, 2, 3] },
724
- link: { openOnClick: false, autolink: false },
824
+ link: false,
725
825
  codeBlock: false,
826
+ trailingNode: { notAfter: ["footnoteDefinition"] },
726
827
  }),
828
+ SemanticLink.configure({ openOnClick: false, autolink: false }),
727
829
  MarkdownCodeBlock,
728
830
  Table.configure({
729
831
  resizable: false,
@@ -737,16 +839,104 @@ export function createMarkdownContentExtensions(
737
839
  options.moreBreakExtension ?? MarkdownMoreBreak,
738
840
  options.embedExtension ?? MarkdownEmbedNode,
739
841
  options.htmlBlockExtension ?? MarkdownHtmlBlockNode,
842
+ MarkdownInlineFootnote,
740
843
  MarkdownFootnoteReference,
741
844
  MarkdownFootnoteDefinition,
742
845
  ];
743
846
  }
744
847
 
745
- function normalizeMarkdownDoc(node: JSONContent): JSONContent {
848
+ function isJsonContent(value: unknown): value is JSONContent {
849
+ return (
850
+ typeof value === "object" &&
851
+ value !== null &&
852
+ typeof (value as { type?: unknown }).type === "string"
853
+ );
854
+ }
855
+
856
+ function collectUsedFootnoteLabels(
857
+ node: JSONContent,
858
+ usedLabels: Set<string>,
859
+ ): void {
860
+ if (node.type === "footnoteReference" || node.type === "footnoteDefinition") {
861
+ const label = normalizeFootnoteLabel(node.attrs?.label);
862
+ if (label) usedLabels.add(getFootnoteLabelKey(label));
863
+ }
864
+
865
+ for (const child of node.content ?? []) {
866
+ collectUsedFootnoteLabels(child, usedLabels);
867
+ }
868
+ }
869
+
870
+ function expandInlineFootnotes(doc: JSONContent): JSONContent {
871
+ if (doc.type !== "doc") return doc;
872
+
873
+ const usedLabels = new Set<string>();
874
+ collectUsedFootnoteLabels(doc, usedLabels);
875
+ const definitions: JSONContent[] = [];
876
+ let nextNumericLabel = 1;
877
+
878
+ const allocateLabel = (): string => {
879
+ while (usedLabels.has(getFootnoteLabelKey(String(nextNumericLabel)))) {
880
+ nextNumericLabel += 1;
881
+ }
882
+
883
+ const label = String(nextNumericLabel);
884
+ usedLabels.add(getFootnoteLabelKey(label));
885
+ nextNumericLabel += 1;
886
+ return label;
887
+ };
888
+
889
+ const expandNode = (node: JSONContent): JSONContent => {
890
+ const inlineContent = node.attrs?.[INLINE_FOOTNOTE_CONTENT_ATTR];
891
+
892
+ if (node.type === "footnoteReference" && Array.isArray(inlineContent)) {
893
+ const label = allocateLabel();
894
+ const attrs: Record<string, unknown> = { ...(node.attrs ?? {}), label };
895
+ delete attrs[INLINE_FOOTNOTE_CONTENT_ATTR];
896
+ const content = inlineContent
897
+ .filter(isJsonContent)
898
+ .map((child) => expandNode(normalizeMarkdownDocument(child)));
899
+
900
+ definitions.push({
901
+ type: "footnoteDefinition",
902
+ attrs: { label },
903
+ content: [
904
+ content.length > 0
905
+ ? { type: "paragraph", content }
906
+ : { type: "paragraph" },
907
+ ],
908
+ });
909
+
910
+ return { ...node, attrs };
911
+ }
912
+
913
+ return node.content
914
+ ? { ...node, content: node.content.map(expandNode) }
915
+ : node;
916
+ };
917
+
918
+ const expanded = expandNode(doc);
919
+ if (definitions.length === 0) return expanded;
920
+
921
+ return {
922
+ ...expanded,
923
+ content: [...(expanded.content ?? []), ...definitions],
924
+ };
925
+ }
926
+
927
+ /**
928
+ * Normalizes Markdown parser output before it enters the editor schema.
929
+ *
930
+ * @param node - Parsed Tiptap document or descendant node
931
+ * @returns A normalized copy safe to load into an editor
932
+ * @example
933
+ * const normalized = normalizeMarkdownDocument(markdownManager.parse(source));
934
+ */
935
+ export function normalizeMarkdownDocument(node: JSONContent): JSONContent {
746
936
  const normalized: JSONContent = { ...node };
747
937
 
748
938
  if (normalized.content) {
749
- normalized.content = normalized.content.map(normalizeMarkdownDoc);
939
+ normalized.content = normalized.content.map(normalizeMarkdownDocument);
750
940
  }
751
941
 
752
942
  if (normalized.marks) {
@@ -810,9 +1000,9 @@ function normalizeMarkdownDoc(node: JSONContent): JSONContent {
810
1000
  child?.type === "text" &&
811
1001
  typeof child.text === "string" &&
812
1002
  nextChild?.type === "footnoteReference" &&
813
- child.text.endsWith("\n")
1003
+ /\n[ \t]*$/.test(child.text)
814
1004
  ) {
815
- const trimmedText = child.text.replace(/\n$/, "");
1005
+ const trimmedText = child.text.replace(/\n[ \t]*$/, "");
816
1006
  if (trimmedText) {
817
1007
  nextContent.push({
818
1008
  ...child,
@@ -830,7 +1020,9 @@ function normalizeMarkdownDoc(node: JSONContent): JSONContent {
830
1020
  normalized.content = nextContent;
831
1021
  }
832
1022
 
833
- return normalized;
1023
+ return normalized.type === "doc"
1024
+ ? expandInlineFootnotes(normalized)
1025
+ : normalized;
834
1026
  }
835
1027
 
836
1028
  function expandCodeBlockFences(markdown: string): string {
@@ -854,7 +1046,7 @@ export function getMarkdownManager(): MarkdownManager {
854
1046
  }
855
1047
 
856
1048
  export function parseMarkdownDocument(markdown: string): JSONContent {
857
- return normalizeMarkdownDoc(getMarkdownManager().parse(markdown));
1049
+ return normalizeMarkdownDocument(getMarkdownManager().parse(markdown));
858
1050
  }
859
1051
 
860
1052
  export function serializeMarkdownDocument(doc: JSONContent): string {
@@ -8,12 +8,16 @@
8
8
 
9
9
  import { parseMarkdownDocument } from "./markdown-manager.js";
10
10
  import { extractBodyText } from "./summary.js";
11
- import { renderTiptapDocument } from "./tiptap-render.js";
11
+ import {
12
+ renderTiptapDocument,
13
+ type TiptapRenderOptions,
14
+ } from "./tiptap-render.js";
12
15
 
13
16
  /**
14
17
  * Renders Markdown content to HTML using Jant's shared Markdown pipeline.
15
18
  *
16
19
  * @param markdown - The Markdown string to convert to HTML
20
+ * @param options - Rendering options, including a stable footnote namespace
17
21
  * @returns The rendered HTML string
18
22
  *
19
23
  * @example
@@ -22,9 +26,12 @@ import { renderTiptapDocument } from "./tiptap-render.js";
22
26
  * // "<h1>Hello</h1><p>This is <strong>bold</strong> text.</p>"
23
27
  * ```
24
28
  */
25
- export function render(markdown: string): string {
29
+ export function render(
30
+ markdown: string,
31
+ options: TiptapRenderOptions = {},
32
+ ): string {
26
33
  if (!markdown.trim()) return "";
27
- return renderTiptapDocument(parseMarkdownDocument(markdown));
34
+ return renderTiptapDocument(parseMarkdownDocument(markdown), options);
28
35
  }
29
36
 
30
37
  /**
@@ -53,10 +53,17 @@ export async function getNavigationData(
53
53
  ): Promise<NavigationData> {
54
54
  // Callers that already fetched nav items can pass them in to avoid a
55
55
  // redundant DB round-trip.
56
- const items =
56
+ const savedItems =
57
57
  options?.preloadedItems ?? (await c.var.services.navItems.list());
58
58
  const currentPath = c.var.publicPath;
59
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.
62
+ const items = appConfig.rssFeedsEnabled
63
+ ? savedItems
64
+ : savedItems.filter(
65
+ (item: NavItem) => item.type !== "system" || item.systemKey !== "rss",
66
+ );
60
67
 
61
68
  const siteName = appConfig.siteName;
62
69
  const siteFooter = appConfig.siteFooter;
@@ -68,7 +75,9 @@ export async function getNavigationData(
68
75
  // Plain text for meta tags / RSS; HTML for homepage display
69
76
  const siteDescription = rawDescription ? toPlainText(rawDescription) : "";
70
77
  const siteDescriptionHtml = rawDescription
71
- ? renderMarkdown(rawDescription)
78
+ ? renderMarkdown(rawDescription, {
79
+ namespace: `${c.var.currentSite.id}-description`,
80
+ })
72
81
  : undefined;
73
82
 
74
83
  // Avatar URL and display flag come from appConfig
@@ -76,7 +85,11 @@ export async function getNavigationData(
76
85
  const showHeaderAvatar = appConfig.showHeaderAvatar;
77
86
 
78
87
  // Render footer markdown
79
- const siteFooterHtml = siteFooter ? renderMarkdown(siteFooter) : undefined;
88
+ const siteFooterHtml = siteFooter
89
+ ? renderMarkdown(siteFooter, {
90
+ namespace: `${c.var.currentSite.id}-footer`,
91
+ })
92
+ : undefined;
80
93
 
81
94
  // Auth state is populated once per request by `attachSession` middleware.
82
95
  const isAuthenticated = c.var.isAuthenticated;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Stored post body HTML projection helpers.
3
+ *
4
+ * TipTap JSON in `post.body` is canonical. `post.body_html` is a materialized
5
+ * projection that may lag behind the current renderer during upgrades or after
6
+ * importing an older snapshot.
7
+ */
8
+
9
+ import type { JSONContent } from "@tiptap/core";
10
+ import { upgradeLegacyFootnotes } from "./footnotes.js";
11
+ import {
12
+ renderTiptapDocument,
13
+ type TiptapRenderResult,
14
+ } from "./tiptap-render.js";
15
+
16
+ /** Current persisted post body HTML contract. V1 is the legacy inline trio. */
17
+ export const POST_BODY_HTML_VERSION = 4;
18
+
19
+ export type PreparedPostBodyHtmlResult =
20
+ | {
21
+ ok: true;
22
+ body: string;
23
+ html: string;
24
+ upgradedLegacyFootnotes: boolean;
25
+ }
26
+ | { ok: false; error: string };
27
+
28
+ export interface StoredPostBodyHtml {
29
+ id: string;
30
+ body: string | null;
31
+ bodyHtml: string | null;
32
+ bodyHtmlVersion: number | null;
33
+ }
34
+
35
+ /**
36
+ * Render canonical post JSON with a stable page-wide footnote namespace.
37
+ *
38
+ * @param postId - Immutable post TypeID
39
+ * @param body - Canonical TipTap JSON
40
+ * @returns Current public HTML, or an empty string for invalid JSON
41
+ * @example
42
+ * ```ts
43
+ * renderPostBodyHtml("pst_example", '{"type":"doc","content":[]}');
44
+ * // ""
45
+ * ```
46
+ */
47
+ export function renderPostBodyHtml(postId: string, body: string): string {
48
+ const result = tryPreparePostBodyHtml(postId, body);
49
+ return result.ok ? result.html : "";
50
+ }
51
+
52
+ /**
53
+ * Normalize recognized historical footnotes and render the current HTML
54
+ * projection in one parse pass.
55
+ *
56
+ * @param postId - Immutable post TypeID
57
+ * @param body - Canonical TipTap JSON
58
+ * @returns Prepared canonical JSON and HTML, or a parse failure
59
+ * @example
60
+ * ```ts
61
+ * tryPreparePostBodyHtml("pst_example", '{"type":"doc","content":[]}');
62
+ * // { ok: true, body: "...", html: "", upgradedLegacyFootnotes: false }
63
+ * ```
64
+ */
65
+ export function tryPreparePostBodyHtml(
66
+ postId: string,
67
+ body: string,
68
+ ): PreparedPostBodyHtmlResult {
69
+ try {
70
+ const parsed = JSON.parse(body) as JSONContent;
71
+ if (parsed.type !== "doc") {
72
+ return { ok: false, error: "TipTap body root must be a doc node." };
73
+ }
74
+
75
+ const upgraded = upgradeLegacyFootnotes(parsed);
76
+ return {
77
+ ok: true,
78
+ body: upgraded.upgraded ? JSON.stringify(upgraded.doc) : body,
79
+ html: renderTiptapDocument(upgraded.doc, { namespace: postId }),
80
+ upgradedLegacyFootnotes: upgraded.upgraded,
81
+ };
82
+ } catch (error) {
83
+ return {
84
+ ok: false,
85
+ error: error instanceof Error ? error.message : "Invalid TipTap JSON.",
86
+ };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Strict variant used by projection rebuilds.
92
+ *
93
+ * @param postId - Immutable post TypeID
94
+ * @param body - Canonical TipTap JSON
95
+ * @returns A result that distinguishes malformed JSON from a valid empty body
96
+ */
97
+ export function tryRenderPostBodyHtml(
98
+ postId: string,
99
+ body: string,
100
+ ): TiptapRenderResult {
101
+ const result = tryPreparePostBodyHtml(postId, body);
102
+ return result.ok
103
+ ? { ok: true, html: result.html }
104
+ : { ok: false, error: result.error };
105
+ }
106
+
107
+ /**
108
+ * Resolve a stored projection to the current public HTML contract.
109
+ *
110
+ * Stale rows are rendered in memory so a partially completed backfill never
111
+ * leaks mixed HTML-contract markup. Invalid historical canonical JSON falls back to the
112
+ * stored HTML for availability; the strict rebuild reports those rows instead
113
+ * of marking them current.
114
+ *
115
+ * @param input - Stored post body and projection fields
116
+ * @returns Current rendered HTML, the legacy fallback, or null for no body
117
+ */
118
+ export function resolvePostBodyHtml(input: StoredPostBodyHtml): string | null {
119
+ if (input.body === null) return null;
120
+
121
+ if (
122
+ input.bodyHtmlVersion !== null &&
123
+ input.bodyHtmlVersion >= POST_BODY_HTML_VERSION &&
124
+ input.bodyHtml !== null
125
+ ) {
126
+ return input.bodyHtml;
127
+ }
128
+
129
+ const result = tryRenderPostBodyHtml(input.id, input.body);
130
+ return result.ok ? result.html : input.bodyHtml;
131
+ }
@@ -82,6 +82,8 @@ function resolvePostSocialImage(
82
82
  interface PostDisplayOptions {
83
83
  isAuthenticated?: boolean;
84
84
  allowDraft?: boolean;
85
+ /** Include draft members when assembling a thread for an authenticated preview. */
86
+ includeDraftThread?: boolean;
85
87
  }
86
88
 
87
89
  function canViewPost(post: Post, options: PostDisplayOptions = {}): boolean {
@@ -170,7 +172,11 @@ export async function assemblePostPageDisplay(
170
172
  const mediaCtx = createMediaContext(c.var.appConfig);
171
173
  const threadPosts = (
172
174
  await c.var.services.posts.getThread(post.threadId)
173
- ).filter((threadPost) => threadPost.status === "published");
175
+ ).filter(
176
+ (threadPost) =>
177
+ threadPost.status === "published" ||
178
+ (options?.includeDraftThread && threadPost.status === "draft"),
179
+ );
174
180
 
175
181
  const allPostIds =
176
182
  threadPosts.length > 1 ? threadPosts.map((p) => p.id) : [post.id];
@@ -49,12 +49,16 @@ export interface RenderPublicPageOptions {
49
49
  navData: NavigationData;
50
50
  /** Page content JSX to render inside SiteLayout */
51
51
  content: Child;
52
+ /** Optional status chrome rendered outside the public site layout. */
53
+ pageChrome?: Child;
52
54
  /** Optional sidebar content for sidebar layout */
53
55
  sidebar?: Child;
54
56
  /** Optional toast notification */
55
57
  toast?: ToastProps;
56
58
  /** Whether to render the shared compose dialog shell */
57
59
  showComposeDialog?: boolean;
60
+ /** Override the site-wide crawler setting for sensitive utility pages. */
61
+ noindex?: boolean;
58
62
  /** Whether to render the site header */
59
63
  showHeader?: boolean;
60
64
  /** Whether to render the home branding credit after the site footer */
@@ -97,9 +101,11 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
97
101
  canonicalHref,
98
102
  navData,
99
103
  content,
104
+ pageChrome,
100
105
  sidebar,
101
106
  toast,
102
107
  showComposeDialog,
108
+ noindex,
103
109
  showHeader,
104
110
  showHomeBranding,
105
111
  composeCollectionId,
@@ -138,7 +144,7 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
138
144
  };
139
145
  const faviconUrl = appConfig.siteAvatarUrl || undefined;
140
146
  const faviconVersion = appConfig.faviconVersion || undefined;
141
- const noindex = appConfig.noindex;
147
+ const resolvedNoindex = noindex ?? appConfig.noindex;
142
148
 
143
149
  return c.html(
144
150
  <BaseLayout
@@ -158,10 +164,11 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
158
164
  canonicalHref={canonicalHref}
159
165
  faviconUrl={faviconUrl}
160
166
  faviconVersion={faviconVersion}
161
- noindex={noindex}
167
+ noindex={resolvedNoindex}
162
168
  isAuthenticated={navData.isAuthenticated}
163
169
  toast={toast}
164
170
  >
171
+ {pageChrome}
165
172
  <SiteLayout {...layoutProps}>{content}</SiteLayout>
166
173
  </BaseLayout>,
167
174
  );