@jant/core 0.6.15 → 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 +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- 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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- 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__/mise-config.test.ts +22 -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 +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- 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 +88 -1
- 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__/compose-format-convert.test.ts +0 -19
- 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 +79 -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 +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- 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 +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- 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 +1142 -268
- 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 +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- 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/post-refresh.ts +135 -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/__tests__/link-input-rules.test.ts +121 -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-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- 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__/backfill-thread-activity.test.ts +234 -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/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -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 +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -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 +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- 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 +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- 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 +1691 -12
- 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__/markdown.test.ts +16 -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 +88 -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__/translit.test.ts +87 -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 +93 -34
- 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 +1021 -66
- 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 +21 -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/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- 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-display.ts +42 -9
- 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 +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -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__/palette.test.ts +44 -0
- 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 +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -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 +11 -2
- 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/__tests__/sitemap.test.ts +68 -1
- 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 +657 -1
- 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__/featured.test.ts +6 -2
- 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__/preview.test.ts +3 -1
- 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 +507 -357
- 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 +183 -19
- 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 +139 -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-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -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 +330 -47
- 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 +281 -24
- 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 +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -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 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- 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 +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- 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 +401 -301
- 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 +122 -3
- 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/DraftPreviewBar.tsx +25 -9
- 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 +55 -23
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.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-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- 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
package/src/db/schema.ts
CHANGED
|
@@ -15,26 +15,37 @@ import {
|
|
|
15
15
|
check,
|
|
16
16
|
} from "drizzle-orm/sqlite-core";
|
|
17
17
|
import { sql } from "drizzle-orm";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
import {
|
|
19
|
+
ARCHIVE_LAYOUTS,
|
|
20
|
+
COLLECTION_DIRECTORY_ENTRY_TYPES,
|
|
21
|
+
COLLECTION_SORT_ORDERS,
|
|
22
|
+
CONTENT_DISPOSITIONS,
|
|
23
|
+
EARLIEST_FILTERABLE_YEAR,
|
|
24
|
+
FORMATS,
|
|
25
|
+
GITHUB_APP_ACCOUNT_TYPES,
|
|
26
|
+
LATEST_FILTERABLE_YEAR,
|
|
27
|
+
NAV_ITEM_PLACEMENTS,
|
|
28
|
+
NAV_ITEM_TYPES,
|
|
29
|
+
PATH_KINDS,
|
|
30
|
+
PUBLIC_ARCHIVE_VISIBILITIES,
|
|
31
|
+
SITE_DOMAIN_KINDS,
|
|
32
|
+
SITE_MEMBER_ROLES,
|
|
33
|
+
SITE_STATUSES,
|
|
34
|
+
SMART_COLLECTION_SORT_ORDERS,
|
|
35
|
+
STATUSES,
|
|
36
|
+
SYSTEM_NAV_KEY_VALUES,
|
|
37
|
+
UPLOAD_SESSION_STATES,
|
|
38
|
+
VISIBILITIES,
|
|
39
|
+
} from "../types/constants.js";
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Render a value list as the `IN (...)` body of a CHECK constraint.
|
|
43
|
+
*
|
|
44
|
+
* Every list it is handed comes from `types/constants.ts`. Declaring one
|
|
45
|
+
* locally instead is how a CHECK once shipped missing an enum value: the
|
|
46
|
+
* shared list grew, the schema's private copy did not, and the generated
|
|
47
|
+
* constraint rejected rows the application treats as valid.
|
|
48
|
+
*/
|
|
38
49
|
function sqlTextEnum(values: readonly string[]) {
|
|
39
50
|
return sql.raw(values.map((value) => `'${value}'`).join(", "));
|
|
40
51
|
}
|
|
@@ -164,8 +175,46 @@ export const posts = sqliteTable(
|
|
|
164
175
|
previewProvider: text("preview_provider"),
|
|
165
176
|
replyToId: text("reply_to_id"),
|
|
166
177
|
threadId: text("thread_id").notNull(),
|
|
178
|
+
/**
|
|
179
|
+
* BCP 47 content language in canonical form (`en`, `zh-Hans`).
|
|
180
|
+
*
|
|
181
|
+
* Uniform across a Thread: replies inherit the Root's value on creation and
|
|
182
|
+
* a language change rewrites every row in the Thread. Language filters can
|
|
183
|
+
* therefore be plain column predicates even on member-grained queries.
|
|
184
|
+
*
|
|
185
|
+
* NULL only exists before multilingual content is first enabled; enabling
|
|
186
|
+
* stamps every NULL row with the primary language.
|
|
187
|
+
*/
|
|
188
|
+
language: text("language"),
|
|
189
|
+
/**
|
|
190
|
+
* Shared translation-group key (TypeID, `tgr_` prefix). Rows sharing a value
|
|
191
|
+
* are translations of one another — one row per language, no direction.
|
|
192
|
+
*
|
|
193
|
+
* Only ever set on Thread Roots (`thread_id = id`); enforced in the post
|
|
194
|
+
* service rather than by a table CHECK, because adding a CHECK to `post`
|
|
195
|
+
* would make drizzle-kit rebuild the table and silently drop the FTS
|
|
196
|
+
* triggers (`post_ai` / `post_ad` / `post_au`) that keep search current.
|
|
197
|
+
*/
|
|
198
|
+
translationGroupId: text("translation_group_id"),
|
|
199
|
+
/**
|
|
200
|
+
* The author added this reply without announcing it. Persisted so both
|
|
201
|
+
* activity timestamps below stay recomputable from the rows alone.
|
|
202
|
+
* Always false on Thread roots.
|
|
203
|
+
*/
|
|
204
|
+
quietReply: integer("quiet_reply", { mode: "boolean" })
|
|
205
|
+
.notNull()
|
|
206
|
+
.default(false),
|
|
167
207
|
publishedAt: integer("published_at"),
|
|
208
|
+
/**
|
|
209
|
+
* Root only: newest published_at in the Thread, excluding quiet replies —
|
|
210
|
+
* "last announced". Drives Latest and the feeds.
|
|
211
|
+
*/
|
|
168
212
|
lastActivityAt: integer("last_activity_at"),
|
|
213
|
+
/**
|
|
214
|
+
* Root only: newest published_at in the Thread, quiet replies included —
|
|
215
|
+
* "last changed". Drives the archive's updated sort.
|
|
216
|
+
*/
|
|
217
|
+
threadUpdatedAt: integer("thread_updated_at"),
|
|
169
218
|
createdAt: integer("created_at").notNull(),
|
|
170
219
|
updatedAt: integer("updated_at").notNull(),
|
|
171
220
|
},
|
|
@@ -213,6 +262,9 @@ export const posts = sqliteTable(
|
|
|
213
262
|
index("idx_post_site_root_published_activity")
|
|
214
263
|
.on(table.siteId, table.lastActivityAt, table.id)
|
|
215
264
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
265
|
+
index("idx_post_site_root_thread_updated")
|
|
266
|
+
.on(table.siteId, table.threadUpdatedAt, table.id)
|
|
267
|
+
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
216
268
|
index("idx_post_site_root_draft_updated")
|
|
217
269
|
.on(table.siteId, table.updatedAt, table.id)
|
|
218
270
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'draft'`),
|
|
@@ -226,6 +278,12 @@ export const posts = sqliteTable(
|
|
|
226
278
|
.where(
|
|
227
279
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
228
280
|
),
|
|
281
|
+
// One Post per language inside a translation group. The `site_id` prefix
|
|
282
|
+
// matches the indexing convention here and lets the same index serve
|
|
283
|
+
// "list the members of this group" without a second index.
|
|
284
|
+
uniqueIndex("uq_post_site_translation_group_language")
|
|
285
|
+
.on(table.siteId, table.translationGroupId, table.language)
|
|
286
|
+
.where(sql`${table.translationGroupId} IS NOT NULL`),
|
|
229
287
|
],
|
|
230
288
|
);
|
|
231
289
|
|
|
@@ -421,6 +479,108 @@ export const collections = sqliteTable(
|
|
|
421
479
|
],
|
|
422
480
|
);
|
|
423
481
|
|
|
482
|
+
// =============================================================================
|
|
483
|
+
// Smart Collections
|
|
484
|
+
// =============================================================================
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* A collection whose members are decided by conditions rather than by tagging.
|
|
488
|
+
*
|
|
489
|
+
* Kept apart from `collection` rather than folded into it. The two share an
|
|
490
|
+
* identity shape — a path registry row, a title, a description — but half the
|
|
491
|
+
* columns of a merged table would be meaningless for half its rows, every query
|
|
492
|
+
* would branch, and a smart collection can filter *by* a collection, which in
|
|
493
|
+
* one table becomes a self-reference and a cycle to defend against. The
|
|
494
|
+
* resemblance belongs in a read model, not in the storage.
|
|
495
|
+
*
|
|
496
|
+
* The condition columns are declared once in `lib/filter-dimensions.ts`; this
|
|
497
|
+
* table is where they land. `NULL` always means "this dimension is not part of
|
|
498
|
+
* the conditions".
|
|
499
|
+
*
|
|
500
|
+
* Never private: `visibility` is constrained to the public archive values, so
|
|
501
|
+
* there is no such thing as a smart collection page that answers 404 to a
|
|
502
|
+
* reader. Per-post visibility still applies exactly as it does on a manual
|
|
503
|
+
* collection page.
|
|
504
|
+
*
|
|
505
|
+
* `collection_id` carries no foreign key, which makes it the one id column here
|
|
506
|
+
* that does not. None of the three actions is the right one: CASCADE would
|
|
507
|
+
* delete a smart collection — a page, with an address — because a collection it
|
|
508
|
+
* filters by went away, SET NULL would silently widen it from "Rust notes" to
|
|
509
|
+
* every post, and RESTRICT can only say `FOREIGN KEY constraint failed`, which
|
|
510
|
+
* no one should ever be shown. The refusal that belongs here is
|
|
511
|
+
* `smartCollections.assertCollectionUnused()`, which names the smart
|
|
512
|
+
* collections standing in the way; a constraint underneath it would add no
|
|
513
|
+
* answer, only turn every bulk delete into an ordering rule nothing enforces.
|
|
514
|
+
*
|
|
515
|
+
* What a dangling id costs is bounded: the condition compiles to an `IN` over a
|
|
516
|
+
* collection no `thread_collection` row carries, so the smart collection matches
|
|
517
|
+
* nothing. Visible and repairable by editing the conditions — not a page that
|
|
518
|
+
* quietly grew to the whole archive.
|
|
519
|
+
*/
|
|
520
|
+
export const smartCollections = sqliteTable(
|
|
521
|
+
"smart_collection",
|
|
522
|
+
{
|
|
523
|
+
id: text("id").primaryKey(),
|
|
524
|
+
siteId: text("site_id")
|
|
525
|
+
.notNull()
|
|
526
|
+
.references(() => sites.id, { onDelete: "cascade" }),
|
|
527
|
+
title: text("title").notNull(),
|
|
528
|
+
description: text("description"),
|
|
529
|
+
|
|
530
|
+
// --- Conditions (see lib/filter-dimensions.ts) --------------------------
|
|
531
|
+
format: text("format", { enum: FORMATS }),
|
|
532
|
+
year: integer("year"),
|
|
533
|
+
// No foreign key, alone among this schema's id columns — see the note under
|
|
534
|
+
// the table on why this one is held in the service instead.
|
|
535
|
+
collectionId: text("collection_id"),
|
|
536
|
+
/** `any` | `none` | comma-joined MEDIA_KINDS. One folded vocabulary. */
|
|
537
|
+
media: text("media"),
|
|
538
|
+
hasTitle: integer("has_title", { mode: "boolean" }),
|
|
539
|
+
hasReplies: integer("has_replies", { mode: "boolean" }),
|
|
540
|
+
visibility: text("visibility", { enum: PUBLIC_ARCHIVE_VISIBILITIES }),
|
|
541
|
+
|
|
542
|
+
// --- Presentation (not conditions) --------------------------------------
|
|
543
|
+
sort: text("sort", { enum: SMART_COLLECTION_SORT_ORDERS })
|
|
544
|
+
.notNull()
|
|
545
|
+
.default("newest"),
|
|
546
|
+
/** NULL follows the site's `ARCHIVE_DEFAULT_LAYOUT`. */
|
|
547
|
+
layout: text("layout", { enum: ARCHIVE_LAYOUTS }),
|
|
548
|
+
|
|
549
|
+
createdAt: integer("created_at").notNull(),
|
|
550
|
+
updatedAt: integer("updated_at").notNull(),
|
|
551
|
+
},
|
|
552
|
+
(table) => [
|
|
553
|
+
check(
|
|
554
|
+
"chk_smart_collection_format",
|
|
555
|
+
sql`${table.format} IS NULL OR ${table.format} IN (${sqlTextEnum(FORMATS)})`,
|
|
556
|
+
),
|
|
557
|
+
check(
|
|
558
|
+
"chk_smart_collection_visibility",
|
|
559
|
+
sql`${table.visibility} IS NULL OR ${table.visibility} IN (${sqlTextEnum(PUBLIC_ARCHIVE_VISIBILITIES)})`,
|
|
560
|
+
),
|
|
561
|
+
check(
|
|
562
|
+
"chk_smart_collection_sort",
|
|
563
|
+
sql`${table.sort} IN (${sqlTextEnum(SMART_COLLECTION_SORT_ORDERS)})`,
|
|
564
|
+
),
|
|
565
|
+
check(
|
|
566
|
+
"chk_smart_collection_layout",
|
|
567
|
+
sql`${table.layout} IS NULL OR ${table.layout} IN (${sqlTextEnum(ARCHIVE_LAYOUTS)})`,
|
|
568
|
+
),
|
|
569
|
+
check(
|
|
570
|
+
"chk_smart_collection_year",
|
|
571
|
+
sql`${table.year} IS NULL OR ${table.year} BETWEEN ${sql.raw(String(EARLIEST_FILTERABLE_YEAR))} AND ${sql.raw(String(LATEST_FILTERABLE_YEAR))}`,
|
|
572
|
+
),
|
|
573
|
+
index("idx_smart_collection_site_created_at").on(
|
|
574
|
+
table.siteId,
|
|
575
|
+
table.createdAt,
|
|
576
|
+
),
|
|
577
|
+
index("idx_smart_collection_site_collection_id").on(
|
|
578
|
+
table.siteId,
|
|
579
|
+
table.collectionId,
|
|
580
|
+
),
|
|
581
|
+
],
|
|
582
|
+
);
|
|
583
|
+
|
|
424
584
|
// =============================================================================
|
|
425
585
|
// Path Registry (slug + alias + redirect)
|
|
426
586
|
// =============================================================================
|
|
@@ -442,6 +602,10 @@ export const pathRegistry = sqliteTable(
|
|
|
442
602
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
443
603
|
onDelete: "cascade",
|
|
444
604
|
}),
|
|
605
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
606
|
+
() => smartCollections.id,
|
|
607
|
+
{ onDelete: "cascade" },
|
|
608
|
+
),
|
|
445
609
|
redirectToPath: text("redirect_to_path"),
|
|
446
610
|
redirectType: integer("redirect_type"),
|
|
447
611
|
archiveQuery: text("archive_query"),
|
|
@@ -451,7 +615,7 @@ export const pathRegistry = sqliteTable(
|
|
|
451
615
|
(table) => [
|
|
452
616
|
check(
|
|
453
617
|
"chk_path_registry_kind",
|
|
454
|
-
sql`${table.kind} IN (
|
|
618
|
+
sql`${table.kind} IN (${sqlTextEnum(PATH_KINDS)})`,
|
|
455
619
|
),
|
|
456
620
|
uniqueIndex("uq_path_registry_site_path").on(table.siteId, table.path),
|
|
457
621
|
uniqueIndex("uq_path_registry_site_post_slug")
|
|
@@ -460,18 +624,28 @@ export const pathRegistry = sqliteTable(
|
|
|
460
624
|
uniqueIndex("uq_path_registry_site_collection_slug")
|
|
461
625
|
.on(table.siteId, table.collectionId)
|
|
462
626
|
.where(sql`${table.kind} = 'slug' AND ${table.collectionId} IS NOT NULL`),
|
|
627
|
+
uniqueIndex("uq_path_registry_site_smart_collection_slug")
|
|
628
|
+
.on(table.siteId, table.smartCollectionId)
|
|
629
|
+
.where(
|
|
630
|
+
sql`${table.kind} = 'slug' AND ${table.smartCollectionId} IS NOT NULL`,
|
|
631
|
+
),
|
|
463
632
|
index("idx_path_registry_site_post_id").on(table.siteId, table.postId),
|
|
464
633
|
index("idx_path_registry_site_collection_id").on(
|
|
465
634
|
table.siteId,
|
|
466
635
|
table.collectionId,
|
|
467
636
|
),
|
|
637
|
+
index("idx_path_registry_site_smart_collection_id").on(
|
|
638
|
+
table.siteId,
|
|
639
|
+
table.smartCollectionId,
|
|
640
|
+
),
|
|
468
641
|
check(
|
|
469
642
|
"chk_path_registry_shape",
|
|
470
643
|
sql`(
|
|
471
644
|
${table.kind} IN ('slug', 'alias')
|
|
472
645
|
AND (
|
|
473
|
-
(${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL)
|
|
474
|
-
OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL)
|
|
646
|
+
(${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NULL)
|
|
647
|
+
OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL AND ${table.smartCollectionId} IS NULL)
|
|
648
|
+
OR (${table.postId} IS NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NOT NULL)
|
|
475
649
|
)
|
|
476
650
|
AND ${table.redirectToPath} IS NULL
|
|
477
651
|
AND ${table.redirectType} IS NULL
|
|
@@ -480,6 +654,7 @@ export const pathRegistry = sqliteTable(
|
|
|
480
654
|
${table.kind} = 'redirect'
|
|
481
655
|
AND ${table.postId} IS NULL
|
|
482
656
|
AND ${table.collectionId} IS NULL
|
|
657
|
+
AND ${table.smartCollectionId} IS NULL
|
|
483
658
|
AND ${table.redirectToPath} IS NOT NULL
|
|
484
659
|
AND ${table.redirectType} IN (301, 302)
|
|
485
660
|
AND ${table.archiveQuery} IS NULL
|
|
@@ -487,6 +662,7 @@ export const pathRegistry = sqliteTable(
|
|
|
487
662
|
${table.kind} = 'archive'
|
|
488
663
|
AND ${table.postId} IS NULL
|
|
489
664
|
AND ${table.collectionId} IS NULL
|
|
665
|
+
AND ${table.smartCollectionId} IS NULL
|
|
490
666
|
AND ${table.redirectToPath} IS NULL
|
|
491
667
|
AND ${table.redirectType} IS NULL
|
|
492
668
|
AND ${table.archiveQuery} IS NOT NULL
|
|
@@ -506,10 +682,16 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
506
682
|
siteId: text("site_id")
|
|
507
683
|
.notNull()
|
|
508
684
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
509
|
-
type: text("type", {
|
|
685
|
+
type: text("type", {
|
|
686
|
+
enum: COLLECTION_DIRECTORY_ENTRY_TYPES,
|
|
687
|
+
}).notNull(),
|
|
510
688
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
511
689
|
onDelete: "cascade",
|
|
512
690
|
}),
|
|
691
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
692
|
+
() => smartCollections.id,
|
|
693
|
+
{ onDelete: "cascade" },
|
|
694
|
+
),
|
|
513
695
|
label: text("label"),
|
|
514
696
|
url: text("url"),
|
|
515
697
|
description: text("description"),
|
|
@@ -520,12 +702,16 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
520
702
|
(table) => [
|
|
521
703
|
check(
|
|
522
704
|
"chk_collection_directory_item_type",
|
|
523
|
-
sql`${table.type} IN (
|
|
705
|
+
sql`${table.type} IN (${sqlTextEnum(COLLECTION_DIRECTORY_ENTRY_TYPES)})`,
|
|
524
706
|
),
|
|
525
707
|
index("idx_collection_directory_item_site_collection_id").on(
|
|
526
708
|
table.siteId,
|
|
527
709
|
table.collectionId,
|
|
528
710
|
),
|
|
711
|
+
index("idx_collection_directory_item_site_smart_collection_id").on(
|
|
712
|
+
table.siteId,
|
|
713
|
+
table.smartCollectionId,
|
|
714
|
+
),
|
|
529
715
|
uniqueIndex("uq_collection_directory_item_site_position").on(
|
|
530
716
|
table.siteId,
|
|
531
717
|
table.position,
|
|
@@ -535,27 +721,41 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
535
721
|
.where(
|
|
536
722
|
sql`${table.type} = 'collection' AND ${table.collectionId} IS NOT NULL`,
|
|
537
723
|
),
|
|
724
|
+
uniqueIndex("uq_collection_directory_item_site_smart_collection_once")
|
|
725
|
+
.on(table.siteId, table.smartCollectionId)
|
|
726
|
+
.where(
|
|
727
|
+
sql`${table.type} = 'smart_collection' AND ${table.smartCollectionId} IS NOT NULL`,
|
|
728
|
+
),
|
|
538
729
|
check(
|
|
539
730
|
"chk_collection_directory_item_shape",
|
|
540
731
|
sql`(
|
|
541
732
|
${table.type} = 'collection'
|
|
542
733
|
AND ${table.collectionId} IS NOT NULL
|
|
734
|
+
AND ${table.smartCollectionId} IS NULL
|
|
735
|
+
AND ${table.label} IS NULL
|
|
736
|
+
AND ${table.url} IS NULL
|
|
737
|
+
) OR (
|
|
738
|
+
${table.type} = 'smart_collection'
|
|
739
|
+
AND ${table.collectionId} IS NULL
|
|
740
|
+
AND ${table.smartCollectionId} IS NOT NULL
|
|
543
741
|
AND ${table.label} IS NULL
|
|
544
742
|
AND ${table.url} IS NULL
|
|
545
743
|
) OR (
|
|
546
744
|
${table.type} = 'divider'
|
|
547
745
|
AND ${table.collectionId} IS NULL
|
|
746
|
+
AND ${table.smartCollectionId} IS NULL
|
|
548
747
|
AND ${table.url} IS NULL
|
|
549
748
|
) OR (
|
|
550
749
|
${table.type} = 'link'
|
|
551
750
|
AND ${table.collectionId} IS NULL
|
|
751
|
+
AND ${table.smartCollectionId} IS NULL
|
|
552
752
|
AND ${table.label} IS NOT NULL
|
|
553
753
|
AND ${table.url} IS NOT NULL
|
|
554
754
|
)`,
|
|
555
755
|
),
|
|
556
756
|
check(
|
|
557
757
|
"chk_collection_directory_item_label",
|
|
558
|
-
sql`${table.type}
|
|
758
|
+
sql`${table.type} NOT IN ('collection', 'smart_collection') OR ${table.label} IS NULL`,
|
|
559
759
|
),
|
|
560
760
|
check(
|
|
561
761
|
"chk_collection_directory_item_description",
|
|
@@ -621,6 +821,10 @@ export const navItems = sqliteTable(
|
|
|
621
821
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
622
822
|
onDelete: "cascade",
|
|
623
823
|
}),
|
|
824
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
825
|
+
() => smartCollections.id,
|
|
826
|
+
{ onDelete: "cascade" },
|
|
827
|
+
),
|
|
624
828
|
postId: text("post_id").references(() => posts.id, {
|
|
625
829
|
onDelete: "cascade",
|
|
626
830
|
}),
|
|
@@ -650,21 +854,31 @@ export const navItems = sqliteTable(
|
|
|
650
854
|
${table.type} = 'link'
|
|
651
855
|
AND ${table.systemKey} IS NULL
|
|
652
856
|
AND ${table.collectionId} IS NULL
|
|
857
|
+
AND ${table.smartCollectionId} IS NULL
|
|
653
858
|
AND ${table.postId} IS NULL
|
|
654
859
|
) OR (
|
|
655
860
|
${table.type} = 'system'
|
|
656
861
|
AND ${table.systemKey} IS NOT NULL
|
|
657
862
|
AND ${table.collectionId} IS NULL
|
|
863
|
+
AND ${table.smartCollectionId} IS NULL
|
|
658
864
|
AND ${table.postId} IS NULL
|
|
659
865
|
) OR (
|
|
660
866
|
${table.type} = 'collection'
|
|
661
867
|
AND ${table.systemKey} IS NULL
|
|
662
868
|
AND ${table.collectionId} IS NOT NULL
|
|
869
|
+
AND ${table.smartCollectionId} IS NULL
|
|
870
|
+
AND ${table.postId} IS NULL
|
|
871
|
+
) OR (
|
|
872
|
+
${table.type} = 'smart_collection'
|
|
873
|
+
AND ${table.systemKey} IS NULL
|
|
874
|
+
AND ${table.collectionId} IS NULL
|
|
875
|
+
AND ${table.smartCollectionId} IS NOT NULL
|
|
663
876
|
AND ${table.postId} IS NULL
|
|
664
877
|
) OR (
|
|
665
878
|
${table.type} = 'page'
|
|
666
879
|
AND ${table.systemKey} IS NULL
|
|
667
880
|
AND ${table.collectionId} IS NULL
|
|
881
|
+
AND ${table.smartCollectionId} IS NULL
|
|
668
882
|
AND ${table.postId} IS NOT NULL
|
|
669
883
|
)`,
|
|
670
884
|
),
|
|
@@ -675,6 +889,9 @@ export const navItems = sqliteTable(
|
|
|
675
889
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
676
890
|
.on(table.siteId, table.collectionId)
|
|
677
891
|
.where(sql`${table.collectionId} IS NOT NULL`),
|
|
892
|
+
uniqueIndex("uq_nav_item_site_smart_collection_id")
|
|
893
|
+
.on(table.siteId, table.smartCollectionId)
|
|
894
|
+
.where(sql`${table.smartCollectionId} IS NOT NULL`),
|
|
678
895
|
uniqueIndex("uq_nav_item_site_post_id")
|
|
679
896
|
.on(table.siteId, table.postId)
|
|
680
897
|
.where(sql`${table.postId} IS NOT NULL`),
|
|
@@ -792,8 +1009,6 @@ export const verification = sqliteTable("verification", {
|
|
|
792
1009
|
// GitHub App Installations (junction table: installation ↔ site, many-to-many)
|
|
793
1010
|
// ---------------------------------------------------------------------------
|
|
794
1011
|
|
|
795
|
-
const GITHUB_APP_ACCOUNT_TYPES = ["User", "Organization"] as const;
|
|
796
|
-
|
|
797
1012
|
export const githubAppInstallation = sqliteTable(
|
|
798
1013
|
"github_app_installation",
|
|
799
1014
|
{
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread activity — one definition, shared by every surface that orders by it.
|
|
3
|
+
*
|
|
4
|
+
* A Thread's activity is when it last **gained a post**. Editing an existing
|
|
5
|
+
* post is not activity: otherwise fixing a typo would drag a years-old thread
|
|
6
|
+
* back to the top of Latest and of every collection that holds it.
|
|
7
|
+
*
|
|
8
|
+
* This lived as three separate copies (the collection thread sort, the
|
|
9
|
+
* collection directory, and the Hugo export). One of them was changed in #144
|
|
10
|
+
* to treat a newer `updated_at` as activity and the others were not, so the
|
|
11
|
+
* same thread ranked differently depending on which page you were looking at,
|
|
12
|
+
* and the drift went unnoticed for months. Adding a fourth copy is the failure
|
|
13
|
+
* mode this module exists to prevent — call it instead.
|
|
14
|
+
*
|
|
15
|
+
* Two flavors of "last activity" exist, and they differ only in how they treat
|
|
16
|
+
* quiet replies:
|
|
17
|
+
*
|
|
18
|
+
* - `last_activity_at` — quiet replies excluded. "Last announced." Latest, the
|
|
19
|
+
* feeds, and collection ordering read this.
|
|
20
|
+
* - `thread_updated_at` — quiet replies included. "Last changed." The
|
|
21
|
+
* archive's updated sort reads this.
|
|
22
|
+
*
|
|
23
|
+
* Both are maintained on the Thread root by the post service; everything here
|
|
24
|
+
* only reads them, with fallbacks for rows that predate the columns.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { sql, type SQL, type SQLWrapper } from "drizzle-orm";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Activity timestamp for a Thread root row, in SQL.
|
|
31
|
+
*
|
|
32
|
+
* Pass column references for a query whose rows are already Thread roots, or
|
|
33
|
+
* raw fragments (`sql.raw("root.last_activity_at")`) when reaching a root
|
|
34
|
+
* through a correlated subquery.
|
|
35
|
+
*
|
|
36
|
+
* **Correlated-subquery warning:** Drizzle only qualifies interpolated columns
|
|
37
|
+
* with their table name when the query has a join. In a single-table
|
|
38
|
+
* `select()`, `${posts.id}` renders as a bare `"id"`, which inside a subquery
|
|
39
|
+
* binds to the subquery's own alias instead of correlating to the outer row —
|
|
40
|
+
* silently, with no error. Write the outer table out (`"post"."id"`) in that
|
|
41
|
+
* case rather than interpolating.
|
|
42
|
+
*
|
|
43
|
+
* @param root - The root's three timestamp columns, most authoritative first
|
|
44
|
+
* @returns COALESCE expression usable in ORDER BY, GROUP BY, or a projection
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* // Rows are already Thread roots (joined on thread_collection.thread_id):
|
|
49
|
+
* const activityAt = buildRootActivityExpr({
|
|
50
|
+
* lastActivityAt: posts.lastActivityAt,
|
|
51
|
+
* publishedAt: posts.publishedAt,
|
|
52
|
+
* updatedAt: posts.updatedAt,
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function buildRootActivityExpr(root: {
|
|
57
|
+
lastActivityAt: SQLWrapper;
|
|
58
|
+
publishedAt: SQLWrapper;
|
|
59
|
+
updatedAt: SQLWrapper;
|
|
60
|
+
}): SQL<number> {
|
|
61
|
+
return sql<number>`COALESCE(
|
|
62
|
+
${root.lastActivityAt},
|
|
63
|
+
${root.publishedAt},
|
|
64
|
+
${root.updatedAt}
|
|
65
|
+
)`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The Thread root's timestamp columns, named for a correlated subquery alias. */
|
|
69
|
+
export function rootActivityColumns(alias: string): {
|
|
70
|
+
lastActivityAt: SQLWrapper;
|
|
71
|
+
publishedAt: SQLWrapper;
|
|
72
|
+
updatedAt: SQLWrapper;
|
|
73
|
+
} {
|
|
74
|
+
return {
|
|
75
|
+
lastActivityAt: sql.raw(`${alias}.last_activity_at`),
|
|
76
|
+
publishedAt: sql.raw(`${alias}.published_at`),
|
|
77
|
+
updatedAt: sql.raw(`${alias}.updated_at`),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Activity timestamp for an already-loaded Thread root, in TypeScript.
|
|
83
|
+
*
|
|
84
|
+
* The same COALESCE order as {@link buildRootActivityExpr}, for callers that
|
|
85
|
+
* hold hydrated posts rather than a query builder.
|
|
86
|
+
*
|
|
87
|
+
* @param root - A Thread root post
|
|
88
|
+
* @returns Unix timestamp (seconds) of the root's last activity
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const activityAt = getRootActivityAt(threadRoot);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function getRootActivityAt(root: {
|
|
96
|
+
lastActivityAt?: number | null;
|
|
97
|
+
publishedAt?: number | null;
|
|
98
|
+
updatedAt: number;
|
|
99
|
+
}): number {
|
|
100
|
+
return root.lastActivityAt ?? root.publishedAt ?? root.updatedAt;
|
|
101
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
detectLocaleFromHeader,
|
|
4
|
+
getCjkFontFromLanguageTag,
|
|
5
|
+
} from "../detect.js";
|
|
3
6
|
|
|
4
7
|
describe("detectLocaleFromHeader", () => {
|
|
5
8
|
it("returns en for en", () => {
|
|
@@ -45,94 +48,36 @@ describe("detectLocaleFromHeader", () => {
|
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
describe("
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expect(
|
|
51
|
+
describe("getCjkFontFromLanguageTag", () => {
|
|
52
|
+
it("maps Simplified Chinese tags to the zh-Hans profile", () => {
|
|
53
|
+
expect(getCjkFontFromLanguageTag("zh-CN")).toBe("zh-Hans");
|
|
54
|
+
expect(getCjkFontFromLanguageTag("zh-SG")).toBe("zh-Hans");
|
|
55
|
+
expect(getCjkFontFromLanguageTag("zh-Hans")).toBe("zh-Hans");
|
|
56
|
+
// Bare `zh` is ambiguous in principle; Simplified is the majority reading.
|
|
57
|
+
expect(getCjkFontFromLanguageTag("zh")).toBe("zh-Hans");
|
|
52
58
|
});
|
|
53
59
|
|
|
54
|
-
it("maps
|
|
55
|
-
expect(
|
|
60
|
+
it("maps Traditional Chinese tags to the zh-Hant profile", () => {
|
|
61
|
+
expect(getCjkFontFromLanguageTag("zh-TW")).toBe("zh-Hant");
|
|
62
|
+
expect(getCjkFontFromLanguageTag("zh-HK")).toBe("zh-Hant");
|
|
63
|
+
expect(getCjkFontFromLanguageTag("zh-MO")).toBe("zh-Hant");
|
|
64
|
+
expect(getCjkFontFromLanguageTag("zh-Hant")).toBe("zh-Hant");
|
|
56
65
|
});
|
|
57
66
|
|
|
58
|
-
it("maps
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expect(detectCjkFontFromHeader("zh")).toBe("zh-Hans");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Traditional Chinese
|
|
67
|
-
it("maps zh-TW to zh-Hant", () => {
|
|
68
|
-
expect(detectCjkFontFromHeader("zh-TW")).toBe("zh-Hant");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("maps zh-HK to zh-Hant", () => {
|
|
72
|
-
expect(detectCjkFontFromHeader("zh-HK")).toBe("zh-Hant");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("maps zh-MO to zh-Hant", () => {
|
|
76
|
-
expect(detectCjkFontFromHeader("zh-MO")).toBe("zh-Hant");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("maps zh-Hant to zh-Hant", () => {
|
|
80
|
-
expect(detectCjkFontFromHeader("zh-Hant")).toBe("zh-Hant");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Japanese
|
|
84
|
-
it("maps ja to ja", () => {
|
|
85
|
-
expect(detectCjkFontFromHeader("ja")).toBe("ja");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("maps ja-JP to ja", () => {
|
|
89
|
-
expect(detectCjkFontFromHeader("ja-JP")).toBe("ja");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Korean
|
|
93
|
-
it("maps ko to ko", () => {
|
|
94
|
-
expect(detectCjkFontFromHeader("ko")).toBe("ko");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("maps ko-KR to ko", () => {
|
|
98
|
-
expect(detectCjkFontFromHeader("ko-KR")).toBe("ko");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// q-value priority
|
|
102
|
-
it("picks highest q-value CJK language", () => {
|
|
103
|
-
expect(detectCjkFontFromHeader("fr;q=0.5,zh-CN;q=0.9,en;q=0.8")).toBe(
|
|
104
|
-
"zh-Hans",
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("picks first CJK when q-values are equal", () => {
|
|
109
|
-
expect(detectCjkFontFromHeader("zh-TW,en")).toBe("zh-Hant");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Non-CJK fallback
|
|
113
|
-
it("returns off for non-CJK language", () => {
|
|
114
|
-
expect(detectCjkFontFromHeader("en-US")).toBe("off");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("returns off for undefined", () => {
|
|
118
|
-
expect(detectCjkFontFromHeader(undefined)).toBe("off");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("returns off for empty string", () => {
|
|
122
|
-
expect(detectCjkFontFromHeader("")).toBe("off");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("skips entries with q=0", () => {
|
|
126
|
-
expect(detectCjkFontFromHeader("zh-CN;q=0,en")).toBe("off");
|
|
67
|
+
it("maps Japanese and Korean tags", () => {
|
|
68
|
+
expect(getCjkFontFromLanguageTag("ja")).toBe("ja");
|
|
69
|
+
expect(getCjkFontFromLanguageTag("ja-JP")).toBe("ja");
|
|
70
|
+
expect(getCjkFontFromLanguageTag("ko")).toBe("ko");
|
|
71
|
+
expect(getCjkFontFromLanguageTag("ko-KR")).toBe("ko");
|
|
127
72
|
});
|
|
128
73
|
|
|
129
74
|
it("is case-insensitive", () => {
|
|
130
|
-
expect(
|
|
75
|
+
expect(getCjkFontFromLanguageTag("ZH-CN")).toBe("zh-Hans");
|
|
131
76
|
});
|
|
132
77
|
|
|
133
|
-
it("
|
|
134
|
-
expect(
|
|
135
|
-
|
|
136
|
-
);
|
|
78
|
+
it("returns nothing for languages with no CJK profile", () => {
|
|
79
|
+
expect(getCjkFontFromLanguageTag("en-US")).toBeUndefined();
|
|
80
|
+
expect(getCjkFontFromLanguageTag("")).toBeUndefined();
|
|
81
|
+
expect(getCjkFontFromLanguageTag(" ")).toBeUndefined();
|
|
137
82
|
});
|
|
138
83
|
});
|