Skip to main content

custom-extensions

Implement MarkdownExtension block and inline source parsers, inline and document transforms, HTML hooks, and portable block and inline component output. Load when adding deterministic custom syntax or rendering behavior across HTML, React, and Octane.

소스 정보

저장소
TanStack/markdown
최근 소스 활동
2026년 10월 1일 02:00
감지된 SKILL.md 언어
영어
스타
428
포크
10

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
custom-extensions
description
Implement MarkdownExtension block and inline source parsers, inline and document transforms, HTML hooks, and portable block and inline component output. Load when adding deterministic custom syntax or rendering behavior across HTML, React, and Octane.
metadata
{"type":"core","library":"@tanstack/markdown","library_version":"1.0.0"}
requires
["render-markdown"]
sources
["TanStack/markdown:docs/guides/extensions.md","TanStack/markdown:docs/reference/extensions.md","TanStack/markdown:src/types.ts","TanStack/markdown:src/parser.ts","TanStack/markdown:src/extensions/callouts.ts","TanStack/markdown:src/extensions/comment-components.ts"]
This skill builds on `render-markdown`. Read it first for parser options, the document AST, and renderer behavior. # Custom Extensions ## Setup Implement a bounded block parser and return a standard `ComponentNode`: ```ts import type { MarkdownExtension } from '@tanstack/markdown' import { renderHtml } from '@tanstack/markdown/html' import { parseMarkdown } from '@tanstack/markdown/parser' function notesExtension(): MarkdownExtension { return { name: 'notes', parseBlock(context) { const first = context.lines[context.index] ?? '' const opening = first.match(/^:::note(?:\s+(.*))?$/) if (!opening) return undefined const body: string[] = [] let cursor = context.index + 1 while (cursor < context.lines.length && context.lines[cursor] !== ':::') { body.push(context.lines[cursor] ?? '') cursor++ } if (context.lines[cursor] !== ':::') return undefined const title = opening[1]?.trim() || 'Note' context.consume(cursor - context.index + 1) return { type: 'component', name: 'note', tagName: 'docs-note', attributes: { title }, properties: { 'data-title': title }, children: context.parseBlocks(body.join('\n')), } }, } } const source = `:::note Cache the AST Parse once and render many times. :::` const extensions = [notesExtension()] const document = parseMarkdown(source, { extensions }) const html = renderHtml(document, { extensions }) console.log(html) ``` `parseBlock` runs before built-in block parsing, and nested `parseBlocks` shares the parent depth budget and heading slugger. ## Core Patterns ### Recognize inline source before formatting Use `inlineParser: { markers, parse(context) }` for syntax that needs original source characters. `markers` is a string of literal first characters. The context supplies `source`, UTF-16 `index`, `options`, `inLink`, and `parseInline(value)` sharing the current depth and scan budget. Return `{ node, length }` with one standard `InlineNode` and a positive in-bounds integer length, or `undefined` to decline. Invalid lengths throw `RangeError`. Escapes and code spans take precedence. Hooks do not run in code, image alt text, or link destinations; they do run in emphasis and link labels. Respect `inLink` when creating automatic links. Indices are local to the current inline container, and ranges cannot cross enclosing inline or block boundaries. Hook dispatch is budgeted, but callback work is trusted and must avoid repeated suffix scans. Returned AST nodes are trusted; validate URLs and component metadata yourself. Use `transformInline` for changes to already-parsed nodes. It cannot distinguish an escaped opener from an ordinary text opener or restore raw Markdown spelling. See the extension guide for a complete source-parser example. ### Transform parsed inline nodes ```ts import type { InlineNode, MarkdownExtension, StrongNode, } from '@tanstack/markdown' import { renderHtml } from '@tanstack/markdown/html' const importantExtension: MarkdownExtension = { name: 'important-inline', transformInline(nodes) { return nodes.map((node): InlineNode => { if (node.type !== 'text' || !node.value.startsWith('IMPORTANT: ')) { return node } const strong: StrongNode = { type: 'strong', children: [{ type: 'text', value: node.value }], } return strong }) }, } const html = renderHtml('IMPORTANT: Back up the database.', { extensions: [importantExtension], }) console.log(html) ``` Transforms receive both built-in inline nodes and extension nodes returned by `inlineParser`, and must return a deterministic replacement array. For custom inline UI, return an `InlineComponentNode` with `type: 'inlineComponent'`, `name`, `attributes`, inline `children`, and optional `tagName` and string `properties`. It uses the same emitted-tag component maps as a block `ComponentNode`, but defaults to `<span>` instead of `<md-comment-component>`. Keep tag and property names under extension control, use phrasing content, and recurse through inline `children` when transforming text nested inside links or emphasis. This does not require `allowHtml`. ### Derive document metadata after parsing ```ts import type { InlineNode, MarkdownExtension, MarkdownHeading, } from '@tanstack/markdown' import { parseMarkdown } from '@tanstack/markdown/parser' function inlineText(nodes: InlineNode[]): string { return nodes .map((node) => { if (node.type === 'text' || node.type === 'inlineCode') return node.value if (node.type === 'image') return node.alt if ('children' in node) return inlineText(node.children) return '' }) .join('') } const topLevelHeadings: MarkdownExtension = { name: 'top-level-headings', transformDocument(document) { const headings: MarkdownHeading[] = document.children.flatMap((node) => node.type === 'heading' && node.id ? [{ id: node.id, text: inlineText(node.children), level: node.depth, }] : [], ) return { ...document, headings } }, } const document = parseMarkdown('# Install\n\n## Configure', { extensions: [topLevelHeadings], }) console.log(document.headings) ``` Document transforms run after blocks and footnotes are complete and may return a new document or mutate the existing one. ### Use an HTML hook only for HTML output ```ts import type { MarkdownExtension } from '@tanstack/markdown' import { calloutsExtension } from '@tanstack/markdown/extensions/callouts' import { renderHtml } from '@tanstack/markdown/html' const compactCalloutHtml: MarkdownExtension = { name: 'compact-callout-html', renderHtml(node, context) { if (node.type !== 'callout') return undefined const children = node.children.map(context.renderBlock).join('\n') return `<aside class="compact-callout">${children}</aside>` }, } const extensions = [calloutsExtension(), compactCalloutHtml] const html = renderHtml('> [!NOTE]\n> Cached.', { extensions }) console.log(html) ``` The returned string is trusted and HTML-specific; nested standard nodes remain escaped because they use `context.renderBlock`. ### Emit portable custom elements ```ts import { commentComponentsExtension } from '@tanstack/markdown/extensions/comment-components' import { renderHtml } from '@tanstack/markdown/html' const panels = commentComponentsExtension({ transformComponent(node) { if (node.name !== 'panel') return node return { ...node, tagName: 'docs-panel', properties: { 'data-kind': node.attributes.kind ?? 'note', }, } }, }) const source = `<!-- ::start:panel kind="warning" --> Check the migration before deploying. <!-- ::end:panel -->` const html = renderHtml(source, { extensions: [panels] }) console.log(html) ``` React and Octane can replace the emitted `docs-panel` tag through their `components` maps. ## Common Mistakes ### HIGH Claiming a block without consuming it Wrong: ```ts import type { MarkdownExtension } from '@tanstack/markdown' import { renderHtml } from '@tanstack/markdown/html' const brokenNotes: MarkdownExtension = { name: 'broken-notes', parseBlock(context) { if (context.lines[context.index] !== ':::note') return undefined return { type: 'paragraph', children: context.parseInline(context.lines[context.index + 1] ?? ''), } }, } console.log(renderHtml(':::note\nCached.\n:::', { extensions: [brokenNotes], })) ``` Correct: ```ts import type { MarkdownExtension } from '@tanstack/markdown' import { renderHtml } from '@tanstack/markdown/html' const notes: MarkdownExtension = { name: 'notes', parseBlock(context) { if (context.lines[context.index] !== ':::note') return undefined const closing = context.lines.indexOf(':::', context.index + 1) if (closing === -1) return undefined const body = context.lines.slice(context.index + 1, closing).join('\n') context.consume(closing - context.index + 1) return { type: 'component', name: 'note', attributes: {}, children: context.parseBlocks(body), } }, } console.log(renderHtml(':::note\nCached.\n:::', { extensions: [notes], })) ``` Returning a node advances only one line unless `consume` records the complete owned block. Source: `docs/guides/extensions.md` ### HIGH Ordering a general parser first Wrong: ```ts import type { MarkdownExtension } from '@tanstack/markdown' import { calloutsExtension } from '@tanstack/markdown/extensions/callouts' import { renderHtml } from '@tanstack/markdown/html' const quotedLine: MarkdownExtension = { name: 'quoted-line', parseBlock(context) { const match = (context.lines[context.index] ?? '').match(/^>\s?(.*)$/) if (!match) return undefined context.consume(1) return { type: 'paragraph', children: context.parseInline(match[1] ?? '') } }, } console.log(renderHtml('> [!TIP]\n> Cache it.', { extensions: [quotedLine, calloutsExtension()], })) ``` Correct: ```ts import type { MarkdownExtension } from '@tanstack/markdown' import { calloutsExtension } from '@tanstack/markdown/extensions/callouts' import { renderHtml } from '@tanstack/markdown/html' const quotedLine: MarkdownExtension = { name: 'quoted-line', parseBlock(context) { const match = (context.lines[context.index] ?? '').match(/^>\s?(.*)$/) if (!match) return undefined context.consume(1) return { type: 'paragraph', children: context.parseInline(match[1] ?? '') } }, } console.log(renderHtml('> [!TIP]\n> Cache it.', { extensions: [calloutsExtension(), quotedLine], })) ``` Extensions run in array order, so the broad quote parser can hide callout syntax from the specific parser. Source: `docs/guides/extensions.md` ### HIGH Using HTML hooks for framework nodes Wrong: ```tsx import type { MarkdownExtension } from '@tanstack/markdown' import { Markdown } from '@tanstack/markdown/react' const htmlOnly: MarkdownExtension = { name: 'html-only', renderHtml(node, context) { if (node.type !== 'paragraph') return undefined return `<aside>${node.children.map(context.renderInline).join('')}</aside>` }, } export function Article() { return <Markdown extensions={[htmlOnly]}>Framework output</Markdown> } ``` Correct: ```tsx import { commentComponentsExtension } from '@tanstack/markdown/extensions/comment-components' import { Markdown } from '@tanstack/markdown/react' import type { ComponentProps } from 'react' const components = commentComponentsExtension({ transformComponent(node) { return node.name === 'panel' ? { ...node, tagName: 'docs-panel' } : node }, }) function Panel(props: ComponentProps<'aside'>) { return <aside {...props} /> } export function Article() { return ( <Markdown extensions={[components]} components={{ 'docs-panel': Panel }} > {'<!-- ::start:panel -->\nPortable output\n<!-- ::end:panel -->'} </Markdown> ) } ``` `renderHtml` hooks do not run in React or Octane; a `ComponentNode` or `InlineComponentNode` and emitted-tag component mapping is the portable path. Source: `docs/guides/extensions.md` ### MEDIUM Changing extensions after parsing Wrong: ```ts import type { MarkdownExtension } from '@tanstack/markdown' import { renderHtml } from '@tanstack/markdown/html' import { parseMarkdown } from '@tanstack/markdown/parser' const emphasisHtml: MarkdownExtension = { name: 'emphasis-html', renderHtml(node, context) { if (node.type !== 'emphasis') return undefined return `<i class="accent">${node.children.map(context.renderInline).join('')}</i>` }, }
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기