@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
|
@@ -11,8 +11,9 @@ async function loadGeneralContent() {
|
|
|
11
11
|
|
|
12
12
|
function renderGeneralContent(
|
|
13
13
|
props: Parameters<Awaited<ReturnType<typeof loadGeneralContent>>>[0],
|
|
14
|
+
locale: Parameters<typeof createI18n>[0] = "en",
|
|
14
15
|
) {
|
|
15
|
-
const i18n = createI18n(
|
|
16
|
+
const i18n = createI18n(locale);
|
|
16
17
|
const c = {
|
|
17
18
|
get(key: string) {
|
|
18
19
|
if (key === "i18n") return i18n;
|
|
@@ -36,9 +37,6 @@ function createProps(
|
|
|
36
37
|
return {
|
|
37
38
|
siteName: "My Blog",
|
|
38
39
|
siteDescription: "A test blog",
|
|
39
|
-
siteLanguage: "en",
|
|
40
|
-
dashboardLanguage: "en",
|
|
41
|
-
cjkSerifFont: "off",
|
|
42
40
|
siteNameFallback: "Fallback Name",
|
|
43
41
|
siteDescriptionFallback: "Fallback Description",
|
|
44
42
|
mainRssFeed: "featured" as const,
|
|
@@ -50,6 +48,30 @@ function createProps(
|
|
|
50
48
|
siteFooter: "Footer text",
|
|
51
49
|
showJantBrandingOnHome: false,
|
|
52
50
|
noindex: false,
|
|
51
|
+
discover: "",
|
|
52
|
+
// The deployment has no answer of its own; a self-hosted site opts in.
|
|
53
|
+
discoverDefault: "" as const,
|
|
54
|
+
discoverPages: {
|
|
55
|
+
home: "https://jant.me/discover",
|
|
56
|
+
links: "https://jant.me/links",
|
|
57
|
+
quotes: "https://jant.me/quotes",
|
|
58
|
+
rules: "https://jant.me/discover/about",
|
|
59
|
+
},
|
|
60
|
+
discoverStatus: {
|
|
61
|
+
announced: true,
|
|
62
|
+
announceError: null,
|
|
63
|
+
announceAt: STALE_ANNOUNCE_AT,
|
|
64
|
+
hasDirectory: true,
|
|
65
|
+
managedByHost: false,
|
|
66
|
+
submitUrl: "https://jant.me/discover/submit",
|
|
67
|
+
declaredMode: "latest" as const,
|
|
68
|
+
publicPostCount: 5,
|
|
69
|
+
featuredPostCount: 2,
|
|
70
|
+
established: true,
|
|
71
|
+
minPublicPosts: 1,
|
|
72
|
+
firstReadMaxHours: 6,
|
|
73
|
+
},
|
|
74
|
+
rssFeedsEnabled: true,
|
|
53
75
|
demoMode,
|
|
54
76
|
aboutPage: {
|
|
55
77
|
state: "missing" as const,
|
|
@@ -69,6 +91,12 @@ function createProps(
|
|
|
69
91
|
};
|
|
70
92
|
}
|
|
71
93
|
|
|
94
|
+
// The first-read line is the one status sentence that expires, so the fixtures
|
|
95
|
+
// place the announcement relative to the clock rather than on a fixed date that
|
|
96
|
+
// drifts in and out of the window as time passes.
|
|
97
|
+
const STALE_ANNOUNCE_AT = Math.floor(Date.now() / 1000) - 7 * 24 * 3600;
|
|
98
|
+
const FRESH_ANNOUNCE_AT = Math.floor(Date.now() / 1000) - 60;
|
|
99
|
+
|
|
72
100
|
describe("GeneralContent", () => {
|
|
73
101
|
it("omits the demo-mode attribute when demo mode is disabled", async () => {
|
|
74
102
|
const html = await renderGeneralContent(createProps(false));
|
|
@@ -81,4 +109,296 @@ describe("GeneralContent", () => {
|
|
|
81
109
|
|
|
82
110
|
expect(html).toMatch(/<jant-settings-general[^>]*demo-mode(?:=|\s|>)/);
|
|
83
111
|
});
|
|
112
|
+
|
|
113
|
+
// The client turns the name and the rules into links by finding them in the
|
|
114
|
+
// help line, so the labels have to agree with the sentence character for
|
|
115
|
+
// character. A translation that reworded one and not the other would only
|
|
116
|
+
// lose that link, but this catches it here rather than in a screenshot.
|
|
117
|
+
it("carries every linked term verbatim inside the help line", async () => {
|
|
118
|
+
const html = await renderGeneralContent(createProps(false));
|
|
119
|
+
|
|
120
|
+
expect(html).toContain(
|
|
121
|
+
"Jant Discover is a directory of Jant blogs, curated by hand by the Jant community",
|
|
122
|
+
);
|
|
123
|
+
expect(html).toContain(
|
|
124
|
+
"appear on the Links and Quotes lists 24 hours after they are published",
|
|
125
|
+
);
|
|
126
|
+
expect(html).toContain("See the Discover community rules.");
|
|
127
|
+
expect(html).toContain(
|
|
128
|
+
""discoverName":"Jant Discover"",
|
|
129
|
+
);
|
|
130
|
+
expect(html).toContain(
|
|
131
|
+
""discoverRules":"Discover community rules"",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Translations are written by hand or by a model, and one that rewords a
|
|
136
|
+
// placeholder's value, or drops the placeholder, loses that link without a
|
|
137
|
+
// word. Every catalog has to keep all four runs verbatim.
|
|
138
|
+
it.each(["en", "zh-Hans", "zh-Hant"] as const)(
|
|
139
|
+
"keeps every linked term inside the %s help line",
|
|
140
|
+
async (locale) => {
|
|
141
|
+
const html = await renderGeneralContent(createProps(false), locale);
|
|
142
|
+
const attribute = /<jant-settings-general[^>]*\slabels="([^"]*)"/.exec(
|
|
143
|
+
html,
|
|
144
|
+
)?.[1];
|
|
145
|
+
const labels = JSON.parse(
|
|
146
|
+
(attribute ?? "{}").replaceAll(""", '"').replaceAll("&", "&"),
|
|
147
|
+
) as Record<string, string>;
|
|
148
|
+
const intro = labels["discoverIntro"] ?? "";
|
|
149
|
+
|
|
150
|
+
for (const term of [
|
|
151
|
+
labels["discoverName"],
|
|
152
|
+
"Links",
|
|
153
|
+
"Quotes",
|
|
154
|
+
labels["discoverRules"],
|
|
155
|
+
]) {
|
|
156
|
+
expect(term).toBeTruthy();
|
|
157
|
+
expect(intro).toContain(term);
|
|
158
|
+
}
|
|
159
|
+
// And the delay, so an author knows how long they have to edit.
|
|
160
|
+
expect(intro).toContain("24");
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
it("hands the browser every page the help line links to", async () => {
|
|
165
|
+
const html = await renderGeneralContent(createProps(false));
|
|
166
|
+
|
|
167
|
+
expect(html).toContain(
|
|
168
|
+
'discover-pages="{"home":"https://jant.me/discover","links":"https://jant.me/links","quotes":"https://jant.me/quotes","rules":"https://jant.me/discover/about"}"',
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// No directory, no addresses: the help line then renders as plain text.
|
|
173
|
+
it("omits the directory's pages when none is configured", async () => {
|
|
174
|
+
const html = await renderGeneralContent(
|
|
175
|
+
createProps(false, { discoverPages: null }),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(html).not.toContain("discover-pages");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// The browser decides what the site declares, because two of the inputs are
|
|
182
|
+
// controls on this page. Resolving `noindex` into the default here would put
|
|
183
|
+
// that decision back on the server and freeze it at page load — the checkbox
|
|
184
|
+
// would then go on claiming a listing the feed had already dropped.
|
|
185
|
+
it("hands the browser the deployment default with noindex unresolved", async () => {
|
|
186
|
+
const html = await renderGeneralContent(
|
|
187
|
+
createProps(false, { discoverDefault: "latest", noindex: true }),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(html).toContain('discover-default="latest"');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// The controls above the block already say that the site is listed and what
|
|
194
|
+
// its feed declares, and a site past the directory's threshold has nothing
|
|
195
|
+
// to do about being past it. A site in that state is told nothing at all.
|
|
196
|
+
it("says nothing when the setting is already doing what it says", async () => {
|
|
197
|
+
const html = await renderGeneralContent(createProps(false));
|
|
198
|
+
|
|
199
|
+
expect(html).toContain('discover-status="{"lines":[]');
|
|
200
|
+
// Nothing failed, so neither the retry nor the manual form is offered.
|
|
201
|
+
expect(html).toContain(""showAnnounce":false");
|
|
202
|
+
expect(html).toContain(""submitUrl":null");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// The status sentences carry runtime numbers, so they are translated on the
|
|
206
|
+
// server rather than in the browser. What reaches the component is finished
|
|
207
|
+
// text — and this one only for as long as the answer is outstanding.
|
|
208
|
+
it("confirms an announcement while its first read is still due", async () => {
|
|
209
|
+
const html = await renderGeneralContent(
|
|
210
|
+
createProps(false, {
|
|
211
|
+
discoverStatus: {
|
|
212
|
+
...createProps(false).discoverStatus,
|
|
213
|
+
announceAt: FRESH_ANNOUNCE_AT,
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(html).toContain(
|
|
219
|
+
"Feed address sent. A directory reads a newly announced feed within 6 hours.",
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// An opt-in that cannot take effect is worth saying, because the ticked box
|
|
224
|
+
// above claims the opposite.
|
|
225
|
+
it("says the opt-in is not yet effective without a public post", async () => {
|
|
226
|
+
const html = await renderGeneralContent(
|
|
227
|
+
createProps(false, {
|
|
228
|
+
discoverStatus: {
|
|
229
|
+
...createProps(false).discoverStatus,
|
|
230
|
+
publicPostCount: 0,
|
|
231
|
+
featuredPostCount: 0,
|
|
232
|
+
established: false,
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(html).toContain(
|
|
238
|
+
"Nothing published yet. jant.me lists a blog once it has one public post.",
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("says a featured-only feed has nothing to carry", async () => {
|
|
243
|
+
const html = await renderGeneralContent(
|
|
244
|
+
createProps(false, {
|
|
245
|
+
discoverStatus: {
|
|
246
|
+
...createProps(false).discoverStatus,
|
|
247
|
+
declaredMode: "featured" as const,
|
|
248
|
+
featuredPostCount: 0,
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(html).toContain("no post is marked Featured");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("offers the manual form only when the announcement failed", async () => {
|
|
257
|
+
const html = await renderGeneralContent(
|
|
258
|
+
createProps(false, {
|
|
259
|
+
discoverStatus: {
|
|
260
|
+
announced: false,
|
|
261
|
+
announceError: "The directory answered 503.",
|
|
262
|
+
announceAt: STALE_ANNOUNCE_AT,
|
|
263
|
+
hasDirectory: true,
|
|
264
|
+
managedByHost: false,
|
|
265
|
+
submitUrl: "https://jant.me/discover/submit",
|
|
266
|
+
declaredMode: "latest",
|
|
267
|
+
publicPostCount: 5,
|
|
268
|
+
featuredPostCount: 2,
|
|
269
|
+
established: true,
|
|
270
|
+
minPublicPosts: 1,
|
|
271
|
+
firstReadMaxHours: 6,
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(html).toContain(
|
|
277
|
+
"The directory could not be reached: The directory answered 503.",
|
|
278
|
+
);
|
|
279
|
+
expect(html).toContain(""showAnnounce":true");
|
|
280
|
+
expect(html).toContain("https://jant.me/discover/submit");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// A site whose mode came from the deployment's own default declares itself
|
|
284
|
+
// without ever having announced. Nothing else on the page sends the address
|
|
285
|
+
// now that the section saves on change, so the button has to.
|
|
286
|
+
it("offers the announcement to a site that has never made one", async () => {
|
|
287
|
+
const html = await renderGeneralContent(
|
|
288
|
+
createProps(false, {
|
|
289
|
+
discoverStatus: {
|
|
290
|
+
announced: null,
|
|
291
|
+
announceError: null,
|
|
292
|
+
announceAt: null,
|
|
293
|
+
hasDirectory: true,
|
|
294
|
+
managedByHost: false,
|
|
295
|
+
submitUrl: "https://jant.me/discover/submit",
|
|
296
|
+
declaredMode: "latest",
|
|
297
|
+
publicPostCount: 5,
|
|
298
|
+
featuredPostCount: 2,
|
|
299
|
+
established: true,
|
|
300
|
+
minPublicPosts: 1,
|
|
301
|
+
firstReadMaxHours: 6,
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
expect(html).toContain(
|
|
307
|
+
"Not announced yet. No directory has been told this site exists.",
|
|
308
|
+
);
|
|
309
|
+
expect(html).toContain(""showAnnounce":true");
|
|
310
|
+
// The manual form stays with the failure it belongs to.
|
|
311
|
+
expect(html).toContain(""submitUrl":null");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("reports nothing at all when the feed declares none", async () => {
|
|
315
|
+
const html = await renderGeneralContent(
|
|
316
|
+
createProps(false, {
|
|
317
|
+
discoverStatus: {
|
|
318
|
+
announced: null,
|
|
319
|
+
announceError: null,
|
|
320
|
+
announceAt: null,
|
|
321
|
+
hasDirectory: true,
|
|
322
|
+
managedByHost: false,
|
|
323
|
+
submitUrl: "https://jant.me/discover/submit",
|
|
324
|
+
declaredMode: "none",
|
|
325
|
+
publicPostCount: 5,
|
|
326
|
+
featuredPostCount: 2,
|
|
327
|
+
established: true,
|
|
328
|
+
minPublicPosts: 1,
|
|
329
|
+
firstReadMaxHours: 6,
|
|
330
|
+
},
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// A site that is not listed is told nothing: not that it is not listed —
|
|
335
|
+
// the unticked checkbox says that — not how close it is to a threshold it
|
|
336
|
+
// has opted out of, and not whether an announcement it never made got
|
|
337
|
+
// through. With no lines the component drops the whole block.
|
|
338
|
+
expect(html).toContain('discover-status="{"lines":[]');
|
|
339
|
+
expect(html).not.toContain("Not announced yet");
|
|
340
|
+
expect(html).not.toContain("jant.me lists a blog once");
|
|
341
|
+
expect(html).toContain(""showAnnounce":false");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// The announcement answers "does the directory know my address". A blog on
|
|
345
|
+
// a hosted platform never has to ask: the control plane enrols its whole
|
|
346
|
+
// fleet, so an owner who has never touched this control is already listed,
|
|
347
|
+
// and telling them otherwise sends them after a task that does not exist.
|
|
348
|
+
it("says nothing about announcing on a hosted blog", async () => {
|
|
349
|
+
const html = await renderGeneralContent(
|
|
350
|
+
createProps(false, {
|
|
351
|
+
discoverStatus: {
|
|
352
|
+
announced: null,
|
|
353
|
+
announceError: null,
|
|
354
|
+
announceAt: null,
|
|
355
|
+
hasDirectory: true,
|
|
356
|
+
managedByHost: true,
|
|
357
|
+
submitUrl: "https://jant.me/discover/submit",
|
|
358
|
+
declaredMode: "latest",
|
|
359
|
+
publicPostCount: 5,
|
|
360
|
+
featuredPostCount: 2,
|
|
361
|
+
established: true,
|
|
362
|
+
minPublicPosts: 1,
|
|
363
|
+
firstReadMaxHours: 6,
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(html).not.toContain("Not announced yet");
|
|
369
|
+
expect(html).not.toContain("https://jant.me/discover/submit");
|
|
370
|
+
expect(html).toContain(""showAnnounce":false");
|
|
371
|
+
expect(html).toContain(""submitUrl":null");
|
|
372
|
+
// Nothing is left: the announcement was the only part of this block a
|
|
373
|
+
// hosted owner could ever have acted on.
|
|
374
|
+
expect(html).toContain('discover-status="{"lines":[]');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// A failed announcement is the one case that used to leave a hosted owner
|
|
378
|
+
// reading an error about their own host's plumbing, with a Retry and a
|
|
379
|
+
// manual submission form for a directory that already has them.
|
|
380
|
+
it("hides the retry and the manual form on a hosted blog", async () => {
|
|
381
|
+
const html = await renderGeneralContent(
|
|
382
|
+
createProps(false, {
|
|
383
|
+
discoverStatus: {
|
|
384
|
+
announced: false,
|
|
385
|
+
announceError: "The directory answered 503.",
|
|
386
|
+
announceAt: STALE_ANNOUNCE_AT,
|
|
387
|
+
hasDirectory: true,
|
|
388
|
+
managedByHost: true,
|
|
389
|
+
submitUrl: "https://jant.me/discover/submit",
|
|
390
|
+
declaredMode: "latest",
|
|
391
|
+
publicPostCount: 5,
|
|
392
|
+
featuredPostCount: 2,
|
|
393
|
+
established: true,
|
|
394
|
+
minPublicPosts: 1,
|
|
395
|
+
firstReadMaxHours: 6,
|
|
396
|
+
},
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
expect(html).not.toContain("The directory could not be reached");
|
|
401
|
+
expect(html).toContain(""showAnnounce":false");
|
|
402
|
+
expect(html).toContain(""submitUrl":null");
|
|
403
|
+
});
|
|
84
404
|
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { renderToString } from "hono/jsx/dom/server";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { I18nProvider } from "../../../../i18n/context.js";
|
|
5
|
+
import { createI18n } from "../../../../i18n/i18n.js";
|
|
6
|
+
|
|
7
|
+
async function loadLanguageContent() {
|
|
8
|
+
const { LanguageContent } = await import("../LanguageContent.js");
|
|
9
|
+
return LanguageContent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type LanguageContentProps = Parameters<
|
|
13
|
+
Awaited<ReturnType<typeof loadLanguageContent>>
|
|
14
|
+
>[0];
|
|
15
|
+
|
|
16
|
+
function renderLanguageContent(overrides: Partial<LanguageContentProps> = {}) {
|
|
17
|
+
const i18n = createI18n("en");
|
|
18
|
+
const c = {
|
|
19
|
+
get(key: string) {
|
|
20
|
+
if (key === "i18n") return i18n;
|
|
21
|
+
return undefined;
|
|
22
|
+
},
|
|
23
|
+
} as unknown as Context;
|
|
24
|
+
|
|
25
|
+
I18nProvider({ c, children: "" });
|
|
26
|
+
|
|
27
|
+
return loadLanguageContent().then((LanguageContent) =>
|
|
28
|
+
renderToString(
|
|
29
|
+
LanguageContent({
|
|
30
|
+
contentLanguage: "zh-Hans",
|
|
31
|
+
dashboardLanguage: "en",
|
|
32
|
+
multilingualEnabled: false,
|
|
33
|
+
additionalLanguages: [],
|
|
34
|
+
unmarkedPostCount: 0,
|
|
35
|
+
sitePathPrefix: "",
|
|
36
|
+
...overrides,
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Pull the JSON blob the component reads its translated copy from. */
|
|
43
|
+
function readLabels(html: string): Record<string, string> {
|
|
44
|
+
const match = /labels="([^"]*)"/.exec(html);
|
|
45
|
+
if (!match?.[1]) throw new Error("expected a labels attribute");
|
|
46
|
+
const decoded = match[1]
|
|
47
|
+
.replace(/"/g, '"')
|
|
48
|
+
.replace(/&/g, "&")
|
|
49
|
+
.replace(/</g, "<")
|
|
50
|
+
.replace(/>/g, ">")
|
|
51
|
+
.replace(/'/g, "'");
|
|
52
|
+
return JSON.parse(decoded) as Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("LanguageContent", () => {
|
|
56
|
+
it("mounts the Lit element with an initial-state payload", async () => {
|
|
57
|
+
const html = await renderLanguageContent({
|
|
58
|
+
multilingualEnabled: true,
|
|
59
|
+
additionalLanguages: ["en", "ja"],
|
|
60
|
+
unmarkedPostCount: 3,
|
|
61
|
+
sitePathPrefix: "/blog",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(html).toContain("<jant-settings-language");
|
|
65
|
+
const state = /id="language-settings-initial-data">(.*?)<\/script>/s.exec(
|
|
66
|
+
html,
|
|
67
|
+
);
|
|
68
|
+
expect(state?.[1]).toBeDefined();
|
|
69
|
+
expect(JSON.parse(state?.[1] ?? "{}")).toEqual({
|
|
70
|
+
contentLanguage: "zh-Hans",
|
|
71
|
+
dashboardLanguage: "en",
|
|
72
|
+
multilingualEnabled: true,
|
|
73
|
+
additionalLanguages: ["en", "ja"],
|
|
74
|
+
unmarkedPostCount: 3,
|
|
75
|
+
sitePathPrefix: "/blog",
|
|
76
|
+
// The browser bundle cannot resolve the docs host, so the server sends it.
|
|
77
|
+
multilingualDocsUrl: "https://jant.me/docs/multilingual",
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Lingui resolves an ICU message completely at `i18n._()` time, so any
|
|
82
|
+
// plural has to be formatted here — a template shipped to the browser would
|
|
83
|
+
// be formatted against an undefined count and render "NaN".
|
|
84
|
+
describe("copy that carries runtime values", () => {
|
|
85
|
+
it("resolves the plural against the real post count", async () => {
|
|
86
|
+
const labels = readLabels(
|
|
87
|
+
await renderLanguageContent({
|
|
88
|
+
unmarkedPostCount: 347,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(labels.enableMarkWarning).toContain("347 existing posts");
|
|
93
|
+
expect(labels.enableMarkWarning).not.toContain("NaN");
|
|
94
|
+
expect(labels.enableMarkWarning).not.toContain("plural");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("uses the singular form for a single post", async () => {
|
|
98
|
+
const labels = readLabels(
|
|
99
|
+
await renderLanguageContent({
|
|
100
|
+
unmarkedPostCount: 1,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(labels.enableMarkWarning).toContain("1 existing post with");
|
|
105
|
+
expect(labels.enableMarkWarning).not.toContain("posts");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("leaves the language slot for the component to fill", async () => {
|
|
109
|
+
const labels = readLabels(
|
|
110
|
+
await renderLanguageContent({
|
|
111
|
+
unmarkedPostCount: 5,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Chosen inside the dialog, so it cannot be resolved here.
|
|
116
|
+
expect(labels.enableMarkWarning).toContain("{language}");
|
|
117
|
+
expect(labels.removeLanguage).toContain("{language}");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("leaves every slot of the change-primary and disable copy", async () => {
|
|
121
|
+
const labels = readLabels(await renderLanguageContent());
|
|
122
|
+
|
|
123
|
+
expect(labels.changePrimaryBody).toContain("{next}");
|
|
124
|
+
expect(labels.changePrimaryBody).toContain("{previous}");
|
|
125
|
+
expect(labels.changePrimaryBody).toContain("{prefix}");
|
|
126
|
+
expect(labels.disableBody).toContain("{prefix}");
|
|
127
|
+
expect(labels.removeLastBody).toContain("{language}");
|
|
128
|
+
expect(labels.removeLastBody).toContain("{prefix}");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("never leaks an unresolved ICU construct into a label", async () => {
|
|
132
|
+
const labels = readLabels(
|
|
133
|
+
await renderLanguageContent({
|
|
134
|
+
unmarkedPostCount: 12,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
for (const [key, value] of Object.entries(labels)) {
|
|
139
|
+
expect(value, key).not.toContain("plural,");
|
|
140
|
+
expect(value, key).not.toContain("NaN");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createI18n } from "../../../../i18n/i18n.js";
|
|
5
|
+
import { getDiscoverCopy } from "../discover-copy.js";
|
|
6
|
+
|
|
7
|
+
const CORE_ROOT = resolve(import.meta.dirname, "../../../../..");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every msgid in a .po file, mapped to its msgstr. Joins the continuation
|
|
11
|
+
* lines a long entry may be wrapped onto.
|
|
12
|
+
*/
|
|
13
|
+
function readCatalog(locale: string): Map<string, string> {
|
|
14
|
+
const po = readFileSync(
|
|
15
|
+
resolve(CORE_ROOT, `src/i18n/locales/settings/${locale}.po`),
|
|
16
|
+
"utf8",
|
|
17
|
+
);
|
|
18
|
+
const entries = new Map<string, string>();
|
|
19
|
+
for (const block of po.split(/\n{2,}/)) {
|
|
20
|
+
const field = (keyword: string) => {
|
|
21
|
+
const match = new RegExp(
|
|
22
|
+
`^${keyword} ("(?:[^"\\\\]|\\\\.)*"(?:\\n"(?:[^"\\\\]|\\\\.)*")*)`,
|
|
23
|
+
"m",
|
|
24
|
+
).exec(block);
|
|
25
|
+
if (!match?.[1]) return null;
|
|
26
|
+
return match[1]
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => JSON.parse(line) as string)
|
|
29
|
+
.join("");
|
|
30
|
+
};
|
|
31
|
+
const id = field("msgid");
|
|
32
|
+
const str = field("msgstr");
|
|
33
|
+
if (id !== null && str !== null) entries.set(id, str);
|
|
34
|
+
}
|
|
35
|
+
return entries;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("getDiscoverCopy", () => {
|
|
39
|
+
// Setup shows `about` alone; Settings shows it and goes on. The opening is
|
|
40
|
+
// one message, so the two can only differ in length, never in wording.
|
|
41
|
+
it("opens the settings line with the sentence setup shows", () => {
|
|
42
|
+
const copy = getDiscoverCopy(createI18n("en"));
|
|
43
|
+
|
|
44
|
+
expect(copy.about).toBe(
|
|
45
|
+
"Jant Discover is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.",
|
|
46
|
+
);
|
|
47
|
+
expect(copy.intro.startsWith(`${copy.about} `)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Tests render through a macro stub that never reaches a translation, so the
|
|
51
|
+
// catalogs are read directly. A translation that drops `{about}` would leave
|
|
52
|
+
// the settings line without its first sentence in that language only.
|
|
53
|
+
it.each(["zh-Hans", "zh-Hant"])(
|
|
54
|
+
"keeps the opening sentence in the %s settings line",
|
|
55
|
+
(locale) => {
|
|
56
|
+
const catalog = readCatalog(locale);
|
|
57
|
+
const intro = [...catalog].find(([id]) => id.startsWith("{about} "));
|
|
58
|
+
const about = catalog.get(
|
|
59
|
+
"{name} is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(intro?.[1]).toContain("{about}");
|
|
63
|
+
expect(about).toContain("{name}");
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Jant Discover checkbox says, wherever it appears.
|
|
3
|
+
*
|
|
4
|
+
* The checkbox is on the settings page, and it is asked once more on the
|
|
5
|
+
* first-run setup screen. One control appearing twice should not describe
|
|
6
|
+
* itself two ways, so both surfaces take their words from here rather than
|
|
7
|
+
* each keeping a wording of its own that drifts when the other is edited.
|
|
8
|
+
*
|
|
9
|
+
* The two help lines differ in length, not in wording. Setup shows only the
|
|
10
|
+
* sentence saying what the directory is: the delay, the lists and the rules
|
|
11
|
+
* matter to an author with posts to show, and setup comes before the first
|
|
12
|
+
* one — anyone curious follows the name to the directory itself. The settings
|
|
13
|
+
* line carries that same sentence as its `{about}` placeholder and goes on
|
|
14
|
+
* from there, so the opening can only ever be edited in one place, in every
|
|
15
|
+
* locale.
|
|
16
|
+
*
|
|
17
|
+
* Lives under `ui/dash/` because that is what puts it in the translated
|
|
18
|
+
* catalog: `lingui.config` splits by path, and only `routes/dash/**`,
|
|
19
|
+
* `ui/dash/**` and the setup page are translated.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { msg } from "@lingui/core/macro";
|
|
23
|
+
import type { I18n } from "../../../i18n/i18n.js";
|
|
24
|
+
import {
|
|
25
|
+
DISCOVER_LIST_NAMES,
|
|
26
|
+
DISCOVER_PUBLIC_DELAY_HOURS,
|
|
27
|
+
} from "../../../lib/discover.js";
|
|
28
|
+
|
|
29
|
+
type Translator = Pick<I18n, "_">;
|
|
30
|
+
|
|
31
|
+
/** The Discover checkbox's words, translated. */
|
|
32
|
+
export interface DiscoverCopy {
|
|
33
|
+
/** The directory's name, as `label`, `about` and `intro` carry it. */
|
|
34
|
+
name: string;
|
|
35
|
+
/** The rules page's name, as the end of `intro` carries it. */
|
|
36
|
+
rules: string;
|
|
37
|
+
/** The checkbox label, on both surfaces. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** What the directory is, in one sentence: setup's whole help line. */
|
|
40
|
+
about: string;
|
|
41
|
+
/** The settings page's help line, which opens with `about`. */
|
|
42
|
+
intro: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Translate the Discover checkbox's label and help lines.
|
|
47
|
+
*
|
|
48
|
+
* The directory's name and the rules page's name are split out so the label
|
|
49
|
+
* and the help lines can carry them as placeholders, leaving word order free
|
|
50
|
+
* per locale; they are returned too, because `discoverIntroRuns` finds each
|
|
51
|
+
* one in the translated line to make it a link.
|
|
52
|
+
*
|
|
53
|
+
* @param i18n - The request's i18n instance
|
|
54
|
+
* @returns The five strings, in the i18n instance's locale
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const copy = getDiscoverCopy(i18n);
|
|
58
|
+
* copy.label; // "Allow Jant Discover to list my site"
|
|
59
|
+
* copy.intro.startsWith(copy.about); // true
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function getDiscoverCopy(i18n: Translator): DiscoverCopy {
|
|
63
|
+
const name = i18n._(
|
|
64
|
+
msg({
|
|
65
|
+
message: "Jant Discover",
|
|
66
|
+
comment:
|
|
67
|
+
"@context: Name of the Jant blog directory. Appears inside the Discover checkbox label, and in the help line under it as the link to the directory.",
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// The run of text the rules link sits on, at the end of the help line.
|
|
72
|
+
const rules = i18n._(
|
|
73
|
+
msg({
|
|
74
|
+
message: "Discover community rules",
|
|
75
|
+
comment:
|
|
76
|
+
"@context: Link text at the end of the help line under the Discover checkbox, leading to the directory's page on how blogs are listed: joining, review, and what takes a blog off. This run of text is rendered as the link, so translate it as it should read inside that sentence.",
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const label = i18n._(
|
|
81
|
+
msg({
|
|
82
|
+
message: "Allow {name} to list my site",
|
|
83
|
+
comment:
|
|
84
|
+
"@context: Checkbox for joining the Jant Discover directory, on the settings page and once on the first-run setup screen. {name} is the directory's name, kept as one run of text.",
|
|
85
|
+
}),
|
|
86
|
+
{ name },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const about = i18n._(
|
|
90
|
+
msg({
|
|
91
|
+
message:
|
|
92
|
+
"{name} is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.",
|
|
93
|
+
comment:
|
|
94
|
+
"@context: What the Jant Discover directory is, in one sentence. The whole help line under the Discover checkbox on the first-run setup screen, and the opening of the settings page's longer one. {name} is the directory's name and is rendered as the link to it, so keep it as one run of text.",
|
|
95
|
+
}),
|
|
96
|
+
{ name },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const intro = i18n._(
|
|
100
|
+
msg({
|
|
101
|
+
message:
|
|
102
|
+
"{about} A post you mark Featured appears on the Discover home page {hours} hours later, and link and quote posts appear on the {links} and {quotes} lists {hours} hours after they are published. You can keep editing them in the meantime. See the {rules}.",
|
|
103
|
+
comment:
|
|
104
|
+
"@context: Help text under the Jant Discover checkbox on the settings page. {about} is the sentence saying what the directory is, translated on its own and shown alone on the setup screen; keep it, and join it to what follows as your language joins two sentences. {links} and {quotes} are the directory's two list names, which stay in English, and {rules} is the name of its rules page; each is rendered as a link, so keep every placeholder as one run of text. {hours} is how long the directory waits before showing a post, which is the time left to edit it. 'Featured' is the mark on a post, as this site's own UI spells it; the lowercase 'link and quote' are the post formats, not the lists.",
|
|
105
|
+
}),
|
|
106
|
+
{
|
|
107
|
+
about,
|
|
108
|
+
hours: DISCOVER_PUBLIC_DELAY_HOURS,
|
|
109
|
+
links: DISCOVER_LIST_NAMES.links,
|
|
110
|
+
quotes: DISCOVER_LIST_NAMES.quotes,
|
|
111
|
+
rules,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return { name, rules, label, about, intro };
|
|
116
|
+
}
|