livemark 0.2.0 → 0.7.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.
- package/LICENSE.md +9 -0
- package/README.md +5 -1
- package/build/actions/changelog/build.d.ts +14 -0
- package/build/actions/changelog/build.js +158 -0
- package/build/actions/changelog/build.unit.js +66 -0
- package/build/actions/config/define.d.ts +6 -0
- package/build/actions/config/define.js +8 -0
- package/build/actions/config/define.unit.js +29 -0
- package/build/actions/config/load.d.ts +5 -0
- package/build/actions/config/load.js +25 -0
- package/build/actions/config/load.unit.js +23 -0
- package/build/commands/build.d.ts +5 -0
- package/build/commands/build.js +14 -0
- package/build/commands/escape.d.ts +9 -0
- package/build/commands/escape.js +56 -0
- package/build/commands/preview.d.ts +5 -0
- package/build/commands/preview.js +18 -0
- package/build/commands/start.d.ts +5 -0
- package/build/commands/start.js +19 -0
- package/build/docs/markdown/includes/example.d.ts +58 -0
- package/build/docs/markdown/includes/example.js +5 -0
- package/build/entrypoints/main.d.ts +2 -0
- package/build/entrypoints/main.js +12 -0
- package/build/helpers/program.d.ts +2 -0
- package/build/helpers/program.js +5 -0
- package/build/index.d.ts +1 -4
- package/build/index.js +2 -5
- package/build/livemark.config.d.ts +58 -0
- package/build/livemark.config.js +25 -0
- package/build/models/config.d.ts +340 -0
- package/build/models/config.js +34 -0
- package/build/models/link.d.ts +13 -0
- package/build/models/link.js +9 -0
- package/build/models/patch.d.ts +21 -0
- package/build/models/patch.js +7 -0
- package/build/models/section.d.ts +75 -0
- package/build/models/section.js +26 -0
- package/build/models/theme.d.ts +72 -0
- package/build/models/theme.js +73 -0
- package/build/program.d.ts +2 -0
- package/build/program.js +16 -0
- package/build/vite.config.d.ts +2 -0
- package/build/vite.config.js +48 -0
- package/build/website/assets/logo.svg +117 -0
- package/build/website/client.tsx +10 -0
- package/build/website/components/Abbr.tsx +17 -0
- package/build/website/components/AnsiCode.tsx +15 -0
- package/build/website/components/Article.tsx +172 -0
- package/build/website/components/BackToTop.tsx +32 -0
- package/build/website/components/Banner.tsx +32 -0
- package/build/website/components/BlogIndex.tsx +121 -0
- package/build/website/components/BlogSidebar.tsx +162 -0
- package/build/website/components/Breadcrumbs.tsx +22 -0
- package/build/website/components/Callout.tsx +72 -0
- package/build/website/components/Card.tsx +48 -0
- package/build/website/components/Cards.tsx +8 -0
- package/build/website/components/ChangelogIndex.tsx +111 -0
- package/build/website/components/ChangelogSidebar.tsx +120 -0
- package/build/website/components/CodeBlock.tsx +135 -0
- package/build/website/components/CodeTabs.tsx +83 -0
- package/build/website/components/Column.tsx +4 -0
- package/build/website/components/Columns.tsx +12 -0
- package/build/website/components/Content.tsx +7 -0
- package/build/website/components/ContentTab.tsx +7 -0
- package/build/website/components/ContentTabs.tsx +82 -0
- package/build/website/components/DefaultCatchBoundary.tsx +54 -0
- package/build/website/components/DefinitionList.tsx +21 -0
- package/build/website/components/Details.tsx +30 -0
- package/build/website/components/FileTree.tsx +52 -0
- package/build/website/components/Footer.tsx +16 -0
- package/build/website/components/FootnoteRef.tsx +52 -0
- package/build/website/components/Heading.tsx +20 -0
- package/build/website/components/InlineBadge.tsx +9 -0
- package/build/website/components/InlineIcon.tsx +10 -0
- package/build/website/components/InlineToc.tsx +31 -0
- package/build/website/components/Layout.tsx +190 -0
- package/build/website/components/LinkButton.tsx +31 -0
- package/build/website/components/Mermaid.tsx +18 -0
- package/build/website/components/MermaidRenderer.tsx +37 -0
- package/build/website/components/MiniSidebar.tsx +49 -0
- package/build/website/components/NotFound.tsx +27 -0
- package/build/website/components/PackageTabs.tsx +74 -0
- package/build/website/components/PageToolbar.tsx +75 -0
- package/build/website/components/PrevNext.tsx +60 -0
- package/build/website/components/Search.tsx +28 -0
- package/build/website/components/SearchDialog.tsx +245 -0
- package/build/website/components/Section.tsx +24 -0
- package/build/website/components/SectionHeader.tsx +35 -0
- package/build/website/components/Sidebar.tsx +160 -0
- package/build/website/components/SidebarLinks.tsx +70 -0
- package/build/website/components/SidebarSections.tsx +71 -0
- package/build/website/components/SiteTitle.tsx +21 -0
- package/build/website/components/SortableTable.tsx +81 -0
- package/build/website/components/SoundCloud.tsx +16 -0
- package/build/website/components/TagIndex.tsx +120 -0
- package/build/website/components/Theme.tsx +39 -0
- package/build/website/components/Toc.tsx +226 -0
- package/build/website/components/YouTube.tsx +16 -0
- package/build/website/components/ZoomImage.tsx +25 -0
- package/build/website/content/article.ts +78 -0
- package/build/website/content/tag.ts +29 -0
- package/build/website/content-collections.ts +208 -0
- package/build/website/elements/accordion.tsx +78 -0
- package/build/website/elements/alert-dialog.tsx +185 -0
- package/build/website/elements/alert.tsx +74 -0
- package/build/website/elements/aspect-ratio.tsx +22 -0
- package/build/website/elements/avatar.tsx +107 -0
- package/build/website/elements/badge.tsx +51 -0
- package/build/website/elements/breadcrumb.tsx +120 -0
- package/build/website/elements/button-group.tsx +87 -0
- package/build/website/elements/button.tsx +57 -0
- package/build/website/elements/calendar.tsx +229 -0
- package/build/website/elements/card.tsx +102 -0
- package/build/website/elements/carousel.tsx +240 -0
- package/build/website/elements/chart.tsx +372 -0
- package/build/website/elements/checkbox.tsx +26 -0
- package/build/website/elements/collapsible.tsx +19 -0
- package/build/website/elements/command.tsx +210 -0
- package/build/website/elements/context-menu.tsx +270 -0
- package/build/website/elements/dialog.tsx +152 -0
- package/build/website/elements/direction.tsx +4 -0
- package/build/website/elements/drawer.tsx +127 -0
- package/build/website/elements/dropdown-menu.tsx +260 -0
- package/build/website/elements/empty.tsx +104 -0
- package/build/website/elements/field.tsx +235 -0
- package/build/website/elements/hover-card.tsx +49 -0
- package/build/website/elements/input-group.tsx +171 -0
- package/build/website/elements/input-otp.tsx +84 -0
- package/build/website/elements/input.tsx +18 -0
- package/build/website/elements/item.tsx +199 -0
- package/build/website/elements/kbd.tsx +26 -0
- package/build/website/elements/label.tsx +17 -0
- package/build/website/elements/menubar.tsx +282 -0
- package/build/website/elements/native-select.tsx +56 -0
- package/build/website/elements/navigation-menu.tsx +171 -0
- package/build/website/elements/pagination.tsx +133 -0
- package/build/website/elements/popover.tsx +88 -0
- package/build/website/elements/progress.tsx +81 -0
- package/build/website/elements/radio-group.tsx +36 -0
- package/build/website/elements/resizable.tsx +48 -0
- package/build/website/elements/scroll-area.tsx +51 -0
- package/build/website/elements/select.tsx +200 -0
- package/build/website/elements/separator.tsx +22 -0
- package/build/website/elements/sheet.tsx +130 -0
- package/build/website/elements/sidebar.tsx +717 -0
- package/build/website/elements/skeleton.tsx +13 -0
- package/build/website/elements/slider.tsx +57 -0
- package/build/website/elements/sonner.tsx +40 -0
- package/build/website/elements/spinner.tsx +16 -0
- package/build/website/elements/switch.tsx +30 -0
- package/build/website/elements/table.tsx +112 -0
- package/build/website/elements/tabs.tsx +79 -0
- package/build/website/elements/textarea.tsx +16 -0
- package/build/website/elements/toggle-group.tsx +87 -0
- package/build/website/elements/toggle.tsx +43 -0
- package/build/website/elements/tooltip.tsx +67 -0
- package/build/website/env.d.ts +14 -0
- package/build/website/helpers/article-icon.ts +61 -0
- package/build/website/helpers/article.ts +90 -0
- package/build/website/helpers/changelog-source.ts +16 -0
- package/build/website/helpers/changelog-source.unit.ts +38 -0
- package/build/website/helpers/dynamic-icon-resolver.tsx +19 -0
- package/build/website/helpers/dynamic-icon.tsx +12 -0
- package/build/website/helpers/favicon.ts +7 -0
- package/build/website/helpers/model.ts +4 -0
- package/build/website/helpers/resolve-asset-path.ts +9 -0
- package/build/website/helpers/resolve-includes.ts +131 -0
- package/build/website/helpers/resolve-includes.unit.ts +126 -0
- package/build/website/helpers/search-text.ts +15 -0
- package/build/website/helpers/section.ts +50 -0
- package/build/website/helpers/section.unit.ts +71 -0
- package/build/website/helpers/style.ts +6 -0
- package/build/website/helpers/theme.ts +27 -0
- package/build/website/helpers/toc-context.ts +10 -0
- package/build/website/helpers/toc.ts +46 -0
- package/build/website/helpers/toc.unit.ts +74 -0
- package/build/website/hooks/in-view.ts +30 -0
- package/build/website/hooks/media.ts +19 -0
- package/build/website/models/article.ts +55 -0
- package/build/website/models/toc.ts +5 -0
- package/build/website/plugins/rehype-inline-code.ts +107 -0
- package/build/website/plugins/rehype-task-list-a11y.ts +33 -0
- package/build/website/plugins/remark-abbr.ts +46 -0
- package/build/website/plugins/remark-abbr.unit.ts +99 -0
- package/build/website/plugins/remark-ansi.ts +248 -0
- package/build/website/plugins/remark-ansi.unit.ts +197 -0
- package/build/website/plugins/remark-badge.ts +50 -0
- package/build/website/plugins/remark-badge.unit.ts +96 -0
- package/build/website/plugins/remark-button.ts +60 -0
- package/build/website/plugins/remark-button.unit.ts +147 -0
- package/build/website/plugins/remark-callout.ts +57 -0
- package/build/website/plugins/remark-card.ts +80 -0
- package/build/website/plugins/remark-code-tabs.ts +87 -0
- package/build/website/plugins/remark-columns.ts +66 -0
- package/build/website/plugins/remark-custom-heading-id.ts +70 -0
- package/build/website/plugins/remark-custom-heading-id.unit.ts +131 -0
- package/build/website/plugins/remark-deflist.ts +36 -0
- package/build/website/plugins/remark-details.ts +49 -0
- package/build/website/plugins/remark-filetree.ts +89 -0
- package/build/website/plugins/remark-filetree.unit.ts +137 -0
- package/build/website/plugins/remark-github-callout.ts +71 -0
- package/build/website/plugins/remark-icon.ts +42 -0
- package/build/website/plugins/remark-image.ts +34 -0
- package/build/website/plugins/remark-media.ts +62 -0
- package/build/website/plugins/remark-media.unit.ts +74 -0
- package/build/website/plugins/remark-mermaid.ts +23 -0
- package/build/website/plugins/remark-npm.ts +52 -0
- package/build/website/plugins/remark-steps.ts +36 -0
- package/build/website/plugins/remark-steps.unit.ts +97 -0
- package/build/website/plugins/remark-tab.ts +100 -0
- package/build/website/plugins/remark-tab.unit.ts +181 -0
- package/build/website/plugins/remark-toc.ts +39 -0
- package/build/website/plugins/remark-toc.unit.ts +82 -0
- package/build/website/plugins/shiki-icon.ts +82 -0
- package/build/website/plugins/shiki-line-highlight.ts +174 -0
- package/build/website/plugins/vite-livemark.ts +91 -0
- package/build/website/routeTree.gen.ts +86 -0
- package/build/website/router.tsx +34 -0
- package/build/website/routes/$.tsx +92 -0
- package/build/website/routes/__root.tsx +97 -0
- package/build/website/server.ts +7 -0
- package/build/website/styles/general.css +163 -0
- package/build/website/styles/markdown.css +214 -0
- package/build/website/vite.config.ts +71 -0
- package/package.json +120 -12
- package/skills/authoring/SKILL.md +97 -0
- package/skills/configuration/SKILL.md +120 -0
- package/skills/customization/SKILL.md +118 -0
- package/build/descriptor/index.d.ts +0 -3
- package/build/descriptor/index.js +0 -4
- package/build/descriptor/load.d.ts +0 -8
- package/build/descriptor/load.js +0 -40
- package/build/descriptor/node.d.ts +0 -125
- package/build/descriptor/node.js +0 -9
- package/build/descriptor/save.d.ts +0 -8
- package/build/descriptor/save.js +0 -17
- package/build/descriptor/save.spec.js +0 -80
- package/build/descriptor/validate.d.ts +0 -9
- package/build/descriptor/validate.js +0 -14
- package/build/descriptor/validate.spec.js +0 -124
- package/build/dialect/Dialect.d.ts +0 -88
- package/build/dialect/Dialect.js +0 -2
- package/build/dialect/index.d.ts +0 -3
- package/build/dialect/index.js +0 -3
- package/build/dialect/load.d.ts +0 -4
- package/build/dialect/load.js +0 -7
- package/build/dialect/save.d.ts +0 -9
- package/build/dialect/save.js +0 -13
- package/build/dialect/validate.d.ts +0 -1
- package/build/dialect/validate.js +0 -4
- package/build/metadata/index.d.ts +0 -3
- package/build/metadata/index.js +0 -4
- package/build/metadata/load.d.ts +0 -10
- package/build/metadata/load.js +0 -55
- package/build/metadata/node.d.ts +0 -125
- package/build/metadata/node.js +0 -9
- package/build/metadata/save.d.ts +0 -8
- package/build/metadata/save.js +0 -17
- package/build/metadata/save.spec.js +0 -80
- package/build/metadata/validate.d.ts +0 -9
- package/build/metadata/validate.js +0 -14
- package/build/metadata/validate.spec.d.ts +0 -1
- package/build/metadata/validate.spec.js +0 -124
- package/build/package/Contributor.d.ts +0 -21
- package/build/package/Contributor.js +0 -2
- package/build/package/Package.d.ts +0 -65
- package/build/package/Package.js +0 -2
- package/build/package/index.d.ts +0 -4
- package/build/package/index.js +0 -3
- package/build/package/load.d.ts +0 -4
- package/build/package/load.js +0 -7
- package/build/package/save.d.ts +0 -9
- package/build/package/save.js +0 -13
- package/build/package/validate.d.ts +0 -1
- package/build/package/validate.js +0 -4
- package/build/resource/License.d.ts +0 -18
- package/build/resource/License.js +0 -2
- package/build/resource/Resource.d.ts +0 -67
- package/build/resource/Resource.js +0 -2
- package/build/resource/Source.d.ts +0 -17
- package/build/resource/Source.js +0 -2
- package/build/resource/index.d.ts +0 -5
- package/build/resource/index.js +0 -2
- package/build/resource/load.d.ts +0 -1
- package/build/resource/load.js +0 -2
- package/build/resource/save.d.ts +0 -9
- package/build/resource/save.js +0 -13
- package/build/resource/types/index.d.ts +0 -1
- package/build/resource/types/index.js +0 -2
- package/build/resource/types/table.d.ts +0 -29
- package/build/resource/types/table.js +0 -2
- package/build/resource/validate.d.ts +0 -1
- package/build/resource/validate.js +0 -4
- package/build/schema/Field.d.ts +0 -5
- package/build/schema/Field.js +0 -2
- package/build/schema/Schema.d.ts +0 -55
- package/build/schema/Schema.js +0 -2
- package/build/schema/fields/any.d.ts +0 -20
- package/build/schema/fields/any.js +0 -2
- package/build/schema/fields/array.d.ts +0 -32
- package/build/schema/fields/array.js +0 -2
- package/build/schema/fields/base.d.ts +0 -42
- package/build/schema/fields/base.js +0 -2
- package/build/schema/fields/boolean.d.ts +0 -28
- package/build/schema/fields/boolean.js +0 -2
- package/build/schema/fields/date.d.ts +0 -35
- package/build/schema/fields/date.js +0 -2
- package/build/schema/fields/datetime.d.ts +0 -35
- package/build/schema/fields/datetime.js +0 -2
- package/build/schema/fields/duration.d.ts +0 -28
- package/build/schema/fields/duration.js +0 -2
- package/build/schema/fields/geojson.d.ts +0 -26
- package/build/schema/fields/geojson.js +0 -2
- package/build/schema/fields/geopoint.d.ts +0 -27
- package/build/schema/fields/geopoint.js +0 -2
- package/build/schema/fields/index.d.ts +0 -16
- package/build/schema/fields/index.js +0 -17
- package/build/schema/fields/integer.d.ts +0 -37
- package/build/schema/fields/integer.js +0 -2
- package/build/schema/fields/list.d.ts +0 -36
- package/build/schema/fields/list.js +0 -2
- package/build/schema/fields/number.d.ts +0 -40
- package/build/schema/fields/number.js +0 -2
- package/build/schema/fields/object.d.ts +0 -32
- package/build/schema/fields/object.js +0 -2
- package/build/schema/fields/string.d.ts +0 -40
- package/build/schema/fields/string.js +0 -2
- package/build/schema/fields/time.d.ts +0 -35
- package/build/schema/fields/time.js +0 -2
- package/build/schema/fields/year.d.ts +0 -28
- package/build/schema/fields/year.js +0 -2
- package/build/schema/fields/yearmonth.d.ts +0 -28
- package/build/schema/fields/yearmonth.js +0 -2
- package/build/schema/index.d.ts +0 -4
- package/build/schema/index.js +0 -2
- package/build/schema/load.d.ts +0 -1
- package/build/schema/load.js +0 -2
- package/build/schema/save.d.ts +0 -9
- package/build/schema/save.js +0 -13
- package/build/schema/validate.d.ts +0 -1
- package/build/schema/validate.js +0 -4
- package/build/tsconfig.tsbuildinfo +0 -1
- package/index.ts +0 -0
- package/tsconfig.json +0 -10
- package/typedoc.json +0 -4
- /package/build/{descriptor/save.spec.d.ts → actions/changelog/build.unit.d.ts} +0 -0
- /package/build/{descriptor/validate.spec.d.ts → actions/config/define.unit.d.ts} +0 -0
- /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,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
|
+
})
|