eslint-plugin-mdx
Version:
ESLint Plugin for MDX
218 lines • 6.97 kB
JavaScript
import { fromMarkdown } from '../from-markdown.js';
import { meta } from '../meta.js';
const UNSATISFIABLE_RULES = new Set([
'eol-last',
'unicode-bom',
]);
const SUPPORTS_AUTOFIX = true;
const BOM = '\uFEFF';
const blocksCache = new Map();
function traverse(node, callbacks) {
if (callbacks[node.type]) {
callbacks[node.type](node);
}
else {
callbacks['*']();
}
const parent = node;
if ('children' in parent) {
for (const child of parent.children) {
traverse(child, callbacks);
}
}
}
const COMMENTS = [
[
/^<!-{2,}/,
/-{2,}>$/,
],
[
/^\/\*+/,
/\*+\/$/,
],
];
const eslintCommentRegex = /^(?:eslint\b|global\s)/u;
function getComment(value, isMdx = false) {
const [commentStart, commentEnd] = COMMENTS[+isMdx];
const commentStartMatched = commentStart.exec(value);
const commentEndMatched = commentEnd.exec(value);
if (commentStartMatched == null || commentEndMatched == null) {
return '';
}
const comment = value
.slice(commentStartMatched[0].length, -commentEndMatched[0].length)
.trim();
if (!eslintCommentRegex.test(comment)) {
return '';
}
return comment;
}
const leadingWhitespaceRegex = /^[>\s]*/u;
function getBeginningOfLineOffset(node) {
return node.position.start.offset - node.position.start.column + 1;
}
function getIndentText(text, node) {
return leadingWhitespaceRegex.exec(text.slice(getBeginningOfLineOffset(node)))[0];
}
function getBlockRangeMap(text, node, comments) {
const startOffset = getBeginningOfLineOffset(node);
const code = text.slice(startOffset, node.position.end.offset);
const lines = code.split('\n');
const baseIndent = getIndentText(text, node).length;
const commentLength = comments.reduce((len, comment) => len + comment.length + 1, 0);
const rangeMap = [
{
indent: baseIndent,
js: 0,
md: 0,
},
];
let jsOffset = commentLength;
let mdOffset = startOffset + lines[0].length + 1;
for (let i = 0; i + 1 < lines.length; i++) {
const line = lines[i + 1];
const leadingWhitespaceLength = leadingWhitespaceRegex.exec(line)[0].length;
const trimLength = Math.min(baseIndent, leadingWhitespaceLength);
rangeMap.push({
indent: trimLength,
js: jsOffset,
md: mdOffset + trimLength - jsOffset,
});
mdOffset += line.length + 1;
jsOffset += line.length - trimLength + 1;
}
return rangeMap;
}
const codeBlockFileNameRegex = /filename=(?<quote>["'])(?<filename>.*?)\1/u;
function fileNameFromMeta(block) {
return codeBlockFileNameRegex
.exec(block.meta)
?.groups.filename.replaceAll(/\s+/gu, '_');
}
function preprocess(sourceText, filename) {
const text = sourceText.startsWith(BOM) ? sourceText.slice(1) : sourceText;
const ast = fromMarkdown(text, filename.endsWith('.mdx'));
const blocks = [];
blocksCache.set(filename, blocks);
let allComments = [];
function mdxExpression(node) {
const comment = getComment(node.value, true);
if (comment) {
allComments.push(comment);
}
else {
allComments = [];
}
}
traverse(ast, {
'*'() {
allComments = [];
},
code(node) {
if (!node.lang) {
return;
}
const comments = [];
for (const comment of allComments) {
if (comment === 'eslint-skip') {
allComments = [];
return;
}
comments.push(`/* ${comment} */`);
}
allComments = [];
blocks.push({
...node,
baseIndentText: getIndentText(text, node),
comments,
rangeMap: getBlockRangeMap(text, node, comments),
});
},
html(node) {
const comment = getComment(node.value);
if (comment) {
allComments.push(comment);
}
else {
allComments = [];
}
},
mdxFlowExpression: mdxExpression,
mdxTextExpression: mdxExpression,
});
return blocks.map((block, index) => {
const [language] = block.lang.trim().split(' ');
return {
filename: fileNameFromMeta(block) ?? `${index}.${language}`,
text: [...block.comments, block.value, ''].join('\n'),
};
});
}
function adjustFix(block, fix) {
return {
range: fix.range.map(range => {
let i = 1;
while (i < block.rangeMap.length && block.rangeMap[i].js <= range) {
i++;
}
return range + block.rangeMap[i - 1].md;
}),
text: fix.text.replaceAll('\n', `\n${block.baseIndentText}`),
};
}
function adjustBlock(block) {
const leadingCommentLines = block.comments.reduce((count, comment) => count + comment.split('\n').length, 0);
const blockStart = block.position.start.line;
return function adjustMessage(message) {
if (!Number.isInteger(message.line)) {
return {
...message,
line: blockStart,
column: block.position.start.column,
};
}
const lineInCode = message.line - leadingCommentLines;
if (lineInCode < 1 || lineInCode >= block.rangeMap.length) {
return null;
}
const out = {
line: lineInCode + blockStart,
column: message.column + block.rangeMap[lineInCode].indent,
};
if (Number.isInteger(message.endLine)) {
out.endLine = message.endLine - leadingCommentLines + blockStart;
}
if (Array.isArray(message.suggestions)) {
out.suggestions = message.suggestions.map(suggestion => ({
...suggestion,
fix: adjustFix(block, suggestion.fix),
}));
}
const adjustedFix = {};
if (message.fix) {
adjustedFix.fix = adjustFix(block, message.fix);
}
return { ...message, ...out, ...adjustedFix };
};
}
function excludeUnsatisfiableRules(message) {
return message && !UNSATISFIABLE_RULES.has(message.ruleId);
}
function postprocess(messages, filename) {
const blocks = blocksCache.get(filename);
blocksCache.delete(filename);
return messages.flatMap((group, i) => {
const adjust = adjustBlock(blocks[i]);
return group.map(adjust).filter(excludeUnsatisfiableRules);
});
}
export const markdownProcessor = {
meta: {
name: 'mdx/markdown',
version: meta.version,
},
preprocess,
postprocess,
supportsAutofix: SUPPORTS_AUTOFIX,
};
//# sourceMappingURL=markdown.js.map