@tanstack/markdown 0.0.9 → 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 CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  A tiny, fast, deterministic Markdown parser and renderer for blogs and documentation.
4
4
 
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
5
+ - 4.9 KB gzip parser
6
+ - 6.7 KB gzip HTML renderer
7
+ - 6.7 KB gzip React adapter
8
+ - 6.7 KB gzip Octane adapter
9
9
  - zero runtime dependencies
10
10
  - serializable AST
11
11
  - safe defaults for raw HTML and executable URLs
@@ -15,6 +15,8 @@ A tiny, fast, deterministic Markdown parser and renderer for blogs and documenta
15
15
  pnpm add @tanstack/markdown
16
16
  ```
17
17
 
18
+ If you use an AI agent, run `npx @tanstack/intent@latest install` so it can discover the package's task-specific skills.
19
+
18
20
  ```ts
19
21
  import { renderHtml } from '@tanstack/markdown/html'
20
22
 
@@ -66,8 +68,17 @@ MARKDOWN_CORPUS_DIRS=../tanstack.com/src/blog:../tanstack.com/docs \
66
68
  pnpm run test:corpus
67
69
  ```
68
70
 
71
+ To audit practical compatibility across local TanStack repositories and a pinned external docs/blog sample:
72
+
73
+ ```bash
74
+ pnpm run corpus:audit:tanstack
75
+ pnpm run corpus:audit:external
76
+ ```
77
+
69
78
  Generated reports:
70
79
 
71
80
  - [Bundle sizes](./reports/sizes.md)
72
81
  - [Benchmarks](./reports/benchmarks.md)
73
82
  - [CommonMark compatibility accounting](./reports/conformance.md)
83
+ - [TanStack repository corpus](./reports/tanstack-corpus.md)
84
+ - [External docs and blogs corpus](./reports/external-corpus.md)
package/dist/inline.js CHANGED
@@ -32,7 +32,7 @@ function parseInlineRaw(value, options, budget = { scans: Math.max(value.length
32
32
  index += 2;
33
33
  continue;
34
34
  }
35
- if (next && /[\\`*_[\]{}()#+\-.!|~>]/.test(next)) {
35
+ if (next && /[\\`*_[\]{}()#+\-.!|~<>]/.test(next)) {
36
36
  text += next;
37
37
  index += 2;
38
38
  continue;
@@ -95,8 +95,20 @@ function parseInlineRaw(value, options, budget = { scans: Math.max(value.length
95
95
  continue;
96
96
  }
97
97
  }
98
+ if (char === '*' && next === '*' && value[index + 2] === '*') {
99
+ const close = findDelimiter(value, index + 3, '***', budget);
100
+ if (close !== -1) {
101
+ pushText();
102
+ nodes.push({
103
+ type: 'emphasis',
104
+ children: parseInlineRaw(value.slice(index + 1, close + 2), options, budget),
105
+ });
106
+ index = close + 3;
107
+ continue;
108
+ }
109
+ }
98
110
  if ((char === '*' && next === '*') || (char === '_' && next === '_')) {
99
- const close = findDelimiter(value, index + 2, char + char, budget);
111
+ const close = char === '_' && !canUseUnderscore(value, index, 2, true) ? -1 : findDelimiter(value, index + 2, char + char, budget);
100
112
  if (close !== -1) {
101
113
  pushText();
102
114
  nodes.push({
@@ -144,8 +156,8 @@ function parseInlineRaw(value, options, budget = { scans: Math.max(value.length
144
156
  }
145
157
  }
146
158
  if (char === '*' || char === '_') {
147
- const close = findDelimiter(value, index + 1, char, budget);
148
- if (close !== -1 && !isIntrawordUnderscore(value, index, close, char)) {
159
+ const close = char === '_' && !canUseUnderscore(value, index, 1, true) ? -1 : findDelimiter(value, index + 1, char, budget);
160
+ if (close !== -1) {
149
161
  pushText();
150
162
  nodes.push({
151
163
  type: 'emphasis',
@@ -157,7 +169,7 @@ function parseInlineRaw(value, options, budget = { scans: Math.max(value.length
157
169
  }
158
170
  if (char === '<' && options.allowHtml) {
159
171
  const close = findCharacter(value, index + 1, '>', budget);
160
- if (close !== -1) {
172
+ if (close !== -1 && isInlineHtml(value.slice(index, close + 1))) {
161
173
  pushText();
162
174
  nodes.push({ type: 'inlineHtml', value: value.slice(index, close + 1) });
163
175
  index = close + 1;
@@ -224,7 +236,8 @@ function parseLinkish(value, open, options, budget) {
224
236
  const closeReference = findBalanced(value, closeBracket + 1, '[', ']', budget);
225
237
  if (closeReference === -1)
226
238
  return undefined;
227
- const definition = options.references?.[normalizeReferenceLabel(value.slice(closeBracket + 2, closeReference))];
239
+ const referenceLabel = value.slice(closeBracket + 2, closeReference);
240
+ const definition = options.references?.[normalizeReferenceLabel(referenceLabel || label)];
228
241
  if (!definition)
229
242
  return undefined;
230
243
  return {
@@ -234,8 +247,26 @@ function parseLinkish(value, open, options, budget) {
234
247
  end: closeReference + 1,
235
248
  };
236
249
  }
250
+ const definition = options.references?.[normalizeReferenceLabel(label)];
251
+ if (definition) {
252
+ return {
253
+ label,
254
+ href: definition.href,
255
+ ...(definition.title ? { title: definition.title } : {}),
256
+ end: closeBracket + 1,
257
+ };
258
+ }
237
259
  return undefined;
238
260
  }
261
+ function isInlineHtml(value) {
262
+ if (/^<!--(?:[\s\S]*--|-?)>$/.test(value))
263
+ return true;
264
+ if (/^<\?[\s\S]*\?>$/.test(value) || /^<![A-Z][\s\S]*>$/.test(value) || /^<!\[CDATA\[[\s\S]*\]\]>$/.test(value))
265
+ return true;
266
+ if (/^<\/[A-Za-z][\w:-]*\s*>$/.test(value))
267
+ return true;
268
+ return /^<[A-Za-z][\w:-]*(?:\s+[A-Za-z_:][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?>$/.test(value);
269
+ }
239
270
  function parseDestination(value) {
240
271
  const match = value.match(/^(\S+)(?:\s+["']([^"']*)["'])?$/);
241
272
  if (!match)
@@ -290,13 +321,15 @@ function findDelimiter(value, start, delimiter, budget) {
290
321
  continue;
291
322
  if (delimiter === '~~' && (value[index - 1] === '~' || value[index + 2] === '~'))
292
323
  continue;
324
+ if (delimiter[0] === '_' && !canUseUnderscore(value, index, delimiter.length, false))
325
+ continue;
293
326
  if (value.startsWith(delimiter, index))
294
327
  return index;
295
328
  }
296
329
  return -1;
297
330
  }
298
331
  function findSingleTildeDelimiter(value, start, budget) {
299
- if (isWhitespace(value[start] ?? ''))
332
+ if (isWhitespace(value[start] ?? '') || /\d/.test(value[start] ?? ''))
300
333
  return -1;
301
334
  for (let index = start; index < value.length; index++) {
302
335
  if (!takeScan(budget))
@@ -330,10 +363,18 @@ function takeScan(budget) {
330
363
  budget.scans--;
331
364
  return true;
332
365
  }
333
- function isIntrawordUnderscore(value, open, close, delimiter) {
334
- if (delimiter !== '_')
335
- return false;
336
- return /\w/.test(value[open - 1] ?? '') && /\w/.test(value[close + 1] ?? '');
366
+ function canUseUnderscore(value, index, size, opening) {
367
+ const before = value[index - 1];
368
+ const after = value[index + size];
369
+ const leftFlanking = !isDelimiterWhitespace(after) && (!isPunctuation(after) || isDelimiterWhitespace(before) || isPunctuation(before));
370
+ const rightFlanking = !isDelimiterWhitespace(before) && (!isPunctuation(before) || isDelimiterWhitespace(after) || isPunctuation(after));
371
+ return opening ? leftFlanking && (!rightFlanking || isPunctuation(before)) : rightFlanking && (!leftFlanking || isPunctuation(after));
372
+ }
373
+ function isDelimiterWhitespace(value) {
374
+ return value === undefined || /\s/.test(value);
375
+ }
376
+ function isPunctuation(value) {
377
+ return value !== undefined && /[^\p{L}\p{N}\s]/u.test(value);
337
378
  }
338
379
  function isWhitespace(value) {
339
380
  return /\s/.test(value);
package/dist/parser.js CHANGED
@@ -127,11 +127,13 @@ class BlockParser {
127
127
  };
128
128
  }
129
129
  parseHeading() {
130
- const match = this.current().match(/^ {0,3}(#{1,6})(?:\s+|$)(.*?)\s*#*\s*$/);
130
+ const match = this.current().match(/^ {0,3}(#{1,6})(?:[ \t]+(.*)|[ \t]*)$/);
131
131
  if (!match)
132
132
  return undefined;
133
133
  const depth = match[1].length;
134
- const children = parseInline(match[2].trim(), this.options);
134
+ const rawValue = match[2] ?? '';
135
+ const value = (/^#+[ \t]*$/.test(rawValue) ? '' : rawValue.replace(/[ \t]+#+[ \t]*$/, '')).trim();
136
+ const children = parseInline(value, this.options);
135
137
  const id = this.createHeadingId(children);
136
138
  this.index++;
137
139
  return id ? { type: 'heading', depth, id, children } : { type: 'heading', depth, children };
@@ -264,6 +266,16 @@ class BlockParser {
264
266
  if (!this.options.allowHtml || !/^ {0,3}<([A-Za-z][\w:-]*|!--|\/[A-Za-z])/.test(this.current()))
265
267
  return undefined;
266
268
  const html = [];
269
+ if (/^ {0,3}<!--/.test(this.current())) {
270
+ while (this.index < this.lines.length) {
271
+ const line = this.current();
272
+ html.push(line);
273
+ this.index++;
274
+ if (line.includes('-->'))
275
+ break;
276
+ }
277
+ return { type: 'html', value: html.join('\n') };
278
+ }
267
279
  while (this.index < this.lines.length && !isBlank(this.current())) {
268
280
  html.push(this.current());
269
281
  this.index++;
@@ -439,11 +451,12 @@ function leadingSpaces(line) {
439
451
  return line.match(/^ */)?.[0].length ?? 0;
440
452
  }
441
453
  function isBlockStart(line, next) {
454
+ const marker = listMarker(line);
442
455
  return (/^ {0,3}(`{3,}|~{3,})/.test(line) ||
443
456
  /^ {0,3}#{1,6}(?:\s+|$)/.test(line) ||
444
457
  /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line) ||
445
458
  /^ {0,3}>\s?/.test(line) ||
446
- listMarker(line) !== undefined ||
459
+ (marker !== undefined && (!marker.ordered || marker.number === 1)) ||
447
460
  (!!next && looksLikeTableHeader(line, next)));
448
461
  }
449
462
  function looksLikeTableHeader(header, delimiter) {
package/package.json CHANGED
@@ -1,13 +1,23 @@
1
1
  {
2
2
  "name": "@tanstack/markdown",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "description": "A tiny, fast, deterministic Markdown renderer for blogs and documentation.",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/markdown.git"
10
+ },
11
+ "homepage": "https://github.com/TanStack/markdown#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/TanStack/markdown/issues"
14
+ },
7
15
  "sideEffects": false,
8
16
  "files": [
9
17
  "dist",
10
- "README.md"
18
+ "README.md",
19
+ "skills",
20
+ "!skills/_artifacts"
11
21
  ],
12
22
  "types": "./dist/index.d.ts",
13
23
  "exports": {
@@ -72,6 +82,7 @@
72
82
  }
73
83
  },
74
84
  "devDependencies": {
85
+ "@tanstack/intent": "^0.3.6",
75
86
  "@types/node": "^24.0.0",
76
87
  "@types/react": "^19.0.0",
77
88
  "@types/react-dom": "^19.0.0",
@@ -93,16 +104,24 @@
93
104
  "unified": "^11.0.5",
94
105
  "vitest": "^3.2.0"
95
106
  },
107
+ "keywords": [
108
+ "tanstack-intent"
109
+ ],
96
110
  "scripts": {
97
111
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
98
112
  "typecheck": "tsc --noEmit",
99
113
  "test": "vitest run",
100
114
  "test:corpus": "vitest run tests/corpus.test.tsx",
115
+ "corpus:audit": "tsx scripts/audit-corpus.ts",
116
+ "corpus:audit:tanstack": "tsx scripts/audit-tanstack-corpus.ts",
117
+ "corpus:audit:external": "tsx scripts/audit-external-corpus.ts",
101
118
  "docs:verify": "node scripts/verify-docs.mjs",
119
+ "test:skills": "intent validate skills --check",
120
+ "skills:stale": "intent stale",
102
121
  "conformance": "tsx scripts/conformance.ts",
103
122
  "bench": "tsx scripts/bench.ts",
104
123
  "size": "tsx scripts/measure-size.ts",
105
124
  "research": "pnpm run conformance && pnpm run size && pnpm run bench",
106
- "verify": "pnpm test && pnpm run typecheck && pnpm run build && pnpm run docs:verify && pnpm run research && pnpm pack --dry-run"
125
+ "verify": "pnpm test && pnpm run typecheck && pnpm run build && pnpm run docs:verify && pnpm run test:skills && pnpm run research && pnpm pack --dry-run"
107
126
  }
108
127
  }