diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 97235743..00000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -demo/markdown-it.min.js -demo/markdownlint-browser.js -demo/markdownlint-browser.min.js -demo/markdownlint-rule-helpers-browser.js -demo/markdownlint-rule-helpers-browser.min.js -example/typescript/type-check.js diff --git a/.eslintrc.json b/.eslintrc.json index 0c799131..765f6bfe 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,27 +1,60 @@ { - "parserOptions": { - "ecmaVersion": 2019 - }, "env": { "node": true, "es6": true }, + "extends": [ + "eslint:all", + "plugin:jsdoc/recommended" + ], + "ignorePatterns": [ + "demo/markdown-it.min.js", + "demo/markdownlint-browser.js", + "demo/markdownlint-browser.min.js", + "demo/markdownlint-rule-helpers-browser.js", + "demo/markdownlint-rule-helpers-browser.min.js", + "example/typescript/type-check.js", + "test-repos/" + ], + "overrides": [ + { + "files": [ + "demo/*.js" + ], + "env": { + "browser": true + }, + "rules": { + "jsdoc/require-jsdoc": "off", + "unicorn/prefer-query-selector": "off", + "unicorn/prefer-add-event-listener": "off", + "no-console": "off", + "no-shadow": "off", + "no-var": "off" + } + }, + { + "files": [ + "example/*.js" + ], + "rules": { + "node/no-missing-require": "off", + "node/no-extraneous-require": "off", + "no-console": "off", + "no-invalid-this": "off", + "no-shadow": "off", + "object-property-newline": "off" + } + } + ], + "parserOptions": { + "ecmaVersion": 2020 + }, "plugins": [ "jsdoc", "node", "unicorn" ], - "extends": [ - "eslint:all", - "plugin:jsdoc/recommended" - ], - "settings": { - "jsdoc": { - "preferredTypes": { - "object": "Object" - } - } - }, "reportUnusedDisableDirectives": true, "rules": { "array-bracket-spacing": ["error", "always"], @@ -29,7 +62,7 @@ "capitalized-comments": "off", "complexity": "off", "dot-location": ["error", "property"], - "func-style": ["error", "declaration"], + "func-style": "off", "function-call-argument-newline": "off", "function-paren-newline": "off", "global-require": "off", @@ -72,7 +105,7 @@ "jsdoc/check-access": "error", "jsdoc/check-alignment": "error", - "jsdoc/check-examples": "error", + "jsdoc/check-examples": "off", "jsdoc/check-indentation": "error", "jsdoc/check-line-alignment": "error", "jsdoc/check-param-names": "error", @@ -174,11 +207,14 @@ "unicorn/no-array-method-this-argument": "error", "unicorn/no-array-push-push": "error", "unicorn/no-array-reduce": "error", + "unicorn/no-await-expression-member": "error", "unicorn/no-console-spaces": "error", "unicorn/no-document-cookie": "error", + "unicorn/no-empty-file": "error", "unicorn/no-for-loop": "error", "unicorn/no-hex-escape": "error", "unicorn/no-instanceof-array": "error", + "unicorn/no-invalid-remove-event-listener": "error", "unicorn/no-keyword-prefix": "off", "unicorn/no-lonely-if": "error", "unicorn/no-nested-ternary": "error", @@ -192,6 +228,7 @@ "unicorn/no-unreadable-array-destructuring": "error", "unicorn/no-unsafe-regex": "off", "unicorn/no-unused-properties": "error", + "unicorn/no-useless-fallback-in-spread": "error", "unicorn/no-useless-length-check": "error", "unicorn/no-useless-spread": "error", "unicorn/no-useless-undefined": "error", @@ -205,12 +242,14 @@ "unicorn/prefer-array-index-of": "error", "unicorn/prefer-array-some": "error", "unicorn/prefer-at": "off", + "unicorn/prefer-code-point": "error", "unicorn/prefer-date-now": "error", "unicorn/prefer-default-parameters": "error", "unicorn/prefer-dom-node-append": "error", "unicorn/prefer-dom-node-dataset": "error", "unicorn/prefer-dom-node-remove": "error", "unicorn/prefer-dom-node-text-content": "error", + "unicorn/prefer-export-from": "error", "unicorn/prefer-includes": "error", "unicorn/prefer-keyboard-event-key": "error", "unicorn/prefer-math-trunc": "error", @@ -241,37 +280,14 @@ "unicorn/require-number-to-fixed-digits-argument": "error", "unicorn/require-post-message-target-origin": "error", "unicorn/string-content": "error", + "unicorn/template-indent": "error", "unicorn/throw-new-error": "error" }, - "overrides": [ - { - "files": [ - "demo/*.js" - ], - "env": { - "browser": true - }, - "rules": { - "jsdoc/require-jsdoc": "off", - "unicorn/prefer-query-selector": "off", - "unicorn/prefer-add-event-listener": "off", - "no-console": "off", - "no-shadow": "off", - "no-var": "off" - } - }, - { - "files": [ - "example/*.js" - ], - "rules": { - "node/no-missing-require": "off", - "node/no-extraneous-require": "off", - "no-console": "off", - "no-invalid-this": "off", - "no-shadow": "off", - "object-property-newline": "off" + "settings": { + "jsdoc": { + "preferredTypes": { + "object": "Object" } } - ] + } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a12dd3bc..d060e1a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,8 @@ jobs: - name: Install Dependencies run: npm install --no-package-lock - name: Run All Validations - if: ${{ matrix.node-version != '10.x' && matrix.node-version != '12.x' }} + if: ${{ matrix.node-version != '12.x' }} run: npm run ci - name: Run Tests Only - if: ${{ matrix.node-version == '10.x' || matrix.node-version == '12.x' }} + if: ${{ matrix.node-version == '12.x' }} run: npm run test diff --git a/.github/workflows/test-repos.yml b/.github/workflows/test-repos.yml index 7ff15af6..b36a0e58 100644 --- a/.github/workflows/test-repos.yml +++ b/.github/workflows/test-repos.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node-version: [12.x] + node-version: [16.x] steps: - uses: actions/checkout@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f8c38b4..bc3f886b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ Try to break the new code now, or else it will get broken later. Run tests before sending a pull request via `npm test` in the [usual manner](https://docs.npmjs.com/misc/scripts). Tests should all pass on all platforms. -The test runner is [tape](https://www.npmjs.com/package/tape) and test cases are located in `test/markdownlint-test*.js`. +The test runner is [AVA](https://github.com/avajs/ava) and test cases are located in `test/markdownlint-test*.js`. When running tests, `test/*.md` files are enumerated, linted, and fail if any violations are missing a corresponding `{MD###}` marker in the test file. For example, the line `### Heading {MD001}` is expected to trigger the rule `MD001`. For cases where the marker text can not be present on the same line, the syntax `{MD###:#}` can be used to include a line number. @@ -33,7 +33,8 @@ Lint before sending a pull request by running `npm run lint`. There should be no issues. Run a full continuous integration pass before sending a pull request via `npm run ci`. -Code coverage should remain at 100%. +Code coverage should always be 100%. +As part of a continuous integration run, generated files may get updated and fail the run - commit them to the repository and rerun continuous integration. Pull requests should contain a single commit. If necessary, squash multiple commits before creating the pull request and when making changes. @@ -43,9 +44,18 @@ Open pull requests against the `next` branch. That's where the latest changes are staged for the next release. Include the text "(fixes #??)" at the end of the commit message so the pull request will be associated with the relevant issue. End commit messages with a period (`.`). -Do not include `package-lock.json` in the pull request. Once accepted, the tag `fixed in next` will be added to the issue. When the commit is merged to the main branch during the release process, the issue will be closed automatically. (See [Closing issues using keywords](https://help.github.com/articles/closing-issues-using-keywords/) for details.) +Please refrain from using slang or meaningless placeholder words. +Sample content can be "text", "code", "heading", or the like. +Sample URLs should use [example.com](https://en.wikipedia.org/wiki/Example.com) which is safe for this purpose. +Profanity is not allowed. + +In order to maintain the permissive MIT license this project uses, all contributions must be your own and released under that license. +Code you add should be an original work and should not be copied from elsewhere. +Taking code from a different project, Stack Overflow, or the like is not allowed. +The use of tools such as GitHub Copilot that generate code from other projects is not allowed. + Thank you! diff --git a/README.md b/README.md index 3439ca31..ceae9997 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,10 @@ and test cases came directly from that project. ### Related * CLI - * [markdownlint-cli command-line interface for Node.js](https://github.com/igorshubovych/markdownlint-cli) - * [markdownlint-cli2 command-line interface for Node.js](https://github.com/DavidAnson/markdownlint-cli2) + * [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) + command-line interface for Node.js ([works with pre-commit](https://github.com/igorshubovych/markdownlint-cli#use-with-pre-commit)) + * [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) + command-line interface for Node.js ([works with pre-commit](https://github.com/DavidAnson/markdownlint-cli2#pre-commit)) * GitHub * [GitHub Super-Linter Action](https://github.com/github/super-linter) * [GitHub Actions problem matcher for markdownlint-cli](https://github.com/xt0rted/markdownlint-problem-matcher) @@ -102,6 +104,8 @@ playground for learning and exploring. * **[MD046](doc/Rules.md#md046)** *code-block-style* - Code block style * **[MD047](doc/Rules.md#md047)** *single-trailing-newline* - Files should end with a single newline character * **[MD048](doc/Rules.md#md048)** *code-fence-style* - Code fence style +* **[MD049](doc/Rules.md#md049)** *emphasis-style* - Emphasis style should be consistent +* **[MD050](doc/Rules.md#md050)** *strong-style* - Strong style should be consistent @@ -125,7 +129,7 @@ rules at once. * **blockquote** - MD027, MD028 * **bullet** - MD004, MD005, MD006, MD007, MD032 * **code** - MD014, MD031, MD038, MD040, MD046, MD048 -* **emphasis** - MD036, MD037 +* **emphasis** - MD036, MD037, MD049, MD050 * **hard_tab** - MD010 * **headers** - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043 @@ -163,10 +167,12 @@ appear in the final markup): * Disable all rules: `` * Enable all rules: `` -* Disable all rules for the next line only: `` +* Disable all rules for the next line only: + `` * Disable one or more rules by name: `` * Enable one or more rules by name: `` -* Disable one or more rules by name for the next line only: `` +* Disable one or more rules by name for the next line only: + `` * Capture the current rule configuration: `` * Restore the captured rule configuration: `` @@ -229,7 +235,7 @@ more rules for a file, the following more advanced syntax is supported: For example: ```markdown - + ``` or @@ -859,7 +865,7 @@ const results = window.markdownlint.sync(options).toString(); ## Examples For ideas how to integrate `markdownlint` into your workflow, refer to the -following projects or one of the tools in the [Related section](#Related): +following projects or one of the tools in the [Related section](#related): * [.NET Documentation](https://docs.microsoft.com/en-us/dotnet/) ([Search repository](https://github.com/dotnet/docs/search?q=markdownlint)) * [ally.js](https://allyjs.io/) ([Search repository](https://github.com/medialize/ally.js/search?q=markdownlint)) @@ -870,6 +876,7 @@ following projects or one of the tools in the [Related section](#Related): * [MDN Web Docs](https://developer.mozilla.org/) ([Search repository](https://github.com/mdn/content/search?q=markdownlint)) * [MkDocs](https://www.mkdocs.org/) ([Search repository](https://github.com/mkdocs/mkdocs/search?q=markdownlint)) * [Mocha](https://mochajs.org/) ([Search repository](https://github.com/mochajs/mocha/search?q=markdownlint)) +* [Pi-hole documentation](https://docs.pi-hole.net) ([Search repository](https://github.com/pi-hole/docs/search?q=markdownlint)) * [Reactable](https://glittershark.github.io/reactable/) ([Search repository](https://github.com/glittershark/reactable/search?q=markdownlint)) * [Sinon.JS](https://sinonjs.org/) ([Search repository](https://github.com/sinonjs/sinon/search?q=markdownlint)) * [TestCafe](https://devexpress.github.io/testcafe/) ([Search repository](https://github.com/DevExpress/testcafe/search?q=markdownlint)) @@ -968,6 +975,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more information. * 0.24.0 - Remove support for end-of-life Node version 10, add support for custom file system module, improve MD010/MD011/MD037/MD043/MD044, improve TypeScript declaration file and JSON schema, update dependencies. +* 0.25.0 - Add MD049/MD050 for consistent emphasis/strong style (both auto-fixable), improve + MD007/MD010/MD032/MD033/MD035/MD037/MD039, support asynchronous custom rules, + improve performance, improve CI process, reduce dependencies, update dependencies. [npm-image]: https://img.shields.io/npm/v/markdownlint.svg [npm-url]: https://www.npmjs.com/package/markdownlint diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index 294c2b84..896a5608 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -1,4 +1,4 @@ -/*! markdownlint 0.24.0 https://github.com/DavidAnson/markdownlint @license MIT */ +/*! markdownlint 0.25.0 https://github.com/DavidAnson/markdownlint @license MIT */ var markdownlint; /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ @@ -25,12 +25,11 @@ module.exports = webpackEmptyContext; /*!*****************************!*\ !*** ../helpers/helpers.js ***! \*****************************/ -/***/ ((module, __unused_webpack_exports, __webpack_require__) => { +/***/ ((module) => { "use strict"; // @ts-check -var os = __webpack_require__(/*! os */ "?591e"); // Regular expression for matching common newline characters // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js var newLineRe = /\r\n?|\n/g; @@ -77,12 +76,18 @@ module.exports.isObject = function isObject(obj) { return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj); }; // Returns true iff the input line is blank (no content) -// Example: Contains nothing, whitespace, or comments -var blankLineRe = />|(?:)/g; +// Example: Contains nothing, whitespace, or comment (unclosed start/end okay) module.exports.isBlankLine = function isBlankLine(line) { // Call to String.replace follows best practices and is not a security check // False-positive for js/incomplete-multi-character-sanitization - return !line || !line.trim() || !line.replace(blankLineRe, "").trim(); + return (!line || + !line.trim() || + !line + .replace(//g, "") + .replace(//g, "") + .replace(/>/g, "") + .trim()); }; /** * Compare function for Array.prototype.sort for ascending order of numbers. @@ -190,6 +195,21 @@ module.exports.fencedCodeBlockStyleFor = return "backtick"; } }; +/** + * Return the string representation of a emphasis or strong markup character. + * + * @param {string} markup Emphasis or strong string. + * @returns {string} String representation. + */ +module.exports.emphasisOrStrongStyleFor = + function emphasisOrStrongStyleFor(markup) { + switch (markup[0]) { + case "*": + return "asterisk"; + default: + return "underscore"; + } + }; /** * Return the number of characters of indent for a token. * @@ -255,6 +275,7 @@ function isMathBlock(token) { token.type.startsWith("math_block") && !token.type.endsWith("_end")); } +module.exports.isMathBlock = isMathBlock; // Get line metadata array module.exports.getLineMetadata = function getLineMetadata(params) { var lineMetadata = params.lines.map(function (line, index) { return [line, index, false, 0, false, false, false, false]; }); @@ -292,14 +313,20 @@ module.exports.getLineMetadata = function getLineMetadata(params) { }); return lineMetadata; }; -// Calls the provided function for each line (with context) -module.exports.forEachLine = function forEachLine(lineMetadata, handler) { +/** + * Calls the provided function for each line. + * + * @param {Object} lineMetadata Line metadata object. + * @param {Function} handler Function taking (line, lineIndex, inCode, onFence, + * inTable, inItem, inBreak, inMath). + * @returns {void} + */ +function forEachLine(lineMetadata, handler) { lineMetadata.forEach(function forMetadata(metadata) { - // Parameters: - // line, lineIndex, inCode, onFence, inTable, inItem, inBreak, inMath handler.apply(void 0, metadata); }); -}; +} +module.exports.forEachLine = forEachLine; // Returns (nested) lists as a flat array (in order) module.exports.flattenLists = function flattenLists(tokens) { var flattenedLists = []; @@ -309,10 +336,6 @@ module.exports.flattenLists = function flattenLists(tokens) { var nestingStack = []; var lastWithMap = { "map": [0, 1] }; tokens.forEach(function (token) { - if (isMathBlock(token) && token.map[1]) { - // markdown-it-texmath plugin does not account for math_block_end - token.map[1]++; - } if ((token.type === "bullet_list_open") || (token.type === "ordered_list_open")) { // Save current context and start a new one @@ -388,7 +411,8 @@ module.exports.forEachHeading = function forEachHeading(params, handler) { * Calls the provided function for each inline code span's content. * * @param {string} input Markdown content. - * @param {Function} handler Callback function. + * @param {Function} handler Callback function taking (code, lineIndex, + * columnIndex, ticks). * @returns {void} */ function forEachInlineCodeSpan(input, handler) { @@ -518,19 +542,35 @@ module.exports.addErrorContext = function addErrorContext(onError, lineNumber, c addError(onError, lineNumber, null, context, range, fixInfo); }; /** - * Returns an array of code span ranges. + * Returns an array of code block and span content ranges. * - * @param {string[]} lines Lines to scan for code span ranges. - * @returns {number[][]} Array of ranges (line, index, length). + * @param {Object} params RuleParams instance. + * @param {Object} lineMetadata Line metadata object. + * @returns {number[][]} Array of ranges (lineIndex, columnIndex, length). */ -module.exports.inlineCodeSpanRanges = function (lines) { +module.exports.codeBlockAndSpanRanges = function (params, lineMetadata) { var exclusions = []; - forEachInlineCodeSpan(lines.join("\n"), function (code, lineIndex, columnIndex) { - var codeLines = code.split(newLineRe); - // eslint-disable-next-line unicorn/no-for-loop - for (var i = 0; i < codeLines.length; i++) { - exclusions.push([lineIndex + i, columnIndex, codeLines[i].length]); - columnIndex = 0; + // Add code block ranges (excludes fences) + forEachLine(lineMetadata, function (line, lineIndex, inCode, onFence) { + if (inCode && !onFence) { + exclusions.push([lineIndex, 0, line.length]); + } + }); + // Add code span ranges (excludes ticks) + filterTokens(params, "inline", function (token) { + if (token.children.some(function (child) { return child.type === "code_inline"; })) { + var tokenLines = params.lines.slice(token.map[0], token.map[1]); + forEachInlineCodeSpan(tokenLines.join("\n"), function (code, lineIndex, columnIndex) { + var codeLines = code.split(newLineRe); + for (var _i = 0, _a = codeLines.entries(); _i < _a.length; _i++) { + var _b = _a[_i], i = _b[0], line = _b[1]; + exclusions.push([ + token.lineNumber - 1 + lineIndex + i, + i ? 0 : columnIndex, + line.length + ]); + } + }); } }); return exclusions; @@ -553,8 +593,8 @@ module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) { var match = line.match(regexp); if (match) { var column = match.index + 1; - var length_1 = match[0].length; - range = [column, length_1]; + var length = match[0].length; + range = [column, length]; } return range; }; @@ -575,6 +615,18 @@ module.exports.frontMatterHasTitle = function emphasisMarkersInContent(params) { var lines = params.lines; var byLine = new Array(lines.length); + // Search links + lines.forEach(function (tokenLine, tokenLineIndex) { + var inLine = []; + var linkMatch = null; + while ((linkMatch = linkRe.exec(tokenLine))) { + var markerMatch = null; + while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) { + inLine.push(linkMatch.index + markerMatch.index); + } + } + byLine[tokenLineIndex] = inLine; + }); // Search code spans filterTokens(params, "inline", function (token) { var children = token.children, lineNumber = token.lineNumber, map = token.map; @@ -583,30 +635,18 @@ function emphasisMarkersInContent(params) { forEachInlineCodeSpan(tokenLines.join("\n"), function (code, lineIndex, column, tickCount) { var codeLines = code.split(newLineRe); codeLines.forEach(function (codeLine, codeLineIndex) { + var byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex; + var inLine = byLine[byLineIndex]; + var codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount; var match = null; while ((match = emphasisMarkersRe.exec(codeLine))) { - var byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex; - var inLine = byLine[byLineIndex] || []; - var codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount; inLine.push(codeLineOffset + match.index); - byLine[byLineIndex] = inLine; } + byLine[byLineIndex] = inLine; }); }); } }); - // Search links - lines.forEach(function (tokenLine, tokenLineIndex) { - var linkMatch = null; - while ((linkMatch = linkRe.exec(tokenLine))) { - var markerMatch = null; - while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) { - var inLine = byLine[tokenLineIndex] || []; - inLine.push(linkMatch.index + markerMatch.index); - byLine[tokenLineIndex] = inLine; - } - } - }); return byLine; } module.exports.emphasisMarkersInContent = emphasisMarkersInContent; @@ -614,9 +654,10 @@ module.exports.emphasisMarkersInContent = emphasisMarkersInContent; * Gets the most common line ending, falling back to the platform default. * * @param {string} input Markdown content to analyze. + * @param {string} [platform] Platform identifier (process.platform). * @returns {string} Preferred line ending. */ -function getPreferredLineEnding(input) { +function getPreferredLineEnding(input, platform) { var cr = 0; var lf = 0; var crlf = 0; @@ -637,7 +678,8 @@ function getPreferredLineEnding(input) { }); var preferredLineEnding = null; if (!cr && !lf && !crlf) { - preferredLineEnding = os.EOL; + preferredLineEnding = + ((platform || process.platform) === "win32") ? "\r\n" : "\n"; } else if ((lf >= crlf) && (lf >= cr)) { preferredLineEnding = "\n"; @@ -745,6 +787,79 @@ module.exports.applyFixes = function applyFixes(input, errors) { // Return corrected input return lines.filter(function (line) { return line !== null; }).join(lineEnding); }; +/** + * Gets the range and fixInfo values for reporting an error if the expected + * text is found on the specified line. + * + * @param {string[]} lines Lines of Markdown content. + * @param {number} lineIndex Line index to check. + * @param {string} search Text to search for. + * @param {string} replace Text to replace with. + * @returns {Object} Range and fixInfo wrapper. + */ +function getRangeAndFixInfoIfFound(lines, lineIndex, search, replace) { + var range = null; + var fixInfo = null; + var searchIndex = lines[lineIndex].indexOf(search); + if (searchIndex !== -1) { + var column = searchIndex + 1; + var length = search.length; + range = [column, length]; + fixInfo = { + "editColumn": column, + "deleteCount": length, + "insertText": replace + }; + } + return { + range: range, + fixInfo: fixInfo + }; +} +module.exports.getRangeAndFixInfoIfFound = getRangeAndFixInfoIfFound; +/** + * Gets the next (subsequent) child token if it is of the expected type. + * + * @param {Object} parentToken Parent token. + * @param {Object} childToken Child token basis. + * @param {string} nextType Token type of next token. + * @param {string} nextNextType Token type of next-next token. + * @returns {Object} Next token. + */ +function getNextChildToken(parentToken, childToken, nextType, nextNextType) { + var children = parentToken.children; + var index = children.indexOf(childToken); + if ((index !== -1) && + (children.length > index + 2) && + (children[index + 1].type === nextType) && + (children[index + 2].type === nextNextType)) { + return children[index + 1]; + } + return null; +} +module.exports.getNextChildToken = getNextChildToken; +/** + * Calls Object.freeze() on an object and its children. + * + * @param {Object} obj Object to deep freeze. + * @returns {Object} Object passed to the function. + */ +function deepFreeze(obj) { + var pending = [obj]; + var current = null; + while ((current = pending.shift())) { + Object.freeze(current); + for (var _i = 0, _a = Object.getOwnPropertyNames(current); _i < _a.length; _i++) { + var name = _a[_i]; + var value = current[name]; + if (value && (typeof value === "object")) { + pending.push(value); + } + } + } + return obj; +} +module.exports.deepFreeze = deepFreeze; /***/ }), @@ -758,6 +873,13 @@ module.exports.applyFixes = function applyFixes(input, errors) { "use strict"; // @ts-check +var codeBlockAndSpanRanges = null; +module.exports.codeBlockAndSpanRanges = function (value) { + if (value) { + codeBlockAndSpanRanges = value; + } + return codeBlockAndSpanRanges; +}; var flattenedLists = null; module.exports.flattenedLists = function (value) { if (value) { @@ -765,13 +887,6 @@ module.exports.flattenedLists = function (value) { } return flattenedLists; }; -var inlineCodeSpanRanges = null; -module.exports.inlineCodeSpanRanges = function (value) { - if (value) { - inlineCodeSpanRanges = value; - } - return inlineCodeSpanRanges; -}; var lineMetadata = null; module.exports.lineMetadata = function (value) { if (value) { @@ -780,12 +895,28 @@ module.exports.lineMetadata = function (value) { return lineMetadata; }; module.exports.clear = function () { + codeBlockAndSpanRanges = null; flattenedLists = null; - inlineCodeSpanRanges = null; lineMetadata = null; }; +/***/ }), + +/***/ "../lib/constants.js": +/*!***************************!*\ + !*** ../lib/constants.js ***! + \***************************/ +/***/ ((module) => { + +"use strict"; +// @ts-check + +module.exports.deprecatedRuleNames = ["MD002", "MD006"]; +module.exports.homepage = "https://github.com/DavidAnson/markdownlint"; +module.exports.version = "0.25.0"; + + /***/ }), /***/ "../lib/markdownlint.js": @@ -808,14 +939,19 @@ var __assign = (this && this.__assign) || function () { }; return __assign.apply(this, arguments); }; -var __spreadArray = (this && this.__spreadArray) || function (to, from) { - for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) - to[j] = from[i]; - return to; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); }; var path = __webpack_require__(/*! path */ "?b85c"); -var promisify = __webpack_require__(/*! util */ "?96a2").promisify; +var promisify = (__webpack_require__(/*! util */ "?96a2").promisify); var markdownIt = __webpack_require__(/*! markdown-it */ "markdown-it"); +var deprecatedRuleNames = (__webpack_require__(/*! ./constants */ "../lib/constants.js").deprecatedRuleNames); var rules = __webpack_require__(/*! ./rules */ "../lib/rules.js"); var helpers = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"); var cache = __webpack_require__(/*! ./cache */ "../lib/cache.js"); @@ -823,14 +959,14 @@ var cache = __webpack_require__(/*! ./cache */ "../lib/cache.js"); // eslint-disable-next-line camelcase, max-len, no-inline-comments, no-undef var dynamicRequire = (typeof require === "undefined") ? __webpack_require__("../lib sync recursive") : /* c8 ignore next */ require; // Capture native require implementation for dynamic loading of modules -var deprecatedRuleNames = ["MD002", "MD006"]; /** * Validate the list of rules for structure and reuse. * * @param {Rule[]} ruleList List of rules. + * @param {boolean} synchronous Whether to execute synchronously. * @returns {string} Error message if validation fails. */ -function validateRuleList(ruleList) { +function validateRuleList(ruleList, synchronous) { var result = null; if (ruleList.length === rules.length) { // No need to validate if only using built-in rules @@ -867,6 +1003,15 @@ function validateRuleList(ruleList) { (Object.getPrototypeOf(rule.information) !== URL.prototype)) { result = newError("information"); } + if (!result && + (rule.asynchronous !== undefined) && + (typeof rule.asynchronous !== "boolean")) { + result = newError("asynchronous"); + } + if (!result && rule.asynchronous && synchronous) { + result = new Error("Custom rule " + rule.names.join("/") + " at index " + customIndex + + " is asynchronous and can not be used in a synchronous context."); + } if (!result) { rule.names.forEach(function forName(name) { var nameUpper = name.toUpperCase(); @@ -983,23 +1128,22 @@ function removeFrontMatter(content, frontMatter) { * @returns {void} */ function annotateTokens(tokens, lines) { - var tableMap = null; + var trMap = null; tokens.forEach(function forToken(token) { - // Handle missing maps for table head/body - if ((token.type === "thead_open") || - (token.type === "tbody_open")) { - tableMap = __spreadArray([], token.map); + // Provide missing maps for table content + if (token.type === "tr_open") { + trMap = token.map; } - else if ((token.type === "tr_close") && - tableMap) { - tableMap[0]++; + else if (token.type === "tr_close") { + trMap = null; } - else if ((token.type === "thead_close") || - (token.type === "tbody_close")) { - tableMap = null; + if (!token.map && trMap) { + token.map = __spreadArray([], trMap, true); } - if (tableMap && !token.map) { - token.map = __spreadArray([], tableMap); + // Adjust maps for math blocks + if (helpers.isMathBlock(token) && token.map[1]) { + // markdown-it-texmath plugin does not account for math_block_end + token.map[1]++; } // Update token metadata if (token.map) { @@ -1120,8 +1264,7 @@ function getEnabledRulesPerLineNumber(ruleList, lines, frontMatterLines, noInlin var enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length); // Helper functions // eslint-disable-next-line jsdoc/require-jsdoc - function handleInlineConfig(perLine, forEachMatch, forEachLine) { - var input = perLine ? lines : [lines.join("\n")]; + function handleInlineConfig(input, forEachMatch, forEachLine) { input.forEach(function (line, lineIndex) { if (!noInlineConfig) { var match = null; @@ -1150,6 +1293,7 @@ function getEnabledRulesPerLineNumber(ruleList, lines, frontMatterLines, noInlin } // eslint-disable-next-line jsdoc/require-jsdoc function applyEnableDisable(action, parameter, state) { + state = __assign({}, state); var enabled = (action.startsWith("ENABLE")); var items = parameter ? parameter.trim().toUpperCase().split(/\s+/) : @@ -1159,38 +1303,40 @@ function getEnabledRulesPerLineNumber(ruleList, lines, frontMatterLines, noInlin state[ruleName] = enabled; }); }); + return state; } // eslint-disable-next-line jsdoc/require-jsdoc function enableDisableFile(action, parameter) { if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) { - applyEnableDisable(action, parameter, enabledRules); + enabledRules = applyEnableDisable(action, parameter, enabledRules); } } // eslint-disable-next-line jsdoc/require-jsdoc function captureRestoreEnableDisable(action, parameter) { if (action === "CAPTURE") { - capturedRules = __assign({}, enabledRules); + capturedRules = enabledRules; } else if (action === "RESTORE") { - enabledRules = __assign({}, capturedRules); + enabledRules = capturedRules; } else if ((action === "ENABLE") || (action === "DISABLE")) { - enabledRules = __assign({}, enabledRules); - applyEnableDisable(action, parameter, enabledRules); + enabledRules = applyEnableDisable(action, parameter, enabledRules); } } // eslint-disable-next-line jsdoc/require-jsdoc function updateLineState() { - enabledRulesPerLineNumber.push(__assign({}, enabledRules)); + enabledRulesPerLineNumber.push(enabledRules); } // eslint-disable-next-line jsdoc/require-jsdoc function disableNextLine(action, parameter, lineNumber) { if (action === "DISABLE-NEXT-LINE") { - applyEnableDisable(action, parameter, enabledRulesPerLineNumber[lineNumber + 1] || {}); + var nextLineNumber = frontMatterLines.length + lineNumber + 1; + enabledRulesPerLineNumber[nextLineNumber] = + applyEnableDisable(action, parameter, enabledRulesPerLineNumber[nextLineNumber] || {}); } } // Handle inline comments - handleInlineConfig(false, configureFile); + handleInlineConfig([lines.join("\n")], configureFile); var effectiveConfig = getEffectiveConfig(ruleList, config, aliasToRuleNames); ruleList.forEach(function (rule) { var ruleName = rule.names[0].toUpperCase(); @@ -1198,44 +1344,15 @@ function getEnabledRulesPerLineNumber(ruleList, lines, frontMatterLines, noInlin enabledRules[ruleName] = !!effectiveConfig[ruleName]; }); capturedRules = enabledRules; - handleInlineConfig(true, enableDisableFile); - handleInlineConfig(true, captureRestoreEnableDisable, updateLineState); - handleInlineConfig(true, disableNextLine); + handleInlineConfig(lines, enableDisableFile); + handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState); + handleInlineConfig(lines, disableNextLine); // Return results return { effectiveConfig: effectiveConfig, enabledRulesPerLineNumber: enabledRulesPerLineNumber }; } -/** - * Compare function for Array.prototype.sort for ascending order of errors. - * - * @param {LintError} a First error. - * @param {LintError} b Second error. - * @returns {number} Positive value if a>b, negative value if b array[index - 1].lineNumber); -} /** * Lints a string containing Markdown content. * @@ -1267,27 +1384,25 @@ function lintContent(ruleList, name, content, md, config, frontMatter, handleRul var _a = getEnabledRulesPerLineNumber(ruleList, lines, frontMatterLines, noInlineConfig, config, aliasToRuleNames), effectiveConfig = _a.effectiveConfig, enabledRulesPerLineNumber = _a.enabledRulesPerLineNumber; // Create parameters for rules var params = { - name: name, - tokens: tokens, - lines: lines, - frontMatterLines: frontMatterLines + "name": helpers.deepFreeze(name), + "tokens": helpers.deepFreeze(tokens), + "lines": helpers.deepFreeze(lines), + "frontMatterLines": helpers.deepFreeze(frontMatterLines) }; cache.lineMetadata(helpers.getLineMetadata(params)); cache.flattenedLists(helpers.flattenLists(params.tokens)); - cache.inlineCodeSpanRanges(helpers.inlineCodeSpanRanges(params.lines)); + cache.codeBlockAndSpanRanges(helpers.codeBlockAndSpanRanges(params, cache.lineMetadata())); // Function to run for each rule - var result = (resultVersion === 0) ? {} : []; + var results = []; // eslint-disable-next-line jsdoc/require-jsdoc function forRule(rule) { // Configure rule - var ruleNameFriendly = rule.names[0]; - var ruleName = ruleNameFriendly.toUpperCase(); + var ruleName = rule.names[0].toUpperCase(); params.config = effectiveConfig[ruleName]; // eslint-disable-next-line jsdoc/require-jsdoc function throwError(property) { throw new Error("Property '" + property + "' of onError parameter is incorrect."); } - var errors = []; // eslint-disable-next-line jsdoc/require-jsdoc function onError(errorInfo) { if (!errorInfo || @@ -1296,6 +1411,10 @@ function lintContent(ruleList, name, content, md, config, frontMatter, handleRul (errorInfo.lineNumber > lines.length)) { throwError("lineNumber"); } + var lineNumber = errorInfo.lineNumber + frontMatterLines.length; + if (!enabledRulesPerLineNumber[lineNumber][ruleName]) { + return; + } if (errorInfo.detail && !helpers.isString(errorInfo.detail)) { throwError("detail"); @@ -1356,83 +1475,115 @@ function lintContent(ruleList, name, content, md, config, frontMatter, handleRul cleanFixInfo.insertText = fixInfo.insertText; } } - errors.push({ - "lineNumber": errorInfo.lineNumber + frontMatterLines.length, - "detail": errorInfo.detail || null, - "context": errorInfo.context || null, - "range": errorInfo.range ? __spreadArray([], errorInfo.range) : null, + results.push({ + lineNumber: lineNumber, + "ruleName": rule.names[0], + "ruleNames": rule.names, + "ruleDescription": rule.description, + "ruleInformation": rule.information ? rule.information.href : null, + "errorDetail": errorInfo.detail || null, + "errorContext": errorInfo.context || null, + "errorRange": errorInfo.range ? __spreadArray([], errorInfo.range, true) : null, "fixInfo": fixInfo ? cleanFixInfo : null }); } - // Call (possibly external) rule function - if (handleRuleFailures) { - try { - rule["function"](params, onError); + // Call (possibly external) rule function to report errors + var catchCallsOnError = function (error) { return onError({ + "lineNumber": 1, + "detail": "This rule threw an exception: ".concat(error.message || error) + }); }; + var invokeRuleFunction = function () { return rule.function(params, onError); }; + if (rule.asynchronous) { + // Asynchronous rule, ensure it returns a Promise + var ruleFunctionPromise = Promise.resolve().then(invokeRuleFunction); + return handleRuleFailures ? + ruleFunctionPromise.catch(catchCallsOnError) : + ruleFunctionPromise; + } + // Synchronous rule + try { + invokeRuleFunction(); + } + catch (error) { + if (handleRuleFailures) { + catchCallsOnError(error); } - catch (error) { - onError({ - "lineNumber": 1, - "detail": "This rule threw an exception: " + error.message - }); + else { + throw error; + } + } + return null; + } + // eslint-disable-next-line jsdoc/require-jsdoc + function formatResults() { + // Sort results by rule name by line number + results.sort(function (a, b) { return (a.ruleName.localeCompare(b.ruleName) || + a.lineNumber - b.lineNumber); }); + if (resultVersion < 3) { + // Remove fixInfo and multiple errors for the same rule and line number + var noPrevious_1 = { + "ruleName": null, + "lineNumber": -1 + }; + results = results.filter(function (error, index, array) { + delete error.fixInfo; + var previous = array[index - 1] || noPrevious_1; + return ((error.ruleName !== previous.ruleName) || + (error.lineNumber !== previous.lineNumber)); + }); + } + if (resultVersion === 0) { + // Return a dictionary of rule->[line numbers] + var dictionary = {}; + for (var _i = 0, results_1 = results; _i < results_1.length; _i++) { + var error = results_1[_i]; + var ruleLines = dictionary[error.ruleName] || []; + ruleLines.push(error.lineNumber); + dictionary[error.ruleName] = ruleLines; + } + // @ts-ignore + results = dictionary; + } + else if (resultVersion === 1) { + // Use ruleAlias instead of ruleNames + for (var _a = 0, results_2 = results; _a < results_2.length; _a++) { + var error = results_2[_a]; + error.ruleAlias = error.ruleNames[1] || error.ruleName; + delete error.ruleNames; } } else { - rule["function"](params, onError); - } - // Record any errors (significant performance benefit from length check) - if (errors.length > 0) { - errors.sort(lineNumberComparison); - var filteredErrors = errors - .filter((resultVersion === 3) ? - filterAllValues : - uniqueFilterForSortedErrors) - .filter(function removeDisabledRules(error) { - return enabledRulesPerLineNumber[error.lineNumber][ruleName]; - }) - .map(function formatResults(error) { - if (resultVersion === 0) { - return error.lineNumber; - } - var errorObject = {}; - errorObject.lineNumber = error.lineNumber; - if (resultVersion === 1) { - errorObject.ruleName = ruleNameFriendly; - errorObject.ruleAlias = rule.names[1] || rule.names[0]; - } - else { - errorObject.ruleNames = rule.names; - } - errorObject.ruleDescription = rule.description; - errorObject.ruleInformation = - rule.information ? rule.information.href : null; - errorObject.errorDetail = error.detail; - errorObject.errorContext = error.context; - errorObject.errorRange = error.range; - if (resultVersion === 3) { - errorObject.fixInfo = error.fixInfo; - } - return errorObject; - }); - if (filteredErrors.length > 0) { - if (resultVersion === 0) { - result[ruleNameFriendly] = filteredErrors; - } - else { - Array.prototype.push.apply(result, filteredErrors); - } + // resultVersion 2 or 3: Remove unwanted ruleName + for (var _b = 0, results_3 = results; _b < results_3.length; _b++) { + var error = results_3[_b]; + delete error.ruleName; } } + return results; } // Run all rules + var ruleListAsync = ruleList.filter(function (rule) { return rule.asynchronous; }); + var ruleListSync = ruleList.filter(function (rule) { return !rule.asynchronous; }); + var ruleListAsyncFirst = __spreadArray(__spreadArray([], ruleListAsync, true), ruleListSync, true); + var callbackSuccess = function () { return callback(null, formatResults()); }; + var callbackError = function (error) { return callback(error instanceof Error ? error : new Error(error)); }; try { - ruleList.forEach(forRule); + var ruleResults = ruleListAsyncFirst.map(forRule); + if (ruleListAsync.length > 0) { + Promise.all(ruleResults.slice(0, ruleListAsync.length)) + .then(callbackSuccess) + .catch(callbackError); + } + else { + callbackSuccess(); + } } catch (error) { - cache.clear(); - return callback(error); + callbackError(error); + } + finally { + cache.clear(); } - cache.clear(); - return callback(null, result); } /** * Lints a file containing Markdown content. @@ -1480,13 +1631,13 @@ function lintInput(options, synchronous, callback) { callback = callback || function noop() { }; // eslint-disable-next-line unicorn/prefer-spread var ruleList = rules.concat(options.customRules || []); - var ruleErr = validateRuleList(ruleList); + var ruleErr = validateRuleList(ruleList, synchronous); if (ruleErr) { return callback(ruleErr); } var files = []; if (Array.isArray(options.files)) { - files = __spreadArray([], options.files); + files = __spreadArray([], options.files, true); } else if (options.files) { files = [String(options.files)]; @@ -1509,65 +1660,63 @@ function lintInput(options, synchronous, callback) { var fs = options.fs || __webpack_require__(/*! fs */ "?ec0a"); var results = newResults(ruleList); var done = false; - // Linting of strings is always synchronous - var syncItem = null; - // eslint-disable-next-line jsdoc/require-jsdoc - function syncCallback(err, result) { - if (err) { - done = true; - return callback(err); - } - results[syncItem] = result; - return null; - } - while (!done && (syncItem = stringsKeys.shift())) { - lintContent(ruleList, syncItem, strings[syncItem] || "", md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, syncCallback); - } - if (synchronous) { - // Lint files synchronously - while (!done && (syncItem = files.shift())) { - lintFile(ruleList, syncItem, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, fs, synchronous, syncCallback); - } - return done || callback(null, results); - } - // Lint files asynchronously var concurrency = 0; // eslint-disable-next-line jsdoc/require-jsdoc - function lintConcurrently() { - var asyncItem = files.shift(); - if (done) { - // Nothing to do + function lintWorker() { + var currentItem = null; + // eslint-disable-next-line jsdoc/require-jsdoc + function lintWorkerCallback(err, result) { + concurrency--; + if (err) { + done = true; + return callback(err); + } + results[currentItem] = result; + if (!synchronous) { + lintWorker(); + } + return null; } - else if (asyncItem) { + if (done) { + // Abort for error or nothing left to do + } + else if (files.length > 0) { + // Lint next file concurrency++; - lintFile(ruleList, asyncItem, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, fs, synchronous, function (err, result) { - concurrency--; - if (err) { - done = true; - return callback(err); - } - results[asyncItem] = result; - lintConcurrently(); - return null; - }); + currentItem = files.shift(); + lintFile(ruleList, currentItem, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, fs, synchronous, lintWorkerCallback); + } + else if (stringsKeys.length > 0) { + // Lint next string + concurrency++; + currentItem = stringsKeys.shift(); + lintContent(ruleList, currentItem, strings[currentItem] || "", md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, lintWorkerCallback); } else if (concurrency === 0) { + // Finish done = true; return callback(null, results); } return null; } - // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access - // delay suggests that a concurrency factor of 8 can eliminate the impact - // of that delay (i.e., total time is the same as with no delay). - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); + if (synchronous) { + while (!done) { + lintWorker(); + } + } + else { + // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access + // delay suggests that a concurrency factor of 8 can eliminate the impact + // of that delay (i.e., total time is the same as with no delay). + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + } return null; } /** @@ -1618,19 +1767,20 @@ function parseConfiguration(name, content, parsers) { var config = null; var message = ""; var errors = []; + var index = 0; // Try each parser (parsers || [JSON.parse]).every(function (parser) { try { config = parser(content); } catch (error) { - errors.push(error.message); + errors.push("Parser ".concat(index++, ": ").concat(error.message)); } return !config; }); // Message if unable to parse if (!config) { - errors.unshift("Unable to parse '" + name + "'"); + errors.unshift("Unable to parse '".concat(name, "'")); message = errors.join("; "); } return { @@ -1729,9 +1879,9 @@ function readConfig(file, parsers, fs, callback) { return callback(new Error(message)); } // Extend configuration - var configExtends = config["extends"]; + var configExtends = config.extends; if (configExtends) { - delete config["extends"]; + delete config.extends; return resolveConfigExtends(file, configExtends, fs, function (_, resolvedExtends) { return readConfig(resolvedExtends, parsers, fs, function (errr, extendsConfig) { if (errr) { return callback(errr); @@ -1776,9 +1926,9 @@ function readConfigSync(file, parsers, fs) { throw new Error(message); } // Extend configuration - var configExtends = config["extends"]; + var configExtends = config.extends; if (configExtends) { - delete config["extends"]; + delete config.extends; var resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs); return __assign(__assign({}, readConfigSync(resolvedExtends, parsers, fs)), config); } @@ -1790,7 +1940,7 @@ function readConfigSync(file, parsers, fs) { * @returns {string} SemVer string. */ function getVersion() { - return __webpack_require__(/*! ../package.json */ "../package.json").version; + return (__webpack_require__(/*! ./constants */ "../lib/constants.js").version); } // Export a/synchronous/Promise APIs markdownlint.sync = markdownlintSync; @@ -1844,7 +1994,7 @@ module.exports = { "use strict"; // @ts-check -var addErrorDetailIf = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addErrorDetailIf; +var addErrorDetailIf = (__webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addErrorDetailIf); module.exports = { "names": ["MD002", "first-heading-h1", "first-header-h1"], "description": "First heading should be a top-level heading", @@ -1922,7 +2072,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, listItemMarkerRe = _a.listItemMarkerRe, unorderedListStyleFor = _a.unorderedListStyleFor; -var flattenedLists = __webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists; +var flattenedLists = (__webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists); var expectedStyleToMarker = { "dash": "-", "plus": "+", @@ -1967,8 +2117,8 @@ module.exports = { var match = item.line.match(listItemMarkerRe); if (match) { var column = match.index + 1; - var length_1 = match[0].length; - range = [column, length_1]; + var length = match[0].length; + range = [column, length]; fixInfo = { "editColumn": match[1].length + 1, "deleteCount": 1, @@ -1995,7 +2145,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, addErrorDetailIf = _a.addErrorDetailIf, indentFor = _a.indentFor, listItemMarkerRe = _a.listItemMarkerRe, orderedListItemMarkerRe = _a.orderedListItemMarkerRe, rangeFromRegExp = _a.rangeFromRegExp; -var flattenedLists = __webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists; +var flattenedLists = (__webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists); module.exports = { "names": ["MD005", "list-indent"], "description": "Inconsistent indentation for list items at the same level", @@ -2025,8 +2175,8 @@ module.exports = { } else { var detail = endMatching ? - "Expected: (" + expectedEnd + "); Actual: (" + actualEnd + ")" : - "Expected: " + expectedIndent + "; Actual: " + actualIndent; + "Expected: (".concat(expectedEnd, "); Actual: (").concat(actualEnd, ")") : + "Expected: ".concat(expectedIndent, "; Actual: ").concat(actualIndent); var expected = endMatching ? expectedEnd - markerLength : expectedIndent; @@ -2059,7 +2209,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, listItemMarkerRe = _a.listItemMarkerRe, rangeFromRegExp = _a.rangeFromRegExp; -var flattenedLists = __webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists; +var flattenedLists = (__webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists); module.exports = { "names": ["MD006", "ul-start-left"], "description": "Consider starting bulleted lists at the beginning of the line", @@ -2091,7 +2241,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, indentFor = _a.indentFor, listItemMarkerRe = _a.listItemMarkerRe; -var flattenedLists = __webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists; +var flattenedLists = (__webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists); module.exports = { "names": ["MD007", "ul-indent"], "description": "Unordered list indentation", @@ -2099,12 +2249,13 @@ module.exports = { "function": function MD007(params, onError) { var indent = Number(params.config.indent || 2); var startIndented = !!params.config.start_indented; + var startIndent = Number(params.config.start_indent || indent); flattenedLists().forEach(function (list) { if (list.unordered && list.parentsUnordered) { list.items.forEach(function (item) { var lineNumber = item.lineNumber, line = item.line; - var expectedNesting = list.nesting + (startIndented ? 1 : 0); - var expectedIndent = expectedNesting * indent; + var expectedIndent = (startIndented ? startIndent : 0) + + (list.nesting * indent); var actualIndent = indentFor(item); var range = null; var editColumn = 1; @@ -2137,7 +2288,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, filterTokens = _a.filterTokens, forEachInlineCodeSpan = _a.forEachInlineCodeSpan, forEachLine = _a.forEachLine, includesSorted = _a.includesSorted, newLineRe = _a.newLineRe, numericSortAscending = _a.numericSortAscending; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata); module.exports = { "names": ["MD009", "no-trailing-spaces"], "description": "Trailing spaces", @@ -2212,8 +2363,8 @@ module.exports = { "use strict"; // @ts-check -var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine, overlapsAnyRange = _a.overlapsAnyRange; +var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, lineMetadata = _b.lineMetadata; var tabRe = /\t+/g; module.exports = { "names": ["MD010", "no-hard-tabs"], @@ -2221,22 +2372,26 @@ module.exports = { "tags": ["whitespace", "hard_tab"], "function": function MD010(params, onError) { var codeBlocks = params.config.code_blocks; - var includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; + var includeCode = (codeBlocks === undefined) ? true : !!codeBlocks; var spacesPerTab = params.config.spaces_per_tab; var spaceMultiplier = (spacesPerTab === undefined) ? 1 : Math.max(0, Number(spacesPerTab)); + var exclusions = includeCode ? [] : codeBlockAndSpanRanges(); forEachLine(lineMetadata(), function (line, lineIndex, inCode) { - if (!inCode || includeCodeBlocks) { + if (includeCode || !inCode) { var match = null; while ((match = tabRe.exec(line)) !== null) { - var column = match.index + 1; - var length_1 = match[0].length; - addError(onError, lineIndex + 1, "Column: " + column, null, [column, length_1], { - "editColumn": column, - "deleteCount": length_1, - "insertText": "".padEnd(length_1 * spaceMultiplier) - }); + var index = match.index; + var column = index + 1; + var length = match[0].length; + if (!overlapsAnyRange(exclusions, lineIndex, index, length)) { + addError(onError, lineIndex + 1, "Column: " + column, null, [column, length], { + "editColumn": column, + "deleteCount": length, + "insertText": "".padEnd(length * spaceMultiplier) + }); + } } } }); @@ -2256,28 +2411,28 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine, overlapsAnyRange = _a.overlapsAnyRange; -var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), inlineCodeSpanRanges = _b.inlineCodeSpanRanges, lineMetadata = _b.lineMetadata; +var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, lineMetadata = _b.lineMetadata; var reversedLinkRe = /(^|[^\\])\(([^)]+)\)\[([^\]^][^\]]*)](?!\()/g; module.exports = { "names": ["MD011", "no-reversed-links"], "description": "Reversed link syntax", "tags": ["links"], "function": function MD011(params, onError) { - var exclusions = inlineCodeSpanRanges(); + var exclusions = codeBlockAndSpanRanges(); forEachLine(lineMetadata(), function (line, lineIndex, inCode, onFence) { if (!inCode && !onFence) { var match = null; while ((match = reversedLinkRe.exec(line)) !== null) { var reversedLink = match[0], preChar = match[1], linkText = match[2], linkDestination = match[3]; var index = match.index + preChar.length; - var length_1 = match[0].length - preChar.length; + var length = match[0].length - preChar.length; if (!linkText.endsWith("\\") && !linkDestination.endsWith("\\") && - !overlapsAnyRange(exclusions, lineIndex, index, length_1)) { - addError(onError, lineIndex + 1, reversedLink.slice(preChar.length), null, [index + 1, length_1], { + !overlapsAnyRange(exclusions, lineIndex, index, length)) { + addError(onError, lineIndex + 1, reversedLink.slice(preChar.length), null, [index + 1, length], { "editColumn": index + 1, - "deleteCount": length_1, - "insertText": "[" + linkText + "](" + linkDestination + ")" + "deleteCount": length, + "insertText": "[".concat(linkText, "](").concat(linkDestination, ")") }); } } @@ -2299,7 +2454,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, forEachLine = _a.forEachLine; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata); module.exports = { "names": ["MD012", "no-multiple-blanks"], "description": "Multiple consecutive blank lines", @@ -2331,7 +2486,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, filterTokens = _a.filterTokens, forEachHeading = _a.forEachHeading, forEachLine = _a.forEachLine, includesSorted = _a.includesSorted; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata); var longLineRePrefix = "^.{"; var longLineRePostfixRelaxed = "}.*\\s.*$"; var longLineRePostfixStrict = "}.+$"; @@ -2441,8 +2596,8 @@ module.exports = { var match = dollarCommandRe.exec(line); if (match) { var column = match[1].length + 1; - var length_1 = match[2].length; - dollarInstances.push([i, lineTrim, column, length_1]); + var length = match[2].length; + dollarInstances.push([i, lineTrim, column, length]); } else { allDollars = false; @@ -2476,7 +2631,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorContext = _a.addErrorContext, forEachLine = _a.forEachLine; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata); module.exports = { "names": ["MD018", "no-missing-space-atx"], "description": "No space after hash on atx style heading", @@ -2544,7 +2699,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorContext = _a.addErrorContext, forEachLine = _a.forEachLine; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata); module.exports = { "names": ["MD020", "no-missing-space-closed-atx"], "description": "No space inside hashes on closed atx style heading", @@ -2559,7 +2714,7 @@ module.exports = { var rightHashLength = rightHash.length; var left = !leftSpaceLength; var right = !rightSpaceLength || rightEscape; - var rightEscapeReplacement = rightEscape ? rightEscape + " " : ""; + var rightEscapeReplacement = rightEscape ? "".concat(rightEscape, " ") : ""; if (left || right) { var range = left ? [ @@ -2573,7 +2728,7 @@ module.exports = { addErrorContext(onError, lineIndex + 1, line.trim(), left, right, range, { "editColumn": 1, "deleteCount": line.length, - "insertText": leftHash + " " + content + " " + rightEscapeReplacement + rightHash + "insertText": "".concat(leftHash, " ").concat(content, " ").concat(rightEscapeReplacement).concat(rightHash) }); } } @@ -2609,7 +2764,7 @@ module.exports = { var left = leftSpaceLength > 1; var right = rightSpaceLength > 1; if (left || right) { - var length_1 = line.length; + var length = line.length; var leftHashLength = leftHash.length; var rightHashLength = rightHash.length; var range = left ? @@ -2618,13 +2773,13 @@ module.exports = { leftHashLength + leftSpaceLength + 1 ] : [ - length_1 - trailSpaceLength - rightHashLength - rightSpaceLength, + length - trailSpaceLength - rightHashLength - rightSpaceLength, rightSpaceLength + rightHashLength + 1 ]; addErrorContext(onError, lineNumber, line.trim(), left, right, range, { "editColumn": 1, - "deleteCount": length_1, - "insertText": leftHash + " " + content + " " + rightHash + "deleteCount": length, + "insertText": "".concat(leftHash, " ").concat(content, " ").concat(rightHash) }); } } @@ -2829,10 +2984,10 @@ module.exports = { if (match && !endOfLineHtmlEntityRe.test(trimmedLine)) { var fullMatch = match[0]; var column = match.index + 1; - var length_1 = fullMatch.length; - addError(onError, lineNumber, "Punctuation: '" + fullMatch + "'", null, [column, length_1], { + var length = fullMatch.length; + addError(onError, lineNumber, "Punctuation: '".concat(fullMatch, "'"), null, [column, length], { "editColumn": column, - "deleteCount": length_1 + "deleteCount": length }); } }); @@ -2906,7 +3061,7 @@ module.exports = { "use strict"; // @ts-check -var addError = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addError; +var addError = (__webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addError); module.exports = { "names": ["MD028", "no-blanks-blockquote"], "description": "Blank line inside blockquote", @@ -2942,7 +3097,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorDetailIf = _a.addErrorDetailIf, listItemMarkerRe = _a.listItemMarkerRe, orderedListItemMarkerRe = _a.orderedListItemMarkerRe, rangeFromRegExp = _a.rangeFromRegExp; -var flattenedLists = __webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists; +var flattenedLists = (__webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists); var listStyleExamples = { "one": "1/1/1", "ordered": "1/2/3", @@ -3011,8 +3166,8 @@ module.exports = { "use strict"; // @ts-check -var addErrorDetailIf = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addErrorDetailIf; -var flattenedLists = __webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists; +var addErrorDetailIf = (__webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addErrorDetailIf); +var flattenedLists = (__webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists); module.exports = { "names": ["MD030", "list-marker-space"], "description": "Spaces after list markers", @@ -3061,7 +3216,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorContext = _a.addErrorContext, forEachLine = _a.forEachLine, isBlankLine = _a.isBlankLine; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata); var codeFencePrefixRe = /^(.*?)\s*[`~]/; module.exports = { "names": ["MD031", "blanks-around-fences"], @@ -3080,7 +3235,7 @@ module.exports = { var _a = line.match(codeFencePrefixRe) || [], prefix = _a[1]; var fixInfo = (prefix === undefined) ? null : { "lineNumber": i + (onTopFence ? 1 : 2), - "insertText": prefix + "\n" + "insertText": "".concat(prefix, "\n") }; addErrorContext(onError, i + 1, lines[i].trim(), null, null, null, fixInfo); } @@ -3101,7 +3256,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorContext = _a.addErrorContext, isBlankLine = _a.isBlankLine; -var flattenedLists = __webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists; +var flattenedLists = (__webpack_require__(/*! ./cache */ "../lib/cache.js").flattenedLists); var quotePrefixRe = /^[>\s]*/; module.exports = { "names": ["MD032", "blanks-around-lists"], @@ -3115,7 +3270,7 @@ module.exports = { var line = lines[firstIndex]; var quotePrefix = line.match(quotePrefixRe)[0].trimEnd(); addErrorContext(onError, firstIndex + 1, line.trim(), null, null, null, { - "insertText": quotePrefix + "\n" + "insertText": "".concat(quotePrefix, "\n") }); } var lastIndex = list.lastLineIndex - 1; @@ -3124,7 +3279,7 @@ module.exports = { var quotePrefix = line.match(quotePrefixRe)[0].trimEnd(); addErrorContext(onError, lastIndex + 1, line.trim(), null, null, null, { "lineNumber": lastIndex + 2, - "insertText": quotePrefix + "\n" + "insertText": "".concat(quotePrefix, "\n") }); } }); @@ -3143,11 +3298,10 @@ module.exports = { "use strict"; // @ts-check -var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine, unescapeMarkdown = _a.unescapeMarkdown; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine, overlapsAnyRange = _a.overlapsAnyRange, unescapeMarkdown = _a.unescapeMarkdown; +var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, lineMetadata = _b.lineMetadata; var htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g; var linkDestinationRe = /]\(\s*$/; -var inlineCodeRe = /^[^`]*(`+[^`]+`+[^`]+)*`+[^`]*$/; // See https://spec.commonmark.org/0.29/#autolinks var emailAddressRe = // eslint-disable-next-line max-len @@ -3160,6 +3314,7 @@ module.exports = { var allowedElements = params.config.allowed_elements; allowedElements = Array.isArray(allowedElements) ? allowedElements : []; allowedElements = allowedElements.map(function (element) { return element.toLowerCase(); }); + var exclusions = codeBlockAndSpanRanges(); forEachLine(lineMetadata(), function (line, lineIndex, inCode) { var match = null; // eslint-disable-next-line no-unmodified-loop-condition @@ -3167,12 +3322,12 @@ module.exports = { var tag = match[0], content = match[1], element = match[2]; if (!allowedElements.includes(element.toLowerCase()) && !tag.endsWith("\\>") && - !emailAddressRe.test(content)) { + !emailAddressRe.test(content) && + !overlapsAnyRange(exclusions, lineIndex, match.index, match[0].length)) { var prefix = line.substring(0, match.index); - if (!linkDestinationRe.test(prefix) && !inlineCodeRe.test(prefix)) { + if (!linkDestinationRe.test(prefix)) { var unescaped = unescapeMarkdown(prefix + "<", "_"); - if (!unescaped.endsWith("_") && - ((unescaped + "`").match(/`/g).length % 2)) { + if (!unescaped.endsWith("_")) { addError(onError, lineIndex + 1, "Element: " + element, null, [match.index + 1, tag.length]); } } @@ -3232,7 +3387,7 @@ module.exports = { var fixInfo = range ? { "editColumn": range[0], "deleteCount": range[1], - "insertText": "<" + bareUrl + ">" + "insertText": "<".concat(bareUrl, ">") } : null; addErrorContext(onError, lineNumber, bareUrl, null, null, range, fixInfo); } @@ -3262,12 +3417,12 @@ module.exports = { "tags": ["hr"], "function": function MD035(params, onError) { var style = String(params.config.style || "consistent"); - filterTokens(params, "hr", function forToken(token) { - var lineTrim = token.line.trim(); + filterTokens(params, "hr", function (token) { + var lineNumber = token.lineNumber, markup = token.markup; if (style === "consistent") { - style = lineTrim; + style = markup; } - addErrorDetailIf(onError, token.lineNumber, style, lineTrim); + addErrorDetailIf(onError, lineNumber, style, markup); }); } }; @@ -3350,8 +3505,9 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorContext = _a.addErrorContext, emphasisMarkersInContent = _a.emphasisMarkersInContent, forEachLine = _a.forEachLine, isBlankLine = _a.isBlankLine; -var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var lineMetadata = (__webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata); var emphasisRe = /(^|[^\\]|\\\\)(?:(\*\*?\*?)|(__?_?))/g; +var embeddedUnderscoreRe = /([A-Za-z0-9])_([A-Za-z0-9])/g; var asteriskListItemMarkerRe = /^([\s>]*)\*(\s+)/; var leftSpaceRe = /^\s+/; var rightSpaceRe = /\s+$/; @@ -3390,20 +3546,20 @@ module.exports = { var contextEnd = matchIndex + contextLength; var context = line.substring(contextStart, contextEnd); var column = contextStart + 1; - var length_1 = contextEnd - contextStart; + var length = contextEnd - contextStart; var leftMarker = line.substring(contextStart, emphasisIndex); var rightMarker = match ? (match[2] || match[3]) : ""; - var fixedText = "" + leftMarker + content.trim() + rightMarker; + var fixedText = "".concat(leftMarker).concat(content.trim()).concat(rightMarker); return [ onError, lineIndex + 1, context, leftSpace, rightSpace, - [column, length_1], + [column, length], { "editColumn": column, - "deleteCount": length_1, + "deleteCount": length, "insertText": fixedText } ]; @@ -3431,14 +3587,15 @@ module.exports = { // Emphasis has no meaning here return; } + var patchedLine = line.replace(embeddedUnderscoreRe, "$1 $2"); if (onItemStart) { // Trim overlapping '*' list item marker - line = line.replace(asteriskListItemMarkerRe, "$1 $2"); + patchedLine = patchedLine.replace(asteriskListItemMarkerRe, "$1 $2"); } var match = null; // Match all emphasis-looking runs in the line... - while ((match = emphasisRe.exec(line))) { - var ignoreMarkersForLine = ignoreMarkersByLine[lineIndex] || []; + while ((match = emphasisRe.exec(patchedLine))) { + var ignoreMarkersForLine = ignoreMarkersByLine[lineIndex]; var matchIndex = match.index + match[1].length; if (ignoreMarkersForLine.includes(matchIndex)) { // Ignore emphasis markers inside code spans and links @@ -3581,7 +3738,7 @@ module.exports = { // @ts-check var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addErrorContext = _a.addErrorContext, filterTokens = _a.filterTokens; -var spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=\(\S*\))/; +var spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=((?:\([^)]*\))|(?:\[[^\]]*\])))/; module.exports = { "names": ["MD039", "no-space-in-links"], "description": "Spaces inside link text", @@ -3610,16 +3767,16 @@ module.exports = { var match = line.slice(lineIndex).match(spaceInLinkRe); if (match) { var column = match.index + lineIndex + 1; - var length_1 = match[0].length; - range = [column, length_1]; + var length = match[0].length; + range = [column, length]; fixInfo = { "editColumn": column + 1, - "deleteCount": length_1 - 2, + "deleteCount": length - 2, "insertText": linkText.trim() }; - lineIndex = column + length_1 - 1; + lineIndex = column + length - 1; } - addErrorContext(onError, lineNumber, "[" + linkText + "]", left, right, range, fixInfo); + addErrorContext(onError, lineNumber, "[".concat(linkText, "]"), left, right, range, fixInfo); } } else if ((type === "softbreak") || (type === "hardbreak")) { @@ -3682,7 +3839,7 @@ module.exports = { var tag = "h" + level; var foundFrontMatterTitle = frontMatterHasTitle(params.frontMatterLines, params.config.front_matter_title); if (!foundFrontMatterTitle) { - var htmlHeadingRe_1 = new RegExp("^]", "i"); + var htmlHeadingRe_1 = new RegExp("^]"), "i"); params.tokens.every(function (token) { var isError = false; if (token.type === "html_block") { @@ -3784,7 +3941,6 @@ module.exports = { var matchAny_1 = false; var hasError_1 = false; var anyHeadings_1 = false; - // eslint-disable-next-line func-style var getExpected_1 = function () { return requiredHeadings[i_1++] || "[None]"; }; forEachHeading(params, function (heading, content) { if (!hasError_1) { @@ -3837,7 +3993,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, overlapsAnyRange = _a.overlapsAnyRange, linkRe = _a.linkRe, linkReferenceRe = _a.linkReferenceRe; -var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), inlineCodeSpanRanges = _b.inlineCodeSpanRanges, lineMetadata = _b.lineMetadata; +var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, lineMetadata = _b.lineMetadata; module.exports = { "names": ["MD044", "proper-names"], "description": "Proper names should have the correct capitalization", @@ -3867,13 +4023,13 @@ module.exports = { } }); if (!includeCodeBlocks) { - exclusions.push.apply(exclusions, inlineCodeSpanRanges()); + exclusions.push.apply(exclusions, codeBlockAndSpanRanges()); } - var _loop_1 = function (name_1) { - var escapedName = escapeForRegExp(name_1); - var startNamePattern = /^\W/.test(name_1) ? "" : "\\b_*"; - var endNamePattern = /\W$/.test(name_1) ? "" : "_*\\b"; - var namePattern = "(" + startNamePattern + ")(" + escapedName + ")" + endNamePattern; + var _loop_1 = function (name) { + var escapedName = escapeForRegExp(name); + var startNamePattern = /^\W/.test(name) ? "" : "\\b_*"; + var endNamePattern = /\W$/.test(name) ? "" : "_*\\b"; + var namePattern = "(".concat(startNamePattern, ")(").concat(escapedName, ")").concat(endNamePattern); var nameRe = new RegExp(namePattern, "gi"); forEachLine(lineMetadata(), function (line, lineIndex, inCode, onFence) { if (includeCodeBlocks || (!inCode && !onFence)) { @@ -3881,22 +4037,22 @@ module.exports = { while ((match = nameRe.exec(line)) !== null) { var leftMatch = match[1], nameMatch = match[2]; var index = match.index + leftMatch.length; - var length_1 = nameMatch.length; - if (!overlapsAnyRange(exclusions, lineIndex, index, length_1)) { - addErrorDetailIf(onError, lineIndex + 1, name_1, nameMatch, null, null, [index + 1, length_1], { + var length = nameMatch.length; + if (!overlapsAnyRange(exclusions, lineIndex, index, length)) { + addErrorDetailIf(onError, lineIndex + 1, name, nameMatch, null, null, [index + 1, length], { "editColumn": index + 1, - "deleteCount": length_1, - "insertText": name_1 + "deleteCount": length, + "insertText": name }); } - exclusions.push([lineIndex, index, length_1]); + exclusions.push([lineIndex, index, length]); } } }); }; for (var _i = 0, names_1 = names; _i < names_1.length; _i++) { - var name_1 = names_1[_i]; - _loop_1(name_1); + var name = names_1[_i]; + _loop_1(name); } } }; @@ -3939,7 +4095,7 @@ module.exports = { "use strict"; // @ts-check -var addErrorDetailIf = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addErrorDetailIf; +var addErrorDetailIf = (__webpack_require__(/*! ../helpers */ "../helpers/helpers.js").addErrorDetailIf); var tokenTypeToStyle = { "fence": "fenced", "code_block": "indented" @@ -4024,6 +4180,88 @@ module.exports = { }; +/***/ }), + +/***/ "../lib/md049.js": +/*!***********************!*\ + !*** ../lib/md049.js ***! + \***********************/ +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; +// @ts-check + +var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, emphasisOrStrongStyleFor = _a.emphasisOrStrongStyleFor, forEachInlineChild = _a.forEachInlineChild, getNextChildToken = _a.getNextChildToken, getRangeAndFixInfoIfFound = _a.getRangeAndFixInfoIfFound; +module.exports = { + "names": ["MD049", "emphasis-style"], + "description": "Emphasis style should be consistent", + "tags": ["emphasis"], + "function": function MD049(params, onError) { + var expectedStyle = String(params.config.style || "consistent"); + forEachInlineChild(params, "em_open", function (token, parent) { + var lineNumber = token.lineNumber, markup = token.markup; + var markupStyle = emphasisOrStrongStyleFor(markup); + if (expectedStyle === "consistent") { + expectedStyle = markupStyle; + } + if (expectedStyle !== markupStyle) { + var rangeAndFixInfo = {}; + var contentToken = getNextChildToken(parent, token, "text", "em_close"); + if (contentToken) { + var content = contentToken.content; + var actual = "".concat(markup).concat(content).concat(markup); + var expectedMarkup = (expectedStyle === "asterisk") ? "*" : "_"; + var expected = "".concat(expectedMarkup).concat(content).concat(expectedMarkup); + rangeAndFixInfo = getRangeAndFixInfoIfFound(params.lines, lineNumber - 1, actual, expected); + } + addError(onError, lineNumber, "Expected: ".concat(expectedStyle, "; Actual: ").concat(markupStyle), null, rangeAndFixInfo.range, rangeAndFixInfo.fixInfo); + } + }); + } +}; + + +/***/ }), + +/***/ "../lib/md050.js": +/*!***********************!*\ + !*** ../lib/md050.js ***! + \***********************/ +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; +// @ts-check + +var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, emphasisOrStrongStyleFor = _a.emphasisOrStrongStyleFor, forEachInlineChild = _a.forEachInlineChild, getNextChildToken = _a.getNextChildToken, getRangeAndFixInfoIfFound = _a.getRangeAndFixInfoIfFound; +module.exports = { + "names": ["MD050", "strong-style"], + "description": "Strong style should be consistent", + "tags": ["emphasis"], + "function": function MD050(params, onError) { + var expectedStyle = String(params.config.style || "consistent"); + forEachInlineChild(params, "strong_open", function (token, parent) { + var lineNumber = token.lineNumber, markup = token.markup; + var markupStyle = emphasisOrStrongStyleFor(markup); + if (expectedStyle === "consistent") { + expectedStyle = markupStyle; + } + if (expectedStyle !== markupStyle) { + var rangeAndFixInfo = {}; + var contentToken = getNextChildToken(parent, token, "text", "strong_close"); + if (contentToken) { + var content = contentToken.content; + var actual = "".concat(markup).concat(content).concat(markup); + var expectedMarkup = (expectedStyle === "asterisk") ? "**" : "__"; + var expected = "".concat(expectedMarkup).concat(content).concat(expectedMarkup); + rangeAndFixInfo = getRangeAndFixInfoIfFound(params.lines, lineNumber - 1, actual, expected); + } + addError(onError, lineNumber, "Expected: ".concat(expectedStyle, "; Actual: ").concat(markupStyle), null, rangeAndFixInfo.range, rangeAndFixInfo.fixInfo); + } + }); + } +}; + + /***/ }), /***/ "../lib/rules.js": @@ -4035,9 +4273,7 @@ module.exports = { "use strict"; // @ts-check -var packageJson = __webpack_require__(/*! ../package.json */ "../package.json"); -var homepage = packageJson.homepage; -var version = packageJson.version; +var _a = __webpack_require__(/*! ./constants */ "../lib/constants.js"), homepage = _a.homepage, version = _a.version; var rules = [ __webpack_require__(/*! ./md001 */ "../lib/md001.js"), __webpack_require__(/*! ./md002 */ "../lib/md002.js"), @@ -4082,13 +4318,15 @@ var rules = [ __webpack_require__(/*! ./md045 */ "../lib/md045.js"), __webpack_require__(/*! ./md046 */ "../lib/md046.js"), __webpack_require__(/*! ./md047 */ "../lib/md047.js"), - __webpack_require__(/*! ./md048 */ "../lib/md048.js") + __webpack_require__(/*! ./md048 */ "../lib/md048.js"), + __webpack_require__(/*! ./md049 */ "../lib/md049.js"), + __webpack_require__(/*! ./md050 */ "../lib/md050.js") ]; rules.forEach(function (rule) { var name = rule.names[0].toLowerCase(); // eslint-disable-next-line dot-notation rule["information"] = - new URL(homepage + "/blob/v" + version + "/doc/Rules.md#" + name); + new URL("".concat(homepage, "/blob/v").concat(version, "/doc/Rules.md#").concat(name)); }); module.exports = rules; @@ -4106,16 +4344,6 @@ module.exports = markdownit; /***/ }), -/***/ "?591e": -/*!********************!*\ - !*** os (ignored) ***! - \********************/ -/***/ (() => { - -/* (ignored) */ - -/***/ }), - /***/ "?ec0a": /*!********************!*\ !*** fs (ignored) ***! @@ -4144,17 +4372,6 @@ module.exports = markdownit; /* (ignored) */ -/***/ }), - -/***/ "../package.json": -/*!***********************!*\ - !*** ../package.json ***! - \***********************/ -/***/ ((module) => { - -"use strict"; -module.exports = JSON.parse('{"name":"markdownlint","version":"0.24.0","description":"A Node.js style checker and lint tool for Markdown/CommonMark files.","main":"lib/markdownlint.js","types":"lib/markdownlint.d.ts","author":"David Anson (https://dlaa.me/)","license":"MIT","homepage":"https://github.com/DavidAnson/markdownlint","repository":{"type":"git","url":"https://github.com/DavidAnson/markdownlint.git"},"bugs":"https://github.com/DavidAnson/markdownlint/issues","scripts":{"build-config":"npm run build-config-schema && npm run build-config-example","build-config-example":"node schema/build-config-example.js","build-config-schema":"node schema/build-config-schema.js","build-declaration":"tsc --allowJs --declaration --emitDeclarationOnly --resolveJsonModule lib/markdownlint.js && node scripts delete \'lib/{c,md,r}*.d.ts\' \'helpers/*.d.ts\'","build-demo":"node scripts copy node_modules/markdown-it/dist/markdown-it.min.js demo/markdown-it.min.js && cd demo && webpack --no-stats","build-example":"npm install --no-save --ignore-scripts grunt grunt-cli gulp through2","ci":"npm-run-all --continue-on-error --parallel declaration lint --parallel build-config build-demo test-cover && git diff --exit-code","clone-test-repos-dotnet-docs":"cd test-repos && git clone https://github.com/dotnet/docs dotnet-docs --depth 1 --no-tags --quiet","clone-test-repos-eslint-eslint":"cd test-repos && git clone https://github.com/eslint/eslint eslint-eslint --depth 1 --no-tags --quiet","clone-test-repos-mkdocs-mkdocs":"cd test-repos && git clone https://github.com/mkdocs/mkdocs mkdocs-mkdocs --depth 1 --no-tags --quiet","clone-test-repos-mochajs-mocha":"cd test-repos && git clone https://github.com/mochajs/mocha mochajs-mocha --depth 1 --no-tags --quiet","clone-test-repos-pi-hole-docs":"cd test-repos && git clone https://github.com/pi-hole/docs pi-hole-docs --depth 1 --no-tags --quiet","clone-test-repos-v8-v8-dev":"cd test-repos && git clone https://github.com/v8/v8.dev v8-v8-dev --depth 1 --no-tags --quiet","clone-test-repos-webhintio-hint":"cd test-repos && git clone https://github.com/webhintio/hint webhintio-hint --depth 1 --no-tags --quiet","clone-test-repos-webpack-webpack-js-org":"cd test-repos && git clone https://github.com/webpack/webpack.js.org webpack-webpack-js-org --depth 1 --no-tags --quiet","clone-test-repos":"mkdir test-repos && cd test-repos && npm run clone-test-repos-eslint-eslint && npm run clone-test-repos-mkdocs-mkdocs && npm run clone-test-repos-mochajs-mocha && npm run clone-test-repos-pi-hole-docs && npm run clone-test-repos-webhintio-hint && npm run clone-test-repos-webpack-webpack-js-org","clone-test-repos-large":"npm run clone-test-repos && cd test-repos && npm run clone-test-repos-dotnet-docs && npm run clone-test-repos-v8-v8-dev","declaration":"npm run build-declaration && npm run test-declaration","example":"cd example && node standalone.js && grunt markdownlint --force && gulp markdownlint","lint":"eslint --max-warnings 0 .","lint-test-repos":"ava --timeout=5m test/markdownlint-test-repos.js","test":"ava test/markdownlint-test.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js","test-cover":"c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 npm test","test-declaration":"cd example/typescript && tsc && node type-check.js","test-extra":"ava --timeout=5m test/markdownlint-test-extra.js"},"engines":{"node":">=10"},"dependencies":{"markdown-it":"12.2.0"},"devDependencies":{"ava":"~3.15.0","c8":"~7.8.0","eslint":"~7.32.0","eslint-plugin-jsdoc":"~36.0.7","eslint-plugin-node":"~11.1.0","eslint-plugin-unicorn":"~35.0.0","globby":"~11.0.4","js-yaml":"~4.1.0","markdown-it-for-inline":"~0.1.1","markdown-it-sub":"~1.0.0","markdown-it-sup":"~1.0.0","markdown-it-texmath":"~0.9.1","markdownlint-rule-helpers":"~0.14.0","npm-run-all":"~4.1.5","strip-json-comments":"~3.1.1","toml":"~3.0.0","ts-loader":"~9.2.5","tv4":"~1.3.0","typescript":"~4.3.5","webpack":"~5.51.1","webpack-cli":"~4.8.0"},"keywords":["markdown","lint","md","CommonMark","markdownlint"]}'); - /***/ }) /******/ }); diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 3d775c70..4f79ffb5 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -1,8 +1,7 @@ { "compilerOptions": { "allowJs": true, - "outDir": "unused", - "resolveJsonModule": true + "outDir": "unused" }, "include": [ "../lib/*.js" diff --git a/demo/webpack.config.js b/demo/webpack.config.js index cab533ca..73a2b173 100644 --- a/demo/webpack.config.js +++ b/demo/webpack.config.js @@ -22,7 +22,8 @@ function config(options) { { "loader": "ts-loader", "options": { - "configFile": "../demo/tsconfig.json" + "configFile": "../demo/tsconfig.json", + "transpileOnly": true } } ] @@ -43,7 +44,6 @@ function config(options) { "resolve": { "fallback": { "fs": false, - "os": false, "path": false, "util": false } diff --git a/doc/CustomRules.md b/doc/CustomRules.md index 874bf9d5..42aca6b5 100644 --- a/doc/CustomRules.md +++ b/doc/CustomRules.md @@ -41,7 +41,8 @@ A rule is implemented as an `Object` with one optional and four required propert - `description` is a required `String` value that describes the rule in output messages. - `information` is an optional (absolute) `URL` of a link to more information about the rule. - `tags` is a required `Array` of `String` values that groups related rules for easier customization. -- `function` is a required synchronous `Function` that implements the rule and is passed two parameters: +- `asynchronous` is an optional `Boolean` value that indicates whether the rule returns a `Promise` and runs asynchronously. +- `function` is a required `Function` that implements the rule and is passed two parameters: - `params` is an `Object` with properties that describe the content being analyzed: - `name` is a `String` that identifies the input file/string. - `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token) @@ -51,7 +52,7 @@ A rule is implemented as an `Object` with one optional and four required propert - `config` is an `Object` corresponding to the rule's entry in `options.config` (if present). - `onError` is a function that takes a single `Object` parameter with one required and four optional properties: - `lineNumber` is a required `Number` specifying the 1-based line number of the error. - - `details` is an optional `String` with information about what caused the error. + - `detail` is an optional `String` with information about what caused the error. - `context` is an optional `String` with relevant text surrounding the error location. - `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error. - `fixInfo` is an optional `Object` with information about how to fix the error (all properties are optional, but @@ -66,15 +67,25 @@ A rule is implemented as an `Object` with one optional and four required propert The collection of helper functions shared by the built-in rules is available for use by custom rules in the [markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers). +### Asynchronous Rules + +If a rule needs to perform asynchronous operations (such as fetching a network resource), it can specify the value `true` for its `asynchronous` property. +Asynchronous rules should return a `Promise` from their `function` implementation that is resolved when the rule completes. +(The value passed to `resolve(...)` is ignored.) +Linting violations from asynchronous rules are reported via the `onError` function just like for synchronous rules. + +**Note**: Asynchronous rules cannot be referenced in a synchronous calling context (i.e., `markdownlint.sync(...)`). +Attempting to do so throws an exception. + ## Examples - [Simple rules used by the project's test cases](../test/rules) - [Code for all `markdownlint` built-in rules](../lib) - [Package configuration for publishing to npm](../test/rules/npm) - Packages should export a single rule object or an `Array` of rule objects -- [Custom rules from the Microsoft/vscode-docs-authoring repository](https://github.com/microsoft/vscode-docs-authoring/tree/master/packages/docs-linting/markdownlint-custom-rules) +- [Custom rules from the Microsoft/vscode-docs-authoring repository](https://github.com/microsoft/vscode-docs-authoring/tree/main/packages/docs-linting/markdownlint-custom-rules) - [Custom rules from the axibase/docs-util repository](https://github.com/axibase/docs-util/tree/master/linting-rules) -- [Custom rules from the webhintio/hint repository](https://github.com/webhintio/hint/blob/master/scripts/lint-markdown.js) +- [Custom rules from the webhintio/hint repository](https://github.com/webhintio/hint/blob/main/scripts/lint-markdown.js) ## References diff --git a/doc/Rules.md b/doc/Rules.md index 7ca6afb8..7c3bd922 100644 --- a/doc/Rules.md +++ b/doc/Rules.md @@ -292,7 +292,11 @@ Tags: bullet, ul, indentation Aliases: ul-indent -Parameters: indent, start_indented (number; default 2, boolean; default false) + + +Parameters: indent, start_indented, start_indent (number; default 2, boolean; default false, number; defaults to indent) + + Fixable: Most violations can be fixed by tooling @@ -319,7 +323,9 @@ rule). The `start_indented` parameter allows the first level of lists to be indented by the configured number of spaces rather than starting at zero (the inverse of -MD006). +MD006). The `start_indent` parameter allows the first level of lists to be indented +by a different number of spaces than the rest (ignored when `start_indented` is not +set). Rationale: Indenting by 2 spaces allows the content of a nested list to be in line with the start of the content of the parent list when a single space is @@ -418,12 +424,14 @@ Some text * Spaces used to indent the list item instead ``` -You have the option to exclude this rule for code blocks. To do so, set the -`code_blocks` parameter to `false`. Code blocks are included by default since -handling of tabs by tools is often inconsistent (ex: using 4 vs. 8 spaces). +You have the option to exclude this rule for code blocks and spans. To do so, +set the `code_blocks` parameter to `false`. Code blocks and spans are included +by default since handling of tabs by Markdown tools can be inconsistent (e.g., +using 4 vs. 8 spaces). -If you would like the fixer to change tabs to x spaces, then configure the `spaces_per_tab` -parameter to the number x. The default value would be 1. +By default, violations of this rule are fixed by replacing the tab with 1 space +character. To use a different number of spaces, set the `spaces_per_tab` +parameter to the desired value. Rationale: Hard tabs are often rendered inconsistently by different editors and can be harder to work with than spaces. @@ -1331,20 +1339,20 @@ For more information, see . ``` Note: To use a bare URL without it being converted into a link, enclose it in -a code block, otherwise in some markdown parsers it _will_ be converted: +a code block, otherwise in some markdown parsers it *will* be converted: ```markdown `https://www.example.com` ``` -Note: The following scenario does _not_ trigger this rule to avoid conflicts +Note: The following scenario does *not* trigger this rule to avoid conflicts with `MD011`/`no-reversed-links`: ```markdown [https://www.example.com] ``` -The use of quotes around a bare link will _not_ trigger this rule, either: +The use of quotes around a bare link will *not* trigger this rule, either: ```markdown "https://www.example.com" @@ -1362,8 +1370,8 @@ Tags: hr Aliases: hr-style -Parameters: style ("consistent", "---", "***", or other string specifying the -horizontal rule; default "consistent") +Parameters: style ("consistent", "---", "***", "___", or other string specifying +the horizontal rule; default "consistent") This rule is triggered when inconsistent styles of horizontal rules are used in the document: @@ -1762,7 +1770,8 @@ 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. +Set the `code_blocks` parameter to `false` to disable this rule for code blocks +and spans. Rationale: Incorrect capitalization of proper names is usually a mistake. @@ -1911,3 +1920,67 @@ The configured list style can be a specific symbol to use (backtick, tilde), or can require that usage be consistent within the document. Rationale: Consistent formatting makes it easier to understand a document. + + + +## MD049 - Emphasis style should be consistent + +Tags: emphasis + +Aliases: emphasis-style + +Parameters: style ("consistent", "asterisk", "underscore"; default "consistent") + +Fixable: Most violations can be fixed by tooling + +This rule is triggered when the symbols used in the document for emphasis do not +match the configured emphasis style: + +```markdown +*Text* +_Text_ +``` + +To fix this issue, use the configured emphasis style throughout the document: + +```markdown +*Text* +*Text* +``` + +The configured emphasis style can be a specific symbol to use ("asterisk", +"underscore"), or can require that usage be consistent within the document. + +Rationale: Consistent formatting makes it easier to understand a document. + + + +## MD050 - Strong style should be consistent + +Tags: emphasis + +Aliases: strong-style + +Parameters: style ("consistent", "asterisk", "underscore"; default "consistent") + +Fixable: Most violations can be fixed by tooling + +This rule is triggered when the symbols used in the document for strong do not +match the configured strong style: + +```markdown +**Text** +__Text__ +``` + +To fix this issue, use the configured strong style throughout the document: + +```markdown +**Text** +**Text** +``` + +The configured strong style can be a specific symbol to use ("asterisk", +"underscore"), or can require that usage be consistent within the document. + +Rationale: Consistent formatting makes it easier to understand a document. diff --git a/example/typescript/type-check.ts b/example/typescript/type-check.ts index 3d0ee90b..05229066 100644 --- a/example/typescript/type-check.ts +++ b/example/typescript/type-check.ts @@ -139,7 +139,7 @@ const testRule = { let ruleOnErrorInfo: markdownlint.RuleOnErrorInfo; ruleOnErrorInfo = { "lineNumber": 1, - "details": "details", + "detail": "detail", "context": "context", "range": [ 1, 2 ], "fixInfo": { diff --git a/helpers/helpers.js b/helpers/helpers.js index d13ae4b2..9db231f8 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -2,8 +2,6 @@ "use strict"; -const os = require("os"); - // Regular expression for matching common newline characters // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js const newLineRe = /\r\n?|\n/g; @@ -63,12 +61,20 @@ module.exports.isObject = function isObject(obj) { }; // Returns true iff the input line is blank (no content) -// Example: Contains nothing, whitespace, or comments -const blankLineRe = />|(?:)/g; +// Example: Contains nothing, whitespace, or comment (unclosed start/end okay) module.exports.isBlankLine = function isBlankLine(line) { // Call to String.replace follows best practices and is not a security check // False-positive for js/incomplete-multi-character-sanitization - return !line || !line.trim() || !line.replace(blankLineRe, "").trim(); + return ( + !line || + !line.trim() || + !line + .replace(//g, "") + .replace(//g, "") + .replace(/>/g, "") + .trim() + ); }; /** @@ -181,6 +187,22 @@ module.exports.fencedCodeBlockStyleFor = } }; +/** + * Return the string representation of a emphasis or strong markup character. + * + * @param {string} markup Emphasis or strong string. + * @returns {string} String representation. + */ +module.exports.emphasisOrStrongStyleFor = + function emphasisOrStrongStyleFor(markup) { + switch (markup[0]) { + case "*": + return "asterisk"; + default: + return "underscore"; + } + }; + /** * Return the number of characters of indent for a token. * @@ -252,6 +274,7 @@ function isMathBlock(token) { !token.type.endsWith("_end") ); } +module.exports.isMathBlock = isMathBlock; // Get line metadata array module.exports.getLineMetadata = function getLineMetadata(params) { @@ -293,14 +316,20 @@ module.exports.getLineMetadata = function getLineMetadata(params) { return lineMetadata; }; -// Calls the provided function for each line (with context) -module.exports.forEachLine = function forEachLine(lineMetadata, handler) { +/** + * Calls the provided function for each line. + * + * @param {Object} lineMetadata Line metadata object. + * @param {Function} handler Function taking (line, lineIndex, inCode, onFence, + * inTable, inItem, inBreak, inMath). + * @returns {void} + */ +function forEachLine(lineMetadata, handler) { lineMetadata.forEach(function forMetadata(metadata) { - // Parameters: - // line, lineIndex, inCode, onFence, inTable, inItem, inBreak, inMath handler(...metadata); }); -}; +} +module.exports.forEachLine = forEachLine; // Returns (nested) lists as a flat array (in order) module.exports.flattenLists = function flattenLists(tokens) { @@ -311,10 +340,6 @@ module.exports.flattenLists = function flattenLists(tokens) { const nestingStack = []; let lastWithMap = { "map": [ 0, 1 ] }; tokens.forEach((token) => { - if (isMathBlock(token) && token.map[1]) { - // markdown-it-texmath plugin does not account for math_block_end - token.map[1]++; - } if ((token.type === "bullet_list_open") || (token.type === "ordered_list_open")) { // Save current context and start a new one @@ -386,7 +411,8 @@ module.exports.forEachHeading = function forEachHeading(params, handler) { * Calls the provided function for each inline code span's content. * * @param {string} input Markdown content. - * @param {Function} handler Callback function. + * @param {Function} handler Callback function taking (code, lineIndex, + * columnIndex, ticks). * @returns {void} */ function forEachInlineCodeSpan(input, handler) { @@ -521,26 +547,39 @@ module.exports.addErrorContext = function addErrorContext( }; /** - * Returns an array of code span ranges. + * Returns an array of code block and span content ranges. * - * @param {string[]} lines Lines to scan for code span ranges. - * @returns {number[][]} Array of ranges (line, index, length). + * @param {Object} params RuleParams instance. + * @param {Object} lineMetadata Line metadata object. + * @returns {number[][]} Array of ranges (lineIndex, columnIndex, length). */ -module.exports.inlineCodeSpanRanges = (lines) => { +module.exports.codeBlockAndSpanRanges = (params, lineMetadata) => { const exclusions = []; - forEachInlineCodeSpan( - lines.join("\n"), - (code, lineIndex, columnIndex) => { - const codeLines = code.split(newLineRe); - // eslint-disable-next-line unicorn/no-for-loop - for (let i = 0; i < codeLines.length; i++) { - exclusions.push( - [ lineIndex + i, columnIndex, codeLines[i].length ] - ); - columnIndex = 0; - } + // Add code block ranges (excludes fences) + forEachLine(lineMetadata, (line, lineIndex, inCode, onFence) => { + if (inCode && !onFence) { + exclusions.push([ lineIndex, 0, line.length ]); } - ); + }); + // Add code span ranges (excludes ticks) + filterTokens(params, "inline", (token) => { + if (token.children.some((child) => child.type === "code_inline")) { + const tokenLines = params.lines.slice(token.map[0], token.map[1]); + forEachInlineCodeSpan( + tokenLines.join("\n"), + (code, lineIndex, columnIndex) => { + const codeLines = code.split(newLineRe); + for (const [ i, line ] of codeLines.entries()) { + exclusions.push([ + token.lineNumber - 1 + lineIndex + i, + i ? 0 : columnIndex, + line.length + ]); + } + } + ); + } + }); return exclusions; }; @@ -596,6 +635,18 @@ module.exports.frontMatterHasTitle = function emphasisMarkersInContent(params) { const { lines } = params; const byLine = new Array(lines.length); + // Search links + lines.forEach((tokenLine, tokenLineIndex) => { + const inLine = []; + let linkMatch = null; + while ((linkMatch = linkRe.exec(tokenLine))) { + let markerMatch = null; + while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) { + inLine.push(linkMatch.index + markerMatch.index); + } + } + byLine[tokenLineIndex] = inLine; + }); // Search code spans filterTokens(params, "inline", (token) => { const { children, lineNumber, map } = token; @@ -606,31 +657,19 @@ function emphasisMarkersInContent(params) { (code, lineIndex, column, tickCount) => { const codeLines = code.split(newLineRe); codeLines.forEach((codeLine, codeLineIndex) => { + const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex; + const inLine = byLine[byLineIndex]; + const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount; let match = null; while ((match = emphasisMarkersRe.exec(codeLine))) { - const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex; - const inLine = byLine[byLineIndex] || []; - const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount; inLine.push(codeLineOffset + match.index); - byLine[byLineIndex] = inLine; } + byLine[byLineIndex] = inLine; }); } ); } }); - // Search links - lines.forEach((tokenLine, tokenLineIndex) => { - let linkMatch = null; - while ((linkMatch = linkRe.exec(tokenLine))) { - let markerMatch = null; - while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) { - const inLine = byLine[tokenLineIndex] || []; - inLine.push(linkMatch.index + markerMatch.index); - byLine[tokenLineIndex] = inLine; - } - } - }); return byLine; } module.exports.emphasisMarkersInContent = emphasisMarkersInContent; @@ -639,9 +678,10 @@ module.exports.emphasisMarkersInContent = emphasisMarkersInContent; * Gets the most common line ending, falling back to the platform default. * * @param {string} input Markdown content to analyze. + * @param {string} [platform] Platform identifier (process.platform). * @returns {string} Preferred line ending. */ -function getPreferredLineEnding(input) { +function getPreferredLineEnding(input, platform) { let cr = 0; let lf = 0; let crlf = 0; @@ -662,7 +702,8 @@ function getPreferredLineEnding(input) { }); let preferredLineEnding = null; if (!cr && !lf && !crlf) { - preferredLineEnding = os.EOL; + preferredLineEnding = + ((platform || process.platform) === "win32") ? "\r\n" : "\n"; } else if ((lf >= crlf) && (lf >= cr)) { preferredLineEnding = "\n"; } else if (crlf >= cr) { @@ -777,3 +818,80 @@ module.exports.applyFixes = function applyFixes(input, errors) { // Return corrected input return lines.filter((line) => line !== null).join(lineEnding); }; + +/** + * Gets the range and fixInfo values for reporting an error if the expected + * text is found on the specified line. + * + * @param {string[]} lines Lines of Markdown content. + * @param {number} lineIndex Line index to check. + * @param {string} search Text to search for. + * @param {string} replace Text to replace with. + * @returns {Object} Range and fixInfo wrapper. + */ +function getRangeAndFixInfoIfFound(lines, lineIndex, search, replace) { + let range = null; + let fixInfo = null; + const searchIndex = lines[lineIndex].indexOf(search); + if (searchIndex !== -1) { + const column = searchIndex + 1; + const length = search.length; + range = [ column, length ]; + fixInfo = { + "editColumn": column, + "deleteCount": length, + "insertText": replace + }; + } + return { + range, + fixInfo + }; +} +module.exports.getRangeAndFixInfoIfFound = getRangeAndFixInfoIfFound; + +/** + * Gets the next (subsequent) child token if it is of the expected type. + * + * @param {Object} parentToken Parent token. + * @param {Object} childToken Child token basis. + * @param {string} nextType Token type of next token. + * @param {string} nextNextType Token type of next-next token. + * @returns {Object} Next token. + */ +function getNextChildToken(parentToken, childToken, nextType, nextNextType) { + const { children } = parentToken; + const index = children.indexOf(childToken); + if ( + (index !== -1) && + (children.length > index + 2) && + (children[index + 1].type === nextType) && + (children[index + 2].type === nextNextType) + ) { + return children[index + 1]; + } + return null; +} +module.exports.getNextChildToken = getNextChildToken; + +/** + * Calls Object.freeze() on an object and its children. + * + * @param {Object} obj Object to deep freeze. + * @returns {Object} Object passed to the function. + */ +function deepFreeze(obj) { + const pending = [ obj ]; + let current = null; + while ((current = pending.shift())) { + Object.freeze(current); + for (const name of Object.getOwnPropertyNames(current)) { + const value = current[name]; + if (value && (typeof value === "object")) { + pending.push(value); + } + } + } + return obj; +} +module.exports.deepFreeze = deepFreeze; diff --git a/helpers/package.json b/helpers/package.json index 5b25454e..22e67a6a 100644 --- a/helpers/package.json +++ b/helpers/package.json @@ -1,6 +1,6 @@ { "name": "markdownlint-rule-helpers", - "version": "0.15.0", + "version": "0.16.0", "description": "A collection of markdownlint helper functions for custom rules", "main": "helpers.js", "author": "David Anson (https://dlaa.me/)", diff --git a/lib/cache.js b/lib/cache.js index a2ad39af..4d7fa662 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -2,6 +2,14 @@ "use strict"; +let codeBlockAndSpanRanges = null; +module.exports.codeBlockAndSpanRanges = (value) => { + if (value) { + codeBlockAndSpanRanges = value; + } + return codeBlockAndSpanRanges; +}; + let flattenedLists = null; module.exports.flattenedLists = (value) => { if (value) { @@ -10,14 +18,6 @@ module.exports.flattenedLists = (value) => { return flattenedLists; }; -let inlineCodeSpanRanges = null; -module.exports.inlineCodeSpanRanges = (value) => { - if (value) { - inlineCodeSpanRanges = value; - } - return inlineCodeSpanRanges; -}; - let lineMetadata = null; module.exports.lineMetadata = (value) => { if (value) { @@ -27,7 +27,7 @@ module.exports.lineMetadata = (value) => { }; module.exports.clear = () => { + codeBlockAndSpanRanges = null; flattenedLists = null; - inlineCodeSpanRanges = null; lineMetadata = null; }; diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 00000000..4b24b2a2 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,7 @@ +// @ts-check + +"use strict"; + +module.exports.deprecatedRuleNames = [ "MD002", "MD006" ]; +module.exports.homepage = "https://github.com/DavidAnson/markdownlint"; +module.exports.version = "0.25.0"; diff --git a/lib/markdownlint.d.ts b/lib/markdownlint.d.ts index f59ef569..d636a4d4 100644 --- a/lib/markdownlint.d.ts +++ b/lib/markdownlint.d.ts @@ -206,9 +206,9 @@ type RuleOnErrorInfo = { */ lineNumber: number; /** - * Details about the error. + * Detail about the error. */ - details?: string; + detail?: string; /** * Context for the error. */ @@ -263,6 +263,10 @@ type Rule = { * Rule tag(s). */ tags: string[]; + /** + * True if asynchronous. + */ + asynchronous?: boolean; /** * Rule implementation. */ diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 7f181142..eba87c0e 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -5,6 +5,7 @@ const path = require("path"); const { promisify } = require("util"); const markdownIt = require("markdown-it"); +const { deprecatedRuleNames } = require("./constants"); const rules = require("./rules"); const helpers = require("../helpers"); const cache = require("./cache"); @@ -14,15 +15,14 @@ const cache = require("./cache"); const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? require : /* c8 ignore next */ __non_webpack_require__; // Capture native require implementation for dynamic loading of modules -const deprecatedRuleNames = [ "MD002", "MD006" ]; - /** * Validate the list of rules for structure and reuse. * * @param {Rule[]} ruleList List of rules. + * @param {boolean} synchronous Whether to execute synchronously. * @returns {string} Error message if validation fails. */ -function validateRuleList(ruleList) { +function validateRuleList(ruleList, synchronous) { let result = null; if (ruleList.length === rules.length) { // No need to validate if only using built-in rules @@ -62,6 +62,19 @@ function validateRuleList(ruleList) { ) { result = newError("information"); } + if ( + !result && + (rule.asynchronous !== undefined) && + (typeof rule.asynchronous !== "boolean") + ) { + result = newError("asynchronous"); + } + if (!result && rule.asynchronous && synchronous) { + result = new Error( + "Custom rule " + rule.names.join("/") + " at index " + customIndex + + " is asynchronous and can not be used in a synchronous context." + ); + } if (!result) { rule.names.forEach(function forName(name) { const nameUpper = name.toUpperCase(); @@ -182,27 +195,21 @@ function removeFrontMatter(content, frontMatter) { * @returns {void} */ function annotateTokens(tokens, lines) { - let tableMap = null; + let trMap = null; tokens.forEach(function forToken(token) { - // Handle missing maps for table head/body - if ( - (token.type === "thead_open") || - (token.type === "tbody_open") - ) { - tableMap = [ ...token.map ]; - } else if ( - (token.type === "tr_close") && - tableMap - ) { - tableMap[0]++; - } else if ( - (token.type === "thead_close") || - (token.type === "tbody_close") - ) { - tableMap = null; + // Provide missing maps for table content + if (token.type === "tr_open") { + trMap = token.map; + } else if (token.type === "tr_close") { + trMap = null; } - if (tableMap && !token.map) { - token.map = [ ...tableMap ]; + if (!token.map && trMap) { + token.map = [ ...trMap ]; + } + // Adjust maps for math blocks + if (helpers.isMathBlock(token) && token.map[1]) { + // markdown-it-texmath plugin does not account for math_block_end + token.map[1]++; } // Update token metadata if (token.map) { @@ -335,8 +342,7 @@ function getEnabledRulesPerLineNumber( 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") ]; + function handleInlineConfig(input, forEachMatch, forEachLine) { input.forEach((line, lineIndex) => { if (!noInlineConfig) { let match = null; @@ -367,6 +373,7 @@ function getEnabledRulesPerLineNumber( } // eslint-disable-next-line jsdoc/require-jsdoc function applyEnableDisable(action, parameter, state) { + state = { ...state }; const enabled = (action.startsWith("ENABLE")); const items = parameter ? parameter.trim().toUpperCase().split(/\s+/) : @@ -376,40 +383,42 @@ function getEnabledRulesPerLineNumber( state[ruleName] = enabled; }); }); + return state; } // eslint-disable-next-line jsdoc/require-jsdoc function enableDisableFile(action, parameter) { if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) { - applyEnableDisable(action, parameter, enabledRules); + enabledRules = applyEnableDisable(action, parameter, enabledRules); } } // eslint-disable-next-line jsdoc/require-jsdoc function captureRestoreEnableDisable(action, parameter) { if (action === "CAPTURE") { - capturedRules = { ...enabledRules }; + capturedRules = enabledRules; } else if (action === "RESTORE") { - enabledRules = { ...capturedRules }; + enabledRules = capturedRules; } else if ((action === "ENABLE") || (action === "DISABLE")) { - enabledRules = { ...enabledRules }; - applyEnableDisable(action, parameter, enabledRules); + enabledRules = applyEnableDisable(action, parameter, enabledRules); } } // eslint-disable-next-line jsdoc/require-jsdoc function updateLineState() { - enabledRulesPerLineNumber.push({ ...enabledRules }); + enabledRulesPerLineNumber.push(enabledRules); } // eslint-disable-next-line jsdoc/require-jsdoc function disableNextLine(action, parameter, lineNumber) { if (action === "DISABLE-NEXT-LINE") { - applyEnableDisable( - action, - parameter, - enabledRulesPerLineNumber[lineNumber + 1] || {} - ); + const nextLineNumber = frontMatterLines.length + lineNumber + 1; + enabledRulesPerLineNumber[nextLineNumber] = + applyEnableDisable( + action, + parameter, + enabledRulesPerLineNumber[nextLineNumber] || {} + ); } } // Handle inline comments - handleInlineConfig(false, configureFile); + handleInlineConfig([ lines.join("\n") ], configureFile); const effectiveConfig = getEffectiveConfig( ruleList, config, aliasToRuleNames); ruleList.forEach((rule) => { @@ -418,9 +427,9 @@ function getEnabledRulesPerLineNumber( enabledRules[ruleName] = !!effectiveConfig[ruleName]; }); capturedRules = enabledRules; - handleInlineConfig(true, enableDisableFile); - handleInlineConfig(true, captureRestoreEnableDisable, updateLineState); - handleInlineConfig(true, disableNextLine); + handleInlineConfig(lines, enableDisableFile); + handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState); + handleInlineConfig(lines, disableNextLine); // Return results return { effectiveConfig, @@ -428,38 +437,6 @@ function getEnabledRulesPerLineNumber( }; } -/** - * Compare function for Array.prototype.sort for ascending order of errors. - * - * @param {LintError} a First error. - * @param {LintError} b Second error. - * @returns {number} Positive value if a>b, negative value if b array[index - 1].lineNumber); -} - /** * Lints a string containing Markdown content. * @@ -509,28 +486,28 @@ function lintContent( ); // Create parameters for rules const params = { - name, - tokens, - lines, - frontMatterLines + "name": helpers.deepFreeze(name), + "tokens": helpers.deepFreeze(tokens), + "lines": helpers.deepFreeze(lines), + "frontMatterLines": helpers.deepFreeze(frontMatterLines) }; cache.lineMetadata(helpers.getLineMetadata(params)); cache.flattenedLists(helpers.flattenLists(params.tokens)); - cache.inlineCodeSpanRanges(helpers.inlineCodeSpanRanges(params.lines)); + cache.codeBlockAndSpanRanges( + helpers.codeBlockAndSpanRanges(params, cache.lineMetadata()) + ); // Function to run for each rule - const result = (resultVersion === 0) ? {} : []; + let results = []; // eslint-disable-next-line jsdoc/require-jsdoc function forRule(rule) { // Configure rule - const ruleNameFriendly = rule.names[0]; - const ruleName = ruleNameFriendly.toUpperCase(); + const ruleName = rule.names[0].toUpperCase(); params.config = effectiveConfig[ruleName]; // eslint-disable-next-line jsdoc/require-jsdoc function throwError(property) { throw new Error( "Property '" + property + "' of onError parameter is incorrect."); } - const errors = []; // eslint-disable-next-line jsdoc/require-jsdoc function onError(errorInfo) { if (!errorInfo || @@ -539,6 +516,10 @@ function lintContent( (errorInfo.lineNumber > lines.length)) { throwError("lineNumber"); } + const lineNumber = errorInfo.lineNumber + frontMatterLines.length; + if (!enabledRulesPerLineNumber[lineNumber][ruleName]) { + return; + } if (errorInfo.detail && !helpers.isString(errorInfo.detail)) { throwError("detail"); @@ -549,12 +530,12 @@ function lintContent( } if (errorInfo.range && (!Array.isArray(errorInfo.range) || - (errorInfo.range.length !== 2) || - !helpers.isNumber(errorInfo.range[0]) || - (errorInfo.range[0] < 1) || - !helpers.isNumber(errorInfo.range[1]) || - (errorInfo.range[1] < 1) || - ((errorInfo.range[0] + errorInfo.range[1] - 1) > + (errorInfo.range.length !== 2) || + !helpers.isNumber(errorInfo.range[0]) || + (errorInfo.range[0] < 1) || + !helpers.isNumber(errorInfo.range[1]) || + (errorInfo.range[1] < 1) || + ((errorInfo.range[0] + errorInfo.range[1] - 1) > lines[errorInfo.lineNumber - 1].length))) { throwError("range"); } @@ -599,78 +580,114 @@ function lintContent( cleanFixInfo.insertText = fixInfo.insertText; } } - errors.push({ - "lineNumber": errorInfo.lineNumber + frontMatterLines.length, - "detail": errorInfo.detail || null, - "context": errorInfo.context || null, - "range": errorInfo.range ? [ ...errorInfo.range ] : null, + results.push({ + lineNumber, + "ruleName": rule.names[0], + "ruleNames": rule.names, + "ruleDescription": rule.description, + "ruleInformation": rule.information ? rule.information.href : null, + "errorDetail": errorInfo.detail || null, + "errorContext": errorInfo.context || null, + "errorRange": errorInfo.range ? [ ...errorInfo.range ] : null, "fixInfo": fixInfo ? cleanFixInfo : null }); } - // Call (possibly external) rule function - if (handleRuleFailures) { - try { - rule.function(params, onError); - } catch (error) { - onError({ - "lineNumber": 1, - "detail": `This rule threw an exception: ${error.message}` - }); + // Call (possibly external) rule function to report errors + const catchCallsOnError = (error) => onError({ + "lineNumber": 1, + "detail": `This rule threw an exception: ${error.message || error}` + }); + const invokeRuleFunction = () => rule.function(params, onError); + if (rule.asynchronous) { + // Asynchronous rule, ensure it returns a Promise + const ruleFunctionPromise = + Promise.resolve().then(invokeRuleFunction); + return handleRuleFailures ? + ruleFunctionPromise.catch(catchCallsOnError) : + ruleFunctionPromise; + } + // Synchronous rule + try { + invokeRuleFunction(); + } catch (error) { + if (handleRuleFailures) { + catchCallsOnError(error); + } else { + throw error; + } + } + return null; + } + // eslint-disable-next-line jsdoc/require-jsdoc + function formatResults() { + // Sort results by rule name by line number + results.sort((a, b) => ( + a.ruleName.localeCompare(b.ruleName) || + a.lineNumber - b.lineNumber + )); + if (resultVersion < 3) { + // Remove fixInfo and multiple errors for the same rule and line number + const noPrevious = { + "ruleName": null, + "lineNumber": -1 + }; + results = results.filter((error, index, array) => { + delete error.fixInfo; + const previous = array[index - 1] || noPrevious; + return ( + (error.ruleName !== previous.ruleName) || + (error.lineNumber !== previous.lineNumber) + ); + }); + } + if (resultVersion === 0) { + // Return a dictionary of rule->[line numbers] + const dictionary = {}; + for (const error of results) { + const ruleLines = dictionary[error.ruleName] || []; + ruleLines.push(error.lineNumber); + dictionary[error.ruleName] = ruleLines; + } + // @ts-ignore + results = dictionary; + } else if (resultVersion === 1) { + // Use ruleAlias instead of ruleNames + for (const error of results) { + error.ruleAlias = error.ruleNames[1] || error.ruleName; + delete error.ruleNames; } } else { - rule.function(params, onError); - } - // Record any errors (significant performance benefit from length check) - if (errors.length > 0) { - errors.sort(lineNumberComparison); - const filteredErrors = errors - .filter((resultVersion === 3) ? - filterAllValues : - uniqueFilterForSortedErrors) - .filter(function removeDisabledRules(error) { - return enabledRulesPerLineNumber[error.lineNumber][ruleName]; - }) - .map(function formatResults(error) { - if (resultVersion === 0) { - return error.lineNumber; - } - const errorObject = {}; - errorObject.lineNumber = error.lineNumber; - if (resultVersion === 1) { - errorObject.ruleName = ruleNameFriendly; - errorObject.ruleAlias = rule.names[1] || rule.names[0]; - } else { - errorObject.ruleNames = rule.names; - } - errorObject.ruleDescription = rule.description; - errorObject.ruleInformation = - rule.information ? rule.information.href : null; - errorObject.errorDetail = error.detail; - errorObject.errorContext = error.context; - errorObject.errorRange = error.range; - if (resultVersion === 3) { - errorObject.fixInfo = error.fixInfo; - } - return errorObject; - }); - if (filteredErrors.length > 0) { - if (resultVersion === 0) { - result[ruleNameFriendly] = filteredErrors; - } else { - Array.prototype.push.apply(result, filteredErrors); - } + // resultVersion 2 or 3: Remove unwanted ruleName + for (const error of results) { + delete error.ruleName; } } + return results; } // Run all rules + const ruleListAsync = ruleList.filter((rule) => rule.asynchronous); + const ruleListSync = ruleList.filter((rule) => !rule.asynchronous); + const ruleListAsyncFirst = [ + ...ruleListAsync, + ...ruleListSync + ]; + const callbackSuccess = () => callback(null, formatResults()); + const callbackError = + (error) => callback(error instanceof Error ? error : new Error(error)); try { - ruleList.forEach(forRule); + const ruleResults = ruleListAsyncFirst.map(forRule); + if (ruleListAsync.length > 0) { + Promise.all(ruleResults.slice(0, ruleListAsync.length)) + .then(callbackSuccess) + .catch(callbackError); + } else { + callbackSuccess(); + } } catch (error) { + callbackError(error); + } finally { cache.clear(); - return callback(error); } - cache.clear(); - return callback(null, result); } /** @@ -731,7 +748,7 @@ function lintInput(options, synchronous, callback) { callback = callback || function noop() {}; // eslint-disable-next-line unicorn/prefer-spread const ruleList = rules.concat(options.customRules || []); - const ruleErr = validateRuleList(ruleList); + const ruleErr = validateRuleList(ruleList, synchronous); if (ruleErr) { return callback(ruleErr); } @@ -759,62 +776,32 @@ function lintInput(options, synchronous, callback) { const fs = options.fs || require("fs"); const results = newResults(ruleList); let done = false; - // Linting of strings is always synchronous - let syncItem = null; - // eslint-disable-next-line jsdoc/require-jsdoc - function syncCallback(err, result) { - if (err) { - done = true; - return callback(err); - } - results[syncItem] = result; - return null; - } - while (!done && (syncItem = stringsKeys.shift())) { - lintContent( - ruleList, - syncItem, - strings[syncItem] || "", - md, - config, - frontMatter, - handleRuleFailures, - noInlineConfig, - resultVersion, - syncCallback - ); - } - if (synchronous) { - // Lint files synchronously - while (!done && (syncItem = files.shift())) { - lintFile( - ruleList, - syncItem, - md, - config, - frontMatter, - handleRuleFailures, - noInlineConfig, - resultVersion, - fs, - synchronous, - syncCallback - ); - } - return done || callback(null, results); - } - // Lint files asynchronously let concurrency = 0; // eslint-disable-next-line jsdoc/require-jsdoc - function lintConcurrently() { - const asyncItem = files.shift(); + function lintWorker() { + let currentItem = null; + // eslint-disable-next-line jsdoc/require-jsdoc + function lintWorkerCallback(err, result) { + concurrency--; + if (err) { + done = true; + return callback(err); + } + results[currentItem] = result; + if (!synchronous) { + lintWorker(); + } + return null; + } if (done) { - // Nothing to do - } else if (asyncItem) { + // Abort for error or nothing left to do + } else if (files.length > 0) { + // Lint next file concurrency++; + currentItem = files.shift(); lintFile( ruleList, - asyncItem, + currentItem, md, config, frontMatter, @@ -823,34 +810,48 @@ function lintInput(options, synchronous, callback) { resultVersion, fs, synchronous, - (err, result) => { - concurrency--; - if (err) { - done = true; - return callback(err); - } - results[asyncItem] = result; - lintConcurrently(); - return null; - } + lintWorkerCallback + ); + } else if (stringsKeys.length > 0) { + // Lint next string + concurrency++; + currentItem = stringsKeys.shift(); + lintContent( + ruleList, + currentItem, + strings[currentItem] || "", + md, + config, + frontMatter, + handleRuleFailures, + noInlineConfig, + resultVersion, + lintWorkerCallback ); } else if (concurrency === 0) { + // Finish done = true; return callback(null, results); } return null; } - // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access - // delay suggests that a concurrency factor of 8 can eliminate the impact - // of that delay (i.e., total time is the same as with no delay). - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); - lintConcurrently(); + if (synchronous) { + while (!done) { + lintWorker(); + } + } else { + // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access + // delay suggests that a concurrency factor of 8 can eliminate the impact + // of that delay (i.e., total time is the same as with no delay). + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + lintWorker(); + } return null; } @@ -906,12 +907,13 @@ function parseConfiguration(name, content, parsers) { let config = null; let message = ""; const errors = []; + let index = 0; // Try each parser (parsers || [ JSON.parse ]).every((parser) => { try { config = parser(content); } catch (error) { - errors.push(error.message); + errors.push(`Parser ${index++}: ${error.message}`); } return !config; }); @@ -1102,7 +1104,7 @@ function readConfigSync(file, parsers, fs) { * @returns {string} SemVer string. */ function getVersion() { - return require("../package.json").version; + return require("./constants").version; } // Export a/synchronous/Promise APIs @@ -1172,7 +1174,7 @@ module.exports = markdownlint; * * @typedef {Object} RuleOnErrorInfo * @property {number} lineNumber Line number (1-based). - * @property {string} [details] Details about the error. + * @property {string} [detail] Detail about the error. * @property {string} [context] Context for the error. * @property {number[]} [range] Column number (1-based) and length. * @property {RuleOnErrorFixInfo} [fixInfo] Fix information. @@ -1196,6 +1198,7 @@ module.exports = markdownlint; * @property {string} description Rule description. * @property {URL} [information] Link to more information. * @property {string[]} tags Rule tag(s). + * @property {boolean} [asynchronous] True if asynchronous. * @property {RuleFunction} function Rule implementation. */ diff --git a/lib/md007.js b/lib/md007.js index 0a02b618..1bb6f342 100644 --- a/lib/md007.js +++ b/lib/md007.js @@ -13,12 +13,14 @@ module.exports = { "function": function MD007(params, onError) { const indent = Number(params.config.indent || 2); const startIndented = !!params.config.start_indented; + const startIndent = Number(params.config.start_indent || indent); flattenedLists().forEach((list) => { if (list.unordered && list.parentsUnordered) { list.items.forEach((item) => { const { lineNumber, line } = item; - const expectedNesting = list.nesting + (startIndented ? 1 : 0); - const expectedIndent = expectedNesting * indent; + const expectedIndent = + (startIndented ? startIndent : 0) + + (list.nesting * indent); const actualIndent = indentFor(item); let range = null; let editColumn = 1; diff --git a/lib/md010.js b/lib/md010.js index de20f6cc..2a359313 100644 --- a/lib/md010.js +++ b/lib/md010.js @@ -2,8 +2,8 @@ "use strict"; -const { addError, forEachLine } = require("../helpers"); -const { lineMetadata } = require("./cache"); +const { addError, forEachLine, overlapsAnyRange } = require("../helpers"); +const { codeBlockAndSpanRanges, lineMetadata } = require("./cache"); const tabRe = /\t+/g; @@ -13,28 +13,33 @@ module.exports = { "tags": [ "whitespace", "hard_tab" ], "function": function MD010(params, onError) { const codeBlocks = params.config.code_blocks; - const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; + const includeCode = (codeBlocks === undefined) ? true : !!codeBlocks; const spacesPerTab = params.config.spaces_per_tab; const spaceMultiplier = (spacesPerTab === undefined) ? 1 : Math.max(0, Number(spacesPerTab)); + const exclusions = includeCode ? [] : codeBlockAndSpanRanges(); forEachLine(lineMetadata(), (line, lineIndex, inCode) => { - if (!inCode || includeCodeBlocks) { + if (includeCode || !inCode) { let match = null; while ((match = tabRe.exec(line)) !== null) { - const column = match.index + 1; + const { index } = match; + const column = index + 1; const length = match[0].length; - addError( - onError, - lineIndex + 1, - "Column: " + column, - null, - [ column, length ], - { - "editColumn": column, - "deleteCount": length, - "insertText": "".padEnd(length * spaceMultiplier) - }); + if (!overlapsAnyRange(exclusions, lineIndex, index, length)) { + addError( + onError, + lineIndex + 1, + "Column: " + column, + null, + [ column, length ], + { + "editColumn": column, + "deleteCount": length, + "insertText": "".padEnd(length * spaceMultiplier) + } + ); + } } } }); diff --git a/lib/md011.js b/lib/md011.js index 0c906dc0..11672e5d 100644 --- a/lib/md011.js +++ b/lib/md011.js @@ -3,7 +3,7 @@ "use strict"; const { addError, forEachLine, overlapsAnyRange } = require("../helpers"); -const { inlineCodeSpanRanges, lineMetadata } = require("./cache"); +const { codeBlockAndSpanRanges, lineMetadata } = require("./cache"); const reversedLinkRe = /(^|[^\\])\(([^)]+)\)\[([^\]^][^\]]*)](?!\()/g; @@ -13,7 +13,7 @@ module.exports = { "description": "Reversed link syntax", "tags": [ "links" ], "function": function MD011(params, onError) { - const exclusions = inlineCodeSpanRanges(); + const exclusions = codeBlockAndSpanRanges(); forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => { if (!inCode && !onFence) { let match = null; diff --git a/lib/md033.js b/lib/md033.js index 9c002741..c4a6fe98 100644 --- a/lib/md033.js +++ b/lib/md033.js @@ -2,12 +2,13 @@ "use strict"; -const { addError, forEachLine, unescapeMarkdown } = require("../helpers"); -const { lineMetadata } = require("./cache"); +const { + addError, forEachLine, overlapsAnyRange, unescapeMarkdown +} = require("../helpers"); +const { codeBlockAndSpanRanges, lineMetadata } = require("./cache"); const htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g; const linkDestinationRe = /]\(\s*$/; -const inlineCodeRe = /^[^`]*(`+[^`]+`+[^`]+)*`+[^`]*$/; // See https://spec.commonmark.org/0.29/#autolinks const emailAddressRe = // eslint-disable-next-line max-len @@ -21,19 +22,22 @@ module.exports = { let allowedElements = params.config.allowed_elements; allowedElements = Array.isArray(allowedElements) ? allowedElements : []; allowedElements = allowedElements.map((element) => element.toLowerCase()); + const exclusions = codeBlockAndSpanRanges(); forEachLine(lineMetadata(), (line, lineIndex, inCode) => { let match = null; // eslint-disable-next-line no-unmodified-loop-condition while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) { const [ tag, content, element ] = match; - if (!allowedElements.includes(element.toLowerCase()) && + if ( + !allowedElements.includes(element.toLowerCase()) && !tag.endsWith("\\>") && - !emailAddressRe.test(content)) { + !emailAddressRe.test(content) && + !overlapsAnyRange(exclusions, lineIndex, match.index, match[0].length) + ) { const prefix = line.substring(0, match.index); - if (!linkDestinationRe.test(prefix) && !inlineCodeRe.test(prefix)) { + if (!linkDestinationRe.test(prefix)) { const unescaped = unescapeMarkdown(prefix + "<", "_"); - if (!unescaped.endsWith("_") && - ((unescaped + "`").match(/`/g).length % 2)) { + if (!unescaped.endsWith("_")) { addError(onError, lineIndex + 1, "Element: " + element, null, [ match.index + 1, tag.length ]); } diff --git a/lib/md035.js b/lib/md035.js index a2149474..80425867 100644 --- a/lib/md035.js +++ b/lib/md035.js @@ -10,12 +10,12 @@ module.exports = { "tags": [ "hr" ], "function": function MD035(params, onError) { let style = String(params.config.style || "consistent"); - filterTokens(params, "hr", function forToken(token) { - const lineTrim = token.line.trim(); + filterTokens(params, "hr", (token) => { + const { lineNumber, markup } = token; if (style === "consistent") { - style = lineTrim; + style = markup; } - addErrorDetailIf(onError, token.lineNumber, style, lineTrim); + addErrorDetailIf(onError, lineNumber, style, markup); }); } }; diff --git a/lib/md037.js b/lib/md037.js index 14c5c032..2466038f 100644 --- a/lib/md037.js +++ b/lib/md037.js @@ -7,6 +7,7 @@ const { addErrorContext, emphasisMarkersInContent, forEachLine, isBlankLine } = const { lineMetadata } = require("./cache"); const emphasisRe = /(^|[^\\]|\\\\)(?:(\*\*?\*?)|(__?_?))/g; +const embeddedUnderscoreRe = /([A-Za-z0-9])_([A-Za-z0-9])/g; const asteriskListItemMarkerRe = /^([\s>]*)\*(\s+)/; const leftSpaceRe = /^\s+/; const rightSpaceRe = /\s+$/; @@ -98,14 +99,15 @@ module.exports = { // Emphasis has no meaning here return; } + let patchedLine = line.replace(embeddedUnderscoreRe, "$1 $2"); if (onItemStart) { // Trim overlapping '*' list item marker - line = line.replace(asteriskListItemMarkerRe, "$1 $2"); + patchedLine = patchedLine.replace(asteriskListItemMarkerRe, "$1 $2"); } let match = null; // Match all emphasis-looking runs in the line... - while ((match = emphasisRe.exec(line))) { - const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex] || []; + while ((match = emphasisRe.exec(patchedLine))) { + const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex]; const matchIndex = match.index + match[1].length; if (ignoreMarkersForLine.includes(matchIndex)) { // Ignore emphasis markers inside code spans and links diff --git a/lib/md039.js b/lib/md039.js index c65a9cbe..8397cfc5 100644 --- a/lib/md039.js +++ b/lib/md039.js @@ -4,7 +4,8 @@ const { addErrorContext, filterTokens } = require("../helpers"); -const spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=\(\S*\))/; +const spaceInLinkRe = + /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=((?:\([^)]*\))|(?:\[[^\]]*\])))/; module.exports = { "names": [ "MD039", "no-space-in-links" ], diff --git a/lib/md043.js b/lib/md043.js index 5805305b..37b3cbd2 100644 --- a/lib/md043.js +++ b/lib/md043.js @@ -20,7 +20,6 @@ module.exports = { let matchAny = false; let hasError = false; let anyHeadings = false; - // eslint-disable-next-line func-style const getExpected = () => requiredHeadings[i++] || "[None]"; forEachHeading(params, (heading, content) => { if (!hasError) { diff --git a/lib/md044.js b/lib/md044.js index 2d8da0e4..d8bc16fd 100644 --- a/lib/md044.js +++ b/lib/md044.js @@ -4,7 +4,7 @@ const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine, overlapsAnyRange, linkRe, linkReferenceRe } = require("../helpers"); -const { inlineCodeSpanRanges, lineMetadata } = require("./cache"); +const { codeBlockAndSpanRanges, lineMetadata } = require("./cache"); module.exports = { "names": [ "MD044", "proper-names" ], @@ -36,7 +36,7 @@ module.exports = { } }); if (!includeCodeBlocks) { - exclusions.push(...inlineCodeSpanRanges()); + exclusions.push(...codeBlockAndSpanRanges()); } for (const name of names) { const escapedName = escapeForRegExp(name); diff --git a/lib/md049.js b/lib/md049.js new file mode 100644 index 00000000..4d36df55 --- /dev/null +++ b/lib/md049.js @@ -0,0 +1,45 @@ +// @ts-check + +"use strict"; + +const { addError, emphasisOrStrongStyleFor, forEachInlineChild, + getNextChildToken, getRangeAndFixInfoIfFound } = require("../helpers"); + +module.exports = { + "names": [ "MD049", "emphasis-style" ], + "description": "Emphasis style should be consistent", + "tags": [ "emphasis" ], + "function": function MD049(params, onError) { + let expectedStyle = String(params.config.style || "consistent"); + forEachInlineChild(params, "em_open", (token, parent) => { + const { lineNumber, markup } = token; + const markupStyle = emphasisOrStrongStyleFor(markup); + if (expectedStyle === "consistent") { + expectedStyle = markupStyle; + } + if (expectedStyle !== markupStyle) { + let rangeAndFixInfo = {}; + const contentToken = getNextChildToken( + parent, token, "text", "em_close" + ); + if (contentToken) { + const { content } = contentToken; + const actual = `${markup}${content}${markup}`; + const expectedMarkup = (expectedStyle === "asterisk") ? "*" : "_"; + const expected = `${expectedMarkup}${content}${expectedMarkup}`; + rangeAndFixInfo = getRangeAndFixInfoIfFound( + params.lines, lineNumber - 1, actual, expected + ); + } + addError( + onError, + lineNumber, + `Expected: ${expectedStyle}; Actual: ${markupStyle}`, + null, + rangeAndFixInfo.range, + rangeAndFixInfo.fixInfo + ); + } + }); + } +}; diff --git a/lib/md050.js b/lib/md050.js new file mode 100644 index 00000000..2ca9260e --- /dev/null +++ b/lib/md050.js @@ -0,0 +1,45 @@ +// @ts-check + +"use strict"; + +const { addError, emphasisOrStrongStyleFor, forEachInlineChild, + getNextChildToken, getRangeAndFixInfoIfFound } = require("../helpers"); + +module.exports = { + "names": [ "MD050", "strong-style" ], + "description": "Strong style should be consistent", + "tags": [ "emphasis" ], + "function": function MD050(params, onError) { + let expectedStyle = String(params.config.style || "consistent"); + forEachInlineChild(params, "strong_open", (token, parent) => { + const { lineNumber, markup } = token; + const markupStyle = emphasisOrStrongStyleFor(markup); + if (expectedStyle === "consistent") { + expectedStyle = markupStyle; + } + if (expectedStyle !== markupStyle) { + let rangeAndFixInfo = {}; + const contentToken = getNextChildToken( + parent, token, "text", "strong_close" + ); + if (contentToken) { + const { content } = contentToken; + const actual = `${markup}${content}${markup}`; + const expectedMarkup = (expectedStyle === "asterisk") ? "**" : "__"; + const expected = `${expectedMarkup}${content}${expectedMarkup}`; + rangeAndFixInfo = getRangeAndFixInfoIfFound( + params.lines, lineNumber - 1, actual, expected + ); + } + addError( + onError, + lineNumber, + `Expected: ${expectedStyle}; Actual: ${markupStyle}`, + null, + rangeAndFixInfo.range, + rangeAndFixInfo.fixInfo + ); + } + }); + } +}; diff --git a/lib/rules.js b/lib/rules.js index 2b957061..28c21214 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -2,9 +2,7 @@ "use strict"; -const packageJson = require("../package.json"); -const homepage = packageJson.homepage; -const version = packageJson.version; +const { homepage, version } = require("./constants"); const rules = [ require("./md001"), @@ -50,7 +48,9 @@ const rules = [ require("./md045"), require("./md046"), require("./md047"), - require("./md048") + require("./md048"), + require("./md049"), + require("./md050") ]; rules.forEach((rule) => { const name = rule.names[0].toLowerCase(); diff --git a/package.json b/package.json index 3d80e5a0..a88e4545 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "markdownlint", - "version": "0.24.0", + "version": "0.25.0", "description": "A Node.js style checker and lint tool for Markdown/CommonMark files.", "main": "lib/markdownlint.js", "types": "lib/markdownlint.d.ts", @@ -19,7 +19,7 @@ "build-declaration": "tsc --allowJs --declaration --emitDeclarationOnly --resolveJsonModule lib/markdownlint.js && node scripts delete 'lib/{c,md,r}*.d.ts' 'helpers/*.d.ts'", "build-demo": "node scripts copy node_modules/markdown-it/dist/markdown-it.min.js demo/markdown-it.min.js && cd demo && webpack --no-stats", "build-example": "npm install --no-save --ignore-scripts grunt grunt-cli gulp through2", - "ci": "npm-run-all --continue-on-error --parallel declaration lint --parallel build-config build-demo test-cover && git diff --exit-code", + "ci": "npm-run-all --continue-on-error --parallel build-config lint serial-declaration-demo test-cover && git diff --exit-code", "clone-test-repos-dotnet-docs": "cd test-repos && git clone https://github.com/dotnet/docs dotnet-docs --depth 1 --no-tags --quiet", "clone-test-repos-eslint-eslint": "cd test-repos && git clone https://github.com/eslint/eslint eslint-eslint --depth 1 --no-tags --quiet", "clone-test-repos-mkdocs-mkdocs": "cd test-repos && git clone https://github.com/mkdocs/mkdocs mkdocs-mkdocs --depth 1 --no-tags --quiet", @@ -32,41 +32,46 @@ "clone-test-repos-large": "npm run clone-test-repos && cd test-repos && npm run clone-test-repos-dotnet-docs && npm run clone-test-repos-v8-v8-dev", "declaration": "npm run build-declaration && npm run test-declaration", "example": "cd example && node standalone.js && grunt markdownlint --force && gulp markdownlint", + "docker-npm-install": "docker run --rm --tty --name npm-install --volume $PWD:/home/workdir --workdir /home/workdir --user node node:16 npm install", + "docker-npm-run-upgrade": "docker run --rm --tty --name npm-run-upgrade --volume $PWD:/home/workdir --workdir /home/workdir --user node node:16 npm run upgrade", "lint": "eslint --max-warnings 0 .", "lint-test-repos": "ava --timeout=5m test/markdownlint-test-repos.js", - "test": "ava test/markdownlint-test.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js", + "serial-declaration-demo": "npm run build-declaration && npm-run-all --continue-on-error --parallel build-demo test-declaration", + "test": "ava test/markdownlint-test.js test/markdownlint-test-config.js test/markdownlint-test-custom-rules.js test/markdownlint-test-helpers.js test/markdownlint-test-result-object.js test/markdownlint-test-scenarios.js", "test-cover": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 npm test", "test-declaration": "cd example/typescript && tsc && node type-check.js", - "test-extra": "ava --timeout=5m test/markdownlint-test-extra.js" + "test-extra": "ava --timeout=5m test/markdownlint-test-extra-parse.js test/markdownlint-test-extra-type.js", + "upgrade": "npx npm-check-updates --upgrade" }, "engines": { - "node": ">=10" + "node": ">=12" }, "dependencies": { - "markdown-it": "12.2.0" + "markdown-it": "12.3.0" }, "devDependencies": { "ava": "~3.15.0", - "c8": "~7.8.0", - "eslint": "~7.32.0", - "eslint-plugin-jsdoc": "~36.0.7", + "c8": "~7.10.0", + "eslint": "~8.5.0", + "eslint-plugin-jsdoc": "~37.4.0", "eslint-plugin-node": "~11.1.0", - "eslint-plugin-unicorn": "~35.0.0", - "globby": "~11.0.4", + "eslint-plugin-unicorn": "~39.0.0", + "globby": "~12.0.2", "js-yaml": "~4.1.0", "markdown-it-for-inline": "~0.1.1", "markdown-it-sub": "~1.0.0", "markdown-it-sup": "~1.0.0", - "markdown-it-texmath": "~0.9.1", - "markdownlint-rule-helpers": "~0.14.0", + "markdown-it-texmath": "~0.9.7", + "markdownlint-rule-github-internal-links": "~0.1.0", + "markdownlint-rule-helpers": "~0.15.0", "npm-run-all": "~4.1.5", - "strip-json-comments": "~3.1.1", + "strip-json-comments": "~4.0.0", "toml": "~3.0.0", - "ts-loader": "~9.2.5", + "ts-loader": "~9.2.6", "tv4": "~1.3.0", - "typescript": "~4.3.5", - "webpack": "~5.51.1", - "webpack-cli": "~4.8.0" + "typescript": "~4.5.4", + "webpack": "~5.65.0", + "webpack-cli": "~4.9.1" }, "keywords": [ "markdown", diff --git a/schema/.markdownlint.jsonc b/schema/.markdownlint.jsonc index 1f2493b1..a7bfa5b2 100644 --- a/schema/.markdownlint.jsonc +++ b/schema/.markdownlint.jsonc @@ -39,7 +39,9 @@ // Spaces for indent "indent": 2, // Whether to indent the first level of the list - "start_indented": false + "start_indented": false, + // Spaces for first level indent (when start_indented is set) + "start_indent": 2 }, // MD009/no-trailing-spaces - Trailing spaces @@ -248,5 +250,17 @@ "MD048": { // Code fence style "style": "consistent" + }, + + // MD049/emphasis-style - Emphasis style should be consistent + "MD049": { + // Emphasis style should be consistent + "style": "consistent" + }, + + // MD050/strong-style - Strong style should be consistent + "MD050": { + // Strong style should be consistent + "style": "consistent" } } \ No newline at end of file diff --git a/schema/.markdownlint.yaml b/schema/.markdownlint.yaml index ba61bef1..ce92ec5c 100644 --- a/schema/.markdownlint.yaml +++ b/schema/.markdownlint.yaml @@ -36,6 +36,8 @@ MD007: indent: 2 # Whether to indent the first level of the list start_indented: false + # Spaces for first level indent (when start_indented is set) + start_indent: 2 # MD009/no-trailing-spaces - Trailing spaces MD009: @@ -224,4 +226,14 @@ MD047: true # MD048/code-fence-style - Code fence style MD048: # Code fence style + style: "consistent" + +# MD049/emphasis-style - Emphasis style should be consistent +MD049: + # Emphasis style should be consistent + style: "consistent" + +# MD050/strong-style - Strong style should be consistent +MD050: + # Strong style should be consistent style: "consistent" \ No newline at end of file diff --git a/schema/build-config-schema.js b/schema/build-config-schema.js index fbebe5af..f0585e19 100644 --- a/schema/build-config-schema.js +++ b/schema/build-config-schema.js @@ -107,6 +107,12 @@ rules.forEach(function forRule(rule) { "description": "Whether to indent the first level of the list", "type": "boolean", "default": false + }, + "start_indent": { + "description": + "Spaces for first level indent (when start_indented is set)", + "type": "integer", + "default": 2 } }; break; @@ -396,6 +402,34 @@ rules.forEach(function forRule(rule) { } }; break; + case "MD049": + scheme.properties = { + "style": { + "description": "Emphasis style should be consistent", + "type": "string", + "enum": [ + "consistent", + "asterisk", + "underscore" + ], + "default": "consistent" + } + }; + break; + case "MD050": + scheme.properties = { + "style": { + "description": "Strong style should be consistent", + "type": "string", + "enum": [ + "consistent", + "asterisk", + "underscore" + ], + "default": "consistent" + } + }; + break; default: custom = false; break; diff --git a/schema/markdownlint-config-schema.json b/schema/markdownlint-config-schema.json index a434e7cd..5c974fd7 100644 --- a/schema/markdownlint-config-schema.json +++ b/schema/markdownlint-config-schema.json @@ -238,6 +238,11 @@ "description": "Whether to indent the first level of the list", "type": "boolean", "default": false + }, + "start_indent": { + "description": "Spaces for first level indent (when start_indented is set)", + "type": "integer", + "default": 2 } }, "additionalProperties": false @@ -259,6 +264,11 @@ "description": "Whether to indent the first level of the list", "type": "boolean", "default": false + }, + "start_indent": { + "description": "Spaces for first level indent (when start_indented is set)", + "type": "integer", + "default": 2 } }, "additionalProperties": false @@ -1439,6 +1449,90 @@ }, "additionalProperties": false }, + "MD049": { + "description": "MD049/emphasis-style - Emphasis style should be consistent", + "type": [ + "boolean", + "object" + ], + "default": true, + "properties": { + "style": { + "description": "Emphasis style should be consistent", + "type": "string", + "enum": [ + "consistent", + "asterisk", + "underscore" + ], + "default": "consistent" + } + }, + "additionalProperties": false + }, + "emphasis-style": { + "description": "MD049/emphasis-style - Emphasis style should be consistent", + "type": [ + "boolean", + "object" + ], + "default": true, + "properties": { + "style": { + "description": "Emphasis style should be consistent", + "type": "string", + "enum": [ + "consistent", + "asterisk", + "underscore" + ], + "default": "consistent" + } + }, + "additionalProperties": false + }, + "MD050": { + "description": "MD050/strong-style - Strong style should be consistent", + "type": [ + "boolean", + "object" + ], + "default": true, + "properties": { + "style": { + "description": "Strong style should be consistent", + "type": "string", + "enum": [ + "consistent", + "asterisk", + "underscore" + ], + "default": "consistent" + } + }, + "additionalProperties": false + }, + "strong-style": { + "description": "MD050/strong-style - Strong style should be consistent", + "type": [ + "boolean", + "object" + ], + "default": true, + "properties": { + "style": { + "description": "Strong style should be consistent", + "type": "string", + "enum": [ + "consistent", + "asterisk", + "underscore" + ], + "default": "consistent" + } + }, + "additionalProperties": false + }, "headings": { "description": "headings - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043", "type": "boolean", @@ -1535,7 +1629,7 @@ "default": true }, "emphasis": { - "description": "emphasis - MD036, MD037", + "description": "emphasis - MD036, MD037, MD049, MD050", "type": "boolean", "default": true }, diff --git a/scripts/index.js b/scripts/index.js index 0f31207b..de3f9141 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -2,20 +2,27 @@ "use strict"; -const fs = require("fs"); -const globby = require("globby"); +const fs = require("fs").promises; const [ command, ...args ] = process.argv.slice(2); -if (command === "copy") { - const [ src, dest ] = args; - fs.copyFileSync(src, dest); -} else if (command === "delete") { - for (const arg of args) { - for (const file of globby.sync(arg)) { - fs.unlinkSync(file); - } +// eslint-disable-next-line unicorn/prefer-top-level-await +(async() => { + if (command === "copy") { + const [ src, dest ] = args; + await fs.copyFile(src, dest); + } else if (command === "delete") { + // eslint-disable-next-line node/no-unsupported-features/es-syntax + const { globby } = await import("globby"); + await Promise.all( + args.flatMap( + (glob) => globby(glob) + .then( + (files) => files.map((file) => fs.unlink(file)) + ) + ) + ); + } else { + throw new Error(`Unsupported command: ${command}`); } -} else { - throw new Error(`Unsupported command: ${command}`); -} +})(); diff --git a/test/break-all-the-rules.md b/test/break-all-the-rules.md index c3f83234..140d87cd 100644 --- a/test/break-all-the-rules.md +++ b/test/break-all-the-rules.md @@ -64,7 +64,7 @@ https://example.com/page {MD034} _Section {MD036} Heading_ -Emphasis *with * space {MD037} +Emphasis _with _ space {MD037} Code `with ` space {MD038} @@ -85,4 +85,12 @@ markdownLint {MD044} ![](image.jpg) {MD045} ## Heading 10 {MD022} +Emphasis _with_ underscore style + +Emphasis *with* different style {MD049} + +Strong __with__ underscore style + +Strong **with** different style {MD050} + EOF {MD047} \ No newline at end of file diff --git a/test/code-blocks-and-spans.md b/test/code-blocks-and-spans.md new file mode 100644 index 00000000..54a243a9 --- /dev/null +++ b/test/code-blocks-and-spans.md @@ -0,0 +1,38 @@ +# Code Blocks and Spans {MD044} + +Text CODE text {MD044} + +Text `CODE` text + +```lang +CODE + +CODE +``` + +`CODE` text `CODE` + + CODE + + CODE + +Text `CODE +CODE` text +text text +text `CODE +CODE CODE +CODE` text + +Text `CODE {MD044} + +Text `CODE {MD044} + + diff --git a/test/code-with-tabs-allowed.md b/test/code-with-tabs-allowed.md new file mode 100644 index 00000000..b86f0a36 --- /dev/null +++ b/test/code-with-tabs-allowed.md @@ -0,0 +1,33 @@ +# Code With Tabs Allowed + +Text text {MD010} + +Text `code code` text + +Text ` code` text + +Text `code ` text + +Text `code code +code code +code code` text + + console.log(" "); + +```js +console.log(" "); +``` + +```j s {MD010} +console.log(" "); +``` + + console.log(""); + + diff --git a/test/code-with-tabs-blocked.md b/test/code-with-tabs-blocked.md new file mode 100644 index 00000000..10464557 --- /dev/null +++ b/test/code-with-tabs-blocked.md @@ -0,0 +1,33 @@ +# Code With Tabs Blocked + +Text text {MD010} + +Text `code code` text {MD010} + +Text ` code` text {MD010} + +Text `code ` text {MD010} + +Text `code code +code code {MD010} +code code` text + + console.log(" "); // {MD010} + +```js +console.log(" "); // {MD010} +``` + +```j s {MD010} +console.log(" "); // {MD010} +``` + + console.log(""); // {MD010} + + diff --git a/test/detailed-results-MD031-MD040.json b/test/detailed-results-MD031-MD040.json index 7935d0c1..84228acf 100644 --- a/test/detailed-results-MD031-MD040.json +++ b/test/detailed-results-MD031-MD040.json @@ -1,4 +1,5 @@ { "default": true, - "MD041": false + "MD041": false, + "MD049": false } diff --git a/test/detailed-results-MD041-MD050.md b/test/detailed-results-MD041-MD050.md index 2b2550eb..d4b6fd0b 100644 --- a/test/detailed-results-MD041-MD050.md +++ b/test/detailed-results-MD041-MD050.md @@ -28,4 +28,18 @@ Fenced code Fenced code ~~~ +Mixed *emphasis* on _this_ line *with* multiple _issues_ + +Mixed __strong emphasis__ on **this** line __with__ multiple **issues** + +Inconsistent +emphasis _text +spanning_ many +lines + +Inconsistent +strong **emphasis +spanning** many +lines + Missing newline character \ No newline at end of file diff --git a/test/detailed-results-MD041-MD050.md.fixed b/test/detailed-results-MD041-MD050.md.fixed index cf4b4f70..a26adc9b 100644 --- a/test/detailed-results-MD041-MD050.md.fixed +++ b/test/detailed-results-MD041-MD050.md.fixed @@ -28,4 +28,18 @@ Fenced code Fenced code ~~~ +Mixed *emphasis* on *this* line *with* multiple *issues* + +Mixed __strong emphasis__ on __this__ line __with__ multiple __issues__ + +Inconsistent +emphasis _text +spanning_ many +lines + +Inconsistent +strong **emphasis +spanning** many +lines + Missing newline character diff --git a/test/detailed-results-MD041-MD050.results.json b/test/detailed-results-MD041-MD050.results.json index 70fa4e3a..d9b7b38b 100644 --- a/test/detailed-results-MD041-MD050.results.json +++ b/test/detailed-results-MD041-MD050.results.json @@ -78,7 +78,7 @@ "fixInfo": null }, { - "lineNumber": 31, + "lineNumber": 45, "ruleNames": [ "MD043", "required-headings", @@ -178,7 +178,7 @@ "fixInfo": null }, { - "lineNumber": 31, + "lineNumber": 45, "ruleNames": [ "MD047", "single-trailing-newline" @@ -208,5 +208,111 @@ "errorContext": null, "errorRange": null, "fixInfo": null + }, + { + "errorContext": null, + "errorDetail": "Expected: asterisk; Actual: underscore", + "errorRange": [ + 21, + 6 + ], + "fixInfo": { + "deleteCount": 6, + "editColumn": 21, + "insertText": "*this*" + }, + "lineNumber": 31, + "ruleDescription": "Emphasis style should be consistent", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md049", + "ruleNames": [ + "MD049", + "emphasis-style" + ] + }, + { + "errorContext": null, + "errorDetail": "Expected: asterisk; Actual: underscore", + "errorRange": [ + 49, + 8 + ], + "fixInfo": { + "deleteCount": 8, + "editColumn": 49, + "insertText": "*issues*" + }, + "lineNumber": 31, + "ruleDescription": "Emphasis style should be consistent", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md049", + "ruleNames": [ + "MD049", + "emphasis-style" + ] + }, + { + "errorContext": null, + "errorDetail": "Expected: asterisk; Actual: underscore", + "errorRange": null, + "fixInfo": null, + "lineNumber": 36, + "ruleDescription": "Emphasis style should be consistent", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md049", + "ruleNames": [ + "MD049", + "emphasis-style" + ] + }, + { + "errorContext": null, + "errorDetail": "Expected: underscore; Actual: asterisk", + "errorRange": [ + 30, + 8 + ], + "fixInfo": { + "deleteCount": 8, + "editColumn": 30, + "insertText": "__this__" + }, + "lineNumber": 33, + "ruleDescription": "Strong style should be consistent", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md050", + "ruleNames": [ + "MD050", + "strong-style" + ] + }, + { + "errorContext": null, + "errorDetail": "Expected: underscore; Actual: asterisk", + "errorRange": [ + 62, + 10 + ], + "fixInfo": { + "deleteCount": 10, + "editColumn": 62, + "insertText": "__issues__" + }, + "lineNumber": 33, + "ruleDescription": "Strong style should be consistent", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md050", + "ruleNames": [ + "MD050", + "strong-style" + ] + }, + { + "errorContext": null, + "errorDetail": "Expected: underscore; Actual: asterisk", + "errorRange": null, + "fixInfo": null, + "lineNumber": 41, + "ruleDescription": "Strong style should be consistent", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md050", + "ruleNames": [ + "MD050", + "strong-style" + ] } ] \ No newline at end of file diff --git a/test/detailed-results-links.md b/test/detailed-results-links.md index 3548c6c5..c9d90dc3 100644 --- a/test/detailed-results-links.md +++ b/test/detailed-results-links.md @@ -28,3 +28,17 @@ Text more \* text https://example.com/same more \[ te Text https://example.com/first more text https://example.com/second still more text https://example.com/third done (Incorrect link syntax)[https://www.example.com/] + +Text [link ](https://example.com/) text. + +Text [ link](https://example.com/) text. + +Text [ link ](https://example.com/) text. + +Text [link ][reference] text. + +Text [ link][reference] text. + +Text [ link ][reference] text. + +[reference]: https://example.com/ diff --git a/test/detailed-results-links.md.fixed b/test/detailed-results-links.md.fixed index 90f735cf..ab3cf6e0 100644 --- a/test/detailed-results-links.md.fixed +++ b/test/detailed-results-links.md.fixed @@ -28,3 +28,17 @@ Text more \* text https://example.com/same more \[ te Text more text still more text done [Incorrect link syntax](https://www.example.com/) + +Text [link](https://example.com/) text. + +Text [link](https://example.com/) text. + +Text [link](https://example.com/) text. + +Text [link][reference] text. + +Text [link][reference] text. + +Text [link][reference] text. + +[reference]: https://example.com/ diff --git a/test/detailed-results-links.results.json b/test/detailed-results-links.results.json index 9d6c721a..8dc8bae9 100644 --- a/test/detailed-results-links.results.json +++ b/test/detailed-results-links.results.json @@ -271,5 +271,125 @@ "deleteCount": 25, "insertText": "" } + }, + { + "lineNumber": 32, + "ruleNames": [ + "MD039", + "no-space-in-links" + ], + "ruleDescription": "Spaces inside link text", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", + "errorDetail": null, + "errorContext": "[link ]", + "errorRange": [ + 6, + 7 + ], + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "link" + } + }, + { + "lineNumber": 34, + "ruleNames": [ + "MD039", + "no-space-in-links" + ], + "ruleDescription": "Spaces inside link text", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", + "errorDetail": null, + "errorContext": "[ link]", + "errorRange": [ + 6, + 7 + ], + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "link" + } + }, + { + "lineNumber": 36, + "ruleNames": [ + "MD039", + "no-space-in-links" + ], + "ruleDescription": "Spaces inside link text", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", + "errorDetail": null, + "errorContext": "[ link ]", + "errorRange": [ + 6, + 8 + ], + "fixInfo": { + "editColumn": 7, + "deleteCount": 6, + "insertText": "link" + } + }, + { + "lineNumber": 38, + "ruleNames": [ + "MD039", + "no-space-in-links" + ], + "ruleDescription": "Spaces inside link text", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", + "errorDetail": null, + "errorContext": "[link ]", + "errorRange": [ + 6, + 7 + ], + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "link" + } + }, + { + "lineNumber": 40, + "ruleNames": [ + "MD039", + "no-space-in-links" + ], + "ruleDescription": "Spaces inside link text", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", + "errorDetail": null, + "errorContext": "[ link]", + "errorRange": [ + 6, + 7 + ], + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "link" + } + }, + { + "lineNumber": 42, + "ruleNames": [ + "MD039", + "no-space-in-links" + ], + "ruleDescription": "Spaces inside link text", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", + "errorDetail": null, + "errorContext": "[ link ]", + "errorRange": [ + 6, + 8 + ], + "fixInfo": { + "editColumn": 7, + "deleteCount": 6, + "insertText": "link" + } } ] \ No newline at end of file diff --git a/test/emphasis-not-heading-in-blockquote.md b/test/emphasis-not-heading-in-blockquote.md index 79534d6e..f9991e91 100644 --- a/test/emphasis-not-heading-in-blockquote.md +++ b/test/emphasis-not-heading-in-blockquote.md @@ -8,11 +8,11 @@ Text Text -> *Text* +> *Text* {MD049} Text -> *Text text text* +> *Text text text* {MD049} Text diff --git a/test/emphasis_instead_of_headings.md b/test/emphasis_instead_of_headings.md index 72b8148d..c0724a00 100644 --- a/test/emphasis_instead_of_headings.md +++ b/test/emphasis_instead_of_headings.md @@ -9,7 +9,7 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -__Section 1.1: another section {MD036}__ +__Section 1.1: another section {MD036} {MD050}__ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis @@ -27,7 +27,7 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -_Section 3: oh no more sections {MD036}_ +_Section 3: oh no more sections {MD036} {MD049}_ This is a normal paragraph **that just happens to have emphasized text in** diff --git a/test/emphasis_style_asterisk.json b/test/emphasis_style_asterisk.json new file mode 100644 index 00000000..cd077666 --- /dev/null +++ b/test/emphasis_style_asterisk.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD049": { + "style": "asterisk" + } +} diff --git a/test/emphasis_style_asterisk.md b/test/emphasis_style_asterisk.md new file mode 100644 index 00000000..95fe5f30 --- /dev/null +++ b/test/emphasis_style_asterisk.md @@ -0,0 +1,5 @@ +# Emphasis style asterisk + +This is *fine* + +This is _not_ {MD049} diff --git a/test/emphasis_style_underscore.json b/test/emphasis_style_underscore.json new file mode 100644 index 00000000..3d583521 --- /dev/null +++ b/test/emphasis_style_underscore.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD049": { + "style": "underscore" + } +} diff --git a/test/emphasis_style_underscore.md b/test/emphasis_style_underscore.md new file mode 100644 index 00000000..84c20592 --- /dev/null +++ b/test/emphasis_style_underscore.md @@ -0,0 +1,5 @@ +# Emphasis style underscore + +This is _fine_ + +This is *not* {MD049} diff --git a/test/fix_102_extra_nodes_in_link_text.md b/test/fix_102_extra_nodes_in_link_text.md index 74cc494e..133386d8 100644 --- a/test/fix_102_extra_nodes_in_link_text.md +++ b/test/fix_102_extra_nodes_in_link_text.md @@ -8,3 +8,5 @@ [test **test** test](www.test.com) [test __test__ test](www.test.com) [this should not raise](www.shouldnotraise.com) + + diff --git a/test/front-matter-with-disable-next-line.md b/test/front-matter-with-disable-next-line.md new file mode 100644 index 00000000..74953fdd --- /dev/null +++ b/test/front-matter-with-disable-next-line.md @@ -0,0 +1,20 @@ +--- +front: matter +--- + +# Front Matter with Disable-Next-Line + + +
+ +
{MD033} + + +
+ +
{MD033} +
{MD033} + +
+
{MD033} +
{MD033} diff --git a/test/hr-in-blockquote-dash.md b/test/hr-in-blockquote-dash.md new file mode 100644 index 00000000..689563bb --- /dev/null +++ b/test/hr-in-blockquote-dash.md @@ -0,0 +1,39 @@ +# HR in Blockquote, Dash + +--- + +*** + +___ + +{MD035:5} {MD035:7} + +> Text +> +> --- +> +> *** +> +> ___ +> +> Text + +{MD035:15} {MD035:17} + +- - - + +> Text +> +> > Text +> > +> > --- +> > +> > *** +> > +> > ___ +> > +> > Text +> +> Text + +{MD035:31} {MD035:33} diff --git a/test/hr-in-blockquote-star.md b/test/hr-in-blockquote-star.md new file mode 100644 index 00000000..ad24cc0e --- /dev/null +++ b/test/hr-in-blockquote-star.md @@ -0,0 +1,39 @@ +# HR in Blockquote, Star + +*** + +___ + +--- + +{MD035:5} {MD035:7} + +> Text +> +> --- +> +> *** +> +> ___ +> +> Text + +{MD035:13} {MD035:17} + +* * * + +> Text +> +> > Text +> > +> > --- +> > +> > *** +> > +> > ___ +> > +> > Text +> +> Text + +{MD035:29} {MD035:33} diff --git a/test/hr-in-blockquote-under.md b/test/hr-in-blockquote-under.md new file mode 100644 index 00000000..6829fc30 --- /dev/null +++ b/test/hr-in-blockquote-under.md @@ -0,0 +1,39 @@ +# HR in Blockquote, Under + +___ + +--- + +*** + +{MD035:5} {MD035:7} + +> Text +> +> --- +> +> *** +> +> ___ +> +> Text + +{MD035:13} {MD035:15} + +_ _ _ + +> Text +> +> > Text +> > +> > --- +> > +> > *** +> > +> > ___ +> > +> > Text +> +> Text + +{MD035:29} {MD035:31} diff --git a/test/hr_style_dashes.md b/test/hr_style_dashes.md index 3d90b12b..59e17f8b 100644 --- a/test/hr_style_dashes.md +++ b/test/hr_style_dashes.md @@ -20,5 +20,4 @@ _____ *** -{MD035:3} {MD035:5} {MD035:7} {MD035:11} {MD035:13} {MD035:15} {MD035:17} -{MD035:19} {MD035:21} +{MD035:3} {MD035:5} {MD035:7} {MD035:13} {MD035:15} {MD035:17} {MD035:19} {MD035:21} diff --git a/test/hr_style_inconsistent.md b/test/hr_style_inconsistent.md index 20d4c745..0da18c58 100644 --- a/test/hr_style_inconsistent.md +++ b/test/hr_style_inconsistent.md @@ -20,5 +20,4 @@ _____ *** -{MD035:5} {MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17} -{MD035:19} +{MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17} {MD035:19} diff --git a/test/hr_style_stars.md b/test/hr_style_stars.md index b096afb0..f9e7839c 100644 --- a/test/hr_style_stars.md +++ b/test/hr_style_stars.md @@ -20,5 +20,4 @@ _____ *** -{MD035:5} {MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17} -{MD035:19} +{MD035:7} {MD035:9} {MD035:11} {MD035:13} {MD035:15} {MD035:17} {MD035:19} diff --git a/test/inline_html.md b/test/inline_html.md index e76a4245..995113c1 100644 --- a/test/inline_html.md +++ b/test/inline_html.md @@ -22,6 +22,63 @@ Text \` text `` text \` text `` text Text \`\` text `` text Text `` text \` text `` text +## Elements in multiple line code spans + +Text `code +` + +`code +` + +`code +` text + +Text `code +code + +` + +``code ``` ```` ` +code +`` + +Text `code + +code` text + +Text `code code +code ` text + +Text `code +code code` text + +Text `code code +code code +code code` text + +Text ````code code +code code +code code```` text + +Text `code code +code ` text +text `code code +code code` text + +Text `code code +code code` text +text `code code +code ` text + +Text `code code +code ` text +text `code code +code ` text + +Text `code code +code` text text `code {MD033} +code code` text + ## Slash in element name Text **\\another\directory\\** text @@ -39,3 +96,25 @@ Text **\\another\directory\\** text Google {MD033} Google {MD033} + +## Unterminated code span followed by element in code span + +Text text `text text + +Text `` text + +Text +text `text +text + +Text `code code` text + +```lang +code {MD046:112} + + +``` + +Text `code code` text + +Text text {MD033} diff --git a/test/links-with-markup.md b/test/links-with-markup.md index 44d5f33e..9934bda2 100644 --- a/test/links-with-markup.md +++ b/test/links-with-markup.md @@ -10,7 +10,7 @@ [This link has `code` and right space ](link) {MD039} -[ This link has _emphasis_ and left space](link) {MD039} +[ This link has *emphasis* and left space](link) {MD039} [This](link) line has [multiple](link) links. diff --git a/test/list-indentation-start-indent-indent.md b/test/list-indentation-start-indent-indent.md new file mode 100644 index 00000000..727cc951 --- /dev/null +++ b/test/list-indentation-start-indent-indent.md @@ -0,0 +1,35 @@ +# List Indentation start_indent/indent + + * item 1 + * item 2 + * item 2.1 + * item 2.2 + * item 2.2.1 + * item 2.3 + * item 2.3.1 {MD007} + * item 3 + * item 4 {MD005} {MD007} + +Text + + * item 1 {MD007} + * item 2 {MD007} + * item 2.1 + * item 2.2 + * item 2.2.1 + +Text + + * item 1 + * item 2 + * item 2.1 {MD007} + * item 2.2 {MD007} + * item 2.2.1 {MD007} + + diff --git a/test/list-indentation-start-indent-no-indent.md b/test/list-indentation-start-indent-no-indent.md new file mode 100644 index 00000000..27ec4acd --- /dev/null +++ b/test/list-indentation-start-indent-no-indent.md @@ -0,0 +1,34 @@ +# List Indentation start_indent/no indent + + * item 1 + * item 2 + * item 2.1 + * item 2.2 + * item 2.2.1 + * item 2.3 + * item 2.3.1 {MD007} + * item 3 + * item 4 {MD005} {MD007} + +Text + + * item 1 {MD007} + * item 2 {MD007} + * item 2.1 {MD007} + * item 2.2 {MD007} + * item 2.2.1 {MD007} + +Text + + * item 1 + * item 2 + * item 2.1 {MD007} + * item 2.2 {MD007} + * item 2.2.1 {MD007} + + diff --git a/test/list-indentation-start-indented.json b/test/list-indentation-start-indented-indent.json similarity index 100% rename from test/list-indentation-start-indented.json rename to test/list-indentation-start-indented-indent.json diff --git a/test/list-indentation-start-indented.md b/test/list-indentation-start-indented-indent.md similarity index 100% rename from test/list-indentation-start-indented.md rename to test/list-indentation-start-indented-indent.md diff --git a/test/list-indentation-start-indented-no-indent.md b/test/list-indentation-start-indented-no-indent.md new file mode 100644 index 00000000..b30e1b6a --- /dev/null +++ b/test/list-indentation-start-indented-no-indent.md @@ -0,0 +1,46 @@ +# List Indentation - Start Indented/No Indent + + * item 1 + * item 2 + * item 2.1 + * item 2.2 + * item 2.2.1 + * item 2.3 + * item 3 + +## Disallowed List Indentation - Starts at Zero + +* item 1 {MD007} +* item 2 {MD007} + * item 2.1 {MD007} + * item 2.2 {MD007} + * item 2.2.1 {MD007} + * item 2.3 {MD007} +* item 3 {MD007} + +## Disallowed List Indentation - Starts at One + + * item 1 {MD007} + * item 2 {MD007} + * item 2.1 {MD007} + * item 2.2 {MD007} + * item 2.2.1 {MD007} + * item 2.3 {MD007} + * item 2.3.1 + * item 3 {MD007} + +## Disallowed List Indentation - Starts at Three + + * item 1 {MD007} + * item 2 {MD007} + * item 2.1 {MD007} + * item 2.2 {MD007} + * item 2.2.1 {MD007} + * item 2.3 {MD007} + * item 3 {MD007} + + diff --git a/test/lists-with-commented-items.md b/test/lists-with-commented-items.md new file mode 100644 index 00000000..6fa15c3b --- /dev/null +++ b/test/lists-with-commented-items.md @@ -0,0 +1,38 @@ +# Lists with Commented Items + +Text + +- item +- item + +- item +- item + +Text + +- item +- item + +- item +- item + +Text + +- item + +- item + +Text + +- item +- item + + +- item +- item + +Text diff --git a/test/long_lines.md b/test/long_lines.md index 17b61371..2ba3aa85 100644 --- a/test/long_lines.md +++ b/test/long_lines.md @@ -32,15 +32,15 @@ This long line includes a simple [reference][label] link and is long enough to v *[This long line is comprised of an emphasized link](https://example.com "This is the long link's title")* -_[This long line is comprised of an emphasized link](https://example.com "This is the long link's title")_ +_[This long line is comprised of an emphasized link {MD049}](https://example.com "This is the long link's title")_ **[This long line is comprised of a bolded link](https://example.com "This is the long link's title")** -__[This long line is comprised of a bolded link](https://example.com "This is the long link's title")__ +__[This long line is comprised of a bolded link {MD050}](https://example.com "This is the long link's title")__ -_**[This long line is comprised of an emphasized and bolded link](https://example.com "This is the long link's title")**_ +_**[This long line is comprised of an emphasized and bolded link {MD049}](https://example.com "This is the long link's title")**_ -**_[This long line is comprised of an emphasized and bolded link](https://example.com "This is the long link's title")_** +**_[This long line is comprised of an emphasized and bolded link {MD049}](https://example.com "This is the long link's title")_** *[](https://example.com "This long line is comprised of an emphasized link with empty text and a non-empty title")* diff --git a/test/markdownlint-test-config.js b/test/markdownlint-test-config.js new file mode 100644 index 00000000..6c88ed5a --- /dev/null +++ b/test/markdownlint-test-config.js @@ -0,0 +1,484 @@ +// @ts-check + +"use strict"; + +const path = require("path"); +const test = require("ava").default; +const markdownlint = require("../lib/markdownlint"); + +test.cb("configSingle", (t) => { + t.plan(2); + markdownlint.readConfig("./test/config/config-child.json", + function callback(err, actual) { + t.falsy(err); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configAbsolute", (t) => { + t.plan(2); + markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"), + function callback(err, actual) { + t.falsy(err); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configMultiple", (t) => { + t.plan(2); + markdownlint.readConfig("./test/config/config-grandparent.json", + function callback(err, actual) { + t.falsy(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configMultipleWithRequireResolve", (t) => { + t.plan(2); + markdownlint.readConfig("./test/config/config-packageparent.json", + function callback(err, actual) { + t.falsy(err); + const expected = { + ...require("./node_modules/pseudo-package/config-frompackage.json"), + ...require("./config/config-packageparent.json") + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configCustomFileSystem", (t) => { + t.plan(5); + const file = path.resolve("/dir/file.json"); + const extended = path.resolve("/dir/extended.json"); + const fileContent = { + "extends": extended, + "default": true, + "MD001": false + }; + const extendedContent = { + "MD001": true, + "MD002": true + }; + const fsApi = { + "access": (p, m, cb) => { + t.is(p, extended); + return (cb || m)(); + }, + "readFile": (p, o, cb) => { + switch (p) { + case file: + t.is(p, file); + return cb(null, JSON.stringify(fileContent)); + case extended: + t.is(p, extended); + return cb(null, JSON.stringify(extendedContent)); + default: + return t.fail(); + } + } + }; + markdownlint.readConfig( + file, + null, + fsApi, + function callback(err, actual) { + t.falsy(err); + const expected = { + ...extendedContent, + ...fileContent + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configBadFile", (t) => { + t.plan(4); + markdownlint.readConfig("./test/config/config-badfile.json", + function callback(err, result) { + t.truthy(err, "Did not get an error for bad file."); + t.true(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + t.is(err.code, "ENOENT", "Error code for bad file not ENOENT."); + t.true(!result, "Got result for bad file."); + t.end(); + }); +}); + +test.cb("configBadChildFile", (t) => { + t.plan(4); + markdownlint.readConfig("./test/config/config-badchildfile.json", + function callback(err, result) { + t.truthy(err, "Did not get an error for bad child file."); + t.true(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + t.is(err.code, "ENOENT", + "Error code for bad child file not ENOENT."); + t.true(!result, "Got result for bad child file."); + t.end(); + }); +}); + +test.cb("configBadChildPackage", (t) => { + t.plan(4); + markdownlint.readConfig("./test/config/config-badchildpackage.json", + function callback(err, result) { + t.truthy(err, "Did not get an error for bad child package."); + t.true(err instanceof Error, "Error not instance of Error."); + // @ts-ignore + t.is(err.code, "ENOENT", + "Error code for bad child package not ENOENT."); + t.true(!result, "Got result for bad child package."); + t.end(); + }); +}); + +test.cb("configBadJson", (t) => { + t.plan(3); + markdownlint.readConfig("./test/config/config-badjson.json", + function callback(err, result) { + t.truthy(err, "Did not get an error for bad JSON."); + t.true(err instanceof Error, "Error not instance of Error."); + t.true(!result, "Got result for bad JSON."); + t.end(); + }); +}); + +test.cb("configBadChildJson", (t) => { + t.plan(3); + markdownlint.readConfig("./test/config/config-badchildjson.json", + function callback(err, result) { + t.truthy(err, "Did not get an error for bad child JSON."); + t.true(err instanceof Error, "Error not instance of Error."); + t.true(!result, "Got result for bad child JSON."); + t.end(); + }); +}); + +test.cb("configSingleYaml", (t) => { + t.plan(2); + markdownlint.readConfig( + "./test/config/config-child.yaml", + // @ts-ignore + [ require("js-yaml").load ], + function callback(err, actual) { + t.falsy(err); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configMultipleYaml", (t) => { + t.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent.yaml", + // @ts-ignore + [ require("js-yaml").load ], + function callback(err, actual) { + t.falsy(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configMultipleHybrid", (t) => { + t.plan(2); + markdownlint.readConfig( + "./test/config/config-grandparent-hybrid.yaml", + // @ts-ignore + [ JSON.parse, require("toml").parse, require("js-yaml").load ], + function callback(err, actual) { + t.falsy(err); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + t.like(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configBadHybrid", (t) => { + t.plan(4); + markdownlint.readConfig( + "./test/config/config-badcontent.txt", + // @ts-ignore + [ JSON.parse, require("toml").parse, require("js-yaml").load ], + function callback(err, result) { + t.truthy(err, "Did not get an error for bad child JSON."); + t.true(err instanceof Error, "Error not instance of Error."); + t.truthy(err.message.match( + // eslint-disable-next-line max-len + /^Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+;/ + ), "Error message unexpected."); + t.true(!result, "Got result for bad child JSON."); + t.end(); + }); +}); + +test("configSingleSync", (t) => { + t.plan(1); + const actual = markdownlint.readConfigSync("./test/config/config-child.json"); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); +}); + +test("configAbsoluteSync", (t) => { + t.plan(1); + const actual = markdownlint.readConfigSync( + path.join(__dirname, "config", "config-child.json")); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); +}); + +test("configMultipleSync", (t) => { + t.plan(1); + const actual = + markdownlint.readConfigSync("./test/config/config-grandparent.json"); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); +}); + +test("configBadFileSync", (t) => { + t.plan(1); + t.throws( + function badFileCall() { + markdownlint.readConfigSync("./test/config/config-badfile.json"); + }, + { + "message": /ENOENT/ + }, + "Did not get correct exception for bad file." + ); +}); + +test("configBadChildFileSync", (t) => { + t.plan(1); + t.throws( + function badChildFileCall() { + markdownlint.readConfigSync("./test/config/config-badchildfile.json"); + }, + { + "message": /ENOENT/ + }, + "Did not get correct exception for bad child file." + ); +}); + +test("configBadJsonSync", (t) => { + t.plan(1); + t.throws( + function badJsonCall() { + markdownlint.readConfigSync("./test/config/config-badjson.json"); + }, + { + "message": + // eslint-disable-next-line max-len + /Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+/ + }, + "Did not get correct exception for bad JSON." + ); +}); + +test("configBadChildJsonSync", (t) => { + t.plan(1); + t.throws( + function badChildJsonCall() { + markdownlint.readConfigSync("./test/config/config-badchildjson.json"); + }, + { + "message": + // eslint-disable-next-line max-len + /Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+/ + }, + "Did not get correct exception for bad child JSON." + ); +}); + +test("configSingleYamlSync", (t) => { + t.plan(1); + const actual = markdownlint.readConfigSync( + // @ts-ignore + "./test/config/config-child.yaml", [ require("js-yaml").load ]); + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); +}); + +test("configMultipleYamlSync", (t) => { + t.plan(1); + const actual = markdownlint.readConfigSync( + // @ts-ignore + "./test/config/config-grandparent.yaml", [ require("js-yaml").load ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); +}); + +test("configMultipleHybridSync", (t) => { + t.plan(1); + const actual = markdownlint.readConfigSync( + "./test/config/config-grandparent-hybrid.yaml", + // @ts-ignore + [ JSON.parse, require("toml").parse, require("js-yaml").load ]); + const expected = { + ...require("./config/config-child.json"), + ...require("./config/config-parent.json"), + ...require("./config/config-grandparent.json") + }; + delete expected.extends; + t.like(actual, expected, "Config object not correct."); +}); + +test("configCustomFileSystemSync", (t) => { + t.plan(4); + const file = path.resolve("/dir/file.json"); + const extended = path.resolve("/dir/extended.json"); + const fileContent = { + "extends": extended, + "default": true, + "MD001": false + }; + const extendedContent = { + "MD001": true, + "MD002": true + }; + const fsApi = { + "accessSync": (p) => { + t.is(p, extended); + }, + "readFileSync": (p) => { + switch (p) { + case file: + t.is(p, file); + return JSON.stringify(fileContent); + case extended: + t.is(p, extended); + return JSON.stringify(extendedContent); + default: + return t.fail(); + } + } + }; + const actual = markdownlint.readConfigSync(file, null, fsApi); + const expected = { + ...extendedContent, + ...fileContent + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); +}); + +test("configBadHybridSync", (t) => { + t.plan(1); + t.throws( + function badHybridCall() { + markdownlint.readConfigSync( + "./test/config/config-badcontent.txt", + // @ts-ignore + [ JSON.parse, require("toml").parse, require("js-yaml").load ]); + }, + { + // eslint-disable-next-line max-len + "message": /^Unable to parse '[^']*'; Parser \d+: Unexpected token \S+ in JSON at position \d+;/ + }, + "Did not get correct exception for bad content." + ); +}); + +test.cb("configSinglePromise", (t) => { + t.plan(1); + markdownlint.promises.readConfig("./test/config/config-child.json") + .then((actual) => { + const expected = require("./config/config-child.json"); + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configCustomFileSystemPromise", (t) => { + t.plan(4); + const file = path.resolve("/dir/file.json"); + const extended = path.resolve("/dir/extended.json"); + const fileContent = { + "extends": extended, + "default": true, + "MD001": false + }; + const extendedContent = { + "MD001": true, + "MD002": true + }; + const fsApi = { + "access": (p, m, cb) => { + t.is(p, extended); + return (cb || m)(); + }, + "readFile": (p, o, cb) => { + switch (p) { + case file: + t.is(p, file); + return cb(null, JSON.stringify(fileContent)); + case extended: + t.is(p, extended); + return cb(null, JSON.stringify(extendedContent)); + default: + return t.fail(); + } + } + }; + markdownlint.promises.readConfig(file, null, fsApi) + .then((actual) => { + const expected = { + ...extendedContent, + ...fileContent + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + +test.cb("configBadFilePromise", (t) => { + t.plan(2); + markdownlint.promises.readConfig("./test/config/config-badfile.json") + .then( + null, + (error) => { + t.truthy(error, "Did not get an error for bad JSON."); + t.true(error instanceof Error, "Error not instance of Error."); + t.end(); + } + ); +}); diff --git a/test/markdownlint-test-custom-rules.js b/test/markdownlint-test-custom-rules.js index 5975a64e..59e68a18 100644 --- a/test/markdownlint-test-custom-rules.js +++ b/test/markdownlint-test-custom-rules.js @@ -2,12 +2,11 @@ "use strict"; +const fs = require("fs").promises; const test = require("ava").default; -const packageJson = require("../package.json"); const markdownlint = require("../lib/markdownlint"); const customRules = require("./rules/rules.js"); -const homepage = packageJson.homepage; -const version = packageJson.version; +const { homepage, version } = require("../package.json"); test.cb("customRulesV0", (t) => { t.plan(4); @@ -328,7 +327,7 @@ test.cb("customRulesConfig", (t) => { }); }); -test("customRulesNpmPackage", (t) => { +test.cb("customRulesNpmPackage", (t) => { t.plan(2); const options = { "customRules": [ require("./rules/npm") ], @@ -345,11 +344,12 @@ test("customRulesNpmPackage", (t) => { }; // @ts-ignore t.deepEqual(actualResult, expectedResult, "Undetected issues."); + t.end(); }); }); test("customRulesBadProperty", (t) => { - t.plan(23); + t.plan(27); [ { "propertyName": "names", @@ -364,6 +364,10 @@ test("customRulesBadProperty", (t) => { "propertyName": "information", "propertyValues": [ 10, [], "string", "https://example.com" ] }, + { + "propertyName": "asynchronous", + "propertyValues": [ null, 10, "", [] ] + }, { "propertyName": "tags", "propertyValues": @@ -395,7 +399,7 @@ test("customRulesBadProperty", (t) => { }); }); -test("customRulesUsedNameName", (t) => { +test.cb("customRulesUsedNameName", (t) => { t.plan(4); markdownlint({ "customRules": [ @@ -414,10 +418,11 @@ test("customRulesUsedNameName", (t) => { "already used as a name or tag.", "Incorrect message for duplicate name."); t.true(!result, "Got result for duplicate name."); + t.end(); }); }); -test("customRulesUsedNameTag", (t) => { +test.cb("customRulesUsedNameTag", (t) => { t.plan(4); markdownlint({ "customRules": [ @@ -435,10 +440,11 @@ test("customRulesUsedNameTag", (t) => { "Name 'HtMl' of custom rule at index 0 is already used as a name or tag.", "Incorrect message for duplicate name."); t.true(!result, "Got result for duplicate name."); + t.end(); }); }); -test("customRulesUsedTagName", (t) => { +test.cb("customRulesUsedTagName", (t) => { t.plan(4); markdownlint({ "customRules": [ @@ -463,6 +469,7 @@ test("customRulesUsedTagName", (t) => { "already used as a name.", "Incorrect message for duplicate name."); t.true(!result, "Got result for duplicate tag."); + t.end(); }); }); @@ -517,7 +524,7 @@ test("customRulesThrowForFileSync", (t) => { ); }); -test("customRulesThrowForString", (t) => { +test.cb("customRulesThrowForString", (t) => { t.plan(4); const exceptionMessage = "Test exception message"; markdownlint({ @@ -540,10 +547,69 @@ test("customRulesThrowForString", (t) => { t.is(err.message, exceptionMessage, "Incorrect message for function thrown."); t.true(!result, "Got result for function thrown."); + t.end(); }); }); -test("customRulesOnErrorNull", (t) => { +test("customRulesThrowForStringSync", (t) => { + t.plan(1); + const exceptionMessage = "Test exception message"; + t.throws( + function customRuleThrowsCall() { + markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function throws() { + throw new Error(exceptionMessage); + } + } + ], + "strings": { + "string": "String" + } + }); + }, + { + "message": exceptionMessage + }, + "Did not get correct exception for function thrown." + ); +}); + +test.cb("customRulesOnErrorNull", (t) => { + t.plan(4); + markdownlint({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorNull(params, onError) { + onError(null); + } + } + ], + "strings": { + "string": "String" + } + }, + function callback(err, result) { + t.truthy(err, "Did not get an error for function thrown."); + t.true(err instanceof Error, "Error not instance of Error."); + t.is( + err.message, + "Property 'lineNumber' of onError parameter is incorrect.", + "Did not get correct exception for null object." + ); + t.true(!result, "Got result for function thrown."); + t.end(); + }); +}); + +test("customRulesOnErrorNullSync", (t) => { t.plan(1); const options = { "customRules": [ @@ -802,7 +868,7 @@ test("customRulesOnErrorValid", (t) => { }); }); -test("customRulesOnErrorLazy", (t) => { +test.cb("customRulesOnErrorLazy", (t) => { t.plan(2); const options = { "customRules": [ @@ -840,10 +906,11 @@ test("customRulesOnErrorLazy", (t) => { ] }; t.deepEqual(actualResult, expectedResult, "Undetected issues."); + t.end(); }); }); -test("customRulesOnErrorModified", (t) => { +test.cb("customRulesOnErrorModified", (t) => { t.plan(2); const errorObject = { "lineNumber": 1, @@ -900,98 +967,11 @@ test("customRulesOnErrorModified", (t) => { ] }; t.deepEqual(actualResult, expectedResult, "Undetected issues."); - }); -}); - -test.cb("customRulesThrowForFileHandled", (t) => { - t.plan(2); - const exceptionMessage = "Test exception message"; - markdownlint({ - "customRules": [ - { - "names": [ "name" ], - "description": "description", - "tags": [ "tag" ], - "function": function throws() { - throw new Error(exceptionMessage); - } - } - ], - "files": [ "./test/custom-rules.md" ], - "handleRuleFailures": true - }, function callback(err, actualResult) { - t.falsy(err); - const expectedResult = { - "./test/custom-rules.md": [ - { - "lineNumber": 1, - "ruleNames": [ "name" ], - "ruleDescription": "description", - "ruleInformation": null, - "errorDetail": - `This rule threw an exception: ${exceptionMessage}`, - "errorContext": null, - "errorRange": null - } - ] - }; - t.deepEqual(actualResult, expectedResult, "Undetected issues."); t.end(); }); }); -test("customRulesThrowForStringHandled", (t) => { - t.plan(2); - const exceptionMessage = "Test exception message"; - const informationUrl = "https://example.com/rule"; - markdownlint({ - "customRules": [ - { - "names": [ "name" ], - "description": "description", - "information": new URL(informationUrl), - "tags": [ "tag" ], - "function": function throws() { - throw new Error(exceptionMessage); - } - } - ], - "strings": { - "string": "String\n" - }, - "handleRuleFailures": true - }, function callback(err, actualResult) { - t.falsy(err); - const expectedResult = { - "string": [ - { - "lineNumber": 1, - "ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ], - "ruleDescription": - "First line in a file should be a top-level heading", - "ruleInformation": - `${homepage}/blob/v${version}/doc/Rules.md#md041`, - "errorDetail": null, - "errorContext": "String", - "errorRange": null - }, - { - "lineNumber": 1, - "ruleNames": [ "name" ], - "ruleDescription": "description", - "ruleInformation": informationUrl, - "errorDetail": - `This rule threw an exception: ${exceptionMessage}`, - "errorContext": null, - "errorRange": null - } - ] - }; - t.deepEqual(actualResult, expectedResult, "Undetected issues."); - }); -}); - -test("customRulesOnErrorInvalidHandled", (t) => { +test.cb("customRulesOnErrorInvalidHandled", (t) => { t.plan(2); markdownlint({ "customRules": [ @@ -1001,8 +981,7 @@ test("customRulesOnErrorInvalidHandled", (t) => { "tags": [ "tag" ], "function": function onErrorInvalid(params, onError) { onError({ - "lineNumber": 13, - "details": "N/A" + "lineNumber": 13 }); } } @@ -1028,9 +1007,48 @@ test("customRulesOnErrorInvalidHandled", (t) => { ] }; t.deepEqual(actualResult, expectedResult, "Undetected issues."); + t.end(); }); }); +test("customRulesOnErrorInvalidHandledSync", (t) => { + t.plan(1); + const actualResult = markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": function onErrorInvalid(params, onError) { + onError({ + "lineNumber": 13, + "detail": "N/A" + }); + } + } + ], + "strings": { + "string": "# Heading\n" + }, + "handleRuleFailures": true + }); + const expectedResult = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": "This rule threw an exception: " + + "Property 'lineNumber' of onError parameter is incorrect.", + "errorContext": null, + "errorRange": null + } + ] + }; + t.deepEqual(actualResult, expectedResult, "Undetected issues."); +}); + test.cb("customRulesFileName", (t) => { t.plan(2); const options = { @@ -1052,7 +1070,7 @@ test.cb("customRulesFileName", (t) => { }); }); -test("customRulesStringName", (t) => { +test.cb("customRulesStringName", (t) => { t.plan(2); const options = { "customRules": [ @@ -1071,6 +1089,7 @@ test("customRulesStringName", (t) => { }; markdownlint(options, function callback(err) { t.falsy(err); + t.end(); }); }); @@ -1123,3 +1142,385 @@ test.cb("customRulesLintJavaScript", (t) => { t.end(); }); }); + +test("customRulesAsyncThrowsInSyncContext", (t) => { + t.plan(1); + const options = { + "customRules": [ + { + "names": [ "name1", "name2" ], + "description": "description", + "tags": [ "tag" ], + "asynchronous": true, + "function": () => {} + } + ], + "strings": { + "string": "Unused" + } + }; + t.throws( + () => markdownlint.sync(options), + { + "message": "Custom rule name1/name2 at index 0 is asynchronous and " + + "can not be used in a synchronous context." + }, + "Did not get correct exception for async rule in sync context." + ); +}); + +test("customRulesAsyncReadFiles", (t) => { + t.plan(3); + const options = { + "customRules": [ + { + "names": [ "name1" ], + "description": "description1", + "information": new URL("https://example.com/asyncRule1"), + "tags": [ "tag" ], + "asynchronous": true, + "function": + (params, onError) => fs.readFile(__filename, "utf8").then( + (content) => { + t.true(content.length > 0); + onError({ + "lineNumber": 1, + "detail": "detail1", + "context": "context1", + "range": [ 2, 3 ] + }); + } + ) + }, + { + "names": [ "name2" ], + "description": "description2", + "tags": [ "tag" ], + "asynchronous": true, + "function": + async(params, onError) => { + const content = await fs.readFile(__filename, "utf8"); + t.true(content.length > 0); + onError({ + "lineNumber": 1, + "detail": "detail2", + "context": "context2" + }); + } + } + ], + "strings": { + "string": "# Heading" + } + }; + const expected = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 9, 1 ] + }, + { + "lineNumber": 1, + "ruleNames": [ "name1" ], + "ruleDescription": "description1", + "ruleInformation": "https://example.com/asyncRule1", + "errorDetail": "detail1", + "errorContext": "context1", + "errorRange": [ 2, 3 ] + }, + { + "lineNumber": 1, + "ruleNames": [ "name2" ], + "ruleDescription": "description2", + "ruleInformation": null, + "errorDetail": "detail2", + "errorContext": "context2", + "errorRange": null + } + ] + }; + return markdownlint.promises.markdownlint(options) + .then((actual) => t.deepEqual(actual, expected, "Unexpected issues.")); +}); + +test("customRulesAsyncIgnoresSyncReturn", (t) => { + t.plan(1); + const options = { + "customRules": [ + { + "names": [ "sync" ], + "description": "description", + "information": new URL("https://example.com/asyncRule"), + "tags": [ "tag" ], + "asynchronous": false, + "function": () => new Promise(() => { + // Never resolves + }) + }, + { + "names": [ "async" ], + "description": "description", + "information": new URL("https://example.com/asyncRule"), + "tags": [ "tag" ], + "asynchronous": true, + "function": (params, onError) => new Promise((resolve) => { + onError({ "lineNumber": 1 }); + resolve(); + }) + } + ], + "strings": { + "string": "# Heading" + } + }; + const expected = { + "string": [ + { + "lineNumber": 1, + "ruleNames": [ "async" ], + "ruleDescription": "description", + "ruleInformation": "https://example.com/asyncRule", + "errorDetail": null, + "errorContext": null, + "errorRange": null + }, + { + "lineNumber": 1, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, + "errorDetail": null, + "errorContext": null, + "errorRange": [ 9, 1 ] + } + ] + }; + return markdownlint.promises.markdownlint(options) + .then((actual) => t.deepEqual(actual, expected, "Unexpected issues.")); +}); + +const errorMessage = "Custom error message."; +const stringScenarios = [ + [ + "Files", + [ "./test/custom-rules.md" ], + null + ], + [ + "Strings", + null, + { "./test/custom-rules.md": "# Heading\n" } + ] +]; + +[ + [ + "customRulesThrowString", + () => { + throw errorMessage; + } + ], + [ + "customRulesThrowError", + () => { + throw new Error(errorMessage); + } + ] +].forEach((flavor) => { + const [ name, func ] = flavor; + const customRule = [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": func + } + ]; + const expectedResult = { + "./test/custom-rules.md": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": `This rule threw an exception: ${errorMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + stringScenarios.forEach((inputs) => { + const [ subname, files, strings ] = inputs; + + test.cb(`${name}${subname}UnhandledAsync`, (t) => { + t.plan(4); + markdownlint({ + // @ts-ignore + "customRules": customRule, + // @ts-ignore + files, + // @ts-ignore + strings + }, function callback(err, result) { + t.truthy(err, "Did not get an error for exception."); + t.true(err instanceof Error, "Error not instance of Error."); + t.is(err.message, errorMessage, "Incorrect message for exception."); + t.true(!result, "Got result for exception."); + t.end(); + }); + }); + + test.cb(`${name}${subname}HandledAsync`, (t) => { + t.plan(2); + markdownlint({ + // @ts-ignore + "customRules": customRule, + // @ts-ignore + files, + // @ts-ignore + strings, + "handleRuleFailures": true + }, function callback(err, actualResult) { + t.falsy(err); + t.deepEqual(actualResult, expectedResult, "Undetected issues."); + t.end(); + }); + }); + + test(`${name}${subname}UnhandledSync`, (t) => { + t.plan(1); + t.throws( + () => markdownlint.sync({ + // @ts-ignore + "customRules": customRule, + // @ts-ignore + files, + // @ts-ignore + strings + }), + { + "message": errorMessage + }, + "Unexpected exception." + ); + }); + + test(`${name}${subname}HandledSync`, (t) => { + t.plan(1); + const actualResult = markdownlint.sync({ + // @ts-ignore + "customRules": customRule, + // @ts-ignore + files, + // @ts-ignore + strings, + "handleRuleFailures": true + }); + t.deepEqual(actualResult, expectedResult, "Undetected issues."); + }); + }); +}); + +[ + [ + "customRulesAsyncExceptionString", + () => { + throw errorMessage; + } + ], + [ + "customRulesAsyncExceptionError", + () => { + throw new Error(errorMessage); + } + ], + [ + "customRulesAsyncDeferredString", + () => fs.readFile(__filename, "utf8").then( + () => { + throw errorMessage; + } + ) + ], + [ + "customRulesAsyncDeferredError", + () => fs.readFile(__filename, "utf8").then( + () => { + throw new Error(errorMessage); + } + ) + ], + [ + "customRulesAsyncRejectString", + () => Promise.reject(errorMessage) + ], + [ + "customRulesAsyncRejectError", + () => Promise.reject(new Error(errorMessage)) + ] +].forEach((flavor) => { + const [ name, func ] = flavor; + const customRule = { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "asynchronous": true, + "function": func + }; + stringScenarios.forEach((inputs) => { + const [ subname, files, strings ] = inputs; + + test.cb(`${name}${subname}Unhandled`, (t) => { + t.plan(4); + markdownlint({ + // @ts-ignore + "customRules": [ customRule ], + // @ts-ignore + files, + // @ts-ignore + strings + }, function callback(err, result) { + t.truthy(err, "Did not get an error for rejection."); + t.true(err instanceof Error, "Error not instance of Error."); + t.is(err.message, errorMessage, "Incorrect message for rejection."); + t.true(!result, "Got result for rejection."); + t.end(); + }); + }); + + test.cb(`${name}${subname}Handled`, (t) => { + t.plan(2); + markdownlint({ + // @ts-ignore + "customRules": [ customRule ], + // @ts-ignore + files, + // @ts-ignore + strings, + "handleRuleFailures": true + }, function callback(err, actualResult) { + t.falsy(err); + const expectedResult = { + "./test/custom-rules.md": [ + { + "lineNumber": 1, + "ruleNames": [ "name" ], + "ruleDescription": "description", + "ruleInformation": null, + "errorDetail": `This rule threw an exception: ${errorMessage}`, + "errorContext": null, + "errorRange": null + } + ] + }; + t.deepEqual(actualResult, expectedResult, "Undetected issues."); + t.end(); + }); + }); + }); +}); diff --git a/test/markdownlint-test-extra-parse.js b/test/markdownlint-test-extra-parse.js new file mode 100644 index 00000000..7c96500d --- /dev/null +++ b/test/markdownlint-test-extra-parse.js @@ -0,0 +1,16 @@ +// @ts-check + +"use strict"; + +const test = require("ava").default; +const markdownlint = require("../lib/markdownlint"); + +// Parses all Markdown files in all package dependencies +test("parseAllFiles", async(t) => { + t.plan(1); + // eslint-disable-next-line node/no-unsupported-features/es-syntax + const { globby } = await import("globby"); + const files = await globby("**/*.{md,markdown}"); + await markdownlint.promises.markdownlint({ files }); + t.pass(); +}); diff --git a/test/markdownlint-test-extra.js b/test/markdownlint-test-extra-type.js similarity index 71% rename from test/markdownlint-test-extra.js rename to test/markdownlint-test-extra-type.js index d6e643df..827957aa 100644 --- a/test/markdownlint-test-extra.js +++ b/test/markdownlint-test-extra-type.js @@ -4,7 +4,6 @@ const fs = require("fs"); const path = require("path"); -const globby = require("globby"); const test = require("ava").default; const markdownlint = require("../lib/markdownlint"); @@ -27,15 +26,3 @@ files.filter((file) => /\.md$/.test(file)).forEach((file) => { t.pass(); }); }); - -// Parses all Markdown files in all package dependencies -test.cb("parseAllFiles", (t) => { - t.plan(1); - const options = { - "files": globby.sync("**/*.{md,markdown}") - }; - markdownlint(options, (err) => { - t.falsy(err); - t.end(); - }); -}); diff --git a/test/markdownlint-test-helpers.js b/test/markdownlint-test-helpers.js index 5e05f3ce..05d4cf3a 100644 --- a/test/markdownlint-test-helpers.js +++ b/test/markdownlint-test-helpers.js @@ -226,7 +226,7 @@ bar` }); test("isBlankLine", (t) => { - t.plan(25); + t.plan(29); const blankLines = [ null, "", @@ -244,7 +244,11 @@ test("isBlankLine", (t) => { "> ", "> > > \t", "> ", - ">>" + ">>", + " ", + "-->" ]; blankLines.forEach((line) => t.true(helpers.isBlankLine(line), line || "")); const nonBlankLines = [ @@ -253,9 +257,9 @@ test("isBlankLine", (t) => { ".", "> .", " text", - "", - "" + "text ", + "text text" ]; nonBlankLines.forEach((line) => t.true(!helpers.isBlankLine(line), line)); }); @@ -383,7 +387,7 @@ test("forEachInlineCodeSpan", (t) => { }); test("getPreferredLineEnding", (t) => { - t.plan(17); + t.plan(19); const testCases = [ [ "", os.EOL ], [ "\r", "\r" ], @@ -408,6 +412,16 @@ test("getPreferredLineEnding", (t) => { const actual = helpers.getPreferredLineEnding(input); t.is(actual, expected, "Incorrect line ending returned."); }); + t.is( + helpers.getPreferredLineEnding("", "linux"), + "\n", + "Incorrect line ending for linux" + ); + t.is( + helpers.getPreferredLineEnding("", "win32"), + "\r\n", + "Incorrect line ending for win32" + ); }); test("applyFix", (t) => { @@ -934,3 +948,37 @@ test("applyFixes", (t) => { t.is(actual, expected, "Incorrect fix applied."); }); }); + +test("deepFreeze", (t) => { + t.plan(6); + const obj = { + "prop": true, + "func": () => true, + "sub": { + "prop": [ 1 ], + "sub": { + "prop": "one" + } + } + }; + t.is(helpers.deepFreeze(obj), obj, "Did not return object."); + [ + () => { + obj.prop = false; + }, + () => { + obj.func = () => false; + }, + () => { + obj.sub.prop = []; + }, + () => { + obj.sub.prop[0] = 0; + }, + () => { + obj.sub.sub.prop = "zero"; + } + ].forEach((scenario) => { + t.throws(scenario, null, "Assigned to frozen object."); + }); +}); diff --git a/test/markdownlint-test-repos.js b/test/markdownlint-test-repos.js index 02327e39..aefd9889 100644 --- a/test/markdownlint-test-repos.js +++ b/test/markdownlint-test-repos.js @@ -6,44 +6,29 @@ const { existsSync } = require("fs"); // eslint-disable-next-line unicorn/import-style const { join } = require("path"); const { promisify } = require("util"); -const globby = require("globby"); const jsYaml = require("js-yaml"); -const stripJsonComments = require("strip-json-comments"); const test = require("ava").default; const markdownlint = require("../lib/markdownlint"); const markdownlintPromise = promisify(markdownlint); const readConfigPromise = promisify(markdownlint.readConfig); -/** - * Parses JSONC text. - * - * @param {string} json JSON to parse. - * @returns {Object} Object representation. - */ -function jsoncParse(json) { - return JSON.parse(stripJsonComments(json)); -} - -/** - * Parses YAML text. - * - * @param {string} yaml YAML to parse. - * @returns {Object} Object representation. - */ -function yamlParse(yaml) { - return jsYaml.load(yaml); -} - /** * Lints a test repository. * * @param {Object} t Test instance. * @param {string[]} globPatterns Array of files to in/exclude. * @param {string} configPath Path to config file. + * @param {RegExp[]} [ignoreRes] Array of RegExp violations to ignore. * @returns {Promise} Test result. */ -function lintTestRepo(t, globPatterns, configPath) { +async function lintTestRepo(t, globPatterns, configPath, ignoreRes) { t.plan(1); + // eslint-disable-next-line node/no-unsupported-features/es-syntax + const { globby } = await import("globby"); + // eslint-disable-next-line node/no-unsupported-features/es-syntax + const { "default": stripJsonComments } = await import("strip-json-comments"); + const jsoncParse = (json) => JSON.parse(stripJsonComments(json)); + const yamlParse = (yaml) => jsYaml.load(yaml); return Promise.all([ globby(globPatterns), // @ts-ignore @@ -57,7 +42,14 @@ function lintTestRepo(t, globPatterns, configPath) { // eslint-disable-next-line no-console console.log(`${t.title}: Linting ${files.length} files...`); return markdownlintPromise(options).then((results) => { - const resultsString = results.toString(); + let resultsString = results.toString(); + for (const ignoreRe of (ignoreRes || [])) { + const lengthBefore = resultsString.length; + resultsString = resultsString.replace(ignoreRe, ""); + if (resultsString.length === lengthBefore) { + t.fail(`Unnecessary ignore: ${ignoreRe}`); + } + } if (resultsString.length > 0) { // eslint-disable-next-line no-console console.log(resultsString); @@ -67,13 +59,26 @@ function lintTestRepo(t, globPatterns, configPath) { }); } +/** + * Excludes a list of globs. + * + * @param {string} rootDir Root directory for globs. + * @param {...string} globs Globs to exclude. + * @returns {string[]} Array of excluded globs. + */ +function excludeGlobs(rootDir, ...globs) { + return globs.map((glob) => "!" + join(rootDir, glob)); +} + // Run markdownlint the same way the corresponding repositories do test("https://github.com/eslint/eslint", (t) => { const rootDir = "./test-repos/eslint-eslint"; const globPatterns = [ join(rootDir, "docs/**/*.md") ]; const configPath = join(rootDir, ".markdownlint.yml"); - return lintTestRepo(t, globPatterns, configPath); + const ignoreRes = + [ /^[^:]+\/array-callback-return\.md: \d+: MD050\/.*$\r?\n?/gm ]; + return lintTestRepo(t, globPatterns, configPath, ignoreRes); }); test("https://github.com/mkdocs/mkdocs", (t) => { @@ -81,8 +86,13 @@ test("https://github.com/mkdocs/mkdocs", (t) => { const globPatterns = [ join(rootDir, "README.md"), join(rootDir, "CONTRIBUTING.md"), - join(rootDir, "docs/**/*.md"), - "!" + join(rootDir, "docs/CNAME") + join(rootDir, "docs"), + ...excludeGlobs( + rootDir, + "docs/CNAME", + "docs/**/*.css", + "docs/**/*.png" + ) ]; const configPath = join(rootDir, ".markdownlintrc"); return lintTestRepo(t, globPatterns, configPath); @@ -106,14 +116,16 @@ test("https://github.com/pi-hole/docs", (t) => { const rootDir = "./test-repos/pi-hole-docs"; const globPatterns = [ join(rootDir, "**/*.md") ]; const configPath = join(rootDir, ".markdownlint.json"); - return lintTestRepo(t, globPatterns, configPath); + const ignoreRes = + [ /^[^:]+\/(unbound|index|prerequisites)\.md: \d+: MD049\/.*$\r?\n?/gm ]; + return lintTestRepo(t, globPatterns, configPath, ignoreRes); }); test("https://github.com/webhintio/hint", (t) => { const rootDir = "./test-repos/webhintio-hint"; const globPatterns = [ join(rootDir, "**/*.md"), - "!" + join(rootDir, "**/CHANGELOG.md") + ...excludeGlobs(rootDir, "**/CHANGELOG.md") ]; const configPath = join(rootDir, ".markdownlintrc"); return lintTestRepo(t, globPatterns, configPath); @@ -132,12 +144,10 @@ const dotnetDocsDir = "./test-repos/dotnet-docs"; if (existsSync(dotnetDocsDir)) { test("https://github.com/dotnet/docs", (t) => { const rootDir = dotnetDocsDir; - const globPatterns = [ - join(rootDir, "**/*.md"), - "!" + join(rootDir, "samples/**/*.md") - ]; + const globPatterns = [ join(rootDir, "**/*.md") ]; const configPath = join(rootDir, ".markdownlint.json"); - return lintTestRepo(t, globPatterns, configPath); + const ignoreRes = [ /^[^:]+: \d+: (MD049|MD050)\/.*$\r?\n?/gm ]; + return lintTestRepo(t, globPatterns, configPath, ignoreRes); }); } @@ -147,6 +157,7 @@ if (existsSync(v8v8DevDir)) { const rootDir = v8v8DevDir; const globPatterns = [ join(rootDir, "src/**/*.md") ]; const configPath = join(rootDir, ".markdownlint.json"); - return lintTestRepo(t, globPatterns, configPath); + const ignoreRes = [ /^[^:]+: \d+: MD049\/.*$\r?\n?/gm ]; + return lintTestRepo(t, globPatterns, configPath, ignoreRes); }); } diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index b3956673..45227323 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -10,11 +10,11 @@ const pluginInline = require("markdown-it-for-inline"); const pluginSub = require("markdown-it-sub"); const pluginSup = require("markdown-it-sup"); const pluginTexMath = require("markdown-it-texmath"); -const stripJsonComments = require("strip-json-comments"); const test = require("ava").default; const tv4 = require("tv4"); const { homepage, version } = require("../package.json"); const markdownlint = require("../lib/markdownlint"); +const constants = require("../lib/constants"); const rules = require("../lib/rules"); const customRules = require("./rules/rules.js"); const configSchema = require("../schema/markdownlint-config-schema.json"); @@ -24,7 +24,7 @@ const pluginTexMathOptions = { "renderToString": () => "" } }; -const deprecatedRuleNames = new Set([ "MD002", "MD006" ]); +const deprecatedRuleNames = new Set(constants.deprecatedRuleNames); const configSchemaStrict = { ...configSchema, "additionalProperties": false @@ -83,11 +83,12 @@ test.cb("projectFilesNoInlineConfig", (t) => { "doc/Prettier.md", "helpers/README.md" ], - "noInlineConfig": true, "config": { "line-length": { "line_length": 150 }, "no-duplicate-heading": false - } + }, + "customRules": [ require("markdownlint-rule-github-internal-links") ], + "noInlineConfig": true }; markdownlint(options, function callback(err, actual) { t.falsy(err); @@ -492,8 +493,10 @@ test.cb("styleAll", (t) => { "MD042": [ 81 ], "MD045": [ 85 ], "MD046": [ 49, 73, 77 ], - "MD047": [ 88 ], - "MD048": [ 77 ] + "MD047": [ 96 ], + "MD048": [ 77 ], + "MD049": [ 90 ], + "MD050": [ 94 ] } }; // @ts-ignore @@ -535,8 +538,10 @@ test.cb("styleRelaxed", (t) => { "MD042": [ 81 ], "MD045": [ 85 ], "MD046": [ 49, 73, 77 ], - "MD047": [ 88 ], - "MD048": [ 77 ] + "MD047": [ 96 ], + "MD048": [ 77 ], + "MD049": [ 90 ], + "MD050": [ 94 ] } }; // @ts-ignore @@ -836,8 +841,9 @@ test.cb("customFileSystemAsync", (t) => { t.end(); }); }); + test.cb("readme", (t) => { - t.plan(115); + t.plan(119); const tagToRules = {}; rules.forEach(function forRule(rule) { rule.tags.forEach(function forTag(tag) { @@ -913,7 +919,7 @@ test.cb("readme", (t) => { }); test.cb("rules", (t) => { - t.plan(336); + t.plan(352); fs.readFile("doc/Rules.md", "utf8", (err, contents) => { t.falsy(err); @@ -924,7 +930,6 @@ test.cb("rules", (t) => { let ruleHasAliases = true; let ruleUsesParams = null; const tagAliasParameterRe = /, |: | /; - // eslint-disable-next-line func-style const testTagsAliasesParams = (r) => { // eslint-disable-next-line unicorn/prefer-default-parameters r = r || "[NO RULE]"; @@ -1064,9 +1069,10 @@ test("validateConfigSchemaAppliesToUnknownProperties", (t) => { } }); -test("validateConfigExampleJson", (t) => { +test("validateConfigExampleJson", async(t) => { t.plan(2); - + // eslint-disable-next-line node/no-unsupported-features/es-syntax + const { "default": stripJsonComments } = await import("strip-json-comments"); // Validate JSONC const fileJson = ".markdownlint.jsonc"; const dataJson = fs.readFileSync( @@ -1078,7 +1084,6 @@ test("validateConfigExampleJson", (t) => { // @ts-ignore tv4.validate(jsonObject, configSchemaStrict), fileJson + "\n" + JSON.stringify(tv4.error, null, 2)); - // Validate YAML const fileYaml = ".markdownlint.yaml"; const dataYaml = fs.readFileSync( @@ -1090,483 +1095,8 @@ test("validateConfigExampleJson", (t) => { "YAML example does not match JSON example."); }); -test.cb("configSingle", (t) => { - t.plan(2); - markdownlint.readConfig("./test/config/config-child.json", - function callback(err, actual) { - t.falsy(err); - const expected = require("./config/config-child.json"); - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configAbsolute", (t) => { - t.plan(2); - markdownlint.readConfig(path.join(__dirname, "config", "config-child.json"), - function callback(err, actual) { - t.falsy(err); - const expected = require("./config/config-child.json"); - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configMultiple", (t) => { - t.plan(2); - markdownlint.readConfig("./test/config/config-grandparent.json", - function callback(err, actual) { - t.falsy(err); - const expected = { - ...require("./config/config-child.json"), - ...require("./config/config-parent.json"), - ...require("./config/config-grandparent.json") - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configMultipleWithRequireResolve", (t) => { - t.plan(2); - markdownlint.readConfig("./test/config/config-packageparent.json", - function callback(err, actual) { - t.falsy(err); - const expected = { - ...require("./node_modules/pseudo-package/config-frompackage.json"), - ...require("./config/config-packageparent.json") - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configCustomFileSystem", (t) => { - t.plan(5); - const file = path.resolve("/dir/file.json"); - const extended = path.resolve("/dir/extended.json"); - const fileContent = { - "extends": extended, - "default": true, - "MD001": false - }; - const extendedContent = { - "MD001": true, - "MD002": true - }; - const fsApi = { - "access": (p, m, cb) => { - t.is(p, extended); - return (cb || m)(); - }, - "readFile": (p, o, cb) => { - switch (p) { - case file: - t.is(p, file); - return cb(null, JSON.stringify(fileContent)); - case extended: - t.is(p, extended); - return cb(null, JSON.stringify(extendedContent)); - default: - return t.fail(); - } - } - }; - markdownlint.readConfig( - file, - null, - fsApi, - function callback(err, actual) { - t.falsy(err); - const expected = { - ...extendedContent, - ...fileContent - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configBadFile", (t) => { - t.plan(4); - markdownlint.readConfig("./test/config/config-badfile.json", - function callback(err, result) { - t.truthy(err, "Did not get an error for bad file."); - t.true(err instanceof Error, "Error not instance of Error."); - // @ts-ignore - t.is(err.code, "ENOENT", "Error code for bad file not ENOENT."); - t.true(!result, "Got result for bad file."); - t.end(); - }); -}); - -test.cb("configBadChildFile", (t) => { - t.plan(4); - markdownlint.readConfig("./test/config/config-badchildfile.json", - function callback(err, result) { - t.truthy(err, "Did not get an error for bad child file."); - t.true(err instanceof Error, "Error not instance of Error."); - // @ts-ignore - t.is(err.code, "ENOENT", - "Error code for bad child file not ENOENT."); - t.true(!result, "Got result for bad child file."); - t.end(); - }); -}); - -test.cb("configBadChildPackage", (t) => { - t.plan(4); - markdownlint.readConfig("./test/config/config-badchildpackage.json", - function callback(err, result) { - t.truthy(err, "Did not get an error for bad child package."); - t.true(err instanceof Error, "Error not instance of Error."); - // @ts-ignore - t.is(err.code, "ENOENT", - "Error code for bad child package not ENOENT."); - t.true(!result, "Got result for bad child package."); - t.end(); - }); -}); - -test.cb("configBadJson", (t) => { - t.plan(3); - markdownlint.readConfig("./test/config/config-badjson.json", - function callback(err, result) { - t.truthy(err, "Did not get an error for bad JSON."); - t.true(err instanceof Error, "Error not instance of Error."); - t.true(!result, "Got result for bad JSON."); - t.end(); - }); -}); - -test.cb("configBadChildJson", (t) => { - t.plan(3); - markdownlint.readConfig("./test/config/config-badchildjson.json", - function callback(err, result) { - t.truthy(err, "Did not get an error for bad child JSON."); - t.true(err instanceof Error, "Error not instance of Error."); - t.true(!result, "Got result for bad child JSON."); - t.end(); - }); -}); - -test.cb("configSingleYaml", (t) => { - t.plan(2); - markdownlint.readConfig( - "./test/config/config-child.yaml", - // @ts-ignore - [ require("js-yaml").load ], - function callback(err, actual) { - t.falsy(err); - const expected = require("./config/config-child.json"); - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configMultipleYaml", (t) => { - t.plan(2); - markdownlint.readConfig( - "./test/config/config-grandparent.yaml", - // @ts-ignore - [ require("js-yaml").load ], - function callback(err, actual) { - t.falsy(err); - const expected = { - ...require("./config/config-child.json"), - ...require("./config/config-parent.json"), - ...require("./config/config-grandparent.json") - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configMultipleHybrid", (t) => { - t.plan(2); - markdownlint.readConfig( - "./test/config/config-grandparent-hybrid.yaml", - // @ts-ignore - [ JSON.parse, require("toml").parse, require("js-yaml").load ], - function callback(err, actual) { - t.falsy(err); - const expected = { - ...require("./config/config-child.json"), - ...require("./config/config-parent.json"), - ...require("./config/config-grandparent.json") - }; - delete expected.extends; - t.like(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configBadHybrid", (t) => { - t.plan(4); - markdownlint.readConfig( - "./test/config/config-badcontent.txt", - // @ts-ignore - [ JSON.parse, require("toml").parse, require("js-yaml").load ], - function callback(err, result) { - t.truthy(err, "Did not get an error for bad child JSON."); - t.true(err instanceof Error, "Error not instance of Error."); - t.truthy(err.message.match( - // eslint-disable-next-line max-len - /^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+;/ - ), "Error message unexpected."); - t.true(!result, "Got result for bad child JSON."); - t.end(); - }); -}); - -test("configSingleSync", (t) => { - t.plan(1); - const actual = markdownlint.readConfigSync("./test/config/config-child.json"); - const expected = require("./config/config-child.json"); - t.deepEqual(actual, expected, "Config object not correct."); -}); - -test("configAbsoluteSync", (t) => { - t.plan(1); - const actual = markdownlint.readConfigSync( - path.join(__dirname, "config", "config-child.json")); - const expected = require("./config/config-child.json"); - t.deepEqual(actual, expected, "Config object not correct."); -}); - -test("configMultipleSync", (t) => { - t.plan(1); - const actual = - markdownlint.readConfigSync("./test/config/config-grandparent.json"); - const expected = { - ...require("./config/config-child.json"), - ...require("./config/config-parent.json"), - ...require("./config/config-grandparent.json") - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); -}); - -test("configBadFileSync", (t) => { - t.plan(1); - t.throws( - function badFileCall() { - markdownlint.readConfigSync("./test/config/config-badfile.json"); - }, - { - "message": /ENOENT/ - }, - "Did not get correct exception for bad file." - ); -}); - -test("configBadChildFileSync", (t) => { - t.plan(1); - t.throws( - function badChildFileCall() { - markdownlint.readConfigSync("./test/config/config-badchildfile.json"); - }, - { - "message": /ENOENT/ - }, - "Did not get correct exception for bad child file." - ); -}); - -test("configBadJsonSync", (t) => { - t.plan(1); - t.throws( - function badJsonCall() { - markdownlint.readConfigSync("./test/config/config-badjson.json"); - }, - { - "message": - /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/ - }, - "Did not get correct exception for bad JSON." - ); -}); - -test("configBadChildJsonSync", (t) => { - t.plan(1); - t.throws( - function badChildJsonCall() { - markdownlint.readConfigSync("./test/config/config-badchildjson.json"); - }, - { - "message": - /Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+/ - }, - "Did not get correct exception for bad child JSON." - ); -}); - -test("configSingleYamlSync", (t) => { - t.plan(1); - const actual = markdownlint.readConfigSync( - // @ts-ignore - "./test/config/config-child.yaml", [ require("js-yaml").load ]); - const expected = require("./config/config-child.json"); - t.deepEqual(actual, expected, "Config object not correct."); -}); - -test("configMultipleYamlSync", (t) => { - t.plan(1); - const actual = markdownlint.readConfigSync( - // @ts-ignore - "./test/config/config-grandparent.yaml", [ require("js-yaml").load ]); - const expected = { - ...require("./config/config-child.json"), - ...require("./config/config-parent.json"), - ...require("./config/config-grandparent.json") - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); -}); - -test("configMultipleHybridSync", (t) => { - t.plan(1); - const actual = markdownlint.readConfigSync( - "./test/config/config-grandparent-hybrid.yaml", - // @ts-ignore - [ JSON.parse, require("toml").parse, require("js-yaml").load ]); - const expected = { - ...require("./config/config-child.json"), - ...require("./config/config-parent.json"), - ...require("./config/config-grandparent.json") - }; - delete expected.extends; - t.like(actual, expected, "Config object not correct."); -}); - -test("configCustomFileSystemSync", (t) => { - t.plan(4); - const file = path.resolve("/dir/file.json"); - const extended = path.resolve("/dir/extended.json"); - const fileContent = { - "extends": extended, - "default": true, - "MD001": false - }; - const extendedContent = { - "MD001": true, - "MD002": true - }; - const fsApi = { - "accessSync": (p) => { - t.is(p, extended); - }, - "readFileSync": (p) => { - switch (p) { - case file: - t.is(p, file); - return JSON.stringify(fileContent); - case extended: - t.is(p, extended); - return JSON.stringify(extendedContent); - default: - return t.fail(); - } - } - }; - const actual = markdownlint.readConfigSync(file, null, fsApi); - const expected = { - ...extendedContent, - ...fileContent - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); -}); - -test("configBadHybridSync", (t) => { - t.plan(1); - t.throws( - function badHybridCall() { - markdownlint.readConfigSync( - "./test/config/config-badcontent.txt", - // @ts-ignore - [ JSON.parse, require("toml").parse, require("js-yaml").load ]); - }, - { - // eslint-disable-next-line max-len - "message": /^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+;/ - }, - "Did not get correct exception for bad content." - ); -}); - -test.cb("configSinglePromise", (t) => { - t.plan(1); - markdownlint.promises.readConfig("./test/config/config-child.json") - .then((actual) => { - const expected = require("./config/config-child.json"); - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configCustomFileSystemPromise", (t) => { - t.plan(4); - const file = path.resolve("/dir/file.json"); - const extended = path.resolve("/dir/extended.json"); - const fileContent = { - "extends": extended, - "default": true, - "MD001": false - }; - const extendedContent = { - "MD001": true, - "MD002": true - }; - const fsApi = { - "access": (p, m, cb) => { - t.is(p, extended); - return (cb || m)(); - }, - "readFile": (p, o, cb) => { - switch (p) { - case file: - t.is(p, file); - return cb(null, JSON.stringify(fileContent)); - case extended: - t.is(p, extended); - return cb(null, JSON.stringify(extendedContent)); - default: - return t.fail(); - } - } - }; - markdownlint.promises.readConfig(file, null, fsApi) - .then((actual) => { - const expected = { - ...extendedContent, - ...fileContent - }; - delete expected.extends; - t.deepEqual(actual, expected, "Config object not correct."); - t.end(); - }); -}); - -test.cb("configBadFilePromise", (t) => { - t.plan(2); - markdownlint.promises.readConfig("./test/config/config-badfile.json") - .then( - null, - (error) => { - t.truthy(error, "Did not get an error for bad JSON."); - t.true(error instanceof Error, "Error not instance of Error."); - t.end(); - } - ); -}); - test("allBuiltInRulesHaveValidUrl", (t) => { - t.plan(132); + t.plan(138); rules.forEach(function forRule(rule) { t.truthy(rule.information); t.true(Object.getPrototypeOf(rule.information) === URL.prototype); @@ -1712,9 +1242,49 @@ test.cb("texmath test files with texmath plugin", (t) => { }); }); +test("token-map-spans", (t) => { + t.plan(38); + const options = { + "customRules": [ + { + "names": [ "token-map-spans" ], + "description": "token-map-spans", + "tags": [ "tms" ], + "function": function tokenMapSpans(params) { + const tokenLines = []; + let lastLineNumber = -1; + const inlines = params.tokens.filter((c) => c.type === "inline"); + for (const token of inlines) { + t.truthy(token.map); + for (let i = token.map[0]; i < token.map[1]; i++) { + if (tokenLines.includes(i)) { + t.true( + lastLineNumber === token.lineNumber, + `Line ${i + 1} is part of token maps from multiple lines.` + ); + } else { + tokenLines.push(i); + } + lastLineNumber = token.lineNumber; + } + } + } + } + ], + "files": [ "./test/token-map-spans.md" ] + }; + markdownlint.sync(options); +}); + test("getVersion", (t) => { t.plan(1); const actual = markdownlint.getVersion(); const expected = version; t.is(actual, expected, "Version string not correct."); }); + +test("constants", (t) => { + t.plan(2); + t.is(constants.homepage, homepage); + t.is(constants.version, version); +}); diff --git a/test/mixed-emphasis-markers.md b/test/mixed-emphasis-markers.md index 63131d67..92c22393 100644 --- a/test/mixed-emphasis-markers.md +++ b/test/mixed-emphasis-markers.md @@ -1,17 +1,19 @@ # Mixed Emphasis Markers -This paragraph *uses* both _kinds_ of emphasis marker. +This paragraph *uses* both _kinds_ of emphasis marker. {MD049} -This paragraph _uses_ both *kinds* of emphasis marker. +This paragraph _uses_ both *kinds* of emphasis marker. {MD049} -This paragraph *nests both _kinds_ of emphasis* marker. +This paragraph *nests both _kinds_ of emphasis* marker. {MD049} This paragraph *nests both __kinds__ of emphasis* marker. -This paragraph **nests both __kinds__ of emphasis** marker. +This paragraph **nests both __kinds__ of emphasis** marker. {MD050} -This paragraph _nests both *kinds* of emphasis_ marker. +This paragraph _nests both *kinds* of emphasis_ marker. {MD049} -This paragraph _nests both **kinds** of emphasis_ marker. +This paragraph _nests both **kinds** of emphasis_ marker. {MD049} {MD050} -This paragraph __nests both **kinds** of emphasis__ marker. +This paragraph __nests both **kinds** of emphasis__ marker. {MD050} + + diff --git a/test/proper-names-projects.md b/test/proper-names-projects.md index 0360e14c..c44c5cb0 100644 --- a/test/proper-names-projects.md +++ b/test/proper-names-projects.md @@ -40,7 +40,7 @@ Quoted "Vue" and "vue-router" Emphasized *Vue* and *vue-router* -Underscored _Vue_ and _vue-router_ +Underscored _Vue_ and _vue-router_ {MD049} Call it npm But not Npm {MD044} diff --git a/test/proper-names.md b/test/proper-names.md index 57f5a7c2..c9e9bc6d 100644 --- a/test/proper-names.md +++ b/test/proper-names.md @@ -6,8 +6,6 @@ Quoted "Markdownlint" {MD044} Emphasized *Markdownlint* {MD044} -Emphasized _Markdownlint_ {MD044} - JavaScript is a language JavaScript is not Java @@ -52,7 +50,7 @@ HTML javascript {MD033} {MD044} node.js is runtime {MD044} ```js -javascript is code {MD044} {MD046:54} +javascript is code {MD044} {MD046:52} node.js is runtime {MD044} ``` diff --git a/test/reversed-link-issue-with-markdownlint-12.md b/test/reversed-link-issue-with-markdownlint-12.md index 2383d467..be398a2b 100644 --- a/test/reversed-link-issue-with-markdownlint-12.md +++ b/test/reversed-link-issue-with-markdownlint-12.md @@ -2,7 +2,7 @@ |Pattern|Description| |-------------|-----------------| -|`(?:\["'\](?<1>\[^"'\]*)["']|(?<1>\S+))`|...| +|`(?:\["'\](?<1>\[^"'\]*)["']|(?<1>\S+))`|{MD011}| |Pattern|Description| |-------------|-----------------| diff --git a/test/rules/lint-javascript.js b/test/rules/lint-javascript.js index 441e1a6d..0c8f4d1f 100644 --- a/test/rules/lint-javascript.js +++ b/test/rules/lint-javascript.js @@ -4,7 +4,7 @@ const { filterTokens } = require("markdownlint-rule-helpers"); const eslint = require("eslint"); -const cliEngine = new eslint.CLIEngine({}); +const eslintInstance = new eslint.ESLint(); const linter = new eslint.Linter(); const languageJavaScript = /js|javascript/i; @@ -28,23 +28,27 @@ module.exports = { "names": [ "lint-javascript" ], "description": "Rule that lints JavaScript code", "tags": [ "test", "lint", "javascript" ], + "asynchronous": true, "function": (params, onError) => { filterTokens(params, "fence", (fence) => { if (languageJavaScript.test(fence.info)) { - let config = cliEngine.getConfigForFile(params.name); - config = cleanJsdocRulesFromEslintConfig(config); - const results = linter.verify(fence.content, config); - results.forEach((result) => { - const lineNumber = fence.lineNumber + result.line; - onError({ - "lineNumber": lineNumber, - "detail": result.message, - "context": params.lines[lineNumber - 1] + return eslintInstance.calculateConfigForFile(params.name) + .then((config) => { + config = cleanJsdocRulesFromEslintConfig(config); + const results = linter.verify(fence.content, config); + results.forEach((result) => { + const lineNumber = fence.lineNumber + result.line; + onError({ + "lineNumber": lineNumber, + "detail": result.message, + "context": params.lines[lineNumber - 1] + }); + }); }); - }); } + return Promise.resolve(); }); - // Unused: + // Unsupported: // filterTokens("code_block"), language unknown // filterTokens("code_inline"), too brief } diff --git a/test/spaces-inside-emphasis-markers-multiple-lines.md b/test/spaces-inside-emphasis-markers-multiple-lines.md index 18e43b1b..4c0b777a 100644 --- a/test/spaces-inside-emphasis-markers-multiple-lines.md +++ b/test/spaces-inside-emphasis-markers-multiple-lines.md @@ -100,7 +100,7 @@ code Mixed `code_span` scenarios -are _also_ okay. +are _also_ okay. {MD049} Mixed `code*span` scenarios diff --git a/test/spaces_inside_emphasis_markers.md b/test/spaces_inside_emphasis_markers.md index 6735a2c3..0e716a8e 100644 --- a/test/spaces_inside_emphasis_markers.md +++ b/test/spaces_inside_emphasis_markers.md @@ -1,5 +1,7 @@ # Heading + + Line with *Normal emphasis* Line with **Normal strong** @@ -345,3 +347,13 @@ text [reference*link] star * star text ```yaml /* autogenerated */ # YAML... ``` + +new_value from *old_value* and *older_value*. + +:ballot_box_with_check: _Emoji syntax_ + +some_snake_case_function() is _called_ + +_~/.ssh/id_rsa_ and _emphasis_ + +Partial *em*phasis of a *wo*rd. diff --git a/test/spaces_inside_link_text.md b/test/spaces_inside_link_text.md index 8e6baa15..84176ff4 100644 --- a/test/spaces_inside_link_text.md +++ b/test/spaces_inside_link_text.md @@ -50,3 +50,15 @@ Wrapped [ link with leading space ](https://example.com) {MD039} Non-wrapped [ link with leading space](https://example.com) {MD039} + +[][ref] + +[link][ref] + +[link ][ref] {MD039} + +[ link][ref] {MD039} + +[ link ][ref] {MD039} + +[ref]: https://example.com diff --git a/test/strong_style_asterisk.json b/test/strong_style_asterisk.json new file mode 100644 index 00000000..4a45038c --- /dev/null +++ b/test/strong_style_asterisk.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD050": { + "style": "asterisk" + } +} diff --git a/test/strong_style_asterisk.md b/test/strong_style_asterisk.md new file mode 100644 index 00000000..4004291d --- /dev/null +++ b/test/strong_style_asterisk.md @@ -0,0 +1,5 @@ +# Strong style asterisk + +This is **fine** + +This is __not__ {MD050} diff --git a/test/strong_style_underscore.json b/test/strong_style_underscore.json new file mode 100644 index 00000000..494d8f8f --- /dev/null +++ b/test/strong_style_underscore.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD050": { + "style": "underscore" + } +} diff --git a/test/strong_style_underscore.md b/test/strong_style_underscore.md new file mode 100644 index 00000000..83800344 --- /dev/null +++ b/test/strong_style_underscore.md @@ -0,0 +1,5 @@ +# Strong style underscore + +This is __fine__ + +This is **not** {MD050} diff --git a/test/table-content-with-issues.md b/test/table-content-with-issues.md new file mode 100644 index 00000000..c1fbf0af --- /dev/null +++ b/test/table-content-with-issues.md @@ -0,0 +1,13 @@ +# Table Content With Issues + +| Content | Issue | +|------------------------------|---------| +| Text | N/A | +| (link)[https://example.com] | {MD011} | +|
| {MD033} | +| https://example.com | {MD034} | +| * emphasis* | {MD037} | +| __strong __ | {MD037} | +| ` code` | {MD038} | +| [link ](https://example.com) | {MD039} | +| [link]() | {MD042} | diff --git a/test/token-map-spans.md b/test/token-map-spans.md new file mode 100644 index 00000000..f54fac0e --- /dev/null +++ b/test/token-map-spans.md @@ -0,0 +1,42 @@ +# Token Map Spans + +Text *emphasis* text __strong__ text `code` text [link](https://example.com). + +Paragraph with *emphasis +spanning lines* and __strong +spanning lines__ and `code +spanning lines` and [link +spanning lines](https://example.com). + +> Blockquote +> [link](https://example.com) +> > Nested +> > blockquote +> > [link](https://example.com) + +Heading +------- + +```lang +Fenced +code +``` + + Indented + code + +1. List +2. List + - Sub-list + - Sub-list +3. List + +| Table | Column 1 | Column 2 | Column 3 | Column 4 | +|-------|------------|------------|----------|----------------------------| +| Text | *emphasis* | __strong__ | `code` | [link](https://example.com) | +| Text | *emphasis* | __strong__ | `code` | [link](https://example.com) | + +