@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 +15 -4
- package/dist/inline.js +52 -11
- package/dist/parser.js +16 -3
- package/package.json +22 -3
- package/skills/custom-extensions/SKILL.md +444 -0
- package/skills/docs-features/SKILL.md +436 -0
- package/skills/docs-features/references/docs-metadata.md +320 -0
- package/skills/octane-rendering/SKILL.md +351 -0
- package/skills/production-pipelines/SKILL.md +493 -0
- package/skills/react-rendering/SKILL.md +309 -0
- package/skills/render-markdown/SKILL.md +335 -0
- package/skills/render-markdown/references/ast-and-options.md +366 -0
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
|
-
- 6.
|
|
7
|
-
- 6.
|
|
8
|
-
- 6.
|
|
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 && /[\\`*_[\]{}()
|
|
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
|
|
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
|
|
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
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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})(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
}
|