Processor for extracting fenced code blocks from Markdown files to enable individual linting of embedded JavaScript, TypeScript, and other code languages.
The processor extracts fenced code blocks from Markdown files and presents them as separate virtual files to ESLint for individual linting.
/**
* ESLint processor for Markdown files
* Extracts code blocks for individual linting
*/
interface Processor {
/**
* Processor metadata
*/
meta: {
name: "@eslint/markdown/markdown";
version: "7.2.0";
};
/**
* Extract code blocks from Markdown content
* @param text - The Markdown file content
* @param filename - The Markdown file name
* @returns Array of extracted code blocks as virtual files
*/
preprocess(text: string, filename: string): Array<{
text: string;
filename: string;
}>;
/**
* Combine lint messages from code blocks back to original positions
* @param messages - Array of message arrays from each code block
* @param filename - The original Markdown file name
* @returns Combined messages with corrected positions
*/
postprocess(
messages: Array<Message[]>,
filename: string
): Message[];
/**
* Whether the processor supports ESLint's --fix flag
*/
supportsAutofix: true;
}Usage Example:
// Using the processor configuration
import markdown from "@eslint/markdown";
export default [
...markdown.configs.processor
];// Manual processor configuration
import markdown from "@eslint/markdown";
export default [
{
plugins: { markdown }
},
{
files: ["**/*.md"],
processor: "markdown/markdown"
},
{
files: ["**/*.md/*.js"],
rules: {
"no-console": "off",
"no-undef": "off"
}
}
];The processor automatically extracts fenced code blocks from Markdown files based on their language identifiers.
Supported Code Block Formats:
```javascript
// This will be extracted as .js file
console.log("Hello, world!");
```
```typescript
// This will be extracted as .ts file
const message: string = "Hello, TypeScript!";
```
```jsx
// This will be extracted as .jsx file
const element = <div>Hello, React!</div>;
```
```python
# This will be extracted as .py file
print("Hello, Python!")
```Each extracted code block becomes a virtual file with a predictable naming pattern:
/**
* Virtual filename pattern for extracted code blocks
* Format: {original-filename}/{block-index}.{extension}
*
* Examples:
* - README.md/0.js (first JavaScript block in README.md)
* - docs/api.md/1.ts (second TypeScript block in docs/api.md)
* - guide.md/0.jsx (first JSX block in guide.md)
*/
interface VirtualFile {
text: string;
filename: string;
}Language Mapping:
javascript, ecmascript → .jstypescript → .tsjsx → .jsxtsx → .tsxpython → .pyjava → .javac → .ccpp, c++ → .cppmarkdown, md → .mdThe processor supports ESLint configuration comments in HTML format immediately preceding code blocks:
<!-- eslint-disable no-console -->
<!-- eslint-env node -->
```javascript
console.log("This won't trigger no-console rule");
```
<!-- global jQuery -->
```javascript
jQuery("#element").hide();
```Use the special <!-- eslint-skip --> comment to prevent a code block from being processed:
<!-- eslint-skip -->
```javascript
{
// This block won't be linted (useful for JSON with comments)
"config": "value"
}
```
```
### Filename Meta Support
Code blocks can specify virtual filenames using the `filename` meta attribute:
````markdown
```javascript filename="src/utils.js"
export function helper() {
return "Hello!";
}
```This allows ESLint configurations to target specific virtual files:
// Target specific virtual files
export default [
{
files: ["*.md/**/utils.js"],
rules: {
"prefer-const": "error"
}
}
];The processor supports ESLint's --fix flag and can automatically fix issues in code blocks:
eslint --fix README.mdFixed code is properly merged back into the original Markdown content while preserving formatting and indentation.
Lint messages from code blocks are transformed back to the original Markdown file positions.
/**
* ESLint message with position information
*/
interface Message {
ruleId?: string;
severity: 1 | 2;
message: string;
line: number;
column: number;
nodeType?: string;
source?: string;
endLine?: number;
endColumn?: number;
fix?: Fix;
}
/**
* Auto-fix information
*/
interface Fix {
range: [number, number];
text: string;
}/**
* Code block with metadata and range mapping
*/
interface Block extends Code {
/**
* Base indentation text for the code block
*/
baseIndentText: string;
/**
* Configuration comments preceding the block
*/
comments: string[];
/**
* Range mapping between Markdown and extracted code positions
*/
rangeMap: RangeMap[];
}
/**
* Position mapping between different coordinate systems
*/
interface RangeMap {
/**
* Indentation offset
*/
indent: number;
/**
* JavaScript code position
*/
js: number;
/**
* Markdown source position
*/
md: number;
}/**
* Internal state for tracking code blocks during processing
*/
interface ProcessorState {
/**
* Cache of extracted blocks by filename
*/
blocksCache: Map<string, Block[]>;
/**
* Whether autofix is supported
*/
supportsAutofix: boolean;
/**
* Rules that cannot be satisfied in code blocks
*/
unsatisfiableRules: Set<string>;
}import markdown from "@eslint/markdown";
export default [
...markdown.configs.processor
];import markdown from "@eslint/markdown";
export default [
{
plugins: { markdown }
},
{
// Enable processor for all .md files
files: ["**/*.md"],
processor: "markdown/markdown"
},
{
// Configure JavaScript code blocks
files: ["**/*.md/*.js"],
languageOptions: {
parserOptions: {
ecmaFeatures: {
impliedStrict: true
}
}
},
rules: {
"no-undef": "off",
"no-unused-vars": "off",
"no-console": "off"
}
},
{
// Configure TypeScript code blocks
files: ["**/*.md/*.ts"],
rules: {
"@typescript-eslint/no-unused-vars": "off"
}
}
];export default [
{
// Target specific virtual files
files: ["README.md/*.js"],
rules: {
"prefer-const": "error"
}
},
{
// Target all JS blocks in docs directory
files: ["docs/**/*.md/*.js"],
rules: {
"no-console": "warn"
}
}
];