@jant/core 0.6.14 → 0.6.15
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 +81 -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-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- 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__/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 +64 -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 +18 -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__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- 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-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- 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 +86 -74
- 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 +4 -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/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__/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/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 +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -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-collection-migration.test.ts +364 -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/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/_journal.json +28 -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/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/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -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 +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- 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__/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 +8 -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__/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__/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/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -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 +7 -1
- 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 +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -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__/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 +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -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 +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -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__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -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 +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -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 +226 -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 +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -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 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- 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 +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- 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/NoteCard.tsx +10 -20
- 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/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 +7 -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 +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- 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/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -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
|
@@ -176,6 +176,34 @@
|
|
|
176
176
|
"when": 1781807044981,
|
|
177
177
|
"tag": "0024_high_violations",
|
|
178
178
|
"breakpoints": true
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"idx": 25,
|
|
182
|
+
"version": "7",
|
|
183
|
+
"when": 1784131458842,
|
|
184
|
+
"tag": "0025_moaning_epoch",
|
|
185
|
+
"breakpoints": true
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"idx": 26,
|
|
189
|
+
"version": "7",
|
|
190
|
+
"when": 1784343834845,
|
|
191
|
+
"tag": "0026_talented_killer_shrike",
|
|
192
|
+
"breakpoints": true
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"idx": 27,
|
|
196
|
+
"version": "7",
|
|
197
|
+
"when": 1784436805462,
|
|
198
|
+
"tag": "0027_short_cannonball",
|
|
199
|
+
"breakpoints": true
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"idx": 28,
|
|
203
|
+
"version": "7",
|
|
204
|
+
"when": 1784531392184,
|
|
205
|
+
"tag": "0028_perfect_sharon_carter",
|
|
206
|
+
"breakpoints": true
|
|
179
207
|
}
|
|
180
208
|
]
|
|
181
209
|
}
|
package/src/db/pg/schema.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
customType,
|
|
20
20
|
} from "drizzle-orm/pg-core";
|
|
21
21
|
import { sql } from "drizzle-orm";
|
|
22
|
+
import { SYSTEM_NAV_KEY_VALUES } from "../../types/constants.js";
|
|
22
23
|
|
|
23
24
|
const SITE_STATUSES = ["active", "suspended"] as const;
|
|
24
25
|
const SITE_DOMAIN_KINDS = ["primary", "alias"] as const;
|
|
@@ -27,16 +28,8 @@ const FORMATS = ["note", "link", "quote"] as const;
|
|
|
27
28
|
const STATUSES = ["draft", "published"] as const;
|
|
28
29
|
const VISIBILITIES = ["public", "latest_hidden", "private"] as const;
|
|
29
30
|
const COLLECTION_SORT_ORDERS = ["newest", "oldest", "rating_desc"] as const;
|
|
30
|
-
const NAV_ITEM_TYPES = ["link", "system", "collection"] as const;
|
|
31
|
+
const NAV_ITEM_TYPES = ["link", "system", "collection", "page"] as const;
|
|
31
32
|
const NAV_ITEM_PLACEMENTS = ["header", "more"] as const;
|
|
32
|
-
const SYSTEM_NAV_KEYS = [
|
|
33
|
-
"latest",
|
|
34
|
-
"featured",
|
|
35
|
-
"collections",
|
|
36
|
-
"archive",
|
|
37
|
-
"rss",
|
|
38
|
-
"settings",
|
|
39
|
-
] as const;
|
|
40
33
|
const UPLOAD_SESSION_STATES = [
|
|
41
34
|
"pending",
|
|
42
35
|
"uploaded",
|
|
@@ -167,6 +160,7 @@ export const posts = pgTable(
|
|
|
167
160
|
url: text("url"),
|
|
168
161
|
body: text("body"),
|
|
169
162
|
bodyHtml: text("body_html"),
|
|
163
|
+
bodyHtmlVersion: integer("body_html_version").notNull().default(1),
|
|
170
164
|
bodyText: text("body_text"),
|
|
171
165
|
quoteText: text("quote_text"),
|
|
172
166
|
summary: text("summary"),
|
|
@@ -242,8 +236,8 @@ export const posts = pgTable(
|
|
|
242
236
|
.where(
|
|
243
237
|
sql`${table.replyToId} IS NOT NULL AND ${table.status} = 'published'`,
|
|
244
238
|
),
|
|
245
|
-
index("
|
|
246
|
-
.on(table.siteId, table.
|
|
239
|
+
index("idx_post_site_featured_thread_published")
|
|
240
|
+
.on(table.siteId, table.threadId, table.publishedAt, table.id)
|
|
247
241
|
.where(
|
|
248
242
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
249
243
|
),
|
|
@@ -591,16 +585,16 @@ export const collectionDirectoryItems = pgTable(
|
|
|
591
585
|
);
|
|
592
586
|
|
|
593
587
|
// =============================================================================
|
|
594
|
-
//
|
|
588
|
+
// Thread-Collection Junction Table (M:N)
|
|
595
589
|
// =============================================================================
|
|
596
590
|
|
|
597
|
-
export const
|
|
598
|
-
"
|
|
591
|
+
export const threadCollections = pgTable(
|
|
592
|
+
"thread_collection",
|
|
599
593
|
{
|
|
600
594
|
siteId: text("site_id")
|
|
601
595
|
.notNull()
|
|
602
596
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
603
|
-
|
|
597
|
+
threadId: text("thread_id")
|
|
604
598
|
.notNull()
|
|
605
599
|
.references(() => posts.id, { onDelete: "cascade" }),
|
|
606
600
|
collectionId: text("collection_id")
|
|
@@ -611,16 +605,16 @@ export const postCollections = pgTable(
|
|
|
611
605
|
pinnedAt: integer("pinned_at"),
|
|
612
606
|
},
|
|
613
607
|
(table) => [
|
|
614
|
-
primaryKey({ columns: [table.siteId, table.
|
|
615
|
-
index("
|
|
608
|
+
primaryKey({ columns: [table.siteId, table.threadId, table.collectionId] }),
|
|
609
|
+
index("idx_thread_collection_site_collection_id").on(
|
|
616
610
|
table.siteId,
|
|
617
611
|
table.collectionId,
|
|
618
612
|
),
|
|
619
|
-
index("
|
|
613
|
+
index("idx_thread_collection_site_collection_created_thread").on(
|
|
620
614
|
table.siteId,
|
|
621
615
|
table.collectionId,
|
|
622
616
|
table.createdAt,
|
|
623
|
-
table.
|
|
617
|
+
table.threadId,
|
|
624
618
|
),
|
|
625
619
|
],
|
|
626
620
|
);
|
|
@@ -642,11 +636,14 @@ export const navItems = pgTable(
|
|
|
642
636
|
.notNull()
|
|
643
637
|
.default("link"),
|
|
644
638
|
systemKey: text("system_key", {
|
|
645
|
-
enum:
|
|
639
|
+
enum: SYSTEM_NAV_KEY_VALUES,
|
|
646
640
|
}),
|
|
647
641
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
648
642
|
onDelete: "cascade",
|
|
649
643
|
}),
|
|
644
|
+
postId: text("post_id").references(() => posts.id, {
|
|
645
|
+
onDelete: "cascade",
|
|
646
|
+
}),
|
|
650
647
|
label: text("label").notNull(),
|
|
651
648
|
url: text("url").notNull(),
|
|
652
649
|
placement: text("placement", {
|
|
@@ -673,14 +670,22 @@ export const navItems = pgTable(
|
|
|
673
670
|
${table.type} = 'link'
|
|
674
671
|
AND ${table.systemKey} IS NULL
|
|
675
672
|
AND ${table.collectionId} IS NULL
|
|
673
|
+
AND ${table.postId} IS NULL
|
|
676
674
|
) OR (
|
|
677
675
|
${table.type} = 'system'
|
|
678
676
|
AND ${table.systemKey} IS NOT NULL
|
|
679
677
|
AND ${table.collectionId} IS NULL
|
|
678
|
+
AND ${table.postId} IS NULL
|
|
680
679
|
) OR (
|
|
681
680
|
${table.type} = 'collection'
|
|
682
681
|
AND ${table.systemKey} IS NULL
|
|
683
682
|
AND ${table.collectionId} IS NOT NULL
|
|
683
|
+
AND ${table.postId} IS NULL
|
|
684
|
+
) OR (
|
|
685
|
+
${table.type} = 'page'
|
|
686
|
+
AND ${table.systemKey} IS NULL
|
|
687
|
+
AND ${table.collectionId} IS NULL
|
|
688
|
+
AND ${table.postId} IS NOT NULL
|
|
684
689
|
)`,
|
|
685
690
|
),
|
|
686
691
|
uniqueIndex("uq_nav_item_site_position").on(table.siteId, table.position),
|
|
@@ -690,6 +695,9 @@ export const navItems = pgTable(
|
|
|
690
695
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
691
696
|
.on(table.siteId, table.collectionId)
|
|
692
697
|
.where(sql`${table.collectionId} IS NOT NULL`),
|
|
698
|
+
uniqueIndex("uq_nav_item_site_post_id")
|
|
699
|
+
.on(table.siteId, table.postId)
|
|
700
|
+
.where(sql`${table.postId} IS NOT NULL`),
|
|
693
701
|
],
|
|
694
702
|
);
|
|
695
703
|
|
|
@@ -1,25 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "demo-current",
|
|
3
|
-
"baseMigrationTag": "
|
|
3
|
+
"baseMigrationTag": "0026_absent_rhodey",
|
|
4
4
|
"seedPath": "./demo-current.sql",
|
|
5
5
|
"assertions": [
|
|
6
6
|
{
|
|
7
7
|
"name": "published posts exist",
|
|
8
8
|
"sql": "SELECT COUNT(*) AS count FROM post",
|
|
9
9
|
"column": "count",
|
|
10
|
-
"
|
|
10
|
+
"equals": 3
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "collections exist",
|
|
14
14
|
"sql": "SELECT COUNT(*) AS count FROM collection",
|
|
15
15
|
"column": "count",
|
|
16
|
-
"
|
|
16
|
+
"equals": 2
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "fts index populated",
|
|
20
20
|
"sql": "SELECT COUNT(*) AS count FROM post_fts",
|
|
21
21
|
"column": "count",
|
|
22
|
-
"
|
|
22
|
+
"equals": 3
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "thread collection union preserves both collections",
|
|
26
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection",
|
|
27
|
+
"column": "count",
|
|
28
|
+
"equals": 2
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "shared collection metadata is aggregated across the thread",
|
|
32
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection WHERE site_id = 'sit_01kme7j7j1f4m7s2k8r5c9t0qb' AND thread_id = 'pst_01kme7ja2mf4m7s2k8r5c9t0qf' AND collection_id = 'col_01kme7j8f9f4m7s2k8r5c9t0qc' AND created_at = 1774200040 AND position = 1 AND pinned_at = 1774200500",
|
|
33
|
+
"column": "count",
|
|
34
|
+
"equals": 1
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "child-only collection becomes thread membership",
|
|
38
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection WHERE site_id = 'sit_01kme7j7j1f4m7s2k8r5c9t0qb' AND thread_id = 'pst_01kme7ja2mf4m7s2k8r5c9t0qf' AND collection_id = 'col_01kme7j8f9f4m7s2k8r5c9t0qd' AND created_at = 1774200060 AND position = 4 AND pinned_at = 1774200600",
|
|
39
|
+
"column": "count",
|
|
40
|
+
"equals": 1
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "thread collection targets are roots",
|
|
44
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection tc INNER JOIN post root ON root.site_id = tc.site_id AND root.id = tc.thread_id WHERE root.reply_to_id IS NOT NULL OR root.thread_id <> root.id",
|
|
45
|
+
"column": "count",
|
|
46
|
+
"equals": 0
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "legacy post collection table is removed",
|
|
50
|
+
"sql": "SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name = 'post_collection'",
|
|
51
|
+
"column": "count",
|
|
52
|
+
"equals": 0
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "foreign keys remain valid",
|
|
56
|
+
"sql": "PRAGMA foreign_key_check",
|
|
57
|
+
"rowCount": 0
|
|
23
58
|
}
|
|
24
59
|
]
|
|
25
60
|
}
|
|
@@ -31,6 +31,24 @@ INSERT INTO "collection" (
|
|
|
31
31
|
1774200010
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
INSERT INTO "collection" (
|
|
35
|
+
"id",
|
|
36
|
+
"site_id",
|
|
37
|
+
"title",
|
|
38
|
+
"description",
|
|
39
|
+
"sort_order",
|
|
40
|
+
"created_at",
|
|
41
|
+
"updated_at"
|
|
42
|
+
) VALUES (
|
|
43
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qd',
|
|
44
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
45
|
+
'Child-only Notes',
|
|
46
|
+
'Collection assigned only to a reply before the Thread migration.',
|
|
47
|
+
'newest',
|
|
48
|
+
1774200011,
|
|
49
|
+
1774200011
|
|
50
|
+
);
|
|
51
|
+
|
|
34
52
|
INSERT INTO "nav_item" (
|
|
35
53
|
"id",
|
|
36
54
|
"site_id",
|
|
@@ -149,18 +167,115 @@ INSERT INTO "post" (
|
|
|
149
167
|
1774200020
|
|
150
168
|
);
|
|
151
169
|
|
|
152
|
-
INSERT INTO "
|
|
170
|
+
INSERT INTO "post" (
|
|
171
|
+
"id",
|
|
153
172
|
"site_id",
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
173
|
+
"format",
|
|
174
|
+
"status",
|
|
175
|
+
"visibility",
|
|
176
|
+
"title",
|
|
177
|
+
"body",
|
|
178
|
+
"body_html",
|
|
179
|
+
"body_text",
|
|
180
|
+
"reply_to_id",
|
|
181
|
+
"thread_id",
|
|
182
|
+
"published_at",
|
|
183
|
+
"last_activity_at",
|
|
184
|
+
"created_at",
|
|
185
|
+
"updated_at"
|
|
157
186
|
) VALUES (
|
|
187
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
158
188
|
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
189
|
+
'note',
|
|
190
|
+
'published',
|
|
191
|
+
NULL,
|
|
192
|
+
'First rehearsal reply',
|
|
193
|
+
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"First migration rehearsal reply."}]}]}',
|
|
194
|
+
'<p>First migration rehearsal reply.</p>',
|
|
195
|
+
'First migration rehearsal reply.',
|
|
159
196
|
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
160
|
-
'
|
|
161
|
-
|
|
197
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
198
|
+
1774200030,
|
|
199
|
+
1774200030,
|
|
200
|
+
1774200030,
|
|
201
|
+
1774200030
|
|
162
202
|
);
|
|
163
203
|
|
|
204
|
+
INSERT INTO "post" (
|
|
205
|
+
"id",
|
|
206
|
+
"site_id",
|
|
207
|
+
"format",
|
|
208
|
+
"status",
|
|
209
|
+
"visibility",
|
|
210
|
+
"title",
|
|
211
|
+
"body",
|
|
212
|
+
"body_html",
|
|
213
|
+
"body_text",
|
|
214
|
+
"reply_to_id",
|
|
215
|
+
"thread_id",
|
|
216
|
+
"published_at",
|
|
217
|
+
"last_activity_at",
|
|
218
|
+
"created_at",
|
|
219
|
+
"updated_at"
|
|
220
|
+
) VALUES (
|
|
221
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qi',
|
|
222
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
223
|
+
'note',
|
|
224
|
+
'published',
|
|
225
|
+
NULL,
|
|
226
|
+
'Second rehearsal reply',
|
|
227
|
+
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Second migration rehearsal reply."}]}]}',
|
|
228
|
+
'<p>Second migration rehearsal reply.</p>',
|
|
229
|
+
'Second migration rehearsal reply.',
|
|
230
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
231
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
232
|
+
1774200040,
|
|
233
|
+
1774200040,
|
|
234
|
+
1774200040,
|
|
235
|
+
1774200040
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
INSERT INTO "post_collection" (
|
|
239
|
+
"site_id",
|
|
240
|
+
"post_id",
|
|
241
|
+
"collection_id",
|
|
242
|
+
"created_at",
|
|
243
|
+
"position",
|
|
244
|
+
"pinned_at"
|
|
245
|
+
) VALUES
|
|
246
|
+
(
|
|
247
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
248
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
249
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qc',
|
|
250
|
+
1774200021,
|
|
251
|
+
7,
|
|
252
|
+
1774200300
|
|
253
|
+
),
|
|
254
|
+
(
|
|
255
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
256
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
257
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qc',
|
|
258
|
+
1774200040,
|
|
259
|
+
5,
|
|
260
|
+
NULL
|
|
261
|
+
),
|
|
262
|
+
(
|
|
263
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
264
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qi',
|
|
265
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qc',
|
|
266
|
+
1774200030,
|
|
267
|
+
1,
|
|
268
|
+
1774200500
|
|
269
|
+
),
|
|
270
|
+
(
|
|
271
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
272
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
273
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qd',
|
|
274
|
+
1774200060,
|
|
275
|
+
4,
|
|
276
|
+
1774200600
|
|
277
|
+
);
|
|
278
|
+
|
|
164
279
|
INSERT INTO "path_registry" (
|
|
165
280
|
"id",
|
|
166
281
|
"site_id",
|
package/src/db/schema.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
check,
|
|
16
16
|
} from "drizzle-orm/sqlite-core";
|
|
17
17
|
import { sql } from "drizzle-orm";
|
|
18
|
+
import { SYSTEM_NAV_KEY_VALUES } from "../types/constants.js";
|
|
18
19
|
|
|
19
20
|
const SITE_STATUSES = ["active", "suspended"] as const;
|
|
20
21
|
const SITE_DOMAIN_KINDS = ["primary", "alias"] as const;
|
|
@@ -23,16 +24,8 @@ const FORMATS = ["note", "link", "quote"] as const;
|
|
|
23
24
|
const STATUSES = ["draft", "published"] as const;
|
|
24
25
|
const VISIBILITIES = ["public", "latest_hidden", "private"] as const;
|
|
25
26
|
const COLLECTION_SORT_ORDERS = ["newest", "oldest", "rating_desc"] as const;
|
|
26
|
-
const NAV_ITEM_TYPES = ["link", "system", "collection"] as const;
|
|
27
|
+
const NAV_ITEM_TYPES = ["link", "system", "collection", "page"] as const;
|
|
27
28
|
const NAV_ITEM_PLACEMENTS = ["header", "more"] as const;
|
|
28
|
-
const SYSTEM_NAV_KEYS = [
|
|
29
|
-
"latest",
|
|
30
|
-
"featured",
|
|
31
|
-
"collections",
|
|
32
|
-
"archive",
|
|
33
|
-
"rss",
|
|
34
|
-
"settings",
|
|
35
|
-
] as const;
|
|
36
29
|
const UPLOAD_SESSION_STATES = [
|
|
37
30
|
"pending",
|
|
38
31
|
"uploaded",
|
|
@@ -161,6 +154,7 @@ export const posts = sqliteTable(
|
|
|
161
154
|
url: text("url"),
|
|
162
155
|
body: text("body"),
|
|
163
156
|
bodyHtml: text("body_html"),
|
|
157
|
+
bodyHtmlVersion: integer("body_html_version").notNull().default(1),
|
|
164
158
|
bodyText: text("body_text"),
|
|
165
159
|
quoteText: text("quote_text"),
|
|
166
160
|
summary: text("summary"),
|
|
@@ -227,8 +221,8 @@ export const posts = sqliteTable(
|
|
|
227
221
|
.where(
|
|
228
222
|
sql`${table.replyToId} IS NOT NULL AND ${table.status} = 'published'`,
|
|
229
223
|
),
|
|
230
|
-
index("
|
|
231
|
-
.on(table.siteId, table.
|
|
224
|
+
index("idx_post_site_featured_thread_published")
|
|
225
|
+
.on(table.siteId, table.threadId, table.publishedAt, table.id)
|
|
232
226
|
.where(
|
|
233
227
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
234
228
|
),
|
|
@@ -571,16 +565,16 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
571
565
|
);
|
|
572
566
|
|
|
573
567
|
// =============================================================================
|
|
574
|
-
//
|
|
568
|
+
// Thread-Collection Junction Table (M:N)
|
|
575
569
|
// =============================================================================
|
|
576
570
|
|
|
577
|
-
export const
|
|
578
|
-
"
|
|
571
|
+
export const threadCollections = sqliteTable(
|
|
572
|
+
"thread_collection",
|
|
579
573
|
{
|
|
580
574
|
siteId: text("site_id")
|
|
581
575
|
.notNull()
|
|
582
576
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
583
|
-
|
|
577
|
+
threadId: text("thread_id")
|
|
584
578
|
.notNull()
|
|
585
579
|
.references(() => posts.id, { onDelete: "cascade" }),
|
|
586
580
|
collectionId: text("collection_id")
|
|
@@ -591,16 +585,16 @@ export const postCollections = sqliteTable(
|
|
|
591
585
|
pinnedAt: integer("pinned_at"),
|
|
592
586
|
},
|
|
593
587
|
(table) => [
|
|
594
|
-
primaryKey({ columns: [table.siteId, table.
|
|
595
|
-
index("
|
|
588
|
+
primaryKey({ columns: [table.siteId, table.threadId, table.collectionId] }),
|
|
589
|
+
index("idx_thread_collection_site_collection_id").on(
|
|
596
590
|
table.siteId,
|
|
597
591
|
table.collectionId,
|
|
598
592
|
),
|
|
599
|
-
index("
|
|
593
|
+
index("idx_thread_collection_site_collection_created_thread").on(
|
|
600
594
|
table.siteId,
|
|
601
595
|
table.collectionId,
|
|
602
596
|
table.createdAt,
|
|
603
|
-
table.
|
|
597
|
+
table.threadId,
|
|
604
598
|
),
|
|
605
599
|
],
|
|
606
600
|
);
|
|
@@ -622,11 +616,14 @@ export const navItems = sqliteTable(
|
|
|
622
616
|
.notNull()
|
|
623
617
|
.default("link"),
|
|
624
618
|
systemKey: text("system_key", {
|
|
625
|
-
enum:
|
|
619
|
+
enum: SYSTEM_NAV_KEY_VALUES,
|
|
626
620
|
}),
|
|
627
621
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
628
622
|
onDelete: "cascade",
|
|
629
623
|
}),
|
|
624
|
+
postId: text("post_id").references(() => posts.id, {
|
|
625
|
+
onDelete: "cascade",
|
|
626
|
+
}),
|
|
630
627
|
label: text("label").notNull(),
|
|
631
628
|
url: text("url").notNull(),
|
|
632
629
|
placement: text("placement", {
|
|
@@ -653,14 +650,22 @@ export const navItems = sqliteTable(
|
|
|
653
650
|
${table.type} = 'link'
|
|
654
651
|
AND ${table.systemKey} IS NULL
|
|
655
652
|
AND ${table.collectionId} IS NULL
|
|
653
|
+
AND ${table.postId} IS NULL
|
|
656
654
|
) OR (
|
|
657
655
|
${table.type} = 'system'
|
|
658
656
|
AND ${table.systemKey} IS NOT NULL
|
|
659
657
|
AND ${table.collectionId} IS NULL
|
|
658
|
+
AND ${table.postId} IS NULL
|
|
660
659
|
) OR (
|
|
661
660
|
${table.type} = 'collection'
|
|
662
661
|
AND ${table.systemKey} IS NULL
|
|
663
662
|
AND ${table.collectionId} IS NOT NULL
|
|
663
|
+
AND ${table.postId} IS NULL
|
|
664
|
+
) OR (
|
|
665
|
+
${table.type} = 'page'
|
|
666
|
+
AND ${table.systemKey} IS NULL
|
|
667
|
+
AND ${table.collectionId} IS NULL
|
|
668
|
+
AND ${table.postId} IS NOT NULL
|
|
664
669
|
)`,
|
|
665
670
|
),
|
|
666
671
|
uniqueIndex("uq_nav_item_site_position").on(table.siteId, table.position),
|
|
@@ -670,6 +675,9 @@ export const navItems = sqliteTable(
|
|
|
670
675
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
671
676
|
.on(table.siteId, table.collectionId)
|
|
672
677
|
.where(sql`${table.collectionId} IS NOT NULL`),
|
|
678
|
+
uniqueIndex("uq_nav_item_site_post_id")
|
|
679
|
+
.on(table.siteId, table.postId)
|
|
680
|
+
.where(sql`${table.postId} IS NOT NULL`),
|
|
673
681
|
],
|
|
674
682
|
);
|
|
675
683
|
|
|
@@ -18,8 +18,14 @@ describe("detectLocaleFromHeader", () => {
|
|
|
18
18
|
expect(detectLocaleFromHeader("fr")).toBe("en");
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it("
|
|
22
|
-
expect(detectLocaleFromHeader("zh-CN")).toBe("
|
|
21
|
+
it("maps Simplified Chinese browser locales to zh-Hans", () => {
|
|
22
|
+
expect(detectLocaleFromHeader("zh-CN")).toBe("zh-Hans");
|
|
23
|
+
expect(detectLocaleFromHeader("zh-SG")).toBe("zh-Hans");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("maps Traditional Chinese browser locales to zh-Hant", () => {
|
|
27
|
+
expect(detectLocaleFromHeader("zh-TW")).toBe("zh-Hant");
|
|
28
|
+
expect(detectLocaleFromHeader("zh-HK")).toBe("zh-Hant");
|
|
23
29
|
});
|
|
24
30
|
|
|
25
31
|
it("returns en for undefined", () => {
|
package/src/i18n/detect.ts
CHANGED
|
@@ -43,6 +43,11 @@ function mapTagToLocale(tag: string): Locale | undefined {
|
|
|
43
43
|
const normalized = tag.trim().toLowerCase();
|
|
44
44
|
if (!normalized) return undefined;
|
|
45
45
|
|
|
46
|
+
const cjkProfile = getCjkFontFromLanguageTag(normalized);
|
|
47
|
+
if (cjkProfile === "zh-Hans" || cjkProfile === "zh-Hant") {
|
|
48
|
+
return cjkProfile;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
const primary = normalized.split("-")[0];
|
|
47
52
|
if (isLocale(primary)) return primary;
|
|
48
53
|
|
|
@@ -90,7 +95,7 @@ export function detectLocaleFromHeader(header: string | undefined): Locale {
|
|
|
90
95
|
return baseLocale;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
/** Valid CJK
|
|
98
|
+
/** Valid CJK font profile values */
|
|
94
99
|
export const CJK_SERIF_FONT_VALUES = [
|
|
95
100
|
"off",
|
|
96
101
|
"zh-Hans",
|
|
@@ -111,12 +116,16 @@ export function isCjkSerifFont(value: unknown): value is CjkSerifFont {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
/**
|
|
114
|
-
* Map a BCP 47 language tag to a CJK
|
|
119
|
+
* Map a BCP 47 language tag to a CJK font profile.
|
|
115
120
|
*
|
|
116
121
|
* @param tag - BCP 47 language tag (e.g. "zh-CN", "ja")
|
|
117
|
-
* @returns CJK
|
|
122
|
+
* @returns CJK font profile, or `undefined` if not a CJK language
|
|
123
|
+
* @example
|
|
124
|
+
* getCjkFontFromLanguageTag("zh-TW") // "zh-Hant"
|
|
118
125
|
*/
|
|
119
|
-
function
|
|
126
|
+
export function getCjkFontFromLanguageTag(
|
|
127
|
+
tag: string,
|
|
128
|
+
): Exclude<CjkSerifFont, "off"> | undefined {
|
|
120
129
|
const normalized = tag.trim().toLowerCase();
|
|
121
130
|
if (!normalized) return undefined;
|
|
122
131
|
|
|
@@ -177,7 +186,7 @@ export function detectCjkFontFromHeader(
|
|
|
177
186
|
entries.sort((a, b) => b.q - a.q);
|
|
178
187
|
|
|
179
188
|
for (const { tag } of entries) {
|
|
180
|
-
const font =
|
|
189
|
+
const font = getCjkFontFromLanguageTag(tag);
|
|
181
190
|
if (font) return font;
|
|
182
191
|
}
|
|
183
192
|
|