From 6ce426cf88864ae917821c37a9a6d6da99796e89 Mon Sep 17 00:00:00 2001 From: David Anson Date: Sun, 5 Apr 2020 19:47:12 -0700 Subject: [PATCH] Add support for "markdownlint-configure-file" inline comments (fixes #264). --- README.md | 27 ++++ helpers/helpers.js | 2 +- lib/markdownlint.js | 147 +++++++++++------- test/inline-configure-file-invalid.md | 5 + ...nline-configure-file-multiple-instances.md | 33 ++++ test/inline-configure-file-multiple-lines.md | 20 +++ test/inline-configure-file-single-line.md | 5 + 7 files changed, 183 insertions(+), 56 deletions(-) create mode 100644 test/inline-configure-file-invalid.md create mode 100644 test/inline-configure-file-multiple-instances.md create mode 100644 test/inline-configure-file-multiple-lines.md create mode 100644 test/inline-configure-file-single-line.md diff --git a/README.md b/README.md index 0ed26460..11d9ac24 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,33 @@ the following syntax is supported: This can be used to "hide" `markdownlint` comments at the bottom of a file. +In cases where it is desirable to change the configuration of one or more rules +for a file, the following more advanced syntax is supported: + +* Confiure: `` + +For example: + +```markdown + +``` + +or + +```markdown + +``` + +These changes apply to the entire file regardless of where the comment is +located. Multiple such comments (if present) are applied top-to-bottom. + ## API ### Linting diff --git a/helpers/helpers.js b/helpers/helpers.js index 24fea984..d272abd5 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -17,7 +17,7 @@ module.exports.frontMatterRe = // Regular expression for matching inline disable/enable comments const inlineCommentRe = // eslint-disable-next-line max-len - //ig; + //ig; module.exports.inlineCommentRe = inlineCommentRe; // Regular expressions for range matching diff --git a/lib/markdownlint.js b/lib/markdownlint.js index c5dffb28..fdca779a 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -300,72 +300,105 @@ function getEffectiveConfig(ruleList, config, aliasToRuleNames) { * @param {string[]} lines List of content lines. * @param {string[]} frontMatterLines List of front matter lines. * @param {boolean} noInlineConfig Whether to allow inline configuration. - * @param {Configuration} effectiveConfig Effective configuration. + * @param {Configuration} config Configuration object. * @param {Object.} aliasToRuleNames Map of alias to rule * names. - * @returns {Object.[]} Enabled rules for each line. + * @returns {Object} Effective configuration and enabled rules per line number. */ function getEnabledRulesPerLineNumber( ruleList, lines, frontMatterLines, noInlineConfig, - effectiveConfig, + config, aliasToRuleNames) { + // Shared variables let enabledRules = {}; + let capturedRules = {}; const allRuleNames = []; + const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length); + // Helper functions + // eslint-disable-next-line jsdoc/require-jsdoc + function handleInlineConfig(perLine, forEachMatch, forEachLine) { + const input = perLine ? lines : [ lines.join("\n") ]; + input.forEach((line) => { + if (!noInlineConfig) { + let match = null; + while ((match = helpers.inlineCommentRe.exec(line))) { + const action = (match[1] || match[3]).toUpperCase(); + const parameter = match[2] || match[4]; + forEachMatch(action, parameter); + } + } + if (forEachLine) { + forEachLine(); + } + }); + } + // eslint-disable-next-line jsdoc/require-jsdoc + function configureFile(action, parameter) { + if (action === "CONFIGURE-FILE") { + try { + const json = JSON.parse(parameter); + config = { + ...config, + ...json + }; + } catch (ex) { + // Ignore parse errors for inline configuration + } + } + } + // eslint-disable-next-line jsdoc/require-jsdoc + function applyEnableDisable(action, parameter) { + const enabled = (action.startsWith("ENABLE")); + const items = parameter ? + parameter.trim().toUpperCase().split(/\s+/) : + allRuleNames; + items.forEach((nameUpper) => { + (aliasToRuleNames[nameUpper] || []).forEach((ruleName) => { + enabledRules[ruleName] = enabled; + }); + }); + } + // eslint-disable-next-line jsdoc/require-jsdoc + function enableDisableFile(action, parameter) { + if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) { + applyEnableDisable(action, parameter); + } + } + // eslint-disable-next-line jsdoc/require-jsdoc + function captureRestoreEnableDisable(action, parameter) { + if (action === "CAPTURE") { + capturedRules = { ...enabledRules }; + } else if (action === "RESTORE") { + enabledRules = { ...capturedRules }; + } else if ((action === "ENABLE") || (action === "DISABLE")) { + enabledRules = { ...enabledRules }; + applyEnableDisable(action, parameter); + } + } + // eslint-disable-next-line jsdoc/require-jsdoc + function updateLineState() { + enabledRulesPerLineNumber.push(enabledRules); + } + // Handle inline comments + handleInlineConfig(false, configureFile); + const effectiveConfig = getEffectiveConfig( + ruleList, config, aliasToRuleNames); ruleList.forEach((rule) => { const ruleName = rule.names[0].toUpperCase(); allRuleNames.push(ruleName); enabledRules[ruleName] = !!effectiveConfig[ruleName]; }); - let capturedRules = enabledRules; - // eslint-disable-next-line jsdoc/require-jsdoc - function forMatch(match, byLine) { - const action = match[1].toUpperCase(); - if (action === "CAPTURE") { - if (byLine) { - capturedRules = { ...enabledRules }; - } - } else if (action === "RESTORE") { - if (byLine) { - enabledRules = { ...capturedRules }; - } - } else { - // action in [ENABLE, DISABLE, ENABLE-FILE, DISABLE-FILE] - const isfile = action.endsWith("-FILE"); - if ((byLine && !isfile) || (!byLine && isfile)) { - const enabled = (action.startsWith("ENABLE")); - const items = match[2] ? - match[2].trim().toUpperCase().split(/\s+/) : - allRuleNames; - items.forEach((nameUpper) => { - (aliasToRuleNames[nameUpper] || []).forEach((ruleName) => { - enabledRules[ruleName] = enabled; - }); - }); - } - } - } - const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length); - [ false, true ].forEach((byLine) => { - lines.forEach((line) => { - if (!noInlineConfig) { - let match = helpers.inlineCommentRe.exec(line); - if (match) { - enabledRules = { ...enabledRules }; - while (match) { - forMatch(match, byLine); - match = helpers.inlineCommentRe.exec(line); - } - } - } - if (byLine) { - enabledRulesPerLineNumber.push(enabledRules); - } - }); - }); - return enabledRulesPerLineNumber; + capturedRules = enabledRules; + handleInlineConfig(true, enableDisableFile); + handleInlineConfig(true, captureRestoreEnableDisable, updateLineState); + // Return results + return { + effectiveConfig, + enabledRulesPerLineNumber + }; } /** @@ -438,11 +471,15 @@ function lintContent( const lines = content.split(helpers.newLineRe); annotateTokens(tokens, lines); const aliasToRuleNames = mapAliasToRuleNames(ruleList); - const effectiveConfig = - getEffectiveConfig(ruleList, config, aliasToRuleNames); - const enabledRulesPerLineNumber = getEnabledRulesPerLineNumber( - ruleList, lines, frontMatterLines, noInlineConfig, - effectiveConfig, aliasToRuleNames); + const { effectiveConfig, enabledRulesPerLineNumber } = + getEnabledRulesPerLineNumber( + ruleList, + lines, + frontMatterLines, + noInlineConfig, + config, + aliasToRuleNames + ); // Create parameters for rules const params = { name, diff --git a/test/inline-configure-file-invalid.md b/test/inline-configure-file-invalid.md new file mode 100644 index 00000000..af7d0ce6 --- /dev/null +++ b/test/inline-configure-file-invalid.md @@ -0,0 +1,5 @@ +# Inline Configure File Invalid + +Not normally too long of a line, but it would have been from an inline config. + + diff --git a/test/inline-configure-file-multiple-instances.md b/test/inline-configure-file-multiple-instances.md new file mode 100644 index 00000000..5be93d36 --- /dev/null +++ b/test/inline-configure-file-multiple-instances.md @@ -0,0 +1,33 @@ +# Inline Configure File Multiple Instances + +*** +{MD035:3} + +Trailing spaces: + + + +*** +{MD035:17} + +Trailing spaces: + + + +*** +{MD035:30} + +Trailing spaces: diff --git a/test/inline-configure-file-multiple-lines.md b/test/inline-configure-file-multiple-lines.md new file mode 100644 index 00000000..c7abf263 --- /dev/null +++ b/test/inline-configure-file-multiple-lines.md @@ -0,0 +1,20 @@ +# Inline Configure File Multiple Lines + +*** +{MD035:3} + +Trailing spaces: + + + +*** +{MD035:17} + +Trailing spaces: diff --git a/test/inline-configure-file-single-line.md b/test/inline-configure-file-single-line.md new file mode 100644 index 00000000..65668a9e --- /dev/null +++ b/test/inline-configure-file-single-line.md @@ -0,0 +1,5 @@ +# Inline Configure File Single Line + +Not normally too long of a line, but it is here from an inline config. {MD013} + +