@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
|
@@ -95,6 +95,24 @@ describe("generatePostSlug", () => {
|
|
|
95
95
|
}),
|
|
96
96
|
).rejects.toThrow("Could not generate a unique slug");
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it("romanizes Korean titles", async () => {
|
|
100
|
+
const slug = await generatePostSlug({
|
|
101
|
+
title: "개발 일지",
|
|
102
|
+
idLength: 5,
|
|
103
|
+
isAvailable: alwaysAvailable,
|
|
104
|
+
});
|
|
105
|
+
expect(slug).toBe("gaebal-ilji");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("falls back to random IDs for kanji-heavy Japanese titles", async () => {
|
|
109
|
+
const slug = await generatePostSlug({
|
|
110
|
+
title: "日本語のタイトルです",
|
|
111
|
+
idLength: 5,
|
|
112
|
+
isAvailable: alwaysAvailable,
|
|
113
|
+
});
|
|
114
|
+
expect(slug).toMatch(/^[a-z0-9]{5}-[a-z0-9]{5}$/);
|
|
115
|
+
});
|
|
98
116
|
});
|
|
99
117
|
|
|
100
118
|
describe("random slug (no title, no slug)", () => {
|
|
@@ -123,4 +141,74 @@ describe("generatePostSlug", () => {
|
|
|
123
141
|
).rejects.toThrow("Could not generate a unique slug");
|
|
124
142
|
});
|
|
125
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
|
+
});
|
|
126
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
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { hangulToLatin, kanaToRomaji } from "../translit.js";
|
|
3
|
+
|
|
4
|
+
describe("hangulToLatin", () => {
|
|
5
|
+
it("romanizes basic syllables", () => {
|
|
6
|
+
expect(hangulToLatin("안녕하세요")).toBe("annyeonghaseyo");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("applies liaison before silent-ㅇ syllables", () => {
|
|
10
|
+
expect(hangulToLatin("한국어")).toBe("hangugeo");
|
|
11
|
+
expect(hangulToLatin("일지")).toBe("ilji");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("drops carried ㅎ in liaison", () => {
|
|
15
|
+
expect(hangulToLatin("좋아")).toBe("joa");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("splits compound finals in liaison", () => {
|
|
19
|
+
expect(hangulToLatin("읽어")).toBe("ilgeo");
|
|
20
|
+
expect(hangulToLatin("값이")).toBe("gapsi");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("keeps final ㅇ as ng before vowels", () => {
|
|
24
|
+
expect(hangulToLatin("강아지")).toBe("gangaji");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("uses phonetic finals", () => {
|
|
28
|
+
expect(hangulToLatin("옷")).toBe("ot");
|
|
29
|
+
expect(hangulToLatin("부엌")).toBe("bueok");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("preserves spaces and non-hangul characters", () => {
|
|
33
|
+
expect(hangulToLatin("개발 일지 3일차")).toBe("gaebal ilji 3ilcha");
|
|
34
|
+
expect(hangulToLatin("React 공부")).toBe("React gongbu");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("romanizes standalone compatibility jamo", () => {
|
|
38
|
+
expect(hangulToLatin("ㅋㅋㅋ")).toBe("kkk");
|
|
39
|
+
expect(hangulToLatin("ㅠㅠ")).toBe("yuyu");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("kanaToRomaji", () => {
|
|
44
|
+
it("romanizes hiragana", () => {
|
|
45
|
+
expect(kanaToRomaji("こんにちは")).toBe("konnichiha");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("romanizes katakana", () => {
|
|
49
|
+
expect(kanaToRomaji("カタカナタイトル")).toBe("katakanataitoru");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("drops the chōonpu long-vowel mark", () => {
|
|
53
|
+
expect(kanaToRomaji("ハンバーガー")).toBe("hanbaga");
|
|
54
|
+
expect(kanaToRomaji("ラーメン")).toBe("ramen");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("geminates consonants after sokuon", () => {
|
|
58
|
+
expect(kanaToRomaji("がっこう")).toBe("gakkou");
|
|
59
|
+
expect(kanaToRomaji("きっぷ")).toBe("kippu");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses tch for sokuon before chi", () => {
|
|
63
|
+
expect(kanaToRomaji("まっちゃ")).toBe("matcha");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles digraphs", () => {
|
|
67
|
+
expect(kanaToRomaji("きょうと")).toBe("kyouto");
|
|
68
|
+
expect(kanaToRomaji("しゃしん")).toBe("shashin");
|
|
69
|
+
expect(kanaToRomaji("じゅんび")).toBe("junbi");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles extended katakana combos", () => {
|
|
73
|
+
expect(kanaToRomaji("ファン")).toBe("fan");
|
|
74
|
+
expect(kanaToRomaji("パーティー")).toBe("pati");
|
|
75
|
+
expect(kanaToRomaji("チェック")).toBe("chekku");
|
|
76
|
+
expect(kanaToRomaji("ヴァイオリン")).toBe("vaiorin");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("passes non-kana characters through unchanged", () => {
|
|
80
|
+
expect(kanaToRomaji("東京タワー")).toBe("東京tawa");
|
|
81
|
+
expect(kanaToRomaji("abc カフェ")).toBe("abc kafe");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("ignores a dangling sokuon", () => {
|
|
85
|
+
expect(kanaToRomaji("あっ")).toBe("a");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -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,9 +4,12 @@ import {
|
|
|
4
4
|
extractDisplayDomain,
|
|
5
5
|
isFullUrl,
|
|
6
6
|
isSafeAbsoluteUrl,
|
|
7
|
+
looksLikeAddress,
|
|
7
8
|
normalizePath,
|
|
9
|
+
sanitizeRichTextHref,
|
|
10
|
+
sanitizeUrl,
|
|
8
11
|
stripSitePathPrefix,
|
|
9
|
-
|
|
12
|
+
toInternalPath,
|
|
10
13
|
toSameSitePath,
|
|
11
14
|
} from "../url.js";
|
|
12
15
|
|
|
@@ -211,62 +214,118 @@ describe("isSafeAbsoluteUrl", () => {
|
|
|
211
214
|
});
|
|
212
215
|
});
|
|
213
216
|
|
|
214
|
-
describe("
|
|
215
|
-
it("
|
|
216
|
-
expect(
|
|
217
|
+
describe("URL sanitizers", () => {
|
|
218
|
+
it("keeps action protocols scoped to rich-text links", () => {
|
|
219
|
+
expect(sanitizeRichTextHref("tel:+15551234567")).toBe("tel:+15551234567");
|
|
220
|
+
expect(sanitizeRichTextHref("sms:xxxxx@xx.com")).toBe("sms:xxxxx@xx.com");
|
|
221
|
+
|
|
222
|
+
expect(sanitizeUrl("tel:+15551234567")).toBe("");
|
|
223
|
+
expect(sanitizeUrl("sms:xxxxx@xx.com")).toBe("");
|
|
217
224
|
});
|
|
218
225
|
|
|
219
|
-
it("
|
|
220
|
-
expect(
|
|
221
|
-
|
|
222
|
-
);
|
|
226
|
+
it("rejects executable protocols in every context", () => {
|
|
227
|
+
expect(sanitizeRichTextHref("javascript:alert(1)")).toBe("");
|
|
228
|
+
expect(sanitizeRichTextHref("data:text/html,<h1>Hi</h1>")).toBe("");
|
|
229
|
+
expect(sanitizeUrl("javascript:alert(1)")).toBe("");
|
|
223
230
|
});
|
|
231
|
+
});
|
|
224
232
|
|
|
225
|
-
|
|
226
|
-
|
|
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",
|
|
237
|
+
);
|
|
227
238
|
});
|
|
228
239
|
|
|
229
|
-
it("
|
|
230
|
-
expect(
|
|
240
|
+
it("rejects paths outside the configured site prefix", () => {
|
|
241
|
+
expect(stripSitePathPrefix("/_assets/client.css", "/blog")).toBe(null);
|
|
231
242
|
});
|
|
243
|
+
});
|
|
232
244
|
|
|
233
|
-
|
|
234
|
-
|
|
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);
|
|
235
249
|
});
|
|
236
250
|
|
|
237
|
-
it("
|
|
238
|
-
|
|
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);
|
|
239
257
|
});
|
|
258
|
+
});
|
|
240
259
|
|
|
241
|
-
|
|
242
|
-
|
|
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");
|
|
243
276
|
});
|
|
244
277
|
|
|
245
|
-
it("
|
|
246
|
-
expect(
|
|
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("/");
|
|
247
285
|
});
|
|
248
286
|
|
|
249
|
-
it("
|
|
250
|
-
|
|
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
|
+
);
|
|
251
293
|
});
|
|
252
294
|
|
|
253
|
-
it("
|
|
254
|
-
|
|
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
|
+
);
|
|
255
304
|
});
|
|
256
305
|
|
|
257
|
-
it("
|
|
258
|
-
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();
|
|
259
317
|
});
|
|
260
|
-
});
|
|
261
318
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expect(
|
|
265
|
-
"/
|
|
266
|
-
);
|
|
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");
|
|
267
324
|
});
|
|
268
325
|
|
|
269
|
-
it("
|
|
270
|
-
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("/");
|
|
271
330
|
});
|
|
272
331
|
});
|