@jant/core 0.3.36 → 0.3.38

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.
Files changed (271) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -1 +1 @@
1
- /*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"標籤和網址是必填的\"],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"Display text for the link\"],\"+owNNn\":[\"文章\"],\"+zy2Nq\":[\"類型\"],\"/0D1Xp\":[\"編輯收藏集\"],\"/DFKdU\":[\"輸入引用...\"],\"/R/sGB\":[\"密碼已成功更改。\"],\"/Rj5P4\":[\"您的姓名\"],\"/eDNhN\":[\"Open with Featured posts\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"這將為您的網站和儀表板設置主題。所有顏色主題都支持深色模式。\"],\"0JkyS7\":[\"Create your first page\"],\"0OGSSc\":[\"Avatar display updated.\"],\"0a6MpL\":[\"新重定向\"],\"0ieXE7\":[\"最高評價\"],\"0yIy82\":[\"尚未有精選文章。\"],\"1Bp4MW\":[\"Images are automatically optimized (resized, converted to WebP). Video, audio, and PDF files are uploaded as-is (max 200MB).\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"筆記\"],\"1Jp3EJ\":[\"Your media library is empty. Upload your first file to get started.\"],\"1Oj1sI\":[\"已保存順序\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"1u8a3u\":[\"Search emojis...\"],\"2N0qpv\":[\"文章標題...\"],\"2TgGnr\":[\"Name, description, language\"],\"2cFU6q\":[\"網站頁腳\"],\"2fUwEY\":[\"選擇媒體\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"頁面標題...\"],\"2z9Tm/\":[\"Nothing published yet. Write your first post to get started.\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302(臨時)\"],\"3uSoGn\":[\"Header Nav Links\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"4/SFQS\":[\"查看網站\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4JBD+x\":[\"保存失敗。請再試一次。\"],\"4KzVT6\":[\"刪除頁面\"],\"4Ml90q\":[\"SEO\"],\"4WO1Hp\":[\"No matching collections.\"],\"4XdN4s\":[\"Profile, password\"],\"4b3oEV\":[\"內容\"],\"4mDPGp\":[\"此頁面的 URL 路徑。使用小寫字母、數字和連字符。\"],\"538Vy5\":[\"尚未有導航項目。請在下方添加頁面、鏈接或啟用系統項目。\"],\"6C8dEg\":[\"附加文本\"],\"6EwmNQ\":[\"Favicon and header icon\"],\"6WdDG7\":[\"頁面\"],\"6YtxFj\":[\"名稱\"],\"6tU2jr\":[\"找不到任何收藏。\"],\"71Xwww\":[\"Invalid request\"],\"7G4SBz\":[\"頁面內容(支持Markdown)...\"],\"7MZxzw\":[\"Password changed.\"],\"7Mk+/h\":[\"更新收藏集\"],\"7Q1KKN\":[\"來源路徑\"],\"7aECQB\":[\"無效或已過期的連結\"],\"7aYVPs\":[\"所有頁面都在導航中\"],\"7nGhhM\":[\"你在想什麼?\"],\"7p5kLi\":[\"儀表板\"],\"7vhWI8\":[\"新密碼\"],\"81nFIS\":[\"Passwords don't match. Make sure both fields are identical.\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"That theme isn't available. Pick another one.\"],\"8HgKQc\":[\"SEO 設定已成功儲存。\"],\"8WX0J+\":[\"您的想法(可選)\"],\"8WtVZw\":[\"無法保存帖子。請再試一次。\"],\"8ZsakT\":[\"密碼\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8qX8Jl\":[\"選擇一個字體搭配以供您的網站使用。所有選項均使用系統字體以加快加載速度。\"],\"8tM8+a\":[\"儲存為草稿\"],\"8xE385\":[\"添加到導航\"],\"9+vGLh\":[\"自訂 CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"9rS2kh\":[\"Add More\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"Ap948/\":[\"Listed\"],\"AyHO4m\":[\"這個收藏是關於什麼的?\"],\"Az4JB1\":[\"Use Featured as default home view\"],\"B373X+\":[\"編輯文章\"],\"B495Gs\":[\"檔案館\"],\"BjF0Jv\":[\"僅限小寫字母、數字和連字符\"],\"Cl55aD\":[\"當前密碼不正確。\"],\"D3uuEX\":[\"尚未選擇任何媒體。\"],\"D558v3\":[\"A display name is required.\"],\"D9Oea+\":[\"永久鏈接\"],\"DCKkhU\":[\"當前密碼\"],\"DHhJ7s\":[\"上一頁\"],\"DMvU/i\":[\"Write your first post\"],\"DPfwMq\":[\"完成\"],\"DVljCN\":[\"Choose a page…\"],\"DoJzLz\":[\"收藏夾\"],\"E80cJw\":[\"刪除此媒體將永久從存儲中移除它。\"],\"EB4HRn\":[\"No pages yet. Create one to add static content to your site.\"],\"EEYbdt\":[\"發佈\"],\"EGwzOK\":[\"完成設置\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"FESYvt\":[\"為視障人士描述這個...\"],\"FGrimz\":[\"新帖子\"],\"FdE+3J\":[\"No matching pages.\"],\"FkMol5\":[\"精選\"],\"Fxf4jq\":[\"描述(可選)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"身份驗證未配置\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"建立您的管理員帳戶。\"],\"GbVAnd\":[\"此密碼重設連結無效或已過期。請生成一個新的連結。\"],\"GlEzsR\":[\"為搜尋引擎和訂閱閱讀器提供的簡短介紹。僅限純文字。\"],\"GorKul\":[\"歡迎來到 Jant\"],\"GrZ6fH\":[\"新頁面\"],\"GvJQun\":[\"No featured posts. Mark a post as featured to highlight it here.\"],\"GxkJXS\":[\"上傳中...\"],\"H29JXm\":[\"+ ALT\"],\"H4lgRd\":[\"Authentication isn't set up. Check your server config.\"],\"HNEHJP\":[\"Demo credentials are pre-filled — hit Sign In to continue.\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"未找到任何帖子。\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"Save Changes\"],\"IagCbF\":[\"網址\"],\"IdYvsa\":[\"Post updated.\"],\"J4FNfC\":[\"此集合中沒有帖子。\"],\"J6bLeg\":[\"添加自定義連結到任何 URL\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"JcD7qf\":[\"More actions\"],\"Jed1wB\":[\"需要幫助嗎?請訪問<0>文檔</0>。\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 將媒體嵌入到您的帖子中。\"],\"KbS2K9\":[\"重設密碼\"],\"KdSsVl\":[\"作者(可選)\"],\"KiJn9B\":[\"備註\"],\"KmGXnO\":[\"您確定要刪除這篇文章嗎?這個操作無法撤銷。\"],\"KsIZ3c\":[\"Footer saved successfully.\"],\"KuCcWu\":[\"顯示在所有文章和頁面的底部。支持Markdown。\"],\"L0gres\":[\"The rest will be tucked into a ··· menu\"],\"L85WcV\":[\"縮略名\"],\"LdyooL\":[\"鏈接\"],\"LkA8jz\":[\"添加替代文字\"],\"LkvLQe\":[\"No pages yet.\"],\"LsskIi\":[\"URL redirects\"],\"M1RvTd\":[\"點擊圖片以查看完整大小\"],\"M2kIWU\":[\"字型主題\"],\"M6CbAU\":[\"切換編輯面板\"],\"M8kJqa\":[\"草稿\"],\"M8lheL\":[\"最大可見導航連結\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"標題\"],\"MLSRl9\":[\"引用文本\"],\"MWBOxm\":[\"收藏(可選)\"],\"MZbQHL\":[\"未找到結果。\"],\"MdMyne\":[\"來源連結(選填)\"],\"Mhf/H/\":[\"建立重定向\"],\"MnbH31\":[\"頁面\"],\"MqghUt\":[\"搜尋帖子...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"儲存 CSS\"],\"Naqg3G\":[\"未提供檔案\"],\"O3oNi5\":[\"電子郵件\"],\"OCNZaU\":[\"重定向來源的路徑\"],\"ODiSoW\":[\"尚未有帖子。\"],\"ONWvwQ\":[\"上傳\"],\"OVSkIF\":[\"敏捷的棕色狐狸跳過懶惰的狗。\"],\"OeUWA7\":[\"新增頁面\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PJnyHS\":[\"Max visible links saved\"],\"PKhdhq\":[\"Links shown in header\"],\"PZ7HJ8\":[\"部落格頭像\"],\"Pbm2/N\":[\"創建收藏夾\"],\"QEbNBb\":[\"Path (e.g. /archive) or full URL (e.g. https://example.com)\"],\"QLkhbH\":[\"被引用的文本...\"],\"Qjlym2\":[\"無法創建帳戶\"],\"QyDt3L\":[\"File uploaded.\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"連結\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 則帖子的主題\"],\"RxsRD6\":[\"時區\"],\"S79Ozx\":[\"Profile updated.\"],\"SGJDS5\":[\"儲存未配置\"],\"SJmfuf\":[\"網站名稱\"],\"ST+lN2\":[\"尚未上傳任何媒體。\"],\"T0bsor\":[\"設置已成功保存。\"],\"TEhb9O\":[\"Add Divider\"],\"TNFigk\":[\"預設首頁視圖\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"設定\"],\"U5v6Gh\":[\"編輯頁面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"Save Settings\"],\"UIMXHD\":[\"Remove Divider\"],\"UeOiKl\":[\"無效的輸入\"],\"Ui5/i3\":[\"允許搜尋引擎索引我的網站是可以的\"],\"Uj/btJ\":[\"在我的網站標頭中顯示頭像\"],\"UxKoFf\":[\"導航\"],\"UzGRD9\":[\"Home view saved\"],\"V4WsyL\":[\"新增連結\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"頭像顯示設置已成功保存。\"],\"VhMDMg\":[\"更改密碼\"],\"Vn3jYy\":[\"導航項目\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"一般設定\"],\"WmZ/rP\":[\"到路徑\"],\"WpXcBJ\":[\"Nothing here yet.\"],\"X+8FMk\":[\"Current password doesn't match. Try again.\"],\"X1G9eY\":[\"Navigation Preview\"],\"XRuL0i\":[\"Create page\"],\"XrnWzN\":[\"已發佈!\"],\"XxJL62\":[\"Custom styling\"],\"Y+7JGK\":[\"創建頁面\"],\"Y75ho6\":[\"Other pages\"],\"YIix5Y\":[\"搜尋...\"],\"Z2lfX1\":[\"選擇圖示\"],\"Z3FXyt\":[\"載入中...\"],\"Z6NwTi\":[\"儲存為草稿\"],\"ZGs2so\":[\"Delete this collection permanently? Posts inside won't be removed.\"],\"ZM33w8\":[\"Visit Blog\"],\"ZQKLI1\":[\"危險區域\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"引用\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"aAIQg2\":[\"外觀\"],\"aHTB7P\":[\"附加在您帖子上的補充內容\"],\"aT4jc4\":[\"無效的電子郵件或密碼\"],\"aaGV/9\":[\"New Link\"],\"ajBsih\":[\"發佈文章成功。\"],\"alKG0+\":[\"字型主題\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"關於這個部落格\"],\"b+/jO6\":[\"301(永久)\"],\"b+FyBD\":[\"Add page to navigation\"],\"b+JhJf\":[\"Max visible links\"],\"b4VwHs\":[\"未提供檔案。\"],\"bDqhXY\":[\"刪除失敗。請再試一次。\"],\"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\":[\"丟棄\"],\"c9l3fG\":[\"Are you sure you want to delete this collection?\"],\"cSDy01\":[\"Custom CSS updated.\"],\"cTUByn\":[\"最新的在前\"],\"ccaIM9\":[\"更多連結\"],\"cnGeoo\":[\"刪除\"],\"d3LAu6\":[\"No media attached.\"],\"dEgA5A\":[\"取消\"],\"dStw5E\":[\"將現有頁面添加到您的導航中\"],\"dmCcPs\":[\"這是用於您的網站圖標和蘋果觸控圖標。為了獲得最佳效果,請上傳至少 180x180 像素的正方形圖片。\"],\"dtQNkT\":[\"選擇的字體主題無效。\"],\"dyJ1Db\":[\"Back to site\"],\"e6Jr7Q\":[\"← 返回收藏夾\"],\"eKSKaB\":[\"No posts match this filter.\"],\"ePK91l\":[\"編輯\"],\"eWLklq\":[\"引用\"],\"ebQKK7\":[\"Site\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"示範帳戶已預填。只需點擊登入。\"],\"etJ+C8\":[\"No redirects yet. Create one to forward traffic from old URLs.\"],\"etgedT\":[\"Emojis\"],\"f/bxrN\":[\"名稱是必填的。\"],\"f6e0Ry\":[\"Article\"],\"f8fH8W\":[\"Design\"],\"fDGOiR\":[\"密碼不匹配。\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fqDzSu\":[\"評分\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"Un-nav\"],\"g98BZB\":[\"Header links, featured\"],\"gDx5MG\":[\"Edit Link\"],\"gJH6Bs\":[\"替代文字改善可及性\"],\"gOwwEy\":[\"儲存空間未配置。\"],\"gj52YE\":[\"This collection is empty. Add posts from the editor.\"],\"gpaPhA\":[\"Helps screen readers describe the image\"],\"hDRU5q\":[\"找到 \",[\"0\"],\" 個結果\"],\"hG89Ed\":[\"Image\"],\"hGiKlz\":[\"Settings updated.\"],\"hQAbqI\":[\"尚未有頁面。創建您的第一個頁面以開始使用。\"],\"hWOZIv\":[\"請輸入您的新密碼。\"],\"hXzOVo\":[\"下一頁\"],\"he3ygx\":[\"複製\"],\"heSQoS\":[\"粘貼一個網址...\"],\"hmXTCY\":[\"選擇的主題無效。\"],\"hqwRVF\":[\"Couldn't update your profile. Try again in a moment.\"],\"hrL0Be\":[\"圖示(可選)\"],\"i0vDGK\":[\"排序順序\"],\"i6nDCI\":[\"Choose a new password.\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"網址(可選)\"],\"iEUzMn\":[\"系統\"],\"iEqmSU\":[\"自訂 CSS 已成功儲存。\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"idD8Ev\":[\"Saved\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"已發佈的頁面可以通過其標識符訪問。草稿不可見。\"],\"jUV7CU\":[\"上傳頭像\"],\"jVUmOK\":[\"支援Markdown\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"你在想什麼...\"],\"k1ifdL\":[\"處理中...\"],\"kI1qVD\":[\"格式\"],\"kL1h6U\":[\"移除分隔線\"],\"kNiQp6\":[\"置頂\"],\"kPMIr+\":[\"給它一個標題...\"],\"kd7eBB\":[\"Create Link\"],\"kfcRb0\":[\"Avatar\"],\"kj6ppi\":[\"條目\"],\"kr39oD\":[\"No collections yet. Start one to organize posts by topic.\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"搜尋圖示...\"],\"l6ANt9\":[\"最低評價\"],\"lO1Oow\":[\"上傳成功!\"],\"m16K6M\":[\"Click to retry all\"],\"m16xKo\":[\"新增\"],\"mO5HMZ\":[\"All pages are already in navigation.\"],\"mTOYla\":[\"View all posts →\"],\"mcmuCe\":[\"Icons\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登入\"],\"n6QD94\":[\"最舊的在前\"],\"nFukaP\":[\"Wrong email or password. Check your credentials and try again.\"],\"nWRfmt\":[\"Typography\"],\"o21Y+P\":[\"條目\"],\"o4dofa\":[\"AUTH_SECRET 未配置\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"顏色主題\"],\"oSiRP0\":[\"系統連結\"],\"oYPBa0\":[\"更新頁面\"],\"p2/GCq\":[\"確認密碼\"],\"pB0OKE\":[\"新分隔線\"],\"pI2MWS\":[\"Search pages…\"],\"pRhYH2\":[\"收藏中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pnve/d\":[\"個人資料已成功儲存。\"],\"psoxDF\":[\"That font theme isn't available. Pick another one.\"],\"q+hNag\":[\"集合\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"添加媒體\"],\"qt89I8\":[\"草稿已保存。\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"顏色主題\"],\"rTP4rB\":[\"This file will be permanently removed from storage. Posts using it will show a broken link.\"],\"rdUucN\":[\"預覽\"],\"rlonmB\":[\"Couldn't delete. Try again in a moment.\"],\"rzNUSl\":[\"包含 1 則貼文的主題\"],\"sDGoxy\":[\"切換內建導航項目。啟用的項目會與頁面和連結一起顯示在您的導航中。\"],\"sGajR7\":[\"線程開始\"],\"smzF8S\":[\"顯示 \",[\"remainingCount\"],\" 個更多 \",[\"0\"]],\"ssqvZi\":[\"保存個人資料\"],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t/gII1\":[\"Delete this post permanently? This can't be undone.\"],\"tKlWWY\":[\"Emoji\"],\"tQCppt\":[\"Couldn't save your post. Try again in a moment.\"],\"tb99Fd\":[\"Post published.\"],\"tfDRzk\":[\"保存\"],\"tfQNeI\":[\"No pages found.\"],\"tfrt7B\":[\"未配置任何重定向。\"],\"thAGHG\":[\"SEO settings updated.\"],\"tiq7kl\":[\"頁面 \",[\"page\"]],\"toJdZA\":[\"Reorder\"],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"已發佈\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"狀態\"],\"vERlcd\":[\"個人資料\"],\"vGjmyl\":[\"Deleted\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vXIe7J\":[\"語言\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"添加自定義 CSS 以覆蓋任何樣式。使用數據屬性,如 [data-page]、[data-post]、[data-format] 來針對特定元素。\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzU4k9\":[\"新收藏集\"],\"w8Rv8T\":[\"標籤是必需的\"],\"wEF6Ix\":[\"目的地路徑或 URL\"],\"wK4H1r\":[\"Visit Site\"],\"wK4OTM\":[\"標題(選填)\"],\"wL3cK8\":[\"最新\"],\"wM5UXj\":[\"刪除媒體\"],\"wRR604\":[\"頁面\"],\"wc+17X\":[\"/* 您的自訂 CSS 在這裡 */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"無標題\"],\"x+doid\":[\"圖片會自動優化:調整大小至最大 1920 像素,轉換為 WebP 格式,並去除元數據。\"],\"x0mzE0\":[\"創建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xVvw1i\":[\"This reset link is no longer valid. Request a new one to continue.\"],\"xYilR2\":[\"媒體\"],\"y0R9F0\":[\"帖子已成功更新。\"],\"y28hnO\":[\"文章\"],\"y2o/Y0\":[\"This Link Has Expired\"],\"yQ2kGp\":[\"載入更多\"],\"ycM1Xg\":[\"No results. Try different keywords.\"],\"yjkELF\":[\"確認新密碼\"],\"yz7wBu\":[\"關閉\"],\"yzF66j\":[\"連結\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 個結果\"],\"zBFr9G\":[\"粘貼一篇長文章、AI 回應或任何文本...\\n\\nMarkdown 格式將被保留。\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL安全識別碼(小寫、數字、連字符)。對於CJK標題,slug將在伺服器上自動生成。\"],\"zl926n\":[\"When off, visitors see your latest posts first\"],\"zucql+\":[\"菜單\"]}")as Messages;
1
+ /*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+AXdXp\":[\"標籤和 URL 為必填\"],\"+zy2Nq\":[\"類型\"],\"/DFKdU\":[\"輸入引述...\"],\"/eDNhN\":[\"以精選貼文開啟\"],\"/rTz0M\":[\"音訊\"],\"0OGSSc\":[\"頭像顯示已更新。\"],\"0ieXE7\":[\"評分最高\"],\"1DBGsz\":[\"筆記\"],\"1F6Mzc\":[\"目前尚無導覽項目。新增連結或在下方啟用系統項目。\"],\"1Oj1sI\":[\"排序已儲存\"],\"1u8a3u\":[\"搜尋表情符號...\"],\"2B7HLH\":[\"New post\"],\"2TgGnr\":[\"名稱、描述、語言\"],\"2cFU6q\":[\"網站頁尾\"],\"3Cw1AI\":[\"新增集合\"],\"3VrybB\":[\"重新導向\"],\"3Yvsaz\":[\"302 (暫時)\"],\"3wKq0C\":[\"無法儲存。 請稍後再試。\"],\"4Jge8E\":[\"Active Sessions\"],\"4Ml90q\":[\"SEO\"],\"4WO1Hp\":[\"找不到符合的集合。\"],\"4cEClj\":[\"Sessions\"],\"68qugd\":[\"變更您的登入密碼\"],\"6EwmNQ\":[\"網站小圖示與頁首圖示\"],\"6V3Ea3\":[\"已複製\"],\"6lGV3K\":[\"顯示較少\"],\"7FaY4u\":[\"使用方式\"],\"7MZxzw\":[\"密碼已變更.\"],\"7d1a0d\":[\"Public\"],\"7nGhhM\":[\"在想什麼?\"],\"7vhWI8\":[\"新密碼\"],\"81nFIS\":[\"密碼不相符. 請確保兩個欄位相同.\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"該主題不可用。請選擇其他主題。\"],\"8Btgys\":[\"草稿已刪除.\"],\"8U2Z7f\":[\"新增自訂網址\"],\"8WX0J+\":[\"你的想法(選填)\"],\"8ZsakT\":[\"密碼\"],\"8bpHix\":[\"無法建立您的帳戶。請檢查資料後再試一次。\"],\"8qX8Jl\":[\"為您的網站選擇字型配對。所有選項皆使用系統字型以快速載入。\"],\"8tM8+a\":[\"儲存為草稿\"],\"9+vGLh\":[\"自訂 CSS\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AyHO4m\":[\"這個收藏是關於什麼?\"],\"B495Gs\":[\"封存\"],\"B4ESok\":[\"API 參考\"],\"Bmaby2\":[\"所有格式\"],\"D9Oea+\":[\"永久連結\"],\"DCKkhU\":[\"目前密碼\"],\"DHhJ7s\":[\"上一頁\"],\"DOx286\":[\"草稿已還原。\"],\"DPfwMq\":[\"完成\"],\"DoJzLz\":[\"收藏集\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"完成設定\"],\"EO3I6h\":[\"上傳失敗。請稍後再試。\"],\"Enslfm\":[\"目的地\"],\"FESYvt\":[\"為視障者描述此內容...\"],\"FM+KeU\":[\"尚無草稿. 儲存草稿後將會顯示在此.\"],\"FMgjDM\":[\"切換內建導覽項目. 已啟用的項目會連同連結一起出現在您的導覽中.\"],\"FkMol5\":[\"精選\"],\"Fxf4jq\":[\"描述(選填)\"],\"GX2VMa\":[\"建立您的管理員帳號.\"],\"GXsAby\":[\"撤銷\"],\"GlEzsR\":[\"供搜尋引擎與訂閱閱讀器使用的簡短介紹。僅限純文字。\"],\"GorKul\":[\"歡迎使用 Jant\"],\"GvJQun\":[\"目前沒有精選貼文。將貼文標記為精選即可在此處突顯\"],\"GxkJXS\":[\"上傳中...\"],\"H29JXm\":[\"+ 替代文字\"],\"H4lgRd\":[\"身分驗證尚未設定. 請檢查您的伺服器設定.\"],\"HG79RB\":[\"Post as Private\"],\"HKH+W+\":[\"Data\"],\"HNEHJP\":[\"示範憑證已預先填入 — 按「登入」以繼續。\"],\"Hp1l6f\":[\"Current\"],\"I6gXOa\":[\"路徑\"],\"ICsA6P\":[\"You have unsaved changes\"],\"IW5PBo\":[\"複製 Token\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"回覆\"],\"J6bLeg\":[\"新增自訂連結至任意 URL\"],\"JL7LF5\":[\"available CSS variables, data attributes, and examples.\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"請立即複製您的 token — 之後不會再顯示。\"],\"JuN5GC\":[\"未選取任何檔案。請選擇要上傳的檔案。\"],\"KbS2K9\":[\"重設密碼\"],\"KdSsVl\":[\"作者 (選填)\"],\"KiJn9B\":[\"筆記\"],\"KlZ+t+\":[\"%name% + %count% more\"],\"KuCcWu\":[\"顯示於所有文章與頁面的底部。支援 Markdown.\"],\"L0gres\":[\"其餘項目會被收納到 ··· 選單\"],\"L85WcV\":[\"網址別名\"],\"LcvzvX\":[\"輕觸以重試\"],\"LdyooL\":[\"連結\"],\"LkA8jz\":[\"新增替代文字\"],\"M2kIWU\":[\"字型主題\"],\"M6CbAU\":[\"切換編輯面板\"],\"M8kJqa\":[\"草稿\"],\"MHrjPM\":[\"標題\"],\"MSc/Yq\":[\"Do you want to publish your changes or discard them?\"],\"MXSt4t\":[\"建立於 \",[\"0\"]],\"MdMyne\":[\"來源連結(選填)\"],\"MqghUt\":[\"搜尋文章...\"],\"Myqkib\":[\"建立一個集合以開始。\"],\"NU2Fqi\":[\"儲存 CSS\"],\"NhHXCr\":[\"自訂 URL 路徑 (不含前導斜線)\"],\"Nldjdr\":[\"尚未有自訂網址。建立一個來新增轉址或為文章設定自訂路徑。\"],\"O1367B\":[\"所有收藏集\"],\"O3oNi5\":[\"電子郵件\"],\"OVSkIF\":[\"敏捷的棕色狐狸躍過那隻懶惰的狗。\"],\"PJnyHS\":[\"已儲存最大可見導覽連結數\"],\"PKhdhq\":[\"頁首顯示的連結數量\"],\"PZ7HJ8\":[\"部落格頭像\"],\"PxJ9W6\":[\"產生 Token\"],\"Qnrzvb\":[\"啟用的 API 權杖\"],\"QyDt3L\":[\"檔案已上傳。\"],\"Rj01Fz\":[\"連結\"],\"RxsRD6\":[\"時區\"],\"S8NCfs\":[\"儲存到草稿,稍後可編輯並發佈。\"],\"SJmfuf\":[\"網站名稱\"],\"SKZhW9\":[\"令牌名稱\"],\"TEhb9O\":[\"新增分隔線\"],\"TWt5I9\":[\"還有 \",[\"hiddenCount\"],\" 則 \",[\"0\"]],\"UIMXHD\":[\"移除分隔線\"],\"Ui5/i3\":[\"允許搜尋引擎索引我的網站\"],\"Uj/btJ\":[\"在我的網站標頭顯示頭像\"],\"UxKoFf\":[\"導覽\"],\"UzGRD9\":[\"首頁檢視已儲存\"],\"V4WsyL\":[\"新增連結\"],\"VhMDMg\":[\"變更密碼\"],\"Vn3jYy\":[\"導覽項目\"],\"WDcQq9\":[\"未列出\"],\"Weq9zb\":[\"一般\"],\"WpXcBJ\":[\"這裡還沒有任何內容.\"],\"X+8FMk\":[\"目前的密碼不符。請再試一次。\"],\"X1G9eY\":[\"導覽預覽\"],\"XrnWzN\":[\"已發佈!\"],\"XxJL62\":[\"自訂樣式\"],\"Y/F35r\":[\"使用 curl 建立貼文:\"],\"Y6hOOP\":[\"Signed in \",[\"0\"]],\"YIix5Y\":[\"搜尋...\"],\"YdG2RF\":[\"Export Site\"],\"Z6NwTi\":[\"儲存為草稿\"],\"ZGs2so\":[\"永久刪除此收藏?其中的文章不會被刪除。\"],\"ZQKLI1\":[\"危險區域\"],\"ZhhOwV\":[\"引用\"],\"ZiooJI\":[\"API 令牌\"],\"ZmSeP+\":[\"要儲存為草稿嗎?\"],\"ZmUkwN\":[\"新增自訂連結至導覽列\"],\"a14mj8\":[\"Unknown device\"],\"aFkzVF\":[\"目標貼文或集合的 URL slug\"],\"aHTB7P\":[\"附加於您的貼文的補充內容\"],\"alKG0+\":[\"字型主題\"],\"an5hVd\":[\"圖片\"],\"anibOb\":[\"關於本部落格\"],\"any7NR\":[\"Theming guide\"],\"b+/jO6\":[\"301 (永久)\"],\"bHYIks\":[\"登出\"],\"bzSI52\":[\"放棄\"],\"c3MN2z\":[\"所有可用的端點和請求格式.\"],\"cSDy01\":[\"自訂 CSS 已更新。\"],\"cTUByn\":[\"由新到舊\"],\"ccaIM9\":[\"更多連結\"],\"cgmi4V\":[\"刪除草稿\"],\"cnGeoo\":[\"刪除\"],\"d/o/BH\":[\"無法發布。已儲存為草稿。\"],\"dEgA5A\":[\"取消\"],\"dmCcPs\":[\"這會用於你的 favicon 和 apple-touch-icon。為達最佳效果,請上傳至少 180x180 像素的正方形圖片。\"],\"ePK91l\":[\"編輯\"],\"eRujag\":[\"Post as Featured\"],\"eWLklq\":[\"引用\"],\"ebQKK7\":[\"網站\"],\"egK+Yy\":[\"用於腳本與自動化的 Bearer 令牌\"],\"ehj/zN\":[\"重新導向類型\"],\"eneWvv\":[\"草稿\"],\"etgedT\":[\"表情符號\"],\"f4MAoA\":[\"部分上傳失敗。已儲存為草稿。\"],\"f8fH8W\":[\"設計\"],\"fFDkGR\":[\"Sessions, password, export\"],\"fMPkxb\":[\"顯示更多\"],\"fqDzSu\":[\"評分\"],\"ft3WqP\":[\"套用於整個網站,包括管理頁面。所有主題皆支援深色模式。\"],\"fttd2R\":[\"我的收藏\"],\"g98BZB\":[\"頁首連結,精選\"],\"gj52YE\":[\"此收藏為空。請從編輯器新增文章。\"],\"gpaPhA\":[\"協助螢幕閱讀器描述圖片\"],\"gtQsRO\":[\"建立自訂網址\"],\"hDRU5q\":[\"找到 \",[\"0\"],\" 筆結果\"],\"hFkbiy\":[\"No active sessions found.\"],\"hGiKlz\":[\"設定已更新.\"],\"hGmyDl\":[\"權杖讓你在不用登入的情況下,從腳本、捷徑和其他工具存取 API。\"],\"hXzOVo\":[\"下一頁\"],\"harFs3\":[\"重新導向與自訂路徑\"],\"heSQoS\":[\"貼上網址...\"],\"i0vDGK\":[\"排序順序\"],\"i6nDCI\":[\"請選擇新密碼.\"],\"iEUzMn\":[\"系統\"],\"iH8pgl\":[\"Back\"],\"idD8Ev\":[\"已儲存\"],\"j5nQL2\":[\"例如 iOS Shortcuts\"],\"jUV7CU\":[\"上傳頭像\"],\"jVUmOK\":[\"支援 Markdown\"],\"jgBjXJ\":[\"撤銷此權杖?任何使用它的腳本將會停止運作。\"],\"ji7oVU\":[\"編輯貼文\"],\"jpctdh\":[\"檢視\"],\"jvyYZG\":[\"在想什麼...\"],\"k1ifdL\":[\"處理中...\"],\"k97t+V\":[\"Download as a Zola static site (.zip)\"],\"kNiQp6\":[\"已釘選\"],\"kPMIr+\":[\"為連結加上標題...\"],\"ke1gWS\":[\"自訂 URL\"],\"kfcRb0\":[\"頭像\"],\"kj6ppi\":[\"項目\"],\"kr39oD\":[\"尚未建立任何收藏集。建立一個以主題整理貼文。\"],\"l/6JHD\":[\"搜尋圖示...\"],\"l6ANt9\":[\"評分最低\"],\"lLW3vJ\":[\"目標網址別名\"],\"m16xKo\":[\"新增\"],\"mKT7g0\":[\"文字附件\"],\"mSNmrX\":[\"列出貼文:\"],\"mcmuCe\":[\"圖示\"],\"n1ekoW\":[\"登入\"],\"n6QD94\":[\"由舊到新\"],\"nFukaP\":[\"電子郵件或密碼錯誤。請檢查您的登入資訊後重試。\"],\"nWRfmt\":[\"排版\"],\"o/vNDE\":[\"lets you override any theme variable.\"],\"o21Y+P\":[\"項目\"],\"oKOOsY\":[\"色彩主題\"],\"oSiRP0\":[\"系統連結\"],\"p2/GCq\":[\"確認密碼\"],\"pS8mgN\":[\"Manage active sign-in sessions\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"psoxDF\":[\"該字型主題不可用。請選擇其他一個。\"],\"q+hNag\":[\"集合\"],\"q8RviX\":[\"有標題\"],\"qt89I8\":[\"草稿已儲存。\"],\"r09tue\":[\"沒有貼文符合這些篩選條件。請調整您的選擇或清除所有篩選條件。\"],\"rFmBG3\":[\"色彩主題\"],\"rlonmB\":[\"無法刪除. 請稍後再試.\"],\"sER+bs\":[\"檔案\"],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"tKlWWY\":[\"表情符號\"],\"tLO9d0\":[\"Post Unlisted\"],\"tfDRzk\":[\"儲存\"],\"thAGHG\":[\"已更新 SEO 設定.\"],\"tiq7kl\":[\"第 \",[\"page\"],\" 頁\"],\"toJdZA\":[\"重新排序\"],\"u3wRF+\":[\"已發佈\"],\"u6KOjV\":[\"Want more control?\"],\"ui6aMF\":[\"These devices are currently signed in to your account. Revoke any session you don't recognize.\"],\"vGjmyl\":[\"已刪除\"],\"vSJd18\":[\"影片\"],\"vXCC6J\":[\"有些地方看起來不對。請檢查表單並再試一次。\"],\"vXIe7J\":[\"語言\"],\"vmQmHx\":[\"新增自訂 CSS 以覆寫任何樣式。使用像 [data-page], [data-post], [data-format] 的資料屬性來針對特定元素。\"],\"vpSPA1\":[\"驗證祕鑰遺失。請檢查您的環境變數。\"],\"vzU4k9\":[\"建立新集合\"],\"w8Rv8T\":[\"標籤為必填\"],\"wJ+GRy\":[\"All visibility\"],\"wL3cK8\":[\"最新\"],\"wc+17X\":[\"/* 在此放入自訂 CSS */\"],\"wja8aL\":[\"無標題\"],\"wm3Zlr\":[\"所有年份\"],\"xCWek4\":[\"檔案儲存尚未設定。請檢查您的伺服器設定。\"],\"xVvw1i\":[\"此重設連結已不再有效。請申請新的連結以繼續。\"],\"xYilR2\":[\"媒體\"],\"xeiujy\":[\"Text\"],\"y28hnO\":[\"發佈\"],\"y2o/Y0\":[\"此連結已過期\"],\"yQ2kGp\":[\"載入更多\"],\"ycM1Xg\":[\"沒有結果。請嘗試不同的關鍵字。\"],\"yjkELF\":[\"確認新密碼\"],\"ylo1I0\":[\"上次使用於 \",[\"0\"]],\"yzF66j\":[\"連結\"],\"z8ajIE\":[\"找到 1 個結果\"],\"zBFr9G\":[\"貼上長文章、AI 回覆或任何文字...\\n\\nMarkdown 格式會被保留.\"],\"zJDAbh\":[\"不儲存\"],\"zennIg\":[\"URL 安全的識別字串(小寫、數字、連字號)。對於 CJK 標題,slug 將在伺服器上自動產生。\"],\"zl926n\":[\"關閉時, 訪客會先看到你的最新文章\"],\"zwBp5t\":[\"Private\"]}")as Messages;
@@ -5,6 +5,7 @@
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 { detectLocaleFromHeader } from "./detect.js";
8
9
  declare module "hono" {
9
10
  interface ContextVariableMap {
10
11
  lang: Locale;
@@ -30,6 +31,11 @@ export function i18nMiddleware(): MiddlewareHandler {
30
31
  const siteLang = allSettings["SITE_LANGUAGE"];
31
32
  if (siteLang && isLocale(siteLang)) {
32
33
  lang = siteLang;
34
+ } else {
35
+ const acceptLanguage = c.req.header("Accept-Language");
36
+ if (acceptLanguage) {
37
+ lang = detectLocaleFromHeader(acceptLanguage);
38
+ }
33
39
  }
34
40
  }
35
41
 
package/src/index.ts CHANGED
@@ -16,18 +16,15 @@ export type {
16
16
  NavItemType,
17
17
  Bindings,
18
18
  Post,
19
- Page,
20
19
  Media,
21
20
  MediaAttachment,
22
21
  PostWithMedia,
23
22
  Collection,
24
23
  NavItem,
25
- Redirect,
24
+ CustomUrl,
26
25
  Setting,
27
26
  CreatePost,
28
27
  UpdatePost,
29
- CreatePage,
30
- UpdatePage,
31
28
  CreateNavItem,
32
29
  UpdateNavItem,
33
30
  CreateCollection,
@@ -35,12 +32,13 @@ export type {
35
32
  AppConfig,
36
33
  // View Model types
37
34
  PostView,
38
- PageView,
39
35
  MediaView,
40
36
  NavItemView,
41
37
  SearchResultView,
42
38
  TimelineItemView,
43
39
  ArchiveGroup,
40
+ // Page props
41
+ ArchiveFilters,
44
42
  // Feed types
45
43
  FeedData,
46
44
  SitemapData,
@@ -57,11 +55,11 @@ export {
57
55
  NAV_ITEM_TYPES,
58
56
  MAX_MEDIA_ATTACHMENTS,
59
57
  MAX_PINNED_POSTS,
58
+ MEDIA_KINDS,
60
59
  } from "./types.js";
61
60
 
62
61
  // Utilities
63
62
  export * as time from "./lib/time.js";
64
- export * as sqid from "./lib/sqid.js";
65
63
  export * as url from "./lib/url.js";
66
64
  export * as markdown from "./lib/markdown.js";
67
65
 
@@ -71,11 +69,11 @@ export {
71
69
  toPostView,
72
70
  toPostViews,
73
71
  toMediaView,
74
- toPageView,
75
72
  toNavItemView,
76
73
  toNavItemViews,
77
74
  toSearchResultView,
78
75
  toArchiveGroups,
76
+ toArchiveGroupsWithMedia,
79
77
  } from "./lib/view.js";
80
78
  export type { MediaContext } from "./lib/view.js";
81
79
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Blurhash Placeholder Tests
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { blurhashToDataUrl } from "../blurhash-placeholder.js";
7
+
8
+ describe("blurhashToDataUrl", () => {
9
+ // A known valid blurhash string
10
+ const HASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
11
+
12
+ it("returns a data URL with BMP MIME type", () => {
13
+ const url = blurhashToDataUrl(HASH);
14
+ expect(url).toMatch(/^data:image\/bmp;base64,[A-Za-z0-9+/=]+$/);
15
+ });
16
+
17
+ it("produces valid BMP header bytes", () => {
18
+ const url = blurhashToDataUrl(HASH, 4, 3);
19
+ const base64 = url.replace("data:image/bmp;base64,", "");
20
+ const binary = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
21
+
22
+ // BMP magic bytes
23
+ expect(binary[0]).toBe(0x42); // 'B'
24
+ expect(binary[1]).toBe(0x4d); // 'M'
25
+
26
+ // File size matches buffer length
27
+ const view = new DataView(binary.buffer);
28
+ expect(view.getUint32(2, true)).toBe(binary.length);
29
+
30
+ // Pixel data offset = 54
31
+ expect(view.getUint32(10, true)).toBe(54);
32
+
33
+ // DIB header size = 40
34
+ expect(view.getUint32(14, true)).toBe(40);
35
+
36
+ // Image dimensions
37
+ expect(view.getInt32(18, true)).toBe(4); // width
38
+ expect(view.getInt32(22, true)).toBe(3); // height
39
+
40
+ // Bits per pixel = 24
41
+ expect(view.getUint16(28, true)).toBe(24);
42
+ });
43
+
44
+ it("uses default dimensions of 4x3", () => {
45
+ const url = blurhashToDataUrl(HASH);
46
+ const base64 = url.replace("data:image/bmp;base64,", "");
47
+ const binary = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
48
+ const view = new DataView(binary.buffer);
49
+
50
+ expect(view.getInt32(18, true)).toBe(4);
51
+ expect(view.getInt32(22, true)).toBe(3);
52
+ });
53
+
54
+ it("respects custom dimensions", () => {
55
+ const url = blurhashToDataUrl(HASH, 8, 6);
56
+ const base64 = url.replace("data:image/bmp;base64,", "");
57
+ const binary = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
58
+ const view = new DataView(binary.buffer);
59
+
60
+ expect(view.getInt32(18, true)).toBe(8);
61
+ expect(view.getInt32(22, true)).toBe(6);
62
+ });
63
+
64
+ it("produces consistent output for the same input", () => {
65
+ const url1 = blurhashToDataUrl(HASH);
66
+ const url2 = blurhashToDataUrl(HASH);
67
+ expect(url1).toBe(url2);
68
+ });
69
+
70
+ it("produces different output for different hashes", () => {
71
+ const url1 = blurhashToDataUrl(HASH);
72
+ const url2 = blurhashToDataUrl("LGF5]+Yk^6#M@-5c,1J5@[or[Q6.");
73
+ expect(url1).not.toBe(url2);
74
+ });
75
+ });
@@ -8,7 +8,6 @@ describe("RESERVED_PATHS", () => {
8
8
  expect(RESERVED_PATHS).toContain("feed");
9
9
  expect(RESERVED_PATHS).toContain("signin");
10
10
  expect(RESERVED_PATHS).toContain("search");
11
- expect(RESERVED_PATHS).toContain("p");
12
11
  expect(RESERVED_PATHS).toContain("c");
13
12
  });
14
13
  });
@@ -0,0 +1,358 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { markdownToTiptapJson } from "../markdown-to-tiptap.js";
3
+ import { renderTiptapJson } from "../tiptap-render.js";
4
+
5
+ interface TiptapMark {
6
+ type: string;
7
+ attrs?: Record<string, unknown>;
8
+ }
9
+
10
+ interface TiptapNode {
11
+ type: string;
12
+ content?: TiptapNode[];
13
+ text?: string;
14
+ marks?: TiptapMark[];
15
+ attrs?: Record<string, unknown>;
16
+ }
17
+
18
+ describe("markdownToTiptapJson", () => {
19
+ function parse(md: string) {
20
+ return JSON.parse(markdownToTiptapJson(md));
21
+ }
22
+
23
+ describe("block elements", () => {
24
+ it("converts a simple paragraph", () => {
25
+ const doc = parse("Hello world");
26
+ expect(doc.type).toBe("doc");
27
+ expect(doc.content).toHaveLength(1);
28
+ expect(doc.content[0].type).toBe("paragraph");
29
+ expect(doc.content[0].content[0].text).toBe("Hello world");
30
+ });
31
+
32
+ it("converts multiple paragraphs", () => {
33
+ const doc = parse("First paragraph.\n\nSecond paragraph.");
34
+ expect(doc.content).toHaveLength(2);
35
+ expect(doc.content[0].content[0].text).toBe("First paragraph.");
36
+ expect(doc.content[1].content[0].text).toBe("Second paragraph.");
37
+ });
38
+
39
+ it("converts headings with correct levels", () => {
40
+ const doc = parse("# H1\n\n## H2\n\n### H3");
41
+ expect(doc.content[0].type).toBe("heading");
42
+ expect(doc.content[0].attrs.level).toBe(1);
43
+ expect(doc.content[1].type).toBe("heading");
44
+ expect(doc.content[1].attrs.level).toBe(2);
45
+ expect(doc.content[2].type).toBe("heading");
46
+ expect(doc.content[2].attrs.level).toBe(3);
47
+ });
48
+
49
+ it("converts fenced code blocks", () => {
50
+ const doc = parse("```javascript\nconsole.log('hi')\n```");
51
+ expect(doc.content[0].type).toBe("codeBlock");
52
+ expect(doc.content[0].attrs.language).toBe("javascript");
53
+ expect(doc.content[0].content[0].text).toBe("console.log('hi')");
54
+ });
55
+
56
+ it("converts code blocks without language", () => {
57
+ const doc = parse("```\nplain code\n```");
58
+ expect(doc.content[0].type).toBe("codeBlock");
59
+ expect(doc.content[0].attrs).toBeUndefined();
60
+ expect(doc.content[0].content[0].text).toBe("plain code");
61
+ });
62
+
63
+ it("converts blockquotes", () => {
64
+ const doc = parse("> Quoted text");
65
+ expect(doc.content[0].type).toBe("blockquote");
66
+ expect(doc.content[0].content[0].type).toBe("paragraph");
67
+ expect(doc.content[0].content[0].content[0].text).toBe("Quoted text");
68
+ });
69
+
70
+ it("converts nested blockquotes", () => {
71
+ const doc = parse("> Outer\n>\n> > Inner");
72
+ expect(doc.content[0].type).toBe("blockquote");
73
+ // The inner blockquote should be nested
74
+ const inner = doc.content[0].content.find(
75
+ (n: Record<string, unknown>) => n.type === "blockquote",
76
+ );
77
+ expect(inner).toBeDefined();
78
+ });
79
+
80
+ it("converts bullet lists", () => {
81
+ const doc = parse("- Item 1\n- Item 2\n- Item 3");
82
+ expect(doc.content[0].type).toBe("bulletList");
83
+ expect(doc.content[0].content).toHaveLength(3);
84
+ expect(doc.content[0].content[0].type).toBe("listItem");
85
+ });
86
+
87
+ it("converts ordered lists", () => {
88
+ const doc = parse("1. First\n2. Second\n3. Third");
89
+ expect(doc.content[0].type).toBe("orderedList");
90
+ expect(doc.content[0].content).toHaveLength(3);
91
+ });
92
+
93
+ it("converts horizontal rules", () => {
94
+ const doc = parse("Above\n\n---\n\nBelow");
95
+ const hr = doc.content.find(
96
+ (n: Record<string, unknown>) => n.type === "horizontalRule",
97
+ );
98
+ expect(hr).toBeDefined();
99
+ });
100
+
101
+ it("converts <!--more--> to moreBreak", () => {
102
+ const doc = parse("Before\n\n<!--more-->\n\nAfter");
103
+ const moreBreak = doc.content.find(
104
+ (n: Record<string, unknown>) => n.type === "moreBreak",
105
+ );
106
+ expect(moreBreak).toBeDefined();
107
+ });
108
+
109
+ it("converts tables", () => {
110
+ const md = "| A | B |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |";
111
+ const doc = parse(md);
112
+ expect(doc.content[0].type).toBe("table");
113
+ const rows = doc.content[0].content;
114
+ expect(rows).toHaveLength(3); // 1 header + 2 body
115
+ expect(rows[0].content[0].type).toBe("tableHeader");
116
+ expect(rows[1].content[0].type).toBe("tableCell");
117
+ });
118
+ });
119
+
120
+ describe("inline elements", () => {
121
+ it("converts bold text", () => {
122
+ const doc = parse("This is **bold** text");
123
+ const content = doc.content[0].content;
124
+ const boldNode = content.find(
125
+ (n: Record<string, unknown>) =>
126
+ n.text === "bold" &&
127
+ Array.isArray(n.marks) &&
128
+ (n.marks as TiptapMark[]).some((m) => m.type === "bold"),
129
+ );
130
+ expect(boldNode).toBeDefined();
131
+ });
132
+
133
+ it("converts italic text", () => {
134
+ const doc = parse("This is *italic* text");
135
+ const content = doc.content[0].content;
136
+ const italicNode = content.find(
137
+ (n: Record<string, unknown>) =>
138
+ n.text === "italic" &&
139
+ Array.isArray(n.marks) &&
140
+ (n.marks as TiptapMark[]).some((m) => m.type === "italic"),
141
+ );
142
+ expect(italicNode).toBeDefined();
143
+ });
144
+
145
+ it("converts inline code", () => {
146
+ const doc = parse("Use `console.log` here");
147
+ const content = doc.content[0].content;
148
+ const codeNode = content.find(
149
+ (n: Record<string, unknown>) =>
150
+ n.text === "console.log" &&
151
+ Array.isArray(n.marks) &&
152
+ (n.marks as TiptapMark[]).some((m) => m.type === "code"),
153
+ );
154
+ expect(codeNode).toBeDefined();
155
+ });
156
+
157
+ it("converts strikethrough text", () => {
158
+ const doc = parse("This is ~~deleted~~ text");
159
+ const content = doc.content[0].content;
160
+ const strikeNode = content.find(
161
+ (n: Record<string, unknown>) =>
162
+ n.text === "deleted" &&
163
+ Array.isArray(n.marks) &&
164
+ (n.marks as TiptapMark[]).some((m) => m.type === "strike"),
165
+ );
166
+ expect(strikeNode).toBeDefined();
167
+ });
168
+
169
+ it("converts links", () => {
170
+ const doc = parse("[click here](https://example.com)");
171
+ const content = doc.content[0].content;
172
+ const linkNode = content.find(
173
+ (n: Record<string, unknown>) =>
174
+ Array.isArray(n.marks) &&
175
+ (n.marks as TiptapMark[]).some(
176
+ (m) =>
177
+ m.type === "link" &&
178
+ (m.attrs as Record<string, unknown>)?.href ===
179
+ "https://example.com",
180
+ ),
181
+ );
182
+ expect(linkNode).toBeDefined();
183
+ expect(linkNode.text).toBe("click here");
184
+ });
185
+
186
+ it("converts images to image nodes", () => {
187
+ const doc = parse("![Alt text](https://example.com/img.png)");
188
+ // Image might be inside a paragraph or as a top-level node
189
+ const findImage = (nodes: TiptapNode[]): TiptapNode | undefined => {
190
+ for (const n of nodes) {
191
+ if (n.type === "image") return n;
192
+ if (n.content) {
193
+ const found = findImage(n.content);
194
+ if (found) return found;
195
+ }
196
+ }
197
+ return undefined;
198
+ };
199
+ const img = findImage(doc.content);
200
+ expect(img).toBeDefined();
201
+ expect(img?.attrs?.src).toBe("https://example.com/img.png");
202
+ expect(img?.attrs?.alt).toBe("Alt text");
203
+ });
204
+
205
+ it("converts nested marks (bold + italic)", () => {
206
+ const doc = parse("***bold and italic***");
207
+ const content = doc.content[0].content;
208
+ // Find a node with both bold and italic marks
209
+ const nested = content.find(
210
+ (n: Record<string, unknown>) =>
211
+ Array.isArray(n.marks) &&
212
+ (n.marks as TiptapMark[]).some((m) => m.type === "bold") &&
213
+ (n.marks as TiptapMark[]).some((m) => m.type === "italic"),
214
+ );
215
+ expect(nested).toBeDefined();
216
+ });
217
+ });
218
+
219
+ it("handles empty input", () => {
220
+ const doc = parse("");
221
+ expect(doc.type).toBe("doc");
222
+ expect(doc.content).toHaveLength(1);
223
+ expect(doc.content[0].type).toBe("paragraph");
224
+ });
225
+ });
226
+
227
+ describe("end-to-end: Markdown → markdownToTiptapJson → renderTiptapJson", () => {
228
+ it("renders paragraphs correctly", () => {
229
+ const json = markdownToTiptapJson("Hello world");
230
+ const html = renderTiptapJson(json);
231
+ expect(html).toBe("<p>Hello world</p>");
232
+ });
233
+
234
+ it("renders headings correctly", () => {
235
+ const json = markdownToTiptapJson("## Section Title");
236
+ const html = renderTiptapJson(json);
237
+ expect(html).toBe("<h2>Section Title</h2>");
238
+ });
239
+
240
+ it("renders bold text correctly", () => {
241
+ const json = markdownToTiptapJson("This is **bold** text");
242
+ const html = renderTiptapJson(json);
243
+ expect(html).toContain("<strong>bold</strong>");
244
+ });
245
+
246
+ it("renders italic text correctly", () => {
247
+ const json = markdownToTiptapJson("This is *italic* text");
248
+ const html = renderTiptapJson(json);
249
+ expect(html).toContain("<em>italic</em>");
250
+ });
251
+
252
+ it("renders links correctly", () => {
253
+ const json = markdownToTiptapJson("[click](https://example.com)");
254
+ const html = renderTiptapJson(json);
255
+ expect(html).toContain('href="https://example.com"');
256
+ expect(html).toContain("click");
257
+ });
258
+
259
+ it("renders code blocks correctly", () => {
260
+ const json = markdownToTiptapJson("```js\nconst x = 1;\n```");
261
+ const html = renderTiptapJson(json);
262
+ expect(html).toContain("<pre><code");
263
+ expect(html).toContain("const x = 1;");
264
+ });
265
+
266
+ it("renders blockquotes correctly", () => {
267
+ const json = markdownToTiptapJson("> Quoted text");
268
+ const html = renderTiptapJson(json);
269
+ expect(html).toContain("<blockquote>");
270
+ expect(html).toContain("Quoted text");
271
+ });
272
+
273
+ it("renders bullet lists correctly", () => {
274
+ const json = markdownToTiptapJson("- Item 1\n- Item 2");
275
+ const html = renderTiptapJson(json);
276
+ expect(html).toContain("<ul>");
277
+ expect(html).toContain("<li>");
278
+ expect(html).toContain("Item 1");
279
+ expect(html).toContain("Item 2");
280
+ });
281
+
282
+ it("renders ordered lists correctly", () => {
283
+ const json = markdownToTiptapJson("1. First\n2. Second");
284
+ const html = renderTiptapJson(json);
285
+ expect(html).toContain("<ol>");
286
+ expect(html).toContain("First");
287
+ expect(html).toContain("Second");
288
+ });
289
+
290
+ it("renders horizontal rules correctly", () => {
291
+ const json = markdownToTiptapJson("Above\n\n---\n\nBelow");
292
+ const html = renderTiptapJson(json);
293
+ expect(html).toContain("<hr>");
294
+ });
295
+
296
+ it("renders <!--more--> correctly", () => {
297
+ const json = markdownToTiptapJson("Before\n\n<!--more-->\n\nAfter");
298
+ const html = renderTiptapJson(json);
299
+ expect(html).toContain("<!--more-->");
300
+ });
301
+
302
+ it("renders tables correctly", () => {
303
+ const json = markdownToTiptapJson("| A | B |\n| --- | --- |\n| 1 | 2 |");
304
+ const html = renderTiptapJson(json);
305
+ expect(html).toContain("<table>");
306
+ expect(html).toContain("<th>");
307
+ expect(html).toContain("<td>");
308
+ });
309
+
310
+ it("renders inline code correctly", () => {
311
+ const json = markdownToTiptapJson("Use `console.log` here");
312
+ const html = renderTiptapJson(json);
313
+ expect(html).toContain("<code>console.log</code>");
314
+ });
315
+
316
+ it("renders strikethrough correctly", () => {
317
+ const json = markdownToTiptapJson("This is ~~deleted~~ text");
318
+ const html = renderTiptapJson(json);
319
+ expect(html).toContain("<s>deleted</s>");
320
+ });
321
+
322
+ it("renders a complex document", () => {
323
+ const md = [
324
+ "# My Post",
325
+ "",
326
+ "This is a **bold** and *italic* paragraph with a [link](https://example.com).",
327
+ "",
328
+ "## Code Example",
329
+ "",
330
+ "```typescript",
331
+ "const x = 42;",
332
+ "```",
333
+ "",
334
+ "- Item 1",
335
+ "- Item 2",
336
+ "",
337
+ "> A wise quote",
338
+ "",
339
+ "---",
340
+ "",
341
+ "Final paragraph.",
342
+ ].join("\n");
343
+
344
+ const json = markdownToTiptapJson(md);
345
+ const html = renderTiptapJson(json);
346
+
347
+ expect(html).toContain("<h1>My Post</h1>");
348
+ expect(html).toContain("<strong>bold</strong>");
349
+ expect(html).toContain("<em>italic</em>");
350
+ expect(html).toContain('href="https://example.com"');
351
+ expect(html).toContain("<h2>Code Example</h2>");
352
+ expect(html).toContain("const x = 42;");
353
+ expect(html).toContain("<ul>");
354
+ expect(html).toContain("<blockquote>");
355
+ expect(html).toContain("<hr>");
356
+ expect(html).toContain("Final paragraph.");
357
+ });
358
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateRandomId } from "../nanoid.js";
3
+
4
+ describe("generateRandomId", () => {
5
+ it("returns a string of the specified length", () => {
6
+ expect(generateRandomId(5)).toHaveLength(5);
7
+ expect(generateRandomId(8)).toHaveLength(8);
8
+ expect(generateRandomId(1)).toHaveLength(1);
9
+ });
10
+
11
+ it("uses only lowercase alphanumeric characters", () => {
12
+ for (let i = 0; i < 50; i++) {
13
+ const id = generateRandomId(10);
14
+ expect(id).toMatch(/^[0-9a-z]+$/);
15
+ }
16
+ });
17
+
18
+ it("generates unique values", () => {
19
+ const ids = new Set<string>();
20
+ for (let i = 0; i < 100; i++) {
21
+ ids.add(generateRandomId(8));
22
+ }
23
+ // With 36^8 possible values, collisions should be essentially impossible
24
+ expect(ids.size).toBe(100);
25
+ });
26
+ });