@jant/core 0.6.14 → 0.6.16
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 +88 -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-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- 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__/mise-config.test.ts +22 -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 +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -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 +82 -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__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- 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 +177 -95
- 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 +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -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/post-refresh.ts +135 -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__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- 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 +88 -10
- 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/link-input-rules.ts +4 -4
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- 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 +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -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-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -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/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -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/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -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 +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- 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__/feed.test.ts +168 -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 +32 -4
- package/src/lib/__tests__/markdown.test.ts +24 -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__/slug.test.ts +18 -0
- 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__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -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/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -5
- 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 +47 -8
- 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 +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -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__/palette.test.ts +44 -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 +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -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 +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -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__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -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 +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -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 +224 -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 +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -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 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- 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 +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- 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 +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/ArchivePage.tsx +131 -24
- 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 +70 -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 +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -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,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
parseFootnoteDefinition,
|
|
24
26
|
} from "./footnotes.js";
|
|
25
27
|
import { renderMarkdownImage, type RichImageAttrs } from "./rich-image.js";
|
|
28
|
+
import { sanitizeRichTextHref } from "./url.js";
|
|
26
29
|
|
|
27
30
|
export const MARKDOWN_MARKED_OPTIONS = {
|
|
28
31
|
gfm: true,
|
|
@@ -217,6 +220,10 @@ const MarkdownCodeBlock = CodeBlock.extend({
|
|
|
217
220
|
},
|
|
218
221
|
});
|
|
219
222
|
|
|
223
|
+
const SemanticLink = Link.extend({
|
|
224
|
+
clearable: false,
|
|
225
|
+
});
|
|
226
|
+
|
|
220
227
|
const MarkdownFigureImageSupport = Extension.create({
|
|
221
228
|
name: "markdownFigureImageSupport",
|
|
222
229
|
|
|
@@ -539,6 +546,100 @@ function createFootnoteReferenceMarkdownToken() {
|
|
|
539
546
|
};
|
|
540
547
|
}
|
|
541
548
|
|
|
549
|
+
const INLINE_FOOTNOTE_CONTENT_ATTR = "__jantInlineFootnoteContent";
|
|
550
|
+
|
|
551
|
+
function parseInlineFootnoteSource(
|
|
552
|
+
src: string,
|
|
553
|
+
): { content: string; raw: string } | null {
|
|
554
|
+
if (!src.startsWith("^[")) return null;
|
|
555
|
+
|
|
556
|
+
let bracketDepth = 1;
|
|
557
|
+
let codeDelimiterLength = 0;
|
|
558
|
+
|
|
559
|
+
for (let index = 2; index < src.length; index += 1) {
|
|
560
|
+
const character = src[index];
|
|
561
|
+
|
|
562
|
+
if (character === "\n" || character === "\r") return null;
|
|
563
|
+
|
|
564
|
+
if (character === "\\" && codeDelimiterLength === 0) {
|
|
565
|
+
index += 1;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (character === "`") {
|
|
570
|
+
let delimiterLength = 1;
|
|
571
|
+
while (src[index + delimiterLength] === "`") {
|
|
572
|
+
delimiterLength += 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (codeDelimiterLength === 0) {
|
|
576
|
+
codeDelimiterLength = delimiterLength;
|
|
577
|
+
} else if (codeDelimiterLength === delimiterLength) {
|
|
578
|
+
codeDelimiterLength = 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
index += delimiterLength - 1;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (codeDelimiterLength > 0) continue;
|
|
586
|
+
|
|
587
|
+
if (character === "[") {
|
|
588
|
+
bracketDepth += 1;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (character !== "]") continue;
|
|
593
|
+
bracketDepth -= 1;
|
|
594
|
+
if (bracketDepth !== 0) continue;
|
|
595
|
+
|
|
596
|
+
const content = src.slice(2, index);
|
|
597
|
+
if (!content.trim()) return null;
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
content,
|
|
601
|
+
raw: src.slice(0, index + 1),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const MarkdownInlineFootnote = Extension.create({
|
|
609
|
+
name: "inlineFootnote",
|
|
610
|
+
|
|
611
|
+
parseMarkdown: (token, helpers) =>
|
|
612
|
+
helpers.createNode("footnoteReference", {
|
|
613
|
+
[INLINE_FOOTNOTE_CONTENT_ATTR]: Array.isArray(token.tokens)
|
|
614
|
+
? helpers.parseInline(token.tokens)
|
|
615
|
+
: [],
|
|
616
|
+
}),
|
|
617
|
+
|
|
618
|
+
markdownTokenizer: {
|
|
619
|
+
name: "inlineFootnote",
|
|
620
|
+
level: "inline",
|
|
621
|
+
start(src: string) {
|
|
622
|
+
return src.indexOf("^[");
|
|
623
|
+
},
|
|
624
|
+
tokenize(src: string, _tokens: unknown[], helpers) {
|
|
625
|
+
const inlineFootnote = parseInlineFootnoteSource(src);
|
|
626
|
+
if (!inlineFootnote) return undefined;
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
type: "inlineFootnote",
|
|
630
|
+
raw: inlineFootnote.raw,
|
|
631
|
+
tokens: helpers.inlineTokens(inlineFootnote.content),
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
function normalizeFootnoteDataLabel(label: unknown): string {
|
|
638
|
+
const normalized = normalizeFootnoteLabel(label);
|
|
639
|
+
const legacyDisplayLabel = normalized.match(/^\[\^([^\]]+)\]:?$/)?.[1];
|
|
640
|
+
return normalizeFootnoteLabel(legacyDisplayLabel ?? normalized);
|
|
641
|
+
}
|
|
642
|
+
|
|
542
643
|
export const MarkdownFootnoteReference = Node.create({
|
|
543
644
|
name: "footnoteReference",
|
|
544
645
|
group: "inline",
|
|
@@ -560,7 +661,7 @@ export const MarkdownFootnoteReference = Node.create({
|
|
|
560
661
|
getAttrs(dom) {
|
|
561
662
|
const element = dom as QueryableElement;
|
|
562
663
|
return {
|
|
563
|
-
label:
|
|
664
|
+
label: normalizeFootnoteDataLabel(
|
|
564
665
|
element.getAttribute("data-footnote-label"),
|
|
565
666
|
),
|
|
566
667
|
};
|
|
@@ -615,7 +716,7 @@ export const MarkdownFootnoteDefinition = Node.create({
|
|
|
615
716
|
getAttrs(dom) {
|
|
616
717
|
const element = dom as QueryableElement;
|
|
617
718
|
return {
|
|
618
|
-
label:
|
|
719
|
+
label: normalizeFootnoteDataLabel(
|
|
619
720
|
element.getAttribute("data-footnote-label"),
|
|
620
721
|
),
|
|
621
722
|
};
|
|
@@ -631,7 +732,7 @@ export const MarkdownFootnoteDefinition = Node.create({
|
|
|
631
732
|
"div",
|
|
632
733
|
{
|
|
633
734
|
"data-footnote-definition": "",
|
|
634
|
-
"data-footnote-label":
|
|
735
|
+
"data-footnote-label": label,
|
|
635
736
|
class: "tiptap-footnote-definition",
|
|
636
737
|
},
|
|
637
738
|
0,
|
|
@@ -721,10 +822,15 @@ export function createMarkdownContentExtensions(
|
|
|
721
822
|
return [
|
|
722
823
|
StarterKit.configure({
|
|
723
824
|
heading: { levels: [1, 2, 3] },
|
|
724
|
-
link:
|
|
825
|
+
link: false,
|
|
725
826
|
codeBlock: false,
|
|
726
827
|
trailingNode: { notAfter: ["footnoteDefinition"] },
|
|
727
828
|
}),
|
|
829
|
+
SemanticLink.configure({
|
|
830
|
+
openOnClick: false,
|
|
831
|
+
autolink: false,
|
|
832
|
+
isAllowedUri: (url) => sanitizeRichTextHref(url) !== "",
|
|
833
|
+
}),
|
|
728
834
|
MarkdownCodeBlock,
|
|
729
835
|
Table.configure({
|
|
730
836
|
resizable: false,
|
|
@@ -738,11 +844,91 @@ export function createMarkdownContentExtensions(
|
|
|
738
844
|
options.moreBreakExtension ?? MarkdownMoreBreak,
|
|
739
845
|
options.embedExtension ?? MarkdownEmbedNode,
|
|
740
846
|
options.htmlBlockExtension ?? MarkdownHtmlBlockNode,
|
|
847
|
+
MarkdownInlineFootnote,
|
|
741
848
|
MarkdownFootnoteReference,
|
|
742
849
|
MarkdownFootnoteDefinition,
|
|
743
850
|
];
|
|
744
851
|
}
|
|
745
852
|
|
|
853
|
+
function isJsonContent(value: unknown): value is JSONContent {
|
|
854
|
+
return (
|
|
855
|
+
typeof value === "object" &&
|
|
856
|
+
value !== null &&
|
|
857
|
+
typeof (value as { type?: unknown }).type === "string"
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function collectUsedFootnoteLabels(
|
|
862
|
+
node: JSONContent,
|
|
863
|
+
usedLabels: Set<string>,
|
|
864
|
+
): void {
|
|
865
|
+
if (node.type === "footnoteReference" || node.type === "footnoteDefinition") {
|
|
866
|
+
const label = normalizeFootnoteLabel(node.attrs?.label);
|
|
867
|
+
if (label) usedLabels.add(getFootnoteLabelKey(label));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
for (const child of node.content ?? []) {
|
|
871
|
+
collectUsedFootnoteLabels(child, usedLabels);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function expandInlineFootnotes(doc: JSONContent): JSONContent {
|
|
876
|
+
if (doc.type !== "doc") return doc;
|
|
877
|
+
|
|
878
|
+
const usedLabels = new Set<string>();
|
|
879
|
+
collectUsedFootnoteLabels(doc, usedLabels);
|
|
880
|
+
const definitions: JSONContent[] = [];
|
|
881
|
+
let nextNumericLabel = 1;
|
|
882
|
+
|
|
883
|
+
const allocateLabel = (): string => {
|
|
884
|
+
while (usedLabels.has(getFootnoteLabelKey(String(nextNumericLabel)))) {
|
|
885
|
+
nextNumericLabel += 1;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const label = String(nextNumericLabel);
|
|
889
|
+
usedLabels.add(getFootnoteLabelKey(label));
|
|
890
|
+
nextNumericLabel += 1;
|
|
891
|
+
return label;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const expandNode = (node: JSONContent): JSONContent => {
|
|
895
|
+
const inlineContent = node.attrs?.[INLINE_FOOTNOTE_CONTENT_ATTR];
|
|
896
|
+
|
|
897
|
+
if (node.type === "footnoteReference" && Array.isArray(inlineContent)) {
|
|
898
|
+
const label = allocateLabel();
|
|
899
|
+
const attrs: Record<string, unknown> = { ...(node.attrs ?? {}), label };
|
|
900
|
+
delete attrs[INLINE_FOOTNOTE_CONTENT_ATTR];
|
|
901
|
+
const content = inlineContent
|
|
902
|
+
.filter(isJsonContent)
|
|
903
|
+
.map((child) => expandNode(normalizeMarkdownDocument(child)));
|
|
904
|
+
|
|
905
|
+
definitions.push({
|
|
906
|
+
type: "footnoteDefinition",
|
|
907
|
+
attrs: { label },
|
|
908
|
+
content: [
|
|
909
|
+
content.length > 0
|
|
910
|
+
? { type: "paragraph", content }
|
|
911
|
+
: { type: "paragraph" },
|
|
912
|
+
],
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
return { ...node, attrs };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return node.content
|
|
919
|
+
? { ...node, content: node.content.map(expandNode) }
|
|
920
|
+
: node;
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const expanded = expandNode(doc);
|
|
924
|
+
if (definitions.length === 0) return expanded;
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
...expanded,
|
|
928
|
+
content: [...(expanded.content ?? []), ...definitions],
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
746
932
|
/**
|
|
747
933
|
* Normalizes Markdown parser output before it enters the editor schema.
|
|
748
934
|
*
|
|
@@ -839,7 +1025,9 @@ export function normalizeMarkdownDocument(node: JSONContent): JSONContent {
|
|
|
839
1025
|
normalized.content = nextContent;
|
|
840
1026
|
}
|
|
841
1027
|
|
|
842
|
-
return normalized
|
|
1028
|
+
return normalized.type === "doc"
|
|
1029
|
+
? expandInlineFootnotes(normalized)
|
|
1030
|
+
: normalized;
|
|
843
1031
|
}
|
|
844
1032
|
|
|
845
1033
|
function expandCodeBlockFences(markdown: string): 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
|
@@ -9,7 +9,7 @@ import type { Context } from "hono";
|
|
|
9
9
|
import type { Bindings, Post, PostView } from "../types.js";
|
|
10
10
|
import type { AppVariables } from "../types/app-context.js";
|
|
11
11
|
import { buildMediaMap } from "./media-helpers.js";
|
|
12
|
-
import { createMediaContext, toPostView } from "./view.js";
|
|
12
|
+
import { createMediaContext, resolveDraftTailId, toPostView } from "./view.js";
|
|
13
13
|
|
|
14
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
15
|
|
|
@@ -82,6 +82,13 @@ function resolvePostSocialImage(
|
|
|
82
82
|
interface PostDisplayOptions {
|
|
83
83
|
isAuthenticated?: boolean;
|
|
84
84
|
allowDraft?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Force draft members into the assembled thread. The author already sees
|
|
87
|
+
* them whenever they are signed in; this only covers the draft-preview
|
|
88
|
+
* route, where the request is authenticated but `isAuthenticated` has
|
|
89
|
+
* already been forced to `true` for private-post access.
|
|
90
|
+
*/
|
|
91
|
+
includeDraftThread?: boolean;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
function canViewPost(post: Post, options: PostDisplayOptions = {}): boolean {
|
|
@@ -118,11 +125,16 @@ export async function assemblePostCardView(
|
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
121
|
-
const [rawMediaMap, collectionsMap, lastPostMap, aliasesMap] =
|
|
128
|
+
const [rawMediaMap, collectionsMap, lastPostMap, draftTailMap, aliasesMap] =
|
|
122
129
|
await Promise.all([
|
|
123
130
|
c.var.services.media.getByPostIds([post.id]),
|
|
124
131
|
c.var.services.collections.getCollectionsByPostIds([post.id]),
|
|
125
|
-
c.var.services.posts.
|
|
132
|
+
c.var.services.posts.getThreadTailIds([post.threadId]),
|
|
133
|
+
c.var.isAuthenticated
|
|
134
|
+
? c.var.services.posts.getThreadTailIds([post.threadId], {
|
|
135
|
+
includeDrafts: true,
|
|
136
|
+
})
|
|
137
|
+
: Promise.resolve(new Map<string, string>()),
|
|
126
138
|
c.var.services.paths.getPostAliases([post.id]),
|
|
127
139
|
]);
|
|
128
140
|
|
|
@@ -142,6 +154,13 @@ export async function assemblePostCardView(
|
|
|
142
154
|
aliasesMap.get(post.id)?.[0],
|
|
143
155
|
);
|
|
144
156
|
|
|
157
|
+
const draftTailId = resolveDraftTailId(
|
|
158
|
+
post.threadId,
|
|
159
|
+
lastPostMap,
|
|
160
|
+
draftTailMap,
|
|
161
|
+
);
|
|
162
|
+
if (draftTailId) view.draftTailId = draftTailId;
|
|
163
|
+
|
|
145
164
|
return view;
|
|
146
165
|
}
|
|
147
166
|
|
|
@@ -168,9 +187,20 @@ export async function assemblePostPageDisplay(
|
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
190
|
+
// Drafts render for the author so the thread they see ends where the server
|
|
191
|
+
// thinks it ends — otherwise a trailing draft silently blocks replies while
|
|
192
|
+
// staying invisible. Read the real auth state rather than the option, which
|
|
193
|
+
// callers force to `true` to bypass the private-post check.
|
|
194
|
+
const includeDrafts = Boolean(
|
|
195
|
+
options?.includeDraftThread || c.var.isAuthenticated,
|
|
196
|
+
);
|
|
171
197
|
const threadPosts = (
|
|
172
198
|
await c.var.services.posts.getThread(post.threadId)
|
|
173
|
-
).filter(
|
|
199
|
+
).filter(
|
|
200
|
+
(threadPost) =>
|
|
201
|
+
threadPost.status === "published" ||
|
|
202
|
+
(includeDrafts && threadPost.status === "draft"),
|
|
203
|
+
);
|
|
174
204
|
|
|
175
205
|
const allPostIds =
|
|
176
206
|
threadPosts.length > 1 ? threadPosts.map((p) => p.id) : [post.id];
|
|
@@ -213,13 +243,22 @@ export async function assemblePostPageDisplay(
|
|
|
213
243
|
)
|
|
214
244
|
: undefined;
|
|
215
245
|
|
|
216
|
-
|
|
246
|
+
// Page metadata describes what a reader gets, so it never draws on drafts:
|
|
247
|
+
// an unpublished reply must not supply the og:image or bump the modified
|
|
248
|
+
// time. Falls back to the post itself when nothing in the thread is
|
|
249
|
+
// published (the draft-preview route, which is noindex anyway).
|
|
250
|
+
const publishedViews = threadPostViews?.filter(
|
|
251
|
+
(view) => view.status === "published",
|
|
252
|
+
);
|
|
253
|
+
const metaViews = publishedViews?.length ? publishedViews : [postView];
|
|
254
|
+
|
|
255
|
+
const socialImage = resolvePostSocialImage(postView, metaViews);
|
|
217
256
|
|
|
218
|
-
|
|
219
|
-
const
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- metaViews is never empty
|
|
258
|
+
const rootView = metaViews[0]!;
|
|
220
259
|
// ISO 8601 strings from toISOString() are all UTC and zero-padded, so
|
|
221
260
|
// lexical comparison matches chronological order.
|
|
222
|
-
const articleModifiedTime =
|
|
261
|
+
const articleModifiedTime = metaViews
|
|
223
262
|
.map((p) => p.updatedAt)
|
|
224
263
|
.reduce((latest, t) => (t > latest ? t : latest));
|
|
225
264
|
|
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
|
);
|