Skip to content

Commit 7075f00

Browse files
JoshuaKGoldbergnzakasmdjermanovic
authored
feat: allow specifying filename in block meta (#318)
* 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]>
1 parent 1091da8 commit 7075f00

File tree

6 files changed

+163
-2
lines changed

6 files changed

+163
-2
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,20 @@ In order to see `@eslint/markdown` work its magic within Markdown code blocks in
141141

142142
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.
143143

144+
## File Name Details
145+
146+
This processor will use file names from blocks if a `filename` meta is present.
147+
148+
For example, the following block will result in a parsed file name of `src/index.js`:
149+
150+
````md
151+
```js filename="src/index.js"
152+
export const value = "Hello, world!";
153+
```
154+
````
155+
156+
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"`.
157+
144158
## Contributing
145159

146160
```sh

src/processor.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,19 @@ function getBlockRangeMap(text, node, comments) {
241241
return rangeMap;
242242
}
243243

244+
const codeBlockFileNameRegex = /filename=(?<quote>["'])(?<filename>.*?)\1/u;
245+
246+
/**
247+
* Parses the file name from a block meta, if available.
248+
* @param {Block} block A code block.
249+
* @returns {string | null | undefined} The filename, if parsed from block meta.
250+
*/
251+
function fileNameFromMeta(block) {
252+
return block.meta
253+
?.match(codeBlockFileNameRegex)
254+
?.groups.filename.replaceAll(/\s+/gu, "_");
255+
}
256+
244257
const languageToFileExtension = {
245258
javascript: "js",
246259
ecmascript: "js",
@@ -328,7 +341,7 @@ function preprocess(sourceText, filename) {
328341
: language;
329342

330343
return {
331-
filename: `${index}.${fileExtension}`,
344+
filename: fileNameFromMeta(block) ?? `${index}.${fileExtension}`,
332345
text: [...block.comments, block.value, ""].join("\n"),
333346
};
334347
});

src/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export interface BlockBase {
1313
rangeMap: RangeMap[];
1414
}
1515

16-
export interface Block extends Node, BlockBase {}
16+
export interface Block extends Node, BlockBase {
17+
meta: string | null;
18+
}
1719

1820
export type Message = Linter.LintMessage;
1921

tests/fixtures/filename.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Test
2+
3+
```js filename="a/b/C D E.js"
4+
console.log("...");
5+
```

tests/plugin.test.js

+15
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,21 @@ describe("FlatESLint", () => {
14361436
assert.strictEqual(results[0].messages[4].column, 2);
14371437
});
14381438

1439+
it("parses when file name includes lowercase, uppercase, slashes, and spaces", async () => {
1440+
const results = await eslint.lintFiles([
1441+
path.resolve(__dirname, "./fixtures/filename.md"),
1442+
]);
1443+
1444+
assert.strictEqual(results.length, 1);
1445+
assert.strictEqual(results[0].messages.length, 1);
1446+
assert.strictEqual(
1447+
results[0].messages[0].message,
1448+
"Unexpected console statement.",
1449+
);
1450+
assert.strictEqual(results[0].messages[0].line, 4);
1451+
assert.strictEqual(results[0].messages[0].column, 1);
1452+
});
1453+
14391454
// https://github.com/eslint/markdown/issues/181
14401455
it("should work when called on nested code blocks in the same file", async () => {
14411456
/*

tests/processor.test.js

+112
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,118 @@ describe("processor", () => {
345345
assert.strictEqual(blocks[0].filename, "0.js");
346346
});
347347

348+
it("should parse a double-quoted filename from meta", () => {
349+
const code =
350+
prefix +
351+
[
352+
'``` js filename="abc.js"',
353+
"var answer = 6 * 7;",
354+
"```",
355+
].join("\n");
356+
const blocks = processor.preprocess(code);
357+
358+
assert.strictEqual(blocks.length, 1);
359+
assert.strictEqual(blocks[0].filename, "abc.js");
360+
});
361+
362+
it("should parse a single-quoted filename from meta", () => {
363+
const code =
364+
prefix +
365+
[
366+
"``` js filename='abc.js'",
367+
"var answer = 6 * 7;",
368+
"```",
369+
].join("\n");
370+
const blocks = processor.preprocess(code);
371+
372+
assert.strictEqual(blocks.length, 1);
373+
assert.strictEqual(blocks[0].filename, "abc.js");
374+
});
375+
376+
it("should parse a double-quoted filename in a directory from meta", () => {
377+
const code =
378+
prefix +
379+
[
380+
'``` js filename="abc/def.js"',
381+
"var answer = 6 * 7;",
382+
"```",
383+
].join("\n");
384+
const blocks = processor.preprocess(code);
385+
386+
assert.strictEqual(blocks.length, 1);
387+
assert.strictEqual(blocks[0].filename, "abc/def.js");
388+
});
389+
390+
it("should parse a single-quoted filename in a directory from meta", () => {
391+
const code =
392+
prefix +
393+
[
394+
"``` js filename='abc/def.js'",
395+
"var answer = 6 * 7;",
396+
"```",
397+
].join("\n");
398+
const blocks = processor.preprocess(code);
399+
400+
assert.strictEqual(blocks.length, 1);
401+
assert.strictEqual(blocks[0].filename, "abc/def.js");
402+
});
403+
404+
it("should parse a filename with lowercase, uppercase, slashes, and spaces", () => {
405+
const code =
406+
prefix +
407+
[
408+
"``` js filename='a/_b/C D E\tF \t G.js'",
409+
"var answer = 6 * 7;",
410+
"```",
411+
].join("\n");
412+
const blocks = processor.preprocess(code);
413+
414+
assert.strictEqual(blocks.length, 1);
415+
assert.strictEqual(blocks[0].filename, "a/_b/C_D_E_F_G.js");
416+
});
417+
418+
it("should parse a filename each from two meta", () => {
419+
const code =
420+
prefix +
421+
[
422+
"``` js filename='abc/def.js'",
423+
"var answer = 6 * 7;",
424+
"```",
425+
"",
426+
"``` js filename='abc/def.js'",
427+
"var answer = 6 * 7;",
428+
"```",
429+
].join("\n");
430+
const blocks = processor.preprocess(code);
431+
432+
assert.strictEqual(blocks.length, 2);
433+
assert.strictEqual(blocks[0].filename, "abc/def.js");
434+
assert.strictEqual(blocks[1].filename, "abc/def.js");
435+
});
436+
437+
for (const [descriptor, meta] of [
438+
["a blank", "filename"],
439+
["a numeric", "filename=123"],
440+
["a null", "filename=null"],
441+
["an undefined", "filename=undefined"],
442+
["a improperly quoted", "filename='abc.js\""],
443+
["an uppercase FILENAME", "FILENAME='abc.js'"],
444+
]) {
445+
it(`should not parse ${descriptor} filename from meta`, () => {
446+
const code =
447+
prefix +
448+
[
449+
`\`\`\` js ${meta}`,
450+
"var answer = 6 * 7;",
451+
"```",
452+
].join("\n");
453+
const blocks = processor.preprocess(code);
454+
455+
assert.strictEqual(blocks.length, 1);
456+
assert.strictEqual(blocks[0].filename, "0.js");
457+
});
458+
}
459+
348460
it("should ignore trailing whitespace in the info string", () => {
349461
const code =
350462
prefix +

0 commit comments

Comments
 (0)