livemark 0.0.0-dev
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 +3 -0
- package/build/actions/config/define.d.ts +6 -0
- package/build/actions/config/define.js +8 -0
- package/build/actions/config/define.unit.d.ts +1 -0
- package/build/actions/config/define.unit.js +34 -0
- package/build/actions/config/load.d.ts +5 -0
- package/build/actions/config/load.js +22 -0
- package/build/actions/config/load.unit.d.ts +1 -0
- package/build/actions/config/load.unit.js +23 -0
- package/build/commands/build.d.ts +5 -0
- package/build/commands/build.js +13 -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 +9 -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 -0
- package/build/index.js +2 -0
- package/build/livemark.config.d.ts +9 -0
- package/build/livemark.config.js +5 -0
- package/build/models/config.d.ts +33 -0
- package/build/models/config.js +14 -0
- package/build/program.d.ts +2 -0
- package/build/program.js +14 -0
- package/build/vite.config.d.ts +2 -0
- package/build/vite.config.js +48 -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/Banner.tsx +32 -0
- package/build/website/components/Breadcrumbs.tsx +19 -0
- package/build/website/components/Callout.tsx +72 -0
- package/build/website/components/Cards.tsx +66 -0
- package/build/website/components/CodeBlock.tsx +118 -0
- package/build/website/components/CodeTabs.tsx +83 -0
- package/build/website/components/Columns.tsx +17 -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 +9 -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 +21 -0
- package/build/website/components/InlineToc.tsx +31 -0
- package/build/website/components/Layout.tsx +220 -0
- package/build/website/components/LinkButton.tsx +31 -0
- package/build/website/components/Mermaid.tsx +37 -0
- package/build/website/components/NotFound.tsx +27 -0
- package/build/website/components/PackageTabs.tsx +74 -0
- package/build/website/components/PageToolbar.tsx +65 -0
- package/build/website/components/PrevNext.tsx +52 -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/SiteTitle.tsx +16 -0
- package/build/website/components/SortableTable.tsx +81 -0
- package/build/website/components/SoundCloud.tsx +16 -0
- package/build/website/components/Theme.tsx +40 -0
- package/build/website/components/Toc.tsx +99 -0
- package/build/website/components/YouTube.tsx +16 -0
- package/build/website/components/ZoomImage.tsx +30 -0
- package/build/website/content-collections.ts +181 -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 +10 -0
- package/build/website/globals.d.ts +8 -0
- package/build/website/helpers/article-icon.ts +59 -0
- package/build/website/helpers/articles.ts +24 -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/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 +51 -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 +40 -0
- package/build/website/plugins/rehype-inline-code.ts +107 -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 +247 -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 +25 -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/public/logo.svg +72 -0
- package/build/website/routeTree.gen.ts +86 -0
- package/build/website/router.tsx +34 -0
- package/build/website/routes/$.tsx +149 -0
- package/build/website/routes/__root.tsx +84 -0
- package/build/website/routes/index.tsx +13 -0
- package/build/website/server.ts +7 -0
- package/build/website/styles/general.css +347 -0
- package/build/website/vite.config.ts +36 -0
- package/entrypoints/main.ts +12 -0
- package/package.json +120 -0
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { Root } from "mdast"
|
|
2
|
+
import type { Plugin } from "unified"
|
|
3
|
+
|
|
4
|
+
interface AnsiState {
|
|
5
|
+
bold: boolean
|
|
6
|
+
dim: boolean
|
|
7
|
+
italic: boolean
|
|
8
|
+
underline: boolean
|
|
9
|
+
strikethrough: boolean
|
|
10
|
+
fg: string | undefined
|
|
11
|
+
bg: string | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const STANDARD_COLORS: ReadonlyArray<string> = [
|
|
15
|
+
"#4f5b66", // 0 black
|
|
16
|
+
"#bf616a", // 1 red
|
|
17
|
+
"#a3be8c", // 2 green
|
|
18
|
+
"#ebcb8b", // 3 yellow
|
|
19
|
+
"#8fa1b3", // 4 blue
|
|
20
|
+
"#b48ead", // 5 magenta
|
|
21
|
+
"#96b5b4", // 6 cyan
|
|
22
|
+
"#c0c5ce", // 7 white
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const BRIGHT_COLORS: ReadonlyArray<string> = [
|
|
26
|
+
"#65737e", // 8 bright black
|
|
27
|
+
"#bf616a", // 9 bright red
|
|
28
|
+
"#a3be8c", // 10 bright green
|
|
29
|
+
"#ebcb8b", // 11 bright yellow
|
|
30
|
+
"#8fa1b3", // 12 bright blue
|
|
31
|
+
"#b48ead", // 13 bright magenta
|
|
32
|
+
"#96b5b4", // 14 bright cyan
|
|
33
|
+
"#eff1f5", // 15 bright white
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
function color256(n: number): string | undefined {
|
|
37
|
+
if (n < 8) return STANDARD_COLORS[n]
|
|
38
|
+
if (n < 16) return BRIGHT_COLORS[n - 8]
|
|
39
|
+
if (n < 232) {
|
|
40
|
+
const idx = n - 16
|
|
41
|
+
const r = Math.floor(idx / 36)
|
|
42
|
+
const g = Math.floor((idx % 36) / 6)
|
|
43
|
+
const b = idx % 6
|
|
44
|
+
const toHex = (v: number) =>
|
|
45
|
+
(v === 0 ? 0 : 55 + v * 40).toString(16).padStart(2, "0")
|
|
46
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
47
|
+
}
|
|
48
|
+
if (n < 256) {
|
|
49
|
+
const v = (8 + (n - 232) * 10).toString(16).padStart(2, "0")
|
|
50
|
+
return `#${v}${v}${v}`
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function escapeHtml(text: string): string {
|
|
56
|
+
return text
|
|
57
|
+
.replace(/&/g, "&")
|
|
58
|
+
.replace(/</g, "<")
|
|
59
|
+
.replace(/>/g, ">")
|
|
60
|
+
.replace(/"/g, """)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildStyle(state: AnsiState): string {
|
|
64
|
+
const parts: string[] = []
|
|
65
|
+
if (state.fg) parts.push(`color:${state.fg}`)
|
|
66
|
+
if (state.bg) parts.push(`background-color:${state.bg}`)
|
|
67
|
+
if (state.bold) parts.push("font-weight:bold")
|
|
68
|
+
if (state.dim) parts.push("opacity:0.7")
|
|
69
|
+
if (state.italic) parts.push("font-style:italic")
|
|
70
|
+
if (state.underline) parts.push("text-decoration:underline")
|
|
71
|
+
if (state.strikethrough) parts.push("text-decoration:line-through")
|
|
72
|
+
return parts.join(";")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeDefaultState(): AnsiState {
|
|
76
|
+
return {
|
|
77
|
+
bold: false,
|
|
78
|
+
dim: false,
|
|
79
|
+
italic: false,
|
|
80
|
+
underline: false,
|
|
81
|
+
strikethrough: false,
|
|
82
|
+
fg: undefined,
|
|
83
|
+
bg: undefined,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function applyParams(state: AnsiState, params: ReadonlyArray<number>) {
|
|
88
|
+
let i = 0
|
|
89
|
+
while (i < params.length) {
|
|
90
|
+
const code = params[i]!
|
|
91
|
+
switch (code) {
|
|
92
|
+
case 0: {
|
|
93
|
+
const reset = makeDefaultState()
|
|
94
|
+
state.bold = reset.bold
|
|
95
|
+
state.dim = reset.dim
|
|
96
|
+
state.italic = reset.italic
|
|
97
|
+
state.underline = reset.underline
|
|
98
|
+
state.strikethrough = reset.strikethrough
|
|
99
|
+
state.fg = reset.fg
|
|
100
|
+
state.bg = reset.bg
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
case 1:
|
|
104
|
+
state.bold = true
|
|
105
|
+
break
|
|
106
|
+
case 2:
|
|
107
|
+
state.dim = true
|
|
108
|
+
break
|
|
109
|
+
case 3:
|
|
110
|
+
state.italic = true
|
|
111
|
+
break
|
|
112
|
+
case 4:
|
|
113
|
+
state.underline = true
|
|
114
|
+
break
|
|
115
|
+
case 9:
|
|
116
|
+
state.strikethrough = true
|
|
117
|
+
break
|
|
118
|
+
case 22:
|
|
119
|
+
state.bold = false
|
|
120
|
+
state.dim = false
|
|
121
|
+
break
|
|
122
|
+
case 23:
|
|
123
|
+
state.italic = false
|
|
124
|
+
break
|
|
125
|
+
case 24:
|
|
126
|
+
state.underline = false
|
|
127
|
+
break
|
|
128
|
+
case 29:
|
|
129
|
+
state.strikethrough = false
|
|
130
|
+
break
|
|
131
|
+
case 39:
|
|
132
|
+
state.fg = undefined
|
|
133
|
+
break
|
|
134
|
+
case 49:
|
|
135
|
+
state.bg = undefined
|
|
136
|
+
break
|
|
137
|
+
default:
|
|
138
|
+
if (code >= 30 && code <= 37) {
|
|
139
|
+
state.fg = STANDARD_COLORS[code - 30]
|
|
140
|
+
} else if (code >= 40 && code <= 47) {
|
|
141
|
+
state.bg = STANDARD_COLORS[code - 40]
|
|
142
|
+
} else if (code >= 90 && code <= 97) {
|
|
143
|
+
state.fg = BRIGHT_COLORS[code - 90]
|
|
144
|
+
} else if (code >= 100 && code <= 107) {
|
|
145
|
+
state.bg = BRIGHT_COLORS[code - 100]
|
|
146
|
+
} else if (code === 38 || code === 48) {
|
|
147
|
+
const kind = params[i + 1]
|
|
148
|
+
if (kind === 5) {
|
|
149
|
+
const colorIdx = params[i + 2]
|
|
150
|
+
if (colorIdx !== undefined) {
|
|
151
|
+
const c = color256(colorIdx)
|
|
152
|
+
if (code === 38) state.fg = c
|
|
153
|
+
else state.bg = c
|
|
154
|
+
}
|
|
155
|
+
i += 2
|
|
156
|
+
} else if (kind === 2) {
|
|
157
|
+
const r = params[i + 2] ?? 0
|
|
158
|
+
const g = params[i + 3] ?? 0
|
|
159
|
+
const b = params[i + 4] ?? 0
|
|
160
|
+
const toHex = (v: number) => v.toString(16).padStart(2, "0")
|
|
161
|
+
const c = `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
162
|
+
if (code === 38) state.fg = c
|
|
163
|
+
else state.bg = c
|
|
164
|
+
i += 4
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
i++
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Convert a string containing ANSI escape sequences into HTML with inline styles */
|
|
174
|
+
export function ansiToHtml(input: string): string {
|
|
175
|
+
const sgr = /\u001b\[([0-9;]*)m/g
|
|
176
|
+
const state = makeDefaultState()
|
|
177
|
+
const parts: string[] = []
|
|
178
|
+
let lastIndex = 0
|
|
179
|
+
|
|
180
|
+
let match = sgr.exec(input)
|
|
181
|
+
while (match !== null) {
|
|
182
|
+
const textBefore = input.slice(lastIndex, match.index)
|
|
183
|
+
if (textBefore) {
|
|
184
|
+
const style = buildStyle(state)
|
|
185
|
+
if (style) {
|
|
186
|
+
parts.push(`<span style="${style}">${escapeHtml(textBefore)}</span>`)
|
|
187
|
+
} else {
|
|
188
|
+
parts.push(escapeHtml(textBefore))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const rawParams = match[1]!
|
|
193
|
+
const params =
|
|
194
|
+
rawParams === ""
|
|
195
|
+
? [0]
|
|
196
|
+
: rawParams.split(";").map(s => Number.parseInt(s, 10))
|
|
197
|
+
applyParams(state, params)
|
|
198
|
+
lastIndex = match.index + match[0].length
|
|
199
|
+
match = sgr.exec(input)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const trailing = input.slice(lastIndex)
|
|
203
|
+
if (trailing) {
|
|
204
|
+
const style = buildStyle(state)
|
|
205
|
+
if (style) {
|
|
206
|
+
parts.push(`<span style="${style}">${escapeHtml(trailing)}</span>`)
|
|
207
|
+
} else {
|
|
208
|
+
parts.push(escapeHtml(trailing))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return parts.join("")
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Walk a mdast tree and convert ansi code blocks into AnsiCode JSX elements */
|
|
216
|
+
export function transformAnsiCodeBlocks(tree: Root) {
|
|
217
|
+
const children = tree.children
|
|
218
|
+
for (let i = 0; i < children.length; i++) {
|
|
219
|
+
const node = children[i]
|
|
220
|
+
if (node?.type !== "code" || node.lang !== "ansi") continue
|
|
221
|
+
|
|
222
|
+
const html = ansiToHtml(node.value)
|
|
223
|
+
|
|
224
|
+
const jsxNode = {
|
|
225
|
+
type: "mdxJsxFlowElement" as const,
|
|
226
|
+
name: "AnsiCode",
|
|
227
|
+
attributes: [
|
|
228
|
+
{
|
|
229
|
+
type: "mdxJsxAttribute" as const,
|
|
230
|
+
name: "html",
|
|
231
|
+
value: html,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
children: [],
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// @ts-expect-error mdxJsxFlowElement is not in mdast types
|
|
238
|
+
children[i] = jsxNode
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Remark plugin that converts ansi code blocks into colored HTML output */
|
|
243
|
+
export const remarkAnsi: Plugin<[], Root> = () => {
|
|
244
|
+
return tree => {
|
|
245
|
+
transformAnsiCodeBlocks(tree)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { Root } from "mdast"
|
|
2
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
3
|
+
import { ansiToHtml, transformAnsiCodeBlocks } from "./remark-ansi.ts"
|
|
4
|
+
|
|
5
|
+
function makeTree(...children: Root["children"]): Root {
|
|
6
|
+
return { type: "root", children }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("ansiToHtml", () => {
|
|
10
|
+
it("should return plain text unchanged", () => {
|
|
11
|
+
expect(ansiToHtml("hello world")).toBe("hello world")
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("should escape HTML characters", () => {
|
|
15
|
+
expect(ansiToHtml("<div>&</div>")).toBe("<div>&</div>")
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("should render standard foreground colors", () => {
|
|
19
|
+
const input = "\x1b[31mred text\x1b[0m"
|
|
20
|
+
expect(ansiToHtml(input)).toBe(
|
|
21
|
+
'<span style="color:#bf616a">red text</span>',
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("should render bold text", () => {
|
|
26
|
+
const input = "\x1b[1mbold\x1b[0m"
|
|
27
|
+
expect(ansiToHtml(input)).toBe('<span style="font-weight:bold">bold</span>')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("should render italic text", () => {
|
|
31
|
+
const input = "\x1b[3mitalic\x1b[0m"
|
|
32
|
+
expect(ansiToHtml(input)).toBe(
|
|
33
|
+
'<span style="font-style:italic">italic</span>',
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should render underline text", () => {
|
|
38
|
+
const input = "\x1b[4munderline\x1b[0m"
|
|
39
|
+
expect(ansiToHtml(input)).toBe(
|
|
40
|
+
'<span style="text-decoration:underline">underline</span>',
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("should render dim text", () => {
|
|
45
|
+
const input = "\x1b[2mdim\x1b[0m"
|
|
46
|
+
expect(ansiToHtml(input)).toBe('<span style="opacity:0.7">dim</span>')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should render strikethrough text", () => {
|
|
50
|
+
const input = "\x1b[9mstruck\x1b[0m"
|
|
51
|
+
expect(ansiToHtml(input)).toBe(
|
|
52
|
+
'<span style="text-decoration:line-through">struck</span>',
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should render background colors", () => {
|
|
57
|
+
const input = "\x1b[42mgreen bg\x1b[0m"
|
|
58
|
+
expect(ansiToHtml(input)).toBe(
|
|
59
|
+
'<span style="background-color:#a3be8c">green bg</span>',
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("should combine foreground and background", () => {
|
|
64
|
+
const input = "\x1b[31;42mred on green\x1b[0m"
|
|
65
|
+
expect(ansiToHtml(input)).toBe(
|
|
66
|
+
'<span style="color:#bf616a;background-color:#a3be8c">red on green</span>',
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("should handle bright foreground colors", () => {
|
|
71
|
+
const input = "\x1b[91mbright red\x1b[0m"
|
|
72
|
+
expect(ansiToHtml(input)).toBe(
|
|
73
|
+
'<span style="color:#bf616a">bright red</span>',
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should handle bright background colors", () => {
|
|
78
|
+
const input = "\x1b[102mbright green bg\x1b[0m"
|
|
79
|
+
expect(ansiToHtml(input)).toBe(
|
|
80
|
+
'<span style="background-color:#a3be8c">bright green bg</span>',
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("should handle 256-color foreground", () => {
|
|
85
|
+
const input = "\x1b[38;5;196mcolor\x1b[0m"
|
|
86
|
+
expect(ansiToHtml(input)).toBe('<span style="color:#ff0000">color</span>')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("should handle 256-color background", () => {
|
|
90
|
+
const input = "\x1b[48;5;21mbg\x1b[0m"
|
|
91
|
+
expect(ansiToHtml(input)).toBe(
|
|
92
|
+
'<span style="background-color:#0000ff">bg</span>',
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("should handle 24-bit RGB foreground", () => {
|
|
97
|
+
const input = "\x1b[38;2;255;128;0mrgb\x1b[0m"
|
|
98
|
+
expect(ansiToHtml(input)).toBe('<span style="color:#ff8000">rgb</span>')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("should handle 24-bit RGB background", () => {
|
|
102
|
+
const input = "\x1b[48;2;0;128;255mbg\x1b[0m"
|
|
103
|
+
expect(ansiToHtml(input)).toBe(
|
|
104
|
+
'<span style="background-color:#0080ff">bg</span>',
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("should handle reset via empty escape", () => {
|
|
109
|
+
const input = "\x1b[31mred\x1b[mnormal"
|
|
110
|
+
expect(ansiToHtml(input)).toBe(
|
|
111
|
+
'<span style="color:#bf616a">red</span>normal',
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("should handle multiple segments", () => {
|
|
116
|
+
const input = "start \x1b[32mgreen\x1b[0m middle \x1b[34mblue\x1b[0m end"
|
|
117
|
+
expect(ansiToHtml(input)).toBe(
|
|
118
|
+
'start <span style="color:#a3be8c">green</span> middle <span style="color:#8fa1b3">blue</span> end',
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("should handle combined bold and color", () => {
|
|
123
|
+
const input = "\x1b[1;33mbold yellow\x1b[0m"
|
|
124
|
+
expect(ansiToHtml(input)).toBe(
|
|
125
|
+
'<span style="color:#ebcb8b;font-weight:bold">bold yellow</span>',
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should handle 256-color grayscale", () => {
|
|
130
|
+
const input = "\x1b[38;5;232mblack\x1b[0m"
|
|
131
|
+
expect(ansiToHtml(input)).toBe('<span style="color:#080808">black</span>')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("should reset individual attributes", () => {
|
|
135
|
+
const input = "\x1b[1;3mbold italic\x1b[23mnot italic\x1b[0m"
|
|
136
|
+
expect(ansiToHtml(input)).toBe(
|
|
137
|
+
'<span style="font-weight:bold;font-style:italic">bold italic</span><span style="font-weight:bold">not italic</span>',
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe("transformAnsiCodeBlocks", () => {
|
|
143
|
+
function process(tree: Root) {
|
|
144
|
+
transformAnsiCodeBlocks(tree)
|
|
145
|
+
return tree
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
it("should convert ansi code block to AnsiCode JSX element", () => {
|
|
149
|
+
const tree = process(
|
|
150
|
+
makeTree({ type: "code", lang: "ansi", value: "\x1b[31mhello\x1b[0m" }),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const node = tree.children[0]
|
|
154
|
+
expect(node).toMatchObject({
|
|
155
|
+
type: "mdxJsxFlowElement",
|
|
156
|
+
name: "AnsiCode",
|
|
157
|
+
attributes: [
|
|
158
|
+
{
|
|
159
|
+
type: "mdxJsxAttribute",
|
|
160
|
+
name: "html",
|
|
161
|
+
value: '<span style="color:#bf616a">hello</span>',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("should ignore non-ansi code blocks", () => {
|
|
168
|
+
const tree = process(
|
|
169
|
+
makeTree({ type: "code", lang: "javascript", value: "const x = 1" }),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const node = tree.children[0]
|
|
173
|
+
expect(node).toMatchObject({
|
|
174
|
+
type: "code",
|
|
175
|
+
lang: "javascript",
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it("should handle plain text in ansi code block", () => {
|
|
180
|
+
const tree = process(
|
|
181
|
+
makeTree({ type: "code", lang: "ansi", value: "no escapes here" }),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const node = tree.children[0]
|
|
185
|
+
expect(node).toMatchObject({
|
|
186
|
+
type: "mdxJsxFlowElement",
|
|
187
|
+
name: "AnsiCode",
|
|
188
|
+
attributes: [
|
|
189
|
+
{
|
|
190
|
+
type: "mdxJsxAttribute",
|
|
191
|
+
name: "html",
|
|
192
|
+
value: "no escapes here",
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
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 :badge text directives into InlineBadge JSX elements */
|
|
7
|
+
export function transformBadgeDirectives(tree: Root) {
|
|
8
|
+
visit(tree, "textDirective", (node: TextDirective, index, parent) => {
|
|
9
|
+
if (node.name !== "badge" || index === undefined || !parent) return
|
|
10
|
+
|
|
11
|
+
const label = node.children
|
|
12
|
+
.map(c => ("value" in c ? String(c.value) : ""))
|
|
13
|
+
.join("")
|
|
14
|
+
|
|
15
|
+
const variant = node.attributes?.variant
|
|
16
|
+
|
|
17
|
+
const jsxNode = {
|
|
18
|
+
type: "mdxJsxTextElement" as const,
|
|
19
|
+
name: "InlineBadge",
|
|
20
|
+
attributes: [
|
|
21
|
+
{
|
|
22
|
+
type: "mdxJsxAttribute" as const,
|
|
23
|
+
name: "label",
|
|
24
|
+
value: label,
|
|
25
|
+
},
|
|
26
|
+
...(variant
|
|
27
|
+
? [
|
|
28
|
+
{
|
|
29
|
+
type: "mdxJsxAttribute" as const,
|
|
30
|
+
name: "variant",
|
|
31
|
+
value: variant,
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
: []),
|
|
35
|
+
],
|
|
36
|
+
children: [],
|
|
37
|
+
data: { _mdxExplicitJsx: true },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// @ts-expect-error mdxJsxTextElement is not in mdast types
|
|
41
|
+
parent.children.splice(index, 1, jsxNode)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Remark plugin that converts :badge text directives into InlineBadge JSX elements */
|
|
46
|
+
export const remarkBadge: Plugin<[], Root> = () => {
|
|
47
|
+
return tree => {
|
|
48
|
+
transformBadgeDirectives(tree)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { transformBadgeDirectives } from "./remark-badge.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("transformBadgeDirectives", () => {
|
|
24
|
+
function process(tree: Root) {
|
|
25
|
+
transformBadgeDirectives(tree)
|
|
26
|
+
return tree
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it("should convert :badge directive to InlineBadge JSX element", () => {
|
|
30
|
+
const directive = makeTextDirective("badge", "Beta")
|
|
31
|
+
const tree = process(
|
|
32
|
+
makeTree({
|
|
33
|
+
type: "paragraph",
|
|
34
|
+
children: [directive],
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const paragraph = tree.children[0]
|
|
39
|
+
if (!paragraph || paragraph.type !== "paragraph")
|
|
40
|
+
throw new Error("Expected paragraph")
|
|
41
|
+
const node = paragraph.children[0]
|
|
42
|
+
expect(node).toMatchObject({
|
|
43
|
+
type: "mdxJsxTextElement",
|
|
44
|
+
name: "InlineBadge",
|
|
45
|
+
attributes: [{ type: "mdxJsxAttribute", name: "label", value: "Beta" }],
|
|
46
|
+
children: [],
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("should pass variant attribute when provided", () => {
|
|
51
|
+
const directive = makeTextDirective("badge", "Deprecated", {
|
|
52
|
+
variant: "destructive",
|
|
53
|
+
})
|
|
54
|
+
const tree = process(
|
|
55
|
+
makeTree({
|
|
56
|
+
type: "paragraph",
|
|
57
|
+
children: [directive],
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const paragraph = tree.children[0]
|
|
62
|
+
if (!paragraph || paragraph.type !== "paragraph")
|
|
63
|
+
throw new Error("Expected paragraph")
|
|
64
|
+
const node = paragraph.children[0]
|
|
65
|
+
expect(node).toMatchObject({
|
|
66
|
+
type: "mdxJsxTextElement",
|
|
67
|
+
name: "InlineBadge",
|
|
68
|
+
attributes: [
|
|
69
|
+
{ type: "mdxJsxAttribute", name: "label", value: "Deprecated" },
|
|
70
|
+
{
|
|
71
|
+
type: "mdxJsxAttribute",
|
|
72
|
+
name: "variant",
|
|
73
|
+
value: "destructive",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("should ignore non-badge text directives", () => {
|
|
80
|
+
const directive = makeTextDirective("other", "Text")
|
|
81
|
+
const tree = process(
|
|
82
|
+
makeTree({
|
|
83
|
+
type: "paragraph",
|
|
84
|
+
children: [directive],
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const paragraph = tree.children[0]
|
|
89
|
+
if (!paragraph || paragraph.type !== "paragraph")
|
|
90
|
+
throw new Error("Expected paragraph")
|
|
91
|
+
expect(paragraph.children[0]).toMatchObject({
|
|
92
|
+
type: "textDirective",
|
|
93
|
+
name: "other",
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
})
|