@tanstack/markdown 0.0.9 → 0.0.11

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.
@@ -0,0 +1,309 @@
1
+ ---
2
+ name: 'react-rendering'
3
+ description: >
4
+ Render Markdown source or a MarkdownDocument with @tanstack/markdown/react
5
+ using Markdown, renderMarkdownReact, component replacements, and React static
6
+ SSR. Load for React article components, emitted-tag mappings, pre-parsed
7
+ documents, custom elements, or renderer parity.
8
+ metadata:
9
+ type: framework
10
+ library: '@tanstack/markdown'
11
+ framework: 'react'
12
+ library_version: '0.0.10'
13
+ requires:
14
+ - 'render-markdown'
15
+ sources:
16
+ - 'TanStack/markdown:docs/guides/react.md'
17
+ - 'TanStack/markdown:docs/reference/react.md'
18
+ - 'TanStack/markdown:docs/guides/extensions.md'
19
+ - 'TanStack/markdown:docs/core-concepts/security.md'
20
+ - 'TanStack/markdown:src/react.ts'
21
+ - 'TanStack/markdown:tests/ssr-react.test.tsx'
22
+ ---
23
+
24
+ This skill builds on [render-markdown](../render-markdown/SKILL.md). Read it first for the syntax profile, parser options, AST, and core trust boundaries.
25
+
26
+ # React Rendering
27
+
28
+ ## Setup
29
+
30
+ Render source directly and keep the surrounding semantic element in the application:
31
+
32
+ ```tsx
33
+ import { Markdown } from '@tanstack/markdown/react'
34
+
35
+ export function Article({ source }: { source: string }) {
36
+ return (
37
+ <article>
38
+ <Markdown>{source}</Markdown>
39
+ </article>
40
+ )
41
+ }
42
+ ```
43
+
44
+ `Markdown` returns a React fragment. The React adapter requires React 18 or newer.
45
+
46
+ ## Hooks and Components
47
+
48
+ ### Replace emitted links with an application component
49
+
50
+ ```tsx
51
+ import type { ComponentProps } from 'react'
52
+ import { Markdown } from '@tanstack/markdown/react'
53
+
54
+ function ArticleLink(props: ComponentProps<'a'>) {
55
+ const external = props.href?.startsWith('http') ?? false
56
+
57
+ return (
58
+ <a
59
+ {...props}
60
+ rel={external ? 'noreferrer' : props.rel}
61
+ target={external ? '_blank' : props.target}
62
+ />
63
+ )
64
+ }
65
+
66
+ export function Article({ source }: { source: string }) {
67
+ return (
68
+ <article>
69
+ <Markdown components={{ a: ArticleLink }}>{source}</Markdown>
70
+ </article>
71
+ )
72
+ }
73
+ ```
74
+
75
+ `components` is keyed by emitted intrinsic or custom-element tag names.
76
+
77
+ ### Parse once before repeated React rendering
78
+
79
+ ```tsx
80
+ import { parseMarkdown } from '@tanstack/markdown/parser'
81
+ import { Markdown } from '@tanstack/markdown/react'
82
+
83
+ const document = parseMarkdown('# Cached article\n\nRendered from an AST.')
84
+
85
+ export function Article() {
86
+ return (
87
+ <article>
88
+ <Markdown>{document}</Markdown>
89
+ </article>
90
+ )
91
+ }
92
+ ```
93
+
94
+ Apply parser options and document transforms when creating the document; renderer options cannot retroactively change its structure.
95
+
96
+ ### Render static React markup on the server
97
+
98
+ ```tsx
99
+ import { renderToStaticMarkup } from 'react-dom/server'
100
+ import { Markdown } from '@tanstack/markdown/react'
101
+
102
+ const source = '# Server-rendered article'
103
+
104
+ export const html = renderToStaticMarkup(
105
+ <article>
106
+ <Markdown>{source}</Markdown>
107
+ </article>,
108
+ )
109
+ ```
110
+
111
+ Core static output is tested for structural equivalence with `renderHtml()`.
112
+
113
+ ### Compose through the lower-level node API
114
+
115
+ ```tsx
116
+ import type { ReactNode } from 'react'
117
+ import { parseMarkdown } from '@tanstack/markdown/parser'
118
+ import { renderMarkdownReact } from '@tanstack/markdown/react'
119
+
120
+ const document = parseMarkdown('# Parsed once')
121
+
122
+ export function ArticleBody(): ReactNode {
123
+ return renderMarkdownReact(document)
124
+ }
125
+ ```
126
+
127
+ Use `renderBlockReact` or `renderInlineReact` only when custom tree composition needs individual public AST nodes.
128
+
129
+ ## Common Mistakes
130
+
131
+ ### HIGH Injecting HTML instead of React nodes
132
+
133
+ Wrong:
134
+
135
+ ```tsx
136
+ import { renderHtml } from '@tanstack/markdown'
137
+
138
+ export function Article({ source }: { source: string }) {
139
+ const html = renderHtml(source)
140
+ return <article dangerouslySetInnerHTML={{ __html: html }} />
141
+ }
142
+ ```
143
+
144
+ Correct:
145
+
146
+ ```tsx
147
+ import { Markdown } from '@tanstack/markdown/react'
148
+
149
+ export function Article({ source }: { source: string }) {
150
+ return (
151
+ <article>
152
+ <Markdown>{source}</Markdown>
153
+ </article>
154
+ )
155
+ }
156
+ ```
157
+
158
+ The HTML-string path bypasses React component replacement and creates an unnecessary trusted-HTML boundary.
159
+
160
+ Source: `docs/guides/react.md`
161
+
162
+ ### HIGH Mapping AST node names instead of tags
163
+
164
+ Wrong:
165
+
166
+ ```tsx
167
+ import type { ComponentProps } from 'react'
168
+ import { Markdown } from '@tanstack/markdown/react'
169
+
170
+ function ArticleLink(props: ComponentProps<'a'>) {
171
+ return <a {...props} data-navigation="article" />
172
+ }
173
+
174
+ export function Article() {
175
+ return (
176
+ <Markdown components={{ link: ArticleLink }}>
177
+ {'Read the [guide](/guide).'}
178
+ </Markdown>
179
+ )
180
+ }
181
+ ```
182
+
183
+ Correct:
184
+
185
+ ```tsx
186
+ import type { ComponentProps } from 'react'
187
+ import { Markdown } from '@tanstack/markdown/react'
188
+
189
+ function ArticleLink(props: ComponentProps<'a'>) {
190
+ return <a {...props} data-navigation="article" />
191
+ }
192
+
193
+ export function Article() {
194
+ return (
195
+ <Markdown components={{ a: ArticleLink }}>
196
+ {'Read the [guide](/guide).'}
197
+ </Markdown>
198
+ )
199
+ }
200
+ ```
201
+
202
+ The map is keyed by emitted tag names such as `a`, not AST discriminants such as `link`.
203
+
204
+ Source: `docs/guides/react.md`
205
+
206
+ ### HIGH Expecting HTML extension hooks in React
207
+
208
+ Wrong:
209
+
210
+ ```tsx
211
+ import type { MarkdownExtension } from '@tanstack/markdown'
212
+ import { Markdown } from '@tanstack/markdown/react'
213
+
214
+ const paragraphHtml: MarkdownExtension = {
215
+ name: 'paragraph-html',
216
+ renderHtml(node) {
217
+ return node.type === 'paragraph'
218
+ ? '<aside>Rendered only by the HTML renderer</aside>'
219
+ : undefined
220
+ },
221
+ }
222
+
223
+ export function Article() {
224
+ return <Markdown extensions={[paragraphHtml]}>Ordinary content</Markdown>
225
+ }
226
+ ```
227
+
228
+ Correct:
229
+
230
+ ```tsx
231
+ import type { MarkdownExtension } from '@tanstack/markdown'
232
+ import type { PropsWithChildren } from 'react'
233
+ import { Markdown } from '@tanstack/markdown/react'
234
+
235
+ const panelExtension: MarkdownExtension = {
236
+ name: 'panel',
237
+ transformDocument(document) {
238
+ return {
239
+ ...document,
240
+ children: [
241
+ {
242
+ type: 'component',
243
+ name: 'panel',
244
+ tagName: 'doc-panel',
245
+ attributes: {},
246
+ children: document.children,
247
+ },
248
+ ],
249
+ }
250
+ },
251
+ }
252
+
253
+ function Panel({ children }: PropsWithChildren) {
254
+ return <aside className="documentation-panel">{children}</aside>
255
+ }
256
+
257
+ export function Article() {
258
+ return (
259
+ <Markdown
260
+ components={{ 'doc-panel': Panel }}
261
+ extensions={[panelExtension]}
262
+ >
263
+ Ordinary content
264
+ </Markdown>
265
+ )
266
+ }
267
+ ```
268
+
269
+ `MarkdownExtension.renderHtml` is HTML-specific; portable custom output uses `ComponentNode` and an emitted-tag component mapping.
270
+
271
+ Source: `docs/guides/extensions.md`
272
+
273
+ ### CRITICAL Enabling raw HTML for untrusted input
274
+
275
+ Wrong:
276
+
277
+ ```tsx
278
+ import { Markdown } from '@tanstack/markdown/react'
279
+
280
+ export function UserPost({ source }: { source: string }) {
281
+ return <Markdown allowHtml>{source}</Markdown>
282
+ }
283
+ ```
284
+
285
+ Correct:
286
+
287
+ ```tsx
288
+ import { Markdown } from '@tanstack/markdown/react'
289
+
290
+ export function UserPost({ source }: { source: string }) {
291
+ return <Markdown>{source}</Markdown>
292
+ }
293
+ ```
294
+
295
+ With `allowHtml`, React uses `dangerouslySetInnerHTML`; TanStack Markdown does not sanitize that raw HTML.
296
+
297
+ Source: `docs/core-concepts/security.md`
298
+
299
+ ### HIGH Tension: renderer parity versus React customization
300
+
301
+ Core React SSR is tested against `renderHtml()`. React component replacements, raw HTML, highlighter output, and HTML-only extension hooks are application-controlled boundaries outside that parity guarantee.
302
+
303
+ See also: [custom-extensions](../custom-extensions/SKILL.md) for portable `ComponentNode` output.
304
+
305
+ ## Cross-References
306
+
307
+ - [render-markdown](../render-markdown/SKILL.md) - shared `MarkdownInput`, parser options, AST behavior, and syntax profile.
308
+ - [custom-extensions](../custom-extensions/SKILL.md) - emit custom tags that React can replace through `components`.
309
+ - [production-pipelines](../production-pipelines/SKILL.md) - audit raw HTML, highlighter output, untrusted content, and SSR boundaries.
@@ -0,0 +1,335 @@
1
+ ---
2
+ name: render-markdown
3
+ description: >
4
+ Parse Markdown with parseMarkdown or parseInline, render HTML with renderHtml,
5
+ renderDocument, renderBlock, or renderInline, configure frontmatter and
6
+ heading IDs, and reuse the serializable MarkdownDocument AST. Load for
7
+ @tanstack/markdown core syntax, parser options, HTML output, references,
8
+ footnotes, lists, tables, or AST work.
9
+ metadata:
10
+ type: core
11
+ library: '@tanstack/markdown'
12
+ library_version: '0.0.10'
13
+ sources:
14
+ - 'TanStack/markdown:docs/quick-start.md'
15
+ - 'TanStack/markdown:docs/core-concepts/document-model.md'
16
+ - 'TanStack/markdown:docs/core-concepts/parsing.md'
17
+ - 'TanStack/markdown:docs/core-concepts/rendering.md'
18
+ - 'TanStack/markdown:docs/core-concepts/syntax-profile.md'
19
+ - 'TanStack/markdown:src/parser.ts'
20
+ - 'TanStack/markdown:src/html.ts'
21
+ - 'TanStack/markdown:src/types.ts'
22
+ ---
23
+
24
+ # Render Markdown
25
+
26
+ ## Setup
27
+
28
+ Install the framework-neutral package:
29
+
30
+ ```bash
31
+ pnpm add @tanstack/markdown
32
+ ```
33
+
34
+ Render supported Markdown to HTML:
35
+
36
+ ```ts
37
+ import { renderHtml } from '@tanstack/markdown/html'
38
+
39
+ const source = `# Release notes
40
+
41
+ - Small browser bundle
42
+ - Serializable AST
43
+ - Safe HTML defaults`
44
+
45
+ const html = renderHtml(source)
46
+
47
+ console.log(html)
48
+ ```
49
+
50
+ Use the narrow `@tanstack/markdown/parser` and
51
+ `@tanstack/markdown/html` entry points when the default entry's combined
52
+ exports are unnecessary.
53
+
54
+ ## Core Patterns
55
+
56
+ ### Parse once and render a reusable document
57
+
58
+ ```ts
59
+ import { renderHtml } from '@tanstack/markdown/html'
60
+ import { parseMarkdown } from '@tanstack/markdown/parser'
61
+ import type { MarkdownDocument } from '@tanstack/markdown'
62
+
63
+ const source = `# Guide
64
+
65
+ Read the [installation notes](/installation).`
66
+
67
+ const document = parseMarkdown(source)
68
+ const serialized = JSON.stringify(document)
69
+ const restored = JSON.parse(serialized) as MarkdownDocument
70
+ const html = renderHtml(restored)
71
+
72
+ console.log(html)
73
+ ```
74
+
75
+ All complete-document renderers accept a source string or an existing
76
+ `MarkdownDocument`; a document input skips parsing.
77
+
78
+ ### Configure frontmatter and heading IDs while parsing
79
+
80
+ ```ts
81
+ import { renderHtml } from '@tanstack/markdown/html'
82
+ import { parseMarkdown } from '@tanstack/markdown/parser'
83
+
84
+ const source = `---
85
+ title: Installation
86
+ published: true
87
+ ---
88
+
89
+ # Install
90
+
91
+ ## Install`
92
+
93
+ const document = parseMarkdown(source, {
94
+ frontmatter: true,
95
+ headingIds(text, lineIndex) {
96
+ const slug = text.toLowerCase().replaceAll(' ', '-')
97
+ return `docs-${lineIndex}-${slug}`
98
+ },
99
+ })
100
+
101
+ console.log(document.frontmatter)
102
+ console.log(renderHtml(document))
103
+ ```
104
+
105
+ Frontmatter remains an unparsed string. Heading IDs are generated during
106
+ parsing and are duplicate-safe under the default slugger.
107
+
108
+ ### Add visible heading anchors while rendering
109
+
110
+ ```ts
111
+ import { renderHtml } from '@tanstack/markdown/html'
112
+ import { parseMarkdown } from '@tanstack/markdown/parser'
113
+
114
+ const document = parseMarkdown('# API')
115
+
116
+ const html = renderHtml(document, {
117
+ headingAnchors: {
118
+ content: '#',
119
+ className: 'heading-anchor',
120
+ ariaHidden: true,
121
+ tabIndex: -1,
122
+ },
123
+ })
124
+
125
+ console.log(html)
126
+ ```
127
+
128
+ Heading IDs are a parse concern; visible anchor links are a render concern.
129
+
130
+ ### Parse inline content only when no block context is needed
131
+
132
+ ```ts
133
+ import { parseInline, renderInline } from '@tanstack/markdown'
134
+
135
+ const nodes = parseInline('Use **stable** APIs and `parseMarkdown`.')
136
+ const html = nodes.map(node => renderInline(node)).join('')
137
+
138
+ console.log(html)
139
+ ```
140
+
141
+ `parseInline` handles inline syntax only. Use `parseMarkdown` for headings,
142
+ lists, tables, frontmatter, reference definitions, and footnote definitions.
143
+
144
+ ## Behavioral Contracts
145
+
146
+ - Parsing is synchronous, deterministic, and normalizes line endings.
147
+ - Raw block and inline HTML are recognized only with `allowHtml: true`.
148
+ - Link and image URLs have unsafe executable protocols removed.
149
+ - Tight lists place simple content directly under `<li>`; loose lists retain
150
+ paragraph wrappers; task checkboxes remain inline with labels.
151
+ - Reference definitions resolve case-insensitively before block parsing.
152
+ - Footnotes render in first-reference order with collision-safe IDs and
153
+ repeated-reference back links.
154
+ - Code fence metadata is recorded in the AST; highlighting is external.
155
+ - The AST is public but pre-1.0. Regenerate persisted documents when an
156
+ upgrade changes node contracts.
157
+
158
+ ## Compatibility and Option Timing
159
+
160
+ TanStack Markdown targets controlled blog and documentation content rather
161
+ than complete CommonMark, GFM, MDX, or arbitrary HTML parsing. Before adding
162
+ syntax, require corpus evidence, regression coverage, renderer parity, and an
163
+ accepted bundle cost. See `production-pipelines/SKILL.md` for compatibility,
164
+ security, highlighting, and size gates.
165
+
166
+ Parsing fixes the document structure. Apply `frontmatter`, `headingIds`,
167
+ `allowHtml`, parser state, and parser/transform extensions before caching the
168
+ AST. Renderer-only options such as `headingAnchors`, `highlighter`, and
169
+ `codeLineNumbers` may be applied when rendering an existing document.
170
+
171
+ ## Common Mistakes
172
+
173
+ ### HIGH Assuming complete CommonMark or GFM behavior
174
+
175
+ Wrong:
176
+
177
+ ```ts
178
+ import { renderHtml } from '@tanstack/markdown/html'
179
+
180
+ const markdownFromAnywhere = `Heading
181
+ ===
182
+
183
+ Visit https://example.com`
184
+
185
+ const html = renderHtml(markdownFromAnywhere)
186
+
187
+ console.log(html)
188
+ ```
189
+
190
+ Correct:
191
+
192
+ ```ts
193
+ import { renderHtml } from '@tanstack/markdown/html'
194
+
195
+ const controlledMarkdown = `# Heading
196
+
197
+ Visit [example](https://example.com)`
198
+
199
+ const html = renderHtml(controlledMarkdown)
200
+
201
+ console.log(html)
202
+ ```
203
+
204
+ Setext headings and automatic URL linking are outside the supported syntax
205
+ profile, so unsupported input can remain literal or have different structure.
206
+
207
+ Source: `docs/core-concepts/syntax-profile.md`
208
+
209
+ See also: `production-pipelines/SKILL.md` - compatibility breadth must be
210
+ justified against corpus evidence and the bundle budget.
211
+
212
+ ### MEDIUM Reparsing unchanged content on every render
213
+
214
+ Wrong:
215
+
216
+ ```ts
217
+ import { renderHtml } from '@tanstack/markdown/html'
218
+
219
+ const source = '# Cached article'
220
+
221
+ function renderArticle() {
222
+ return renderHtml(source)
223
+ }
224
+
225
+ console.log(renderArticle())
226
+ console.log(renderArticle())
227
+ ```
228
+
229
+ Correct:
230
+
231
+ ```ts
232
+ import { renderHtml } from '@tanstack/markdown/html'
233
+ import { parseMarkdown } from '@tanstack/markdown/parser'
234
+
235
+ const source = '# Cached article'
236
+ const document = parseMarkdown(source)
237
+
238
+ function renderArticle() {
239
+ return renderHtml(document)
240
+ }
241
+
242
+ console.log(renderArticle())
243
+ console.log(renderArticle())
244
+ ```
245
+
246
+ Complete-document renderers parse string inputs on every call, while a
247
+ `MarkdownDocument` input skips repeated parsing.
248
+
249
+ Source: `docs/core-concepts/document-model.md`
250
+
251
+ See also: `production-pipelines/SKILL.md` - parse-ahead caching must preserve
252
+ the parser option and extension set.
253
+
254
+ ### HIGH Applying parser options after parsing
255
+
256
+ Wrong:
257
+
258
+ ```ts
259
+ import { renderHtml } from '@tanstack/markdown/html'
260
+ import { parseMarkdown } from '@tanstack/markdown/parser'
261
+
262
+ const document = parseMarkdown('# API')
263
+ const html = renderHtml(document, { headingIds: false })
264
+
265
+ console.log(html)
266
+ ```
267
+
268
+ Correct:
269
+
270
+ ```ts
271
+ import { renderHtml } from '@tanstack/markdown/html'
272
+ import { parseMarkdown } from '@tanstack/markdown/parser'
273
+
274
+ const document = parseMarkdown('# API', { headingIds: false })
275
+ const html = renderHtml(document)
276
+
277
+ console.log(html)
278
+ ```
279
+
280
+ Parse-only options do not retroactively alter a pre-parsed AST.
281
+
282
+ Source: `docs/reference/html.md`
283
+
284
+ See also: `custom-extensions/SKILL.md` - parser and transform extensions must
285
+ also be present while creating a cached document.
286
+
287
+ ### MEDIUM Using parseInline for document definitions
288
+
289
+ Wrong:
290
+
291
+ ```ts
292
+ import { parseInline, renderInline } from '@tanstack/markdown'
293
+
294
+ const source = `[Guide][guide]
295
+
296
+ [guide]: /guide`
297
+
298
+ const html = parseInline(source).map(node => renderInline(node)).join('')
299
+
300
+ console.log(html)
301
+ ```
302
+
303
+ Correct:
304
+
305
+ ```ts
306
+ import { renderHtml } from '@tanstack/markdown/html'
307
+ import { parseMarkdown } from '@tanstack/markdown/parser'
308
+
309
+ const source = `[Guide][guide]
310
+
311
+ [guide]: /guide`
312
+
313
+ const document = parseMarkdown(source)
314
+ const html = renderHtml(document)
315
+
316
+ console.log(html)
317
+ ```
318
+
319
+ Standalone inline parsing does not extract document-level reference or
320
+ footnote definitions unless internal parser state is supplied explicitly.
321
+
322
+ Source: `docs/reference/default-entry.md`
323
+
324
+ ## Related Skills
325
+
326
+ - `production-pipelines/SKILL.md` - trust boundaries, syntax highlighting,
327
+ practical compatibility, caching, tests, performance, and bundle budgets.
328
+ - `react-rendering/SKILL.md` - render the same source or AST as React nodes.
329
+ - `octane-rendering/SKILL.md` - render the same source or AST as Octane nodes.
330
+ - `custom-extensions/SKILL.md` - add deterministic parser and renderer hooks.
331
+ - `docs-features/SKILL.md` - use the first-party documentation extensions.
332
+
333
+ ## References
334
+
335
+ - [AST nodes, parser state, and render options](references/ast-and-options.md)