@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
|
@@ -18,7 +18,7 @@ import { createPostService } from "../../services/post.js";
|
|
|
18
18
|
import { createMediaService } from "../../services/media.js";
|
|
19
19
|
import { createPathService } from "../../services/path.js";
|
|
20
20
|
import { createCollectionService } from "../../services/collection.js";
|
|
21
|
-
import {
|
|
21
|
+
import { posts as postTable, sites } from "../../db/schema.js";
|
|
22
22
|
import { buildMediaMap } from "../media-helpers.js";
|
|
23
23
|
import {
|
|
24
24
|
assembleCollectionTimeline,
|
|
@@ -157,18 +157,14 @@ describe("Timeline data assembly", () => {
|
|
|
157
157
|
return {
|
|
158
158
|
post: postWithMedia,
|
|
159
159
|
threadPreview: {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
...threadCtx.penultimateReply,
|
|
169
|
-
mediaAttachments: [],
|
|
170
|
-
}
|
|
171
|
-
: undefined,
|
|
160
|
+
leadingReplies: threadCtx.leadingReplies.map((reply) => ({
|
|
161
|
+
...reply,
|
|
162
|
+
mediaAttachments: [],
|
|
163
|
+
})),
|
|
164
|
+
trailingReplies: threadCtx.trailingReplies.map((reply) => ({
|
|
165
|
+
...reply,
|
|
166
|
+
mediaAttachments: [],
|
|
167
|
+
})),
|
|
172
168
|
latestReply: {
|
|
173
169
|
...threadCtx.latestReply,
|
|
174
170
|
mediaAttachments: [],
|
|
@@ -183,7 +179,9 @@ describe("Timeline data assembly", () => {
|
|
|
183
179
|
|
|
184
180
|
expect(items).toHaveLength(1);
|
|
185
181
|
expect(items[0]?.threadPreview).toBeDefined();
|
|
186
|
-
expect(items[0]?.threadPreview?.
|
|
182
|
+
expect(items[0]?.threadPreview?.leadingReplies[0]?.bodyText).toBe(
|
|
183
|
+
"Reply 1",
|
|
184
|
+
);
|
|
187
185
|
expect(items[0]?.threadPreview?.latestReply.bodyText).toBe("Reply 2");
|
|
188
186
|
expect(items[0]?.threadPreview?.totalReplyCount).toBe(2);
|
|
189
187
|
});
|
|
@@ -347,6 +345,7 @@ describe("Timeline data assembly", () => {
|
|
|
347
345
|
|
|
348
346
|
expect(result.items).toHaveLength(1);
|
|
349
347
|
expect(result.items[0]?.post.id).toBe(root.id);
|
|
348
|
+
expect(result.items[0]?.curatedThread?.showContextRatings).toBe(false);
|
|
350
349
|
expect(result.items[0]?.curatedThread?.segments).toEqual([
|
|
351
350
|
expect.objectContaining({
|
|
352
351
|
post: expect.objectContaining({ id: root.id }),
|
|
@@ -526,22 +525,26 @@ describe("Timeline data assembly", () => {
|
|
|
526
525
|
]);
|
|
527
526
|
});
|
|
528
527
|
|
|
529
|
-
it("orders
|
|
528
|
+
it("orders Featured Threads by the latest selected Post publication time", async () => {
|
|
530
529
|
const olderFeaturedRoot = await postService.create({
|
|
531
530
|
format: "note",
|
|
532
531
|
bodyMarkdown: "Older featured root",
|
|
532
|
+
publishedAt: 1000,
|
|
533
533
|
});
|
|
534
534
|
const olderFeaturedReply = await postService.create({
|
|
535
535
|
format: "note",
|
|
536
536
|
bodyMarkdown: "Older featured reply",
|
|
537
537
|
replyToId: olderFeaturedRoot.id,
|
|
538
|
+
publishedAt: 3000,
|
|
538
539
|
});
|
|
539
540
|
const newerFeaturedRoot = await postService.create({
|
|
540
541
|
format: "note",
|
|
541
542
|
bodyMarkdown: "Newer featured root",
|
|
542
543
|
featured: true,
|
|
544
|
+
publishedAt: 2000,
|
|
543
545
|
});
|
|
544
546
|
|
|
547
|
+
// Featured timestamps deliberately disagree with publication order.
|
|
545
548
|
await db
|
|
546
549
|
.update(postTable)
|
|
547
550
|
.set({ featuredAt: 100 })
|
|
@@ -555,6 +558,7 @@ describe("Timeline data assembly", () => {
|
|
|
555
558
|
format: "note",
|
|
556
559
|
bodyMarkdown: "Later non-featured reply",
|
|
557
560
|
replyToId: olderFeaturedReply.id,
|
|
561
|
+
publishedAt: 4000,
|
|
558
562
|
});
|
|
559
563
|
|
|
560
564
|
const result = await assembleFeaturedTimeline(createTimelineContext(), {
|
|
@@ -562,11 +566,63 @@ describe("Timeline data assembly", () => {
|
|
|
562
566
|
});
|
|
563
567
|
|
|
564
568
|
expect(result.items).toHaveLength(2);
|
|
565
|
-
expect(result.items[0]?.post.id).toBe(
|
|
566
|
-
expect(result.items[1]?.post.id).toBe(
|
|
569
|
+
expect(result.items[0]?.post.id).toBe(olderFeaturedRoot.id);
|
|
570
|
+
expect(result.items[1]?.post.id).toBe(newerFeaturedRoot.id);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("loads only the Root, Featured Posts, and final Post for curated display", async () => {
|
|
574
|
+
const root = await postService.create({
|
|
575
|
+
format: "note",
|
|
576
|
+
bodyMarkdown: "Root",
|
|
577
|
+
publishedAt: 1000,
|
|
578
|
+
});
|
|
579
|
+
const hiddenReplyA = await postService.create({
|
|
580
|
+
format: "note",
|
|
581
|
+
bodyMarkdown: "Hidden reply A",
|
|
582
|
+
replyToId: root.id,
|
|
583
|
+
publishedAt: 2000,
|
|
584
|
+
});
|
|
585
|
+
const featuredReply = await postService.create({
|
|
586
|
+
format: "note",
|
|
587
|
+
bodyMarkdown: "Featured reply",
|
|
588
|
+
replyToId: hiddenReplyA.id,
|
|
589
|
+
featured: true,
|
|
590
|
+
publishedAt: 3000,
|
|
591
|
+
});
|
|
592
|
+
const hiddenReplyB = await postService.create({
|
|
593
|
+
format: "note",
|
|
594
|
+
bodyMarkdown: "Hidden reply B",
|
|
595
|
+
replyToId: featuredReply.id,
|
|
596
|
+
publishedAt: 4000,
|
|
597
|
+
});
|
|
598
|
+
const finalReply = await postService.create({
|
|
599
|
+
format: "note",
|
|
600
|
+
bodyMarkdown: "Final reply",
|
|
601
|
+
replyToId: hiddenReplyB.id,
|
|
602
|
+
publishedAt: 5000,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
const result = await postService.getFeaturedThreadTimelineData([root.id]);
|
|
606
|
+
const thread = result.get(root.id);
|
|
607
|
+
|
|
608
|
+
expect(thread?.posts).toEqual([
|
|
609
|
+
expect.objectContaining({
|
|
610
|
+
post: expect.objectContaining({ id: root.id }),
|
|
611
|
+
position: 0,
|
|
612
|
+
}),
|
|
613
|
+
expect.objectContaining({
|
|
614
|
+
post: expect.objectContaining({ id: featuredReply.id }),
|
|
615
|
+
position: 2,
|
|
616
|
+
}),
|
|
617
|
+
expect.objectContaining({
|
|
618
|
+
post: expect.objectContaining({ id: finalReply.id }),
|
|
619
|
+
position: 4,
|
|
620
|
+
}),
|
|
621
|
+
]);
|
|
622
|
+
expect(thread?.featuredPostIds).toEqual([featuredReply.id]);
|
|
567
623
|
});
|
|
568
624
|
|
|
569
|
-
it("
|
|
625
|
+
it("renders every post in each collected Thread and sorts by Thread activity", async () => {
|
|
570
626
|
const collection = await collectionService.create({
|
|
571
627
|
slug: "reading",
|
|
572
628
|
title: "Reading",
|
|
@@ -595,26 +651,12 @@ describe("Timeline data assembly", () => {
|
|
|
595
651
|
bodyMarkdown: "Second thread root",
|
|
596
652
|
});
|
|
597
653
|
|
|
598
|
-
await
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
},
|
|
605
|
-
{
|
|
606
|
-
siteId: DEFAULT_TEST_SITE_ID,
|
|
607
|
-
postId: collectedReplyB.id,
|
|
608
|
-
collectionId: collection.id,
|
|
609
|
-
createdAt: 200,
|
|
610
|
-
},
|
|
611
|
-
{
|
|
612
|
-
siteId: DEFAULT_TEST_SITE_ID,
|
|
613
|
-
postId: secondRoot.id,
|
|
614
|
-
collectionId: collection.id,
|
|
615
|
-
createdAt: 300,
|
|
616
|
-
},
|
|
617
|
-
]);
|
|
654
|
+
await collectionService.addThread(collection.id, firstRoot.id, {
|
|
655
|
+
createdAt: 200,
|
|
656
|
+
});
|
|
657
|
+
await collectionService.addThread(collection.id, secondRoot.id, {
|
|
658
|
+
createdAt: 300,
|
|
659
|
+
});
|
|
618
660
|
|
|
619
661
|
const result = await assembleCollectionTimeline(createTimelineContext(), {
|
|
620
662
|
collectionIds: [collection.id],
|
|
@@ -625,6 +667,7 @@ describe("Timeline data assembly", () => {
|
|
|
625
667
|
expect(result.items).toHaveLength(2);
|
|
626
668
|
expect(result.items[0]?.post.id).toBe(secondRoot.id);
|
|
627
669
|
expect(result.items[1]?.post.id).toBe(firstRoot.id);
|
|
670
|
+
expect(result.items[1]?.curatedThread?.showContextRatings).toBe(true);
|
|
628
671
|
expect(result.items[1]?.curatedThread?.segments).toEqual([
|
|
629
672
|
expect.objectContaining({
|
|
630
673
|
post: expect.objectContaining({ id: firstRoot.id }),
|
|
@@ -634,17 +677,22 @@ describe("Timeline data assembly", () => {
|
|
|
634
677
|
expect.objectContaining({
|
|
635
678
|
post: expect.objectContaining({ id: collectedReplyA.id }),
|
|
636
679
|
hiddenBeforeCount: 0,
|
|
637
|
-
highlighted:
|
|
680
|
+
highlighted: false,
|
|
681
|
+
}),
|
|
682
|
+
expect.objectContaining({
|
|
683
|
+
post: expect.objectContaining({ id: hiddenMiddleReply.id }),
|
|
684
|
+
hiddenBeforeCount: 0,
|
|
685
|
+
highlighted: false,
|
|
638
686
|
}),
|
|
639
687
|
expect.objectContaining({
|
|
640
688
|
post: expect.objectContaining({ id: collectedReplyB.id }),
|
|
641
|
-
hiddenBeforeCount:
|
|
689
|
+
hiddenBeforeCount: 0,
|
|
642
690
|
highlighted: true,
|
|
643
691
|
}),
|
|
644
692
|
]);
|
|
645
693
|
});
|
|
646
694
|
|
|
647
|
-
it("
|
|
695
|
+
it("renders the complete Thread and highlights its latest Post", async () => {
|
|
648
696
|
const collection = await collectionService.create({
|
|
649
697
|
slug: "root-only",
|
|
650
698
|
title: "Root only",
|
|
@@ -659,10 +707,7 @@ describe("Timeline data assembly", () => {
|
|
|
659
707
|
replyToId: root.id,
|
|
660
708
|
});
|
|
661
709
|
|
|
662
|
-
await
|
|
663
|
-
siteId: DEFAULT_TEST_SITE_ID,
|
|
664
|
-
postId: root.id,
|
|
665
|
-
collectionId: collection.id,
|
|
710
|
+
await collectionService.addThread(collection.id, root.id, {
|
|
666
711
|
createdAt: 100,
|
|
667
712
|
});
|
|
668
713
|
|
|
@@ -678,17 +723,17 @@ describe("Timeline data assembly", () => {
|
|
|
678
723
|
expect.objectContaining({
|
|
679
724
|
post: expect.objectContaining({ id: root.id }),
|
|
680
725
|
hiddenBeforeCount: 0,
|
|
681
|
-
highlighted:
|
|
726
|
+
highlighted: false,
|
|
682
727
|
}),
|
|
683
728
|
expect.objectContaining({
|
|
684
729
|
post: expect.objectContaining({ id: uncollectedReply.id }),
|
|
685
730
|
hiddenBeforeCount: 0,
|
|
686
|
-
highlighted:
|
|
731
|
+
highlighted: true,
|
|
687
732
|
}),
|
|
688
733
|
]);
|
|
689
734
|
});
|
|
690
735
|
|
|
691
|
-
it("
|
|
736
|
+
it("deduplicates Threads selected through multiple collections", async () => {
|
|
692
737
|
const smart = await collectionService.create({
|
|
693
738
|
slug: "smart",
|
|
694
739
|
title: "Smart",
|
|
@@ -721,26 +766,15 @@ describe("Timeline data assembly", () => {
|
|
|
721
766
|
bodyMarkdown: "Second thread root",
|
|
722
767
|
});
|
|
723
768
|
|
|
724
|
-
await
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
postId: movieReply.id,
|
|
734
|
-
collectionId: movies.id,
|
|
735
|
-
createdAt: 200,
|
|
736
|
-
},
|
|
737
|
-
{
|
|
738
|
-
siteId: DEFAULT_TEST_SITE_ID,
|
|
739
|
-
postId: secondRoot.id,
|
|
740
|
-
collectionId: movies.id,
|
|
741
|
-
createdAt: 300,
|
|
742
|
-
},
|
|
743
|
-
]);
|
|
769
|
+
await collectionService.addThread(smart.id, firstRoot.id, {
|
|
770
|
+
createdAt: 100,
|
|
771
|
+
});
|
|
772
|
+
await collectionService.addThread(movies.id, firstRoot.id, {
|
|
773
|
+
createdAt: 200,
|
|
774
|
+
});
|
|
775
|
+
await collectionService.addThread(movies.id, secondRoot.id, {
|
|
776
|
+
createdAt: 300,
|
|
777
|
+
});
|
|
744
778
|
|
|
745
779
|
const result = await assembleCollectionTimeline(createTimelineContext(), {
|
|
746
780
|
collectionIds: [smart.id, movies.id],
|
|
@@ -760,11 +794,16 @@ describe("Timeline data assembly", () => {
|
|
|
760
794
|
expect.objectContaining({
|
|
761
795
|
post: expect.objectContaining({ id: smartReply.id }),
|
|
762
796
|
hiddenBeforeCount: 0,
|
|
763
|
-
highlighted:
|
|
797
|
+
highlighted: false,
|
|
798
|
+
}),
|
|
799
|
+
expect.objectContaining({
|
|
800
|
+
post: expect.objectContaining({ id: hiddenMiddleReply.id }),
|
|
801
|
+
hiddenBeforeCount: 0,
|
|
802
|
+
highlighted: false,
|
|
764
803
|
}),
|
|
765
804
|
expect.objectContaining({
|
|
766
805
|
post: expect.objectContaining({ id: movieReply.id }),
|
|
767
|
-
hiddenBeforeCount:
|
|
806
|
+
hiddenBeforeCount: 0,
|
|
768
807
|
highlighted: true,
|
|
769
808
|
}),
|
|
770
809
|
]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
TIMEZONES,
|
|
4
|
+
getTimeZoneOptions,
|
|
4
5
|
isSupportedTimeZone,
|
|
5
6
|
mapIanaToTimezone,
|
|
6
7
|
normalizeTimeZone,
|
|
@@ -69,8 +70,15 @@ describe("normalizeTimeZone", () => {
|
|
|
69
70
|
expect(normalizeTimeZone("London")).toBe("Europe/London");
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
it("
|
|
73
|
-
expect(normalizeTimeZone("
|
|
73
|
+
it("preserves runtime-supported zones instead of collapsing their rules", () => {
|
|
74
|
+
expect(normalizeTimeZone("America/Phoenix")).toBe("America/Phoenix");
|
|
75
|
+
expect(normalizeTimeZone("Australia/Darwin")).toBe("Australia/Darwin");
|
|
76
|
+
expect(normalizeTimeZone("Etc/GMT-8")).toBe("Etc/GMT-8");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("normalizes fixed offsets and UTC aliases through Intl", () => {
|
|
80
|
+
expect(normalizeTimeZone("+08")).toBe("+08:00");
|
|
81
|
+
expect(normalizeTimeZone("+08:00")).toBe("+08:00");
|
|
74
82
|
expect(normalizeTimeZone("Etc/UTC")).toBe("UTC");
|
|
75
83
|
});
|
|
76
84
|
|
|
@@ -81,9 +89,13 @@ describe("normalizeTimeZone", () => {
|
|
|
81
89
|
});
|
|
82
90
|
|
|
83
91
|
describe("isSupportedTimeZone", () => {
|
|
84
|
-
it("accepts
|
|
92
|
+
it("accepts runtime zones, fixed offsets, and legacy values", () => {
|
|
85
93
|
expect(isSupportedTimeZone("Asia/Shanghai")).toBe(true);
|
|
86
94
|
expect(isSupportedTimeZone("Asia/Calcutta")).toBe(true);
|
|
95
|
+
expect(isSupportedTimeZone("America/Phoenix")).toBe(true);
|
|
96
|
+
expect(isSupportedTimeZone("Australia/Darwin")).toBe(true);
|
|
97
|
+
expect(isSupportedTimeZone("Etc/GMT-8")).toBe(true);
|
|
98
|
+
expect(isSupportedTimeZone("+08:00")).toBe(true);
|
|
87
99
|
expect(isSupportedTimeZone("Beijing")).toBe(true);
|
|
88
100
|
});
|
|
89
101
|
|
|
@@ -92,3 +104,19 @@ describe("isSupportedTimeZone", () => {
|
|
|
92
104
|
expect(isSupportedTimeZone("Unknown/Zone")).toBe(false);
|
|
93
105
|
});
|
|
94
106
|
});
|
|
107
|
+
|
|
108
|
+
describe("getTimeZoneOptions", () => {
|
|
109
|
+
it("adds an unlisted current timezone without expanding the curated list", () => {
|
|
110
|
+
const options = getTimeZoneOptions("America/Phoenix");
|
|
111
|
+
|
|
112
|
+
expect(options).toHaveLength(TIMEZONES.length + 1);
|
|
113
|
+
expect(options.at(-1)).toMatchObject({
|
|
114
|
+
label: "America/Phoenix",
|
|
115
|
+
value: "America/Phoenix",
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("does not duplicate a curated timezone", () => {
|
|
120
|
+
expect(getTimeZoneOptions("Asia/Shanghai")).toBe(TIMEZONES);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
renderTiptapDocument,
|
|
4
|
+
renderTiptapDocumentAroundBoundary,
|
|
4
5
|
renderTiptapJson,
|
|
6
|
+
tryRenderTiptapJson,
|
|
5
7
|
trimTiptapBody,
|
|
6
8
|
} from "../tiptap-render.js";
|
|
7
9
|
|
|
@@ -68,7 +70,7 @@ describe("renderTiptapDocument", () => {
|
|
|
68
70
|
);
|
|
69
71
|
});
|
|
70
72
|
|
|
71
|
-
it("renders
|
|
73
|
+
it("renders canonical semantic endnotes without layout wrappers", () => {
|
|
72
74
|
const html = renderTiptapDocument(
|
|
73
75
|
doc(
|
|
74
76
|
p(text("Body copy"), {
|
|
@@ -81,16 +83,21 @@ describe("renderTiptapDocument", () => {
|
|
|
81
83
|
content: [p(text("Footnote body"))],
|
|
82
84
|
},
|
|
83
85
|
),
|
|
86
|
+
{ namespace: "pst_test" },
|
|
84
87
|
);
|
|
85
88
|
|
|
86
89
|
expect(html).toBe(
|
|
87
|
-
'<p>Body copy<
|
|
88
|
-
'<
|
|
89
|
-
'<
|
|
90
|
+
'<p>Body copy<sup class="footnote-ref">' +
|
|
91
|
+
'<a id="fnref-0sk9zhgh1dhm5-1-1" href="#fn-0sk9zhgh1dhm5-1" role="doc-noteref">1</a></sup></p>' +
|
|
92
|
+
'<section class="footnote-endnotes" role="doc-endnotes"><ol class="footnote-list">' +
|
|
93
|
+
'<li id="fn-0sk9zhgh1dhm5-1" class="footnote"><p>Footnote body <span class="footnote-backlinks">' +
|
|
94
|
+
'<a href="#fnref-0sk9zhgh1dhm5-1-1" class="footnote-backref" role="doc-backlink">↩︎</a>' +
|
|
95
|
+
"</span></p></li></ol></section>",
|
|
90
96
|
);
|
|
97
|
+
expect(html).not.toMatch(/<div|\sstyle=|\sdata-footnote-/);
|
|
91
98
|
});
|
|
92
99
|
|
|
93
|
-
it("renders footnote
|
|
100
|
+
it("renders a missing footnote definition without a dead link", () => {
|
|
94
101
|
const html = renderTiptapDocument(
|
|
95
102
|
doc(
|
|
96
103
|
p(text("Body copy"), {
|
|
@@ -100,11 +107,255 @@ describe("renderTiptapDocument", () => {
|
|
|
100
107
|
),
|
|
101
108
|
);
|
|
102
109
|
|
|
103
|
-
expect(html).toBe(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
expect(html).toBe('<p>Body copy<sup class="footnote-ref">1</sup></p>');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("uses a native list start when a missing definition creates a leading numbering gap", () => {
|
|
114
|
+
const html = renderTiptapDocument(
|
|
115
|
+
doc(
|
|
116
|
+
p(text("Missing"), {
|
|
117
|
+
type: "footnoteReference",
|
|
118
|
+
attrs: { label: "missing" },
|
|
119
|
+
}),
|
|
120
|
+
p(text("Defined"), {
|
|
121
|
+
type: "footnoteReference",
|
|
122
|
+
attrs: { label: "defined" },
|
|
123
|
+
}),
|
|
124
|
+
{
|
|
125
|
+
type: "footnoteDefinition",
|
|
126
|
+
attrs: { label: "defined" },
|
|
127
|
+
content: [p(text("Second definition"))],
|
|
128
|
+
},
|
|
129
|
+
),
|
|
107
130
|
);
|
|
131
|
+
|
|
132
|
+
expect(html).toContain('<sup class="footnote-ref">1</sup>');
|
|
133
|
+
expect(html).toContain('href="#fn-2" role="doc-noteref">2</a>');
|
|
134
|
+
expect(html).toContain('<ol class="footnote-list" start="2">');
|
|
135
|
+
expect(html).toContain('<li id="fn-2" class="footnote">');
|
|
136
|
+
expect(html).not.toContain('value="2"');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("uses a native list-item value only for an internal numbering gap", () => {
|
|
140
|
+
const html = renderTiptapDocument(
|
|
141
|
+
doc(
|
|
142
|
+
p({ type: "footnoteReference", attrs: { label: "one" } }),
|
|
143
|
+
p({ type: "footnoteReference", attrs: { label: "missing" } }),
|
|
144
|
+
p({ type: "footnoteReference", attrs: { label: "three" } }),
|
|
145
|
+
{
|
|
146
|
+
type: "footnoteDefinition",
|
|
147
|
+
attrs: { label: "one" },
|
|
148
|
+
content: [p(text("First definition"))],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: "footnoteDefinition",
|
|
152
|
+
attrs: { label: "three" },
|
|
153
|
+
content: [p(text("Third definition"))],
|
|
154
|
+
},
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(html).toContain('<ol class="footnote-list">');
|
|
159
|
+
expect(html).toContain('<li id="fn-1" class="footnote">');
|
|
160
|
+
expect(html).toContain('<li id="fn-3" class="footnote" value="3">');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("renders repeated references once with unique reciprocal backlinks", () => {
|
|
164
|
+
const html = renderTiptapDocument(
|
|
165
|
+
doc(
|
|
166
|
+
p(text("First"), {
|
|
167
|
+
type: "footnoteReference",
|
|
168
|
+
attrs: { label: "Note" },
|
|
169
|
+
}),
|
|
170
|
+
p(text("Second"), {
|
|
171
|
+
type: "footnoteReference",
|
|
172
|
+
attrs: { label: "note" },
|
|
173
|
+
}),
|
|
174
|
+
{
|
|
175
|
+
type: "footnoteDefinition",
|
|
176
|
+
attrs: { label: "NOTE" },
|
|
177
|
+
content: [p(text("Shared definition"))],
|
|
178
|
+
},
|
|
179
|
+
),
|
|
180
|
+
{ namespace: "pst_repeat" },
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(html.match(/role="doc-endnotes"/g)).toHaveLength(1);
|
|
184
|
+
expect(html.match(/<ol class="footnote-list">/g)).toHaveLength(1);
|
|
185
|
+
expect(html.match(/<li id="fn-/g)).toHaveLength(1);
|
|
186
|
+
expect(html).toContain('id="fnref-0r3u815ai35bm-1-1"');
|
|
187
|
+
expect(html).toContain('id="fnref-0r3u815ai35bm-1-2"');
|
|
188
|
+
expect(html.match(/href="#fn-0r3u815ai35bm-1"/g)).toHaveLength(2);
|
|
189
|
+
expect(html).toContain(
|
|
190
|
+
'href="#fnref-0r3u815ai35bm-1-1" class="footnote-backref" role="doc-backlink">↩︎1</a>',
|
|
191
|
+
);
|
|
192
|
+
expect(html).toContain(
|
|
193
|
+
'href="#fnref-0r3u815ai35bm-1-2" class="footnote-backref" role="doc-backlink">↩︎2</a>',
|
|
194
|
+
);
|
|
195
|
+
expect(html.indexOf("Shared definition")).toBeGreaterThan(
|
|
196
|
+
html.indexOf("Second"),
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("keeps all definitions in one native ordered list", () => {
|
|
201
|
+
const html = renderTiptapDocument(
|
|
202
|
+
doc(
|
|
203
|
+
p(text("First"), {
|
|
204
|
+
type: "footnoteReference",
|
|
205
|
+
attrs: { label: "first" },
|
|
206
|
+
}),
|
|
207
|
+
p(text("Second"), {
|
|
208
|
+
type: "footnoteReference",
|
|
209
|
+
attrs: { label: "second" },
|
|
210
|
+
}),
|
|
211
|
+
{
|
|
212
|
+
type: "footnoteDefinition",
|
|
213
|
+
attrs: { label: "first" },
|
|
214
|
+
content: [p(text("First definition"))],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: "footnoteDefinition",
|
|
218
|
+
attrs: { label: "second" },
|
|
219
|
+
content: [p(text("Second definition"))],
|
|
220
|
+
},
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(html.match(/<ol class="footnote-list">/g)).toHaveLength(1);
|
|
225
|
+
expect(html).toContain('<li id="fn-1" class="footnote">');
|
|
226
|
+
expect(html).toContain('<li id="fn-2" class="footnote">');
|
|
227
|
+
expect(html).not.toMatch(/\s(?:start|value|style)=/);
|
|
228
|
+
expect(html.indexOf("First definition")).toBeGreaterThan(
|
|
229
|
+
html.indexOf("Second"),
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("keeps rich footnote blocks inside a native endnote list item", () => {
|
|
234
|
+
const html = renderTiptapDocument(
|
|
235
|
+
doc(
|
|
236
|
+
p(text("Body"), {
|
|
237
|
+
type: "footnoteReference",
|
|
238
|
+
attrs: { label: "rich" },
|
|
239
|
+
}),
|
|
240
|
+
{
|
|
241
|
+
type: "footnoteDefinition",
|
|
242
|
+
attrs: { label: "rich" },
|
|
243
|
+
content: [
|
|
244
|
+
p(text("First paragraph")),
|
|
245
|
+
{
|
|
246
|
+
type: "bulletList",
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "listItem",
|
|
250
|
+
content: [p(text("List item"))],
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(html).toContain(
|
|
260
|
+
'<section class="footnote-endnotes" role="doc-endnotes"><ol class="footnote-list"',
|
|
261
|
+
);
|
|
262
|
+
expect(html).toContain('<li id="fn-1" class="footnote">');
|
|
263
|
+
expect(html).toContain(
|
|
264
|
+
'<p>First paragraph</p><ul><li><p>List item</p></li></ul><p class="footnote-backlinks"><a href="#fnref-1-1" class="footnote-backref" role="doc-backlink">↩︎</a></p>',
|
|
265
|
+
);
|
|
266
|
+
expect(html).not.toContain('<div class="footnote-body"');
|
|
267
|
+
expect(html).not.toContain('role="doc-footnote"');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("appends backlinks to the authored final paragraph of rich definitions", () => {
|
|
271
|
+
const html = renderTiptapDocument(
|
|
272
|
+
doc(p({ type: "footnoteReference", attrs: { label: "rich" } }), {
|
|
273
|
+
type: "footnoteDefinition",
|
|
274
|
+
attrs: { label: "rich" },
|
|
275
|
+
content: [
|
|
276
|
+
{
|
|
277
|
+
type: "bulletList",
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "listItem",
|
|
281
|
+
content: [p(text("List item"))],
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
p(text("Final paragraph")),
|
|
286
|
+
],
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(html).toContain(
|
|
291
|
+
'<ul><li><p>List item</p></li></ul><p>Final paragraph <span class="footnote-backlinks">',
|
|
292
|
+
);
|
|
293
|
+
expect(html).not.toContain('<p class="footnote-backlinks">');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("uses the render namespace to isolate documents sharing a page", () => {
|
|
297
|
+
const input = doc(p({ type: "footnoteReference", attrs: { label: "1" } }), {
|
|
298
|
+
type: "footnoteDefinition",
|
|
299
|
+
attrs: { label: "1" },
|
|
300
|
+
content: [p(text("Definition"))],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const first = renderTiptapDocument(input, { namespace: "pst_first" });
|
|
304
|
+
const second = renderTiptapDocument(input, { namespace: "pst_second" });
|
|
305
|
+
|
|
306
|
+
expect(first).toContain('id="fn-3t66v0e9xc2qx-1"');
|
|
307
|
+
expect(second).toContain('id="fn-2b338ib1g2med-1"');
|
|
308
|
+
expect(first).not.toContain("2b338ib1g2med");
|
|
309
|
+
expect(second).not.toContain("3t66v0e9xc2qx");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("keeps immutable entity namespaces compact in public fragment IDs", () => {
|
|
313
|
+
const input = doc(p({ type: "footnoteReference", attrs: { label: "1" } }), {
|
|
314
|
+
type: "footnoteDefinition",
|
|
315
|
+
attrs: { label: "1" },
|
|
316
|
+
content: [p(text("Definition"))],
|
|
317
|
+
});
|
|
318
|
+
const namespace = "pst_01kxwe4tkwfqfsrwcgqgeg1b7k";
|
|
319
|
+
const html = renderTiptapDocument(input, { namespace });
|
|
320
|
+
const referenceId = html.match(/<a id="([^"]+)"/)?.[1];
|
|
321
|
+
const definitionId = html.match(/<li id="([^"]+)"/)?.[1];
|
|
322
|
+
|
|
323
|
+
expect(referenceId).toBe("fnref-0nj7nw33xdf9h-1-1");
|
|
324
|
+
expect(definitionId).toBe("fn-0nj7nw33xdf9h-1");
|
|
325
|
+
expect(referenceId?.length).toBeLessThan(30);
|
|
326
|
+
expect(html).not.toContain(namespace);
|
|
327
|
+
expect(html).toContain(`href="#${definitionId}"`);
|
|
328
|
+
expect(html).toContain(`href="#${referenceId}"`);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("splits at a source boundary using the full repeated-footnote plan", () => {
|
|
332
|
+
const input = doc(
|
|
333
|
+
p(text("First"), {
|
|
334
|
+
type: "footnoteReference",
|
|
335
|
+
attrs: { label: "shared" },
|
|
336
|
+
}),
|
|
337
|
+
p(text("Second"), {
|
|
338
|
+
type: "footnoteReference",
|
|
339
|
+
attrs: { label: "shared" },
|
|
340
|
+
}),
|
|
341
|
+
{
|
|
342
|
+
type: "footnoteDefinition",
|
|
343
|
+
attrs: { label: "shared" },
|
|
344
|
+
content: [p(text("Definition"))],
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
const options = { namespace: "pst_split" };
|
|
348
|
+
const full = renderTiptapDocument(input, options);
|
|
349
|
+
const split = renderTiptapDocumentAroundBoundary(input, 1, options);
|
|
350
|
+
|
|
351
|
+
expect(split).not.toBeNull();
|
|
352
|
+
expect(split!.beforeHtml + split!.afterHtml).toBe(full);
|
|
353
|
+
expect(split!.beforeHtml).toContain("fnref-2q7yplng9am9n-1-1");
|
|
354
|
+
expect(split!.beforeHtml).not.toContain("footnote-document");
|
|
355
|
+
expect(split!.beforeHtml).not.toContain('role="doc-endnotes"');
|
|
356
|
+
expect(split!.afterHtml).toContain('id="fnref-2q7yplng9am9n-1-2"');
|
|
357
|
+
expect(split!.afterHtml).toContain('role="doc-endnotes"');
|
|
358
|
+
expect(split!.afterHtml.endsWith("</section>")).toBe(true);
|
|
108
359
|
});
|
|
109
360
|
|
|
110
361
|
it("renders code blocks as escaped text, not nested inline markup", () => {
|
|
@@ -141,6 +392,14 @@ describe("renderTiptapJson", () => {
|
|
|
141
392
|
it("returns an empty string for non-doc JSON", () => {
|
|
142
393
|
expect(renderTiptapDocument({ type: "paragraph" })).toBe("");
|
|
143
394
|
});
|
|
395
|
+
|
|
396
|
+
it("distinguishes invalid JSON from a valid empty document", () => {
|
|
397
|
+
expect(tryRenderTiptapJson("not json")).toMatchObject({ ok: false });
|
|
398
|
+
expect(tryRenderTiptapJson(JSON.stringify(doc()))).toEqual({
|
|
399
|
+
ok: true,
|
|
400
|
+
html: "",
|
|
401
|
+
});
|
|
402
|
+
});
|
|
144
403
|
});
|
|
145
404
|
|
|
146
405
|
describe("trimTiptapBody", () => {
|