@jant/core 0.6.14 → 0.6.16
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/bin/commands/import-site.js +88 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +82 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +177 -95
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +24 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +47 -8
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +224 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -402,6 +402,12 @@ describe("tiptapJsonToMarkdown", () => {
|
|
|
402
402
|
const md = "Body copy[^1]\n\n[^1]: Footnote body";
|
|
403
403
|
expect(roundtrip(md)).toBe(md);
|
|
404
404
|
});
|
|
405
|
+
|
|
406
|
+
it("canonicalizes inline footnotes as references plus definitions", () => {
|
|
407
|
+
expect(roundtrip("Body^[Inline **bold** note.]")).toBe(
|
|
408
|
+
"Body[^1]\n\n[^1]: Inline **bold** note.",
|
|
409
|
+
);
|
|
410
|
+
});
|
|
405
411
|
});
|
|
406
412
|
|
|
407
413
|
describe("edge cases", () => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { hangulToLatin, kanaToRomaji } from "../translit.js";
|
|
3
|
+
|
|
4
|
+
describe("hangulToLatin", () => {
|
|
5
|
+
it("romanizes basic syllables", () => {
|
|
6
|
+
expect(hangulToLatin("안녕하세요")).toBe("annyeonghaseyo");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("applies liaison before silent-ㅇ syllables", () => {
|
|
10
|
+
expect(hangulToLatin("한국어")).toBe("hangugeo");
|
|
11
|
+
expect(hangulToLatin("일지")).toBe("ilji");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("drops carried ㅎ in liaison", () => {
|
|
15
|
+
expect(hangulToLatin("좋아")).toBe("joa");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("splits compound finals in liaison", () => {
|
|
19
|
+
expect(hangulToLatin("읽어")).toBe("ilgeo");
|
|
20
|
+
expect(hangulToLatin("값이")).toBe("gapsi");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("keeps final ㅇ as ng before vowels", () => {
|
|
24
|
+
expect(hangulToLatin("강아지")).toBe("gangaji");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("uses phonetic finals", () => {
|
|
28
|
+
expect(hangulToLatin("옷")).toBe("ot");
|
|
29
|
+
expect(hangulToLatin("부엌")).toBe("bueok");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("preserves spaces and non-hangul characters", () => {
|
|
33
|
+
expect(hangulToLatin("개발 일지 3일차")).toBe("gaebal ilji 3ilcha");
|
|
34
|
+
expect(hangulToLatin("React 공부")).toBe("React gongbu");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("romanizes standalone compatibility jamo", () => {
|
|
38
|
+
expect(hangulToLatin("ㅋㅋㅋ")).toBe("kkk");
|
|
39
|
+
expect(hangulToLatin("ㅠㅠ")).toBe("yuyu");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("kanaToRomaji", () => {
|
|
44
|
+
it("romanizes hiragana", () => {
|
|
45
|
+
expect(kanaToRomaji("こんにちは")).toBe("konnichiha");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("romanizes katakana", () => {
|
|
49
|
+
expect(kanaToRomaji("カタカナタイトル")).toBe("katakanataitoru");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("drops the chōonpu long-vowel mark", () => {
|
|
53
|
+
expect(kanaToRomaji("ハンバーガー")).toBe("hanbaga");
|
|
54
|
+
expect(kanaToRomaji("ラーメン")).toBe("ramen");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("geminates consonants after sokuon", () => {
|
|
58
|
+
expect(kanaToRomaji("がっこう")).toBe("gakkou");
|
|
59
|
+
expect(kanaToRomaji("きっぷ")).toBe("kippu");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses tch for sokuon before chi", () => {
|
|
63
|
+
expect(kanaToRomaji("まっちゃ")).toBe("matcha");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles digraphs", () => {
|
|
67
|
+
expect(kanaToRomaji("きょうと")).toBe("kyouto");
|
|
68
|
+
expect(kanaToRomaji("しゃしん")).toBe("shashin");
|
|
69
|
+
expect(kanaToRomaji("じゅんび")).toBe("junbi");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles extended katakana combos", () => {
|
|
73
|
+
expect(kanaToRomaji("ファン")).toBe("fan");
|
|
74
|
+
expect(kanaToRomaji("パーティー")).toBe("pati");
|
|
75
|
+
expect(kanaToRomaji("チェック")).toBe("chekku");
|
|
76
|
+
expect(kanaToRomaji("ヴァイオリン")).toBe("vaiorin");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("passes non-kana characters through unchanged", () => {
|
|
80
|
+
expect(kanaToRomaji("東京タワー")).toBe("東京tawa");
|
|
81
|
+
expect(kanaToRomaji("abc カフェ")).toBe("abc kafe");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("ignores a dangling sokuon", () => {
|
|
85
|
+
expect(kanaToRomaji("あっ")).toBe("a");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
isFullUrl,
|
|
6
6
|
isSafeAbsoluteUrl,
|
|
7
7
|
normalizePath,
|
|
8
|
+
sanitizeRichTextHref,
|
|
9
|
+
sanitizeUrl,
|
|
8
10
|
stripSitePathPrefix,
|
|
9
11
|
slugify,
|
|
10
12
|
toSameSitePath,
|
|
@@ -211,6 +213,22 @@ describe("isSafeAbsoluteUrl", () => {
|
|
|
211
213
|
});
|
|
212
214
|
});
|
|
213
215
|
|
|
216
|
+
describe("URL sanitizers", () => {
|
|
217
|
+
it("keeps action protocols scoped to rich-text links", () => {
|
|
218
|
+
expect(sanitizeRichTextHref("tel:+15551234567")).toBe("tel:+15551234567");
|
|
219
|
+
expect(sanitizeRichTextHref("sms:xxxxx@xx.com")).toBe("sms:xxxxx@xx.com");
|
|
220
|
+
|
|
221
|
+
expect(sanitizeUrl("tel:+15551234567")).toBe("");
|
|
222
|
+
expect(sanitizeUrl("sms:xxxxx@xx.com")).toBe("");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("rejects executable protocols in every context", () => {
|
|
226
|
+
expect(sanitizeRichTextHref("javascript:alert(1)")).toBe("");
|
|
227
|
+
expect(sanitizeRichTextHref("data:text/html,<h1>Hi</h1>")).toBe("");
|
|
228
|
+
expect(sanitizeUrl("javascript:alert(1)")).toBe("");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
214
232
|
describe("slugify", () => {
|
|
215
233
|
it("converts text to lowercase hyphenated slug", () => {
|
|
216
234
|
expect(slugify("Hello World")).toBe("hello-world");
|
|
@@ -257,6 +275,41 @@ describe("slugify", () => {
|
|
|
257
275
|
it("handles CJK characters with spaces", () => {
|
|
258
276
|
expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
|
|
259
277
|
});
|
|
278
|
+
|
|
279
|
+
it("romanizes Korean titles", () => {
|
|
280
|
+
expect(slugify("안녕하세요 세계")).toBe("annyeonghaseyo-segye");
|
|
281
|
+
expect(slugify("개발 일지 3일차")).toBe("gaebal-ilji-3ilcha");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("romanizes kana-only Japanese titles", () => {
|
|
285
|
+
expect(slugify("カタカナタイトル")).toBe("katakanataitoru");
|
|
286
|
+
expect(slugify("ハンバーガー")).toBe("hanbaga");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("keeps Latin words in mostly-kana Japanese titles", () => {
|
|
290
|
+
expect(slugify("React入門ガイド")).toBe("react-gaido");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("returns empty for kanji-heavy Japanese titles instead of garbling them", () => {
|
|
294
|
+
// Kanji carries the meaning; without a reading dictionary any output
|
|
295
|
+
// would be gibberish, so callers should fall back to a random ID.
|
|
296
|
+
expect(slugify("日本語のタイトルです")).toBe("");
|
|
297
|
+
expect(slugify("東京タワーに行った")).toBe("");
|
|
298
|
+
expect(slugify("こんにちは世界")).toBe("");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("returns empty for scripts with no usable transliteration", () => {
|
|
302
|
+
expect(slugify("שלום עולם")).toBe("");
|
|
303
|
+
expect(slugify("สวัสดีชาวโลก")).toBe("");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns empty when most of the title is untransliterable", () => {
|
|
307
|
+
expect(slugify("שלום world")).toBe("");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("returns empty for emoji-only titles", () => {
|
|
311
|
+
expect(slugify("🎉🎉")).toBe("");
|
|
312
|
+
});
|
|
260
313
|
});
|
|
261
314
|
|
|
262
315
|
describe("stripSitePathPrefix", () => {
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
SearchResult,
|
|
21
21
|
Post,
|
|
22
22
|
} from "../../types.js";
|
|
23
|
+
import { renderTiptapJson } from "../tiptap-render.js";
|
|
23
24
|
const EMPTY_CTX: MediaContext = {};
|
|
24
25
|
const CTX_WITH_URLS: MediaContext = {
|
|
25
26
|
r2PublicUrl: "https://cdn.example.com",
|
|
@@ -271,6 +272,58 @@ describe("toPostView", () => {
|
|
|
271
272
|
);
|
|
272
273
|
});
|
|
273
274
|
|
|
275
|
+
it("places the summary boundary after a footnote repeated in the hidden tail", () => {
|
|
276
|
+
const firstText = "A".repeat(300);
|
|
277
|
+
const secondText = "B".repeat(300);
|
|
278
|
+
const body = JSON.stringify({
|
|
279
|
+
type: "doc",
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "paragraph",
|
|
283
|
+
content: [
|
|
284
|
+
{ type: "text", text: firstText },
|
|
285
|
+
{ type: "footnoteReference", attrs: { label: "shared" } },
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: "paragraph",
|
|
290
|
+
content: [
|
|
291
|
+
{ type: "text", text: secondText },
|
|
292
|
+
{ type: "footnoteReference", attrs: { label: "shared" } },
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
type: "footnoteDefinition",
|
|
297
|
+
attrs: { label: "shared" },
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: "paragraph",
|
|
301
|
+
content: [{ type: "text", text: "Shared definition" }],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
});
|
|
307
|
+
const bodyHtml = renderTiptapJson(body, { namespace: UUID_1 });
|
|
308
|
+
const view = toPostView(
|
|
309
|
+
makePostWithMedia({ title: "Article", body, bodyHtml }),
|
|
310
|
+
EMPTY_CTX,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(view.summaryHasMore).toBe(true);
|
|
314
|
+
const markerIndex = view.bodyHtml?.indexOf('<span id="continue"></span>');
|
|
315
|
+
expect(markerIndex).toBeGreaterThan(
|
|
316
|
+
view.bodyHtml?.indexOf(firstText) ?? -1,
|
|
317
|
+
);
|
|
318
|
+
expect(markerIndex).toBeLessThan(view.bodyHtml?.indexOf(secondText) ?? -1);
|
|
319
|
+
expect(view.bodyHtml).toContain('href="#fnref-1jp3895hp1ag7-1-2"');
|
|
320
|
+
expect(view.bodyHtml).not.toContain("footnote-document");
|
|
321
|
+
expect(view.bodyHtml?.match(/<ol class="footnote-list">/g)).toHaveLength(1);
|
|
322
|
+
expect(view.bodyHtml?.indexOf("Shared definition")).toBeGreaterThan(
|
|
323
|
+
view.bodyHtml?.indexOf(secondText) ?? -1,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
274
327
|
it("does not attach summaryHtml for short untitled notes", () => {
|
|
275
328
|
const body = JSON.stringify({
|
|
276
329
|
type: "doc",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { Bindings } from "../../types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { POST_BODY_HTML_VERSION } from "../post-body-html.js";
|
|
4
|
+
import {
|
|
5
|
+
normalizeWorkerCacheKeyUrl,
|
|
6
|
+
withWorkerResponseCache,
|
|
7
|
+
} from "../worker-response-cache.js";
|
|
4
8
|
|
|
5
9
|
interface StoredResponse {
|
|
6
10
|
body: Uint8Array;
|
|
@@ -59,6 +63,19 @@ function createCloudflareBindings(overrides: Partial<Bindings> = {}): Bindings {
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
describe("withWorkerResponseCache", () => {
|
|
66
|
+
it("versions cache keys with the current body HTML contract", () => {
|
|
67
|
+
const url = new URL(
|
|
68
|
+
normalizeWorkerCacheKeyUrl(
|
|
69
|
+
"https://example.com/feed?__jant_body_html=old&utm_source=test",
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(url.searchParams.get("__jant_body_html")).toBe(
|
|
74
|
+
String(POST_BODY_HTML_VERSION),
|
|
75
|
+
);
|
|
76
|
+
expect(url.searchParams.has("utm_source")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
62
79
|
it("caches anonymous phase-one feed responses and normalizes tracking params", async () => {
|
|
63
80
|
const cache = createMemoryCache();
|
|
64
81
|
const bindings = createCloudflareBindings();
|
package/src/lib/api-settings.ts
CHANGED
|
@@ -1,31 +1,258 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_FIELDS,
|
|
3
|
+
type Bindings,
|
|
4
|
+
type ConfigEditorDefinition,
|
|
5
|
+
type ConfigEditorFieldState,
|
|
6
|
+
type ConfigEditorKey,
|
|
7
|
+
type ConfigEditorResettableKey,
|
|
8
|
+
type ConfigEditorVisibleKey,
|
|
9
|
+
type ConfigKey,
|
|
10
|
+
} from "../types.js";
|
|
11
|
+
import { getSupportedLocaleEntries } from "../i18n/supported-locales.js";
|
|
2
12
|
import { SETTINGS_KEYS } from "./constants.js";
|
|
3
|
-
import {
|
|
13
|
+
import { getEnvString } from "./env.js";
|
|
14
|
+
import { normalizeEditableSettingValue } from "./schemas.js";
|
|
15
|
+
import { getTimeZoneOptions } from "./timezones.js";
|
|
4
16
|
|
|
5
17
|
export const demoLockedSettingKeys = new Set<ConfigKey>(["NOINDEX"]);
|
|
6
18
|
|
|
7
|
-
/** Config keys
|
|
19
|
+
/** Config keys explicitly approved for runtime editing. */
|
|
8
20
|
export const editableSettingKeys = Object.entries(CONFIG_FIELDS)
|
|
21
|
+
.filter(([, field]) => "editor" in field)
|
|
22
|
+
.map(([key]) => key as ConfigEditorKey);
|
|
23
|
+
|
|
24
|
+
/** Safe settings shown in Config Editor, including dedicated-page links. */
|
|
25
|
+
export const configEditorVisibleKeys = Object.entries(CONFIG_FIELDS)
|
|
26
|
+
.filter(([, field]) => "editor" in field || "configEditorLink" in field)
|
|
27
|
+
.map(([key]) => key as ConfigEditorVisibleKey);
|
|
28
|
+
|
|
29
|
+
/** Config Editor keys whose DB override can be safely removed directly. */
|
|
30
|
+
export const resettableConfigEditorKeys = Object.entries(CONFIG_FIELDS)
|
|
9
31
|
.filter(
|
|
10
|
-
([, field]) =>
|
|
32
|
+
([, field]) =>
|
|
33
|
+
"editor" in field ||
|
|
34
|
+
("configEditorLink" in field &&
|
|
35
|
+
"resettable" in field.configEditorLink &&
|
|
36
|
+
field.configEditorLink.resettable === true),
|
|
11
37
|
)
|
|
12
|
-
.map(([key]) => key as
|
|
38
|
+
.map(([key]) => key as ConfigEditorResettableKey);
|
|
39
|
+
|
|
40
|
+
export function isEditableSettingKey(key: string): key is ConfigEditorKey {
|
|
41
|
+
return editableSettingKeys.includes(key as ConfigEditorKey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isResettableConfigEditorKey(
|
|
45
|
+
key: string,
|
|
46
|
+
): key is ConfigEditorResettableKey {
|
|
47
|
+
return resettableConfigEditorKeys.includes(key as ConfigEditorResettableKey);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function tryNormalizeResolvedSettingValue(
|
|
51
|
+
key: ConfigEditorKey,
|
|
52
|
+
value: string,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
try {
|
|
55
|
+
return normalizeEditableSettingValue(key, value);
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getEditableSettingFallbackValue(
|
|
62
|
+
key: ConfigEditorKey,
|
|
63
|
+
env?: Bindings,
|
|
64
|
+
allSettings: Record<string, string> = {},
|
|
65
|
+
): string {
|
|
66
|
+
const field = CONFIG_FIELDS[key];
|
|
67
|
+
const envKeys = "envKeys" in field ? field.envKeys : undefined;
|
|
68
|
+
const envValue = env ? getEnvString(env, ...(envKeys ?? [])) : undefined;
|
|
69
|
+
if (envValue) {
|
|
70
|
+
const normalizedEnv = tryNormalizeResolvedSettingValue(key, envValue);
|
|
71
|
+
if (normalizedEnv !== undefined) return normalizedEnv;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalizedDefault = tryNormalizeResolvedSettingValue(
|
|
75
|
+
key,
|
|
76
|
+
field.defaultValue,
|
|
77
|
+
);
|
|
78
|
+
if (normalizedDefault !== undefined) return normalizedDefault;
|
|
79
|
+
|
|
80
|
+
if ("fallbackKey" in field && field.fallbackKey) {
|
|
81
|
+
return getEditableSettingValue(allSettings, field.fallbackKey, env);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error(`Missing valid Config Editor fallback for ${key}`);
|
|
85
|
+
}
|
|
13
86
|
|
|
14
87
|
export function getEditableSettingValue(
|
|
15
88
|
allSettings: Record<string, string>,
|
|
16
|
-
key:
|
|
89
|
+
key: ConfigEditorKey,
|
|
90
|
+
env?: Bindings,
|
|
17
91
|
): string {
|
|
18
|
-
const
|
|
19
|
-
|
|
92
|
+
const fallbackValue = getEditableSettingFallbackValue(key, env, allSettings);
|
|
93
|
+
const value = Object.hasOwn(allSettings, key)
|
|
94
|
+
? allSettings[key]
|
|
95
|
+
: fallbackValue;
|
|
96
|
+
return (
|
|
97
|
+
tryNormalizeResolvedSettingValue(key, value ?? fallbackValue) ??
|
|
98
|
+
fallbackValue
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getEditorConstraints(
|
|
103
|
+
key: ConfigEditorKey,
|
|
104
|
+
definition: ConfigEditorDefinition,
|
|
105
|
+
value: string,
|
|
106
|
+
fallbackValue: string,
|
|
107
|
+
): Pick<
|
|
108
|
+
ConfigEditorFieldState,
|
|
109
|
+
"maxLength" | "min" | "max" | "step" | "options"
|
|
110
|
+
> {
|
|
111
|
+
switch (definition.type) {
|
|
112
|
+
case "boolean":
|
|
113
|
+
return {};
|
|
114
|
+
case "string":
|
|
115
|
+
return { maxLength: definition.maxLength };
|
|
116
|
+
case "number":
|
|
117
|
+
return {
|
|
118
|
+
min: definition.min,
|
|
119
|
+
max: definition.max,
|
|
120
|
+
step: definition.step,
|
|
121
|
+
};
|
|
122
|
+
case "enum":
|
|
123
|
+
if (definition.options) return { options: definition.options };
|
|
124
|
+
if (definition.optionsSource === "contentLanguage") {
|
|
125
|
+
const options = getSupportedLocaleEntries().map((entry) => entry.tag);
|
|
126
|
+
for (const candidate of [fallbackValue, value]) {
|
|
127
|
+
if (candidate && !options.includes(candidate))
|
|
128
|
+
options.push(candidate);
|
|
129
|
+
}
|
|
130
|
+
return { options };
|
|
131
|
+
}
|
|
132
|
+
if (definition.optionsSource === "timeZone") {
|
|
133
|
+
const options = getTimeZoneOptions(value).map((entry) => entry.value);
|
|
134
|
+
for (const candidate of [fallbackValue, value]) {
|
|
135
|
+
if (candidate && !options.includes(candidate))
|
|
136
|
+
options.push(candidate);
|
|
137
|
+
}
|
|
138
|
+
return { options };
|
|
139
|
+
}
|
|
140
|
+
return { options: [value] };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildLinkedConfigEditorField(
|
|
145
|
+
allSettings: Record<string, string>,
|
|
146
|
+
env: Bindings,
|
|
147
|
+
key: ConfigEditorVisibleKey,
|
|
148
|
+
): ConfigEditorFieldState {
|
|
149
|
+
const field = CONFIG_FIELDS[key];
|
|
150
|
+
if (!("configEditorLink" in field)) {
|
|
151
|
+
throw new Error(`Missing Config Editor link metadata for ${key}`);
|
|
152
|
+
}
|
|
153
|
+
const definition = field.configEditorLink;
|
|
154
|
+
const resettable =
|
|
155
|
+
"resettable" in definition && definition.resettable === true;
|
|
156
|
+
const modified = Object.hasOwn(allSettings, key);
|
|
157
|
+
const storedValue = allSettings[key] ?? "";
|
|
158
|
+
|
|
159
|
+
if (definition.display === "configured") {
|
|
160
|
+
const envKeys = "envKeys" in field ? field.envKeys : [];
|
|
161
|
+
const fallbackRawValue =
|
|
162
|
+
getEnvString(env, ...envKeys) || field.defaultValue;
|
|
163
|
+
const effectiveValue = modified ? storedValue : fallbackRawValue;
|
|
164
|
+
return {
|
|
165
|
+
key,
|
|
166
|
+
mode: "link",
|
|
167
|
+
type: definition.type,
|
|
168
|
+
value: effectiveValue.trim() ? "true" : "false",
|
|
169
|
+
fallbackValue: fallbackRawValue.trim() ? "true" : "false",
|
|
170
|
+
modified,
|
|
171
|
+
locked: false,
|
|
172
|
+
settingsPath: definition.settingsPath,
|
|
173
|
+
display: definition.display,
|
|
174
|
+
...(resettable && { resettable: true }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fieldEnvKeys = "envKeys" in field ? field.envKeys : [];
|
|
179
|
+
let fallbackValue: string =
|
|
180
|
+
("fallbackValue" in definition ? definition.fallbackValue : undefined) ??
|
|
181
|
+
getEnvString(env, ...fieldEnvKeys) ??
|
|
182
|
+
field.defaultValue;
|
|
183
|
+
if ("fallbackKey" in definition && definition.fallbackKey) {
|
|
184
|
+
const fallbackField = CONFIG_FIELDS[definition.fallbackKey];
|
|
185
|
+
const envKeys = "envKeys" in fallbackField ? fallbackField.envKeys : [];
|
|
186
|
+
fallbackValue = getEnvString(env, ...envKeys) || fallbackField.defaultValue;
|
|
187
|
+
}
|
|
188
|
+
const value = storedValue.trim() || fallbackValue;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
key,
|
|
192
|
+
mode: "link",
|
|
193
|
+
type: definition.type,
|
|
194
|
+
value: definition.type === "boolean" ? String(value === "true") : value,
|
|
195
|
+
fallbackValue,
|
|
196
|
+
modified,
|
|
197
|
+
locked: false,
|
|
198
|
+
settingsPath: definition.settingsPath,
|
|
199
|
+
display: definition.display,
|
|
200
|
+
...(resettable && { resettable: true }),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function buildConfigEditorFields(
|
|
205
|
+
allSettings: Record<string, string>,
|
|
206
|
+
env: Bindings,
|
|
207
|
+
demoMode: boolean,
|
|
208
|
+
): ConfigEditorFieldState[] {
|
|
209
|
+
return configEditorVisibleKeys.map((key) => {
|
|
210
|
+
if ("configEditorLink" in CONFIG_FIELDS[key]) {
|
|
211
|
+
return buildLinkedConfigEditorField(allSettings, env, key);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!isEditableSettingKey(key)) {
|
|
215
|
+
throw new Error(`Missing Config Editor metadata for ${key}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const definition: ConfigEditorDefinition = CONFIG_FIELDS[key].editor;
|
|
219
|
+
const fallbackValue = getEditableSettingFallbackValue(
|
|
220
|
+
key,
|
|
221
|
+
env,
|
|
222
|
+
allSettings,
|
|
223
|
+
);
|
|
224
|
+
const value =
|
|
225
|
+
demoMode && key === SETTINGS_KEYS.NOINDEX
|
|
226
|
+
? "true"
|
|
227
|
+
: getEditableSettingValue(allSettings, key, env);
|
|
228
|
+
const fallbackKey = (CONFIG_FIELDS[key] as { fallbackKey?: "PAGE_SIZE" })
|
|
229
|
+
.fallbackKey;
|
|
230
|
+
const base = {
|
|
231
|
+
key,
|
|
232
|
+
mode: "edit" as const,
|
|
233
|
+
type: definition.type,
|
|
234
|
+
value,
|
|
235
|
+
fallbackValue,
|
|
236
|
+
modified: Object.hasOwn(allSettings, key),
|
|
237
|
+
locked: demoMode && demoLockedSettingKeys.has(key),
|
|
238
|
+
...(fallbackKey ? { fallbackKey } : {}),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
...base,
|
|
243
|
+
...getEditorConstraints(key, definition, value, fallbackValue),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
20
246
|
}
|
|
21
247
|
|
|
22
248
|
export function buildEditableSettingsResponse(
|
|
23
249
|
allSettings: Record<string, string>,
|
|
24
250
|
demoMode: boolean,
|
|
251
|
+
env?: Bindings,
|
|
25
252
|
): Record<string, string> {
|
|
26
253
|
const result: Record<string, string> = {};
|
|
27
254
|
for (const key of editableSettingKeys) {
|
|
28
|
-
result[key] = getEditableSettingValue(allSettings, key);
|
|
255
|
+
result[key] = getEditableSettingValue(allSettings, key, env);
|
|
29
256
|
}
|
|
30
257
|
if (demoMode) {
|
|
31
258
|
result.NOINDEX = "true";
|
|
@@ -38,14 +265,14 @@ export function partitionEditableSettingUpdates(
|
|
|
38
265
|
updates: Record<string, string>,
|
|
39
266
|
demoMode: boolean,
|
|
40
267
|
): {
|
|
41
|
-
filteredUpdates: Partial<Record<
|
|
268
|
+
filteredUpdates: Partial<Record<ConfigEditorKey, string>>;
|
|
42
269
|
rejectedKeys: string[];
|
|
43
270
|
} {
|
|
44
|
-
const filteredUpdates: Partial<Record<
|
|
271
|
+
const filteredUpdates: Partial<Record<ConfigEditorKey, string>> = {};
|
|
45
272
|
const rejectedKeys: string[] = [];
|
|
46
273
|
|
|
47
274
|
for (const [key, value] of Object.entries(updates)) {
|
|
48
|
-
const configKey = key as
|
|
275
|
+
const configKey = key as ConfigEditorKey;
|
|
49
276
|
|
|
50
277
|
if (
|
|
51
278
|
demoMode &&
|
|
@@ -18,9 +18,9 @@ export function isRatingSortOrder(
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Returns true when a collection has enough rated
|
|
21
|
+
* Returns true when a collection has enough rated Threads to make rating sort useful.
|
|
22
22
|
*
|
|
23
|
-
* @param
|
|
23
|
+
* @param ratedThreadCount - Number of Threads with at least one rating
|
|
24
24
|
* @returns Whether rating sort should be shown to readers
|
|
25
25
|
*
|
|
26
26
|
* @example
|
|
@@ -28,13 +28,15 @@ export function isRatingSortOrder(
|
|
|
28
28
|
* supportsCollectionRatingSort(2);
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
export function supportsCollectionRatingSort(
|
|
32
|
-
|
|
31
|
+
export function supportsCollectionRatingSort(
|
|
32
|
+
ratedThreadCount: number,
|
|
33
|
+
): boolean {
|
|
34
|
+
return ratedThreadCount > 1;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Resolves the sort order for a collection page, falling back when rating
|
|
37
|
-
* sorting is requested but the collection does not have enough rated
|
|
39
|
+
* sorting is requested but the collection does not have enough rated Threads.
|
|
38
40
|
*
|
|
39
41
|
* @param requestedSort - Sort order from the request query
|
|
40
42
|
* @param defaultSort - Collection default sort order
|
package/src/lib/constants.ts
CHANGED
|
@@ -22,12 +22,14 @@ export const RESERVED_PATHS = [
|
|
|
22
22
|
"reset",
|
|
23
23
|
"collections",
|
|
24
24
|
"compose",
|
|
25
|
+
"preview",
|
|
25
26
|
"new",
|
|
26
27
|
"static",
|
|
27
28
|
"assets",
|
|
28
29
|
"_assets",
|
|
29
30
|
"healthz",
|
|
30
31
|
"readyz",
|
|
32
|
+
"skill.md",
|
|
31
33
|
] as const;
|
|
32
34
|
|
|
33
35
|
export type ReservedPath = (typeof RESERVED_PATHS)[number];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const NON_FEED_PATH_PREFIXES = ["/api", "/settings", "/compose", "/_"];
|
|
2
|
+
|
|
3
|
+
function hasPathPrefix(path: string, prefix: string): boolean {
|
|
4
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check whether an internal application path is an Atom feed endpoint.
|
|
9
|
+
*
|
|
10
|
+
* @param path - Request pathname after any configured site prefix is removed
|
|
11
|
+
* @returns Whether the path is a canonical or legacy feed URL
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* isRssFeedPath("/reading/feed"); // true
|
|
15
|
+
* isRssFeedPath("/api/posts"); // false
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function isRssFeedPath(path: string): boolean {
|
|
19
|
+
if (path === "/feed" || path.startsWith("/feed/")) return true;
|
|
20
|
+
if (NON_FEED_PATH_PREFIXES.some((prefix) => hasPathPrefix(path, prefix))) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return path.endsWith("/feed") || path.endsWith("/feed/atom.xml");
|
|
25
|
+
}
|