diff --git a/README.md b/README.md
index 0e10de9f..59f69609 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,10 @@
> If you like this package, please give it a star on [GitHub](https://github.com/lumirlumir/npm-eslint-markdown)!⭐
> Your support helps us improve and maintain the project.
-Lint your Markdown with ESLint. Additional rules for use with `@eslint/markdown`.🛠️
+Lint your Markdown with ESLint.
+
+- Additional rules for use with `@eslint/markdown`.🛠️
+- A blazing fast Markdown parser for ESLint.⚡
## Documentation
@@ -31,6 +34,18 @@ This [`eslint-markdown`](https://github.com/lumirlumir/npm-eslint-markdown#readm
So, we **highly recommend** using the `eslint-markdown` plugin alongside ESLint's built-in Markdown support, `@eslint/markdown`.
+## Included Packages
+
+This repository is maintained as a monorepo and includes the following two packages.
+
+### `eslint-markdown` - [Docs](https://eslint-markdown.lumir.page/docs/packages/eslint-markdown) | [Repository](https://github.com/lumirlumir/npm-eslint-markdown/tree/main/packages/eslint-markdown) | [npm](https://www.npmjs.com/package/eslint-markdown)
+
+Lint your Markdown with ESLint. Additional rules for use with `@eslint/markdown`.🛠️
+
+### `@eslint-markdown/parser` - [Docs](https://eslint-markdown.lumir.page/docs/packages/parser) | [Repository](https://github.com/lumirlumir/npm-eslint-markdown/tree/main/packages/parser) | [npm](https://www.npmjs.com/package/@eslint-markdown/parser)
+
+A blazing fast Markdown parser for ESLint.⚡
+
## Code of Conduct
See [Code of Conduct](https://github.com/lumirlumir/.github/blob/main/CODE_OF_CONDUCT.md#contributor-covenant-code-of-conduct).
diff --git a/package-lock.json b/package-lock.json
index b423118a..c8e4e13a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -380,6 +380,140 @@
"node": ">=18"
}
},
+ "node_modules/@bruits/satteri-darwin-arm64": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz",
+ "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@bruits/satteri-darwin-x64": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz",
+ "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@bruits/satteri-linux-arm64-gnu": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz",
+ "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@bruits/satteri-linux-arm64-musl": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz",
+ "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@bruits/satteri-linux-x64-gnu": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz",
+ "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@bruits/satteri-linux-x64-musl": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz",
+ "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@bruits/satteri-wasm32-wasi": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz",
+ "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.2.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@bruits/satteri-win32-arm64-msvc": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz",
+ "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@bruits/satteri-win32-x64-msvc": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz",
+ "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@codecov/bundler-plugin-core": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-2.0.1.tgz",
@@ -419,6 +553,37 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
@@ -903,6 +1068,10 @@
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
+ "node_modules/@eslint-markdown/parser": {
+ "resolved": "packages/parser",
+ "link": true
+ },
"node_modules/@eslint/config-array": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
@@ -1295,6 +1464,27 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
+ "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
+ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
+ }
+ },
"node_modules/@next/eslint-plugin-next": {
"version": "16.2.10",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.10.tgz",
@@ -1974,6 +2164,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -2005,14 +2205,21 @@
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
"node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dev": true,
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
@@ -8588,6 +8795,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/satteri": {
+ "version": "0.10.5",
+ "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz",
+ "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.5",
+ "@types/hast": "^3.0.5",
+ "@types/mdast": "^4.0.4",
+ "@types/unist": "^3.0.3"
+ },
+ "optionalDependencies": {
+ "@bruits/satteri-darwin-arm64": "0.10.5",
+ "@bruits/satteri-darwin-x64": "0.10.5",
+ "@bruits/satteri-linux-arm64-gnu": "0.10.5",
+ "@bruits/satteri-linux-arm64-musl": "0.10.5",
+ "@bruits/satteri-linux-x64-gnu": "0.10.5",
+ "@bruits/satteri-linux-x64-musl": "0.10.5",
+ "@bruits/satteri-wasm32-wasi": "0.10.5",
+ "@bruits/satteri-win32-arm64-msvc": "0.10.5",
+ "@bruits/satteri-win32-x64-msvc": "0.10.5"
+ }
+ },
+ "node_modules/satteri/node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
@@ -9253,6 +9489,13 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -10246,6 +10489,26 @@
"dev": true,
"license": "MIT"
},
+ "packages/parser": {
+ "name": "@eslint-markdown/parser",
+ "version": "0.0.0-canary.0",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.4",
+ "satteri": "^0.10.5"
+ },
+ "devDependencies": {
+ "@eslint/markdown": "^7.5.1",
+ "eslint": "^9.39.5",
+ "vitest": "^4.1.9"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/lumirlumir"
+ }
+ },
"website": {
"devDependencies": {
"@codecov/bundler-plugin-core": "^2.0.1",
diff --git a/package.json b/package.json
index cd59e610..2f67488d 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,10 @@
"test:types": "tsc -b",
"test:unit": "vitest run",
"test:pkg:em": "npm run test -w packages/eslint-markdown",
- "build": "npm run build:pkg:em && npm run build:w",
+ "test:pkg:p": "npm run test -w packages/parser",
+ "build": "npm run build:pkg:p && npm run build:pkg:em && npm run build:w",
"build:pkg:em": "npm run build -w packages/eslint-markdown",
+ "build:pkg:p": "npm run build -w packages/parser",
"build:w": "npm run build -w website",
"build:w:vp": "npm run build:vp -w website",
"build:w:em": "npm run build:em -w website",
diff --git a/packages/parser/package.json b/packages/parser/package.json
new file mode 100644
index 00000000..bfc0b800
--- /dev/null
+++ b/packages/parser/package.json
@@ -0,0 +1,75 @@
+{
+ "name": "@eslint-markdown/parser",
+ "version": "0.0.0-canary.0",
+ "type": "module",
+ "sideEffects": false,
+ "description": "A blazing fast Markdown parser for ESLint.⚡",
+ "exports": {
+ ".": {
+ "types": "./build/index.d.ts",
+ "default": "./build/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "./build/*"
+ ],
+ ".": [
+ "./build/index.d.ts"
+ ]
+ }
+ },
+ "files": [
+ "build",
+ "LICENSE.md",
+ "README.md"
+ ],
+ "keywords": [
+ "eslint",
+ "eslintparser",
+ "eslint-parser",
+ "parser",
+ "markdown",
+ "mark",
+ "md",
+ "commonmark",
+ "gfm"
+ ],
+ "author": "lumir (https://github.com/lumirlumir)",
+ "funding": "https://github.com/sponsors/lumirlumir",
+ "license": "MIT",
+ "homepage": "https://eslint-markdown.lumir.page",
+ "repository": {
+ "type": "github",
+ "url": "git+https://github.com/lumirlumir/npm-eslint-markdown.git",
+ "directory": "packages/parser"
+ },
+ "bugs": {
+ "url": "https://github.com/lumirlumir/npm-eslint-markdown/issues"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "publishConfig": {
+ "access": "public",
+ "provenance": true
+ },
+ "scripts": {
+ "prepublishOnly": "npm run build",
+ "build": "tsc -b tsconfig.build.json && node ../../scripts/cp.js ../../LICENSE.md LICENSE.md ../../README.md README.md",
+ "test": "npm run test:types && npm run test:unit",
+ "test:types": "tsc -b ./tsconfig.json",
+ "test:unit": "vitest run"
+ },
+ "dependencies": {
+ "@types/mdast": "^4.0.4",
+ "satteri": "^0.10.5"
+ },
+ "devDependencies": {
+ "@eslint/markdown": "^7.5.1",
+ "eslint": "^9.39.5",
+ "vitest": "^4.1.9"
+ }
+}
diff --git a/packages/parser/src/index.test-d.ts b/packages/parser/src/index.test-d.ts
new file mode 100644
index 00000000..fdcd03f3
--- /dev/null
+++ b/packages/parser/src/index.test-d.ts
@@ -0,0 +1,136 @@
+/**
+ * @fileoverview Type test for `index.ts`.
+ */
+
+// --------------------------------------------------------------------------------
+// Import
+// --------------------------------------------------------------------------------
+
+import parser, {
+ meta,
+ parse,
+ type Json,
+ type JsonData,
+ type ParseOptions,
+} from '@eslint-markdown/parser';
+import type {
+ Json as ESLintMarkdownJson,
+ JsonData as ESLintMarkdownJsonData,
+ MarkdownLanguageOptions,
+} from '@eslint/markdown';
+import type { Data, FrontmatterContent, PhrasingContent, Root, RootContent } from 'mdast';
+
+// --------------------------------------------------------------------------------
+// Test
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// #region Json (type)
+
+({}) as Json satisfies Extract;
+({}) as Json satisfies Extract;
+
+({}) as Json satisfies ESLintMarkdownJson;
+
+// #endregion Json (type)
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// #region JsonData (type)
+
+({}) as JsonData satisfies Data;
+
+({}) as JsonData satisfies ESLintMarkdownJsonData;
+
+// #endregion JsonData (type)
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// #region satteri mdast augmentation (type)
+
+'toml' satisfies RootContent['type'];
+'math' satisfies RootContent['type'];
+'inlineMath' satisfies PhrasingContent['type'];
+
+// #endregion satteri mdast augmentation (type)
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// #region ParseOptions (type)
+
+let parseOptions: ParseOptions;
+
+parseOptions = {};
+parseOptions = {
+ mode: 'commonmark',
+};
+parseOptions = {
+ mode: 'gfm',
+};
+parseOptions = {
+ frontmatter: false,
+};
+parseOptions = {
+ frontmatter: 'yaml',
+};
+parseOptions = {
+ frontmatter: 'toml',
+};
+parseOptions = {
+ frontmatter: 'json',
+};
+parseOptions = {
+ math: false,
+};
+parseOptions = {
+ math: true,
+};
+parseOptions = {
+ mode: 'gfm',
+ frontmatter: 'json',
+ math: true,
+};
+
+// @ts-expect-error -- `mode` only accepts supported Markdown modes.
+parseOptions = { mode: 'mdx' };
+// @ts-expect-error -- `frontmatter` does not accept `true`.
+parseOptions = { frontmatter: true };
+// @ts-expect-error -- `frontmatter` only accepts supported front matter formats.
+parseOptions = { frontmatter: 'xml' };
+// @ts-expect-error -- `math` must be a boolean.
+parseOptions = { math: 'true' };
+
+({}) as MarkdownLanguageOptions['frontmatter'] satisfies ParseOptions['frontmatter'];
+
+// #endregion ParseOptions (type)
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// #region meta (named export)
+
+meta.name satisfies string;
+meta.name satisfies '@eslint-markdown/parser';
+
+meta.version satisfies string;
+
+// #endregion meta (named export)
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// #region parse (named export)
+
+parse satisfies Function;
+parse satisfies (text: string, options?: ParseOptions) => Root;
+
+// #endregion parse (named export)
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// #region parser (default export)
+
+parser satisfies object;
+parser.meta satisfies typeof meta;
+parser.parse satisfies typeof parse;
+
+// #endregion parser (default export)
+// --------------------------------------------------------------------------------
diff --git a/packages/parser/src/index.test.ts b/packages/parser/src/index.test.ts
new file mode 100644
index 00000000..74bfc6fc
--- /dev/null
+++ b/packages/parser/src/index.test.ts
@@ -0,0 +1,224 @@
+/**
+ * @fileoverview Tests for `index.ts`.
+ */
+
+// --------------------------------------------------------------------------------
+// Import
+// --------------------------------------------------------------------------------
+
+import { assert, describe, it } from 'vitest';
+import parser, { meta, parse } from './index.js';
+import pkg from '../package.json' with { type: 'json' };
+
+// --------------------------------------------------------------------------------
+// Test
+// --------------------------------------------------------------------------------
+
+describe('index', () => {
+ describe('named export', () => {
+ describe('meta', () => {
+ it('should have a `name` property', () => {
+ assert.strictEqual(typeof meta.name, 'string');
+ assert.strictEqual(meta.name, pkg.name);
+ });
+
+ it('should have a `version` property', () => {
+ assert.strictEqual(typeof meta.version, 'string');
+ assert.strictEqual(meta.version, pkg.version);
+ });
+ });
+
+ describe('parse', () => {
+ it('should be a function', () => {
+ assert.strictEqual(typeof parse, 'function');
+ });
+
+ it('should parse Markdown as CommonMark by default', () => {
+ const ast = parse('~hi~');
+
+ assert(ast.type === 'root');
+ assert.strictEqual(ast.children.length, 1);
+
+ const paragraph = ast.children[0];
+
+ assert(paragraph.type === 'paragraph');
+ assert.strictEqual(paragraph.children.length, 1);
+
+ const text = paragraph.children[0];
+
+ assert(text.type === 'text');
+ assert.strictEqual(text.value, '~hi~');
+ });
+
+ it("should parse Markdown as CommonMark when `mode` is set to `'commonmark'`", () => {
+ const ast = parse('~hi~', { mode: 'commonmark' });
+
+ assert(ast.type === 'root');
+ assert.strictEqual(ast.children.length, 1);
+
+ const paragraph = ast.children[0];
+
+ assert(paragraph.type === 'paragraph');
+ assert.strictEqual(paragraph.children.length, 1);
+
+ const text = paragraph.children[0];
+
+ assert(text.type === 'text');
+ assert.strictEqual(text.value, '~hi~');
+ });
+
+ it("should parse Markdown as GFM when `mode` is set to `'gfm'`", () => {
+ const ast = parse('~hi~', { mode: 'gfm' });
+
+ assert(ast.type === 'root');
+ assert.strictEqual(ast.children.length, 1);
+
+ const paragraph = ast.children[0];
+
+ assert(paragraph.type === 'paragraph');
+ assert.strictEqual(paragraph.children.length, 1);
+
+ const del = paragraph.children[0];
+
+ assert(del.type === 'delete');
+ assert.strictEqual(del.children.length, 1);
+
+ const text = del.children[0];
+
+ assert(text.type === 'text');
+ assert.strictEqual(text.value, 'hi');
+ });
+
+ it('should not parse frontmatter when `frontmatter` is set to `false`', () => {
+ const ast = parse('---\ntitle: Example\n---\n\n# Hello', {
+ frontmatter: false,
+ });
+
+ assert(ast.type === 'root');
+
+ const firstChild = ast.children[0];
+
+ assert(firstChild.type === 'thematicBreak');
+ });
+
+ it("should parse YAML frontmatter when `frontmatter` is set to `'yaml'`", () => {
+ const ast = parse('---\ntitle: Example\n---\n\n# Hello', {
+ frontmatter: 'yaml',
+ });
+
+ assert(ast.type === 'root');
+
+ const frontmatter = ast.children[0];
+
+ assert(frontmatter.type === 'yaml');
+ assert.strictEqual(frontmatter.value, 'title: Example');
+ });
+
+ it("should parse TOML frontmatter when `frontmatter` is set to `'toml'`", () => {
+ const ast = parse('+++\ntitle = "Example"\n+++\n\n# Hello', {
+ frontmatter: 'toml',
+ });
+
+ assert(ast.type === 'root');
+
+ const frontmatter = ast.children[0];
+
+ assert(frontmatter.type === 'toml');
+ assert.strictEqual(frontmatter.value, 'title = "Example"');
+ });
+
+ it("should parse JSON frontmatter when `frontmatter` is set to `'json'`", () => {
+ const ast = parse('---\n{"title": "Example"}\n---\n\n# Hello', {
+ frontmatter: 'json',
+ });
+
+ assert(ast.type === 'root');
+
+ const frontmatter = ast.children[0];
+
+ assert(frontmatter.type === 'json');
+ assert.strictEqual(frontmatter.value, '{"title": "Example"}');
+ });
+
+ it("should not parse TOML frontmatter when `frontmatter` is set to `'yaml'`", () => {
+ const ast = parse('+++\ntitle = "Example"\n+++', {
+ frontmatter: 'yaml',
+ });
+
+ assert(ast.type === 'root');
+
+ const firstChild = ast.children[0];
+
+ assert(firstChild.type === 'paragraph');
+ });
+
+ it("should not parse YAML frontmatter when `frontmatter` is set to `'toml'`", () => {
+ const ast = parse('---\ntitle: Example\n---', {
+ frontmatter: 'toml',
+ });
+
+ assert(ast.type === 'root');
+
+ const firstChild = ast.children[0];
+
+ assert(firstChild.type === 'thematicBreak');
+ });
+
+ it("should not parse TOML frontmatter when `frontmatter` is set to `'json'`", () => {
+ const ast = parse('+++\ntitle = "Example"\n+++', {
+ frontmatter: 'json',
+ });
+
+ assert(ast.type === 'root');
+
+ const firstChild = ast.children[0];
+
+ assert(firstChild.type === 'paragraph');
+ });
+
+ it("should parse an empty document when `frontmatter` is set to `'json'`", () => {
+ const ast = parse('', { frontmatter: 'json' });
+
+ assert(ast.type === 'root');
+ assert.strictEqual(ast.children.length, 0);
+ });
+
+ it('should parse inline and block math when `math` is set to `true`', () => {
+ const ast = parse('Inline $x$ and block:\n\n$$\ny = 2\n$$', { math: true });
+
+ assert(ast.type === 'root');
+
+ const paragraph = ast.children[0];
+
+ assert(paragraph.type === 'paragraph');
+ assert.strictEqual(paragraph.children.length, 3);
+
+ const inlineMath = paragraph.children[1];
+
+ assert(inlineMath.type === 'inlineMath');
+ assert.strictEqual(inlineMath.value, 'x');
+
+ const math = ast.children[1];
+
+ assert(math.type === 'math');
+ assert.strictEqual(math.value, 'y = 2');
+ });
+ });
+ });
+
+ describe('default export', () => {
+ it('should have a `meta` property', () => {
+ assert.strictEqual(typeof parser.meta, 'object');
+
+ assert.strictEqual(typeof parser.meta.name, 'string');
+ assert.strictEqual(parser.meta.name, pkg.name);
+
+ assert.strictEqual(typeof parser.meta.version, 'string');
+ assert.strictEqual(parser.meta.version, pkg.version);
+ });
+
+ it('should have a `parse` property', () => {
+ assert.strictEqual(typeof parser.parse, 'function');
+ });
+ });
+});
diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts
new file mode 100644
index 00000000..4bdf3148
--- /dev/null
+++ b/packages/parser/src/index.ts
@@ -0,0 +1,168 @@
+/**
+ * @fileoverview Entry file for the `@eslint-markdown/parser` package.
+ * @see https://eslint.org/docs/latest/use/configure/parser
+ * @see https://eslint.org/docs/latest/extend/custom-parsers
+ */
+
+// --------------------------------------------------------------------------------
+// Reference Directive
+// --------------------------------------------------------------------------------
+
+///
+
+// --------------------------------------------------------------------------------
+// Import
+// --------------------------------------------------------------------------------
+
+import type { Data, Literal, Root } from 'mdast';
+import { markdownToMdast } from 'satteri';
+import pkg from '../package.json' with { type: 'json' };
+
+// --------------------------------------------------------------------------------
+// Typedef
+// --------------------------------------------------------------------------------
+
+/**
+ * Markdown JSON.
+ */
+export interface Json extends Literal {
+ /**
+ * Node type of mdast JSON.
+ */
+ type: 'json';
+ /**
+ * Data associated with the mdast JSON.
+ */
+ data?: JsonData | undefined;
+}
+
+/**
+ * Info associated with mdast JSON nodes by the ecosystem.
+ */
+export type JsonData = Data;
+
+/**
+ * Registers mdast JSON nodes as valid front matter and root content.
+ */
+declare module 'mdast' {
+ interface FrontmatterContentMap {
+ json: Json;
+ }
+
+ interface RootContentMap {
+ json: Json;
+ }
+}
+
+/**
+ * The options for parsing markdown.
+ */
+export interface ParseOptions {
+ /**
+ * The options for parsing markdown.
+ * @default 'commonmark'
+ */
+ mode?: 'commonmark' | 'gfm';
+
+ /**
+ * The options for parsing frontmatter.
+ * @default false
+ */
+ frontmatter?: false | 'yaml' | 'toml' | 'json';
+
+ /**
+ * The options for parsing math.
+ * @default false
+ */
+ math?: boolean;
+}
+
+// --------------------------------------------------------------------------------
+// Named Export
+// --------------------------------------------------------------------------------
+
+/**
+ * Metadata describing the parser.
+ */
+export const meta = {
+ /**
+ * The name of the parser.
+ */
+ name: '@eslint-markdown/parser',
+
+ /**
+ * The version of the parser.
+ */
+ version: pkg.version,
+} as const;
+
+/**
+ * Parse Markdown source text into an mdast syntax tree at blazing-fast speed.
+ *
+ * @param text The Markdown source text to parse.
+ * @param options The parsing options.
+ * @returns The mdast syntax tree.
+ * @example
+ * ```js
+ * import { parse } from '@eslint-markdown/parser';
+ *
+ * const ast = parse('# Hello, world!', {
+ * mode: 'commonmark',
+ * frontmatter: false,
+ * math: false,
+ * });
+ * ```
+ */
+export function parse(
+ text: string,
+ { mode = 'commonmark', frontmatter = false, math = false }: ParseOptions = {},
+): Root {
+ /*
+ * ESLint removes the BOM before calling the parser, so the BOM does not need to
+ * be handled here and `startsWith()` is safe to use.
+ * See: https://github.com/eslint/markdown/blob/v8.0.3/src/language/markdown-language.js#L209-L210
+ *
+ * We can also remove this logic once the following issue is resolved:
+ * https://github.com/bruits/satteri/issues/194
+ */
+ const frontmatterEnabled =
+ (frontmatter === 'toml' && text.startsWith('+++')) ||
+ ((frontmatter === 'yaml' || frontmatter === 'json') && text.startsWith('---'));
+
+ const ast = markdownToMdast(text, {
+ features: {
+ gfm: mode === 'gfm',
+ frontmatter: frontmatterEnabled,
+ math,
+ },
+ });
+
+ /*
+ * `satteri` does not support JSON front matter. Because JSON and YAML front
+ * matter both use `---` delimiters, we can reuse the YAML front matter handling.
+ *
+ * To ensure compatibility with `@eslint/markdown`, convert `yaml` node to a
+ * `json` node when `frontmatter` is set to `'json'`.
+ *
+ * Front matter can only appear at the beginning of a document, so we can safely
+ * assume that the root node's first child is the front matter node.
+ */
+ if (frontmatter === 'json' && ast.type === 'root' && ast.children[0]?.type === 'yaml') {
+ ast.children[0] = {
+ ...ast.children[0],
+ type: 'json',
+ };
+ }
+
+ // TODO: Remove `as Root` when https://github.com/bruits/satteri/issues/191 is resolved.
+ return ast as Root;
+}
+
+// --------------------------------------------------------------------------------
+// Default Export
+// --------------------------------------------------------------------------------
+
+export default {
+ meta,
+ parse,
+};
diff --git a/packages/parser/tsconfig.build.json b/packages/parser/tsconfig.build.json
new file mode 100644
index 00000000..90002812
--- /dev/null
+++ b/packages/parser/tsconfig.build.json
@@ -0,0 +1,35 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": [
+ "src/**/*.js",
+ "src/**/*.mjs",
+ "src/**/*.cjs",
+ "src/**/*.jsx",
+ "src/**/*.ts",
+ "src/**/*.mts",
+ "src/**/*.cts",
+ "src/**/*.tsx"
+ ],
+ "exclude": [
+ "src/**/*.test.js",
+ "src/**/*.test.mjs",
+ "src/**/*.test.cjs",
+ "src/**/*.test.ts",
+ "src/**/*.test.mts",
+ "src/**/*.test.cts",
+ "src/**/*.test-d.ts",
+ "src/**/*.test-d.mts",
+ "src/**/*.test-d.cts",
+ "src/tests/**/*.js",
+ "src/tests/**/*.ts",
+ "**/fixtures/**"
+ ],
+ "compilerOptions": {
+ /* Emit */
+ "emitDeclarationOnly": false,
+ "outDir": "build",
+
+ /* Projects */
+ "tsBuildInfoFile": "${configDir}/.tsbuildinfo/tsconfig.build.tsbuildinfo"
+ }
+}
diff --git a/packages/parser/tsconfig.json b/packages/parser/tsconfig.json
new file mode 100644
index 00000000..decbcf27
--- /dev/null
+++ b/packages/parser/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.build.json"
+ },
+ {
+ "path": "./tsconfig.test-d.json"
+ },
+ {
+ "path": "./tsconfig.test.json"
+ }
+ ]
+}
diff --git a/packages/parser/tsconfig.test-d.json b/packages/parser/tsconfig.test-d.json
new file mode 100644
index 00000000..93263822
--- /dev/null
+++ b/packages/parser/tsconfig.test-d.json
@@ -0,0 +1,21 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*.test-d.ts", "src/**/*.test-d.mts", "src/**/*.test-d.cts"],
+ "exclude": [],
+ "references": [
+ {
+ "path": "./tsconfig.build.json"
+ }
+ ],
+ "compilerOptions": {
+ /* Type Checking */
+ "noUnusedLocals": false, // Turning it off since it's a test.
+ "noUnusedParameters": false, // Turning it off since it's a test.
+
+ /* Emit */
+ "noEmit": true,
+
+ /* Projects */
+ "tsBuildInfoFile": "${configDir}/.tsbuildinfo/tsconfig.test-d.tsbuildinfo"
+ }
+}
diff --git a/packages/parser/tsconfig.test.json b/packages/parser/tsconfig.test.json
new file mode 100644
index 00000000..99668bf1
--- /dev/null
+++ b/packages/parser/tsconfig.test.json
@@ -0,0 +1,24 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": [
+ "src/**/*.test.js",
+ "src/**/*.test.mjs",
+ "src/**/*.test.cjs",
+ "src/**/*.test.ts",
+ "src/**/*.test.mts",
+ "src/**/*.test.cts"
+ ],
+ "exclude": ["**/fixtures/**"],
+ "references": [
+ {
+ "path": "./tsconfig.build.json"
+ }
+ ],
+ "compilerOptions": {
+ /* Emit */
+ "noEmit": true,
+
+ /* Projects */
+ "tsBuildInfoFile": "${configDir}/.tsbuildinfo/tsconfig.test.tsbuildinfo"
+ }
+}
diff --git a/packages/parser/vitest.config.js b/packages/parser/vitest.config.js
new file mode 100644
index 00000000..87d1c9bc
--- /dev/null
+++ b/packages/parser/vitest.config.js
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ include: ['src/**/*.test.{js,mjs,cjs,ts,mts,cts}'],
+
+ // Vitest's built-in type checking is still experimental, so we intentionally keep it disabled.
+ // I prefer the native TypeScript type-checking flow and rely on the repo's project references
+ // for better performance, familiar behavior, and more accurate diagnostics.
+ typecheck: {
+ enabled: false, // Set to true if you want to enable type checking during tests.
+ include: ['src/**/*.test-d.{ts,mts,cts,tsx}'],
+ },
+ },
+});
diff --git a/tsconfig.json b/tsconfig.json
index e89979be..c43caf5a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,6 +3,9 @@
"references": [
{
"path": "./packages/eslint-markdown/tsconfig.json"
+ },
+ {
+ "path": "./packages/parser/tsconfig.json"
}
]
}
diff --git a/website/.vitepress/config.js b/website/.vitepress/config.js
index 32731595..aa01bcd9 100644
--- a/website/.vitepress/config.js
+++ b/website/.vitepress/config.js
@@ -222,6 +222,10 @@ export default defineConfig({
text: 'eslint-markdown',
link: 'eslint-markdown',
},
+ {
+ text: '@eslint-markdown/parser',
+ link: 'parser',
+ },
],
},
diff --git a/website/docs/packages/parser.md b/website/docs/packages/parser.md
new file mode 100644
index 00000000..ed986a69
--- /dev/null
+++ b/website/docs/packages/parser.md
@@ -0,0 +1,64 @@
+---
+description: "Package documentation and configuration options for the `@eslint-markdown/parser` Markdown parser."
+---
+
+# `@eslint-markdown/parser`
+
+> A blazing fast Markdown parser for ESLint.
+
+`@eslint-markdown/parser` parses Markdown source text into an [mdast](https://github.com/syntax-tree/mdast#readme) syntax tree. It supports CommonMark and GitHub Flavored Markdown (GFM), with optional support for YAML, TOML, or JSON front matter and math syntax.
+
+## Usage
+
+The default export provides the parser metadata and the `parse` function. The same values are also available as named exports.
+
+```js
+import parser from '@eslint-markdown/parser';
+
+const ast = parser.parse('# Hello, world!', {
+ mode: 'gfm',
+ frontmatter: 'yaml',
+ math: true,
+});
+```
+
+## Configuration
+
+Pass parser options as the second argument to `parse`:
+
+```ts
+interface ParseOptions {
+ mode?: 'commonmark' | 'gfm';
+ frontmatter?: false | 'yaml' | 'toml' | 'json';
+ math?: boolean;
+}
+```
+
+### `mode`
+
+> Default: `'commonmark'`
+
+Controls the Markdown dialect. Use `'commonmark'` for [CommonMark](https://commonmark.org/) or `'gfm'` for [GitHub Flavored Markdown](https://github.github.com/gfm/).
+
+### `frontmatter`
+
+> Default: `false`
+
+Controls front matter parsing. Use `false` to disable front matter, `'yaml'` or `'json'` for front matter delimited by `---`, or `'toml'` for front matter delimited by `+++`.
+
+### `math`
+
+> Default: `false`
+
+Enables inline and block math syntax when set to `true`.
+
+## Exports
+
+| Name | Description |
+| :------------- | :-------------------------------------------------------------------------- |
+| `default` | Parser object containing `meta` and `parse`. |
+| `meta` | Object containing the package `name` and `version`. |
+| `parse` | Function that parses Markdown source text into an mdast `Root` syntax tree. |
+| `Json` | TypeScript type for JSON front matter nodes. |
+| `JsonData` | TypeScript type for data associated with JSON front matter nodes. |
+| `ParseOptions` | TypeScript type for the options accepted by `parse`. |