@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
|
@@ -141,4 +141,74 @@ describe("generatePostSlug", () => {
|
|
|
141
141
|
).rejects.toThrow("Could not generate a unique slug");
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
describe("language suffix", () => {
|
|
146
|
+
/** Every slug is taken except the ones listed. */
|
|
147
|
+
const onlyAvailable = (...free: string[]) => {
|
|
148
|
+
const allowed = new Set(free);
|
|
149
|
+
return async (slug: string) => allowed.has(slug);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
it("is not used when the base slug is free", async () => {
|
|
153
|
+
const slug = await generatePostSlug({
|
|
154
|
+
title: "Book Review",
|
|
155
|
+
idLength: 5,
|
|
156
|
+
languageSuffix: "en",
|
|
157
|
+
isAvailable: alwaysAvailable,
|
|
158
|
+
});
|
|
159
|
+
expect(slug).toBe("book-review");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("is tried before a random suffix when the base collides", async () => {
|
|
163
|
+
const slug = await generatePostSlug({
|
|
164
|
+
title: "Book Review",
|
|
165
|
+
idLength: 5,
|
|
166
|
+
languageSuffix: "en",
|
|
167
|
+
isAvailable: onlyAvailable("book-review-en"),
|
|
168
|
+
});
|
|
169
|
+
expect(slug).toBe("book-review-en");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("keeps the multi-part tag intact", async () => {
|
|
173
|
+
const slug = await generatePostSlug({
|
|
174
|
+
title: "Book Review",
|
|
175
|
+
idLength: 5,
|
|
176
|
+
languageSuffix: "zh-hant",
|
|
177
|
+
isAvailable: onlyAvailable("book-review-zh-hant"),
|
|
178
|
+
});
|
|
179
|
+
expect(slug).toBe("book-review-zh-hant");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("falls back to a random suffix when the language slug is taken too", async () => {
|
|
183
|
+
const slug = await generatePostSlug({
|
|
184
|
+
title: "Book Review",
|
|
185
|
+
idLength: 5,
|
|
186
|
+
languageSuffix: "en",
|
|
187
|
+
isAvailable: async (candidate: string) =>
|
|
188
|
+
candidate !== "book-review" && candidate !== "book-review-en",
|
|
189
|
+
});
|
|
190
|
+
expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("goes straight to a random suffix without one", async () => {
|
|
194
|
+
const slug = await generatePostSlug({
|
|
195
|
+
title: "Book Review",
|
|
196
|
+
idLength: 5,
|
|
197
|
+
isAvailable: async (candidate: string) => candidate !== "book-review",
|
|
198
|
+
});
|
|
199
|
+
expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("skips a language candidate that lands on a reserved path", async () => {
|
|
203
|
+
const slug = await generatePostSlug({
|
|
204
|
+
title: "feed",
|
|
205
|
+
idLength: 5,
|
|
206
|
+
languageSuffix: "en",
|
|
207
|
+
isAvailable: alwaysAvailable,
|
|
208
|
+
});
|
|
209
|
+
// "feed" is reserved, so the base is never offered and the language
|
|
210
|
+
// candidate is never reached — the random fallback owns this case.
|
|
211
|
+
expect(slug).toMatch(/^feed-[a-z0-9]{5}$/);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
144
214
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { slugify } from "../slugify.js";
|
|
3
|
+
|
|
4
|
+
describe("slugify", () => {
|
|
5
|
+
it("converts text to lowercase hyphenated slug", () => {
|
|
6
|
+
expect(slugify("Hello World")).toBe("hello-world");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("removes special characters", () => {
|
|
10
|
+
expect(slugify("Hello World! This is a Test.")).toBe(
|
|
11
|
+
"hello-world-this-is-a-test",
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("collapses multiple spaces", () => {
|
|
16
|
+
expect(slugify("Multiple Spaces")).toBe("multiple-spaces");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("trims leading/trailing whitespace and hyphens", () => {
|
|
20
|
+
expect(slugify(" Multiple Spaces ")).toBe("multiple-spaces");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("replaces underscores with hyphens", () => {
|
|
24
|
+
expect(slugify("hello_world")).toBe("hello-world");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("handles already-slugified text", () => {
|
|
28
|
+
expect(slugify("already-a-slug")).toBe("already-a-slug");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("handles empty string", () => {
|
|
32
|
+
expect(slugify("")).toBe("");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("transliterates accented characters", () => {
|
|
36
|
+
expect(slugify("café & résumé")).toBe("cafe-and-resume");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("converts Chinese characters to pinyin", () => {
|
|
40
|
+
expect(slugify("书评")).toBe("shu-ping");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles mixed Chinese and English text", () => {
|
|
44
|
+
expect(slugify("我的 Blog")).toBe("wo-de-blog");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles CJK characters with spaces", () => {
|
|
48
|
+
expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("romanizes Korean titles", () => {
|
|
52
|
+
expect(slugify("안녕하세요 세계")).toBe("annyeonghaseyo-segye");
|
|
53
|
+
expect(slugify("개발 일지 3일차")).toBe("gaebal-ilji-3ilcha");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("romanizes kana-only Japanese titles", () => {
|
|
57
|
+
expect(slugify("カタカナタイトル")).toBe("katakanataitoru");
|
|
58
|
+
expect(slugify("ハンバーガー")).toBe("hanbaga");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("keeps Latin words in mostly-kana Japanese titles", () => {
|
|
62
|
+
expect(slugify("React入門ガイド")).toBe("react-gaido");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns empty for kanji-heavy Japanese titles instead of garbling them", () => {
|
|
66
|
+
// Kanji carries the meaning; without a reading dictionary any output
|
|
67
|
+
// would be gibberish, so callers should fall back to a random ID.
|
|
68
|
+
expect(slugify("日本語のタイトルです")).toBe("");
|
|
69
|
+
expect(slugify("東京タワーに行った")).toBe("");
|
|
70
|
+
expect(slugify("こんにちは世界")).toBe("");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns empty for scripts with no usable transliteration", () => {
|
|
74
|
+
expect(slugify("שלום עולם")).toBe("");
|
|
75
|
+
expect(slugify("สวัสดีชาวโลก")).toBe("");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns empty when most of the title is untransliterable", () => {
|
|
79
|
+
expect(slugify("שלום world")).toBe("");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns empty for emoji-only titles", () => {
|
|
83
|
+
expect(slugify("🎉🎉")).toBe("");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -210,6 +210,38 @@ describe("createS3Driver", () => {
|
|
|
210
210
|
"x-amz-checksum-sha256": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
|
+
|
|
214
|
+
it("omits a request checksum when the caller did not supply one", async () => {
|
|
215
|
+
const driver = createS3Driver({
|
|
216
|
+
endpoint: "https://s3.example.com",
|
|
217
|
+
bucket: "jant-media",
|
|
218
|
+
accessKeyId: "access-key",
|
|
219
|
+
secretAccessKey: "secret-key",
|
|
220
|
+
region: "auto",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const presigned = await driver.presignPut?.("media/test.webp", {
|
|
224
|
+
contentType: "image/webp",
|
|
225
|
+
contentDisposition: "inline",
|
|
226
|
+
cacheControl: "public, max-age=31536000, immutable",
|
|
227
|
+
expiresInSeconds: 900,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!presigned) {
|
|
231
|
+
throw new Error("Expected presigned PUT target");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Presigning runs with an empty body, so a default request checksum would
|
|
235
|
+
// pin CRC32-of-nothing to the URL and every real upload would 400.
|
|
236
|
+
const params = new URL(presigned.url).searchParams;
|
|
237
|
+
expect(params.has("x-amz-checksum-crc32")).toBe(false);
|
|
238
|
+
expect(params.has("x-amz-sdk-checksum-algorithm")).toBe(false);
|
|
239
|
+
expect(presigned.headers).toEqual({
|
|
240
|
+
"Content-Type": "image/webp",
|
|
241
|
+
"Content-Disposition": "inline",
|
|
242
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
243
|
+
});
|
|
244
|
+
});
|
|
213
245
|
});
|
|
214
246
|
|
|
215
247
|
describe("createR2Driver", () => {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
foldThreadReplies,
|
|
4
|
+
getThreadHiddenCount,
|
|
5
|
+
THREAD_LEADING_REPLIES,
|
|
6
|
+
THREAD_TRAILING_REPLIES,
|
|
7
|
+
} from "../thread-fold.js";
|
|
8
|
+
|
|
9
|
+
const reply = (n: number) => ({ id: `reply-${n}` });
|
|
10
|
+
const replies = (count: number) =>
|
|
11
|
+
Array.from({ length: count }, (_unused, index) => reply(index + 1));
|
|
12
|
+
|
|
13
|
+
const ids = (list: { id: string }[]) => list.map((item) => item.id);
|
|
14
|
+
|
|
15
|
+
describe("foldThreadReplies", () => {
|
|
16
|
+
it("has no fold for a thread with no replies", () => {
|
|
17
|
+
expect(foldThreadReplies([])).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// The window is the first two plus the last three, so anything up to six
|
|
21
|
+
// posts arrives whole and there is nothing for a gap link to stand for.
|
|
22
|
+
it.each([1, 2, 3, 4, 5])("hides nothing with %i replies", (count) => {
|
|
23
|
+
const fold = foldThreadReplies(replies(count));
|
|
24
|
+
|
|
25
|
+
expect(fold?.hiddenCount).toBe(0);
|
|
26
|
+
expect(fold?.firstHiddenReply).toBeNull();
|
|
27
|
+
// Every reply is shown exactly once, the hero included.
|
|
28
|
+
const shown = [
|
|
29
|
+
...ids(fold?.leadingReplies ?? []),
|
|
30
|
+
...ids(fold?.trailingReplies ?? []),
|
|
31
|
+
fold?.latestReply.id,
|
|
32
|
+
];
|
|
33
|
+
expect(new Set(shown).size).toBe(shown.length);
|
|
34
|
+
expect(shown.length).toBe(count);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// The shortest thread puts one reply in every role at once. It must still
|
|
38
|
+
// be rendered once, not three times.
|
|
39
|
+
it("shows a lone reply once, as the hero", () => {
|
|
40
|
+
const fold = foldThreadReplies(replies(1));
|
|
41
|
+
|
|
42
|
+
expect(fold?.latestReply.id).toBe("reply-1");
|
|
43
|
+
expect(fold?.leadingReplies).toEqual([]);
|
|
44
|
+
expect(fold?.trailingReplies).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("folds the middle out of a long thread", () => {
|
|
48
|
+
const fold = foldThreadReplies(replies(7));
|
|
49
|
+
|
|
50
|
+
expect(ids(fold?.leadingReplies ?? [])).toEqual(["reply-1", "reply-2"]);
|
|
51
|
+
expect(ids(fold?.trailingReplies ?? [])).toEqual(["reply-5", "reply-6"]);
|
|
52
|
+
expect(fold?.latestReply.id).toBe("reply-7");
|
|
53
|
+
expect(fold?.hiddenCount).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// The gap stands for a run of posts and opens the first of them, so a reader
|
|
57
|
+
// following it lands where the missing stretch starts.
|
|
58
|
+
it("points the gap at the earliest post it hides", () => {
|
|
59
|
+
expect(foldThreadReplies(replies(20))?.firstHiddenReply?.id).toBe(
|
|
60
|
+
"reply-3",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps the window fixed however long the thread runs", () => {
|
|
65
|
+
const fold = foldThreadReplies(replies(50));
|
|
66
|
+
const shown =
|
|
67
|
+
(fold?.leadingReplies.length ?? 0) +
|
|
68
|
+
(fold?.trailingReplies.length ?? 0) +
|
|
69
|
+
1;
|
|
70
|
+
|
|
71
|
+
expect(shown).toBe(THREAD_LEADING_REPLIES + THREAD_TRAILING_REPLIES);
|
|
72
|
+
expect(fold?.hiddenCount).toBe(50 - shown);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("getThreadHiddenCount", () => {
|
|
77
|
+
// The site selects its buckets in SQL and they overlap on short threads, so
|
|
78
|
+
// the count has to dedupe rather than subtract two lengths.
|
|
79
|
+
it("counts a reply in two buckets once", () => {
|
|
80
|
+
expect(
|
|
81
|
+
getThreadHiddenCount({
|
|
82
|
+
leadingReplies: [reply(1), reply(2)],
|
|
83
|
+
trailingReplies: [reply(1)],
|
|
84
|
+
latestReply: reply(2),
|
|
85
|
+
totalReplyCount: 2,
|
|
86
|
+
}),
|
|
87
|
+
).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("counts what a long thread leaves out", () => {
|
|
91
|
+
expect(
|
|
92
|
+
getThreadHiddenCount({
|
|
93
|
+
leadingReplies: [reply(1), reply(2)],
|
|
94
|
+
trailingReplies: [reply(6), reply(7)],
|
|
95
|
+
latestReply: reply(8),
|
|
96
|
+
totalReplyCount: 8,
|
|
97
|
+
}),
|
|
98
|
+
).toBe(3);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// A reply published between the count and the selection would otherwise
|
|
102
|
+
// report a negative number of hidden posts.
|
|
103
|
+
it("never goes below zero", () => {
|
|
104
|
+
expect(
|
|
105
|
+
getThreadHiddenCount({
|
|
106
|
+
leadingReplies: [reply(1), reply(2)],
|
|
107
|
+
trailingReplies: [],
|
|
108
|
+
latestReply: reply(3),
|
|
109
|
+
totalReplyCount: 1,
|
|
110
|
+
}),
|
|
111
|
+
).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -64,6 +64,20 @@ describe("sniffImageMimeType", () => {
|
|
|
64
64
|
expect(sniffImageMimeType(bytes)).toBe("image/avif");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it.each(["heic", "heix", "hevc", "hevx", "mif1", "msf1"])(
|
|
68
|
+
"detects HEIC/HEIF via the %s ftyp brand",
|
|
69
|
+
(brand) => {
|
|
70
|
+
const bytes = new Uint8Array(16);
|
|
71
|
+
bytes.set([...new TextEncoder().encode("ftyp")], 4);
|
|
72
|
+
bytes.set([...new TextEncoder().encode(brand)], 8);
|
|
73
|
+
expect(sniffImageMimeType(bytes)).toBe("image/heic");
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
it("keeps HEIC out of the sideload formats", () => {
|
|
78
|
+
expect(isAllowedSideloadImageType("image/heic")).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
67
81
|
it("detects SVG from an <svg> root", () => {
|
|
68
82
|
const svg = new TextEncoder().encode(
|
|
69
83
|
'<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg"></svg>',
|
|
@@ -83,8 +83,7 @@ describe("fetchImageBytes", () => {
|
|
|
83
83
|
);
|
|
84
84
|
|
|
85
85
|
const headers = fetchMock.mock.calls[0]?.[1]?.headers as
|
|
86
|
-
|
|
87
|
-
| undefined;
|
|
86
|
+
Record<string, string> | undefined;
|
|
88
87
|
expect(headers?.Referer).toBe("https://img9.doubanio.com/");
|
|
89
88
|
});
|
|
90
89
|
|
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
extractDisplayDomain,
|
|
5
5
|
isFullUrl,
|
|
6
6
|
isSafeAbsoluteUrl,
|
|
7
|
+
looksLikeAddress,
|
|
7
8
|
normalizePath,
|
|
8
9
|
sanitizeRichTextHref,
|
|
9
10
|
sanitizeUrl,
|
|
10
11
|
stripSitePathPrefix,
|
|
11
|
-
|
|
12
|
+
toInternalPath,
|
|
12
13
|
toSameSitePath,
|
|
13
14
|
} from "../url.js";
|
|
14
15
|
|
|
@@ -229,97 +230,102 @@ describe("URL sanitizers", () => {
|
|
|
229
230
|
});
|
|
230
231
|
});
|
|
231
232
|
|
|
232
|
-
describe("
|
|
233
|
-
it("
|
|
234
|
-
expect(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
it("removes special characters", () => {
|
|
238
|
-
expect(slugify("Hello World! This is a Test.")).toBe(
|
|
239
|
-
"hello-world-this-is-a-test",
|
|
233
|
+
describe("stripSitePathPrefix", () => {
|
|
234
|
+
it("rewrites prefixed asset paths into the internal asset namespace", () => {
|
|
235
|
+
expect(stripSitePathPrefix("/blog/_assets/client.css", "/blog")).toBe(
|
|
236
|
+
"/_assets/client.css",
|
|
240
237
|
);
|
|
241
238
|
});
|
|
242
239
|
|
|
243
|
-
it("
|
|
244
|
-
expect(
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("trims leading/trailing whitespace and hyphens", () => {
|
|
248
|
-
expect(slugify(" Multiple Spaces ")).toBe("multiple-spaces");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("replaces underscores with hyphens", () => {
|
|
252
|
-
expect(slugify("hello_world")).toBe("hello-world");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("handles already-slugified text", () => {
|
|
256
|
-
expect(slugify("already-a-slug")).toBe("already-a-slug");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("handles empty string", () => {
|
|
260
|
-
expect(slugify("")).toBe("");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("transliterates accented characters", () => {
|
|
264
|
-
expect(slugify("café & résumé")).toBe("cafe-and-resume");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("converts Chinese characters to pinyin", () => {
|
|
268
|
-
expect(slugify("书评")).toBe("shu-ping");
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("handles mixed Chinese and English text", () => {
|
|
272
|
-
expect(slugify("我的 Blog")).toBe("wo-de-blog");
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("handles CJK characters with spaces", () => {
|
|
276
|
-
expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
|
|
240
|
+
it("rejects paths outside the configured site prefix", () => {
|
|
241
|
+
expect(stripSitePathPrefix("/_assets/client.css", "/blog")).toBe(null);
|
|
277
242
|
});
|
|
243
|
+
});
|
|
278
244
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
expect(
|
|
245
|
+
describe("looksLikeAddress", () => {
|
|
246
|
+
it("recognizes what a browser would have produced", () => {
|
|
247
|
+
expect(looksLikeAddress("/about")).toBe(true);
|
|
248
|
+
expect(looksLikeAddress(" https://example.com/about ")).toBe(true);
|
|
282
249
|
});
|
|
283
250
|
|
|
284
|
-
it("
|
|
285
|
-
|
|
286
|
-
|
|
251
|
+
it("leaves search words alone", () => {
|
|
252
|
+
// The line that keeps a picker's search box a search box: "about" is a
|
|
253
|
+
// word someone is looking for, not the page /about.
|
|
254
|
+
expect(looksLikeAddress("about")).toBe(false);
|
|
255
|
+
expect(looksLikeAddress("coffee notes")).toBe(false);
|
|
256
|
+
expect(looksLikeAddress("example.com")).toBe(false);
|
|
287
257
|
});
|
|
258
|
+
});
|
|
288
259
|
|
|
289
|
-
|
|
290
|
-
|
|
260
|
+
describe("toInternalPath", () => {
|
|
261
|
+
const SITE = {
|
|
262
|
+
siteOrigins: ["https://example.com"],
|
|
263
|
+
sitePathPrefix: "/blog",
|
|
264
|
+
languagePrefixes: ["zh-hans", "en"],
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
it("reads a path, a full URL, and a prefixed URL as the same page", () => {
|
|
268
|
+
expect(toInternalPath("/about", SITE)).toBe("/about");
|
|
269
|
+
expect(toInternalPath("about", SITE)).toBe("/about");
|
|
270
|
+
expect(toInternalPath("https://example.com/blog/about", SITE)).toBe(
|
|
271
|
+
"/about",
|
|
272
|
+
);
|
|
273
|
+
expect(toInternalPath("/blog/about", SITE)).toBe("/about");
|
|
274
|
+
// A typed path without the deployment prefix still means this site.
|
|
275
|
+
expect(toInternalPath("/about", SITE)).toBe("/about");
|
|
291
276
|
});
|
|
292
277
|
|
|
293
|
-
it("
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
278
|
+
it("strips a language prefix, because a view is not another page", () => {
|
|
279
|
+
expect(toInternalPath("/en/about", SITE)).toBe("/about");
|
|
280
|
+
expect(toInternalPath("https://example.com/blog/zh-hans/about", SITE)).toBe(
|
|
281
|
+
"/about",
|
|
282
|
+
);
|
|
283
|
+
// The prefix alone is that view's home.
|
|
284
|
+
expect(toInternalPath("/en", SITE)).toBe("/");
|
|
299
285
|
});
|
|
300
286
|
|
|
301
|
-
it("
|
|
302
|
-
|
|
303
|
-
|
|
287
|
+
it("keeps a first segment that only looks like a language", () => {
|
|
288
|
+
// A single-language site is free to have a post at /en — nothing is
|
|
289
|
+
// serving a view there.
|
|
290
|
+
expect(toInternalPath("/en/about", { sitePathPrefix: "" })).toBe(
|
|
291
|
+
"/en/about",
|
|
292
|
+
);
|
|
304
293
|
});
|
|
305
294
|
|
|
306
|
-
it("
|
|
307
|
-
|
|
295
|
+
it("matches any host the site answers on", () => {
|
|
296
|
+
const origins = {
|
|
297
|
+
siteOrigins: ["https://example.com", "https://alias.dev"],
|
|
298
|
+
};
|
|
299
|
+
expect(toInternalPath("https://alias.dev/about", origins)).toBe("/about");
|
|
300
|
+
// Scheme and port differ in dev; the host is what decides.
|
|
301
|
+
expect(toInternalPath("http://example.com:8787/about", origins)).toBe(
|
|
302
|
+
"/about",
|
|
303
|
+
);
|
|
308
304
|
});
|
|
309
305
|
|
|
310
|
-
it("
|
|
311
|
-
expect(
|
|
306
|
+
it("refuses everything that is not a page on this site", () => {
|
|
307
|
+
expect(toInternalPath("https://other.com/about", SITE)).toBeNull();
|
|
308
|
+
expect(toInternalPath("mailto:hi@example.com", SITE)).toBeNull();
|
|
309
|
+
expect(toInternalPath("tel:+15551234567", SITE)).toBeNull();
|
|
310
|
+
expect(toInternalPath("//other.com/about", SITE)).toBeNull();
|
|
311
|
+
expect(toInternalPath("#section", SITE)).toBeNull();
|
|
312
|
+
expect(toInternalPath(" ", SITE)).toBeNull();
|
|
313
|
+
// A full URL inside our host but outside the deployment.
|
|
314
|
+
expect(
|
|
315
|
+
toInternalPath("https://example.com/other-app/about", SITE),
|
|
316
|
+
).toBeNull();
|
|
312
317
|
});
|
|
313
|
-
});
|
|
314
318
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
expect(
|
|
318
|
-
"/
|
|
319
|
-
);
|
|
319
|
+
it("drops the query unless the caller is navigating", () => {
|
|
320
|
+
expect(toInternalPath("/about?ref=x#top", SITE)).toBe("/about");
|
|
321
|
+
expect(
|
|
322
|
+
toInternalPath("/about?ref=x#top", { ...SITE, keepQuery: true }),
|
|
323
|
+
).toBe("/about?ref=x#top");
|
|
320
324
|
});
|
|
321
325
|
|
|
322
|
-
it("
|
|
323
|
-
expect(
|
|
326
|
+
it("normalizes trailing slashes and relative steps", () => {
|
|
327
|
+
expect(toInternalPath("/about/", SITE)).toBe("/about");
|
|
328
|
+
expect(toInternalPath("/collections/../about", SITE)).toBe("/about");
|
|
329
|
+
expect(toInternalPath("/", SITE)).toBe("/");
|
|
324
330
|
});
|
|
325
331
|
});
|