mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-22 05:40:48 +02:00
Re-implement MD038 to handle multi-line spans better and rely less on RegExp.
This commit is contained in:
parent
3b49414183
commit
ff50da3b42
7 changed files with 235 additions and 40 deletions
|
@ -70,7 +70,7 @@
|
||||||
"max-lines-per-function": "off",
|
"max-lines-per-function": "off",
|
||||||
"max-nested-callbacks": "error",
|
"max-nested-callbacks": "error",
|
||||||
"max-params": ["error", 9],
|
"max-params": ["error", 9],
|
||||||
"max-statements": ["error", 22],
|
"max-statements": ["error", 33],
|
||||||
"max-statements-per-line": "error",
|
"max-statements-per-line": "error",
|
||||||
"multiline-comment-style": ["error", "separate-lines"],
|
"multiline-comment-style": ["error", "separate-lines"],
|
||||||
"multiline-ternary": "off",
|
"multiline-ternary": "off",
|
||||||
|
|
67
lib/md038.js
67
lib/md038.js
|
@ -4,49 +4,42 @@
|
||||||
|
|
||||||
const shared = require("./shared");
|
const shared = require("./shared");
|
||||||
|
|
||||||
const inlineCodeSpansRe = /(?:^|[^\\])((`+)((?:[\s\S]*?[^`])|)\2(?!`))/g;
|
const startRe = /^\s([^`]|$)/;
|
||||||
|
const endRe = /[^`]\s$/;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"names": [ "MD038", "no-space-in-code" ],
|
"names": [ "MD038", "no-space-in-code" ],
|
||||||
"description": "Spaces inside code span elements",
|
"description": "Spaces inside code span elements",
|
||||||
"tags": [ "whitespace", "code" ],
|
"tags": [ "whitespace", "code" ],
|
||||||
"function": function MD038(params, onError) {
|
"function": function MD038(params, onError) {
|
||||||
let lastParent = null;
|
shared.filterTokens(params, "inline", (token) => {
|
||||||
shared.forEachInlineChild(params, "code_inline",
|
if (token.children.some((child) => child.type === "code_inline")) {
|
||||||
function forToken(token, parent) {
|
const tokenLines = params.lines.slice(token.map[0], token.map[1]);
|
||||||
if (lastParent !== parent) {
|
shared.forEachInlineCodeSpan(
|
||||||
lastParent = parent;
|
tokenLines.join("\n"),
|
||||||
inlineCodeSpansRe.lastIndex = 0;
|
(code, lineIndex, columnIndex, tickCount) => {
|
||||||
}
|
let rangeIndex = columnIndex - tickCount;
|
||||||
const match = inlineCodeSpansRe.exec(parent.content);
|
let rangeLength = code.length + (2 * tickCount);
|
||||||
const content = (match || [])[3];
|
let rangeLineOffset = 0;
|
||||||
const leftError = /^\s([^`]|$)/.test(content);
|
const codeLines = code.split(shared.newLineRe);
|
||||||
const rightError = /[^`]\s$/.test(content);
|
const left = startRe.test(code);
|
||||||
if (leftError || rightError) {
|
const right = !left && endRe.test(code);
|
||||||
const inlineCodeSpan = match[1];
|
if (right && (codeLines.length > 1)) {
|
||||||
const leftContent = parent.content.substr(0,
|
rangeIndex = 0;
|
||||||
match.index + (match[0].length - inlineCodeSpan.length));
|
rangeLineOffset = codeLines.length - 1;
|
||||||
const leftContentLines = leftContent.split(shared.newLineRe);
|
|
||||||
const inlineCodeSpanLines = inlineCodeSpan.split(shared.newLineRe);
|
|
||||||
let range = [
|
|
||||||
leftContentLines[leftContentLines.length - 1].length + 1,
|
|
||||||
inlineCodeSpanLines[0].length
|
|
||||||
];
|
|
||||||
if (leftError) {
|
|
||||||
shared.addErrorContext(onError, token.lineNumber,
|
|
||||||
inlineCodeSpan, true, false, range);
|
|
||||||
} else {
|
|
||||||
if (inlineCodeSpanLines.length > 1) {
|
|
||||||
range = [
|
|
||||||
1,
|
|
||||||
inlineCodeSpanLines[inlineCodeSpanLines.length - 1].length
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
shared.addErrorContext(onError,
|
if (left || right) {
|
||||||
token.lineNumber + content.split(shared.newLineRe).length - 1,
|
if (codeLines.length > 1) {
|
||||||
inlineCodeSpan, false, true, range);
|
rangeLength = codeLines[rangeLineOffset].length + tickCount;
|
||||||
}
|
}
|
||||||
}
|
const context = tokenLines[lineIndex + rangeLineOffset]
|
||||||
});
|
.substring(rangeIndex, rangeIndex + rangeLength);
|
||||||
|
shared.addErrorContext(
|
||||||
|
onError, token.lineNumber + lineIndex + rangeLineOffset,
|
||||||
|
context, left, right, [ rangeIndex + 1, rangeLength ]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -239,6 +239,69 @@ module.exports.forEachHeading = function forEachHeading(params, callback) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calls the provided function for each inline code span's content
|
||||||
|
module.exports.forEachInlineCodeSpan =
|
||||||
|
function forEachInlineCodeSpan(input, callback) {
|
||||||
|
let currentLine = 0;
|
||||||
|
let currentColumn = 0;
|
||||||
|
let index = 0;
|
||||||
|
while (index < input.length) {
|
||||||
|
let startIndex = -1;
|
||||||
|
let startLine = -1;
|
||||||
|
let startColumn = -1;
|
||||||
|
let tickCount = 0;
|
||||||
|
let currentTicks = 0;
|
||||||
|
// Deliberate <= so trailing 0 completes the last span (ex: "text `code`")
|
||||||
|
for (; index <= input.length; index++) {
|
||||||
|
const char = input[index];
|
||||||
|
if (char === "`") {
|
||||||
|
// Count backticks at start or end of code span
|
||||||
|
currentTicks++;
|
||||||
|
if ((startIndex === -1) || (startColumn === -1)) {
|
||||||
|
startIndex = index + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((startIndex >= 0) &&
|
||||||
|
(startColumn >= 0) &&
|
||||||
|
(tickCount === currentTicks)) {
|
||||||
|
// Found end backticks; invoke callback for code span
|
||||||
|
callback(
|
||||||
|
input.substring(startIndex, index - currentTicks),
|
||||||
|
startLine, startColumn, tickCount);
|
||||||
|
startIndex = -1;
|
||||||
|
startColumn = -1;
|
||||||
|
} else if ((startIndex >= 0) && (startColumn === -1)) {
|
||||||
|
// Found start backticks
|
||||||
|
tickCount = currentTicks;
|
||||||
|
startLine = currentLine;
|
||||||
|
startColumn = currentColumn;
|
||||||
|
}
|
||||||
|
// Not in backticks
|
||||||
|
currentTicks = 0;
|
||||||
|
}
|
||||||
|
if (char === "\n") {
|
||||||
|
// On next line
|
||||||
|
currentLine++;
|
||||||
|
currentColumn = 0;
|
||||||
|
} else if ((char === "\\") &&
|
||||||
|
((startIndex === -1) || (startColumn === -1))) {
|
||||||
|
// Escape character outside code, skip next
|
||||||
|
index++;
|
||||||
|
currentColumn += 2;
|
||||||
|
} else {
|
||||||
|
// On next column
|
||||||
|
currentColumn++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (startIndex >= 0) {
|
||||||
|
// Restart loop after unmatched start backticks (ex: "`text``code``")
|
||||||
|
index = startIndex;
|
||||||
|
currentLine = startLine;
|
||||||
|
currentColumn = startColumn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Returns (nested) lists as a flat array (in order)
|
// Returns (nested) lists as a flat array (in order)
|
||||||
module.exports.flattenLists = function flattenLists() {
|
module.exports.flattenLists = function flattenLists() {
|
||||||
return tokenCache.flattenedLists;
|
return tokenCache.flattenedLists;
|
||||||
|
|
|
@ -46,3 +46,13 @@ text.
|
||||||
|
|
||||||
* List
|
* List
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Text
|
||||||
|
text ```code
|
||||||
|
span code
|
||||||
|
span code ``` text
|
||||||
|
text
|
||||||
|
text text ```` code
|
||||||
|
span code
|
||||||
|
span```` text
|
||||||
|
text.
|
||||||
|
|
|
@ -185,7 +185,7 @@
|
||||||
"ruleDescription": "Spaces inside code span elements",
|
"ruleDescription": "Spaces inside code span elements",
|
||||||
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038",
|
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038",
|
||||||
"errorDetail": null,
|
"errorDetail": null,
|
||||||
"errorContext": "` code\nspan`",
|
"errorContext": "` code",
|
||||||
"errorRange": [ 6, 6 ]
|
"errorRange": [ 6, 6 ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -194,9 +194,27 @@
|
||||||
"ruleDescription": "Spaces inside code span elements",
|
"ruleDescription": "Spaces inside code span elements",
|
||||||
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038",
|
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038",
|
||||||
"errorDetail": null,
|
"errorDetail": null,
|
||||||
"errorContext": "`code\nspan `",
|
"errorContext": "span `",
|
||||||
"errorRange": [ 1, 7 ]
|
"errorRange": [ 1, 7 ]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"lineNumber": 53,
|
||||||
|
"ruleNames": [ "MD038", "no-space-in-code" ],
|
||||||
|
"ruleDescription": "Spaces inside code span elements",
|
||||||
|
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038",
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": "span code ```",
|
||||||
|
"errorRange": [ 1, 13 ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lineNumber": 55,
|
||||||
|
"ruleNames": [ "MD038", "no-space-in-code" ],
|
||||||
|
"ruleDescription": "Spaces inside code span elements",
|
||||||
|
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038",
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": "```` code",
|
||||||
|
"errorRange": [ 11, 9 ]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"lineNumber": 19,
|
"lineNumber": 19,
|
||||||
"ruleNames": [ "MD039", "no-space-in-links" ],
|
"ruleNames": [ "MD039", "no-space-in-links" ],
|
||||||
|
|
|
@ -56,3 +56,13 @@ text `code
|
||||||
span code
|
span code
|
||||||
span ` text {MD038}
|
span ` text {MD038}
|
||||||
text.
|
text.
|
||||||
|
|
||||||
|
Text
|
||||||
|
text `code
|
||||||
|
span code
|
||||||
|
span ` text {MD038}
|
||||||
|
text
|
||||||
|
text ` code {MD038}
|
||||||
|
span code
|
||||||
|
span` text
|
||||||
|
text.
|
||||||
|
|
|
@ -1420,6 +1420,107 @@ module.exports.trimLeftRight = function trimLeftRight(test) {
|
||||||
test.done();
|
test.done();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) {
|
||||||
|
test.expect(94);
|
||||||
|
const testCases =
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"`code`",
|
||||||
|
[ [ "code", 0, 1, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text `code` text",
|
||||||
|
[ [ "code", 0, 6, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text `code` text `edoc`",
|
||||||
|
[
|
||||||
|
[ "code", 0, 6, 1 ],
|
||||||
|
[ "edoc", 0, 18, 1 ]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text `code` text `edoc` text",
|
||||||
|
[
|
||||||
|
[ "code", 0, 6, 1 ],
|
||||||
|
[ "edoc", 0, 18, 1 ]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text ``code`code`` text",
|
||||||
|
[ [ "code`code", 0, 7, 2 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"`code `` code`",
|
||||||
|
[ [ "code `` code", 0, 1, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"`code\\`text`",
|
||||||
|
[ [ "code\\", 0, 1, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"``\ncode\n``",
|
||||||
|
[ [ "\ncode\n", 0, 2, 2 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text\n`code`\ntext",
|
||||||
|
[ [ "code", 1, 1, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text\ntext\n`code`\ntext\n`edoc`\ntext",
|
||||||
|
[
|
||||||
|
[ "code", 2, 1, 1 ],
|
||||||
|
[ "edoc", 4, 1, 1 ]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text `code\nedoc` text",
|
||||||
|
[ [ "code\nedoc", 0, 6, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"> text `code` text",
|
||||||
|
[ [ "code", 0, 8, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"> text\n> `code`\n> text",
|
||||||
|
[ [ "code", 1, 3, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"> text\n> `code\n> edoc`\n> text",
|
||||||
|
[ [ "code\n> edoc", 1, 3, 1 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"```text``",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text `text text",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"`text``code``",
|
||||||
|
[ [ "code", 0, 7, 2 ] ]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"text \\` text `code`",
|
||||||
|
[ [ "code", 0, 14, 1 ] ]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const [ input, expecteds ] = testCase;
|
||||||
|
shared.forEachInlineCodeSpan(input, (code, line, column, ticks) => {
|
||||||
|
const [ expectedCode, expectedLine, expectedColumn, expectedTicks ] =
|
||||||
|
expecteds.shift();
|
||||||
|
test.equal(code, expectedCode, input);
|
||||||
|
test.equal(line, expectedLine, input);
|
||||||
|
test.equal(column, expectedColumn, input);
|
||||||
|
test.equal(ticks, expectedTicks, input);
|
||||||
|
});
|
||||||
|
test.equal(expecteds.length, 0, "length");
|
||||||
|
});
|
||||||
|
test.done();
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.configSingle = function configSingle(test) {
|
module.exports.configSingle = function configSingle(test) {
|
||||||
test.expect(2);
|
test.expect(2);
|
||||||
markdownlint.readConfig("./test/config/config-child.json",
|
markdownlint.readConfig("./test/config/config-child.json",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue