livemark 0.2.0 → 0.7.0

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 (346) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +5 -1
  3. package/build/actions/changelog/build.d.ts +14 -0
  4. package/build/actions/changelog/build.js +158 -0
  5. package/build/actions/changelog/build.unit.js +66 -0
  6. package/build/actions/config/define.d.ts +6 -0
  7. package/build/actions/config/define.js +8 -0
  8. package/build/actions/config/define.unit.js +29 -0
  9. package/build/actions/config/load.d.ts +5 -0
  10. package/build/actions/config/load.js +25 -0
  11. package/build/actions/config/load.unit.js +23 -0
  12. package/build/commands/build.d.ts +5 -0
  13. package/build/commands/build.js +14 -0
  14. package/build/commands/escape.d.ts +9 -0
  15. package/build/commands/escape.js +56 -0
  16. package/build/commands/preview.d.ts +5 -0
  17. package/build/commands/preview.js +18 -0
  18. package/build/commands/start.d.ts +5 -0
  19. package/build/commands/start.js +19 -0
  20. package/build/docs/markdown/includes/example.d.ts +58 -0
  21. package/build/docs/markdown/includes/example.js +5 -0
  22. package/build/entrypoints/main.d.ts +2 -0
  23. package/build/entrypoints/main.js +12 -0
  24. package/build/helpers/program.d.ts +2 -0
  25. package/build/helpers/program.js +5 -0
  26. package/build/index.d.ts +1 -4
  27. package/build/index.js +2 -5
  28. package/build/livemark.config.d.ts +58 -0
  29. package/build/livemark.config.js +25 -0
  30. package/build/models/config.d.ts +340 -0
  31. package/build/models/config.js +34 -0
  32. package/build/models/link.d.ts +13 -0
  33. package/build/models/link.js +9 -0
  34. package/build/models/patch.d.ts +21 -0
  35. package/build/models/patch.js +7 -0
  36. package/build/models/section.d.ts +75 -0
  37. package/build/models/section.js +26 -0
  38. package/build/models/theme.d.ts +72 -0
  39. package/build/models/theme.js +73 -0
  40. package/build/program.d.ts +2 -0
  41. package/build/program.js +16 -0
  42. package/build/vite.config.d.ts +2 -0
  43. package/build/vite.config.js +48 -0
  44. package/build/website/assets/logo.svg +117 -0
  45. package/build/website/client.tsx +10 -0
  46. package/build/website/components/Abbr.tsx +17 -0
  47. package/build/website/components/AnsiCode.tsx +15 -0
  48. package/build/website/components/Article.tsx +172 -0
  49. package/build/website/components/BackToTop.tsx +32 -0
  50. package/build/website/components/Banner.tsx +32 -0
  51. package/build/website/components/BlogIndex.tsx +121 -0
  52. package/build/website/components/BlogSidebar.tsx +162 -0
  53. package/build/website/components/Breadcrumbs.tsx +22 -0
  54. package/build/website/components/Callout.tsx +72 -0
  55. package/build/website/components/Card.tsx +48 -0
  56. package/build/website/components/Cards.tsx +8 -0
  57. package/build/website/components/ChangelogIndex.tsx +111 -0
  58. package/build/website/components/ChangelogSidebar.tsx +120 -0
  59. package/build/website/components/CodeBlock.tsx +135 -0
  60. package/build/website/components/CodeTabs.tsx +83 -0
  61. package/build/website/components/Column.tsx +4 -0
  62. package/build/website/components/Columns.tsx +12 -0
  63. package/build/website/components/Content.tsx +7 -0
  64. package/build/website/components/ContentTab.tsx +7 -0
  65. package/build/website/components/ContentTabs.tsx +82 -0
  66. package/build/website/components/DefaultCatchBoundary.tsx +54 -0
  67. package/build/website/components/DefinitionList.tsx +21 -0
  68. package/build/website/components/Details.tsx +30 -0
  69. package/build/website/components/FileTree.tsx +52 -0
  70. package/build/website/components/Footer.tsx +16 -0
  71. package/build/website/components/FootnoteRef.tsx +52 -0
  72. package/build/website/components/Heading.tsx +20 -0
  73. package/build/website/components/InlineBadge.tsx +9 -0
  74. package/build/website/components/InlineIcon.tsx +10 -0
  75. package/build/website/components/InlineToc.tsx +31 -0
  76. package/build/website/components/Layout.tsx +190 -0
  77. package/build/website/components/LinkButton.tsx +31 -0
  78. package/build/website/components/Mermaid.tsx +18 -0
  79. package/build/website/components/MermaidRenderer.tsx +37 -0
  80. package/build/website/components/MiniSidebar.tsx +49 -0
  81. package/build/website/components/NotFound.tsx +27 -0
  82. package/build/website/components/PackageTabs.tsx +74 -0
  83. package/build/website/components/PageToolbar.tsx +75 -0
  84. package/build/website/components/PrevNext.tsx +60 -0
  85. package/build/website/components/Search.tsx +28 -0
  86. package/build/website/components/SearchDialog.tsx +245 -0
  87. package/build/website/components/Section.tsx +24 -0
  88. package/build/website/components/SectionHeader.tsx +35 -0
  89. package/build/website/components/Sidebar.tsx +160 -0
  90. package/build/website/components/SidebarLinks.tsx +70 -0
  91. package/build/website/components/SidebarSections.tsx +71 -0
  92. package/build/website/components/SiteTitle.tsx +21 -0
  93. package/build/website/components/SortableTable.tsx +81 -0
  94. package/build/website/components/SoundCloud.tsx +16 -0
  95. package/build/website/components/TagIndex.tsx +120 -0
  96. package/build/website/components/Theme.tsx +39 -0
  97. package/build/website/components/Toc.tsx +226 -0
  98. package/build/website/components/YouTube.tsx +16 -0
  99. package/build/website/components/ZoomImage.tsx +25 -0
  100. package/build/website/content/article.ts +78 -0
  101. package/build/website/content/tag.ts +29 -0
  102. package/build/website/content-collections.ts +208 -0
  103. package/build/website/elements/accordion.tsx +78 -0
  104. package/build/website/elements/alert-dialog.tsx +185 -0
  105. package/build/website/elements/alert.tsx +74 -0
  106. package/build/website/elements/aspect-ratio.tsx +22 -0
  107. package/build/website/elements/avatar.tsx +107 -0
  108. package/build/website/elements/badge.tsx +51 -0
  109. package/build/website/elements/breadcrumb.tsx +120 -0
  110. package/build/website/elements/button-group.tsx +87 -0
  111. package/build/website/elements/button.tsx +57 -0
  112. package/build/website/elements/calendar.tsx +229 -0
  113. package/build/website/elements/card.tsx +102 -0
  114. package/build/website/elements/carousel.tsx +240 -0
  115. package/build/website/elements/chart.tsx +372 -0
  116. package/build/website/elements/checkbox.tsx +26 -0
  117. package/build/website/elements/collapsible.tsx +19 -0
  118. package/build/website/elements/command.tsx +210 -0
  119. package/build/website/elements/context-menu.tsx +270 -0
  120. package/build/website/elements/dialog.tsx +152 -0
  121. package/build/website/elements/direction.tsx +4 -0
  122. package/build/website/elements/drawer.tsx +127 -0
  123. package/build/website/elements/dropdown-menu.tsx +260 -0
  124. package/build/website/elements/empty.tsx +104 -0
  125. package/build/website/elements/field.tsx +235 -0
  126. package/build/website/elements/hover-card.tsx +49 -0
  127. package/build/website/elements/input-group.tsx +171 -0
  128. package/build/website/elements/input-otp.tsx +84 -0
  129. package/build/website/elements/input.tsx +18 -0
  130. package/build/website/elements/item.tsx +199 -0
  131. package/build/website/elements/kbd.tsx +26 -0
  132. package/build/website/elements/label.tsx +17 -0
  133. package/build/website/elements/menubar.tsx +282 -0
  134. package/build/website/elements/native-select.tsx +56 -0
  135. package/build/website/elements/navigation-menu.tsx +171 -0
  136. package/build/website/elements/pagination.tsx +133 -0
  137. package/build/website/elements/popover.tsx +88 -0
  138. package/build/website/elements/progress.tsx +81 -0
  139. package/build/website/elements/radio-group.tsx +36 -0
  140. package/build/website/elements/resizable.tsx +48 -0
  141. package/build/website/elements/scroll-area.tsx +51 -0
  142. package/build/website/elements/select.tsx +200 -0
  143. package/build/website/elements/separator.tsx +22 -0
  144. package/build/website/elements/sheet.tsx +130 -0
  145. package/build/website/elements/sidebar.tsx +717 -0
  146. package/build/website/elements/skeleton.tsx +13 -0
  147. package/build/website/elements/slider.tsx +57 -0
  148. package/build/website/elements/sonner.tsx +40 -0
  149. package/build/website/elements/spinner.tsx +16 -0
  150. package/build/website/elements/switch.tsx +30 -0
  151. package/build/website/elements/table.tsx +112 -0
  152. package/build/website/elements/tabs.tsx +79 -0
  153. package/build/website/elements/textarea.tsx +16 -0
  154. package/build/website/elements/toggle-group.tsx +87 -0
  155. package/build/website/elements/toggle.tsx +43 -0
  156. package/build/website/elements/tooltip.tsx +67 -0
  157. package/build/website/env.d.ts +14 -0
  158. package/build/website/helpers/article-icon.ts +61 -0
  159. package/build/website/helpers/article.ts +90 -0
  160. package/build/website/helpers/changelog-source.ts +16 -0
  161. package/build/website/helpers/changelog-source.unit.ts +38 -0
  162. package/build/website/helpers/dynamic-icon-resolver.tsx +19 -0
  163. package/build/website/helpers/dynamic-icon.tsx +12 -0
  164. package/build/website/helpers/favicon.ts +7 -0
  165. package/build/website/helpers/model.ts +4 -0
  166. package/build/website/helpers/resolve-asset-path.ts +9 -0
  167. package/build/website/helpers/resolve-includes.ts +131 -0
  168. package/build/website/helpers/resolve-includes.unit.ts +126 -0
  169. package/build/website/helpers/search-text.ts +15 -0
  170. package/build/website/helpers/section.ts +50 -0
  171. package/build/website/helpers/section.unit.ts +71 -0
  172. package/build/website/helpers/style.ts +6 -0
  173. package/build/website/helpers/theme.ts +27 -0
  174. package/build/website/helpers/toc-context.ts +10 -0
  175. package/build/website/helpers/toc.ts +46 -0
  176. package/build/website/helpers/toc.unit.ts +74 -0
  177. package/build/website/hooks/in-view.ts +30 -0
  178. package/build/website/hooks/media.ts +19 -0
  179. package/build/website/models/article.ts +55 -0
  180. package/build/website/models/toc.ts +5 -0
  181. package/build/website/plugins/rehype-inline-code.ts +107 -0
  182. package/build/website/plugins/rehype-task-list-a11y.ts +33 -0
  183. package/build/website/plugins/remark-abbr.ts +46 -0
  184. package/build/website/plugins/remark-abbr.unit.ts +99 -0
  185. package/build/website/plugins/remark-ansi.ts +248 -0
  186. package/build/website/plugins/remark-ansi.unit.ts +197 -0
  187. package/build/website/plugins/remark-badge.ts +50 -0
  188. package/build/website/plugins/remark-badge.unit.ts +96 -0
  189. package/build/website/plugins/remark-button.ts +60 -0
  190. package/build/website/plugins/remark-button.unit.ts +147 -0
  191. package/build/website/plugins/remark-callout.ts +57 -0
  192. package/build/website/plugins/remark-card.ts +80 -0
  193. package/build/website/plugins/remark-code-tabs.ts +87 -0
  194. package/build/website/plugins/remark-columns.ts +66 -0
  195. package/build/website/plugins/remark-custom-heading-id.ts +70 -0
  196. package/build/website/plugins/remark-custom-heading-id.unit.ts +131 -0
  197. package/build/website/plugins/remark-deflist.ts +36 -0
  198. package/build/website/plugins/remark-details.ts +49 -0
  199. package/build/website/plugins/remark-filetree.ts +89 -0
  200. package/build/website/plugins/remark-filetree.unit.ts +137 -0
  201. package/build/website/plugins/remark-github-callout.ts +71 -0
  202. package/build/website/plugins/remark-icon.ts +42 -0
  203. package/build/website/plugins/remark-image.ts +34 -0
  204. package/build/website/plugins/remark-media.ts +62 -0
  205. package/build/website/plugins/remark-media.unit.ts +74 -0
  206. package/build/website/plugins/remark-mermaid.ts +23 -0
  207. package/build/website/plugins/remark-npm.ts +52 -0
  208. package/build/website/plugins/remark-steps.ts +36 -0
  209. package/build/website/plugins/remark-steps.unit.ts +97 -0
  210. package/build/website/plugins/remark-tab.ts +100 -0
  211. package/build/website/plugins/remark-tab.unit.ts +181 -0
  212. package/build/website/plugins/remark-toc.ts +39 -0
  213. package/build/website/plugins/remark-toc.unit.ts +82 -0
  214. package/build/website/plugins/shiki-icon.ts +82 -0
  215. package/build/website/plugins/shiki-line-highlight.ts +174 -0
  216. package/build/website/plugins/vite-livemark.ts +91 -0
  217. package/build/website/routeTree.gen.ts +86 -0
  218. package/build/website/router.tsx +34 -0
  219. package/build/website/routes/$.tsx +92 -0
  220. package/build/website/routes/__root.tsx +97 -0
  221. package/build/website/server.ts +7 -0
  222. package/build/website/styles/general.css +163 -0
  223. package/build/website/styles/markdown.css +214 -0
  224. package/build/website/vite.config.ts +71 -0
  225. package/entrypoints/main.ts +12 -0
  226. package/package.json +120 -12
  227. package/build/descriptor/index.d.ts +0 -3
  228. package/build/descriptor/index.js +0 -4
  229. package/build/descriptor/load.d.ts +0 -8
  230. package/build/descriptor/load.js +0 -40
  231. package/build/descriptor/node.d.ts +0 -125
  232. package/build/descriptor/node.js +0 -9
  233. package/build/descriptor/save.d.ts +0 -8
  234. package/build/descriptor/save.js +0 -17
  235. package/build/descriptor/save.spec.js +0 -80
  236. package/build/descriptor/validate.d.ts +0 -9
  237. package/build/descriptor/validate.js +0 -14
  238. package/build/descriptor/validate.spec.js +0 -124
  239. package/build/dialect/Dialect.d.ts +0 -88
  240. package/build/dialect/Dialect.js +0 -2
  241. package/build/dialect/index.d.ts +0 -3
  242. package/build/dialect/index.js +0 -3
  243. package/build/dialect/load.d.ts +0 -4
  244. package/build/dialect/load.js +0 -7
  245. package/build/dialect/save.d.ts +0 -9
  246. package/build/dialect/save.js +0 -13
  247. package/build/dialect/validate.d.ts +0 -1
  248. package/build/dialect/validate.js +0 -4
  249. package/build/metadata/index.d.ts +0 -3
  250. package/build/metadata/index.js +0 -4
  251. package/build/metadata/load.d.ts +0 -10
  252. package/build/metadata/load.js +0 -55
  253. package/build/metadata/node.d.ts +0 -125
  254. package/build/metadata/node.js +0 -9
  255. package/build/metadata/save.d.ts +0 -8
  256. package/build/metadata/save.js +0 -17
  257. package/build/metadata/save.spec.js +0 -80
  258. package/build/metadata/validate.d.ts +0 -9
  259. package/build/metadata/validate.js +0 -14
  260. package/build/metadata/validate.spec.d.ts +0 -1
  261. package/build/metadata/validate.spec.js +0 -124
  262. package/build/package/Contributor.d.ts +0 -21
  263. package/build/package/Contributor.js +0 -2
  264. package/build/package/Package.d.ts +0 -65
  265. package/build/package/Package.js +0 -2
  266. package/build/package/index.d.ts +0 -4
  267. package/build/package/index.js +0 -3
  268. package/build/package/load.d.ts +0 -4
  269. package/build/package/load.js +0 -7
  270. package/build/package/save.d.ts +0 -9
  271. package/build/package/save.js +0 -13
  272. package/build/package/validate.d.ts +0 -1
  273. package/build/package/validate.js +0 -4
  274. package/build/resource/License.d.ts +0 -18
  275. package/build/resource/License.js +0 -2
  276. package/build/resource/Resource.d.ts +0 -67
  277. package/build/resource/Resource.js +0 -2
  278. package/build/resource/Source.d.ts +0 -17
  279. package/build/resource/Source.js +0 -2
  280. package/build/resource/index.d.ts +0 -5
  281. package/build/resource/index.js +0 -2
  282. package/build/resource/load.d.ts +0 -1
  283. package/build/resource/load.js +0 -2
  284. package/build/resource/save.d.ts +0 -9
  285. package/build/resource/save.js +0 -13
  286. package/build/resource/types/index.d.ts +0 -1
  287. package/build/resource/types/index.js +0 -2
  288. package/build/resource/types/table.d.ts +0 -29
  289. package/build/resource/types/table.js +0 -2
  290. package/build/resource/validate.d.ts +0 -1
  291. package/build/resource/validate.js +0 -4
  292. package/build/schema/Field.d.ts +0 -5
  293. package/build/schema/Field.js +0 -2
  294. package/build/schema/Schema.d.ts +0 -55
  295. package/build/schema/Schema.js +0 -2
  296. package/build/schema/fields/any.d.ts +0 -20
  297. package/build/schema/fields/any.js +0 -2
  298. package/build/schema/fields/array.d.ts +0 -32
  299. package/build/schema/fields/array.js +0 -2
  300. package/build/schema/fields/base.d.ts +0 -42
  301. package/build/schema/fields/base.js +0 -2
  302. package/build/schema/fields/boolean.d.ts +0 -28
  303. package/build/schema/fields/boolean.js +0 -2
  304. package/build/schema/fields/date.d.ts +0 -35
  305. package/build/schema/fields/date.js +0 -2
  306. package/build/schema/fields/datetime.d.ts +0 -35
  307. package/build/schema/fields/datetime.js +0 -2
  308. package/build/schema/fields/duration.d.ts +0 -28
  309. package/build/schema/fields/duration.js +0 -2
  310. package/build/schema/fields/geojson.d.ts +0 -26
  311. package/build/schema/fields/geojson.js +0 -2
  312. package/build/schema/fields/geopoint.d.ts +0 -27
  313. package/build/schema/fields/geopoint.js +0 -2
  314. package/build/schema/fields/index.d.ts +0 -16
  315. package/build/schema/fields/index.js +0 -17
  316. package/build/schema/fields/integer.d.ts +0 -37
  317. package/build/schema/fields/integer.js +0 -2
  318. package/build/schema/fields/list.d.ts +0 -36
  319. package/build/schema/fields/list.js +0 -2
  320. package/build/schema/fields/number.d.ts +0 -40
  321. package/build/schema/fields/number.js +0 -2
  322. package/build/schema/fields/object.d.ts +0 -32
  323. package/build/schema/fields/object.js +0 -2
  324. package/build/schema/fields/string.d.ts +0 -40
  325. package/build/schema/fields/string.js +0 -2
  326. package/build/schema/fields/time.d.ts +0 -35
  327. package/build/schema/fields/time.js +0 -2
  328. package/build/schema/fields/year.d.ts +0 -28
  329. package/build/schema/fields/year.js +0 -2
  330. package/build/schema/fields/yearmonth.d.ts +0 -28
  331. package/build/schema/fields/yearmonth.js +0 -2
  332. package/build/schema/index.d.ts +0 -4
  333. package/build/schema/index.js +0 -2
  334. package/build/schema/load.d.ts +0 -1
  335. package/build/schema/load.js +0 -2
  336. package/build/schema/save.d.ts +0 -9
  337. package/build/schema/save.js +0 -13
  338. package/build/schema/validate.d.ts +0 -1
  339. package/build/schema/validate.js +0 -4
  340. package/build/tsconfig.tsbuildinfo +0 -1
  341. package/index.ts +0 -0
  342. package/tsconfig.json +0 -10
  343. package/typedoc.json +0 -4
  344. /package/build/{descriptor/save.spec.d.ts → actions/changelog/build.unit.d.ts} +0 -0
  345. /package/build/{descriptor/validate.spec.d.ts → actions/config/define.unit.d.ts} +0 -0
  346. /package/build/{metadata/save.spec.d.ts → actions/config/load.unit.d.ts} +0 -0
@@ -0,0 +1,46 @@
1
+ import GithubSlugger from "github-slugger"
2
+ import type { TocItem } from "../models/toc.ts"
3
+
4
+ const CUSTOM_ID_PATTERN = /\s*\[#([^\]]+)\]\s*$/
5
+ const TOC_HIDDEN_PATTERN = /\s*\[!toc\]\s*$/
6
+ const TOC_ONLY_PATTERN = /\s*\[toc\]\s*$/
7
+ const STEP_PATTERN = /\s*\[step\]\s*$/
8
+
9
+ /** Extract TOC headings from raw markdown content */
10
+ export function extractToc(content: string): TocItem[] {
11
+ const slugger = new GithubSlugger()
12
+ const items: TocItem[] = []
13
+ const headingRegex = /^(#{2,4})\s+(.+)$/
14
+ const lines = content.split("\n")
15
+ let inCodeBlock = false
16
+
17
+ for (const line of lines) {
18
+ if (line.startsWith("```") || line.startsWith("````")) {
19
+ inCodeBlock = !inCodeBlock
20
+ continue
21
+ }
22
+ if (inCodeBlock) continue
23
+
24
+ const match = headingRegex.exec(line)
25
+ if (!match) continue
26
+
27
+ const depth = match[1]!.length
28
+ const rawTitle = match[2]!
29
+
30
+ if (TOC_HIDDEN_PATTERN.test(rawTitle)) continue
31
+ if (STEP_PATTERN.test(rawTitle)) continue
32
+
33
+ const idMatch = CUSTOM_ID_PATTERN.exec(rawTitle)
34
+ const title = rawTitle
35
+ .replace(CUSTOM_ID_PATTERN, "")
36
+ .replace(TOC_ONLY_PATTERN, "")
37
+ .replace(STEP_PATTERN, "")
38
+ .replace(/\*\*(.+?)\*\*/g, "$1")
39
+ .replace(/`(.+?)`/g, "$1")
40
+ const slug = idMatch?.[1] ?? slugger.slug(title)
41
+
42
+ items.push({ url: `#${slug}`, title, depth })
43
+ }
44
+
45
+ return items
46
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { extractToc } from "./toc.ts"
3
+
4
+ describe("extractToc", () => {
5
+ it("should extract headings with auto-generated slugs", () => {
6
+ const content = "## Introduction\n\n### Getting Started"
7
+ const toc = extractToc(content)
8
+ expect(toc).toEqual([
9
+ { url: "#introduction", title: "Introduction", depth: 2 },
10
+ { url: "#getting-started", title: "Getting Started", depth: 3 },
11
+ ])
12
+ })
13
+
14
+ it("should use custom id when [#custom-id] syntax is present", () => {
15
+ const content = "## My Section [#custom-id]\n\n### Sub Section"
16
+ const toc = extractToc(content)
17
+ expect(toc).toEqual([
18
+ { url: "#custom-id", title: "My Section", depth: 2 },
19
+ { url: "#sub-section", title: "Sub Section", depth: 3 },
20
+ ])
21
+ })
22
+
23
+ it("should strip [#id] from the displayed title", () => {
24
+ const content = "## Installation [#install]"
25
+ const toc = extractToc(content)
26
+ expect(toc).toEqual([{ url: "#install", title: "Installation", depth: 2 }])
27
+ })
28
+
29
+ it("should handle inline formatting with custom ids", () => {
30
+ const content = "## **Bold** Title [#bold-title]"
31
+ const toc = extractToc(content)
32
+ expect(toc).toEqual([{ url: "#bold-title", title: "Bold Title", depth: 2 }])
33
+ })
34
+
35
+ it("should handle code formatting with custom ids", () => {
36
+ const content = "## `config` Options [#config-opts]"
37
+ const toc = extractToc(content)
38
+ expect(toc).toEqual([
39
+ { url: "#config-opts", title: "config Options", depth: 2 },
40
+ ])
41
+ })
42
+
43
+ it("should skip h1 headings", () => {
44
+ const content = "# Title\n\n## Section"
45
+ const toc = extractToc(content)
46
+ expect(toc).toEqual([{ url: "#section", title: "Section", depth: 2 }])
47
+ })
48
+
49
+ it("should exclude [step] headings from TOC", () => {
50
+ const content =
51
+ "## Overview\n\n### Install Dependencies [step]\n\n### Configure Project [step]"
52
+ const toc = extractToc(content)
53
+ expect(toc).toEqual([{ url: "#overview", title: "Overview", depth: 2 }])
54
+ })
55
+
56
+ it("should skip headings with [!toc] annotation", () => {
57
+ const content = "## Visible\n\n## Hidden From TOC [!toc]\n\n## Also Visible"
58
+ const toc = extractToc(content)
59
+ expect(toc).toEqual([
60
+ { url: "#visible", title: "Visible", depth: 2 },
61
+ { url: "#also-visible", title: "Also Visible", depth: 2 },
62
+ ])
63
+ })
64
+
65
+ it("should include [toc] headings with annotation stripped from title", () => {
66
+ const content = "## Regular\n\n## TOC Only [toc]\n\n## Another"
67
+ const toc = extractToc(content)
68
+ expect(toc).toEqual([
69
+ { url: "#regular", title: "Regular", depth: 2 },
70
+ { url: "#toc-only", title: "TOC Only", depth: 2 },
71
+ { url: "#another", title: "Another", depth: 2 },
72
+ ])
73
+ })
74
+ })
@@ -0,0 +1,30 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react"
2
+
3
+ export function useInView() {
4
+ const [isVisible, setIsVisible] = useState(false)
5
+ const elementRef = useRef<Element | null>(null)
6
+
7
+ const ref = useCallback((node: Element | null) => {
8
+ elementRef.current = node
9
+ }, [])
10
+
11
+ useEffect(() => {
12
+ const node = elementRef.current
13
+ if (!node) return
14
+
15
+ const observer = new IntersectionObserver(
16
+ entries => {
17
+ if (entries[0]?.isIntersecting) {
18
+ setIsVisible(true)
19
+ observer.disconnect()
20
+ }
21
+ },
22
+ { threshold: 0.1 },
23
+ )
24
+
25
+ observer.observe(node)
26
+ return () => observer.disconnect()
27
+ }, [])
28
+
29
+ return { ref, isVisible }
30
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from "zod"
2
+ import { nonEmptyString } from "../helpers/model.ts"
3
+ import type { TocItem } from "./toc.ts"
4
+
5
+ /** Frontmatter schema for a markdown article. */
6
+ export type Article = z.infer<typeof Article>
7
+ export const Article = z.object({
8
+ content: z.string(),
9
+ title: nonEmptyString.optional(),
10
+ label: nonEmptyString.optional(),
11
+ description: nonEmptyString.optional(),
12
+ icon: nonEmptyString.optional(),
13
+ order: z.number().optional(),
14
+ path: nonEmptyString.optional(),
15
+ group: nonEmptyString.optional(),
16
+ sidebar: z.boolean().default(true),
17
+ toc: z.boolean().default(true),
18
+ image: nonEmptyString.optional(),
19
+ author: z.union([nonEmptyString, z.array(nonEmptyString)]).optional(),
20
+ date: nonEmptyString.optional(),
21
+ tags: z.array(nonEmptyString).optional(),
22
+ })
23
+
24
+ /** Processed article ready for page rendering (result of content-collections transform). */
25
+ export interface ArticleView {
26
+ path: string
27
+ content: string
28
+ file: string
29
+ mdx: string
30
+ tocItems: TocItem[]
31
+ toc?: boolean
32
+ lastUpdated?: string
33
+ sidebar?: boolean
34
+ image?: string
35
+ author?: string | string[]
36
+ date?: string
37
+ tags?: string[]
38
+ }
39
+
40
+ export interface ArticleNode {
41
+ path: string
42
+ title: string
43
+ label?: string
44
+ icon: string
45
+ children: ArticleNode[]
46
+ }
47
+
48
+ /**
49
+ * A contiguous section of root articles sharing the same `group` label.
50
+ * `name: undefined` represents the leading ungrouped section (at most one).
51
+ */
52
+ export interface ArticleGroup {
53
+ name?: string
54
+ nodes: ArticleNode[]
55
+ }
@@ -0,0 +1,5 @@
1
+ export interface TocItem {
2
+ url: string
3
+ title: string
4
+ depth: number
5
+ }
@@ -0,0 +1,107 @@
1
+ import type { Plugin } from "unified"
2
+ import { visit } from "unist-util-visit"
3
+ import {
4
+ type BundledLanguage,
5
+ bundledLanguages,
6
+ getSingletonHighlighter,
7
+ } from "shiki"
8
+
9
+ interface HastText {
10
+ type: "text"
11
+ value: string
12
+ }
13
+
14
+ type PropertyValue =
15
+ | boolean
16
+ | number
17
+ | string
18
+ | null
19
+ | undefined
20
+ | Array<string | number>
21
+
22
+ interface HastElement {
23
+ type: "element"
24
+ tagName: string
25
+ properties: Record<string, PropertyValue>
26
+ children: (HastElement | HastText)[]
27
+ }
28
+
29
+ interface HastRoot {
30
+ type: "root"
31
+ children: (HastElement | HastText)[]
32
+ }
33
+
34
+ type HastNode = HastRoot | HastElement | HastText
35
+
36
+ const LANG_PREFIX = /^\{:(\w+)\}/
37
+
38
+ function isBundledLanguage(lang: string): lang is BundledLanguage {
39
+ return lang in bundledLanguages
40
+ }
41
+
42
+ /** Rehype plugin that applies Shiki syntax highlighting to inline code spans with a `{:lang}` prefix. */
43
+ export const rehypeInlineCode: Plugin<[], HastRoot> = () => {
44
+ return async (tree: HastRoot) => {
45
+ const highlighter = await getSingletonHighlighter({
46
+ themes: ["catppuccin-latte", "catppuccin-mocha"],
47
+ langs: [],
48
+ })
49
+
50
+ const nodes: {
51
+ node: HastElement
52
+ lang: BundledLanguage
53
+ code: string
54
+ }[] = []
55
+
56
+ visit(
57
+ tree,
58
+ "element",
59
+ (
60
+ node: HastNode,
61
+ _index: number | undefined,
62
+ parent: HastNode | undefined,
63
+ ) => {
64
+ if (node.type !== "element") return
65
+ if (node.tagName !== "code") return
66
+ if (parent && parent.type === "element" && parent.tagName === "pre")
67
+ return
68
+
69
+ const textChild = node.children[0]
70
+ if (!textChild || textChild.type !== "text") return
71
+
72
+ const match = LANG_PREFIX.exec(textChild.value)
73
+ if (!match) return
74
+
75
+ const lang = match[1]!
76
+ if (!isBundledLanguage(lang)) return
77
+
78
+ const code = textChild.value.slice(match[0].length)
79
+ nodes.push({ node, lang, code })
80
+ },
81
+ )
82
+
83
+ for (const { node, lang, code } of nodes) {
84
+ await highlighter.loadLanguage(lang)
85
+ const result = highlighter.codeToHast(code, {
86
+ themes: { light: "catppuccin-latte", dark: "catppuccin-mocha" },
87
+ lang,
88
+ })
89
+
90
+ const pre = result.children[0]
91
+ if (pre && pre.type === "element" && "children" in pre) {
92
+ const codeEl = pre.children[0]
93
+ if (codeEl && codeEl.type === "element" && "children" in codeEl) {
94
+ node.children = codeEl.children.filter(
95
+ (c): c is HastElement | HastText =>
96
+ c.type === "element" || c.type === "text",
97
+ )
98
+ node.properties = {
99
+ ...node.properties,
100
+ className: "shiki-inline",
101
+ style: codeEl.properties?.style,
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,33 @@
1
+ import { visit } from "unist-util-visit"
2
+
3
+ interface HastNode {
4
+ type: string
5
+ tagName?: string
6
+ properties?: Record<string, unknown>
7
+ children?: HastNode[]
8
+ }
9
+
10
+ /** Give each GFM task-list checkbox an accessible name so screen readers and
11
+ * axe-based audits don't flag them as form controls without labels. */
12
+ export function rehypeTaskListA11y() {
13
+ return (tree: HastNode) => {
14
+ visit(tree, "element", (node: HastNode) => {
15
+ if (node.tagName !== "li") return
16
+ const classes = node.properties?.className
17
+ const hasTaskClass =
18
+ Array.isArray(classes) && classes.includes("task-list-item")
19
+ if (!hasTaskClass) return
20
+
21
+ for (const child of node.children ?? []) {
22
+ if (child.type !== "element") continue
23
+ if (child.tagName !== "input") continue
24
+ if (child.properties?.type !== "checkbox") continue
25
+ child.properties = {
26
+ ...child.properties,
27
+ ariaLabel: child.properties?.checked ? "Completed task" : "Task",
28
+ }
29
+ break
30
+ }
31
+ })
32
+ }
33
+ }
@@ -0,0 +1,46 @@
1
+ import type { Root } from "mdast"
2
+ import type { TextDirective } from "mdast-util-directive"
3
+ import type { Plugin } from "unified"
4
+ import { visit } from "unist-util-visit"
5
+
6
+ /** Walk a mdast tree and convert :abbr text directives into Abbr JSX elements */
7
+ export function transformAbbrDirectives(tree: Root) {
8
+ visit(tree, "textDirective", (node: TextDirective, index, parent) => {
9
+ if (node.name !== "abbr" || index === undefined || !parent) return
10
+
11
+ const text = node.children
12
+ .map(c => ("value" in c ? String(c.value) : ""))
13
+ .join("")
14
+
15
+ const title = node.attributes?.title ?? ""
16
+
17
+ const jsxNode = {
18
+ type: "mdxJsxTextElement" as const,
19
+ name: "Abbr",
20
+ attributes: [
21
+ {
22
+ type: "mdxJsxAttribute" as const,
23
+ name: "text",
24
+ value: text,
25
+ },
26
+ {
27
+ type: "mdxJsxAttribute" as const,
28
+ name: "title",
29
+ value: title,
30
+ },
31
+ ],
32
+ children: [],
33
+ data: { _mdxExplicitJsx: true },
34
+ }
35
+
36
+ // @ts-expect-error mdxJsxTextElement is not in mdast types
37
+ parent.children.splice(index, 1, jsxNode)
38
+ })
39
+ }
40
+
41
+ /** Remark plugin that converts :abbr text directives into Abbr JSX elements */
42
+ export const remarkAbbr: Plugin<[], Root> = () => {
43
+ return tree => {
44
+ transformAbbrDirectives(tree)
45
+ }
46
+ }
@@ -0,0 +1,99 @@
1
+ import type { Root } from "mdast"
2
+ import type { TextDirective } from "mdast-util-directive"
3
+ import { describe, expect, it } from "vite-plus/test"
4
+ import { transformAbbrDirectives } from "./remark-abbr.ts"
5
+
6
+ function makeTextDirective(
7
+ name: string,
8
+ text: string,
9
+ attributes?: Record<string, string>,
10
+ ): TextDirective {
11
+ return {
12
+ type: "textDirective",
13
+ name,
14
+ attributes: attributes ?? {},
15
+ children: [{ type: "text", value: text }],
16
+ }
17
+ }
18
+
19
+ function makeTree(...children: Root["children"]): Root {
20
+ return { type: "root", children }
21
+ }
22
+
23
+ describe("transformAbbrDirectives", () => {
24
+ function process(tree: Root) {
25
+ transformAbbrDirectives(tree)
26
+ return tree
27
+ }
28
+
29
+ it("should convert :abbr directive to Abbr JSX element", () => {
30
+ const directive = makeTextDirective("abbr", "HTML", {
31
+ title: "HyperText Markup Language",
32
+ })
33
+ const tree = process(
34
+ makeTree({
35
+ type: "paragraph",
36
+ children: [directive],
37
+ }),
38
+ )
39
+
40
+ const paragraph = tree.children[0]
41
+ if (!paragraph || paragraph.type !== "paragraph")
42
+ throw new Error("Expected paragraph")
43
+ const node = paragraph.children[0]
44
+ expect(node).toMatchObject({
45
+ type: "mdxJsxTextElement",
46
+ name: "Abbr",
47
+ attributes: [
48
+ { type: "mdxJsxAttribute", name: "text", value: "HTML" },
49
+ {
50
+ type: "mdxJsxAttribute",
51
+ name: "title",
52
+ value: "HyperText Markup Language",
53
+ },
54
+ ],
55
+ children: [],
56
+ })
57
+ })
58
+
59
+ it("should use empty string when title is not provided", () => {
60
+ const directive = makeTextDirective("abbr", "CSS")
61
+ const tree = process(
62
+ makeTree({
63
+ type: "paragraph",
64
+ children: [directive],
65
+ }),
66
+ )
67
+
68
+ const paragraph = tree.children[0]
69
+ if (!paragraph || paragraph.type !== "paragraph")
70
+ throw new Error("Expected paragraph")
71
+ const node = paragraph.children[0]
72
+ expect(node).toMatchObject({
73
+ type: "mdxJsxTextElement",
74
+ name: "Abbr",
75
+ attributes: [
76
+ { type: "mdxJsxAttribute", name: "text", value: "CSS" },
77
+ { type: "mdxJsxAttribute", name: "title", value: "" },
78
+ ],
79
+ })
80
+ })
81
+
82
+ it("should ignore non-abbr text directives", () => {
83
+ const directive = makeTextDirective("other", "Text")
84
+ const tree = process(
85
+ makeTree({
86
+ type: "paragraph",
87
+ children: [directive],
88
+ }),
89
+ )
90
+
91
+ const paragraph = tree.children[0]
92
+ if (!paragraph || paragraph.type !== "paragraph")
93
+ throw new Error("Expected paragraph")
94
+ expect(paragraph.children[0]).toMatchObject({
95
+ type: "textDirective",
96
+ name: "other",
97
+ })
98
+ })
99
+ })