@jant/core 0.6.16 → 0.7.1
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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +749 -220
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +19 -61
- package/bin/commands/site/snapshot/import.js +23 -24
- package/bin/lib/d1-query.js +92 -18
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +94 -29
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +37 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
- package/dist/client/_assets/client-DAhoqPdr.css +2 -0
- package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
- package/dist/client/_assets/client-knSEyUJO.js +295 -0
- package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
- package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
- package/dist/github-sync-Gw4orAHk.js +8398 -0
- package/dist/index.js +2 -5
- package/dist/node.js +11 -9
- package/package.json +15 -5
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +390 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +842 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +86 -22
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +137 -108
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +622 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +555 -73
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +601 -119
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +614 -132
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +131 -3
- package/src/lib/__tests__/schemas.test.ts +69 -5
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +439 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +62 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +111 -6
- package/src/lib/schemas.ts +251 -15
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +43 -2
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +159 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +53 -25
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +75 -0
- package/src/node/index.ts +3 -0
- package/src/node/request-handler.ts +3 -1
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +6 -3
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +161 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +44 -7
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +9 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +714 -111
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +487 -379
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +43 -18
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +228 -45
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +22 -5
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +67 -1
- package/src/runtime/readiness.ts +15 -0
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +236 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +120 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +408 -39
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +29 -24
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +103 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +13 -2
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +880 -72
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +22 -2
- package/src/services/media.ts +22 -7
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +254 -6
- package/src/services/post.ts +1234 -190
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +27 -14
- package/src/services/site.ts +160 -32
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1090 -7216
- package/src/types/app-context.ts +23 -0
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +74 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +277 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/export-Dl5KCiEs.js +0 -5562
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/client/tiptap/exitable-marks.ts +0 -73
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Extension,
|
|
3
3
|
Node,
|
|
4
|
+
getSchema,
|
|
4
5
|
type AnyExtension,
|
|
5
6
|
type Extensions,
|
|
6
7
|
type JSONContent,
|
|
7
8
|
} from "@tiptap/core";
|
|
9
|
+
import { Fragment, type Schema } from "@tiptap/pm/model";
|
|
8
10
|
import { MarkdownManager } from "@tiptap/markdown";
|
|
9
11
|
import CodeBlock from "@tiptap/extension-code-block";
|
|
12
|
+
import { OrderedList } from "@tiptap/extension-list";
|
|
13
|
+
import HardBreak from "@tiptap/extension-hard-break";
|
|
14
|
+
import Paragraph from "@tiptap/extension-paragraph";
|
|
15
|
+
import Bold from "@tiptap/extension-bold";
|
|
16
|
+
import Italic from "@tiptap/extension-italic";
|
|
17
|
+
import Strike from "@tiptap/extension-strike";
|
|
10
18
|
import Link from "@tiptap/extension-link";
|
|
11
19
|
import StarterKit from "@tiptap/starter-kit";
|
|
12
20
|
import {
|
|
@@ -224,6 +232,333 @@ const SemanticLink = Link.extend({
|
|
|
224
232
|
clearable: false,
|
|
225
233
|
});
|
|
226
234
|
|
|
235
|
+
const LINE_START_BLOCK_SYNTAX: ReadonlyArray<[RegExp, string]> = [
|
|
236
|
+
// `1986. A good year`, `2) Second`: an ordered-list marker
|
|
237
|
+
[/^([ \t]{0,3})(\d{1,9})([.)])(?=[ \t]|$)/, "$1$2\\$3"],
|
|
238
|
+
// `- note`, `+ note`: a bullet-list marker (`*` is escaped inline already)
|
|
239
|
+
[/^([ \t]{0,3})([-+])(?=[ \t]|$)/, "$1\\$2"],
|
|
240
|
+
// `# note`: an ATX heading
|
|
241
|
+
[/^([ \t]{0,3})(#{1,6})(?=[ \t]|$)/, "$1\\$2"],
|
|
242
|
+
// `---`, `===`: a thematic break or a setext underline for the line above
|
|
243
|
+
[/^([ \t]{0,3})([-=])(?=[-= \t]*$)/, "$1\\$2"],
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Backslash-escape block syntax that a paragraph line happens to open with.
|
|
248
|
+
*
|
|
249
|
+
* Text nodes only get inline escaping, so a paragraph reading `1. Pony`, or a
|
|
250
|
+
* hard break followed by `- note`, came back from the Markdown as a list.
|
|
251
|
+
*
|
|
252
|
+
* @param markdown - A paragraph's rendered Markdown
|
|
253
|
+
* @returns The same Markdown with each line's leading block syntax escaped
|
|
254
|
+
* @example
|
|
255
|
+
* escapeLineStartBlockSyntax("Update: \n1. Pony"); // "Update: \n1\\. Pony"
|
|
256
|
+
*/
|
|
257
|
+
function escapeLineStartBlockSyntax(markdown: string): string {
|
|
258
|
+
return markdown
|
|
259
|
+
.split("\n")
|
|
260
|
+
.map((line) =>
|
|
261
|
+
LINE_START_BLOCK_SYNTAX.reduce(
|
|
262
|
+
(escaped, [pattern, replacement]) =>
|
|
263
|
+
escaped.replace(pattern, replacement),
|
|
264
|
+
line,
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The HTML tag each emphasis mark falls back to. */
|
|
271
|
+
const HTML_EMPHASIS_TAGS = {
|
|
272
|
+
bold: "strong",
|
|
273
|
+
italic: "em",
|
|
274
|
+
strike: "s",
|
|
275
|
+
} as const;
|
|
276
|
+
|
|
277
|
+
type EmphasisMarkType = keyof typeof HTML_EMPHASIS_TAGS;
|
|
278
|
+
|
|
279
|
+
const EMPHASIS_MARK_TYPES = Object.keys(
|
|
280
|
+
HTML_EMPHASIS_TAGS,
|
|
281
|
+
) as EmphasisMarkType[];
|
|
282
|
+
|
|
283
|
+
/** Marks that write their own delimiter characters around the text. */
|
|
284
|
+
const DELIMITED_MARK_TYPES = new Set<string>([
|
|
285
|
+
...EMPHASIS_MARK_TYPES,
|
|
286
|
+
"code",
|
|
287
|
+
"link",
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Serialization-only mark attribute: write this run with HTML tags. It is
|
|
292
|
+
* set by `markEmphasisDelimiters` on a copy of the document and never stored.
|
|
293
|
+
*/
|
|
294
|
+
const HTML_EMPHASIS_ATTR = "markdownAsHtml";
|
|
295
|
+
|
|
296
|
+
type FlankingClass = "space" | "punctuation" | "other";
|
|
297
|
+
|
|
298
|
+
function classifyFlankingChar(char: string | undefined): FlankingClass {
|
|
299
|
+
if (char === undefined || /\s/u.test(char)) return "space";
|
|
300
|
+
return /[\p{P}\p{S}]/u.test(char) ? "punctuation" : "other";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function hasMark(node: JSONContent | undefined, type: string): boolean {
|
|
304
|
+
return node?.marks?.some((mark) => mark.type === type) ?? false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Whether a `**`, `*`, or `~~` at one end of a run can open or close it.
|
|
309
|
+
*
|
|
310
|
+
* CommonMark's flanking rule, which GFM strikethrough shares: the character
|
|
311
|
+
* on the text side must not be whitespace, and when it is punctuation, the
|
|
312
|
+
* character on the outside must be whitespace or punctuation too. The rule is
|
|
313
|
+
* symmetric, so one check covers the opening and the closing delimiter.
|
|
314
|
+
*
|
|
315
|
+
* @param nodes - The inline nodes of one block
|
|
316
|
+
* @param index - The run's first node (`side: "start"`) or last (`"end"`)
|
|
317
|
+
* @param side - Which end of the run
|
|
318
|
+
* @param markType - The run's mark
|
|
319
|
+
* @returns True when Markdown delimiters work at this end
|
|
320
|
+
*/
|
|
321
|
+
function delimiterFlanks(
|
|
322
|
+
nodes: JSONContent[],
|
|
323
|
+
index: number,
|
|
324
|
+
side: "start" | "end",
|
|
325
|
+
markType: string,
|
|
326
|
+
): boolean {
|
|
327
|
+
const node = nodes[index];
|
|
328
|
+
const neighbor = nodes[side === "start" ? index - 1 : index + 1];
|
|
329
|
+
const chars = [...(node?.text ?? "")];
|
|
330
|
+
const edge = side === "start" ? chars : chars.slice().reverse();
|
|
331
|
+
const innerChar = edge.find((char) => !/\s/u.test(char));
|
|
332
|
+
|
|
333
|
+
// Another mark opening or closing at the same edge writes its delimiter
|
|
334
|
+
// between ours and the text; it counts as punctuation.
|
|
335
|
+
const sharesEdge =
|
|
336
|
+
node?.marks?.some(
|
|
337
|
+
(mark) =>
|
|
338
|
+
mark.type !== markType &&
|
|
339
|
+
DELIMITED_MARK_TYPES.has(mark.type) &&
|
|
340
|
+
!hasMark(neighbor, mark.type),
|
|
341
|
+
) ?? false;
|
|
342
|
+
const inner = sharesEdge ? "punctuation" : classifyFlankingChar(innerChar);
|
|
343
|
+
|
|
344
|
+
// The serializer moves whitespace at a run's edge outside the delimiter.
|
|
345
|
+
let outer: FlankingClass;
|
|
346
|
+
if (edge[0] !== undefined && /\s/u.test(edge[0])) outer = "space";
|
|
347
|
+
else if (!neighbor || neighbor.type === "hardBreak") outer = "space";
|
|
348
|
+
else if (neighbor.type === "text") {
|
|
349
|
+
const neighborChars = [...(neighbor.text ?? "")];
|
|
350
|
+
outer = classifyFlankingChar(
|
|
351
|
+
side === "start" ? neighborChars.at(-1) : neighborChars[0],
|
|
352
|
+
);
|
|
353
|
+
} else outer = "punctuation"; // ``, `[^1]`
|
|
354
|
+
|
|
355
|
+
return inner !== "space" && (inner !== "punctuation" || outer !== "other");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function markEmphasisRuns(nodes: JSONContent[]): JSONContent[] {
|
|
359
|
+
const result = nodes.map((node) => ({ ...node }));
|
|
360
|
+
|
|
361
|
+
for (const markType of EMPHASIS_MARK_TYPES) {
|
|
362
|
+
let start = 0;
|
|
363
|
+
while (start < result.length) {
|
|
364
|
+
if (result[start]?.type !== "text" || !hasMark(result[start], markType)) {
|
|
365
|
+
start += 1;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The serializer closes marks around any non-text node, so a run is
|
|
370
|
+
// consecutive text nodes.
|
|
371
|
+
let end = start;
|
|
372
|
+
while (
|
|
373
|
+
result[end + 1]?.type === "text" &&
|
|
374
|
+
hasMark(result[end + 1], markType)
|
|
375
|
+
) {
|
|
376
|
+
end += 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (
|
|
380
|
+
!delimiterFlanks(result, start, "start", markType) ||
|
|
381
|
+
!delimiterFlanks(result, end, "end", markType)
|
|
382
|
+
) {
|
|
383
|
+
for (let index = start; index <= end; index += 1) {
|
|
384
|
+
const node = result[index] as JSONContent;
|
|
385
|
+
node.marks = node.marks?.map((mark) =>
|
|
386
|
+
mark.type === markType
|
|
387
|
+
? {
|
|
388
|
+
...mark,
|
|
389
|
+
attrs: { ...mark.attrs, [HTML_EMPHASIS_ATTR]: true },
|
|
390
|
+
}
|
|
391
|
+
: mark,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
start = end + 1;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Flag the emphasis runs whose Markdown delimiters a CommonMark parser would
|
|
405
|
+
* leave as literal characters, so they serialize as HTML tags instead.
|
|
406
|
+
*
|
|
407
|
+
* Chinese and Japanese put no space around punctuation, so `**说话。**来的人`
|
|
408
|
+
* is common, and neither Hugo (goldmark) nor Jant's own parser reads the
|
|
409
|
+
* closing `**` after `。` followed by `来`. `<strong>…</strong>` reads the
|
|
410
|
+
* same in both, and the rest of the Markdown stays as it was.
|
|
411
|
+
*
|
|
412
|
+
* @param node - A TipTap document or descendant
|
|
413
|
+
* @returns A copy with unflankable runs flagged
|
|
414
|
+
* @example
|
|
415
|
+
* markEmphasisDelimiters(doc); // bold "说话。" before "来" gets the flag
|
|
416
|
+
*/
|
|
417
|
+
function markEmphasisDelimiters(node: JSONContent): JSONContent {
|
|
418
|
+
if (!node.content || node.type === "codeBlock") return node;
|
|
419
|
+
|
|
420
|
+
const content = node.content.map(markEmphasisDelimiters);
|
|
421
|
+
const hasInline = content.some(
|
|
422
|
+
(child) => child.type === "text" || child.type === "hardBreak",
|
|
423
|
+
);
|
|
424
|
+
return { ...node, content: hasInline ? markEmphasisRuns(content) : content };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderEmphasis(
|
|
428
|
+
node: JSONContent,
|
|
429
|
+
content: string,
|
|
430
|
+
markType: EmphasisMarkType,
|
|
431
|
+
delimiter: string,
|
|
432
|
+
): string {
|
|
433
|
+
if (!node.attrs?.[HTML_EMPHASIS_ATTR]) {
|
|
434
|
+
return `${delimiter}${content}${delimiter}`;
|
|
435
|
+
}
|
|
436
|
+
const tag = HTML_EMPHASIS_TAGS[markType];
|
|
437
|
+
return `<${tag}>${content}</${tag}>`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const MarkdownBold = Bold.extend({
|
|
441
|
+
renderMarkdown(node, helpers) {
|
|
442
|
+
return renderEmphasis(node, helpers.renderChildren(node), "bold", "**");
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const MarkdownItalic = Italic.extend({
|
|
447
|
+
renderMarkdown(node, helpers) {
|
|
448
|
+
return renderEmphasis(node, helpers.renderChildren(node), "italic", "*");
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const MarkdownStrike = Strike.extend({
|
|
453
|
+
renderMarkdown(node, helpers) {
|
|
454
|
+
return renderEmphasis(node, helpers.renderChildren(node), "strike", "~~");
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/** Marked token for each HTML emphasis tag the parser accepts. */
|
|
459
|
+
const HTML_EMPHASIS_TOKEN_TYPES: Record<string, "strong" | "em" | "del"> = {
|
|
460
|
+
strong: "strong",
|
|
461
|
+
b: "strong",
|
|
462
|
+
em: "em",
|
|
463
|
+
i: "em",
|
|
464
|
+
s: "del",
|
|
465
|
+
del: "del",
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const HTML_EMPHASIS_OPEN_PATTERN = /<(?:strong|b|em|i|s|del)>/i;
|
|
469
|
+
const HTML_EMPHASIS_PATTERN = /^<(strong|b|em|i|s|del)>([\s\S]*?)<\/\1>/i;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Reads the HTML tags `markEmphasisDelimiters` writes back as marks.
|
|
473
|
+
*
|
|
474
|
+
* Only bare tags: `<strong onclick=…>` and every other tag stay text, as
|
|
475
|
+
* inline HTML always has. The content between the tags is Markdown.
|
|
476
|
+
*/
|
|
477
|
+
const MarkdownHtmlEmphasis = Extension.create({
|
|
478
|
+
name: "markdownHtmlEmphasis",
|
|
479
|
+
|
|
480
|
+
markdownTokenizer: {
|
|
481
|
+
name: "htmlEmphasis",
|
|
482
|
+
level: "inline",
|
|
483
|
+
start(src: string) {
|
|
484
|
+
return src.search(HTML_EMPHASIS_OPEN_PATTERN);
|
|
485
|
+
},
|
|
486
|
+
tokenize(src: string, _tokens, helpers) {
|
|
487
|
+
const match = HTML_EMPHASIS_PATTERN.exec(src);
|
|
488
|
+
if (!match) return undefined;
|
|
489
|
+
|
|
490
|
+
const [raw, tag = "", text = ""] = match;
|
|
491
|
+
const type = HTML_EMPHASIS_TOKEN_TYPES[tag.toLowerCase()];
|
|
492
|
+
if (!type) return undefined;
|
|
493
|
+
return { type, raw, text, tokens: helpers.inlineTokens(text) };
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Hard breaks as two trailing spaces, except where that line would be blank.
|
|
500
|
+
*
|
|
501
|
+
* A break at the start of a paragraph, or right after another break, puts
|
|
502
|
+
* the spaces on a line of their own. A line of spaces is blank in Markdown:
|
|
503
|
+
* it ended the paragraph, and `- ` left a list item empty with the text
|
|
504
|
+
* after it outside the list. The backslash form keeps something on the line.
|
|
505
|
+
*/
|
|
506
|
+
const MarkdownHardBreak = HardBreak.extend({
|
|
507
|
+
renderMarkdown(_node, _helpers, context) {
|
|
508
|
+
const previous = context?.previousNode;
|
|
509
|
+
return !previous || previous.type === "hardBreak" ? "\\\n" : " \n";
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const renderParagraphMarkdown = Paragraph.config.renderMarkdown;
|
|
514
|
+
|
|
515
|
+
const MarkdownParagraph = Paragraph.extend({
|
|
516
|
+
renderMarkdown(node, helpers, context) {
|
|
517
|
+
const rendered =
|
|
518
|
+
renderParagraphMarkdown?.call(this, node, helpers, context) ?? "";
|
|
519
|
+
return escapeLineStartBlockSyntax(rendered);
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Ordered lists with CommonMark markers: digits only.
|
|
525
|
+
*
|
|
526
|
+
* Tiptap's ordered list also reads letters and roman numerals as markers
|
|
527
|
+
* (`a.`, `IV.`, anything of one or two letters), so a line such as
|
|
528
|
+
* `PS. 补充一句` or `Mr. Smith went` became a list item and lost its first
|
|
529
|
+
* word. Its tokenizer also measured a marker's width without the `.`, which
|
|
530
|
+
* left one stray space on every line of a code block inside a list item.
|
|
531
|
+
*
|
|
532
|
+
* Jant's Markdown is CommonMark plus GFM (docs/internal/markdown-contract.md),
|
|
533
|
+
* the same dialect Hugo reads in an export, so list tokenizing goes back to
|
|
534
|
+
* marked: a tokenizer that never matches leaves the built-in one in charge,
|
|
535
|
+
* and marked's list items go to `listItem` the way bullet lists' do. Tiptap's
|
|
536
|
+
* plain-text paste plugin goes too; it applied the same markers, and the
|
|
537
|
+
* editors already parse pasted plain text as Markdown (`MarkdownClipboard`).
|
|
538
|
+
*/
|
|
539
|
+
const CommonMarkOrderedList = OrderedList.extend({
|
|
540
|
+
markdownTokenizer: {
|
|
541
|
+
name: "orderedList",
|
|
542
|
+
level: "block",
|
|
543
|
+
start: () => -1,
|
|
544
|
+
tokenize: () => undefined,
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
parseMarkdown: (token, helpers) => {
|
|
548
|
+
if (token.type !== "list" || !token.ordered) return [];
|
|
549
|
+
const start = typeof token.start === "number" ? token.start : 1;
|
|
550
|
+
return {
|
|
551
|
+
type: "orderedList",
|
|
552
|
+
...(start === 1 ? {} : { attrs: { start } }),
|
|
553
|
+
content: token.items ? helpers.parseChildren(token.items) : [],
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
addProseMirrorPlugins() {
|
|
558
|
+
return [];
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
|
|
227
562
|
const MarkdownFigureImageSupport = Extension.create({
|
|
228
563
|
name: "markdownFigureImageSupport",
|
|
229
564
|
|
|
@@ -824,8 +1159,21 @@ export function createMarkdownContentExtensions(
|
|
|
824
1159
|
heading: { levels: [1, 2, 3] },
|
|
825
1160
|
link: false,
|
|
826
1161
|
codeBlock: false,
|
|
1162
|
+
orderedList: false,
|
|
1163
|
+
paragraph: false,
|
|
1164
|
+
hardBreak: false,
|
|
1165
|
+
bold: false,
|
|
1166
|
+
italic: false,
|
|
1167
|
+
strike: false,
|
|
827
1168
|
trailingNode: { notAfter: ["footnoteDefinition"] },
|
|
828
1169
|
}),
|
|
1170
|
+
MarkdownParagraph,
|
|
1171
|
+
MarkdownHardBreak,
|
|
1172
|
+
MarkdownBold,
|
|
1173
|
+
MarkdownItalic,
|
|
1174
|
+
MarkdownStrike,
|
|
1175
|
+
MarkdownHtmlEmphasis,
|
|
1176
|
+
CommonMarkOrderedList,
|
|
829
1177
|
SemanticLink.configure({
|
|
830
1178
|
openOnClick: false,
|
|
831
1179
|
autolink: false,
|
|
@@ -1050,12 +1398,54 @@ export function getMarkdownManager(): MarkdownManager {
|
|
|
1050
1398
|
return sharedMarkdownManager;
|
|
1051
1399
|
}
|
|
1052
1400
|
|
|
1401
|
+
let sharedContentSchema: Schema | null = null;
|
|
1402
|
+
|
|
1403
|
+
function getContentSchema(): Schema {
|
|
1404
|
+
sharedContentSchema ??= getSchema(createMarkdownContentExtensions());
|
|
1405
|
+
return sharedContentSchema;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Fill in the children a node's schema requires but the document left out.
|
|
1410
|
+
*
|
|
1411
|
+
* A list item must hold a paragraph, a blockquote a block, a doc a block.
|
|
1412
|
+
* Documents that skipped one (an empty `1. ` item from an older Markdown
|
|
1413
|
+
* parser, or JSON posted through the API) made the Markdown serializer throw.
|
|
1414
|
+
* Each empty node that cannot be empty gets what `createAndFill` would give it.
|
|
1415
|
+
*
|
|
1416
|
+
* @param node - A TipTap document or descendant
|
|
1417
|
+
* @returns A copy whose empty required containers are filled
|
|
1418
|
+
* @example
|
|
1419
|
+
* fillRequiredContent({ type: "listItem", content: [] });
|
|
1420
|
+
* // { type: "listItem", content: [{ type: "paragraph" }] }
|
|
1421
|
+
*/
|
|
1422
|
+
export function fillRequiredContent(node: JSONContent): JSONContent {
|
|
1423
|
+
const nodeType = node.type ? getContentSchema().nodes[node.type] : undefined;
|
|
1424
|
+
if (!nodeType || nodeType.isLeaf) return node;
|
|
1425
|
+
|
|
1426
|
+
const content = node.content?.map(fillRequiredContent);
|
|
1427
|
+
if ((content?.length ?? 0) === 0 && !nodeType.contentMatch.validEnd) {
|
|
1428
|
+
const filled = nodeType.contentMatch.fillBefore(Fragment.empty, true);
|
|
1429
|
+
if (filled) {
|
|
1430
|
+
return { ...node, content: filled.toJSON() as JSONContent[] };
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
return content ? { ...node, content } : node;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1053
1437
|
export function parseMarkdownDocument(markdown: string): JSONContent {
|
|
1054
|
-
return
|
|
1438
|
+
return fillRequiredContent(
|
|
1439
|
+
normalizeMarkdownDocument(getMarkdownManager().parse(markdown)),
|
|
1440
|
+
);
|
|
1055
1441
|
}
|
|
1056
1442
|
|
|
1057
1443
|
export function serializeMarkdownDocument(doc: JSONContent): string {
|
|
1058
1444
|
return expandCodeBlockFences(
|
|
1059
|
-
getMarkdownManager().serialize(
|
|
1445
|
+
getMarkdownManager().serialize(
|
|
1446
|
+
normalizeFootnoteArtifacts(
|
|
1447
|
+
markEmphasisDelimiters(fillRequiredContent(doc)),
|
|
1448
|
+
),
|
|
1449
|
+
),
|
|
1060
1450
|
);
|
|
1061
1451
|
}
|
package/src/lib/media-helpers.ts
CHANGED
|
@@ -58,9 +58,21 @@ export function buildMediaMap(
|
|
|
58
58
|
fit: "scale-down",
|
|
59
59
|
})
|
|
60
60
|
: mediaUrl;
|
|
61
|
-
|
|
61
|
+
// A poster is a still frame at whatever resolution the source video
|
|
62
|
+
// had, so it needs the same transform as any other image — without it
|
|
63
|
+
// a 4K clip puts a multi-megabyte PNG in the feed. Matches the limits
|
|
64
|
+
// `toMediaView` uses for the same field.
|
|
65
|
+
const posterRawUrl = m.posterKey
|
|
62
66
|
? getMediaUrl(m.posterKey, publicUrl, sitePathPrefix)
|
|
63
67
|
: null;
|
|
68
|
+
const posterUrl = posterRawUrl
|
|
69
|
+
? getImageUrl(posterRawUrl, imageTransformUrl, {
|
|
70
|
+
width: 640,
|
|
71
|
+
quality: 80,
|
|
72
|
+
format: "auto",
|
|
73
|
+
fit: "scale-down",
|
|
74
|
+
})
|
|
75
|
+
: null;
|
|
64
76
|
return {
|
|
65
77
|
id: m.id,
|
|
66
78
|
url: mediaUrl,
|
package/src/lib/navigation.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Context } from "hono";
|
|
8
8
|
import type { Collection, NavItem, NavItemView } from "../types.js";
|
|
9
|
+
import { isFeedNavKey } from "../types/constants.js";
|
|
9
10
|
import { toNavItemViews } from "./view.js";
|
|
11
|
+
import { languageScopeBasePath, viewBasePath } from "./view-language.js";
|
|
10
12
|
import { render as renderMarkdown, toPlainText } from "./markdown.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -15,7 +17,15 @@ import { render as renderMarkdown, toPlainText } from "./markdown.js";
|
|
|
15
17
|
export interface NavigationData {
|
|
16
18
|
links: NavItemView[];
|
|
17
19
|
currentPath: string;
|
|
20
|
+
/** Deployment path prefix, such as `/blog`. */
|
|
18
21
|
sitePathPrefix: string;
|
|
22
|
+
/**
|
|
23
|
+
* Public path prefix for reader-facing surfaces. Equals `sitePathPrefix`
|
|
24
|
+
* outside a language view, and carries the language prefix inside one
|
|
25
|
+
* (`/blog/en`). Pass this — not `sitePathPrefix` — to anything building
|
|
26
|
+
* links to the home page, archive, search, feeds or collections.
|
|
27
|
+
*/
|
|
28
|
+
basePath: string;
|
|
19
29
|
siteName: string;
|
|
20
30
|
/** Plain-text description for meta tags and RSS/Atom feeds */
|
|
21
31
|
siteDescription: string;
|
|
@@ -23,11 +33,37 @@ export interface NavigationData {
|
|
|
23
33
|
siteDescriptionHtml?: string;
|
|
24
34
|
isAuthenticated: boolean;
|
|
25
35
|
collections: Collection[];
|
|
26
|
-
|
|
36
|
+
/** Header/drawer mark, already sized down — not the canonical asset. */
|
|
37
|
+
siteAvatarThumbUrl?: string;
|
|
27
38
|
showHeaderAvatar?: boolean;
|
|
28
39
|
siteFooterHtml?: string;
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Collect the collections and smart collections already placed in navigation.
|
|
44
|
+
*
|
|
45
|
+
* The directory offers both kinds the same "Add to Navigation" action, so it
|
|
46
|
+
* needs one list to check against; TypeID prefixes (`col_`, `smc_`) keep the
|
|
47
|
+
* two apart without a second field.
|
|
48
|
+
*
|
|
49
|
+
* @param links - Navigation items as rendered for this request
|
|
50
|
+
* @returns TypeIDs of every collection and smart collection in navigation
|
|
51
|
+
* @example
|
|
52
|
+
* const placed = collectNavigationCollectionIds(navData.links);
|
|
53
|
+
* // ["col_01abc", "smc_01xyz"]
|
|
54
|
+
*/
|
|
55
|
+
export function collectNavigationCollectionIds(links: NavItemView[]): string[] {
|
|
56
|
+
return links.flatMap((item) => {
|
|
57
|
+
if (item.type === "collection" && item.collectionId) {
|
|
58
|
+
return [item.collectionId];
|
|
59
|
+
}
|
|
60
|
+
if (item.type === "smart_collection" && item.smartCollectionId) {
|
|
61
|
+
return [item.smartCollectionId];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
/**
|
|
32
68
|
* Fetch navigation data for public pages.
|
|
33
69
|
*
|
|
@@ -49,20 +85,45 @@ export interface NavigationData {
|
|
|
49
85
|
*/
|
|
50
86
|
export async function getNavigationData(
|
|
51
87
|
c: Context,
|
|
52
|
-
options?: {
|
|
88
|
+
options?: {
|
|
89
|
+
preloadedItems?: NavItem[];
|
|
90
|
+
includeCollections?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Content language whose view the page's chrome should live in. Post
|
|
93
|
+
* pages pass their post's language here: their URLs carry no language
|
|
94
|
+
* prefix, so `viewLang` is never set on them, yet a Japanese post's
|
|
95
|
+
* logo and nav should lead to `/ja`, not to the primary view.
|
|
96
|
+
*/
|
|
97
|
+
languageScope?: string | null;
|
|
98
|
+
},
|
|
53
99
|
): Promise<NavigationData> {
|
|
100
|
+
const appConfig = c.var.appConfig;
|
|
101
|
+
const langBase =
|
|
102
|
+
options?.languageScope !== undefined
|
|
103
|
+
? languageScopeBasePath(c, options.languageScope)
|
|
104
|
+
: viewBasePath(c);
|
|
105
|
+
// An empty prefix means the primary language's view, whose nav items already
|
|
106
|
+
// point where they should — so the whole primary-language site, multilingual
|
|
107
|
+
// or not, never pays for the translation lookup.
|
|
108
|
+
const scopeLanguage = langBase
|
|
109
|
+
? (options?.languageScope ?? c.var.viewLang ?? null)
|
|
110
|
+
: null;
|
|
111
|
+
|
|
54
112
|
// Callers that already fetched nav items can pass them in to avoid a
|
|
55
113
|
// redundant DB round-trip.
|
|
56
114
|
const savedItems =
|
|
57
|
-
options?.preloadedItems ??
|
|
115
|
+
options?.preloadedItems ??
|
|
116
|
+
(await c.var.services.navItems.list({ language: scopeLanguage }));
|
|
58
117
|
const currentPath = c.var.publicPath;
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
118
|
+
// Keep the saved feed items untouched so their placement and custom labels
|
|
119
|
+
// come back when feeds are re-enabled. Only the rendered projection is
|
|
120
|
+
// filtered. Both entries go: `rss` addresses a feed that is off, and
|
|
121
|
+
// `subscribe` a page that 404s without one.
|
|
62
122
|
const items = appConfig.rssFeedsEnabled
|
|
63
123
|
? savedItems
|
|
64
124
|
: savedItems.filter(
|
|
65
|
-
(item: NavItem) =>
|
|
125
|
+
(item: NavItem) =>
|
|
126
|
+
item.type !== "system" || !isFeedNavKey(item.systemKey),
|
|
66
127
|
);
|
|
67
128
|
|
|
68
129
|
const siteName = appConfig.siteName;
|
|
@@ -80,8 +141,10 @@ export async function getNavigationData(
|
|
|
80
141
|
})
|
|
81
142
|
: undefined;
|
|
82
143
|
|
|
83
|
-
// Avatar URL and display flag come from appConfig
|
|
84
|
-
|
|
144
|
+
// Avatar URL and display flag come from appConfig. The chrome takes the
|
|
145
|
+
// sized variant: the full-resolution asset belongs to the social card and
|
|
146
|
+
// the settings editor, not to a 32px mark on every page.
|
|
147
|
+
const siteAvatarThumbUrl = appConfig.siteAvatarThumbUrl || undefined;
|
|
85
148
|
const showHeaderAvatar = appConfig.showHeaderAvatar;
|
|
86
149
|
|
|
87
150
|
// Render footer markdown
|
|
@@ -107,6 +170,8 @@ export async function getNavigationData(
|
|
|
107
170
|
? await c.var.services.navItems.getCollectionFreshness(collectionNavIds)
|
|
108
171
|
: undefined;
|
|
109
172
|
|
|
173
|
+
const basePath = `${appConfig.sitePathPrefix}${langBase}`;
|
|
174
|
+
|
|
110
175
|
const links = toNavItemViews(
|
|
111
176
|
items,
|
|
112
177
|
currentPath,
|
|
@@ -114,6 +179,7 @@ export async function getNavigationData(
|
|
|
114
179
|
appConfig.sitePathPrefix,
|
|
115
180
|
collectionFreshness,
|
|
116
181
|
appConfig.siteOrigin,
|
|
182
|
+
basePath,
|
|
117
183
|
);
|
|
118
184
|
|
|
119
185
|
// Only load collections when authenticated (for compose dialog)
|
|
@@ -125,13 +191,14 @@ export async function getNavigationData(
|
|
|
125
191
|
links,
|
|
126
192
|
currentPath,
|
|
127
193
|
sitePathPrefix: appConfig.sitePathPrefix,
|
|
194
|
+
basePath,
|
|
128
195
|
siteName,
|
|
129
196
|
siteDescription,
|
|
130
197
|
siteDescriptionHtml,
|
|
131
198
|
isAuthenticated,
|
|
132
199
|
collections,
|
|
133
|
-
|
|
134
|
-
showHeaderAvatar: showHeaderAvatar && !!
|
|
200
|
+
siteAvatarThumbUrl,
|
|
201
|
+
showHeaderAvatar: showHeaderAvatar && !!siteAvatarThumbUrl,
|
|
135
202
|
siteFooterHtml,
|
|
136
203
|
};
|
|
137
204
|
}
|
package/src/lib/pagination.ts
CHANGED
|
@@ -38,10 +38,18 @@ export function formatPageLabel(
|
|
|
38
38
|
return `Page ${String(currentPage)}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Slots a numbered pagination control fills once there are more pages. */
|
|
42
|
+
const PAGE_SLOT_COUNT = 7;
|
|
43
|
+
|
|
41
44
|
/**
|
|
42
45
|
* Computes which page numbers to display in a numbered pagination control.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
46
|
+
*
|
|
47
|
+
* Seven or fewer pages are all shown. Past that, the control always fills
|
|
48
|
+
* exactly seven slots — the first page, the last page, and a three-page
|
|
49
|
+
* window around the current page — so it keeps the same width, and the
|
|
50
|
+
* Previous/Next links stay put, as the reader moves through pages. Skipped
|
|
51
|
+
* pages are represented by 0 (ellipsis marker). An ellipsis always stands in
|
|
52
|
+
* for at least two pages; where it would hide just one, that page is shown.
|
|
45
53
|
*
|
|
46
54
|
* @param currentPage - The current active page (1-indexed)
|
|
47
55
|
* @param totalPages - Total number of pages
|
|
@@ -50,36 +58,32 @@ export function formatPageLabel(
|
|
|
50
58
|
* @example
|
|
51
59
|
* ```ts
|
|
52
60
|
* getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
|
|
53
|
-
* getPageNumbers(1,
|
|
54
|
-
* getPageNumbers(10,
|
|
61
|
+
* getPageNumbers(1, 19) // [1, 2, 3, 4, 5, 0, 19]
|
|
62
|
+
* getPageNumbers(10, 19) // [1, 0, 9, 10, 11, 0, 19]
|
|
63
|
+
* getPageNumbers(19, 19) // [1, 0, 15, 16, 17, 18, 19]
|
|
55
64
|
* ```
|
|
56
65
|
*/
|
|
57
66
|
export function getPageNumbers(
|
|
58
67
|
currentPage: number,
|
|
59
68
|
totalPages: number,
|
|
60
69
|
): number[] {
|
|
61
|
-
if (totalPages <=
|
|
70
|
+
if (totalPages <= PAGE_SLOT_COUNT) {
|
|
62
71
|
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (currentPage < totalPages) pages.add(currentPage + 1);
|
|
71
|
-
|
|
72
|
-
const sorted = [...pages].sort((a, b) => a - b);
|
|
73
|
-
|
|
74
|
-
// Insert 0 for gaps
|
|
75
|
-
const result: number[] = [];
|
|
76
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
77
|
-
const current = sorted[i] as number;
|
|
78
|
-
if (i > 0 && current - (sorted[i - 1] as number) > 1) {
|
|
79
|
-
result.push(0); // ellipsis marker
|
|
80
|
-
}
|
|
81
|
-
result.push(current);
|
|
82
|
-
}
|
|
74
|
+
// The second slot holds page 2 or an ellipsis, and the second-to-last slot
|
|
75
|
+
// holds page totalPages - 1 or an ellipsis, so the window around the current
|
|
76
|
+
// page starts no earlier than page 3 and ends no later than totalPages - 2.
|
|
77
|
+
const windowStart = Math.min(Math.max(currentPage - 1, 3), totalPages - 4);
|
|
78
|
+
const windowEnd = windowStart + 2;
|
|
83
79
|
|
|
84
|
-
return
|
|
80
|
+
return [
|
|
81
|
+
1,
|
|
82
|
+
windowStart === 3 ? 2 : 0,
|
|
83
|
+
windowStart,
|
|
84
|
+
windowStart + 1,
|
|
85
|
+
windowEnd,
|
|
86
|
+
windowEnd === totalPages - 2 ? totalPages - 1 : 0,
|
|
87
|
+
totalPages,
|
|
88
|
+
];
|
|
85
89
|
}
|