@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.
@@ -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.