@tanstack/markdown 0.0.6 → 0.0.7

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,36 +1,52 @@
1
1
  # TanStack Markdown
2
2
 
3
- A tiny deterministic TanStack-flavored Markdown renderer.
3
+ A tiny, fast, deterministic Markdown renderer for blogs and documentation.
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.
4
6
 
5
7
  ```bash
6
8
  pnpm add @tanstack/markdown
7
9
  ```
8
10
 
9
- ## Commands
11
+ ```ts
12
+ import { renderHtml } from '@tanstack/markdown/html'
13
+
14
+ const html = renderHtml('# Fast by default')
15
+ ```
16
+
17
+ ```tsx
18
+ import { Markdown } from '@tanstack/markdown/react'
19
+
20
+ export function Article({ source }: { source: string }) {
21
+ return <Markdown>{source}</Markdown>
22
+ }
23
+ ```
24
+
25
+ Raw HTML is escaped and executable link protocols are removed by default. `allowHtml` and highlighter output are explicit trusted-content boundaries.
26
+
27
+ ## Scope
28
+
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.
30
+
31
+ See the [docs Markdown profile](./docs/profile.md) for the supported contract and deliberate non-goals.
32
+
33
+ ## Verification
10
34
 
11
35
  ```bash
12
- pnpm test
13
- pnpm run typecheck
14
- pnpm run build
15
- pnpm run research
36
+ pnpm run verify
16
37
  ```
17
38
 
18
- ## Entry Points
39
+ To include downstream repositories in the corpus gate:
19
40
 
20
- - `src/parser.ts`: Markdown subset parser to a serializable AST
21
- - `src/html.ts`: HTML string renderer
22
- - `src/react.ts`: React adapter
23
- - `src/extensions/*`: optional docs-site extension functions
24
- - `scripts/measure-size.ts`: bundle size matrix
25
- - `scripts/bench.ts`: CPU benchmark matrix
41
+ ```bash
42
+ MARKDOWN_CORPUS_DIRS=../tanstack.com/src/blog:../tanstack.com/docs pnpm run test:corpus
43
+ ```
26
44
 
27
45
  ## Reports
28
46
 
29
47
  - [Bundle sizes](./reports/sizes.md)
30
48
  - [Benchmarks](./reports/benchmarks.md)
49
+ - [CommonMark compatibility accounting](./reports/conformance.md)
31
50
  - [Audit](./docs/audit.md)
32
51
  - [Spec and API](./docs/spec.md)
33
- - [Goal and coverage map](./docs/goal.md)
34
- - [Bundle strategy](./docs/bundle-strategy.md)
35
52
  - [Extensions](./docs/extensions.md)
36
- - [Recommendation](./docs/recommendation.md)
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyD,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAA;AAG7K,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAGpF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAwChF;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CA0BlF;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAE9F"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyD,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAA;AAG7K,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAGpF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CA0ChF;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CA0BlF;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAE9F"}
package/dist/html.js CHANGED
@@ -28,9 +28,11 @@ export function renderBlock(node, options = {}) {
28
28
  return `<blockquote>\n${node.children.map(child => renderBlock(child, options)).join('\n')}\n</blockquote>`;
29
29
  case 'table': {
30
30
  const header = `<thead><tr>${node.header.map((cell, index) => renderTableCell('th', cell, node.align[index], options)).join('')}</tr></thead>`;
31
- const body = `<tbody>${node.rows
32
- .map(row => `<tr>${row.map((cell, index) => renderTableCell('td', cell, node.align[index], options)).join('')}</tr>`)
33
- .join('')}</tbody>`;
31
+ const body = node.rows.length
32
+ ? `<tbody>${node.rows
33
+ .map(row => `<tr>${row.map((cell, index) => renderTableCell('td', cell, node.align[index], options)).join('')}</tr>`)
34
+ .join('')}</tbody>`
35
+ : '';
34
36
  return `<table>${header}${body}</table>`;
35
37
  }
36
38
  case 'footnotes':
package/dist/inline.js CHANGED
@@ -7,7 +7,12 @@ export function parseInline(value, options = {}) {
7
7
  }
8
8
  return result;
9
9
  }
10
- function parseInlineRaw(value, options) {
10
+ const maxInlineDepth = 32;
11
+ const scansPerCharacter = 16;
12
+ function parseInlineRaw(value, options, budget = { scans: Math.max(value.length * scansPerCharacter, 1024), depth: 0 }) {
13
+ if (budget.depth >= maxInlineDepth)
14
+ return value ? [{ type: 'text', value }] : [];
15
+ budget.depth++;
11
16
  const nodes = [];
12
17
  let index = 0;
13
18
  let text = '';
@@ -35,7 +40,7 @@ function parseInlineRaw(value, options) {
35
40
  }
36
41
  if (char === '`') {
37
42
  const tickCount = countRun(value, index, '`');
38
- const close = findClosingRun(value, index + tickCount, '`', tickCount);
43
+ const close = findClosingRun(value, index + tickCount, '`', tickCount, budget);
39
44
  if (close !== -1) {
40
45
  pushText();
41
46
  nodes.push({
@@ -45,15 +50,18 @@ function parseInlineRaw(value, options) {
45
50
  index = close + tickCount;
46
51
  continue;
47
52
  }
53
+ text += value.slice(index, index + tickCount);
54
+ index += tickCount;
55
+ continue;
48
56
  }
49
57
  if (char === '!' && next === '[') {
50
- const parsed = parseLinkish(value, index + 1, options);
58
+ const parsed = parseLinkish(value, index + 1, options, budget);
51
59
  if (parsed) {
52
60
  pushText();
53
61
  nodes.push({
54
62
  type: 'image',
55
63
  src: sanitizeUrl(parsed.href),
56
- alt: textFromMarkdown(parsed.label),
64
+ alt: textFromMarkdown(parsed.label, budget),
57
65
  ...(parsed.title ? { title: parsed.title } : {}),
58
66
  });
59
67
  index = parsed.end;
@@ -61,14 +69,14 @@ function parseInlineRaw(value, options) {
61
69
  }
62
70
  }
63
71
  if (char === '[') {
64
- const footnote = parseFootnoteReference(value, index, options);
72
+ const footnote = parseFootnoteReference(value, index, options, budget);
65
73
  if (footnote) {
66
74
  pushText();
67
75
  nodes.push(footnote.node);
68
76
  index = footnote.end;
69
77
  continue;
70
78
  }
71
- const parsed = parseLinkish(value, index, options);
79
+ const parsed = parseLinkish(value, index, options, budget);
72
80
  if (parsed) {
73
81
  const href = sanitizeUrl(parsed.href);
74
82
  pushText();
@@ -77,23 +85,23 @@ function parseInlineRaw(value, options) {
77
85
  type: 'link',
78
86
  href,
79
87
  ...(parsed.title ? { title: parsed.title } : {}),
80
- children: parseInlineRaw(parsed.label, options),
88
+ children: parseInlineRaw(parsed.label, options, budget),
81
89
  });
82
90
  }
83
91
  else {
84
- nodes.push(...parseInlineRaw(parsed.label, options));
92
+ nodes.push(...parseInlineRaw(parsed.label, options, budget));
85
93
  }
86
94
  index = parsed.end;
87
95
  continue;
88
96
  }
89
97
  }
90
98
  if ((char === '*' && next === '*') || (char === '_' && next === '_')) {
91
- const close = findDelimiter(value, index + 2, char + char);
99
+ const close = findDelimiter(value, index + 2, char + char, budget);
92
100
  if (close !== -1) {
93
101
  pushText();
94
102
  nodes.push({
95
103
  type: 'strong',
96
- children: parseInlineRaw(value.slice(index + 2, close), options),
104
+ children: parseInlineRaw(value.slice(index + 2, close), options, budget),
97
105
  });
98
106
  index = close + 2;
99
107
  continue;
@@ -102,13 +110,19 @@ function parseInlineRaw(value, options) {
102
110
  index += 2;
103
111
  continue;
104
112
  }
113
+ if (char === '~' && next === '~' && value[index + 2] === '~') {
114
+ const run = countRun(value, index, '~');
115
+ text += value.slice(index, index + run);
116
+ index += run;
117
+ continue;
118
+ }
105
119
  if (char === '~' && next === '~') {
106
- const close = findDelimiter(value, index + 2, '~~');
120
+ const close = findDelimiter(value, index + 2, '~~', budget);
107
121
  if (close !== -1) {
108
122
  pushText();
109
123
  nodes.push({
110
124
  type: 'strike',
111
- children: parseInlineRaw(value.slice(index + 2, close), options),
125
+ children: parseInlineRaw(value.slice(index + 2, close), options, budget),
112
126
  });
113
127
  index = close + 2;
114
128
  continue;
@@ -118,31 +132,31 @@ function parseInlineRaw(value, options) {
118
132
  continue;
119
133
  }
120
134
  if (char === '~') {
121
- const close = findSingleTildeDelimiter(value, index + 1);
135
+ const close = findSingleTildeDelimiter(value, index + 1, budget);
122
136
  if (close !== -1) {
123
137
  pushText();
124
138
  nodes.push({
125
139
  type: 'strike',
126
- children: parseInlineRaw(value.slice(index + 1, close), options),
140
+ children: parseInlineRaw(value.slice(index + 1, close), options, budget),
127
141
  });
128
142
  index = close + 1;
129
143
  continue;
130
144
  }
131
145
  }
132
146
  if (char === '*' || char === '_') {
133
- const close = findDelimiter(value, index + 1, char);
147
+ const close = findDelimiter(value, index + 1, char, budget);
134
148
  if (close !== -1 && !isIntrawordUnderscore(value, index, close, char)) {
135
149
  pushText();
136
150
  nodes.push({
137
151
  type: 'emphasis',
138
- children: parseInlineRaw(value.slice(index + 1, close), options),
152
+ children: parseInlineRaw(value.slice(index + 1, close), options, budget),
139
153
  });
140
154
  index = close + 1;
141
155
  continue;
142
156
  }
143
157
  }
144
158
  if (char === '<' && options.allowHtml) {
145
- const close = value.indexOf('>', index + 1);
159
+ const close = findCharacter(value, index + 1, '>', budget);
146
160
  if (close !== -1) {
147
161
  pushText();
148
162
  nodes.push({ type: 'inlineHtml', value: value.slice(index, close + 1) });
@@ -154,12 +168,13 @@ function parseInlineRaw(value, options) {
154
168
  index++;
155
169
  }
156
170
  pushText();
171
+ budget.depth--;
157
172
  return nodes;
158
173
  }
159
- function parseFootnoteReference(value, open, options) {
174
+ function parseFootnoteReference(value, open, options, budget) {
160
175
  if (value[open + 1] !== '^')
161
176
  return undefined;
162
- const close = value.indexOf(']', open + 2);
177
+ const close = findCharacter(value, open + 2, ']', budget);
163
178
  if (close === -1)
164
179
  return undefined;
165
180
  const label = value.slice(open + 2, close);
@@ -185,13 +200,13 @@ function parseFootnoteReference(value, open, options) {
185
200
  end: close + 1,
186
201
  };
187
202
  }
188
- function parseLinkish(value, open, options) {
189
- const closeBracket = findBalanced(value, open, '[', ']');
203
+ function parseLinkish(value, open, options, budget) {
204
+ const closeBracket = findBalanced(value, open, '[', ']', budget);
190
205
  if (closeBracket === -1)
191
206
  return undefined;
192
207
  const label = value.slice(open + 1, closeBracket);
193
208
  if (value[closeBracket + 1] === '(') {
194
- const closeParen = findBalanced(value, closeBracket + 1, '(', ')');
209
+ const closeParen = findBalanced(value, closeBracket + 1, '(', ')', budget);
195
210
  if (closeParen === -1)
196
211
  return undefined;
197
212
  const destination = value.slice(closeBracket + 2, closeParen).trim();
@@ -206,7 +221,7 @@ function parseLinkish(value, open, options) {
206
221
  };
207
222
  }
208
223
  if (value[closeBracket + 1] === '[') {
209
- const closeReference = findBalanced(value, closeBracket + 1, '[', ']');
224
+ const closeReference = findBalanced(value, closeBracket + 1, '[', ']', budget);
210
225
  if (closeReference === -1)
211
226
  return undefined;
212
227
  const definition = options.references?.[normalizeReferenceLabel(value.slice(closeBracket + 2, closeReference))];
@@ -230,9 +245,11 @@ function parseDestination(value) {
230
245
  ...(match[2] ? { title: match[2] } : {}),
231
246
  };
232
247
  }
233
- function findBalanced(value, openIndex, open, close) {
248
+ function findBalanced(value, openIndex, open, close, budget) {
234
249
  let depth = 0;
235
250
  for (let index = openIndex; index < value.length; index++) {
251
+ if (!takeScan(budget))
252
+ return -1;
236
253
  if (value[index - 1] === '\\')
237
254
  continue;
238
255
  if (value[index] === open)
@@ -245,38 +262,51 @@ function findBalanced(value, openIndex, open, close) {
245
262
  }
246
263
  return -1;
247
264
  }
248
- function countRun(value, index, char) {
265
+ function countRun(value, index, char, budget) {
249
266
  let count = 0;
250
- while (value[index + count] === char)
267
+ while (value[index + count] === char) {
268
+ if (budget && !takeScan(budget))
269
+ break;
251
270
  count++;
271
+ }
252
272
  return count;
253
273
  }
254
- function findClosingRun(value, start, char, count) {
274
+ function findClosingRun(value, start, char, count, budget) {
255
275
  for (let index = start; index < value.length; index++) {
276
+ if (!takeScan(budget))
277
+ return -1;
256
278
  if (value[index] !== char)
257
279
  continue;
258
- if (countRun(value, index, char) >= count)
280
+ if (countRun(value, index, char, budget) >= count)
259
281
  return index;
260
282
  }
261
283
  return -1;
262
284
  }
263
- function findDelimiter(value, start, delimiter) {
285
+ function findDelimiter(value, start, delimiter, budget) {
264
286
  for (let index = start; index < value.length; index++) {
287
+ if (!takeScan(budget))
288
+ return -1;
265
289
  if (value[index - 1] === '\\')
266
290
  continue;
291
+ if (delimiter === '~~' && (value[index - 1] === '~' || value[index + 2] === '~'))
292
+ continue;
267
293
  if (value.startsWith(delimiter, index))
268
294
  return index;
269
295
  }
270
296
  return -1;
271
297
  }
272
- function findSingleTildeDelimiter(value, start) {
298
+ function findSingleTildeDelimiter(value, start, budget) {
273
299
  if (isWhitespace(value[start] ?? ''))
274
300
  return -1;
275
301
  for (let index = start; index < value.length; index++) {
302
+ if (!takeScan(budget))
303
+ return -1;
276
304
  if (value[index - 1] === '\\')
277
305
  continue;
278
306
  if (value[index] !== '~')
279
307
  continue;
308
+ if (value[index - 1] === '~')
309
+ continue;
280
310
  if (value[index + 1] === '~')
281
311
  continue;
282
312
  if (isWhitespace(value[index - 1] ?? ''))
@@ -285,6 +315,21 @@ function findSingleTildeDelimiter(value, start) {
285
315
  }
286
316
  return -1;
287
317
  }
318
+ function findCharacter(value, start, character, budget) {
319
+ for (let index = start; index < value.length; index++) {
320
+ if (!takeScan(budget))
321
+ return -1;
322
+ if (value[index] === character)
323
+ return index;
324
+ }
325
+ return -1;
326
+ }
327
+ function takeScan(budget) {
328
+ if (budget.scans <= 0)
329
+ return false;
330
+ budget.scans--;
331
+ return true;
332
+ }
288
333
  function isIntrawordUnderscore(value, open, close, delimiter) {
289
334
  if (delimiter !== '_')
290
335
  return false;
@@ -293,8 +338,8 @@ function isIntrawordUnderscore(value, open, close, delimiter) {
293
338
  function isWhitespace(value) {
294
339
  return /\s/.test(value);
295
340
  }
296
- function textFromMarkdown(value) {
297
- return parseInlineRaw(value, {}).map(node => (node.type === 'text' || node.type === 'inlineCode' ? node.value : '')).join('');
341
+ function textFromMarkdown(value, budget) {
342
+ return parseInlineRaw(value, {}, budget).map(node => (node.type === 'text' || node.type === 'inlineCode' ? node.value : '')).join('');
298
343
  }
299
344
  function mergeText(nodes) {
300
345
  const result = [];
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAQV,gBAAgB,EAChB,YAAY,EAGb,MAAM,YAAY,CAAA;AAKnB,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,gBAAgB,CAyC5F"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAQV,gBAAgB,EAChB,YAAY,EAGb,MAAM,YAAY,CAAA;AAWnB,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,gBAAgB,CAyC5F"}
package/dist/parser.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { parseInline } from './inline.js';
2
2
  import { createSlugger, footnoteId, isBlank, normalizeInput, normalizeReferenceLabel, plainText, stripIndent } from './utils.js';
3
+ const maxBlockDepth = 64;
3
4
  export function parseMarkdown(markdown, options = {}) {
4
5
  const normalized = normalizeInput(markdown);
5
6
  const frontmatterEnabled = options.frontmatter !== false;
@@ -40,13 +41,21 @@ class BlockParser {
40
41
  lines;
41
42
  options;
42
43
  slugger;
44
+ budget;
43
45
  index = 0;
44
- constructor(lines, options, slugger) {
46
+ constructor(lines, options, slugger, budget = { depth: 0 }) {
45
47
  this.lines = lines;
46
48
  this.options = options;
47
49
  this.slugger = slugger;
50
+ this.budget = budget;
48
51
  }
49
52
  parse() {
53
+ if (this.budget.depth >= maxBlockDepth) {
54
+ const value = this.lines.slice(this.index).join('\n');
55
+ this.index = this.lines.length;
56
+ return value ? [{ type: 'paragraph', children: parseInline(value, this.options) }] : [];
57
+ }
58
+ this.budget.depth++;
50
59
  const nodes = [];
51
60
  while (this.index < this.lines.length) {
52
61
  if (isBlank(this.current())) {
@@ -68,6 +77,7 @@ class BlockParser {
68
77
  this.parseParagraph();
69
78
  nodes.push(node);
70
79
  }
80
+ this.budget.depth--;
71
81
  return nodes;
72
82
  }
73
83
  parseExtensionBlock() {
@@ -78,7 +88,7 @@ class BlockParser {
78
88
  index: this.index,
79
89
  options: this.options,
80
90
  parseInline: value => parseInline(value, this.options),
81
- parseBlocks: value => new BlockParser(normalizeInput(value).split('\n'), this.options, this.slugger).parse(),
91
+ parseBlocks: value => new BlockParser(normalizeInput(value).split('\n'), this.options, this.slugger, this.budget).parse(),
82
92
  consume: lines => {
83
93
  consumed = lines;
84
94
  },
@@ -151,7 +161,7 @@ class BlockParser {
151
161
  }
152
162
  return {
153
163
  type: 'blockquote',
154
- children: new BlockParser(quoted, this.options, this.slugger).parse(),
164
+ children: new BlockParser(quoted, this.options, this.slugger, this.budget).parse(),
155
165
  };
156
166
  }
157
167
  parseList() {
@@ -211,7 +221,7 @@ class BlockParser {
211
221
  itemLines.push(line.trimStart());
212
222
  this.index++;
213
223
  }
214
- const children = new BlockParser(itemLines, this.options, this.slugger).parse();
224
+ const children = new BlockParser(itemLines, this.options, this.slugger, this.budget).parse();
215
225
  const item = { type: 'listItem', children };
216
226
  if (checked !== undefined)
217
227
  item.checked = checked;
@@ -232,13 +242,15 @@ class BlockParser {
232
242
  return undefined;
233
243
  const headerCells = splitTableRow(header);
234
244
  const align = splitTableRow(delimiter).map(parseAlign);
245
+ const columns = headerCells.length;
235
246
  const rows = [];
236
247
  this.index += 2;
237
248
  while (this.index < this.lines.length) {
238
249
  const line = this.current();
239
- if (isBlank(line) || !line.includes('|'))
250
+ if (isBlank(line) || isBlockStart(line, this.next()))
240
251
  break;
241
- rows.push(splitTableRow(line).map(value => cell(value, this.options)));
252
+ const values = splitTableRow(line);
253
+ rows.push(Array.from({ length: columns }, (_, index) => cell(values[index] ?? '', this.options)));
242
254
  this.index++;
243
255
  }
244
256
  return {
@@ -434,7 +446,7 @@ function looksLikeTableHeader(header, delimiter) {
434
446
  if (!header.includes('|'))
435
447
  return false;
436
448
  const cells = splitTableRow(delimiter);
437
- return cells.length > 0 && cells.every(cell => /^:?-{2,}:?$/.test(cell.trim()));
449
+ return cells.length === splitTableRow(header).length && cells.every(cell => /^:?-+:?$/.test(cell.trim()));
438
450
  }
439
451
  function splitTableRow(value) {
440
452
  let row = value.trim();
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEnE,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,oBAAqB,SAAQ,aAAa;IACzD,UAAU,CAAC,EAAE,YAAY,CAAA;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,oBAAoB;IACzD,QAAQ,EAAE,aAAa,CAAA;CACxB;AAED,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,EAAE,aAAa,GAAG,YAAY,CAE9E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB,GAAG,SAAS,CAGvG;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,oBAAyB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,YAAY,CA6DhH;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,oBAAyB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAwC/G"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEnE,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,oBAAqB,SAAQ,aAAa;IACzD,UAAU,CAAC,EAAE,YAAY,CAAA;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,oBAAoB;IACzD,QAAQ,EAAE,aAAa,CAAA;CACxB;AAED,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,EAAE,aAAa,GAAG,YAAY,CAE9E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB,GAAG,SAAS,CAGvG;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,GAAE,oBAAyB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,YAAY,CA+DhH;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,GAAE,oBAAyB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAwC/G"}
package/dist/react.js CHANGED
@@ -22,7 +22,9 @@ export function renderBlockReact(node, options = {}, key) {
22
22
  case 'blockquote':
23
23
  return h(options, 'blockquote', { key }, node.children.map((child, index) => renderBlockReact(child, options, `${key}:${index}`)));
24
24
  case 'table':
25
- return h(options, 'table', { key }, h(options, 'thead', null, h(options, 'tr', null, node.header.map((cell, index) => renderTableCellReact('th', cell, node.align[index], options, index)))), h(options, 'tbody', null, node.rows.map((row, rowIndex) => h(options, 'tr', { key: rowIndex }, row.map((cell, index) => renderTableCellReact('td', cell, node.align[index], options, index))))));
25
+ return h(options, 'table', { key }, h(options, 'thead', null, h(options, 'tr', null, node.header.map((cell, index) => renderTableCellReact('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) => renderTableCellReact('td', cell, node.align[index], options, index)))))
27
+ : null);
26
28
  case 'footnotes':
27
29
  return renderFootnotesReact(node.items, options, key);
28
30
  case 'thematicBreak':
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@tanstack/markdown",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
- "description": "A tiny deterministic TanStack-flavored markdown renderer.",
5
+ "description": "A tiny, fast, deterministic Markdown renderer for blogs and documentation.",
6
6
  "license": "MIT",
7
- "packageManager": "pnpm@11.4.0",
8
7
  "sideEffects": false,
9
8
  "files": [
10
9
  "dist",
@@ -53,14 +52,6 @@
53
52
  "import": "./dist/extensions/tabs.js"
54
53
  }
55
54
  },
56
- "scripts": {
57
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
58
- "typecheck": "tsc --noEmit",
59
- "test": "vitest run",
60
- "bench": "tsx scripts/bench.ts",
61
- "size": "tsx scripts/measure-size.ts",
62
- "research": "pnpm run size && pnpm run bench"
63
- },
64
55
  "publishConfig": {
65
56
  "access": "public"
66
57
  },
@@ -77,6 +68,7 @@
77
68
  "@types/react": "^19.0.0",
78
69
  "@types/react-dom": "^19.0.0",
79
70
  "commonmark": "^0.31.2",
71
+ "commonmark-spec": "0.31.2",
80
72
  "esbuild": "^0.25.0",
81
73
  "markdown-it": "^14.2.0",
82
74
  "markdown-wasm": "^1.2.0",
@@ -91,5 +83,16 @@
91
83
  "typescript": "^5.8.0",
92
84
  "unified": "^11.0.5",
93
85
  "vitest": "^3.2.0"
86
+ },
87
+ "scripts": {
88
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
89
+ "typecheck": "tsc --noEmit",
90
+ "test": "vitest run",
91
+ "test:corpus": "vitest run tests/corpus.test.tsx",
92
+ "conformance": "tsx scripts/conformance.ts",
93
+ "bench": "tsx scripts/bench.ts",
94
+ "size": "tsx scripts/measure-size.ts",
95
+ "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"
94
97
  }
95
- }
98
+ }