@stream-mdx/core 0.0.1 → 0.0.2
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/CHANGELOG.md +13 -0
- package/README.md +26 -37
- package/dist/code-highlighting.cjs +0 -1
- package/dist/code-highlighting.mjs +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.mjs +0 -1
- package/dist/inline-parser.cjs +0 -1
- package/dist/inline-parser.mjs +0 -1
- package/dist/mixed-content.cjs +0 -1
- package/dist/mixed-content.mjs +0 -1
- package/dist/perf/backpressure.cjs +0 -1
- package/dist/perf/backpressure.mjs +0 -1
- package/dist/perf/patch-batching.cjs +0 -1
- package/dist/perf/patch-batching.mjs +0 -1
- package/dist/perf/patch-coalescing.cjs +0 -1
- package/dist/perf/patch-coalescing.mjs +0 -1
- package/dist/security.cjs +0 -1
- package/dist/security.mjs +0 -1
- package/dist/streaming/custom-matcher.cjs +0 -1
- package/dist/streaming/custom-matcher.mjs +0 -1
- package/dist/types.cjs +0 -1
- package/dist/types.mjs +0 -1
- package/dist/utils.cjs +0 -1
- package/dist/utils.mjs +0 -1
- package/dist/worker-html-sanitizer.cjs +0 -1
- package/dist/worker-html-sanitizer.mjs +0 -1
- package/package.json +4 -2
- package/dist/code-highlighting.cjs.map +0 -1
- package/dist/code-highlighting.mjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/inline-parser.cjs.map +0 -1
- package/dist/inline-parser.mjs.map +0 -1
- package/dist/mixed-content.cjs.map +0 -1
- package/dist/mixed-content.mjs.map +0 -1
- package/dist/perf/backpressure.cjs.map +0 -1
- package/dist/perf/backpressure.mjs.map +0 -1
- package/dist/perf/patch-batching.cjs.map +0 -1
- package/dist/perf/patch-batching.mjs.map +0 -1
- package/dist/perf/patch-coalescing.cjs.map +0 -1
- package/dist/perf/patch-coalescing.mjs.map +0 -1
- package/dist/security.cjs.map +0 -1
- package/dist/security.mjs.map +0 -1
- package/dist/streaming/custom-matcher.cjs.map +0 -1
- package/dist/streaming/custom-matcher.mjs.map +0 -1
- package/dist/types.cjs.map +0 -1
- package/dist/types.mjs.map +0 -1
- package/dist/utils.cjs.map +0 -1
- package/dist/utils.mjs.map +0 -1
- package/dist/worker-html-sanitizer.cjs.map +0 -1
- package/dist/worker-html-sanitizer.mjs.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @stream-mdx/core
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9e94660: Docs and release-quality improvements: ship package READMEs/CHANGELOGs, add pack+install smoke tests, expose MDX parity helper entrypoints, and add a deployable docs site workflow.
|
|
8
|
+
|
|
9
|
+
## 0.0.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Release maintenance: CI/build fixes, missing runtime deps (e.g. `rehype-katex`), and improved docs/README wiring for the `stream-mdx` package page.
|
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# `@stream-mdx/core`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Core types + utilities shared across the StreamMDX stack.
|
|
4
|
+
|
|
5
|
+
This package is intentionally React-free. It contains structured-clone-safe types and helpers used by both the worker and the renderer.
|
|
6
|
+
|
|
7
|
+
Most consumers should install `stream-mdx` and follow the main docs. Use `@stream-mdx/core` directly if you’re building tooling or customizing lower-level behavior.
|
|
4
8
|
|
|
5
9
|
## Install
|
|
6
10
|
|
|
@@ -8,47 +12,32 @@ Pure TypeScript primitives shared across the streaming renderer stack. This pack
|
|
|
8
12
|
npm install @stream-mdx/core
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
##
|
|
15
|
+
## Entry points
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
- `@stream-mdx/core` (root)
|
|
18
|
+
- `@stream-mdx/core/types`
|
|
19
|
+
- `@stream-mdx/core/utils`
|
|
20
|
+
- `@stream-mdx/core/code-highlighting`
|
|
21
|
+
- `@stream-mdx/core/inline-parser`
|
|
22
|
+
- `@stream-mdx/core/mixed-content`
|
|
23
|
+
- `@stream-mdx/core/worker-html-sanitizer`
|
|
24
|
+
- `@stream-mdx/core/security`
|
|
25
|
+
- `@stream-mdx/core/perf/backpressure`
|
|
26
|
+
- `@stream-mdx/core/perf/patch-batching`
|
|
27
|
+
- `@stream-mdx/core/perf/patch-coalescing`
|
|
28
|
+
- `@stream-mdx/core/streaming/custom-matcher`
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Usage
|
|
30
|
+
## Example
|
|
28
31
|
|
|
29
32
|
```ts
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const snapshot = createInitialSnapshot();
|
|
35
|
-
for (const patch of patches) {
|
|
36
|
-
applyPatchBatch(snapshot, [patch]);
|
|
37
|
-
}
|
|
38
|
-
return snapshot;
|
|
33
|
+
import { DEFAULT_BACKPRESSURE_CONFIG } from "@stream-mdx/core/perf/backpressure";
|
|
34
|
+
|
|
35
|
+
export function makeConfig(overrides?: Partial<typeof DEFAULT_BACKPRESSURE_CONFIG>) {
|
|
36
|
+
return { ...DEFAULT_BACKPRESSURE_CONFIG, ...overrides };
|
|
39
37
|
}
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
> For end-to-end math/MDX registration steps (worker + renderer), see [`docs/STREAMING_MARKDOWN_PLUGINS_COOKBOOK.md#5-math--mdx-workerrenderer-registration`](../../docs/STREAMING_MARKDOWN_PLUGINS_COOKBOOK.md#5-math--mdx-workerrenderer-registration).
|
|
45
|
-
|
|
46
|
-
## Security notes
|
|
47
|
-
|
|
48
|
-
- Sanitization helpers assume you pass trusted markdown inputs or run the worker in an isolated thread. If you enable raw HTML rendering, ensure you serve KaTeX/MDX assets from trusted origins and set CSP headers accordingly.
|
|
49
|
-
- `worker-html-sanitizer` exports a minimal schema. Override/augment it if you need to allow additional tags/attributes (e.g., custom `data-*` props).
|
|
50
|
-
|
|
51
|
-
## Roadmap
|
|
40
|
+
## Docs
|
|
52
41
|
|
|
53
|
-
-
|
|
54
|
-
-
|
|
42
|
+
- API reference: `docs/PUBLIC_API.md`
|
|
43
|
+
- Security model: `docs/SECURITY_MODEL.md`
|
package/dist/index.cjs
CHANGED
package/dist/index.mjs
CHANGED
package/dist/inline-parser.cjs
CHANGED
package/dist/inline-parser.mjs
CHANGED
package/dist/mixed-content.cjs
CHANGED
package/dist/mixed-content.mjs
CHANGED
package/dist/security.cjs
CHANGED
package/dist/security.mjs
CHANGED
package/dist/types.cjs
CHANGED
package/dist/types.mjs
CHANGED
package/dist/utils.cjs
CHANGED
package/dist/utils.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-mdx/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Core types, snapshot utilities, and perf helpers for the Streaming Markdown V2 stack",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -78,7 +78,9 @@
|
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"files": [
|
|
81
|
-
"dist"
|
|
81
|
+
"dist",
|
|
82
|
+
"README.md",
|
|
83
|
+
"CHANGELOG.md"
|
|
82
84
|
],
|
|
83
85
|
"sideEffects": false,
|
|
84
86
|
"scripts": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/code-highlighting.ts"],"sourcesContent":["export type HighlightedLine = string | null;\n\nfunction normalizeNewlines(input: string): string {\n return input.replace(/\\r\\n?/g, \"\\n\");\n}\n\nfunction isFenceLine(line: string): boolean {\n return /^```/.test(line.trim());\n}\n\nexport function stripCodeFence(raw: string): { code: string; info: string; hadFence: boolean } {\n if (!raw) {\n return { code: \"\", info: \"\", hadFence: false };\n }\n const normalized = normalizeNewlines(raw);\n const lines = normalized.split(\"\\n\");\n if (lines.length === 0) {\n return { code: normalized, info: \"\", hadFence: false };\n }\n\n const firstLine = lines[0];\n if (!isFenceLine(firstLine)) {\n return { code: normalized, info: \"\", hadFence: false };\n }\n\n const info = firstLine.slice(3).trim();\n let endIndex = lines.length - 1;\n while (endIndex > 0 && lines[endIndex].trim().length === 0) {\n endIndex--;\n }\n if (endIndex > 0 && isFenceLine(lines[endIndex])) {\n const codeLines = lines.slice(1, endIndex);\n return { code: codeLines.join(\"\\n\"), info, hadFence: true };\n }\n\n // During streaming, code block may not have closing fence yet\n // Still extract and return the info from the opening fence\n const codeLines = lines.slice(1);\n return { code: codeLines.join(\"\\n\"), info, hadFence: true };\n}\n\nfunction getDomParser(): DOMParser | null {\n if (typeof window !== \"undefined\" && typeof window.DOMParser === \"function\") {\n return new window.DOMParser();\n }\n if (typeof DOMParser !== \"undefined\") {\n try {\n return new DOMParser();\n } catch (e) {\n return null;\n }\n }\n return null;\n}\n\nexport function extractHighlightedLines(html: string, fallbackLength: number): HighlightedLine[] {\n if (!html) {\n return new Array(Math.max(0, fallbackLength)).fill(null);\n }\n\n const parser = getDomParser();\n if (parser) {\n try {\n const doc = parser.parseFromString(`<div>${html}</div>`, \"text/html\");\n const nodes = doc.querySelectorAll(\"span.line\");\n if (nodes.length > 0) {\n const lines: HighlightedLine[] = [];\n for (const node of nodes) {\n lines.push(node instanceof Element ? node.innerHTML : null);\n }\n return normalizeHighlightedLines(lines, fallbackLength);\n }\n } catch (error) {\n // fall through to manual extraction\n }\n }\n\n return manualExtractHighlightedLines(html, fallbackLength);\n}\n\nfunction normalizeHighlightedLines(lines: HighlightedLine[], fallbackLength: number): HighlightedLine[] {\n if (!lines || lines.length === 0) {\n return new Array(Math.max(0, fallbackLength)).fill(null);\n }\n const length = Math.max(fallbackLength, lines.length);\n const result: HighlightedLine[] = new Array(length).fill(null);\n for (let i = 0; i < lines.length; i++) {\n result[i] = lines[i];\n }\n return result;\n}\n\nfunction manualExtractHighlightedLines(html: string, fallbackLength: number): HighlightedLine[] {\n const lineRegex = /<span class=\"line\"(?:\\s+[^>]*)?>/gi;\n const lines: string[] = [];\n let match: RegExpExecArray | null = lineRegex.exec(html);\n\n while (match !== null) {\n const openTagEnd = html.indexOf(\">\", match.index);\n if (openTagEnd === -1) break;\n let cursor = openTagEnd + 1;\n let depth = 1;\n let buffer = \"\";\n\n while (cursor < html.length && depth > 0) {\n const nextTagStart = html.indexOf(\"<\", cursor);\n if (nextTagStart === -1) {\n buffer += html.slice(cursor);\n cursor = html.length;\n break;\n }\n if (nextTagStart > cursor) {\n buffer += html.slice(cursor, nextTagStart);\n }\n cursor = nextTagStart;\n if (html.startsWith(\"</span>\", cursor)) {\n depth -= 1;\n cursor += 7;\n if (depth === 0) break;\n buffer += \"</span>\";\n continue;\n }\n const tagEnd = html.indexOf(\">\", cursor);\n if (tagEnd === -1) {\n buffer += html.slice(cursor);\n cursor = html.length;\n break;\n }\n const tag = html.slice(cursor, tagEnd + 1);\n if (/^<span\\b/i.test(tag)) {\n depth += 1;\n }\n buffer += tag;\n cursor = tagEnd + 1;\n }\n\n lines.push(buffer);\n lineRegex.lastIndex = cursor;\n match = lineRegex.exec(html);\n }\n\n return normalizeHighlightedLines(lines, fallbackLength);\n}\n\nexport function dedentIndentedCode(raw: string): string {\n if (!raw) return \"\";\n const normalized = normalizeNewlines(raw);\n const lines = normalized.split(\"\\n\");\n let minIndent = Number.POSITIVE_INFINITY;\n for (const line of lines) {\n const match = line.match(/^\\s+/);\n if (!match) continue;\n minIndent = Math.min(minIndent, match[0].length);\n }\n if (!Number.isFinite(minIndent) || minIndent === 0) {\n return normalized;\n }\n return lines.map((line) => (line.startsWith(\" \".repeat(minIndent)) ? line.slice(minIndent) : line)).join(\"\\n\");\n}\n\nexport function extractCodeLines(raw: string): string[] {\n if (!raw) return [];\n const normalized = normalizeNewlines(raw);\n const { code, hadFence } = stripCodeFence(normalized);\n if (hadFence) {\n return code.split(\"\\n\");\n }\n if (/^\\s{4}/m.test(normalized)) {\n return dedentIndentedCode(normalized).split(\"\\n\");\n }\n return normalized.split(\"\\n\");\n}\n\nexport function extractCodeWrapperAttributes(html: string): {\n preAttrs?: Record<string, string>;\n codeAttrs?: Record<string, string>;\n} {\n if (!html) {\n return {};\n }\n const preMatch = html.match(/<pre\\b([^>]*)>/i);\n const codeMatch = html.match(/<code\\b([^>]*)>/i);\n return {\n preAttrs: preMatch ? filterAllowedAttributes(parseAttributeFragment(preMatch[1] ?? \"\")) : undefined,\n codeAttrs: codeMatch ? filterAllowedAttributes(parseAttributeFragment(codeMatch[1] ?? \"\")) : undefined,\n };\n}\n\nfunction parseAttributeFragment(fragment: string): Record<string, string> {\n const attrs: Record<string, string> = {};\n const regex = /([a-zA-Z_:][\\w:.-]*)\\s*=\\s*\"([^\"]*)\"/g;\n let match: RegExpExecArray | null = regex.exec(fragment);\n while (match !== null) {\n const [, name, value] = match;\n attrs[name] = value;\n match = regex.exec(fragment);\n }\n return attrs;\n}\n\nfunction filterAllowedAttributes(attrs: Record<string, string>): Record<string, string> {\n const allowed = new Set([\"class\", \"style\", \"data-theme\"]);\n const filtered: Record<string, string> = {};\n for (const [key, value] of Object.entries(attrs)) {\n if (allowed.has(key) || key.startsWith(\"data-\")) {\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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","names":["codeLines"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/code-highlighting.ts"],"sourcesContent":["export type HighlightedLine = string | null;\n\nfunction normalizeNewlines(input: string): string {\n return input.replace(/\\r\\n?/g, \"\\n\");\n}\n\nfunction isFenceLine(line: string): boolean {\n return /^```/.test(line.trim());\n}\n\nexport function stripCodeFence(raw: string): { code: string; info: string; hadFence: boolean } {\n if (!raw) {\n return { code: \"\", info: \"\", hadFence: false };\n }\n const normalized = normalizeNewlines(raw);\n const lines = normalized.split(\"\\n\");\n if (lines.length === 0) {\n return { code: normalized, info: \"\", hadFence: false };\n }\n\n const firstLine = lines[0];\n if (!isFenceLine(firstLine)) {\n return { code: normalized, info: \"\", hadFence: false };\n }\n\n const info = firstLine.slice(3).trim();\n let endIndex = lines.length - 1;\n while (endIndex > 0 && lines[endIndex].trim().length === 0) {\n endIndex--;\n }\n if (endIndex > 0 && isFenceLine(lines[endIndex])) {\n const codeLines = lines.slice(1, endIndex);\n return { code: codeLines.join(\"\\n\"), info, hadFence: true };\n }\n\n // During streaming, code block may not have closing fence yet\n // Still extract and return the info from the opening fence\n const codeLines = lines.slice(1);\n return { code: codeLines.join(\"\\n\"), info, hadFence: true };\n}\n\nfunction getDomParser(): DOMParser | null {\n if (typeof window !== \"undefined\" && typeof window.DOMParser === \"function\") {\n return new window.DOMParser();\n }\n if (typeof DOMParser !== \"undefined\") {\n try {\n return new DOMParser();\n } catch (e) {\n return null;\n }\n }\n return null;\n}\n\nexport function extractHighlightedLines(html: string, fallbackLength: number): HighlightedLine[] {\n if (!html) {\n return new Array(Math.max(0, fallbackLength)).fill(null);\n }\n\n const parser = getDomParser();\n if (parser) {\n try {\n const doc = parser.parseFromString(`<div>${html}</div>`, \"text/html\");\n const nodes = doc.querySelectorAll(\"span.line\");\n if (nodes.length > 0) {\n const lines: HighlightedLine[] = [];\n for (const node of nodes) {\n lines.push(node instanceof Element ? node.innerHTML : null);\n }\n return normalizeHighlightedLines(lines, fallbackLength);\n }\n } catch (error) {\n // fall through to manual extraction\n }\n }\n\n return manualExtractHighlightedLines(html, fallbackLength);\n}\n\nfunction normalizeHighlightedLines(lines: HighlightedLine[], fallbackLength: number): HighlightedLine[] {\n if (!lines || lines.length === 0) {\n return new Array(Math.max(0, fallbackLength)).fill(null);\n }\n const length = Math.max(fallbackLength, lines.length);\n const result: HighlightedLine[] = new Array(length).fill(null);\n for (let i = 0; i < lines.length; i++) {\n result[i] = lines[i];\n }\n return result;\n}\n\nfunction manualExtractHighlightedLines(html: string, fallbackLength: number): HighlightedLine[] {\n const lineRegex = /<span class=\"line\"(?:\\s+[^>]*)?>/gi;\n const lines: string[] = [];\n let match: RegExpExecArray | null = lineRegex.exec(html);\n\n while (match !== null) {\n const openTagEnd = html.indexOf(\">\", match.index);\n if (openTagEnd === -1) break;\n let cursor = openTagEnd + 1;\n let depth = 1;\n let buffer = \"\";\n\n while (cursor < html.length && depth > 0) {\n const nextTagStart = html.indexOf(\"<\", cursor);\n if (nextTagStart === -1) {\n buffer += html.slice(cursor);\n cursor = html.length;\n break;\n }\n if (nextTagStart > cursor) {\n buffer += html.slice(cursor, nextTagStart);\n }\n cursor = nextTagStart;\n if (html.startsWith(\"</span>\", cursor)) {\n depth -= 1;\n cursor += 7;\n if (depth === 0) break;\n buffer += \"</span>\";\n continue;\n }\n const tagEnd = html.indexOf(\">\", cursor);\n if (tagEnd === -1) {\n buffer += html.slice(cursor);\n cursor = html.length;\n break;\n }\n const tag = html.slice(cursor, tagEnd + 1);\n if (/^<span\\b/i.test(tag)) {\n depth += 1;\n }\n buffer += tag;\n cursor = tagEnd + 1;\n }\n\n lines.push(buffer);\n lineRegex.lastIndex = cursor;\n match = lineRegex.exec(html);\n }\n\n return normalizeHighlightedLines(lines, fallbackLength);\n}\n\nexport function dedentIndentedCode(raw: string): string {\n if (!raw) return \"\";\n const normalized = normalizeNewlines(raw);\n const lines = normalized.split(\"\\n\");\n let minIndent = Number.POSITIVE_INFINITY;\n for (const line of lines) {\n const match = line.match(/^\\s+/);\n if (!match) continue;\n minIndent = Math.min(minIndent, match[0].length);\n }\n if (!Number.isFinite(minIndent) || minIndent === 0) {\n return normalized;\n }\n return lines.map((line) => (line.startsWith(\" \".repeat(minIndent)) ? line.slice(minIndent) : line)).join(\"\\n\");\n}\n\nexport function extractCodeLines(raw: string): string[] {\n if (!raw) return [];\n const normalized = normalizeNewlines(raw);\n const { code, hadFence } = stripCodeFence(normalized);\n if (hadFence) {\n return code.split(\"\\n\");\n }\n if (/^\\s{4}/m.test(normalized)) {\n return dedentIndentedCode(normalized).split(\"\\n\");\n }\n return normalized.split(\"\\n\");\n}\n\nexport function extractCodeWrapperAttributes(html: string): {\n preAttrs?: Record<string, string>;\n codeAttrs?: Record<string, string>;\n} {\n if (!html) {\n return {};\n }\n const preMatch = html.match(/<pre\\b([^>]*)>/i);\n const codeMatch = html.match(/<code\\b([^>]*)>/i);\n return {\n preAttrs: preMatch ? filterAllowedAttributes(parseAttributeFragment(preMatch[1] ?? \"\")) : undefined,\n codeAttrs: codeMatch ? filterAllowedAttributes(parseAttributeFragment(codeMatch[1] ?? \"\")) : undefined,\n };\n}\n\nfunction parseAttributeFragment(fragment: string): Record<string, string> {\n const attrs: Record<string, string> = {};\n const regex = /([a-zA-Z_:][\\w:.-]*)\\s*=\\s*\"([^\"]*)\"/g;\n let match: RegExpExecArray | null = regex.exec(fragment);\n while (match !== null) {\n const [, name, value] = match;\n attrs[name] = value;\n match = regex.exec(fragment);\n }\n return attrs;\n}\n\nfunction filterAllowedAttributes(attrs: Record<string, string>): Record<string, string> {\n const allowed = new Set([\"class\", \"style\", \"data-theme\"]);\n const filtered: Record<string, string> = {};\n for (const [key, value] of Object.entries(attrs)) {\n if (allowed.has(key) || key.startsWith(\"data-\")) {\n filtered[key] = value;\n }\n }\n return filtered;\n}\n"],"mappings":"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","names":["codeLines"]}
|