Skip to content

Commit

Permalink
feat: allow specifying filename in block meta (#318)
Browse files Browse the repository at this point in the history
* feat: allow specifying filename in block meta

* Update package.json

* Add tests for filename with lowercase, uppercase, slashes, and spaces

* Include rudimentary docs

* Remove spaces too

* Apply suggestions from code review

Co-authored-by: Nicholas C. Zakas <[email protected]>

* Update tests/processor.test.js

Co-authored-by: Milos Djermanovic <[email protected]>

* fix: backtick-quotes are invalid syntax

---------

Co-authored-by: Nicholas C. Zakas <[email protected]>
Co-authored-by: Milos Djermanovic <[email protected]>
  • Loading branch information
3 people authored Mar 4, 2025
1 parent 1091da8 commit 7075f00
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 2 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ In order to see `@eslint/markdown` work its magic within Markdown code blocks in

However, this reports a problem when viewing Markdown which does not have configuration, so you may wish to use the cursor scope "source.embedded.js", but note that `@eslint/markdown` configuration comments and skip directives won't work in this context.

## File Name Details

This processor will use file names from blocks if a `filename` meta is present.

For example, the following block will result in a parsed file name of `src/index.js`:

````md
```js filename="src/index.js"
export const value = "Hello, world!";
```
````

This can be useful for user configurations that include linting overrides for specific file paths. In this example, you could then target the specific code block in your configuration using `"file-name.md/*src/index.js"`.

## Contributing

```sh
Expand Down
15 changes: 14 additions & 1 deletion src/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,19 @@ function getBlockRangeMap(text, node, comments) {
return rangeMap;
}

const codeBlockFileNameRegex = /filename=(?<quote>["'])(?<filename>.*?)\1/u;

/**
* Parses the file name from a block meta, if available.
* @param {Block} block A code block.
* @returns {string | null | undefined} The filename, if parsed from block meta.
*/
function fileNameFromMeta(block) {
return block.meta
?.match(codeBlockFileNameRegex)
?.groups.filename.replaceAll(/\s+/gu, "_");
}

const languageToFileExtension = {
javascript: "js",
ecmascript: "js",
Expand Down Expand Up @@ -328,7 +341,7 @@ function preprocess(sourceText, filename) {
: language;

return {
filename: `${index}.${fileExtension}`,
filename: fileNameFromMeta(block) ?? `${index}.${fileExtension}`,
text: [...block.comments, block.value, ""].join("\n"),
};
});
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export interface BlockBase {
rangeMap: RangeMap[];
}

export interface Block extends Node, BlockBase {}
export interface Block extends Node, BlockBase {
meta: string | null;
}

export type Message = Linter.LintMessage;

Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/filename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Test

```js filename="a/b/C D E.js"
console.log("...");
```
15 changes: 15 additions & 0 deletions tests/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,21 @@ describe("FlatESLint", () => {
assert.strictEqual(results[0].messages[4].column, 2);
});

it("parses when file name includes lowercase, uppercase, slashes, and spaces", async () => {
const results = await eslint.lintFiles([
path.resolve(__dirname, "./fixtures/filename.md"),
]);

assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].messages.length, 1);
assert.strictEqual(
results[0].messages[0].message,
"Unexpected console statement.",
);
assert.strictEqual(results[0].messages[0].line, 4);
assert.strictEqual(results[0].messages[0].column, 1);
});

// https://github.com/eslint/markdown/issues/181
it("should work when called on nested code blocks in the same file", async () => {
/*
Expand Down
112 changes: 112 additions & 0 deletions tests/processor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,118 @@ describe("processor", () => {
assert.strictEqual(blocks[0].filename, "0.js");
});

it("should parse a double-quoted filename from meta", () => {
const code =
prefix +
[
'``` js filename="abc.js"',
"var answer = 6 * 7;",
"```",
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 1);
assert.strictEqual(blocks[0].filename, "abc.js");
});

it("should parse a single-quoted filename from meta", () => {
const code =
prefix +
[
"``` js filename='abc.js'",
"var answer = 6 * 7;",
"```",
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 1);
assert.strictEqual(blocks[0].filename, "abc.js");
});

it("should parse a double-quoted filename in a directory from meta", () => {
const code =
prefix +
[
'``` js filename="abc/def.js"',
"var answer = 6 * 7;",
"```",
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 1);
assert.strictEqual(blocks[0].filename, "abc/def.js");
});

it("should parse a single-quoted filename in a directory from meta", () => {
const code =
prefix +
[
"``` js filename='abc/def.js'",
"var answer = 6 * 7;",
"```",
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 1);
assert.strictEqual(blocks[0].filename, "abc/def.js");
});

it("should parse a filename with lowercase, uppercase, slashes, and spaces", () => {
const code =
prefix +
[
"``` js filename='a/_b/C D E\tF \t G.js'",
"var answer = 6 * 7;",
"```",
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 1);
assert.strictEqual(blocks[0].filename, "a/_b/C_D_E_F_G.js");
});

it("should parse a filename each from two meta", () => {
const code =
prefix +
[
"``` js filename='abc/def.js'",
"var answer = 6 * 7;",
"```",
"",
"``` js filename='abc/def.js'",
"var answer = 6 * 7;",
"```",
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 2);
assert.strictEqual(blocks[0].filename, "abc/def.js");
assert.strictEqual(blocks[1].filename, "abc/def.js");
});

for (const [descriptor, meta] of [
["a blank", "filename"],
["a numeric", "filename=123"],
["a null", "filename=null"],
["an undefined", "filename=undefined"],
["a improperly quoted", "filename='abc.js\""],
["an uppercase FILENAME", "FILENAME='abc.js'"],
]) {
it(`should not parse ${descriptor} filename from meta`, () => {
const code =
prefix +
[
`\`\`\` js ${meta}`,
"var answer = 6 * 7;",
"```",
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 1);
assert.strictEqual(blocks[0].filename, "0.js");
});
}

it("should ignore trailing whitespace in the info string", () => {
const code =
prefix +
Expand Down

0 comments on commit 7075f00

Please sign in to comment.