diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index ce8cdce8..9f75fecd 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -44,7 +44,8 @@ var inlineCommentStartRe = /()/ig; module.exports.inlineCommentStartRe = inlineCommentStartRe; // Regular expression for matching HTML elements -module.exports.htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g; +var htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g; +module.exports.htmlElementRe = htmlElementRe; // Regular expressions for range matching module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*(?:\/|[^\s\]"'\W])/ig; module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/; @@ -598,6 +599,24 @@ module.exports.codeBlockAndSpanRanges = function (params, lineMetadata) { }); return exclusions; }; +/** + * Returns an array of HTML element ranges. + * + * @param {Object} params RuleParams instance. + * @param {Object} lineMetadata Line metadata object. + * @returns {number[][]} Array of ranges (lineIndex, columnIndex, length). + */ +module.exports.htmlElementRanges = function (params, lineMetadata) { + var exclusions = []; + forEachLine(lineMetadata, function (line, lineIndex, inCode) { + var match = null; + // eslint-disable-next-line no-unmodified-loop-condition + while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) { + exclusions.push([lineIndex, match.index, match[0].length]); + } + }); + return exclusions; +}; /** * Determines whether the specified range overlaps another range. * @@ -1041,6 +1060,13 @@ module.exports.flattenedLists = function (value) { } return flattenedLists; }; +var htmlElementRanges = null; +module.exports.htmlElementRanges = function (value) { + if (value) { + htmlElementRanges = value; + } + return htmlElementRanges; +}; var lineMetadata = null; module.exports.lineMetadata = function (value) { if (value) { @@ -1051,6 +1077,7 @@ module.exports.lineMetadata = function (value) { module.exports.clear = function () { codeBlockAndSpanRanges = null; flattenedLists = null; + htmlElementRanges = null; lineMetadata = null; }; @@ -1547,9 +1574,11 @@ function lintContent(ruleList, name, content, md, config, frontMatter, handleRul lines: lines, frontMatterLines: frontMatterLines }); - cache.lineMetadata(helpers.getLineMetadata(paramsBase)); + var lineMetadata = helpers.getLineMetadata(paramsBase); + cache.lineMetadata(lineMetadata); + cache.codeBlockAndSpanRanges(helpers.codeBlockAndSpanRanges(paramsBase, lineMetadata)); + cache.htmlElementRanges(helpers.htmlElementRanges(paramsBase, lineMetadata)); cache.flattenedLists(helpers.flattenLists(paramsBase.tokens)); - cache.codeBlockAndSpanRanges(helpers.codeBlockAndSpanRanges(paramsBase, cache.lineMetadata())); // Function to run for each rule var results = []; // eslint-disable-next-line jsdoc/require-jsdoc @@ -4157,7 +4186,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, bareUrlRe = _a.bareUrlRe, escapeForRegExp = _a.escapeForRegExp, forEachLine = _a.forEachLine, forEachLink = _a.forEachLink, overlapsAnyRange = _a.overlapsAnyRange, linkReferenceRe = _a.linkReferenceRe; -var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, lineMetadata = _b.lineMetadata; +var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, htmlElementRanges = _b.htmlElementRanges, lineMetadata = _b.lineMetadata; module.exports = { "names": ["MD044", "proper-names"], "description": "Proper names should have the correct capitalization", @@ -4168,6 +4197,8 @@ module.exports = { names.sort(function (a, b) { return (b.length - a.length) || a.localeCompare(b); }); var codeBlocks = params.config.code_blocks; var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; + var htmlElements = params.config.html_elements; + var includeHtmlElements = (htmlElements === undefined) ? true : !!htmlElements; var exclusions = []; forEachLine(lineMetadata(), function (line, lineIndex) { if (linkReferenceRe.test(line)) { @@ -4188,6 +4219,9 @@ module.exports = { if (!includeCodeBlocks) { exclusions.push.apply(exclusions, codeBlockAndSpanRanges()); } + if (!includeHtmlElements) { + exclusions.push.apply(exclusions, htmlElementRanges()); + } var _loop_1 = function (name) { var escapedName = escapeForRegExp(name); var startNamePattern = /^\W/.test(name) ? "" : "\\b_*"; diff --git a/doc/Rules.md b/doc/Rules.md index f2fc69d2..5f6f3429 100644 --- a/doc/Rules.md +++ b/doc/Rules.md @@ -1752,7 +1752,7 @@ Tags: spelling Aliases: proper-names -Parameters: names, code_blocks (string array; default `null`, boolean; default `true`) +Parameters: names, code_blocks, html_elements (string array; default `null`, boolean; default `true`, boolean; default `true`) Fixable: Most violations can be fixed by tooling @@ -1771,7 +1771,9 @@ the proper capitalization, specify the desired letter case in the `names` array: ``` Set the `code_blocks` parameter to `false` to disable this rule for code blocks -and spans. +and spans. Set the `html_elements` parameter to `false` to disable this rule +for HTML elements and attributes (such as when using a proper name as part of +a path for `a`/`href` or `img`/`src`). Rationale: Incorrect capitalization of proper names is usually a mistake. diff --git a/helpers/helpers.js b/helpers/helpers.js index 4e44a8cb..95d508d6 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -19,7 +19,8 @@ const inlineCommentStartRe = module.exports.inlineCommentStartRe = inlineCommentStartRe; // Regular expression for matching HTML elements -module.exports.htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g; +const htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g; +module.exports.htmlElementRe = htmlElementRe; // Regular expressions for range matching module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*(?:\/|[^\s\]"'\W])/ig; @@ -603,6 +604,25 @@ module.exports.codeBlockAndSpanRanges = (params, lineMetadata) => { return exclusions; }; +/** + * Returns an array of HTML element ranges. + * + * @param {Object} params RuleParams instance. + * @param {Object} lineMetadata Line metadata object. + * @returns {number[][]} Array of ranges (lineIndex, columnIndex, length). + */ +module.exports.htmlElementRanges = (params, lineMetadata) => { + const exclusions = []; + forEachLine(lineMetadata, (line, lineIndex, inCode) => { + let match = null; + // eslint-disable-next-line no-unmodified-loop-condition + while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) { + exclusions.push([ lineIndex, match.index, match[0].length ]); + } + }); + return exclusions; +}; + /** * Determines whether the specified range overlaps another range. * diff --git a/lib/cache.js b/lib/cache.js index 4d7fa662..33b99f55 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -18,6 +18,14 @@ module.exports.flattenedLists = (value) => { return flattenedLists; }; +let htmlElementRanges = null; +module.exports.htmlElementRanges = (value) => { + if (value) { + htmlElementRanges = value; + } + return htmlElementRanges; +}; + let lineMetadata = null; module.exports.lineMetadata = (value) => { if (value) { @@ -29,5 +37,6 @@ module.exports.lineMetadata = (value) => { module.exports.clear = () => { codeBlockAndSpanRanges = null; flattenedLists = null; + htmlElementRanges = null; lineMetadata = null; }; diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 8da66cd4..0535f996 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -495,11 +495,15 @@ function lintContent( lines, frontMatterLines }); - cache.lineMetadata(helpers.getLineMetadata(paramsBase)); - cache.flattenedLists(helpers.flattenLists(paramsBase.tokens)); + const lineMetadata = helpers.getLineMetadata(paramsBase); + cache.lineMetadata(lineMetadata); cache.codeBlockAndSpanRanges( - helpers.codeBlockAndSpanRanges(paramsBase, cache.lineMetadata()) + helpers.codeBlockAndSpanRanges(paramsBase, lineMetadata) ); + cache.htmlElementRanges( + helpers.htmlElementRanges(paramsBase, lineMetadata) + ); + cache.flattenedLists(helpers.flattenLists(paramsBase.tokens)); // Function to run for each rule let results = []; // eslint-disable-next-line jsdoc/require-jsdoc diff --git a/lib/md044.js b/lib/md044.js index 0806c5c6..b1d9cc68 100644 --- a/lib/md044.js +++ b/lib/md044.js @@ -4,7 +4,8 @@ const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine, forEachLink, overlapsAnyRange, linkReferenceRe } = require("../helpers"); -const { codeBlockAndSpanRanges, lineMetadata } = require("./cache"); +const { codeBlockAndSpanRanges, htmlElementRanges, lineMetadata } = + require("./cache"); module.exports = { "names": [ "MD044", "proper-names" ], @@ -15,7 +16,11 @@ module.exports = { names = Array.isArray(names) ? names : []; names.sort((a, b) => (b.length - a.length) || a.localeCompare(b)); const codeBlocks = params.config.code_blocks; - const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; + const includeCodeBlocks = + (codeBlocks === undefined) ? true : !!codeBlocks; + const htmlElements = params.config.html_elements; + const includeHtmlElements = + (htmlElements === undefined) ? true : !!htmlElements; const exclusions = []; forEachLine(lineMetadata(), (line, lineIndex) => { if (linkReferenceRe.test(line)) { @@ -37,6 +42,9 @@ module.exports = { if (!includeCodeBlocks) { exclusions.push(...codeBlockAndSpanRanges()); } + if (!includeHtmlElements) { + exclusions.push(...htmlElementRanges()); + } for (const name of names) { const escapedName = escapeForRegExp(name); const startNamePattern = /^\W/.test(name) ? "" : "\\b_*"; diff --git a/schema/.markdownlint.jsonc b/schema/.markdownlint.jsonc index 60e54faa..c0021a4b 100644 --- a/schema/.markdownlint.jsonc +++ b/schema/.markdownlint.jsonc @@ -231,7 +231,9 @@ // List of proper names "names": [], // Include code blocks - "code_blocks": true + "code_blocks": true, + // Include HTML elements + "html_elements": true }, // MD045/no-alt-text - Images should have alternate text (alt text) diff --git a/schema/.markdownlint.yaml b/schema/.markdownlint.yaml index 2a0eba8b..07d6a838 100644 --- a/schema/.markdownlint.yaml +++ b/schema/.markdownlint.yaml @@ -211,6 +211,8 @@ MD044: names: [] # Include code blocks code_blocks: true + # Include HTML elements + html_elements: true # MD045/no-alt-text - Images should have alternate text (alt text) MD045: true diff --git a/schema/build-config-schema.js b/schema/build-config-schema.js index 8ea6c2c7..510980aa 100644 --- a/schema/build-config-schema.js +++ b/schema/build-config-schema.js @@ -391,6 +391,11 @@ rules.forEach(function forRule(rule) { "description": "Include code blocks", "type": "boolean", "default": true + }, + "html_elements": { + "description": "Include HTML elements", + "type": "boolean", + "default": true } }; break; diff --git a/schema/markdownlint-config-schema.json b/schema/markdownlint-config-schema.json index dc85dc91..f9a21756 100644 --- a/schema/markdownlint-config-schema.json +++ b/schema/markdownlint-config-schema.json @@ -759,6 +759,11 @@ "description": "Include code blocks", "type": "boolean", "default": true + }, + "html_elements": { + "description": "Include HTML elements", + "type": "boolean", + "default": true } }, "additionalProperties": false diff --git a/test/markdownlint-test-helpers.js b/test/markdownlint-test-helpers.js index 44d2acb9..99572a3e 100644 --- a/test/markdownlint-test-helpers.js +++ b/test/markdownlint-test-helpers.js @@ -1283,3 +1283,43 @@ test("forEachLink", (t) => { t.is(matches.length, 0, "Missing match"); } }); + +test("htmlElementRanges", (t) => { + t.plan(1); + const params = { + "lines": [ + "# Heading", + "", + "Text text text", + "text text", + "text text text", + "", + "

", + "Text text text", + "

", + "", + "```", + "
", + "```", + "", + "Text `
` text", + "text
text" + ], + "tokens": [ + { + "type": "code_block", + "map": [ 10, 12 ] + } + ] + }; + const expected = [ + [ 3, 5, 12 ], + [ 6, 0, 3 ], + [ 7, 5, 4 ], + [ 14, 6, 5 ], + [ 15, 5, 5 ] + ]; + const lineMetadata = helpers.getLineMetadata(params); + const actual = helpers.htmlElementRanges(params, lineMetadata); + t.deepEqual(actual, expected); +}); diff --git a/test/proper-names-no-html.json b/test/proper-names-no-html.json new file mode 100644 index 00000000..b00d6f3c --- /dev/null +++ b/test/proper-names-no-html.json @@ -0,0 +1,10 @@ +{ + "default": true, + "MD033": false, + "MD044": { + "names": [ + "JavaScript" + ], + "html_elements": false + } +} diff --git a/test/proper-names-no-html.md b/test/proper-names-no-html.md new file mode 100644 index 00000000..0b6fc358 --- /dev/null +++ b/test/proper-names-no-html.md @@ -0,0 +1,21 @@ +# Proper Names No HTML + +Okay text JavaScript. + +Bad text javascript. {MD044} + +Bad code `javascript`. {MD044} + + + + + +
+ + + + + +javascript diff --git a/test/proper-names.json b/test/proper-names.json index 797e8d9a..d6f46886 100644 --- a/test/proper-names.json +++ b/test/proper-names.json @@ -1,5 +1,6 @@ { "default": true, + "MD033": false, "MD044": { "names": [ "markdownlint", diff --git a/test/proper-names.md b/test/proper-names.md index 2225d53b..11b7560d 100644 --- a/test/proper-names.md +++ b/test/proper-names.md @@ -40,7 +40,7 @@ Code in `javascript` {MD044} Execute `via the node.js engine` {MD044} -HTML javascript {MD033} {MD044} +HTML javascript {MD044} * Use NPM {MD044} @@ -84,3 +84,12 @@ Text referencing multiplecase name. Text referencing MultipleCase name. Text referencing MULTIPLECASE name. {MD044} Text referencing mULTIPLEcASE name. + + + + + +