@ai-react-markdown/core 1.4.2 → 1.4.4
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/README.md +140 -22
- package/dist/index.cjs +777 -336
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -29
- package/dist/index.d.ts +252 -29
- package/dist/index.js +735 -296
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -49,6 +49,14 @@ For LaTeX math rendering, include the KaTeX stylesheet:
|
|
|
49
49
|
import 'katex/dist/katex.min.css';
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
`katex` is declared as an **optional peer dependency**. It ships transitively via `rehype-katex`, so hoisted installers (npm, yarn classic, default pnpm) resolve the import automatically. Strict-isolation installers (yarn PnP, `pnpm --node-linker=isolated`) need it installed explicitly:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install katex
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Skip the install only if you have no `import 'katex/…'` calls in your app and don't render math.
|
|
59
|
+
|
|
52
60
|
For the built-in default typography, include the typography CSS:
|
|
53
61
|
|
|
54
62
|
```tsx
|
|
@@ -81,21 +89,23 @@ function StreamingChat({ content, isStreaming }: { content: string; isStreaming:
|
|
|
81
89
|
|
|
82
90
|
### `AIMarkdownProps<TConfig, TRenderData>`
|
|
83
91
|
|
|
84
|
-
| Prop | Type | Default | Description
|
|
85
|
-
| ---------------------- | -------------------------------- | ------------------------------- |
|
|
86
|
-
| `content` | `string` | **(required)** | Raw markdown content to render.
|
|
87
|
-
| `streaming` | `boolean` | `false` | Whether content is actively being streamed (e.g. from an LLM).
|
|
88
|
-
| `fontSize` | `number \| string` | `'0.9375rem'` | Base font size. Numbers are treated as pixels.
|
|
89
|
-
| `variant` | `AIMarkdownVariant` | `'default'` | Typography variant name.
|
|
90
|
-
| `colorScheme` | `AIMarkdownColorScheme` | `'light'` | Color scheme name (`'light'`, `'dark'`, or custom).
|
|
91
|
-
| `config` | `PartialDeep<TConfig>` | `undefined` | Partial render config, deep-merged with defaults.
|
|
92
|
-
| `defaultConfig` | `TConfig` | `defaultAIMarkdownRenderConfig` | Base config to merge against. Sub-packages can pass extended defaults.
|
|
93
|
-
| `metadata` | `TRenderData` | `undefined` | Arbitrary data passed to custom components via a dedicated context.
|
|
94
|
-
| `contentPreprocessors` | `AIMDContentPreprocessor[]` | `[]` | Additional preprocessors run after the built-in LaTeX preprocessor.
|
|
95
|
-
| `customComponents` | `AIMarkdownCustomComponents` | `undefined` | `react-markdown` component overrides for specific HTML elements.
|
|
96
|
-
| `Typography` | `AIMarkdownTypographyComponent` | `DefaultTypography` | Typography wrapper component.
|
|
97
|
-
| `ExtraStyles` | `AIMarkdownExtraStylesComponent` | `undefined` | Optional extra style wrapper rendered between typography and content.
|
|
92
|
+
| Prop | Type | Default | Description |
|
|
93
|
+
| ---------------------- | -------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| `content` | `string` | **(required)** | Raw markdown content to render. |
|
|
95
|
+
| `streaming` | `boolean` | `false` | Whether content is actively being streamed (e.g. from an LLM). |
|
|
96
|
+
| `fontSize` | `number \| string` | `'0.9375rem'` | Base font size. Numbers are treated as pixels. |
|
|
97
|
+
| `variant` | `AIMarkdownVariant` | `'default'` | Typography variant name. |
|
|
98
|
+
| `colorScheme` | `AIMarkdownColorScheme` | `'light'` | Color scheme name (`'light'`, `'dark'`, or custom). |
|
|
99
|
+
| `config` | `PartialDeep<TConfig>` | `undefined` | Partial render config, deep-merged with defaults. |
|
|
100
|
+
| `defaultConfig` | `TConfig` | `defaultAIMarkdownRenderConfig` | Base config to merge against. Sub-packages can pass extended defaults. |
|
|
101
|
+
| `metadata` | `TRenderData` | `undefined` | Arbitrary data passed to custom components via a dedicated context. |
|
|
102
|
+
| `contentPreprocessors` | `AIMDContentPreprocessor[]` | `[]` | Additional preprocessors run after the built-in LaTeX preprocessor. |
|
|
103
|
+
| `customComponents` | `AIMarkdownCustomComponents` | `undefined` | `react-markdown` component overrides for specific HTML elements. |
|
|
104
|
+
| `Typography` | `AIMarkdownTypographyComponent` | `DefaultTypography` | Typography wrapper component. |
|
|
105
|
+
| `ExtraStyles` | `AIMarkdownExtraStylesComponent` | `undefined` | Optional extra style wrapper rendered between typography and content. |
|
|
98
106
|
| `documentId` | `string` | auto via `useId()` | Stable id for the _logical markdown document_ this `<AIMarkdown>` is rendering. Used as the id namespace for clobberable attributes (`id`, hash hrefs) so two documents on the same page do not cross-link (footnote `[^1]` in message A won't scroll to `[^1]` in message B). When one document is split into chunks rendered by multiple `<AIMarkdown>` instances, pass the SAME `documentId` to every chunk so prefixes align. The value is passed through `encodeURIComponent` before being injected into HTML attributes, so any string is safe (React's `useId()` output, your own opaque ids, user-supplied UUIDs). Long ids (>16 chars, e.g. UUIDs) are hashed via MurmurHash3 to a short Base62 form **inside the rendered `id="…"`/`href="#…"` prefix only** to keep HTML compact; `state.documentId` itself and registry keying via `useDocumentRegistry` stay raw, so deep linking and any consumer code reading `documentId` are unaffected. |
|
|
107
|
+
| `urlTransform` | `UrlTransform \| null` | `defaultUrlTransform` | Override the URL allowlist applied to `href`, `src`, and similar attributes. The default mirrors GitHub: `http`, `https`, `irc`, `ircs`, `mailto`, `xmpp`. Pass a function defined at module scope (or memoized) to permit additional schemes — see [Custom URL Schemes and Sanitization](#custom-url-schemes-and-sanitization). |
|
|
108
|
+
| `sanitizeSchema` | `SanitizeSchema` | library default | Override the `rehype-sanitize` schema. Build with [`extendSanitizeSchema`](#custom-url-schemes-and-sanitization) so the library's cross-chunk tag and KaTeX className allowlists survive — hand-rolling silently drops them. |
|
|
99
109
|
|
|
100
110
|
## Configuration
|
|
101
111
|
|
|
@@ -192,6 +202,108 @@ function MyHelper({ documentId }: { documentId: string }) {
|
|
|
192
202
|
}
|
|
193
203
|
```
|
|
194
204
|
|
|
205
|
+
## Custom URL Schemes and Sanitization
|
|
206
|
+
|
|
207
|
+
By default `<AIMarkdown>` only renders links and images whose URLs use the standard set of safe protocols (`http`, `https`, `irc`, `ircs`, `mailto`, `xmpp`). Anything else — `javascript:`, `data:`, or your own `myapp://` — is stripped. This protects against XSS in LLM-generated markdown but also means private application schemes are unreachable without configuration.
|
|
208
|
+
|
|
209
|
+
### The Two-Gate Model
|
|
210
|
+
|
|
211
|
+
Sanitization runs in **two independent gates** (defense in depth):
|
|
212
|
+
|
|
213
|
+
1. **`urlTransform`** — runs first, on every URL-bearing attribute, and rewrites disallowed URLs to `''`. Called per-attribute with the attribute name (`'href'` / `'src'` / …) so key-aware transforms can discriminate (e.g. allow a scheme on `href` but not on `src` to block tracker pixels).
|
|
214
|
+
2. **`rehype-sanitize` schema** — runs second, and drops the URL when the protocol is not in the schema's per-attribute allowlist (`protocols.href`, `protocols.src`, `protocols.cite`).
|
|
215
|
+
|
|
216
|
+
For a private scheme to render, **both gates must permit it**. Allowing only one is the most common pitfall.
|
|
217
|
+
|
|
218
|
+
**Cross-chunk symmetry.** When `<AIMarkdown>` instances are wrapped in `<AIMarkdownDocuments>`, link/image references resolved across chunks (chunk A defines `[evil]: …`, chunk B writes `[click][evil]`) go through both gates as well — the same `urlTransform` and `sanitizeSchema` you pass to `<AIMarkdown>` apply at render time. The per-attribute key (`'href'` vs `'src'`) is honored: a key-aware policy that permits a scheme on `<a>` but not `<img>` will produce identical behavior whether the reference is in-chunk or cross-chunk.
|
|
219
|
+
|
|
220
|
+
### Allowing a Custom Scheme
|
|
221
|
+
|
|
222
|
+
Define both gates at module scope so their reference identity is stable across renders (this keeps the per-block memo cache warm):
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
import AIMarkdown, { defaultUrlTransform, extendSanitizeSchema } from '@ai-react-markdown/core';
|
|
226
|
+
|
|
227
|
+
// Gate 1: compose with the default so https/mailto/etc. still work.
|
|
228
|
+
const ALLOWED = /^myapp:/i;
|
|
229
|
+
const URL_TRANSFORM = (url, key, node) => (ALLOWED.test(url) ? url : defaultUrlTransform(url, key, node));
|
|
230
|
+
|
|
231
|
+
// Gate 2: extend the library schema so it permits the scheme on href + src.
|
|
232
|
+
const SCHEMA = extendSanitizeSchema((s) => {
|
|
233
|
+
s.protocols.href.push('myapp');
|
|
234
|
+
s.protocols.src.push('myapp');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
function App() {
|
|
238
|
+
return <AIMarkdown content={markdown} urlTransform={URL_TRANSFORM} sanitizeSchema={SCHEMA} />;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### `extendSanitizeSchema((draft) => Schema | void)`
|
|
243
|
+
|
|
244
|
+
Hands you a deep clone of the library's default sanitize schema. Mutate it freely (the original singleton is never touched) or return a replacement object. Library invariants — cross-chunk coordination tags (`cross-chunk-link`, `cross-chunk-image`, `footnote-sup`), the KaTeX `math-inline` / `math-display` className allowlist, the `<mark>` allowance — survive untouched. **Hand-rolling a schema that doesn't spread these invariants silently breaks coordinated rendering**, which is why the helper is the recommended path.
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
const SCHEMA = extendSanitizeSchema((s) => {
|
|
248
|
+
s.tagNames.push('my-widget'); // add a tag
|
|
249
|
+
s.protocols.href.push('myapp'); // permit a protocol
|
|
250
|
+
s.attributes['my-widget'] = ['data-id', 'data-mode']; // allow attributes
|
|
251
|
+
// No `return` needed — mutate-only is fine.
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Footguns** (also documented in JSDoc):
|
|
256
|
+
|
|
257
|
+
- Returning `null` is treated like returning nothing (the mutated draft is used).
|
|
258
|
+
- Reassigning the local parameter (`s = { ... }`) does NOT replace the draft — JS only rebinds the local. Either mutate the original or `return` an explicit value.
|
|
259
|
+
- Throwing inside the modifier propagates uncaught. Usually fine because the helper is called once at module load.
|
|
260
|
+
|
|
261
|
+
### Reference Stability and the Cache
|
|
262
|
+
|
|
263
|
+
Both `urlTransform` and `sanitizeSchema` participate in the per-block memo cache, but they are stabilized **asymmetrically**:
|
|
264
|
+
|
|
265
|
+
- **`urlTransform`** is tracked by identity only. A new function reference every render flushes the cache. Callers MUST supply a stable reference (module scope or `useMemo`).
|
|
266
|
+
- **`sanitizeSchema`** is tracked by identity AND additionally stabilized internally via a deep-equal safety net (`useStableValue`). An inline-but-deep-equal schema still works, just with a one-time deep compare on each render — cheaper than a cache flush but not free.
|
|
267
|
+
|
|
268
|
+
Why the asymmetry: function identity can't be deep-compared (two closures with identical bodies are always non-equal), so for `urlTransform` only the call-site can produce a stable reference. `sanitizeSchema` is plain data, so a deep compare is meaningful and serves as a guardrail for callers who forget the module-scope rule.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
// 🚫 Anti-pattern — `urlTransform` is recreated every render and discards
|
|
272
|
+
// the entire markdown cache. `sanitizeSchema` would too without the
|
|
273
|
+
// internal deep-equal safety net, but you still pay the deep-compare cost.
|
|
274
|
+
<AIMarkdown
|
|
275
|
+
urlTransform={(url, k, n) => /* … */}
|
|
276
|
+
sanitizeSchema={extendSanitizeSchema((s) => /* … */)}
|
|
277
|
+
/>
|
|
278
|
+
|
|
279
|
+
// ✅ Stable — both refs are minted once at module scope.
|
|
280
|
+
const URL_TRANSFORM = (url, k, n) => /* … */;
|
|
281
|
+
const SCHEMA = extendSanitizeSchema((s) => /* … */);
|
|
282
|
+
<AIMarkdown urlTransform={URL_TRANSFORM} sanitizeSchema={SCHEMA} />
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
In development the library will `console.warn` after detecting 3+ identity flips on either prop. The warning is dead-code-eliminated in production builds. Define both values at module scope, or memoize with `useMemo` if they depend on state.
|
|
286
|
+
|
|
287
|
+
### Regex Escaping for `+` / `-` / `.` in Scheme Names
|
|
288
|
+
|
|
289
|
+
Per RFC 3986 scheme names may contain `+`, `-`, and `.` — all regex metacharacters. Write `/^web\+app:/i`, **not** `/^web+app:/i` (the latter would match `we`, `wee`, `weee`, …, silently broadening the allowlist).
|
|
290
|
+
|
|
291
|
+
### Inspecting the Default Schema
|
|
292
|
+
|
|
293
|
+
`extendSanitizeSchema` hands the modifier a deep clone of the library default. That makes the helper itself the cleanest introspection path — no separate export of the singleton is needed:
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
extendSanitizeSchema((s) => {
|
|
297
|
+
console.log('default sanitize schema:', s);
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Why no direct `sanitizeSchema` export? Because the obvious extension pattern — `{ ...sanitizeSchema, … }` — is a shallow spread. Nested arrays (`protocols.href`, `attributes.a`, `ancestors.*`, …) stay aliased to the singleton; a subsequent `.protocols.href.push(...)` mutates it, and the change leaks into every other `<AIMarkdown>` in your app that doesn't override `sanitizeSchema`. `extendSanitizeSchema` always works on a deep clone, so this class of bug is impossible by construction.
|
|
302
|
+
|
|
303
|
+
### API Stability of `UrlTransform` and `SanitizeSchema`
|
|
304
|
+
|
|
305
|
+
Both prop types track their respective upstream packages — `UrlTransform` follows `react-markdown`'s shape and `SanitizeSchema` follows `rehype-sanitize`'s. They may evolve with those packages' major versions. Hand-construct schemas via the helpers (rather than typing your own from scratch) and you'll inherit any upstream-driven changes automatically.
|
|
306
|
+
|
|
195
307
|
## Hooks
|
|
196
308
|
|
|
197
309
|
### `useAIMarkdownRenderState<TConfig>()`
|
|
@@ -213,15 +325,15 @@ function CustomCodeBlock({ children }: PropsWithChildren) {
|
|
|
213
325
|
|
|
214
326
|
**Returns** `AIMarkdownRenderState<TConfig>`:
|
|
215
327
|
|
|
216
|
-
| Field | Type | Description
|
|
217
|
-
| --------------- | ----------------------- |
|
|
218
|
-
| `streaming` | `boolean` | Whether content is being streamed.
|
|
219
|
-
| `fontSize` | `string` | Resolved CSS font-size value.
|
|
220
|
-
| `variant` | `AIMarkdownVariant` | Active typography variant.
|
|
221
|
-
| `colorScheme` | `AIMarkdownColorScheme` | Active color scheme.
|
|
222
|
-
| `documentId` | `string` | Stable id for the logical markdown document — caller-supplied or auto-generated via `useId()`.
|
|
328
|
+
| Field | Type | Description |
|
|
329
|
+
| --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
330
|
+
| `streaming` | `boolean` | Whether content is being streamed. |
|
|
331
|
+
| `fontSize` | `string` | Resolved CSS font-size value. |
|
|
332
|
+
| `variant` | `AIMarkdownVariant` | Active typography variant. |
|
|
333
|
+
| `colorScheme` | `AIMarkdownColorScheme` | Active color scheme. |
|
|
334
|
+
| `documentId` | `string` | Stable id for the logical markdown document — caller-supplied or auto-generated via `useId()`. |
|
|
223
335
|
| `clobberPrefix` | `string` | URI-safe id prefix derived from `documentId` (with MurmurHash3 → Base62 shortening applied for >16-char ids), used by every clobberable HTML attribute (`id=…` / `href="#…"`). Read this from the render state rather than recomputing locally when writing components that emit anchors — the prefix's exact byte form is not part of the stability contract and may shift across versions. |
|
|
224
|
-
| `config` | `TConfig` | Active render configuration (merged with defaults).
|
|
336
|
+
| `config` | `TConfig` | Active render configuration (merged with defaults). |
|
|
225
337
|
|
|
226
338
|
### `useAIMarkdownMetadata<TMetadata>()`
|
|
227
339
|
|
|
@@ -462,6 +574,7 @@ The metadata and render state providers are deliberately separated so that metad
|
|
|
462
574
|
- `AIMarkdownVariant`
|
|
463
575
|
- `AIMarkdownColorScheme`
|
|
464
576
|
- `AIMDContentPreprocessor`
|
|
577
|
+
- `UrlTransform`, `SanitizeSchema` -- prop-type aliases for the URL handling props (track upstream `react-markdown` / `rehype-sanitize` shapes)
|
|
465
578
|
- `PartialDeep`
|
|
466
579
|
- Cross-chunk registry types: `Registry`, `ChunkData`, `FootnoteDef`, `LinkDef`, `RefRecord`, `RefKind`
|
|
467
580
|
|
|
@@ -470,6 +583,11 @@ The metadata and render state providers are deliberately separated so that metad
|
|
|
470
583
|
- `AIMarkdownRenderExtraSyntax`
|
|
471
584
|
- `AIMarkdownRenderDisplayOptimizeAbility`
|
|
472
585
|
- `defaultAIMarkdownRenderConfig`
|
|
586
|
+
- `defaultUrlTransform` -- the library's built-in URL-allowlist transform; compose with this when supplying a custom `urlTransform`
|
|
587
|
+
|
|
588
|
+
### Helpers
|
|
589
|
+
|
|
590
|
+
- `extendSanitizeSchema((draft) => Schema | void)` -- mutate-and-return factory that produces a sanitize schema from a deep clone of the library default; preserves cross-chunk and KaTeX invariants
|
|
473
591
|
|
|
474
592
|
### Hooks (re-exported)
|
|
475
593
|
|