@tanstack/markdown 0.0.8 → 0.0.10
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 +15 -4
- package/dist/inline.js +52 -11
- package/dist/parser.js +22 -5
- package/package.json +22 -3
- package/skills/custom-extensions/SKILL.md +444 -0
- package/skills/docs-features/SKILL.md +436 -0
- package/skills/docs-features/references/docs-metadata.md +320 -0
- package/skills/octane-rendering/SKILL.md +351 -0
- package/skills/production-pipelines/SKILL.md +493 -0
- package/skills/react-rendering/SKILL.md +309 -0
- package/skills/render-markdown/SKILL.md +335 -0
- package/skills/render-markdown/references/ast-and-options.md +366 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Docs Metadata Contracts
|
|
2
|
+
|
|
3
|
+
All `ComponentNode.properties` values are strings. Values described as JSON are produced with `JSON.stringify`. HTML rendering escapes them as attribute values; DOM `dataset` access returns the decoded string.
|
|
4
|
+
|
|
5
|
+
## Preset Composition
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
interface DocsMarkdownOptions {
|
|
9
|
+
collectHeadings?: boolean | HeadingCollectionOptions
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface HeadingCollectionOptions {
|
|
13
|
+
skipComponentNames?: ReadonlySet<string> | string[]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`docsMarkdownExtensions()` returns these extensions in order:
|
|
18
|
+
|
|
19
|
+
1. `calloutsExtension()`
|
|
20
|
+
2. `commentComponentsExtension({ transformComponent: transformDocsComponent })`
|
|
21
|
+
3. `headingCollectionExtension()` unless `collectHeadings` is `false`
|
|
22
|
+
|
|
23
|
+
An object passed as `collectHeadings` configures heading collection. `true` or an omitted value uses defaults.
|
|
24
|
+
|
|
25
|
+
## Comment Components
|
|
26
|
+
|
|
27
|
+
Single form:
|
|
28
|
+
|
|
29
|
+
```md
|
|
30
|
+
<!-- ::separator tone="subtle" compact -->
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Block form:
|
|
34
|
+
|
|
35
|
+
```md
|
|
36
|
+
<!-- ::start:example tone="subtle" -->
|
|
37
|
+
|
|
38
|
+
Component body
|
|
39
|
+
|
|
40
|
+
<!-- ::end:example -->
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Names become lowercase. Attribute names preserve their parsed spelling, and every value is a string. Supported values are double-quoted, single-quoted, unquoted, or valueless; a valueless attribute becomes `"true"`.
|
|
44
|
+
|
|
45
|
+
The initial AST shape is:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
interface CommentComponentShape {
|
|
49
|
+
type: 'component'
|
|
50
|
+
name: string
|
|
51
|
+
attributes: Record<string, string>
|
|
52
|
+
children: BlockNode[]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Without a transform-provided `tagName`, HTML uses `md-comment-component`, adds `data-component="<name>"`, and adds `data-attributes="<JSON attributes>"` unless a transform already supplied `data-attributes`.
|
|
57
|
+
|
|
58
|
+
An unmatched block start produces an empty component and consumes only the start line. Its apparent body is parsed normally outside the component.
|
|
59
|
+
|
|
60
|
+
## Callouts
|
|
61
|
+
|
|
62
|
+
Input:
|
|
63
|
+
|
|
64
|
+
```md
|
|
65
|
+
> [!WARNING] Deployment warning
|
|
66
|
+
> Back up the database first.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`KIND` accepts letters only and becomes lowercase. An omitted title becomes title-cased from the kind. The AST contract is:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
interface CalloutNode {
|
|
73
|
+
type: 'callout'
|
|
74
|
+
kind: string
|
|
75
|
+
title: string
|
|
76
|
+
children: BlockNode[]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
HTML uses:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title">Deployment warning</p><div class="markdown-alert-content"><p>Back up the database first.</p></div></div>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Heading Collection
|
|
87
|
+
|
|
88
|
+
The document property is:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
interface MarkdownHeading {
|
|
92
|
+
id: string
|
|
93
|
+
text: string
|
|
94
|
+
level: number
|
|
95
|
+
framework?: string
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Only headings with generated IDs are collected. Traversal includes lists, blockquotes, callouts, and components. By default, any subtree inside a component whose `name` is `tabs` is skipped.
|
|
100
|
+
|
|
101
|
+
Passing `skipComponentNames` replaces the default set; include `'tabs'` explicitly when adding more names:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { docsMarkdownExtensions } from '@tanstack/markdown/extensions/docs'
|
|
105
|
+
|
|
106
|
+
export const extensions = docsMarkdownExtensions({
|
|
107
|
+
collectHeadings: {
|
|
108
|
+
skipComponentNames: ['tabs', 'example'],
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Within an `md-framework-panel`, collected nested headings receive the panel's `data-framework` value.
|
|
114
|
+
|
|
115
|
+
## Tab Metadata
|
|
116
|
+
|
|
117
|
+
### Shared tab descriptors
|
|
118
|
+
|
|
119
|
+
Heading, file, and bundler tabs set:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
interface TabsProperty {
|
|
123
|
+
'data-attributes': string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface TabsMetadata {
|
|
127
|
+
tabs: Array<{
|
|
128
|
+
slug: string
|
|
129
|
+
name: string
|
|
130
|
+
}>
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Each generated panel is:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
interface TabPanelShape {
|
|
138
|
+
type: 'component'
|
|
139
|
+
name: 'tab-panel'
|
|
140
|
+
tagName: 'md-tab-panel'
|
|
141
|
+
attributes: {}
|
|
142
|
+
properties: {
|
|
143
|
+
'data-tab-slug': string
|
|
144
|
+
'data-tab-index': string
|
|
145
|
+
'data-content'?: 'code-only' | 'mixed'
|
|
146
|
+
}
|
|
147
|
+
children: BlockNode[]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`data-tab-index` is a zero-based integer encoded as a string.
|
|
152
|
+
|
|
153
|
+
### Heading tabs
|
|
154
|
+
|
|
155
|
+
Missing or unknown `variant` values dispatch to heading tabs. Sections split at the shallowest heading depth found among direct component children. The section heading is removed from panel children.
|
|
156
|
+
|
|
157
|
+
Slugs use the heading ID when present. Otherwise they are lowercase ASCII slugs with non-alphanumeric characters removed, repeated whitespace converted to `-`, repeated hyphens collapsed, and length capped at 64 characters. The fallback is `tab-<one-based-index>`.
|
|
158
|
+
|
|
159
|
+
No headings means the original component is returned.
|
|
160
|
+
|
|
161
|
+
### File tabs
|
|
162
|
+
|
|
163
|
+
Only direct `code` children become files; other children are discarded when at least one code block exists.
|
|
164
|
+
|
|
165
|
+
Tab descriptors use:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
{
|
|
169
|
+
slug: `file-${zeroBasedIndex}`,
|
|
170
|
+
name: code.title || code.file || 'Untitled',
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The root also sets:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
interface FilesMetadata {
|
|
178
|
+
files: Array<{
|
|
179
|
+
title: string
|
|
180
|
+
code: string
|
|
181
|
+
language: string
|
|
182
|
+
}>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface FileTabsProperties {
|
|
186
|
+
'data-attributes': string
|
|
187
|
+
'data-files-meta': string
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`data-files-meta` is JSON-encoded `FilesMetadata`. `title` falls back through `code.title`, `code.file`, then `"Untitled"`; `language` falls back to `"plaintext"`. Every panel contains its original code node.
|
|
192
|
+
|
|
193
|
+
No direct code children means the original component is returned.
|
|
194
|
+
|
|
195
|
+
### Package-manager tabs
|
|
196
|
+
|
|
197
|
+
Accepted variants are `package-manager` and `package-managers`. Each nonempty source line uses:
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
framework: package-one package-two
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Framework names become lowercase. Repeated framework lines append package arrays rather than merging them.
|
|
204
|
+
|
|
205
|
+
The root sets:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
interface PackageManagerMetadata {
|
|
209
|
+
packagesByFramework: Record<string, string[][]>
|
|
210
|
+
mode: 'install' | 'dev-install' | 'local-install'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface PackageManagerProperties {
|
|
214
|
+
'data-package-manager-meta': string
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`data-package-manager-meta` is JSON-encoded `PackageManagerMetadata`. Only `dev-install` and `local-install` are preserved; an omitted, differently cased, or unknown mode resolves after lowercasing to `install`. Successful transformation replaces all children with an empty array and emits no `md-tab-panel` children.
|
|
219
|
+
|
|
220
|
+
No valid `framework: packages` line means the original component is returned.
|
|
221
|
+
|
|
222
|
+
### Bundler tabs
|
|
223
|
+
|
|
224
|
+
The only supported section names are `vite` and `rsbuild`, matched case-insensitively and emitted in that fixed order when present. Sections split at the shallowest direct heading depth.
|
|
225
|
+
|
|
226
|
+
The root sets:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
interface BundlerMetadata {
|
|
230
|
+
bundlers: Array<'vite' | 'rsbuild'>
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface BundlerTabsProperties {
|
|
234
|
+
'data-attributes': string
|
|
235
|
+
'data-bundler-meta': string
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`data-bundler-meta` is JSON-encoded `BundlerMetadata`. Each panel additionally sets `data-content` to `"code-only"` only when the section has exactly one child and that child is a code block; every other shape uses `"mixed"`.
|
|
240
|
+
|
|
241
|
+
No supported section means the original component is returned.
|
|
242
|
+
|
|
243
|
+
## Framework Metadata
|
|
244
|
+
|
|
245
|
+
Framework transforms split only on direct level-one headings. A valid root sets:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
interface FrameworkMetadata {
|
|
249
|
+
codeBlocksByFramework: Record<
|
|
250
|
+
string,
|
|
251
|
+
Array<{
|
|
252
|
+
title: string
|
|
253
|
+
code: string
|
|
254
|
+
language: string
|
|
255
|
+
}>
|
|
256
|
+
>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface FrameworkProperties {
|
|
260
|
+
'data-available-frameworks': string
|
|
261
|
+
'data-framework-meta': string
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`data-available-frameworks` is a JSON array of lowercase heading text in source order. `data-framework-meta` is JSON-encoded `FrameworkMetadata`. Only direct code children of each framework section enter `codeBlocksByFramework`; title defaults to `""` and language to `"plaintext"`.
|
|
266
|
+
|
|
267
|
+
Each panel is:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
interface FrameworkPanelShape {
|
|
271
|
+
type: 'component'
|
|
272
|
+
name: 'framework-panel'
|
|
273
|
+
tagName: 'md-framework-panel'
|
|
274
|
+
attributes: {}
|
|
275
|
+
properties: {
|
|
276
|
+
'data-framework': string
|
|
277
|
+
}
|
|
278
|
+
children: BlockNode[]
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The level-one selector heading is removed. Descendant headings deeper than level one receive the lowercase framework label, including headings nested through blockquotes, callouts, lists, and components.
|
|
283
|
+
|
|
284
|
+
No direct level-one heading means the original component is returned.
|
|
285
|
+
|
|
286
|
+
## Code Fence Metadata
|
|
287
|
+
|
|
288
|
+
Code metadata is parsed by the core parser, not by `docsMarkdownExtensions`.
|
|
289
|
+
|
|
290
|
+
````md
|
|
291
|
+
```tsx file="app.tsx" framework="React" {2,4-6}
|
|
292
|
+
export function App() {
|
|
293
|
+
return <main>Docs</main>
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
````
|
|
297
|
+
|
|
298
|
+
The code node fields are:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
interface CodeBlockNode {
|
|
302
|
+
type: 'code'
|
|
303
|
+
lang?: string
|
|
304
|
+
meta?: string
|
|
305
|
+
title?: string
|
|
306
|
+
framework?: string
|
|
307
|
+
file?: string
|
|
308
|
+
value: string
|
|
309
|
+
highlightLines?: number[]
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- The first language token accepts letters, digits, `_`, `+`, `.`, `#`, and `-`.
|
|
314
|
+
- `meta` stores the trimmed text after the language.
|
|
315
|
+
- The first `title=` or `file=` value sets both `title` and `file`.
|
|
316
|
+
- `framework=` becomes lowercase.
|
|
317
|
+
- `{2,4-6}` and `lines=2,4-6` produce sorted, unique positive line numbers.
|
|
318
|
+
- Invalid ranges are ignored, and each individual range expands to at most 1,000 lines.
|
|
319
|
+
|
|
320
|
+
HTML code blocks expose `data-lang`, `data-code-title`, `data-filename`, and `data-framework` when the corresponding values exist. Highlight lines are passed to a configured highlighter; metadata alone does not create token or line markup.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 'octane-rendering'
|
|
3
|
+
description: >
|
|
4
|
+
Render Markdown source or a MarkdownDocument with @tanstack/markdown/octane
|
|
5
|
+
using Markdown, renderMarkdownOctane, ComponentBody replacements, TSRX, and
|
|
6
|
+
octane/server static SSR. Load for Octane descriptors, custom emitted tags,
|
|
7
|
+
pre-parsed documents, SSR return values, or renderer parity.
|
|
8
|
+
metadata:
|
|
9
|
+
type: framework
|
|
10
|
+
library: '@tanstack/markdown'
|
|
11
|
+
framework: 'octane'
|
|
12
|
+
library_version: '0.0.10'
|
|
13
|
+
requires:
|
|
14
|
+
- 'render-markdown'
|
|
15
|
+
sources:
|
|
16
|
+
- 'TanStack/markdown:docs/guides/octane.md'
|
|
17
|
+
- 'TanStack/markdown:docs/reference/octane.md'
|
|
18
|
+
- 'TanStack/markdown:docs/guides/extensions.md'
|
|
19
|
+
- 'TanStack/markdown:docs/core-concepts/security.md'
|
|
20
|
+
- 'TanStack/markdown:src/octane.ts'
|
|
21
|
+
- 'TanStack/markdown:tests/ssr-octane.test.ts'
|
|
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
|
+
# Octane Rendering
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
Install the optional Octane peer and render a fragment descriptor:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add @tanstack/markdown octane
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createElement } from 'octane'
|
|
38
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
39
|
+
|
|
40
|
+
export function Article({ source }: { source: string }) {
|
|
41
|
+
return createElement(
|
|
42
|
+
'article',
|
|
43
|
+
null,
|
|
44
|
+
Markdown({
|
|
45
|
+
children: source,
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The adapter requires `octane@0.1.12` or newer. `Markdown` returns an `ElementDescriptor`, not an HTML string.
|
|
52
|
+
|
|
53
|
+
## Hooks and Components
|
|
54
|
+
|
|
55
|
+
### Render Markdown from TSRX
|
|
56
|
+
|
|
57
|
+
```tsrx
|
|
58
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
59
|
+
|
|
60
|
+
export function Article({ source }: { source: string }) @{
|
|
61
|
+
<article>
|
|
62
|
+
<Markdown>{source}</Markdown>
|
|
63
|
+
</article>
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The component returns an Octane fragment descriptor, so the application owns the surrounding semantic element.
|
|
68
|
+
|
|
69
|
+
### Replace emitted links with an Octane component
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createElement, type ComponentBody } from 'octane'
|
|
73
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
74
|
+
|
|
75
|
+
const ArticleLink: ComponentBody<any> = props => {
|
|
76
|
+
const external =
|
|
77
|
+
typeof props.href === 'string' && props.href.startsWith('http')
|
|
78
|
+
|
|
79
|
+
return createElement('a', {
|
|
80
|
+
...props,
|
|
81
|
+
rel: external ? 'noreferrer' : props.rel,
|
|
82
|
+
target: external ? '_blank' : props.target,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function Article({ source }: { source: string }) {
|
|
87
|
+
return Markdown({
|
|
88
|
+
children: source,
|
|
89
|
+
components: { a: ArticleLink },
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`components` accepts host tag strings or Octane `ComponentBody` values keyed by emitted tag name.
|
|
95
|
+
|
|
96
|
+
### Render static markup and styles
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { renderToStaticMarkup } from 'octane/server'
|
|
100
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
101
|
+
|
|
102
|
+
const source = '# Server-rendered article'
|
|
103
|
+
|
|
104
|
+
export const { html, css } = renderToStaticMarkup(Markdown, {
|
|
105
|
+
children: source,
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`octane/server` returns an object containing both `html` and `css`.
|
|
110
|
+
|
|
111
|
+
### Parse once before repeated Octane rendering
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { parseMarkdown } from '@tanstack/markdown/parser'
|
|
115
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
116
|
+
|
|
117
|
+
const document = parseMarkdown('# Cached article\n\nRendered from an AST.')
|
|
118
|
+
|
|
119
|
+
export const article = Markdown({
|
|
120
|
+
children: document,
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Apply parser options and document transforms before caching the document.
|
|
125
|
+
|
|
126
|
+
### Compose through the lower-level node API
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { Fragment, createElement } from 'octane'
|
|
130
|
+
import { parseMarkdown } from '@tanstack/markdown/parser'
|
|
131
|
+
import { renderMarkdownOctane } from '@tanstack/markdown/octane'
|
|
132
|
+
|
|
133
|
+
const document = parseMarkdown('# Parsed once')
|
|
134
|
+
|
|
135
|
+
export function ArticleBody() {
|
|
136
|
+
return createElement(
|
|
137
|
+
Fragment,
|
|
138
|
+
null,
|
|
139
|
+
...renderMarkdownOctane(document),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use `renderBlockOctane` or `renderInlineOctane` only when custom tree composition needs individual public AST nodes.
|
|
145
|
+
|
|
146
|
+
## Common Mistakes
|
|
147
|
+
|
|
148
|
+
### HIGH Treating descriptors as HTML strings
|
|
149
|
+
|
|
150
|
+
Wrong:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
154
|
+
|
|
155
|
+
const source = '# Article'
|
|
156
|
+
|
|
157
|
+
export const html = String(Markdown({ children: source }))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Correct:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { renderToStaticMarkup } from 'octane/server'
|
|
164
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
165
|
+
|
|
166
|
+
const source = '# Article'
|
|
167
|
+
|
|
168
|
+
export const { html } = renderToStaticMarkup(Markdown, {
|
|
169
|
+
children: source,
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`Markdown` returns an `ElementDescriptor`; serialize it through `octane/server` when an HTML string is required.
|
|
174
|
+
|
|
175
|
+
Source: `docs/reference/octane.md`
|
|
176
|
+
|
|
177
|
+
### HIGH Using a React component in the map
|
|
178
|
+
|
|
179
|
+
Wrong:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import type { ComponentProps } from 'react'
|
|
183
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
184
|
+
|
|
185
|
+
function ReactLink(props: ComponentProps<'a'>) {
|
|
186
|
+
return <a {...props} />
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const article = Markdown({
|
|
190
|
+
children: '[Guide](/guide)',
|
|
191
|
+
components: { a: ReactLink },
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Correct:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { createElement, type ComponentBody } from 'octane'
|
|
199
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
200
|
+
|
|
201
|
+
const OctaneLink: ComponentBody<any> = props =>
|
|
202
|
+
createElement('a', {
|
|
203
|
+
...props,
|
|
204
|
+
'data-navigation': 'article',
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
export const article = Markdown({
|
|
208
|
+
children: '[Guide](/guide)',
|
|
209
|
+
components: { a: OctaneLink },
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Octane replacements must be host tag strings or Octane `ComponentBody` values.
|
|
214
|
+
|
|
215
|
+
Source: `docs/guides/octane.md`
|
|
216
|
+
|
|
217
|
+
### MEDIUM Ignoring the static-render return structure
|
|
218
|
+
|
|
219
|
+
Wrong:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { renderToStaticMarkup } from 'octane/server'
|
|
223
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
224
|
+
|
|
225
|
+
export const markup = renderToStaticMarkup(Markdown, {
|
|
226
|
+
children: '# Article',
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Correct:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { renderToStaticMarkup } from 'octane/server'
|
|
234
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
235
|
+
|
|
236
|
+
export const { html, css } = renderToStaticMarkup(Markdown, {
|
|
237
|
+
children: '# Article',
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The static renderer returns `{ html, css }`, not a bare HTML string.
|
|
242
|
+
|
|
243
|
+
Source: `docs/guides/octane.md`
|
|
244
|
+
|
|
245
|
+
### HIGH Expecting HTML extension hooks in Octane
|
|
246
|
+
|
|
247
|
+
Wrong:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import type { MarkdownExtension } from '@tanstack/markdown'
|
|
251
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
252
|
+
|
|
253
|
+
const paragraphHtml: MarkdownExtension = {
|
|
254
|
+
name: 'paragraph-html',
|
|
255
|
+
renderHtml(node) {
|
|
256
|
+
return node.type === 'paragraph'
|
|
257
|
+
? '<aside>Rendered only by the HTML renderer</aside>'
|
|
258
|
+
: undefined
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const article = Markdown({
|
|
263
|
+
children: 'Ordinary content',
|
|
264
|
+
extensions: [paragraphHtml],
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Correct:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import type { MarkdownExtension } from '@tanstack/markdown'
|
|
272
|
+
import { createElement, type ComponentBody } from 'octane'
|
|
273
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
274
|
+
|
|
275
|
+
const panelExtension: MarkdownExtension = {
|
|
276
|
+
name: 'panel',
|
|
277
|
+
transformDocument(document) {
|
|
278
|
+
return {
|
|
279
|
+
...document,
|
|
280
|
+
children: [
|
|
281
|
+
{
|
|
282
|
+
type: 'component',
|
|
283
|
+
name: 'panel',
|
|
284
|
+
tagName: 'doc-panel',
|
|
285
|
+
attributes: {},
|
|
286
|
+
children: document.children,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const Panel: ComponentBody<any> = props =>
|
|
294
|
+
createElement('aside', {
|
|
295
|
+
...props,
|
|
296
|
+
className: 'documentation-panel',
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
export const article = Markdown({
|
|
300
|
+
children: 'Ordinary content',
|
|
301
|
+
components: { 'doc-panel': Panel },
|
|
302
|
+
extensions: [panelExtension],
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`MarkdownExtension.renderHtml` is HTML-specific; portable custom output uses `ComponentNode` and an emitted-tag component mapping.
|
|
307
|
+
|
|
308
|
+
Source: `docs/guides/extensions.md`
|
|
309
|
+
|
|
310
|
+
### CRITICAL Enabling raw HTML for untrusted input
|
|
311
|
+
|
|
312
|
+
Wrong:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
316
|
+
|
|
317
|
+
export function UserPost({ source }: { source: string }) {
|
|
318
|
+
return Markdown({
|
|
319
|
+
allowHtml: true,
|
|
320
|
+
children: source,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Correct:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { Markdown } from '@tanstack/markdown/octane'
|
|
329
|
+
|
|
330
|
+
export function UserPost({ source }: { source: string }) {
|
|
331
|
+
return Markdown({
|
|
332
|
+
children: source,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
With `allowHtml`, Octane receives `dangerouslySetInnerHTML`; TanStack Markdown does not sanitize that raw HTML.
|
|
338
|
+
|
|
339
|
+
Source: `docs/core-concepts/security.md`
|
|
340
|
+
|
|
341
|
+
### HIGH Tension: renderer parity versus Octane customization
|
|
342
|
+
|
|
343
|
+
Core Octane static output is tested against `renderHtml()`. Octane component replacements, raw HTML, highlighter output, and HTML-only extension hooks are application-controlled boundaries outside that parity guarantee.
|
|
344
|
+
|
|
345
|
+
See also: [custom-extensions](../custom-extensions/SKILL.md) for portable `ComponentNode` output.
|
|
346
|
+
|
|
347
|
+
## Cross-References
|
|
348
|
+
|
|
349
|
+
- [render-markdown](../render-markdown/SKILL.md) - shared `MarkdownInput`, parser options, AST behavior, and syntax profile.
|
|
350
|
+
- [custom-extensions](../custom-extensions/SKILL.md) - emit custom tags that Octane can replace through `components`.
|
|
351
|
+
- [production-pipelines](../production-pipelines/SKILL.md) - audit raw HTML, highlighter output, untrusted content, and static SSR boundaries.
|