@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.
- package/bin/commands/import-site.js +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-HnzmsaqU.js → app-C-oi_t8W.js} +7419 -6350
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-3_OaxhTs.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-Dfbq9aot.js → export-CsFx6F_M.js} +1273 -131
- package/dist/{github-sync--PSoE-Ne.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-BGTLx8Mf.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +373 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +102 -71
- package/src/client/components/jant-compose-dialog.ts +86 -22
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +872 -2
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +91 -13
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/footnotes.ts +156 -0
- package/src/client/tiptap/markdown-clipboard.ts +614 -12
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +36 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +202 -10
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +952 -64
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +186 -6
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +23 -9
- package/src/ui/layouts/SiteLayout.tsx +19 -13
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-CWJFPjuR.js +0 -6
- package/dist/client/_assets/client-DCrcHVJl.css +0 -2
- package/dist/client/_assets/client-auth-TsYq90xm.js +0 -4733
- 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,
|
package/src/lib/hugo-markdown.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Per-collection membership metadata.
|
|
20
|
-
*
|
|
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
|
-
//
|
|
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
|
package/src/lib/jant-branding.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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":
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
1049
|
+
return normalizeMarkdownDocument(getMarkdownManager().parse(markdown));
|
|
858
1050
|
}
|
|
859
1051
|
|
|
860
1052
|
export function serializeMarkdownDocument(doc: JSONContent): string {
|
package/src/lib/markdown.ts
CHANGED
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
import { parseMarkdownDocument } from "./markdown-manager.js";
|
|
10
10
|
import { extractBodyText } from "./summary.js";
|
|
11
|
-
import {
|
|
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(
|
|
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
|
/**
|
package/src/lib/navigation.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|
package/src/lib/post-display.ts
CHANGED
|
@@ -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(
|
|
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];
|
package/src/lib/render.tsx
CHANGED
|
@@ -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
|
|
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={
|
|
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
|
);
|