livemark 0.23.0 → 0.24.1

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 (281) hide show
  1. package/.agents/skills/change-review/SKILL.md +47 -0
  2. package/{skills/configuration → .agents/skills/livemark-configuration}/SKILL.md +2 -4
  3. package/build/actions/changelog/build.d.ts +21 -9
  4. package/build/actions/changelog/build.js +31 -23
  5. package/build/actions/changelog/build.unit.js +156 -2
  6. package/build/actions/config/load.js +1 -1
  7. package/build/actions/config/load.unit.js +1 -1
  8. package/build/actions/target/build.js +2 -5
  9. package/build/commands/build.js +1 -1
  10. package/build/commands/escape.js +1 -1
  11. package/build/commands/preview.js +1 -1
  12. package/build/commands/start.js +1 -1
  13. package/build/components/Abbr.js +2 -2
  14. package/build/components/Article.js +2 -2
  15. package/build/components/BlogIndex.js +2 -4
  16. package/build/components/Callout.js +2 -2
  17. package/build/components/ChangelogIndex.js +1 -1
  18. package/build/components/CodeBlock.js +3 -5
  19. package/build/components/CodeTabs.js +1 -1
  20. package/build/components/Columns.js +2 -4
  21. package/build/components/Content.js +2 -2
  22. package/build/components/ContentTab.js +1 -1
  23. package/build/components/ContentTabs.js +1 -1
  24. package/build/components/DefinitionList.js +1 -1
  25. package/build/components/FileTree.js +1 -1
  26. package/build/components/FootnoteRef.js +2 -2
  27. package/build/components/InlineToc.js +1 -1
  28. package/build/components/Layout.js +1 -1
  29. package/build/components/LinkButton.js +2 -2
  30. package/build/components/Mermaid.js +1 -1
  31. package/build/components/PackageTabs.js +2 -2
  32. package/build/components/PageToolbar.js +2 -2
  33. package/build/components/PrevNext.js +3 -7
  34. package/build/components/SearchDialog.js +1 -1
  35. package/build/components/Section.js +2 -2
  36. package/build/components/SectionHeader.js +2 -2
  37. package/build/components/Sidebar.js +2 -2
  38. package/build/components/SidebarArticles.js +2 -2
  39. package/build/components/SidebarLinks.js +2 -2
  40. package/build/components/TagIndex.js +2 -4
  41. package/build/components/Toc.js +1 -1
  42. package/build/content/article.js +1 -1
  43. package/build/content-collections.js +6 -7
  44. package/build/elements/accordion.js +2 -2
  45. package/build/elements/alert-dialog.js +5 -5
  46. package/build/elements/alert.js +2 -2
  47. package/build/elements/aspect-ratio.js +2 -2
  48. package/build/elements/avatar.d.ts +1 -1
  49. package/build/elements/avatar.js +3 -3
  50. package/build/elements/badge.js +2 -2
  51. package/build/elements/breadcrumb.js +2 -2
  52. package/build/elements/button-group.d.ts +1 -1
  53. package/build/elements/button-group.js +3 -3
  54. package/build/elements/button.js +2 -2
  55. package/build/elements/calendar.js +5 -5
  56. package/build/elements/card.js +2 -2
  57. package/build/elements/carousel.js +2 -2
  58. package/build/elements/chart.js +5 -15
  59. package/build/elements/checkbox.js +2 -2
  60. package/build/elements/collapsible.js +3 -3
  61. package/build/elements/command.js +2 -2
  62. package/build/elements/context-menu.js +5 -5
  63. package/build/elements/dialog.js +2 -2
  64. package/build/elements/direction.d.ts +1 -1
  65. package/build/elements/direction.js +2 -2
  66. package/build/elements/drawer.js +2 -2
  67. package/build/elements/dropdown-menu.js +3 -3
  68. package/build/elements/empty.d.ts +1 -1
  69. package/build/elements/empty.js +3 -3
  70. package/build/elements/field.js +2 -2
  71. package/build/elements/hover-card.js +3 -3
  72. package/build/elements/input-group.js +2 -2
  73. package/build/elements/input-otp.js +2 -2
  74. package/build/elements/input.js +2 -2
  75. package/build/elements/item.js +2 -2
  76. package/build/elements/kbd.js +2 -2
  77. package/build/elements/label.js +2 -2
  78. package/build/elements/menubar.js +2 -2
  79. package/build/elements/native-select.js +2 -2
  80. package/build/elements/navigation-menu.js +2 -2
  81. package/build/elements/pagination.js +3 -3
  82. package/build/elements/popover.js +2 -2
  83. package/build/elements/progress.d.ts +1 -1
  84. package/build/elements/progress.js +3 -3
  85. package/build/elements/radio-group.js +2 -2
  86. package/build/elements/resizable.js +3 -3
  87. package/build/elements/scroll-area.js +2 -2
  88. package/build/elements/select.js +2 -2
  89. package/build/elements/separator.js +2 -2
  90. package/build/elements/sheet.js +2 -2
  91. package/build/elements/sidebar.js +3 -4
  92. package/build/elements/skeleton.js +2 -2
  93. package/build/elements/slider.js +2 -2
  94. package/build/elements/spinner.js +2 -2
  95. package/build/elements/switch.js +2 -2
  96. package/build/elements/table.js +2 -2
  97. package/build/elements/tabs.js +2 -2
  98. package/build/elements/textarea.js +2 -2
  99. package/build/elements/toggle-group.js +2 -2
  100. package/build/elements/toggle.js +2 -2
  101. package/build/elements/tooltip.js +2 -2
  102. package/build/helpers/changelog-source.js +1 -1
  103. package/build/helpers/dynamic-icon-resolver.js +1 -1
  104. package/build/helpers/resolve-asset-path.unit.js +1 -1
  105. package/build/helpers/resolve-base.unit.js +2 -2
  106. package/build/helpers/resolve-includes.js +2 -2
  107. package/build/helpers/resolve-includes.unit.js +1 -1
  108. package/build/helpers/rss.js +2 -4
  109. package/build/helpers/rss.unit.js +1 -1
  110. package/build/helpers/section.js +2 -3
  111. package/build/helpers/section.unit.js +4 -13
  112. package/build/helpers/toc.unit.js +2 -4
  113. package/build/plugins/rehype-inline-code.js +2 -2
  114. package/build/plugins/rehype-task-list-a11y.js +1 -1
  115. package/build/plugins/remark-abbr.js +2 -4
  116. package/build/plugins/remark-ansi.js +2 -4
  117. package/build/plugins/remark-ansi.unit.js +1 -1
  118. package/build/plugins/remark-badge.js +2 -4
  119. package/build/plugins/remark-button.unit.js +1 -1
  120. package/build/plugins/remark-callout.js +1 -1
  121. package/build/plugins/remark-card.js +1 -1
  122. package/build/plugins/remark-code-tabs.js +4 -8
  123. package/build/plugins/remark-columns.js +1 -1
  124. package/build/plugins/remark-custom-heading-id.unit.js +1 -1
  125. package/build/plugins/remark-deflist.js +1 -1
  126. package/build/plugins/remark-details.js +2 -4
  127. package/build/plugins/remark-filetree.js +1 -1
  128. package/build/plugins/remark-filetree.unit.js +1 -1
  129. package/build/plugins/remark-media.js +3 -7
  130. package/build/plugins/remark-media.unit.js +1 -1
  131. package/build/plugins/remark-steps.unit.js +1 -1
  132. package/build/plugins/remark-tab.js +4 -2
  133. package/build/plugins/remark-tab.unit.js +3 -7
  134. package/build/plugins/remark-toc.unit.js +1 -1
  135. package/build/plugins/shiki-icon.js +1 -1
  136. package/build/plugins/shiki-line-highlight.js +1 -1
  137. package/build/plugins/vite-livemark.js +1 -1
  138. package/build/plugins/vite-robots.js +1 -1
  139. package/build/plugins/vite-robots.unit.js +1 -1
  140. package/build/routes/$.js +1 -1
  141. package/build/vite.config.js +3 -8
  142. package/package.json +17 -19
  143. package/source/actions/changelog/build.ts +32 -35
  144. package/source/actions/changelog/build.unit.ts +191 -1
  145. package/source/actions/config/load.ts +1 -3
  146. package/source/actions/config/load.unit.ts +2 -7
  147. package/source/actions/target/build.ts +1 -4
  148. package/source/commands/build.ts +1 -7
  149. package/source/commands/escape.ts +3 -8
  150. package/source/commands/preview.ts +1 -7
  151. package/source/commands/start.ts +1 -7
  152. package/source/components/Abbr.tsx +1 -5
  153. package/source/components/Article.tsx +1 -5
  154. package/source/components/BlogIndex.tsx +3 -9
  155. package/source/components/Callout.tsx +2 -7
  156. package/source/components/ChangelogIndex.tsx +1 -3
  157. package/source/components/CodeBlock.tsx +4 -14
  158. package/source/components/CodeTabs.tsx +1 -3
  159. package/source/components/Columns.tsx +1 -3
  160. package/source/components/Content.tsx +1 -5
  161. package/source/components/ContentTab.tsx +1 -4
  162. package/source/components/ContentTabs.tsx +3 -10
  163. package/source/components/DefinitionList.tsx +1 -4
  164. package/source/components/FileTree.tsx +1 -5
  165. package/source/components/FootnoteRef.tsx +1 -5
  166. package/source/components/InlineToc.tsx +1 -4
  167. package/source/components/Layout.tsx +3 -10
  168. package/source/components/LinkButton.tsx +2 -8
  169. package/source/components/Mermaid.tsx +1 -3
  170. package/source/components/PackageTabs.tsx +1 -5
  171. package/source/components/PageToolbar.tsx +1 -5
  172. package/source/components/PrevNext.tsx +2 -6
  173. package/source/components/SearchDialog.tsx +2 -6
  174. package/source/components/Section.tsx +1 -1
  175. package/source/components/SectionHeader.tsx +2 -8
  176. package/source/components/Sidebar.tsx +2 -7
  177. package/source/components/SidebarArticles.tsx +4 -20
  178. package/source/components/SidebarLinks.tsx +2 -6
  179. package/source/components/TagIndex.tsx +3 -9
  180. package/source/components/Toc.tsx +2 -7
  181. package/source/content/article.ts +1 -2
  182. package/source/content-collections.ts +6 -8
  183. package/source/elements/accordion.tsx +1 -1
  184. package/source/elements/alert-dialog.tsx +8 -30
  185. package/source/elements/alert.tsx +2 -5
  186. package/source/elements/aspect-ratio.tsx +1 -1
  187. package/source/elements/avatar.tsx +5 -21
  188. package/source/elements/badge.tsx +3 -5
  189. package/source/elements/breadcrumb.tsx +4 -14
  190. package/source/elements/button-group.tsx +2 -7
  191. package/source/elements/button.tsx +1 -1
  192. package/source/elements/calendar.tsx +7 -24
  193. package/source/elements/card.tsx +1 -1
  194. package/source/elements/carousel.tsx +4 -11
  195. package/source/elements/chart.tsx +9 -34
  196. package/source/elements/checkbox.tsx +1 -1
  197. package/source/elements/collapsible.tsx +2 -6
  198. package/source/elements/command.tsx +4 -13
  199. package/source/elements/context-menu.tsx +8 -25
  200. package/source/elements/dialog.tsx +4 -14
  201. package/source/elements/direction.tsx +1 -4
  202. package/source/elements/drawer.tsx +4 -10
  203. package/source/elements/dropdown-menu.tsx +5 -19
  204. package/source/elements/empty.tsx +3 -13
  205. package/source/elements/field.tsx +3 -7
  206. package/source/elements/hover-card.tsx +2 -4
  207. package/source/elements/input-group.tsx +15 -28
  208. package/source/elements/input-otp.tsx +1 -1
  209. package/source/elements/input.tsx +1 -1
  210. package/source/elements/item.tsx +4 -13
  211. package/source/elements/kbd.tsx +1 -1
  212. package/source/elements/label.tsx +1 -1
  213. package/source/elements/menubar.tsx +5 -14
  214. package/source/elements/native-select.tsx +4 -15
  215. package/source/elements/navigation-menu.tsx +2 -5
  216. package/source/elements/pagination.tsx +4 -14
  217. package/source/elements/popover.tsx +2 -5
  218. package/source/elements/progress.tsx +4 -16
  219. package/source/elements/radio-group.tsx +1 -1
  220. package/source/elements/resizable.tsx +4 -12
  221. package/source/elements/scroll-area.tsx +2 -6
  222. package/source/elements/select.tsx +4 -14
  223. package/source/elements/separator.tsx +1 -1
  224. package/source/elements/sheet.tsx +3 -10
  225. package/source/elements/sidebar.tsx +7 -25
  226. package/source/elements/skeleton.tsx +1 -1
  227. package/source/elements/slider.tsx +1 -1
  228. package/source/elements/spinner.tsx +1 -1
  229. package/source/elements/switch.tsx +1 -1
  230. package/source/elements/table.tsx +4 -13
  231. package/source/elements/tabs.tsx +1 -1
  232. package/source/elements/textarea.tsx +1 -1
  233. package/source/elements/toggle-group.tsx +2 -4
  234. package/source/elements/toggle.tsx +1 -1
  235. package/source/elements/tooltip.tsx +3 -10
  236. package/source/helpers/changelog-source.ts +1 -3
  237. package/source/helpers/dynamic-icon-resolver.tsx +1 -4
  238. package/source/helpers/resolve-asset-path.unit.ts +6 -12
  239. package/source/helpers/resolve-base.unit.ts +1 -2
  240. package/source/helpers/resolve-includes.ts +2 -8
  241. package/source/helpers/resolve-includes.unit.ts +9 -36
  242. package/source/helpers/rss.ts +3 -9
  243. package/source/helpers/rss.unit.ts +1 -3
  244. package/source/helpers/section.ts +1 -4
  245. package/source/helpers/section.unit.ts +5 -18
  246. package/source/helpers/toc.unit.ts +1 -3
  247. package/source/plugins/rehype-inline-code.ts +5 -21
  248. package/source/plugins/rehype-task-list-a11y.ts +1 -2
  249. package/source/plugins/remark-abbr.ts +1 -3
  250. package/source/plugins/remark-ansi.ts +2 -5
  251. package/source/plugins/remark-ansi.unit.ts +6 -18
  252. package/source/plugins/remark-badge.ts +1 -3
  253. package/source/plugins/remark-button.unit.ts +3 -9
  254. package/source/plugins/remark-callout.ts +35 -40
  255. package/source/plugins/remark-card.ts +25 -29
  256. package/source/plugins/remark-code-tabs.ts +3 -7
  257. package/source/plugins/remark-columns.ts +13 -17
  258. package/source/plugins/remark-custom-heading-id.unit.ts +2 -6
  259. package/source/plugins/remark-deflist.ts +1 -2
  260. package/source/plugins/remark-details.ts +28 -34
  261. package/source/plugins/remark-filetree.ts +28 -34
  262. package/source/plugins/remark-filetree.unit.ts +1 -4
  263. package/source/plugins/remark-media.ts +2 -6
  264. package/source/plugins/remark-media.unit.ts +2 -6
  265. package/source/plugins/remark-steps.unit.ts +2 -7
  266. package/source/plugins/remark-tab.ts +26 -30
  267. package/source/plugins/remark-tab.unit.ts +2 -6
  268. package/source/plugins/remark-toc.unit.ts +1 -3
  269. package/source/plugins/shiki-icon.ts +1 -4
  270. package/source/plugins/shiki-line-highlight.ts +4 -11
  271. package/source/plugins/vite-livemark.ts +3 -10
  272. package/source/plugins/vite-robots.ts +1 -6
  273. package/source/plugins/vite-robots.unit.ts +1 -3
  274. package/source/routes/$.tsx +1 -3
  275. package/source/styles/general.css +1 -5
  276. package/source/vite.config.ts +2 -7
  277. package/build/utils/style.d.ts +0 -2
  278. package/build/utils/style.js +0 -6
  279. package/source/utils/style.ts +0 -6
  280. /package/{skills/authoring → .agents/skills/livemark-authoring}/SKILL.md +0 -0
  281. /package/{skills/customization → .agents/skills/livemark-customization}/SKILL.md +0 -0
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "livemark",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "Livemark description",
5
5
  "license": "MIT",
6
6
  "author": "Evgeny Karev",
7
- "repository": "https://github.com/datisthq/livemark",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/datisthq/livemark"
10
+ },
8
11
  "bin": {
9
12
  "livemark": "./build/entrypoints/main.js"
10
13
  },
11
14
  "files": [
12
15
  "build",
13
16
  "source",
14
- "skills"
17
+ ".agents/skills"
15
18
  ],
16
19
  "type": "module",
17
20
  "sideEffects": false,
@@ -20,8 +23,7 @@
20
23
  "./components/*": "./source/components/*.tsx",
21
24
  "./elements/*": "./source/elements/*.tsx",
22
25
  "./hooks/*": "./source/hooks/*.ts",
23
- "./styles/*": "./source/styles/*.css",
24
- "./utils/*": "./source/utils/*.ts"
26
+ "./styles/*": "./source/styles/*.css"
25
27
  },
26
28
  "dependencies": {
27
29
  "@base-ui/react": "1.3.0",
@@ -50,14 +52,14 @@
50
52
  "@vitejs/plugin-react": "6.0.1",
51
53
  "chokidar": "^5.0.0",
52
54
  "class-variance-authority": "0.7.1",
53
- "clsx": "2.1.1",
54
55
  "cmdk": "1.1.1",
56
+ "cn": "0.2.4",
55
57
  "commander": "14.0.3",
56
58
  "embla-carousel-react": "8.6.0",
57
59
  "es-toolkit": "1.44.0",
58
60
  "github-slugger": "^2.0.0",
59
61
  "input-otp": "1.4.2",
60
- "katex": "^0.16.44",
62
+ "katex": "0.18.9",
61
63
  "lucide-react": "0.577.0",
62
64
  "mdast-util-directive": "^3.1.0",
63
65
  "mdast-util-to-string": "^4.0.0",
@@ -81,31 +83,28 @@
81
83
  "shadcn": "3.7.0",
82
84
  "shiki": "4.0.2",
83
85
  "sonner": "2.0.7",
84
- "tailwind-merge": "3.5.0",
85
86
  "tailwindcss": "4.2.2",
86
87
  "tw-animate-css": "1.4.0",
87
88
  "unified": "^11.0.5",
88
89
  "unist-util-visit": "^5.1.0",
89
90
  "vaul": "1.1.2",
90
- "vite": "8.0.13",
91
+ "vite": "8.3.1",
91
92
  "vite-plugin-svgr": "5.2.0",
92
93
  "zod": "4.3.6"
93
94
  },
94
95
  "devDependencies": {
95
96
  "@plausible-analytics/tracker": "^0.4.4",
96
- "@semantic-release/exec": "7.1.0",
97
97
  "@types/node": "24.2.0",
98
98
  "@typescript/native-preview": "7.0.0-dev.20260316.1",
99
- "@vitest/coverage-v8": "4.1.6",
100
- "conventional-changelog-conventionalcommits": "9.1.0",
99
+ "@vitest/coverage-v8": "5.0.1",
101
100
  "lefthook": "2.1.1",
102
101
  "livemark": "link:.",
103
- "npm-check-updates": "18.0.1",
102
+ "npm-check-updates": "23.1.0",
104
103
  "react": "19.2.4",
105
104
  "react-dom": "19.2.4",
106
- "semantic-release": "24.2.9",
107
105
  "tempy": "3.2.0",
108
- "vite-plus": "0.1.21",
106
+ "vite-plus": "1.0.0",
107
+ "vitest": "5.0.1",
109
108
  "wrangler": "4.85.0"
110
109
  },
111
110
  "peerDependencies": {
@@ -113,15 +112,15 @@
113
112
  "react-dom": "^19.0.0"
114
113
  },
115
114
  "engines": {
116
- "node": "^24.0.0",
115
+ "node": "^24.11.0",
117
116
  "pnpm": "^11.0.0"
118
117
  },
119
118
  "scripts": {
120
119
  "build": "tsgo --project tsconfig.build.json",
121
- "deps": "ncu -u",
120
+ "deps": "ncu -u --cooldown 7d",
122
121
  "docs:build": "node source/entrypoints/main.ts build",
123
122
  "docs:start": "node source/entrypoints/main.ts start",
124
- "format": "vp fmt",
123
+ "format": "vp fmt --no-error-on-unmatched-pattern",
125
124
  "leaks": "gitleaks git --config=.gitleaks.toml --verbose --redact",
126
125
  "lint": "vp fmt --check && vp lint",
127
126
  "release": "pnpm publish --access public --ignore-scripts --no-git-checks",
@@ -130,7 +129,6 @@
130
129
  "test": "pnpm lint && pnpm type && pnpm unit",
131
130
  "type": "tsgo --noEmit",
132
131
  "unit": "vp test",
133
- "vers": "pnpm version --no-git-tag-version",
134
132
  "vulns": "semgrep --config=p/typescript --config=p/nodejs --config=p/owasp-top-ten --config=p/secrets --exclude=\"**/fixtures/**\" --metrics=off"
135
133
  }
136
134
  }
@@ -1,11 +1,5 @@
1
- import {
2
- access,
3
- mkdir,
4
- readFile,
5
- readdir,
6
- rm,
7
- writeFile,
8
- } from "node:fs/promises"
1
+ import { createHash } from "node:crypto"
2
+ import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
9
3
  import { dirname, join, resolve } from "node:path"
10
4
  import slugify from "@sindresorhus/slugify"
11
5
  import type { Root, RootContent } from "mdast"
@@ -49,24 +43,31 @@ const stringifier = unified().use(remarkStringify, {
49
43
  emphasis: "_",
50
44
  })
51
45
 
52
- /** Glob (absolute path) covering all cached changelog markdown files for
53
- * a given consumer. Used by content-collections to include them in the
54
- * article scan. */
55
- export function cacheIncludeGlob(config: Config) {
56
- return join(targetDirFor(config.configPath), CACHE_SUBDIR, "**", "*.md")
46
+ /** Glob (absolute path) covering the cached changelog markdown files of
47
+ * one changelog section. Used by content-collections to include them in
48
+ * the article scan. */
49
+ export function cacheIncludeGlob(section: ChangelogSection, config: Config) {
50
+ return join(changelogCacheDir(section, config), "*.md")
57
51
  }
58
52
 
59
- /** Build the per-version changelog cache files. Livemark allows one
60
- * changelog per project, so all cache state lives in a single
61
- * `cache/changelog/`: the rendered `.md` entries plus a `meta.json`
62
- * holding the etag for conditional GETs. On refresh we wipe only the
63
- * `.md` files, leaving meta intact. */
64
- export async function buildChangelog(
65
- section: ChangelogSection,
66
- config: Config,
67
- ) {
68
- const targetDir = targetDirFor(config.configPath)
69
- const cacheDir = join(targetDir, CACHE_SUBDIR)
53
+ /** Directory holding one changelog section's cache state: the rendered
54
+ * `.md` entries plus a `meta.json` holding the etag for conditional
55
+ * GETs. Each section gets its own subdirectory of `cache/changelog/`,
56
+ * keyed by its prefix's slug plus a hash of the raw prefix
57
+ * (`/python/changelog/` → `python-changelog-<hash>`), so several
58
+ * changelogs never overwrite each other's entries even when their
59
+ * prefixes slugify alike. */
60
+ export function changelogCacheDir(section: ChangelogSection, config: Config) {
61
+ const slug = slugify(section.prefix) || "index"
62
+ const hash = createHash("sha256").update(section.prefix).digest("hex").slice(0, 8)
63
+ return join(targetDirFor(config.configPath), CACHE_SUBDIR, `${slug}-${hash}`)
64
+ }
65
+
66
+ /** Build the per-version changelog cache files of one changelog section
67
+ * into its own cache directory. On refresh we wipe only that
68
+ * directory's `.md` files, leaving meta intact. */
69
+ export async function buildChangelog(section: ChangelogSection, config: Config) {
70
+ const cacheDir = changelogCacheDir(section, config)
70
71
  const metaPath = join(cacheDir, "meta.json")
71
72
 
72
73
  const entries = isGitHubUrl(section.source)
@@ -147,10 +148,12 @@ function processBody(body: string) {
147
148
  return escapeMdxAngles(stringifier.stringify(tree).trimEnd())
148
149
  }
149
150
 
150
- /** Escape `<` to `\<` outside fenced code blocks and inline backtick spans
151
+ /** Escape `<` to `&lt;` outside fenced code blocks and inline backtick spans
151
152
  * so commit messages with placeholders like `<subdir>` don't get parsed as
152
- * JSX by MDX. */
153
- function escapeMdxAngles(markdown: string) {
153
+ * JSX by MDX. The entity is used rather than a `\<` backslash escape because
154
+ * MDX's JSX-aware parser does not honour the backslash and still reads
155
+ * `\<name>` as an unclosed tag. */
156
+ export function escapeMdxAngles(markdown: string) {
154
157
  let inFence = false
155
158
  return markdown
156
159
  .split("\n")
@@ -160,9 +163,7 @@ function escapeMdxAngles(markdown: string) {
160
163
  return line
161
164
  }
162
165
  if (inFence) return line
163
- return line.replace(/`[^`]*`|</g, match =>
164
- match === "<" ? "\\<" : match,
165
- )
166
+ return line.replace(/`[^`]*`|</g, match => (match === "<" ? "&lt;" : match))
166
167
  })
167
168
  .join("\n")
168
169
  }
@@ -230,11 +231,7 @@ async function buildFromGitHub(
230
231
  }
231
232
 
232
233
  function wrapFrontmatter(entry: ChangelogEntry, section: ChangelogSection) {
233
- const lines = [
234
- "---",
235
- `title: ${entry.title}`,
236
- `path: ${section.prefix}${entry.slug}/`,
237
- ]
234
+ const lines = ["---", `title: ${entry.title}`, `path: ${section.prefix}${entry.slug}/`]
238
235
  if (entry.date) lines.push(`date: ${entry.date}`)
239
236
  if (entry.sourceUrl) {
240
237
  lines.push(`sourceUrl: ${entry.sourceUrl}`, "sourceAction: view")
@@ -1,5 +1,159 @@
1
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
2
+ import { basename, dirname, join } from "node:path"
3
+ import { temporaryDirectoryTask } from "tempy"
1
4
  import { describe, expect, it } from "vite-plus/test"
2
- import { splitLocalChangelog } from "./build.ts"
5
+ import { partitionBySection } from "../../helpers/section.ts"
6
+ import { Config } from "../../models/config.ts"
7
+ import type { ChangelogSection } from "../../models/section.ts"
8
+ import { targetDirFor } from "../target/build.ts"
9
+ import {
10
+ buildChangelog,
11
+ cacheIncludeGlob,
12
+ changelogCacheDir,
13
+ escapeMdxAngles,
14
+ splitLocalChangelog,
15
+ } from "./build.ts"
16
+
17
+ describe("buildChangelog", () => {
18
+ it("keeps several changelog sections apart when they share a version", () =>
19
+ temporaryDirectoryTask(async root => {
20
+ const configPath = join(root, "livemark.config.ts")
21
+ const standard: ChangelogSection = {
22
+ type: "changelog",
23
+ title: "Changelog",
24
+ prefix: "/changelog/",
25
+ source: "standard/CHANGELOG.md",
26
+ version: true,
27
+ position: "sidebar",
28
+ }
29
+ const python: ChangelogSection = {
30
+ type: "changelog",
31
+ title: "Changelog",
32
+ prefix: "/python/changelog/",
33
+ source: "python/CHANGELOG.md",
34
+ version: true,
35
+ position: "sidebar",
36
+ }
37
+ const config = makeConfig(root, [standard, python])
38
+
39
+ await writeChangelog(
40
+ root,
41
+ "standard",
42
+ "## 0.2.0\n\nStandard two.\n\n## 0.1.0\n\nStandard one.\n",
43
+ )
44
+ await writeChangelog(root, "python", "## 0.2.0\n\nPython two.\n")
45
+
46
+ try {
47
+ await buildChangelog(standard, config)
48
+ await buildChangelog(python, config)
49
+
50
+ const standardDir = changelogCacheDir(standard, config)
51
+ const pythonDir = changelogCacheDir(python, config)
52
+ const cacheRoot = join(targetDirFor(configPath), "cache/changelog")
53
+ expect(dirname(standardDir)).toBe(cacheRoot)
54
+ expect(dirname(pythonDir)).toBe(cacheRoot)
55
+ expect(basename(standardDir)).toMatch(/^changelog-[0-9a-f]{8}$/)
56
+ expect(basename(pythonDir)).toMatch(/^python-changelog-[0-9a-f]{8}$/)
57
+ expect(cacheIncludeGlob(python, config)).toBe(join(pythonDir, "*.md"))
58
+
59
+ expect((await readdir(standardDir)).sort()).toEqual(["0-1-0.md", "0-2-0.md"])
60
+ expect(await readdir(pythonDir)).toEqual(["0-2-0.md"])
61
+
62
+ const standardEntry = await readFile(join(standardDir, "0-2-0.md"), "utf-8")
63
+ const pythonEntry = await readFile(join(pythonDir, "0-2-0.md"), "utf-8")
64
+ expect(standardEntry).toContain("path: /changelog/0-2-0/")
65
+ expect(standardEntry).toContain("Standard two.")
66
+ expect(pythonEntry).toContain("path: /python/changelog/0-2-0/")
67
+ expect(pythonEntry).toContain("Python two.")
68
+
69
+ const olderEntry = await readFile(join(standardDir, "0-1-0.md"), "utf-8")
70
+ const articles = [standardEntry, olderEntry, pythonEntry].map(markdown => ({
71
+ path: markdown.match(/^path: (.+)$/m)?.[1] ?? "",
72
+ }))
73
+ const buckets = partitionBySection(articles, [standard, python])
74
+ expect(buckets.get("/changelog/")?.map(a => a.path)).toEqual([
75
+ "/changelog/0-2-0/",
76
+ "/changelog/0-1-0/",
77
+ ])
78
+ expect(buckets.get("/python/changelog/")?.map(a => a.path)).toEqual([
79
+ "/python/changelog/0-2-0/",
80
+ ])
81
+ } finally {
82
+ await rm(targetDirFor(configPath), { recursive: true, force: true })
83
+ }
84
+ }))
85
+
86
+ it("cleans only its own cache directory on rebuild", () =>
87
+ temporaryDirectoryTask(async root => {
88
+ const configPath = join(root, "livemark.config.ts")
89
+ const standard: ChangelogSection = {
90
+ type: "changelog",
91
+ title: "Changelog",
92
+ prefix: "/changelog/",
93
+ source: "standard/CHANGELOG.md",
94
+ position: "sidebar",
95
+ }
96
+ const python: ChangelogSection = {
97
+ type: "changelog",
98
+ title: "Changelog",
99
+ prefix: "/python/changelog/",
100
+ source: "python/CHANGELOG.md",
101
+ position: "sidebar",
102
+ }
103
+ const config = makeConfig(root, [standard, python])
104
+
105
+ await writeChangelog(root, "standard", "## 0.2.0\n\nStandard two.\n")
106
+ await writeChangelog(root, "python", "## 0.3.0\n\nPython three.\n")
107
+
108
+ try {
109
+ await buildChangelog(standard, config)
110
+ await buildChangelog(python, config)
111
+ await writeChangelog(root, "standard", "## 0.4.0\n\nStandard four.\n")
112
+ await buildChangelog(standard, config)
113
+
114
+ expect(await readdir(changelogCacheDir(standard, config))).toEqual(["0-4-0.md"])
115
+ expect(await readdir(changelogCacheDir(python, config))).toEqual(["0-3-0.md"])
116
+ } finally {
117
+ await rm(targetDirFor(configPath), { recursive: true, force: true })
118
+ }
119
+ }))
120
+
121
+ it("keeps sections apart when their prefixes slugify to the same key", () =>
122
+ temporaryDirectoryTask(async root => {
123
+ const configPath = join(root, "livemark.config.ts")
124
+ const nested: ChangelogSection = {
125
+ type: "changelog",
126
+ title: "Changelog",
127
+ prefix: "/python/changelog/",
128
+ source: "nested/CHANGELOG.md",
129
+ position: "sidebar",
130
+ }
131
+ const flat: ChangelogSection = {
132
+ type: "changelog",
133
+ title: "Changelog",
134
+ prefix: "/python-changelog/",
135
+ source: "flat/CHANGELOG.md",
136
+ position: "sidebar",
137
+ }
138
+ const config = makeConfig(root, [nested, flat])
139
+
140
+ await writeChangelog(root, "nested", "## 0.1.0\n\nNested one.\n")
141
+ await writeChangelog(root, "flat", "## 0.2.0\n\nFlat two.\n")
142
+
143
+ try {
144
+ await buildChangelog(nested, config)
145
+ await buildChangelog(flat, config)
146
+
147
+ const nestedDir = changelogCacheDir(nested, config)
148
+ const flatDir = changelogCacheDir(flat, config)
149
+ expect(nestedDir).not.toBe(flatDir)
150
+ expect(await readdir(nestedDir)).toEqual(["0-1-0.md"])
151
+ expect(await readdir(flatDir)).toEqual(["0-2-0.md"])
152
+ } finally {
153
+ await rm(targetDirFor(configPath), { recursive: true, force: true })
154
+ }
155
+ }))
156
+ })
3
157
 
4
158
  describe("splitLocalChangelog", () => {
5
159
  it("splits by h2 headings", () => {
@@ -67,3 +221,39 @@ Release notes.
67
221
  expect(entries[0]?.date).toBeUndefined()
68
222
  })
69
223
  })
224
+
225
+ describe("escapeMdxAngles", () => {
226
+ it("escapes a bare angle placeholder as an HTML entity", () => {
227
+ // `\<` is honoured by remark but NOT by MDX's JSX-aware parser, which still
228
+ // reads `\<name>` as an unclosed tag. Only the entity survives both.
229
+ expect(escapeMdxAngles("a <name> b")).toBe("a &lt;name> b")
230
+ })
231
+
232
+ it("escapes a placeholder that follows a bare URL", () => {
233
+ const input = "See https://fairspec.org/profiles/latest/<name>.json, ok."
234
+ expect(escapeMdxAngles(input)).toBe(
235
+ "See https://fairspec.org/profiles/latest/&lt;name>.json, ok.",
236
+ )
237
+ })
238
+
239
+ it("leaves inline code spans alone", () => {
240
+ expect(escapeMdxAngles("use `--resource <name>` here")).toBe(
241
+ "use `--resource <name>` here",
242
+ )
243
+ })
244
+
245
+ it("leaves fenced code blocks alone", () => {
246
+ const input = "```sh\nfoo <bar>\n```"
247
+ expect(escapeMdxAngles(input)).toBe(input)
248
+ })
249
+ })
250
+
251
+ function makeConfig(root: string, sections: ChangelogSection[]) {
252
+ const configPath = join(root, "livemark.config.ts")
253
+ return { ...Config.parse({ root, configPath, sections }), base: undefined }
254
+ }
255
+
256
+ async function writeChangelog(root: string, dir: string, body: string) {
257
+ await mkdir(join(root, dir), { recursive: true })
258
+ await writeFile(join(root, dir, "CHANGELOG.md"), body, "utf-8")
259
+ }
@@ -33,9 +33,7 @@ function detectCommand() {
33
33
  * hash, watcher) still has a stable per-project key.
34
34
  */
35
35
  export async function loadConfig(path?: string): Promise<Config> {
36
- const configIndex = process.argv.findIndex(
37
- a => a === "-c" || a === "--config",
38
- )
36
+ const configIndex = process.argv.findIndex(a => a === "-c" || a === "--config")
39
37
  const cliPath = configIndex !== -1 ? process.argv[configIndex + 1] : undefined
40
38
  const explicit = path ?? cliPath
41
39
  const resolved = resolve(explicit ?? "livemark.config.ts")
@@ -6,9 +6,7 @@ import { loadConfig } from "./load.ts"
6
6
 
7
7
  describe("loadConfig", () => {
8
8
  it("should throw when an explicit path points at a missing file", async () => {
9
- await expect(loadConfig("/nonexistent/config.ts")).rejects.toThrow(
10
- /not found/,
11
- )
9
+ await expect(loadConfig("/nonexistent/config.ts")).rejects.toThrow(/not found/)
12
10
  })
13
11
 
14
12
  it("should return defaults when no path is given and cwd has no config", () =>
@@ -29,10 +27,7 @@ describe("loadConfig", () => {
29
27
  it("should load config from explicit path", () =>
30
28
  temporaryDirectoryTask(async tmpDir => {
31
29
  const configPath = join(tmpDir, "livemark.config.ts")
32
- fs.writeFileSync(
33
- configPath,
34
- 'export default { include: "articles/**/*.mdx" }\n',
35
- )
30
+ fs.writeFileSync(configPath, 'export default { include: "articles/**/*.mdx" }\n')
36
31
  const config = await loadConfig(configPath)
37
32
  expect(config.include).toBe("articles/**/*.mdx")
38
33
  expect(config.root).toBe(tmpDir)
@@ -81,9 +81,6 @@ export function buildTarget(configPath: string) {
81
81
  * rebuilding it. Used by code that needs to write into the target
82
82
  * (e.g. cache writers) outside of `buildTarget`'s setup phase. */
83
83
  export function targetDirFor(configPath: string) {
84
- const hash = createHash("sha256")
85
- .update(configPath)
86
- .digest("hex")
87
- .slice(0, 16)
84
+ const hash = createHash("sha256").update(configPath).digest("hex").slice(0, 16)
88
85
  return join(TARGETS_ROOT, hash)
89
86
  }
@@ -2,13 +2,7 @@ import { join } from "node:path"
2
2
  import { Command } from "commander"
3
3
  import { createBuilder } from "vite"
4
4
 
5
- const configFile = join(
6
- import.meta.dirname,
7
- "..",
8
- "..",
9
- "source",
10
- "vite.config.ts",
11
- )
5
+ const configFile = join(import.meta.dirname, "..", "..", "source", "vite.config.ts")
12
6
 
13
7
  /**
14
8
  * Build the site for production.
@@ -29,14 +29,11 @@ export const escape = new Command("escape")
29
29
  .action((relPath?: string) => {
30
30
  if (!relPath) {
31
31
  for (const file of listEscapable()) console.log(file)
32
- console.log(
33
- pc.dim("\nRun 'livemark escape <path>' to copy a file into .livemark/"),
34
- )
32
+ console.log(pc.dim("\nRun 'livemark escape <path>' to copy a file into .livemark/"))
35
33
  return
36
34
  }
37
35
 
38
- const isTopLevelFile =
39
- !relPath.includes("/") && ESCAPABLE_FILES.includes(relPath)
36
+ const isTopLevelFile = !relPath.includes("/") && ESCAPABLE_FILES.includes(relPath)
40
37
  const [first] = relPath.split("/")
41
38
  const isFolderPath =
42
39
  !!first && ESCAPABLE_FOLDERS.includes(first) && !relPath.includes("..")
@@ -63,9 +60,7 @@ export const escape = new Command("escape")
63
60
 
64
61
  mkdirSync(dirname(dest), { recursive: true })
65
62
  writeFileSync(dest, readFileSync(src))
66
- console.log(
67
- `Copied ${pc.dim(relPath)} → ${pc.green(`.livemark/${relPath}`)}`,
68
- )
63
+ console.log(`Copied ${pc.dim(relPath)} → ${pc.green(`.livemark/${relPath}`)}`)
69
64
  })
70
65
 
71
66
  function listEscapable() {
@@ -2,13 +2,7 @@ import { join } from "node:path"
2
2
  import { Command } from "commander"
3
3
  import { preview as vitePreview } from "vite"
4
4
 
5
- const configFile = join(
6
- import.meta.dirname,
7
- "..",
8
- "..",
9
- "source",
10
- "vite.config.ts",
11
- )
5
+ const configFile = join(import.meta.dirname, "..", "..", "source", "vite.config.ts")
12
6
 
13
7
  /**
14
8
  * Preview the production build locally.
@@ -2,13 +2,7 @@ import { join } from "node:path"
2
2
  import { Command } from "commander"
3
3
  import { createServer } from "vite"
4
4
 
5
- const configFile = join(
6
- import.meta.dirname,
7
- "..",
8
- "..",
9
- "source",
10
- "vite.config.ts",
11
- )
5
+ const configFile = join(import.meta.dirname, "..", "..", "source", "vite.config.ts")
12
6
 
13
7
  /**
14
8
  * Start a development server.
@@ -1,8 +1,4 @@
1
- import {
2
- Tooltip,
3
- TooltipContent,
4
- TooltipTrigger,
5
- } from "../elements/tooltip.tsx"
1
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../elements/tooltip.tsx"
6
2
 
7
3
  /** Renders an abbreviation with a shadcn tooltip showing its full form */
8
4
  export function Abbr(props: { text: string; title: string }) {
@@ -13,11 +13,7 @@ import { Column } from "./Column.tsx"
13
13
  import { Columns } from "./Columns.tsx"
14
14
  import { ContentTab } from "./ContentTab.tsx"
15
15
  import { ContentTabs } from "./ContentTabs.tsx"
16
- import {
17
- DefinitionDetail,
18
- DefinitionList,
19
- DefinitionTerm,
20
- } from "./DefinitionList.tsx"
16
+ import { DefinitionDetail, DefinitionList, DefinitionTerm } from "./DefinitionList.tsx"
21
17
  import { Details } from "./Details.tsx"
22
18
  import { FileTree } from "./FileTree.tsx"
23
19
  import { Footer } from "./Footer.tsx"
@@ -31,9 +31,7 @@ export function BlogIndex(props: { sectionPrefix: string }) {
31
31
  const parts = [`## ${post.title}`]
32
32
  if (post.date) parts.push(formatDate(post.date))
33
33
  if (post.author) {
34
- parts.push(
35
- Array.isArray(post.author) ? post.author.join(", ") : post.author,
36
- )
34
+ parts.push(Array.isArray(post.author) ? post.author.join(", ") : post.author)
37
35
  }
38
36
  if (post.description) parts.push("", post.description)
39
37
  return parts.join("\n")
@@ -77,16 +75,12 @@ export function BlogIndex(props: { sectionPrefix: string }) {
77
75
  {post.date && <span>{formatDate(post.date)}</span>}
78
76
  {post.author && (
79
77
  <span>
80
- {Array.isArray(post.author)
81
- ? post.author.join(", ")
82
- : post.author}
78
+ {Array.isArray(post.author) ? post.author.join(", ") : post.author}
83
79
  </span>
84
80
  )}
85
81
  </p>
86
82
  {post.description && (
87
- <p className="text-muted-foreground mt-2">
88
- {post.description}
89
- </p>
83
+ <p className="text-muted-foreground mt-2">{post.description}</p>
90
84
  )}
91
85
  </article>
92
86
  ))}
@@ -1,5 +1,5 @@
1
1
  import { AlertTriangle, Flame, Info, Lightbulb } from "lucide-react"
2
- import { cn } from "../utils/style.ts"
2
+ import { cn } from "cn"
3
3
 
4
4
  type CalloutType = "note" | "tip" | "info" | "warning" | "danger"
5
5
 
@@ -54,12 +54,7 @@ export function Callout(props: {
54
54
  const title = props.title ?? config.label
55
55
 
56
56
  return (
57
- <aside
58
- className={cn(
59
- "not-prose my-4 rounded-xl border px-4 py-3",
60
- config.classes,
61
- )}
62
- >
57
+ <aside className={cn("not-prose my-4 rounded-xl border px-4 py-3", config.classes)}>
63
58
  <div className="flex items-center gap-2 font-semibold text-sm mb-2">
64
59
  <Icon className="size-4 shrink-0" />
65
60
  <span>{title}</span>
@@ -74,9 +74,7 @@ export function ChangelogIndex(props: { sectionPrefix: string }) {
74
74
  </p>
75
75
  )}
76
76
  {entry.description && (
77
- <p className="text-muted-foreground mt-2">
78
- {entry.description}
79
- </p>
77
+ <p className="text-muted-foreground mt-2">{entry.description}</p>
80
78
  )}
81
79
  </article>
82
80
  ))}