@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 CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "0.5.6",
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
- "lowlight": "^3.3.0",
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 { common, createLowlight } from "lowlight"
3
- import { toHtml } from "hast-util-to-html"
1
+ import { useEffect, useMemo, useState } from "react"
2
+ import { parse } from "gpu-lexer"
4
3
 
5
- const lowlight = createLowlight(common)
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 LANGUAGE_ALIASES: Record<string, string> = {
8
- js: "javascript",
9
- jsx: "javascript",
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 normalizeLanguage(language: string | null) {
18
- if (!language) return null
19
- const normalized = language.toLowerCase()
20
- return LANGUAGE_ALIASES[normalized] ?? normalized
15
+ function escapeHtml(value: string) {
16
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;")
21
17
  }
22
18
 
23
- function highlightedHtml(code: string, language: string | null) {
24
- const normalized = normalizeLanguage(language)
25
- if (normalized && lowlight.registered(normalized)) {
26
- try {
27
- return toHtml(lowlight.highlight(normalized, code))
28
- } catch {
29
- /* fall through to plain */
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 html = useMemo(() => {
46
- if (lineNumbers) {
47
- return lines.map((line) => highlightedHtml(line || " ", language))
48
- }
49
- return highlightedHtml(code, language)
50
- }, [code, language, lineNumbers, lines])
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
- if (lineNumbers) {
53
- return (
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
- if (html === null) return <code>{code}</code>
70
- return <code className="hljs" dangerouslySetInnerHTML={{ __html: html as string }} />
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
  }