@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,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View language resolution.
|
|
3
|
+
*
|
|
4
|
+
* These are the rules the language routes are built on: which first path
|
|
5
|
+
* segment counts as a language, what the primary language's own prefix means,
|
|
6
|
+
* and how links stay inside the view they were rendered in.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import type { Context } from "hono";
|
|
11
|
+
import type { AppConfig } from "../../types/config.js";
|
|
12
|
+
import {
|
|
13
|
+
buildLanguageSwitcher,
|
|
14
|
+
buildSurfaceAlternates,
|
|
15
|
+
getViewLang,
|
|
16
|
+
getViewLanguages,
|
|
17
|
+
isPerLanguageSurface,
|
|
18
|
+
isPrefixedLanguageView,
|
|
19
|
+
languageScopeBasePath,
|
|
20
|
+
languageUrlPrefixes,
|
|
21
|
+
resolveLanguageView,
|
|
22
|
+
toLanguagePath,
|
|
23
|
+
toViewPath,
|
|
24
|
+
viewBasePath,
|
|
25
|
+
viewRelativePath,
|
|
26
|
+
} from "../view-language.js";
|
|
27
|
+
|
|
28
|
+
interface FakeContextOptions {
|
|
29
|
+
path?: string;
|
|
30
|
+
query?: string;
|
|
31
|
+
viewLang?: string;
|
|
32
|
+
siteLanguage?: string;
|
|
33
|
+
additionalLanguages?: string[];
|
|
34
|
+
multilingualEnabled?: boolean;
|
|
35
|
+
sitePathPrefix?: string;
|
|
36
|
+
siteUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Minimal stand-in for the request context these helpers actually read. */
|
|
40
|
+
function fakeContext(options: FakeContextOptions = {}) {
|
|
41
|
+
const {
|
|
42
|
+
path = "/",
|
|
43
|
+
query = "",
|
|
44
|
+
viewLang,
|
|
45
|
+
siteLanguage = "zh-Hans",
|
|
46
|
+
additionalLanguages = ["en"],
|
|
47
|
+
multilingualEnabled = true,
|
|
48
|
+
sitePathPrefix = "",
|
|
49
|
+
siteUrl = "https://example.com",
|
|
50
|
+
} = options;
|
|
51
|
+
|
|
52
|
+
const appConfig = {
|
|
53
|
+
siteLanguage,
|
|
54
|
+
additionalLanguages,
|
|
55
|
+
multilingualEnabled,
|
|
56
|
+
sitePathPrefix,
|
|
57
|
+
siteUrl,
|
|
58
|
+
} as unknown as AppConfig;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
req: { path, url: `https://example.com${path}${query}` },
|
|
62
|
+
var: { appConfig, viewLang },
|
|
63
|
+
} as unknown as Parameters<typeof resolveLanguageView>[0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("resolveLanguageView", () => {
|
|
67
|
+
it("serves a configured language prefix as a view", () => {
|
|
68
|
+
const c = fakeContext({ path: "/en/archive" });
|
|
69
|
+
|
|
70
|
+
expect(resolveLanguageView(c)).toEqual({ kind: "view", lang: "en" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("matches the prefix case-insensitively", () => {
|
|
74
|
+
const c = fakeContext({
|
|
75
|
+
path: "/zh-hant",
|
|
76
|
+
additionalLanguages: ["zh-Hant"],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(resolveLanguageView(c)).toEqual({ kind: "view", lang: "zh-Hant" });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("passes a segment that is not a configured language", () => {
|
|
83
|
+
expect(resolveLanguageView(fakeContext({ path: "/fr" }))).toEqual({
|
|
84
|
+
kind: "pass",
|
|
85
|
+
});
|
|
86
|
+
expect(resolveLanguageView(fakeContext({ path: "/hello" }))).toEqual({
|
|
87
|
+
kind: "pass",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("passes everything on a site with no second language", () => {
|
|
92
|
+
// Without this, a post at /en on a single-language site would 404.
|
|
93
|
+
const c = fakeContext({ path: "/en", additionalLanguages: [] });
|
|
94
|
+
|
|
95
|
+
expect(resolveLanguageView(c)).toEqual({ kind: "pass" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("ignores the primary language repeated in the additional list", () => {
|
|
99
|
+
const c = fakeContext({
|
|
100
|
+
path: "/zh-hans",
|
|
101
|
+
additionalLanguages: ["zh-Hans"],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(resolveLanguageView(c)).toEqual({ kind: "pass" });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("the primary language's own prefix", () => {
|
|
108
|
+
it("redirects to the root it aliases", () => {
|
|
109
|
+
const c = fakeContext({ path: "/zh-hans" });
|
|
110
|
+
|
|
111
|
+
expect(resolveLanguageView(c)).toEqual({ kind: "redirect", to: "/" });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("strips the prefix from sub-paths, keeping the query", () => {
|
|
115
|
+
const c = fakeContext({ path: "/zh-hans/archive", query: "?year=2024" });
|
|
116
|
+
|
|
117
|
+
expect(resolveLanguageView(c)).toEqual({
|
|
118
|
+
kind: "redirect",
|
|
119
|
+
to: "/archive?year=2024",
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("keeps the deployment path prefix", () => {
|
|
124
|
+
const c = fakeContext({
|
|
125
|
+
path: "/zh-hans/archive",
|
|
126
|
+
sitePathPrefix: "/blog",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(resolveLanguageView(c)).toEqual({
|
|
130
|
+
kind: "redirect",
|
|
131
|
+
to: "/blog/archive",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("after multilingual is switched off", () => {
|
|
137
|
+
it("redirects a previously configured prefix", () => {
|
|
138
|
+
// The configuration is retained so old links and feed subscriptions
|
|
139
|
+
// keep working rather than 404ing.
|
|
140
|
+
const c = fakeContext({ path: "/en/feed", multilingualEnabled: false });
|
|
141
|
+
|
|
142
|
+
expect(resolveLanguageView(c)).toEqual({ kind: "redirect", to: "/feed" });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("languageUrlPrefixes", () => {
|
|
148
|
+
it("lists every prefix a path can arrive under, primary included", () => {
|
|
149
|
+
// The primary language has no view of its own, but `/zh-hans/hello`
|
|
150
|
+
// redirects to `/hello` — so it still names the same page.
|
|
151
|
+
expect(
|
|
152
|
+
languageUrlPrefixes({
|
|
153
|
+
siteLanguage: "zh-Hans",
|
|
154
|
+
additionalLanguages: ["en", "ja"],
|
|
155
|
+
}),
|
|
156
|
+
).toEqual(["zh-hans", "en", "ja"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("is empty on a site with one language", () => {
|
|
160
|
+
// Nothing serves a view there, so `/en/hello` is just a post's address.
|
|
161
|
+
expect(
|
|
162
|
+
languageUrlPrefixes({
|
|
163
|
+
siteLanguage: "en",
|
|
164
|
+
additionalLanguages: [],
|
|
165
|
+
}),
|
|
166
|
+
).toEqual([]);
|
|
167
|
+
expect(
|
|
168
|
+
languageUrlPrefixes({
|
|
169
|
+
siteLanguage: "en",
|
|
170
|
+
additionalLanguages: ["en"],
|
|
171
|
+
}),
|
|
172
|
+
).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("getViewLang", () => {
|
|
177
|
+
it("returns the prefix language inside a language view", () => {
|
|
178
|
+
expect(getViewLang(fakeContext({ viewLang: "en" }))).toBe("en");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("returns the primary language at the root of a multilingual site", () => {
|
|
182
|
+
// The root is the primary language's view — otherwise `/` would show every
|
|
183
|
+
// language while `/en` showed one.
|
|
184
|
+
expect(getViewLang(fakeContext())).toBe("zh-Hans");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns null on a single-language site", () => {
|
|
188
|
+
expect(getViewLang(fakeContext({ multilingualEnabled: false }))).toBeNull();
|
|
189
|
+
expect(getViewLang(fakeContext({ additionalLanguages: [] }))).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("isPrefixedLanguageView", () => {
|
|
194
|
+
it("is true only under a language prefix", () => {
|
|
195
|
+
expect(isPrefixedLanguageView(fakeContext({ viewLang: "en" }))).toBe(true);
|
|
196
|
+
expect(isPrefixedLanguageView(fakeContext())).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("path helpers", () => {
|
|
201
|
+
it("prefixes in-view links and leaves root links alone", () => {
|
|
202
|
+
const inView = fakeContext({ viewLang: "en" });
|
|
203
|
+
const atRoot = fakeContext();
|
|
204
|
+
|
|
205
|
+
expect(toViewPath(inView, "/")).toBe("/en");
|
|
206
|
+
expect(toViewPath(inView, "/archive")).toBe("/en/archive");
|
|
207
|
+
expect(toViewPath(atRoot, "/")).toBe("/");
|
|
208
|
+
expect(toViewPath(atRoot, "/archive")).toBe("/archive");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("composes with the deployment path prefix", () => {
|
|
212
|
+
const c = fakeContext({ viewLang: "en", sitePathPrefix: "/blog" });
|
|
213
|
+
|
|
214
|
+
expect(toViewPath(c, "/")).toBe("/blog/en");
|
|
215
|
+
expect(toViewPath(c, "/archive")).toBe("/blog/en/archive");
|
|
216
|
+
expect(viewBasePath(c)).toBe("/en");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("strips the prefix when resolving against site content", () => {
|
|
220
|
+
expect(
|
|
221
|
+
viewRelativePath(fakeContext({ path: "/en/hello", viewLang: "en" })),
|
|
222
|
+
).toBe("/hello");
|
|
223
|
+
expect(viewRelativePath(fakeContext({ path: "/en", viewLang: "en" }))).toBe(
|
|
224
|
+
"/",
|
|
225
|
+
);
|
|
226
|
+
expect(viewRelativePath(fakeContext({ path: "/hello" }))).toBe("/hello");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("builds paths into another language's view", () => {
|
|
230
|
+
const c = fakeContext({ viewLang: "en" });
|
|
231
|
+
|
|
232
|
+
expect(toLanguagePath(c, "zh-Hans", "/archive")).toBe("/archive");
|
|
233
|
+
expect(toLanguagePath(c, "en", "/archive")).toBe("/en/archive");
|
|
234
|
+
expect(toLanguagePath(c, null, "/")).toBe("/");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("getViewLanguages", () => {
|
|
239
|
+
it("lists the primary language first", () => {
|
|
240
|
+
expect(
|
|
241
|
+
getViewLanguages(fakeContext({ additionalLanguages: ["en", "ja"] })),
|
|
242
|
+
).toEqual(["zh-Hans", "en", "ja"]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("is empty when the site serves one language", () => {
|
|
246
|
+
expect(
|
|
247
|
+
getViewLanguages(fakeContext({ multilingualEnabled: false })),
|
|
248
|
+
).toEqual([]);
|
|
249
|
+
expect(getViewLanguages(fakeContext({ additionalLanguages: [] }))).toEqual(
|
|
250
|
+
[],
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("languageScopeBasePath", () => {
|
|
256
|
+
it("scopes an active non-primary language to its prefix", () => {
|
|
257
|
+
expect(languageScopeBasePath(fakeContext(), "en")).toBe("/en");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("keeps the primary language at the root", () => {
|
|
261
|
+
expect(languageScopeBasePath(fakeContext(), "zh-Hans")).toBe("");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("stays at the root without a language", () => {
|
|
265
|
+
expect(languageScopeBasePath(fakeContext(), null)).toBe("");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("stays at the root while multilingual is off", () => {
|
|
269
|
+
// A post keeps its language column after the feature is switched off,
|
|
270
|
+
// but there is no /en view to scope its chrome to any more.
|
|
271
|
+
expect(
|
|
272
|
+
languageScopeBasePath(fakeContext({ multilingualEnabled: false }), "en"),
|
|
273
|
+
).toBe("");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("ignores a language the site no longer publishes", () => {
|
|
277
|
+
expect(languageScopeBasePath(fakeContext(), "ja")).toBe("");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("buildSurfaceAlternates", () => {
|
|
282
|
+
it("points at the same surface in every language", () => {
|
|
283
|
+
const c = fakeContext({ path: "/en/archive", viewLang: "en" });
|
|
284
|
+
|
|
285
|
+
expect(buildSurfaceAlternates(c)).toEqual([
|
|
286
|
+
{ hreflang: "zh-Hans", href: "https://example.com/archive" },
|
|
287
|
+
{ hreflang: "en", href: "https://example.com/en/archive" },
|
|
288
|
+
{ hreflang: "x-default", href: "https://example.com/archive" },
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("keeps the query string, so a filtered view alternates correctly", () => {
|
|
293
|
+
const c = fakeContext({ path: "/archive", query: "?year=2024" });
|
|
294
|
+
|
|
295
|
+
expect(buildSurfaceAlternates(c)[1]).toEqual({
|
|
296
|
+
hreflang: "en",
|
|
297
|
+
href: "https://example.com/en/archive?year=2024",
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("is empty without an absolute site URL, which hreflang requires", () => {
|
|
302
|
+
expect(buildSurfaceAlternates(fakeContext({ siteUrl: "" }))).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("is empty on a single-language site", () => {
|
|
306
|
+
expect(
|
|
307
|
+
buildSurfaceAlternates(fakeContext({ multilingualEnabled: false })),
|
|
308
|
+
).toEqual([]);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe("buildLanguageSwitcher", () => {
|
|
313
|
+
it("offers the current surface in each language", () => {
|
|
314
|
+
const c = fakeContext({ path: "/en/archive", viewLang: "en" });
|
|
315
|
+
|
|
316
|
+
expect(buildLanguageSwitcher(c)).toEqual([
|
|
317
|
+
{
|
|
318
|
+
lang: "zh-Hans",
|
|
319
|
+
label: "简体中文",
|
|
320
|
+
href: "/archive",
|
|
321
|
+
isCurrent: false,
|
|
322
|
+
isPrimary: true,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
lang: "en",
|
|
326
|
+
label: "English",
|
|
327
|
+
href: "/en/archive",
|
|
328
|
+
isCurrent: true,
|
|
329
|
+
isPrimary: false,
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("takes an explicit destination per language", () => {
|
|
335
|
+
// A post links to its translation, not to its own path in another view.
|
|
336
|
+
const c = fakeContext({ path: "/my-post" });
|
|
337
|
+
|
|
338
|
+
expect(
|
|
339
|
+
buildLanguageSwitcher(c, {
|
|
340
|
+
hrefByLanguage: new Map([["en", "/the-translation"]]),
|
|
341
|
+
fallbackPath: "/",
|
|
342
|
+
currentLang: "zh-Hans",
|
|
343
|
+
}),
|
|
344
|
+
).toEqual([
|
|
345
|
+
{
|
|
346
|
+
lang: "zh-Hans",
|
|
347
|
+
label: "简体中文",
|
|
348
|
+
href: "/",
|
|
349
|
+
isCurrent: true,
|
|
350
|
+
isPrimary: true,
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
lang: "en",
|
|
354
|
+
label: "English",
|
|
355
|
+
href: "/the-translation",
|
|
356
|
+
isCurrent: false,
|
|
357
|
+
isPrimary: false,
|
|
358
|
+
},
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("falls back to a language's home when there is no translation", () => {
|
|
363
|
+
const c = fakeContext({ path: "/my-post" });
|
|
364
|
+
|
|
365
|
+
const options = buildLanguageSwitcher(c, { fallbackPath: "/" });
|
|
366
|
+
expect(options.map((option) => option.href)).toEqual(["/", "/en"]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("is empty on a single-language site", () => {
|
|
370
|
+
expect(
|
|
371
|
+
buildLanguageSwitcher(fakeContext({ additionalLanguages: [] })),
|
|
372
|
+
).toEqual([]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("aims at each language's home from a page no language view serves", () => {
|
|
376
|
+
// /ja/settings/language would be a 404 — the switcher must never mint one.
|
|
377
|
+
const c = fakeContext({ path: "/settings/language" });
|
|
378
|
+
|
|
379
|
+
expect(buildLanguageSwitcher(c).map((option) => option.href)).toEqual([
|
|
380
|
+
"/",
|
|
381
|
+
"/en",
|
|
382
|
+
]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("isPerLanguageSurface", () => {
|
|
387
|
+
it("recognizes the surfaces the language routes serve", () => {
|
|
388
|
+
expect(isPerLanguageSurface("/")).toBe(true);
|
|
389
|
+
expect(isPerLanguageSurface("/archive")).toBe(true);
|
|
390
|
+
expect(isPerLanguageSurface("/collections/reading")).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("rejects everything without a per-language counterpart", () => {
|
|
394
|
+
expect(isPerLanguageSurface("/settings")).toBe(false);
|
|
395
|
+
expect(isPerLanguageSurface("/settings/language")).toBe(false);
|
|
396
|
+
expect(isPerLanguageSurface("/my-post")).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("counts a collection selection and its feed, and nothing deeper", () => {
|
|
400
|
+
// `langGet()` serves /collections/:selection and its feed, and nothing
|
|
401
|
+
// else under /collections.
|
|
402
|
+
expect(isPerLanguageSurface("/collections/a+b")).toBe(true);
|
|
403
|
+
expect(isPerLanguageSurface("/collections/a+b/feed")).toBe(true);
|
|
404
|
+
expect(isPerLanguageSurface("/collections/a+b/feed/atom")).toBe(false);
|
|
405
|
+
expect(isPerLanguageSurface("/collections/reading/edit")).toBe(false);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("ignores a query string, so a built link can be passed as-is", () => {
|
|
409
|
+
expect(isPerLanguageSurface("/archive?media=any")).toBe(true);
|
|
410
|
+
expect(isPerLanguageSurface("/search?q=hello")).toBe(true);
|
|
411
|
+
expect(isPerLanguageSurface("/settings?tab=general")).toBe(false);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { Bindings } from "../../types.js";
|
|
3
3
|
import { POST_BODY_HTML_VERSION } from "../post-body-html.js";
|
|
4
|
+
import { withConditionalResponse } from "../http-cache.js";
|
|
4
5
|
import {
|
|
5
6
|
normalizeWorkerCacheKeyUrl,
|
|
6
7
|
withWorkerResponseCache,
|
|
@@ -326,3 +327,92 @@ describe("withWorkerResponseCache", () => {
|
|
|
326
327
|
expect(nextCallCount).toBe(1);
|
|
327
328
|
});
|
|
328
329
|
});
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Revalidation composed with the cache, the way `createApp()` composes them:
|
|
333
|
+
* conditional handling on the outside, so it sees a cache hit and a cache miss
|
|
334
|
+
* alike.
|
|
335
|
+
*/
|
|
336
|
+
describe("withWorkerResponseCache under conditional requests", () => {
|
|
337
|
+
const FEED_URL = "https://example.com/feed";
|
|
338
|
+
const ETAG = '"feed-v1"';
|
|
339
|
+
|
|
340
|
+
function feedResponse() {
|
|
341
|
+
return new Response("<feed/>", {
|
|
342
|
+
headers: {
|
|
343
|
+
"Cache-Control": "public, max-age=60",
|
|
344
|
+
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
345
|
+
ETag: ETAG,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function poll(
|
|
351
|
+
cache: ReturnType<typeof createMemoryCache>,
|
|
352
|
+
ifNoneMatch: string | undefined,
|
|
353
|
+
next: () => Promise<Response>,
|
|
354
|
+
) {
|
|
355
|
+
const request = new Request(
|
|
356
|
+
FEED_URL,
|
|
357
|
+
ifNoneMatch ? { headers: { "If-None-Match": ifNoneMatch } } : undefined,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
return withConditionalResponse(
|
|
361
|
+
request,
|
|
362
|
+
await withWorkerResponseCache({
|
|
363
|
+
bindings: createCloudflareBindings(),
|
|
364
|
+
cache,
|
|
365
|
+
request,
|
|
366
|
+
next,
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
it("stores the full document even when the poll that filled the cache got a 304", async () => {
|
|
372
|
+
const cache = createMemoryCache();
|
|
373
|
+
let renders = 0;
|
|
374
|
+
const render = async () => {
|
|
375
|
+
renders += 1;
|
|
376
|
+
return feedResponse();
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// A reader that already holds this version, arriving on a cache miss.
|
|
380
|
+
const conditional = await poll(cache, ETAG, render);
|
|
381
|
+
expect(conditional.status).toBe(304);
|
|
382
|
+
expect(await conditional.text()).toBe("");
|
|
383
|
+
expect(renders).toBe(1);
|
|
384
|
+
|
|
385
|
+
// The next reader gets the document from the cache, not another render.
|
|
386
|
+
const unconditional = await poll(cache, undefined, render);
|
|
387
|
+
expect(unconditional.status).toBe(200);
|
|
388
|
+
expect(await unconditional.text()).toBe("<feed/>");
|
|
389
|
+
expect(renders).toBe(1);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("revalidates a cache hit without reaching a route", async () => {
|
|
393
|
+
const cache = createMemoryCache();
|
|
394
|
+
let renders = 0;
|
|
395
|
+
const render = async () => {
|
|
396
|
+
renders += 1;
|
|
397
|
+
return feedResponse();
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
await poll(cache, undefined, render);
|
|
401
|
+
const revalidated = await poll(cache, ETAG, render);
|
|
402
|
+
|
|
403
|
+
expect(revalidated.status).toBe(304);
|
|
404
|
+
expect(revalidated.headers.get("ETag")).toBe(ETAG);
|
|
405
|
+
expect(revalidated.headers.get("Cache-Control")).toBe("public, max-age=60");
|
|
406
|
+
expect(renders).toBe(1);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("serves the document again once the cached version no longer matches", async () => {
|
|
410
|
+
const cache = createMemoryCache();
|
|
411
|
+
|
|
412
|
+
await poll(cache, undefined, async () => feedResponse());
|
|
413
|
+
const stale = await poll(cache, '"feed-v0"', async () => feedResponse());
|
|
414
|
+
|
|
415
|
+
expect(stale.status).toBe(200);
|
|
416
|
+
expect(await stale.text()).toBe("<feed/>");
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Addresses the author pasted.
|
|
3
|
+
*
|
|
4
|
+
* Pickers ask "which page?" and the author often answers with a URL rather
|
|
5
|
+
* than a title — they have the page open in another tab. Turning that answer
|
|
6
|
+
* into something the path registry can resolve is the same job everywhere, and
|
|
7
|
+
* it is fiddly enough (site prefix, language prefix, which hosts are ours) to
|
|
8
|
+
* be worth doing in one place.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from "hono";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import type { Bindings } from "../types/bindings.js";
|
|
14
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
15
|
+
import { toInternalPath } from "./url.js";
|
|
16
|
+
import { languageUrlPrefixes } from "./view-language.js";
|
|
17
|
+
|
|
18
|
+
type AddressContext = Context<{ Bindings: Bindings; Variables: AppVariables }>;
|
|
19
|
+
|
|
20
|
+
/** Query shape of every "what is at this address?" endpoint. */
|
|
21
|
+
export const AddressQuerySchema = z.object({
|
|
22
|
+
url: z.string().trim().min(1).max(2048),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert an address into the internal path this site resolves it against.
|
|
27
|
+
*
|
|
28
|
+
* Accepts a path or a full URL on any host the site answers on — the
|
|
29
|
+
* configured one *and* the host of the current request, because a hosted site
|
|
30
|
+
* is routinely reached on a domain its canonical URL knows nothing about.
|
|
31
|
+
*
|
|
32
|
+
* @param c - Request context, after `withConfig()`
|
|
33
|
+
* @param input - What the author typed or pasted
|
|
34
|
+
* @returns Internal path rooted at `/`, or `null` when the address is not ours
|
|
35
|
+
* @example
|
|
36
|
+
* requestInternalPath(c, "https://example.com/en/about"); // "/about"
|
|
37
|
+
* requestInternalPath(c, "https://elsewhere.com/about"); // null
|
|
38
|
+
*/
|
|
39
|
+
export function requestInternalPath(
|
|
40
|
+
c: AddressContext,
|
|
41
|
+
input: string,
|
|
42
|
+
): string | null {
|
|
43
|
+
const { siteOrigin, sitePathPrefix } = c.var.appConfig;
|
|
44
|
+
|
|
45
|
+
let requestOrigin = "";
|
|
46
|
+
try {
|
|
47
|
+
requestOrigin = new URL(c.req.url).origin;
|
|
48
|
+
} catch {
|
|
49
|
+
// A request URL that will not parse is not one we can match against.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return toInternalPath(input, {
|
|
53
|
+
siteOrigins: [siteOrigin, requestOrigin].filter(Boolean),
|
|
54
|
+
sitePathPrefix,
|
|
55
|
+
languagePrefixes: languageUrlPrefixes(c.var.appConfig),
|
|
56
|
+
});
|
|
57
|
+
}
|
package/src/lib/api-posts.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isTextAttachment } from "../services/media.js";
|
|
2
2
|
import type { Media, Post } from "../types.js";
|
|
3
|
+
import { getPostDisplayTitle } from "./post-meta.js";
|
|
3
4
|
import { getImageUrl, getMediaUrl, getPublicUrlForProvider } from "./image.js";
|
|
4
5
|
import { toPublicPath } from "./url.js";
|
|
5
6
|
|
|
@@ -7,6 +8,12 @@ export type ApiPostResponse = Omit<Post, "title" | "url"> & {
|
|
|
7
8
|
attachments?: ReturnType<typeof toApiAttachment>[];
|
|
8
9
|
collectionIds?: string[];
|
|
9
10
|
title?: string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Short plain-text name for this Post, derived when it has no title of its
|
|
13
|
+
* own. Use it wherever the Post is referenced from somewhere else; a slug is
|
|
14
|
+
* a URL, not a name.
|
|
15
|
+
*/
|
|
16
|
+
displayTitle: string;
|
|
10
17
|
url?: string | null;
|
|
11
18
|
sourceName?: string | null;
|
|
12
19
|
sourceUrl?: string | null;
|
|
@@ -82,11 +89,15 @@ export function toApiPost(
|
|
|
82
89
|
} = {},
|
|
83
90
|
): ApiPostResponse {
|
|
84
91
|
const { title, url, ...rest } = post;
|
|
92
|
+
// A short name for this Post wherever it is referenced from somewhere else —
|
|
93
|
+
// notes are usually untitled, so the client cannot just read `title`.
|
|
94
|
+
const displayTitle = getPostDisplayTitle(post);
|
|
85
95
|
|
|
86
96
|
if (post.format === "quote") {
|
|
87
97
|
return {
|
|
88
98
|
...rest,
|
|
89
99
|
...extras,
|
|
100
|
+
displayTitle,
|
|
90
101
|
sourceName: title ?? null,
|
|
91
102
|
sourceUrl: url ?? null,
|
|
92
103
|
};
|
|
@@ -95,6 +106,7 @@ export function toApiPost(
|
|
|
95
106
|
return {
|
|
96
107
|
...rest,
|
|
97
108
|
...extras,
|
|
109
|
+
displayTitle,
|
|
98
110
|
title: title ?? null,
|
|
99
111
|
url: url ?? null,
|
|
100
112
|
};
|