@jant/core 0.3.27 → 0.3.29
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/reset-password.js +22 -0
- package/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +25 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
|
@@ -1 +1 @@
|
|
|
1
|
-
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"
|
|
1
|
+
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"標籤和網址是必填的\"],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"加入導航\"],\"+bHzpy\":[\"顯示連結的文字\"],\"+owNNn\":[\"文章\"],\"+zy2Nq\":[\"類型\"],\"/0D1Xp\":[\"編輯收藏集\"],\"/DFKdU\":[\"輸入引用...\"],\"/R/sGB\":[\"密碼已成功更改。\"],\"/Rj5P4\":[\"您的姓名\"],\"/rkqRV\":[\"所有頁面都在您的導航中。\"],\"07Epll\":[\"這將為您的網站和儀表板設置主題。所有顏色主題都支持深色模式。\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"新重定向\"],\"0ieXE7\":[\"最高評價\"],\"0yIy82\":[\"尚未有精選文章。\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"筆記\"],\"1Oj1sI\":[\"已保存順序\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"文章標題...\"],\"2cFU6q\":[\"網站頁腳\"],\"2fUwEY\":[\"選擇媒體\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"頁面標題...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302(臨時)\"],\"4/SFQS\":[\"查看網站\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4KzVT6\":[\"刪除頁面\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"內容\"],\"4mDPGp\":[\"此頁面的 URL 路徑。使用小寫字母、數字和連字符。\"],\"6C8dEg\":[\"附加文本\"],\"6WdDG7\":[\"頁面\"],\"6YtxFj\":[\"名稱\"],\"6tU2jr\":[\"No collections found.\"],\"71Xwww\":[\"無效的請求\"],\"7G4SBz\":[\"頁面內容(支持Markdown)...\"],\"7Mk+/h\":[\"更新收藏集\"],\"7Q1KKN\":[\"來源路徑\"],\"7aECQB\":[\"無效或已過期的連結\"],\"7nGhhM\":[\"你在想什麼?\"],\"7p5kLi\":[\"儀表板\"],\"7vhWI8\":[\"新密碼\"],\"87a/t/\":[\"標籤\"],\"8HgKQc\":[\"SEO 設定已成功儲存。\"],\"8WX0J+\":[\"您的想法(可選)\"],\"8WtVZw\":[\"無法保存帖子。請再試一次。\"],\"8ZsakT\":[\"密碼\"],\"8qX8Jl\":[\"選擇一個字體搭配以供您的網站使用。所有選項均使用系統字體以加快加載速度。\"],\"8tM8+a\":[\"儲存為草稿\"],\"9+vGLh\":[\"自訂 CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AyHO4m\":[\"這個收藏是關於什麼的?\"],\"B373X+\":[\"編輯文章\"],\"B495Gs\":[\"檔案館\"],\"BjF0Jv\":[\"僅限小寫字母、數字和連字符\"],\"Cl55aD\":[\"當前密碼不正確。\"],\"D3uuEX\":[\"尚未選擇任何媒體。\"],\"D9Oea+\":[\"永久鏈接\"],\"DCKkhU\":[\"當前密碼\"],\"DHhJ7s\":[\"上一頁\"],\"DPfwMq\":[\"完成\"],\"DoJzLz\":[\"收藏夾\"],\"E80cJw\":[\"刪除此媒體將永久從存儲中移除它。\"],\"EEYbdt\":[\"發佈\"],\"EGwzOK\":[\"完成設置\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"Eq6YVV\":[\"分數\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FGrimz\":[\"新帖子\"],\"FkMol5\":[\"精選\"],\"Fxf4jq\":[\"描述(可選)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"身份驗證未配置\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"您的網站導航\"],\"GX2VMa\":[\"建立您的管理員帳戶。\"],\"GbVAnd\":[\"此密碼重設連結無效或已過期。請生成一個新的連結。\"],\"GorKul\":[\"歡迎來到 Jant\"],\"GrZ6fH\":[\"新頁面\"],\"GxkJXS\":[\"上傳中...\"],\"H29JXm\":[\"+ ALT\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"未找到任何帖子。\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"保存更改\"],\"IagCbF\":[\"網址\"],\"J4FNfC\":[\"此集合中沒有帖子。\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"需要幫助嗎?請訪問<0>文檔</0>。\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 將媒體嵌入到您的帖子中。\"],\"KbS2K9\":[\"重設密碼\"],\"KdSsVl\":[\"作者(可選)\"],\"KiJn9B\":[\"備註\"],\"KmGXnO\":[\"您確定要刪除這篇文章嗎?這個操作無法撤銷。\"],\"KsIZ3c\":[\"頁腳已成功保存。\"],\"KuCcWu\":[\"Displayed at the bottom of all posts and pages. Markdown supported.\"],\"L85WcV\":[\"縮略名\"],\"LdyooL\":[\"鏈接\"],\"LkA8jz\":[\"Add alt text\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"點擊圖片以查看完整大小\"],\"M2kIWU\":[\"字型主題\"],\"M8kJqa\":[\"草稿\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"標題\"],\"MLSRl9\":[\"引用文本\"],\"MWBOxm\":[\"收藏(可選)\"],\"MZbQHL\":[\"未找到結果。\"],\"MdMyne\":[\"來源連結(選填)\"],\"Mhf/H/\":[\"建立重定向\"],\"MnbH31\":[\"頁面\"],\"MqghUt\":[\"搜尋帖子...\"],\"N0APCr\":[\"這會顯示在您預設首頁的部落格文章上方。這也用於您首頁的 meta 描述。\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"儲存 CSS\"],\"Naqg3G\":[\"未提供檔案\"],\"O3oNi5\":[\"電子郵件\"],\"OCNZaU\":[\"重定向來源的路徑\"],\"ODiSoW\":[\"尚未有帖子。\"],\"ONWvwQ\":[\"上傳\"],\"OVSkIF\":[\"敏捷的棕色狐狸跳過懶惰的狗。\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PZ7HJ8\":[\"部落格頭像\"],\"Pbm2/N\":[\"創建收藏夾\"],\"QEbNBb\":[\"路徑(例如 /archive)或完整網址(例如 https://example.com)\"],\"QLkhbH\":[\"被引用的文本...\"],\"Qjlym2\":[\"無法創建帳戶\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"連結\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 則帖子的主題\"],\"RxsRD6\":[\"時區\"],\"SGJDS5\":[\"儲存未配置\"],\"SJmfuf\":[\"網站名稱\"],\"ST+lN2\":[\"尚未上傳任何媒體。\"],\"T0bsor\":[\"設置已成功保存。\"],\"TNFigk\":[\"預設首頁視圖\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"設定\"],\"U5v6Gh\":[\"編輯頁面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"Save Settings\"],\"UeOiKl\":[\"無效的輸入\"],\"Ui5/i3\":[\"允許搜尋引擎索引我的網站是可以的\"],\"Uj/btJ\":[\"在我的網站標頭中顯示頭像\"],\"UxKoFf\":[\"Navigation\"],\"V4WsyL\":[\"新增連結\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"頭像顯示設置已成功保存。\"],\"VhMDMg\":[\"更改密碼\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"一般設定\"],\"WmZ/rP\":[\"到路徑\"],\"XrnWzN\":[\"Published!\"],\"Y+7JGK\":[\"創建頁面\"],\"Y75ho6\":[\"其他頁面\"],\"YIix5Y\":[\"Search...\"],\"Z2lfX1\":[\"選擇圖示\"],\"Z3FXyt\":[\"載入中...\"],\"Z6NwTi\":[\"儲存為草稿\"],\"ZQKLI1\":[\"危險區域\"],\"ZUpE9/\":[\"這會顯示在您所有的文章和頁面的底部。支持 Markdown。\"],\"ZhhOwV\":[\"引用\"],\"aAIQg2\":[\"外觀\"],\"aHTB7P\":[\"附加在您帖子上的補充內容\"],\"aT4jc4\":[\"無效的電子郵件或密碼\"],\"aaGV/9\":[\"新連結\"],\"ajBsih\":[\"發佈文章成功。\"],\"alKG0+\":[\"Font Theme\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"關於這個部落格\"],\"b+/jO6\":[\"301(永久)\"],\"b4VwHs\":[\"未提供檔案。\"],\"bG4pfW\":[\"Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.\"],\"bHYIks\":[\"登出\"],\"bcE7Mx\":[\"無法更新個人資料。\"],\"biOepV\":[\"← Back to home\"],\"bzSI52\":[\"丟棄\"],\"cTUByn\":[\"最新的在前\"],\"cnGeoo\":[\"刪除\"],\"dEgA5A\":[\"取消\"],\"dmCcPs\":[\"這是用於您的網站圖標和蘋果觸控圖標。為了獲得最佳效果,請上傳至少 180x180 像素的正方形圖片。\"],\"dtQNkT\":[\"選擇的字體主題無效。\"],\"e6Jr7Q\":[\"← 返回收藏夾\"],\"ePK91l\":[\"編輯\"],\"eWLklq\":[\"引用\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"示範帳戶已預填。只需點擊登入。\"],\"f/bxrN\":[\"名稱是必填的。\"],\"f6e0Ry\":[\"Article\"],\"fDGOiR\":[\"密碼不匹配。\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"取消導航\"],\"gDx5MG\":[\"編輯連結\"],\"gJH6Bs\":[\"Alt text improves accessibility\"],\"gOwwEy\":[\"儲存空間未配置。\"],\"hDRU5q\":[\"找到 \",[\"0\"],\" 個結果\"],\"hG89Ed\":[\"Image\"],\"hWOZIv\":[\"請輸入您的新密碼。\"],\"hXzOVo\":[\"下一頁\"],\"he3ygx\":[\"複製\"],\"heSQoS\":[\"粘貼一個網址...\"],\"hmXTCY\":[\"選擇的主題無效。\"],\"hrL0Be\":[\"圖示(可選)\"],\"i0vDGK\":[\"排序順序\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"網址(可選)\"],\"iEqmSU\":[\"自訂 CSS 已成功儲存。\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"已發佈的頁面可以通過其標識符訪問。草稿不可見。\"],\"jUV7CU\":[\"上傳頭像\"],\"jVUmOK\":[\"支援Markdown\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"你在想什麼...\"],\"k1ifdL\":[\"處理中...\"],\"kI1qVD\":[\"格式\"],\"kL1h6U\":[\"移除分隔線\"],\"kNiQp6\":[\"置頂\"],\"kPMIr+\":[\"給它一個標題...\"],\"kd7eBB\":[\"建立連結\"],\"kj6ppi\":[\"條目\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"搜尋圖示...\"],\"l6ANt9\":[\"最低評價\"],\"lO1Oow\":[\"上傳成功!\"],\"m16xKo\":[\"Add\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登入\"],\"n6QD94\":[\"最舊的在前\"],\"o21Y+P\":[\"條目\"],\"o4dofa\":[\"AUTH_SECRET 未配置\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"Color Theme\"],\"oYPBa0\":[\"更新頁面\"],\"p2/GCq\":[\"確認密碼\"],\"pB0OKE\":[\"新分隔線\"],\"pRhYH2\":[\"收藏中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pnve/d\":[\"個人資料已成功儲存。\"],\"q+hNag\":[\"集合\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"添加媒體\"],\"qt89I8\":[\"草稿已保存。\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"顏色主題\"],\"rdUucN\":[\"預覽\"],\"rzNUSl\":[\"包含 1 則貼文的主題\"],\"sGajR7\":[\"線程開始\"],\"smzF8S\":[\"顯示 \",[\"remainingCount\"],\" 個更多 \",[\"0\"]],\"ssqvZi\":[\"保存個人資料\"],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"移除\"],\"tfDRzk\":[\"保存\"],\"tfrt7B\":[\"未配置任何重定向。\"],\"tiq7kl\":[\"頁面 \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"已發佈\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"狀態\"],\"vERlcd\":[\"個人資料\"],\"vXIe7J\":[\"語言\"],\"vh0C9b\":[\"尚未有導航連結。請將頁面添加到導航或創建連結。\"],\"vmQmHx\":[\"添加自定義 CSS 以覆蓋任何樣式。使用數據屬性,如 [data-page]、[data-post]、[data-format] 來針對特定元素。\"],\"vzU4k9\":[\"新收藏集\"],\"wEF6Ix\":[\"目的地路徑或 URL\"],\"wK4OTM\":[\"標題(選填)\"],\"wL3cK8\":[\"最新\"],\"wM5UXj\":[\"刪除媒體\"],\"wRR604\":[\"頁面\"],\"wc+17X\":[\"/* 您的自訂 CSS 在這裡 */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"無標題\"],\"x+doid\":[\"圖片會自動優化:調整大小至最大 1920 像素,轉換為 WebP 格式,並去除元數據。\"],\"x0mzE0\":[\"創建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"媒體\"],\"y0R9F0\":[\"帖子已成功更新。\"],\"y28hnO\":[\"文章\"],\"yQ2kGp\":[\"載入更多\"],\"yjkELF\":[\"確認新密碼\"],\"yz7wBu\":[\"關閉\"],\"yzF66j\":[\"連結\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 個結果\"],\"zBFr9G\":[\"粘貼一篇長文章、AI 回應或任何文本...\\n\\nMarkdown 格式將被保留。\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL安全識別碼(小寫、數字、連字符)。對於CJK標題,slug將在伺服器上自動生成。\"]}")as Messages;
|
package/src/i18n/middleware.ts
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
import type { MiddlewareHandler } from "hono";
|
|
6
6
|
import type { I18n } from "@lingui/core";
|
|
7
7
|
import { createI18n, isLocale, baseLocale, type Locale } from "./i18n.js";
|
|
8
|
-
import type { Services } from "../services/index.js";
|
|
9
|
-
|
|
10
8
|
declare module "hono" {
|
|
11
9
|
interface ContextVariableMap {
|
|
12
10
|
lang: Locale;
|
|
@@ -25,15 +23,13 @@ export function i18nMiddleware(): MiddlewareHandler {
|
|
|
25
23
|
return async (c, next) => {
|
|
26
24
|
let lang: Locale = baseLocale;
|
|
27
25
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} catch {
|
|
36
|
-
// Ignore errors, fall back to default locale
|
|
26
|
+
const allSettings = c.get("allSettings") as
|
|
27
|
+
| Record<string, string>
|
|
28
|
+
| undefined;
|
|
29
|
+
if (allSettings) {
|
|
30
|
+
const siteLang = allSettings["SITE_LANGUAGE"];
|
|
31
|
+
if (siteLang && isLocale(siteLang)) {
|
|
32
|
+
lang = siteLang;
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseCollectionIcon,
|
|
4
|
+
renderCollectionIcon,
|
|
5
|
+
createIconValue,
|
|
6
|
+
getIconSvg,
|
|
7
|
+
DEFAULT_ICON_NAME,
|
|
8
|
+
DEFAULT_ICON_COLOR,
|
|
9
|
+
} from "../icons.js";
|
|
10
|
+
|
|
11
|
+
describe("icons", () => {
|
|
12
|
+
describe("getIconSvg", () => {
|
|
13
|
+
it("returns SVG for a valid icon name", () => {
|
|
14
|
+
const svg = getIconSvg("library");
|
|
15
|
+
expect(svg).toContain("<svg");
|
|
16
|
+
expect(svg).toContain("lucide-library");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("handles multi-word kebab-case names", () => {
|
|
20
|
+
const svg = getIconSvg("book-open");
|
|
21
|
+
expect(svg).toContain("<svg");
|
|
22
|
+
expect(svg).toContain("lucide-book-open");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns null for unknown icon names", () => {
|
|
26
|
+
expect(getIconSvg("nonexistent-icon-xyz")).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("parseCollectionIcon", () => {
|
|
31
|
+
it("parses valid JSON icon data", () => {
|
|
32
|
+
const json = JSON.stringify({
|
|
33
|
+
name: "library",
|
|
34
|
+
svg: "<svg>test</svg>",
|
|
35
|
+
color: "#3b82f6",
|
|
36
|
+
});
|
|
37
|
+
const result = parseCollectionIcon(json);
|
|
38
|
+
expect(result).toEqual({
|
|
39
|
+
name: "library",
|
|
40
|
+
svg: "<svg>test</svg>",
|
|
41
|
+
color: "#3b82f6",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null for null input", () => {
|
|
46
|
+
expect(parseCollectionIcon(null)).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns null for empty string", () => {
|
|
50
|
+
expect(parseCollectionIcon("")).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns null for legacy emoji values", () => {
|
|
54
|
+
expect(parseCollectionIcon("📚")).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns null for legacy text values", () => {
|
|
58
|
+
expect(parseCollectionIcon("laptop")).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns null for malformed JSON", () => {
|
|
62
|
+
expect(parseCollectionIcon("{invalid}")).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns null for JSON missing required fields", () => {
|
|
66
|
+
expect(parseCollectionIcon('{"name":"library"}')).toBeNull();
|
|
67
|
+
expect(
|
|
68
|
+
parseCollectionIcon('{"name":"library","svg":"<svg>"}'),
|
|
69
|
+
).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns null for JSON with wrong field types", () => {
|
|
73
|
+
expect(
|
|
74
|
+
parseCollectionIcon('{"name":123,"svg":"<svg>","color":"#fff"}'),
|
|
75
|
+
).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("createIconValue", () => {
|
|
80
|
+
it("creates a JSON string with name, svg, and color", () => {
|
|
81
|
+
const result = createIconValue("library", "<svg>test</svg>", "#3b82f6");
|
|
82
|
+
const parsed = JSON.parse(result);
|
|
83
|
+
expect(parsed).toEqual({
|
|
84
|
+
name: "library",
|
|
85
|
+
svg: "<svg>test</svg>",
|
|
86
|
+
color: "#3b82f6",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("round-trips through parseCollectionIcon", () => {
|
|
91
|
+
const value = createIconValue("star", "<svg>star</svg>", "#f59e0b");
|
|
92
|
+
const parsed = parseCollectionIcon(value);
|
|
93
|
+
expect(parsed).toEqual({
|
|
94
|
+
name: "star",
|
|
95
|
+
svg: "<svg>star</svg>",
|
|
96
|
+
color: "#f59e0b",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("renderCollectionIcon", () => {
|
|
102
|
+
it("renders structured icon with color and size", () => {
|
|
103
|
+
const icon = createIconValue(
|
|
104
|
+
"library",
|
|
105
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M1 1"/></svg>',
|
|
106
|
+
"#3b82f6",
|
|
107
|
+
);
|
|
108
|
+
const html = renderCollectionIcon(icon, { size: 16 });
|
|
109
|
+
expect(html).toContain('width="16"');
|
|
110
|
+
expect(html).toContain('height="16"');
|
|
111
|
+
expect(html).toContain("color: #3b82f6");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("renders legacy emoji as a span", () => {
|
|
115
|
+
const html = renderCollectionIcon("📚");
|
|
116
|
+
expect(html).toBe("<span>📚</span>");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("renders legacy text as escaped span", () => {
|
|
120
|
+
const html = renderCollectionIcon("laptop");
|
|
121
|
+
expect(html).toBe("<span>laptop</span>");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("escapes HTML in legacy text values", () => {
|
|
125
|
+
const html = renderCollectionIcon('<script>alert("xss")</script>');
|
|
126
|
+
expect(html).not.toContain("<script>");
|
|
127
|
+
expect(html).toContain("<script>");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("returns empty string for null without fallback", () => {
|
|
131
|
+
expect(renderCollectionIcon(null)).toBe("");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("renders default icon for null with fallback", () => {
|
|
135
|
+
const html = renderCollectionIcon(null, { fallback: true });
|
|
136
|
+
expect(html).toContain("<svg");
|
|
137
|
+
expect(html).toContain(DEFAULT_ICON_COLOR);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("uses default size of 24", () => {
|
|
141
|
+
const icon = createIconValue(
|
|
142
|
+
"star",
|
|
143
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M1 1"/></svg>',
|
|
144
|
+
"#f59e0b",
|
|
145
|
+
);
|
|
146
|
+
const html = renderCollectionIcon(icon);
|
|
147
|
+
expect(html).toContain('width="24"');
|
|
148
|
+
expect(html).toContain('height="24"');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("backward compatibility", () => {
|
|
153
|
+
it("handles icon field containing plain emoji from older data", () => {
|
|
154
|
+
// Old collections might have icon: "🎵" stored directly
|
|
155
|
+
const parsed = parseCollectionIcon("🎵");
|
|
156
|
+
expect(parsed).toBeNull();
|
|
157
|
+
|
|
158
|
+
const html = renderCollectionIcon("🎵");
|
|
159
|
+
expect(html).toBe("<span>🎵</span>");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("handles icon field containing text from older data", () => {
|
|
163
|
+
// Old collections might have icon: "laptop" stored directly
|
|
164
|
+
const parsed = parseCollectionIcon("laptop");
|
|
165
|
+
expect(parsed).toBeNull();
|
|
166
|
+
|
|
167
|
+
const html = renderCollectionIcon("laptop");
|
|
168
|
+
expect(html).toBe("<span>laptop</span>");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("constants", () => {
|
|
173
|
+
it("DEFAULT_ICON_NAME is a valid icon", () => {
|
|
174
|
+
const svg = getIconSvg(DEFAULT_ICON_NAME);
|
|
175
|
+
expect(svg).toContain("<svg");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveConfig } from "../resolve-config.js";
|
|
3
|
+
import type { Bindings } from "../../types/bindings.js";
|
|
4
|
+
|
|
5
|
+
function makeEnv(overrides: Partial<Bindings> = {}): Bindings {
|
|
6
|
+
return {
|
|
7
|
+
DB: {} as D1Database,
|
|
8
|
+
R2: undefined as unknown as R2Bucket,
|
|
9
|
+
AUTH_SECRET: "",
|
|
10
|
+
SITE_URL: "https://example.com",
|
|
11
|
+
R2_PUBLIC_URL: "",
|
|
12
|
+
IMAGE_TRANSFORM_URL: "",
|
|
13
|
+
S3_PUBLIC_URL: "",
|
|
14
|
+
STORAGE_DRIVER: "",
|
|
15
|
+
...overrides,
|
|
16
|
+
} as Bindings;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("resolveConfig", () => {
|
|
20
|
+
it("uses defaults when no settings or env overrides", () => {
|
|
21
|
+
const config = resolveConfig(makeEnv(), {});
|
|
22
|
+
|
|
23
|
+
expect(config.siteName).toBe("Jant");
|
|
24
|
+
expect(config.siteDescription).toBe("A microblog powered by Jant");
|
|
25
|
+
expect(config.siteLanguage).toBe("en");
|
|
26
|
+
expect(config.homeDefaultView).toBe("latest");
|
|
27
|
+
expect(config.timeZone).toBe("UTC");
|
|
28
|
+
expect(config.noindex).toBe(false);
|
|
29
|
+
expect(config.pageSize).toBe(20);
|
|
30
|
+
expect(config.rssFeedLimit).toBe(50);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("DB settings override ENV and defaults", () => {
|
|
34
|
+
const config = resolveConfig(
|
|
35
|
+
makeEnv({ SITE_NAME: "FromEnv" } as Partial<Bindings>),
|
|
36
|
+
{ SITE_NAME: "FromDB" },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(config.siteName).toBe("FromDB");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("ENV overrides defaults when DB is empty", () => {
|
|
43
|
+
const config = resolveConfig(
|
|
44
|
+
makeEnv({ SITE_NAME: "FromEnv" } as Partial<Bindings>),
|
|
45
|
+
{},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(config.siteName).toBe("FromEnv");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("resolves siteDescriptionExplicit correctly", () => {
|
|
52
|
+
// Default only -> not explicit
|
|
53
|
+
const config1 = resolveConfig(makeEnv(), {});
|
|
54
|
+
expect(config1.siteDescriptionExplicit).toBe(false);
|
|
55
|
+
|
|
56
|
+
// DB setting -> explicit
|
|
57
|
+
const config2 = resolveConfig(makeEnv(), {
|
|
58
|
+
SITE_DESCRIPTION: "Custom description",
|
|
59
|
+
});
|
|
60
|
+
expect(config2.siteDescriptionExplicit).toBe(true);
|
|
61
|
+
|
|
62
|
+
// ENV setting -> explicit
|
|
63
|
+
const config3 = resolveConfig(
|
|
64
|
+
makeEnv({
|
|
65
|
+
SITE_DESCRIPTION: "Env description",
|
|
66
|
+
} as Partial<Bindings>),
|
|
67
|
+
{},
|
|
68
|
+
);
|
|
69
|
+
expect(config3.siteDescriptionExplicit).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("resolves media URLs from env", () => {
|
|
73
|
+
const config = resolveConfig(
|
|
74
|
+
makeEnv({
|
|
75
|
+
R2_PUBLIC_URL: "https://r2.example.com",
|
|
76
|
+
IMAGE_TRANSFORM_URL: "https://img.example.com",
|
|
77
|
+
S3_PUBLIC_URL: "https://s3.example.com",
|
|
78
|
+
STORAGE_DRIVER: "s3",
|
|
79
|
+
}),
|
|
80
|
+
{},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(config.r2PublicUrl).toBe("https://r2.example.com");
|
|
84
|
+
expect(config.imageTransformUrl).toBe("https://img.example.com");
|
|
85
|
+
expect(config.s3PublicUrl).toBe("https://s3.example.com");
|
|
86
|
+
expect(config.storageDriver).toBe("s3");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("resolves siteAvatarUrl from storage key", () => {
|
|
90
|
+
const config = resolveConfig(
|
|
91
|
+
makeEnv({
|
|
92
|
+
R2_PUBLIC_URL: "https://r2.example.com",
|
|
93
|
+
STORAGE_DRIVER: "r2",
|
|
94
|
+
}),
|
|
95
|
+
{ SITE_AVATAR: "media/2024/01/avatar.jpg" },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(config.siteAvatar).toBe("media/2024/01/avatar.jpg");
|
|
99
|
+
expect(config.siteAvatarUrl).toBe(
|
|
100
|
+
"https://r2.example.com/media/2024/01/avatar.jpg",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns empty siteAvatarUrl when no avatar set", () => {
|
|
105
|
+
const config = resolveConfig(makeEnv(), {});
|
|
106
|
+
expect(config.siteAvatarUrl).toBe("");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("resolves boolean fields correctly", () => {
|
|
110
|
+
const config = resolveConfig(makeEnv(), {
|
|
111
|
+
NOINDEX: "true",
|
|
112
|
+
SHOW_HEADER_AVATAR: "true",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(config.noindex).toBe(true);
|
|
116
|
+
expect(config.showHeaderAvatar).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("resolves authConfigured from AUTH_SECRET", () => {
|
|
120
|
+
const noAuth = resolveConfig(makeEnv(), {});
|
|
121
|
+
expect(noAuth.authConfigured).toBe(false);
|
|
122
|
+
|
|
123
|
+
const withAuth = resolveConfig(makeEnv({ AUTH_SECRET: "supersecret" }), {});
|
|
124
|
+
expect(withAuth.authConfigured).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("parses numeric fields with fallbacks", () => {
|
|
128
|
+
// Valid numbers
|
|
129
|
+
const config1 = resolveConfig(
|
|
130
|
+
makeEnv({
|
|
131
|
+
PAGE_SIZE: "10",
|
|
132
|
+
RSS_FEED_LIMIT: "25",
|
|
133
|
+
}),
|
|
134
|
+
{},
|
|
135
|
+
);
|
|
136
|
+
expect(config1.pageSize).toBe(10);
|
|
137
|
+
expect(config1.rssFeedLimit).toBe(25);
|
|
138
|
+
|
|
139
|
+
// Invalid numbers fall back to defaults
|
|
140
|
+
const config2 = resolveConfig(
|
|
141
|
+
makeEnv({
|
|
142
|
+
PAGE_SIZE: "not-a-number",
|
|
143
|
+
RSS_FEED_LIMIT: "invalid",
|
|
144
|
+
}),
|
|
145
|
+
{},
|
|
146
|
+
);
|
|
147
|
+
expect(config2.pageSize).toBe(20);
|
|
148
|
+
expect(config2.rssFeedLimit).toBe(50);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("resolves fallbacks without DB values", () => {
|
|
152
|
+
const config = resolveConfig(
|
|
153
|
+
makeEnv({ SITE_NAME: "EnvName" } as Partial<Bindings>),
|
|
154
|
+
{ SITE_NAME: "DBName" },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// fallback should use ENV > Default, skipping DB
|
|
158
|
+
expect(config.fallbacks.siteName).toBe("EnvName");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("resolves theme fields from DB settings", () => {
|
|
162
|
+
const config = resolveConfig(makeEnv(), {
|
|
163
|
+
THEME: "blue",
|
|
164
|
+
FONT_THEME: "serif",
|
|
165
|
+
CUSTOM_CSS: "body { color: red; }",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(config.themeId).toBe("blue");
|
|
169
|
+
expect(config.fontThemeId).toBe("serif");
|
|
170
|
+
expect(config.customCSS).toBe("body { color: red; }");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("resolves defaultThemeId from env", () => {
|
|
174
|
+
const config = resolveConfig(
|
|
175
|
+
makeEnv({ DEFAULT_THEME: "dark" } as Partial<Bindings>),
|
|
176
|
+
{},
|
|
177
|
+
);
|
|
178
|
+
expect(config.defaultThemeId).toBe("dark");
|
|
179
|
+
|
|
180
|
+
// Falls back to default
|
|
181
|
+
const config2 = resolveConfig(makeEnv(), {});
|
|
182
|
+
expect(config2.defaultThemeId).toBe("halloween");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -283,14 +283,20 @@ describe("CreatePostSchema", () => {
|
|
|
283
283
|
expect(result.rating).toBeUndefined();
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
-
it("accepts optional
|
|
287
|
-
const result = CreatePostSchema.parse({
|
|
288
|
-
|
|
286
|
+
it("accepts optional collectionIds as array of positive integers", () => {
|
|
287
|
+
const result = CreatePostSchema.parse({
|
|
288
|
+
...validPost,
|
|
289
|
+
collectionIds: [1, 2, 3],
|
|
290
|
+
});
|
|
291
|
+
expect(result.collectionIds).toEqual([1, 2, 3]);
|
|
289
292
|
});
|
|
290
293
|
|
|
291
|
-
it("accepts empty string
|
|
292
|
-
const result = CreatePostSchema.parse({
|
|
293
|
-
|
|
294
|
+
it("accepts empty string collectionIds (transforms to undefined)", () => {
|
|
295
|
+
const result = CreatePostSchema.parse({
|
|
296
|
+
...validPost,
|
|
297
|
+
collectionIds: "",
|
|
298
|
+
});
|
|
299
|
+
expect(result.collectionIds).toBeUndefined();
|
|
294
300
|
});
|
|
295
301
|
|
|
296
302
|
it("accepts optional replyToId", () => {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildThemeStyle } from "../theme.js";
|
|
3
|
+
import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
|
|
4
|
+
|
|
5
|
+
describe("buildThemeStyle", () => {
|
|
6
|
+
it("returns empty string when no theme and no variables", () => {
|
|
7
|
+
expect(buildThemeStyle(undefined)).toBe("");
|
|
8
|
+
expect(buildThemeStyle(undefined, {})).toBe("");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("generates CSS with font overrides only (no color theme)", () => {
|
|
12
|
+
const theme = BUILTIN_FONT_THEMES.find(
|
|
13
|
+
(f) => f.id === "classic-editorial",
|
|
14
|
+
) as (typeof BUILTIN_FONT_THEMES)[number];
|
|
15
|
+
const fontOverrides = {
|
|
16
|
+
"--font-body": theme.bodyFontFamily,
|
|
17
|
+
"--font-heading": theme.headingFontFamily,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const css = buildThemeStyle(undefined, fontOverrides);
|
|
21
|
+
|
|
22
|
+
expect(css).toContain(":root:root");
|
|
23
|
+
expect(css).toContain("--font-body:");
|
|
24
|
+
expect(css).toContain("--font-heading:");
|
|
25
|
+
expect(css).toContain("Charter");
|
|
26
|
+
expect(css).toContain(":root.dark");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("font override merges with color theme", () => {
|
|
30
|
+
const fakeTheme = {
|
|
31
|
+
id: "test",
|
|
32
|
+
name: "Test",
|
|
33
|
+
light: { "--primary": "oklch(0.5 0.1 200)" },
|
|
34
|
+
dark: { "--primary": "oklch(0.7 0.1 200)" },
|
|
35
|
+
};
|
|
36
|
+
const fontOverrides = {
|
|
37
|
+
"--font-body": "Georgia, serif",
|
|
38
|
+
"--font-heading": "Futura, sans-serif",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const css = buildThemeStyle(fakeTheme, fontOverrides);
|
|
42
|
+
|
|
43
|
+
expect(css).toContain("--primary:");
|
|
44
|
+
expect(css).toContain("--font-body: Georgia, serif");
|
|
45
|
+
expect(css).toContain("--font-heading: Futura, sans-serif");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("cssVariables override theme values", () => {
|
|
49
|
+
const fakeTheme = {
|
|
50
|
+
id: "test",
|
|
51
|
+
name: "Test",
|
|
52
|
+
light: { "--font-body": "should-be-overridden" },
|
|
53
|
+
dark: {},
|
|
54
|
+
};
|
|
55
|
+
const overrides = { "--font-body": "Charter, serif" };
|
|
56
|
+
|
|
57
|
+
const css = buildThemeStyle(fakeTheme, overrides);
|
|
58
|
+
|
|
59
|
+
expect(css).toContain("--font-body: Charter, serif");
|
|
60
|
+
expect(css).not.toContain("should-be-overridden");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -6,7 +6,7 @@ describe("TIMEZONES", () => {
|
|
|
6
6
|
expect(TIMEZONES.length).toBeGreaterThan(30);
|
|
7
7
|
const utc = TIMEZONES.find((tz) => tz.value === "UTC");
|
|
8
8
|
expect(utc).toBeDefined();
|
|
9
|
-
expect(utc
|
|
9
|
+
expect(utc?.offset).toBe("+00:00");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it("each entry has required fields", () => {
|
|
@@ -135,4 +135,16 @@ describe("slugify", () => {
|
|
|
135
135
|
it("removes non-word characters", () => {
|
|
136
136
|
expect(slugify("café & résumé")).toBe("caf-rsum");
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
it("converts Chinese characters to pinyin", () => {
|
|
140
|
+
expect(slugify("书评")).toBe("shu-ping");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("handles mixed Chinese and English text", () => {
|
|
144
|
+
expect(slugify("我的 Blog")).toBe("wo-de-blog");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("handles CJK characters with spaces", () => {
|
|
148
|
+
expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
|
|
149
|
+
});
|
|
138
150
|
});
|
|
@@ -41,7 +41,6 @@ function makePost(overrides: Partial<Post> = {}): Post {
|
|
|
41
41
|
bodyHtml: "<p>Hello world</p>",
|
|
42
42
|
quoteText: null,
|
|
43
43
|
rating: null,
|
|
44
|
-
collectionId: null,
|
|
45
44
|
replyToId: null,
|
|
46
45
|
threadId: null,
|
|
47
46
|
deletedAt: null,
|
|
@@ -212,7 +211,6 @@ describe("toPostView", () => {
|
|
|
212
211
|
expect(view.url).toBeUndefined();
|
|
213
212
|
expect(view.quoteText).toBeUndefined();
|
|
214
213
|
expect(view.rating).toBeUndefined();
|
|
215
|
-
expect(view.collectionId).toBeUndefined();
|
|
216
214
|
expect(view.replyToId).toBeUndefined();
|
|
217
215
|
expect(view.threadRootId).toBeUndefined();
|
|
218
216
|
});
|
|
@@ -266,16 +264,14 @@ describe("toPostView", () => {
|
|
|
266
264
|
expect(view.pinned).toBe(false);
|
|
267
265
|
});
|
|
268
266
|
|
|
269
|
-
it("preserves rating
|
|
267
|
+
it("preserves rating when set", () => {
|
|
270
268
|
const view = toPostView(
|
|
271
269
|
makePostWithMedia({
|
|
272
270
|
rating: 5,
|
|
273
|
-
collectionId: 42,
|
|
274
271
|
}),
|
|
275
272
|
EMPTY_CTX,
|
|
276
273
|
);
|
|
277
274
|
expect(view.rating).toBe(5);
|
|
278
|
-
expect(view.collectionId).toBe(42);
|
|
279
275
|
});
|
|
280
276
|
|
|
281
277
|
it("converts media attachments to MediaView", () => {
|