@jant/core 0.6.15 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +88 -1
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +93 -34
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +11 -2
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +401 -301
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +55 -23
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
package/src/services/export.ts
CHANGED
|
@@ -27,8 +27,9 @@ import {
|
|
|
27
27
|
getDefaultJantAppleTouchIconBytes,
|
|
28
28
|
getDefaultJantFaviconIcoBytes,
|
|
29
29
|
} from "../lib/jant-branding.js";
|
|
30
|
+
import { getRootActivityAt } from "../db/thread-activity.js";
|
|
30
31
|
import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
|
|
31
|
-
import { extractBodyText } from "../lib/summary.js";
|
|
32
|
+
import { extractBodyText, extractTimelineSummary } from "../lib/summary.js";
|
|
32
33
|
import { getMediaUrl, getPublicUrlForProvider } from "../lib/image.js";
|
|
33
34
|
import { render as renderMarkdown } from "../lib/markdown.js";
|
|
34
35
|
import { formatRelativeAge, toISOString } from "../lib/time.js";
|
|
@@ -84,6 +85,7 @@ import type { StorageDriver } from "../lib/storage.js";
|
|
|
84
85
|
import { base64ToUint8Array } from "../lib/favicon.js";
|
|
85
86
|
import {
|
|
86
87
|
SYSTEM_NAV_KEYS,
|
|
88
|
+
isFeedNavKey,
|
|
87
89
|
type Collection,
|
|
88
90
|
type Media,
|
|
89
91
|
type NavItem,
|
|
@@ -109,6 +111,10 @@ export interface SiteConfig {
|
|
|
109
111
|
siteUrl: string;
|
|
110
112
|
siteDescription: string;
|
|
111
113
|
siteLanguage: string;
|
|
114
|
+
/** Whether the source site serves per-language browsing views. */
|
|
115
|
+
multilingualEnabled: boolean;
|
|
116
|
+
/** Non-primary content languages, in switcher order. */
|
|
117
|
+
additionalLanguages: readonly string[];
|
|
112
118
|
showJantBrandingOnHome: boolean;
|
|
113
119
|
/** Whether anonymous JSON reads are enabled on the source Jant site. */
|
|
114
120
|
publicApiEnabled: boolean;
|
|
@@ -116,6 +122,8 @@ export interface SiteConfig {
|
|
|
116
122
|
rssFeedsEnabled: boolean;
|
|
117
123
|
/** "latest" or "featured" — drives the default RSS nav link in the exported site. */
|
|
118
124
|
mainRssFeed: string;
|
|
125
|
+
/** "list" or "grid" — the layout the exported archive page opens with. */
|
|
126
|
+
archiveDefaultLayout: string;
|
|
119
127
|
siteFooter: string;
|
|
120
128
|
showHeaderAvatar: boolean;
|
|
121
129
|
siteAvatarUrl: string;
|
|
@@ -136,7 +144,13 @@ export interface SiteConfig {
|
|
|
136
144
|
sitePathPrefix?: string;
|
|
137
145
|
navItems: Pick<
|
|
138
146
|
NavItem,
|
|
139
|
-
|
|
147
|
+
| "type"
|
|
148
|
+
| "systemKey"
|
|
149
|
+
| "label"
|
|
150
|
+
| "targetTitle"
|
|
151
|
+
| "url"
|
|
152
|
+
| "position"
|
|
153
|
+
| "placement"
|
|
140
154
|
>[];
|
|
141
155
|
/** Items per page for Hugo pagination — kept in sync with the main site's PAGE_SIZE. */
|
|
142
156
|
pageSize: number;
|
|
@@ -254,7 +268,10 @@ export function createExportService(
|
|
|
254
268
|
async generateHugoFiles() {
|
|
255
269
|
const collectionDirectoryDataPromise =
|
|
256
270
|
typeof services.collections.listDirectoryData === "function"
|
|
257
|
-
?
|
|
271
|
+
? // The export builds a public static site, so it sees what a
|
|
272
|
+
// signed-out reader sees. Language narrowing is not applied: the
|
|
273
|
+
// export walks every language itself.
|
|
274
|
+
services.collections.listDirectoryData({ isAuthenticated: false })
|
|
258
275
|
: Promise.resolve(null);
|
|
259
276
|
|
|
260
277
|
// 1. Query all data
|
|
@@ -303,14 +320,30 @@ export function createExportService(
|
|
|
303
320
|
allPosts,
|
|
304
321
|
collectionsByRoot,
|
|
305
322
|
);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
323
|
+
// Smart collections are left out of a static export. Their membership is
|
|
324
|
+
// a query, and the exported site has no database to run it against, so
|
|
325
|
+
// the honest options are "omit" or "freeze today's matches under a name
|
|
326
|
+
// that promises to keep updating". Round-tripping them is out of scope
|
|
327
|
+
// (see the smart collections design notes); omitting is what that
|
|
328
|
+
// decision means here.
|
|
329
|
+
const exportableDirectoryItems: ExportCollectionDirectorySourceItem[] =
|
|
330
|
+
collectionDirectoryData?.items
|
|
331
|
+
? collectionDirectoryData.items
|
|
332
|
+
.filter((item) => item.type !== "smart_collection")
|
|
333
|
+
.map((item) => ({
|
|
334
|
+
type: item.type as "collection" | "divider" | "link",
|
|
335
|
+
label: item.label,
|
|
336
|
+
url: item.url,
|
|
337
|
+
description: item.description,
|
|
338
|
+
collection: item.collection,
|
|
339
|
+
}))
|
|
340
|
+
: allCollections.map((collection) => ({
|
|
311
341
|
type: "collection" as const,
|
|
312
342
|
collection,
|
|
313
|
-
}))
|
|
343
|
+
}));
|
|
344
|
+
const exportedCollectionDirectoryItems =
|
|
345
|
+
buildExportedCollectionDirectoryItems(
|
|
346
|
+
exportableDirectoryItems,
|
|
314
347
|
collectionSlugMap,
|
|
315
348
|
collectionMetrics,
|
|
316
349
|
);
|
|
@@ -396,13 +429,28 @@ export function createExportService(
|
|
|
396
429
|
const usedSlugs = new Set<string>();
|
|
397
430
|
for (const s of slugMap.values()) usedSlugs.add(s);
|
|
398
431
|
for (const s of collectionSlugMap.values()) usedSlugs.add(s);
|
|
399
|
-
|
|
432
|
+
const hasFeaturedSection = !usedSlugs.has("featured");
|
|
433
|
+
if (hasFeaturedSection) {
|
|
400
434
|
exportFiles.push({
|
|
401
435
|
path: "content/featured/_index.md",
|
|
402
436
|
content: await buildFeaturedSection(siteConfig.rssFeedsEnabled),
|
|
403
437
|
});
|
|
404
438
|
}
|
|
405
439
|
|
|
440
|
+
// Feed subscribers are the one audience the export can lose silently:
|
|
441
|
+
// their reader polls a `/feed` address that this site does not serve.
|
|
442
|
+
// With feeds off there is nothing to redirect to, and the live site had
|
|
443
|
+
// no feeds to have been subscribed to either.
|
|
444
|
+
if (siteConfig.rssFeedsEnabled) {
|
|
445
|
+
exportFiles.push({
|
|
446
|
+
path: "static/_redirects",
|
|
447
|
+
content: buildFeedRedirects(
|
|
448
|
+
siteConfig.mainRssFeed,
|
|
449
|
+
hasFeaturedSection,
|
|
450
|
+
),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
406
454
|
// Single data file consumed by templates via `hugo.Data.jant`. The
|
|
407
455
|
// collection directory lives on the same object as `directory` so
|
|
408
456
|
// everything Jant owns round-trips in one place.
|
|
@@ -545,7 +593,7 @@ export function createExportService(
|
|
|
545
593
|
|
|
546
594
|
exportFiles.push({
|
|
547
595
|
path: "README.md",
|
|
548
|
-
content: buildReadme(siteConfig
|
|
596
|
+
content: buildReadme(siteConfig, hasFeaturedSection),
|
|
549
597
|
});
|
|
550
598
|
exportFiles.push({
|
|
551
599
|
path: ".gitignore",
|
|
@@ -675,6 +723,25 @@ function buildExportedCollectionEntriesForThread(
|
|
|
675
723
|
return resolved;
|
|
676
724
|
}
|
|
677
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Whether the site's timeline cuts this post's text short.
|
|
728
|
+
*
|
|
729
|
+
* Shares the boundary with the served feed rather than approximating it: Hugo
|
|
730
|
+
* has its own summary rule and would cut somewhere else, so the answer is
|
|
731
|
+
* computed here and carried as front matter.
|
|
732
|
+
*
|
|
733
|
+
* @param post - The post being exported
|
|
734
|
+
* @returns true when the timeline shows less than the post's page
|
|
735
|
+
* @example
|
|
736
|
+
* isPostTruncated(longArticle); // true
|
|
737
|
+
*/
|
|
738
|
+
function isPostTruncated(post: Post): boolean {
|
|
739
|
+
return (
|
|
740
|
+
extractTimelineSummary(post.body, !!post.title, { namespace: post.id })
|
|
741
|
+
?.hasMore === true
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
678
745
|
function collectionEntriesToRefs(
|
|
679
746
|
entries: readonly ExportedCollectionEntry[],
|
|
680
747
|
): HugoCollectionRef[] {
|
|
@@ -885,7 +952,13 @@ async function buildThreadBundle(
|
|
|
885
952
|
format: root.format,
|
|
886
953
|
status: root.status,
|
|
887
954
|
visibility: root.visibility,
|
|
955
|
+
// Language is Thread-uniform, so only the root emits it; the importer
|
|
956
|
+
// re-derives replies from their root. `translation_group` is an opaque
|
|
957
|
+
// shared key — importing the whole site keeps translated posts linked.
|
|
958
|
+
language: root.language ?? undefined,
|
|
959
|
+
translation_group: root.translationGroupId ?? undefined,
|
|
888
960
|
summary_text: getArchiveSummaryText(root) ?? undefined,
|
|
961
|
+
truncated: isPostTruncated(root) || undefined,
|
|
889
962
|
link_url: root.format === "link" && root.url ? root.url : undefined,
|
|
890
963
|
source_name: root.format === "quote" && root.title ? root.title : undefined,
|
|
891
964
|
source_url: root.format === "quote" && root.url ? root.url : undefined,
|
|
@@ -968,12 +1041,14 @@ async function buildThreadBundle(
|
|
|
968
1041
|
status: reply.status,
|
|
969
1042
|
visibility: reply.visibility,
|
|
970
1043
|
summary_text: getArchiveSummaryText(reply) ?? undefined,
|
|
1044
|
+
truncated: isPostTruncated(reply) || undefined,
|
|
971
1045
|
link_url: reply.format === "link" && reply.url ? reply.url : undefined,
|
|
972
1046
|
source_name:
|
|
973
1047
|
reply.format === "quote" && reply.title ? reply.title : undefined,
|
|
974
1048
|
source_url: reply.format === "quote" && reply.url ? reply.url : undefined,
|
|
975
1049
|
quote_text: reply.quoteText ?? undefined,
|
|
976
1050
|
rating: reply.rating ?? undefined,
|
|
1051
|
+
quiet_reply: reply.quietReply ? true : undefined,
|
|
977
1052
|
featured_at:
|
|
978
1053
|
reply.featuredAt !== null ? toISOString(reply.featuredAt) : undefined,
|
|
979
1054
|
pinned_at:
|
|
@@ -1326,11 +1401,7 @@ function buildExportedCollectionMetrics(
|
|
|
1326
1401
|
continue;
|
|
1327
1402
|
}
|
|
1328
1403
|
|
|
1329
|
-
const activityAt =
|
|
1330
|
-
post.lastActivityAt ??
|
|
1331
|
-
post.publishedAt ??
|
|
1332
|
-
post.updatedAt ??
|
|
1333
|
-
post.createdAt;
|
|
1404
|
+
const activityAt = getRootActivityAt(post);
|
|
1334
1405
|
const threadCollections = collectionsByThread.get(post.id) ?? [];
|
|
1335
1406
|
|
|
1336
1407
|
for (const collection of threadCollections) {
|
|
@@ -1358,13 +1429,21 @@ function buildExportedCollectionMetrics(
|
|
|
1358
1429
|
// Nav item resolution
|
|
1359
1430
|
// ---------------------------------------------------------------------------
|
|
1360
1431
|
|
|
1432
|
+
/**
|
|
1433
|
+
* Label to bake into the static site.
|
|
1434
|
+
*
|
|
1435
|
+
* Mirrors `getNavItemDisplayLabel`: a stored label is the author's own words
|
|
1436
|
+
* and wins, an empty one means the item follows what it points at. The export
|
|
1437
|
+
* has no i18n runtime, so built-in destinations fall back to their English
|
|
1438
|
+
* default rather than a translated one.
|
|
1439
|
+
*/
|
|
1361
1440
|
function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
|
|
1362
1441
|
if (item.label) return item.label;
|
|
1363
1442
|
if (item.systemKey) {
|
|
1364
1443
|
const definition = SYSTEM_NAV_KEYS[item.systemKey as SystemNavKey];
|
|
1365
1444
|
if (definition) return definition.defaultLabel;
|
|
1366
1445
|
}
|
|
1367
|
-
return item.label;
|
|
1446
|
+
return item.targetTitle?.trim() || item.label;
|
|
1368
1447
|
}
|
|
1369
1448
|
|
|
1370
1449
|
/**
|
|
@@ -1391,7 +1470,10 @@ function resolveNavItemUrl(
|
|
|
1391
1470
|
}
|
|
1392
1471
|
if (item.systemKey === "collections") return "/collections/";
|
|
1393
1472
|
if (item.systemKey === "archive") return "/archive/";
|
|
1394
|
-
|
|
1473
|
+
// `subscribe` addresses a Jant route, and the exported site has no page at
|
|
1474
|
+
// it. Rather than exporting a nav entry that 404s, it resolves to the feed
|
|
1475
|
+
// that page would have recommended — the same file `rss` points at.
|
|
1476
|
+
if (item.systemKey === "rss" || item.systemKey === "subscribe") {
|
|
1395
1477
|
return mainRssFeed === "featured" ? "/featured/index.xml" : "/index.xml";
|
|
1396
1478
|
}
|
|
1397
1479
|
return item.url;
|
|
@@ -1470,6 +1552,7 @@ function buildHugoToml(config: SiteConfig): string {
|
|
|
1470
1552
|
"[params]",
|
|
1471
1553
|
` description = "${escapeTomlString(config.siteDescription)}"`,
|
|
1472
1554
|
` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
|
|
1555
|
+
` archive_default_layout = "${escapeTomlString(config.archiveDefaultLayout)}"`,
|
|
1473
1556
|
` public_api_enabled = ${config.publicApiEnabled}`,
|
|
1474
1557
|
` rss_feeds_enabled = ${config.rssFeedsEnabled}`,
|
|
1475
1558
|
` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
|
|
@@ -1512,7 +1595,10 @@ function buildJantDataToml(
|
|
|
1512
1595
|
`site_name = "${escapeTomlString(config.siteName)}"`,
|
|
1513
1596
|
`site_description = "${escapeTomlString(config.siteDescription)}"`,
|
|
1514
1597
|
`site_language = "${escapeTomlString(config.siteLanguage)}"`,
|
|
1598
|
+
`multilingual_enabled = ${config.multilingualEnabled}`,
|
|
1599
|
+
`additional_languages = "${escapeTomlString(config.additionalLanguages.join(","))}"`,
|
|
1515
1600
|
`main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
|
|
1601
|
+
`archive_default_layout = "${escapeTomlString(config.archiveDefaultLayout)}"`,
|
|
1516
1602
|
`public_api_enabled = ${config.publicApiEnabled}`,
|
|
1517
1603
|
`rss_feeds_enabled = ${config.rssFeedsEnabled}`,
|
|
1518
1604
|
`show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
|
|
@@ -1552,10 +1638,12 @@ function buildJantDataToml(
|
|
|
1552
1638
|
// `settings` is authenticated-only and has no corresponding page in the
|
|
1553
1639
|
// static Hugo site — drop it at export time so it never shows up in nav.
|
|
1554
1640
|
if (item.systemKey === "settings") continue;
|
|
1641
|
+
// Both feed entries resolve to a feed file, so with feeds off both would
|
|
1642
|
+
// export a link to something that was never written.
|
|
1555
1643
|
if (
|
|
1556
1644
|
!config.rssFeedsEnabled &&
|
|
1557
1645
|
item.type === "system" &&
|
|
1558
|
-
item.systemKey
|
|
1646
|
+
isFeedNavKey(item.systemKey)
|
|
1559
1647
|
) {
|
|
1560
1648
|
continue;
|
|
1561
1649
|
}
|
|
@@ -1616,9 +1704,95 @@ function buildJantDataToml(
|
|
|
1616
1704
|
}
|
|
1617
1705
|
|
|
1618
1706
|
// ---------------------------------------------------------------------------
|
|
1619
|
-
// README + .gitignore
|
|
1707
|
+
// README + .gitignore + _redirects
|
|
1620
1708
|
// ---------------------------------------------------------------------------
|
|
1621
1709
|
|
|
1710
|
+
/**
|
|
1711
|
+
* Build the `static/_redirects` file that keeps existing feed subscribers
|
|
1712
|
+
* working after the site moves to this export.
|
|
1713
|
+
*
|
|
1714
|
+
* Jant serves its feeds under `/feed` (`/feed`, `/latest/feed`,
|
|
1715
|
+
* `/featured/feed`, `/archive/feed`, `/{collection}/feed`); Hugo serves the
|
|
1716
|
+
* same documents as `index.xml` inside each section. The two namespaces do
|
|
1717
|
+
* not overlap, so every subscriber's saved address 404s the moment the
|
|
1718
|
+
* exported site goes up.
|
|
1719
|
+
*
|
|
1720
|
+
* Hugo's `aliases:` mechanism cannot cover this the way it covers post URLs:
|
|
1721
|
+
* an alias page is a meta-refresh plus a script, and feed readers fetch and
|
|
1722
|
+
* parse XML without running either. Only an HTTP redirect reaches them, which
|
|
1723
|
+
* is host configuration rather than Hugo output — so the export emits the one
|
|
1724
|
+
* format two common static hosts read verbatim (Cloudflare Pages and Netlify).
|
|
1725
|
+
* Hosts that do not read `_redirects` ignore the file; the README carries the
|
|
1726
|
+
* rule for them to translate.
|
|
1727
|
+
*
|
|
1728
|
+
* @param mainRssFeed - The site's main feed setting, "latest" or "featured".
|
|
1729
|
+
* @param hasFeaturedSection - Whether the export emitted a featured section.
|
|
1730
|
+
* It is skipped when a post or collection already owns the `featured` slug,
|
|
1731
|
+
* and pointing a redirect at a feed that was never built would turn a live
|
|
1732
|
+
* subscription into a 404 rather than leaving it merely stale.
|
|
1733
|
+
* @returns The `_redirects` file body.
|
|
1734
|
+
* @example
|
|
1735
|
+
* buildFeedRedirects("latest", true); // "# ...\n/feed /index.xml 301\n..."
|
|
1736
|
+
*/
|
|
1737
|
+
function buildFeedRedirects(
|
|
1738
|
+
mainRssFeed: string,
|
|
1739
|
+
hasFeaturedSection: boolean,
|
|
1740
|
+
): string {
|
|
1741
|
+
const LATEST = "/index.xml";
|
|
1742
|
+
const FEATURED = "/featured/index.xml";
|
|
1743
|
+
// `/feed` follows the site's main-feed setting, mirroring `renderMainFeed`.
|
|
1744
|
+
// Without a featured section there is nothing to point it at, so it falls
|
|
1745
|
+
// back to the latest feed: a wider feed than the reader subscribed to, but
|
|
1746
|
+
// a live one.
|
|
1747
|
+
const main =
|
|
1748
|
+
mainRssFeed === "featured" && hasFeaturedSection ? FEATURED : LATEST;
|
|
1749
|
+
const featured = hasFeaturedSection ? FEATURED : LATEST;
|
|
1750
|
+
|
|
1751
|
+
const rules: [string, string][] = [
|
|
1752
|
+
["/feed", main],
|
|
1753
|
+
["/latest/feed", LATEST],
|
|
1754
|
+
["/featured/feed", featured],
|
|
1755
|
+
["/archive/feed", "/archive/index.xml"],
|
|
1756
|
+
];
|
|
1757
|
+
const legacy: [string, string][] = [
|
|
1758
|
+
["/feed/latest", LATEST],
|
|
1759
|
+
["/feed/all", LATEST],
|
|
1760
|
+
["/feed/featured", featured],
|
|
1761
|
+
["/feed/atom.xml", main],
|
|
1762
|
+
["/feed/latest/atom.xml", LATEST],
|
|
1763
|
+
["/feed/all/atom.xml", LATEST],
|
|
1764
|
+
["/feed/featured/atom.xml", featured],
|
|
1765
|
+
];
|
|
1766
|
+
|
|
1767
|
+
// The format only needs whitespace between the columns; padding to the
|
|
1768
|
+
// widest source path keeps the file readable for whoever has to translate
|
|
1769
|
+
// it into another host's redirect syntax.
|
|
1770
|
+
const width = Math.max(
|
|
1771
|
+
...[...rules, ...legacy].map(([from]) => from.length),
|
|
1772
|
+
"/:slug/feed".length,
|
|
1773
|
+
);
|
|
1774
|
+
const line = ([from, to]: [string, string]) =>
|
|
1775
|
+
`${from.padEnd(width)} ${to} 301`;
|
|
1776
|
+
|
|
1777
|
+
return `# Feed addresses moved in this export.
|
|
1778
|
+
#
|
|
1779
|
+
# Jant served its feeds under /feed; Hugo serves them as index.xml inside each
|
|
1780
|
+
# section. Cloudflare Pages and Netlify read this file and issue the redirects
|
|
1781
|
+
# below, so readers who subscribed to the old addresses keep receiving posts.
|
|
1782
|
+
# Other hosts ignore this file — see README.md for the same rules to translate
|
|
1783
|
+
# into your host's configuration.
|
|
1784
|
+
|
|
1785
|
+
${rules.map(line).join("\n")}
|
|
1786
|
+
|
|
1787
|
+
# Aliases the live site answered with a 308. They would die at the same moment.
|
|
1788
|
+
${legacy.map(line).join("\n")}
|
|
1789
|
+
|
|
1790
|
+
# Collection feeds, by slug. Collections are the only root-level sections that
|
|
1791
|
+
# emit Atom, so any other slug 404s here exactly as it did on the live site.
|
|
1792
|
+
${line(["/:slug/feed", "/:slug/index.xml"])}
|
|
1793
|
+
`;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1622
1796
|
function buildGitignore(): string {
|
|
1623
1797
|
return `# Hugo build output
|
|
1624
1798
|
public/
|
|
@@ -1636,7 +1810,90 @@ Thumbs.db
|
|
|
1636
1810
|
`;
|
|
1637
1811
|
}
|
|
1638
1812
|
|
|
1639
|
-
|
|
1813
|
+
/**
|
|
1814
|
+
* Render a two-column Markdown table with padded cells.
|
|
1815
|
+
*
|
|
1816
|
+
* The values differ per site, so building the rows by hand would leave the
|
|
1817
|
+
* committed README ragged for some exports and aligned for others.
|
|
1818
|
+
*
|
|
1819
|
+
* @param headers - The two column headers.
|
|
1820
|
+
* @param rows - Cell pairs; each cell is wrapped in backticks.
|
|
1821
|
+
* @returns The table as Markdown, without a trailing newline.
|
|
1822
|
+
* @example
|
|
1823
|
+
* renderMarkdownTable(["Jant", "This export"], [["/feed", "/index.xml"]]);
|
|
1824
|
+
*/
|
|
1825
|
+
function renderMarkdownTable(
|
|
1826
|
+
headers: [string, string],
|
|
1827
|
+
rows: [string, string][],
|
|
1828
|
+
): string {
|
|
1829
|
+
const cells = rows.map(
|
|
1830
|
+
([from, to]) => [`\`${from}\``, `\`${to}\``] as [string, string],
|
|
1831
|
+
);
|
|
1832
|
+
const widths: [number, number] = [
|
|
1833
|
+
Math.max(headers[0].length, ...cells.map((r) => r[0].length)),
|
|
1834
|
+
Math.max(headers[1].length, ...cells.map((r) => r[1].length)),
|
|
1835
|
+
];
|
|
1836
|
+
const line = (a: string, b: string) =>
|
|
1837
|
+
`| ${a.padEnd(widths[0])} | ${b.padEnd(widths[1])} |`;
|
|
1838
|
+
return [
|
|
1839
|
+
line(headers[0], headers[1]),
|
|
1840
|
+
`| ${"-".repeat(widths[0])} | ${"-".repeat(widths[1])} |`,
|
|
1841
|
+
...cells.map((r) => line(r[0], r[1])),
|
|
1842
|
+
].join("\n");
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Build the export's README.
|
|
1847
|
+
*
|
|
1848
|
+
* @param config - The exported site's configuration.
|
|
1849
|
+
* @param hasFeaturedSection - Whether a featured section was emitted, which
|
|
1850
|
+
* decides where the feed redirects point.
|
|
1851
|
+
* @returns Markdown for `README.md` at the root of the export.
|
|
1852
|
+
* @example
|
|
1853
|
+
* buildReadme(config, true); // "# My Site — Hugo Export\n..."
|
|
1854
|
+
*/
|
|
1855
|
+
function buildReadme(config: SiteConfig, hasFeaturedSection: boolean): string {
|
|
1856
|
+
const siteName = config.siteName;
|
|
1857
|
+
const featuredFeed = hasFeaturedSection
|
|
1858
|
+
? "/featured/index.xml"
|
|
1859
|
+
: "/index.xml";
|
|
1860
|
+
const mainFeed =
|
|
1861
|
+
config.mainRssFeed === "featured" ? featuredFeed : "/index.xml";
|
|
1862
|
+
// Nothing to say about feeds on a site that publishes none, and no
|
|
1863
|
+
// `_redirects` file was written for it either.
|
|
1864
|
+
const staticListing = config.rssFeedsEnabled
|
|
1865
|
+
? "\n _redirects — Feed redirects (see Feeds above)"
|
|
1866
|
+
: "";
|
|
1867
|
+
// The Notes list below says post URLs survive the move. Left alone, that
|
|
1868
|
+
// reads as a promise about every URL, and feed addresses are the exception.
|
|
1869
|
+
const feedNote = config.rssFeedsEnabled
|
|
1870
|
+
? "\n- Feed addresses are the exception: they move to `index.xml` and stay reachable only through `static/_redirects`. See [Feeds](#feeds)."
|
|
1871
|
+
: "";
|
|
1872
|
+
// Nothing to say about feeds on a site that publishes none, and no
|
|
1873
|
+
// `_redirects` file was written for it either.
|
|
1874
|
+
const feedRows: [string, string][] = [
|
|
1875
|
+
["/feed", mainFeed],
|
|
1876
|
+
["/latest/feed", "/index.xml"],
|
|
1877
|
+
["/featured/feed", featuredFeed],
|
|
1878
|
+
["/archive/feed", "/archive/index.xml"],
|
|
1879
|
+
["/{collection}/feed", "/{collection}/index.xml"],
|
|
1880
|
+
];
|
|
1881
|
+
const feedTable = renderMarkdownTable(["Jant", "This export"], feedRows);
|
|
1882
|
+
const feedsSection = config.rssFeedsEnabled
|
|
1883
|
+
? `## Feeds
|
|
1884
|
+
|
|
1885
|
+
The feed addresses changed. Jant served them under \`/feed\`; Hugo serves them as \`index.xml\` inside each section:
|
|
1886
|
+
|
|
1887
|
+
${feedTable}
|
|
1888
|
+
|
|
1889
|
+
A reader who is already subscribed holds one of the old addresses, and a feed reader that gets a 404 stops delivering posts. \`static/_redirects\` maps every old address to its new one with a 301. Cloudflare Pages and Netlify read that file as published; on any other host, translate its rules into that host's redirect configuration before you point the domain here.
|
|
1890
|
+
|
|
1891
|
+
Hugo's \`aliases:\` cannot cover this. An alias page redirects with a meta refresh and a script, and feed readers fetch XML without running either — only an HTTP redirect reaches them.
|
|
1892
|
+
|
|
1893
|
+
The **Subscribe** entry in the site navigation points at \`${mainFeed}\`. The exported site has no \`/subscribe\` page; that page belongs to the Jant runtime.
|
|
1894
|
+
|
|
1895
|
+
`
|
|
1896
|
+
: "";
|
|
1640
1897
|
return `# ${siteName} — Hugo Export
|
|
1641
1898
|
|
|
1642
1899
|
This is a static site exported from [Jant](https://github.com/jant-me/jant), ready to build with [Hugo](https://gohugo.io/).
|
|
@@ -1681,7 +1938,7 @@ hugo --minify
|
|
|
1681
1938
|
|
|
1682
1939
|
The output goes to the \`public/\` directory. Upload it to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, etc.).
|
|
1683
1940
|
|
|
1684
|
-
## Project structure
|
|
1941
|
+
${feedsSection}## Project structure
|
|
1685
1942
|
|
|
1686
1943
|
\`\`\`
|
|
1687
1944
|
hugo.toml — Site configuration (baseURL, title, theme, params)
|
|
@@ -1697,7 +1954,7 @@ content/
|
|
|
1697
1954
|
data/
|
|
1698
1955
|
jant.toml — Nav items, branding, display preferences, ordered collections directory
|
|
1699
1956
|
themes/jant/ — Bundled Hugo theme (overrideable via layouts/ at the site root)
|
|
1700
|
-
static/ — Copy files here to add them to the published site
|
|
1957
|
+
static/ — Copy files here to add them to the published site${staticListing}
|
|
1701
1958
|
\`\`\`
|
|
1702
1959
|
|
|
1703
1960
|
## Customizing
|
|
@@ -1723,7 +1980,7 @@ Safe to re-run; files already on disk are reused. Anything that fails to downloa
|
|
|
1723
1980
|
## Notes
|
|
1724
1981
|
|
|
1725
1982
|
- Each thread is a Hugo branch bundle. Replies live as nested leaf bundles with \`build.render = "never"\` so they do not produce standalone URLs; they render inside the thread page.
|
|
1726
|
-
- \`/{reply-slug}/\` URLs are preserved via \`aliases:\` on the root post, so old links still land on the right thread anchor
|
|
1983
|
+
- \`/{reply-slug}/\` URLs are preserved via \`aliases:\` on the root post, so old links still land on the right thread anchor.${feedNote}
|
|
1727
1984
|
- Media is emitted under \`static/media/{id}.ext\` and referenced from a flat \`media:\` array on each post. When a storage provider has a configured public URL (R2/S3/local proxy), the exporter links to the provider URL instead of re-bundling the bytes.
|
|
1728
1985
|
- Posts with \`draft: true\` in front matter are only built when you pass \`--buildDrafts\` to \`hugo\` / \`hugo serve\`.
|
|
1729
1986
|
`;
|
package/src/services/index.ts
CHANGED
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
createCollectionService,
|
|
22
22
|
type CollectionService,
|
|
23
23
|
} from "./collection.js";
|
|
24
|
+
import {
|
|
25
|
+
createSmartCollectionService,
|
|
26
|
+
type SmartCollectionService,
|
|
27
|
+
} from "./smart-collection.js";
|
|
24
28
|
import { createSearchService, type SearchService } from "./search.js";
|
|
25
29
|
import { createNavItemService, type NavItemService } from "./navigation.js";
|
|
26
30
|
import { createAuthService, type AuthService } from "./auth.js";
|
|
@@ -36,6 +40,7 @@ import {
|
|
|
36
40
|
type SiteProfileService,
|
|
37
41
|
} from "./site-profile.js";
|
|
38
42
|
import { createAboutPageService, type AboutPageService } from "./about-page.js";
|
|
43
|
+
import { createLanguageService, type LanguageService } from "./language.js";
|
|
39
44
|
import {
|
|
40
45
|
createUploadSessionService,
|
|
41
46
|
type UploadSessionService,
|
|
@@ -57,6 +62,7 @@ export interface Services {
|
|
|
57
62
|
media: MediaService;
|
|
58
63
|
uploads: UploadSessionService;
|
|
59
64
|
collections: CollectionService;
|
|
65
|
+
smartCollections: SmartCollectionService;
|
|
60
66
|
search: SearchService;
|
|
61
67
|
navItems: NavItemService;
|
|
62
68
|
auth: AuthService;
|
|
@@ -66,6 +72,7 @@ export interface Services {
|
|
|
66
72
|
siteMembers: SiteMemberService;
|
|
67
73
|
siteProfile: SiteProfileService;
|
|
68
74
|
aboutPage: AboutPageService;
|
|
75
|
+
language: LanguageService;
|
|
69
76
|
githubAppInstallations: GitHubAppInstallationsService;
|
|
70
77
|
telegram: TelegramService;
|
|
71
78
|
}
|
|
@@ -82,6 +89,8 @@ export function createServices(
|
|
|
82
89
|
siteResolutionMode?: "single-site" | "host-based";
|
|
83
90
|
enforceHostedMediaQuota?: boolean;
|
|
84
91
|
hostedControlPlane?: HostedControlPlaneClient | null;
|
|
92
|
+
/** Signing key for derived tokens, e.g. the account-deletion CSRF token. */
|
|
93
|
+
authSecret?: string;
|
|
85
94
|
},
|
|
86
95
|
): Services {
|
|
87
96
|
const databaseSchema = config?.schema ?? sqliteSchemaBundle;
|
|
@@ -89,7 +98,7 @@ export function createServices(
|
|
|
89
98
|
const site = createSiteService(db, databaseSchema);
|
|
90
99
|
const settings = createSettingsService(db, siteId, databaseSchema, dialect);
|
|
91
100
|
const paths = createPathService(db, siteId, databaseSchema);
|
|
92
|
-
const navItems = createNavItemService(db, siteId, databaseSchema);
|
|
101
|
+
const navItems = createNavItemService(db, siteId, databaseSchema, paths);
|
|
93
102
|
const posts = createPostService(
|
|
94
103
|
db,
|
|
95
104
|
{
|
|
@@ -100,12 +109,21 @@ export function createServices(
|
|
|
100
109
|
paths,
|
|
101
110
|
databaseSchema,
|
|
102
111
|
);
|
|
112
|
+
const smartCollections = createSmartCollectionService(
|
|
113
|
+
db,
|
|
114
|
+
siteId,
|
|
115
|
+
paths,
|
|
116
|
+
posts,
|
|
117
|
+
databaseSchema,
|
|
118
|
+
config?.slugIdLength ?? 5,
|
|
119
|
+
);
|
|
103
120
|
const collections = createCollectionService(
|
|
104
121
|
db,
|
|
105
122
|
siteId,
|
|
106
123
|
paths,
|
|
107
124
|
databaseSchema,
|
|
108
125
|
dialect,
|
|
126
|
+
smartCollections,
|
|
109
127
|
);
|
|
110
128
|
const media = createMediaService(db, siteId, databaseSchema, dialect, {
|
|
111
129
|
enforceHostedQuota: config?.enforceHostedMediaQuota ?? false,
|
|
@@ -121,6 +139,7 @@ export function createServices(
|
|
|
121
139
|
media,
|
|
122
140
|
uploads: createUploadSessionService(db, siteId, media, databaseSchema),
|
|
123
141
|
collections,
|
|
142
|
+
smartCollections,
|
|
124
143
|
search: createSearchService(rawQuery, siteId, dialect),
|
|
125
144
|
navItems,
|
|
126
145
|
auth: createAuthService(
|
|
@@ -128,13 +147,16 @@ export function createServices(
|
|
|
128
147
|
settings,
|
|
129
148
|
{
|
|
130
149
|
databaseDialect: dialect,
|
|
150
|
+
authSecret: config?.authSecret,
|
|
131
151
|
},
|
|
132
152
|
databaseSchema,
|
|
133
153
|
),
|
|
134
154
|
apiTokens: createApiTokenService(db, siteId, databaseSchema),
|
|
135
|
-
bootstrap: createBootstrapService(db, {
|
|
155
|
+
bootstrap: createBootstrapService(db, siteId, {
|
|
136
156
|
schema: databaseSchema,
|
|
157
|
+
databaseDialect: dialect,
|
|
137
158
|
bootstrapSite: config?.bootstrapSite,
|
|
159
|
+
siteResolutionMode: config?.siteResolutionMode,
|
|
138
160
|
}),
|
|
139
161
|
siteAdmin: createSiteAdminService(db, databaseSchema, dialect, {
|
|
140
162
|
siteResolutionMode: config?.siteResolutionMode,
|
|
@@ -156,7 +178,9 @@ export function createServices(
|
|
|
156
178
|
paths,
|
|
157
179
|
posts,
|
|
158
180
|
collections,
|
|
181
|
+
smartCollections,
|
|
159
182
|
}),
|
|
183
|
+
language: createLanguageService({ settings, posts, paths }),
|
|
160
184
|
githubAppInstallations: createGitHubAppInstallationsService(
|
|
161
185
|
db,
|
|
162
186
|
databaseSchema,
|
|
@@ -170,17 +194,24 @@ export type { SiteService } from "./site.js";
|
|
|
170
194
|
export type { PathService } from "./path.js";
|
|
171
195
|
export type { PostService, PostFilters, PostDeleteDeps } from "./post.js";
|
|
172
196
|
export type { CustomUrlService } from "./custom-url.js";
|
|
197
|
+
export type {
|
|
198
|
+
SmartCollectionService,
|
|
199
|
+
SmartCollectionViewer,
|
|
200
|
+
} from "./smart-collection.js";
|
|
173
201
|
export type { MediaService, MediaFilters } from "./media.js";
|
|
174
202
|
export type { UploadSessionService } from "./upload-session.js";
|
|
175
203
|
export type { CollectionService } from "./collection.js";
|
|
176
204
|
export type { SearchService, SearchResult, SearchOptions } from "./search.js";
|
|
177
205
|
export type { NavItemService } from "./navigation.js";
|
|
178
206
|
export type { AboutPageService, AboutPageStatus } from "./about-page.js";
|
|
207
|
+
export type { LanguageService, LanguageState } from "./language.js";
|
|
179
208
|
export type { AuthService, DeleteAccountDeps } from "./auth.js";
|
|
180
209
|
export type { ApiTokenService } from "./api-token.js";
|
|
181
210
|
export type {
|
|
182
211
|
BootstrapService,
|
|
183
|
-
|
|
212
|
+
CompleteSiteSetupData,
|
|
213
|
+
CompleteSiteSetupDeps,
|
|
214
|
+
ProvisionOwnerAccountData,
|
|
184
215
|
} from "./bootstrap.js";
|
|
185
216
|
export type { SiteMemberService } from "./site-member.js";
|
|
186
217
|
export type { SiteProfileService } from "./site-profile.js";
|