@jant/core 0.6.14 → 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-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.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-compose-dialog.test.ts +370 -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 +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- 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__/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/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 +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 +32 -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 +188 -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 +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 +829 -29
- 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 +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/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-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
package/src/lib/tiptap-render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { JSONContent } from "@tiptap/core";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
getFootnoteLabelKey,
|
|
11
11
|
normalizeFootnoteArtifacts,
|
|
12
12
|
normalizeFootnoteLabel,
|
|
13
13
|
} from "./footnotes.js";
|
|
@@ -30,6 +30,7 @@ interface TiptapNode {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
interface RenderContext {
|
|
33
|
+
footnotes: FootnotePlan;
|
|
33
34
|
renderChildren(content?: TiptapNode[]): string;
|
|
34
35
|
renderNode(node: TiptapNode): string;
|
|
35
36
|
renderText(text: string, marks?: TiptapMark[]): string;
|
|
@@ -38,6 +39,40 @@ interface RenderContext {
|
|
|
38
39
|
type MarkRenderer = (html: string, mark: TiptapMark) => string;
|
|
39
40
|
type NodeRenderer = (node: TiptapNode, context: RenderContext) => string;
|
|
40
41
|
|
|
42
|
+
export interface TiptapRenderOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Stable namespace for fragment IDs when multiple rendered documents can
|
|
45
|
+
* share a page. Persisted post HTML passes the immutable post TypeID.
|
|
46
|
+
*/
|
|
47
|
+
namespace?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type TiptapRenderResult =
|
|
51
|
+
| { ok: true; html: string }
|
|
52
|
+
| { ok: false; error: string };
|
|
53
|
+
|
|
54
|
+
interface FootnoteReferencePlan {
|
|
55
|
+
definitionId: string;
|
|
56
|
+
referenceId: string;
|
|
57
|
+
number: number;
|
|
58
|
+
occurrence: number;
|
|
59
|
+
label: string;
|
|
60
|
+
hasDefinition: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface FootnoteGroup {
|
|
64
|
+
definitionId: string;
|
|
65
|
+
number: number;
|
|
66
|
+
label: string;
|
|
67
|
+
definition: TiptapNode | null;
|
|
68
|
+
references: FootnoteReferencePlan[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface FootnotePlan {
|
|
72
|
+
groups: FootnoteGroup[];
|
|
73
|
+
referencesByNode: WeakMap<TiptapNode, FootnoteReferencePlan>;
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
function getStringAttr(
|
|
42
77
|
attrs: Record<string, unknown> | undefined,
|
|
43
78
|
name: string,
|
|
@@ -80,47 +115,206 @@ function renderTableCell(
|
|
|
80
115
|
return `<${tagName}${colspanAttr}${rowspanAttr}>${context.renderChildren(node.content)}</${tagName}>`;
|
|
81
116
|
}
|
|
82
117
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
|
|
118
|
+
const FOOTNOTE_NAMESPACE_HASH_OFFSET = 0xcbf29ce484222325n;
|
|
119
|
+
const FOOTNOTE_NAMESPACE_HASH_PRIME = 0x100000001b3n;
|
|
120
|
+
const FOOTNOTE_NAMESPACE_HASH_MASK = 0xffffffffffffffffn;
|
|
121
|
+
|
|
122
|
+
function createFootnoteIdScope(namespace: string | undefined): string {
|
|
123
|
+
const normalized = (namespace ?? "")
|
|
124
|
+
.trim()
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
127
|
+
.replace(/^-+|-+$/g, "");
|
|
128
|
+
if (!normalized) return "";
|
|
129
|
+
|
|
130
|
+
// Fragment IDs are public implementation details, not content IDs. Hash the
|
|
131
|
+
// immutable entity namespace so HTML stays readable without embedding a full
|
|
132
|
+
// 30-character TypeID. A 64-bit scope keeps collision risk negligible even
|
|
133
|
+
// across a page containing thousands of independently rendered documents.
|
|
134
|
+
let hash = FOOTNOTE_NAMESPACE_HASH_OFFSET;
|
|
135
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
136
|
+
hash ^= BigInt(normalized.charCodeAt(index));
|
|
137
|
+
hash =
|
|
138
|
+
(hash * FOOTNOTE_NAMESPACE_HASH_PRIME) & FOOTNOTE_NAMESPACE_HASH_MASK;
|
|
95
139
|
}
|
|
96
|
-
|
|
140
|
+
|
|
141
|
+
return hash.toString(36).padStart(13, "0");
|
|
97
142
|
}
|
|
98
143
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
144
|
+
function collectFootnoteReferences(
|
|
145
|
+
node: TiptapNode,
|
|
146
|
+
visit: (reference: TiptapNode) => void,
|
|
147
|
+
): void {
|
|
148
|
+
if (node.type === "footnoteReference") {
|
|
149
|
+
visit(node);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const child of node.content ?? []) {
|
|
154
|
+
collectFootnoteReferences(child, visit);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createFootnotePlan(
|
|
159
|
+
doc: TiptapNode,
|
|
160
|
+
options: TiptapRenderOptions,
|
|
161
|
+
): FootnotePlan {
|
|
162
|
+
const definitions = new Map<string, TiptapNode>();
|
|
163
|
+
const bodyNodes = (doc.content ?? []).filter(
|
|
164
|
+
(node) => node.type !== "footnoteDefinition",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
for (const node of doc.content ?? []) {
|
|
168
|
+
if (node.type !== "footnoteDefinition") continue;
|
|
169
|
+
const key = getFootnoteLabelKey(getStringAttr(node.attrs, "label"));
|
|
170
|
+
if (key && !definitions.has(key)) definitions.set(key, node);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const namespace = createFootnoteIdScope(options.namespace);
|
|
174
|
+
const idScope = namespace ? `${namespace}-` : "";
|
|
175
|
+
const groupsByKey = new Map<string, FootnoteGroup>();
|
|
176
|
+
const groups: FootnoteGroup[] = [];
|
|
177
|
+
const referencesByNode = new WeakMap<TiptapNode, FootnoteReferencePlan>();
|
|
178
|
+
|
|
179
|
+
for (const owner of bodyNodes) {
|
|
180
|
+
collectFootnoteReferences(owner, (reference) => {
|
|
181
|
+
const normalizedLabel = normalizeFootnoteLabel(
|
|
182
|
+
getStringAttr(reference.attrs, "label"),
|
|
183
|
+
);
|
|
184
|
+
const label = normalizedLabel || "footnote";
|
|
185
|
+
const key = getFootnoteLabelKey(label);
|
|
186
|
+
let group = groupsByKey.get(key);
|
|
187
|
+
|
|
188
|
+
if (!group) {
|
|
189
|
+
const number = groupsByKey.size + 1;
|
|
190
|
+
group = {
|
|
191
|
+
definitionId: `fn-${idScope}${number}`,
|
|
192
|
+
number,
|
|
193
|
+
label,
|
|
194
|
+
definition: definitions.get(key) ?? null,
|
|
195
|
+
references: [],
|
|
196
|
+
};
|
|
197
|
+
groupsByKey.set(key, group);
|
|
198
|
+
groups.push(group);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const occurrence = group.references.length + 1;
|
|
202
|
+
const referencePlan: FootnoteReferencePlan = {
|
|
203
|
+
definitionId: group.definitionId,
|
|
204
|
+
referenceId: `fnref-${idScope}${group.number}-${occurrence}`,
|
|
205
|
+
number: group.number,
|
|
206
|
+
occurrence,
|
|
207
|
+
label: group.label,
|
|
208
|
+
hasDefinition: group.definition !== null,
|
|
209
|
+
};
|
|
210
|
+
group.references.push(referencePlan);
|
|
211
|
+
referencesByNode.set(reference, referencePlan);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { groups, referencesByNode };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderFootnoteReference(
|
|
219
|
+
node: TiptapNode,
|
|
220
|
+
context: RenderContext,
|
|
221
|
+
): string {
|
|
222
|
+
const plan = context.footnotes.referencesByNode.get(node);
|
|
223
|
+
const fallbackLabel =
|
|
224
|
+
normalizeFootnoteLabel(getStringAttr(node.attrs, "label")) || "footnote";
|
|
225
|
+
const label = plan?.label ?? fallbackLabel;
|
|
226
|
+
const number = plan?.number ?? label;
|
|
227
|
+
|
|
228
|
+
if (!plan?.hasDefinition) {
|
|
229
|
+
return `<sup class="footnote-ref">${escapeHtml(String(number))}</sup>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
`<sup class="footnote-ref">` +
|
|
234
|
+
`<a id="${escapeHtml(plan.referenceId)}" href="#${escapeHtml(plan.definitionId)}" role="doc-noteref">` +
|
|
235
|
+
`${plan.number}</a></sup>`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderFootnoteGroup(
|
|
240
|
+
group: FootnoteGroup,
|
|
241
|
+
context: RenderContext,
|
|
242
|
+
includeValue: boolean,
|
|
243
|
+
): string {
|
|
244
|
+
if (!group.definition) return "";
|
|
245
|
+
|
|
246
|
+
const definitionContent = group.definition.content ?? [];
|
|
247
|
+
const backlinks = group.references
|
|
248
|
+
.map((reference) => {
|
|
249
|
+
const occurrence =
|
|
250
|
+
group.references.length > 1 ? String(reference.occurrence) : "";
|
|
251
|
+
return (
|
|
252
|
+
`<a href="#${escapeHtml(reference.referenceId)}" class="footnote-backref" ` +
|
|
253
|
+
`role="doc-backlink">↩︎${occurrence}</a>`
|
|
254
|
+
);
|
|
255
|
+
})
|
|
256
|
+
.join(" ");
|
|
257
|
+
const backlinksHtml = `<span class="footnote-backlinks">${backlinks}</span>`;
|
|
258
|
+
const lastBlock = definitionContent.at(-1);
|
|
259
|
+
let bodyHtml: string;
|
|
260
|
+
|
|
261
|
+
if (lastBlock?.type === "paragraph") {
|
|
262
|
+
const precedingBlocks = definitionContent.slice(0, -1);
|
|
263
|
+
const lastParagraphHtml = context.renderChildren(lastBlock.content);
|
|
264
|
+
const separator = lastParagraphHtml ? " " : "";
|
|
265
|
+
bodyHtml =
|
|
266
|
+
context.renderChildren(precedingBlocks) +
|
|
267
|
+
`<p>${lastParagraphHtml}${separator}${backlinksHtml}</p>`;
|
|
268
|
+
} else {
|
|
269
|
+
bodyHtml =
|
|
270
|
+
context.renderChildren(definitionContent) +
|
|
271
|
+
`<p class="footnote-backlinks">${backlinks}</p>`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const valueAttr = includeValue ? ` value="${group.number}"` : "";
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
`<li id="${escapeHtml(group.definitionId)}" class="footnote"${valueAttr}>` +
|
|
278
|
+
bodyHtml +
|
|
279
|
+
`</li>`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderFootnoteEndnotes(context: RenderContext): string {
|
|
284
|
+
const groups = context.footnotes.groups.filter(
|
|
285
|
+
(group) => group.definition !== null,
|
|
286
|
+
);
|
|
287
|
+
if (groups.length === 0) return "";
|
|
288
|
+
|
|
289
|
+
const firstNumber = groups[0]?.number ?? 1;
|
|
290
|
+
let expectedNumber = firstNumber;
|
|
291
|
+
const items = groups
|
|
292
|
+
.map((group) => {
|
|
293
|
+
const includeValue = group.number !== expectedNumber;
|
|
294
|
+
expectedNumber = group.number + 1;
|
|
295
|
+
return renderFootnoteGroup(group, context, includeValue);
|
|
296
|
+
})
|
|
297
|
+
.join("");
|
|
298
|
+
const startAttr = firstNumber === 1 ? "" : ` start="${firstNumber}"`;
|
|
104
299
|
|
|
105
|
-
|
|
300
|
+
return (
|
|
301
|
+
`<section class="footnote-endnotes" role="doc-endnotes">` +
|
|
302
|
+
`<ol class="footnote-list"${startAttr}>${items}</ol>` +
|
|
303
|
+
`</section>`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderDocumentContent(
|
|
106
308
|
node: TiptapNode,
|
|
107
309
|
context: RenderContext,
|
|
108
310
|
): string {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
? stripSingleParagraph(context.renderChildren(definitionNode.content))
|
|
114
|
-
: "";
|
|
115
|
-
|
|
116
|
-
// The `footref` / `footref-toggle` classes are not styled by us; they mark the
|
|
117
|
-
// Tufte sidenote trio as an Org-mode-style footnote so HTML-to-Markdown readers
|
|
118
|
-
// (Defuddle, used by Obsidian Web Clipper) recover `[^n]` references instead of
|
|
119
|
-
// silently dropping `span.sidenote`. See docs/internal/markdown-contract.md.
|
|
311
|
+
const bodyNodes = (node.content ?? []).filter(
|
|
312
|
+
(child) => child.type !== "footnoteDefinition",
|
|
313
|
+
);
|
|
314
|
+
|
|
120
315
|
return (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`<span class="sidenote">${bodyHtml}</span>`
|
|
316
|
+
bodyNodes.map((bodyNode) => context.renderNode(bodyNode)).join("") +
|
|
317
|
+
renderFootnoteEndnotes(context)
|
|
124
318
|
);
|
|
125
319
|
}
|
|
126
320
|
|
|
@@ -140,30 +334,7 @@ const MARK_RENDERERS: Record<string, MarkRenderer> = {
|
|
|
140
334
|
};
|
|
141
335
|
|
|
142
336
|
const NODE_RENDERERS: Record<string, NodeRenderer> = {
|
|
143
|
-
doc: (node, context) =>
|
|
144
|
-
const content = node.content ?? [];
|
|
145
|
-
|
|
146
|
-
// Build definition lookup so footnoteReference can inline sidenotes
|
|
147
|
-
const definitionMap = new Map<string, TiptapNode>();
|
|
148
|
-
for (const child of content) {
|
|
149
|
-
if (child.type === "footnoteDefinition") {
|
|
150
|
-
const label = normalizeFootnoteLabel(
|
|
151
|
-
getStringAttr(child.attrs, "label"),
|
|
152
|
-
);
|
|
153
|
-
if (label) definitionMap.set(label, child);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
activeDefinitionMap = definitionMap.size > 0 ? definitionMap : null;
|
|
158
|
-
try {
|
|
159
|
-
const bodyNodes = content.filter(
|
|
160
|
-
(child) => child.type !== "footnoteDefinition",
|
|
161
|
-
);
|
|
162
|
-
return context.renderChildren(bodyNodes);
|
|
163
|
-
} finally {
|
|
164
|
-
activeDefinitionMap = null;
|
|
165
|
-
}
|
|
166
|
-
},
|
|
337
|
+
doc: (node, context) => renderDocumentContent(node, context),
|
|
167
338
|
paragraph: (node, context) =>
|
|
168
339
|
`<p>${context.renderChildren(node.content)}</p>`,
|
|
169
340
|
heading: (node, context) => {
|
|
@@ -213,7 +384,7 @@ const NODE_RENDERERS: Record<string, NodeRenderer> = {
|
|
|
213
384
|
return `<div class="tiptap-html-block">${html}</div>`;
|
|
214
385
|
},
|
|
215
386
|
moreBreak: () => "<!--more-->",
|
|
216
|
-
footnoteReference: (node, context) =>
|
|
387
|
+
footnoteReference: (node, context) => renderFootnoteReference(node, context),
|
|
217
388
|
footnoteDefinition: () => "",
|
|
218
389
|
};
|
|
219
390
|
|
|
@@ -230,40 +401,144 @@ function renderText(text: string, marks: TiptapMark[] = []): string {
|
|
|
230
401
|
return html;
|
|
231
402
|
}
|
|
232
403
|
|
|
233
|
-
function renderChildren(content: TiptapNode[] = []): string {
|
|
234
|
-
return content.map(renderNode).join("");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
404
|
function renderUnknownNode(node: TiptapNode, context: RenderContext): string {
|
|
238
405
|
return node.content ? context.renderChildren(node.content) : "";
|
|
239
406
|
}
|
|
240
407
|
|
|
241
|
-
function
|
|
242
|
-
const
|
|
243
|
-
|
|
408
|
+
function createRenderContext(footnotes: FootnotePlan): RenderContext {
|
|
409
|
+
const context: RenderContext = {
|
|
410
|
+
footnotes,
|
|
411
|
+
renderChildren(content: TiptapNode[] = []) {
|
|
412
|
+
return content.map((node) => context.renderNode(node)).join("");
|
|
413
|
+
},
|
|
414
|
+
renderNode(node) {
|
|
415
|
+
const renderNodeType = NODE_RENDERERS[node.type] ?? renderUnknownNode;
|
|
416
|
+
return renderNodeType(node, context);
|
|
417
|
+
},
|
|
418
|
+
renderText,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
return context;
|
|
244
422
|
}
|
|
245
423
|
|
|
246
|
-
const RENDER_CONTEXT: RenderContext = {
|
|
247
|
-
renderChildren,
|
|
248
|
-
renderNode,
|
|
249
|
-
renderText,
|
|
250
|
-
};
|
|
251
|
-
|
|
252
424
|
/**
|
|
253
425
|
* Renders a parsed TipTap document to HTML.
|
|
254
426
|
*
|
|
255
427
|
* @param doc - Parsed TipTap document
|
|
428
|
+
* @param options - Rendering options, including a stable footnote ID namespace
|
|
256
429
|
* @returns HTML string
|
|
257
430
|
*/
|
|
258
|
-
export function renderTiptapDocument(
|
|
431
|
+
export function renderTiptapDocument(
|
|
432
|
+
doc: JSONContent,
|
|
433
|
+
options: TiptapRenderOptions = {},
|
|
434
|
+
): string {
|
|
259
435
|
if (doc.type !== "doc") return "";
|
|
260
|
-
|
|
436
|
+
const normalized = normalizeFootnoteArtifacts(doc) as TiptapNode;
|
|
437
|
+
const context = createRenderContext(createFootnotePlan(normalized, options));
|
|
438
|
+
return context.renderNode(normalized);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface TiptapBoundaryRenderResult {
|
|
442
|
+
beforeHtml: string;
|
|
443
|
+
afterHtml: string;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Render a document as two byte-compatible segments around a top-level source
|
|
448
|
+
* boundary.
|
|
449
|
+
*
|
|
450
|
+
* The full document is planned before either segment is rendered, so a
|
|
451
|
+
* footnote before the boundary still includes backlinks to repeated references
|
|
452
|
+
* after it. Joining the two strings is therefore identical to a normal render.
|
|
453
|
+
*
|
|
454
|
+
* @param doc - Parsed TipTap document
|
|
455
|
+
* @param boundaryIndex - Index in the original `doc.content` array
|
|
456
|
+
* @param options - Rendering options, including a stable footnote ID namespace
|
|
457
|
+
* @returns The rendered segments, or null for an invalid root or boundary
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* renderTiptapDocumentAroundBoundary(
|
|
461
|
+
* { type: "doc", content: [{ type: "paragraph" }] },
|
|
462
|
+
* 1,
|
|
463
|
+
* );
|
|
464
|
+
* // { beforeHtml: "<p></p>", afterHtml: "" }
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
export function renderTiptapDocumentAroundBoundary(
|
|
468
|
+
doc: JSONContent,
|
|
469
|
+
boundaryIndex: number,
|
|
470
|
+
options: TiptapRenderOptions = {},
|
|
471
|
+
): TiptapBoundaryRenderResult | null {
|
|
472
|
+
const originalContent = doc.content ?? [];
|
|
473
|
+
if (
|
|
474
|
+
doc.type !== "doc" ||
|
|
475
|
+
!Array.isArray(doc.content) ||
|
|
476
|
+
!Number.isInteger(boundaryIndex) ||
|
|
477
|
+
boundaryIndex < 0 ||
|
|
478
|
+
boundaryIndex > originalContent.length
|
|
479
|
+
) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const normalized = normalizeFootnoteArtifacts(doc) as TiptapNode;
|
|
484
|
+
const context = createRenderContext(createFootnotePlan(normalized, options));
|
|
485
|
+
const normalizedBoundaryIndex = Math.min(
|
|
486
|
+
boundaryIndex,
|
|
487
|
+
normalized.content?.length ?? 0,
|
|
488
|
+
);
|
|
489
|
+
let beforeBodyHtml = "";
|
|
490
|
+
let afterBodyHtml = "";
|
|
491
|
+
|
|
492
|
+
for (const [index, node] of (normalized.content ?? []).entries()) {
|
|
493
|
+
if (node.type === "footnoteDefinition") continue;
|
|
494
|
+
const html = context.renderNode(node);
|
|
495
|
+
if (index < normalizedBoundaryIndex) {
|
|
496
|
+
beforeBodyHtml += html;
|
|
497
|
+
} else {
|
|
498
|
+
afterBodyHtml += html;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
beforeHtml: beforeBodyHtml,
|
|
504
|
+
afterHtml: afterBodyHtml + renderFootnoteEndnotes(context),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Strictly parses and renders a TipTap JSON document.
|
|
510
|
+
*
|
|
511
|
+
* Unlike `renderTiptapJson`, this distinguishes invalid canonical JSON from a
|
|
512
|
+
* valid empty document. Projection rebuilds use it so malformed historical
|
|
513
|
+
* content is reported and never marked as current.
|
|
514
|
+
*
|
|
515
|
+
* @param json - TipTap JSON string
|
|
516
|
+
* @param options - Rendering options
|
|
517
|
+
* @returns A discriminated render result
|
|
518
|
+
*/
|
|
519
|
+
export function tryRenderTiptapJson(
|
|
520
|
+
json: string,
|
|
521
|
+
options: TiptapRenderOptions = {},
|
|
522
|
+
): TiptapRenderResult {
|
|
523
|
+
try {
|
|
524
|
+
const doc = JSON.parse(json) as JSONContent;
|
|
525
|
+
if (doc.type !== "doc") {
|
|
526
|
+
return { ok: false, error: "TipTap body root must be a doc node." };
|
|
527
|
+
}
|
|
528
|
+
return { ok: true, html: renderTiptapDocument(doc, options) };
|
|
529
|
+
} catch (error) {
|
|
530
|
+
return {
|
|
531
|
+
ok: false,
|
|
532
|
+
error: error instanceof Error ? error.message : "Invalid TipTap JSON.",
|
|
533
|
+
};
|
|
534
|
+
}
|
|
261
535
|
}
|
|
262
536
|
|
|
263
537
|
/**
|
|
264
538
|
* Renders a Tiptap JSON document to an HTML string.
|
|
265
539
|
*
|
|
266
540
|
* @param json - Tiptap JSON string or parsed document object
|
|
541
|
+
* @param options - Rendering options, including a stable footnote ID namespace
|
|
267
542
|
* @returns HTML string
|
|
268
543
|
*
|
|
269
544
|
* @example
|
|
@@ -272,13 +547,12 @@ export function renderTiptapDocument(doc: JSONContent): string {
|
|
|
272
547
|
* // "<p>Hello</p>"
|
|
273
548
|
* ```
|
|
274
549
|
*/
|
|
275
|
-
export function renderTiptapJson(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
550
|
+
export function renderTiptapJson(
|
|
551
|
+
json: string,
|
|
552
|
+
options: TiptapRenderOptions = {},
|
|
553
|
+
): string {
|
|
554
|
+
const result = tryRenderTiptapJson(json, options);
|
|
555
|
+
return result.ok ? result.html : "";
|
|
282
556
|
}
|
|
283
557
|
|
|
284
558
|
/**
|
package/src/lib/view.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Theme components receive only View types -- no lib/ imports needed.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { JSONContent } from "@tiptap/core";
|
|
8
9
|
import type {
|
|
9
10
|
Post,
|
|
10
11
|
PostWithMedia,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
import { getCollectionPagePath } from "./collection-paths.js";
|
|
36
37
|
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
37
38
|
import { extractSummaryHtml, extractBodyText } from "./summary.js";
|
|
38
|
-
import {
|
|
39
|
+
import { renderTiptapDocumentAroundBoundary } from "./tiptap-render.js";
|
|
39
40
|
import { highlightText } from "./search-snippet.js";
|
|
40
41
|
import { isFullUrl, toPublicPath, toSameSitePath } from "./url.js";
|
|
41
42
|
|
|
@@ -154,8 +155,9 @@ const NOTE_SUMMARY_MIN_HIDDEN_CHARS = 200;
|
|
|
154
155
|
* The summary HTML is not a byte-prefix of `bodyHtml` — structural nodes
|
|
155
156
|
* (horizontalRule, moreBreak, image) appear in `bodyHtml` but are excluded from
|
|
156
157
|
* the summary, so slicing `bodyHtml` by summary length lands mid-tag and
|
|
157
|
-
* corrupts the markup. Instead we
|
|
158
|
-
*
|
|
158
|
+
* corrupts the markup. Instead we split a full-document render at the exact
|
|
159
|
+
* source boundary. The full render plan keeps repeated-footnote backlinks
|
|
160
|
+
* byte-compatible across the split.
|
|
159
161
|
*
|
|
160
162
|
* @param bodyJson - Tiptap JSON string for the post body
|
|
161
163
|
* @param bodyHtml - Rendered body HTML to splice the marker into
|
|
@@ -169,6 +171,7 @@ function spliceAtSummaryBoundary(
|
|
|
169
171
|
bodyHtml: string,
|
|
170
172
|
breakAtIndex: number,
|
|
171
173
|
markerHtml: string,
|
|
174
|
+
namespace: string,
|
|
172
175
|
): string | null {
|
|
173
176
|
try {
|
|
174
177
|
const doc = JSON.parse(bodyJson) as { type?: string; content?: unknown[] };
|
|
@@ -180,12 +183,13 @@ function spliceAtSummaryBoundary(
|
|
|
180
183
|
) {
|
|
181
184
|
return null;
|
|
182
185
|
}
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
const split = renderTiptapDocumentAroundBoundary(
|
|
187
|
+
doc as JSONContent,
|
|
188
|
+
breakAtIndex,
|
|
189
|
+
{ namespace },
|
|
190
|
+
);
|
|
191
|
+
if (!split || split.beforeHtml + split.afterHtml !== bodyHtml) return null;
|
|
192
|
+
return split.beforeHtml + markerHtml + split.afterHtml;
|
|
189
193
|
} catch {
|
|
190
194
|
// Better an untouched body than corrupted markup.
|
|
191
195
|
return null;
|
|
@@ -243,13 +247,13 @@ function clipPreviewText(
|
|
|
243
247
|
*
|
|
244
248
|
* @param post - Post with media attachments from database
|
|
245
249
|
* @param _ctx - Media context with URL configuration
|
|
246
|
-
* @param
|
|
250
|
+
* @param threadCollections - Optional shared Thread collections projected here
|
|
247
251
|
* @returns Render-ready PostView with pre-computed fields
|
|
248
252
|
*/
|
|
249
253
|
export function toPostView(
|
|
250
254
|
post: PostWithMedia,
|
|
251
255
|
ctx: MediaContext,
|
|
252
|
-
|
|
256
|
+
threadCollections?: Collection[],
|
|
253
257
|
isLastInThread?: boolean,
|
|
254
258
|
aliasPath?: string,
|
|
255
259
|
pinnedInCollection?: boolean,
|
|
@@ -288,6 +292,7 @@ export function toPostView(
|
|
|
288
292
|
isArticle ? ARTICLE_SUMMARY_MAX_BLOCKS : NOTE_SUMMARY_MAX_BLOCKS,
|
|
289
293
|
isArticle ? ARTICLE_SUMMARY_MAX_CHARS : NOTE_SUMMARY_MAX_CHARS,
|
|
290
294
|
isArticle ? 0 : NOTE_SUMMARY_MIN_HIDDEN_CHARS,
|
|
295
|
+
{ namespace: id },
|
|
291
296
|
);
|
|
292
297
|
if (result && isArticle) {
|
|
293
298
|
summaryHtml = result.html;
|
|
@@ -298,6 +303,7 @@ export function toPostView(
|
|
|
298
303
|
post.bodyHtml,
|
|
299
304
|
result.breakAtIndex,
|
|
300
305
|
'<span id="continue"></span>',
|
|
306
|
+
id,
|
|
301
307
|
);
|
|
302
308
|
if (spliced) bodyHtmlWithAnchor = spliced;
|
|
303
309
|
}
|
|
@@ -307,6 +313,7 @@ export function toPostView(
|
|
|
307
313
|
post.bodyHtml,
|
|
308
314
|
result.breakAtIndex,
|
|
309
315
|
"<span data-note-break></span>",
|
|
316
|
+
id,
|
|
310
317
|
);
|
|
311
318
|
if (spliced) {
|
|
312
319
|
bodyHtmlWithAnchor = spliced;
|
|
@@ -316,11 +323,13 @@ export function toPostView(
|
|
|
316
323
|
}
|
|
317
324
|
|
|
318
325
|
// Convert collection tags
|
|
319
|
-
const collections: CollectionTagView[] = (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
326
|
+
const collections: CollectionTagView[] = (threadCollections ?? []).map(
|
|
327
|
+
(c) => ({
|
|
328
|
+
slug: c.slug,
|
|
329
|
+
title: c.title,
|
|
330
|
+
url: toPublicPath(getCollectionPagePath(c.slug), ctx.sitePathPrefix),
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
324
333
|
|
|
325
334
|
// Convert media attachments
|
|
326
335
|
const media: MediaView[] = post.mediaAttachments.map((m) => ({
|
|
@@ -429,12 +438,12 @@ export function toPostViewFromPost(
|
|
|
429
438
|
ctx: MediaContext,
|
|
430
439
|
isLastInThread?: boolean,
|
|
431
440
|
aliasPath?: string,
|
|
432
|
-
|
|
441
|
+
threadCollections?: Collection[],
|
|
433
442
|
): PostView {
|
|
434
443
|
return toPostView(
|
|
435
444
|
{ ...post, mediaAttachments: [] },
|
|
436
445
|
ctx,
|
|
437
|
-
|
|
446
|
+
threadCollections,
|
|
438
447
|
isLastInThread,
|
|
439
448
|
aliasPath,
|
|
440
449
|
);
|
|
@@ -534,6 +543,7 @@ export function toNavItemView(
|
|
|
534
543
|
type: item.type as NavItemType,
|
|
535
544
|
systemKey: item.systemKey,
|
|
536
545
|
collectionId: item.collectionId,
|
|
546
|
+
postId: item.postId,
|
|
537
547
|
label,
|
|
538
548
|
url: publicUrl,
|
|
539
549
|
placement: item.placement as NavItemPlacement,
|
|
@@ -587,14 +597,14 @@ export function toSearchResultView(
|
|
|
587
597
|
ctx: MediaContext,
|
|
588
598
|
query?: string,
|
|
589
599
|
aliasPath?: string,
|
|
590
|
-
|
|
600
|
+
threadCollections?: Collection[],
|
|
591
601
|
): SearchResultView {
|
|
592
602
|
const post = toPostViewFromPost(
|
|
593
603
|
result.post,
|
|
594
604
|
ctx,
|
|
595
605
|
undefined,
|
|
596
606
|
aliasPath,
|
|
597
|
-
|
|
607
|
+
threadCollections,
|
|
598
608
|
);
|
|
599
609
|
|
|
600
610
|
let titleHighlighted: string | undefined;
|