Improve handling of nested tags and blocks by MD033/no-inline-html (fixes #179).

This commit is contained in:
David Anson 2019-04-29 22:09:03 -07:00
parent 44fac78721
commit 4c7ffdd335
7 changed files with 372 additions and 18 deletions

View file

@ -115,6 +115,19 @@ module.exports.escapeForRegExp = function escapeForRegExp(str) {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
}; };
// Un-escapes Markdown content (simple algorithm; not a parser)
const escapedMarkdownRe = /\\./g;
module.exports.unescapeMarkdown =
function unescapeMarkdown(markdown, replacement) {
return markdown.replace(escapedMarkdownRe, (match) => {
const char = match[1];
if ("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".includes(char)) {
return replacement || char;
}
return match;
});
};
// Returns the indent for a token // Returns the indent for a token
function indentFor(token) { function indentFor(token) {
const line = token.line.replace(/^[\s>]*(> |>)/, ""); const line = token.line.replace(/^[\s>]*(> |>)/, "");

View file

@ -2,10 +2,12 @@
"use strict"; "use strict";
const { addError, filterTokens, forEachInlineChild, newLineRe, const { addError, bareUrlRe, forEachLine, unescapeMarkdown } =
rangeFromRegExp } = require("../helpers"); require("../helpers");
const { lineMetadata } = require("./cache");
const htmlRe = /<[^>]*>/; const htmlElementRe = /<(\w+)(?:[^>]*)?>/g;
const linkDestinationRe = /]\(\s*$/;
module.exports = { module.exports = {
"names": [ "MD033", "no-inline-html" ], "names": [ "MD033", "no-inline-html" ],
@ -14,21 +16,21 @@ module.exports = {
"function": function MD033(params, onError) { "function": function MD033(params, onError) {
const allowedElements = (params.config.allowed_elements || []) const allowedElements = (params.config.allowed_elements || [])
.map((element) => element.toLowerCase()); .map((element) => element.toLowerCase());
function forToken(token) { forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
token.content.split(newLineRe) let match = null;
.forEach((line, offset) => { // eslint-disable-next-line no-unmodified-loop-condition
const allowed = (line.match(/<[^/\s>!]*/g) || []) while (!inCode && (match = htmlElementRe.exec(line))) {
.filter((element) => element.length > 1) const [ tag, element ] = match;
.map((element) => element.slice(1).toLowerCase()) if (!allowedElements.includes(element.toLowerCase()) &&
.filter((element) => !allowedElements.includes(element)); !tag.endsWith("\\>") && !bareUrlRe.test(tag)) {
if (allowed.length) { const prefix = line.substring(0, match.index);
addError(onError, token.lineNumber + offset, if (!linkDestinationRe.test(prefix) &&
"Element: " + allowed[0], null, !unescapeMarkdown(prefix + "<", "_").endsWith("_")) {
rangeFromRegExp(token.line, htmlRe)); addError(onError, lineIndex + 1, "Element: " + element,
null, [ match.index + 1, tag.length ]);
} }
}); }
} }
filterTokens(params, "html_block", forToken); });
forEachInlineChild(params, "html_inline", forToken);
} }
}; };

View file

@ -0,0 +1,9 @@
{
"default": true,
"MD033": {
"allowed_elements": [
"strong"
]
},
"MD046": false
}

View file

@ -0,0 +1,81 @@
# Detailed HTML Results
Text
<em>Block block</em>
Text <em>inline inline</em> text
Text
<strong>Block block</strong>
Text <strong>inline inline</strong> text
Text
<p>
Block
block <em>block</em> block
block
block <strong>block</strong> block
block
block <em>block</em> block <strong>block</strong> block
block <strong>block</strong> block <em>block</em> block
</p>
Text
<strong><em>Block</em> block</strong>
Text <strong><em>inline</em> inline</strong> text
Text
<em><strong>Block</strong> block</em>
Text <em><strong>inline</strong> inline</em> text
Text
Text <em>inline</em> text <strong>inline</strong> text <em>inline</em> text
Text <strong>inline</strong> text <em>inline</em> text <strong>inline</strong> text
Text
\<not>Block block\</not>
\\<problem>Block block\\</problem>
<not\>Block block</not\>
Text \<not>inline inline\</not> text
Text \\<problem>inline inline\\</problem> text
Text <not\>inline inline</not\> text
Text
> Text <em>inline inline</em> text
> text <strong>inline inline</strong> text
Text
Text <em>inline inline</em> text
text <strong>inline inline</strong> text
Text
```html
Text <em>inline inline</em> text
text <strong>inline inline</strong> text
```
Text
Text <a href="#anchor">inline</a> text
text <img src="src.png"/> text
Text

View file

@ -0,0 +1,155 @@
[
{
"lineNumber": 5,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 1, 4 ]
},
{
"lineNumber": 7,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 6, 4 ]
},
{
"lineNumber": 17,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: p",
"errorContext": null,
"errorRange": [ 1, 3 ]
},
{
"lineNumber": 19,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 7, 4 ]
},
{
"lineNumber": 23,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 7, 4 ]
},
{
"lineNumber": 24,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 36, 4 ]
},
{
"lineNumber": 29,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 9, 4 ]
},
{
"lineNumber": 31,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 14, 4 ]
},
{
"lineNumber": 35,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 1, 4 ]
},
{
"lineNumber": 37,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 6, 4 ]
},
{
"lineNumber": 41,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 6, 4 ]
},
{
"lineNumber": 43,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 35, 4 ]
},
{
"lineNumber": 49,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: problem",
"errorContext": null,
"errorRange": [ 3, 9 ]
},
{
"lineNumber": 55,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: problem",
"errorContext": null,
"errorRange": [ 8, 9 ]
},
{
"lineNumber": 61,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: em",
"errorContext": null,
"errorRange": [ 8, 4 ]
},
{
"lineNumber": 78,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: a",
"errorContext": null,
"errorRange": [ 6, 18 ]
},
{
"lineNumber": 79,
"ruleNames": [ "MD033", "no-inline-html" ],
"ruleDescription": "Inline HTML",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033",
"errorDetail": "Element: img",
"errorContext": null,
"errorRange": [ 6, 20 ]
}
]

View file

@ -6,24 +6,56 @@
[text](<>) {MD042} [text](<>) {MD042}
[text]( <> ) {MD042}
[text](<> "title") {MD042}
[text]( <> "title" ) {MD042}
[text](#) {MD042} [text](#) {MD042}
[text]( # ) {MD042}
[text](# "title") {MD042}
[text]( # "title" ) {MD042}
[text][frag] {MD042} [text][frag] {MD042}
[text][ frag ] {MD042}
[frag]: # [frag]: #
## Non-empty links ## Non-empty links
[text](link) [text](link)
[text]( link )
[text](link "title") [text](link "title")
[text]( link "title" )
[text](<link>) [text](<link>)
[text]( <link> )
[text](<link> "title")
[text]( <link> "title" )
[text](#frag) [text](#frag)
[text]( #frag )
[text](#frag "title")
[text]( #frag "title" )
[text][ref] [text][ref]
[text][ ref ]
[ref]: link [ref]: link
[text] [text]

View file

@ -1406,6 +1406,68 @@ function clearHtmlCommentTextEmbedded(test) {
test.done(); test.done();
}; };
module.exports.unescapeMarkdown = function unescapeMarkdown(test) {
test.expect(7);
// Test cases from https://spec.commonmark.org/0.29/#backslash-escapes
const testCases = [
[
"\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;" +
"\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~",
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
],
[
"\\→\\A\\a\\ \\3\\φ\\«",
"\\→\\A\\a\\ \\3\\φ\\«"
],
[
`\\*not emphasized*
\\<br/> not a tag
\\[not a link](/foo)
\\\`not code\`
1\\. not a list
\\* not a list
\\# not a heading
\\[foo]: /url "not a reference"
\\&ouml; not a character entity`,
`*not emphasized*
<br/> not a tag
[not a link](/foo)
\`not code\`
1. not a list
* not a list
# not a heading
[foo]: /url "not a reference"
&ouml; not a character entity`
],
[
"\\\\*emphasis*",
"\\*emphasis*"
],
[
`foo\\
bar`,
`foo\\
bar`
],
[
"Text \\<",
"Text _",
"_"
],
[
"Text \\\\<",
"Text _<",
"_"
]
];
testCases.forEach(function forTestCase(testCase) {
const [ markdown, expected, replacement ] = testCase;
const actual = helpers.unescapeMarkdown(markdown, replacement);
test.equal(actual, expected);
});
test.done();
};
module.exports.isBlankLine = function isBlankLine(test) { module.exports.isBlankLine = function isBlankLine(test) {
test.expect(25); test.expect(25);
const blankLines = [ const blankLines = [