Skip to content

Commit 35a979c

Browse files
lumirlumirnzakas
andauthored
feat: support front matter (#328)
* wip: install `mdast-util-frontmatter` and `micromark-extension-frontmatter` * wip: update `types.ts` * wip: update `markdown-language.js` * wip: refactor `markdown-language.js` * wip: update `README.md` * wip: add some tests * wip: add more tests * wip: update `README.md` * wip: update comments in `markdown-language.js` * wip: update `README.md` * wip: fix typos in `README.md` * wip: update `types.ts` * wip: update `markdown-language.js` * wip: update `markdown-language.test.js` * wip: update README.md Co-authored-by: Nicholas C. Zakas <[email protected]> * wip: update README.md Co-authored-by: Nicholas C. Zakas <[email protected]> * wip: rename `mdastExtensions` to `MdastExtensions` --------- Co-authored-by: Nicholas C. Zakas <[email protected]>
1 parent 13dfd5e commit 35a979c

File tree

5 files changed

+292
-14
lines changed

5 files changed

+292
-14
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,41 @@ export default [
121121
];
122122
```
123123

124+
### Language Options
125+
126+
#### Enabling Front Matter in both `commonmark` and `gfm`
127+
128+
By default, Markdown parsers do not support [front matter](https://jekyllrb.com/docs/front-matter/). To enable front matter in both `commonmark` and `gfm`, you can use the `frontmatter` option in `languageOptions`.
129+
130+
> `@eslint/markdown` internally uses [`micromark-extension-frontmatter`](https://github.com/micromark/micromark-extension-frontmatter) and [`mdast-util-frontmatter`](https://github.com/syntax-tree/mdast-util-frontmatter) to parse front matter.
131+
132+
| **Option Value** | **Description** |
133+
|------------------|------------------------------------------------------------|
134+
| `false` | Disables front matter parsing in Markdown files. (Default) |
135+
| `"yaml"` | Enables YAML front matter parsing in Markdown files. |
136+
| `"toml"` | Enables TOML front matter parsing in Markdown files. |
137+
138+
```js
139+
// eslint.config.js
140+
import markdown from "@eslint/markdown";
141+
142+
export default [
143+
{
144+
files: ["**/*.md"],
145+
plugins: {
146+
markdown
147+
},
148+
language: "markdown/gfm",
149+
languageOptions: {
150+
frontmatter: "yaml", // Or pass `"toml"` to enable TOML front matter parsing.
151+
},
152+
rules: {
153+
"markdown/no-html": "error"
154+
}
155+
}
156+
];
157+
```
158+
124159
### Processors
125160

126161
| **Processor Name** | **Description** |

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@
8383
"@eslint/core": "^0.10.0",
8484
"@eslint/plugin-kit": "^0.2.5",
8585
"mdast-util-from-markdown": "^2.0.2",
86+
"mdast-util-frontmatter": "^2.0.1",
8687
"mdast-util-gfm": "^3.0.0",
88+
"micromark-extension-frontmatter": "^2.0.0",
8789
"micromark-extension-gfm": "^3.0.0"
8890
},
8991
"engines": {

src/language/markdown-language.js

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,68 @@
1111

1212
import { MarkdownSourceCode } from "./markdown-source-code.js";
1313
import { fromMarkdown } from "mdast-util-from-markdown";
14+
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
1415
import { gfmFromMarkdown } from "mdast-util-gfm";
16+
import { frontmatter } from "micromark-extension-frontmatter";
1517
import { gfm } from "micromark-extension-gfm";
1618

1719
//-----------------------------------------------------------------------------
1820
// Types
1921
//-----------------------------------------------------------------------------
2022

2123
/** @typedef {import("mdast").Root} RootNode */
24+
/** @typedef {import("mdast-util-from-markdown").Options['extensions']} Extensions */
25+
/** @typedef {import("mdast-util-from-markdown").Options['mdastExtensions']} MdastExtensions */
2226
/** @typedef {import("@eslint/core").Language} Language */
2327
/** @typedef {import("@eslint/core").File} File */
2428
/** @typedef {import("@eslint/core").ParseResult<RootNode>} ParseResult */
2529
/** @typedef {import("@eslint/core").OkParseResult<RootNode>} OkParseResult */
30+
/** @typedef {import("../types.ts").MarkdownLanguageOptions} MarkdownLanguageOptions */
31+
/** @typedef {import("../types.ts").MarkdownLanguageContext} MarkdownLanguageContext */
2632
/** @typedef {"commonmark"|"gfm"} ParserMode */
2733

34+
//-----------------------------------------------------------------------------
35+
// Helpers
36+
//-----------------------------------------------------------------------------
37+
38+
/**
39+
* Create parser options based on `mode` and `languageOptions`.
40+
* @param {ParserMode} mode The markdown parser mode.
41+
* @param {MarkdownLanguageOptions} languageOptions Language options.
42+
* @returns {{extensions: Extensions, mdastExtensions: MdastExtensions}} Parser options for micromark and mdast
43+
*/
44+
function createParserOptions(mode, languageOptions) {
45+
/** @type {Extensions} */
46+
const extensions = [];
47+
/** @type {MdastExtensions} */
48+
const mdastExtensions = [];
49+
50+
// 1. `mode`: Add GFM extensions if mode is "gfm"
51+
if (mode === "gfm") {
52+
extensions.push(gfm());
53+
mdastExtensions.push(gfmFromMarkdown());
54+
}
55+
56+
// 2. `languageOptions.frontmatter`: Handle frontmatter options
57+
const frontmatterOption = languageOptions?.frontmatter;
58+
59+
// Skip frontmatter entirely if false
60+
if (frontmatterOption !== false) {
61+
if (frontmatterOption === "yaml") {
62+
extensions.push(frontmatter(["yaml"]));
63+
mdastExtensions.push(frontmatterFromMarkdown(["yaml"]));
64+
} else if (frontmatterOption === "toml") {
65+
extensions.push(frontmatter(["toml"]));
66+
mdastExtensions.push(frontmatterFromMarkdown(["toml"]));
67+
}
68+
}
69+
70+
return {
71+
extensions,
72+
mdastExtensions,
73+
};
74+
}
75+
2876
//-----------------------------------------------------------------------------
2977
// Exports
3078
//-----------------------------------------------------------------------------
@@ -58,6 +106,14 @@ export class MarkdownLanguage {
58106
*/
59107
nodeTypeKey = "type";
60108

109+
/**
110+
* Default language options. User-defined options are merged with this object.
111+
* @type {MarkdownLanguageOptions}
112+
*/
113+
defaultLanguageOptions = {
114+
frontmatter: false,
115+
};
116+
61117
/**
62118
* The Markdown parser mode.
63119
* @type {ParserMode}
@@ -75,24 +131,33 @@ export class MarkdownLanguage {
75131
}
76132
}
77133

78-
/* eslint-disable no-unused-vars -- Required to complete interface. */
79134
/**
80135
* Validates the language options.
81-
* @param {Object} languageOptions The language options to validate.
136+
* @param {MarkdownLanguageOptions} languageOptions The language options to validate.
82137
* @returns {void}
83138
* @throws {Error} When the language options are invalid.
84139
*/
85140
validateLanguageOptions(languageOptions) {
86-
// no-op
141+
const frontmatterOption = languageOptions?.frontmatter;
142+
const validFrontmatterOptions = new Set([false, "yaml", "toml"]);
143+
144+
if (
145+
frontmatterOption !== undefined &&
146+
!validFrontmatterOptions.has(frontmatterOption)
147+
) {
148+
throw new Error(
149+
`Invalid language option value \`${frontmatterOption}\` for frontmatter.`,
150+
);
151+
}
87152
}
88-
/* eslint-enable no-unused-vars -- Required to complete interface. */
89153

90154
/**
91155
* Parses the given file into an AST.
92156
* @param {File} file The virtual file to parse.
157+
* @param {MarkdownLanguageContext} context The options to use for parsing.
93158
* @returns {ParseResult} The result of parsing.
94159
*/
95-
parse(file) {
160+
parse(file, context) {
96161
// Note: BOM already removed
97162
const text = /** @type {string} */ (file.body);
98163

@@ -103,13 +168,10 @@ export class MarkdownLanguage {
103168
* problem that ESLint identified just like any other.
104169
*/
105170
try {
106-
const options =
107-
this.#mode === "gfm"
108-
? {
109-
extensions: [gfm()],
110-
mdastExtensions: [gfmFromMarkdown()],
111-
}
112-
: { extensions: [] };
171+
const options = createParserOptions(
172+
this.#mode,
173+
context?.languageOptions,
174+
);
113175
const root = fromMarkdown(text, options);
114176

115177
return {

src/types.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import type {
1414
} from "mdast";
1515
import type { Linter } from "eslint";
1616
import type {
17+
LanguageOptions,
18+
LanguageContext,
1719
RuleDefinition,
1820
RuleVisitor,
1921
SourceLocation,
@@ -55,12 +57,27 @@ export type Message = Linter.LintMessage;
5557

5658
export type RuleType = "problem" | "suggestion" | "layout";
5759

60+
/**
61+
* Language options provided for Markdown files.
62+
*/
63+
export interface MarkdownLanguageOptions extends LanguageOptions {
64+
/**
65+
* The options for parsing frontmatter.
66+
*/
67+
frontmatter?: false | "yaml" | "toml";
68+
}
69+
70+
/**
71+
* The context object that is passed to the Markdown language plugin methods.
72+
*/
73+
export type MarkdownLanguageContext = LanguageContext<MarkdownLanguageOptions>;
74+
5875
/**
5976
* The `SourceCode` interface for Markdown files.
6077
*/
6178
export interface IMarkdownSourceCode
6279
extends TextSourceCode<{
63-
LangOptions: {};
80+
LangOptions: MarkdownLanguageOptions;
6481
RootNode: Root;
6582
SyntaxElementWithLoc: Node;
6683
ConfigNode: { value: string; position: SourceLocation };
@@ -103,7 +120,7 @@ export type MarkdownRuleDefinition<
103120
> = RuleDefinition<
104121
// Language specific type options (non-configurable)
105122
{
106-
LangOptions: {};
123+
LangOptions: MarkdownLanguageOptions;
107124
Code: IMarkdownSourceCode;
108125
Visitor: MarkdownRuleVisitor;
109126
Node: Node;

0 commit comments

Comments
 (0)