@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -99,6 +99,79 @@ function filesToMap(
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
describe("createExportService (Hugo)", () => {
|
|
102
|
+
// Hugo has its own summary rule and would cut somewhere else, so whether the
|
|
103
|
+
// timeline truncates a post is decided here and carried as front matter.
|
|
104
|
+
// Written only when true — a consumer reads its presence.
|
|
105
|
+
it("marks a truncated post in front matter and leaves a short one unmarked", async () => {
|
|
106
|
+
const para = (text: string) => ({
|
|
107
|
+
type: "paragraph",
|
|
108
|
+
content: [{ type: "text", text }],
|
|
109
|
+
});
|
|
110
|
+
const long = makePost({
|
|
111
|
+
id: "post-long",
|
|
112
|
+
slug: "long-read",
|
|
113
|
+
title: "A long read",
|
|
114
|
+
threadId: "post-long",
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
type: "doc",
|
|
117
|
+
content: [
|
|
118
|
+
para("Alpha ".repeat(90).trim()),
|
|
119
|
+
para("Omega ".repeat(90).trim()),
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
const short = makePost({
|
|
124
|
+
id: "post-short",
|
|
125
|
+
slug: "short-note",
|
|
126
|
+
title: null,
|
|
127
|
+
threadId: "post-short",
|
|
128
|
+
body: JSON.stringify({ type: "doc", content: [para("Short enough.")] }),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const files = await createExportService(
|
|
132
|
+
{
|
|
133
|
+
posts: { list: async () => [long, short] },
|
|
134
|
+
paths: {
|
|
135
|
+
getPostSlugMap: async () =>
|
|
136
|
+
new Map([
|
|
137
|
+
["post-long", "long-read"],
|
|
138
|
+
["post-short", "short-note"],
|
|
139
|
+
]),
|
|
140
|
+
getPostAliases: async () => new Map(),
|
|
141
|
+
getCollectionSlugMap: async () => new Map(),
|
|
142
|
+
},
|
|
143
|
+
collections: {
|
|
144
|
+
list: async () => [],
|
|
145
|
+
listDirectoryData: async () => ({
|
|
146
|
+
collections: [],
|
|
147
|
+
items: [],
|
|
148
|
+
directoryItems: [],
|
|
149
|
+
}),
|
|
150
|
+
getCollectionsByPostIds: async () => new Map(),
|
|
151
|
+
getCollectionEntriesByThreadIds: async () => new Map(),
|
|
152
|
+
},
|
|
153
|
+
media: { getByPostIds: async () => new Map() },
|
|
154
|
+
} as unknown as ServicesArg,
|
|
155
|
+
makeSiteConfig(),
|
|
156
|
+
).generateHugoFiles();
|
|
157
|
+
|
|
158
|
+
const read = async (path: string) => {
|
|
159
|
+
const file = files.find((f) => f.path === path);
|
|
160
|
+
expect(file, `missing ${path}`).toBeDefined();
|
|
161
|
+
const { frontMatter } = await parseFrontMatter(
|
|
162
|
+
typeof file!.content === "string"
|
|
163
|
+
? file!.content
|
|
164
|
+
: new TextDecoder().decode(file!.content),
|
|
165
|
+
);
|
|
166
|
+
return frontMatter;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
expect((await read("content/long-read/_index.md")).truncated).toBe(true);
|
|
170
|
+
expect(
|
|
171
|
+
(await read("content/short-note/_index.md")).truncated,
|
|
172
|
+
).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
102
175
|
it("emits a branch bundle per root post with YAML front matter in stable key order", async () => {
|
|
103
176
|
const root = makePost({
|
|
104
177
|
id: "post-root",
|
|
@@ -496,6 +569,164 @@ describe("createExportService (Hugo)", () => {
|
|
|
496
569
|
expect(data).toContain('url = "/index.xml"');
|
|
497
570
|
});
|
|
498
571
|
|
|
572
|
+
it("resolves the nav Subscribe link to the feed the page would recommend", async () => {
|
|
573
|
+
const service = createExportService(
|
|
574
|
+
buildServices({ posts: [] }),
|
|
575
|
+
makeSiteConfig({
|
|
576
|
+
mainRssFeed: "featured",
|
|
577
|
+
navItems: [
|
|
578
|
+
{
|
|
579
|
+
type: "system",
|
|
580
|
+
systemKey: "subscribe",
|
|
581
|
+
label: "",
|
|
582
|
+
url: "/subscribe",
|
|
583
|
+
position: 0,
|
|
584
|
+
placement: "more",
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
}),
|
|
588
|
+
);
|
|
589
|
+
const data = filesToMap(await service.generateHugoFiles()).get(
|
|
590
|
+
"data/jant.toml",
|
|
591
|
+
) as string;
|
|
592
|
+
|
|
593
|
+
// There is no /subscribe page in the exported site to link at.
|
|
594
|
+
expect(data).not.toContain('url = "/subscribe"');
|
|
595
|
+
expect(data).toContain('url = "/featured/index.xml"');
|
|
596
|
+
expect(data).toContain('label = "Subscribe"');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("redirects every runtime feed address to its exported counterpart", async () => {
|
|
600
|
+
const service = createExportService(
|
|
601
|
+
buildServices({ posts: [] }),
|
|
602
|
+
makeSiteConfig({ mainRssFeed: "latest" }),
|
|
603
|
+
);
|
|
604
|
+
const redirects = filesToMap(await service.generateHugoFiles()).get(
|
|
605
|
+
"static/_redirects",
|
|
606
|
+
) as string;
|
|
607
|
+
|
|
608
|
+
// Feed readers hold the runtime addresses; nothing in the Hugo tree
|
|
609
|
+
// answers them without these.
|
|
610
|
+
const rules = redirects
|
|
611
|
+
.split("\n")
|
|
612
|
+
.filter((l) => l.startsWith("/"))
|
|
613
|
+
.map((l) => l.split(/\s+/));
|
|
614
|
+
expect(new Map(rules.map((r) => [r[0], r[1]]))).toEqual(
|
|
615
|
+
new Map([
|
|
616
|
+
["/feed", "/index.xml"],
|
|
617
|
+
["/latest/feed", "/index.xml"],
|
|
618
|
+
["/featured/feed", "/featured/index.xml"],
|
|
619
|
+
["/archive/feed", "/archive/index.xml"],
|
|
620
|
+
// The 308 aliases the live site still answers.
|
|
621
|
+
["/feed/latest", "/index.xml"],
|
|
622
|
+
["/feed/all", "/index.xml"],
|
|
623
|
+
["/feed/featured", "/featured/index.xml"],
|
|
624
|
+
["/feed/atom.xml", "/index.xml"],
|
|
625
|
+
["/feed/latest/atom.xml", "/index.xml"],
|
|
626
|
+
["/feed/all/atom.xml", "/index.xml"],
|
|
627
|
+
["/feed/featured/atom.xml", "/featured/index.xml"],
|
|
628
|
+
["/:slug/feed", "/:slug/index.xml"],
|
|
629
|
+
]),
|
|
630
|
+
);
|
|
631
|
+
// A feed reader follows a 301 and rewrites its stored address; a 302
|
|
632
|
+
// would leave it polling the dead one forever.
|
|
633
|
+
expect(rules.every((r) => r[2] === "301")).toBe(true);
|
|
634
|
+
// First match wins in both hosts that read this file, so the collection
|
|
635
|
+
// catch-all must not shadow the fixed sections.
|
|
636
|
+
expect(rules.at(-1)?.[0]).toBe("/:slug/feed");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("points /feed at the featured feed when that is the site's main feed", async () => {
|
|
640
|
+
const service = createExportService(
|
|
641
|
+
buildServices({ posts: [] }),
|
|
642
|
+
makeSiteConfig({ mainRssFeed: "featured" }),
|
|
643
|
+
);
|
|
644
|
+
const redirects = filesToMap(await service.generateHugoFiles()).get(
|
|
645
|
+
"static/_redirects",
|
|
646
|
+
) as string;
|
|
647
|
+
|
|
648
|
+
expect(redirects).toMatch(/^\/feed\s+\/featured\/index\.xml\s+301$/m);
|
|
649
|
+
expect(redirects).toMatch(
|
|
650
|
+
/^\/feed\/atom\.xml\s+\/featured\/index\.xml\s+301$/m,
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("falls back to the latest feed when no featured section was emitted", async () => {
|
|
655
|
+
// A post owning the `featured` slug takes the section's place, so
|
|
656
|
+
// /featured/index.xml is never built. Redirecting there would turn a live
|
|
657
|
+
// subscription into a 404; the latest feed is wider but real.
|
|
658
|
+
const service = createExportService(
|
|
659
|
+
buildServices({ posts: [makePost({ id: "post-1", slug: "featured" })] }),
|
|
660
|
+
makeSiteConfig({ mainRssFeed: "featured" }),
|
|
661
|
+
);
|
|
662
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
663
|
+
const redirects = files.get("static/_redirects") as string;
|
|
664
|
+
|
|
665
|
+
// The post took the path the featured section would have used, so the
|
|
666
|
+
// section — and /featured/index.xml with it — was never built.
|
|
667
|
+
expect(files.get("content/featured/_index.md") as string).not.toContain(
|
|
668
|
+
"type: featured",
|
|
669
|
+
);
|
|
670
|
+
expect(redirects).not.toContain("/featured/index.xml");
|
|
671
|
+
expect(redirects).toMatch(/^\/feed\s+\/index\.xml\s+301$/m);
|
|
672
|
+
expect(redirects).toMatch(/^\/featured\/feed\s+\/index\.xml\s+301$/m);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("writes no feed redirects when the site publishes no feeds", async () => {
|
|
676
|
+
const service = createExportService(
|
|
677
|
+
buildServices({ posts: [] }),
|
|
678
|
+
makeSiteConfig({ rssFeedsEnabled: false }),
|
|
679
|
+
);
|
|
680
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
681
|
+
|
|
682
|
+
expect(files.has("static/_redirects")).toBe(false);
|
|
683
|
+
expect(files.get("README.md") as string).not.toContain("## Feeds");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("tells the README reader that feed addresses are the exception to URL continuity", async () => {
|
|
687
|
+
const service = createExportService(
|
|
688
|
+
buildServices({ posts: [] }),
|
|
689
|
+
makeSiteConfig({ mainRssFeed: "latest" }),
|
|
690
|
+
);
|
|
691
|
+
const readme = filesToMap(await service.generateHugoFiles()).get(
|
|
692
|
+
"README.md",
|
|
693
|
+
) as string;
|
|
694
|
+
|
|
695
|
+
// The Notes list advertises that post URLs survive the move; without this
|
|
696
|
+
// it reads as a promise covering every URL.
|
|
697
|
+
expect(readme).toContain("## Feeds");
|
|
698
|
+
expect(readme).toContain("`/latest/feed`");
|
|
699
|
+
expect(readme).toContain("static/_redirects");
|
|
700
|
+
expect(readme).toContain("Feed addresses are the exception");
|
|
701
|
+
// No /subscribe page exists in the export, and the README says where the
|
|
702
|
+
// nav entry lands instead.
|
|
703
|
+
expect(readme).toContain("has no `/subscribe` page");
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("hides system Subscribe navigation when feeds are off", async () => {
|
|
707
|
+
const service = createExportService(
|
|
708
|
+
buildServices({ posts: [] }),
|
|
709
|
+
makeSiteConfig({
|
|
710
|
+
rssFeedsEnabled: false,
|
|
711
|
+
navItems: [
|
|
712
|
+
{
|
|
713
|
+
type: "system",
|
|
714
|
+
systemKey: "subscribe",
|
|
715
|
+
label: "System subscribe",
|
|
716
|
+
url: "/subscribe",
|
|
717
|
+
position: 0,
|
|
718
|
+
placement: "more",
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
723
|
+
const data = filesToMap(await service.generateHugoFiles()).get(
|
|
724
|
+
"data/jant.toml",
|
|
725
|
+
) as string;
|
|
726
|
+
|
|
727
|
+
expect(data).not.toContain('label = "System subscribe"');
|
|
728
|
+
});
|
|
729
|
+
|
|
499
730
|
it("emits last_activity_at on root post front matter so feed <updated> tracks thread bumps", async () => {
|
|
500
731
|
const post = makePost({
|
|
501
732
|
id: "post-1",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `src/styles`, resolved from the working directory rather than
|
|
6
|
+
* `import.meta.url`: under the `happy-dom` environment `import.meta.url` is an
|
|
7
|
+
* `http:` URL that `readFileSync` rejects, and Vitest runs with the package
|
|
8
|
+
* root as its working directory in every environment.
|
|
9
|
+
*/
|
|
10
|
+
const STYLES_DIR = resolve(process.cwd(), "src/styles");
|
|
11
|
+
|
|
12
|
+
/** Hand-written component CSS a signed-out visitor downloads. */
|
|
13
|
+
const READER_SHEETS = ["ui.css", "components.css"] as const;
|
|
14
|
+
|
|
15
|
+
/** Hand-written component CSS that only loads on an authenticated page. */
|
|
16
|
+
const AUTHOR_SHEETS = ["ui-author.css", "components-author.css"] as const;
|
|
17
|
+
|
|
18
|
+
function read(names: readonly string[]): string {
|
|
19
|
+
return names
|
|
20
|
+
.map((name) => readFileSync(resolve(STYLES_DIR, name), "utf8"))
|
|
21
|
+
.join("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every `@layer components` rule Jant hand-writes, both audiences together.
|
|
26
|
+
*
|
|
27
|
+
* `ui.css`/`components.css` carry what readers need; `ui-author.css` and
|
|
28
|
+
* `components-author.css` carry the composer, editor, settings, config editor
|
|
29
|
+
* and dash chrome that only load once someone is signed in. A test asserting
|
|
30
|
+
* that a rule is declared cares that it exists at all, not which half it
|
|
31
|
+
* landed in — so moving a rule between them must never break a test.
|
|
32
|
+
*
|
|
33
|
+
* @returns The concatenated contents of all four stylesheets.
|
|
34
|
+
* @example
|
|
35
|
+
* expect(readComponentCss()).toMatch(/\.compose-post-meta-panel\s*\{/);
|
|
36
|
+
*/
|
|
37
|
+
export function readComponentCss(): string {
|
|
38
|
+
return read([...READER_SHEETS, ...AUTHOR_SHEETS]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Only the reader halves.
|
|
43
|
+
*
|
|
44
|
+
* Use when the point of the assertion is that a rule ships to signed-out
|
|
45
|
+
* visitors — or, with a negative matcher, that it does not.
|
|
46
|
+
*
|
|
47
|
+
* @returns The concatenated contents of `ui.css` and `components.css`.
|
|
48
|
+
* @example
|
|
49
|
+
* expect(readReaderCss()).not.toMatch(/\.compose-dialog\s*\{/);
|
|
50
|
+
*/
|
|
51
|
+
export function readReaderCss(): string {
|
|
52
|
+
return read(READER_SHEETS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Only the author halves.
|
|
57
|
+
*
|
|
58
|
+
* @returns The concatenated contents of `ui-author.css` and
|
|
59
|
+
* `components-author.css`.
|
|
60
|
+
* @example
|
|
61
|
+
* expect(readAuthorCss()).toMatch(/\.settings-root\s*\{/);
|
|
62
|
+
*/
|
|
63
|
+
export function readAuthorCss(): string {
|
|
64
|
+
return read(AUTHOR_SHEETS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every stylesheet Jant hand-writes, each paired with its filename.
|
|
69
|
+
*
|
|
70
|
+
* `readComponentCss()` and friends concatenate, which is right for asking
|
|
71
|
+
* whether a rule exists. This one keeps the files apart so a failure can name
|
|
72
|
+
* the file and line it found the problem on — `tokens.css` and `preset.css`
|
|
73
|
+
* are in here too, because breakpoints live in all six.
|
|
74
|
+
*
|
|
75
|
+
* @returns One entry per stylesheet, in load order.
|
|
76
|
+
* @example
|
|
77
|
+
* for (const { name, css } of readAllStylesheets()) {
|
|
78
|
+
* expect(css, name).not.toMatch(/@media \(max-width: 767px\)/);
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
export function readAllStylesheets(): readonly {
|
|
82
|
+
name: string;
|
|
83
|
+
css: string;
|
|
84
|
+
}[] {
|
|
85
|
+
return [
|
|
86
|
+
{ name: "preset.css", path: resolve(process.cwd(), "src/preset.css") },
|
|
87
|
+
...[...READER_SHEETS, ...AUTHOR_SHEETS, "tokens.css", "site-media.css"].map(
|
|
88
|
+
(name) => ({ name, path: resolve(STYLES_DIR, name) }),
|
|
89
|
+
),
|
|
90
|
+
].map(({ name, path }) => ({ name, css: readFileSync(path, "utf8") }));
|
|
91
|
+
}
|
|
@@ -134,3 +134,36 @@ export function createTestDatabase(options?: { fts?: boolean }) {
|
|
|
134
134
|
|
|
135
135
|
return { db, sqlite };
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/** The most bound parameters Cloudflare D1 accepts in one statement. */
|
|
139
|
+
export const D1_MAX_BOUND_PARAMETERS = 100;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Make a test database refuse what D1 refuses: a statement with more than
|
|
143
|
+
* {@link D1_MAX_BOUND_PARAMETERS} bound parameters.
|
|
144
|
+
*
|
|
145
|
+
* better-sqlite3 accepts tens of thousands, so a query that puts every id in
|
|
146
|
+
* one `IN (…)` list passes every test and fails in production once the list
|
|
147
|
+
* grows. Install this after seeding, so only the code under test is held to it.
|
|
148
|
+
*
|
|
149
|
+
* @param sqlite - The handle `createTestDatabase` or `createTestApp` returned
|
|
150
|
+
* @returns Nothing; the handle is changed in place
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const { app, sqlite } = createTestApp();
|
|
154
|
+
* await seed(app);
|
|
155
|
+
* enforceD1BoundParameterLimit(sqlite);
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function enforceD1BoundParameterLimit(sqlite: Database.Database): void {
|
|
159
|
+
const prepare = sqlite.prepare.bind(sqlite);
|
|
160
|
+
sqlite.prepare = ((source: string) => {
|
|
161
|
+
const count = source.match(/\?/g)?.length ?? 0;
|
|
162
|
+
if (count > D1_MAX_BOUND_PARAMETERS) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`too many SQL variables: ${count} bound parameters, D1 allows ${D1_MAX_BOUND_PARAMETERS}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return prepare(source);
|
|
168
|
+
}) as Database.Database["prepare"];
|
|
169
|
+
}
|
|
@@ -87,10 +87,13 @@ export function makeSiteConfig(over: Partial<SiteConfig> = {}): SiteConfig {
|
|
|
87
87
|
siteUrl: "https://example.com",
|
|
88
88
|
siteDescription: "Export fixture",
|
|
89
89
|
siteLanguage: "en",
|
|
90
|
+
multilingualEnabled: false,
|
|
91
|
+
additionalLanguages: [],
|
|
90
92
|
showJantBrandingOnHome: true,
|
|
91
93
|
publicApiEnabled: true,
|
|
92
94
|
rssFeedsEnabled: true,
|
|
93
95
|
mainRssFeed: "latest",
|
|
96
|
+
archiveDefaultLayout: "list",
|
|
94
97
|
siteFooter: "",
|
|
95
98
|
showHeaderAvatar: false,
|
|
96
99
|
siteAvatarUrl: "",
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
readAuthorCss,
|
|
4
|
+
readComponentCss,
|
|
5
|
+
readReaderCss,
|
|
6
|
+
} from "./helpers/component-css.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `client.css` is what a signed-out visitor waits on before the page paints,
|
|
10
|
+
* and `styles/ui.css` plus `styles/components.css` are the hand-written half
|
|
11
|
+
* of it. Author-only component CSS belongs in `styles/ui-author.css` and
|
|
12
|
+
* `styles/components-author.css`, which `BaseLayout` links only on
|
|
13
|
+
* authenticated pages.
|
|
14
|
+
*
|
|
15
|
+
* Class families that only ever appear in authenticated markup. The editor's
|
|
16
|
+
* `.tiptap-*` chrome is here too, minus the names TipTap also writes into
|
|
17
|
+
* published post HTML — readers see those.
|
|
18
|
+
*/
|
|
19
|
+
const AUTHOR_ONLY = [
|
|
20
|
+
"compose",
|
|
21
|
+
"command",
|
|
22
|
+
"settings",
|
|
23
|
+
"picker",
|
|
24
|
+
"confirm",
|
|
25
|
+
"draft",
|
|
26
|
+
"theme-preview",
|
|
27
|
+
// The floating compose button on phones, and the page modifier that keeps
|
|
28
|
+
// room for it. Reader layout renders both, but only for a signed-in author.
|
|
29
|
+
"site-mobile-compose",
|
|
30
|
+
"site-page-mobile-compose-enabled",
|
|
31
|
+
// From `components.css`: the config editor, the navigation manager, the
|
|
32
|
+
// custom URL manager, the dash chrome and the form skeletons. `.nav-*` here
|
|
33
|
+
// is `jant-nav-manager`, not the site header — that is `.site-header-nav`.
|
|
34
|
+
// `.collection-picker-*` is spelled out because plain `.collection-*` is a
|
|
35
|
+
// reader family (the collection pages).
|
|
36
|
+
"config",
|
|
37
|
+
"nav",
|
|
38
|
+
"custom",
|
|
39
|
+
"dash",
|
|
40
|
+
"skel",
|
|
41
|
+
"collection-picker",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** TipTap class names that land in reader-rendered post HTML. */
|
|
45
|
+
const READER_TIPTAP = [
|
|
46
|
+
"tiptap-table",
|
|
47
|
+
"tiptap-html-block",
|
|
48
|
+
"tiptap-more-break",
|
|
49
|
+
"tiptap-embed-figure",
|
|
50
|
+
"tiptap-embed-frame",
|
|
51
|
+
"tiptap-embed-fallback",
|
|
52
|
+
"tiptap-footnote-reference",
|
|
53
|
+
"tiptap-footnote-definition",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `.compose-thread-layout` shares the compose rail's marker vocabulary with
|
|
58
|
+
* the reading rail's `.thread-group` in one declaration, so that the two
|
|
59
|
+
* cannot drift apart. That single grouped selector is the documented
|
|
60
|
+
* exception; see the comment on the rule in `styles/ui.css`.
|
|
61
|
+
*/
|
|
62
|
+
const READER_SHEET_EXCEPTIONS = [".compose-thread-layout"];
|
|
63
|
+
|
|
64
|
+
/** Every class named in a selector, ignoring declaration bodies. */
|
|
65
|
+
function selectorClasses(css: string): string[] {
|
|
66
|
+
const found: string[] = [];
|
|
67
|
+
// Selectors are what precedes a `{` that opens a block; declarations end in
|
|
68
|
+
// `;`, so anything after the last `;` or `}` is the selector text.
|
|
69
|
+
for (const chunk of css.split("{")) {
|
|
70
|
+
const selector = chunk.slice(
|
|
71
|
+
Math.max(chunk.lastIndexOf(";"), chunk.lastIndexOf("}")) + 1,
|
|
72
|
+
);
|
|
73
|
+
const withoutComments = selector.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
74
|
+
for (const match of withoutComments.matchAll(
|
|
75
|
+
/\.(-?[A-Za-z_][A-Za-z0-9_-]*)/g,
|
|
76
|
+
)) {
|
|
77
|
+
found.push(match[1]!);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("stylesheet audience", () => {
|
|
84
|
+
it("keeps author-only component CSS out of the reader stylesheet", () => {
|
|
85
|
+
const css = readReaderCss();
|
|
86
|
+
const exceptions = new Set(
|
|
87
|
+
READER_SHEET_EXCEPTIONS.map((name) => name.replace(/^\./, "")),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const leaked = [
|
|
91
|
+
...new Set(
|
|
92
|
+
selectorClasses(css).filter(
|
|
93
|
+
(name) =>
|
|
94
|
+
!exceptions.has(name) &&
|
|
95
|
+
AUTHOR_ONLY.some(
|
|
96
|
+
(family) => name === family || name.startsWith(`${family}-`),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
].sort();
|
|
101
|
+
|
|
102
|
+
expect(leaked).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("keeps the editor chrome out of the reader stylesheet", () => {
|
|
106
|
+
const readerTiptap = new Set(READER_TIPTAP);
|
|
107
|
+
const leaked = [
|
|
108
|
+
...new Set(
|
|
109
|
+
selectorClasses(readReaderCss()).filter(
|
|
110
|
+
(name) =>
|
|
111
|
+
(name === "tiptap" || name.startsWith("tiptap-")) &&
|
|
112
|
+
!readerTiptap.has(name),
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
].sort();
|
|
116
|
+
|
|
117
|
+
expect(leaked).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("still styles the TipTap output a published post carries", () => {
|
|
121
|
+
// `lib/embed-render.ts` and `lib/markdown-manager.ts` write these into post
|
|
122
|
+
// HTML, and they are the ones with rules of their own — a reader holding
|
|
123
|
+
// only `client.css` has to be able to style them. The rest of
|
|
124
|
+
// READER_TIPTAP reaches published posts too but is styled by element
|
|
125
|
+
// through `.prose`, so there is no rule here to keep.
|
|
126
|
+
const css = readReaderCss();
|
|
127
|
+
for (const name of [
|
|
128
|
+
"tiptap-embed-figure",
|
|
129
|
+
"tiptap-embed-frame",
|
|
130
|
+
"tiptap-embed-fallback",
|
|
131
|
+
"tiptap-more-break",
|
|
132
|
+
]) {
|
|
133
|
+
expect(selectorClasses(css)).toContain(name);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("moved real weight, not a handful of rules", () => {
|
|
138
|
+
// The split is only worth its complexity while the author half is
|
|
139
|
+
// substantial. If it ever shrinks to a rounding error, fold it back in.
|
|
140
|
+
expect(readAuthorCss().length).toBeGreaterThan(readReaderCss().length / 2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("keeps the author half of components.css out of the reader sheet", () => {
|
|
144
|
+
// `preset.css` imports `components.css` on every page, so the settings
|
|
145
|
+
// pages and the config editor must not be left in it.
|
|
146
|
+
const author = readAuthorCss();
|
|
147
|
+
expect(author).toContain(".settings-root");
|
|
148
|
+
expect(author).toContain(".config-editor-page");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("leaves the reader-side families of components.css in place", () => {
|
|
152
|
+
// Verified against every `class=` usage in the tree: `BaseLayout` renders
|
|
153
|
+
// the toast container on every page, and `.page-intro-*` is the header of
|
|
154
|
+
// the archive and collections pages, both public.
|
|
155
|
+
const reader = readReaderCss();
|
|
156
|
+
expect(reader).toContain(".toast-container");
|
|
157
|
+
expect(reader).toContain(".page-intro");
|
|
158
|
+
});
|
|
159
|
+
});
|