@tanstack/markdown 0.0.7 → 0.0.9

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 CHANGED
@@ -1,8 +1,15 @@
1
1
  # TanStack Markdown
2
2
 
3
- A tiny, fast, deterministic Markdown renderer for blogs and documentation.
3
+ A tiny, fast, deterministic Markdown parser and renderer for blogs and documentation.
4
4
 
5
- The current browser bundles are 4.6 KB gzip for the parser, 6.4 KB for HTML rendering, and 6.3 KB for the React adapter. The package has no runtime dependencies and does not bundle syntax highlighting.
5
+ - 4.6 KB gzip parser
6
+ - 6.4 KB gzip HTML renderer
7
+ - 6.3 KB gzip React adapter
8
+ - 6.3 KB gzip Octane adapter
9
+ - zero runtime dependencies
10
+ - serializable AST
11
+ - safe defaults for raw HTML and executable URLs
12
+ - optional docs extensions and external syntax highlighting
6
13
 
7
14
  ```bash
8
15
  pnpm add @tanstack/markdown
@@ -22,13 +29,29 @@ export function Article({ source }: { source: string }) {
22
29
  }
23
30
  ```
24
31
 
25
- Raw HTML is escaped and executable link protocols are removed by default. `allowHtml` and highlighter output are explicit trusted-content boundaries.
32
+ ```tsrx
33
+ import { Markdown } from '@tanstack/markdown/octane'
26
34
 
27
- ## Scope
35
+ export function Article({ source }: { source: string }) @{
36
+ <Markdown>{source}</Markdown>
37
+ }
38
+ ```
39
+
40
+ TanStack Markdown targets controlled technical content. It supports the Markdown used by blogs and docs, then spends its remaining complexity budget on deterministic output, renderer parity, malformed-input resilience, and small entry points. It is intentionally not a complete CommonMark, GFM, MDX, or general content-processing implementation.
28
41
 
29
- This is deliberately not a complete CommonMark, GFM, MDX, or content-processing implementation. It implements the syntax used by technical blogs and documentation, then spends its complexity budget on deterministic output, safe defaults, React/HTML parity, malformed-input resilience, and small entry points.
42
+ ## Documentation
30
43
 
31
- See the [docs Markdown profile](./docs/profile.md) for the supported contract and deliberate non-goals.
44
+ - [Overview](./docs/overview.md)
45
+ - [Installation](./docs/installation.md)
46
+ - [Quick Start](./docs/quick-start.md)
47
+ - [Comparison](./docs/comparison.md)
48
+ - [Syntax Profile](./docs/core-concepts/syntax-profile.md)
49
+ - [React Guide](./docs/guides/react.md)
50
+ - [Octane Guide](./docs/guides/octane.md)
51
+ - [Syntax Highlighting](./docs/guides/syntax-highlighting.md)
52
+ - [Extensions](./docs/guides/extensions.md)
53
+ - [API Reference](./docs/reference/index.md)
54
+ - [Architecture](./docs/project/architecture.md)
32
55
 
33
56
  ## Verification
34
57
 
@@ -36,17 +59,15 @@ See the [docs Markdown profile](./docs/profile.md) for the supported contract an
36
59
  pnpm run verify
37
60
  ```
38
61
 
39
- To include downstream repositories in the corpus gate:
62
+ To include downstream content in the corpus gate:
40
63
 
41
64
  ```bash
42
- MARKDOWN_CORPUS_DIRS=../tanstack.com/src/blog:../tanstack.com/docs pnpm run test:corpus
65
+ MARKDOWN_CORPUS_DIRS=../tanstack.com/src/blog:../tanstack.com/docs \
66
+ pnpm run test:corpus
43
67
  ```
44
68
 
45
- ## Reports
69
+ Generated reports:
46
70
 
47
71
  - [Bundle sizes](./reports/sizes.md)
48
72
  - [Benchmarks](./reports/benchmarks.md)
49
73
  - [CommonMark compatibility accounting](./reports/conformance.md)
50
- - [Audit](./docs/audit.md)
51
- - [Spec and API](./docs/spec.md)
52
- - [Extensions](./docs/extensions.md)
@@ -0,0 +1,15 @@
1
+ import type { ComponentBody, ElementDescriptor, OctaneNode } from 'octane';
2
+ import type { BlockNode, InlineNode, MarkdownInput, RenderOptions } from './types.js';
3
+ type ComponentMap = Partial<Record<string, string | ComponentBody<any>>>;
4
+ export interface MarkdownOctaneOptions extends RenderOptions {
5
+ components?: ComponentMap;
6
+ }
7
+ export interface MarkdownProps extends MarkdownOctaneOptions {
8
+ children: MarkdownInput;
9
+ }
10
+ export declare function Markdown({ children, ...options }: MarkdownProps): ElementDescriptor;
11
+ export declare function renderMarkdownOctane(input: MarkdownInput, options?: MarkdownOctaneOptions): OctaneNode[];
12
+ export declare function renderBlockOctane(node: BlockNode, options?: MarkdownOctaneOptions, key?: string): ElementDescriptor;
13
+ export declare function renderInlineOctane(node: InlineNode, options?: MarkdownOctaneOptions, key?: string): OctaneNode;
14
+ export {};
15
+ //# sourceMappingURL=octane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"octane.d.ts","sourceRoot":"","sources":["../src/octane.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAE1E,OAAO,KAAK,EAAE,SAAS,EAAmC,UAAU,EAAE,aAAa,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAA;AAErI,KAAK,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAExE,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,UAAU,CAAC,EAAE,YAAY,CAAA;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,qBAAqB;IAC1D,QAAQ,EAAE,aAAa,CAAA;CACxB;AAED,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,EAAE,aAAa,GAAG,iBAAiB,CAEnF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,qBAA0B,GAAG,UAAU,EAAE,CAG5G;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,qBAA0B,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA+DvH;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,qBAA0B,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,UAAU,CAwClH"}
package/dist/octane.js ADDED
@@ -0,0 +1,189 @@
1
+ import { Fragment, createElement } from 'octane';
2
+ import { parseMarkdown } from './parser.js';
3
+ export function Markdown({ children, ...options }) {
4
+ return createElement(Fragment, null, ...renderMarkdownOctane(children, options));
5
+ }
6
+ export function renderMarkdownOctane(input, options = {}) {
7
+ const document = typeof input === 'string' ? parseMarkdown(input, options) : input;
8
+ return document.children.map((node, index) => renderBlockOctane(node, options, `b:${index}`));
9
+ }
10
+ export function renderBlockOctane(node, options = {}, key) {
11
+ switch (node.type) {
12
+ case 'heading':
13
+ return h(options, `h${node.depth}`, { key, ...(node.id ? { id: node.id } : {}), ...(node.framework ? { 'data-framework': node.framework } : {}) }, renderInlines(node.children, options), renderHeadingAnchorOctane(node.id, options));
14
+ case 'paragraph':
15
+ return h(options, 'p', { key }, renderInlines(node.children, options));
16
+ case 'code':
17
+ return renderCodeBlockOctane(node, options, key);
18
+ case 'list': {
19
+ const tag = node.ordered ? 'ol' : 'ul';
20
+ return h(options, tag, { key, ...(node.ordered && node.start && node.start !== 1 ? { start: node.start } : {}) }, node.items.map((item, index) => h(options, 'li', { key: index }, renderListItemChildrenOctane(item.children, item.checked, node.loose, options, `${index}`))));
21
+ }
22
+ case 'blockquote':
23
+ return h(options, 'blockquote', { key }, node.children.map((child, index) => renderBlockOctane(child, options, `${key}:${index}`)));
24
+ case 'table':
25
+ return h(options, 'table', { key }, h(options, 'thead', null, h(options, 'tr', null, node.header.map((cell, index) => renderTableCellOctane('th', cell, node.align[index], options, index)))), node.rows.length
26
+ ? h(options, 'tbody', null, node.rows.map((row, rowIndex) => h(options, 'tr', { key: rowIndex }, row.map((cell, index) => renderTableCellOctane('td', cell, node.align[index], options, index)))))
27
+ : null);
28
+ case 'footnotes':
29
+ return renderFootnotesOctane(node.items, options, key);
30
+ case 'thematicBreak':
31
+ return h(options, 'hr', { key });
32
+ case 'html':
33
+ return options.allowHtml
34
+ ? h(options, 'div', { key, dangerouslySetInnerHTML: { __html: node.value } })
35
+ : h(options, 'p', { key }, node.value);
36
+ case 'callout':
37
+ return h(options, 'div', { key, className: `markdown-alert markdown-alert-${node.kind.toLowerCase()}` }, h(options, 'p', { className: 'markdown-alert-title' }, node.title), h(options, 'div', { className: 'markdown-alert-content' }, node.children.map((child, index) => renderBlockOctane(child, options, `${key}:${index}`))));
38
+ case 'component':
39
+ return renderComponentOctane(node, options, key);
40
+ }
41
+ }
42
+ export function renderInlineOctane(node, options = {}, key) {
43
+ switch (node.type) {
44
+ case 'text':
45
+ return node.value;
46
+ case 'inlineCode':
47
+ return h(options, 'code', { key }, node.value);
48
+ case 'strong':
49
+ return h(options, 'strong', { key }, renderInlines(node.children, options));
50
+ case 'emphasis':
51
+ return h(options, 'em', { key }, renderInlines(node.children, options));
52
+ case 'strike':
53
+ return h(options, 'del', { key }, renderInlines(node.children, options));
54
+ case 'footnoteReference':
55
+ return h(options, 'sup', { key }, h(options, 'a', {
56
+ id: `user-content-fnref-${footnoteReferenceId(node)}`,
57
+ 'data-footnote-ref': '',
58
+ 'aria-describedby': 'footnote-label',
59
+ href: `#user-content-fn-${node.id}`,
60
+ }, node.number));
61
+ case 'link':
62
+ return h(options, 'a', { key, href: node.href, ...(node.title ? { title: node.title } : {}) }, renderInlines(node.children, options));
63
+ case 'image':
64
+ return h(options, 'img', { key, src: node.src, alt: node.alt, ...(node.title ? { title: node.title } : {}) });
65
+ case 'break':
66
+ return h(options, 'br', { key });
67
+ case 'inlineHtml':
68
+ return options.allowHtml
69
+ ? h(options, 'span', { key, dangerouslySetInnerHTML: { __html: node.value } })
70
+ : node.value;
71
+ }
72
+ }
73
+ function renderInlines(nodes, options) {
74
+ return nodes.map((node, index) => renderInlineOctane(node, options, `i:${index}`));
75
+ }
76
+ function renderCodeBlockOctane(node, options, key) {
77
+ const lang = node.lang ?? 'plaintext';
78
+ const highlighter = options.highlighter;
79
+ const content = highlighter ? undefined : node.value;
80
+ const highlighted = highlighter
81
+ ? {
82
+ dangerouslySetInnerHTML: {
83
+ __html: highlighter(node.value, lang, {
84
+ ...(node.highlightLines && { highlightLines: node.highlightLines }),
85
+ ...(options.codeLineNumbers !== undefined && { lineNumbers: options.codeLineNumbers }),
86
+ }),
87
+ },
88
+ }
89
+ : undefined;
90
+ const pre = h(options, 'pre', {
91
+ className: 'tm-code',
92
+ 'data-lang': lang,
93
+ ...(node.title ? { 'data-code-title': node.title } : {}),
94
+ ...(node.file ? { 'data-filename': node.file } : {}),
95
+ ...(node.framework ? { 'data-framework': node.framework } : {}),
96
+ }, h(options, 'code', { className: `language-${lang}`, ...highlighted }, content));
97
+ if (!node.title)
98
+ return h(options, Fragment, { key }, pre);
99
+ return h(options, 'figure', { key, className: 'tm-code-frame', 'data-lang': lang }, h(options, 'figcaption', null, node.title), pre);
100
+ }
101
+ function renderListItemChildrenOctane(children, checked, loose, options, key) {
102
+ const [first, ...rest] = children;
103
+ const task = checked === undefined
104
+ ? []
105
+ : [
106
+ h(options, 'input', {
107
+ key: `${key}:checkbox`,
108
+ type: 'checkbox',
109
+ disabled: true,
110
+ checked,
111
+ readOnly: true,
112
+ }),
113
+ ' ',
114
+ ];
115
+ if (first?.type === 'paragraph') {
116
+ const content = [...task, ...renderInlines(first.children, options)];
117
+ return [
118
+ ...(loose ? [h(options, 'p', { key: `${key}:paragraph` }, content)] : content),
119
+ ...rest.flatMap((child, childIndex) => renderListChildOctane(child, loose, options, `${key}:${childIndex + 1}`)),
120
+ ];
121
+ }
122
+ return [...task, ...children.flatMap((child, childIndex) => renderListChildOctane(child, loose, options, `${key}:${childIndex}`))];
123
+ }
124
+ function renderListChildOctane(child, loose, options, key) {
125
+ return !loose && child.type === 'paragraph' ? renderInlines(child.children, options) : [renderBlockOctane(child, options, key)];
126
+ }
127
+ function renderTableCellOctane(tag, cell, align, options, key) {
128
+ return h(options, tag, { key, ...(align ? { style: { textAlign: align } } : {}) }, renderInlines(cell.children, options));
129
+ }
130
+ function renderFootnotesOctane(items, options, key) {
131
+ return h(options, 'section', { key, 'data-footnotes': '', className: 'footnotes' }, h(options, 'h2', { id: 'footnote-label', className: 'sr-only' }, 'Footnotes', renderHeadingAnchorOctane('footnote-label', options)), h(options, 'ol', null, items.map(item => h(options, 'li', { key: item.id, id: `user-content-fn-${item.id}` }, renderFootnoteItemOctane(item, options)))));
132
+ }
133
+ function renderFootnoteItemOctane(item, options) {
134
+ const lastIndex = item.children.length - 1;
135
+ const backrefs = renderFootnoteBackrefsOctane(item, options);
136
+ if (lastIndex < 0)
137
+ return [h(options, 'p', { key: 'backref-wrapper' }, backrefs.slice(1))];
138
+ return item.children.map((child, index) => {
139
+ if (index === lastIndex && child.type === 'paragraph') {
140
+ return h(options, 'p', { key: index }, renderInlines(child.children, options), backrefs);
141
+ }
142
+ return renderBlockOctane(child, options, `${index}`);
143
+ });
144
+ }
145
+ function renderFootnoteBackrefsOctane(item, options) {
146
+ const result = [];
147
+ for (let index = 1; index <= (item.referenceCount ?? 1); index++) {
148
+ const referenceId = index === 1 ? item.id : `${item.id}-${index}`;
149
+ const label = index === 1 ? `${item.number}` : `${item.number}-${index}`;
150
+ result.push(' ', h(options, 'a', {
151
+ key: index,
152
+ 'data-footnote-backref': '',
153
+ 'aria-label': `Back to reference ${label}`,
154
+ className: 'data-footnote-backref',
155
+ href: `#user-content-fnref-${referenceId}`,
156
+ }, '\u21a9'));
157
+ }
158
+ return result;
159
+ }
160
+ function footnoteReferenceId(node) {
161
+ return node.referenceIndex && node.referenceIndex > 1 ? `${node.id}-${node.referenceIndex}` : node.id;
162
+ }
163
+ function h(options, tag, props, ...children) {
164
+ const component = typeof tag === 'string' ? options.components?.[tag] ?? tag : tag;
165
+ return createElement(component, props ?? undefined, ...children);
166
+ }
167
+ function renderComponentOctane(node, options, key) {
168
+ const tag = node.tagName ?? 'md-comment-component';
169
+ const props = {
170
+ ...(node.properties ?? {}),
171
+ };
172
+ if (!node.tagName) {
173
+ props['data-component'] = node.name;
174
+ if (!props['data-attributes'])
175
+ props['data-attributes'] = JSON.stringify(node.attributes);
176
+ }
177
+ return h(options, tag, { key, ...props }, node.children.map((child, index) => renderBlockOctane(child, options, `${key}:${index}`)));
178
+ }
179
+ function renderHeadingAnchorOctane(id, options) {
180
+ if (!id || !options.headingAnchors)
181
+ return null;
182
+ const anchorOptions = typeof options.headingAnchors === 'object' ? options.headingAnchors : {};
183
+ return h(options, 'a', {
184
+ href: `#${id}`,
185
+ 'aria-hidden': anchorOptions.ariaHidden ?? true,
186
+ className: anchorOptions.className ?? 'anchor-heading anchor-heading-link',
187
+ tabIndex: anchorOptions.tabIndex ?? -1,
188
+ }, anchorOptions.content ?? '#');
189
+ }
package/dist/parser.js CHANGED
@@ -368,9 +368,13 @@ function extractDefinitions(lines) {
368
368
  }
369
369
  continue;
370
370
  }
371
- const definition = line.match(/^ {0,3}\[([^\]\n]+)\]:[ \t]*(\S+)[ \t]*$/);
371
+ const definition = line.match(/^ {0,3}\[([^\]\n]+)\]:[ \t]*(\S+)(?:[ \t]+(?:"([^"]*)"|'([^']*)'|\(([^)]*)\)))?[ \t]*$/);
372
372
  if (definition) {
373
- references[normalizeReferenceLabel(definition[1])] = { href: definition[2].replace(/^<|>$/g, '') };
373
+ const title = definition[3] ?? definition[4] ?? definition[5];
374
+ references[normalizeReferenceLabel(definition[1])] = {
375
+ href: definition[2].replace(/^<|>$/g, ''),
376
+ ...(title !== undefined ? { title } : {}),
377
+ };
374
378
  index++;
375
379
  continue;
376
380
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/markdown",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "A tiny, fast, deterministic Markdown renderer for blogs and documentation.",
6
6
  "license": "MIT",
@@ -27,6 +27,10 @@
27
27
  "types": "./dist/react.d.ts",
28
28
  "import": "./dist/react.js"
29
29
  },
30
+ "./octane": {
31
+ "types": "./dist/octane.d.ts",
32
+ "import": "./dist/octane.js"
33
+ },
30
34
  "./extensions/callouts": {
31
35
  "types": "./dist/extensions/callouts.d.ts",
32
36
  "import": "./dist/extensions/callouts.js"
@@ -56,9 +60,13 @@
56
60
  "access": "public"
57
61
  },
58
62
  "peerDependencies": {
63
+ "octane": ">=0.1.12",
59
64
  "react": ">=18"
60
65
  },
61
66
  "peerDependenciesMeta": {
67
+ "octane": {
68
+ "optional": true
69
+ },
62
70
  "react": {
63
71
  "optional": true
64
72
  }
@@ -74,6 +82,7 @@
74
82
  "markdown-wasm": "^1.2.0",
75
83
  "marked": "^18.0.5",
76
84
  "micromark": "^4.0.2",
85
+ "octane": "0.1.12",
77
86
  "react": "^19.0.0",
78
87
  "react-dom": "^19.0.0",
79
88
  "rehype-stringify": "^10.0.1",
@@ -89,10 +98,11 @@
89
98
  "typecheck": "tsc --noEmit",
90
99
  "test": "vitest run",
91
100
  "test:corpus": "vitest run tests/corpus.test.tsx",
101
+ "docs:verify": "node scripts/verify-docs.mjs",
92
102
  "conformance": "tsx scripts/conformance.ts",
93
103
  "bench": "tsx scripts/bench.ts",
94
104
  "size": "tsx scripts/measure-size.ts",
95
105
  "research": "pnpm run conformance && pnpm run size && pnpm run bench",
96
- "verify": "pnpm test && pnpm run typecheck && pnpm run build && pnpm run research && pnpm pack --dry-run"
106
+ "verify": "pnpm test && pnpm run typecheck && pnpm run build && pnpm run docs:verify && pnpm run research && pnpm pack --dry-run"
97
107
  }
98
108
  }