@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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dictionary-free transliteration helpers for slug generation.
|
|
3
|
+
*
|
|
4
|
+
* These converters cover the scripts limax handles poorly:
|
|
5
|
+
*
|
|
6
|
+
* - Korean hangul → Revised Romanization (limax strips hangul entirely)
|
|
7
|
+
* - Japanese kana → Hepburn romaji (limax mangles long vowels and digraphs)
|
|
8
|
+
*
|
|
9
|
+
* Both are pure algorithms over Unicode code points — no dictionaries — so
|
|
10
|
+
* they stay small enough for the Workers bundle and the lazy client chunk.
|
|
11
|
+
* Japanese kanji is intentionally out of scope: readings are context-dependent
|
|
12
|
+
* and need a morphological dictionary, which is why `slugify` falls back to a
|
|
13
|
+
* random ID for kanji-heavy Japanese titles instead of guessing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Matches hiragana, katakana, and the chōonpu (ー) long-vowel mark. */
|
|
17
|
+
export const KANA_RE = /[ぁ-ゖァ-ヺー]/u;
|
|
18
|
+
|
|
19
|
+
/** Matches precomposed hangul syllables and compatibility jamo. */
|
|
20
|
+
export const HANGUL_RE = /[가-힣ㄱ-ㆎ]/u;
|
|
21
|
+
|
|
22
|
+
/** Matches Han (CJK ideograph) characters. */
|
|
23
|
+
export const HAN_RE = /\p{Script=Han}/u;
|
|
24
|
+
|
|
25
|
+
// --- Korean: Revised Romanization ---------------------------------------
|
|
26
|
+
|
|
27
|
+
const HANGUL_SYLLABLE_BASE = 0xac00;
|
|
28
|
+
const HANGUL_SYLLABLE_END = 0xd7a3;
|
|
29
|
+
const VOWEL_COUNT = 21;
|
|
30
|
+
const FINAL_COUNT = 28;
|
|
31
|
+
|
|
32
|
+
// prettier-ignore
|
|
33
|
+
const RR_INITIALS = [
|
|
34
|
+
"g", "kk", "n", "d", "tt", "r", "m", "b", "pp", "s",
|
|
35
|
+
"ss", "", "j", "jj", "ch", "k", "t", "p", "h",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// prettier-ignore
|
|
39
|
+
const RR_VOWELS = [
|
|
40
|
+
"a", "ae", "ya", "yae", "eo", "e", "yeo", "ye", "o", "wa",
|
|
41
|
+
"wae", "oe", "yo", "u", "wo", "we", "wi", "yu", "eu", "ui", "i",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Finals use their phonetic (syllable-final) value, e.g. ㅅ → "t".
|
|
45
|
+
// prettier-ignore
|
|
46
|
+
const RR_FINALS = [
|
|
47
|
+
"", "k", "k", "k", "n", "n", "n", "t", "l", "k", "m", "l", "l", "l",
|
|
48
|
+
"p", "l", "m", "p", "p", "t", "t", "ng", "t", "t", "k", "t", "p", "t",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Liaison: when the next syllable starts with the silent initial ㅇ, the
|
|
52
|
+
// final consonant is pronounced as that syllable's initial instead
|
|
53
|
+
// (한국어 → han-gu-geo, not han-guk-eo). Each entry is
|
|
54
|
+
// [what remains as the final, what carries over as the next initial].
|
|
55
|
+
// ㅎ is dropped when carried (좋아 → joa); final ㅇ (ng) never carries.
|
|
56
|
+
// prettier-ignore
|
|
57
|
+
const RR_LIAISON: readonly (readonly [string, string])[] = [
|
|
58
|
+
["", ""], ["", "g"], ["", "kk"], ["k", "s"], ["", "n"], ["n", "j"],
|
|
59
|
+
["n", ""], ["", "d"], ["", "r"], ["l", "g"], ["l", "m"], ["l", "b"],
|
|
60
|
+
["l", "s"], ["l", "t"], ["l", "p"], ["l", ""], ["", "m"], ["", "b"],
|
|
61
|
+
["p", "s"], ["", "s"], ["", "ss"], ["ng", ""], ["", "j"], ["", "ch"],
|
|
62
|
+
["", "k"], ["", "t"], ["", "p"], ["", ""],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Standalone compatibility jamo (ㅋㅋㅋ, ㅠㅠ) — common in informal titles.
|
|
66
|
+
const COMPAT_JAMO: Record<string, string> = {
|
|
67
|
+
ㄱ: "g",
|
|
68
|
+
ㄲ: "kk",
|
|
69
|
+
ㄴ: "n",
|
|
70
|
+
ㄷ: "d",
|
|
71
|
+
ㄸ: "tt",
|
|
72
|
+
ㄹ: "r",
|
|
73
|
+
ㅁ: "m",
|
|
74
|
+
ㅂ: "b",
|
|
75
|
+
ㅃ: "pp",
|
|
76
|
+
ㅅ: "s",
|
|
77
|
+
ㅆ: "ss",
|
|
78
|
+
ㅇ: "ng",
|
|
79
|
+
ㅈ: "j",
|
|
80
|
+
ㅉ: "jj",
|
|
81
|
+
ㅊ: "ch",
|
|
82
|
+
ㅋ: "k",
|
|
83
|
+
ㅌ: "t",
|
|
84
|
+
ㅍ: "p",
|
|
85
|
+
ㅎ: "h",
|
|
86
|
+
ㅏ: "a",
|
|
87
|
+
ㅐ: "ae",
|
|
88
|
+
ㅑ: "ya",
|
|
89
|
+
ㅒ: "yae",
|
|
90
|
+
ㅓ: "eo",
|
|
91
|
+
ㅔ: "e",
|
|
92
|
+
ㅕ: "yeo",
|
|
93
|
+
ㅖ: "ye",
|
|
94
|
+
ㅗ: "o",
|
|
95
|
+
ㅘ: "wa",
|
|
96
|
+
ㅙ: "wae",
|
|
97
|
+
ㅚ: "oe",
|
|
98
|
+
ㅛ: "yo",
|
|
99
|
+
ㅜ: "u",
|
|
100
|
+
ㅝ: "wo",
|
|
101
|
+
ㅞ: "we",
|
|
102
|
+
ㅟ: "wi",
|
|
103
|
+
ㅠ: "yu",
|
|
104
|
+
ㅡ: "eu",
|
|
105
|
+
ㅢ: "ui",
|
|
106
|
+
ㅣ: "i",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
interface HangulSyllable {
|
|
110
|
+
initial: number;
|
|
111
|
+
vowel: number;
|
|
112
|
+
final: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function decomposeHangul(codePoint: number): HangulSyllable | null {
|
|
116
|
+
if (codePoint < HANGUL_SYLLABLE_BASE || codePoint > HANGUL_SYLLABLE_END) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const index = codePoint - HANGUL_SYLLABLE_BASE;
|
|
120
|
+
return {
|
|
121
|
+
initial: Math.floor(index / (VOWEL_COUNT * FINAL_COUNT)),
|
|
122
|
+
vowel: Math.floor(index / FINAL_COUNT) % VOWEL_COUNT,
|
|
123
|
+
final: index % FINAL_COUNT,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Transliterates Korean hangul to Latin using Revised Romanization.
|
|
129
|
+
*
|
|
130
|
+
* Handles precomposed syllables with basic liaison (a final consonant before
|
|
131
|
+
* a silent-ㅇ syllable carries over as its initial) and standalone
|
|
132
|
+
* compatibility jamo. Assimilation sound rules (e.g. 종로 → "Jongno") are
|
|
133
|
+
* intentionally skipped — slugs need recognizable output, not phonetic
|
|
134
|
+
* perfection. Non-hangul characters pass through unchanged.
|
|
135
|
+
*
|
|
136
|
+
* @param text - Input text, possibly mixing hangul with other characters
|
|
137
|
+
* @returns Text with hangul replaced by its romanization
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* hangulToLatin("안녕하세요"); // "annyeonghaseyo"
|
|
142
|
+
* hangulToLatin("한국어"); // "hangugeo" (liaison)
|
|
143
|
+
* hangulToLatin("개발 일지"); // "gaebal ilji"
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function hangulToLatin(text: string): string {
|
|
147
|
+
const chars = [...text];
|
|
148
|
+
let out = "";
|
|
149
|
+
let carried: string | null = null;
|
|
150
|
+
|
|
151
|
+
for (const [i, char] of chars.entries()) {
|
|
152
|
+
const syllable = decomposeHangul(char.codePointAt(0) ?? 0);
|
|
153
|
+
if (!syllable) {
|
|
154
|
+
carried = null;
|
|
155
|
+
out += COMPAT_JAMO[char] ?? char;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const initial = carried ?? RR_INITIALS[syllable.initial] ?? "";
|
|
160
|
+
carried = null;
|
|
161
|
+
const next = decomposeHangul(chars[i + 1]?.codePointAt(0) ?? 0);
|
|
162
|
+
let final = RR_FINALS[syllable.final] ?? "";
|
|
163
|
+
if (syllable.final !== 0 && next && RR_INITIALS[next.initial] === "") {
|
|
164
|
+
const [remains, carry] = RR_LIAISON[syllable.final] ?? ["", ""];
|
|
165
|
+
final = remains;
|
|
166
|
+
carried = carry;
|
|
167
|
+
}
|
|
168
|
+
out += initial + (RR_VOWELS[syllable.vowel] ?? "") + final;
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Japanese: Hepburn romaji -------------------------------------------
|
|
174
|
+
|
|
175
|
+
// prettier-ignore
|
|
176
|
+
const KANA_BASE: Record<string, string> = {
|
|
177
|
+
あ: "a", い: "i", う: "u", え: "e", お: "o",
|
|
178
|
+
か: "ka", き: "ki", く: "ku", け: "ke", こ: "ko",
|
|
179
|
+
さ: "sa", し: "shi", す: "su", せ: "se", そ: "so",
|
|
180
|
+
た: "ta", ち: "chi", つ: "tsu", て: "te", と: "to",
|
|
181
|
+
な: "na", に: "ni", ぬ: "nu", ね: "ne", の: "no",
|
|
182
|
+
は: "ha", ひ: "hi", ふ: "fu", へ: "he", ほ: "ho",
|
|
183
|
+
ま: "ma", み: "mi", む: "mu", め: "me", も: "mo",
|
|
184
|
+
や: "ya", ゆ: "yu", よ: "yo",
|
|
185
|
+
ら: "ra", り: "ri", る: "ru", れ: "re", ろ: "ro",
|
|
186
|
+
わ: "wa", ゐ: "i", ゑ: "e", を: "o", ん: "n",
|
|
187
|
+
が: "ga", ぎ: "gi", ぐ: "gu", げ: "ge", ご: "go",
|
|
188
|
+
ざ: "za", じ: "ji", ず: "zu", ぜ: "ze", ぞ: "zo",
|
|
189
|
+
だ: "da", ぢ: "ji", づ: "zu", で: "de", ど: "do",
|
|
190
|
+
ば: "ba", び: "bi", ぶ: "bu", べ: "be", ぼ: "bo",
|
|
191
|
+
ぱ: "pa", ぴ: "pi", ぷ: "pu", ぺ: "pe", ぽ: "po",
|
|
192
|
+
ゔ: "vu",
|
|
193
|
+
ぁ: "a", ぃ: "i", ぅ: "u", ぇ: "e", ぉ: "o",
|
|
194
|
+
ゃ: "ya", ゅ: "yu", ょ: "yo", ゎ: "wa",
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Digraphs and extended katakana combos (ファ, ティ, チェ, …). Keys are the
|
|
198
|
+
// hiragana-normalized two-character sequences.
|
|
199
|
+
// prettier-ignore
|
|
200
|
+
const KANA_COMBO: Record<string, string> = {
|
|
201
|
+
きゃ: "kya", きゅ: "kyu", きょ: "kyo",
|
|
202
|
+
ぎゃ: "gya", ぎゅ: "gyu", ぎょ: "gyo",
|
|
203
|
+
しゃ: "sha", しゅ: "shu", しょ: "sho",
|
|
204
|
+
じゃ: "ja", じゅ: "ju", じょ: "jo",
|
|
205
|
+
ちゃ: "cha", ちゅ: "chu", ちょ: "cho",
|
|
206
|
+
ぢゃ: "ja", ぢゅ: "ju", ぢょ: "jo",
|
|
207
|
+
にゃ: "nya", にゅ: "nyu", にょ: "nyo",
|
|
208
|
+
ひゃ: "hya", ひゅ: "hyu", ひょ: "hyo",
|
|
209
|
+
びゃ: "bya", びゅ: "byu", びょ: "byo",
|
|
210
|
+
ぴゃ: "pya", ぴゅ: "pyu", ぴょ: "pyo",
|
|
211
|
+
みゃ: "mya", みゅ: "myu", みょ: "myo",
|
|
212
|
+
りゃ: "rya", りゅ: "ryu", りょ: "ryo",
|
|
213
|
+
ふぁ: "fa", ふぃ: "fi", ふぇ: "fe", ふぉ: "fo",
|
|
214
|
+
てぃ: "ti", でぃ: "di", とぅ: "tu", どぅ: "du",
|
|
215
|
+
うぃ: "wi", うぇ: "we", うぉ: "wo",
|
|
216
|
+
ゔぁ: "va", ゔぃ: "vi", ゔぇ: "ve", ゔぉ: "vo",
|
|
217
|
+
ちぇ: "che", しぇ: "she", じぇ: "je",
|
|
218
|
+
つぁ: "tsa", つぃ: "tsi", つぇ: "tse", つぉ: "tso",
|
|
219
|
+
いぇ: "ye",
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const KATAKANA_START = 0x30a1;
|
|
223
|
+
const KATAKANA_END = 0x30f6;
|
|
224
|
+
const KATAKANA_TO_HIRAGANA_SHIFT = 0x60;
|
|
225
|
+
const SOKUON = "っ";
|
|
226
|
+
const CHOONPU = "ー";
|
|
227
|
+
|
|
228
|
+
function normalizeToHiragana(char: string): string {
|
|
229
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
230
|
+
if (codePoint >= KATAKANA_START && codePoint <= KATAKANA_END) {
|
|
231
|
+
return String.fromCodePoint(codePoint - KATAKANA_TO_HIRAGANA_SHIFT);
|
|
232
|
+
}
|
|
233
|
+
return char;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Transliterates Japanese kana to macron-less Hepburn romaji.
|
|
238
|
+
*
|
|
239
|
+
* Handles hiragana and katakana, digraphs (きゃ → "kya"), extended katakana
|
|
240
|
+
* loan combos (ファ → "fa"), sokuon gemination (まっちゃ → "matcha"), and
|
|
241
|
+
* drops the chōonpu long-vowel mark (ハンバーガー → "hanbaga"), matching how
|
|
242
|
+
* macrons are conventionally stripped in URLs. Kanji and other non-kana
|
|
243
|
+
* characters pass through unchanged.
|
|
244
|
+
*
|
|
245
|
+
* @param text - Input text, possibly mixing kana with other characters
|
|
246
|
+
* @returns Text with kana replaced by romaji
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* kanaToRomaji("カタカナタイトル"); // "katakanataitoru"
|
|
251
|
+
* kanaToRomaji("ハンバーガー"); // "hanbaga"
|
|
252
|
+
* kanaToRomaji("まっちゃ"); // "matcha"
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export function kanaToRomaji(text: string): string {
|
|
256
|
+
const chars = [...text].map(normalizeToHiragana);
|
|
257
|
+
let out = "";
|
|
258
|
+
let geminate = false;
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < chars.length; i++) {
|
|
261
|
+
const char = chars[i] ?? "";
|
|
262
|
+
if (char === SOKUON) {
|
|
263
|
+
geminate = true;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (char === CHOONPU) {
|
|
267
|
+
geminate = false;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const combo = KANA_COMBO[char + (chars[i + 1] ?? "")];
|
|
272
|
+
const romaji = combo ?? KANA_BASE[char];
|
|
273
|
+
if (combo) i++;
|
|
274
|
+
if (romaji === undefined) {
|
|
275
|
+
geminate = false;
|
|
276
|
+
out += char;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (geminate) {
|
|
280
|
+
geminate = false;
|
|
281
|
+
if (/^[^aeiou]/.test(romaji)) {
|
|
282
|
+
out += romaji.startsWith("ch") ? "t" : (romaji[0] ?? "");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
out += romaji;
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
package/src/lib/upload.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Shared file validation and storage key generation for upload routes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { MediaKind } from "../types/constants.js";
|
|
7
|
+
import type { ContentDisposition, MediaKind } from "../types/constants.js";
|
|
8
8
|
import { createEntityId } from "./ids.js";
|
|
9
9
|
|
|
10
10
|
const MEDIA_ROOT_PREFIX = "media";
|
|
@@ -161,16 +161,6 @@ const MIME_CATEGORY_MAP = new Map<string, MediaCategory>([
|
|
|
161
161
|
...CODE_MIME_TYPES.map((t) => [t, "code" as const] as const),
|
|
162
162
|
]);
|
|
163
163
|
|
|
164
|
-
/**
|
|
165
|
-
* Accept string for file inputs. Accepts all file types.
|
|
166
|
-
*
|
|
167
|
-
* @example
|
|
168
|
-
* ```ts
|
|
169
|
-
* <input type="file" accept={UPLOAD_ACCEPT} />
|
|
170
|
-
* ```
|
|
171
|
-
*/
|
|
172
|
-
export const UPLOAD_ACCEPT = "*/*";
|
|
173
|
-
|
|
174
164
|
export type MediaCategory =
|
|
175
165
|
| "image"
|
|
176
166
|
| "video"
|
|
@@ -183,7 +173,7 @@ export type MediaCategory =
|
|
|
183
173
|
| "3d"
|
|
184
174
|
| "code";
|
|
185
175
|
|
|
186
|
-
export type UploadContentDisposition =
|
|
176
|
+
export type UploadContentDisposition = ContentDisposition;
|
|
187
177
|
|
|
188
178
|
export interface StoredUploadPolicy {
|
|
189
179
|
contentDisposition: UploadContentDisposition;
|
|
@@ -326,10 +316,22 @@ export function imageExtensionForMimeType(contentType: string): string | null {
|
|
|
326
316
|
return IMAGE_MIME_EXTENSIONS[contentType] ?? null;
|
|
327
317
|
}
|
|
328
318
|
|
|
319
|
+
/** ISO BMFF `ftyp` major brands that mean a HEIC/HEIF still image. */
|
|
320
|
+
const HEIC_FTYP_BRANDS = new Set([
|
|
321
|
+
"heic",
|
|
322
|
+
"heix",
|
|
323
|
+
"hevc",
|
|
324
|
+
"hevx",
|
|
325
|
+
"mif1",
|
|
326
|
+
"msf1",
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
329
|
/**
|
|
330
330
|
* Identify an image format from its leading bytes (magic numbers), so a remote
|
|
331
331
|
* sideload can trust the actual content rather than the server's content-type
|
|
332
|
-
* header. Recognizes every {@link isAllowedSideloadImageType} format
|
|
332
|
+
* header. Recognizes every {@link isAllowedSideloadImageType} format, plus
|
|
333
|
+
* HEIC/HEIF — which no browser decodes natively and no sideload accepts, but
|
|
334
|
+
* which the composer converts before upload once it knows what it holds.
|
|
333
335
|
*
|
|
334
336
|
* @param bytes - The leading bytes of the file (≥ ~256 bytes recommended)
|
|
335
337
|
* @returns The detected image MIME type, or null if unrecognized
|
|
@@ -367,6 +369,7 @@ export function sniffImageMimeType(bytes: Uint8Array): string | null {
|
|
|
367
369
|
if (bytes.length >= 12 && readAscii(bytes, 4, 4) === "ftyp") {
|
|
368
370
|
const brand = readAscii(bytes, 8, 4);
|
|
369
371
|
if (brand === "avif" || brand === "avis") return "image/avif";
|
|
372
|
+
if (HEIC_FTYP_BRANDS.has(brand)) return "image/heic";
|
|
370
373
|
}
|
|
371
374
|
if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) {
|
|
372
375
|
return "image/bmp";
|
package/src/lib/url.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* URL Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import limax from "limax";
|
|
6
|
-
|
|
7
5
|
function normalizeSitePathname(pathname: string): string {
|
|
8
6
|
if (pathname === "/" || pathname === "") return "";
|
|
9
7
|
const trimmed = pathname.replace(/\/+$/, "");
|
|
@@ -134,25 +132,159 @@ export function toSameSitePath(url: string, siteOrigin: string): string | null {
|
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
* Any `scheme:` at the start. `mailto:` and `tel:` address something real but
|
|
136
|
+
* never a page here, and an unrecognized scheme must not be read as a path.
|
|
137
|
+
*/
|
|
138
|
+
const ANY_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
|
|
139
|
+
|
|
140
|
+
export interface InternalPathOptions {
|
|
141
|
+
/**
|
|
142
|
+
* Origins this site answers on. Pass the configured one *and* the origin of
|
|
143
|
+
* the current request: a hosted site is often reached on a custom domain the
|
|
144
|
+
* canonical URL knows nothing about.
|
|
145
|
+
*/
|
|
146
|
+
siteOrigins?: readonly string[];
|
|
147
|
+
/** Public site path prefix, such as `/blog`. */
|
|
148
|
+
sitePathPrefix?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Language URL prefixes the site serves, without slashes (`["en", "ja"]`).
|
|
151
|
+
* A leading one is removed, because `/en/about` and `/about` are the same
|
|
152
|
+
* page seen from two views.
|
|
153
|
+
*/
|
|
154
|
+
languagePrefixes?: readonly string[];
|
|
155
|
+
/** Keep `?query` and `#hash`. Off by default: they name a spot, not a page. */
|
|
156
|
+
keepQuery?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether a string is meant as an address rather than as search words.
|
|
141
161
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
162
|
+
* Deliberately narrow — a leading slash or a full `http(s)` URL. Anything
|
|
163
|
+
* looser would swallow real queries: a search for "about" is not a search for
|
|
164
|
+
* `/about`.
|
|
144
165
|
*
|
|
166
|
+
* @param input - What the author typed or pasted
|
|
167
|
+
* @returns True when the input should be resolved instead of searched
|
|
145
168
|
* @example
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* //
|
|
169
|
+
* looksLikeAddress("/about"); // true
|
|
170
|
+
* looksLikeAddress("https://example.com/about"); // true
|
|
171
|
+
* looksLikeAddress("about"); // false
|
|
172
|
+
*/
|
|
173
|
+
export function looksLikeAddress(input: string): boolean {
|
|
174
|
+
const trimmed = input.trim();
|
|
175
|
+
return trimmed.startsWith("/") || isFullUrl(trimmed);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert an address the author pasted into an internal app path.
|
|
149
180
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
181
|
+
* Accepts what a browser address bar would produce — a full URL on one of the
|
|
182
|
+
* site's own origins, or a path — and returns the path the router resolves
|
|
183
|
+
* against: no site path prefix, no language prefix, rooted at `/`. Returns
|
|
184
|
+
* `null` for anything that is not a page on this site, so callers can tell an
|
|
185
|
+
* external link apart from a bad one.
|
|
186
|
+
*
|
|
187
|
+
* Case is left alone; storage-level normalization belongs to the path
|
|
188
|
+
* registry, which lowercases on its own terms.
|
|
189
|
+
*
|
|
190
|
+
* A full URL has to sit inside the deployment to count. A typed path does not:
|
|
191
|
+
* `/about` on a site served from `/blog` is the About page, because that is
|
|
192
|
+
* what someone typing it means.
|
|
193
|
+
*
|
|
194
|
+
* @param input - A full URL, or a path
|
|
195
|
+
* @param options - The site's origins, prefixes, and what to keep
|
|
196
|
+
* @returns Internal path rooted at `/`, or `null` when the address is not ours
|
|
197
|
+
* @example
|
|
198
|
+
* toInternalPath("https://example.com/blog/en/about", {
|
|
199
|
+
* siteOrigins: ["https://example.com"],
|
|
200
|
+
* sitePathPrefix: "/blog",
|
|
201
|
+
* languagePrefixes: ["en"],
|
|
202
|
+
* }); // "/about"
|
|
203
|
+
* toInternalPath("https://other.com/about", { siteOrigins: ["https://example.com"] }); // null
|
|
153
204
|
*/
|
|
154
|
-
export function
|
|
155
|
-
|
|
205
|
+
export function toInternalPath(
|
|
206
|
+
input: string,
|
|
207
|
+
options: InternalPathOptions = {},
|
|
208
|
+
): string | null {
|
|
209
|
+
const value = input.trim();
|
|
210
|
+
if (!value) return null;
|
|
211
|
+
|
|
212
|
+
const {
|
|
213
|
+
siteOrigins = [],
|
|
214
|
+
sitePathPrefix = "",
|
|
215
|
+
languagePrefixes = [],
|
|
216
|
+
keepQuery = false,
|
|
217
|
+
} = options;
|
|
218
|
+
|
|
219
|
+
const isFull = isFullUrl(value);
|
|
220
|
+
let candidate: string | null = null;
|
|
221
|
+
if (isFull) {
|
|
222
|
+
for (const origin of siteOrigins) {
|
|
223
|
+
const path = toSameSitePath(value, origin);
|
|
224
|
+
if (path !== null) {
|
|
225
|
+
candidate = path;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// A full URL on a host we do not serve is somebody else's page.
|
|
230
|
+
if (candidate === null) return null;
|
|
231
|
+
} else if (
|
|
232
|
+
value.startsWith("//") ||
|
|
233
|
+
ANY_SCHEME_RE.test(value) ||
|
|
234
|
+
value.startsWith("#")
|
|
235
|
+
) {
|
|
236
|
+
return null;
|
|
237
|
+
} else {
|
|
238
|
+
candidate = value;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let url: URL;
|
|
242
|
+
try {
|
|
243
|
+
// A fixed base resolves `..`, percent-encoding, and stray backslashes the
|
|
244
|
+
// same way the browser that produced the address did.
|
|
245
|
+
url = new URL(candidate, "https://jant.invalid");
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const withoutSite = stripSitePathPrefix(
|
|
251
|
+
url.pathname,
|
|
252
|
+
normalizeSitePathPrefix(sitePathPrefix),
|
|
253
|
+
);
|
|
254
|
+
if (withoutSite === null && isFull) {
|
|
255
|
+
// A URL on our host but outside the deployment: another app, not a page.
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// A typed path that does not carry the prefix is already an internal one —
|
|
260
|
+
// an author writing `/about` on a site served from `/blog` means the About
|
|
261
|
+
// page, not a different site.
|
|
262
|
+
const internal = stripLanguagePrefix(
|
|
263
|
+
withoutSite ?? url.pathname,
|
|
264
|
+
languagePrefixes,
|
|
265
|
+
);
|
|
266
|
+
const trimmed =
|
|
267
|
+
internal.length > 1 ? internal.replace(/\/+$/, "") || "/" : internal;
|
|
268
|
+
|
|
269
|
+
return keepQuery ? `${trimmed}${url.search}${url.hash}` : trimmed;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Drop a leading `/en`-style segment when the site serves that language. */
|
|
273
|
+
function stripLanguagePrefix(
|
|
274
|
+
pathname: string,
|
|
275
|
+
languagePrefixes: readonly string[],
|
|
276
|
+
): string {
|
|
277
|
+
if (languagePrefixes.length === 0) return pathname;
|
|
278
|
+
|
|
279
|
+
const rest = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
280
|
+
const slash = rest.indexOf("/");
|
|
281
|
+
const first = (slash === -1 ? rest : rest.slice(0, slash)).toLowerCase();
|
|
282
|
+
if (!first) return pathname;
|
|
283
|
+
if (!languagePrefixes.some((prefix) => prefix.toLowerCase() === first)) {
|
|
284
|
+
return pathname;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return slash === -1 ? "/" : rest.slice(slash);
|
|
156
288
|
}
|
|
157
289
|
|
|
158
290
|
/**
|
|
@@ -183,6 +315,25 @@ export function extractDisplayDomain(url: string): string | null {
|
|
|
183
315
|
}
|
|
184
316
|
|
|
185
317
|
const SAFE_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
318
|
+
const SAFE_RICH_TEXT_HREF_PROTOCOLS = new Set([
|
|
319
|
+
...SAFE_URL_PROTOCOLS,
|
|
320
|
+
"tel:",
|
|
321
|
+
"sms:",
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
function sanitizeUrlWithProtocols(
|
|
325
|
+
url: string,
|
|
326
|
+
protocols: ReadonlySet<string>,
|
|
327
|
+
): string {
|
|
328
|
+
try {
|
|
329
|
+
const parsed = new URL(url, "https://placeholder.invalid");
|
|
330
|
+
// Relative URLs resolve against the placeholder and get https: — allow them
|
|
331
|
+
if (protocols.has(parsed.protocol)) return url;
|
|
332
|
+
return "";
|
|
333
|
+
} catch {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
186
337
|
|
|
187
338
|
/**
|
|
188
339
|
* Checks whether a string is a safe absolute URL.
|
|
@@ -231,14 +382,27 @@ export function isSafeAbsoluteUrl(url: string): boolean {
|
|
|
231
382
|
* ```
|
|
232
383
|
*/
|
|
233
384
|
export function sanitizeUrl(url: string): string {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
385
|
+
return sanitizeUrlWithProtocols(url, SAFE_URL_PROTOCOLS);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Sanitizes a rich-text link destination.
|
|
390
|
+
*
|
|
391
|
+
* In addition to safe web and email URLs, rich-text links may launch the
|
|
392
|
+
* device's phone or messaging app through `tel:` and `sms:`. Relative links
|
|
393
|
+
* remain valid, while executable and resource-only protocols are rejected.
|
|
394
|
+
*
|
|
395
|
+
* @param url - The link destination to sanitize
|
|
396
|
+
* @returns The original URL if safe for an anchor, or an empty string otherwise
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```ts
|
|
400
|
+
* sanitizeRichTextHref("sms:+15551234567"); // "sms:+15551234567"
|
|
401
|
+
* sanitizeRichTextHref("javascript:alert(1)"); // ""
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
export function sanitizeRichTextHref(url: string): string {
|
|
405
|
+
return sanitizeUrlWithProtocols(url, SAFE_RICH_TEXT_HREF_PROTOCOLS);
|
|
242
406
|
}
|
|
243
407
|
|
|
244
408
|
/**
|
package/src/lib/version.ts
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version and
|
|
3
|
-
*
|
|
4
|
-
* In Vite dev, `__JANT_DEV__` is replaced with `true` via Vite's `define` config.
|
|
5
|
-
* In production (wrangler/esbuild), the typeof check evaluates to false safely.
|
|
2
|
+
* Version and build asset paths
|
|
6
3
|
*
|
|
7
4
|
* `__JANT_VERSION__` is replaced by Vite's `define` during both dev and lib build.
|
|
8
5
|
*
|
|
9
6
|
* `__CLIENT_JS_FILE__` and `__CLIENT_AUTH_JS_FILE__` are content-addressed
|
|
10
7
|
* internal paths (e.g. `/_assets/client-HASH.js`) embedded by the Worker build
|
|
11
8
|
* from the Vite client manifest. Used only in production (IS_VITE_DEV=false).
|
|
9
|
+
* `__CLIENT_COMPOSE_PRELOAD__` lists every file the composer entry needs —
|
|
10
|
+
* the entry and the chunks it imports — for the layout's modulepreload links.
|
|
11
|
+
*
|
|
12
|
+
* The dev flag itself lives in `build-env.ts`, which client bundles can import
|
|
13
|
+
* without these Worker-only globals.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
declare const __JANT_DEV__: boolean | undefined;
|
|
15
16
|
declare const __JANT_VERSION__: string;
|
|
16
17
|
declare const __CLIENT_JS_FILE__: string;
|
|
17
18
|
declare const __CLIENT_AUTH_JS_FILE__: string;
|
|
19
|
+
declare const __CLIENT_COMPOSE_PRELOAD__: readonly string[];
|
|
18
20
|
declare const __CLIENT_CSS_FILE__: string;
|
|
21
|
+
declare const __CLIENT_AUTHOR_CSS_FILE__: string;
|
|
19
22
|
declare const __CLIENT_CJK_CSS_FILE__: string;
|
|
20
23
|
declare const __CLIENT_CJK_TC_CSS_FILE__: string;
|
|
21
24
|
declare const __CLIENT_CJK_JP_CSS_FILE__: string;
|
|
22
25
|
declare const __CLIENT_CJK_KR_CSS_FILE__: string;
|
|
23
26
|
|
|
24
|
-
export const IS_VITE_DEV =
|
|
25
|
-
typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
26
|
-
|
|
27
27
|
export const CORE_VERSION = __JANT_VERSION__;
|
|
28
28
|
export const CLIENT_JS_FILE = __CLIENT_JS_FILE__;
|
|
29
29
|
export const CLIENT_AUTH_JS_FILE = __CLIENT_AUTH_JS_FILE__;
|
|
30
|
+
export const CLIENT_COMPOSE_PRELOAD = __CLIENT_COMPOSE_PRELOAD__;
|
|
30
31
|
export const CLIENT_CSS_FILE = __CLIENT_CSS_FILE__;
|
|
32
|
+
export const CLIENT_AUTHOR_CSS_FILE = __CLIENT_AUTHOR_CSS_FILE__;
|
|
31
33
|
export const CLIENT_CJK_CSS_FILE = __CLIENT_CJK_CSS_FILE__;
|
|
32
34
|
export const CLIENT_CJK_TC_CSS_FILE = __CLIENT_CJK_TC_CSS_FILE__;
|
|
33
35
|
export const CLIENT_CJK_JP_CSS_FILE = __CLIENT_CJK_JP_CSS_FILE__;
|