Skip to content

Commit 8b77e69

Browse files
authored
feat: add JSON frontmatter support (#411)
1 parent e86343c commit 8b77e69

5 files changed

Lines changed: 112 additions & 6 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ By default, Markdown parsers do not support [front matter](https://jekyllrb.com/
181181
| `false` | Disables front matter parsing in Markdown files. (Default) |
182182
| `"yaml"` | Enables YAML front matter parsing in Markdown files. |
183183
| `"toml"` | Enables TOML front matter parsing in Markdown files. |
184+
| `"json"` | Enables JSON front matter parsing in Markdown files. |
184185

185186
```js
186187
// eslint.config.js
@@ -195,7 +196,7 @@ export default defineConfig([
195196
},
196197
language: "markdown/gfm",
197198
languageOptions: {
198-
frontmatter: "yaml", // Or pass `"toml"` to enable TOML front matter parsing.
199+
frontmatter: "yaml", // Or pass `"toml"` or `"json"` to enable TOML or JSON front matter parsing.
199200
},
200201
rules: {
201202
"markdown/no-html": "error"

src/language/markdown-language.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ import { gfm } from "micromark-extension-gfm";
3535
// Helpers
3636
//-----------------------------------------------------------------------------
3737

38+
/**
39+
* Parser configuration for JSON frontmatter.
40+
* Example of supported frontmatter format:
41+
* ```markdown
42+
* ---
43+
* {
44+
* "title": "My Document",
45+
* "date": "2025-06-09"
46+
* }
47+
* ---
48+
* ```
49+
*/
50+
const jsonFrontmatterConfig = {
51+
type: "json",
52+
marker: "-",
53+
};
54+
3855
/**
3956
* Create parser options based on `mode` and `languageOptions`.
4057
* @param {ParserMode} mode The markdown parser mode.
@@ -64,6 +81,11 @@ function createParserOptions(mode, languageOptions) {
6481
} else if (frontmatterOption === "toml") {
6582
extensions.push(frontmatter(["toml"]));
6683
mdastExtensions.push(frontmatterFromMarkdown(["toml"]));
84+
} else if (frontmatterOption === "json") {
85+
extensions.push(frontmatter(jsonFrontmatterConfig));
86+
mdastExtensions.push(
87+
frontmatterFromMarkdown(jsonFrontmatterConfig),
88+
);
6789
}
6890
}
6991

@@ -139,7 +161,12 @@ export class MarkdownLanguage {
139161
*/
140162
validateLanguageOptions(languageOptions) {
141163
const frontmatterOption = languageOptions?.frontmatter;
142-
const validFrontmatterOptions = new Set([false, "yaml", "toml"]);
164+
const validFrontmatterOptions = new Set([
165+
false,
166+
"yaml",
167+
"toml",
168+
"json",
169+
]);
143170

144171
if (
145172
frontmatterOption !== undefined &&

src/types.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,33 @@ export interface Toml extends Literal {
101101
*/
102102
export interface TomlData extends Data {}
103103

104+
/**
105+
* Markdown JSON.
106+
*/
107+
export interface Json extends Literal {
108+
/**
109+
* Node type of mdast JSON.
110+
*/
111+
type: "json";
112+
/**
113+
* Data associated with the mdast JSON.
114+
*/
115+
data?: JsonData | undefined;
116+
}
117+
118+
/**
119+
* Info associated with mdast JSON nodes by the ecosystem.
120+
*/
121+
export interface JsonData extends Data {}
122+
104123
/**
105124
* Language options provided for Markdown files.
106125
*/
107126
export interface MarkdownLanguageOptions extends LanguageOptions {
108127
/**
109128
* The options for parsing frontmatter.
110129
*/
111-
frontmatter?: false | "yaml" | "toml";
130+
frontmatter?: false | "yaml" | "toml" | "json";
112131
}
113132

114133
/**
@@ -148,7 +167,8 @@ export interface MarkdownRuleVisitor
148167
| TableCell
149168
| TableRow
150169
| Yaml // Extensions (front matter)
151-
| Toml as NodeType["type"]]?: (
170+
| Toml
171+
| Json as NodeType["type"]]?: (
152172
node: NodeType,
153173
parent?: Parent,
154174
) => void;

tests/language/markdown-language.test.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import assert from "node:assert";
1616

1717
describe("MarkdownLanguage", () => {
1818
describe("validateLanguageOptions()", () => {
19-
it("should throw an error if `frontmatter` is not `false`, `'yaml'`, or `'toml'`", () => {
19+
it("should throw an error if `frontmatter` is not `false`, `'yaml'`, `'toml'`, or `'json'`", () => {
2020
const language = new MarkdownLanguage();
2121

2222
assert.throws(() => {
@@ -57,6 +57,9 @@ describe("MarkdownLanguage", () => {
5757
assert.doesNotThrow(() => {
5858
language.validateLanguageOptions({ frontmatter: "toml" });
5959
});
60+
assert.doesNotThrow(() => {
61+
language.validateLanguageOptions({ frontmatter: "json" });
62+
});
6063
});
6164

6265
it("should not throw an error when `frontmatter` has a correct value in gfm mode", () => {
@@ -71,6 +74,9 @@ describe("MarkdownLanguage", () => {
7174
assert.doesNotThrow(() => {
7275
language.validateLanguageOptions({ frontmatter: "toml" });
7376
});
77+
assert.doesNotThrow(() => {
78+
language.validateLanguageOptions({ frontmatter: "json" });
79+
});
7480
});
7581
});
7682

@@ -214,6 +220,56 @@ describe("MarkdownLanguage", () => {
214220
assert.strictEqual(result.ast.children[1].type, "heading");
215221
assert.strictEqual(result.ast.children[2].type, "paragraph");
216222
});
223+
224+
it("should parse JSON frontmatter in commonmark mode when `frontmatter: 'json'` is set", () => {
225+
const language = new MarkdownLanguage({ mode: "commonmark" });
226+
const result = language.parse(
227+
{
228+
body: '---\n{\n"title": "Hello"\n}\n---\n\n# Hello, World!\n\nHello, World!',
229+
path: "test.md",
230+
},
231+
{
232+
languageOptions: {
233+
frontmatter: "json",
234+
},
235+
},
236+
);
237+
238+
assert.strictEqual(result.ok, true);
239+
assert.strictEqual(result.ast.type, "root");
240+
assert.strictEqual(result.ast.children[0].type, "json");
241+
assert.strictEqual(
242+
result.ast.children[0].value,
243+
'{\n"title": "Hello"\n}',
244+
);
245+
assert.strictEqual(result.ast.children[1].type, "heading");
246+
assert.strictEqual(result.ast.children[2].type, "paragraph");
247+
});
248+
249+
it("should parse JSON frontmatter in gfm mode when `frontmatter: 'json'` is set", () => {
250+
const language = new MarkdownLanguage({ mode: "gfm" });
251+
const result = language.parse(
252+
{
253+
body: '---\n{\n"title": "Hello"\n}\n---\n\n# Hello, World!\n\nHello, World!',
254+
path: "test.md",
255+
},
256+
{
257+
languageOptions: {
258+
frontmatter: "json",
259+
},
260+
},
261+
);
262+
263+
assert.strictEqual(result.ok, true);
264+
assert.strictEqual(result.ast.type, "root");
265+
assert.strictEqual(result.ast.children[0].type, "json");
266+
assert.strictEqual(
267+
result.ast.children[0].value,
268+
'{\n"title": "Hello"\n}',
269+
);
270+
assert.strictEqual(result.ast.children[1].type, "heading");
271+
assert.strictEqual(result.ast.children[2].type, "paragraph");
272+
});
217273
});
218274

219275
describe("createSourceCode()", () => {

tests/types/types.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import markdown, {
77
SourceRange,
88
type RuleModule,
99
} from "@eslint/markdown";
10-
import { Toml } from "@eslint/markdown/types";
10+
import { Toml, Json } from "@eslint/markdown/types";
1111
import { ESLint, Linter } from "eslint";
1212
import type {
1313
// Nodes (abstract)
@@ -164,6 +164,8 @@ typeof processorPlugins satisfies {};
164164
"yaml:exit": (...args) => testVisitor<Yaml>(...args),
165165
toml: (...args) => testVisitor<Toml>(...args),
166166
"toml:exit": (...args) => testVisitor<Toml>(...args),
167+
json: (...args) => testVisitor<Json>(...args),
168+
"json:exit": (...args) => testVisitor<Json>(...args),
167169

168170
// Unknown selectors allowed
169171
"heading[depth=1]"(node: MarkdownNode, parent?: ParentNode) {},

0 commit comments

Comments
 (0)