@tanstack/markdown 0.0.12 → 0.0.14

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/dist/parser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseInline } from './inline.js';
2
- import { createSlugger, footnoteId, isBlank, normalizeInput, normalizeReferenceLabel, plainText, stripIndent } from './utils.js';
2
+ import { createSlugger, footnoteId, isBlank, normalizeInput, normalizeReferenceLabel, parseDestination, plainText, stripIndent } from './utils.js';
3
3
  const maxBlockDepth = 64;
4
4
  export function parseMarkdown(markdown, options = {}) {
5
5
  const normalized = normalizeInput(markdown);
@@ -26,10 +26,11 @@ export function parseMarkdown(markdown, options = {}) {
26
26
  ...(hasFootnotes ? { footnotes: definitions.footnotes, footnoteOrder, footnoteCounts } : {}),
27
27
  }
28
28
  : options;
29
- const parser = new BlockParser(lines, parseOptions, createSlugger());
29
+ const slugger = createSlugger();
30
+ const parser = new BlockParser(lines, parseOptions, slugger);
30
31
  const children = parser.parse();
31
32
  if (hasFootnotes && footnoteOrder.length > 0) {
32
- children.push(createFootnotesBlock(definitions.footnotes, footnoteOrder, parseOptions));
33
+ children.push(createFootnotesBlock(definitions.footnotes, footnoteOrder, parseOptions, slugger));
33
34
  }
34
35
  let document = frontmatter === undefined ? { type: 'root', children } : { type: 'root', frontmatter, children };
35
36
  for (const extension of parseOptions.extensions ?? []) {
@@ -43,6 +44,7 @@ class BlockParser {
43
44
  slugger;
44
45
  budget;
45
46
  index = 0;
47
+ loose = false;
46
48
  constructor(lines, options, slugger, budget = { depth: 0 }) {
47
49
  this.lines = lines;
48
50
  this.options = options;
@@ -59,6 +61,8 @@ class BlockParser {
59
61
  const nodes = [];
60
62
  while (this.index < this.lines.length) {
61
63
  if (isBlank(this.current())) {
64
+ if (nodes.length)
65
+ this.loose = true;
62
66
  this.index++;
63
67
  continue;
64
68
  }
@@ -101,23 +105,21 @@ class BlockParser {
101
105
  return undefined;
102
106
  }
103
107
  parseFence() {
104
- const match = this.current().match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
108
+ const match = this.current().match(/^( {0,3})(`{3,}|~{3,})(.*)$/);
105
109
  if (!match)
106
110
  return undefined;
107
- const fence = match[1];
108
- const marker = fence[0];
109
- const fenceSize = fence.length;
110
- const info = match[2].trim();
111
+ const fence = match[2];
112
+ const info = match[3].trim();
111
113
  const code = [];
112
114
  this.index++;
113
115
  while (this.index < this.lines.length) {
114
116
  const line = this.current();
115
117
  const close = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
116
- if (close && close[1][0] === marker && close[1].length >= fenceSize) {
118
+ if (close?.[1].startsWith(fence)) {
117
119
  this.index++;
118
120
  break;
119
121
  }
120
- code.push(line);
122
+ code.push(stripIndent(line, match[1].length));
121
123
  this.index++;
122
124
  }
123
125
  return {
@@ -150,15 +152,13 @@ class BlockParser {
150
152
  const quoted = [];
151
153
  while (this.index < this.lines.length) {
152
154
  const line = this.current();
153
- if (isBlank(line)) {
154
- quoted.push('');
155
- this.index++;
156
- continue;
157
- }
158
155
  const match = line.match(/^ {0,3}>\s?(.*)$/);
159
- if (!match)
160
- break;
161
- quoted.push(match[1]);
156
+ if (!match) {
157
+ if (!isBlank(line))
158
+ break;
159
+ this.loose = true;
160
+ }
161
+ quoted.push(match?.[1] ?? '');
162
162
  this.index++;
163
163
  }
164
164
  return {
@@ -173,11 +173,10 @@ class BlockParser {
173
173
  const items = [];
174
174
  const ordered = first.ordered;
175
175
  const baseIndent = first.indent;
176
- const start = ordered ? first.number : undefined;
177
176
  let loose = false;
178
177
  while (this.index < this.lines.length) {
179
178
  const marker = listMarker(this.current());
180
- if (!marker || !sameListType(marker, first) || marker.indent !== baseIndent)
179
+ if (!marker || marker.marker !== first.marker || marker.indent !== baseIndent)
181
180
  break;
182
181
  let firstLine = marker.content;
183
182
  const task = firstLine.match(/^\[([ xX])\]\s+(.*)$/);
@@ -200,7 +199,7 @@ class BlockParser {
200
199
  break;
201
200
  const followingMarker = listMarker(following);
202
201
  if (followingMarker?.indent === baseIndent) {
203
- if (!sameListType(followingMarker, first))
202
+ if (followingMarker.marker !== first.marker)
204
203
  break;
205
204
  loose = true;
206
205
  this.index = nextIndex;
@@ -208,9 +207,8 @@ class BlockParser {
208
207
  }
209
208
  if (leadingSpaces(following) < marker.contentIndent)
210
209
  break;
211
- loose = true;
212
- itemLines.push('');
213
- this.index = nextIndex;
210
+ while (this.index < nextIndex)
211
+ itemLines.push(stripIndent(this.lines[this.index++], marker.contentIndent));
214
212
  continue;
215
213
  }
216
214
  if (leadingSpaces(line) >= marker.contentIndent) {
@@ -223,7 +221,9 @@ class BlockParser {
223
221
  itemLines.push(line.trimStart());
224
222
  this.index++;
225
223
  }
226
- const children = new BlockParser(itemLines, this.options, this.slugger, this.budget).parse();
224
+ const parser = new BlockParser(itemLines, this.options, this.slugger, this.budget);
225
+ const children = parser.parse();
226
+ loose ||= parser.loose;
227
227
  const item = { type: 'listItem', children };
228
228
  if (checked !== undefined)
229
229
  item.checked = checked;
@@ -232,7 +232,7 @@ class BlockParser {
232
232
  return {
233
233
  type: 'list',
234
234
  ordered,
235
- ...(ordered && start !== undefined ? { start } : {}),
235
+ ...(ordered && { start: first.number }),
236
236
  ...(loose && { loose: true }),
237
237
  items,
238
238
  };
@@ -322,7 +322,7 @@ function parseCodeInfo(info) {
322
322
  const titleMatch = meta.match(/(?:^|\s)(?:title|file)=(?:"([^"]+)"|'([^']+)'|([^\s}]+))/);
323
323
  const frameworkMatch = meta.match(/(?:^|\s)framework=(?:"([^"]+)"|'([^']+)'|([^\s}]+))/);
324
324
  const rangeMatch = meta.match(/\{([^}]+)\}|(?:^|\s)lines=([^\s]+)/);
325
- const highlightLines = parseLineRanges(rangeMatch?.[1] ?? rangeMatch?.[2] ?? '');
325
+ const highlightLines = rangeMatch ? parseLineRanges(rangeMatch[1] ?? rangeMatch[2]) : [];
326
326
  const title = titleMatch ? titleMatch[1] ?? titleMatch[2] ?? titleMatch[3] : undefined;
327
327
  const framework = frameworkMatch ? frameworkMatch[1] ?? frameworkMatch[2] ?? frameworkMatch[3] : undefined;
328
328
  return {
@@ -336,29 +336,20 @@ function parseCodeInfo(info) {
336
336
  function extractDefinitions(lines) {
337
337
  const references = Object.create(null);
338
338
  const footnotes = Object.create(null);
339
- const footnoteIds = new Map();
339
+ const footnoteIds = createSlugger(footnoteId);
340
340
  const remaining = [];
341
- let fenceMarker;
342
- let fenceSize = 0;
341
+ let activeFence = '';
343
342
  let index = 0;
344
343
  while (index < lines.length) {
345
344
  const line = lines[index];
346
- const fence = line.match(/^ {0,3}(`{3,}|~{3,})/);
345
+ const fence = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
347
346
  if (fence) {
348
- const marker = fence[1][0];
349
- if (!fenceMarker) {
350
- fenceMarker = marker;
351
- fenceSize = fence[1].length;
352
- }
353
- else if (marker === fenceMarker && fence[1].length >= fenceSize) {
354
- fenceMarker = undefined;
355
- fenceSize = 0;
356
- }
357
- remaining.push(line);
358
- index++;
359
- continue;
347
+ if (!activeFence)
348
+ activeFence = fence[1];
349
+ else if (fence[1].startsWith(activeFence) && isBlank(fence[2]))
350
+ activeFence = '';
360
351
  }
361
- if (!fenceMarker) {
352
+ else if (!activeFence) {
362
353
  const footnote = line.match(/^ {0,3}\[\^([^\]\n]+)\]:[ \t]*(.*)$/);
363
354
  if (footnote) {
364
355
  const label = footnote[1];
@@ -373,20 +364,14 @@ function extractDefinitions(lines) {
373
364
  }
374
365
  const key = normalizeReferenceLabel(label);
375
366
  if (!footnotes[key]) {
376
- const baseId = footnoteId(label) || 'footnote';
377
- const count = (footnoteIds.get(baseId) ?? 0) + 1;
378
- footnoteIds.set(baseId, count);
379
- footnotes[key] = { label, content: content.join('\n'), id: count === 1 ? baseId : `${baseId}-${count}` };
367
+ footnotes[key] = { label, content: content.join('\n'), id: footnoteIds(label) };
380
368
  }
381
369
  continue;
382
370
  }
383
- const definition = line.match(/^ {0,3}\[([^\]\n]+)\]:[ \t]*(\S+)(?:[ \t]+(?:"([^"]*)"|'([^']*)'|\(([^)]*)\)))?[ \t]*$/);
384
- if (definition) {
385
- const title = definition[3] ?? definition[4] ?? definition[5];
386
- references[normalizeReferenceLabel(definition[1])] = {
387
- href: definition[2].replace(/^<|>$/g, ''),
388
- ...(title !== undefined ? { title } : {}),
389
- };
371
+ const definition = line.match(/^ {0,3}\[([^\]\n]+)\]:[ \t]*(\S.*)$/);
372
+ const destination = definition && parseDestination(definition[2].trimEnd());
373
+ if (destination) {
374
+ references[normalizeReferenceLabel(definition[1])] ??= destination;
390
375
  index++;
391
376
  continue;
392
377
  }
@@ -396,7 +381,7 @@ function extractDefinitions(lines) {
396
381
  }
397
382
  return { lines: remaining, references, footnotes };
398
383
  }
399
- function createFootnotesBlock(footnotes, footnoteOrder, options) {
384
+ function createFootnotesBlock(footnotes, footnoteOrder, options, slugger) {
400
385
  const items = [];
401
386
  for (let index = 0; index < footnoteOrder.length; index++) {
402
387
  const key = footnoteOrder[index];
@@ -404,9 +389,9 @@ function createFootnotesBlock(footnotes, footnoteOrder, options) {
404
389
  if (!definition)
405
390
  continue;
406
391
  items.push({
407
- id: definition.id ?? (footnoteId(definition.label) || 'footnote'),
392
+ id: definition.id ?? footnoteId(definition.label),
408
393
  number: index + 1,
409
- children: new BlockParser(normalizeInput(definition.content).split('\n'), options, createSlugger()).parse(),
394
+ children: new BlockParser(normalizeInput(definition.content).split('\n'), options, slugger).parse(),
410
395
  });
411
396
  }
412
397
  for (const item of items) {
@@ -424,6 +409,9 @@ function parseLineRanges(value) {
424
409
  continue;
425
410
  const start = Number(match[1]);
426
411
  const end = Number(match[2] ?? match[1]);
412
+ // Larger integers can stop line++ from making progress.
413
+ if (end > Number.MAX_SAFE_INTEGER)
414
+ continue;
427
415
  for (let line = start; line <= end && line < start + 1000; line++)
428
416
  lines.add(line);
429
417
  }
@@ -444,18 +432,12 @@ function listMarker(line) {
444
432
  content: match[4] ?? '',
445
433
  };
446
434
  }
447
- function sameListType(left, right) {
448
- return left.ordered === right.ordered && left.marker === right.marker;
449
- }
450
435
  function leadingSpaces(line) {
451
436
  return line.match(/^ */)?.[0].length ?? 0;
452
437
  }
453
438
  function isBlockStart(line, next) {
454
439
  const marker = listMarker(line);
455
- return (/^ {0,3}(`{3,}|~{3,})/.test(line) ||
456
- /^ {0,3}#{1,6}(?:\s+|$)/.test(line) ||
457
- /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line) ||
458
- /^ {0,3}>\s?/.test(line) ||
440
+ return (/^ {0,3}(?:`{3,}|~{3,}|#{1,6}(?:\s|$)|([-*_])(?:\s*\1){2,}\s*$|>)/.test(line) ||
459
441
  (marker !== undefined && (!marker.ordered || marker.number === 1)) ||
460
442
  (!!next && looksLikeTableHeader(line, next)));
461
443
  }
@@ -466,14 +448,10 @@ function looksLikeTableHeader(header, delimiter) {
466
448
  return cells.length === splitTableRow(header).length && cells.every(cell => /^:?-+:?$/.test(cell.trim()));
467
449
  }
468
450
  function splitTableRow(value) {
469
- let row = value.trim();
470
- if (row.startsWith('|'))
471
- row = row.slice(1);
472
- if (row.endsWith('|'))
473
- row = row.slice(0, -1);
451
+ const row = value.trim();
474
452
  const cells = [];
475
453
  let current = '';
476
- for (let index = 0; index < row.length; index++) {
454
+ for (let index = row.startsWith('|') ? 1 : 0; index < row.length; index++) {
477
455
  const char = row[index];
478
456
  if (char === '\\' && row[index + 1] === '|') {
479
457
  current += '|';
@@ -487,7 +465,8 @@ function splitTableRow(value) {
487
465
  }
488
466
  current += char;
489
467
  }
490
- cells.push(current.trim());
468
+ if (current || !row.endsWith('|') || !cells.length)
469
+ cells.push(current.trim());
491
470
  return cells;
492
471
  }
493
472
  function parseAlign(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,aAAa,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAElG,OAAO,KAAK,EAAE,SAAS,EAAmC,UAAU,EAAE,aAAa,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAA;AAErI,KAAK,oBAAoB,GAAG,MAAM,GAAG,CAAC,iBAAiB,CAAA;AACvD,KAAK,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAExE,MAAM,MAAM,sBAAsB,CAAC,OAAO,SAAS,oBAAoB,IAAI,wBAAwB,CAAC,OAAO,CAAC,CAAA;AAE5G,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC;KACtC,OAAO,IAAI,oBAAoB,GAAG,oBAAoB,GAAG,aAAa,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC;CACzG,CAAC,GACA,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAA;AAEzD,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"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,aAAa,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGlG,OAAO,KAAK,EAAE,SAAS,EAAmC,UAAU,EAAE,aAAa,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAA;AAErI,KAAK,oBAAoB,GAAG,MAAM,GAAG,CAAC,iBAAiB,CAAA;AACvD,KAAK,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAExE,MAAM,MAAM,sBAAsB,CAAC,OAAO,SAAS,oBAAoB,IAAI,wBAAwB,CAAC,OAAO,CAAC,CAAA;AAE5G,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC;KACtC,OAAO,IAAI,oBAAoB,GAAG,oBAAoB,GAAG,aAAa,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC;CACzG,CAAC,GACA,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAA;AAEzD,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
@@ -1,5 +1,6 @@
1
1
  import { Fragment, createElement } from 'react';
2
2
  import { parseMarkdown } from './parser.js';
3
+ import { footnoteReferenceId } from './utils.js';
3
4
  export function Markdown({ children, ...options }) {
4
5
  return createElement(Fragment, null, renderMarkdownReact(children, options));
5
6
  }
@@ -17,7 +18,7 @@ export function renderBlockReact(node, options = {}, key) {
17
18
  return renderCodeBlockReact(node, options, key);
18
19
  case 'list': {
19
20
  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 }, renderListItemChildrenReact(item.children, item.checked, node.loose, options, `${index}`))));
21
+ return h(options, tag, { key, ...(node.ordered && node.start !== undefined && node.start !== 1 ? { start: node.start } : {}) }, node.items.map((item, index) => h(options, 'li', { key: index }, renderListItemChildrenReact(item.children, item.checked, node.loose, options, `${index}`))));
21
22
  }
22
23
  case 'blockquote':
23
24
  return h(options, 'blockquote', { key }, node.children.map((child, index) => renderBlockReact(child, options, `${key}:${index}`)));
@@ -53,7 +54,7 @@ export function renderInlineReact(node, options = {}, key) {
53
54
  return h(options, 'del', { key }, renderInlines(node.children, options));
54
55
  case 'footnoteReference':
55
56
  return h(options, 'sup', { key }, h(options, 'a', {
56
- id: `user-content-fnref-${footnoteReferenceId(node)}`,
57
+ id: `user-content-fnref-${footnoteReferenceId(node.id, node.referenceIndex)}`,
57
58
  'data-footnote-ref': '',
58
59
  'aria-describedby': 'footnote-label',
59
60
  href: `#user-content-fn-${node.id}`,
@@ -102,8 +103,7 @@ function renderCodeBlockReact(node, options, key) {
102
103
  return h(options, 'figure', { key, className: 'tm-code-frame', 'data-lang': lang }, h(options, 'figcaption', null, node.title), pre);
103
104
  }
104
105
  function renderListItemChildrenReact(children, checked, loose, options, key) {
105
- const [first, ...rest] = children;
106
- const task = checked === undefined
106
+ let result = checked === undefined
107
107
  ? []
108
108
  : [
109
109
  h(options, 'input', {
@@ -115,17 +115,18 @@ function renderListItemChildrenReact(children, checked, loose, options, key) {
115
115
  }),
116
116
  ' ',
117
117
  ];
118
- if (first?.type === 'paragraph') {
119
- const content = [...task, ...renderInlines(first.children, options)];
120
- return [
121
- ...(loose ? [h(options, 'p', { key: `${key}:paragraph` }, content)] : content),
122
- ...rest.flatMap((child, childIndex) => renderListChildReact(child, loose, options, `${key}:${childIndex + 1}`)),
123
- ];
118
+ for (let index = 0; index < children.length; index++) {
119
+ const child = children[index];
120
+ if (child.type === 'paragraph' && (!loose || index === 0)) {
121
+ for (const inline of renderInlines(child.children, options))
122
+ result.push(inline);
123
+ if (loose)
124
+ result = [h(options, 'p', { key: `${key}:paragraph` }, result)];
125
+ }
126
+ else
127
+ result.push(renderBlockReact(child, options, `${key}:${index}`));
124
128
  }
125
- return [...task, ...children.flatMap((child, childIndex) => renderListChildReact(child, loose, options, `${key}:${childIndex}`))];
126
- }
127
- function renderListChildReact(child, loose, options, key) {
128
- return !loose && child.type === 'paragraph' ? renderInlines(child.children, options) : [renderBlockReact(child, options, key)];
129
+ return result;
129
130
  }
130
131
  function renderTableCellReact(tag, cell, align, options, key) {
131
132
  return h(options, tag, { key, ...(align ? { style: { textAlign: align } } : {}) }, renderInlines(cell.children, options));
@@ -136,19 +137,20 @@ function renderFootnotesReact(items, options, key) {
136
137
  function renderFootnoteItemReact(item, options) {
137
138
  const lastIndex = item.children.length - 1;
138
139
  const backrefs = renderFootnoteBackrefsReact(item, options);
139
- if (lastIndex < 0)
140
- return [h(options, 'p', { key: 'backref-wrapper' }, backrefs.slice(1))];
141
- return item.children.map((child, index) => {
140
+ const result = item.children.map((child, index) => {
142
141
  if (index === lastIndex && child.type === 'paragraph') {
143
142
  return h(options, 'p', { key: index }, renderInlines(child.children, options), backrefs);
144
143
  }
145
144
  return renderBlockReact(child, options, `${index}`);
146
145
  });
146
+ if (item.children[lastIndex]?.type !== 'paragraph')
147
+ result.push(h(options, 'p', { key: 'backref-wrapper' }, backrefs.slice(1)));
148
+ return result;
147
149
  }
148
150
  function renderFootnoteBackrefsReact(item, options) {
149
151
  const result = [];
150
152
  for (let index = 1; index <= (item.referenceCount ?? 1); index++) {
151
- const referenceId = index === 1 ? item.id : `${item.id}-${index}`;
153
+ const referenceId = footnoteReferenceId(item.id, index);
152
154
  const label = index === 1 ? `${item.number}` : `${item.number}-${index}`;
153
155
  result.push(' ', h(options, 'a', {
154
156
  key: index,
@@ -160,9 +162,6 @@ function renderFootnoteBackrefsReact(item, options) {
160
162
  }
161
163
  return result;
162
164
  }
163
- function footnoteReferenceId(node) {
164
- return node.referenceIndex && node.referenceIndex > 1 ? `${node.id}-${node.referenceIndex}` : node.id;
165
- }
166
165
  function h(options, tag, props, ...children) {
167
166
  const component = typeof tag === 'string' ? options.components?.[tag] ?? tag : tag;
168
167
  return createElement(component, props, ...children);
package/dist/utils.d.ts CHANGED
@@ -1,13 +1,17 @@
1
- import type { InlineNode } from './types.js';
1
+ import type { InlineNode, LinkReferenceDefinition } from './types.js';
2
2
  export declare function normalizeInput(value: string): string;
3
3
  export declare function escapeHtml(value: string): string;
4
4
  export declare function escapeAttr(value: string): string;
5
5
  export declare function isBlank(value: string): boolean;
6
6
  export declare function stripIndent(value: string, size: number): string;
7
7
  export declare function plainText(nodes: InlineNode[]): string;
8
- export declare function createSlugger(): (value: string) => string;
8
+ export declare function createSlugger(normalize?: typeof headingSlug): (value: string) => string;
9
+ declare function headingSlug(value: string): string;
9
10
  export declare function sanitizeUrl(value: string): string;
11
+ export declare function parseDestination(value: string): LinkReferenceDefinition | undefined;
10
12
  export declare function normalizeReferenceLabel(value: string): string;
11
13
  export declare function footnoteId(label: string): string;
14
+ export declare function footnoteReferenceId(id: string, index?: number): string;
12
15
  export declare function splitLines(value: string): string[];
16
+ export {};
13
17
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAU5C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAQrD;AAED,wBAAgB,aAAa,KAGnB,OAAO,MAAM,YActB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAIlD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAUrE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAQrD;AAED,wBAAgB,aAAa,CAAC,SAAS,qBAAc,IAG3C,OAAO,MAAM,YAStB;AAED,iBAAS,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ1C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CAMnF;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,MAAM,CAEjE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAIlD"}
package/dist/utils.js CHANGED
@@ -35,40 +35,50 @@ export function plainText(nodes) {
35
35
  }
36
36
  return value;
37
37
  }
38
- export function createSlugger() {
38
+ export function createSlugger(normalize = headingSlug) {
39
39
  const seen = new Map();
40
40
  return (value) => {
41
- const base = value
42
- .toLowerCase()
43
- .normalize('NFKD')
44
- .replace(/[\u0300-\u036f]/g, '')
45
- .replace(/&[a-z0-9#]+;/gi, '')
46
- .replace(/[^a-z0-9]+/g, '-')
47
- .replace(/^-+|-+$/g, '') || 'section';
48
- const count = seen.get(base) ?? 0;
49
- seen.set(base, count + 1);
50
- return count === 0 ? base : `${base}-${count + 1}`;
41
+ const base = normalize(value);
42
+ let count = seen.get(base) ?? 1;
43
+ let id = base;
44
+ while (seen.has(id))
45
+ id = `${base}-${++count}`;
46
+ seen.set(base, count);
47
+ seen.set(id, 1);
48
+ return id;
51
49
  };
52
50
  }
51
+ function headingSlug(value) {
52
+ return value
53
+ .toLowerCase()
54
+ .normalize('NFKD')
55
+ .replace(/[\u0300-\u036f]/g, '')
56
+ .replace(/&[a-z0-9#]+;/gi, '')
57
+ .replace(/[^a-z0-9]+/g, '-')
58
+ .replace(/^-+|-+$/g, '') || 'section';
59
+ }
53
60
  export function sanitizeUrl(value) {
54
- const trimmed = value.trim().replace(/[\u0000-\u001F\u007F\s]+/g, '');
55
- if (!trimmed)
56
- return '';
57
- if (/^(#|\/|\.\/|\.\.\/)/.test(trimmed))
58
- return trimmed;
59
- if (/^(https?:|mailto:|tel:)/i.test(trimmed))
60
- return trimmed;
61
- if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed))
62
- return '';
63
- return trimmed;
61
+ const trimmed = value.replace(/[\u0000-\u001F\u007F\s]+/g, '');
62
+ return /^(?!https?:|mailto:|tel:)[a-z][a-z0-9+.-]*:/i.test(trimmed) ? '' : trimmed;
63
+ }
64
+ export function parseDestination(value) {
65
+ const match = value.match(/^(?:<([^<>\n]*)>|([^<>\s]*?))(?:\s+(?:"([^"]*)"|'([^']*)'|\(([^)]*)\)))?$/);
66
+ if (!match)
67
+ return undefined;
68
+ const href = match[1] ?? match[2];
69
+ const title = match[3] ?? match[4] ?? match[5];
70
+ return { href: href.replace(/\\([!-/:-@\[-`{-~])/g, '$1'), ...(title ? { title } : {}) };
64
71
  }
65
72
  export function normalizeReferenceLabel(value) {
66
- return value.trim().toLowerCase();
73
+ return value.trim().replace(/\s+/g, ' ').toLowerCase();
67
74
  }
68
75
  export function footnoteId(label) {
69
76
  return normalizeReferenceLabel(label)
70
77
  .replace(/[^a-z0-9_-]+/g, '-')
71
- .replace(/^-+|-+$/g, '');
78
+ .replace(/^-+|-+$/g, '') || 'footnote';
79
+ }
80
+ export function footnoteReferenceId(id, index = 1) {
81
+ return index > 1 ? `${id}-${index}` : id;
72
82
  }
73
83
  export function splitLines(value) {
74
84
  const lines = value.split('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/markdown",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "type": "module",
5
5
  "description": "A tiny, fast, deterministic Markdown renderer for blogs and documentation.",
6
6
  "license": "MIT",
@@ -12,7 +12,6 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/TanStack/markdown/issues"
14
14
  },
15
- "packageManager": "pnpm@11.4.0",
16
15
  "sideEffects": false,
17
16
  "files": [
18
17
  "dist",
@@ -71,27 +70,6 @@
71
70
  "import": "./dist/extensions/tabs.js"
72
71
  }
73
72
  },
74
- "scripts": {
75
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
76
- "typecheck": "tsc --noEmit",
77
- "test": "vitest run",
78
- "test:corpus": "vitest run tests/corpus.test.tsx",
79
- "corpus:audit": "tsx scripts/audit-corpus.ts",
80
- "corpus:audit:tanstack": "tsx scripts/audit-tanstack-corpus.ts",
81
- "corpus:audit:external": "tsx scripts/audit-external-corpus.ts",
82
- "docs:verify": "node scripts/verify-docs.mjs",
83
- "test:skills": "intent validate skills --check && pnpm run skills:check-version",
84
- "skills:check-version": "node scripts/sync-skill-version.mjs --check",
85
- "skills:sync-version": "node scripts/sync-skill-version.mjs",
86
- "skills:stale": "intent stale",
87
- "conformance": "tsx scripts/conformance.ts",
88
- "bench": "tsx scripts/bench.ts",
89
- "size": "tsx scripts/measure-size.ts",
90
- "research": "pnpm run conformance && pnpm run size && pnpm run bench",
91
- "verify": "pnpm test && pnpm run typecheck && pnpm run build && pnpm run docs:verify && pnpm run test:skills && pnpm run research && pnpm pack --dry-run",
92
- "prepublishOnly": "pnpm run verify",
93
- "version": "pnpm run skills:sync-version"
94
- },
95
73
  "publishConfig": {
96
74
  "access": "public"
97
75
  },
@@ -108,6 +86,7 @@
108
86
  }
109
87
  },
110
88
  "devDependencies": {
89
+ "@changesets/cli": "3.0.2",
111
90
  "@tanstack/highlight": "0.0.6",
112
91
  "@tanstack/intent": "^0.3.6",
113
92
  "@types/node": "^24.0.0",
@@ -133,5 +112,29 @@
133
112
  },
134
113
  "keywords": [
135
114
  "tanstack-intent"
136
- ]
137
- }
115
+ ],
116
+ "scripts": {
117
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
118
+ "typecheck": "tsc --noEmit",
119
+ "test": "vitest run",
120
+ "test:corpus": "vitest run tests/corpus.test.tsx",
121
+ "corpus:audit": "tsx scripts/audit-corpus.ts",
122
+ "corpus:audit:tanstack": "tsx scripts/audit-tanstack-corpus.ts",
123
+ "corpus:audit:external": "tsx scripts/audit-external-corpus.ts",
124
+ "docs:verify": "node scripts/verify-docs.mjs",
125
+ "test:skills": "intent validate skills --check && pnpm run skills:check-version",
126
+ "skills:check-version": "node scripts/sync-skill-version.mjs --check",
127
+ "skills:sync-version": "node scripts/sync-skill-version.mjs",
128
+ "skills:stale": "intent stale",
129
+ "changeset": "changeset",
130
+ "changeset:status": "changeset status",
131
+ "release:version": "changeset version && pnpm run skills:sync-version",
132
+ "release:publish": "pnpm run verify && changeset publish",
133
+ "conformance": "tsx scripts/conformance.ts",
134
+ "bench": "tsx scripts/bench.ts",
135
+ "size": "tsx scripts/measure-size.ts",
136
+ "research": "pnpm run conformance && pnpm run size && pnpm run bench",
137
+ "verify": "pnpm test && pnpm run typecheck && pnpm run build && pnpm run docs:verify && pnpm run test:skills && pnpm run research && pnpm pack --dry-run",
138
+ "version": "pnpm run skills:sync-version"
139
+ }
140
+ }
@@ -7,7 +7,7 @@ description: >
7
7
  metadata:
8
8
  type: core
9
9
  library: '@tanstack/markdown'
10
- library_version: '0.0.12'
10
+ library_version: '0.0.14'
11
11
  requires:
12
12
  - 'render-markdown'
13
13
  sources:
@@ -9,7 +9,7 @@ description: >
9
9
  metadata:
10
10
  type: core
11
11
  library: '@tanstack/markdown'
12
- library_version: '0.0.12'
12
+ library_version: '0.0.14'
13
13
  requires:
14
14
  - 'render-markdown'
15
15
  sources:
@@ -9,7 +9,7 @@ metadata:
9
9
  type: framework
10
10
  library: '@tanstack/markdown'
11
11
  framework: 'octane'
12
- library_version: '0.0.12'
12
+ library_version: '0.0.14'
13
13
  requires:
14
14
  - 'render-markdown'
15
15
  sources:
@@ -8,7 +8,7 @@ description: >
8
8
  metadata:
9
9
  type: lifecycle
10
10
  library: '@tanstack/markdown'
11
- library_version: '0.0.12'
11
+ library_version: '0.0.14'
12
12
  requires:
13
13
  - 'render-markdown'
14
14
  sources:
@@ -127,7 +127,7 @@ import { parseMarkdown } from '@tanstack/markdown/parser'
127
127
  const cache = new Map<string, MarkdownDocument>()
128
128
 
129
129
  export function renderCachedArticle(key: string, source: string): string {
130
- const cacheKey = `markdown-0.0.12:${key}`
130
+ const cacheKey = `markdown-0.0.14:${key}`
131
131
  let document = cache.get(cacheKey)
132
132
  if (!document) {
133
133
  document = parseMarkdown(source, {
@@ -9,7 +9,7 @@ metadata:
9
9
  type: framework
10
10
  library: '@tanstack/markdown'
11
11
  framework: 'react'
12
- library_version: '0.0.12'
12
+ library_version: '0.0.14'
13
13
  requires:
14
14
  - 'render-markdown'
15
15
  sources:
@@ -9,7 +9,7 @@ description: >
9
9
  metadata:
10
10
  type: core
11
11
  library: '@tanstack/markdown'
12
- library_version: '0.0.12'
12
+ library_version: '0.0.14'
13
13
  sources:
14
14
  - 'TanStack/markdown:docs/quick-start.md'
15
15
  - 'TanStack/markdown:docs/core-concepts/document-model.md'