@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,89 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { sendDiscoverPing } from "../discover-ping.js";
|
|
3
|
+
|
|
4
|
+
const NOW = 1_800_000_000;
|
|
5
|
+
|
|
6
|
+
/** No real waiting between attempts; the retry itself is what is under test. */
|
|
7
|
+
const options = {
|
|
8
|
+
endpoint: "https://directory.example/api/discover/ping",
|
|
9
|
+
feedUrl: "https://example.com/latest/feed",
|
|
10
|
+
now: () => NOW,
|
|
11
|
+
sleep: async () => {},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("sendDiscoverPing", () => {
|
|
15
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 202 }));
|
|
19
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.unstubAllGlobals();
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("posts the feed address and nothing else", async () => {
|
|
28
|
+
const outcome = await sendDiscoverPing(options);
|
|
29
|
+
|
|
30
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
31
|
+
const [url, init] = fetchMock.mock.calls[0] as [
|
|
32
|
+
string,
|
|
33
|
+
globalThis.RequestInit,
|
|
34
|
+
];
|
|
35
|
+
expect(url).toBe("https://directory.example/api/discover/ping");
|
|
36
|
+
expect(init.method).toBe("POST");
|
|
37
|
+
expect(JSON.parse(String(init.body))).toEqual({
|
|
38
|
+
feed: "https://example.com/latest/feed",
|
|
39
|
+
});
|
|
40
|
+
expect(outcome).toMatchObject({
|
|
41
|
+
at: NOW,
|
|
42
|
+
ok: true,
|
|
43
|
+
feedUrl: "https://example.com/latest/feed",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not retry an announcement that got through", async () => {
|
|
48
|
+
await sendDiscoverPing(options);
|
|
49
|
+
|
|
50
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// The failure worth retrying automatically: a directory mid-deploy, or one
|
|
54
|
+
// answering a transient 5xx. One more attempt costs nothing and fixes it.
|
|
55
|
+
it("retries once, and reports success when the second attempt lands", async () => {
|
|
56
|
+
fetchMock
|
|
57
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
58
|
+
.mockResolvedValueOnce(new Response(null, { status: 202 }));
|
|
59
|
+
|
|
60
|
+
const outcome = await sendDiscoverPing(options);
|
|
61
|
+
|
|
62
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
63
|
+
expect(outcome.ok).toBe(true);
|
|
64
|
+
expect(outcome.error).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// The whole point of the rewrite: a lost announcement leaves evidence the
|
|
68
|
+
// owner can read, instead of a line in a log nobody sees.
|
|
69
|
+
it("reports the status a refusing directory answered", async () => {
|
|
70
|
+
fetchMock.mockResolvedValue(new Response(null, { status: 403 }));
|
|
71
|
+
|
|
72
|
+
const outcome = await sendDiscoverPing(options);
|
|
73
|
+
|
|
74
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
75
|
+
expect(outcome).toMatchObject({ at: NOW, ok: false, status: 403 });
|
|
76
|
+
expect(outcome.error).toContain("403");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("reports a network failure rather than throwing", async () => {
|
|
80
|
+
fetchMock.mockRejectedValue(new Error("connect ECONNREFUSED"));
|
|
81
|
+
|
|
82
|
+
const outcome = await sendDiscoverPing(options);
|
|
83
|
+
|
|
84
|
+
expect(outcome.ok).toBe(false);
|
|
85
|
+
expect(outcome.error).toContain("ECONNREFUSED");
|
|
86
|
+
// No `status`: nothing answered, so claiming one would be a fabrication.
|
|
87
|
+
expect(outcome.status).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
discoverIntroRuns,
|
|
4
|
+
getDiscoverFeedPath,
|
|
5
|
+
getDiscoverPageUrls,
|
|
6
|
+
getDiscoverSubmitUrl,
|
|
7
|
+
linkTerms,
|
|
8
|
+
measureDiscoverMaturity,
|
|
9
|
+
parseDiscoverSetting,
|
|
10
|
+
resolveDiscoverMode,
|
|
11
|
+
} from "../discover.js";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_DISCOVER_PING_URL,
|
|
14
|
+
getDiscoverDirectoryBaseUrl,
|
|
15
|
+
getDiscoverPingUrl,
|
|
16
|
+
} from "../env.js";
|
|
17
|
+
|
|
18
|
+
function resolve(
|
|
19
|
+
overrides: Partial<Parameters<typeof resolveDiscoverMode>[0]>,
|
|
20
|
+
) {
|
|
21
|
+
return resolveDiscoverMode({
|
|
22
|
+
storedValue: null,
|
|
23
|
+
defaultValue: null,
|
|
24
|
+
demoMode: false,
|
|
25
|
+
noindex: false,
|
|
26
|
+
rssFeedsEnabled: true,
|
|
27
|
+
...overrides,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("parseDiscoverSetting", () => {
|
|
32
|
+
it("accepts the three stored values", () => {
|
|
33
|
+
expect(parseDiscoverSetting("latest")).toBe("latest");
|
|
34
|
+
expect(parseDiscoverSetting("featured")).toBe("featured");
|
|
35
|
+
expect(parseDiscoverSetting("off")).toBe("off");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("treats absent, blank, and unrecognized values as unset", () => {
|
|
39
|
+
expect(parseDiscoverSetting(undefined)).toBeNull();
|
|
40
|
+
expect(parseDiscoverSetting(null)).toBeNull();
|
|
41
|
+
expect(parseDiscoverSetting("")).toBeNull();
|
|
42
|
+
expect(parseDiscoverSetting(" ")).toBeNull();
|
|
43
|
+
expect(parseDiscoverSetting("true")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("resolveDiscoverMode", () => {
|
|
48
|
+
// The declaration is the consent record. A self-hosted site nobody
|
|
49
|
+
// configured has said nothing, and a directory must read that as a no.
|
|
50
|
+
it("keeps an untouched self-hosted site out", () => {
|
|
51
|
+
expect(resolve({})).toBe("none");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("honours the owner's stored choice", () => {
|
|
55
|
+
expect(resolve({ storedValue: "featured" })).toBe("featured");
|
|
56
|
+
expect(resolve({ storedValue: "latest" })).toBe("latest");
|
|
57
|
+
expect(resolve({ storedValue: "off" })).toBe("none");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// How hosted Jant lists its fleet without asking every owner.
|
|
61
|
+
it("takes the deployment default when nothing is stored", () => {
|
|
62
|
+
expect(resolve({ defaultValue: "latest" })).toBe("latest");
|
|
63
|
+
expect(resolve({ defaultValue: "featured" })).toBe("featured");
|
|
64
|
+
expect(resolve({ defaultValue: "off" })).toBe("none");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("lets the owner overrule the deployment default", () => {
|
|
68
|
+
expect(resolve({ defaultValue: "latest", storedValue: "off" })).toBe(
|
|
69
|
+
"none",
|
|
70
|
+
);
|
|
71
|
+
expect(resolve({ defaultValue: "off", storedValue: "latest" })).toBe(
|
|
72
|
+
"latest",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Hiding from search engines and being surfaced by a directory contradict
|
|
77
|
+
// each other, and the quieter reading is the safe one.
|
|
78
|
+
it("reads noindex as none while the setting is unset", () => {
|
|
79
|
+
expect(resolve({ noindex: true })).toBe("none");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// The whole reason the two are separate: a deployment-wide "list my blogs"
|
|
83
|
+
// is a default, not the answer of the owner who hid this one from search.
|
|
84
|
+
it("puts noindex above the deployment default", () => {
|
|
85
|
+
expect(resolve({ noindex: true, defaultValue: "latest" })).toBe("none");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("lets a stored choice override noindex", () => {
|
|
89
|
+
expect(resolve({ noindex: true, storedValue: "latest" })).toBe("latest");
|
|
90
|
+
expect(resolve({ noindex: true, storedValue: "featured" })).toBe(
|
|
91
|
+
"featured",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Demos exist to be thrown away; nothing they publish belongs in a directory.
|
|
96
|
+
it("locks demo sites out even when they ask to be listed", () => {
|
|
97
|
+
expect(resolve({ demoMode: true })).toBe("none");
|
|
98
|
+
expect(resolve({ demoMode: true, storedValue: "featured" })).toBe("none");
|
|
99
|
+
expect(resolve({ demoMode: true, defaultValue: "latest" })).toBe("none");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Every feed path 404s with feeds off, so there would be nothing to poll.
|
|
103
|
+
it("declares none when the site publishes no feeds", () => {
|
|
104
|
+
expect(resolve({ rssFeedsEnabled: false })).toBe("none");
|
|
105
|
+
expect(resolve({ rssFeedsEnabled: false, storedValue: "latest" })).toBe(
|
|
106
|
+
"none",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("ignores values it does not recognize, on either side", () => {
|
|
111
|
+
expect(resolve({ storedValue: "sometimes", defaultValue: "latest" })).toBe(
|
|
112
|
+
"latest",
|
|
113
|
+
);
|
|
114
|
+
expect(resolve({ defaultValue: "sometimes" })).toBe("none");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("getDiscoverFeedPath", () => {
|
|
119
|
+
it("names the feed each mode draws from", () => {
|
|
120
|
+
expect(getDiscoverFeedPath("latest")).toBe("/latest/feed");
|
|
121
|
+
expect(getDiscoverFeedPath("featured")).toBe("/featured/feed");
|
|
122
|
+
expect(getDiscoverFeedPath("none")).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The two states that matter are spelled differently, and collapsing them is
|
|
128
|
+
* how "switch the ping off" would quietly become "switch it on".
|
|
129
|
+
*/
|
|
130
|
+
describe("getDiscoverPingUrl", () => {
|
|
131
|
+
it("uses Jant's directory when nothing is configured", () => {
|
|
132
|
+
expect(getDiscoverPingUrl({})).toBe(DEFAULT_DISCOVER_PING_URL);
|
|
133
|
+
expect(getDiscoverPingUrl(undefined)).toBe(DEFAULT_DISCOVER_PING_URL);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("announces nowhere when the binding is set but empty", () => {
|
|
137
|
+
expect(getDiscoverPingUrl({ DISCOVER_PING_URL: "" })).toBeUndefined();
|
|
138
|
+
expect(getDiscoverPingUrl({ DISCOVER_PING_URL: " " })).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("uses a directory of the operator's own", () => {
|
|
142
|
+
expect(
|
|
143
|
+
getDiscoverPingUrl({
|
|
144
|
+
DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
|
|
145
|
+
}),
|
|
146
|
+
).toBe("https://directory.example/api/discover/ping");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Naming the directory twice is what let a hosted deployment announce its
|
|
150
|
+
// blogs to jant.me, which had never heard of them.
|
|
151
|
+
it("announces to the control plane that hosts this deployment", () => {
|
|
152
|
+
expect(
|
|
153
|
+
getDiscoverPingUrl({
|
|
154
|
+
HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
|
|
155
|
+
}),
|
|
156
|
+
).toBe("https://cloud.example/api/discover/ping");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Server-to-server, like every other core to control-plane call.
|
|
160
|
+
it("prefers the control plane's internal address", () => {
|
|
161
|
+
expect(
|
|
162
|
+
getDiscoverPingUrl({
|
|
163
|
+
HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
|
|
164
|
+
HOSTED_CONTROL_PLANE_INTERNAL_BASE_URL: "http://127.0.0.1:3300",
|
|
165
|
+
}),
|
|
166
|
+
).toBe("http://127.0.0.1:3300/api/discover/ping");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("lets an explicit directory override the control plane", () => {
|
|
170
|
+
expect(
|
|
171
|
+
getDiscoverPingUrl({
|
|
172
|
+
DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
|
|
173
|
+
HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
|
|
174
|
+
}),
|
|
175
|
+
).toBe("https://directory.example/api/discover/ping");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("still announces nowhere when a hosted deployment empties the binding", () => {
|
|
179
|
+
expect(
|
|
180
|
+
getDiscoverPingUrl({
|
|
181
|
+
DISCOVER_PING_URL: "",
|
|
182
|
+
HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
|
|
183
|
+
}),
|
|
184
|
+
).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("falls back to Jant's directory when the control plane URL is unusable", () => {
|
|
188
|
+
expect(
|
|
189
|
+
getDiscoverPingUrl({ HOSTED_CONTROL_PLANE_BASE_URL: "not a url" }),
|
|
190
|
+
).toBe(DEFAULT_DISCOVER_PING_URL);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The address a browser opens, for the same directory the ping goes to. The
|
|
196
|
+
* two must never name different directories, which is why they share a source.
|
|
197
|
+
*/
|
|
198
|
+
describe("getDiscoverDirectoryBaseUrl", () => {
|
|
199
|
+
it("uses Jant's directory when nothing is configured", () => {
|
|
200
|
+
expect(getDiscoverDirectoryBaseUrl({})).toBe("https://jant.me/");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// The public address, not the internal one the ping uses: this ends up in an
|
|
204
|
+
// href, and `127.0.0.1` is not a directory anybody can visit.
|
|
205
|
+
it("links to the control plane's public address", () => {
|
|
206
|
+
expect(
|
|
207
|
+
getDiscoverDirectoryBaseUrl({
|
|
208
|
+
HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud.example",
|
|
209
|
+
HOSTED_CONTROL_PLANE_INTERNAL_BASE_URL: "http://127.0.0.1:3300",
|
|
210
|
+
}),
|
|
211
|
+
).toBe("https://cloud.example/");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("follows an explicitly configured directory", () => {
|
|
215
|
+
expect(
|
|
216
|
+
getDiscoverDirectoryBaseUrl({
|
|
217
|
+
DISCOVER_PING_URL: "https://directory.example/api/discover/ping",
|
|
218
|
+
}),
|
|
219
|
+
).toBe("https://directory.example/");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("has nothing to link to when announcing is off", () => {
|
|
223
|
+
expect(
|
|
224
|
+
getDiscoverDirectoryBaseUrl({ DISCOVER_PING_URL: "" }),
|
|
225
|
+
).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("measureDiscoverMaturity", () => {
|
|
230
|
+
it("passes a blog that has published something", () => {
|
|
231
|
+
expect(measureDiscoverMaturity({ publicPostCount: 5 })).toEqual({
|
|
232
|
+
publicPostCount: 5,
|
|
233
|
+
established: true,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// One post is the whole threshold; what keeps throwaways out is the opt-in.
|
|
238
|
+
it("passes a blog with a single public post", () => {
|
|
239
|
+
expect(measureDiscoverMaturity({ publicPostCount: 1 })).toMatchObject({
|
|
240
|
+
established: true,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("holds back a blog with nothing public in it", () => {
|
|
245
|
+
expect(measureDiscoverMaturity({ publicPostCount: 0 })).toEqual({
|
|
246
|
+
publicPostCount: 0,
|
|
247
|
+
established: false,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("getDiscoverSubmitUrl", () => {
|
|
253
|
+
it("finds the form beside the directory's own ping endpoint", () => {
|
|
254
|
+
expect(getDiscoverSubmitUrl("https://jant.me/api/discover/ping")).toBe(
|
|
255
|
+
"https://jant.me/discover/submit",
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Provider-neutral on purpose: a site announcing to its own directory must
|
|
260
|
+
// not be sent to somebody else's form.
|
|
261
|
+
it("follows a directory of your own", () => {
|
|
262
|
+
expect(
|
|
263
|
+
getDiscoverSubmitUrl("https://directory.example/api/discover/ping"),
|
|
264
|
+
).toBe("https://directory.example/discover/submit");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("has nothing to offer when no directory is configured", () => {
|
|
268
|
+
expect(getDiscoverSubmitUrl(undefined)).toBeNull();
|
|
269
|
+
expect(getDiscoverSubmitUrl("")).toBeNull();
|
|
270
|
+
expect(getDiscoverSubmitUrl("not a url")).toBeNull();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("getDiscoverPageUrls", () => {
|
|
275
|
+
// The settings page links the directory's name to the directory itself, so
|
|
276
|
+
// "what is Discover" is answered by the list rather than by a page about it.
|
|
277
|
+
// The three lists sit flat on jant.me; only the rules sit under /discover.
|
|
278
|
+
it("finds the directory's pages behind its own ping endpoint", () => {
|
|
279
|
+
expect(getDiscoverPageUrls("https://jant.me/api/discover/ping")).toEqual({
|
|
280
|
+
home: "https://jant.me/discover",
|
|
281
|
+
links: "https://jant.me/links",
|
|
282
|
+
quotes: "https://jant.me/quotes",
|
|
283
|
+
rules: "https://jant.me/discover/about",
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("follows a directory of your own", () => {
|
|
288
|
+
expect(
|
|
289
|
+
getDiscoverPageUrls("https://directory.example/api/discover/ping"),
|
|
290
|
+
).toEqual({
|
|
291
|
+
home: "https://directory.example/discover",
|
|
292
|
+
links: "https://directory.example/links",
|
|
293
|
+
quotes: "https://directory.example/quotes",
|
|
294
|
+
rules: "https://directory.example/discover/about",
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("has nothing to link to when no directory is configured", () => {
|
|
299
|
+
expect(getDiscoverPageUrls(undefined)).toBeNull();
|
|
300
|
+
expect(getDiscoverPageUrls("")).toBeNull();
|
|
301
|
+
expect(getDiscoverPageUrls("not a url")).toBeNull();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("linkTerms", () => {
|
|
306
|
+
const links = [
|
|
307
|
+
{ term: "Jant Discover", href: "/discover" },
|
|
308
|
+
{ term: "Links", href: "/links" },
|
|
309
|
+
{ term: "Quotes", href: "/quotes" },
|
|
310
|
+
{ term: "Discover community rules", href: "/discover/about" },
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
it("links every term where the sentence puts it", () => {
|
|
314
|
+
expect(
|
|
315
|
+
linkTerms(
|
|
316
|
+
"Jant Discover lists link and quote posts on Links and Quotes. See the Discover community rules.",
|
|
317
|
+
links,
|
|
318
|
+
),
|
|
319
|
+
).toEqual([
|
|
320
|
+
{ text: "Jant Discover", href: "/discover" },
|
|
321
|
+
{ text: " lists link and quote posts on " },
|
|
322
|
+
{ text: "Links", href: "/links" },
|
|
323
|
+
{ text: " and " },
|
|
324
|
+
{ text: "Quotes", href: "/quotes" },
|
|
325
|
+
{ text: ". See the " },
|
|
326
|
+
{ text: "Discover community rules", href: "/discover/about" },
|
|
327
|
+
{ text: "." },
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// A translation owns its word order; the links follow the words, not the
|
|
332
|
+
// order they were given in.
|
|
333
|
+
it("follows the translation's word order", () => {
|
|
334
|
+
expect(
|
|
335
|
+
linkTerms("详见 Discover 社区规则。Jant Discover 是一个目录。", [
|
|
336
|
+
{ term: "Jant Discover", href: "/discover" },
|
|
337
|
+
{ term: "Discover 社区规则", href: "/discover/about" },
|
|
338
|
+
]),
|
|
339
|
+
).toEqual([
|
|
340
|
+
{ text: "详见 " },
|
|
341
|
+
{ text: "Discover 社区规则", href: "/discover/about" },
|
|
342
|
+
{ text: "。" },
|
|
343
|
+
{ text: "Jant Discover", href: "/discover" },
|
|
344
|
+
{ text: " 是一个目录。" },
|
|
345
|
+
]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// "Discover" alone sits inside "Jant Discover"; whichever the sentence
|
|
349
|
+
// mentions first, the longer name keeps its whole run.
|
|
350
|
+
it("never lets a term inside another take the longer one's place", () => {
|
|
351
|
+
expect(
|
|
352
|
+
linkTerms("Jant Discover reads your feed. Discover shows it.", [
|
|
353
|
+
{ term: "Discover", href: "/about" },
|
|
354
|
+
{ term: "Jant Discover", href: "/discover" },
|
|
355
|
+
]),
|
|
356
|
+
).toEqual([
|
|
357
|
+
{ text: "Jant Discover", href: "/discover" },
|
|
358
|
+
{ text: " reads your feed. " },
|
|
359
|
+
{ text: "Discover", href: "/about" },
|
|
360
|
+
{ text: " shows it." },
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("leaves a term the translation dropped as plain text", () => {
|
|
365
|
+
expect(
|
|
366
|
+
linkTerms("A directory of Jant blogs.", [
|
|
367
|
+
{ term: "Jant Discover", href: "/discover" },
|
|
368
|
+
]),
|
|
369
|
+
).toEqual([{ text: "A directory of Jant blogs." }]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("links nothing for an empty term or address", () => {
|
|
373
|
+
expect(
|
|
374
|
+
linkTerms("See Links.", [
|
|
375
|
+
{ term: "", href: "/links" },
|
|
376
|
+
{ term: "Links", href: "" },
|
|
377
|
+
]),
|
|
378
|
+
).toEqual([{ text: "See Links." }]);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("discoverIntroRuns", () => {
|
|
383
|
+
const terms = { name: "Jant Discover", rules: "Discover community rules" };
|
|
384
|
+
const intro =
|
|
385
|
+
"Jant Discover lists link and quote posts on Links and Quotes. See the Discover community rules.";
|
|
386
|
+
|
|
387
|
+
// The settings page and the setup screen both render the line through here,
|
|
388
|
+
// so this is the one place that says which words go where.
|
|
389
|
+
it("links the name, both lists and the rules to their pages", () => {
|
|
390
|
+
expect(
|
|
391
|
+
discoverIntroRuns(intro, terms, getDiscoverPageUrls("https://jant.me/")),
|
|
392
|
+
).toEqual([
|
|
393
|
+
{ text: "Jant Discover", href: "https://jant.me/discover" },
|
|
394
|
+
{ text: " lists link and quote posts on " },
|
|
395
|
+
{ text: "Links", href: "https://jant.me/links" },
|
|
396
|
+
{ text: " and " },
|
|
397
|
+
{ text: "Quotes", href: "https://jant.me/quotes" },
|
|
398
|
+
{ text: ". See the " },
|
|
399
|
+
{
|
|
400
|
+
text: "Discover community rules",
|
|
401
|
+
href: "https://jant.me/discover/about",
|
|
402
|
+
},
|
|
403
|
+
{ text: "." },
|
|
404
|
+
]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("leaves the whole line plain when there is no directory", () => {
|
|
408
|
+
expect(discoverIntroRuns(intro, terms, null)).toEqual([{ text: intro }]);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
FEED_LIMIT_MAX,
|
|
3
4
|
getFeedEntryUpdatedAt,
|
|
4
5
|
getRssPublishedBefore,
|
|
6
|
+
parseFeedLimit,
|
|
5
7
|
} from "../feed-policy.js";
|
|
6
8
|
|
|
9
|
+
describe("parseFeedLimit", () => {
|
|
10
|
+
it("keeps the site's length when the parameter is absent", () => {
|
|
11
|
+
expect(parseFeedLimit(undefined, 50)).toBe(50);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("honours a request above or below the site's length", () => {
|
|
15
|
+
expect(parseFeedLimit("200", 50)).toBe(200);
|
|
16
|
+
expect(parseFeedLimit("5", 50)).toBe(5);
|
|
17
|
+
expect(parseFeedLimit("050", 50)).toBe(50);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("gives the most there is to a request past the ceiling", () => {
|
|
21
|
+
expect(parseFeedLimit(String(FEED_LIMIT_MAX), 50)).toBe(FEED_LIMIT_MAX);
|
|
22
|
+
expect(parseFeedLimit(String(FEED_LIMIT_MAX + 1), 50)).toBe(FEED_LIMIT_MAX);
|
|
23
|
+
expect(parseFeedLimit("99999999999999999999", 50)).toBe(FEED_LIMIT_MAX);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("ignores anything that is not a positive whole number", () => {
|
|
27
|
+
for (const raw of ["", "0", "-3", "1.5", "abc", "10abc", " 10", "1e3"]) {
|
|
28
|
+
expect(parseFeedLimit(raw, 50)).toBe(50);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
7
33
|
describe("feed policy", () => {
|
|
8
34
|
it("makes the exact delay boundary eligible", () => {
|
|
9
35
|
expect(getRssPublishedBefore(300, 1_000)).toBe(701);
|