@jant/core 0.6.16 → 0.7.0
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/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- 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-CD3FhrOB.js +257 -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-CgaL2jCs.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-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- 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__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -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__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- 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 +541 -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.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -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/bubble-menu.ts +47 -16
- 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/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__/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 +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- 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__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- 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__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- 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__/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 +437 -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-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 +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- 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-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- 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 +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- 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-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- 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__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- 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 +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -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 +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- 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 +5 -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__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- 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-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/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- 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 +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- 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/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -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 +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- 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/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- 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/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- 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 +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -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 +64 -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 +275 -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/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/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content language detection.
|
|
3
|
+
*
|
|
4
|
+
* The detector is a suggestion engine, so these tests care about two things:
|
|
5
|
+
* that it is right on the cases it claims to be reliable on (Hangul, kana,
|
|
6
|
+
* Simplified vs Traditional over a sentence), and that it refuses rather than
|
|
7
|
+
* guesses everywhere else — a wrong confident answer costs the author an edit
|
|
8
|
+
* they may never notice they need.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
detectContentLanguage,
|
|
14
|
+
detectScript,
|
|
15
|
+
readContentLanguage,
|
|
16
|
+
suggestPostLanguage,
|
|
17
|
+
} from "../lang-detect.js";
|
|
18
|
+
|
|
19
|
+
describe("detectScript", () => {
|
|
20
|
+
it("reads Hangul as Korean", () => {
|
|
21
|
+
expect(detectScript("안녕하세요, 반갑습니다")).toBe("ko");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reads Korean mixed with hanja as Korean", () => {
|
|
25
|
+
// Korean borrows Han characters but never kana, so Hangul settles it.
|
|
26
|
+
expect(detectScript("韓國語 문법 정리")).toBe("ko");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("reads kana as Japanese", () => {
|
|
30
|
+
expect(detectScript("これはテストです")).toBe("ja");
|
|
31
|
+
expect(detectScript("日本語の文章を書く")).toBe("ja");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("reads Simplified-only characters as Simplified", () => {
|
|
35
|
+
expect(detectScript("国学说这时会对后")).toBe("zh-Hans");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("reads Traditional-only characters as Traditional", () => {
|
|
39
|
+
expect(detectScript("國學說這時會對後")).toBe("zh-Hant");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("decides a mixed passage by majority", () => {
|
|
43
|
+
// A Traditional post that quotes a short Simplified phrase.
|
|
44
|
+
expect(detectScript("這篇文章談的是國學說法,裡頭引了一句「这个」")).toBe(
|
|
45
|
+
"zh-Hant",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("names the script but not the variant when the two sides tie", () => {
|
|
50
|
+
// Three distinctive characters each. Which variant is a coin toss; that
|
|
51
|
+
// the text is Chinese is not in doubt.
|
|
52
|
+
expect(detectScript("這個說这个说")).toBe("han");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("names the script but not the variant for characters shared by both", () => {
|
|
56
|
+
// "我今天很好" is identical in Simplified and Traditional.
|
|
57
|
+
expect(detectScript("我今天很好")).toBe("han");
|
|
58
|
+
// Repetition across paragraphs adds length, never a distinguishing
|
|
59
|
+
// character — the case that first showed this up.
|
|
60
|
+
expect(detectScript("哈哈哈哈哈哈哈啊哈哈哈\n\n".repeat(5))).toBe("han");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("reads Latin script as Latin, accents included", () => {
|
|
64
|
+
expect(detectScript("Hello there")).toBe("latin");
|
|
65
|
+
expect(detectScript("Café crème à Paris")).toBe("latin");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("refuses text with no letters at all", () => {
|
|
69
|
+
expect(detectScript("123 — !!! 🙂")).toBeNull();
|
|
70
|
+
expect(detectScript("")).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("refuses a fragment, whichever script it is in", () => {
|
|
74
|
+
// The first few characters of a post decide nothing — an answer here would
|
|
75
|
+
// change on the next keystroke, which is worse than no answer.
|
|
76
|
+
expect(detectScript("H")).toBeNull();
|
|
77
|
+
expect(detectScript("Hello")).toBeNull();
|
|
78
|
+
expect(detectScript("这会")).toBeNull();
|
|
79
|
+
expect(detectScript("こん")).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("answers once there are a couple of words", () => {
|
|
83
|
+
expect(detectScript("Hello there")).toBe("latin");
|
|
84
|
+
expect(detectScript("这是中文")).toBe("zh-Hans");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("reads a passage by what it is mostly in", () => {
|
|
88
|
+
// A Han character used to speak for the whole post no matter how much
|
|
89
|
+
// English surrounded it.
|
|
90
|
+
expect(
|
|
91
|
+
detectScript(
|
|
92
|
+
"The word 说 comes up in every other sentence of this chapter.",
|
|
93
|
+
),
|
|
94
|
+
).toBe("latin");
|
|
95
|
+
// ...and the reverse still holds: a Chinese passage quoting a English
|
|
96
|
+
// word is Chinese.
|
|
97
|
+
expect(detectScript("这篇讲的是国学时间,里头引了 React")).toBe("zh-Hans");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("detectContentLanguage", () => {
|
|
102
|
+
const zhEn = { languages: ["zh-Hans", "en"], fallback: "zh-Hans" };
|
|
103
|
+
|
|
104
|
+
it("picks the language matching the detected script", () => {
|
|
105
|
+
expect(detectContentLanguage("这是中文", zhEn)).toBe("zh-Hans");
|
|
106
|
+
expect(detectContentLanguage("This is English", zhEn)).toBe("en");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("matches Simplified and Traditional as distinct languages", () => {
|
|
110
|
+
const both = {
|
|
111
|
+
languages: ["zh-Hans", "zh-Hant", "en"],
|
|
112
|
+
fallback: "zh-Hans",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
expect(detectContentLanguage("國學說這時會對後", both)).toBe("zh-Hant");
|
|
116
|
+
expect(detectContentLanguage("国学说这时会对后", both)).toBe("zh-Hans");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("matches a language tag that only implies its script", () => {
|
|
120
|
+
// The site configured zh-TW, not zh-Hant.
|
|
121
|
+
expect(
|
|
122
|
+
detectContentLanguage("國學說這時", {
|
|
123
|
+
languages: ["en", "zh-TW"],
|
|
124
|
+
fallback: "en",
|
|
125
|
+
}),
|
|
126
|
+
).toBe("zh-TW");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("will not choose between two non-CJK languages", () => {
|
|
130
|
+
// The Latin alphabet does not distinguish English from French.
|
|
131
|
+
expect(
|
|
132
|
+
detectContentLanguage("Bonjour tout le monde", {
|
|
133
|
+
languages: ["en", "fr", "ja"],
|
|
134
|
+
fallback: "en",
|
|
135
|
+
}),
|
|
136
|
+
).toBe("en");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("reads shared Han as the site's one Chinese language", () => {
|
|
140
|
+
// Text written the same way in both variants. With one of the two
|
|
141
|
+
// published there is nothing left to be ambiguous about.
|
|
142
|
+
expect(detectContentLanguage("哈哈哈哈哈哈哈啊哈哈哈", zhEn)).toBe(
|
|
143
|
+
"zh-Hans",
|
|
144
|
+
);
|
|
145
|
+
expect(
|
|
146
|
+
detectContentLanguage("我今天很好", {
|
|
147
|
+
languages: ["zh-TW", "en"],
|
|
148
|
+
fallback: "en",
|
|
149
|
+
}),
|
|
150
|
+
).toBe("zh-TW");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("reads shared Han as Simplified when that is the only language", () => {
|
|
154
|
+
// Nothing to weigh it against: every candidate the site has is Simplified
|
|
155
|
+
// Chinese, so text that is certainly Chinese is certainly that.
|
|
156
|
+
expect(
|
|
157
|
+
readContentLanguage("哈哈哈哈哈哈哈啊哈哈哈", {
|
|
158
|
+
languages: ["zh-Hans"],
|
|
159
|
+
}),
|
|
160
|
+
).toBe("zh-Hans");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("will not choose between Simplified and Traditional", () => {
|
|
164
|
+
// Both published, and the text distinguishes neither: the fallback is the
|
|
165
|
+
// only honest answer.
|
|
166
|
+
expect(
|
|
167
|
+
readContentLanguage("我今天很好", {
|
|
168
|
+
languages: ["zh-Hans", "zh-Hant", "en"],
|
|
169
|
+
}),
|
|
170
|
+
).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("falls back when the site does not publish the detected script", () => {
|
|
174
|
+
expect(
|
|
175
|
+
detectContentLanguage("これは日本語です", {
|
|
176
|
+
languages: ["zh-Hans", "en"],
|
|
177
|
+
fallback: "zh-Hans",
|
|
178
|
+
}),
|
|
179
|
+
).toBe("zh-Hans");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("never returns a language outside the configured set", () => {
|
|
183
|
+
const samples = [
|
|
184
|
+
"안녕하세요",
|
|
185
|
+
"これはテスト",
|
|
186
|
+
"國學說這時",
|
|
187
|
+
"国学说这时",
|
|
188
|
+
"Hello",
|
|
189
|
+
"12345",
|
|
190
|
+
"",
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
for (const sample of samples) {
|
|
194
|
+
expect(zhEn.languages, sample).toContain(
|
|
195
|
+
detectContentLanguage(sample, zhEn),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("suggestPostLanguage", () => {
|
|
202
|
+
const site = { languages: ["zh-Hans", "en"], primary: "zh-Hans" };
|
|
203
|
+
|
|
204
|
+
it("reads the language out of the text", () => {
|
|
205
|
+
expect(suggestPostLanguage({ ...site, text: "Hello there" })).toBe("en");
|
|
206
|
+
expect(suggestPostLanguage({ ...site, text: "这是中文" })).toBe("zh-Hans");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("falls back to the primary language for empty text", () => {
|
|
210
|
+
expect(suggestPostLanguage({ ...site, text: " " })).toBe("zh-Hans");
|
|
211
|
+
expect(suggestPostLanguage(site)).toBe("zh-Hans");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("keeps the primary language until the text says otherwise", () => {
|
|
215
|
+
// Half a word in: the composer shows the language of the page the author
|
|
216
|
+
// is writing from, and stays there until there is something to read.
|
|
217
|
+
expect(suggestPostLanguage({ ...site, text: "Hel" })).toBe("zh-Hans");
|
|
218
|
+
expect(suggestPostLanguage({ ...site, text: "Hello there" })).toBe("en");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("returns null on a site that is not multilingual", () => {
|
|
222
|
+
// Nothing is stamped before the author turns the feature on.
|
|
223
|
+
expect(
|
|
224
|
+
suggestPostLanguage({
|
|
225
|
+
languages: [],
|
|
226
|
+
primary: "zh-Hans",
|
|
227
|
+
text: "Hello there",
|
|
228
|
+
}),
|
|
229
|
+
).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Media } from "../../types.js";
|
|
3
|
+
import { buildMediaMap } from "../media-helpers.js";
|
|
4
|
+
|
|
5
|
+
const R2_PUBLIC_URL = "https://cdn.example.com";
|
|
6
|
+
const IMAGE_TRANSFORM_URL = "https://example.com/cdn-cgi/image";
|
|
7
|
+
|
|
8
|
+
function makeMedia(overrides: Partial<Media> = {}): Media {
|
|
9
|
+
return {
|
|
10
|
+
id: "med_01j0000000000000000000000",
|
|
11
|
+
siteId: "sit_test00000000000000000000000",
|
|
12
|
+
postId: "pst_01j0000000000000000000000",
|
|
13
|
+
filename: "clip.mp4",
|
|
14
|
+
originalName: "clip.mp4",
|
|
15
|
+
mimeType: "video/mp4",
|
|
16
|
+
size: 4_000_000,
|
|
17
|
+
storageKey: "media/clip.mp4",
|
|
18
|
+
provider: "r2",
|
|
19
|
+
width: 1920,
|
|
20
|
+
height: 1080,
|
|
21
|
+
durationSeconds: 12,
|
|
22
|
+
alt: null,
|
|
23
|
+
position: "a0",
|
|
24
|
+
blurhash: null,
|
|
25
|
+
waveform: null,
|
|
26
|
+
posterKey: "posters/clip-poster.png",
|
|
27
|
+
summary: null,
|
|
28
|
+
chars: null,
|
|
29
|
+
mediaKind: "video",
|
|
30
|
+
createdAt: 1706745600,
|
|
31
|
+
updatedAt: 1706745600,
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function firstAttachment(media: Media) {
|
|
37
|
+
const map = buildMediaMap(
|
|
38
|
+
new Map([["pst_01j0000000000000000000000", [media]]]),
|
|
39
|
+
R2_PUBLIC_URL,
|
|
40
|
+
IMAGE_TRANSFORM_URL,
|
|
41
|
+
);
|
|
42
|
+
const list = map.get("pst_01j0000000000000000000000");
|
|
43
|
+
expect(list).toHaveLength(1);
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked above
|
|
45
|
+
return list![0]!;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("buildMediaMap", () => {
|
|
49
|
+
it("sizes a video poster through the image transform", () => {
|
|
50
|
+
// A poster is a still frame at the source video's resolution, so an
|
|
51
|
+
// untransformed one puts the full-size original in the timeline. This is
|
|
52
|
+
// the same treatment `toMediaView` gives the field.
|
|
53
|
+
expect(firstAttachment(makeMedia()).posterUrl).toBe(
|
|
54
|
+
"https://example.com/cdn-cgi/image/width=640,quality=80,format=auto,fit=scale-down/https://cdn.example.com/posters/clip-poster.png",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("leaves the poster URL untransformed when no transform is configured", () => {
|
|
59
|
+
const map = buildMediaMap(
|
|
60
|
+
new Map([["pst_01j0000000000000000000000", [makeMedia()]]]),
|
|
61
|
+
R2_PUBLIC_URL,
|
|
62
|
+
);
|
|
63
|
+
expect(map.get("pst_01j0000000000000000000000")?.[0]?.posterUrl).toBe(
|
|
64
|
+
"https://cdn.example.com/posters/clip-poster.png",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("has no poster URL without a poster key", () => {
|
|
69
|
+
expect(
|
|
70
|
+
firstAttachment(makeMedia({ posterKey: null })).posterUrl,
|
|
71
|
+
).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("leaves a video's own URL alone", () => {
|
|
75
|
+
// Only images go through the transform; asking it to resize an mp4
|
|
76
|
+
// returns nothing useful.
|
|
77
|
+
expect(firstAttachment(makeMedia()).previewUrl).toBe(
|
|
78
|
+
"https://cdn.example.com/media/clip.mp4",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Context } from "hono";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import type { NavItemView } from "../../types.js";
|
|
4
|
+
import {
|
|
5
|
+
collectNavigationCollectionIds,
|
|
6
|
+
getNavigationData,
|
|
7
|
+
} from "../navigation.js";
|
|
4
8
|
|
|
5
9
|
describe("getNavigationData", () => {
|
|
6
10
|
it("renders site footer markdown through the shared pipeline", async () => {
|
|
@@ -99,6 +103,61 @@ describe("getNavigationData", () => {
|
|
|
99
103
|
expect(result.links[1]?.url).toBe("/");
|
|
100
104
|
});
|
|
101
105
|
|
|
106
|
+
it("hides the built-in Subscribe item when feeds are disabled", async () => {
|
|
107
|
+
const context = {
|
|
108
|
+
var: {
|
|
109
|
+
publicPath: "/",
|
|
110
|
+
appConfig: {
|
|
111
|
+
siteName: "Jant",
|
|
112
|
+
sitePathPrefix: "",
|
|
113
|
+
siteOrigin: "https://example.com",
|
|
114
|
+
siteDescription: "",
|
|
115
|
+
siteDescriptionExplicit: false,
|
|
116
|
+
siteAvatarUrl: "",
|
|
117
|
+
showHeaderAvatar: false,
|
|
118
|
+
siteFooter: "",
|
|
119
|
+
rssFeedsEnabled: false,
|
|
120
|
+
},
|
|
121
|
+
services: {
|
|
122
|
+
navItems: {
|
|
123
|
+
list: async () => [
|
|
124
|
+
{
|
|
125
|
+
id: "nav_subscribe",
|
|
126
|
+
type: "system",
|
|
127
|
+
systemKey: "subscribe",
|
|
128
|
+
label: "Subscribe",
|
|
129
|
+
url: "/subscribe",
|
|
130
|
+
placement: "header",
|
|
131
|
+
position: "a0",
|
|
132
|
+
createdAt: 1,
|
|
133
|
+
updatedAt: 1,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "nav_archive",
|
|
137
|
+
type: "system",
|
|
138
|
+
systemKey: "archive",
|
|
139
|
+
label: "All",
|
|
140
|
+
url: "/archive",
|
|
141
|
+
placement: "header",
|
|
142
|
+
position: "a1",
|
|
143
|
+
createdAt: 1,
|
|
144
|
+
updatedAt: 1,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
collections: { list: async () => [] },
|
|
149
|
+
},
|
|
150
|
+
isAuthenticated: false,
|
|
151
|
+
session: null,
|
|
152
|
+
},
|
|
153
|
+
} as unknown as Context;
|
|
154
|
+
|
|
155
|
+
const result = await getNavigationData(context);
|
|
156
|
+
|
|
157
|
+
// The page it addresses 404s without feeds, so the entry has to go too.
|
|
158
|
+
expect(result.links.map((link) => link.url)).toEqual(["/archive"]);
|
|
159
|
+
});
|
|
160
|
+
|
|
102
161
|
it("hides only the built-in RSS item when feeds are disabled", async () => {
|
|
103
162
|
const context = {
|
|
104
163
|
var: {
|
|
@@ -153,4 +212,79 @@ describe("getNavigationData", () => {
|
|
|
153
212
|
|
|
154
213
|
expect(result.links.map((link) => link.id)).toEqual(["nav_custom"]);
|
|
155
214
|
});
|
|
215
|
+
|
|
216
|
+
// A post page's URL carries no language prefix, so its chrome is scoped by
|
|
217
|
+
// the post's language instead of the request path.
|
|
218
|
+
it("scopes the base path to an explicit language", async () => {
|
|
219
|
+
const makeContext = (languageScope: string) =>
|
|
220
|
+
({
|
|
221
|
+
var: {
|
|
222
|
+
publicPath: "/some-post",
|
|
223
|
+
appConfig: {
|
|
224
|
+
siteName: "Jant",
|
|
225
|
+
sitePathPrefix: "",
|
|
226
|
+
siteDescription: "",
|
|
227
|
+
siteDescriptionExplicit: false,
|
|
228
|
+
siteAvatarUrl: "",
|
|
229
|
+
showHeaderAvatar: false,
|
|
230
|
+
siteFooter: "",
|
|
231
|
+
siteLanguage: "zh-Hans",
|
|
232
|
+
additionalLanguages: ["ja"],
|
|
233
|
+
multilingualEnabled: true,
|
|
234
|
+
rssFeedsEnabled: true,
|
|
235
|
+
},
|
|
236
|
+
services: {
|
|
237
|
+
navItems: {
|
|
238
|
+
list: async () => [
|
|
239
|
+
{
|
|
240
|
+
id: "nav_latest",
|
|
241
|
+
type: "system",
|
|
242
|
+
systemKey: "latest",
|
|
243
|
+
label: "Latest",
|
|
244
|
+
url: "/",
|
|
245
|
+
placement: "header",
|
|
246
|
+
position: "a0",
|
|
247
|
+
createdAt: 1,
|
|
248
|
+
updatedAt: 1,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
collections: {
|
|
253
|
+
listByRecentActivity: async () => [],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
isAuthenticated: false,
|
|
257
|
+
session: null,
|
|
258
|
+
},
|
|
259
|
+
}) as unknown as Context;
|
|
260
|
+
|
|
261
|
+
const japanese = await getNavigationData(makeContext("ja"), {
|
|
262
|
+
languageScope: "ja",
|
|
263
|
+
});
|
|
264
|
+
expect(japanese.basePath).toBe("/ja");
|
|
265
|
+
expect(japanese.links[0]?.url).toBe("/ja");
|
|
266
|
+
|
|
267
|
+
const primary = await getNavigationData(makeContext("zh-Hans"), {
|
|
268
|
+
languageScope: "zh-Hans",
|
|
269
|
+
});
|
|
270
|
+
expect(primary.basePath).toBe("");
|
|
271
|
+
expect(primary.links[0]?.url).toBe("/");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("collectNavigationCollectionIds", () => {
|
|
276
|
+
// The directory used to read only `collection` items, so a smart collection
|
|
277
|
+
// already in the navigation still offered "Add to Navigation".
|
|
278
|
+
it("names both kinds of collection and ignores everything else", () => {
|
|
279
|
+
const links = [
|
|
280
|
+
{ id: "nav_1", type: "system", systemKey: "home" },
|
|
281
|
+
{ id: "nav_2", type: "collection", collectionId: "col_1" },
|
|
282
|
+
{ id: "nav_3", type: "smart_collection", smartCollectionId: "smc_1" },
|
|
283
|
+
{ id: "nav_4", type: "link", url: "https://example.com" },
|
|
284
|
+
{ id: "nav_5", type: "collection" },
|
|
285
|
+
{ id: "nav_6", type: "smart_collection" },
|
|
286
|
+
] as unknown as NavItemView[];
|
|
287
|
+
|
|
288
|
+
expect(collectNavigationCollectionIds(links)).toEqual(["col_1", "smc_1"]);
|
|
289
|
+
});
|
|
156
290
|
});
|
|
@@ -31,9 +31,9 @@ describe("resolveConfig", () => {
|
|
|
31
31
|
expect(config.publicApiEnabled).toBe(true);
|
|
32
32
|
expect(config.rssFeedsEnabled).toBe(true);
|
|
33
33
|
expect(config.demoMode).toBe(false);
|
|
34
|
-
expect(config.pageSize).toBe(
|
|
35
|
-
expect(config.searchPageSize).toBe(
|
|
36
|
-
expect(config.archivePageSize).toBe(
|
|
34
|
+
expect(config.pageSize).toBe(25);
|
|
35
|
+
expect(config.searchPageSize).toBe(25);
|
|
36
|
+
expect(config.archivePageSize).toBe(25);
|
|
37
37
|
expect(config.rssFeedLimit).toBe(50);
|
|
38
38
|
expect(config.rssPublishDelaySeconds).toBe(300);
|
|
39
39
|
});
|
|
@@ -81,6 +81,34 @@ describe("resolveConfig", () => {
|
|
|
81
81
|
expect(config.mainRssFeed).toBe("featured");
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it("resolves archiveDefaultLayout from DB, env, and defaults", () => {
|
|
85
|
+
const defaultConfig = resolveConfig(makeEnv(), {});
|
|
86
|
+
expect(defaultConfig.archiveDefaultLayout).toBe("list");
|
|
87
|
+
|
|
88
|
+
const envConfig = resolveConfig(
|
|
89
|
+
makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "grid" }),
|
|
90
|
+
{},
|
|
91
|
+
);
|
|
92
|
+
expect(envConfig.archiveDefaultLayout).toBe("grid");
|
|
93
|
+
|
|
94
|
+
const dbConfig = resolveConfig(
|
|
95
|
+
makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "list" }),
|
|
96
|
+
{
|
|
97
|
+
ARCHIVE_DEFAULT_LAYOUT: "grid",
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
expect(dbConfig.archiveDefaultLayout).toBe("grid");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("falls back when the archive layout setting is invalid", () => {
|
|
104
|
+
const config = resolveConfig(
|
|
105
|
+
makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "carousel" }),
|
|
106
|
+
{},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(config.archiveDefaultLayout).toBe("list");
|
|
110
|
+
});
|
|
111
|
+
|
|
84
112
|
it("normalizes legacy time zone values from the database", () => {
|
|
85
113
|
const config = resolveConfig(makeEnv(), { TIME_ZONE: "Beijing" });
|
|
86
114
|
|
|
@@ -172,6 +200,39 @@ describe("resolveConfig", () => {
|
|
|
172
200
|
expect(config.siteAvatarUrl).toBe(
|
|
173
201
|
"https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
174
202
|
);
|
|
203
|
+
// Without a transform there is nothing to size it down with, so the
|
|
204
|
+
// header falls back to the canonical asset.
|
|
205
|
+
expect(config.siteAvatarThumbUrl).toBe(config.siteAvatarUrl);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("sizes the header avatar down when an image transform is configured", () => {
|
|
209
|
+
const config = resolveConfig(
|
|
210
|
+
makeEnv({
|
|
211
|
+
R2_PUBLIC_URL: "https://r2.example.com",
|
|
212
|
+
IMAGE_TRANSFORM_URL: "https://r2.example.com/cdn-cgi/image",
|
|
213
|
+
STORAGE_DRIVER: "r2",
|
|
214
|
+
}),
|
|
215
|
+
{
|
|
216
|
+
SITE_AVATAR:
|
|
217
|
+
"media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// The canonical URL stays full-resolution: it is what the social card and
|
|
222
|
+
// the settings editor need.
|
|
223
|
+
expect(config.siteAvatarUrl).toBe(
|
|
224
|
+
"https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
225
|
+
);
|
|
226
|
+
expect(config.siteAvatarThumbUrl).toBe(
|
|
227
|
+
"https://r2.example.com/cdn-cgi/image/width=128,quality=80,format=auto,fit=scale-down/https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("leaves both avatar URLs empty without a stored avatar", () => {
|
|
232
|
+
const config = resolveConfig(makeEnv({}), {});
|
|
233
|
+
|
|
234
|
+
expect(config.siteAvatarUrl).toBe("");
|
|
235
|
+
expect(config.siteAvatarThumbUrl).toBe("");
|
|
175
236
|
});
|
|
176
237
|
|
|
177
238
|
it("returns empty siteAvatarUrl when no avatar set", () => {
|
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
CreatePostApiSchema,
|
|
10
10
|
UpdatePostSchema,
|
|
11
11
|
UpdatePostApiSchema,
|
|
12
|
-
|
|
12
|
+
SetupAccountSchema,
|
|
13
13
|
SigninSchema,
|
|
14
14
|
UpdateSiteSettingsSchema,
|
|
15
15
|
normalizeEmail,
|
|
16
16
|
parseFormData,
|
|
17
|
+
PostFilterSelectionSchema,
|
|
17
18
|
parseFormDataOptional,
|
|
18
19
|
validateAttachmentCount,
|
|
19
20
|
} from "../schemas.js";
|
|
@@ -75,10 +76,9 @@ describe("normalizeEmail", () => {
|
|
|
75
76
|
});
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
describe("
|
|
79
|
+
describe("SetupAccountSchema", () => {
|
|
79
80
|
it("normalizes email before returning parsed data", () => {
|
|
80
|
-
const result =
|
|
81
|
-
siteName: "Jant",
|
|
81
|
+
const result = SetupAccountSchema.parse({
|
|
82
82
|
email: " Admin@Example.COM ",
|
|
83
83
|
password: "password123",
|
|
84
84
|
});
|
|
@@ -816,3 +816,41 @@ describe("validateAttachmentCount", () => {
|
|
|
816
816
|
);
|
|
817
817
|
});
|
|
818
818
|
});
|
|
819
|
+
|
|
820
|
+
describe("PostFilterSelectionSchema", () => {
|
|
821
|
+
it("accepts a selection a smart collection may store", () => {
|
|
822
|
+
const result = PostFilterSelectionSchema.safeParse({
|
|
823
|
+
format: "note",
|
|
824
|
+
media: "any",
|
|
825
|
+
visibility: "featured",
|
|
826
|
+
collection: ["col_01m0f291t3fzvte3vj2g8d611z"],
|
|
827
|
+
});
|
|
828
|
+
expect(result.success).toBe(true);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("accepts the empty selection, which means every post", () => {
|
|
832
|
+
expect(PostFilterSelectionSchema.safeParse({}).success).toBe(true);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("refuses the private visibility a smart collection can never name", () => {
|
|
836
|
+
expect(
|
|
837
|
+
PostFilterSelectionSchema.safeParse({ visibility: "private" }).success,
|
|
838
|
+
).toBe(false);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it("refuses a key that is not a dimension", () => {
|
|
842
|
+
expect(
|
|
843
|
+
PostFilterSelectionSchema.safeParse({ language: "en" }).success,
|
|
844
|
+
).toBe(false);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("names the condition it rejected", () => {
|
|
848
|
+
const result = PostFilterSelectionSchema.safeParse({ year: 1970 });
|
|
849
|
+
expect(result.success).toBe(false);
|
|
850
|
+
if (result.success) return;
|
|
851
|
+
expect(result.error.issues[0]?.path).toEqual(["year"]);
|
|
852
|
+
expect(result.error.issues[0]?.message).toBe(
|
|
853
|
+
"Invalid value for the year condition",
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderSiteUnavailablePage } from "../site-unavailable-page.js";
|
|
3
|
+
|
|
4
|
+
describe("renderSiteUnavailablePage", () => {
|
|
5
|
+
it("links back to the configured hosting provider", () => {
|
|
6
|
+
const html = renderSiteUnavailablePage({
|
|
7
|
+
HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud-jant.localtest.me",
|
|
8
|
+
HOSTED_CONTROL_PLANE_PROVIDER_NAME: "Jant Cloud",
|
|
9
|
+
SITE_RESOLUTION_MODE: "host-based",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(html).toContain("This site is offline");
|
|
13
|
+
expect(html).toContain(
|
|
14
|
+
'<a href="https://cloud-jant.localtest.me/app">Jant Cloud</a>',
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("stays provider-neutral when no control plane is configured", () => {
|
|
19
|
+
const html = renderSiteUnavailablePage({});
|
|
20
|
+
|
|
21
|
+
expect(html).toContain("There's nothing to read here right now");
|
|
22
|
+
expect(html).toContain("sign in to your hosting provider");
|
|
23
|
+
expect(html).not.toContain("<a ");
|
|
24
|
+
});
|
|
25
|
+
});
|