@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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Service
|
|
3
|
+
*
|
|
4
|
+
* Owns the multilingual content lifecycle: which languages a site serves, which
|
|
5
|
+
* one the unprefixed root URLs belong to, and the ordering rules that keep
|
|
6
|
+
* those two settings consistent with the `post.language` column.
|
|
7
|
+
*
|
|
8
|
+
* These operations are multi-step by nature — enabling has to check URL-prefix
|
|
9
|
+
* conflicts, write two settings, and stamp every unmarked post, in that order —
|
|
10
|
+
* so they live here rather than in routes, and this service is the only writer
|
|
11
|
+
* of `MULTILINGUAL_ENABLED` and `ADDITIONAL_LANGUAGES`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
15
|
+
import type { Database } from "../db/index.js";
|
|
16
|
+
import {
|
|
17
|
+
sqliteSchemaBundle,
|
|
18
|
+
type DatabaseSchema,
|
|
19
|
+
} from "../db/schema-bundle.js";
|
|
20
|
+
import {
|
|
21
|
+
formatLanguageList,
|
|
22
|
+
isValidContentLanguage,
|
|
23
|
+
normalizeContentLanguage,
|
|
24
|
+
parseLanguageList,
|
|
25
|
+
toLanguagePrefix,
|
|
26
|
+
} from "../i18n/locales.js";
|
|
27
|
+
import { RESERVED_PATHS } from "../lib/constants.js";
|
|
28
|
+
import {
|
|
29
|
+
ConflictError,
|
|
30
|
+
LanguageInUseError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
} from "../lib/errors.js";
|
|
33
|
+
import type { PathService } from "./path.js";
|
|
34
|
+
import type { PostService } from "./post.js";
|
|
35
|
+
import type { SettingsService } from "./settings.js";
|
|
36
|
+
|
|
37
|
+
/** The site's language configuration, as the rest of the app reads it. */
|
|
38
|
+
export interface LanguageState {
|
|
39
|
+
/** Whether per-language browsing views are served. */
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
/** Canonical tag served from the unprefixed root URLs. */
|
|
42
|
+
primary: string;
|
|
43
|
+
/** Canonical tags served under a URL prefix, in switcher order. */
|
|
44
|
+
additional: string[];
|
|
45
|
+
/** Every configured language, primary first. */
|
|
46
|
+
all: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What the enable dialog needs to describe what it is about to do. */
|
|
50
|
+
export interface LanguageEnablePreview {
|
|
51
|
+
/** Posts that would be stamped with the primary language. */
|
|
52
|
+
pendingCount: number;
|
|
53
|
+
/** Current primary language, prefilled into the dialog. */
|
|
54
|
+
primary: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EnableMultilingualInput {
|
|
58
|
+
/** Primary language; may differ from the stored one when the author edits it in the dialog. */
|
|
59
|
+
primary: string;
|
|
60
|
+
/** At least one language to serve under a prefix. */
|
|
61
|
+
additional: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LanguageService {
|
|
65
|
+
/** Read the site's language configuration. */
|
|
66
|
+
getState(): Promise<LanguageState>;
|
|
67
|
+
/** Numbers and defaults for the enable confirmation dialog. */
|
|
68
|
+
getEnablePreview(): Promise<LanguageEnablePreview>;
|
|
69
|
+
/**
|
|
70
|
+
* Turn on multilingual content.
|
|
71
|
+
*
|
|
72
|
+
* Validates the language set, rejects prefixes that would shadow an existing
|
|
73
|
+
* URL, writes both settings, then stamps every post that has no language yet
|
|
74
|
+
* with the primary language — so the root view keeps showing exactly what it
|
|
75
|
+
* showed a moment earlier.
|
|
76
|
+
*
|
|
77
|
+
* @returns How many posts were stamped
|
|
78
|
+
* @throws {ValidationError} When the language set is empty or malformed
|
|
79
|
+
* @throws {ConflictError} When a language prefix collides with a stored path
|
|
80
|
+
*/
|
|
81
|
+
enable(input: EnableMultilingualInput): Promise<{ markedCount: number }>;
|
|
82
|
+
/**
|
|
83
|
+
* Turn off multilingual content, keeping the configuration and every stored
|
|
84
|
+
* `post.language` value so re-enabling restores the same setup and the old
|
|
85
|
+
* prefixes can still redirect.
|
|
86
|
+
*/
|
|
87
|
+
disable(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Move the root URLs to another configured language.
|
|
90
|
+
*
|
|
91
|
+
* Swaps the two lists in one step: the new primary leaves the additional
|
|
92
|
+
* list and the old primary joins it. Doing only half would leave the old
|
|
93
|
+
* primary's posts with no view at all — filtered out of the root, and with no
|
|
94
|
+
* prefix of their own.
|
|
95
|
+
*/
|
|
96
|
+
setPrimary(language: string): Promise<void>;
|
|
97
|
+
/** Add a language to serve under a URL prefix. */
|
|
98
|
+
addLanguage(language: string): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Stop serving a language.
|
|
101
|
+
*
|
|
102
|
+
* Only allowed while no post is written in it. With posts present there is no
|
|
103
|
+
* good answer — hiding, migrating, or redirecting them all guess at intent —
|
|
104
|
+
* so this reports the count and leaves the choice to the author.
|
|
105
|
+
*
|
|
106
|
+
* Removing the last additional language turns multilingual content off with
|
|
107
|
+
* it: per-language views need a second language to mean anything, and a site
|
|
108
|
+
* left with the flag on and nothing to serve under it would report the
|
|
109
|
+
* feature as on everywhere the setting is read while behaving as
|
|
110
|
+
* single-language everywhere it is used.
|
|
111
|
+
*
|
|
112
|
+
* @returns Whether multilingual content was turned off along with it
|
|
113
|
+
* @throws {ConflictError} When posts still use the language
|
|
114
|
+
*/
|
|
115
|
+
removeLanguage(language: string): Promise<{ multilingualDisabled: boolean }>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface LanguageServiceDeps {
|
|
119
|
+
settings: SettingsService;
|
|
120
|
+
posts: PostService;
|
|
121
|
+
paths: PathService;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Language tags whose URL prefix can never work, whatever the site contains. */
|
|
125
|
+
const RESERVED_PREFIXES = new Set<string>(RESERVED_PATHS);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read the site's language configuration directly from its settings rows.
|
|
129
|
+
*
|
|
130
|
+
* A standalone reader rather than a method on the service because slug and
|
|
131
|
+
* custom-URL validation need it from inside other services, where threading a
|
|
132
|
+
* whole `LanguageService` through would be more wiring than the two values are
|
|
133
|
+
* worth. The service remains the only *writer*.
|
|
134
|
+
*
|
|
135
|
+
* @param db - Site database handle
|
|
136
|
+
* @param siteId - Site the settings belong to
|
|
137
|
+
* @param databaseSchema - Dialect-specific schema bundle
|
|
138
|
+
* @returns The primary language tag; every language the site publishes, primary
|
|
139
|
+
* first (empty while multilingual content is off); and the URL prefixes
|
|
140
|
+
* language views currently serve
|
|
141
|
+
* @example
|
|
142
|
+
* await readLanguageSettings(db, siteId);
|
|
143
|
+
* // => { primary: "zh-Hans", languages: ["zh-Hans", "en"], reservedPrefixes: ["en"] }
|
|
144
|
+
*/
|
|
145
|
+
export async function readLanguageSettings(
|
|
146
|
+
db: Database,
|
|
147
|
+
siteId: string,
|
|
148
|
+
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
149
|
+
): Promise<{
|
|
150
|
+
primary: string;
|
|
151
|
+
languages: string[];
|
|
152
|
+
reservedPrefixes: string[];
|
|
153
|
+
}> {
|
|
154
|
+
const { settings } = databaseSchema;
|
|
155
|
+
const rows = await db
|
|
156
|
+
.select({ key: settings.key, value: settings.value })
|
|
157
|
+
.from(settings)
|
|
158
|
+
.where(
|
|
159
|
+
and(
|
|
160
|
+
eq(settings.siteId, siteId),
|
|
161
|
+
inArray(settings.key, [
|
|
162
|
+
"SITE_LANGUAGE",
|
|
163
|
+
"MULTILINGUAL_ENABLED",
|
|
164
|
+
"ADDITIONAL_LANGUAGES",
|
|
165
|
+
]),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const values = new Map(rows.map((row) => [row.key, row.value]));
|
|
169
|
+
const primary = normalizeContentLanguage(values.get("SITE_LANGUAGE") ?? "");
|
|
170
|
+
const additional =
|
|
171
|
+
values.get("MULTILINGUAL_ENABLED") === "true"
|
|
172
|
+
? parseLanguageList(values.get("ADDITIONAL_LANGUAGES")).filter(
|
|
173
|
+
(tag) => tag !== primary,
|
|
174
|
+
)
|
|
175
|
+
: [];
|
|
176
|
+
// Empty — not `[primary]` — when the site publishes one language, so callers
|
|
177
|
+
// can treat "no languages" as "this site is not multilingual".
|
|
178
|
+
const languages = additional.length > 0 ? [primary, ...additional] : [];
|
|
179
|
+
return {
|
|
180
|
+
primary,
|
|
181
|
+
languages,
|
|
182
|
+
reservedPrefixes: additional.map(toLanguagePrefix),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requireValidLanguage(value: string): string {
|
|
187
|
+
const trimmed = value.trim();
|
|
188
|
+
if (!trimmed || !isValidContentLanguage(trimmed)) {
|
|
189
|
+
throw new ValidationError(
|
|
190
|
+
"Enter a valid BCP 47 language tag (e.g. en, zh-Hans, ja).",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return normalizeContentLanguage(trimmed);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function createLanguageService(
|
|
197
|
+
deps: LanguageServiceDeps,
|
|
198
|
+
): LanguageService {
|
|
199
|
+
const { settings, posts, paths } = deps;
|
|
200
|
+
|
|
201
|
+
async function readState(): Promise<LanguageState> {
|
|
202
|
+
const [enabledValue, primaryValue, additionalValue] = await Promise.all([
|
|
203
|
+
settings.get("MULTILINGUAL_ENABLED"),
|
|
204
|
+
settings.get("SITE_LANGUAGE"),
|
|
205
|
+
settings.get("ADDITIONAL_LANGUAGES"),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const primary = primaryValue?.trim()
|
|
209
|
+
? normalizeContentLanguage(primaryValue)
|
|
210
|
+
: "en";
|
|
211
|
+
// Defensive: a hand-edited settings row could list the primary language
|
|
212
|
+
// twice. The prefix for it would never route, so drop it here rather than
|
|
213
|
+
// letting a dead entry reach the switcher.
|
|
214
|
+
const additional = parseLanguageList(additionalValue).filter(
|
|
215
|
+
(tag) => tag !== primary,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
enabled: enabledValue === "true" && additional.length > 0,
|
|
220
|
+
primary,
|
|
221
|
+
additional,
|
|
222
|
+
all: [primary, ...additional],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Reject a language whose URL prefix would shadow something already served.
|
|
228
|
+
*
|
|
229
|
+
* Language prefixes are checked here, when a language is added, rather than
|
|
230
|
+
* being reserved globally: the two-letter ISO codes are ordinary English
|
|
231
|
+
* words (`it`, `is`, `no`, `go`, `my`), and permanently confiscating those
|
|
232
|
+
* slugs from every site for an off-by-default feature is out of proportion.
|
|
233
|
+
*/
|
|
234
|
+
async function assertPrefixAvailable(language: string): Promise<void> {
|
|
235
|
+
const prefix = toLanguagePrefix(language);
|
|
236
|
+
|
|
237
|
+
if (RESERVED_PREFIXES.has(prefix)) {
|
|
238
|
+
throw new ConflictError(
|
|
239
|
+
`Jant already uses /${prefix} for its own pages, so it cannot serve a language there.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const clashes = await paths.findPathsUnderSegment(prefix, 3);
|
|
244
|
+
const first = clashes[0];
|
|
245
|
+
if (first) {
|
|
246
|
+
throw new ConflictError(
|
|
247
|
+
`/${first.path} is already taken. Change that URL, then add this language.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizeAdditional(
|
|
253
|
+
tags: readonly string[],
|
|
254
|
+
primary: string,
|
|
255
|
+
): string[] {
|
|
256
|
+
const seen = new Set<string>();
|
|
257
|
+
const result: string[] = [];
|
|
258
|
+
for (const raw of tags) {
|
|
259
|
+
const tag = requireValidLanguage(raw);
|
|
260
|
+
if (tag === primary || seen.has(tag)) continue;
|
|
261
|
+
seen.add(tag);
|
|
262
|
+
result.push(tag);
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function writeLanguages(
|
|
268
|
+
primary: string,
|
|
269
|
+
additional: readonly string[],
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
await settings.set("SITE_LANGUAGE", primary);
|
|
272
|
+
if (additional.length > 0) {
|
|
273
|
+
await settings.set(
|
|
274
|
+
"ADDITIONAL_LANGUAGES",
|
|
275
|
+
formatLanguageList(additional),
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
await settings.remove("ADDITIONAL_LANGUAGES");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
getState: readState,
|
|
284
|
+
|
|
285
|
+
async getEnablePreview() {
|
|
286
|
+
const [state, pendingCount] = await Promise.all([
|
|
287
|
+
readState(),
|
|
288
|
+
posts.countMissingLanguage(),
|
|
289
|
+
]);
|
|
290
|
+
return { pendingCount, primary: state.primary };
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
async enable(input) {
|
|
294
|
+
const primary = requireValidLanguage(input.primary);
|
|
295
|
+
const additional = normalizeAdditional(input.additional, primary);
|
|
296
|
+
if (additional.length === 0) {
|
|
297
|
+
throw new ValidationError(
|
|
298
|
+
"Add at least one more language before turning this on.",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// The list must keep a place for every language posts are already
|
|
303
|
+
// written in. Turning multilingual off keeps both the languages and
|
|
304
|
+
// the stamps, so a re-enable that dropped one from the dialog would
|
|
305
|
+
// otherwise strand its posts outside every view — the exact orphaning
|
|
306
|
+
// the remove guard exists to prevent, through a side door.
|
|
307
|
+
const allowed = new Set([primary, ...additional]);
|
|
308
|
+
const stranded = (await posts.listLanguagesInUse()).find(
|
|
309
|
+
(entry) => !allowed.has(entry.language),
|
|
310
|
+
);
|
|
311
|
+
if (stranded) {
|
|
312
|
+
throw new LanguageInUseError(
|
|
313
|
+
stranded.count === 1
|
|
314
|
+
? "One post is still written in a language missing from this list. Keep the language, or change the post's language first."
|
|
315
|
+
: `${stranded.count} posts are still written in a language missing from this list. Keep the language, or change their language first.`,
|
|
316
|
+
stranded.language,
|
|
317
|
+
stranded.count,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Every prefix is checked before anything is written, so a conflict
|
|
322
|
+
// leaves the site exactly as it was.
|
|
323
|
+
for (const language of additional) {
|
|
324
|
+
await assertPrefixAvailable(language);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await writeLanguages(primary, additional);
|
|
328
|
+
await settings.set("MULTILINGUAL_ENABLED", "true");
|
|
329
|
+
|
|
330
|
+
const markedCount = await posts.materializeMissingLanguage(primary);
|
|
331
|
+
return { markedCount };
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
async disable() {
|
|
335
|
+
await settings.remove("MULTILINGUAL_ENABLED");
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
async setPrimary(language) {
|
|
339
|
+
const next = requireValidLanguage(language);
|
|
340
|
+
const state = await readState();
|
|
341
|
+
if (next === state.primary) return;
|
|
342
|
+
|
|
343
|
+
if (!state.additional.includes(next)) {
|
|
344
|
+
throw new ValidationError(
|
|
345
|
+
"Add that language to the site before making it the primary language.",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// The old primary needs a prefix from now on, and the new one no longer
|
|
350
|
+
// does. Both halves in one write: doing either alone would leave a
|
|
351
|
+
// language's posts with nowhere to appear.
|
|
352
|
+
const additional = [
|
|
353
|
+
...state.additional.filter((tag) => tag !== next),
|
|
354
|
+
state.primary,
|
|
355
|
+
];
|
|
356
|
+
await writeLanguages(next, additional);
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
async addLanguage(language) {
|
|
360
|
+
const tag = requireValidLanguage(language);
|
|
361
|
+
const state = await readState();
|
|
362
|
+
if (tag === state.primary) {
|
|
363
|
+
throw new ConflictError(
|
|
364
|
+
"That is already this site's primary language.",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (state.additional.includes(tag)) return;
|
|
368
|
+
|
|
369
|
+
await assertPrefixAvailable(tag);
|
|
370
|
+
await writeLanguages(state.primary, [...state.additional, tag]);
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
async removeLanguage(language) {
|
|
374
|
+
const tag = requireValidLanguage(language);
|
|
375
|
+
const state = await readState();
|
|
376
|
+
if (tag === state.primary) {
|
|
377
|
+
throw new ConflictError(
|
|
378
|
+
"This is the primary language. Make another language primary first.",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (!state.additional.includes(tag)) {
|
|
382
|
+
return { multilingualDisabled: false };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Counted whether or not multilingual is currently on: the settings page
|
|
386
|
+
// stays reachable after it is switched off, and the posts are still there.
|
|
387
|
+
const postCount = await posts.countByLanguage(tag);
|
|
388
|
+
if (postCount > 0) {
|
|
389
|
+
throw new LanguageInUseError(
|
|
390
|
+
postCount === 1
|
|
391
|
+
? "One post is still written in this language. Change its language, or keep the language."
|
|
392
|
+
: `${postCount} posts are still written in this language. Change their language, or keep the language.`,
|
|
393
|
+
tag,
|
|
394
|
+
postCount,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const remaining = state.additional.filter((entry) => entry !== tag);
|
|
399
|
+
await writeLanguages(state.primary, remaining);
|
|
400
|
+
|
|
401
|
+
// Nothing left to serve under a prefix, so the feature goes with it. The
|
|
402
|
+
// flag is only cleared when it was actually on — a removal made while
|
|
403
|
+
// multilingual is off has nothing to turn off, and saying so would be a
|
|
404
|
+
// lie the settings page then has to explain.
|
|
405
|
+
const multilingualDisabled = remaining.length === 0 && state.enabled;
|
|
406
|
+
if (multilingualDisabled) {
|
|
407
|
+
await settings.remove("MULTILINGUAL_ENABLED");
|
|
408
|
+
}
|
|
409
|
+
return { multilingualDisabled };
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
package/src/services/mcp.ts
CHANGED
|
@@ -102,6 +102,7 @@ const SearchPostsToolSchema = z.object({
|
|
|
102
102
|
|
|
103
103
|
const ListMediaToolSchema = z.object({
|
|
104
104
|
limit: z.coerce.number().int().min(1).max(200).optional().default(50),
|
|
105
|
+
cursor: z.string().optional(),
|
|
105
106
|
mimePrefix: z.string().trim().min(1).optional(),
|
|
106
107
|
});
|
|
107
108
|
|
|
@@ -244,6 +245,16 @@ const mcpTools: McpToolDefinition[] = [
|
|
|
244
245
|
replyToId: { type: "string" },
|
|
245
246
|
quietReply: { type: "boolean" },
|
|
246
247
|
publishedAt: { type: "integer" },
|
|
248
|
+
createdAt: {
|
|
249
|
+
type: "integer",
|
|
250
|
+
description:
|
|
251
|
+
"Unix seconds. When moving a post from another site, the time it was written.",
|
|
252
|
+
},
|
|
253
|
+
updatedAt: {
|
|
254
|
+
type: "integer",
|
|
255
|
+
description:
|
|
256
|
+
"Unix seconds. When moving a post from another site, the time it was last edited.",
|
|
257
|
+
},
|
|
247
258
|
attachments: {
|
|
248
259
|
type: "array",
|
|
249
260
|
items: { type: "object" },
|
|
@@ -276,6 +287,8 @@ const mcpTools: McpToolDefinition[] = [
|
|
|
276
287
|
replyToId: input.replyToId,
|
|
277
288
|
quietReply: input.quietReply,
|
|
278
289
|
publishedAt: input.publishedAt,
|
|
290
|
+
createdAt: input.createdAt,
|
|
291
|
+
updatedAt: input.updatedAt,
|
|
279
292
|
},
|
|
280
293
|
input.attachments,
|
|
281
294
|
{
|
|
@@ -433,7 +446,9 @@ const mcpTools: McpToolDefinition[] = [
|
|
|
433
446
|
};
|
|
434
447
|
}
|
|
435
448
|
|
|
436
|
-
return context.services.collections.listDirectoryData(
|
|
449
|
+
return context.services.collections.listDirectoryData({
|
|
450
|
+
isAuthenticated: true,
|
|
451
|
+
});
|
|
437
452
|
},
|
|
438
453
|
},
|
|
439
454
|
{
|
|
@@ -540,12 +555,14 @@ const mcpTools: McpToolDefinition[] = [
|
|
|
540
555
|
},
|
|
541
556
|
{
|
|
542
557
|
name: "jant_media_list",
|
|
543
|
-
description:
|
|
558
|
+
description:
|
|
559
|
+
"List uploaded media, newest first, optionally filtered by MIME prefix. Pass nextCursor back as cursor for the next page.",
|
|
544
560
|
inputSchema: {
|
|
545
561
|
type: "object",
|
|
546
562
|
properties: {
|
|
547
563
|
limit: { type: "integer", minimum: 1, maximum: 200, default: 50 },
|
|
548
564
|
mimePrefix: { type: "string" },
|
|
565
|
+
cursor: { type: "string" },
|
|
549
566
|
},
|
|
550
567
|
additionalProperties: false,
|
|
551
568
|
},
|
|
@@ -554,10 +571,13 @@ const mcpTools: McpToolDefinition[] = [
|
|
|
554
571
|
const media = await context.services.media.list({
|
|
555
572
|
limit: input.limit,
|
|
556
573
|
mimePrefix: input.mimePrefix,
|
|
574
|
+
cursor: input.cursor,
|
|
557
575
|
});
|
|
558
576
|
|
|
559
577
|
return {
|
|
560
578
|
media: media.map((item) => serializeMedia(item, context.appConfig)),
|
|
579
|
+
nextCursor:
|
|
580
|
+
media.length === input.limit ? (media.at(-1)?.id ?? null) : null,
|
|
561
581
|
};
|
|
562
582
|
},
|
|
563
583
|
},
|
package/src/services/media.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
lte,
|
|
18
18
|
} from "drizzle-orm";
|
|
19
19
|
import { generateKeyBetween } from "fractional-indexing";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
batchQueryRows,
|
|
22
|
+
type Database,
|
|
23
|
+
supportsDrizzleTransaction,
|
|
24
|
+
} from "../db/index.js";
|
|
21
25
|
import type { DatabaseDialect } from "../db/dialect.js";
|
|
22
26
|
import {
|
|
23
27
|
sqliteSchemaBundle,
|
|
@@ -201,6 +205,8 @@ export interface MediaFilters {
|
|
|
201
205
|
limit?: number;
|
|
202
206
|
/** Filter by MIME type prefix, e.g. "image/" */
|
|
203
207
|
mimePrefix?: string;
|
|
208
|
+
/** Exclusive media ID cursor: the last item of the previous page. */
|
|
209
|
+
cursor?: string;
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
export interface CreateTextAttachmentData {
|
|
@@ -723,11 +729,15 @@ export function createMediaService(
|
|
|
723
729
|
const result = new Map<string, Media[]>();
|
|
724
730
|
if (postIds.length === 0) return result;
|
|
725
731
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
732
|
+
// A feed asks for every post it renders at once, which can be hundreds.
|
|
733
|
+
// Each post's attachments land in one chunk, so their order holds.
|
|
734
|
+
const rows = await batchQueryRows([...new Set(postIds)], (chunk) =>
|
|
735
|
+
db
|
|
736
|
+
.select()
|
|
737
|
+
.from(media)
|
|
738
|
+
.where(and(eq(media.siteId, siteId), inArray(media.postId, chunk)))
|
|
739
|
+
.orderBy(asc(media.position)),
|
|
740
|
+
);
|
|
731
741
|
|
|
732
742
|
for (const row of rows) {
|
|
733
743
|
const m = toMedia(row);
|
|
@@ -766,11 +776,16 @@ export function createMediaService(
|
|
|
766
776
|
sql`${media.mimeType} LIKE ${filters.mimePrefix + "%"}`,
|
|
767
777
|
);
|
|
768
778
|
}
|
|
779
|
+
if (filters?.cursor) {
|
|
780
|
+
conditions.push(lt(media.id, filters.cursor));
|
|
781
|
+
}
|
|
782
|
+
// Newest first. A TypeID sorts by the time it was minted, so the ID
|
|
783
|
+
// orders like `created_at` and never ties, which a cursor needs.
|
|
769
784
|
const rows = await db
|
|
770
785
|
.select()
|
|
771
786
|
.from(media)
|
|
772
787
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
773
|
-
.orderBy(desc(media.
|
|
788
|
+
.orderBy(desc(media.id))
|
|
774
789
|
.limit(limit);
|
|
775
790
|
return rows.map(toMedia);
|
|
776
791
|
},
|