Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update: compatibility with eslint-mdx #178

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ module.exports = {
"overrides": [
{
"files": ["**/*.md"],
"processor": "markdown/markdown"
"extends": ["plugin:mdx/recommended"],
"processor": "markdown/markdown",
"rules": {
"mdx/remark": 0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The related PR has not been merged and released, disable temporarily.

}
},
{
"files": ["**/*.md/*.js"],
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ node_modules/
npm-debug.log
.eslint-release-info.json
.nyc_output
package-lock.json
yarn.lock
types
83 changes: 67 additions & 16 deletions lib/processor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/**
* @fileoverview Processes Markdown files for consumption by ESLint.
* @author Brandon Mills
*
* @typedef {import('unist').Node} ASTNode
* @typedef {ASTNode & {children: ASTNode[]}} ASTParentNode
* @typedef {import('eslint').Linter.LintMessage} Message
* @typedef {ASTNode & {baseIndentText: string, comments: string[], rangeMap: Array<{js: number, md: number}>}} Block
*/

"use strict";
Expand All @@ -16,13 +21,13 @@ const SUPPORTS_AUTOFIX = true;

const markdown = unified().use(remarkParse);

let blocks = [];
Copy link
Contributor Author

@JounQin JounQin Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blocks will not work when writing markdown codes in code blocks.

Lifecycle:

preprocess('.md') -> preprocess('.md/0_0.md') -> postprocess('.md/0_0.md') -> postprocess('.md')

This was not a problem before because there was no markdown -> md mapper before, so .markdown files were not linted actually.

const blocksCache = {};

/**
* Performs a depth-first traversal of the Markdown AST.
* @param {ASTNode} node A Markdown AST node.
* @param {Object} callbacks A map of node types to callbacks.
* @param {Object} [parent] The node's parent AST node.
* @param {{[key: string]: (node: ASTNode, parent: ASTParentNode) => void}} callbacks A map of node types to callbacks.
* @param {ASTParentNode} [parent] The node's parent AST node.
* @returns {void}
*/
function traverse(node, callbacks, parent) {
Expand All @@ -40,7 +45,7 @@ function traverse(node, callbacks, parent) {
/**
* Converts leading HTML comments to JS block comments.
* @param {string} html The text content of an HTML AST node.
* @returns {string[]} An array of JS block comments.
* @returns {string} JS block comment.
*/
function getComment(html) {
const commentStart = "<!--";
Expand Down Expand Up @@ -116,9 +121,9 @@ function getIndentText(text, node) {
* delta at the beginning of each line.
* @param {string} text The text of the file.
* @param {ASTNode} node A Markdown code block AST node.
* @param {comments} comments List of configuration comment strings that will be
* @param {string[]} comments List of configuration comment strings that will be
* inserted at the beginning of the code block.
* @returns {Object[]} A list of offset-based adjustments, where lookups are
* @returns {Array<{js: number, md: number}>} A list of offset-based adjustments, where lookups are
* done based on the `js` key, which represents the range in the linted JS,
* and the `md` key is the offset delta that, when added to the JS range,
* returns the corresponding location in the original Markdown source.
Expand All @@ -128,7 +133,7 @@ function getBlockRangeMap(text, node, comments) {
/*
* The parser sets the fenced code block's start offset to wherever content
* should normally begin (typically the first column of the line, but more
* inside a list item, for example). The code block's opening fance may be
* inside a list item, for example). The code block's opening fancy may be
* further indented by up to three characters. If the code block has
* additional indenting, the opening fence's first backtick may be up to
* three whitespace characters after the start offset.
Expand Down Expand Up @@ -208,15 +213,52 @@ function getBlockRangeMap(text, node, comments) {
return rangeMap;
}

const LANGUAGES_MAPPER = {
javascript: "js",
javascriptreact: "jsx",
typescript: "ts",
typescriptreact: "tsx",
markdown: "md",
mdown: "md",
mkdn: "md"
};

/**
* get last item from array
* @template T
* @param {T[]} items array
* @returns {T} last item
*/
function last(items) {
return items && items[items.length - 1];
}

/**
* get short language
* @param {string} lang original language
* @returns {string} short language
*/
function getShortLang(lang) {
const language = last(
lang.split(/\s/u)[0].split(".")
).toLowerCase();

return LANGUAGES_MAPPER[language] || language;
}


/**
* Extracts lintable JavaScript code blocks from Markdown text.
* @param {string} text The text of the file.
* @returns {string[]} Source code strings to lint.
* @param {string} filename The filename of the file
* @returns {Array<string | {text: string, filename: string}>} Source code strings to lint.
*/
function preprocess(text) {
function preprocess(text, filename) {
const ast = markdown.parse(text);
const blocks = [];

blocksCache[filename] = blocks;

blocks = [];
traverse(ast, {
code(node, parent) {
const comments = [];
Expand Down Expand Up @@ -252,7 +294,7 @@ function preprocess(text) {
});

return blocks.map((block, index) => ({
filename: `${index}.${block.lang.trim().split(" ")[0]}`,
filename: `${index}.${getShortLang(block.lang)}`,
text: [
...block.comments,
block.value,
Expand All @@ -264,7 +306,7 @@ function preprocess(text) {
/**
* Creates a map function that adjusts messages in a code block.
* @param {Block} block A code block.
* @returns {Function} A function that adjusts messages in a code block.
* @returns {(message: Message) => Message} A function that adjusts messages in a code block.
*/
function adjustBlock(block) {
const leadingCommentLines = block.comments.reduce((count, comment) => count + comment.split("\n").length, 0);
Expand Down Expand Up @@ -330,13 +372,22 @@ function excludeUnsatisfiableRules(message) {

/**
* Transforms generated messages for output.
* @param {Array<Message[]>} messages An array containing one array of messages
* for each code block returned from `preprocess`.
* @param {Array<Message[]>} messages An array containing one array of messages for each code block returned from `preprocess`.
* @param {string} filename The filename of the file
* @returns {Message[]} A flattened array of messages with mapped locations.
*/
function postprocess(messages) {
function postprocess(messages, filename) {
const blocks = blocksCache[filename] || [];

return [].concat(...messages.map((group, i) => {
const adjust = adjustBlock(blocks[i]);
const block = blocks[i];

// non code block message, parsed by `eslint-mdx` for example
if (!block) {
return group;
}

const adjust = adjustBlock(block);

return group.map(adjust).filter(excludeUnsatisfiableRules);
}));
Expand Down
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,36 @@
"prepare": "node ./npm-prepare.js",
"test": "npm run lint && npm run test-cov",
"test-cov": "nyc _mocha -- -c tests/{examples,lib}/**/*.js",
"types": "tsc lib/*.js -d --allowJs --emitDeclarationOnly --lib ES2015 --outDir types",
"generate-release": "eslint-generate-release",
"generate-alpharelease": "eslint-generate-prerelease alpha",
"generate-betarelease": "eslint-generate-prerelease beta",
"generate-rcrelease": "eslint-generate-prerelease rc",
"publish-release": "eslint-publish-release"
},
"main": "index.js",
"types": "types/index.d.ts",
"files": [
"index.js",
"lib/index.js",
"lib/processor.js"
"lib",
"types"
],
"devDependencies": {
"@types/eslint": "^7.2.7",
"chai": "^4.2.0",
"eslint": "^6.8.0",
"eslint-config-eslint": "^6.0.0",
"eslint-plugin-jsdoc": "^15.9.5",
"eslint-plugin-mdx": "^1.9.1",
"eslint-plugin-node": "^9.0.0",
"eslint-release": "^3.1.2",
"mocha": "^6.2.2",
"nyc": "^14.1.1"
"nyc": "^14.1.1",
"typescript": "^4.2.3"
},
"dependencies": {
"remark-parse": "^5.0.0",
"unified": "^6.1.2"
"remark-parse": ">=5.0.0 <9.0.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9.0 has breaking change, its parser is rewritten totally.

"unified": ">=6.0.0"
},
"peerDependencies": {
"eslint": ">=6.0.0"
Expand Down
10 changes: 5 additions & 5 deletions tests/lib/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe("processor", () => {
assert.strictEqual(blocks.length, 2);
assert.strictEqual(blocks[0].filename, "0.js");
assert.strictEqual(blocks[0].text, "backticks\n");
assert.strictEqual(blocks[1].filename, "1.javascript");
assert.strictEqual(blocks[1].filename, "1.js");
assert.strictEqual(blocks[1].text, "tildes\n");
});

Expand Down Expand Up @@ -199,7 +199,7 @@ describe("processor", () => {

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

it("should ignore code fences with unspecified info string", () => {
Expand Down Expand Up @@ -234,7 +234,7 @@ describe("processor", () => {
const blocks = processor.preprocess(code);

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

it("should find code fences with node info string", () => {
Expand Down Expand Up @@ -270,7 +270,7 @@ describe("processor", () => {
const blocks = processor.preprocess(code);

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

it("should ignore anything after the first word of the info string", () => {
Expand Down Expand Up @@ -398,7 +398,7 @@ describe("processor", () => {
assert.strictEqual(blocks.length, 2);
assert.strictEqual(blocks[0].filename, "0.js");
assert.strictEqual(blocks[0].text, "var answer = 6 * 7;\n");
assert.strictEqual(blocks[1].filename, "1.javascript");
assert.strictEqual(blocks[1].filename, "1.js");
assert.strictEqual(blocks[1].text, "console.log(answer);\n");
});

Expand Down