Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json",
"baseBranch": "main",
"access": "public",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"ignore": [],
"updateInternalDependencies": "patch"
}
13 changes: 13 additions & 0 deletions .changeset/smaller-compatible-markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/markdown': patch
---

Improve Markdown compatibility and parsing speed while reducing parser, renderer, and documentation extension bundles.

- Preserve code span whitespace and exact backtick delimiters, handle escaped punctuation and nested emphasis, and retain formatted image alt text.
- Correct nested list tightness, ordered lists starting at zero, indented code fences, escaped table pipes, and link destinations and reference labels. Prevent nested link anchors.
- Avoid generated heading and footnote definition ID collisions, preserve headings across footnote content, and render backreferences for footnotes ending in non-paragraph blocks.
- Keep callouts within quoted content, preserve prototype-shaped comment attributes and package-manager names, and guard oversized code-line ranges.
- Reduce repeated scanning and allocations in inline parsing, React and Octane list rendering, and documentation extensions.

Add regression coverage, ratchet 403 CommonMark examples, and enforce minified, gzip, and Brotli budgets for every public entry point. Existing public APIs and runtime dependencies are unchanged.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Release

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: release-main
cancel-in-progress: false

permissions: {}

jobs:
verify:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
mode: ${{ steps.changesets.outputs.mode }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run verify
- name: Select release operation
id: changesets
uses: changesets/action/select-mode@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2

version:
needs: verify
if: needs.verify.outputs.mode == 'version'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
actions: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Create or update release pull request
id: changesets
uses: changesets/action/version@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2
with:
script: pnpm run release:version
commit-message: Release packages
pr-title: Release packages
pr-base-branch: main
- name: Run CI on the release pull request
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PR: ${{ steps.changesets.outputs.pr-number }}
# Dispatch also works for branches created with GITHUB_TOKEN.
run: |
RELEASE_BRANCH=$(gh pr view "$RELEASE_PR" --json headRefName --jq .headRefName)
gh workflow run ci.yml --ref "$RELEASE_BRANCH"

publish:
needs: verify
if: needs.verify.outputs.mode == 'publish'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Publish to npm and create GitHub releases
uses: changesets/action/publish@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2
with:
script: pnpm run release:publish
create-github-releases: true
push-git-tags: true
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Contributing

TanStack Markdown targets blogs, documentation, and accumulated AI responses. Parser fixes need regression tests and must preserve the bundle-size budgets. Compare performance against the previous revision before adding work to a parsing hot path.

```sh
pnpm install
pnpm run verify
```

The [testing guide](docs/guides/testing.md) covers conformance, corpus audits, and revision comparisons.

## Releases

Include a changeset for package changes with `pnpm changeset`. Use a patch for compatible fixes and performance improvements, a minor for new public APIs, and a major for breaking changes. Check the planned release with `pnpm run changeset:status`.

After a change reaches `main` and verification passes, the Release workflow opens or updates a version PR. It updates the package version, changelog, and bundled skill versions, then dispatches CI on the generated branch. Merge that PR after its checks pass to publish through GitHub Actions and npm trusted publishing.

The npm trusted publisher for `@tanstack/markdown` must allow direct publishing from GitHub repository `TanStack/markdown`, workflow `release.yml`, with no environment name. No npm token is stored in the repository. GitHub Actions must be allowed to create pull requests.

`release:version` and `release:publish` are automation commands. If publishing is interrupted, inspect the workflow logs and npm before retrying. The Release workflow can be dispatched on `main`, and Changesets skips versions already on npm. Never bump or publish locally to work around a failed workflow.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ A tiny, fast, deterministic Markdown parser and renderer for blogs and documenta

- 4.9 KB gzip parser
- 6.7 KB gzip HTML renderer
- 6.7 KB gzip React adapter
- 6.7 KB gzip Octane adapter
- 6.6 KB gzip React adapter
- 6.6 KB gzip Octane adapter
- zero runtime dependencies
- serializable AST
- safe defaults for raw HTML and executable URLs
- optional docs extensions and external syntax highlighting
- optional AI streaming profile

Bundle sizes include the parser and exclude framework runtimes and syntax highlighters.

```bash
pnpm add @tanstack/markdown
```
Expand Down
6 changes: 3 additions & 3 deletions docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ These repository benchmarks bundle representative browser entry points from pinn
| --- | ---: | ---: |
| `@tanstack/markdown/parser` | 4.9 KB | 4.6 KB |
| `@tanstack/markdown/html` | 6.7 KB | 6.2 KB |
| `@tanstack/markdown/react` | 6.7 KB | 6.1 KB |
| `@tanstack/markdown/react` | 6.6 KB | 6.1 KB |
| React with streaming extension | 6.8 KB | 6.3 KB |
| `@tanstack/markdown/octane` | 6.7 KB | 6.1 KB |
| `@tanstack/markdown/octane` | 6.6 KB | 6.1 KB |
| Marked | 12.5 KB | 11.5 KB |
| micromark | 15.4 KB | 13.7 KB |
| markdown-wasm JS + WASM | 31.3 KB | 26.4 KB |
Expand All @@ -56,7 +56,7 @@ The comparison does not represent equivalent feature sets. It shows the cost of

## Compatibility accounting

TanStack Markdown currently matches 348 of 652 CommonMark 0.31.2 examples after serializer normalization. That 53.4% figure is accounting, not a conformance claim or a target to maximize. The project also preserves selected official GFM examples for tables, task lists, and strikethrough.
The generated [compatibility report](../reports/conformance.md) tracks matches against all 652 CommonMark 0.31.2 examples after serializer normalization. Every established match is protected individually by the regression suite. The count is accounting, not a conformance claim or a target to maximize. Selected official GFM examples separately cover tables, task lists, and strikethrough.

Use [commonmark.js](https://github.com/commonmark/commonmark.js), micromark, or a unified pipeline when exact specification behavior is a requirement. Use TanStack Markdown when your corpus fits the [documented profile](core-concepts/syntax-profile) and the smaller, controlled renderer is the better product tradeoff.

Expand Down
8 changes: 8 additions & 0 deletions docs/core-concepts/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ The tested [TanStack Highlight adapter](../guides/syntax-highlighting#tanstack-h

An extension `renderHtml` hook also returns trusted HTML. React and Octane component replacements can enforce application-specific link, image, and navigation policies, but those replacements are outside core renderer parity.

## Document ASTs

Renderers trust document ASTs supplied directly by the application. URL screening happens during Markdown parsing, not when rendering an arbitrary link or image node. Do not accept untrusted JSON as a document AST without validating its structure and applying your URL and HTML policies.

## Resource limits

The core limits parser nesting and inline delimiter scans. These are not a limit on total input size, footnote count, or work performed by extensions. Bound untrusted document sizes in the application, and batch streaming updates instead of rerendering on every incoming character.

## Untrusted content

For user-generated Markdown:
Expand Down
4 changes: 3 additions & 1 deletion docs/guides/docs-preset.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const html = renderHtml(document, {
})
```

The preset is a separate 2.4 KB gzip entry and is not imported by the parser or renderers.
The preset is a separate 2.3 KB gzip entry and is not imported by the parser or renderers.

## Callouts

Expand All @@ -30,6 +30,8 @@ The preset is a separate 2.4 KB gzip entry and is not imported by the parser or

This produces a `CalloutNode` and renders `markdown-alert`, `markdown-alert-title`, and `markdown-alert-content` classes.

Keep blank lines inside a callout quoted with `>`. An unquoted blank line ends the callout. Custom titles are plain text; put Markdown links in the body.

## Heading collection

Heading collection adds table-of-contents data to `document.headings`:
Expand Down
8 changes: 4 additions & 4 deletions docs/guides/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ The generated browser bundle report records:
| --- | ---: | ---: |
| parser | 4.9 KB | 4.6 KB |
| HTML renderer | 6.7 KB | 6.2 KB |
| React adapter | 6.7 KB | 6.1 KB |
| Octane adapter | 6.7 KB | 6.1 KB |
| React adapter | 6.6 KB | 6.1 KB |
| Octane adapter | 6.6 KB | 6.1 KB |
| React adapter with streaming extension | 6.8 KB | 6.3 KB |
| Streaming extension | 0.3 KB | 0.3 KB |
| docs preset | 2.4 KB | 2.2 KB |
| callouts extension | 0.4 KB | 0.3 KB |
| docs preset | 2.3 KB | 2.1 KB |
| callouts extension | 0.3 KB | 0.3 KB |

Framework runtimes are externalized from their adapters, and the highlighter measurement uses only a callback stub. Exact generated bytes live in the [size report](../../reports/sizes.md).

Expand Down
16 changes: 16 additions & 0 deletions docs/guides/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,26 @@ The command compares output against CommonMark 0.31.2 examples, preserves every

Selected official GFM examples separately cover tables, task lists, and strikethrough in the supported profile.

## Comparing a change

Compare the working tree with a Git revision or a saved directory containing `src/`, using the same compiler and runtime:

```bash
node --import tsx scripts/compare-revision.mjs main
```

The script measures minified, gzip, and Brotli sizes, lists gained and lost CommonMark matches, and alternates old and new implementations over nine timing samples. It includes React and Octane node creation and SSR, and retains the medians and every sample in `artifacts/audit-comparison.json`. Run it without other CPU-heavy work. Small timing differences can still be noise, and Node results do not establish browser performance.

Size tests protect both narrow imports and the complete namespace of every public entry point. Their ceilings are the smaller audited working tree, not the earlier npm release. A smaller gzip bundle does not excuse growth in minified or Brotli bytes.

After running both corpus audits, add `--corpus` to compare both revisions in core and docs-extension modes against the same source files and Marked version. Use `--no-bench` when checking only output and size. Review changed output even when a file already differs from Marked. The corpus classification is a triage tool, not proof that two outputs behave identically.

## Resilience

The suite includes fixed-seed generated malformed documents and adversarial cases such as unmatched delimiters, deep blockquotes, nested lists, and parser depth exhaustion. Performance thresholds are generous enough for CI variation but strict enough to catch accidental superlinear behavior.

Synchronous hang regressions, including oversized numeric code-line ranges, run in a subprocess with a deadline. An ordinary test timeout cannot interrupt a blocked JavaScript event loop.

The AI streaming profile renders every character prefix of a representative response and verifies deterministic output, React/HTML parity, safe handling of executable URLs and raw HTML, unfinished-fence behavior, trailing-placeholder suppression, and bounded progressive rendering.

## Documentation coverage
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@
"skills:check-version": "node scripts/sync-skill-version.mjs --check",
"skills:sync-version": "node scripts/sync-skill-version.mjs",
"skills:stale": "intent stale",
"changeset": "changeset",
"changeset:status": "changeset status",
"release:version": "changeset version && pnpm run skills:sync-version",
"release:publish": "pnpm run verify && changeset publish",
"conformance": "tsx scripts/conformance.ts",
"bench": "tsx scripts/bench.ts",
"size": "tsx scripts/measure-size.ts",
Expand All @@ -108,6 +112,7 @@
}
},
"devDependencies": {
"@changesets/cli": "3.0.2",
"@tanstack/highlight": "0.0.6",
"@tanstack/intent": "^0.3.6",
"@types/node": "^24.0.0",
Expand Down
Loading
Loading