@jant/core 0.6.15 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+4u2g6\":[\"A ready-made 1:1 PNG for decks, mockups, directories, and other square placements.\"],\"+DPYOZ\":[\"Add a link to your main RSS feed. Change what /feed returns in General.\"],\"+G8qqW\":[\"Collection saved.\"],\"+IJm1Z\":[\"Muted\"],\"+Irvp3\":[\"Everything on this page is ready to use for articles, launch posts, directories, and product coverage.\"],\"+Qaboy\":[\"Favicon\"],\"+fWu2O\":[\"A calmer, warmer accent makes the default theme feel quieter and more intentional.\"],\"+jw/c1\":[\"Delete image\"],\"+nHhRH\":[\"Use \",[\"brandColorName\"]],\"+siMqD\":[\"Journal\"],\"/DFKdU\":[\"Type the quote...\"],\"/PfPLc\":[\"Label (optional)\"],\"/PoNoq\":[\"Edit link\"],\"/Ui2OV\":[\"Use the reverse logo on dark backgrounds.\"],\"/Ybds4\":[\"Primary Jant logo for websites, docs, press coverage, and editorial layouts.\"],\"/rTz0M\":[\"Audio\"],\"0EcUWz\":[\"Discard changes?\"],\"0Lj7or\":[\"Save text attachment?\"],\"0Vlj/m\":[\"This post isn’t published.\"],\"0XDp7X\":[\"Links should read clearly without glowing against the page.\"],\"0cspe/\":[\"Delete row\"],\"0ieXE7\":[\"Highest rated\"],\"11h9eK\":[\"Includes\"],\"15++NM\":[\"Inline emphasis\"],\"1DBGsz\":[\"Notes\"],\"1NeeWI\":[\"Square assets for avatars, apps, browsers, and shared links\"],\"1THMr2\":[\"Brand pack\"],\"1njn7W\":[\"Light\"],\"27KTPP\":[\"Draft preview\"],\"2B7HLH\":[\"New post\"],\"2C7mSG\":[\"Collection link\"],\"2ETv7R\":[\"Tune color in a real reading context\"],\"2HbvFp\":[\"Real post components\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2koDOQ\":[\"Thread accents\"],\"2lKpcz\":[\"Write your first post to get started.\"],\"2q/Q7x\":[\"Visibility\"],\"2sCqzD\":[\"Use this for websites, docs, articles, and other light or neutral surfaces.\"],\"3Cw1AI\":[\"Add Collection\"],\"3lJk5u\":[\"Keep the artwork unchanged.\"],\"3mdteM\":[\"before deciding whether the accent is carrying too much product energy.\"],\"3neqtf\":[\"Thread accent\"],\"3qkggm\":[\"Fullscreen\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"3xi01/\":[\"Look at the footer metadata last, to make sure the accent is not fighting the typography.\"],\"47iMgt\":[\"Editorial interfaces worth borrowing from\"],\"4D09NB\":[\"Link to your collections page\"],\"4GQThz\":[\"No collections yet. Start one to organize threads by topic.\"],\"4HLTdq\":[\"without media\"],\"4J/OYU\":[\"Collection created.\"],\"4eiXo+\":[\"Leave blank to generate one automatically.\"],\"4pV0kE\":[\"Avatar-ready\"],\"51EYZX\":[\"without title\"],\"5dcjwM\":[\"Choose today or an earlier date, or leave it blank to publish now.\"],\"5pAjd8\":[\"Accent should feel present, not loud.\"],\"5sEkBi\":[\"Open raw asset\"],\"61i3cO\":[\"Toggle header row\"],\"6QCZMo\":[\"Add row below\"],\"6UTABI\":[\"Collection order updated.\"],\"6WAK+2\":[\"Use current date\"],\"6Y4BBO\":[\"An abstract editorial layout in warm paper colors\"],\"6cjUDB\":[\"Brand assets\"],\"6lGV3K\":[\"Show less\"],\"6p0JeQ\":[\"to make sure both still feel like they belong to the same product.\"],\"6sVyMq\":[\"Add a URL before posting this link.\"],\"6yCv8j\":[\"Save these changes to the text attachment, discard them, or keep editing.\"],\"74kJNs\":[\"View earlier notes in this thread\"],\"7DvUqV\":[\"Read the page from top to bottom without looking at the swatches.\"],\"7aris6\":[\"March 15\"],\"7d1a0d\":[\"Public\"],\"7hYXO0\":[\"Use this on dark backgrounds, image-backed surfaces, and any placement where the green logo would lose contrast.\"],\"7ibPpM\":[\"Add column after\"],\"7kMW54\":[\"Open raw SVG\"],\"7nGhhM\":[\"What's on your mind?\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8Btgys\":[\"Draft deleted.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8ZsakT\":[\"Password\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8eC78s\":[\"A calmer accent makes\"],\"8tM8+a\":[\"Save as draft\"],\"90IRF2\":[\"This article is here to answer a specific question: does the default accent still feel calm once it has to carry a full reading experience?\"],\"9SHZas\":[\"Shows 'Settings' when logged in, 'Sign in' when logged out\"],\"9aloPG\":[\"References\"],\"9dr9Nh\":[\"Open external link\"],\"9j5qCS\":[\"Table options\"],\"9qWoxS\":[\"Feed\"],\"A1D8Yt\":[\"What the accent should do\"],\"A1taO8\":[\"Search\"],\"A2Vg/u\":[\"Navigation and reading states\"],\"AjHkcv\":[\"Default preview image for social shares and link unfurls.\"],\"AyHO4m\":[\"What's this collection about?\"],\"B1FFMj\":[\"Download Brand Pack\"],\"B495Gs\":[\"Archive\"],\"BdjLtf\":[\"thread\"],\"Bmaby2\":[\"All formats\"],\"C+9df9\":[\"Quoted or highlighted passages should feel like annotations, not warnings.\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"C4TjpG\":[\"Read less\"],\"CAh1km\":[\"collections\"],\"CH3bgf\":[\"RSS feed for this view\"],\"CmBCXY\":[\"Link updated.\"],\"D4em/+\":[\"Logos\"],\"DHhJ7s\":[\"Previous\"],\"DJLY+/\":[\" and try again.\"],\"DOx286\":[\"Draft restored.\"],\"DPfwMq\":[\"Done\"],\"DSJXZM\":[\"Enter a valid date.\"],\"DYlMYF\":[\"Built-in background\"],\"DoJzLz\":[\"Collections\"],\"Du2B9f\":[\"The default accent should support reading first. Start by comparing it against the\"],\"DxwUcG\":[\"Read the palette as content first\"],\"E3NcGH\":[\"Square logo PNG\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EHWwm1\":[\"The default accent should feel written, not branded.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"EQNPYo\":[\"Featured on \",[\"date\"],\" at \",[\"time\"]],\"EQtz4D\":[\"Open a few links and check whether they still feel native to the page.\"],\"EU3tBD\":[\"Link removed.\"],\"EetoJL\":[\"Guide the eye without taking over the layout.\"],\"Eiv3bO\":[\"Buttons can stay steady, but links, thread markers, and subtle emphasis should feel closer to ink on paper than dashboard chrome.\"],\"ElTnWL\":[\"Published on\"],\"EmQw8O\":[\"If this article still feels like a page you want to keep reading, the palette is probably close.\"],\"EsJdRp\":[\"Save theme\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FEr96N\":[\"Theme\"],\"FGySZL\":[\"The default accent works best when it reads like a fountain-pen underline. Compare it against the\"],\"FM+KeU\":[\"No drafts yet. Save a draft to find it here.\"],\"Fdv5k7\":[\"What to look for while tuning it\"],\"FkMol5\":[\"Featured\"],\"FqCHF/\":[\"Threads\"],\"Fxf4jq\":[\"Description (optional)\"],\"G2u/aQ\":[\"Download official Jant logos, icons, and preview assets.\"],\"GAohqx\":[\"Delete column\"],\"GBJzTZ\":[\"Archive\"],\"GX2VMa\":[\"Create your admin account.\"],\"GY/1J4\":[\"Jant fallback canary string\"],\"GbIOhd\":[\"Reply quietly\"],\"GiRWtR\":[\"Why the default accent should feel written, not branded\"],\"GkpIs2\":[\"Remove this link from Collections? The destination won't change.\"],\"GorKul\":[\"Welcome to Jant\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"H4lgRd\":[\"Authentication isn't set up. Check your server config.\"],\"HFPGej\":[\"No threads match these filters. Try adjusting your selection or clear all filters.\"],\"HG79RB\":[\"Post as Private\"],\"HNEHJP\":[\"Demo credentials are pre-filled — hit Sign In to continue.\"],\"HSI88F\":[\"Delete table\"],\"HbAIQc\":[\"A reference link for checking whether the accent feels editorial instead of promotional.\"],\"HrC0ab\":[\"All posts\"],\"Ht1V3q\":[\"For the same reason, inline code should stay neutral. Something like theme.siteAccent = soften(green, 12%) should not suddenly become the loudest thing on the page.\"],\"I22eN0\":[\"Shared links\"],\"I6zLrz\":[\"Use these when you need a transparent square logo, a shaped tile with a built-in background, a browser icon, or a default preview image.\"],\"ICsA6P\":[\"You have unsaved changes\"],\"IUX7p+\":[\"White logo on the Jant green rounded tile for app icon mockups, touch icons, directory listings, and other square placements that should feel softer.\"],\"IagCbF\":[\"URL\"],\"IjnQHI\":[\"with title\"],\"ImOQa9\":[\"Reply\"],\"J+2Rls\":[\"Leave blank to publish now. Use an earlier date when importing older posts.\"],\"J4tAHl\":[\"Headings should keep their hierarchy even when the accent gets softer.\"],\"JYj5R2\":[\"Browse files\"],\"JcD7qf\":[\"More actions\"],\"JqJ5Xv\":[\"Latest\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"JwLPQ/\":[\"This sign-in link has expired. Return to \"],\"KOqvXP\":[\"Do not recolor, stretch, rotate, outline, or add effects to the logo.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"Khu3PV\":[\"Publish settings\"],\"KiJn9B\":[\"Note\"],\"KlZ+t+\":[\"%name% + %count% more\"],\"KsvRin\":[\"Hide from Latest\"],\"KzmC5L\":[\"Controls\"],\"L7svJg\":[\"Reading\"],\"Lbkbwy\":[\"A quote card for judging accent color against softer, citation-heavy content.\"],\"LcvzvX\":[\"Tap to retry\"],\"LkA8jz\":[\"Add alt text\"],\"LxRg6f\":[\"live theme controls\"],\"M4tzVU\":[\"Latest posts\"],\"M8kJqa\":[\"Drafts\"],\"MHrjPM\":[\"Title\"],\"MILa7n\":[\"Square tile\"],\"MRYGql\":[\"Open image URL\"],\"MSc/Yq\":[\"Do you want to publish your changes or discard them?\"],\"MZTcRq\":[\"Image unavailable\"],\"Mc7+6G\":[\"Enter a valid URL starting with http://, https://, or mailto:.\"],\"MdMyne\":[\"Source link (optional)\"],\"MiMY3Q\":[\"Apple touch icon\"],\"MiyoI7\":[\"default note sample\"],\"MqghUt\":[\"Search posts...\"],\"Myqkib\":[\"Create a collection to get started.\"],\"N8UzTV\":[\"Replies\"],\"NAFbuE\":[\"Search snippet\"],\"NH9Z1R\":[\"Start here\"],\"NqsRbb\":[\"Jant logo\"],\"NvXuWk\":[\"Won't move the thread to the top of latest.\"],\"O1367B\":[\"All collections\"],\"O3oNi5\":[\"Email\"],\"OEdMhi\":[\"The best default color is the one you notice only after reading for a while.\"],\"OEt/to\":[\"Guidelines\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OaoJcz\":[\"Social preview\"],\"OmfDbR\":[\"Site accent\"],\"Ovks1h\":[\"A softer blue feels more like ink than product chrome.\"],\"P/sHNL\":[\"Use this page to judge buttons, links, cards, forms, thread accents, and quiet surfaces before changing a theme globally.\"],\"PBxg/E\":[\"Not now\"],\"Q/uoSA\":[\"Quiet here for now.\"],\"Q2mGA7\":[\"Clear filter\"],\"QBqVyM\":[\"Home screen icon for iPhone and iPad shortcuts.\"],\"QebAts\":[\"Link added.\"],\"Qgbxdw\":[\"Designing a calmer default accent for Jant\"],\"Qn9Ao8\":[\"Circle tile\"],\"Qoq+GP\":[\"Read more\"],\"QyDt3L\":[\"File uploaded.\"],\"R5CMuK\":[\"Jant looks best when the accent feels editorial. Buttons can stay sturdy, but inline emphasis should feel like a pen mark, not a dashboard highlight.\"],\"R8AthW\":[\"Divider\"],\"R9Khdg\":[\"Auto\"],\"RAv3u7\":[\"Compare it against the theme controls\"],\"ROa4Ti\":[\"Interfaces for reading should guide the eye, not keep asking for attention.\"],\"RZOWDv\":[\"Add a custom shortcut to any page or site.\"],\"RdmNnl\":[\"Browser tab\"],\"RfGczC\":[\"Square logo\"],\"Rj01Fz\":[\"Links\"],\"S37om9\":[\"Included assets\"],\"S8NCfs\":[\"Save to drafts to edit and post at a later time.\"],\"SJGVAw\":[\"Feel editorial and slightly quieter.\"],\"SJmfuf\":[\"Site Name\"],\"SSsoa4\":[\"Add to Navigation\"],\"SaNhJE\":[\"feel deliberate instead of washed out.\"],\"SpTWH3\":[\"Download SVG\"],\"SvRuJt\":[\"Field Notes on Interface Tone\"],\"T/R+Qz\":[\"Primary\"],\"TNZKpI\":[\"Danger\"],\"Tn1w2R\":[\"Draft actions\"],\"TvaTxw\":[\"Doesn't appear in Latest. Still appears in collections you add it to.\"],\"UIMXHD\":[\"Remove Divider\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uc5y7o\":[\"Choose the standard logo for websites, docs, directories, and editorial layouts.\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V18SVO\":[\"Use the logo on light backgrounds.\"],\"V4WsyL\":[\"Add Link\"],\"VCA6B2\":[\"These are actual feed components with real footers, summaries, and inline links. Use this section to judge whether the theme still feels calm once it is applied to realistic content.\"],\"VNqFYa\":[\"Loading post...\"],\"WCOanD\":[\"This reference is useful because it treats links and citations as part of the reading rhythm. Keep that in mind while tuning the\"],\"WbIbzR\":[\"Checking link...\"],\"WcWS//\":[\"Download file\"],\"WhsN3P\":[\"A good default accent in Jant should feel like editorial structure, not product branding. That means links, emphasis, and thread cues can be visible without turning the page into UI chrome.\"],\"Wn+/rH\":[\"Transparent square\"],\"XU7b+L\":[\"Primary logo files\"],\"XV1mAn\":[\"Only visible when signed in.\"],\"XrnWzN\":[\"Published!\"],\"Y7WAtz\":[\"有一張圖片無法轉存到你的媒體庫,已保留原連結。\"],\"YIix5Y\":[\"Search...\"],\"YOzD/a\":[\"Replace image\"],\"YUglt2\":[\"Generating a link...\"],\"YXiA6e\":[\"Primary button\"],\"Ygx3Yl\":[\"Small browser icon used in tabs and bookmarks.\"],\"Z4OdvO\":[\"Adding…\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZV5ykW\":[\"Download PNG\"],\"ZhhOwV\":[\"Quote\"],\"ZmSeP+\":[\"Save to drafts?\"],\"ZxFuun\":[[\"count\",\"plural\",{\"one\":[\"Found \",\"#\",\" result\"],\"other\":[\"Found \",\"#\",\" results\"]}]],\"a5j82I\":[\"No collections match that search. Try a different name.\"],\"aBFXGQ\":[\"This collection is empty. Add threads from the editor.\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aMEyv0\":[\"Stay sturdy and readable.\"],\"aN6wx0\":[\"Nothing in Featured yet. Mark a post as featured to show it here.\"],\"aYpXKS\":[\"and checking whether the accent is guiding attention or pulling too hard.\"],\"aaGV/9\":[\"New Link\"],\"af+9p6\":[\"Quiet metadata\"],\"an5hVd\":[\"Images\"],\"ao77hr\":[[\"count\",\"plural\",{\"one\":[\"#\",\" hidden post\"],\"other\":[\"#\",\" hidden posts\"]}]],\"auFlOr\":[\"Icons and previews\"],\"avuFKG\":[\"threads\"],\"b+dane\":[\"Insert %rows% by %cols% table\"],\"bFpC86\":[\"Everything in one download\"],\"bGtMpA\":[\"Add a label and URL.\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bZ9ges\":[\"Add column before\"],\"bbdNeX\":[\"Sign in\"],\"bfCbdi\":[\"Current post\"],\"bkBJmZ\":[\"This is useful as a color check because it puts the accent next to quotation styling, metadata, and a quieter explanatory paragraph. Compare it back to the\"],\"bkMuwo\":[\"Link to posts you've marked as featured.\"],\"bzSI52\":[\"Discard\"],\"c2JRUS\":[\"Generate automatically\"],\"cIoW7X\":[\"Inline link\"],\"cTUByn\":[\"Newest first\"],\"cb7FR8\":[\"White logo on the Jant green square tile for platforms and layouts that expect a true edge-to-edge square.\"],\"cgmi4V\":[\"Delete Draft\"],\"cnGeoo\":[\"Delete\"],\"d+F4pf\":[\"The image should sit quietly inside the article instead of feeling like a card preview.\"],\"d/o/BH\":[\"Couldn't publish. Saved as draft.\"],\"d0DHp4\":[\"Delete this collection permanently? Threads inside won't be removed.\"],\"dD7NPy\":[\"Outline\"],\"dEgA5A\":[\"Cancel\"],\"dHko2w\":[\"single posts\"],\"dUsGbd\":[\"The right accent should disappear into the writing until you need it.\"],\"dXoieq\":[\"Summary\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dbUuAj\":[\"Appears in Latest.\"],\"df4a/r\":[\"Couldn't load this post. Try again.\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"f4MAoA\":[\"Some uploads failed. Saved as draft.\"],\"f5s9EI\":[\"Press N to write\"],\"f6Hub0\":[\"Sort\"],\"f8fH8W\":[\"Design\"],\"fD+f7T\":[\"RSS feed\"],\"fKrDxS\":[\"Brand tile\"],\"fMPkxb\":[\"Show more\"],\"fqDzSu\":[\"Rate\"],\"fttd2R\":[\"My Collection\"],\"fuWzQl\":[\"Nothing here yet. Add threads to one of these collections to fill this view.\"],\"gCcxP/\":[\"Threads can include up to \",[\"count\"],\" posts.\"],\"gFdWl+\":[\"A long-form article sample for checking the default palette in a true reading context.\"],\"gNKz6Z\":[\"Collection deleted.\"],\"gXH9r/\":[\"Open raw PNG\"],\"gpaPhA\":[\"Helps screen readers describe the image\"],\"h5RcXU\":[\"Post hidden\"],\"hLlWo5\":[\"A few simple rules.\"],\"hWpUeY\":[\"Auto link\"],\"hXzOVo\":[\"Next\"],\"heSQoS\":[\"Paste a URL...\"],\"hjeS3W\":[\"Link to your latest posts. Your homepage shows this feed.\"],\"hqeXKW\":[\"Single posts\"],\"hrkGms\":[\"Search\"],\"i0vDGK\":[\"Sort Order\"],\"i5+Y7d\":[\"Download the official Jant logo, icons, and preview files.\"],\"i6kro6\":[\"Edit custom link\"],\"i6nDCI\":[\"Choose a new password.\"],\"iG7KNr\":[\"Logo\"],\"iH8pgl\":[\"Back\"],\"ilSmIt\":[\"Hard edge\"],\"iu7tUI\":[\"Breadcrumb\"],\"jAXE5p\":[\"Reverse logo\"],\"jAqB/k\":[\"Post privately\"],\"jQflRT\":[\"This uses the real single-post detail rendering with a longer article, inline image, tables, lists, quotes, and code. The content column stays at the same width as the live site.\"],\"jd+8Mm\":[\"Social preview image\"],\"jdJOV1\":[\"Settings\"],\"ji7oVU\":[\"Edit post\"],\"jpctdh\":[\"View\"],\"jrsUoG\":[\"Type / for commands\"],\"jvyYZG\":[\"What's on your mind...\"],\"k3Iw35\":[\"Switch to the white logo when the standard green version would lose contrast.\"],\"kPMIr+\":[\"Give it a title...\"],\"kzvWob\":[\"Link to the post archive\"],\"laT1IJ\":[\"iOS home screen\"],\"lb+Xwx\":[\"Custom link\"],\"m16xKo\":[\"Add\"],\"mKT7g0\":[\"Text attachment\"],\"mc/vLq\":[\"This link is already in use. Choose something else.\"],\"muKqfV\":[\"Featured\"],\"n1ekoW\":[\"Sign In\"],\"n3ReIn\":[\"Collections\"],\"n6QD94\":[\"Oldest first\"],\"nFukaP\":[\"Wrong email or password. Check your credentials and try again.\"],\"nJ4U1U\":[\"有 \",[\"count\"],\" 張圖片無法轉存到你的媒體庫,已保留原連結。\"],\"nV6twc\":[\"Organize\"],\"nd8Puv\":[\"White logo on the Jant green circle for profile images, badges, and other round placements where you want a ready-made asset.\"],\"ndrEYW\":[\"When the accent is slightly warmer and less literal, the whole page feels more like a writing space and less like product UI.\"],\"oO0hKx\":[[\"count\",\"plural\",{\"one\":[\"#\",\" more post\"],\"other\":[\"#\",\" more posts\"]}]],\"oTu7Wt\":[\"Combined Collections\"],\"ode0+L\":[\"Theme sample\"],\"ogssnn\":[\"with media\"],\"ovBPCi\":[\"Default\"],\"p1Z67P\":[\"When primary is too rigid, the whole page starts reading like product UI instead of writing space.\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pBHx39\":[\"Dark backgrounds\"],\"pVrU5x\":[\"If this page feels too branded, the first place to soften is the default theme’s site accent, not the border or body text.\"],\"pvnfJD\":[\"Dark\"],\"q+bMmy\":[\"Table controls\"],\"q+hNag\":[\"Collection\"],\"q5YRzz\":[\"Color check\"],\"q8RviX\":[\"Titled\"],\"qc+12k\":[\"Choose table size\"],\"qcawwg\":[\"Publish now\"],\"qiN9NB\":[\"Surface\"],\"qt89I8\":[\"Draft saved.\"],\"quvfGs\":[\"instead of judging it as an isolated swatch.\"],\"r7kcaA\":[\"Drag collections, links, and dividers into the order you want.\"],\"rA2TFI\":[\"Switch the palette and mode without opening settings or changing the active site theme.\"],\"rV8ZnP\":[\"Edit publish date\"],\"rdUucN\":[\"Preview\"],\"s8G5Or\":[\"This upload would exceed your shared hosted media limit. Remove files or upgrade storage to continue.\"],\"s9gHf5\":[\"your-post-link\"],\"sER+bs\":[\"Files\"],\"sQpDn6\":[\"Exit fullscreen\"],\"sgr2wQ\":[\"collection\"],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"syiAKf\":[\"note treatment\"],\"t42hIC\":[\"Everything most people need is in one ZIP.\"],\"tCctex\":[\"The brand pack includes SVG logos, a transparent square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and the default social preview image.\"],\"tKlWWY\":[\"Emoji\"],\"tSWVu5\":[\"Published on \",[\"date\"],\" at \",[\"time\"]],\"tT/bSk\":[\"Add row above\"],\"tZ1tJc\":[\"Edit Navigation\"],\"tfDRzk\":[\"Save\"],\"tg5MRw\":[\"Sign in to start writing.\"],\"tgSBSE\":[\"Remove Link\"],\"uNPC+z\":[\"Couldn't add this collection to navigation. Try again.\"],\"uowbPn\":[\"Remove attachment\"],\"v3E8iS\":[\"A practical checklist\"],\"vSJd18\":[\"Video\"],\"vSYKYI\":[\"Main feed\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vcpc5o\":[\"Close menu\"],\"vdFnYM\":[\"Reset link\"],\"vdvpU5\":[\"/archive?format=quote or https://example.com\"],\"vgpfCi\":[\"Save draft\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzU4k9\":[\"New Collection\"],\"w0Emel\":[\"Suggested link\"],\"w6mlns\":[\"Article detail page\"],\"wJ+GRy\":[\"All visibility\"],\"wL3cK8\":[\"Latest\"],\"wja8aL\":[\"Untitled\"],\"wlnK1t\":[\"A single ZIP with the main logo, reverse logo, square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and social preview image.\"],\"wm3Zlr\":[\"All years\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xVrkxi\":[\"quiet design\"],\"xVvw1i\":[\"This reset link is no longer valid. Request a new one to continue.\"],\"xYilR2\":[\"Media\"],\"xeiujy\":[\"Text\"],\"xhTx3y\":[\"Choose the standard logo for most placements and the reverse logo when you need more contrast.\"],\"xjHB3/\":[\"Continue →\"],\"y28hnO\":[\"Post\"],\"y2o/Y0\":[\"This Link Has Expired\"],\"yGZVl1\":[\"More\"],\"yQ2kGp\":[\"Load more\"],\"yUtAh2\":[\"New Thread\"],\"ycM1Xg\":[\"No results. Try different keywords.\"],\"ynMAhG\":[\"Default logo\"],\"yzF66j\":[\"Link\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zJDAbh\":[\"Don't save\"],\"zcDmsG\":[\"Featured posts\"],\"zoK+eO\":[\"Add a title before posting this link.\"],\"zucql+\":[\"Menu\"],\"zwBp5t\":[\"Private\"]}")as Messages;
|
|
1
|
+
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"+4u2g6\":[\"A ready-made 1:1 PNG for decks, mockups, directories, and other square placements.\"],\"+DPYOZ\":[\"Add a link to your main RSS feed. Change what /feed returns in General.\"],\"+G8qqW\":[\"Collection saved.\"],\"+IJm1Z\":[\"Muted\"],\"+Irvp3\":[\"Everything on this page is ready to use for articles, launch posts, directories, and product coverage.\"],\"+Qaboy\":[\"Favicon\"],\"+fWu2O\":[\"A calmer, warmer accent makes the default theme feel quieter and more intentional.\"],\"+jw/c1\":[\"Delete image\"],\"+nHhRH\":[\"Use \",[\"brandColorName\"]],\"+siMqD\":[\"Journal\"],\"/DFKdU\":[\"Type the quote...\"],\"/PfPLc\":[\"Label (optional)\"],\"/PoNoq\":[\"Edit link\"],\"/Ui2OV\":[\"Use the reverse logo on dark backgrounds.\"],\"/Ybds4\":[\"Primary Jant logo for websites, docs, press coverage, and editorial layouts.\"],\"/rTz0M\":[\"Audio\"],\"0EcUWz\":[\"Discard changes?\"],\"0Lj7or\":[\"Save text attachment?\"],\"0Vlj/m\":[\"This post isn’t published.\"],\"0XDp7X\":[\"Links should read clearly without glowing against the page.\"],\"0cspe/\":[\"Delete row\"],\"0ieXE7\":[\"Highest rated\"],\"11h9eK\":[\"Includes\"],\"15++NM\":[\"Inline emphasis\"],\"1DBGsz\":[\"Notes\"],\"1NeeWI\":[\"Square assets for avatars, apps, browsers, and shared links\"],\"1THMr2\":[\"Brand pack\"],\"1njn7W\":[\"Light\"],\"27KTPP\":[\"Draft preview\"],\"2B7HLH\":[\"New post\"],\"2C7mSG\":[\"Collection link\"],\"2ETv7R\":[\"Tune color in a real reading context\"],\"2HbvFp\":[\"Real post components\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2koDOQ\":[\"Thread accents\"],\"2lKpcz\":[\"Write your first post to get started.\"],\"2q/Q7x\":[\"Visibility\"],\"2sCqzD\":[\"Use this for websites, docs, articles, and other light or neutral surfaces.\"],\"3Cw1AI\":[\"Add Collection\"],\"3lJk5u\":[\"Keep the artwork unchanged.\"],\"3mdteM\":[\"before deciding whether the accent is carrying too much product energy.\"],\"3neqtf\":[\"Thread accent\"],\"3qkggm\":[\"Fullscreen\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"3xi01/\":[\"Look at the footer metadata last, to make sure the accent is not fighting the typography.\"],\"47iMgt\":[\"Editorial interfaces worth borrowing from\"],\"4D09NB\":[\"Link to your collections page\"],\"4GQThz\":[\"No collections yet. Start one to organize threads by topic.\"],\"4HLTdq\":[\"without media\"],\"4J/OYU\":[\"Collection created.\"],\"4eiXo+\":[\"Leave blank to generate one automatically.\"],\"4pV0kE\":[\"Avatar-ready\"],\"51EYZX\":[\"without title\"],\"5dcjwM\":[\"Choose today or an earlier date, or leave it blank to publish now.\"],\"5pAjd8\":[\"Accent should feel present, not loud.\"],\"5sEkBi\":[\"Open raw asset\"],\"61i3cO\":[\"Toggle header row\"],\"6QCZMo\":[\"Add row below\"],\"6UTABI\":[\"Collection order updated.\"],\"6WAK+2\":[\"Use current date\"],\"6Y4BBO\":[\"An abstract editorial layout in warm paper colors\"],\"6cjUDB\":[\"Brand assets\"],\"6lGV3K\":[\"Show less\"],\"6p0JeQ\":[\"to make sure both still feel like they belong to the same product.\"],\"6sVyMq\":[\"Add a URL before posting this link.\"],\"6yCv8j\":[\"Save these changes to the text attachment, discard them, or keep editing.\"],\"74kJNs\":[\"View earlier notes in this thread\"],\"7DvUqV\":[\"Read the page from top to bottom without looking at the swatches.\"],\"7aris6\":[\"March 15\"],\"7d1a0d\":[\"Public\"],\"7hYXO0\":[\"Use this on dark backgrounds, image-backed surfaces, and any placement where the green logo would lose contrast.\"],\"7ibPpM\":[\"Add column after\"],\"7kMW54\":[\"Open raw SVG\"],\"7nGhhM\":[\"What's on your mind?\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8Btgys\":[\"Draft deleted.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8ZsakT\":[\"Password\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8eC78s\":[\"A calmer accent makes\"],\"8tM8+a\":[\"Save as draft\"],\"90IRF2\":[\"This article is here to answer a specific question: does the default accent still feel calm once it has to carry a full reading experience?\"],\"9SHZas\":[\"Shows 'Settings' when logged in, 'Sign in' when logged out\"],\"9aloPG\":[\"References\"],\"9dr9Nh\":[\"Open external link\"],\"9j5qCS\":[\"Table options\"],\"9qWoxS\":[\"Feed\"],\"A1D8Yt\":[\"What the accent should do\"],\"A1taO8\":[\"Search\"],\"A2Vg/u\":[\"Navigation and reading states\"],\"AjHkcv\":[\"Default preview image for social shares and link unfurls.\"],\"AmQ/h/\":[\"Sort order\"],\"AyHO4m\":[\"What's this collection about?\"],\"B1FFMj\":[\"Download Brand Pack\"],\"B495Gs\":[\"Archive\"],\"BdjLtf\":[\"thread\"],\"Bmaby2\":[\"All formats\"],\"C+9df9\":[\"Quoted or highlighted passages should feel like annotations, not warnings.\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"C4TjpG\":[\"Read less\"],\"CAh1km\":[\"collections\"],\"CH3bgf\":[\"RSS feed for this view\"],\"CmBCXY\":[\"Link updated.\"],\"D4em/+\":[\"Logos\"],\"DHhJ7s\":[\"Previous\"],\"DJLY+/\":[\" and try again.\"],\"DOx286\":[\"Draft restored.\"],\"DPfwMq\":[\"Done\"],\"DSJXZM\":[\"Enter a valid date.\"],\"DYlMYF\":[\"Built-in background\"],\"DoJzLz\":[\"Collections\"],\"Du2B9f\":[\"The default accent should support reading first. Start by comparing it against the\"],\"DxwUcG\":[\"Read the palette as content first\"],\"E3NcGH\":[\"Square logo PNG\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EHWwm1\":[\"The default accent should feel written, not branded.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"EQNPYo\":[\"Featured on \",[\"date\"],\" at \",[\"time\"]],\"EQtz4D\":[\"Open a few links and check whether they still feel native to the page.\"],\"EU3tBD\":[\"Link removed.\"],\"EetoJL\":[\"Guide the eye without taking over the layout.\"],\"Eiv3bO\":[\"Buttons can stay steady, but links, thread markers, and subtle emphasis should feel closer to ink on paper than dashboard chrome.\"],\"ElTnWL\":[\"Published on\"],\"EmQw8O\":[\"If this article still feels like a page you want to keep reading, the palette is probably close.\"],\"EsJdRp\":[\"Save theme\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FEr96N\":[\"Theme\"],\"FGySZL\":[\"The default accent works best when it reads like a fountain-pen underline. Compare it against the\"],\"FM+KeU\":[\"No drafts yet. Save a draft to find it here.\"],\"Fdv5k7\":[\"What to look for while tuning it\"],\"FkMol5\":[\"Featured\"],\"FqCHF/\":[\"Threads\"],\"Fxf4jq\":[\"Description (optional)\"],\"G2u/aQ\":[\"Download official Jant logos, icons, and preview assets.\"],\"GAohqx\":[\"Delete column\"],\"GBJzTZ\":[\"Archive\"],\"GX2VMa\":[\"Create your admin account.\"],\"GY/1J4\":[\"Jant fallback canary string\"],\"GbIOhd\":[\"Reply quietly\"],\"GiRWtR\":[\"Why the default accent should feel written, not branded\"],\"GkpIs2\":[\"Remove this link from Collections? The destination won't change.\"],\"GorKul\":[\"Welcome to Jant\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"H4lgRd\":[\"Authentication isn't set up. Check your server config.\"],\"HFPGej\":[\"No threads match these filters. Try adjusting your selection or clear all filters.\"],\"HG79RB\":[\"Post as Private\"],\"HNEHJP\":[\"Demo credentials are pre-filled — hit Sign In to continue.\"],\"HSI88F\":[\"Delete table\"],\"HbAIQc\":[\"A reference link for checking whether the accent feels editorial instead of promotional.\"],\"HrC0ab\":[\"All posts\"],\"Ht1V3q\":[\"For the same reason, inline code should stay neutral. Something like theme.siteAccent = soften(green, 12%) should not suddenly become the loudest thing on the page.\"],\"I22eN0\":[\"Shared links\"],\"I6zLrz\":[\"Use these when you need a transparent square logo, a shaped tile with a built-in background, a browser icon, or a default preview image.\"],\"ICsA6P\":[\"You have unsaved changes\"],\"IUX7p+\":[\"White logo on the Jant green rounded tile for app icon mockups, touch icons, directory listings, and other square placements that should feel softer.\"],\"IagCbF\":[\"URL\"],\"IjnQHI\":[\"with title\"],\"ImOQa9\":[\"Reply\"],\"IvUVJl\":[\"Show as a list with full posts\"],\"J+2Rls\":[\"Leave blank to publish now. Use an earlier date when importing older posts.\"],\"J4tAHl\":[\"Headings should keep their hierarchy even when the accent gets softer.\"],\"JYj5R2\":[\"Browse files\"],\"JcD7qf\":[\"More actions\"],\"JqJ5Xv\":[\"Latest\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"JwLPQ/\":[\"This sign-in link has expired. Return to \"],\"KOqvXP\":[\"Do not recolor, stretch, rotate, outline, or add effects to the logo.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"Khu3PV\":[\"Publish settings\"],\"KiJn9B\":[\"Note\"],\"KlZ+t+\":[\"%name% + %count% more\"],\"KsvRin\":[\"Hide from Latest\"],\"KzmC5L\":[\"Controls\"],\"L7svJg\":[\"Reading\"],\"Lbkbwy\":[\"A quote card for judging accent color against softer, citation-heavy content.\"],\"LcvzvX\":[\"Tap to retry\"],\"LkA8jz\":[\"Add alt text\"],\"LxRg6f\":[\"live theme controls\"],\"M4tzVU\":[\"Latest posts\"],\"M8kJqa\":[\"Drafts\"],\"MHrjPM\":[\"Title\"],\"MILa7n\":[\"Square tile\"],\"MRYGql\":[\"Open image URL\"],\"MSc/Yq\":[\"Do you want to publish your changes or discard them?\"],\"MZTcRq\":[\"Image unavailable\"],\"Mc7+6G\":[\"Enter a valid URL starting with http://, https://, or mailto:.\"],\"MdMyne\":[\"Source link (optional)\"],\"MiMY3Q\":[\"Apple touch icon\"],\"MiyoI7\":[\"default note sample\"],\"MqghUt\":[\"Search posts...\"],\"Myqkib\":[\"Create a collection to get started.\"],\"N8UzTV\":[\"Replies\"],\"NAFbuE\":[\"Search snippet\"],\"NH9Z1R\":[\"Start here\"],\"NqsRbb\":[\"Jant logo\"],\"NvXuWk\":[\"Won't move the thread to the top of latest.\"],\"O1367B\":[\"All collections\"],\"O3oNi5\":[\"Email\"],\"OEdMhi\":[\"The best default color is the one you notice only after reading for a while.\"],\"OEt/to\":[\"Guidelines\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OaiMaf\":[\"View mode\"],\"OaoJcz\":[\"Social preview\"],\"OmfDbR\":[\"Site accent\"],\"Oo7//P\":[\"Edit draft\"],\"Ovks1h\":[\"A softer blue feels more like ink than product chrome.\"],\"P/sHNL\":[\"Use this page to judge buttons, links, cards, forms, thread accents, and quiet surfaces before changing a theme globally.\"],\"PBxg/E\":[\"Not now\"],\"Q/uoSA\":[\"Quiet here for now.\"],\"Q2mGA7\":[\"Clear filter\"],\"QBqVyM\":[\"Home screen icon for iPhone and iPad shortcuts.\"],\"QOhkyl\":[\"Compose\"],\"QebAts\":[\"Link added.\"],\"Qgbxdw\":[\"Designing a calmer default accent for Jant\"],\"Qn9Ao8\":[\"Circle tile\"],\"Qoq+GP\":[\"Read more\"],\"QyDt3L\":[\"File uploaded.\"],\"R5CMuK\":[\"Jant looks best when the accent feels editorial. Buttons can stay sturdy, but inline emphasis should feel like a pen mark, not a dashboard highlight.\"],\"R8AthW\":[\"Divider\"],\"R9Khdg\":[\"Auto\"],\"RAv3u7\":[\"Compare it against the theme controls\"],\"ROa4Ti\":[\"Interfaces for reading should guide the eye, not keep asking for attention.\"],\"RZOWDv\":[\"Add a custom shortcut to any page or site.\"],\"RdmNnl\":[\"Browser tab\"],\"RfGczC\":[\"Square logo\"],\"Rj01Fz\":[\"Links\"],\"S37om9\":[\"Included assets\"],\"S8NCfs\":[\"Save to drafts to edit and post at a later time.\"],\"SJGVAw\":[\"Feel editorial and slightly quieter.\"],\"SJmfuf\":[\"Site Name\"],\"SSsoa4\":[\"Add to Navigation\"],\"SaNhJE\":[\"feel deliberate instead of washed out.\"],\"SbNisn\":[\"Sort by when each thread was last added to\"],\"SpTWH3\":[\"Download SVG\"],\"SvRuJt\":[\"Field Notes on Interface Tone\"],\"T/R+Qz\":[\"Primary\"],\"TNZKpI\":[\"Danger\"],\"Tn1w2R\":[\"Draft actions\"],\"TvaTxw\":[\"Doesn't appear in Latest. Still appears in collections you add it to.\"],\"UIMXHD\":[\"Remove Divider\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uc5y7o\":[\"Choose the standard logo for websites, docs, directories, and editorial layouts.\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V18SVO\":[\"Use the logo on light backgrounds.\"],\"V4WsyL\":[\"Add Link\"],\"VCA6B2\":[\"These are actual feed components with real footers, summaries, and inline links. Use this section to judge whether the theme still feels calm once it is applied to realistic content.\"],\"VNqFYa\":[\"Loading post...\"],\"WCOanD\":[\"This reference is useful because it treats links and citations as part of the reading rhythm. Keep that in mind while tuning the\"],\"WIF6ui\":[\"Continue writing this draft\"],\"WbIbzR\":[\"Checking link...\"],\"WcWS//\":[\"Download file\"],\"WhsN3P\":[\"A good default accent in Jant should feel like editorial structure, not product branding. That means links, emphasis, and thread cues can be visible without turning the page into UI chrome.\"],\"Wn+/rH\":[\"Transparent square\"],\"XU7b+L\":[\"Primary logo files\"],\"XV1mAn\":[\"Only visible when signed in.\"],\"XrnWzN\":[\"Published!\"],\"Y7WAtz\":[\"有一張圖片無法轉存到你的媒體庫,已保留原連結。\"],\"YIix5Y\":[\"Search...\"],\"YOzD/a\":[\"Replace image\"],\"YUglt2\":[\"Generating a link...\"],\"YXiA6e\":[\"Primary button\"],\"Ygx3Yl\":[\"Small browser icon used in tabs and bookmarks.\"],\"Z4OdvO\":[\"Adding…\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZBH98o\":[\"Show as a grid of tiles\"],\"ZV5ykW\":[\"Download PNG\"],\"ZhhOwV\":[\"Quote\"],\"ZmSeP+\":[\"Save to drafts?\"],\"ZxFuun\":[[\"count\",\"plural\",{\"one\":[\"Found \",\"#\",\" result\"],\"other\":[\"Found \",\"#\",\" results\"]}]],\"a5j82I\":[\"No collections match that search. Try a different name.\"],\"aBFXGQ\":[\"This collection is empty. Add threads from the editor.\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aHd4P7\":[\"Close compose\"],\"aMEyv0\":[\"Stay sturdy and readable.\"],\"aN6wx0\":[\"Nothing in Featured yet. Mark a post as featured to show it here.\"],\"aYpXKS\":[\"and checking whether the accent is guiding attention or pulling too hard.\"],\"aaGV/9\":[\"New Link\"],\"af+9p6\":[\"Quiet metadata\"],\"an5hVd\":[\"Images\"],\"ao77hr\":[[\"count\",\"plural\",{\"one\":[\"#\",\" hidden post\"],\"other\":[\"#\",\" hidden posts\"]}]],\"auFlOr\":[\"Icons and previews\"],\"avuFKG\":[\"threads\"],\"b+dane\":[\"Insert %rows% by %cols% table\"],\"bFpC86\":[\"Everything in one download\"],\"bGtMpA\":[\"Add a label and URL.\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bZ9ges\":[\"Add column before\"],\"bbdNeX\":[\"Sign in\"],\"bfCbdi\":[\"Current post\"],\"bkBJmZ\":[\"This is useful as a color check because it puts the accent next to quotation styling, metadata, and a quieter explanatory paragraph. Compare it back to the\"],\"bkMuwo\":[\"Link to posts you've marked as featured.\"],\"bzSI52\":[\"Discard\"],\"c2JRUS\":[\"Generate automatically\"],\"cH5kXP\":[\"Now\"],\"cIoW7X\":[\"Inline link\"],\"cTUByn\":[\"Newest first\"],\"cb7FR8\":[\"White logo on the Jant green square tile for platforms and layouts that expect a true edge-to-edge square.\"],\"cgmi4V\":[\"Delete Draft\"],\"cnGeoo\":[\"Delete\"],\"d+F4pf\":[\"The image should sit quietly inside the article instead of feeling like a card preview.\"],\"d/o/BH\":[\"Couldn't publish. Saved as draft.\"],\"d0DHp4\":[\"Delete this collection permanently? Threads inside won't be removed.\"],\"dD7NPy\":[\"Outline\"],\"dEgA5A\":[\"Cancel\"],\"dHko2w\":[\"single posts\"],\"dUsGbd\":[\"The right accent should disappear into the writing until you need it.\"],\"dXoieq\":[\"Summary\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dbUuAj\":[\"Appears in Latest.\"],\"df4a/r\":[\"Couldn't load this post. Try again.\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"eYZPF3\":[\"/…\"],\"eneWvv\":[\"Draft\"],\"f4MAoA\":[\"Some uploads failed. Saved as draft.\"],\"f5s9EI\":[\"Press N to write\"],\"f6Hub0\":[\"Sort\"],\"f8fH8W\":[\"Design\"],\"fD+f7T\":[\"RSS feed\"],\"fKrDxS\":[\"Brand tile\"],\"fMPkxb\":[\"Show more\"],\"fqDzSu\":[\"Rate\"],\"fttd2R\":[\"My Collection\"],\"fuWzQl\":[\"Nothing here yet. Add threads to one of these collections to fill this view.\"],\"gCcxP/\":[\"Threads can include up to \",[\"count\"],\" posts.\"],\"gFdWl+\":[\"A long-form article sample for checking the default palette in a true reading context.\"],\"gGx5tM\":[\"Editing\"],\"gNKz6Z\":[\"Collection deleted.\"],\"gXH9r/\":[\"Open raw PNG\"],\"gpaPhA\":[\"Helps screen readers describe the image\"],\"h5RcXU\":[\"Post hidden\"],\"hLlWo5\":[\"A few simple rules.\"],\"hXzOVo\":[\"Next\"],\"heSQoS\":[\"Paste a URL...\"],\"hjeS3W\":[\"Link to your latest posts. Your homepage shows this feed.\"],\"hqeXKW\":[\"Single posts\"],\"hrkGms\":[\"Search\"],\"i0vDGK\":[\"Sort Order\"],\"i5+Y7d\":[\"Download the official Jant logo, icons, and preview files.\"],\"i6kro6\":[\"Edit custom link\"],\"i6nDCI\":[\"Choose a new password.\"],\"iG7KNr\":[\"Logo\"],\"iH8pgl\":[\"Back\"],\"iZYiBK\":[\"newest changes first\"],\"ilSmIt\":[\"Hard edge\"],\"iu7tUI\":[\"Breadcrumb\"],\"jAXE5p\":[\"Reverse logo\"],\"jAqB/k\":[\"Post privately\"],\"jQflRT\":[\"This uses the real single-post detail rendering with a longer article, inline image, tables, lists, quotes, and code. The content column stays at the same width as the live site.\"],\"jd+8Mm\":[\"Social preview image\"],\"jdJOV1\":[\"Settings\"],\"ji7oVU\":[\"Edit post\"],\"jpctdh\":[\"View\"],\"jrsUoG\":[\"Type / for commands\"],\"jvyYZG\":[\"What's on your mind...\"],\"k3Iw35\":[\"Switch to the white logo when the standard green version would lose contrast.\"],\"kNiQp6\":[\"Pinned\"],\"kPMIr+\":[\"Give it a title...\"],\"kzvWob\":[\"Link to the post archive\"],\"laT1IJ\":[\"iOS home screen\"],\"lb+Xwx\":[\"Custom link\"],\"m16xKo\":[\"Add\"],\"mKT7g0\":[\"Text attachment\"],\"mc/vLq\":[\"This link is already in use. Choose something else.\"],\"muKqfV\":[\"Featured\"],\"n1ekoW\":[\"Sign In\"],\"n3ReIn\":[\"Collections\"],\"n6QD94\":[\"Oldest first\"],\"nFukaP\":[\"Wrong email or password. Check your credentials and try again.\"],\"nJ4U1U\":[\"有 \",[\"count\"],\" 張圖片無法轉存到你的媒體庫,已保留原連結。\"],\"nV6twc\":[\"Organize\"],\"nd8Puv\":[\"White logo on the Jant green circle for profile images, badges, and other round placements where you want a ready-made asset.\"],\"ndrEYW\":[\"When the accent is slightly warmer and less literal, the whole page feels more like a writing space and less like product UI.\"],\"oO0hKx\":[[\"count\",\"plural\",{\"one\":[\"#\",\" more post\"],\"other\":[\"#\",\" more posts\"]}]],\"oTu7Wt\":[\"Combined Collections\"],\"ode0+L\":[\"Theme sample\"],\"ogssnn\":[\"with media\"],\"ovBPCi\":[\"Default\"],\"p1Z67P\":[\"When primary is too rigid, the whole page starts reading like product UI instead of writing space.\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pBHx39\":[\"Dark backgrounds\"],\"pKeaoC\":[\"Sort by when each thread was published\"],\"pVrU5x\":[\"If this page feels too branded, the first place to soften is the default theme’s site accent, not the border or body text.\"],\"pvnfJD\":[\"Dark\"],\"q+bMmy\":[\"Table controls\"],\"q+hNag\":[\"Collection\"],\"q5YRzz\":[\"Color check\"],\"q8RviX\":[\"Titled\"],\"qc+12k\":[\"Choose table size\"],\"qiN9NB\":[\"Surface\"],\"qt89I8\":[\"Draft saved.\"],\"quvfGs\":[\"instead of judging it as an isolated swatch.\"],\"r7kcaA\":[\"Drag collections, links, and dividers into the order you want.\"],\"rA2TFI\":[\"Switch the palette and mode without opening settings or changing the active site theme.\"],\"rV8ZnP\":[\"Edit publish date\"],\"rdUucN\":[\"Preview\"],\"s8G5Or\":[\"This upload would exceed your shared hosted media limit. Remove files or upgrade storage to continue.\"],\"s9gHf5\":[\"your-post-link\"],\"sER+bs\":[\"Files\"],\"sQpDn6\":[\"Exit fullscreen\"],\"sgr2wQ\":[\"collection\"],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"syiAKf\":[\"note treatment\"],\"t42hIC\":[\"Everything most people need is in one ZIP.\"],\"tCctex\":[\"The brand pack includes SVG logos, a transparent square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and the default social preview image.\"],\"tKlWWY\":[\"Emoji\"],\"tSWVu5\":[\"Published on \",[\"date\"],\" at \",[\"time\"]],\"tT/bSk\":[\"Add row above\"],\"tZ1tJc\":[\"Edit Navigation\"],\"tfDRzk\":[\"Save\"],\"tg5MRw\":[\"Sign in to start writing.\"],\"tgSBSE\":[\"Remove Link\"],\"uNPC+z\":[\"Couldn't add this collection to navigation. Try again.\"],\"uowbPn\":[\"Remove attachment\"],\"v3E8iS\":[\"A practical checklist\"],\"vSJd18\":[\"Video\"],\"vSYKYI\":[\"Main feed\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vcpc5o\":[\"Close menu\"],\"vdFnYM\":[\"Reset link\"],\"vdvpU5\":[\"/archive?format=quote or https://example.com\"],\"vgpfCi\":[\"Save draft\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzU4k9\":[\"New Collection\"],\"w0Emel\":[\"Suggested link\"],\"w1bUkO\":[\"Last edited on \",[\"date\"],\" at \",[\"time\"]],\"w6mlns\":[\"Article detail page\"],\"wJ+GRy\":[\"All visibility\"],\"wL3cK8\":[\"Latest\"],\"wja8aL\":[\"Untitled\"],\"wlnK1t\":[\"A single ZIP with the main logo, reverse logo, square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and social preview image.\"],\"wm3Zlr\":[\"All years\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xVrkxi\":[\"quiet design\"],\"xVvw1i\":[\"This reset link is no longer valid. Request a new one to continue.\"],\"xYilR2\":[\"Media\"],\"xeiujy\":[\"Text\"],\"xhTx3y\":[\"Choose the standard logo for most placements and the reverse logo when you need more contrast.\"],\"xjHB3/\":[\"Continue →\"],\"y28hnO\":[\"Post\"],\"y2o/Y0\":[\"This Link Has Expired\"],\"yGZVl1\":[\"More\"],\"yQ2kGp\":[\"Load more\"],\"ycM1Xg\":[\"No results. Try different keywords.\"],\"ynMAhG\":[\"Default logo\"],\"yzF66j\":[\"Link\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zJDAbh\":[\"Don't save\"],\"zcDmsG\":[\"Featured posts\"],\"zoK+eO\":[\"Add a title before posting this link.\"],\"zucql+\":[\"Menu\"],\"zwBp5t\":[\"Private\"]}")as Messages;
|
|
@@ -51,6 +51,48 @@ function makeFeedData(post: FeedPostView): FeedData {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
describe("feed renderers", () => {
|
|
54
|
+
// Stamping the render time on <updated> tells every reader the feed changed
|
|
55
|
+
// on every poll, which is untrue and useless for change detection.
|
|
56
|
+
it("dates the feed by its newest entry, not the render time", () => {
|
|
57
|
+
const xml = defaultFeedRenderer({
|
|
58
|
+
...makeFeedData(makePostView()),
|
|
59
|
+
posts: [
|
|
60
|
+
makePostView({
|
|
61
|
+
id: "a",
|
|
62
|
+
permalink: "/a",
|
|
63
|
+
feedUpdatedAt: "2026-01-02T00:00:00.000Z",
|
|
64
|
+
}),
|
|
65
|
+
makePostView({
|
|
66
|
+
id: "b",
|
|
67
|
+
permalink: "/b",
|
|
68
|
+
feedUpdatedAt: "2026-05-09T00:00:00.000Z",
|
|
69
|
+
}),
|
|
70
|
+
makePostView({
|
|
71
|
+
id: "c",
|
|
72
|
+
permalink: "/c",
|
|
73
|
+
feedUpdatedAt: "2026-03-04T00:00:00.000Z",
|
|
74
|
+
}),
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const feedUpdated = /<id>[^<]*<\/id>\s*<updated>([^<]+)<\/updated>/.exec(
|
|
79
|
+
xml,
|
|
80
|
+
)?.[1];
|
|
81
|
+
expect(feedUpdated).toBe("2026-05-09T00:00:00.000Z");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("falls back to the render time for an empty feed", () => {
|
|
85
|
+
const xml = defaultFeedRenderer({
|
|
86
|
+
...makeFeedData(makePostView()),
|
|
87
|
+
posts: [],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const feedUpdated = /<id>[^<]*<\/id>\s*<updated>([^<]+)<\/updated>/.exec(
|
|
91
|
+
xml,
|
|
92
|
+
)?.[1];
|
|
93
|
+
expect(feedUpdated).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
94
|
+
});
|
|
95
|
+
|
|
54
96
|
it("keeps Atom entry titles empty for untitled posts and strips script tags from content", () => {
|
|
55
97
|
const xml = defaultFeedRenderer(
|
|
56
98
|
makeFeedData(
|
|
@@ -110,6 +152,24 @@ describe("feed renderers", () => {
|
|
|
110
152
|
expect(xml).not.toContain("<iframe");
|
|
111
153
|
});
|
|
112
154
|
|
|
155
|
+
it("resolves relative links and media URLs inside post HTML", () => {
|
|
156
|
+
const post = makePostView({
|
|
157
|
+
bodyHtml:
|
|
158
|
+
'<p><a href="/related">Related</a> <a href="#footnote">Footnote</a></p>' +
|
|
159
|
+
'<img src="/media/inline.jpg" alt="Inline">' +
|
|
160
|
+
'<video poster="/media/poster.jpg"><source src="/media/clip.mp4"></video>' +
|
|
161
|
+
'<span data-src="/leave-this-relative"></span>',
|
|
162
|
+
});
|
|
163
|
+
const xml = defaultFeedRenderer(makeFeedData(post));
|
|
164
|
+
|
|
165
|
+
expect(xml).toContain('href="https://example.com/related"');
|
|
166
|
+
expect(xml).toContain('href="#footnote"');
|
|
167
|
+
expect(xml).toContain('src="https://example.com/media/inline.jpg"');
|
|
168
|
+
expect(xml).toContain('poster="https://example.com/media/poster.jpg"');
|
|
169
|
+
expect(xml).toContain('src="https://example.com/media/clip.mp4"');
|
|
170
|
+
expect(xml).toContain('data-src="/leave-this-relative"');
|
|
171
|
+
});
|
|
172
|
+
|
|
113
173
|
it("does not expose quote attribution as feed title", () => {
|
|
114
174
|
const xml = defaultFeedRenderer(
|
|
115
175
|
makeFeedData(
|
|
@@ -158,6 +218,57 @@ describe("feed renderers", () => {
|
|
|
158
218
|
);
|
|
159
219
|
});
|
|
160
220
|
|
|
221
|
+
it("renders YouTube Link previews as a linked thumbnail with a provider-aware action", () => {
|
|
222
|
+
const post = makePostView({
|
|
223
|
+
format: "link",
|
|
224
|
+
title: "A useful video",
|
|
225
|
+
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
226
|
+
bodyHtml: "<p>My notes on the video.</p>",
|
|
227
|
+
previewKind: "video",
|
|
228
|
+
previewProvider: "youtube",
|
|
229
|
+
previewImageUrl: "/media/previews/youtube.jpg",
|
|
230
|
+
});
|
|
231
|
+
const xml = defaultFeedRenderer(makeFeedData(post));
|
|
232
|
+
|
|
233
|
+
expect(xml).toContain(
|
|
234
|
+
'<figure><a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"><img src="https://example.com/media/previews/youtube.jpg" alt="A useful video"/></a><figcaption><a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">▶ Watch on YouTube</a></figcaption></figure>',
|
|
235
|
+
);
|
|
236
|
+
expect(xml.indexOf("<figure>")).toBeLessThan(
|
|
237
|
+
xml.indexOf("My notes on the video."),
|
|
238
|
+
);
|
|
239
|
+
expect(xml).not.toContain("<iframe");
|
|
240
|
+
expect(xml).not.toContain('rel="enclosure"');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("renders non-video Link previews without a video action", () => {
|
|
244
|
+
const post = makePostView({
|
|
245
|
+
format: "link",
|
|
246
|
+
title: "Illustrated article",
|
|
247
|
+
url: "https://external.com/illustrated",
|
|
248
|
+
previewKind: "image",
|
|
249
|
+
previewImageUrl: "https://example.com/previews/article.jpg",
|
|
250
|
+
});
|
|
251
|
+
const xml = defaultFeedRenderer(makeFeedData(post));
|
|
252
|
+
|
|
253
|
+
expect(xml).toContain(
|
|
254
|
+
'<figure><a href="https://external.com/illustrated"><img src="https://example.com/previews/article.jpg" alt="Illustrated article"/></a></figure>',
|
|
255
|
+
);
|
|
256
|
+
expect(xml).not.toContain("Watch video");
|
|
257
|
+
expect(xml).not.toContain("Watch on");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("keeps the text fallback when a Link post has no preview image", () => {
|
|
261
|
+
const post = makePostView({
|
|
262
|
+
format: "link",
|
|
263
|
+
title: "Text-only link",
|
|
264
|
+
url: "https://external.com/text-only",
|
|
265
|
+
});
|
|
266
|
+
const xml = defaultFeedRenderer(makeFeedData(post));
|
|
267
|
+
|
|
268
|
+
expect(xml).toContain("<![CDATA[<p>Text-only link</p>");
|
|
269
|
+
expect(xml).not.toContain("<figure>");
|
|
270
|
+
});
|
|
271
|
+
|
|
161
272
|
it("note posts still link to blog permalink without ★", () => {
|
|
162
273
|
const post = makePostView({
|
|
163
274
|
format: "note",
|
|
@@ -272,6 +383,38 @@ describe("feed renderers", () => {
|
|
|
272
383
|
);
|
|
273
384
|
});
|
|
274
385
|
|
|
386
|
+
it("renders a YouTube Link preview inside a thread reply", () => {
|
|
387
|
+
const reply = makePostView({
|
|
388
|
+
id: "reply-video",
|
|
389
|
+
permalink: "/reply-video",
|
|
390
|
+
slug: "reply-video",
|
|
391
|
+
format: "link",
|
|
392
|
+
title: "Thread video",
|
|
393
|
+
url: "https://youtu.be/dQw4w9WgXcQ",
|
|
394
|
+
previewKind: "video",
|
|
395
|
+
previewProvider: "youtube",
|
|
396
|
+
previewImageUrl: "/media/previews/thread-video.jpg",
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const xml = defaultFeedRenderer(
|
|
400
|
+
makeFeedData(
|
|
401
|
+
makePostView({
|
|
402
|
+
title: "Thread Root",
|
|
403
|
+
bodyHtml: "<p>Root content</p>",
|
|
404
|
+
threadReplies: [reply],
|
|
405
|
+
}),
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const replyTitle =
|
|
410
|
+
'<h2><a href="https://youtu.be/dQw4w9WgXcQ">Thread video</a></h2>';
|
|
411
|
+
const preview =
|
|
412
|
+
'<figure><a href="https://youtu.be/dQw4w9WgXcQ"><img src="https://example.com/media/previews/thread-video.jpg" alt="Thread video"/></a><figcaption><a href="https://youtu.be/dQw4w9WgXcQ">▶ Watch on YouTube</a></figcaption></figure>';
|
|
413
|
+
expect(xml).toContain(replyTitle);
|
|
414
|
+
expect(xml).toContain(preview);
|
|
415
|
+
expect(xml.indexOf(replyTitle)).toBeLessThan(xml.indexOf(preview));
|
|
416
|
+
});
|
|
417
|
+
|
|
275
418
|
it("includes thread reply media as Atom enclosures on the combined entry", () => {
|
|
276
419
|
const reply = makePostView({
|
|
277
420
|
id: "reply-1",
|
|
@@ -368,6 +511,31 @@ describe("feed renderers", () => {
|
|
|
368
511
|
);
|
|
369
512
|
});
|
|
370
513
|
|
|
514
|
+
it("resolves relative attachment, poster, and enclosure URLs", () => {
|
|
515
|
+
const post = makePostView({
|
|
516
|
+
media: [
|
|
517
|
+
makeMediaView({
|
|
518
|
+
id: "med_local_vid",
|
|
519
|
+
url: "/media/local-clip.mp4",
|
|
520
|
+
thumbnailUrl: "/media/local-clip-thumb.jpg",
|
|
521
|
+
posterUrl: "/media/local-clip-poster.jpg",
|
|
522
|
+
mimeType: "video/mp4",
|
|
523
|
+
}),
|
|
524
|
+
],
|
|
525
|
+
});
|
|
526
|
+
const xml = defaultFeedRenderer(makeFeedData(post));
|
|
527
|
+
|
|
528
|
+
expect(xml).toContain(
|
|
529
|
+
'<a href="https://example.com/media/local-clip.mp4"><img src="https://example.com/media/local-clip-poster.jpg"',
|
|
530
|
+
);
|
|
531
|
+
expect(xml).toContain(
|
|
532
|
+
'<a href="https://example.com/media/local-clip.mp4">▶ Watch video</a>',
|
|
533
|
+
);
|
|
534
|
+
expect(xml).toContain(
|
|
535
|
+
'<link rel="enclosure" type="video/mp4" href="https://example.com/media/local-clip.mp4"',
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
371
539
|
it("renders audio attachments as a labeled link with duration and size", () => {
|
|
372
540
|
const post = makePostView({
|
|
373
541
|
media: [
|
|
@@ -26,6 +26,22 @@ describe("render", () => {
|
|
|
26
26
|
expect(html).toContain(">link</a>");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
it("renders phone and messaging action links", () => {
|
|
30
|
+
expect(render("[Call](tel:+15551234567)")).toContain(
|
|
31
|
+
'href="tel:+15551234567"',
|
|
32
|
+
);
|
|
33
|
+
expect(render("[Message](sms:xxxxx@xx.com)")).toContain(
|
|
34
|
+
'href="sms:xxxxx@xx.com"',
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("strips executable link destinations", () => {
|
|
39
|
+
const html = render("[unsafe](javascript:alert(1))");
|
|
40
|
+
|
|
41
|
+
expect(html).not.toContain("javascript:");
|
|
42
|
+
expect(html).toContain('href=""');
|
|
43
|
+
});
|
|
44
|
+
|
|
29
45
|
it("renders code blocks", () => {
|
|
30
46
|
const html = render("```\nconst x = 1;\n```");
|
|
31
47
|
expect(html).toContain("<code>");
|
|
@@ -95,6 +95,24 @@ describe("generatePostSlug", () => {
|
|
|
95
95
|
}),
|
|
96
96
|
).rejects.toThrow("Could not generate a unique slug");
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it("romanizes Korean titles", async () => {
|
|
100
|
+
const slug = await generatePostSlug({
|
|
101
|
+
title: "개발 일지",
|
|
102
|
+
idLength: 5,
|
|
103
|
+
isAvailable: alwaysAvailable,
|
|
104
|
+
});
|
|
105
|
+
expect(slug).toBe("gaebal-ilji");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("falls back to random IDs for kanji-heavy Japanese titles", async () => {
|
|
109
|
+
const slug = await generatePostSlug({
|
|
110
|
+
title: "日本語のタイトルです",
|
|
111
|
+
idLength: 5,
|
|
112
|
+
isAvailable: alwaysAvailable,
|
|
113
|
+
});
|
|
114
|
+
expect(slug).toMatch(/^[a-z0-9]{5}-[a-z0-9]{5}$/);
|
|
115
|
+
});
|
|
98
116
|
});
|
|
99
117
|
|
|
100
118
|
describe("random slug (no title, no slug)", () => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { hangulToLatin, kanaToRomaji } from "../translit.js";
|
|
3
|
+
|
|
4
|
+
describe("hangulToLatin", () => {
|
|
5
|
+
it("romanizes basic syllables", () => {
|
|
6
|
+
expect(hangulToLatin("안녕하세요")).toBe("annyeonghaseyo");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("applies liaison before silent-ㅇ syllables", () => {
|
|
10
|
+
expect(hangulToLatin("한국어")).toBe("hangugeo");
|
|
11
|
+
expect(hangulToLatin("일지")).toBe("ilji");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("drops carried ㅎ in liaison", () => {
|
|
15
|
+
expect(hangulToLatin("좋아")).toBe("joa");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("splits compound finals in liaison", () => {
|
|
19
|
+
expect(hangulToLatin("읽어")).toBe("ilgeo");
|
|
20
|
+
expect(hangulToLatin("값이")).toBe("gapsi");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("keeps final ㅇ as ng before vowels", () => {
|
|
24
|
+
expect(hangulToLatin("강아지")).toBe("gangaji");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("uses phonetic finals", () => {
|
|
28
|
+
expect(hangulToLatin("옷")).toBe("ot");
|
|
29
|
+
expect(hangulToLatin("부엌")).toBe("bueok");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("preserves spaces and non-hangul characters", () => {
|
|
33
|
+
expect(hangulToLatin("개발 일지 3일차")).toBe("gaebal ilji 3ilcha");
|
|
34
|
+
expect(hangulToLatin("React 공부")).toBe("React gongbu");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("romanizes standalone compatibility jamo", () => {
|
|
38
|
+
expect(hangulToLatin("ㅋㅋㅋ")).toBe("kkk");
|
|
39
|
+
expect(hangulToLatin("ㅠㅠ")).toBe("yuyu");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("kanaToRomaji", () => {
|
|
44
|
+
it("romanizes hiragana", () => {
|
|
45
|
+
expect(kanaToRomaji("こんにちは")).toBe("konnichiha");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("romanizes katakana", () => {
|
|
49
|
+
expect(kanaToRomaji("カタカナタイトル")).toBe("katakanataitoru");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("drops the chōonpu long-vowel mark", () => {
|
|
53
|
+
expect(kanaToRomaji("ハンバーガー")).toBe("hanbaga");
|
|
54
|
+
expect(kanaToRomaji("ラーメン")).toBe("ramen");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("geminates consonants after sokuon", () => {
|
|
58
|
+
expect(kanaToRomaji("がっこう")).toBe("gakkou");
|
|
59
|
+
expect(kanaToRomaji("きっぷ")).toBe("kippu");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses tch for sokuon before chi", () => {
|
|
63
|
+
expect(kanaToRomaji("まっちゃ")).toBe("matcha");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles digraphs", () => {
|
|
67
|
+
expect(kanaToRomaji("きょうと")).toBe("kyouto");
|
|
68
|
+
expect(kanaToRomaji("しゃしん")).toBe("shashin");
|
|
69
|
+
expect(kanaToRomaji("じゅんび")).toBe("junbi");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles extended katakana combos", () => {
|
|
73
|
+
expect(kanaToRomaji("ファン")).toBe("fan");
|
|
74
|
+
expect(kanaToRomaji("パーティー")).toBe("pati");
|
|
75
|
+
expect(kanaToRomaji("チェック")).toBe("chekku");
|
|
76
|
+
expect(kanaToRomaji("ヴァイオリン")).toBe("vaiorin");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("passes non-kana characters through unchanged", () => {
|
|
80
|
+
expect(kanaToRomaji("東京タワー")).toBe("東京tawa");
|
|
81
|
+
expect(kanaToRomaji("abc カフェ")).toBe("abc kafe");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("ignores a dangling sokuon", () => {
|
|
85
|
+
expect(kanaToRomaji("あっ")).toBe("a");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
isFullUrl,
|
|
6
6
|
isSafeAbsoluteUrl,
|
|
7
7
|
normalizePath,
|
|
8
|
+
sanitizeRichTextHref,
|
|
9
|
+
sanitizeUrl,
|
|
8
10
|
stripSitePathPrefix,
|
|
9
11
|
slugify,
|
|
10
12
|
toSameSitePath,
|
|
@@ -211,6 +213,22 @@ describe("isSafeAbsoluteUrl", () => {
|
|
|
211
213
|
});
|
|
212
214
|
});
|
|
213
215
|
|
|
216
|
+
describe("URL sanitizers", () => {
|
|
217
|
+
it("keeps action protocols scoped to rich-text links", () => {
|
|
218
|
+
expect(sanitizeRichTextHref("tel:+15551234567")).toBe("tel:+15551234567");
|
|
219
|
+
expect(sanitizeRichTextHref("sms:xxxxx@xx.com")).toBe("sms:xxxxx@xx.com");
|
|
220
|
+
|
|
221
|
+
expect(sanitizeUrl("tel:+15551234567")).toBe("");
|
|
222
|
+
expect(sanitizeUrl("sms:xxxxx@xx.com")).toBe("");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("rejects executable protocols in every context", () => {
|
|
226
|
+
expect(sanitizeRichTextHref("javascript:alert(1)")).toBe("");
|
|
227
|
+
expect(sanitizeRichTextHref("data:text/html,<h1>Hi</h1>")).toBe("");
|
|
228
|
+
expect(sanitizeUrl("javascript:alert(1)")).toBe("");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
214
232
|
describe("slugify", () => {
|
|
215
233
|
it("converts text to lowercase hyphenated slug", () => {
|
|
216
234
|
expect(slugify("Hello World")).toBe("hello-world");
|
|
@@ -257,6 +275,41 @@ describe("slugify", () => {
|
|
|
257
275
|
it("handles CJK characters with spaces", () => {
|
|
258
276
|
expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
|
|
259
277
|
});
|
|
278
|
+
|
|
279
|
+
it("romanizes Korean titles", () => {
|
|
280
|
+
expect(slugify("안녕하세요 세계")).toBe("annyeonghaseyo-segye");
|
|
281
|
+
expect(slugify("개발 일지 3일차")).toBe("gaebal-ilji-3ilcha");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("romanizes kana-only Japanese titles", () => {
|
|
285
|
+
expect(slugify("カタカナタイトル")).toBe("katakanataitoru");
|
|
286
|
+
expect(slugify("ハンバーガー")).toBe("hanbaga");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("keeps Latin words in mostly-kana Japanese titles", () => {
|
|
290
|
+
expect(slugify("React入門ガイド")).toBe("react-gaido");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("returns empty for kanji-heavy Japanese titles instead of garbling them", () => {
|
|
294
|
+
// Kanji carries the meaning; without a reading dictionary any output
|
|
295
|
+
// would be gibberish, so callers should fall back to a random ID.
|
|
296
|
+
expect(slugify("日本語のタイトルです")).toBe("");
|
|
297
|
+
expect(slugify("東京タワーに行った")).toBe("");
|
|
298
|
+
expect(slugify("こんにちは世界")).toBe("");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("returns empty for scripts with no usable transliteration", () => {
|
|
302
|
+
expect(slugify("שלום עולם")).toBe("");
|
|
303
|
+
expect(slugify("สวัสดีชาวโลก")).toBe("");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns empty when most of the title is untransliterable", () => {
|
|
307
|
+
expect(slugify("שלום world")).toBe("");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("returns empty for emoji-only titles", () => {
|
|
311
|
+
expect(slugify("🎉🎉")).toBe("");
|
|
312
|
+
});
|
|
260
313
|
});
|
|
261
314
|
|
|
262
315
|
describe("stripSitePathPrefix", () => {
|