@brett_lamy/docstream 0.5.6 → 0.5.7
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/package.json +5 -3
- package/src/docs/HighlightedCode.tsx +42 -55
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "GitBook-aware readonly markdown and AI stream renderer.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
5
8
|
"type": "module",
|
|
6
9
|
"scripts": {
|
|
7
10
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
@@ -71,9 +74,8 @@
|
|
|
71
74
|
"dependencies": {
|
|
72
75
|
"@brett_lamy/viz-engine": ">=0.2.0",
|
|
73
76
|
"framer-motion": "^13.1.1",
|
|
74
|
-
"hast-util-to-html": "^9.0.5",
|
|
75
77
|
"katex": "^0.18.5",
|
|
76
|
-
"
|
|
78
|
+
"gpu-lexer": "^0.0.2",
|
|
77
79
|
"lucide-react": "^1.17.0",
|
|
78
80
|
"mermaid": "^11.15.0",
|
|
79
81
|
"rrweb": "2.0.0-alpha.20",
|
|
@@ -1,71 +1,58 @@
|
|
|
1
|
-
import { useMemo } from "react"
|
|
2
|
-
import {
|
|
3
|
-
import { toHtml } from "hast-util-to-html"
|
|
1
|
+
import { useEffect, useMemo, useState } from "react"
|
|
2
|
+
import { parse } from "gpu-lexer"
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
type Span = {
|
|
5
|
+
type: "plain" | "comment" | "string" | "number" | "keyword" | "type" | "function" | "constant" | "operator"
|
|
6
|
+
start: number
|
|
7
|
+
end: number
|
|
8
|
+
}
|
|
6
9
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
sh: "bash",
|
|
11
|
-
shell: "bash",
|
|
12
|
-
ts: "typescript",
|
|
13
|
-
tsx: "typescript",
|
|
14
|
-
yml: "yaml",
|
|
10
|
+
const COLORS: Record<Span["type"], string | undefined> = {
|
|
11
|
+
plain: undefined, comment: "#6B6B78", string: "#A5D6A7", number: "#F78C6C",
|
|
12
|
+
keyword: "#C792EA", type: "#FFCB6B", function: "#82AAFF", constant: "#F07178", operator: "#89DDFF",
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
const normalized = language.toLowerCase()
|
|
20
|
-
return LANGUAGE_ALIASES[normalized] ?? normalized
|
|
15
|
+
function escapeHtml(value: string) {
|
|
16
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """)
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return null
|
|
19
|
+
function spansToHtml(code: string, spans: Span[]) {
|
|
20
|
+
let cursor = 0
|
|
21
|
+
return spans.map((span) => {
|
|
22
|
+
const prefix = escapeHtml(code.slice(cursor, span.start))
|
|
23
|
+
const text = escapeHtml(code.slice(span.start, span.end))
|
|
24
|
+
cursor = span.end
|
|
25
|
+
const color = COLORS[span.type]
|
|
26
|
+
return `${prefix}${color ? `<span style="color:${color}">${text}</span>` : text}`
|
|
27
|
+
}).join("") + escapeHtml(code.slice(cursor))
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
export function HighlightedCode({
|
|
36
|
-
code,
|
|
37
|
-
language,
|
|
38
|
-
lineNumbers = false,
|
|
39
|
-
}: {
|
|
30
|
+
export function HighlightedCode({ code, language: _language, lineNumbers = false }: {
|
|
40
31
|
code: string
|
|
41
32
|
language: string | null
|
|
42
33
|
lineNumbers?: boolean
|
|
43
34
|
}) {
|
|
44
35
|
const lines = useMemo(() => code.split("\n"), [code])
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
const [spans, setSpans] = useState<Span[] | null>(null)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
let cancelled = false
|
|
40
|
+
setSpans(null)
|
|
41
|
+
parse(code).then((result) => { if (!cancelled) setSpans(result) }).catch(() => { if (!cancelled) setSpans([]) })
|
|
42
|
+
return () => { cancelled = true }
|
|
43
|
+
}, [code])
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<code className="hljs">
|
|
55
|
-
{(html as Array<string | null>).map((lineHtml, index) => (
|
|
56
|
-
<span key={index} className="docs-code-line">
|
|
57
|
-
<span className="docs-code-lineno">{index + 1}</span>
|
|
58
|
-
{lineHtml === null ? (
|
|
59
|
-
<span>{lines[index]}</span>
|
|
60
|
-
) : (
|
|
61
|
-
<span dangerouslySetInnerHTML={{ __html: lineHtml }} />
|
|
62
|
-
)}
|
|
63
|
-
</span>
|
|
64
|
-
))}
|
|
65
|
-
</code>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
45
|
+
const html = useMemo(() => spans && spans.length ? spansToHtml(code, spans) : null, [code, spans])
|
|
46
|
+
if (!lineNumbers) return html === null ? <code>{code}</code> : <code dangerouslySetInnerHTML={{ __html: html }} />
|
|
68
47
|
|
|
69
|
-
|
|
70
|
-
|
|
48
|
+
return <code>{lines.map((line, index) => {
|
|
49
|
+
const start = lines.slice(0, index).reduce((total, value) => total + value.length + 1, 0)
|
|
50
|
+
const lineSpans = spans?.filter((span) => span.start >= start && span.end <= start + line.length)
|
|
51
|
+
.map((span) => ({ ...span, start: span.start - start, end: span.end - start }))
|
|
52
|
+
const lineHtml = lineSpans?.length ? spansToHtml(line, lineSpans) : null
|
|
53
|
+
return <span key={index} className="docs-code-line">
|
|
54
|
+
<span className="docs-code-lineno">{index + 1}</span>
|
|
55
|
+
{lineHtml === null ? <span>{line}</span> : <span dangerouslySetInnerHTML={{ __html: lineHtml }} />}
|
|
56
|
+
</span>
|
|
57
|
+
})}</code>
|
|
71
58
|
}
|