@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
|
@@ -29,10 +29,13 @@ export async function buildSyncSiteConfig(c: {
|
|
|
29
29
|
siteUrl: appConfig.siteUrl,
|
|
30
30
|
siteDescription: appConfig.siteDescription,
|
|
31
31
|
siteLanguage: appConfig.siteLanguage,
|
|
32
|
+
multilingualEnabled: appConfig.multilingualEnabled,
|
|
33
|
+
additionalLanguages: appConfig.additionalLanguages,
|
|
32
34
|
showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
|
|
33
35
|
publicApiEnabled: appConfig.publicApiEnabled,
|
|
34
36
|
rssFeedsEnabled: appConfig.rssFeedsEnabled,
|
|
35
37
|
mainRssFeed: appConfig.mainRssFeed,
|
|
38
|
+
archiveDefaultLayout: appConfig.archiveDefaultLayout,
|
|
36
39
|
siteFooter: appConfig.siteFooter,
|
|
37
40
|
showHeaderAvatar: appConfig.showHeaderAvatar,
|
|
38
41
|
siteAvatarUrl: appConfig.siteAvatarUrl,
|
package/src/lib/hosted-signin.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function getHostedControlPlaneResetUrl(
|
|
|
60
60
|
export function getHostedControlPlaneDashboardUrl(
|
|
61
61
|
env: object | undefined | null,
|
|
62
62
|
): string | null {
|
|
63
|
-
return buildHostedControlPlaneUrl(env, "/
|
|
63
|
+
return buildHostedControlPlaneUrl(env, "/app");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function getHostedControlPlaneAccountUrl(
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional-request helpers.
|
|
3
|
+
*
|
|
4
|
+
* Transport-level only: deriving an entity tag for a response body, and
|
|
5
|
+
* deciding whether the request already holds it. Nothing here knows what the
|
|
6
|
+
* body represents.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const textEncoder = new TextEncoder();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One entity tag inside an `If-None-Match` list, weak prefix included.
|
|
13
|
+
*
|
|
14
|
+
* Matching quoted runs rather than splitting on commas is deliberate: RFC 9110
|
|
15
|
+
* allows a comma inside an entity tag's opaque part, so a naive split can tear
|
|
16
|
+
* one tag into two that match nothing.
|
|
17
|
+
*/
|
|
18
|
+
const ENTITY_TAG = /(?:W\/)?"[^"]*"/g;
|
|
19
|
+
|
|
20
|
+
/** Drop the weak prefix, which the weak comparison in RFC 9110 ignores. */
|
|
21
|
+
function opaqueTag(tag: string): string {
|
|
22
|
+
return tag.startsWith("W/") ? tag.slice(2) : tag;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Derive a strong entity tag from a response body.
|
|
27
|
+
*
|
|
28
|
+
* The tag is a truncated SHA-256 of the exact bytes that would be sent, so
|
|
29
|
+
* two equal tags do mean two identical responses — which is what makes the
|
|
30
|
+
* tag strong rather than `W/`-weak. 128 bits is far past the point where a
|
|
31
|
+
* collision could plausibly pin a stale body in a cache.
|
|
32
|
+
*
|
|
33
|
+
* @param body - Serialized response body
|
|
34
|
+
* @returns Quoted entity tag, ready for an `ETag` header
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const etag = await strongETag(xml); // '"9f86d081884c7d65..."'
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export async function strongETag(body: string): Promise<string> {
|
|
41
|
+
const digest = await crypto.subtle.digest(
|
|
42
|
+
"SHA-256",
|
|
43
|
+
textEncoder.encode(body),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
let hex = "";
|
|
47
|
+
for (const byte of new Uint8Array(digest, 0, 16)) {
|
|
48
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return `"${hex}"`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether a request's `If-None-Match` already holds `etag`.
|
|
56
|
+
*
|
|
57
|
+
* Comparison is the weak one RFC 9110 requires for `If-None-Match`: the weak
|
|
58
|
+
* prefix is ignored on both sides, and any tag in the list can match. `*`
|
|
59
|
+
* matches whenever the resource exists, which it does by the time a response
|
|
60
|
+
* body has been rendered.
|
|
61
|
+
*
|
|
62
|
+
* @param header - Raw `If-None-Match` header value, absent when not sent
|
|
63
|
+
* @param etag - Entity tag of the response about to be sent
|
|
64
|
+
* @returns `true` when the response can be answered with `304`
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* matchesIfNoneMatch('W/"abc", "def"', '"def"'); // true
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function matchesIfNoneMatch(
|
|
71
|
+
header: string | null | undefined,
|
|
72
|
+
etag: string,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (!header) return false;
|
|
75
|
+
if (header.trim() === "*") return true;
|
|
76
|
+
|
|
77
|
+
const target = opaqueTag(etag);
|
|
78
|
+
for (const candidate of header.match(ENTITY_TAG) ?? []) {
|
|
79
|
+
if (opaqueTag(candidate) === target) return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Headers a `304` carries over from the response it stands in for.
|
|
87
|
+
*
|
|
88
|
+
* RFC 9110 asks for the fields a cache would otherwise have to invent or
|
|
89
|
+
* discard: the validator itself, the freshness the entry is stored under, and
|
|
90
|
+
* the identity of what was validated.
|
|
91
|
+
*/
|
|
92
|
+
const NOT_MODIFIED_HEADERS = [
|
|
93
|
+
"Cache-Control",
|
|
94
|
+
"Content-Location",
|
|
95
|
+
"Date",
|
|
96
|
+
"ETag",
|
|
97
|
+
"Expires",
|
|
98
|
+
"Vary",
|
|
99
|
+
] as const;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Answer a conditional request with `304` when the response is one the client
|
|
103
|
+
* already holds.
|
|
104
|
+
*
|
|
105
|
+
* This is the only place in the app that turns a validator into a `304`, and
|
|
106
|
+
* it runs at the outer edge of the request — outside the Worker response
|
|
107
|
+
* cache. Both halves of that matter: a cache hit is revalidated without
|
|
108
|
+
* reaching a route, and a cache miss still renders and stores the whole
|
|
109
|
+
* document before the body is dropped, so the next reader gets a cache entry
|
|
110
|
+
* rather than a `304` that cannot be stored.
|
|
111
|
+
*
|
|
112
|
+
* Anything without an entity tag passes through untouched, so a route opts in
|
|
113
|
+
* simply by emitting one.
|
|
114
|
+
*
|
|
115
|
+
* @param request - Incoming request, read for `If-None-Match`
|
|
116
|
+
* @param response - Response the app produced
|
|
117
|
+
* @returns A bare `304`, or the response unchanged
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* return withConditionalResponse(request, await handle(request));
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function withConditionalResponse(
|
|
124
|
+
request: Request,
|
|
125
|
+
response: Response,
|
|
126
|
+
): Response {
|
|
127
|
+
if (request.method !== "GET" && request.method !== "HEAD") return response;
|
|
128
|
+
if (response.status !== 200) return response;
|
|
129
|
+
|
|
130
|
+
// A 304 drops the body, and with it any header that only makes sense
|
|
131
|
+
// attached to one. A response setting a cookie is left alone rather than
|
|
132
|
+
// stripped down to a validator.
|
|
133
|
+
if (response.headers.has("Set-Cookie")) return response;
|
|
134
|
+
|
|
135
|
+
const etag = response.headers.get("ETag");
|
|
136
|
+
if (!etag) return response;
|
|
137
|
+
if (!matchesIfNoneMatch(request.headers.get("If-None-Match"), etag)) {
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const headers = new Headers();
|
|
142
|
+
for (const name of NOT_MODIFIED_HEADERS) {
|
|
143
|
+
const value = response.headers.get(name);
|
|
144
|
+
if (value !== null) headers.set(name, value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new Response(null, { status: 304, headers });
|
|
148
|
+
}
|
package/src/lib/hugo-markdown.ts
CHANGED
|
@@ -104,6 +104,11 @@ export interface HugoFrontMatter {
|
|
|
104
104
|
status?: string;
|
|
105
105
|
visibility?: string;
|
|
106
106
|
summary_text?: string;
|
|
107
|
+
/**
|
|
108
|
+
* The timeline cuts this post's text short. Written only when true, so a
|
|
109
|
+
* consumer reads its presence rather than its value.
|
|
110
|
+
*/
|
|
111
|
+
truncated?: boolean;
|
|
107
112
|
link_url?: string;
|
|
108
113
|
source_name?: string;
|
|
109
114
|
source_url?: string;
|
|
@@ -111,6 +116,16 @@ export interface HugoFrontMatter {
|
|
|
111
116
|
rating?: number | null;
|
|
112
117
|
featured_at?: string | null;
|
|
113
118
|
pinned_at?: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* BCP 47 content language. Root bundles only — replies take the root's.
|
|
121
|
+
*/
|
|
122
|
+
language?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Opaque shared key linking posts that are translations of one another. Root
|
|
125
|
+
* bundles only. Meaningful within one export; carried across so a full-site
|
|
126
|
+
* restore keeps translated posts linked.
|
|
127
|
+
*/
|
|
128
|
+
translation_group?: string;
|
|
114
129
|
/**
|
|
115
130
|
* Reply bundles only: the reply was published without announcing the Thread
|
|
116
131
|
* on Latest. Round-tripped so a restore rebuilds the same ordering.
|
|
@@ -205,6 +220,7 @@ const FRONT_MATTER_KEY_ORDER: readonly string[] = [
|
|
|
205
220
|
"status",
|
|
206
221
|
"visibility",
|
|
207
222
|
"summary_text",
|
|
223
|
+
"truncated",
|
|
208
224
|
"link_url",
|
|
209
225
|
"source_name",
|
|
210
226
|
"source_url",
|
package/src/lib/icons.ts
CHANGED
|
@@ -24,17 +24,24 @@ function toPascalCase(name: string): string {
|
|
|
24
24
|
* Get SVG markup for a Lucide icon by kebab-case name.
|
|
25
25
|
*
|
|
26
26
|
* @param name - Kebab-case icon name
|
|
27
|
+
* @param className - Extra classes for the `<svg>`, appended to Lucide's own.
|
|
28
|
+
* `icon-fine` is the project's lighter stroke.
|
|
27
29
|
* @returns SVG string or null when the icon is unknown
|
|
28
30
|
*
|
|
29
31
|
* @example
|
|
30
32
|
* ```ts
|
|
31
33
|
* getIconSvg("book-open");
|
|
34
|
+
* getIconSvg("funnel", "icon-fine");
|
|
32
35
|
* ```
|
|
33
36
|
*/
|
|
34
|
-
export function getIconSvg(name: string): string | null {
|
|
37
|
+
export function getIconSvg(name: string, className?: string): string | null {
|
|
35
38
|
const pascalName = toPascalCase(name);
|
|
36
39
|
const svg = (lucideIcons as Record<string, string>)[pascalName];
|
|
37
|
-
|
|
40
|
+
if (typeof svg !== "string") return null;
|
|
41
|
+
if (!className) return svg;
|
|
42
|
+
// Lucide always ships a `class="lucide lucide-…"`, so extend it rather than
|
|
43
|
+
// adding a second class attribute the parser would drop.
|
|
44
|
+
return svg.replace(/class="([^"]*)"/, `class="$1 ${className}"`);
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/**
|
package/src/lib/ids.ts
CHANGED
|
@@ -5,9 +5,12 @@ export const ID_PREFIX = {
|
|
|
5
5
|
site: "sit",
|
|
6
6
|
siteDomain: "sdm",
|
|
7
7
|
post: "pst",
|
|
8
|
+
/** Shared key linking Posts that are translations of one another. */
|
|
9
|
+
translationGroup: "tgr",
|
|
8
10
|
media: "med",
|
|
9
11
|
uploadSession: "upl",
|
|
10
12
|
collection: "col",
|
|
13
|
+
smartCollection: "smc",
|
|
11
14
|
path: "pth",
|
|
12
15
|
collectionDirectoryItem: "cdi",
|
|
13
16
|
navItem: "nav",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links into the published documentation
|
|
3
|
+
*
|
|
4
|
+
* Everything under `docs/` is published on the Jant site: `docs/multilingual.md`
|
|
5
|
+
* is served at `/docs/multilingual`, lowercased and without the extension.
|
|
6
|
+
*
|
|
7
|
+
* The host is resolved once, here, so no call site writes a domain. Development
|
|
8
|
+
* builds point at the control plane running locally, which serves the docs
|
|
9
|
+
* being edited rather than the released ones.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { IS_VITE_DEV } from "./build-env.js";
|
|
13
|
+
|
|
14
|
+
/** Docs host for released builds. */
|
|
15
|
+
const DOCS_ORIGIN = "https://jant.me";
|
|
16
|
+
|
|
17
|
+
/** Docs host during development. */
|
|
18
|
+
const DEV_DOCS_ORIGIN = "https://jant-cloud.localtest.me";
|
|
19
|
+
|
|
20
|
+
/** Documentation index for the current environment. */
|
|
21
|
+
export const JANT_DOCS_BASE_URL = `${
|
|
22
|
+
IS_VITE_DEV ? DEV_DOCS_ORIGIN : DOCS_ORIGIN
|
|
23
|
+
}/docs`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a link to one documentation page.
|
|
27
|
+
*
|
|
28
|
+
* @param page - Published page slug, optionally with a `#section` anchor. It is
|
|
29
|
+
* the file name under `docs/`, lowercased and without `.md`. Omit it to link
|
|
30
|
+
* the documentation index.
|
|
31
|
+
* @returns Absolute URL to that page on the current environment's docs site
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* getJantDocsUrl("multilingual"); // "https://jant.me/docs/multilingual"
|
|
35
|
+
* getJantDocsUrl("configuration#required");
|
|
36
|
+
* // "https://jant.me/docs/configuration#required"
|
|
37
|
+
* getJantDocsUrl(); // "https://jant.me/docs"
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function getJantDocsUrl(page = ""): string {
|
|
41
|
+
return page ? `${JANT_DOCS_BASE_URL}/${page}` : JANT_DOCS_BASE_URL;
|
|
42
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content language detection.
|
|
3
|
+
*
|
|
4
|
+
* A hand-written character-set voter, not a statistical model. It runs in the
|
|
5
|
+
* browser (in compose, for logged-in authors only — public pages load none of
|
|
6
|
+
* it) and on the server (for posts that arrive through the API, the Telegram
|
|
7
|
+
* bot, or MCP, where nobody picked a language).
|
|
8
|
+
*
|
|
9
|
+
* What it is good at, and what it is not:
|
|
10
|
+
*
|
|
11
|
+
* - Hangul and kana are near-unambiguous, so Korean and Japanese are reliable.
|
|
12
|
+
* - Simplified vs Traditional Chinese is decided by voting on characters that
|
|
13
|
+
* exist in only one of the two. One sentence is usually plenty. Text written
|
|
14
|
+
* the same way in both — `我今天很好` — is reported as Chinese of unsettled
|
|
15
|
+
* variant, which is enough on a site publishing only one of them and
|
|
16
|
+
* deliberately not enough on a site publishing both.
|
|
17
|
+
* - Latin script says "not CJK", nothing more. It resolves to a language only
|
|
18
|
+
* when the site publishes exactly one non-CJK language.
|
|
19
|
+
* - Nothing at all is read out of a fragment. See `MIN_SIGNAL`: under a couple
|
|
20
|
+
* of words the honest answer is the caller's default, not a coin toss that
|
|
21
|
+
* changes with the next keystroke.
|
|
22
|
+
*
|
|
23
|
+
* The detector **never** returns an empty value or a language the site does
|
|
24
|
+
* not publish: it always answers with a member of `languages`, falling back to
|
|
25
|
+
* the caller's default. Its output is a *suggestion* the author can change —
|
|
26
|
+
* nothing here decides anything irreversibly.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { getCjkFontFromLanguageTag } from "../i18n/detect.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Characters written only in Simplified Chinese.
|
|
33
|
+
*
|
|
34
|
+
* Every entry is checked to be absent from Traditional writing — a character
|
|
35
|
+
* that serves both (`后` as "empress", `台` in `台北`) would vote for the wrong
|
|
36
|
+
* side and has been left out, while its Traditional counterpart still votes.
|
|
37
|
+
*/
|
|
38
|
+
const SIMPLIFIED_ONLY =
|
|
39
|
+
"们这会说对时国学个来过现发经应关点无义与从门问间东车长马鸟鱼见贝页风飞头语话认识讲论议记让试该请读谁调谈变边达运还进远连" +
|
|
40
|
+
"适选递邮释钟银错锁闭开闻阅队阶阳阴陆陈险隐难静韩须顾频颜题类飘饭饮饰馆驻驾验骑骗体处备够夺奋妇妈宁宝实宽宾寻导尔尘尝尽层" +
|
|
41
|
+
"属岁岂岛币帅帮归当录彻忆态怀恋恶悬惊惧惨愤愿战户扩扫扬担拟挂挤挥换据摄击敌断旧显术机杀杂权条极构柜标树桥检楼欢欧残毁气汉" +
|
|
42
|
+
"汤沟泪洁济浏浅浆涛润涨渐渔渗湾湿满滨滤灭灯灵灾炉烦烧热营爱状犹狮独狱猎猪献猫环电画畅疗疯皱监盘矿码确础礼祸种积称稳穷窃竞" +
|
|
43
|
+
"笔筹简签粮紧纪纯纲纳纵纷纸线组细织终绍结绕绘给络绝统继绩绪续维综绿缓编缘缩缴网罗罚罢习联声职聪肃肠胀脑脱腊舰艰节苏药荐荡" +
|
|
44
|
+
"获萧蓝虏虚虽蚀蜡补装观规觉计订讨训讯词译诉诊证评诚误诸课谅谓谢谱贡财责贤败货质贩贪购贯贱贴贵贷贸费贺资赏赐赔赖赚赛赢赠赶" +
|
|
45
|
+
"趋践轨转轮软载较辅辈辉输辐辑辞迁迈违迟逊遗邓邻郑针钉钓钙钝钞钢钥钦钩钱钻铁铃铅铜铝铺链销锋锐锦键镇镜闪闯闲闷闹阀阁阵际隶" +
|
|
46
|
+
"随雾项顶预领颇颈颗颠饥饱饼馈驰驱驶骂骄鲁鲜鸡鸣鸦鸭鸽鹅鹰麦黄齐齿龄龙龟儿优传伤侦侧俭债倾偿储兰兴军农决净减凤凭则刘创剑剧" +
|
|
47
|
+
"劝办务动励劳势勋医华单卖卫厅历压厌厨参叶号叹吗启呐员呜响哑唤啸喷嘱团园围图圆场坏块坚坛垒垦堕墙壮壳复";
|
|
48
|
+
|
|
49
|
+
/** Characters written only in Traditional Chinese. */
|
|
50
|
+
const TRADITIONAL_ONLY =
|
|
51
|
+
"們這會說對時國學後個來過現發經應關點無義與從門問間東車長馬鳥魚見貝頁風飛頭語話認識講論議記讓試該請讀誰調談變邊達運還進遠" +
|
|
52
|
+
"連適選遞郵釋鐘銀錯鎖閉開聞閱隊階陽陰陸陳險隱難靜韓須顧頻顏題類飄飯飲飾館駐駕驗騎騙體處備夠奪奮婦媽寧寶實寬賓尋導爾塵嘗盡" +
|
|
53
|
+
"層屬歲豈島幣帥幫歸當錄徹憶態懷戀惡懸驚懼慘憤願戰戶擴掃揚擔擬掛擠揮換據攝擊敵斷舊顯術機殺雜權條極構櫃標樹橋檢樓歡歐殘毀氣" +
|
|
54
|
+
"漢湯溝淚潔濟瀏淺漿濤潤漲漸漁滲灣濕滿濱濾滅燈靈災爐煩燒熱營愛狀猶獅獨獄獵豬獻貓環電畫暢療瘋皺監盤礦碼確礎禮禍種積稱穩窮竊" +
|
|
55
|
+
"競筆籌簡簽糧緊紀純綱納縱紛紙線組細織終紹結繞繪給絡絕統繼績緒續維綜綠緩編緣縮繳網羅罰罷習聯聲職聰肅腸脹腦脫臘艦艱節蘇藥薦" +
|
|
56
|
+
"蕩獲蕭藍虜虛雖蝕蠟補裝觀規覺計訂討訓訊詞譯訴診證評誠誤諸課諒謂謝譜貢財責賢敗貨質販貪購貫賤貼貴貸貿費賀資賞賜賠賴賺賽贏贈" +
|
|
57
|
+
"趕趨踐軌轉輪軟載較輔輩輝輸輻輯辭遷邁違遲遜遺鄧鄰鄭針釘釣鈣鈍鈔鋼鑰欽鉤錢鑽鐵鈴鉛銅鋁鋪鏈銷鋒銳錦鍵鎮鏡閃闖閒悶鬧閥閣陣際" +
|
|
58
|
+
"隸隨霧項頂預領頗頸顆顛飢飽餅饋馳驅駛罵驕魯鮮雞鳴鴉鴨鴿鵝鷹麥黃齊齒齡龍龜兒優傳傷偵側儉債傾償儲蘭興軍農衝決淨減鳳憑劃則劉" +
|
|
59
|
+
"創劍劇勸辦務動勵勞勢勳醫華單賣衛廳歷壓厭廚參疊隻臺葉號嘆嗎啟吶員嗚響啞喚嘯噴囑團園圍圖圓場壞塊堅壇壘墾墮牆壯殼複";
|
|
60
|
+
|
|
61
|
+
const SIMPLIFIED_SET = new Set(SIMPLIFIED_ONLY);
|
|
62
|
+
const TRADITIONAL_SET = new Set(TRADITIONAL_ONLY);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Script family a run of text is written in, before it meets the site's
|
|
66
|
+
* languages.
|
|
67
|
+
*
|
|
68
|
+
* `han` is Chinese whose variant the text does not settle: `我今天很好` and
|
|
69
|
+
* `哈哈哈哈` are written identically in Simplified and Traditional. It is a
|
|
70
|
+
* real answer, not a refusal — the text is certainly Chinese — and on a site
|
|
71
|
+
* that publishes only one of the two variants it is all the answer needed.
|
|
72
|
+
*/
|
|
73
|
+
export type DetectedScript =
|
|
74
|
+
"ko" | "ja" | "zh-Hans" | "zh-Hant" | "han" | "latin";
|
|
75
|
+
|
|
76
|
+
interface ScriptCounts {
|
|
77
|
+
hangul: number;
|
|
78
|
+
kana: number;
|
|
79
|
+
han: number;
|
|
80
|
+
latin: number;
|
|
81
|
+
simplified: number;
|
|
82
|
+
traditional: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function countScripts(text: string): ScriptCounts {
|
|
86
|
+
const counts: ScriptCounts = {
|
|
87
|
+
hangul: 0,
|
|
88
|
+
kana: 0,
|
|
89
|
+
han: 0,
|
|
90
|
+
latin: 0,
|
|
91
|
+
simplified: 0,
|
|
92
|
+
traditional: 0,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const char of text) {
|
|
96
|
+
const code = char.codePointAt(0);
|
|
97
|
+
if (code === undefined) continue;
|
|
98
|
+
|
|
99
|
+
// Hangul syllables, Jamo, and compatibility Jamo.
|
|
100
|
+
if (
|
|
101
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
102
|
+
(code >= 0x1100 && code <= 0x11ff) ||
|
|
103
|
+
(code >= 0x3130 && code <= 0x318f)
|
|
104
|
+
) {
|
|
105
|
+
counts.hangul++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Hiragana, katakana, and katakana phonetic extensions.
|
|
110
|
+
if (
|
|
111
|
+
(code >= 0x3040 && code <= 0x309f) ||
|
|
112
|
+
(code >= 0x30a0 && code <= 0x30ff) ||
|
|
113
|
+
(code >= 0x31f0 && code <= 0x31ff)
|
|
114
|
+
) {
|
|
115
|
+
counts.kana++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// CJK Unified Ideographs, Extension A, and compatibility ideographs.
|
|
120
|
+
if (
|
|
121
|
+
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
122
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
123
|
+
(code >= 0xf900 && code <= 0xfaff)
|
|
124
|
+
) {
|
|
125
|
+
counts.han++;
|
|
126
|
+
if (SIMPLIFIED_SET.has(char)) counts.simplified++;
|
|
127
|
+
else if (TRADITIONAL_SET.has(char)) counts.traditional++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Basic Latin letters plus the Latin-1 and Extended-A accented ranges.
|
|
132
|
+
if (
|
|
133
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
134
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
135
|
+
(code >= 0xc0 && code <= 0x24f)
|
|
136
|
+
) {
|
|
137
|
+
counts.latin++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return counts;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What a CJK character is worth against a Latin letter.
|
|
146
|
+
*
|
|
147
|
+
* A Han character is closer to a word than to a letter, and kana and Hangul
|
|
148
|
+
* sit in between — so counting characters straight would let a two-character
|
|
149
|
+
* quote outvote a paragraph. Three is the rough density ratio between CJK and
|
|
150
|
+
* Latin text, and it is the only number here that has to be approximate: it
|
|
151
|
+
* decides ties between scripts, not what any one script means.
|
|
152
|
+
*/
|
|
153
|
+
const CJK_WEIGHT = 3;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* How much evidence it takes before the detector will name a script at all.
|
|
157
|
+
*
|
|
158
|
+
* Ten points is four Han characters, or two English words. Below that the
|
|
159
|
+
* answer would swing on every keystroke — a composer that reads "H" as English
|
|
160
|
+
* and "He 说" as Chinese is not reading, it is twitching. Under the threshold
|
|
161
|
+
* the detector refuses and the caller keeps its own default, which is the
|
|
162
|
+
* language of the page the author is writing from.
|
|
163
|
+
*/
|
|
164
|
+
const MIN_SIGNAL = 10;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Identify the script family a piece of text is written in.
|
|
168
|
+
*
|
|
169
|
+
* Exported for its own sake — the language-set resolution below is a separate
|
|
170
|
+
* concern, and testing the two apart keeps both honest.
|
|
171
|
+
*
|
|
172
|
+
* @param text - Plain text, typically a post body
|
|
173
|
+
* @returns The script family, or null when the text carries too little signal
|
|
174
|
+
* @example
|
|
175
|
+
* detectScript("これはテスト"); // "ja"
|
|
176
|
+
* detectScript("國學說這時"); // "zh-Hant"
|
|
177
|
+
* detectScript("我今天很好"); // "han" — Chinese, variant not settled
|
|
178
|
+
* detectScript("Hi"); // null — too short to be worth an answer
|
|
179
|
+
* detectScript("123 !!!"); // null
|
|
180
|
+
*/
|
|
181
|
+
export function detectScript(text: string): DetectedScript | null {
|
|
182
|
+
const counts = countScripts(text);
|
|
183
|
+
const cjk = (counts.hangul + counts.kana + counts.han) * CJK_WEIGHT;
|
|
184
|
+
|
|
185
|
+
// Which script the text is *mostly* in, not merely which one appears in it:
|
|
186
|
+
// an English paragraph that quotes a Chinese phrase is still English, and
|
|
187
|
+
// before this comparison a single Han character spoke for the whole post.
|
|
188
|
+
if (cjk >= counts.latin && cjk >= MIN_SIGNAL) {
|
|
189
|
+
// Korean mixes Hangul with Han; Japanese mixes kana with Han. Neither
|
|
190
|
+
// borrows the other's syllabary, so either one present settles it.
|
|
191
|
+
if (counts.hangul > 0) return "ko";
|
|
192
|
+
if (counts.kana > 0) return "ja";
|
|
193
|
+
if (counts.simplified > counts.traditional) return "zh-Hans";
|
|
194
|
+
if (counts.traditional > counts.simplified) return "zh-Hant";
|
|
195
|
+
// Han with no distinctive character either way, or the same number on each
|
|
196
|
+
// side. Which variant this is, the text does not say; that it is Chinese,
|
|
197
|
+
// it says clearly. Report that much and let the caller weigh it against the
|
|
198
|
+
// languages the site actually publishes.
|
|
199
|
+
return "han";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return counts.latin >= MIN_SIGNAL ? "latin" : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Read one of the site's languages out of a piece of text, or admit that the
|
|
207
|
+
* text does not say.
|
|
208
|
+
*
|
|
209
|
+
* The distinction is the whole point of this function existing next to
|
|
210
|
+
* `detectContentLanguage`: a caller that substitutes a default cannot tell
|
|
211
|
+
* "this is English" from "I could not tell, here is your default", and a
|
|
212
|
+
* composer that reports the second as the first is lying to the author.
|
|
213
|
+
*
|
|
214
|
+
* @param text - Plain text, typically a post body
|
|
215
|
+
* @param options - The languages the site publishes
|
|
216
|
+
* @returns A tag from `options.languages`, or null when the text does not
|
|
217
|
+
* settle on one of them
|
|
218
|
+
* @example
|
|
219
|
+
* readContentLanguage("國學說這時會對後", {
|
|
220
|
+
* languages: ["zh-Hans", "zh-Hant", "en"],
|
|
221
|
+
* }); // "zh-Hant"
|
|
222
|
+
* readContentLanguage("Hi", { languages: ["zh-Hans", "en"] }); // null
|
|
223
|
+
*/
|
|
224
|
+
export function readContentLanguage(
|
|
225
|
+
text: string,
|
|
226
|
+
options: { languages: readonly string[] },
|
|
227
|
+
): string | null {
|
|
228
|
+
const { languages } = options;
|
|
229
|
+
if (languages.length === 0) return null;
|
|
230
|
+
|
|
231
|
+
const script = detectScript(text);
|
|
232
|
+
if (!script) return null;
|
|
233
|
+
|
|
234
|
+
if (script === "latin") {
|
|
235
|
+
// Latin script only says "not CJK". That names a language only when there
|
|
236
|
+
// is exactly one candidate; with English and French configured, guessing
|
|
237
|
+
// between them from the alphabet alone would be a coin toss.
|
|
238
|
+
const nonCjk = languages.filter((tag) => !getCjkFontFromLanguageTag(tag));
|
|
239
|
+
return nonCjk.length === 1 ? (nonCjk[0] as string) : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (script === "han") {
|
|
243
|
+
// Same shape as the Latin case: the text narrowed it to Chinese but not to
|
|
244
|
+
// a variant, so it names a language only when the site publishes one of
|
|
245
|
+
// them. A site running both Simplified and Traditional gets no answer,
|
|
246
|
+
// which is the honest one — the text genuinely does not say.
|
|
247
|
+
const chinese = languages.filter((tag) => {
|
|
248
|
+
const profile = getCjkFontFromLanguageTag(tag);
|
|
249
|
+
return profile === "zh-Hans" || profile === "zh-Hant";
|
|
250
|
+
});
|
|
251
|
+
return chinese.length === 1 ? (chinese[0] as string) : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// No match means the site does not publish the detected script — say nothing
|
|
255
|
+
// rather than invent a language the author never configured.
|
|
256
|
+
return (
|
|
257
|
+
languages.find((tag) => getCjkFontFromLanguageTag(tag) === script) ?? null
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Suggest which of the site's languages a piece of text is written in.
|
|
263
|
+
*
|
|
264
|
+
* @param text - Plain text, typically a post body
|
|
265
|
+
* @param options - The languages the site publishes, and what to answer when
|
|
266
|
+
* the text gives no usable signal
|
|
267
|
+
* @returns A tag from `options.languages`, always
|
|
268
|
+
* @example
|
|
269
|
+
* detectContentLanguage("國學說這時會對後", {
|
|
270
|
+
* languages: ["zh-Hans", "zh-Hant", "en"],
|
|
271
|
+
* fallback: "zh-Hans",
|
|
272
|
+
* }); // "zh-Hant"
|
|
273
|
+
*/
|
|
274
|
+
export function detectContentLanguage(
|
|
275
|
+
text: string,
|
|
276
|
+
options: { languages: readonly string[]; fallback: string },
|
|
277
|
+
): string {
|
|
278
|
+
return readContentLanguage(text, options) ?? options.fallback;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Suggest the language a new Post should be stored with, when nobody said.
|
|
283
|
+
*
|
|
284
|
+
* Only ever fills a blank: an explicit choice is the author's and is never
|
|
285
|
+
* second-guessed here. Returns null on a site that does not publish more than
|
|
286
|
+
* one language, because nothing is stamped before the feature is turned on.
|
|
287
|
+
*
|
|
288
|
+
* @param options - Text to read, the site's languages, and its primary one
|
|
289
|
+
* @returns Canonical tag from `languages`, or null when the site is single-language
|
|
290
|
+
* @example
|
|
291
|
+
* suggestPostLanguage({
|
|
292
|
+
* text: "Hello there",
|
|
293
|
+
* languages: ["zh-Hans", "en"],
|
|
294
|
+
* primary: "zh-Hans",
|
|
295
|
+
* }); // "en"
|
|
296
|
+
*/
|
|
297
|
+
export function suggestPostLanguage(options: {
|
|
298
|
+
text?: string | null;
|
|
299
|
+
languages: readonly string[];
|
|
300
|
+
primary: string;
|
|
301
|
+
}): string | null {
|
|
302
|
+
const { text, languages, primary } = options;
|
|
303
|
+
|
|
304
|
+
if (languages.length === 0) return null;
|
|
305
|
+
if (!text?.trim()) return primary;
|
|
306
|
+
|
|
307
|
+
return detectContentLanguage(text, { languages, fallback: primary });
|
|
308
|
+
}
|
package/src/lib/media-helpers.ts
CHANGED
|
@@ -58,9 +58,21 @@ export function buildMediaMap(
|
|
|
58
58
|
fit: "scale-down",
|
|
59
59
|
})
|
|
60
60
|
: mediaUrl;
|
|
61
|
-
|
|
61
|
+
// A poster is a still frame at whatever resolution the source video
|
|
62
|
+
// had, so it needs the same transform as any other image — without it
|
|
63
|
+
// a 4K clip puts a multi-megabyte PNG in the feed. Matches the limits
|
|
64
|
+
// `toMediaView` uses for the same field.
|
|
65
|
+
const posterRawUrl = m.posterKey
|
|
62
66
|
? getMediaUrl(m.posterKey, publicUrl, sitePathPrefix)
|
|
63
67
|
: null;
|
|
68
|
+
const posterUrl = posterRawUrl
|
|
69
|
+
? getImageUrl(posterRawUrl, imageTransformUrl, {
|
|
70
|
+
width: 640,
|
|
71
|
+
quality: 80,
|
|
72
|
+
format: "auto",
|
|
73
|
+
fit: "scale-down",
|
|
74
|
+
})
|
|
75
|
+
: null;
|
|
64
76
|
return {
|
|
65
77
|
id: m.id,
|
|
66
78
|
url: mediaUrl,
|