@jant/core 0.6.15 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +88 -1
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +93 -34
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +11 -2
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +401 -301
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +55 -23
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
package/dist/url-BMYO-Zlt.js
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
import limax from "limax";
|
|
2
|
-
//#region \0rolldown/runtime.js
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __exportAll = (all, no_symbols) => {
|
|
5
|
-
let target = {};
|
|
6
|
-
for (var name in all) __defProp(target, name, {
|
|
7
|
-
get: all[name],
|
|
8
|
-
enumerable: true
|
|
9
|
-
});
|
|
10
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
11
|
-
return target;
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region src/lib/url.ts
|
|
15
|
-
/**
|
|
16
|
-
* URL Utilities
|
|
17
|
-
*/ var url_exports = /* @__PURE__ */ __exportAll({
|
|
18
|
-
buildSiteUrl: () => buildSiteUrl,
|
|
19
|
-
extractDisplayDomain: () => extractDisplayDomain,
|
|
20
|
-
extractDomain: () => extractDomain,
|
|
21
|
-
getSiteOrigin: () => getSiteOrigin,
|
|
22
|
-
getSitePathPrefix: () => getSitePathPrefix,
|
|
23
|
-
isFullUrl: () => isFullUrl,
|
|
24
|
-
isSafeAbsoluteUrl: () => isSafeAbsoluteUrl,
|
|
25
|
-
isSafeInternalRedirect: () => isSafeInternalRedirect,
|
|
26
|
-
normalizePath: () => normalizePath,
|
|
27
|
-
normalizeSitePathPrefix: () => normalizeSitePathPrefix,
|
|
28
|
-
normalizeSiteUrl: () => normalizeSiteUrl,
|
|
29
|
-
sanitizeUrl: () => sanitizeUrl,
|
|
30
|
-
slugify: () => slugify,
|
|
31
|
-
stripSitePathPrefix: () => stripSitePathPrefix,
|
|
32
|
-
toAbsoluteAssetUrl: () => toAbsoluteAssetUrl,
|
|
33
|
-
toAbsoluteSiteUrl: () => toAbsoluteSiteUrl,
|
|
34
|
-
toPublicHref: () => toPublicHref,
|
|
35
|
-
toPublicPath: () => toPublicPath,
|
|
36
|
-
toSameSitePath: () => toSameSitePath
|
|
37
|
-
});
|
|
38
|
-
function normalizeSitePathname(pathname) {
|
|
39
|
-
if (pathname === "/" || pathname === "") return "";
|
|
40
|
-
const trimmed = pathname.replace(/\/+$/, "");
|
|
41
|
-
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Normalize a configured public site path prefix.
|
|
45
|
-
*
|
|
46
|
-
* @param sitePathPrefix - Prefix such as `/blog` or `blog`
|
|
47
|
-
* @returns Normalized prefix like `/blog`, or an empty string when rooted
|
|
48
|
-
*/ function normalizeSitePathPrefix(sitePathPrefix) {
|
|
49
|
-
return normalizeSitePathname(sitePathPrefix);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Extracts the hostname (domain) from a URL string.
|
|
53
|
-
*
|
|
54
|
-
* Parses a full URL and returns just the hostname portion (e.g., "example.com" from
|
|
55
|
-
* "https://example.com/path"). Returns `null` if the URL is malformed or cannot be parsed.
|
|
56
|
-
*
|
|
57
|
-
* @param url - The full URL string to extract the domain from
|
|
58
|
-
* @returns The hostname/domain if valid, or `null` if parsing fails
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```ts
|
|
62
|
-
* const domain = extractDomain("https://www.example.com/path");
|
|
63
|
-
* // Returns: "www.example.com"
|
|
64
|
-
*
|
|
65
|
-
* const invalid = extractDomain("not-a-url");
|
|
66
|
-
* // Returns: null
|
|
67
|
-
* ```
|
|
68
|
-
*/ function extractDomain(url) {
|
|
69
|
-
try {
|
|
70
|
-
return new URL(url).hostname;
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Normalizes a path by removing slashes and converting to lowercase.
|
|
77
|
-
*
|
|
78
|
-
* Trims whitespace, converts to lowercase, removes leading and trailing slashes,
|
|
79
|
-
* and collapses multiple consecutive slashes into single slashes. Used to create
|
|
80
|
-
* consistent path representations for routing and storage.
|
|
81
|
-
*
|
|
82
|
-
* @param path - The path string to normalize
|
|
83
|
-
* @returns The normalized path string
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* ```ts
|
|
87
|
-
* const normalized = normalizePath(" /About/Contact// ");
|
|
88
|
-
* // Returns: "about/contact"
|
|
89
|
-
* ```
|
|
90
|
-
*/ function normalizePath(path) {
|
|
91
|
-
return path.trim().toLowerCase().replace(/^\/+|\/+$/g, "").replace(/\/+/g, "/");
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Checks if a string is a full URL with HTTP or HTTPS protocol.
|
|
95
|
-
*
|
|
96
|
-
* Validates whether a string starts with "http://" or "https://", indicating it's
|
|
97
|
-
* a full URL rather than a relative path. Useful for distinguishing between internal
|
|
98
|
-
* paths and external URLs.
|
|
99
|
-
*
|
|
100
|
-
* @param str - The string to check
|
|
101
|
-
* @returns `true` if the string starts with http:// or https://, `false` otherwise
|
|
102
|
-
*
|
|
103
|
-
* @example
|
|
104
|
-
* ```ts
|
|
105
|
-
* isFullUrl("https://example.com"); // Returns: true
|
|
106
|
-
* isFullUrl("/about"); // Returns: false
|
|
107
|
-
* isFullUrl("example.com"); // Returns: false
|
|
108
|
-
* ```
|
|
109
|
-
*/ function isFullUrl(str) {
|
|
110
|
-
return str.startsWith("http://") || str.startsWith("https://");
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* If a full URL points at the site's own host, return its same-site path
|
|
114
|
-
* (`pathname` + `search` + `hash`). Returns `null` when the input is not a full
|
|
115
|
-
* URL, is unparseable, or points at a different host.
|
|
116
|
-
*
|
|
117
|
-
* Self-referential absolute links — e.g. a nav item set to
|
|
118
|
-
* `https://example.com/about` on `example.com` — should behave like the
|
|
119
|
-
* internal path `/about`: no external-link icon, no `target="_blank"`.
|
|
120
|
-
*
|
|
121
|
-
* Matching is by **hostname**, not full origin: scheme and port differences are
|
|
122
|
-
* treated as same-site. This keeps the check intuitive ("same domain") and
|
|
123
|
-
* robust in dev, where the site is often served over `http://host:<port>` while
|
|
124
|
-
* a nav link stores the canonical `https://host` URL. In production (canonical
|
|
125
|
-
* https + default port) hostname match is equivalent to origin match.
|
|
126
|
-
*
|
|
127
|
-
* @param url - Candidate URL (full URL or relative path)
|
|
128
|
-
* @param siteOrigin - The site's own origin, e.g. `https://example.com`
|
|
129
|
-
* @returns The same-site path, or `null` when the URL is external/non-absolute
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* ```ts
|
|
133
|
-
* toSameSitePath("https://example.com/about", "https://example.com"); // "/about"
|
|
134
|
-
* toSameSitePath("https://example.com/about", "http://example.com:8787"); // "/about"
|
|
135
|
-
* toSameSitePath("https://other.com/about", "https://example.com"); // null
|
|
136
|
-
* toSameSitePath("/about", "https://example.com"); // null
|
|
137
|
-
* ```
|
|
138
|
-
*/ function toSameSitePath(url, siteOrigin) {
|
|
139
|
-
if (!siteOrigin || !isFullUrl(url)) return null;
|
|
140
|
-
let parsed;
|
|
141
|
-
let reference;
|
|
142
|
-
try {
|
|
143
|
-
parsed = new URL(url);
|
|
144
|
-
reference = new URL(siteOrigin);
|
|
145
|
-
} catch {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
if (parsed.hostname !== reference.hostname) return null;
|
|
149
|
-
return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Converts text to a URL-friendly slug.
|
|
153
|
-
*
|
|
154
|
-
* Transforms text into a lowercase, hyphen-separated slug using limax for
|
|
155
|
-
* i18n-aware transliteration (CJK → Pinyin, Japanese → Romaji, accented → ASCII).
|
|
156
|
-
*
|
|
157
|
-
* @param text - The text to convert to a slug
|
|
158
|
-
* @returns The slugified string
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* ```ts
|
|
162
|
-
* slugify("Hello World! This is a Test.");
|
|
163
|
-
* // Returns: "hello-world-this-is-a-test"
|
|
164
|
-
*
|
|
165
|
-
* slugify("书评");
|
|
166
|
-
* // Returns: "shu-ping"
|
|
167
|
-
* ```
|
|
168
|
-
*/ function slugify(text) {
|
|
169
|
-
return limax(text, { tone: false }).replace(/_/g, "-");
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Extracts a human-friendly domain name from a URL for display purposes.
|
|
173
|
-
*
|
|
174
|
-
* Parses the URL, strips common prefixes (`www.`, `m.`, `mobile.`), and returns
|
|
175
|
-
* a clean domain. Returns `null` if the URL is malformed.
|
|
176
|
-
*
|
|
177
|
-
* @param url - The full URL string
|
|
178
|
-
* @returns A display-friendly domain string, or `null` if parsing fails
|
|
179
|
-
*
|
|
180
|
-
* @example
|
|
181
|
-
* ```ts
|
|
182
|
-
* extractDisplayDomain("https://www.example.com/path");
|
|
183
|
-
* // Returns: "example.com"
|
|
184
|
-
*
|
|
185
|
-
* extractDisplayDomain("https://m.wikipedia.org/wiki/Test");
|
|
186
|
-
* // Returns: "wikipedia.org"
|
|
187
|
-
*
|
|
188
|
-
* extractDisplayDomain("https://blog.example.com");
|
|
189
|
-
* // Returns: "blog.example.com"
|
|
190
|
-
* ```
|
|
191
|
-
*/ function extractDisplayDomain(url) {
|
|
192
|
-
const hostname = extractDomain(url);
|
|
193
|
-
if (!hostname) return null;
|
|
194
|
-
return hostname.replace(/^(?:www|m|mobile)\./, "");
|
|
195
|
-
}
|
|
196
|
-
var SAFE_URL_PROTOCOLS = new Set([
|
|
197
|
-
"http:",
|
|
198
|
-
"https:",
|
|
199
|
-
"mailto:"
|
|
200
|
-
]);
|
|
201
|
-
/**
|
|
202
|
-
* Checks whether a string is a safe absolute URL.
|
|
203
|
-
*
|
|
204
|
-
* Accepts absolute URLs that use `http:`, `https:`, or `mailto:`. Relative
|
|
205
|
-
* paths are rejected because external post/source URLs must be fully qualified.
|
|
206
|
-
*
|
|
207
|
-
* @param url - The URL string to validate
|
|
208
|
-
* @returns `true` when the URL is absolute and uses an allowed protocol
|
|
209
|
-
*
|
|
210
|
-
* @example
|
|
211
|
-
* ```ts
|
|
212
|
-
* isSafeAbsoluteUrl("https://example.com"); // true
|
|
213
|
-
* isSafeAbsoluteUrl("mailto:hello@example.com"); // true
|
|
214
|
-
* isSafeAbsoluteUrl("example.com"); // false
|
|
215
|
-
* ```
|
|
216
|
-
*/ function isSafeAbsoluteUrl(url) {
|
|
217
|
-
const trimmed = url.trim();
|
|
218
|
-
if (!trimmed) return false;
|
|
219
|
-
try {
|
|
220
|
-
const parsed = new URL(trimmed);
|
|
221
|
-
return SAFE_URL_PROTOCOLS.has(parsed.protocol);
|
|
222
|
-
} catch {
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Sanitizes a URL by ensuring it uses a safe protocol.
|
|
228
|
-
*
|
|
229
|
-
* Returns the URL unchanged if it uses an allowed protocol (http:, https:, mailto:)
|
|
230
|
-
* or is a relative path. Returns an empty string for dangerous protocols like
|
|
231
|
-
* `javascript:`, `data:`, or `vbscript:`.
|
|
232
|
-
*
|
|
233
|
-
* @param url - The URL string to sanitize
|
|
234
|
-
* @returns The original URL if safe, or an empty string if the protocol is disallowed
|
|
235
|
-
*
|
|
236
|
-
* @example
|
|
237
|
-
* ```ts
|
|
238
|
-
* sanitizeUrl("https://example.com"); // "https://example.com"
|
|
239
|
-
* sanitizeUrl("/about"); // "/about"
|
|
240
|
-
* sanitizeUrl("javascript:alert(1)"); // ""
|
|
241
|
-
* sanitizeUrl("data:text/html,<h1>Hi</h1>"); // ""
|
|
242
|
-
* ```
|
|
243
|
-
*/ function sanitizeUrl(url) {
|
|
244
|
-
try {
|
|
245
|
-
const parsed = new URL(url, "https://placeholder.invalid");
|
|
246
|
-
if (SAFE_URL_PROTOCOLS.has(parsed.protocol)) return url;
|
|
247
|
-
return "";
|
|
248
|
-
} catch {
|
|
249
|
-
return "";
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Normalize a public site URL by stripping any trailing slash from the path.
|
|
254
|
-
*
|
|
255
|
-
* @param siteUrl - Full site URL, optionally with a path prefix
|
|
256
|
-
* @returns Normalized site URL, or an empty string when not configured
|
|
257
|
-
*/ function normalizeSiteUrl(siteUrl) {
|
|
258
|
-
const trimmed = siteUrl.trim();
|
|
259
|
-
if (!trimmed) return "";
|
|
260
|
-
const parsed = new URL(trimmed);
|
|
261
|
-
parsed.pathname = normalizeSitePathname(parsed.pathname) || "/";
|
|
262
|
-
if (parsed.pathname !== "/") parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
263
|
-
return parsed.toString().replace(/\/$/, parsed.pathname === "/" ? "/" : "");
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Extract the public path prefix from a full site URL.
|
|
267
|
-
*
|
|
268
|
-
* @param siteUrl - Full site URL
|
|
269
|
-
* @returns Prefix like `/blog`, or an empty string when the site is rooted
|
|
270
|
-
*/ function getSitePathPrefix(siteUrl) {
|
|
271
|
-
if (!siteUrl.trim()) return "";
|
|
272
|
-
return normalizeSitePathname(new URL(siteUrl).pathname);
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Extract the origin from a full site URL.
|
|
276
|
-
*
|
|
277
|
-
* @param siteUrl - Full site URL
|
|
278
|
-
* @returns Origin like `https://example.com`, or an empty string when missing
|
|
279
|
-
*/ function getSiteOrigin(siteUrl) {
|
|
280
|
-
if (!siteUrl.trim()) return "";
|
|
281
|
-
return new URL(siteUrl).origin;
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Build a normalized public site URL from an origin and optional path prefix.
|
|
285
|
-
*
|
|
286
|
-
* @param siteOrigin - Public site origin, such as `https://example.com`
|
|
287
|
-
* @param sitePathPrefix - Public site path prefix, such as `/blog`
|
|
288
|
-
* @returns Normalized site URL, or an empty string when no origin is configured
|
|
289
|
-
*/ function buildSiteUrl(siteOrigin, sitePathPrefix = "") {
|
|
290
|
-
const trimmedOrigin = siteOrigin.trim();
|
|
291
|
-
if (!trimmedOrigin) return "";
|
|
292
|
-
const origin = new URL(trimmedOrigin).origin;
|
|
293
|
-
return normalizeSiteUrl(`${origin}${normalizeSitePathPrefix(sitePathPrefix) || "/"}`);
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Prefix an internal app path with the public site path prefix.
|
|
297
|
-
*
|
|
298
|
-
* Internal paths are always rooted at `/` and never include the deployment
|
|
299
|
-
* prefix. This helper converts them to public-facing paths.
|
|
300
|
-
*
|
|
301
|
-
* @param path - Internal app path, such as `/settings`
|
|
302
|
-
* @param sitePathPrefix - Public site path prefix, such as `/blog`
|
|
303
|
-
* @returns Public path
|
|
304
|
-
*/ function toPublicPath(path, sitePathPrefix = "") {
|
|
305
|
-
if (!path) return sitePathPrefix || "/";
|
|
306
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
307
|
-
if (!sitePathPrefix) return normalizedPath;
|
|
308
|
-
if (normalizedPath === sitePathPrefix || normalizedPath.startsWith(`${sitePathPrefix}/`)) return normalizedPath;
|
|
309
|
-
if (normalizedPath === "/") return sitePathPrefix;
|
|
310
|
-
return `${sitePathPrefix}${normalizedPath}`;
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Convert an app-local href to its public path while leaving external URLs
|
|
314
|
-
* unchanged.
|
|
315
|
-
*
|
|
316
|
-
* @param href - Internal app path or external URL
|
|
317
|
-
* @param sitePathPrefix - Public site path prefix
|
|
318
|
-
* @returns Public-facing href
|
|
319
|
-
*/ function toPublicHref(href, sitePathPrefix = "") {
|
|
320
|
-
if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("#")) return href;
|
|
321
|
-
return toPublicPath(href, sitePathPrefix);
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Check whether a path is a safe same-origin redirect target.
|
|
325
|
-
*
|
|
326
|
-
* Accepts only paths that start with a single `/` (no protocol-relative
|
|
327
|
-
* `//host`, no scheme, no control characters). Callers should use this to
|
|
328
|
-
* validate user-supplied `redirect` query parameters before issuing a
|
|
329
|
-
* `Location` header.
|
|
330
|
-
*
|
|
331
|
-
* @param path - Candidate redirect path
|
|
332
|
-
* @returns `true` when the path is safe to use as an internal redirect
|
|
333
|
-
*
|
|
334
|
-
* @example
|
|
335
|
-
* ```ts
|
|
336
|
-
* isSafeInternalRedirect("/settings") // true
|
|
337
|
-
* isSafeInternalRedirect("//evil.example") // false
|
|
338
|
-
* isSafeInternalRedirect("https://evil.example") // false
|
|
339
|
-
* ```
|
|
340
|
-
*/ function isSafeInternalRedirect(path) {
|
|
341
|
-
if (typeof path !== "string") return false;
|
|
342
|
-
if (!path.startsWith("/")) return false;
|
|
343
|
-
if (path.startsWith("//")) return false;
|
|
344
|
-
if (path.startsWith("/\\")) return false;
|
|
345
|
-
for (let i = 0; i < path.length; i += 1) {
|
|
346
|
-
const code = path.charCodeAt(i);
|
|
347
|
-
if (code < 32 || code === 127) return false;
|
|
348
|
-
}
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Remove the site path prefix from a public request pathname.
|
|
353
|
-
*
|
|
354
|
-
* @param pathname - Public request pathname
|
|
355
|
-
* @param sitePathPrefix - Configured public site path prefix
|
|
356
|
-
* @returns Internal pathname, or `null` when the request is outside the site
|
|
357
|
-
*/ function stripSitePathPrefix(pathname, sitePathPrefix) {
|
|
358
|
-
if (!sitePathPrefix) return pathname || "/";
|
|
359
|
-
if (pathname === sitePathPrefix) return "/";
|
|
360
|
-
if (pathname.startsWith(`${sitePathPrefix}/`)) return pathname.slice(sitePathPrefix.length) || "/";
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Convert an internal or public path into an absolute site URL.
|
|
365
|
-
*
|
|
366
|
-
* @param path - Internal app path or already-public path
|
|
367
|
-
* @param siteUrl - Normalized site URL
|
|
368
|
-
* @param sitePathPrefix - Public site path prefix
|
|
369
|
-
* @returns Absolute URL when siteUrl is configured, otherwise the original path
|
|
370
|
-
*/ function toAbsoluteSiteUrl(path, siteUrl, sitePathPrefix = "") {
|
|
371
|
-
if (!siteUrl) return toPublicPath(path, sitePathPrefix);
|
|
372
|
-
return new URL(toPublicPath(path, sitePathPrefix), siteUrl).toString();
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Resolve a possibly-relative asset URL to an absolute URL, leaving
|
|
376
|
-
* already-absolute (`http(s):`) and protocol-relative (`//host`) URLs
|
|
377
|
-
* untouched. Use for assets — like media — whose stored URL may be either an
|
|
378
|
-
* app-local path or a full CDN URL.
|
|
379
|
-
*
|
|
380
|
-
* @param url - Asset URL: an internal path or an already-absolute URL
|
|
381
|
-
* @param siteUrl - Normalized site URL
|
|
382
|
-
* @param sitePathPrefix - Public site path prefix
|
|
383
|
-
* @returns Absolute URL, or the original value when it is already absolute
|
|
384
|
-
*
|
|
385
|
-
* @example
|
|
386
|
-
* ```ts
|
|
387
|
-
* toAbsoluteAssetUrl("/m/a.png", "https://site.com"); // "https://site.com/m/a.png"
|
|
388
|
-
* toAbsoluteAssetUrl("https://cdn.example/a.png", "x"); // "https://cdn.example/a.png"
|
|
389
|
-
* ```
|
|
390
|
-
*/ function toAbsoluteAssetUrl(url, siteUrl, sitePathPrefix = "") {
|
|
391
|
-
if (isFullUrl(url) || url.startsWith("//")) return url;
|
|
392
|
-
return toAbsoluteSiteUrl(url, siteUrl, sitePathPrefix);
|
|
393
|
-
}
|
|
394
|
-
//#endregion
|
|
395
|
-
export { toPublicPath as _, getSitePathPrefix as a, __exportAll as b, normalizePath as c, sanitizeUrl as d, slugify as f, toPublicHref as g, toAbsoluteSiteUrl as h, getSiteOrigin as i, normalizeSitePathPrefix as l, toAbsoluteAssetUrl as m, extractDisplayDomain as n, isFullUrl as o, stripSitePathPrefix as p, extractDomain as r, isSafeInternalRedirect as s, buildSiteUrl as t, normalizeSiteUrl as u, toSameSitePath as v, url_exports as y };
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
|
|
3
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
5
|
-
consumeCollectionCreatedNotice,
|
|
6
|
-
PENDING_COLLECTION_CREATED_STORAGE_KEY,
|
|
7
|
-
queueCollectionCreatedNotice,
|
|
8
|
-
} from "../collection-created-notice.js";
|
|
9
|
-
|
|
10
|
-
describe("collection created notice", () => {
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
globalThis.sessionStorage.clear();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("queues and consumes the newly created Collection ID once", () => {
|
|
16
|
-
queueCollectionCreatedNotice("col_books");
|
|
17
|
-
|
|
18
|
-
expect(
|
|
19
|
-
globalThis.sessionStorage.getItem(PENDING_COLLECTION_CREATED_STORAGE_KEY),
|
|
20
|
-
).toBe('{"collectionId":"col_books"}');
|
|
21
|
-
expect(consumeCollectionCreatedNotice()).toBe("col_books");
|
|
22
|
-
expect(consumeCollectionCreatedNotice()).toBeNull();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("clears malformed pending state", () => {
|
|
26
|
-
globalThis.sessionStorage.setItem(
|
|
27
|
-
PENDING_COLLECTION_CREATED_STORAGE_KEY,
|
|
28
|
-
'{"collectionId":false}',
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
expect(consumeCollectionCreatedNotice()).toBeNull();
|
|
32
|
-
expect(
|
|
33
|
-
globalThis.sessionStorage.getItem(PENDING_COLLECTION_CREATED_STORAGE_KEY),
|
|
34
|
-
).toBeNull();
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
vi.mock("../toast.js", () => ({
|
|
6
|
-
showToast: vi.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
vi.mock("../collection-created-notice.js", () => ({
|
|
10
|
-
queueCollectionCreatedNotice: vi.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
import "../collection-form-bridge.js";
|
|
14
|
-
|
|
15
|
-
type CollectionFormHarness = HTMLElement & {
|
|
16
|
-
cancelHref?: string;
|
|
17
|
-
initial?: {
|
|
18
|
-
slug?: string;
|
|
19
|
-
};
|
|
20
|
-
loading?: boolean;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function createPageForm(
|
|
24
|
-
cancelHref = "/collections",
|
|
25
|
-
initial: CollectionFormHarness["initial"] = { slug: "books" },
|
|
26
|
-
): CollectionFormHarness {
|
|
27
|
-
document.body.innerHTML = `
|
|
28
|
-
<div
|
|
29
|
-
data-collection-editor-page
|
|
30
|
-
data-collection-editor-save-failed="Couldn't save. Try again in a moment."
|
|
31
|
-
>
|
|
32
|
-
<jant-collection-form></jant-collection-form>
|
|
33
|
-
</div>
|
|
34
|
-
`;
|
|
35
|
-
|
|
36
|
-
const formEl = document.querySelector(
|
|
37
|
-
"jant-collection-form",
|
|
38
|
-
) as CollectionFormHarness | null;
|
|
39
|
-
|
|
40
|
-
if (!formEl) {
|
|
41
|
-
throw new Error("Expected collection form element");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
formEl.cancelHref = cancelHref;
|
|
45
|
-
formEl.initial = initial;
|
|
46
|
-
return formEl;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function flushAsyncWork() {
|
|
50
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
51
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("collection form bridge", () => {
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
document.body.innerHTML = "";
|
|
57
|
-
vi.clearAllMocks();
|
|
58
|
-
vi.restoreAllMocks();
|
|
59
|
-
window.location.href = "http://localhost/collections/new";
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("redirects new collections back to the configured return URL", async () => {
|
|
63
|
-
const formEl = createPageForm("/collections");
|
|
64
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
65
|
-
new Response(JSON.stringify({ id: "col_books", slug: "books" }), {
|
|
66
|
-
status: 201,
|
|
67
|
-
headers: { "Content-Type": "application/json" },
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
formEl.dispatchEvent(
|
|
72
|
-
new CustomEvent("jant:collection-submit", {
|
|
73
|
-
bubbles: true,
|
|
74
|
-
detail: {
|
|
75
|
-
endpoint: "/api/collections",
|
|
76
|
-
isEdit: false,
|
|
77
|
-
data: {
|
|
78
|
-
title: "Books",
|
|
79
|
-
slug: "books",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
}),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
await flushAsyncWork();
|
|
86
|
-
|
|
87
|
-
const { queueCollectionCreatedNotice } =
|
|
88
|
-
await import("../collection-created-notice.js");
|
|
89
|
-
|
|
90
|
-
expect(fetchSpy).toHaveBeenCalledWith("/api/collections", {
|
|
91
|
-
method: "POST",
|
|
92
|
-
headers: {
|
|
93
|
-
"Content-Type": "application/json",
|
|
94
|
-
Accept: "application/json",
|
|
95
|
-
},
|
|
96
|
-
body: JSON.stringify({
|
|
97
|
-
title: "Books",
|
|
98
|
-
slug: "books",
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
expect(window.location.pathname).toBe("/collections");
|
|
102
|
-
expect(queueCollectionCreatedNotice).toHaveBeenCalledWith("col_books");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("redirects edited collections back to the collections page when launched from the list", async () => {
|
|
106
|
-
const formEl = createPageForm("/collections");
|
|
107
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
108
|
-
new Response(JSON.stringify({ slug: "books-renamed" }), {
|
|
109
|
-
status: 200,
|
|
110
|
-
headers: { "Content-Type": "application/json" },
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
formEl.dispatchEvent(
|
|
115
|
-
new CustomEvent("jant:collection-submit", {
|
|
116
|
-
bubbles: true,
|
|
117
|
-
detail: {
|
|
118
|
-
endpoint: "/api/collections/collection-1",
|
|
119
|
-
isEdit: true,
|
|
120
|
-
data: {
|
|
121
|
-
title: "Books renamed",
|
|
122
|
-
slug: "books-renamed",
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
}),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
await flushAsyncWork();
|
|
129
|
-
|
|
130
|
-
expect(fetchSpy).toHaveBeenCalledWith("/api/collections/collection-1", {
|
|
131
|
-
method: "PUT",
|
|
132
|
-
headers: {
|
|
133
|
-
"Content-Type": "application/json",
|
|
134
|
-
Accept: "application/json",
|
|
135
|
-
},
|
|
136
|
-
body: JSON.stringify({
|
|
137
|
-
title: "Books renamed",
|
|
138
|
-
slug: "books-renamed",
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
expect(window.location.pathname).toBe("/collections");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("redirects edited collections back to the updated detail page when launched from detail", async () => {
|
|
145
|
-
window.location.href =
|
|
146
|
-
"http://localhost/collections/books/edit?returnTo=%2Fbooks";
|
|
147
|
-
const formEl = createPageForm("/books");
|
|
148
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
149
|
-
new Response(JSON.stringify({ slug: "books-renamed" }), {
|
|
150
|
-
status: 200,
|
|
151
|
-
headers: { "Content-Type": "application/json" },
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
formEl.dispatchEvent(
|
|
156
|
-
new CustomEvent("jant:collection-submit", {
|
|
157
|
-
bubbles: true,
|
|
158
|
-
detail: {
|
|
159
|
-
endpoint: "/api/collections/collection-1",
|
|
160
|
-
isEdit: true,
|
|
161
|
-
data: {
|
|
162
|
-
title: "Books renamed",
|
|
163
|
-
slug: "books-renamed",
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
}),
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
await flushAsyncWork();
|
|
170
|
-
|
|
171
|
-
expect(window.location.pathname).toBe("/books-renamed");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("rewrites aggregate return paths when a collection slug changes", async () => {
|
|
175
|
-
window.location.href =
|
|
176
|
-
"http://localhost/collections/books/edit?returnTo=%2Fcollections%2Fbooks%2Bessays";
|
|
177
|
-
const formEl = createPageForm("/collections/books+essays", {
|
|
178
|
-
slug: "books",
|
|
179
|
-
});
|
|
180
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
181
|
-
new Response(JSON.stringify({ slug: "books-renamed" }), {
|
|
182
|
-
status: 200,
|
|
183
|
-
headers: { "Content-Type": "application/json" },
|
|
184
|
-
}),
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
formEl.dispatchEvent(
|
|
188
|
-
new CustomEvent("jant:collection-submit", {
|
|
189
|
-
bubbles: true,
|
|
190
|
-
detail: {
|
|
191
|
-
endpoint: "/api/collections/collection-1",
|
|
192
|
-
isEdit: true,
|
|
193
|
-
data: {
|
|
194
|
-
title: "Books renamed",
|
|
195
|
-
slug: "books-renamed",
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
}),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
await flushAsyncWork();
|
|
202
|
-
|
|
203
|
-
expect(fetchSpy).toHaveBeenCalledWith("/api/collections/collection-1", {
|
|
204
|
-
method: "PUT",
|
|
205
|
-
headers: {
|
|
206
|
-
"Content-Type": "application/json",
|
|
207
|
-
Accept: "application/json",
|
|
208
|
-
},
|
|
209
|
-
body: JSON.stringify({
|
|
210
|
-
title: "Books renamed",
|
|
211
|
-
slug: "books-renamed",
|
|
212
|
-
}),
|
|
213
|
-
});
|
|
214
|
-
expect(window.location.pathname).toBe("/collections/books-renamed+essays");
|
|
215
|
-
});
|
|
216
|
-
});
|