commit 721c363001b06345d11f6763aed8e333c91e8e57 Author: Titus Wormer Date: Tue Jun 2 08:34:14 2015 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8b881f0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{json,mdastrc}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e69de29 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..7586723 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,9 @@ +{ + "env": { + "node": true, + "browser": true + }, + "rules": { + "quotes": [2, "single"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..806800e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +*.log +bower_components/ +build/ +components/ +node_modules/ +coverage/ +build.js diff --git a/.jscs.json b/.jscs.json new file mode 100644 index 0000000..7c12a20 --- /dev/null +++ b/.jscs.json @@ -0,0 +1,140 @@ +{ + "plugins": [ + "jscs-jsdoc" + ], + "jsDoc": { + "checkAnnotations": "jsdoc3", + "checkParamNames": true, + "requireParamTypes": true, + "checkRedundantParams": true, + "checkReturnTypes": true, + "checkRedundantReturns": true, + "requireReturnTypes": true, + "checkTypes": "strictNativeCase", + "checkRedundantAccess": true, + "enforceExistence": true, + "requireHyphenBeforeDescription": true + }, + "requireCurlyBraces": [ + "if", + "else", + "for", + "while", + "do", + "try", + "catch" + ], + "requireSpaceAfterKeywords": [ + "if", + "else", + "for", + "while", + "do", + "switch", + "return", + "try", + "catch" + ], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "requireSpacesInConditionalExpression": true, + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true, + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInNamedFunctionExpression": { + "beforeOpeningRoundBrace": true, + "beforeOpeningCurlyBrace": true + }, + "requireBlocksOnNewline": true, + "disallowEmptyBlocks": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideParentheses": true, + "requireSpacesInsideObjectBrackets": "all", + "disallowDanglingUnderscores": true, + "disallowSpaceAfterObjectKeys": true, + "requireCommaBeforeLineBreak": true, + "requireOperatorBeforeLineBreak": [ + "?", + "+", + "-", + "/", + "*", + "=", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + "requireSpaceBeforeBinaryOperators": [ + "+", + "-", + "/", + "*", + "=", + "==", + "===", + "!=", + "!==" + ], + "requireSpaceAfterBinaryOperators": [ + "+", + "-", + "/", + "*", + "=", + "==", + "===", + "!=", + "!==" + ], + "disallowSpaceAfterPrefixUnaryOperators": [ + "++", + "--", + "+", + "-", + "~", + "!" + ], + "disallowSpaceBeforePostfixUnaryOperators": [ + "++", + "--" + ], + "disallowImplicitTypeConversion": [ + "numeric", + "boolean", + "binary", + "string" + ], + "requireCamelCaseOrUpperCaseIdentifiers": true, + "disallowKeywords": [ + "with" + ], + "disallowMultipleLineStrings": true, + "validateLineBreaks": "LF", + "validateQuoteMarks": "'", + "disallowMixedSpacesAndTabs": true, + "disallowTrailingWhitespace": true, + "disallowTrailingComma": true, + "disallowKeywordsOnNewLine": [ + "else" + ], + "requireLineFeedAtFileEnd": true, + "requireCapitalizedConstructors": true, + "safeContextKeyword": "self", + "requireDotNotation": true, + "disallowYodaConditions": true, + "validateJSDoc": { + "checkParamNames": true, + "checkRedundantParams": true, + "requireParamTypes": true + } +} diff --git a/.mdastignore b/.mdastignore new file mode 100644 index 0000000..0374f3c --- /dev/null +++ b/.mdastignore @@ -0,0 +1,7 @@ +# `node_modules/` is already ignored. + +# Duo’s components include docs. +components + +# Do not process fixtures. +test diff --git a/.mdastrc b/.mdastrc new file mode 100644 index 0000000..ec17381 --- /dev/null +++ b/.mdastrc @@ -0,0 +1,10 @@ +{ + "plugins": [ + "./", + "github", + "toc" + ], + "settings": { + "bullet": "*" + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ccc9e8c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +script: npm run-script test-travis +node_js: +- '0.10' +- '0.11' +- '0.12' +- iojs +after_script: npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls +sudo: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fbcbb18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..78ac48f --- /dev/null +++ b/bower.json @@ -0,0 +1,37 @@ +{ + "name": "mdast-lint", + "main": "mdast-lint.js", + "description": "Lint markdown with mdast", + "license": "MIT", + "keywords": [ + "markdown", + "lint", + "validate", + "mdast" + ], + "repository": { + "type": "git", + "url": "https://github.com/wooorm/mdast-lint.git" + }, + "authors": [ + "Titus Wormer " + ], + "ignore": [ + ".*", + "*.log", + "*.png", + "*.svg", + "*.md", + "build/", + "components/", + "coverage/", + "node_modules/", + "script/", + "test/", + "build.js", + "example.js", + "index.js", + "component.json", + "package.json" + ] +} diff --git a/component.json b/component.json new file mode 100644 index 0000000..33a274a --- /dev/null +++ b/component.json @@ -0,0 +1,21 @@ +{ + "name": "mdast-lint", + "version": "0.0.0", + "description": "Lint markdown with mdast", + "license": "MIT", + "keywords": [ + "markdown", + "lint", + "validate", + "mdast" + ], + "repository": "wooorm/mdast-lint", + "dependencies": { + "wooorm/mdast-range": "^0.4.0", + "wooorm/mdast-zone": "^0.2.1" + }, + "scripts": [ + "index.js", + "lib/**/*.js" + ] +} diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..5c7e6ba --- /dev/null +++ b/doc/api.md @@ -0,0 +1,104 @@ +# API + +## Usage + +Dependencies: + +```javascript +var mdast = require('mdast'); +var lint = require('mdast-lint'); +var processor = mdast().use(lint); +``` + +Example document. + +```javascript +var doc = '* Hello\n' + + '\n' + + '- World\n'; +``` + +Process. + +```javascript +processor.process(doc, function (err, res, file) { + var messages = file.messages; + /** + * Yields: + * [ + * { + * "name": "1:3-1:8", + * "file": "", + * "reason": "Incorrect list-item content indent: add 2 spaces", + * "line": 1, + * "column": 3, + * "fatal": false, + * "ruleId": "list-item-indent" + * }, + * { + * "name": "3:1-3:10", + * "file": "", + * "reason": "Invalid ordered list item marker: should be `*`", + * "line": 3, + * "column": 1, + * "fatal": false, + * "ruleId": "unordered-list-marker-style" + * } + * ] + */ +}); +``` + +## [mdast](https://github.com/wooorm/mdast#api).[use](https://github.com/wooorm/mdast#mdastuseplugin-options)(lint, options) + +Adds warnings for style violations to a given [virtual file](https://github.com/wooorm/mdast/blob/master/doc/mdast.3.md#file) +using mdast’s [warning API](https://github.com/wooorm/mdast/blob/master/doc/mdast.3.md#filewarnreason-position). + +When processing a file, these warnings are available at `file.messages`, and +look as follows: + +```json +{ + "fatal": false, + "reason": "Marker style should be `*`", + "file": "~/example.md", + "line": 3, + "column": 1, + "ruleId": "unordered-list-marker-style" +} +``` + +These messages comply to two schema’s. First, they’re valid error objects, +so they can be thrown (while still looking :ok_hand:, due to `Error#toString()` +magic), secondly the `file` object itself can be passed to ESLint’s formatters: + +```javascript +var compact = require('eslint/lib/formatters/compact'); +var mdast = require('mdast'); +var lint = require('mdast-lint'); + +// For example purposes were processing `example.md` as seen above. +mdast().use(lint).process(doc, function (err, res, file) { + compact([file]); + /** + * line 3, col 1, Warning - Invalid ordered list item marker: should be `*` + * + * 1 problem + */ +}); +``` + +Each message contains the following properties: + +* `fatal` (`boolean?`) — Always `false`; + +* `reason` (`string`) — Warning message; + +* `line` (`number`) — Starting line (1-based); + +* `column` (`number`) — Starting column of exception (1-based). + +* `file` (`string`) — File path of exception. + +* `ruleId` (`string`) — Name of the rule which triggered this warning, + useful when looking for how to turn the darn thing off. diff --git a/doc/comparison/markdownlint.md b/doc/comparison/markdownlint.md new file mode 100644 index 0000000..7ee611c --- /dev/null +++ b/doc/comparison/markdownlint.md @@ -0,0 +1,45 @@ +# [markdownlint](https://github.com/mivok/markdownlint) + +This table documents the similarity and difference between +[**markdownlint**](https://github.com/mivok/markdownlint/blob/master/docs/RULES.md) +rules and **mdast-lint**’s rules. + +| markdownlint | mdast | note | +| ------------ | ----------------------------- | ---------------------------------------------------------------------------------------- | +| MD001 | heading-increment | | +| MD002 | first-heading-level | | +| MD003 | heading-style | | +| MD004 | unordered-list-marker-style | | +| MD005 | - | mixture of `list-item-indent`, `list-item-bullet-indent`, and `list-item-content-indent` | +| MD006 | list-item-bullet-indent | | +| MD007 | list-item-bullet-indent | | +| MD009 | - | Partially by hard-break-spaces | +| MD010 | no-tabs | | +| MD011 | no-shortcut-reference-link | Although a different message, this will lead you in the right direction | +| MD012 | no-consecutive-blank-lines | | +| MD013 | maximum-line-length | | +| MD014 | no-shell-dollars | | +| MD018 | no-heading-content-indent | Only works in pedantic mode | +| MD019 | no-heading-content-indent | | +| MD020 | no-heading-content-indent | Only works in pedantic mode | +| MD021 | no-heading-content-indent | | +| MD022 | no-missing-blank-lines | | +| MD023 | no-heading-indent | | +| MD024 | no-duplicate-headings | | +| MD025 | no-multiple-toplevel-headings | | +| MD026 | no-heading-punctuation | | +| MD027 | blockquote-indentation | Won’t warn when that’s your preferred, consistent style | +| MD028 | no-blockquote-without-caret | | +| MD029 | ordered-list-marker-value | markdownlint defaults to `one`, whereas mdast-lint defaults to `ordered` | +| MD030 | list-item-indent | | +| MD031 | no-missing-blank-lines | | +| MD032 | no-missing-blank-lines | | +| MD033 | no-html | | +| MD034 | no-literal-urls | | +| MD035 | rule-style | | +| MD036 | emphasis-heading | mdast-lint only warns when the emphasis is followed by a colon, but that might change. | +| MD037 | no-inline-padding | | +| MD038 | no-inline-padding | | +| MD039 | no-inline-padding | | +| MD039 | no-inline-padding | | +| MD040 | fenced-code-flag | | diff --git a/doc/rules.md b/doc/rules.md new file mode 100644 index 0000000..084eb64 --- /dev/null +++ b/doc/rules.md @@ -0,0 +1,1142 @@ +# List of Rules + +This document describes all available rules, what they +check for, examples of what they warn for, and how to +fix their warnings. + +## Table of Contents + +* [Rules](#rules) + + * [blockquote-indentation](#blockquote-indentation) + * [code-block-style](#code-block-style) + * [definition-case](#definition-case) + * [definition-spacing](#definition-spacing) + * [emphasis-marker](#emphasis-marker) + * [fenced-code-flag](#fenced-code-flag) + * [fenced-code-marker](#fenced-code-marker) + * [file-extension](#file-extension) + * [final-definition](#final-definition) + * [final-newline](#final-newline) + * [first-heading-level](#first-heading-level) + * [hard-break-spaces](#hard-break-spaces) + * [heading-increment](#heading-increment) + * [heading-style](#heading-style) + * [link-title-style](#link-title-style) + * [list-item-bullet-indent](#list-item-bullet-indent) + * [list-item-content-indent](#list-item-content-indent) + * [list-item-indent](#list-item-indent) + * [list-item-spacing](#list-item-spacing) + * [maximum-heading-length](#maximum-heading-length) + * [maximum-line-length](#maximum-line-length) + * [no-auto-link-without-protocol](#no-auto-link-without-protocol) + * [no-blockquote-without-caret](#no-blockquote-without-caret) + * [no-consecutive-blank-lines](#no-consecutive-blank-lines) + * [no-duplicate-definitions](#no-duplicate-definitions) + * [no-duplicate-headings](#no-duplicate-headings) + * [no-emphasis-as-heading](#no-emphasis-as-heading) + * [no-file-name-articles](#no-file-name-articles) + * [no-file-name-consecutive-dashes](#no-file-name-consecutive-dashes) + * [no-file-name-irregular-characters](#no-file-name-irregular-characters) + * [no-file-name-mixed-case](#no-file-name-mixed-case) + * [no-file-name-outer-dashes](#no-file-name-outer-dashes) + * [no-heading-content-indent](#no-heading-content-indent) + * [no-heading-indent](#no-heading-indent) + * [no-heading-punctuation](#no-heading-punctuation) + * [no-html](#no-html) + * [no-inline-padding](#no-inline-padding) + * [no-literal-urls](#no-literal-urls) + * [no-missing-blank-lines](#no-missing-blank-lines) + * [no-multiple-toplevel-headings](#no-multiple-toplevel-headings) + * [no-shell-dollars](#no-shell-dollars) + * [no-shortcut-reference-image](#no-shortcut-reference-image) + * [no-shortcut-reference-link](#no-shortcut-reference-link) + * [no-table-indentation](#no-table-indentation) + * [no-tabs](#no-tabs) + * [ordered-list-marker-style](#ordered-list-marker-style) + * [ordered-list-marker-value](#ordered-list-marker-value) + * [rule-style](#rule-style) + * [strong-marker](#strong-marker) + * [table-cell-padding](#table-cell-padding) + * [table-pipe-alignment](#table-pipe-alignment) + * [table-pipes](#table-pipes) + * [unordered-list-marker-style](#unordered-list-marker-style) + +## Rules + +Remember that rules can always be turned off by +passing false. In addition, when reset is given, values can +be null or undefined in order to be ignored. + +### blockquote-indentation + +```md + + > Hello + ... + > World + + + > Hello + ... + > World + + + > Hello + ... + > World +``` + + Warn when blockquotes are either indented too much or too little. + + Options: `number`, default: `'consistent'`. + + The default value, `consistent`, detects the first used indentation + and will warn when other blockquotes use a different indentation. + +### code-block-style + +````md + + Hello + + ... + + World + + + ``` + Hello + ``` + ... + ```bar + World + ``` + + + Hello + ... + ``` + World + ``` +```` + + Warn when code-blocks do not adhere to a given style. + + Options: `string`, either `'consistent'`, `'fences'`, or `'indented'`, + default: `'consistent'`. + + The default value, `consistent`, detects the first used code-block + style, and will warn when a subsequent code-block uses a different + style. + +### definition-case + +```md + + [example] http://example.com "Example Domain" + + + ![Example] http://example.com/favicon.ico "Example image" +``` + + Warn when definition labels are not lower-case. + +### definition-spacing + +```md + + [example domain] http://example.com "Example Domain" + + + ![example image] http://example.com/favicon.ico "Example image" +``` + + Warn when consecutive white space is used in a definition. + +### emphasis-marker + +```md + + *foo* + *bar* + + + _foo_ + _bar_ +``` + + Warn for violating emphasis markers. + + Options: `string`, either `'consistent'`, `'*'`, or `'_'`, + default: `'consistent'`. + + The default value, `consistent`, detects the first used emphasis + style, and will warn when a subsequent emphasis uses a different + style. + +### fenced-code-flag + +````md + + ```hello + world(); + ``` + + + Hello + + + ``` + world(); + ``` + + + ``` + world(); + ``` + + + ```hello + world(); + ``` +```` + + Warn when fenced code blocks occur without language flag. + + Options: `Array.` or `Object`. + + Providing an array, is a shortcut for just providing the `flags` + property on the object. + + The object can have an array of flags which are deemed valid. + In addition it can have the property `allowEmpty` (`boolean`) + which signifies whether or not to warn for fenced code-blocks without + languge flags. + +### fenced-code-marker + +````md + + ```foo + bar(); + ``` + + ``` + baz(); + ``` + + + ~~~foo + bar(); + ~~~ + + ~~~ + baz(); + ~~~ + + + ~~~foo + bar(); + ~~~ + + ``` + baz(); + ``` +```` + + Warn for violating fenced code markers. + + Options: `string`, either ``'`'``, or `'~'`, default: `'consistent'`. + + The default value, `consistent`, detects the first used fenced code + marker style, and will warn when a subsequent fenced code uses a + different style. + +### file-extension + +```md + Invalid (when `'md'`): readme.mkd, readme.markdown, etc. + Valid (when `'md'`): readme, readme.md +``` + + Warn when the document’s extension differs from the given preferred + extension. + + Does not warn when given documents have no file extensions (such as + `AUTHORS` or `LICENSE`). + + Options: `string`, default: `'md'` — Expected file extension. + +### final-definition + +```md + + ... + + [example] http://example.com "Example Domain" + + + ... + + [example] http://example.com "Example Domain" + + A trailing paragraph. +``` + + Warn when definitions are not placed at the end of the file. + +### final-newline + + Warn when a newline at the end of a file is missing. + + See [StackExchange](http://unix.stackexchange.com/questions/18743) for + why. + +### first-heading-level + +```md + + # Foo + + ## Bar + + + ## Foo + + # Bar +``` + + Warn when the first heading has a level other than `1`. + +### hard-break-spaces + +```md + + + + Lorem ipsum·· + dolor sit amet + + + Lorem ipsum··· + dolor sit amet. +``` + + Warn when too many spaces are used to create a hard break. + +### heading-increment + +```md + + # Foo + + ## Bar + + + # Foo + + ### Bar +``` + + Warn when headings increment with more than 1 level at a time. + +### heading-style + +```md + + # Foo + + ## Bar + + ### Baz + + + # Foo # + + ## Bar # + + ### Baz ### + + + Foo + === + + Bar + --- + + ### Baz + + + Foo + === + + ## Bar + + ### Baz ### +``` + + Warn when a heading does not conform to a given style. + + Options: `string`, either `'consistent'`, `'atx'`, `'atx-closed'`, + or `'setext'`, default: `'consistent'`. + + The default value, `consistent`, detects the first used heading + style, and will warn when a subsequent heading uses a different + style. + +### link-title-style + +```md + + [Example](http://example.com "Example Domain") + [Example](http://example.com "Example Domain") + + + [Example](http://example.com 'Example Domain') + [Example](http://example.com 'Example Domain') + + + [Example](http://example.com (Example Domain)) + [Example](http://example.com (Example Domain)) + + + [Example](http://example.com "Example Domain") + [Example](http://example.com 'Example Domain') + [Example](http://example.com (Example Domain)) +``` + + Warn when link and definition titles occur with incorrect quotes. + + Options: `string`, either `'consistent'`, `'"'`, `'\''`, or + `'()'`, default: `'consistent'`. + + The default value, `consistent`, detects the first used quote + style, and will warn when a subsequent titles use a different + style. + +### list-item-bullet-indent + +```md + + * List item + * List item + + + * List item + * List item +``` + + Warn when list item bullets are indented. + +### list-item-content-indent + +```md + + * List item + + * Nested list item indented by 4 spaces + + + * List item + + * Nested list item indented by 3 spaces +``` + + Warn when the content of a list item has mixed indentation. + +### list-item-indent + +```md + + * List + item. + + 11. List + item. + + + * List item. + + 11. List item + + * List + item. + + 11. List + item. + + + * List item. + + 11. List item + + * List + item. + + 11. List + item. +``` + + Warn when the spacing between a list item’s bullet and its content + violates a given style. + + Options: `string`, either `'tab-size'`, `'mixed'`, or `'space'`, + default: `'tab-size'`. + +### list-item-spacing + +```md + + - Wrapped + item + + - item 2 + + - item 3 + + + - item 1 + - item 2 + - item 3 + + + - Wrapped + item + - item 2 + - item 3 + + + - item 1 + + - item 2 + + - item 3 +``` + + Warn when list looseness is incorrect, such as being tight + when it should be loose, and vice versa. + +### maximum-heading-length + +```md + + # Alpha bravo charlie delta echo + # ![Alpha bravo charlie delta echo](http://example.com/nato.png) + + + # Alpha bravo charlie delta echo foxtrot +``` + + Warn when headings are too long. + + Options: `number`, default: `60`. + + Ignores markdown syntax, only checks the plain text content. + +### maximum-line-length + +```md + + Alpha bravo charlie delta echo. + + Alpha bravo charlie delta echo [foxtrot](./foxtrot.html). + + # Alpha bravo charlie delta echo foxtrot golf hotel. + + # Alpha bravo charlie delta echo foxtrot golf hotel. + + | A | B | C | D | E | F | F | H | + | ----- | ----- | ------- | ----- | ---- | ------- | ---- | ----- | + | Alpha | bravo | charlie | delta | echo | foxtrot | golf | hotel | + + + Alpha bravo charlie delta echo foxtrot golf. + + Alpha bravo charlie delta echo [foxtrot](./foxtrot.html) golf. +``` + + Warn when lines are too long. + + Options: `number`, default: `80`. + + Ignores nodes which cannot be wrapped, such as heasings, tables, + code, and links. + +### no-auto-link-without-protocol + +```md + + + + + + + +``` + + Warn for angle-bracketed links without protocol. + +### no-blockquote-without-caret + +```md + + > Foo... + > + > ...Bar. + + + > Foo... + + > ...Bar. +``` + + Warn when blank lines without carets are found in a blockquote. + +### no-consecutive-blank-lines + +```md + + Foo... + + ...Bar. + + + Foo... + + + ...Bar. +``` + + Warn for too many consecutive blank lines. Knows about the extra line + needed between a list and indented code, and two lists. + +### no-duplicate-definitions + +```md + + [foo]: bar + [baz]: qux + + + [foo]: bar + [foo]: qux +``` + + Warn when duplicate definitions are found. + +### no-duplicate-headings + +```md + + # Foo + + ## Bar + + + # Foo + + ## Foo + + ## [Foo](http://foo.com/bar) +``` + + Warn when duplicate headings are found. + +### no-emphasis-as-heading + +```md + + # Foo: + + Bar. + + + *Foo:* + + Bar. +``` + + Warn when emphasis (including strong), instead of a heading, introduces + a paragraph. + + Currently, only warns when a colon (`:`) is also included, maybe that + could be omitted. + +### no-file-name-articles + +```md + Valid: article.md + Invalid: an-article.md, a-article.md, , the-article.md +``` + + Warn when file name start with an article. + +### no-file-name-consecutive-dashes + +```md + Invalid: docs/plug--ins.md + Valid: docs/plug-ins.md +``` + + Warn when file names contain consecutive dashes. + +### no-file-name-irregular-characters + +```md + Invalid: plug_ins.md, plug ins.md. + Valid: plug-ins.md, plugins.md. +``` + + Warn when file names contain irregular characters: characters other + than alpha-numericals, dashes, and dots (full-stops). + +### no-file-name-mixed-case + +```md + Invalid: Readme.md + Valid: README.md, readme.md +``` + + Warn when a file name uses mixed case: both upper- and lower case + characters. + +### no-file-name-outer-dashes + +```md + Invalid: -readme.md, readme-.md + Valid: readme.md +``` + + Warn when file names contain initial or final dashes. + +### no-heading-content-indent + +```md + + + #··Foo + + ## Bar··## + + ##··Baz + + + #·Foo + + ## Bar·## + + ##·Baz +``` + + Warn when a heading’s content is indented. + +### no-heading-indent + +```md + + + ···# Hello world + + ·Foo + ----- + + ·# Hello world # + + ···Bar + ===== + + + # Hello world + + Foo + ----- + + # Hello world # + + Bar + ===== +``` + + Warn when a heading is indented. + +### no-heading-punctuation + +```md + + # Hello: + + # Hello? + + # Hello! + + # Hello, + + # Hello; + + + # Hello +``` + + Warn when a heading ends with a a group of characters. + Defaults to `'.,;:!?'`. + + Options: `string`, default: `'.,;:!?'`. + + Note that these are added to a regex, in a group (`'[' + char + ']'`), + be careful for escapes and dashes. + +### no-html + +```md + +

Hello

+ + + # Hello +``` + + Warn when HTML nodes are used. + + Ignores comments, because they are used by this tool, mdast, and + because markdown doesn’t have native comments. + +### no-inline-padding + +```md + + * Hello *, [ world ](http://foo.bar/baz) + + + *Hello*, [world](http://foo.bar/baz) +``` + + Warn when inline nodes are padded with spaces between markers and + content. + + Warns for emphasis, strong, delete, image, and link. + +### no-literal-urls + +```md + + http://foo.bar/baz + + + +``` + + Warn when URLs without angle-brackets are used. + +### no-missing-blank-lines + +```md + + # Foo + ## Bar + + + # Foo + + ## Bar +``` + + Warn for missing blank lines before a block node. + +### no-multiple-toplevel-headings + +```md + + # Foo + + # Bar + + + # Foo + + ## Bar +``` + + Warn when multiple top-level headings are used. + +### no-shell-dollars + +```md + + $ echo a + $ echo a > file + + + echo a + echo a > file + + + $ echo a + a + $ echo a > file +``` + + Warn when shell code is prefixed by dollar-characters. + +### no-shortcut-reference-image + +```md + + ![foo] + + [foo]: http://foo.bar/baz.png + + + ![foo][] + + [foo]: http://foo.bar/baz.png +``` + + Warn when shortcut reference images are used. + +### no-shortcut-reference-link + +```md + + [foo] + + [foo]: http://foo.bar/baz + + + [foo][] + + [foo]: http://foo.bar/baz +``` + + Warn when shortcut reference links are used. + +### no-table-indentation + +```md + + | A | B | + | ----- | ----- | + | Alpha | Bravo | + + + | A | B | + | ----- | ----- | + | Alpha | Bravo | +``` + + Warn when tables are indented. + +### no-tabs + +```md + + + Foo»Bar + + »···Foo + + + Foo Bar + + Foo +``` + + Warn when hard-tabs instead of spaces + +### ordered-list-marker-style + +```md + + 1. Foo + + 2. Bar + + + 1) Foo + + 2) Bar +``` + + Warn when the list-item marker style of ordered lists violate a given + style. + + Options: `string`, either `'consistent'`, `'.'`, or `')'`, + default: `'consistent'`. + + Note that `)` is only supported in CommonMark. + + The default value, `consistent`, detects the first used list + style, and will warn when a subsequent list uses a different + style. + +### ordered-list-marker-value + +```md + + 1. Foo + 1. Bar + 1. Baz + + 1. Alpha + 1. Bravo + 1. Charlie + + + 1. Foo + 1. Bar + 1. Baz + + 3. Alpha + 3. Bravo + 3. Charlie + + + 1. Foo + 2. Bar + 3. Baz + + 3. Alpha + 4. Bravo + 5. Charlie +``` + + Warn when the list-item marker values of ordered lists violate a + given style. + + Options: `string`, either `'single'`, `'one'`, or `'ordered'`, + default: `'ordered'`. + + When set to `'ordered'`, list-item bullets should increment by one, + relative to the starting point. When set to `'single'`, bullets should + be the same as the relative starting point. When set to `'one'`, bullets + should always be `1`. + +### rule-style + +```md + + * * * + + * * * + + + _______ + + _______ +``` + + Warn when the horizontal rules violate a given or detected style. + + Options: `string`, either a valid markdown rule, or `consistent`, + default: `'consistent'`. + +### strong-marker + +```md + + **foo** + **bar** + + + __foo__ + __bar__ +``` + + Warn for violating strong markers. + + Options: `string`, either `'consistent'`, `'*'`, or `'_'`, + default: `'consistent'`. + + The default value, `consistent`, detects the first used strong + style, and will warn when a subsequent strong uses a different + style. + +### table-cell-padding + +```md + + | A | B | + | ----- | ----- | + | Alpha | Bravo | + + + |A |B | + |-----|-----| + |Alpha|Bravo| + + + | A | B | + | -----| -----| + | Alpha| Bravo| +``` + + Warn when table cells are incorrectly padded. + + Options: `string`, either `'consistent'`, `'padded'`, or `'compact'`, + default: `'consistent'`. + + The default value, `consistent`, detects the first used cell padding + style, and will warn when a subsequent cells uses a different + style. + +### table-pipe-alignment + +```md + + | A | B | + | ----- | ----- | + | Alpha | Bravo | + + + | A | B | + | -- | -- | + | Alpha | Bravo | +``` + + Warn when table pipes are not aligned. + +### table-pipes + +```md + + | A | B | + | ----- | ----- | + | Alpha | Bravo | + + + A | B + ----- | ----- + Alpha | Bravo +``` + + Warn when table rows are not fenced with pipes. + +### unordered-list-marker-style + +```md + + - Foo + - Bar + + + * Foo + * Bar + + + + Foo + + Bar + + + + Foo + - Bar +``` + + Warn when the list-item marker style of unordered lists violate a given + style. + + Options: `string`, either `'consistent'`, `'-'`, `'*'`, or `'*'`, + default: `'consistent'`. + + The default value, `consistent`, detects the first used list + style, and will warn when a subsequent list uses a different + style. diff --git a/example.js b/example.js new file mode 100644 index 0000000..6c51aec --- /dev/null +++ b/example.js @@ -0,0 +1,16 @@ +var mdast = require('mdast'); +var lint = require('./index.js'); + +var processor = mdast().use(lint); + +// Example document. +var doc = '* Hello\n' + + '\n' + + '- World\n'; + +// Process. +processor.process(doc, function (err, res, file) { + // Yields: + var messages = file.messages; + console.log('json', JSON.stringify(messages, 0, 2)); +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..288ce9a --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./lib'); diff --git a/lib/filter.js b/lib/filter.js new file mode 100644 index 0000000..a0c2493 --- /dev/null +++ b/lib/filter.js @@ -0,0 +1,72 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Filter + * @fileoverview mdast plug-in used internally by + * mdast-lint to filter ruleId’s by enabled and disabled + * ranges. + * @todo Externalise into its own repository. + */ + +'use strict'; + +/** + * Sort all `file`s messages by line/column. Note that + * this works as a plugin, and will also sort warnings + * added by other plug-ins before `mdast-lint` was added. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + */ +function transformer(ast, file) { + if (!file || !file.messages || !file.messages.length) { + return; + } + + file.messages = file.messages.filter(function (message) { + var ranges = file.lintRanges[message.ruleId]; + var index = ranges && ranges.length; + var length = -1; + var range; + + if (!message.line) { + message.line = 1; + } + + if (!message.column) { + message.column = 1; + } + + while (--index > length) { + range = ranges[index]; + + if ( + range.position.line < message.line || + ( + range.position.line === message.line && + range.position.column < message.column + ) + ) { + return range.state === true; + } + } + + /* xistanbul ignore next - Just to be safe */ + return true; + }); +} + +/** + * Return `transformer`. + * + * @return {Function} - See `transformer`. + */ +function attacher() { + return transformer; +} + +/* + * Expose. + */ + +module.exports = attacher; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..21f2146 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,243 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Lint + * @fileoverview mdast plug-in providing warnings when + * detecting style violations. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var range = require('mdast-range'); +var zone = require('mdast-zone'); +var rules = require('./rules'); +var sort = require('./sort'); +var filter = require('./filter'); + +/** + * Factory to create a plugin from a rule. + * + * @example + * attachFactory('foo', console.log, false)() // null + * attachFactory('foo', console.log, {})() // plugin + * + * @param {string} id - Identifier. + * @param {Function} rule - Rule + * @param {*} options - Options for respective rule. + * @return {Function} - See `attach` below. + */ +function attachFactory(id, rule, options) { + /** + * Attach the rule to an mdast instance, unless `false` + * is passed as an option. + * + * @return {Function?} - See `plugin` below. + */ + function attach() { + /** + * Attach the rule to an mdast instance, unless `false` + * is passed as an option. + * + * @param {Node} ast - Root node. + * @param {File} [file] - Virtual file. + * @param {Function} next - Signal end. + */ + function plugin(ast, file, next) { + /* + * Track new messages per file. + */ + + if (file.lintIndex === undefined || file.lintIndex === null) { + file.lintIndex = file.messages.length; + } + + /** + * Add `ruleId` to each new message. + * + * @param {Error?} err - Optional failure. + */ + function done(err) { + var messages = file.messages; + + while (file.lintIndex < messages.length) { + messages[file.lintIndex].ruleId = id; + + file.lintIndex++; + } + + next(err); + } + + /* + * Invoke `rule`, with `options` + */ + + rule(ast, file, options, done); + } + + return options === false ? null : plugin; + } + + return attach; +} + +/** + * Lint attacher. + * + * By default, all rules are turned on unless explicitly + * set to `false`. When `reset: true`, the opposite is + * true: all rules are turned off, unless when given + * a non-nully and non-false value. + * + * @example + * var processor = lint(mdast, { + * 'html': false // Ignore HTML warnings. + * }); + * + * @param {MDAST} mdast - Host object. + * @param {Object?} options - Hash of rule names mapping to + * rule options. + */ +function lint(mdast, options) { + var settings = options || {}; + var reset = settings.reset; + var id; + var setting; + + /* + * Ensure offset information is added. + */ + + mdast.use(range); + + /** + * Get the latest state of a rule. + * + * @param {string} ruleId + * @param {File} [file] + */ + function getState(ruleId, file) { + var ranges = file && file.lintRanges && file.lintRanges[ruleId]; + + if (ranges) { + return ranges[ranges.length - 1].state; + } + + setting = settings[ruleId]; + + if (setting === false) { + return false; + } + + return !reset || (setting !== null && setting !== undefined); + } + + /** + * Store settings on `file`. + * + * @param {File} file + */ + function store(file) { + var ranges = file.lintRanges; + var ruleId; + + if (!ranges) { + ranges = {}; + + for (ruleId in rules) { + ranges[ruleId] = [{ + 'state': getState(ruleId), + 'position': { + 'line': 0, + 'column': 0 + } + }]; + } + + file.lintRanges = ranges; + } + } + + mdast.use(function () { + return function (ast, file) { + store(file); + }; + }); + + /* + * Add each rule as a seperate plugin. + */ + + for (id in rules) { + mdast.use(attachFactory(id, rules[id], settings[id])); + } + + /** + * Handle a new-found marker. + * + * @param {Object} marker + * @param {Object} parser + */ + function onparse(marker, parser) { + var file = parser.file; + var attributes = marker.attributes.split(' '); + var type = attributes[0]; + var ruleId = attributes[1]; + var markers; + var currentState; + var previousState; + + store(file); + + if (type !== 'disable' && type !== 'enable') { + file.fail('Unknown lint keyword `' + type + '`: use either `\'enable\'` or `\'disable\'`', marker.node); + + return; + } + + if (!(ruleId in rules)) { + file.fail('Unknown rule: cannot ' + type + ' `\'' + ruleId + '\'`', marker.node); + + return; + } + + markers = file.lintRanges[ruleId]; + + previousState = getState(ruleId, file); + currentState = type === 'enable'; + + if (currentState !== previousState) { + markers.push({ + 'state': currentState, + 'position': marker.node.position.start + }); + } + } + + mdast.use(zone({ + 'name': 'lint', + 'onparse': onparse + })); + + /* + * Sort messages. + */ + + mdast.use(sort); + + /* + * Filter. + */ + + mdast.use(filter); +} + +/* + * Expose. + */ + +module.exports = lint; diff --git a/lib/rules/blockquote-indentation.js b/lib/rules/blockquote-indentation.js new file mode 100644 index 0000000..6ddb1aa --- /dev/null +++ b/lib/rules/blockquote-indentation.js @@ -0,0 +1,108 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module blockquote-indentation + * @fileoverview + * Warn when blockquotes are either indented too much or too little. + * + * Options: `number`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used indentation + * and will warn when other blockquotes use a different indentation. + * @example + * + * > Hello + * ... + * > World + * + * + * > Hello + * ... + * > World + * + * + * > Hello + * ... + * > World + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var plural = require('../utilities/plural'); +var position = require('../utilities/position'); + +/** + * Get the indent of a blockquote. + * + * @param {Node} node + * @return {number} - Indentation. + */ +function check(node) { + var head = node.children[0]; + var indentation = position.start(head).column - position.start(node).column; + var padding = toString(head).match(/^ +/); + + if (padding) { + indentation += padding[0].length; + } + + return indentation; +} + +/** + * Warn when a blockquote has a too large or too small + * indentation. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {number?} [preferred='consistent'] - Preferred + * indentation between a blockquote and its content. + * When not a number, defaults to the first found + * indentation. + * @param {Function} done - Callback. + */ +function blockquoteIndentation(ast, file, preferred, done) { + preferred = isNaN(preferred) || typeof preferred !== 'number' ? null : preferred; + + visit(ast, 'blockquote', function (node) { + var indent; + var diff; + var word; + + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + indent = check(node); + diff = preferred - indent; + word = diff > 0 ? 'Add' : 'Remove'; + + diff = Math.abs(diff); + + if (diff !== 0) { + file.warn( + word + ' ' + diff + ' ' + plural('space', diff) + + ' between blockquote and content', + position.start(node.children[0]) + ); + } + } else { + preferred = check(node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = blockquoteIndentation; diff --git a/lib/rules/code-block-style.js b/lib/rules/code-block-style.js new file mode 100644 index 0000000..1429a23 --- /dev/null +++ b/lib/rules/code-block-style.js @@ -0,0 +1,133 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module code-block-style + * @fileoverview + * Warn when code-blocks do not adhere to a given style. + * + * Options: `string`, either `'consistent'`, `'fences'`, or `'indented'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used code-block + * style, and will warn when a subsequent code-block uses a different + * style. + * @example + * + * Hello + * + * ... + * + * World + * + * + * ``` + * Hello + * ``` + * ... + * ```bar + * World + * ``` + * + * + * Hello + * ... + * ``` + * World + * ``` + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/* + * Valid styles. + */ + +var STYLES = { + 'null': true, + 'fenced': true, + 'indented': true +}; + +/** + * Warn for violating code-block style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * code block style. Defaults to `'consistent'` when + * not a a string. Otherwise, should be one of + * `'fenced'` or `'indented'`. + * @param {Function} done - Callback. + */ +function codeBlockStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid code block style `' + preferred + '`: use either `\'consistent\'`, `\'fenced\'`, or `\'indented\'`'); + + return; + } + + /** + * Get the style of `node`. + * + * @param {Node} node + * @return {string?} - `'fenced'`, `'indented'`, or + * `null`. + */ + function check(node) { + var initial = start(node).offset; + var final = end(node).offset; + + if (position.isGenerated(node)) { + return null; + } + + if ( + node.lang || + /^\s*([~`])\1{2,}/.test(contents.slice(initial, final)) + ) { + return 'fenced'; + } + + return 'indented'; + } + + visit(ast, 'code', function (node) { + var current = check(node); + + if (!current) { + return; + } + + if (!preferred) { + preferred = current; + } else if (preferred !== current) { + file.warn('Code blocks should be ' + preferred, node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = codeBlockStyle; diff --git a/lib/rules/definition-case.js b/lib/rules/definition-case.js new file mode 100644 index 0000000..ab146fe --- /dev/null +++ b/lib/rules/definition-case.js @@ -0,0 +1,74 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module definition-case + * @fileoverview + * Warn when definition labels are not lower-case. + * @example + * + * [example] http://example.com "Example Domain" + * + * + * ![Example] http://example.com/favicon.ico "Example image" + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Expressions. + */ + +var LABEL = /^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/; + +/** + * Warn when definitions are not placed at the end of the + * file. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function definitionCase(ast, file, preferred, done) { + var contents = file.toString(); + + /** + * Validate a node, either a normal definition or + * a footnote definition. + * + * @param {Node} node + */ + function validate(node) { + var start = position.start(node).offset; + var end = position.end(node).offset; + var label; + + if (position.isGenerated(node)) { + return; + } + + label = contents.slice(start, end).match(LABEL)[1]; + + if (label !== label.toLowerCase()) { + file.warn('Do not use uppper-case characters in definition labels', node); + } + } + + visit(ast, 'definition', validate); + visit(ast, 'footnoteDefinition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = definitionCase; diff --git a/lib/rules/definition-spacing.js b/lib/rules/definition-spacing.js new file mode 100644 index 0000000..355379a --- /dev/null +++ b/lib/rules/definition-spacing.js @@ -0,0 +1,74 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module definition-spacing + * @fileoverview + * Warn when consecutive white space is used in a definition. + * @example + * + * [example domain] http://example.com "Example Domain" + * + * + * ![example image] http://example.com/favicon.ico "Example image" + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Expressions. + */ + +var LABEL = /^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/; + +/** + * Warn when consecutive white space is used in a + * definition. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function definitionSpacing(ast, file, preferred, done) { + var contents = file.toString(); + + /** + * Validate a node, either a normal definition or + * a footnote definition. + * + * @param {Node} node + */ + function validate(node) { + var start = position.start(node).offset; + var end = position.end(node).offset; + var label; + + if (position.isGenerated(node)) { + return; + } + + label = contents.slice(start, end).match(LABEL)[1]; + + if (/[ \t\n]{2,}/.test(label)) { + file.warn('Do not use consecutive white-space in definition labels', node); + } + } + + visit(ast, 'definition', validate); + visit(ast, 'footnoteDefinition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = definitionSpacing; diff --git a/lib/rules/emphasis-marker.js b/lib/rules/emphasis-marker.js new file mode 100644 index 0000000..a4d4832 --- /dev/null +++ b/lib/rules/emphasis-marker.js @@ -0,0 +1,84 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module emphasis-marker + * @fileoverview + * Warn for violating emphasis markers. + * + * Options: `string`, either `'consistent'`, `'*'`, or `'_'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used emphasis + * style, and will warn when a subsequent emphasis uses a different + * style. + * @example + * + * *foo* + * *bar* + * + * + * _foo_ + * _bar_ + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '*': true, + '_': true, + 'null': true +}; + +/** + * Warn when an `emphasis` node has an incorrect marker. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `'*'` or `'_'`, or `'consistent'`. + * @param {Function} done - Callback. + */ +function emphasisMarker(ast, file, preferred, done) { + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (MARKERS[preferred] !== true) { + file.fail('Invalid emphasis marker `' + preferred + '`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`'); + + return; + } + + visit(ast, 'emphasis', function (node) { + var marker = file.toString().charAt(position.start(node).offset); + + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + if (marker !== preferred) { + file.warn('Emphasis should use `' + preferred + '` as a marker', node); + } + } else { + preferred = marker; + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = emphasisMarker; diff --git a/lib/rules/fenced-code-flag.js b/lib/rules/fenced-code-flag.js new file mode 100644 index 0000000..1cfbc14 --- /dev/null +++ b/lib/rules/fenced-code-flag.js @@ -0,0 +1,105 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module fenced-code-flag + * @fileoverview + * Warn when fenced code blocks occur without language flag. + * + * Options: `Array.` or `Object`. + * + * Providing an array, is a shortcut for just providing the `flags` + * property on the object. + * + * The object can have an array of flags which are deemed valid. + * In addition it can have the property `allowEmpty` (`boolean`) + * which signifies whether or not to warn for fenced code-blocks without + * languge flags. + * @example + * + * ```hello + * world(); + * ``` + * + * + * Hello + * + * + * ``` + * world(); + * ``` + * + * + * ``` + * world(); + * ``` + * + * + * ```hello + * world(); + * ``` + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn for fenced code blocks without language flag. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {Object|Array.} [preferred] - List + * of flags deemed valid. + * @param {Function} done - Callback. + */ +function fencedCodeFlag(ast, file, preferred, done) { + var contents = file.toString(); + var allowEmpty = false; + var flags = []; + + if (typeof preferred === 'object' && !('length' in preferred)) { + allowEmpty = Boolean(preferred.allowEmpty); + + preferred = preferred.flags; + } + + if (typeof preferred === 'object' && 'length' in preferred) { + flags = String(preferred).split(','); + } + + visit(ast, 'code', function (node) { + var value = contents.slice(start(node).offset, end(node).offset); + + if (position.isGenerated(node)) { + return; + } + + if (node.lang) { + if (flags.length && flags.indexOf(node.lang) === -1) { + file.warn('Invalid code-language flag', node); + } + } else if (/^\ {0,3}([~`])\1{2,}/.test(value) && !allowEmpty) { + file.warn('Missing code-language flag', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = fencedCodeFlag; diff --git a/lib/rules/fenced-code-marker.js b/lib/rules/fenced-code-marker.js new file mode 100644 index 0000000..e04b1b2 --- /dev/null +++ b/lib/rules/fenced-code-marker.js @@ -0,0 +1,114 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module fenced-code-marker + * @fileoverview + * Warn for violating fenced code markers. + * + * Options: `string`, either `` '`' ``, or `'~'`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used fenced code + * marker style, and will warn when a subsequent fenced code uses a + * different style. + * @example + * + * ```foo + * bar(); + * ``` + * + * ``` + * baz(); + * ``` + * + * + * ~~~foo + * bar(); + * ~~~ + * + * ~~~ + * baz(); + * ~~~ + * + * + * ~~~foo + * bar(); + * ~~~ + * + * ``` + * baz(); + * ``` + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '`': true, + '~': true, + 'null': true +}; + +/** + * Warn for violating fenced code markers. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `` '`' `` or `~`, or `'consistent'`. + * @param {Function} done - Callback. + */ +function fencedCodeMarker(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (MARKERS[preferred] !== true) { + file.fail('Invalid fenced code marker `' + preferred + '`: use either `\'consistent\'`, `` \'\`\' ``, or `\'~\'`'); + + return; + } + + visit(ast, 'code', function (node) { + var marker = contents.substr(position.start(node).offset, 4); + + if (position.isGenerated(node)) { + return; + } + + marker = marker.trimLeft().charAt(0); + + /* + * Ignore unfenced code blocks. + */ + + if (MARKERS[marker] !== true) { + return; + } + + if (preferred) { + if (marker !== preferred) { + file.warn('Fenced code should use ' + preferred + ' as a marker', node); + } + } else { + preferred = marker; + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = fencedCodeMarker; diff --git a/lib/rules/file-extension.js b/lib/rules/file-extension.js new file mode 100644 index 0000000..1f01e5f --- /dev/null +++ b/lib/rules/file-extension.js @@ -0,0 +1,45 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module file-extension + * @fileoverview + * Warn when the document’s extension differs from the given preferred + * extension. + * + * Does not warn when given documents have no file extensions (such as + * `AUTHORS` or `LICENSE`). + * + * Options: `string`, default: `'md'` — Expected file extension. + * @example + * Invalid (when `'md'`): readme.mkd, readme.markdown, etc. + * Valid (when `'md'`): readme, readme.md + */ + +'use strict'; + +/** + * Check file extensions. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='md'] - Expected file + * extension. + * @param {Function} done - Callback. + */ +function fileExtension(ast, file, preferred, done) { + var ext = file.extension; + + preferred = typeof preferred === 'string' ? preferred : 'md'; + + if (ext !== '' && ext !== preferred) { + file.warn('Invalid extension: use `' + preferred + '`'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = fileExtension; diff --git a/lib/rules/final-definition.js b/lib/rules/final-definition.js new file mode 100644 index 0000000..d5322f4 --- /dev/null +++ b/lib/rules/final-definition.js @@ -0,0 +1,75 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module final-definition + * @fileoverview + * Warn when definitions are not placed at the end of the file. + * @example + * + * ... + * + * [example] http://example.com "Example Domain" + * + * + * ... + * + * [example] http://example.com "Example Domain" + * + * A trailing paragraph. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when definitions are not placed at the end of + * the file. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function finalDefinition(ast, file, preferred, done) { + var last = null; + + visit(ast, function (node) { + var line = start(node).line; + + /* + * Ignore generated nodes. + */ + + if (node.type === 'root' || position.isGenerated(node)) { + return; + } + + if (node.type === 'definition') { + if (last !== null && last > line) { + file.warn('Move definitions to the end of the file (after the node at line `' + last + '`)', node); + } + } else if (last === null) { + last = line; + } + }, true); + + done(); +} + +/* + * Expose. + */ + +module.exports = finalDefinition; diff --git a/lib/rules/final-newline.js b/lib/rules/final-newline.js new file mode 100644 index 0000000..6c80bc2 --- /dev/null +++ b/lib/rules/final-newline.js @@ -0,0 +1,38 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module final-newline + * @fileoverview + * Warn when a newline at the end of a file is missing. + * + * See [StackExchange](http://unix.stackexchange.com/questions/18743) for + * why. + */ + +'use strict'; + +/** + * Warn when the list-item marker style of unordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function finalNewline(ast, file, preferred, done) { + var contents = file.toString(); + var last = contents.length - 1; + + if (last > 0 && contents.charAt(last) !== '\n') { + file.warn('Missing newline character at end of file'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = finalNewline; diff --git a/lib/rules/first-heading-level.js b/lib/rules/first-heading-level.js new file mode 100644 index 0000000..e999813 --- /dev/null +++ b/lib/rules/first-heading-level.js @@ -0,0 +1,52 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module first-heading-level + * @fileoverview + * Warn when the first heading has a level other than `1`. + * @example + * + * # Foo + * + * ## Bar + * + * + * ## Foo + * + * # Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when the first heading has a level other than `1`. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function firstHeadingLevel(ast, file, preferred, done) { + visit(ast, 'heading', function (node) { + if (position.isGenerated(node)) { + return null; + } + + if (node.depth !== 1) { + file.warn('First heading level should be `1`', node); + } + + return false; + }); + + done(); +} + +module.exports = firstHeadingLevel; diff --git a/lib/rules/hard-break-spaces.js b/lib/rules/hard-break-spaces.js new file mode 100644 index 0000000..8f83e4c --- /dev/null +++ b/lib/rules/hard-break-spaces.js @@ -0,0 +1,60 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module hard-break-spaces + * @fileoverview + * Warn when too many spaces are used to create a hard break. + * @example + * + * + * + * Lorem ipsum·· + * dolor sit amet + * + * + * Lorem ipsum··· + * dolor sit amet. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when too many spaces are used to create a + * hard break. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function hardBreakSpaces(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'break', function (node) { + var start = position.start(node).offset; + var end = position.end(node).offset; + + if (position.isGenerated(node)) { + return; + } + + if (contents.slice(start, end).length > 3) { + file.warn('Use two spaces for hard line breaks', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = hardBreakSpaces; diff --git a/lib/rules/heading-increment.js b/lib/rules/heading-increment.js new file mode 100644 index 0000000..a0a8f00 --- /dev/null +++ b/lib/rules/heading-increment.js @@ -0,0 +1,63 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module heading-increment + * @fileoverview + * Warn when headings increment with more than 1 level at a time. + * @example + * + * # Foo + * + * ## Bar + * + * + * # Foo + * + * ### Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when headings increment with more than 1 level at + * a time. + * + * Never warns for the first heading. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function headingIncrement(ast, file, preferred, done) { + var prev = null; + + visit(ast, 'heading', function (node) { + var depth = node.depth; + + if (position.isGenerated(node)) { + return; + } + + if (prev && depth > prev + 1) { + file.warn('Heading levels should increment by one level at a time', node); + } + + prev = depth; + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = headingIncrement; diff --git a/lib/rules/heading-style.js b/lib/rules/heading-style.js new file mode 100644 index 0000000..f85fde7 --- /dev/null +++ b/lib/rules/heading-style.js @@ -0,0 +1,98 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module heading-style + * @fileoverview + * Warn when a heading does not conform to a given style. + * + * Options: `string`, either `'consistent'`, `'atx'`, `'atx-closed'`, + * or `'setext'`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used heading + * style, and will warn when a subsequent heading uses a different + * style. + * @example + * + * # Foo + * + * ## Bar + * + * ### Baz + * + * + * # Foo # + * + * ## Bar # + * + * ### Baz ### + * + * + * Foo + * === + * + * Bar + * --- + * + * ### Baz + * + * + * Foo + * === + * + * ## Bar + * + * ### Baz ### + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var style = require('../utilities/heading-style'); +var position = require('../utilities/position'); + +/* + * Types. + */ + +var TYPES = ['atx', 'atx-closed', 'setext']; + +/** + * Warn when a heading does not conform to a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string} [preferred='consistent'] - Preferred + * style, one of `atx`, `atx-closed`, or `setext`. + * Other values default to `'consistent'`, which will + * detect the first used style. + * @param {Function} done - Callback. + */ +function headingStyle(ast, file, preferred, done) { + preferred = TYPES.indexOf(preferred) === -1 ? null : preferred; + + visit(ast, 'heading', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + if (style(node, preferred) !== preferred) { + file.warn('Headings should use ' + preferred, node); + } + } else { + preferred = style(node, preferred); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = headingStyle; diff --git a/lib/rules/index.js b/lib/rules/index.js new file mode 100644 index 0000000..73f391a --- /dev/null +++ b/lib/rules/index.js @@ -0,0 +1,68 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Rules + * @fileoverview Map of rule id’s to rules. + */ + +'use strict'; + +/* + * Expose. + */ + +module.exports = { + 'no-auto-link-without-protocol': require('./no-auto-link-without-protocol'), + 'no-literal-urls': require('./no-literal-urls'), + 'no-consecutive-blank-lines': require('./no-consecutive-blank-lines'), + 'no-missing-blank-lines': require('./no-missing-blank-lines'), + 'blockquote-indentation': require('./blockquote-indentation'), + 'no-blockquote-without-caret': require('./no-blockquote-without-caret'), + 'code-block-style': require('./code-block-style'), + 'definition-case': require('./definition-case'), + 'definition-spacing': require('./definition-spacing'), + 'no-emphasis-as-heading': require('./no-emphasis-as-heading'), + 'emphasis-marker': require('./emphasis-marker'), + 'fenced-code-flag': require('./fenced-code-flag'), + 'fenced-code-marker': require('./fenced-code-marker'), + 'file-extension': require('./file-extension'), + 'final-newline': require('./final-newline'), + 'no-file-name-articles': require('./no-file-name-articles'), + 'no-file-name-consecutive-dashes': require('./no-file-name-consecutive-dashes'), + 'no-file-name-irregular-characters': require('./no-file-name-irregular-characters'), + 'no-file-name-mixed-case': require('./no-file-name-mixed-case'), + 'no-file-name-outer-dashes': require('./no-file-name-outer-dashes'), + 'final-definition': require('./final-definition'), + 'hard-break-spaces': require('./hard-break-spaces'), + 'heading-increment': require('./heading-increment'), + 'no-heading-content-indent': require('./no-heading-content-indent'), + 'no-heading-indent': require('./no-heading-indent'), + 'first-heading-level': require('./first-heading-level'), + 'maximum-heading-length': require('./maximum-heading-length'), + 'no-heading-punctuation': require('./no-heading-punctuation'), + 'heading-style': require('./heading-style'), + 'no-multiple-toplevel-headings': require('./no-multiple-toplevel-headings'), + 'no-duplicate-headings': require('./no-duplicate-headings'), + 'no-duplicate-definitions': require('./no-duplicate-definitions'), + 'no-html': require('./no-html'), + 'no-inline-padding': require('./no-inline-padding'), + 'maximum-line-length': require('./maximum-line-length'), + 'link-title-style': require('./link-title-style'), + 'list-item-bullet-indent': require('./list-item-bullet-indent'), + 'list-item-content-indent': require('./list-item-content-indent'), + 'list-item-indent': require('./list-item-indent'), + 'list-item-spacing': require('./list-item-spacing'), + 'ordered-list-marker-style': require('./ordered-list-marker-style'), + 'ordered-list-marker-value': require('./ordered-list-marker-value'), + 'no-shortcut-reference-image': require('./no-shortcut-reference-image'), + 'no-shortcut-reference-link': require('./no-shortcut-reference-link'), + 'rule-style': require('./rule-style'), + 'no-shell-dollars': require('./no-shell-dollars'), + 'strong-marker': require('./strong-marker'), + 'no-table-indentation': require('./no-table-indentation'), + 'table-pipe-alignment': require('./table-pipe-alignment'), + 'table-cell-padding': require('./table-cell-padding'), + 'table-pipes': require('./table-pipes'), + 'no-tabs': require('./no-tabs'), + 'unordered-list-marker-style': require('./unordered-list-marker-style') +}; diff --git a/lib/rules/link-title-style.js b/lib/rules/link-title-style.js new file mode 100644 index 0000000..8e15131 --- /dev/null +++ b/lib/rules/link-title-style.js @@ -0,0 +1,139 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module link-title-style + * @fileoverview + * Warn when link and definition titles occur with incorrect quotes. + * + * Options: `string`, either `'consistent'`, `'"'`, `'\''`, or + * `'()'`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used quote + * style, and will warn when a subsequent titles use a different + * style. + * @example + * + * [Example](http://example.com "Example Domain") + * [Example](http://example.com "Example Domain") + * + * + * [Example](http://example.com 'Example Domain') + * [Example](http://example.com 'Example Domain') + * + * + * [Example](http://example.com (Example Domain)) + * [Example](http://example.com (Example Domain)) + * + * + * [Example](http://example.com "Example Domain") + * [Example](http://example.com 'Example Domain') + * [Example](http://example.com (Example Domain)) + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '"': true, + '\'': true, + ')': true, + 'null': true +}; + +/* + * Methods. + */ + +var end = position.end; + +/** + * Warn for fenced code blocks without language flag. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `'"'`, `'\''`, `'()'`, or `'consistent'`. + * @param {Function} done - Callback. + */ +function linkTitleStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (preferred === '()' || preferred === '(') { + preferred = ')'; + } + + if (MARKERS[preferred] !== true) { + file.fail('Invalid link title style marker `' + preferred + '`: use either `\'consistent\'`, `\'"\'`, `\'\\\'\'`, or `\'()\'`'); + + return; + } + + /** + * Validate a single node. + * + * @param {Node} node + */ + function validate(node) { + var last = end(node).offset - 1; + var character; + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (node.type !== 'definition') { + last--; + } + + while (last) { + character = contents.charAt(last); + + if (/\s/.test(character)) { + last--; + } else { + break; + } + } + + /* + * Not a title. + */ + + if (!(character in MARKERS)) { + return; + } + + + if (!preferred) { + preferred = character; + } else if (preferred !== character) { + pos = file.offsetToPosition(last + 1); + file.warn('Titles should use `' + (preferred === ')' ? '()' : preferred) + '` as a quote', pos); + } + } + + visit(ast, 'link', validate); + visit(ast, 'image', validate); + visit(ast, 'definition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = linkTitleStyle; diff --git a/lib/rules/list-item-bullet-indent.js b/lib/rules/list-item-bullet-indent.js new file mode 100644 index 0000000..8d98175 --- /dev/null +++ b/lib/rules/list-item-bullet-indent.js @@ -0,0 +1,77 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-bullet-indent + * @fileoverview + * Warn when list item bullets are indented. + * @example + * + * * List item + * * List item + * + * + * * List item + * * List item + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when list item bullets are indented. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function listItemBulletIndent(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'list', function (node) { + var items = node.children; + + items.forEach(function (item) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var indent; + + if (position.isGenerated(node)) { + return; + } + + indent = contents.slice(initial, final).match(/^\s*/)[0].length; + + if (indent !== 0) { + initial = start(head); + + file.warn('Incorrect indentation before bullet: remove ' + indent + ' ' + plural('space', indent), { + 'line': initial.line, + 'column': initial.column - indent + }); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemBulletIndent; diff --git a/lib/rules/list-item-content-indent.js b/lib/rules/list-item-content-indent.js new file mode 100644 index 0000000..571979d --- /dev/null +++ b/lib/rules/list-item-content-indent.js @@ -0,0 +1,112 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-content-indent + * @fileoverview + * Warn when the content of a list item has mixed indentation. + * @example + * + * * List item + * + * * Nested list item indented by 4 spaces + * + * + * * List item + * + * * Nested list item indented by 3 spaces + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when the content of a list item has mixed + * indentation. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function listItemContentIndent(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'listItem', function (node) { + var style; + + node.children.forEach(function (item, index) { + var begin = start(item); + var column = begin.column; + var char; + var diff; + var word; + + if (position.isGenerated(item)) { + return; + } + + /* + * Get indentation for the first child. + * Only the first item can have a checkbox, + * so here we remove that from the column. + */ + + if (index === 0) { + if (Boolean(node.checked) === node.checked) { + char = begin.offset; + + while (contents.charAt(char) !== '[') { + char--; + } + + column -= begin.offset - char; + } + + style = column; + + return; + } + + /* + * Warn for violating children. + */ + + if (column !== style) { + diff = style - column; + word = diff > 0 ? 'add' : 'remove'; + + diff = Math.abs(diff); + + file.warn( + 'Don’t use mixed indentation for children, ' + word + + ' ' + diff + ' ' + plural('space', diff), + { + 'line': start(item).line, + 'column': column + } + ); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemContentIndent; diff --git a/lib/rules/list-item-indent.js b/lib/rules/list-item-indent.js new file mode 100644 index 0000000..f18f05a --- /dev/null +++ b/lib/rules/list-item-indent.js @@ -0,0 +1,148 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-indent + * @fileoverview + * Warn when the spacing between a list item’s bullet and its content + * violates a given style. + * + * Options: `string`, either `'tab-size'`, `'mixed'`, or `'space'`, + * default: `'tab-size'`. + * @example + * + * * List + * item. + * + * 11. List + * item. + * + * + * * List item. + * + * 11. List item + * + * * List + * item. + * + * 11. List + * item. + * + * + * * List item. + * + * 11. List item + * + * * List + * item. + * + * 11. List + * item. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Styles. + */ + +var STYLES = { + 'tab-size': true, + 'mixed': true, + 'space': true +}; + +/** + * Warn when the spacing between a list item’s bullet and + * its content violates a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='tab-size'] - Either + * `'tab-size'`, `'space'`, or `'mixed'`, defaulting + * to the first. + * @param {Function} done - Callback. + */ +function listItemIndent(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' ? 'tab-size' : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid list-item indent style `' + preferred + '`: use either `\'tab-size\'`, `\'space\'`, or `\'mixed\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + var isOrdered = node.ordered; + var offset = node.start || 1; + + if (position.isGenerated(node)) { + return; + } + + items.forEach(function (item, index) { + var head = item.children[0]; + var bulletSize = isOrdered ? String(offset + index).length + 1 : 1; + var tab = Math.ceil(bulletSize / 4) * 4; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + var shouldBe; + var diff; + var word; + + marker = contents.slice(initial, final); + + /* + * Support checkboxes. + */ + + marker = marker.replace(/\[[x ]?\]\s*$/i, ''); + + if (preferred === 'tab-size') { + shouldBe = tab; + } else if (preferred === 'space') { + shouldBe = bulletSize + 1; + } else { + shouldBe = node.loose ? tab : bulletSize + 1; + } + + if (marker.length !== shouldBe) { + diff = shouldBe - marker.length; + word = diff > 0 ? 'add' : 'remove'; + + diff = Math.abs(diff); + + file.warn( + 'Incorrect list-item indent: ' + word + + ' ' + diff + ' ' + plural('space', diff), + start(head) + ); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemIndent; diff --git a/lib/rules/list-item-spacing.js b/lib/rules/list-item-spacing.js new file mode 100644 index 0000000..7d22ec0 --- /dev/null +++ b/lib/rules/list-item-spacing.js @@ -0,0 +1,118 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-spacing + * @fileoverview + * Warn when list looseness is incorrect, such as being tight + * when it should be loose, and vice versa. + * @example + * + * - Wrapped + * item + * + * - item 2 + * + * - item 3 + * + * + * - item 1 + * - item 2 + * - item 3 + * + * + * - Wrapped + * item + * - item 2 + * - item 3 + * + * + * - item 1 + * + * - item 2 + * + * - item 3 + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when list items looseness is incorrect, such as + * being tight when it should be loose, and vice versa. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function listItemSpacing(ast, file, preferred, done) { + visit(ast, 'list', function (node) { + var items = node.children; + var isTightList = true; + var indent = start(node).column; + var type; + + if (position.isGenerated(node)) { + return; + } + + items.forEach(function (item) { + var content = item.children; + var head = content[0]; + var tail = content[content.length - 1]; + var isLoose = (end(tail).line - start(head).line) > 0; + + if (isLoose) { + isTightList = false; + } + }); + + type = isTightList ? 'tight' : 'loose'; + + items.forEach(function (item, index) { + var next = items[index + 1]; + var isTight = end(item).column > indent; + + /* + * Ignore last. + */ + + if (!next) { + return; + } + + /* + * Check if the list item's state does (not) + * match the list's state. + */ + + if (isTight !== isTightList) { + file.warn('List item should be ' + type + ', isn’t', { + 'start': end(item), + 'end': start(next) + }); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemSpacing; diff --git a/lib/rules/maximum-heading-length.js b/lib/rules/maximum-heading-length.js new file mode 100644 index 0000000..5b1b4d8 --- /dev/null +++ b/lib/rules/maximum-heading-length.js @@ -0,0 +1,59 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module maximum-heading-length + * @fileoverview + * Warn when headings are too long. + * + * Options: `number`, default: `60`. + * + * Ignores markdown syntax, only checks the plain text content. + * @example + * + * # Alpha bravo charlie delta echo + * # ![Alpha bravo charlie delta echo](http://example.com/nato.png) + * + * + * # Alpha bravo charlie delta echo foxtrot + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var position = require('../utilities/position'); + +/** + * Warn when headings are too long. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {number?} [preferred=60] - Maximum content + * length. + * @param {Function} done - Callback. + */ +function maximumHeadingLength(ast, file, preferred, done) { + preferred = isNaN(preferred) || typeof preferred !== 'number' ? 60 : preferred; + + visit(ast, 'heading', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (toString(node).length > preferred) { + file.warn('Use headings shorter than `' + preferred + '`', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = maximumHeadingLength; diff --git a/lib/rules/maximum-line-length.js b/lib/rules/maximum-line-length.js new file mode 100644 index 0000000..f1f633b --- /dev/null +++ b/lib/rules/maximum-line-length.js @@ -0,0 +1,178 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module maximum-line-length + * @fileoverview + * Warn when lines are too long. + * + * Options: `number`, default: `80`. + * + * Ignores nodes which cannot be wrapped, such as heasings, tables, + * code, and links. + * @example + * + * Alpha bravo charlie delta echo. + * + * Alpha bravo charlie delta echo [foxtrot](./foxtrot.html). + * + * # Alpha bravo charlie delta echo foxtrot golf hotel. + * + * # Alpha bravo charlie delta echo foxtrot golf hotel. + * + * | A | B | C | D | E | F | F | H | + * | ----- | ----- | ------- | ----- | ---- | ------- | ---- | ----- | + * | Alpha | bravo | charlie | delta | echo | foxtrot | golf | hotel | + * + * + * Alpha bravo charlie delta echo foxtrot golf. + * + * Alpha bravo charlie delta echo [foxtrot](./foxtrot.html) golf. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Check if `node` is applicable, as in, if it should be + * ignored. + * + * @param {Node} node + * @return {boolean} - Whether or not `node` should be + * ignored. + */ +function isApplicable(node) { + return node.type === 'heading' || + node.type === 'table' || + node.type === 'code'; +} + +/** + * Warn when lines are too long. This rule is forgiving + * about lines which cannot be wrapped, such as code, + * tables, and headings, or links at the enc of a line. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {number?} [preferred=80] - Maximum line length. + * @param {Function} done - Callback. + */ +function maximumLineLength(ast, file, preferred, done) { + var style = preferred && preferred !== true ? preferred : 80; + var content = file.toString(); + var matrix = content.split('\n'); + var index = -1; + var length = matrix.length; + var lineLength; + + /** + * Whitelist from `initial` to `final`, zero-based. + * + * @param {number} initial + * @param {number} final + */ + function whitelist(initial, final) { + initial--; + + while (++initial < final) { + matrix[initial] = ''; + } + } + + /* + * Next, white list nodes which cannot be wrapped. + */ + + visit(ast, function (node) { + var applicable = isApplicable(node); + var initial = applicable && start(node).line; + var final = applicable && end(node).line; + + if (!applicable || position.isGenerated(node)) { + return; + } + + whitelist(initial - 1, final); + }); + + /* + * Finally, whitelist URLs, but only if they occur at + * or after the wrap. However, when they do, and + * there’s white-space after it, they are not + * whitelisted. + */ + + visit(ast, 'link', function (node, pos, parent) { + var next = parent.children[pos + 1]; + var initial = start(node); + var final = end(node); + + /* + * Nothing to whitelist when generated. + */ + + if (position.isGenerated(node)) { + return; + } + + /* + * No whitelisting when starting after the border, + * or ending before it. + */ + + if (initial.column > style || final.column < style) { + return; + } + + /* + * No whitelisting when there’s white-space after + * the link. + */ + + if ( + next && + start(next).line === initial.line && + (!next.value || /^(.+?[ \t].+?)/.test(next.value)) + ) { + return; + } + + whitelist(initial.line - 1, final.line); + }); + + /* + * Iterate over every line, and warn for + * violating lines. + */ + + while (++index < length) { + lineLength = matrix[index].length; + + if (lineLength > style) { + file.warn('Line must be at most ' + style + ' characters', { + 'line': index + 1, + 'column': lineLength + 1 + }); + } + } + + done(); +} + +/* + * Expose. + */ + +module.exports = maximumLineLength; diff --git a/lib/rules/no-auto-link-without-protocol.js b/lib/rules/no-auto-link-without-protocol.js new file mode 100644 index 0000000..697313b --- /dev/null +++ b/lib/rules/no-auto-link-without-protocol.js @@ -0,0 +1,84 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-auto-link-without-protocol + * @fileoverview + * Warn for angle-bracketed links without protocol. + * @example + * + * + * + * + * + * + * + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Protocol expression. + * + * @type {RegExp} + * @see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax + */ + +var PROTOCOL = /^[a-z][a-z+.-]+:\/?/i; + +/** + * Assert `node`s reference starts with a protocol. + * + * @param {Node} node + * @return {boolean} + */ +function hasProtocol(node) { + return PROTOCOL.test(toString(node)); +} + +/** + * Warn for angle-bracketed links without protocol. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noAutoLinkWithoutProtocol(ast, file, preferred, done) { + visit(ast, 'link', function (node) { + var head = start(node.children[0]).column; + var tail = end(node.children[node.children.length - 1]).column; + var initial = start(node).column; + var final = end(node).column; + + if (position.isGenerated(node)) { + return; + } + + if (initial === head - 1 && final === tail + 1 && !hasProtocol(node)) { + file.warn('All automatic links must start with a protocol', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noAutoLinkWithoutProtocol; diff --git a/lib/rules/no-blockquote-without-caret.js b/lib/rules/no-blockquote-without-caret.js new file mode 100644 index 0000000..0002cc3 --- /dev/null +++ b/lib/rules/no-blockquote-without-caret.js @@ -0,0 +1,84 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-blockquote-without-caret + * @fileoverview + * Warn when blank lines without carets are found in a blockquote. + * @example + * + * > Foo... + * > + * > ...Bar. + * + * + * > Foo... + * + * > ...Bar. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when blank lines without carets are found in a + * blockquote. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noBlockquoteWithoutCaret(ast, file, preferred, done) { + var contents = file.toString(); + var last = contents.length; + + visit(ast, 'blockquote', function (node) { + var start = position.start(node).line; + var indent = node.position && node.position.indent; + + if (position.isGenerated(node) || !indent || !indent.length) { + return; + } + + indent.forEach(function (column, n) { + var character; + var line = start + n + 1; + var offset = file.positionToOffset({ + 'line': line, + 'column': column + }) - 1; + + while (++offset < last) { + character = contents.charAt(offset); + + if (character === '>') { + return; + } + + /* istanbul ignore else - just for safety */ + if (character !== ' ' && character !== '\t') { + break; + } + } + + file.warn('Missing caret in blockquote', { + 'line': line, + 'column': column + }); + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noBlockquoteWithoutCaret; diff --git a/lib/rules/no-consecutive-blank-lines.js b/lib/rules/no-consecutive-blank-lines.js new file mode 100644 index 0000000..caee89c --- /dev/null +++ b/lib/rules/no-consecutive-blank-lines.js @@ -0,0 +1,124 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-consecutive-blank-lines + * @fileoverview + * Warn for too many consecutive blank lines. Knows about the extra line + * needed between a list and indented code, and two lists. + * @example + * + * Foo... + * + * ...Bar. + * + * + * Foo... + * + * + * ...Bar. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Constants. + */ + +var MAX = 2; + +/** + * Warn for too many consecutive blank lines. Knows + * about the extra line needed between a list and + * indented code, and two lists. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noConsecutiveBlankLines(ast, file, preferred, done) { + /** + * Compare the difference between `start` and `end`, + * and warn when that difference exceens `max`. + * + * @param {Position} start + * @param {Position} end + */ + function compare(start, end, max) { + var diff = end.line - start.line; + var word = diff > 0 ? 'before' : 'after'; + + diff = Math.abs(diff) - max; + + if (diff > 0) { + file.warn('Remove ' + diff + ' ' + plural('line', diff) + ' ' + word + ' node', end); + } + } + + visit(ast, function (node) { + var children = node.children; + + if (position.isGenerated(node)) { + return; + } + + if (children && children[0]) { + /* + * Compare parent and first child. + */ + + compare(position.start(node), position.start(children[0]), 0); + + /* + * Compare between each child. + */ + + children.forEach(function (child, index) { + var prev = children[index - 1]; + var max = MAX; + + if (!prev) { + return; + } + + if ( + ( + prev.type === 'list' && + child.type === 'list' + ) || + ( + child.type === 'code' && + prev.type === 'list' && + !child.lang + ) + ) { + max++; + } + + compare(position.end(prev), position.start(child), max); + }); + + /* + * Compare parent and last child. + */ + + compare(position.end(node), position.end(children[children.length - 1]), 1); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noConsecutiveBlankLines; diff --git a/lib/rules/no-duplicate-definitions.js b/lib/rules/no-duplicate-definitions.js new file mode 100644 index 0000000..2c1775d --- /dev/null +++ b/lib/rules/no-duplicate-definitions.js @@ -0,0 +1,75 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-duplicate-definitions + * @fileoverview + * Warn when duplicate definitions are found. + * @example + * + * [foo]: bar + * [baz]: qux + * + * + * [foo]: bar + * [foo]: qux + */ + +'use strict'; + +/* + * Dependencies. + */ + +var position = require('../utilities/position'); +var visit = require('../utilities/visit'); + +/** + * Warn when definitions with equal content are found. + * + * Matches case-insensitive. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noDuplicateDefinitions(ast, file, preferred, done) { + var map = {}; + + /** + * Check `node`. + * + * @param {Node} node + */ + function validate(node) { + var duplicate = map[node.identifier]; + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (duplicate && duplicate.type) { + pos = position.start(duplicate); + + file.warn( + 'Do not use definitions with the same identifier (' + + pos.line + ':' + pos.column + ')', + node + ); + } + + map[node.identifier] = node; + } + + visit(ast, 'definition', validate); + visit(ast, 'footnoteDefinition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = noDuplicateDefinitions; diff --git a/lib/rules/no-duplicate-headings.js b/lib/rules/no-duplicate-headings.js new file mode 100644 index 0000000..c882ea9 --- /dev/null +++ b/lib/rules/no-duplicate-headings.js @@ -0,0 +1,73 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-duplicate-headings + * @fileoverview + * Warn when duplicate headings are found. + * @example + * + * # Foo + * + * ## Bar + * + * + * # Foo + * + * ## Foo + * + * ## [Foo](http://foo.com/bar) + */ + +'use strict'; + +/* + * Dependencies. + */ + +var position = require('../utilities/position'); +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); + +/** + * Warn when headings with equal content are found. + * + * Matches case-insensitive. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noDuplicateHeadings(ast, file, preferred, done) { + var map = {}; + + visit(ast, 'heading', function (node) { + var value = toString(node).toUpperCase(); + var duplicate = map[value]; + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (duplicate && duplicate.type === 'heading') { + pos = position.start(duplicate); + + file.warn( + 'Do not use headings with similar content (' + + pos.line + ':' + pos.column + ')', + node + ); + } + + map[value] = node; + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noDuplicateHeadings; diff --git a/lib/rules/no-emphasis-as-heading.js b/lib/rules/no-emphasis-as-heading.js new file mode 100644 index 0000000..3c01b52 --- /dev/null +++ b/lib/rules/no-emphasis-as-heading.js @@ -0,0 +1,81 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-emphasis-as-heading + * @fileoverview + * Warn when emphasis (including strong), instead of a heading, introduces + * a paragraph. + * + * Currently, only warns when a colon (`:`) is also included, maybe that + * could be omitted. + * @example + * + * # Foo: + * + * Bar. + * + * + * *Foo:* + * + * Bar. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var position = require('../utilities/position'); + +/** + * Warn when a section (a new paragraph) is introduced + * by emphasis (or strong) and a colon. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noEmphasisAsHeading(ast, file, preferred, done) { + visit(ast, 'paragraph', function (node, index, parent) { + var children = node.children; + var child = children[0]; + var prev = parent.children[index - 1]; + var next = parent.children[index + 1]; + var value; + + if (position.isGenerated(node)) { + return; + } + + if ( + (!prev || prev.type !== 'heading') && + next && + next.type === 'paragraph' && + children.length === 1 && + (child.type === 'emphasis' || child.type === 'strong') + ) { + value = toString(child); + + /* + * TODO: See if removing the punctuation + * necessity is possible? + */ + + if (value.charAt(value.length - 1) === ':') { + file.warn('Don’t use emphasis to introduce a section, use a heading', node); + } + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noEmphasisAsHeading; diff --git a/lib/rules/no-file-name-articles.js b/lib/rules/no-file-name-articles.js new file mode 100644 index 0000000..0e01a50 --- /dev/null +++ b/lib/rules/no-file-name-articles.js @@ -0,0 +1,36 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-articles + * @fileoverview + * Warn when file name start with an article. + * @example + * Valid: article.md + * Invalid: an-article.md, a-article.md, , the-article.md + */ + +'use strict'; + +/** + * Warn when file name start with an article. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameArticles(ast, file, preferred, done) { + var match = file.filename && file.filename.match(/^(the|an?)\b/i); + + if (match) { + file.warn('Do not start file names with `' + match[0] + '`'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameArticles; diff --git a/lib/rules/no-file-name-consecutive-dashes.js b/lib/rules/no-file-name-consecutive-dashes.js new file mode 100644 index 0000000..da4f382 --- /dev/null +++ b/lib/rules/no-file-name-consecutive-dashes.js @@ -0,0 +1,34 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-consecutive-dashes + * @fileoverview + * Warn when file names contain consecutive dashes. + * @example + * Invalid: docs/plug--ins.md + * Valid: docs/plug-ins.md + */ + +'use strict'; + +/** + * Warn when file names contain consecutive dashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameConsecutiveDashes(ast, file, preferred, done) { + if (file.filename && /-{2,}/.test(file.filename)) { + file.warn('Do not use consecutive dashes in a file name'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameConsecutiveDashes; diff --git a/lib/rules/no-file-name-irregular-characters.js b/lib/rules/no-file-name-irregular-characters.js new file mode 100644 index 0000000..820eaf8 --- /dev/null +++ b/lib/rules/no-file-name-irregular-characters.js @@ -0,0 +1,38 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-irregular-characters + * @fileoverview + * Warn when file names contain irregular characters: characters other + * than alpha-numericals, dashes, and dots (full-stops). + * @example + * Invalid: plug_ins.md, plug ins.md. + * Valid: plug-ins.md, plugins.md. + */ + +'use strict'; + +/** + * Warn when file names contain characters other than + * alpha-numericals, dashes, and dots (full-stops). + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameIrregularCharacters(ast, file, preferred, done) { + var match = file.filename && file.filename.match(/[^.a-zA-Z0-9-]/); + + if (match) { + file.warn('Do not use `' + match[0] + '` in a file name'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameIrregularCharacters; diff --git a/lib/rules/no-file-name-mixed-case.js b/lib/rules/no-file-name-mixed-case.js new file mode 100644 index 0000000..454fce0 --- /dev/null +++ b/lib/rules/no-file-name-mixed-case.js @@ -0,0 +1,38 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-mixed-case + * @fileoverview + * Warn when a file name uses mixed case: both upper- and lower case + * characters. + * @example + * Invalid: Readme.md + * Valid: README.md, readme.md + */ + +'use strict'; + +/** + * Warn when a file name uses mixed case: both upper- and + * lower case characters. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameMixedCase(ast, file, preferred, done) { + var name = file.filename; + + if (name && !(name === name.toLowerCase() || name === name.toUpperCase())) { + file.warn('Do not mix casing in file names'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameMixedCase; diff --git a/lib/rules/no-file-name-outer-dashes.js b/lib/rules/no-file-name-outer-dashes.js new file mode 100644 index 0000000..fc8a739 --- /dev/null +++ b/lib/rules/no-file-name-outer-dashes.js @@ -0,0 +1,34 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-outer-dashes + * @fileoverview + * Warn when file names contain initial or final dashes. + * @example + * Invalid: -readme.md, readme-.md + * Valid: readme.md + */ + +'use strict'; + +/** + * Warn when file names contain initial or final dashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameOuterDashes(ast, file, preferred, done) { + if (file.filename && /^-|-$/.test(file.filename)) { + file.warn('Do not use initial or final dashes in a file name'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameOuterDashes; diff --git a/lib/rules/no-heading-content-indent.js b/lib/rules/no-heading-content-indent.js new file mode 100644 index 0000000..b97f16a --- /dev/null +++ b/lib/rules/no-heading-content-indent.js @@ -0,0 +1,119 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-heading-content-indent + * @fileoverview + * Warn when a heading’s content is indented. + * @example + * + * + * #··Foo + * + * ## Bar··## + * + * ##··Baz + * + * + * #·Foo + * + * ## Bar·## + * + * ##·Baz + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var style = require('../utilities/heading-style'); +var plural = require('../utilities/plural'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when a (closed) ATX-heading has too much space + * between the initial hashes and the content, or the + * content and the final hashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noHeadingContentIndent(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'heading', function (node) { + var depth = node.depth; + var children = node.children; + var type = style(node, 'atx'); + var initial; + var final; + var diff; + var word; + var index; + + if (position.isGenerated(node)) { + return; + } + + if (type === 'atx' || type === 'atx-closed') { + initial = start(node); + index = initial.offset; + + while (contents.charAt(index) !== '#') { + index++; + } + + index = depth + (index - initial.offset); + diff = start(children[0]).column - initial.column - 1 - index; + + if (diff) { + word = diff > 0 ? 'Remove' : 'Add'; + diff = Math.abs(diff); + + file.warn( + word + ' ' + diff + ' ' + plural('space', diff) + + ' before this heading’s content', + start(children[0]) + ); + } + } + + /* + * Closed ATX-heading always must have a space + * between their content and the final hashes, + * thus, there is no `add x spaces`. + */ + + if (type === 'atx-closed') { + final = end(children[children.length - 1]); + diff = end(node).column - final.column - 1 - depth; + + if (diff) { + file.warn( + 'Remove ' + diff + ' ' + plural('space', diff) + + ' after this heading’s content', + final + ); + } + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noHeadingContentIndent; diff --git a/lib/rules/no-heading-indent.js b/lib/rules/no-heading-indent.js new file mode 100644 index 0000000..42f07db --- /dev/null +++ b/lib/rules/no-heading-indent.js @@ -0,0 +1,101 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-heading-indent + * @fileoverview + * Warn when a heading is indented. + * @example + * + * + * ···# Hello world + * + * ·Foo + * ----- + * + * ·# Hello world # + * + * ···Bar + * ===== + * + * + * # Hello world + * + * Foo + * ----- + * + * # Hello world # + * + * Bar + * ===== + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var plural = require('../utilities/plural'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when a heading has too much space before the + * initial hashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noHeadingIndent(ast, file, preferred, done) { + var contents = file.toString(); + var length = contents.length; + + visit(ast, 'heading', function (node) { + var initial = start(node); + var begin = initial.offset; + var index = begin - 1; + var character; + var diff; + + if (position.isGenerated(node)) { + return; + } + + while (++index < length) { + character = contents.charAt(index); + + if (character !== ' ' && character !== '\t') { + break; + } + } + + diff = index - begin; + + if (diff) { + file.warn( + 'Remove ' + diff + ' ' + plural('space', diff) + + ' before this heading', + { + 'line': initial.line, + 'column': initial.column + diff + } + ); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noHeadingIndent; diff --git a/lib/rules/no-heading-punctuation.js b/lib/rules/no-heading-punctuation.js new file mode 100644 index 0000000..05d41b3 --- /dev/null +++ b/lib/rules/no-heading-punctuation.js @@ -0,0 +1,71 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-heading-punctuation + * @fileoverview + * Warn when a heading ends with a a group of characters. + * Defaults to `'.,;:!?'`. + * + * Options: `string`, default: `'.,;:!?'`. + * + * Note that these are added to a regex, in a group (`'[' + char + ']'`), + * be careful for escapes and dashes. + * @example + * + * # Hello: + * + * # Hello? + * + * # Hello! + * + * # Hello, + * + * # Hello; + * + * + * # Hello + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var toString = require('../utilities/to-string'); + +/** + * Warn when headings end in some characters. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='\\.,;:!?'] - Group of characters. + * @param {Function} done - Callback. + */ +function noHeadingPunctuation(ast, file, preferred, done) { + preferred = typeof preferred === 'string' ? preferred : '\\.,;:!?'; + + visit(ast, 'heading', function (node) { + var value = toString(node); + + if (position.isGenerated(node)) { + return; + } + + value = value.charAt(value.length - 1); + + if (new RegExp('[' + preferred + ']').test(value)) { + file.warn('Don’t add a trailing `' + value + '` to headings', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noHeadingPunctuation; diff --git a/lib/rules/no-html.js b/lib/rules/no-html.js new file mode 100644 index 0000000..f16b2e5 --- /dev/null +++ b/lib/rules/no-html.js @@ -0,0 +1,45 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-html + * @fileoverview + * Warn when HTML nodes are used. + * + * Ignores comments, because they are used by this tool, mdast, and + * because markdown doesn’t have native comments. + * @example + * + *

Hello

+ * + * + * # Hello + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when HTML nodes are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function html(ast, file, preferred, done) { + visit(ast, 'html', function (node) { + if (!position.isGenerated(node) && !/^\s* + * * Hello *, [ world ](http://foo.bar/baz) + * + * + * *Hello*, [world](http://foo.bar/baz) + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var toString = require('../utilities/to-string'); + +/** + * Warn when inline nodes are padded with spaces between + * markers and content. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noInlinePadding(ast, file, preferred, done) { + visit(ast, function (node) { + var type = node.type; + var contents; + + if (position.isGenerated(node)) { + return; + } + + if ( + type === 'emphasis' || + type === 'strong' || + type === 'delete' || + type === 'image' || + type === 'link' + ) { + contents = toString(node); + + if (contents.charAt(0) === ' ' || contents.charAt(contents.length - 1) === ' ') { + file.warn('Don’t pad `' + type + '` with inner spaces', node); + } + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noInlinePadding; diff --git a/lib/rules/no-literal-urls.js b/lib/rules/no-literal-urls.js new file mode 100644 index 0000000..6fd3e66 --- /dev/null +++ b/lib/rules/no-literal-urls.js @@ -0,0 +1,62 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-literal-urls + * @fileoverview + * Warn when URLs without angle-brackets are used. + * @example + * + * http://foo.bar/baz + * + * + * + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn for literal URLs without angle-brackets. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noLiteralURLs(ast, file, preferred, done) { + visit(ast, 'link', function (node) { + var head = start(node.children[0]).column; + var tail = end(node.children[node.children.length - 1]).column; + var initial = start(node).column; + var final = end(node).column; + + if (position.isGenerated(node)) { + return; + } + + if (initial === head && final === tail) { + file.warn('Don’t use literal URLs without angle brackets', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noLiteralURLs; diff --git a/lib/rules/no-missing-blank-lines.js b/lib/rules/no-missing-blank-lines.js new file mode 100644 index 0000000..987ad55 --- /dev/null +++ b/lib/rules/no-missing-blank-lines.js @@ -0,0 +1,81 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-missing-blank-lines + * @fileoverview + * Warn for missing blank lines before a block node. + * @example + * + * # Foo + * ## Bar + * + * + * # Foo + * + * ## Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Check if `node` is an applicable block-level node. + * + * @param {Node} node + * @return {boolean} - Whether or not `node` is applicable. + */ +function isApplicable(node) { + return [ + 'paragraph', + 'blockquote', + 'heading', + 'code', + 'yaml', + 'html', + 'list', + 'table', + 'horizontalRule' + ].indexOf(node.type) !== -1; +} + +/** + * Warn when there is no empty line between two block + * nodes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noMissingBlankLines(ast, file, preferred, done) { + visit(ast, function (node, index, parent) { + var next = parent && parent.children[index + 1]; + + if (position.isGenerated(node)) { + return; + } + + if ( + next && + isApplicable(node) && + isApplicable(next) && + position.start(next).line === position.end(node).line + 1 + ) { + file.warn('Missing blank line before block node', next); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noMissingBlankLines; diff --git a/lib/rules/no-multiple-toplevel-headings.js b/lib/rules/no-multiple-toplevel-headings.js new file mode 100644 index 0000000..3472b1b --- /dev/null +++ b/lib/rules/no-multiple-toplevel-headings.js @@ -0,0 +1,60 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-multiple-toplevel-headings + * @fileoverview + * Warn when multiple top-level headings are used. + * @example + * + * # Foo + * + * # Bar + * + * + * # Foo + * + * ## Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when multiple top-level headings are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noMultipleToplevelHeadings(ast, file, preferred, done) { + var topLevelheading = false; + + visit(ast, 'heading', function (node) { + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (node.depth === 1) { + if (topLevelheading) { + pos = position.start(node); + + file.warn('Don’t use multiple top level headings (' + pos.line + ':' + pos.column + ')', node); + } + + topLevelheading = node; + } + }); + + done(); +} + +module.exports = noMultipleToplevelHeadings; diff --git a/lib/rules/no-shell-dollars.js b/lib/rules/no-shell-dollars.js new file mode 100644 index 0000000..fe964f6 --- /dev/null +++ b/lib/rules/no-shell-dollars.js @@ -0,0 +1,101 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-shell-dollars + * @fileoverview + * Warn when shell code is prefixed by dollar-characters. + * + * Ignored indented code blocks and fenced code blocks without language + * flag. + * @example + * + * ```bash + * $ echo a + * $ echo a > file + * ``` + * + * + * ```sh + * echo a + * echo a > file + * ``` + * + * + * ```zsh + * $ echo a + * a + * $ echo a > file + * ``` + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * List of shell script file extensions (also used as code + * flags for syntax highlighting on GitHub): + * + * @see https://github.com/github/linguist/blob/5bf8cf5/lib/linguist/languages.yml#L3002 + */ + +var flags = [ + 'sh', + 'bash', + 'bats', + 'cgi', + 'command', + 'fcgi', + 'ksh', + 'tmux', + 'tool', + 'zsh' +]; + +/** + * Warn when shell code is prefixed by dollar-characters. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noShellDollars(ast, file, preferred, done) { + visit(ast, 'code', function (node) { + var language = node.lang; + var value = node.value; + var warn; + + if (!language || position.isGenerated(node)) { + return; + } + + /* + * Check both known shell-code and unknown code. + */ + + if (flags.indexOf(language) !== -1) { + warn = value.length && value.split('\n').every(function (line) { + return Boolean(!line.trim() || line.match(/^\s*\$\s*/)); + }); + + if (warn) { + file.warn('Do not use dollar signs before shell-commands', node); + } + } + }); + + + done(); +} + +/* + * Expose. + */ + +module.exports = noShellDollars; diff --git a/lib/rules/no-shortcut-reference-image.js b/lib/rules/no-shortcut-reference-image.js new file mode 100644 index 0000000..cbadd78 --- /dev/null +++ b/lib/rules/no-shortcut-reference-image.js @@ -0,0 +1,54 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-shortcut-reference-image + * @fileoverview + * Warn when shortcut reference images are used. + * @example + * + * ![foo] + * + * [foo]: http://foo.bar/baz.png + * + * + * ![foo][] + * + * [foo]: http://foo.bar/baz.png + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when shortcut reference images are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noShortcutReferenceImage(ast, file, preferred, done) { + visit(ast, 'imageReference', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (node.referenceType === 'shortcut') { + file.warn('Use the trailing [] on reference images', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noShortcutReferenceImage; diff --git a/lib/rules/no-shortcut-reference-link.js b/lib/rules/no-shortcut-reference-link.js new file mode 100644 index 0000000..b2d9fe4 --- /dev/null +++ b/lib/rules/no-shortcut-reference-link.js @@ -0,0 +1,54 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-shortcut-reference-link + * @fileoverview + * Warn when shortcut reference links are used. + * @example + * + * [foo] + * + * [foo]: http://foo.bar/baz + * + * + * [foo][] + * + * [foo]: http://foo.bar/baz + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when shortcut reference links are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noShortcutReferenceLink(ast, file, preferred, done) { + visit(ast, 'linkReference', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (node.referenceType === 'shortcut') { + file.warn('Use the trailing [] on reference links', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noShortcutReferenceLink; diff --git a/lib/rules/no-table-indentation.js b/lib/rules/no-table-indentation.js new file mode 100644 index 0000000..6cc8596 --- /dev/null +++ b/lib/rules/no-table-indentation.js @@ -0,0 +1,60 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-table-indentation + * @fileoverview + * Warn when tables are indented. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when a table has a too much indentation. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noTableIndentation(ast, file, preferred, done) { + visit(ast, 'table', function (node) { + var contents = file.toString(); + + if (position.isGenerated(node)) { + return; + } + + node.children.forEach(function (row) { + var fence = contents.slice(position.start(row).offset, position.start(row.children[0]).offset); + + if (fence.indexOf('|') > 1) { + file.warn('Do not indent table rows', row); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noTableIndentation; diff --git a/lib/rules/no-tabs.js b/lib/rules/no-tabs.js new file mode 100644 index 0000000..73127b7 --- /dev/null +++ b/lib/rules/no-tabs.js @@ -0,0 +1,48 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-tabs + * @fileoverview + * Warn when hard-tabs instead of spaces + * @example + * + * + * Foo»Bar + * + * »···Foo + * + * + * Foo Bar + * + * Foo + */ + +'use strict'; + +/** + * Warn when hard-tabs instead of spaces are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noTabs(ast, file, preferred, done) { + var content = file.toString(); + var index = -1; + var length = content.length; + + while (++index < length) { + if (content.charAt(index) === '\t') { + file.warn('Use spaces instead of hard-tabs', file.offsetToPosition(index)); + } + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noTabs; diff --git a/lib/rules/ordered-list-marker-style.js b/lib/rules/ordered-list-marker-style.js new file mode 100644 index 0000000..fae6f98 --- /dev/null +++ b/lib/rules/ordered-list-marker-style.js @@ -0,0 +1,116 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module ordered-list-marker-style + * @fileoverview + * Warn when the list-item marker style of ordered lists violate a given + * style. + * + * Options: `string`, either `'consistent'`, `'.'`, or `')'`, + * default: `'consistent'`. + * + * Note that `)` is only supported in CommonMark. + * + * The default value, `consistent`, detects the first used list + * style, and will warn when a subsequent list uses a different + * style. + * @example + * + * 1. Foo + * + * 2. Bar + * + * + * 1) Foo + * + * 2) Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Valid styles. + */ + +var STYLES = { + ')': true, + '.': true, + 'null': true +}; + +/** + * Warn when the list-item marker style of ordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Ordered list + * marker style, either `'.'` or `')'`, defaulting to the + * first found style. + * @param {Function} done - Callback. + */ +function orderedListMarkerStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid ordered list-item marker style `' + preferred + '`: use either `\'.\'` or `\')\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + + if (!node.ordered) { + return; + } + + items.forEach(function (item) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + + if (position.isGenerated(item)) { + return; + } + + marker = contents.slice(initial, final).replace(/\s|\d/g, ''); + + /* + * Support checkboxes. + */ + + marker = marker.replace(/\[[x ]?\]\s*$/i, ''); + + if (!preferred) { + preferred = marker; + } else if (marker !== preferred) { + file.warn('Marker style should be `' + preferred + '`', item); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = orderedListMarkerStyle; diff --git a/lib/rules/ordered-list-marker-value.js b/lib/rules/ordered-list-marker-value.js new file mode 100644 index 0000000..8ad1b59 --- /dev/null +++ b/lib/rules/ordered-list-marker-value.js @@ -0,0 +1,156 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module ordered-list-marker-value + * @fileoverview + * Warn when the list-item marker values of ordered lists violate a + * given style. + * + * Options: `string`, either `'single'`, `'one'`, or `'ordered'`, + * default: `'ordered'`. + * + * When set to `'ordered'`, list-item bullets should increment by one, + * relative to the starting point. When set to `'single'`, bullets should + * be the same as the relative starting point. When set to `'one'`, bullets + * should always be `1`. + * @example + * + * 1. Foo + * 1. Bar + * 1. Baz + * + * 1. Alpha + * 1. Bravo + * 1. Charlie + * + * + * 1. Foo + * 1. Bar + * 1. Baz + * + * 3. Alpha + * 3. Bravo + * 3. Charlie + * + * + * 1. Foo + * 2. Bar + * 3. Baz + * + * 3. Alpha + * 4. Bravo + * 5. Charlie + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Valid styles. + */ + +var STYLES = { + 'ordered': true, + 'single': true, + 'one': true +}; + +/** + * Warn when the list-item markers values of ordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='ordered'] - Ordered list + * marker value, either `'one'` or `'ordered'`, + * defaulting to the latter. + * @param {Function} done - Callback. + */ +function orderedListMarkerValue(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' ? 'ordered' : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid ordered list-item marker value `' + preferred + '`: use either `\'ordered\'` or `\'one\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + var shouldBe = (preferred === 'one' ? 1 : node.start) || 1; + + /* + * Ignore unordered lists. + */ + + if (!node.ordered) { + return; + } + + items.forEach(function (item, index) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + + /* + * Ignore first list item. + */ + + if (index === 0) { + return; + } + + /* + * Increase the expected line number when in + * `ordered` mode. + */ + + if (preferred === 'ordered') { + shouldBe++; + } + + /* + * Ignore generated nodes. + */ + + if (position.isGenerated(item)) { + return; + } + + marker = contents.slice(initial, final).replace(/[\s\.\)]/g, ''); + + /* + * Support checkboxes. + */ + + marker = Number(marker.replace(/\[[x ]?\]\s*$/i, '')); + + if (marker !== shouldBe) { + file.warn('Marker should be `' + shouldBe + '`, was `' + marker + '`', item); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = orderedListMarkerValue; diff --git a/lib/rules/rule-style.js b/lib/rules/rule-style.js new file mode 100644 index 0000000..daa6981 --- /dev/null +++ b/lib/rules/rule-style.js @@ -0,0 +1,97 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module rule-style + * @fileoverview + * Warn when the horizontal rules violate a given or detected style. + * + * Options: `string`, either a valid markdown rule, or `consistent`, + * default: `'consistent'`. + * @example + * + * * * * + * + * * * * + * + * + * _______ + * + * _______ + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when a given style is invalid. + * + * @param {*} style + * @return {boolean} - Whether or not `style` is a + * valid rule style. + */ +function validateRuleStyle(style) { + return style === null || !/[^-_* ]/.test(style); +} + +/** + * Warn when the horizontal rules violate a given or + * detected style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - A valid + * horizontal rule, defaulting to the first found style. + * @param {Function} done - Callback. + */ +function ruleStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (validateRuleStyle(preferred) !== true) { + file.fail('Invalid preferred rule-style: provide a valid markdown rule, or `\'consistent\'`'); + + return; + } + + visit(ast, 'horizontalRule', function (node) { + var initial = start(node).offset; + var final = end(node).offset; + var hr; + + if (position.isGenerated(node)) { + return; + } + + hr = contents.slice(initial, final); + + if (preferred) { + if (hr !== preferred) { + file.warn('Horizontal rules should use `' + preferred + '`', node); + } + } else { + preferred = hr; + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = ruleStyle; diff --git a/lib/rules/strong-marker.js b/lib/rules/strong-marker.js new file mode 100644 index 0000000..884a16c --- /dev/null +++ b/lib/rules/strong-marker.js @@ -0,0 +1,82 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module blockquote-indentation + * @fileoverview + * Warn for violating strong markers. + * + * Options: `string`, either `'consistent'`, `'*'`, or `'_'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used strong + * style, and will warn when a subsequent strong uses a different + * style. + * @example + * + * **foo** + * **bar** + * + * + * __foo__ + * __bar__ + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '*': true, + '_': true, + 'null': true +}; + +/** + * Warn when a `strong` node has an incorrect marker. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `"*"` or `"_"`, or `"consistent"`. + * @param {Function} done - Callback. + */ +function strongMarker(ast, file, preferred, done) { + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (MARKERS[preferred] !== true) { + file.fail('Invalid strong marker `' + preferred + '`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`'); + } else { + visit(ast, 'strong', function (node) { + var marker = file.toString().charAt(position.start(node).offset); + + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + if (marker !== preferred) { + file.warn('Strong should use `' + preferred + '` as a marker', node); + } + } else { + preferred = marker; + } + }); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = strongMarker; diff --git a/lib/rules/table-cell-padding.js b/lib/rules/table-cell-padding.js new file mode 100644 index 0000000..390cd62 --- /dev/null +++ b/lib/rules/table-cell-padding.js @@ -0,0 +1,176 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module table-cell-padding + * @fileoverview + * Warn when table cells are incorrectly padded. + * + * Options: `string`, either `'consistent'`, `'padded'`, or `'compact'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used cell padding + * style, and will warn when a subsequent cells uses a different + * style. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * |A |B | + * |-----|-----| + * |Alpha|Bravo| + * + * + * | A | B | + * | -----| -----| + * | Alpha| Bravo| + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/* + * Valid styles. + */ + +var STYLES = { + 'null': true, + 'padded': true, + 'compact': true +}; + +/** + * Warn when table cells are incorrectly padded. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} preferred - Either `padded` (for + * at least a space), `compact` (for no spaces when + * possible), or `consistent`, which defaults to the + * first found style. + * @param {Function} done - Callback. + */ +function tableCellPadding(ast, file, preferred, done) { + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid table-cell-padding style `' + preferred + '`'); + } + + visit(ast, 'table', function (node) { + var children = node.children; + var contents = file.toString(); + var starts = []; + var ends = []; + var locations; + var positions; + var style; + var type; + var warning; + + if (position.isGenerated(node)) { + return; + } + + /** + * Check a fence. Checks both its initial spacing + * (between a cell and the fence), and its final + * spacing (between the fence and the next cell). + */ + function check(initial, final, cell, next, index) { + var fence = contents.slice(initial, final); + var pos = fence.indexOf('|'); + + if ( + cell && + pos !== -1 && + ( + ends[index] === undefined || + pos < ends[index] + ) + ) { + ends[index] = pos; + } + + if (next && pos !== -1) { + pos = fence.length - pos - 1; + + if (starts[index + 1] === undefined || pos < starts[index + 1]) { + starts[index + 1] = pos; + } + } + } + + children.forEach(function (row) { + var cells = row.children; + + check(start(row).offset, start(cells[0]).offset, null, cells[0], -1); + + cells.forEach(function (cell, index) { + var next = cells[index + 1] || null; + var final = start(next).offset || end(row).offset; + + check(end(cell).offset, final, cell, next, index); + }); + }); + + positions = starts.concat(ends); + + style = preferred === 'padded' ? 1 : preferred === 'compact' ? 0 : null; + + if (preferred === 'padded') { + style = 1; + } else if (preferred === 'compact') { + style = 0; + } else { + positions.some(function (pos) { + /* + * `some` skips non-existant indices, so + * there's no need to check for `!isNaN`. + */ + + style = Math.min(pos, 1); + + return true; + }); + } + + locations = children[0].children.map(function (cell) { + return start(cell); + }).concat(children[0].children.map(function (cell) { + return end(cell); + })); + + type = style === 1 ? 'padded' : 'compact'; + warning = 'Cell should be ' + type + ', isn’t'; + + positions.forEach(function (diff, index) { + if (diff !== style && diff !== undefined) { + file.warn(warning, locations[index]); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = tableCellPadding; diff --git a/lib/rules/table-pipe-alignment.js b/lib/rules/table-pipe-alignment.js new file mode 100644 index 0000000..1bae6d8 --- /dev/null +++ b/lib/rules/table-pipe-alignment.js @@ -0,0 +1,100 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module table-pipe-alignment + * @fileoverview + * Warn when table pipes are not aligned. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * | A | B | + * | -- | -- | + * | Alpha | Bravo | + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when table pipes are not aligned. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function tablePipeAlignment(ast, file, preferred, done) { + visit(ast, 'table', function (node) { + var contents = file.toString(); + var indices = []; + var offset; + var line; + + if (position.isGenerated(node)) { + return; + } + + /** + * Check all pipes after each column are at + * aligned. + */ + function check(initial, final, index) { + var pos = initial + contents.slice(initial, final).indexOf('|') - offset + 1; + + if (indices[index] === undefined) { + indices[index] = pos; + } else if (pos !== indices[index]) { + file.warn('Misaligned table fence', { + 'start': { + 'line': line, + 'column': pos + }, + 'end': { + 'line': line, + 'column': pos + 1 + } + }); + } + } + + node.children.forEach(function (row) { + var cells = row.children; + + line = start(row).line; + offset = start(row).offset; + + check(start(row).offset, start(cells[0]).offset, 0); + + row.children.forEach(function (cell, index) { + var next = start(cells[index + 1]).offset || end(row).offset; + + check(end(cell).offset, next, index + 1); + }); + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = tablePipeAlignment; diff --git a/lib/rules/table-pipes.js b/lib/rules/table-pipes.js new file mode 100644 index 0000000..07a22e0 --- /dev/null +++ b/lib/rules/table-pipes.js @@ -0,0 +1,75 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module table-pipes + * @fileoverview + * Warn when table rows are not fenced with pipes. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * A | B + * ----- | ----- + * Alpha | Bravo + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when a table rows are not fenced with pipes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function tablePipes(ast, file, preferred, done) { + visit(ast, 'table', function (node) { + var contents = file.toString(); + + node.children.forEach(function (row) { + var cells = row.children; + var head = cells[0]; + var tail = cells[cells.length - 1]; + var initial = contents.slice(start(row).offset, start(head).offset); + var final = contents.slice(end(tail).offset, end(row).offset); + + if (position.isGenerated(row)) { + return; + } + + if (initial.indexOf('|') === -1) { + file.warn('Missing initial pipe in table fence', start(row)); + } + + if (final.indexOf('|') === -1) { + file.warn('Missing final pipe in table fence', end(row)); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = tablePipes; diff --git a/lib/rules/unordered-list-marker-style.js b/lib/rules/unordered-list-marker-style.js new file mode 100644 index 0000000..8488ed0 --- /dev/null +++ b/lib/rules/unordered-list-marker-style.js @@ -0,0 +1,121 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module unordered-list-marker-style + * @fileoverview + * Warn when the list-item marker style of unordered lists violate a given + * style. + * + * Options: `string`, either `'consistent'`, `'-'`, `'*'`, or `'*'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used list + * style, and will warn when a subsequent list uses a different + * style. + * @example + * + * - Foo + * - Bar + * + * + * * Foo + * * Bar + * + * + * + Foo + * + Bar + * + * + * + Foo + * - Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Valid styles. + */ + +var STYLES = { + '-': true, + '*': true, + '+': true, + 'null': true +}; + +/** + * Warn when the list-item marker style of unordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Unordered + * list marker style, either `'-'`, `'*'`, or `'+'`, + * defaulting to the first found style. + * @param {Function} done - Callback. + */ +function unorderedListMarkerStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid unordered list-item marker style `' + preferred + '`: use either `\'-\'`, `\'*\'`, or `\'+\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + + if (node.ordered) { + return; + } + + items.forEach(function (item) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + + if (position.isGenerated(item)) { + return; + } + + marker = contents.slice(initial, final).replace(/\s/g, ''); + + /* + * Support checkboxes. + */ + + marker = marker.replace(/\[[x ]?\]\s*$/i, ''); + + if (!preferred) { + preferred = marker; + } else if (marker !== preferred) { + file.warn('Marker style should be `' + preferred + '`', item); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = unorderedListMarkerStyle; diff --git a/lib/sort.js b/lib/sort.js new file mode 100644 index 0000000..355f877 --- /dev/null +++ b/lib/sort.js @@ -0,0 +1,44 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Sort + * @fileoverview mdast plug-in used internally by + * mdast-lint to sort warnings. + * @todo Externalise into its own repository. + */ + +'use strict'; + +/** + * Sort all `file`s messages by line/column. Note that + * this works as a plugin, and will also sort warnings + * added by other plug-ins before `mdast-lint` was added. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + */ +function transformer(ast, file) { + file.messages.sort(function (a, b) { + /* istanbul ignore if - Useful when externalised */ + if (a.line === undefined || b.line === undefined) { + return -1; + } + + return a.line === b.line ? a.column - b.column : a.line - b.line; + }); +} + +/** + * Return `transformer`. + * + * @return {Function} - See `transformer`. + */ +function attacher() { + return transformer; +} + +/* + * Expose. + */ + +module.exports = attacher; diff --git a/lib/utilities/heading-style.js b/lib/utilities/heading-style.js new file mode 100644 index 0000000..2231dc9 --- /dev/null +++ b/lib/utilities/heading-style.js @@ -0,0 +1,79 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module headingStyle + * @fileoverview Utility to check which style a heading + * node is in. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var end = require('./position').end; + +/** + * Get the probable style of an atx-heading, depending on + * preferred style. + * + * @example + * consolidate(1, 'setext') // 'atx' + * consolidate(1, 'atx') // 'atx' + * consolidate(3, 'setext') // 'setext' + * consolidate(3, 'atx') // 'atx' + * + * @param {number} depth + * @param {string?} relative + * @return {string?} - Type. + */ +function consolidate(depth, relative) { + return depth < 3 ? 'atx' : + relative === 'atx' || relative === 'setext' ? relative : null; +} + +/** + * Check the style of a heading. + * + * @param {Node} node + * @param {string?} relative - heading type which we'd wish + * this to be. + * @return {string?} - Type, either `'atx-closed'`, + * `'atx'`, or `'setext'`. + */ +function style(node, relative) { + var last = node.children[node.children.length - 1]; + var depth = node.depth; + + /* + * This can only occur for atx and `'atx-closed'` + * headings. This might incorrectly match `'atx'` + * headings with lots of trailing white space as an + * `'atx-closed'` heading. + */ + + if (!last) { + if (end(node).column < depth * 2) { + return consolidate(depth, relative); + } + + return 'atx-closed'; + } + + if (end(last).line + 1 === end(node).line) { + return 'setext'; + } + + if (end(last).column + depth < end(node).column) { + return 'atx-closed'; + } + + return consolidate(depth, relative); +} + +/* + * Expose. + */ + +module.exports = style; diff --git a/lib/utilities/plural.js b/lib/utilities/plural.js new file mode 100644 index 0000000..0c72d47 --- /dev/null +++ b/lib/utilities/plural.js @@ -0,0 +1,33 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Plural + * @fileoverview Simple functional utility to pluralise + * a word based on a count. + */ + +'use strict'; + +/** + * Simple utility to pluralise `word`, by adding `'s'`, + * when the given `count` is not `1`. + * + * @example + * plural('foo', 0); // 'foos' + * plural('foo', 1); // 'foo' + * plural('foo', 2); // 'foos' + * + * @param {string} word - Singular form. + * @param {number} count - Relative number. + * @return {string} - Original word with an `s` on the end + * if count is not one. + */ +function plural(word, count) { + return (count === 1 ? word : word + 's'); +} + +/* + * Expose. + */ + +module.exports = plural; diff --git a/lib/utilities/position.js b/lib/utilities/position.js new file mode 100644 index 0000000..1464de5 --- /dev/null +++ b/lib/utilities/position.js @@ -0,0 +1,65 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Position + * @fileoverview Utility to get either the starting or the + * ending position of a node, and if its generated + * or not. + */ + +'use strict'; + +/** + * Factory to get a position at `type`. + * + * @param {string} type - Either `'start'` or `'end'`. + * @return {function(Node): Object} + */ +function positionFactory(type) { + /** + * Fet a position in `node` at a bound `type`. + * + * @param {Node} node + * @return {Object} + */ + return function (node) { + return (node && node.position && node.position[type]) || {}; + }; +} + +/* + * Getters. + */ + +var start = positionFactory('start'); +var end = positionFactory('end'); + +/** + * Detect if a node is generated. + * + * @param {Node} node + * @return {boolean} - Whether or not `node` is generated. + */ +function isGenerated(node) { + var initial = start(node); + var final = end(node); + + return initial.line === undefined || initial.column === undefined || + final.line === undefined || final.column === undefined; +} + +/* + * Exports. + */ + +var position = { + 'start': start, + 'end': end, + 'isGenerated': isGenerated +}; + +/* + * Expose. + */ + +module.exports = position; diff --git a/lib/utilities/to-string.js b/lib/utilities/to-string.js new file mode 100644 index 0000000..3e79d95 --- /dev/null +++ b/lib/utilities/to-string.js @@ -0,0 +1,42 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module ToString + * @fileoverview Utility to get the plain text content + * of a node. + */ + +'use strict'; + +/** + * Get the value of `node`. Checks, `value`, + * `alt`, and `title`, in that order. + * + * @param {Node} node + * @return {string} - Textual representation. + */ +function valueOf(node) { + return node && + (node.value ? node.value : + (node.alt ? node.alt : node.title)) || ''; +} + +/** + * Returns the text content of a node. If the node itself + * does not expose plain-text fields, `toString` will + * recursivly try its children. + * + * @param {Node} node + * @return {string} - Textual representation. + */ +function toString(node) { + return valueOf(node) || + (node.children && node.children.map(toString).join('')) || + ''; +} + +/* + * Expose. + */ + +module.exports = toString; diff --git a/lib/utilities/visit.js b/lib/utilities/visit.js new file mode 100644 index 0000000..0f612fb --- /dev/null +++ b/lib/utilities/visit.js @@ -0,0 +1,111 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Visit + * @fileoverview Utility to recursivly walk over mdast + * nodes. + * @todo Externalise into its own repository. + */ + +'use strict'; + +/** + * Walk forwards. + * + * @param {Array.<*>} values + * @param {function(*, number): boolean} callback + * @return {boolean} - False if iteration stopped. + */ +function forwards(values, callback) { + var index = -1; + var length = values.length; + + while (++index < length) { + if (callback(values[index], index) === false) { + return false; + } + } + + return true; +} + +/** + * Walk backwards. + * + * @param {Array.<*>} values + * @param {function(*, number): boolean} callback + * @return {boolean} - False if iteration stopped. + */ +function backwards(values, callback) { + var index = values.length; + var length = -1; + + while (--index > length) { + /* istanbul ignore if - Not used yet... */ + if (callback(values[index], index) === false) { + return false; + } + } + + return true; +} + +/** + * Visit. + * + * @param {Node} tree - Root node + * @param {string} [type] - Optional node type. + * @param {function(node): boolean?} callback - Invoked + * with each found node. Can return `false` to stop + * iteration. + * @param {boolean} [reverse] - By default, `visit` will + * walk forwards, when `reverse` is `true`, `visit` + * walks backwards. + */ +function visit(tree, type, callback, reverse) { + var iterate; + var one; + var all; + + if (typeof type === 'function') { + reverse = callback; + callback = type; + type = null; + } + + iterate = reverse ? backwards : forwards; + + /** + * Visit `children` in `parent`. + */ + all = function (children, parent) { + return iterate(children, function (child, index) { + return one(child, index, parent); + }); + }; + + /** + * Visit a single node. + */ + one = function (node, position, parent) { + var result; + + if (!type || node.type === type) { + result = callback(node, position || 0, parent || null); + } + + if (node.children && result !== false) { + return all(node.children, node); + } + + return result; + }; + + one(tree); +} + +/* + * Expose. + */ + +module.exports = visit; diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..89c57f3 --- /dev/null +++ b/logo.svg @@ -0,0 +1,5 @@ + + + MDAST-LINT + + diff --git a/mdast.js b/mdast.js new file mode 100644 index 0000000..b6fd823 --- /dev/null +++ b/mdast.js @@ -0,0 +1,5949 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.mdast = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o length) { + range = ranges[index]; + + if ( + range.position.line < message.line || + ( + range.position.line === message.line && + range.position.column < message.column + ) + ) { + return range.state === true; + } + } + + /* xistanbul ignore next - Just to be safe */ + return true; + }); +} + +/** + * Return `transformer`. + * + * @return {Function} - See `transformer`. + */ +function attacher() { + return transformer; +} + +/* + * Expose. + */ + +module.exports = attacher; + +},{}],3:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Lint + * @fileoverview mdast plug-in providing warnings when + * detecting style violations. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var range = require('mdast-range'); +var zone = require('mdast-zone'); +var rules = require('./rules'); +var sort = require('./sort'); +var filter = require('./filter'); + +/** + * Factory to create a plugin from a rule. + * + * @example + * attachFactory('foo', console.log, false)() // null + * attachFactory('foo', console.log, {})() // plugin + * + * @param {string} id - Identifier. + * @param {Function} rule - Rule + * @param {*} options - Options for respective rule. + * @return {Function} - See `attach` below. + */ +function attachFactory(id, rule, options) { + /** + * Attach the rule to an mdast instance, unless `false` + * is passed as an option. + * + * @return {Function?} - See `plugin` below. + */ + function attach() { + /** + * Attach the rule to an mdast instance, unless `false` + * is passed as an option. + * + * @param {Node} ast - Root node. + * @param {File} [file] - Virtual file. + * @param {Function} next - Signal end. + */ + function plugin(ast, file, next) { + /* + * Track new messages per file. + */ + + if (file.lintIndex === undefined || file.lintIndex === null) { + file.lintIndex = file.messages.length; + } + + /** + * Add `ruleId` to each new message. + * + * @param {Error?} err - Optional failure. + */ + function done(err) { + var messages = file.messages; + + while (file.lintIndex < messages.length) { + messages[file.lintIndex].ruleId = id; + + file.lintIndex++; + } + + next(err); + } + + /* + * Invoke `rule`, with `options` + */ + + rule(ast, file, options, done); + } + + return options === false ? null : plugin; + } + + return attach; +} + +/** + * Lint attacher. + * + * By default, all rules are turned on unless explicitly + * set to `false`. When `reset: true`, the opposite is + * true: all rules are turned off, unless when given + * a non-nully and non-false value. + * + * @example + * var processor = lint(mdast, { + * 'html': false // Ignore HTML warnings. + * }); + * + * @param {MDAST} mdast - Host object. + * @param {Object?} options - Hash of rule names mapping to + * rule options. + */ +function lint(mdast, options) { + var settings = options || {}; + var reset = settings.reset; + var id; + var setting; + + /* + * Ensure offset information is added. + */ + + mdast.use(range); + + /** + * Get the latest state of a rule. + * + * @param {string} ruleId + * @param {File} [file] + */ + function getState(ruleId, file) { + var ranges = file && file.lintRanges && file.lintRanges[ruleId]; + + if (ranges) { + return ranges[ranges.length - 1].state; + } + + setting = settings[ruleId]; + + if (setting === false) { + return false; + } + + return !reset || (setting !== null && setting !== undefined); + } + + /** + * Store settings on `file`. + * + * @param {File} file + */ + function store(file) { + var ranges = file.lintRanges; + var ruleId; + + if (!ranges) { + ranges = {}; + + for (ruleId in rules) { + ranges[ruleId] = [{ + 'state': getState(ruleId), + 'position': { + 'line': 0, + 'column': 0 + } + }]; + } + + file.lintRanges = ranges; + } + } + + mdast.use(function () { + return function (ast, file) { + store(file); + }; + }); + + /* + * Add each rule as a seperate plugin. + */ + + for (id in rules) { + mdast.use(attachFactory(id, rules[id], settings[id])); + } + + /** + * Handle a new-found marker. + * + * @param {Object} marker + * @param {Object} parser + */ + function onparse(marker, parser) { + var file = parser.file; + var attributes = marker.attributes.split(' '); + var type = attributes[0]; + var ruleId = attributes[1]; + var markers; + var currentState; + var previousState; + + store(file); + + if (type !== 'disable' && type !== 'enable') { + file.fail('Unknown lint keyword `' + type + '`: use either `\'enable\'` or `\'disable\'`', marker.node); + + return; + } + + if (!(ruleId in rules)) { + file.fail('Unknown rule: cannot ' + type + ' `\'' + ruleId + '\'`', marker.node); + + return; + } + + markers = file.lintRanges[ruleId]; + + previousState = getState(ruleId, file); + currentState = type === 'enable'; + + if (currentState !== previousState) { + markers.push({ + 'state': currentState, + 'position': marker.node.position.start + }); + } + } + + mdast.use(zone({ + 'name': 'lint', + 'onparse': onparse + })); + + /* + * Sort messages. + */ + + mdast.use(sort); + + /* + * Filter. + */ + + mdast.use(filter); +} + +/* + * Expose. + */ + +module.exports = lint; + +},{"./filter":2,"./rules":18,"./sort":58,"mdast-range":64,"mdast-zone":65}],4:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module blockquote-indentation + * @fileoverview + * Warn when blockquotes are either indented too much or too little. + * + * Options: `number`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used indentation + * and will warn when other blockquotes use a different indentation. + * @example + * + * > Hello + * ... + * > World + * + * + * > Hello + * ... + * > World + * + * + * > Hello + * ... + * > World + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var plural = require('../utilities/plural'); +var position = require('../utilities/position'); + +/** + * Get the indent of a blockquote. + * + * @param {Node} node + * @return {number} - Indentation. + */ +function check(node) { + var head = node.children[0]; + var indentation = position.start(head).column - position.start(node).column; + var padding = toString(head).match(/^ +/); + + if (padding) { + indentation += padding[0].length; + } + + return indentation; +} + +/** + * Warn when a blockquote has a too large or too small + * indentation. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {number?} [preferred='consistent'] - Preferred + * indentation between a blockquote and its content. + * When not a number, defaults to the first found + * indentation. + * @param {Function} done - Callback. + */ +function blockquoteIndentation(ast, file, preferred, done) { + preferred = isNaN(preferred) || typeof preferred !== 'number' ? null : preferred; + + visit(ast, 'blockquote', function (node) { + var indent; + var diff; + var word; + + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + indent = check(node); + diff = preferred - indent; + word = diff > 0 ? 'Add' : 'Remove'; + + diff = Math.abs(diff); + + if (diff !== 0) { + file.warn( + word + ' ' + diff + ' ' + plural('space', diff) + + ' between blockquote and content', + position.start(node.children[0]) + ); + } + } else { + preferred = check(node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = blockquoteIndentation; + +},{"../utilities/plural":60,"../utilities/position":61,"../utilities/to-string":62,"../utilities/visit":63}],5:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module code-block-style + * @fileoverview + * Warn when code-blocks do not adhere to a given style. + * + * Options: `string`, either `'consistent'`, `'fences'`, or `'indented'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used code-block + * style, and will warn when a subsequent code-block uses a different + * style. + * @example + * + * Hello + * + * ... + * + * World + * + * + * ``` + * Hello + * ``` + * ... + * ```bar + * World + * ``` + * + * + * Hello + * ... + * ``` + * World + * ``` + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/* + * Valid styles. + */ + +var STYLES = { + 'null': true, + 'fenced': true, + 'indented': true +}; + +/** + * Warn for violating code-block style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * code block style. Defaults to `'consistent'` when + * not a a string. Otherwise, should be one of + * `'fenced'` or `'indented'`. + * @param {Function} done - Callback. + */ +function codeBlockStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid code block style `' + preferred + '`: use either `\'consistent\'`, `\'fenced\'`, or `\'indented\'`'); + + return; + } + + /** + * Get the style of `node`. + * + * @param {Node} node + * @return {string?} - `'fenced'`, `'indented'`, or + * `null`. + */ + function check(node) { + var initial = start(node).offset; + var final = end(node).offset; + + if (position.isGenerated(node)) { + return null; + } + + if ( + node.lang || + /^\s*([~`])\1{2,}/.test(contents.slice(initial, final)) + ) { + return 'fenced'; + } + + return 'indented'; + } + + visit(ast, 'code', function (node) { + var current = check(node); + + if (!current) { + return; + } + + if (!preferred) { + preferred = current; + } else if (preferred !== current) { + file.warn('Code blocks should be ' + preferred, node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = codeBlockStyle; + +},{"../utilities/position":61,"../utilities/visit":63}],6:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module definition-case + * @fileoverview + * Warn when definition labels are not lower-case. + * @example + * + * [example] http://example.com "Example Domain" + * + * + * ![Example] http://example.com/favicon.ico "Example image" + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Expressions. + */ + +var LABEL = /^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/; + +/** + * Warn when definitions are not placed at the end of the + * file. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function definitionCase(ast, file, preferred, done) { + var contents = file.toString(); + + /** + * Validate a node, either a normal definition or + * a footnote definition. + * + * @param {Node} node + */ + function validate(node) { + var start = position.start(node).offset; + var end = position.end(node).offset; + var label; + + if (position.isGenerated(node)) { + return; + } + + label = contents.slice(start, end).match(LABEL)[1]; + + if (label !== label.toLowerCase()) { + file.warn('Do not use uppper-case characters in definition labels', node); + } + } + + visit(ast, 'definition', validate); + visit(ast, 'footnoteDefinition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = definitionCase; + +},{"../utilities/position":61,"../utilities/visit":63}],7:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module definition-spacing + * @fileoverview + * Warn when consecutive white space is used in a definition. + * @example + * + * [example domain] http://example.com "Example Domain" + * + * + * ![example image] http://example.com/favicon.ico "Example image" + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Expressions. + */ + +var LABEL = /^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/; + +/** + * Warn when consecutive white space is used in a + * definition. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function definitionSpacing(ast, file, preferred, done) { + var contents = file.toString(); + + /** + * Validate a node, either a normal definition or + * a footnote definition. + * + * @param {Node} node + */ + function validate(node) { + var start = position.start(node).offset; + var end = position.end(node).offset; + var label; + + if (position.isGenerated(node)) { + return; + } + + label = contents.slice(start, end).match(LABEL)[1]; + + if (/[ \t\n]{2,}/.test(label)) { + file.warn('Do not use consecutive white-space in definition labels', node); + } + } + + visit(ast, 'definition', validate); + visit(ast, 'footnoteDefinition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = definitionSpacing; + +},{"../utilities/position":61,"../utilities/visit":63}],8:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module emphasis-marker + * @fileoverview + * Warn for violating emphasis markers. + * + * Options: `string`, either `'consistent'`, `'*'`, or `'_'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used emphasis + * style, and will warn when a subsequent emphasis uses a different + * style. + * @example + * + * *foo* + * *bar* + * + * + * _foo_ + * _bar_ + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '*': true, + '_': true, + 'null': true +}; + +/** + * Warn when an `emphasis` node has an incorrect marker. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `'*'` or `'_'`, or `'consistent'`. + * @param {Function} done - Callback. + */ +function emphasisMarker(ast, file, preferred, done) { + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (MARKERS[preferred] !== true) { + file.fail('Invalid emphasis marker `' + preferred + '`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`'); + + return; + } + + visit(ast, 'emphasis', function (node) { + var marker = file.toString().charAt(position.start(node).offset); + + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + if (marker !== preferred) { + file.warn('Emphasis should use `' + preferred + '` as a marker', node); + } + } else { + preferred = marker; + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = emphasisMarker; + +},{"../utilities/position":61,"../utilities/visit":63}],9:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module fenced-code-flag + * @fileoverview + * Warn when fenced code blocks occur without language flag. + * + * Options: `Array.` or `Object`. + * + * Providing an array, is a shortcut for just providing the `flags` + * property on the object. + * + * The object can have an array of flags which are deemed valid. + * In addition it can have the property `allowEmpty` (`boolean`) + * which signifies whether or not to warn for fenced code-blocks without + * languge flags. + * @example + * + * ```hello + * world(); + * ``` + * + * + * Hello + * + * + * ``` + * world(); + * ``` + * + * + * ``` + * world(); + * ``` + * + * + * ```hello + * world(); + * ``` + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn for fenced code blocks without language flag. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {Object|Array.} [preferred] - List + * of flags deemed valid. + * @param {Function} done - Callback. + */ +function fencedCodeFlag(ast, file, preferred, done) { + var contents = file.toString(); + var allowEmpty = false; + var flags = []; + + if (typeof preferred === 'object' && !('length' in preferred)) { + allowEmpty = Boolean(preferred.allowEmpty); + + preferred = preferred.flags; + } + + if (typeof preferred === 'object' && 'length' in preferred) { + flags = String(preferred).split(','); + } + + visit(ast, 'code', function (node) { + var value = contents.slice(start(node).offset, end(node).offset); + + if (position.isGenerated(node)) { + return; + } + + if (node.lang) { + if (flags.length && flags.indexOf(node.lang) === -1) { + file.warn('Invalid code-language flag', node); + } + } else if (/^\ {0,3}([~`])\1{2,}/.test(value) && !allowEmpty) { + file.warn('Missing code-language flag', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = fencedCodeFlag; + +},{"../utilities/position":61,"../utilities/visit":63}],10:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module fenced-code-marker + * @fileoverview + * Warn for violating fenced code markers. + * + * Options: `string`, either `` '`' ``, or `'~'`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used fenced code + * marker style, and will warn when a subsequent fenced code uses a + * different style. + * @example + * + * ```foo + * bar(); + * ``` + * + * ``` + * baz(); + * ``` + * + * + * ~~~foo + * bar(); + * ~~~ + * + * ~~~ + * baz(); + * ~~~ + * + * + * ~~~foo + * bar(); + * ~~~ + * + * ``` + * baz(); + * ``` + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '`': true, + '~': true, + 'null': true +}; + +/** + * Warn for violating fenced code markers. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `` '`' `` or `~`, or `'consistent'`. + * @param {Function} done - Callback. + */ +function fencedCodeMarker(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (MARKERS[preferred] !== true) { + file.fail('Invalid fenced code marker `' + preferred + '`: use either `\'consistent\'`, `` \'\`\' ``, or `\'~\'`'); + + return; + } + + visit(ast, 'code', function (node) { + var marker = contents.substr(position.start(node).offset, 4); + + if (position.isGenerated(node)) { + return; + } + + marker = marker.trimLeft().charAt(0); + + /* + * Ignore unfenced code blocks. + */ + + if (MARKERS[marker] !== true) { + return; + } + + if (preferred) { + if (marker !== preferred) { + file.warn('Fenced code should use ' + preferred + ' as a marker', node); + } + } else { + preferred = marker; + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = fencedCodeMarker; + +},{"../utilities/position":61,"../utilities/visit":63}],11:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module file-extension + * @fileoverview + * Warn when the document’s extension differs from the given preferred + * extension. + * + * Does not warn when given documents have no file extensions (such as + * `AUTHORS` or `LICENSE`). + * + * Options: `string`, default: `'md'` — Expected file extension. + * @example + * Invalid (when `'md'`): readme.mkd, readme.markdown, etc. + * Valid (when `'md'`): readme, readme.md + */ + +'use strict'; + +/** + * Check file extensions. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='md'] - Expected file + * extension. + * @param {Function} done - Callback. + */ +function fileExtension(ast, file, preferred, done) { + var ext = file.extension; + + preferred = typeof preferred === 'string' ? preferred : 'md'; + + if (ext !== '' && ext !== preferred) { + file.warn('Invalid extension: use `' + preferred + '`'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = fileExtension; + +},{}],12:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module final-definition + * @fileoverview + * Warn when definitions are not placed at the end of the file. + * @example + * + * ... + * + * [example] http://example.com "Example Domain" + * + * + * ... + * + * [example] http://example.com "Example Domain" + * + * A trailing paragraph. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when definitions are not placed at the end of + * the file. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function finalDefinition(ast, file, preferred, done) { + var last = null; + + visit(ast, function (node) { + var line = start(node).line; + + /* + * Ignore generated nodes. + */ + + if (node.type === 'root' || position.isGenerated(node)) { + return; + } + + if (node.type === 'definition') { + if (last !== null && last > line) { + file.warn('Move definitions to the end of the file (after the node at line `' + last + '`)', node); + } + } else if (last === null) { + last = line; + } + }, true); + + done(); +} + +/* + * Expose. + */ + +module.exports = finalDefinition; + +},{"../utilities/position":61,"../utilities/visit":63}],13:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module final-newline + * @fileoverview + * Warn when a newline at the end of a file is missing. + * + * See [StackExchange](http://unix.stackexchange.com/questions/18743) for + * why. + */ + +'use strict'; + +/** + * Warn when the list-item marker style of unordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function finalNewline(ast, file, preferred, done) { + var contents = file.toString(); + var last = contents.length - 1; + + if (last > 0 && contents.charAt(last) !== '\n') { + file.warn('Missing newline character at end of file'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = finalNewline; + +},{}],14:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module first-heading-level + * @fileoverview + * Warn when the first heading has a level other than `1`. + * @example + * + * # Foo + * + * ## Bar + * + * + * ## Foo + * + * # Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when the first heading has a level other than `1`. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function firstHeadingLevel(ast, file, preferred, done) { + visit(ast, 'heading', function (node) { + if (position.isGenerated(node)) { + return null; + } + + if (node.depth !== 1) { + file.warn('First heading level should be `1`', node); + } + + return false; + }); + + done(); +} + +module.exports = firstHeadingLevel; + +},{"../utilities/position":61,"../utilities/visit":63}],15:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module hard-break-spaces + * @fileoverview + * Warn when too many spaces are used to create a hard break. + * @example + * + * + * + * Lorem ipsum·· + * dolor sit amet + * + * + * Lorem ipsum··· + * dolor sit amet. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when too many spaces are used to create a + * hard break. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function hardBreakSpaces(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'break', function (node) { + var start = position.start(node).offset; + var end = position.end(node).offset; + + if (position.isGenerated(node)) { + return; + } + + if (contents.slice(start, end).length > 3) { + file.warn('Use two spaces for hard line breaks', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = hardBreakSpaces; + +},{"../utilities/position":61,"../utilities/visit":63}],16:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module heading-increment + * @fileoverview + * Warn when headings increment with more than 1 level at a time. + * @example + * + * # Foo + * + * ## Bar + * + * + * # Foo + * + * ### Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when headings increment with more than 1 level at + * a time. + * + * Never warns for the first heading. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function headingIncrement(ast, file, preferred, done) { + var prev = null; + + visit(ast, 'heading', function (node) { + var depth = node.depth; + + if (position.isGenerated(node)) { + return; + } + + if (prev && depth > prev + 1) { + file.warn('Heading levels should increment by one level at a time', node); + } + + prev = depth; + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = headingIncrement; + +},{"../utilities/position":61,"../utilities/visit":63}],17:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module heading-style + * @fileoverview + * Warn when a heading does not conform to a given style. + * + * Options: `string`, either `'consistent'`, `'atx'`, `'atx-closed'`, + * or `'setext'`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used heading + * style, and will warn when a subsequent heading uses a different + * style. + * @example + * + * # Foo + * + * ## Bar + * + * ### Baz + * + * + * # Foo # + * + * ## Bar # + * + * ### Baz ### + * + * + * Foo + * === + * + * Bar + * --- + * + * ### Baz + * + * + * Foo + * === + * + * ## Bar + * + * ### Baz ### + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var style = require('../utilities/heading-style'); +var position = require('../utilities/position'); + +/* + * Types. + */ + +var TYPES = ['atx', 'atx-closed', 'setext']; + +/** + * Warn when a heading does not conform to a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string} [preferred='consistent'] - Preferred + * style, one of `atx`, `atx-closed`, or `setext`. + * Other values default to `'consistent'`, which will + * detect the first used style. + * @param {Function} done - Callback. + */ +function headingStyle(ast, file, preferred, done) { + preferred = TYPES.indexOf(preferred) === -1 ? null : preferred; + + visit(ast, 'heading', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + if (style(node, preferred) !== preferred) { + file.warn('Headings should use ' + preferred, node); + } + } else { + preferred = style(node, preferred); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = headingStyle; + +},{"../utilities/heading-style":59,"../utilities/position":61,"../utilities/visit":63}],18:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Rules + * @fileoverview Map of rule id’s to rules. + */ + +'use strict'; + +/* + * Expose. + */ + +module.exports = { + 'no-auto-link-without-protocol': require('./no-auto-link-without-protocol'), + 'no-literal-urls': require('./no-literal-urls'), + 'no-consecutive-blank-lines': require('./no-consecutive-blank-lines'), + 'no-missing-blank-lines': require('./no-missing-blank-lines'), + 'blockquote-indentation': require('./blockquote-indentation'), + 'no-blockquote-without-caret': require('./no-blockquote-without-caret'), + 'code-block-style': require('./code-block-style'), + 'definition-case': require('./definition-case'), + 'definition-spacing': require('./definition-spacing'), + 'no-emphasis-as-heading': require('./no-emphasis-as-heading'), + 'emphasis-marker': require('./emphasis-marker'), + 'fenced-code-flag': require('./fenced-code-flag'), + 'fenced-code-marker': require('./fenced-code-marker'), + 'file-extension': require('./file-extension'), + 'final-newline': require('./final-newline'), + 'no-file-name-articles': require('./no-file-name-articles'), + 'no-file-name-consecutive-dashes': require('./no-file-name-consecutive-dashes'), + 'no-file-name-irregular-characters': require('./no-file-name-irregular-characters'), + 'no-file-name-mixed-case': require('./no-file-name-mixed-case'), + 'no-file-name-outer-dashes': require('./no-file-name-outer-dashes'), + 'final-definition': require('./final-definition'), + 'hard-break-spaces': require('./hard-break-spaces'), + 'heading-increment': require('./heading-increment'), + 'no-heading-content-indent': require('./no-heading-content-indent'), + 'no-heading-indent': require('./no-heading-indent'), + 'first-heading-level': require('./first-heading-level'), + 'maximum-heading-length': require('./maximum-heading-length'), + 'no-heading-punctuation': require('./no-heading-punctuation'), + 'heading-style': require('./heading-style'), + 'no-multiple-toplevel-headings': require('./no-multiple-toplevel-headings'), + 'no-duplicate-headings': require('./no-duplicate-headings'), + 'no-duplicate-definitions': require('./no-duplicate-definitions'), + 'no-html': require('./no-html'), + 'no-inline-padding': require('./no-inline-padding'), + 'maximum-line-length': require('./maximum-line-length'), + 'link-title-style': require('./link-title-style'), + 'list-item-bullet-indent': require('./list-item-bullet-indent'), + 'list-item-content-indent': require('./list-item-content-indent'), + 'list-item-indent': require('./list-item-indent'), + 'list-item-spacing': require('./list-item-spacing'), + 'ordered-list-marker-style': require('./ordered-list-marker-style'), + 'ordered-list-marker-value': require('./ordered-list-marker-value'), + 'no-shortcut-reference-image': require('./no-shortcut-reference-image'), + 'no-shortcut-reference-link': require('./no-shortcut-reference-link'), + 'rule-style': require('./rule-style'), + 'no-shell-dollars': require('./no-shell-dollars'), + 'strong-marker': require('./strong-marker'), + 'no-table-indentation': require('./no-table-indentation'), + 'table-pipe-alignment': require('./table-pipe-alignment'), + 'table-cell-padding': require('./table-cell-padding'), + 'table-pipes': require('./table-pipes'), + 'no-tabs': require('./no-tabs'), + 'unordered-list-marker-style': require('./unordered-list-marker-style') +}; + +},{"./blockquote-indentation":4,"./code-block-style":5,"./definition-case":6,"./definition-spacing":7,"./emphasis-marker":8,"./fenced-code-flag":9,"./fenced-code-marker":10,"./file-extension":11,"./final-definition":12,"./final-newline":13,"./first-heading-level":14,"./hard-break-spaces":15,"./heading-increment":16,"./heading-style":17,"./link-title-style":19,"./list-item-bullet-indent":20,"./list-item-content-indent":21,"./list-item-indent":22,"./list-item-spacing":23,"./maximum-heading-length":24,"./maximum-line-length":25,"./no-auto-link-without-protocol":26,"./no-blockquote-without-caret":27,"./no-consecutive-blank-lines":28,"./no-duplicate-definitions":29,"./no-duplicate-headings":30,"./no-emphasis-as-heading":31,"./no-file-name-articles":32,"./no-file-name-consecutive-dashes":33,"./no-file-name-irregular-characters":34,"./no-file-name-mixed-case":35,"./no-file-name-outer-dashes":36,"./no-heading-content-indent":37,"./no-heading-indent":38,"./no-heading-punctuation":39,"./no-html":40,"./no-inline-padding":41,"./no-literal-urls":42,"./no-missing-blank-lines":43,"./no-multiple-toplevel-headings":44,"./no-shell-dollars":45,"./no-shortcut-reference-image":46,"./no-shortcut-reference-link":47,"./no-table-indentation":48,"./no-tabs":49,"./ordered-list-marker-style":50,"./ordered-list-marker-value":51,"./rule-style":52,"./strong-marker":53,"./table-cell-padding":54,"./table-pipe-alignment":55,"./table-pipes":56,"./unordered-list-marker-style":57}],19:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module link-title-style + * @fileoverview + * Warn when link and definition titles occur with incorrect quotes. + * + * Options: `string`, either `'consistent'`, `'"'`, `'\''`, or + * `'()'`, default: `'consistent'`. + * + * The default value, `consistent`, detects the first used quote + * style, and will warn when a subsequent titles use a different + * style. + * @example + * + * [Example](http://example.com "Example Domain") + * [Example](http://example.com "Example Domain") + * + * + * [Example](http://example.com 'Example Domain') + * [Example](http://example.com 'Example Domain') + * + * + * [Example](http://example.com (Example Domain)) + * [Example](http://example.com (Example Domain)) + * + * + * [Example](http://example.com "Example Domain") + * [Example](http://example.com 'Example Domain') + * [Example](http://example.com (Example Domain)) + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '"': true, + '\'': true, + ')': true, + 'null': true +}; + +/* + * Methods. + */ + +var end = position.end; + +/** + * Warn for fenced code blocks without language flag. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `'"'`, `'\''`, `'()'`, or `'consistent'`. + * @param {Function} done - Callback. + */ +function linkTitleStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (preferred === '()' || preferred === '(') { + preferred = ')'; + } + + if (MARKERS[preferred] !== true) { + file.fail('Invalid link title style marker `' + preferred + '`: use either `\'consistent\'`, `\'"\'`, `\'\\\'\'`, or `\'()\'`'); + + return; + } + + /** + * Validate a single node. + * + * @param {Node} node + */ + function validate(node) { + var last = end(node).offset - 1; + var character; + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (node.type !== 'definition') { + last--; + } + + while (last) { + character = contents.charAt(last); + + if (/\s/.test(character)) { + last--; + } else { + break; + } + } + + /* + * Not a title. + */ + + if (!(character in MARKERS)) { + return; + } + + + if (!preferred) { + preferred = character; + } else if (preferred !== character) { + pos = file.offsetToPosition(last + 1); + file.warn('Titles should use `' + (preferred === ')' ? '()' : preferred) + '` as a quote', pos); + } + } + + visit(ast, 'link', validate); + visit(ast, 'image', validate); + visit(ast, 'definition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = linkTitleStyle; + +},{"../utilities/position":61,"../utilities/visit":63}],20:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-bullet-indent + * @fileoverview + * Warn when list item bullets are indented. + * @example + * + * * List item + * * List item + * + * + * * List item + * * List item + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when list item bullets are indented. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function listItemBulletIndent(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'list', function (node) { + var items = node.children; + + items.forEach(function (item) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var indent; + + if (position.isGenerated(node)) { + return; + } + + indent = contents.slice(initial, final).match(/^\s*/)[0].length; + + if (indent !== 0) { + initial = start(head); + + file.warn('Incorrect indentation before bullet: remove ' + indent + ' ' + plural('space', indent), { + 'line': initial.line, + 'column': initial.column - indent + }); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemBulletIndent; + +},{"../utilities/plural":60,"../utilities/position":61,"../utilities/visit":63}],21:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-content-indent + * @fileoverview + * Warn when the content of a list item has mixed indentation. + * @example + * + * * List item + * + * * Nested list item indented by 4 spaces + * + * + * * List item + * + * * Nested list item indented by 3 spaces + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when the content of a list item has mixed + * indentation. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function listItemContentIndent(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'listItem', function (node) { + var style; + + node.children.forEach(function (item, index) { + var begin = start(item); + var column = begin.column; + var char; + var diff; + var word; + + if (position.isGenerated(item)) { + return; + } + + /* + * Get indentation for the first child. + * Only the first item can have a checkbox, + * so here we remove that from the column. + */ + + if (index === 0) { + if (Boolean(node.checked) === node.checked) { + char = begin.offset; + + while (contents.charAt(char) !== '[') { + char--; + } + + column -= begin.offset - char; + } + + style = column; + + return; + } + + /* + * Warn for violating children. + */ + + if (column !== style) { + diff = style - column; + word = diff > 0 ? 'add' : 'remove'; + + diff = Math.abs(diff); + + file.warn( + 'Don’t use mixed indentation for children, ' + word + + ' ' + diff + ' ' + plural('space', diff), + { + 'line': start(item).line, + 'column': column + } + ); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemContentIndent; + +},{"../utilities/plural":60,"../utilities/position":61,"../utilities/visit":63}],22:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-indent + * @fileoverview + * Warn when the spacing between a list item’s bullet and its content + * violates a given style. + * + * Options: `string`, either `'tab-size'`, `'mixed'`, or `'space'`, + * default: `'tab-size'`. + * @example + * + * * List + * item. + * + * 11. List + * item. + * + * + * * List item. + * + * 11. List item + * + * * List + * item. + * + * 11. List + * item. + * + * + * * List item. + * + * 11. List item + * + * * List + * item. + * + * 11. List + * item. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Styles. + */ + +var STYLES = { + 'tab-size': true, + 'mixed': true, + 'space': true +}; + +/** + * Warn when the spacing between a list item’s bullet and + * its content violates a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='tab-size'] - Either + * `'tab-size'`, `'space'`, or `'mixed'`, defaulting + * to the first. + * @param {Function} done - Callback. + */ +function listItemIndent(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' ? 'tab-size' : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid list-item indent style `' + preferred + '`: use either `\'tab-size\'`, `\'space\'`, or `\'mixed\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + var isOrdered = node.ordered; + var offset = node.start || 1; + + if (position.isGenerated(node)) { + return; + } + + items.forEach(function (item, index) { + var head = item.children[0]; + var bulletSize = isOrdered ? String(offset + index).length + 1 : 1; + var tab = Math.ceil(bulletSize / 4) * 4; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + var shouldBe; + var diff; + var word; + + marker = contents.slice(initial, final); + + /* + * Support checkboxes. + */ + + marker = marker.replace(/\[[x ]?\]\s*$/i, ''); + + if (preferred === 'tab-size') { + shouldBe = tab; + } else if (preferred === 'space') { + shouldBe = bulletSize + 1; + } else { + shouldBe = node.loose ? tab : bulletSize + 1; + } + + if (marker.length !== shouldBe) { + diff = shouldBe - marker.length; + word = diff > 0 ? 'add' : 'remove'; + + diff = Math.abs(diff); + + file.warn( + 'Incorrect list-item indent: ' + word + + ' ' + diff + ' ' + plural('space', diff), + start(head) + ); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemIndent; + +},{"../utilities/plural":60,"../utilities/position":61,"../utilities/visit":63}],23:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module list-item-spacing + * @fileoverview + * Warn when list looseness is incorrect, such as being tight + * when it should be loose, and vice versa. + * @example + * + * - Wrapped + * item + * + * - item 2 + * + * - item 3 + * + * + * - item 1 + * - item 2 + * - item 3 + * + * + * - Wrapped + * item + * - item 2 + * - item 3 + * + * + * - item 1 + * + * - item 2 + * + * - item 3 + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when list items looseness is incorrect, such as + * being tight when it should be loose, and vice versa. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function listItemSpacing(ast, file, preferred, done) { + visit(ast, 'list', function (node) { + var items = node.children; + var isTightList = true; + var indent = start(node).column; + var type; + + if (position.isGenerated(node)) { + return; + } + + items.forEach(function (item) { + var content = item.children; + var head = content[0]; + var tail = content[content.length - 1]; + var isLoose = (end(tail).line - start(head).line) > 0; + + if (isLoose) { + isTightList = false; + } + }); + + type = isTightList ? 'tight' : 'loose'; + + items.forEach(function (item, index) { + var next = items[index + 1]; + var isTight = end(item).column > indent; + + /* + * Ignore last. + */ + + if (!next) { + return; + } + + /* + * Check if the list item's state does (not) + * match the list's state. + */ + + if (isTight !== isTightList) { + file.warn('List item should be ' + type + ', isn’t', { + 'start': end(item), + 'end': start(next) + }); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = listItemSpacing; + +},{"../utilities/position":61,"../utilities/visit":63}],24:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module maximum-heading-length + * @fileoverview + * Warn when headings are too long. + * + * Options: `number`, default: `60`. + * + * Ignores markdown syntax, only checks the plain text content. + * @example + * + * # Alpha bravo charlie delta echo + * # ![Alpha bravo charlie delta echo](http://example.com/nato.png) + * + * + * # Alpha bravo charlie delta echo foxtrot + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var position = require('../utilities/position'); + +/** + * Warn when headings are too long. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {number?} [preferred=60] - Maximum content + * length. + * @param {Function} done - Callback. + */ +function maximumHeadingLength(ast, file, preferred, done) { + preferred = isNaN(preferred) || typeof preferred !== 'number' ? 60 : preferred; + + visit(ast, 'heading', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (toString(node).length > preferred) { + file.warn('Use headings shorter than `' + preferred + '`', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = maximumHeadingLength; + +},{"../utilities/position":61,"../utilities/to-string":62,"../utilities/visit":63}],25:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module maximum-line-length + * @fileoverview + * Warn when lines are too long. + * + * Options: `number`, default: `80`. + * + * Ignores nodes which cannot be wrapped, such as heasings, tables, + * code, and links. + * @example + * + * Alpha bravo charlie delta echo. + * + * Alpha bravo charlie delta echo [foxtrot](./foxtrot.html). + * + * # Alpha bravo charlie delta echo foxtrot golf hotel. + * + * # Alpha bravo charlie delta echo foxtrot golf hotel. + * + * | A | B | C | D | E | F | F | H | + * | ----- | ----- | ------- | ----- | ---- | ------- | ---- | ----- | + * | Alpha | bravo | charlie | delta | echo | foxtrot | golf | hotel | + * + * + * Alpha bravo charlie delta echo foxtrot golf. + * + * Alpha bravo charlie delta echo [foxtrot](./foxtrot.html) golf. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Check if `node` is applicable, as in, if it should be + * ignored. + * + * @param {Node} node + * @return {boolean} - Whether or not `node` should be + * ignored. + */ +function isApplicable(node) { + return node.type === 'heading' || + node.type === 'table' || + node.type === 'code'; +} + +/** + * Warn when lines are too long. This rule is forgiving + * about lines which cannot be wrapped, such as code, + * tables, and headings, or links at the enc of a line. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {number?} [preferred=80] - Maximum line length. + * @param {Function} done - Callback. + */ +function maximumLineLength(ast, file, preferred, done) { + var style = preferred && preferred !== true ? preferred : 80; + var content = file.toString(); + var matrix = content.split('\n'); + var index = -1; + var length = matrix.length; + var lineLength; + + /** + * Whitelist from `initial` to `final`, zero-based. + * + * @param {number} initial + * @param {number} final + */ + function whitelist(initial, final) { + initial--; + + while (++initial < final) { + matrix[initial] = ''; + } + } + + /* + * Next, white list nodes which cannot be wrapped. + */ + + visit(ast, function (node) { + var applicable = isApplicable(node); + var initial = applicable && start(node).line; + var final = applicable && end(node).line; + + if (!applicable || position.isGenerated(node)) { + return; + } + + whitelist(initial - 1, final); + }); + + /* + * Finally, whitelist URLs, but only if they occur at + * or after the wrap. However, when they do, and + * there’s white-space after it, they are not + * whitelisted. + */ + + visit(ast, 'link', function (node, pos, parent) { + var next = parent.children[pos + 1]; + var initial = start(node); + var final = end(node); + + /* + * Nothing to whitelist when generated. + */ + + if (position.isGenerated(node)) { + return; + } + + /* + * No whitelisting when starting after the border, + * or ending before it. + */ + + if (initial.column > style || final.column < style) { + return; + } + + /* + * No whitelisting when there’s white-space after + * the link. + */ + + if ( + next && + start(next).line === initial.line && + (!next.value || /^(.+?[ \t].+?)/.test(next.value)) + ) { + return; + } + + whitelist(initial.line - 1, final.line); + }); + + /* + * Iterate over every line, and warn for + * violating lines. + */ + + while (++index < length) { + lineLength = matrix[index].length; + + if (lineLength > style) { + file.warn('Line must be at most ' + style + ' characters', { + 'line': index + 1, + 'column': lineLength + 1 + }); + } + } + + done(); +} + +/* + * Expose. + */ + +module.exports = maximumLineLength; + +},{"../utilities/position":61,"../utilities/visit":63}],26:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-auto-link-without-protocol + * @fileoverview + * Warn for angle-bracketed links without protocol. + * @example + * + * + * + * + * + * + * + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Protocol expression. + * + * @type {RegExp} + * @see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax + */ + +var PROTOCOL = /^[a-z][a-z+.-]+:\/?/i; + +/** + * Assert `node`s reference starts with a protocol. + * + * @param {Node} node + * @return {boolean} + */ +function hasProtocol(node) { + return PROTOCOL.test(toString(node)); +} + +/** + * Warn for angle-bracketed links without protocol. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noAutoLinkWithoutProtocol(ast, file, preferred, done) { + visit(ast, 'link', function (node) { + var head = start(node.children[0]).column; + var tail = end(node.children[node.children.length - 1]).column; + var initial = start(node).column; + var final = end(node).column; + + if (position.isGenerated(node)) { + return; + } + + if (initial === head - 1 && final === tail + 1 && !hasProtocol(node)) { + file.warn('All automatic links must start with a protocol', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noAutoLinkWithoutProtocol; + +},{"../utilities/position":61,"../utilities/to-string":62,"../utilities/visit":63}],27:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-blockquote-without-caret + * @fileoverview + * Warn when blank lines without carets are found in a blockquote. + * @example + * + * > Foo... + * > + * > ...Bar. + * + * + * > Foo... + * + * > ...Bar. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when blank lines without carets are found in a + * blockquote. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noBlockquoteWithoutCaret(ast, file, preferred, done) { + var contents = file.toString(); + var last = contents.length; + + visit(ast, 'blockquote', function (node) { + var start = position.start(node).line; + var indent = node.position && node.position.indent; + + if (position.isGenerated(node) || !indent || !indent.length) { + return; + } + + indent.forEach(function (column, n) { + var character; + var line = start + n + 1; + var offset = file.positionToOffset({ + 'line': line, + 'column': column + }) - 1; + + while (++offset < last) { + character = contents.charAt(offset); + + if (character === '>') { + return; + } + + /* istanbul ignore else - just for safety */ + if (character !== ' ' && character !== '\t') { + break; + } + } + + file.warn('Missing caret in blockquote', { + 'line': line, + 'column': column + }); + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noBlockquoteWithoutCaret; + +},{"../utilities/position":61,"../utilities/visit":63}],28:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-consecutive-blank-lines + * @fileoverview + * Warn for too many consecutive blank lines. Knows about the extra line + * needed between a list and indented code, and two lists. + * @example + * + * Foo... + * + * ...Bar. + * + * + * Foo... + * + * + * ...Bar. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var plural = require('../utilities/plural'); + +/* + * Constants. + */ + +var MAX = 2; + +/** + * Warn for too many consecutive blank lines. Knows + * about the extra line needed between a list and + * indented code, and two lists. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noConsecutiveBlankLines(ast, file, preferred, done) { + /** + * Compare the difference between `start` and `end`, + * and warn when that difference exceens `max`. + * + * @param {Position} start + * @param {Position} end + */ + function compare(start, end, max) { + var diff = end.line - start.line; + var word = diff > 0 ? 'before' : 'after'; + + diff = Math.abs(diff) - max; + + if (diff > 0) { + file.warn('Remove ' + diff + ' ' + plural('line', diff) + ' ' + word + ' node', end); + } + } + + visit(ast, function (node) { + var children = node.children; + + if (position.isGenerated(node)) { + return; + } + + if (children && children[0]) { + /* + * Compare parent and first child. + */ + + compare(position.start(node), position.start(children[0]), 0); + + /* + * Compare between each child. + */ + + children.forEach(function (child, index) { + var prev = children[index - 1]; + var max = MAX; + + if (!prev) { + return; + } + + if ( + ( + prev.type === 'list' && + child.type === 'list' + ) || + ( + child.type === 'code' && + prev.type === 'list' && + !child.lang + ) + ) { + max++; + } + + compare(position.end(prev), position.start(child), max); + }); + + /* + * Compare parent and last child. + */ + + compare(position.end(node), position.end(children[children.length - 1]), 1); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noConsecutiveBlankLines; + +},{"../utilities/plural":60,"../utilities/position":61,"../utilities/visit":63}],29:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-duplicate-definitions + * @fileoverview + * Warn when duplicate definitions are found. + * @example + * + * [foo]: bar + * [baz]: qux + * + * + * [foo]: bar + * [foo]: qux + */ + +'use strict'; + +/* + * Dependencies. + */ + +var position = require('../utilities/position'); +var visit = require('../utilities/visit'); + +/** + * Warn when definitions with equal content are found. + * + * Matches case-insensitive. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noDuplicateDefinitions(ast, file, preferred, done) { + var map = {}; + + /** + * Check `node`. + * + * @param {Node} node + */ + function validate(node) { + var duplicate = map[node.identifier]; + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (duplicate && duplicate.type) { + pos = position.start(duplicate); + + file.warn( + 'Do not use definitions with the same identifier (' + + pos.line + ':' + pos.column + ')', + node + ); + } + + map[node.identifier] = node; + } + + visit(ast, 'definition', validate); + visit(ast, 'footnoteDefinition', validate); + + done(); +} + +/* + * Expose. + */ + +module.exports = noDuplicateDefinitions; + +},{"../utilities/position":61,"../utilities/visit":63}],30:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-duplicate-headings + * @fileoverview + * Warn when duplicate headings are found. + * @example + * + * # Foo + * + * ## Bar + * + * + * # Foo + * + * ## Foo + * + * ## [Foo](http://foo.com/bar) + */ + +'use strict'; + +/* + * Dependencies. + */ + +var position = require('../utilities/position'); +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); + +/** + * Warn when headings with equal content are found. + * + * Matches case-insensitive. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noDuplicateHeadings(ast, file, preferred, done) { + var map = {}; + + visit(ast, 'heading', function (node) { + var value = toString(node).toUpperCase(); + var duplicate = map[value]; + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (duplicate && duplicate.type === 'heading') { + pos = position.start(duplicate); + + file.warn( + 'Do not use headings with similar content (' + + pos.line + ':' + pos.column + ')', + node + ); + } + + map[value] = node; + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noDuplicateHeadings; + +},{"../utilities/position":61,"../utilities/to-string":62,"../utilities/visit":63}],31:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-emphasis-as-heading + * @fileoverview + * Warn when emphasis (including strong), instead of a heading, introduces + * a paragraph. + * + * Currently, only warns when a colon (`:`) is also included, maybe that + * could be omitted. + * @example + * + * # Foo: + * + * Bar. + * + * + * *Foo:* + * + * Bar. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var toString = require('../utilities/to-string'); +var position = require('../utilities/position'); + +/** + * Warn when a section (a new paragraph) is introduced + * by emphasis (or strong) and a colon. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noEmphasisAsHeading(ast, file, preferred, done) { + visit(ast, 'paragraph', function (node, index, parent) { + var children = node.children; + var child = children[0]; + var prev = parent.children[index - 1]; + var next = parent.children[index + 1]; + var value; + + if (position.isGenerated(node)) { + return; + } + + if ( + (!prev || prev.type !== 'heading') && + next && + next.type === 'paragraph' && + children.length === 1 && + (child.type === 'emphasis' || child.type === 'strong') + ) { + value = toString(child); + + /* + * TODO: See if removing the punctuation + * necessity is possible? + */ + + if (value.charAt(value.length - 1) === ':') { + file.warn('Don’t use emphasis to introduce a section, use a heading', node); + } + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noEmphasisAsHeading; + +},{"../utilities/position":61,"../utilities/to-string":62,"../utilities/visit":63}],32:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-articles + * @fileoverview + * Warn when file name start with an article. + * @example + * Valid: article.md + * Invalid: an-article.md, a-article.md, , the-article.md + */ + +'use strict'; + +/** + * Warn when file name start with an article. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameArticles(ast, file, preferred, done) { + var match = file.filename && file.filename.match(/^(the|an?)\b/i); + + if (match) { + file.warn('Do not start file names with `' + match[0] + '`'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameArticles; + +},{}],33:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-consecutive-dashes + * @fileoverview + * Warn when file names contain consecutive dashes. + * @example + * Invalid: docs/plug--ins.md + * Valid: docs/plug-ins.md + */ + +'use strict'; + +/** + * Warn when file names contain consecutive dashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameConsecutiveDashes(ast, file, preferred, done) { + if (file.filename && /-{2,}/.test(file.filename)) { + file.warn('Do not use consecutive dashes in a file name'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameConsecutiveDashes; + +},{}],34:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-irregular-characters + * @fileoverview + * Warn when file names contain irregular characters: characters other + * than alpha-numericals, dashes, and dots (full-stops). + * @example + * Invalid: plug_ins.md, plug ins.md. + * Valid: plug-ins.md, plugins.md. + */ + +'use strict'; + +/** + * Warn when file names contain characters other than + * alpha-numericals, dashes, and dots (full-stops). + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameIrregularCharacters(ast, file, preferred, done) { + var match = file.filename && file.filename.match(/[^.a-zA-Z0-9-]/); + + if (match) { + file.warn('Do not use `' + match[0] + '` in a file name'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameIrregularCharacters; + +},{}],35:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-mixed-case + * @fileoverview + * Warn when a file name uses mixed case: both upper- and lower case + * characters. + * @example + * Invalid: Readme.md + * Valid: README.md, readme.md + */ + +'use strict'; + +/** + * Warn when a file name uses mixed case: both upper- and + * lower case characters. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameMixedCase(ast, file, preferred, done) { + var name = file.filename; + + if (name && !(name === name.toLowerCase() || name === name.toUpperCase())) { + file.warn('Do not mix casing in file names'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameMixedCase; + +},{}],36:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-file-name-outer-dashes + * @fileoverview + * Warn when file names contain initial or final dashes. + * @example + * Invalid: -readme.md, readme-.md + * Valid: readme.md + */ + +'use strict'; + +/** + * Warn when file names contain initial or final dashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noFileNameOuterDashes(ast, file, preferred, done) { + if (file.filename && /^-|-$/.test(file.filename)) { + file.warn('Do not use initial or final dashes in a file name'); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noFileNameOuterDashes; + +},{}],37:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-heading-content-indent + * @fileoverview + * Warn when a heading’s content is indented. + * @example + * + * + * #··Foo + * + * ## Bar··## + * + * ##··Baz + * + * + * #·Foo + * + * ## Bar·## + * + * ##·Baz + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var style = require('../utilities/heading-style'); +var plural = require('../utilities/plural'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when a (closed) ATX-heading has too much space + * between the initial hashes and the content, or the + * content and the final hashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noHeadingContentIndent(ast, file, preferred, done) { + var contents = file.toString(); + + visit(ast, 'heading', function (node) { + var depth = node.depth; + var children = node.children; + var type = style(node, 'atx'); + var initial; + var final; + var diff; + var word; + var index; + + if (position.isGenerated(node)) { + return; + } + + if (type === 'atx' || type === 'atx-closed') { + initial = start(node); + index = initial.offset; + + while (contents.charAt(index) !== '#') { + index++; + } + + index = depth + (index - initial.offset); + diff = start(children[0]).column - initial.column - 1 - index; + + if (diff) { + word = diff > 0 ? 'Remove' : 'Add'; + diff = Math.abs(diff); + + file.warn( + word + ' ' + diff + ' ' + plural('space', diff) + + ' before this heading’s content', + start(children[0]) + ); + } + } + + /* + * Closed ATX-heading always must have a space + * between their content and the final hashes, + * thus, there is no `add x spaces`. + */ + + if (type === 'atx-closed') { + final = end(children[children.length - 1]); + diff = end(node).column - final.column - 1 - depth; + + if (diff) { + file.warn( + 'Remove ' + diff + ' ' + plural('space', diff) + + ' after this heading’s content', + final + ); + } + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noHeadingContentIndent; + +},{"../utilities/heading-style":59,"../utilities/plural":60,"../utilities/position":61,"../utilities/visit":63}],38:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-heading-indent + * @fileoverview + * Warn when a heading is indented. + * @example + * + * + * ···# Hello world + * + * ·Foo + * ----- + * + * ·# Hello world # + * + * ···Bar + * ===== + * + * + * # Hello world + * + * Foo + * ----- + * + * # Hello world # + * + * Bar + * ===== + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var plural = require('../utilities/plural'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/** + * Warn when a heading has too much space before the + * initial hashes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noHeadingIndent(ast, file, preferred, done) { + var contents = file.toString(); + var length = contents.length; + + visit(ast, 'heading', function (node) { + var initial = start(node); + var begin = initial.offset; + var index = begin - 1; + var character; + var diff; + + if (position.isGenerated(node)) { + return; + } + + while (++index < length) { + character = contents.charAt(index); + + if (character !== ' ' && character !== '\t') { + break; + } + } + + diff = index - begin; + + if (diff) { + file.warn( + 'Remove ' + diff + ' ' + plural('space', diff) + + ' before this heading', + { + 'line': initial.line, + 'column': initial.column + diff + } + ); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noHeadingIndent; + +},{"../utilities/plural":60,"../utilities/position":61,"../utilities/visit":63}],39:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-heading-punctuation + * @fileoverview + * Warn when a heading ends with a a group of characters. + * Defaults to `'.,;:!?'`. + * + * Options: `string`, default: `'.,;:!?'`. + * + * Note that these are added to a regex, in a group (`'[' + char + ']'`), + * be careful for escapes and dashes. + * @example + * + * # Hello: + * + * # Hello? + * + * # Hello! + * + * # Hello, + * + * # Hello; + * + * + * # Hello + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var toString = require('../utilities/to-string'); + +/** + * Warn when headings end in some characters. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='\\.,;:!?'] - Group of characters. + * @param {Function} done - Callback. + */ +function noHeadingPunctuation(ast, file, preferred, done) { + preferred = typeof preferred === 'string' ? preferred : '\\.,;:!?'; + + visit(ast, 'heading', function (node) { + var value = toString(node); + + if (position.isGenerated(node)) { + return; + } + + value = value.charAt(value.length - 1); + + if (new RegExp('[' + preferred + ']').test(value)) { + file.warn('Don’t add a trailing `' + value + '` to headings', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noHeadingPunctuation; + +},{"../utilities/position":61,"../utilities/to-string":62,"../utilities/visit":63}],40:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-html + * @fileoverview + * Warn when HTML nodes are used. + * + * Ignores comments, because they are used by this tool, mdast, and + * because markdown doesn’t have native comments. + * @example + * + *

Hello

+ * + * + * # Hello + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when HTML nodes are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function html(ast, file, preferred, done) { + visit(ast, 'html', function (node) { + if (!position.isGenerated(node) && !/^\s* + * * Hello *, [ world ](http://foo.bar/baz) + * + * + * *Hello*, [world](http://foo.bar/baz) + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); +var toString = require('../utilities/to-string'); + +/** + * Warn when inline nodes are padded with spaces between + * markers and content. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noInlinePadding(ast, file, preferred, done) { + visit(ast, function (node) { + var type = node.type; + var contents; + + if (position.isGenerated(node)) { + return; + } + + if ( + type === 'emphasis' || + type === 'strong' || + type === 'delete' || + type === 'image' || + type === 'link' + ) { + contents = toString(node); + + if (contents.charAt(0) === ' ' || contents.charAt(contents.length - 1) === ' ') { + file.warn('Don’t pad `' + type + '` with inner spaces', node); + } + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noInlinePadding; + +},{"../utilities/position":61,"../utilities/to-string":62,"../utilities/visit":63}],42:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-literal-urls + * @fileoverview + * Warn when URLs without angle-brackets are used. + * @example + * + * http://foo.bar/baz + * + * + * + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn for literal URLs without angle-brackets. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noLiteralURLs(ast, file, preferred, done) { + visit(ast, 'link', function (node) { + var head = start(node.children[0]).column; + var tail = end(node.children[node.children.length - 1]).column; + var initial = start(node).column; + var final = end(node).column; + + if (position.isGenerated(node)) { + return; + } + + if (initial === head && final === tail) { + file.warn('Don’t use literal URLs without angle brackets', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noLiteralURLs; + +},{"../utilities/position":61,"../utilities/visit":63}],43:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-missing-blank-lines + * @fileoverview + * Warn for missing blank lines before a block node. + * @example + * + * # Foo + * ## Bar + * + * + * # Foo + * + * ## Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Check if `node` is an applicable block-level node. + * + * @param {Node} node + * @return {boolean} - Whether or not `node` is applicable. + */ +function isApplicable(node) { + return [ + 'paragraph', + 'blockquote', + 'heading', + 'code', + 'yaml', + 'html', + 'list', + 'table', + 'horizontalRule' + ].indexOf(node.type) !== -1; +} + +/** + * Warn when there is no empty line between two block + * nodes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noMissingBlankLines(ast, file, preferred, done) { + visit(ast, function (node, index, parent) { + var next = parent && parent.children[index + 1]; + + if (position.isGenerated(node)) { + return; + } + + if ( + next && + isApplicable(node) && + isApplicable(next) && + position.start(next).line === position.end(node).line + 1 + ) { + file.warn('Missing blank line before block node', next); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noMissingBlankLines; + +},{"../utilities/position":61,"../utilities/visit":63}],44:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-multiple-toplevel-headings + * @fileoverview + * Warn when multiple top-level headings are used. + * @example + * + * # Foo + * + * # Bar + * + * + * # Foo + * + * ## Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when multiple top-level headings are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noMultipleToplevelHeadings(ast, file, preferred, done) { + var topLevelheading = false; + + visit(ast, 'heading', function (node) { + var pos; + + if (position.isGenerated(node)) { + return; + } + + if (node.depth === 1) { + if (topLevelheading) { + pos = position.start(node); + + file.warn('Don’t use multiple top level headings (' + pos.line + ':' + pos.column + ')', node); + } + + topLevelheading = node; + } + }); + + done(); +} + +module.exports = noMultipleToplevelHeadings; + +},{"../utilities/position":61,"../utilities/visit":63}],45:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-shell-dollars + * @fileoverview + * Warn when shell code is prefixed by dollar-characters. + * @example + * + * $ echo a + * $ echo a > file + * + * + * echo a + * echo a > file + * + * + * $ echo a + * a + * $ echo a > file + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * List of shell script file extensions (also used as code + * flags for syntax highlighting on GitHub): + * + * @see https://github.com/github/linguist/blob/5bf8cf5/lib/linguist/languages.yml#L3002 + */ + +var flags = [ + 'sh', + 'bash', + 'bats', + 'cgi', + 'command', + 'fcgi', + 'ksh', + 'tmux', + 'tool', + 'zsh' +]; + +/** + * Warn when shell code is prefixed by dollar-characters. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noShellDollars(ast, file, preferred, done) { + visit(ast, 'code', function (node) { + var language = node.lang; + var value = node.value; + var warn; + + if (position.isGenerated(node)) { + return; + } + + /* + * Check both known shell-code and unknown code. + */ + + if (!language || flags.indexOf(language) !== -1) { + warn = value.length && value.split('\n').every(function (line) { + return Boolean(!line.trim() || line.match(/^\s*\$\s*/)); + }); + + if (warn) { + file.warn('Do not use dollar signs before shell-commands', node); + } + } + }); + + + done(); +} + +/* + * Expose. + */ + +module.exports = noShellDollars; + +},{"../utilities/position":61,"../utilities/visit":63}],46:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-shortcut-reference-image + * @fileoverview + * Warn when shortcut reference images are used. + * @example + * + * ![foo] + * + * [foo]: http://foo.bar/baz.png + * + * + * ![foo][] + * + * [foo]: http://foo.bar/baz.png + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when shortcut reference images are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noShortcutReferenceImage(ast, file, preferred, done) { + visit(ast, 'imageReference', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (node.referenceType === 'shortcut') { + file.warn('Use the trailing [] on reference images', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noShortcutReferenceImage; + +},{"../utilities/position":61,"../utilities/visit":63}],47:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-shortcut-reference-link + * @fileoverview + * Warn when shortcut reference links are used. + * @example + * + * [foo] + * + * [foo]: http://foo.bar/baz + * + * + * [foo][] + * + * [foo]: http://foo.bar/baz + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when shortcut reference links are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noShortcutReferenceLink(ast, file, preferred, done) { + visit(ast, 'linkReference', function (node) { + if (position.isGenerated(node)) { + return; + } + + if (node.referenceType === 'shortcut') { + file.warn('Use the trailing [] on reference links', node); + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noShortcutReferenceLink; + +},{"../utilities/position":61,"../utilities/visit":63}],48:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-table-indentation + * @fileoverview + * Warn when tables are indented. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/** + * Warn when a table has a too much indentation. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noTableIndentation(ast, file, preferred, done) { + visit(ast, 'table', function (node) { + var contents = file.toString(); + + if (position.isGenerated(node)) { + return; + } + + node.children.forEach(function (row) { + var fence = contents.slice(position.start(row).offset, position.start(row.children[0]).offset); + + if (fence.indexOf('|') > 1) { + file.warn('Do not indent table rows', row); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = noTableIndentation; + +},{"../utilities/position":61,"../utilities/visit":63}],49:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module no-tabs + * @fileoverview + * Warn when hard-tabs instead of spaces + * @example + * + * + * Foo»Bar + * + * »···Foo + * + * + * Foo Bar + * + * Foo + */ + +'use strict'; + +/** + * Warn when hard-tabs instead of spaces are used. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function noTabs(ast, file, preferred, done) { + var content = file.toString(); + var index = -1; + var length = content.length; + + while (++index < length) { + if (content.charAt(index) === '\t') { + file.warn('Use spaces instead of hard-tabs', file.offsetToPosition(index)); + } + } + + done(); +} + +/* + * Expose. + */ + +module.exports = noTabs; + +},{}],50:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module ordered-list-marker-style + * @fileoverview + * Warn when the list-item marker style of ordered lists violate a given + * style. + * + * Options: `string`, either `'consistent'`, `'.'`, or `')'`, + * default: `'consistent'`. + * + * Note that `)` is only supported in CommonMark. + * + * The default value, `consistent`, detects the first used list + * style, and will warn when a subsequent list uses a different + * style. + * @example + * + * 1. Foo + * + * 2. Bar + * + * + * 1) Foo + * + * 2) Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Valid styles. + */ + +var STYLES = { + ')': true, + '.': true, + 'null': true +}; + +/** + * Warn when the list-item marker style of ordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Ordered list + * marker style, either `'.'` or `')'`, defaulting to the + * first found style. + * @param {Function} done - Callback. + */ +function orderedListMarkerStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid ordered list-item marker style `' + preferred + '`: use either `\'.\'` or `\')\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + + if (!node.ordered) { + return; + } + + items.forEach(function (item) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + + if (position.isGenerated(item)) { + return; + } + + marker = contents.slice(initial, final).replace(/\s|\d/g, ''); + + /* + * Support checkboxes. + */ + + marker = marker.replace(/\[[x ]?\]\s*$/i, ''); + + if (!preferred) { + preferred = marker; + } else if (marker !== preferred) { + file.warn('Marker style should be `' + preferred + '`', item); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = orderedListMarkerStyle; + +},{"../utilities/position":61,"../utilities/visit":63}],51:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module ordered-list-marker-value + * @fileoverview + * Warn when the list-item marker values of ordered lists violate a + * given style. + * + * Options: `string`, either `'single'`, `'one'`, or `'ordered'`, + * default: `'ordered'`. + * + * When set to `'ordered'`, list-item bullets should increment by one, + * relative to the starting point. When set to `'single'`, bullets should + * be the same as the relative starting point. When set to `'one'`, bullets + * should always be `1`. + * @example + * + * 1. Foo + * 1. Bar + * 1. Baz + * + * 1. Alpha + * 1. Bravo + * 1. Charlie + * + * + * 1. Foo + * 1. Bar + * 1. Baz + * + * 3. Alpha + * 3. Bravo + * 3. Charlie + * + * + * 1. Foo + * 2. Bar + * 3. Baz + * + * 3. Alpha + * 4. Bravo + * 5. Charlie + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Valid styles. + */ + +var STYLES = { + 'ordered': true, + 'single': true, + 'one': true +}; + +/** + * Warn when the list-item markers values of ordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='ordered'] - Ordered list + * marker value, either `'one'` or `'ordered'`, + * defaulting to the latter. + * @param {Function} done - Callback. + */ +function orderedListMarkerValue(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' ? 'ordered' : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid ordered list-item marker value `' + preferred + '`: use either `\'ordered\'` or `\'one\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + var shouldBe = (preferred === 'one' ? 1 : node.start) || 1; + + /* + * Ignore unordered lists. + */ + + if (!node.ordered) { + return; + } + + items.forEach(function (item, index) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + + /* + * Ignore first list item. + */ + + if (index === 0) { + return; + } + + /* + * Increase the expected line number when in + * `ordered` mode. + */ + + if (preferred === 'ordered') { + shouldBe++; + } + + /* + * Ignore generated nodes. + */ + + if (position.isGenerated(item)) { + return; + } + + marker = contents.slice(initial, final).replace(/[\s\.\)]/g, ''); + + /* + * Support checkboxes. + */ + + marker = Number(marker.replace(/\[[x ]?\]\s*$/i, '')); + + if (marker !== shouldBe) { + file.warn('Marker should be `' + shouldBe + '`, was `' + marker + '`', item); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = orderedListMarkerValue; + +},{"../utilities/position":61,"../utilities/visit":63}],52:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module rule-style + * @fileoverview + * Warn when the horizontal rules violate a given or detected style. + * + * Options: `string`, either a valid markdown rule, or `consistent`, + * default: `'consistent'`. + * @example + * + * * * * + * + * * * * + * + * + * _______ + * + * _______ + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when a given style is invalid. + * + * @param {*} style + * @return {boolean} - Whether or not `style` is a + * valid rule style. + */ +function validateRuleStyle(style) { + return style === null || !/[^-_* ]/.test(style); +} + +/** + * Warn when the horizontal rules violate a given or + * detected style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - A valid + * horizontal rule, defaulting to the first found style. + * @param {Function} done - Callback. + */ +function ruleStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (validateRuleStyle(preferred) !== true) { + file.fail('Invalid preferred rule-style: provide a valid markdown rule, or `\'consistent\'`'); + + return; + } + + visit(ast, 'horizontalRule', function (node) { + var initial = start(node).offset; + var final = end(node).offset; + var hr; + + if (position.isGenerated(node)) { + return; + } + + hr = contents.slice(initial, final); + + if (preferred) { + if (hr !== preferred) { + file.warn('Horizontal rules should use `' + preferred + '`', node); + } + } else { + preferred = hr; + } + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = ruleStyle; + +},{"../utilities/position":61,"../utilities/visit":63}],53:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module blockquote-indentation + * @fileoverview + * Warn for violating strong markers. + * + * Options: `string`, either `'consistent'`, `'*'`, or `'_'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used strong + * style, and will warn when a subsequent strong uses a different + * style. + * @example + * + * **foo** + * **bar** + * + * + * __foo__ + * __bar__ + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Map of valid markers. + */ + +var MARKERS = { + '*': true, + '_': true, + 'null': true +}; + +/** + * Warn when a `strong` node has an incorrect marker. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Preferred + * marker, either `"*"` or `"_"`, or `"consistent"`. + * @param {Function} done - Callback. + */ +function strongMarker(ast, file, preferred, done) { + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (MARKERS[preferred] !== true) { + file.fail('Invalid strong marker `' + preferred + '`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`'); + } else { + visit(ast, 'strong', function (node) { + var marker = file.toString().charAt(position.start(node).offset); + + if (position.isGenerated(node)) { + return; + } + + if (preferred) { + if (marker !== preferred) { + file.warn('Strong should use `' + preferred + '` as a marker', node); + } + } else { + preferred = marker; + } + }); + } + + done(); +} + +/* + * Expose. + */ + +module.exports = strongMarker; + +},{"../utilities/position":61,"../utilities/visit":63}],54:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module table-cell-padding + * @fileoverview + * Warn when table cells are incorrectly padded. + * + * Options: `string`, either `'consistent'`, `'padded'`, or `'compact'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used cell padding + * style, and will warn when a subsequent cells uses a different + * style. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * |A |B | + * |-----|-----| + * |Alpha|Bravo| + * + * + * | A | B | + * | -----| -----| + * | Alpha| Bravo| + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/* + * Valid styles. + */ + +var STYLES = { + 'null': true, + 'padded': true, + 'compact': true +}; + +/** + * Warn when table cells are incorrectly padded. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} preferred - Either `padded` (for + * at least a space), `compact` (for no spaces when + * possible), or `consistent`, which defaults to the + * first found style. + * @param {Function} done - Callback. + */ +function tableCellPadding(ast, file, preferred, done) { + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid table-cell-padding style `' + preferred + '`'); + } + + visit(ast, 'table', function (node) { + var children = node.children; + var contents = file.toString(); + var starts = []; + var ends = []; + var locations; + var positions; + var style; + var type; + var warning; + + if (position.isGenerated(node)) { + return; + } + + /** + * Check a fence. Checks both its initial spacing + * (between a cell and the fence), and its final + * spacing (between the fence and the next cell). + */ + function check(initial, final, cell, next, index) { + var fence = contents.slice(initial, final); + var pos = fence.indexOf('|'); + + if ( + cell && + pos !== -1 && + ( + ends[index] === undefined || + pos < ends[index] + ) + ) { + ends[index] = pos; + } + + if (next && pos !== -1) { + pos = fence.length - pos - 1; + + if (starts[index + 1] === undefined || pos < starts[index + 1]) { + starts[index + 1] = pos; + } + } + } + + children.forEach(function (row) { + var cells = row.children; + + check(start(row).offset, start(cells[0]).offset, null, cells[0], -1); + + cells.forEach(function (cell, index) { + var next = cells[index + 1] || null; + var final = start(next).offset || end(row).offset; + + check(end(cell).offset, final, cell, next, index); + }); + }); + + positions = starts.concat(ends); + + style = preferred === 'padded' ? 1 : preferred === 'compact' ? 0 : null; + + if (preferred === 'padded') { + style = 1; + } else if (preferred === 'compact') { + style = 0; + } else { + positions.some(function (pos) { + /* + * `some` skips non-existant indices, so + * there's no need to check for `!isNaN`. + */ + + style = Math.min(pos, 1); + + return true; + }); + } + + locations = children[0].children.map(function (cell) { + return start(cell); + }).concat(children[0].children.map(function (cell) { + return end(cell); + })); + + type = style === 1 ? 'padded' : 'compact'; + warning = 'Cell should be ' + type + ', isn’t'; + + positions.forEach(function (diff, index) { + if (diff !== style && diff !== undefined) { + file.warn(warning, locations[index]); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = tableCellPadding; + +},{"../utilities/position":61,"../utilities/visit":63}],55:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module table-pipe-alignment + * @fileoverview + * Warn when table pipes are not aligned. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * | A | B | + * | -- | -- | + * | Alpha | Bravo | + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when table pipes are not aligned. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function tablePipeAlignment(ast, file, preferred, done) { + visit(ast, 'table', function (node) { + var contents = file.toString(); + var indices = []; + var offset; + var line; + + if (position.isGenerated(node)) { + return; + } + + /** + * Check all pipes after each column are at + * aligned. + */ + function check(initial, final, index) { + var pos = initial + contents.slice(initial, final).indexOf('|') - offset + 1; + + if (indices[index] === undefined) { + indices[index] = pos; + } else if (pos !== indices[index]) { + file.warn('Misaligned table fence', { + 'start': { + 'line': line, + 'column': pos + }, + 'end': { + 'line': line, + 'column': pos + 1 + } + }); + } + } + + node.children.forEach(function (row) { + var cells = row.children; + + line = start(row).line; + offset = start(row).offset; + + check(start(row).offset, start(cells[0]).offset, 0); + + row.children.forEach(function (cell, index) { + var next = start(cells[index + 1]).offset || end(row).offset; + + check(end(cell).offset, next, index + 1); + }); + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = tablePipeAlignment; + +},{"../utilities/position":61,"../utilities/visit":63}],56:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module table-pipes + * @fileoverview + * Warn when table rows are not fenced with pipes. + * @example + * + * | A | B | + * | ----- | ----- | + * | Alpha | Bravo | + * + * + * A | B + * ----- | ----- + * Alpha | Bravo + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; +var end = position.end; + +/** + * Warn when a table rows are not fenced with pipes. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {*} preferred - Ignored. + * @param {Function} done - Callback. + */ +function tablePipes(ast, file, preferred, done) { + visit(ast, 'table', function (node) { + var contents = file.toString(); + + node.children.forEach(function (row) { + var cells = row.children; + var head = cells[0]; + var tail = cells[cells.length - 1]; + var initial = contents.slice(start(row).offset, start(head).offset); + var final = contents.slice(end(tail).offset, end(row).offset); + + if (position.isGenerated(row)) { + return; + } + + if (initial.indexOf('|') === -1) { + file.warn('Missing initial pipe in table fence', start(row)); + } + + if (final.indexOf('|') === -1) { + file.warn('Missing final pipe in table fence', end(row)); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = tablePipes; + +},{"../utilities/position":61,"../utilities/visit":63}],57:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module unordered-list-marker-style + * @fileoverview + * Warn when the list-item marker style of unordered lists violate a given + * style. + * + * Options: `string`, either `'consistent'`, `'-'`, `'*'`, or `'*'`, + * default: `'consistent'`. + * + * The default value, `consistent`, detects the first used list + * style, and will warn when a subsequent list uses a different + * style. + * @example + * + * - Foo + * - Bar + * + * + * * Foo + * * Bar + * + * + * + Foo + * + Bar + * + * + * + Foo + * - Bar + */ + +'use strict'; + +/* + * Dependencies. + */ + +var visit = require('../utilities/visit'); +var position = require('../utilities/position'); + +/* + * Methods. + */ + +var start = position.start; + +/* + * Valid styles. + */ + +var STYLES = { + '-': true, + '*': true, + '+': true, + 'null': true +}; + +/** + * Warn when the list-item marker style of unordered lists + * violate a given style. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + * @param {string?} [preferred='consistent'] - Unordered + * list marker style, either `'-'`, `'*'`, or `'+'`, + * defaulting to the first found style. + * @param {Function} done - Callback. + */ +function unorderedListMarkerStyle(ast, file, preferred, done) { + var contents = file.toString(); + + preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; + + if (STYLES[preferred] !== true) { + file.fail('Invalid unordered list-item marker style `' + preferred + '`: use either `\'-\'`, `\'*\'`, or `\'+\'`'); + + return; + } + + visit(ast, 'list', function (node) { + var items = node.children; + + if (node.ordered) { + return; + } + + items.forEach(function (item) { + var head = item.children[0]; + var initial = start(item).offset; + var final = start(head).offset; + var marker; + + if (position.isGenerated(item)) { + return; + } + + marker = contents.slice(initial, final).replace(/\s/g, ''); + + /* + * Support checkboxes. + */ + + marker = marker.replace(/\[[x ]?\]\s*$/i, ''); + + if (!preferred) { + preferred = marker; + } else if (marker !== preferred) { + file.warn('Marker style should be `' + preferred + '`', item); + } + }); + }); + + done(); +} + +/* + * Expose. + */ + +module.exports = unorderedListMarkerStyle; + +},{"../utilities/position":61,"../utilities/visit":63}],58:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Sort + * @fileoverview mdast plug-in used internally by + * mdast-lint to sort warnings. + * @todo Externalise into its own repository. + */ + +'use strict'; + +/** + * Sort all `file`s messages by line/column. Note that + * this works as a plugin, and will also sort warnings + * added by other plug-ins before `mdast-lint` was added. + * + * @param {Node} ast - Root node. + * @param {File} file - Virtual file. + */ +function transformer(ast, file) { + file.messages.sort(function (a, b) { + /* istanbul ignore if - Useful when externalised */ + if (a.line === undefined || b.line === undefined) { + return -1; + } + + return a.line === b.line ? a.column - b.column : a.line - b.line; + }); +} + +/** + * Return `transformer`. + * + * @return {Function} - See `transformer`. + */ +function attacher() { + return transformer; +} + +/* + * Expose. + */ + +module.exports = attacher; + +},{}],59:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module headingStyle + * @fileoverview Utility to check which style a heading + * node is in. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var end = require('./position').end; + +/** + * Get the probable style of an atx-heading, depending on + * preferred style. + * + * @example + * consolidate(1, 'setext') // 'atx' + * consolidate(1, 'atx') // 'atx' + * consolidate(3, 'setext') // 'setext' + * consolidate(3, 'atx') // 'atx' + * + * @param {number} depth + * @param {string?} relative + * @return {string?} - Type. + */ +function consolidate(depth, relative) { + return depth < 3 ? 'atx' : + relative === 'atx' || relative === 'setext' ? relative : null; +} + +/** + * Check the style of a heading. + * + * @param {Node} node + * @param {string?} relative - heading type which we'd wish + * this to be. + * @return {string?} - Type, either `'atx-closed'`, + * `'atx'`, or `'setext'`. + */ +function style(node, relative) { + var last = node.children[node.children.length - 1]; + var depth = node.depth; + + /* + * This can only occur for atx and `'atx-closed'` + * headings. This might incorrectly match `'atx'` + * headings with lots of trailing white space as an + * `'atx-closed'` heading. + */ + + if (!last) { + if (end(node).column < depth * 2) { + return consolidate(depth, relative); + } + + return 'atx-closed'; + } + + if (end(last).line + 1 === end(node).line) { + return 'setext'; + } + + if (end(last).column + depth < end(node).column) { + return 'atx-closed'; + } + + return consolidate(depth, relative); +} + +/* + * Expose. + */ + +module.exports = style; + +},{"./position":61}],60:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Plural + * @fileoverview Simple functional utility to pluralise + * a word based on a count. + */ + +'use strict'; + +/** + * Simple utility to pluralise `word`, by adding `'s'`, + * when the given `count` is not `1`. + * + * @example + * plural('foo', 0); // 'foos' + * plural('foo', 1); // 'foo' + * plural('foo', 2); // 'foos' + * + * @param {string} word - Singular form. + * @param {number} count - Relative number. + * @return {string} - Original word with an `s` on the end + * if count is not one. + */ +function plural(word, count) { + return (count === 1 ? word : word + 's'); +} + +/* + * Expose. + */ + +module.exports = plural; + +},{}],61:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Position + * @fileoverview Utility to get either the starting or the + * ending position of a node, and if its generated + * or not. + */ + +'use strict'; + +/** + * Factory to get a position at `type`. + * + * @param {string} type - Either `'start'` or `'end'`. + * @return {function(Node): Object} + */ +function positionFactory(type) { + /** + * Fet a position in `node` at a bound `type`. + * + * @param {Node} node + * @return {Object} + */ + return function (node) { + return (node && node.position && node.position[type]) || {}; + }; +} + +/* + * Getters. + */ + +var start = positionFactory('start'); +var end = positionFactory('end'); + +/** + * Detect if a node is generated. + * + * @param {Node} node + * @return {boolean} - Whether or not `node` is generated. + */ +function isGenerated(node) { + var initial = start(node); + var final = end(node); + + return initial.line === undefined || initial.column === undefined || + final.line === undefined || final.column === undefined; +} + +/* + * Exports. + */ + +var position = { + 'start': start, + 'end': end, + 'isGenerated': isGenerated +}; + +/* + * Expose. + */ + +module.exports = position; + +},{}],62:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module ToString + * @fileoverview Utility to get the plain text content + * of a node. + */ + +'use strict'; + +/** + * Get the value of `node`. Checks, `value`, + * `alt`, and `title`, in that order. + * + * @param {Node} node + * @return {string} - Textual representation. + */ +function valueOf(node) { + return node && + (node.value ? node.value : + (node.alt ? node.alt : node.title)) || ''; +} + +/** + * Returns the text content of a node. If the node itself + * does not expose plain-text fields, `toString` will + * recursivly try its children. + * + * @param {Node} node + * @return {string} - Textual representation. + */ +function toString(node) { + return valueOf(node) || + (node.children && node.children.map(toString).join('')) || + ''; +} + +/* + * Expose. + */ + +module.exports = toString; + +},{}],63:[function(require,module,exports){ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Visit + * @fileoverview Utility to recursivly walk over mdast + * nodes. + * @todo Externalise into its own repository. + */ + +'use strict'; + +/** + * Walk forwards. + * + * @param {Array.<*>} values + * @param {function(*, number): boolean} callback + * @return {boolean} - False if iteration stopped. + */ +function forwards(values, callback) { + var index = -1; + var length = values.length; + + while (++index < length) { + if (callback(values[index], index) === false) { + return false; + } + } + + return true; +} + +/** + * Walk backwards. + * + * @param {Array.<*>} values + * @param {function(*, number): boolean} callback + * @return {boolean} - False if iteration stopped. + */ +function backwards(values, callback) { + var index = values.length; + var length = -1; + + while (--index > length) { + /* istanbul ignore if - Not used yet... */ + if (callback(values[index], index) === false) { + return false; + } + } + + return true; +} + +/** + * Visit. + * + * @param {Node} tree - Root node + * @param {string} [type] - Optional node type. + * @param {function(node): boolean?} callback - Invoked + * with each found node. Can return `false` to stop + * iteration. + * @param {boolean} [reverse] - By default, `visit` will + * walk forwards, when `reverse` is `true`, `visit` + * walks backwards. + */ +function visit(tree, type, callback, reverse) { + var iterate; + var one; + var all; + + if (typeof type === 'function') { + reverse = callback; + callback = type; + type = null; + } + + iterate = reverse ? backwards : forwards; + + /** + * Visit `children` in `parent`. + */ + all = function (children, parent) { + return iterate(children, function (child, index) { + return one(child, index, parent); + }); + }; + + /** + * Visit a single node. + */ + one = function (node, position, parent) { + var result; + + if (!type || node.type === type) { + result = callback(node, position || 0, parent || null); + } + + if (node.children && result !== false) { + return all(node.children, node); + } + + return result; + }; + + one(tree); +} + +/* + * Expose. + */ + +module.exports = visit; + +},{}],64:[function(require,module,exports){ +'use strict'; + +/** + * Visit. + * + * @param {Node} tree + * @param {function(node)} callback + */ +function visit(tree, callback) { + /** + * Visit a single node. + */ + function one(node) { + callback(node); + + var children = node.children; + var index = -1; + var length = children ? children.length : 0; + + while (++index < length) { + one(children[index]); + } + } + + one(tree); +} + +/** + * Calculate offsets for `lines`. + * + * @param {Array.} lines + * @return {Array.} + */ +function toOffsets(lines) { + var total = 0; + var index = -1; + var length = lines.length; + var result = []; + + while (++index < length) { + result[index] = total += lines[index].length + 1; + } + + return result; +} + +/** + * Add an offset based on `offsets` to `position`. + * + * @param {Object} position + */ +function addRange(position, fn) { + position.offset = fn(position); +} + +/** + * Factory to reverse an offset into a line--column + * tuple. + * + * @param {Array.} offsets - Offsets, as returned + * by `toOffsets()`. + * @return {Function} - Bound method. + */ +function positionToOffsetFactory(offsets) { + /** + * Calculate offsets for `lines`. + * + * @param {Object} position - Position. + * @return {Object} - Object with `line` and `colymn` + * properties based on the bound `offsets`. + */ + function positionToOffset(position) { + var line = position && position.line; + var column = position && position.column; + + if (!isNaN(line) && !isNaN(column)) { + return ((offsets[line - 2] || 0) + column - 1) || 0; + } + + return -1; + } + + return positionToOffset; +} + +/** + * Factory to reverse an offset into a line--column + * tuple. + * + * @param {Array.} offsets - Offsets, as returned + * by `toOffsets()`. + * @return {Function} - Bound method. + */ +function offsetToPositionFactory(offsets) { + /** + * Calculate offsets for `lines`. + * + * @param {number} offset - Offset. + * @return {Object} - Object with `line` and `colymn` + * properties based on the bound `offsets`. + */ + function offsetToPosition(offset) { + var index = -1; + var length = offsets.length; + + if (offset < 0) { + return {}; + } + + while (++index < length) { + if (offsets[index] > offset) { + return { + 'line': index + 1, + 'column': (offset - offsets[index - 1] || 0) + 1 + }; + } + } + + return {}; + } + + return offsetToPosition; +} + +/** + * Add ranges for `doc` to `ast`. + * + * @param {Node} ast + * @param {File} file + */ +function transformer(ast, file) { + var contents = String(file).split('\n'); + var positionToOffset; + + /* + * Invalid. + */ + + if (!file || typeof file.contents !== 'string') { + throw new Error('Missing `file` for mdast-range'); + } + + /* + * Construct. + */ + + contents = toOffsets(contents); + positionToOffset = positionToOffsetFactory(contents); + + /* + * Expose methods. + */ + + file.offsetToPosition = offsetToPositionFactory(contents); + file.positionToOffset = positionToOffset; + + /* + * Add `offset` on both `start` and `end`. + */ + + visit(ast, function (node) { + var position = node.position; + + if (position && position.start) { + addRange(position.start, positionToOffset); + } + + if (position && position.end) { + addRange(position.end, positionToOffset); + } + }); +} + +/** + * Attacher. + * + * @return {Function} - `transformer`. + */ +function attacher() { + return transformer; +} + +/* + * Expose. + */ + +module.exports = attacher; + +},{}],65:[function(require,module,exports){ +'use strict'; + +/* + * Methods. + */ + +var splice = [].splice; + +/* + * Expression for parsing parameters. + */ + +var PARAMETERS = new RegExp( + '\\s*' + + '(' + + '[-a-z09_]+' + + ')' + + '(?:' + + '=' + + '(?:' + + '"' + + '(' + + '(?:' + + '\\\\[\\s\\S]' + + '|' + + '[^"]' + + ')+' + + ')' + + '"' + + '|' + + '\'' + + '(' + + '(?:' + + '\\\\[\\s\\S]' + + '|' + + '[^\']' + + ')+' + + ')' + + '\'' + + '|' + + '(' + + '(?:' + + '\\\\[\\s\\S]' + + '|' + + '[^"\'\\s]' + + ')+' + + ')' + + ')' + + ')?' + + '\\s*', + 'gi' +); + +/** + * Create an expression which matches a marker. + * + * @param {string} name + * @return {RegExp} + */ +function marker(name) { + return new RegExp( + '(' + + '\\s*' + + '' + + '\\s*' + + ')' + ); +} + +/** + * Visit. + * + * @param {Node} tree + * @param {function(node, parent)} callback + */ +function visit(tree, callback) { + /** + * Visit one node. + * + * @param {Node} node + * @param {number} index + */ + function one(node, index) { + var parent = this || null; + + callback(node, parent, index); + + if (node.children) { + node.children.forEach(one, node); + } + } + + one(tree); +} + +/** + * Parse `value` into an object. + * + * @param {string} value + * @return {Object} + */ +function parameters(value) { + var attributes = {}; + + value.replace(PARAMETERS, function ($0, $1, $2, $3, $4) { + var result = $2 || $3 || $4 || ''; + + if (result === 'true' || result === '') { + result = true; + } else if (result === 'false') { + result = false; + } else if (!isNaN(result)) { + result = Number(result); + } + + attributes[$1] = result; + + return ''; + }); + + return attributes; +} + +/** + * Factory to test if `node` matches `settings`. + * + * @param {Object} settings + * @param {function(Object)} callback + * @return {Function} + */ +function testFactory(settings, callback) { + var name = settings.name; + var expression = marker(name); + + /** + * Test if `node` matches the bound settings. + * + * @param {Node} node + * @param {Parser|Compiler} [context] + * @return {Object?} + */ + function test(node, context) { + var value = node.value; + var match; + var result; + + if (node.type !== 'html') { + return null; + } + + match = value.match(expression); + + if ( + !match || + match[1].length !== value.length || + match[2] !== settings.name + ) { + return null; + } + + result = { + 'type': match[3] || 'marker', + 'attributes': match[4] || '', + 'parameters': parameters(match[4] || ''), + 'node': node + }; + + if (callback) { + callback(result, context); + } + + return result; + } + + return test; +} + +/** + * Parse factory. + * + * @param {Function} tokenize - Previous parser. + * @param {Object} settings + */ +function parse(tokenize, settings) { + var callback = settings.onparse; + var test = testFactory(settings, function (result, context) { + if (result.type === 'marker') { + callback(result, context); + } + }); + + /** + * Parse HTML. + * + * @return {Node} + */ + return function () { + var node = tokenize.apply(this, arguments); + + test(node, this); + + return node; + }; +} + +/** + * Stringify factory. + * + * @param {Function} compile - Previous compiler. + * @param {Object} settings + */ +function stringify(compile, settings) { + var callback = settings.onstringify; + var test = testFactory(settings, function (result, context) { + if (result.type === 'marker') { + callback(result, context); + } + }); + + /** + * Stringify HTML. + * + * @param {Object} node + * @return {string} + */ + return function (node) { + test(node, this); + + return compile.apply(this, arguments); + }; +} + +/** + * Run factory. + * + * @param {Object} settings + */ +function run(settings) { + var callback = settings.onrun; + var test = testFactory(settings); + var nodes = []; + var start = null; + var scope = null; + var level = 0; + var position; + + /** + * Gather one dimensional zones. + * + * Passed intto `visit`. + * + * @param {Node} node + * @param {Node} parent + * @param {number} index + */ + function gather(node, parent, index) { + var result = test(node); + var type = result && result.type; + + if (scope && parent === scope) { + if (type === 'start') { + level++; + } + + if (type === 'end') { + level--; + } + + if (type === 'end' && level === 0) { + nodes = callback(start, nodes, result, { + 'start': index - nodes.length - 1, + 'end': index, + 'parent': scope + }); + + if (nodes) { + splice.apply( + scope.children, [position, index + 1].concat(nodes) + ); + } + + start = null; + scope = null; + position = null; + nodes = []; + } else { + nodes.push(node); + } + } + + if (!scope && type === 'start') { + level = 1; + position = index; + start = result; + scope = parent; + } + } + + /** + * Modify AST. + * + * @param {Object} node + */ + return function (node) { + visit(node, gather); + }; +} + +/** + * Modify mdast to invoke callbacks when HTML commnts are + * found. + * + * @param {MDAST} mdast + * @param {Object?} options + * @return {Function?} + */ +function attacher(mdast, options) { + var blockTokenizers = mdast.Parser.prototype.blockTokenizers; + var inlineTokenizers = mdast.Parser.prototype.inlineTokenizers; + var stringifiers = mdast.Compiler.prototype; + + if (options.onparse) { + blockTokenizers.html = parse(blockTokenizers.html, options); + inlineTokenizers.tag = parse(inlineTokenizers.tag, options); + } + + if (options.onstringify) { + stringifiers.html = stringify(stringifiers.html, options); + } + + if (options.onrun) { + return run(options); + } + + return null; +} + +/** + * Wrap `zone` to be passed into `mdast.use()`. + * + * Reason for this is that **mdast** only allows a single + * function to be `use`d once. + * + * @param {Object} options + * @return {Function} + */ +function wrapper(options) { + if (!options || !options.name) { + throw new Error('Missing `name` in `options`'); + } + + return function (mdast) { + return attacher(mdast, options); + }; +} + +/* + * Expose. + */ + +module.exports = wrapper; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/mdast.min.js b/mdast.min.js new file mode 100644 index 0000000..0f7cd03 --- /dev/null +++ b/mdast.min.js @@ -0,0 +1 @@ +!function(b,a){typeof exports==='object'&&typeof module!=='undefined'?module.exports=b():typeof define==='function'&&define.amd?define([],b):(typeof window!=='undefined'?a=window:typeof global!=='undefined'?a=global:typeof self!=='undefined'?a=self:a=this,a.mdast=b())}(function(){return function a(b,c,e){function f(d,k){if(!c[d]){if(!b[d]){var i=typeof require=='function'&&require;if(!k&&i)return i(d,!0);if(g)return g(d,!0);var j=new Error("Cannot find module '"+d+"'");throw j.code='MODULE_NOT_FOUND',j}var h=c[d]={exports:{}};b[d][0].call(h.exports,function(c){var a=b[d][1][c];return f(a?a:c)},h,h.exports,a,b,c,e)}return c[d].exports}var g=typeof require=='function'&&require;for(var d=0;df)if(c=d[e],c.position.line0?'Add':'Remove',d=Math.abs(d),d!==0&&g.warn(i+' '+d+' '+f('space',d)+' between blockquote and content',a.start(e.children[0]))):b=c(e)}),h()}var d=b('../utilities/visit'),e=b('../utilities/to-string'),f=b('../utilities/plural'),a=b('../utilities/position');g.exports=h},{'../utilities/plural':60,'../utilities/position':61,'../utilities/to-string':62,'../utilities/visit':63}],5:[function(b,g,i){'use strict';function h(i,g,b,j){function k(b){var c=d(b).offset,f=e(b).offset;return a.isGenerated(b)?null:b.lang||/^\s*([~`])\1{2,}/.test(h.slice(c,f))?'fenced':'indented'}var h=g.toString();if(b=typeof b!=='string'||b==='consistent'?null:b,f[b]!==!0){g.fail('Invalid code block style `'+b+"`: use either `'consistent'`, `'fenced'`, or `'indented'`");return}c(i,'code',function(c){var a=k(c);if(!a)return;b?b!==a&&g.warn('Code blocks should be '+b,c):b=a}),j()}var c=b('../utilities/visit'),a=b('../utilities/position'),d=a.start,e=a.end,f={null:!0,fenced:!0,indented:!0};g.exports=h},{'../utilities/position':61,'../utilities/visit':63}],6:[function(c,e,g){'use strict';function f(c,e,i,h){function f(b){var f=a.start(b).offset,h=a.end(b).offset,c;if(a.isGenerated(b))return;c=g.slice(f,h).match(d)[1],c!==c.toLowerCase()&&e.warn('Do not use uppper-case characters in definition labels',b)}var g=e.toString();b(c,'definition',f),b(c,'footnoteDefinition',f),h()}var b=c('../utilities/visit'),a=c('../utilities/position'),d=/^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/;e.exports=f},{'../utilities/position':61,'../utilities/visit':63}],7:[function(c,e,g){'use strict';function f(c,e,i,h){function f(b){var c=a.start(b).offset,f=a.end(b).offset,h;if(a.isGenerated(b))return;h=g.slice(c,f).match(d)[1],/[ \t\n]{2,}/.test(h)&&e.warn('Do not use consecutive white-space in definition labels',b)}var g=e.toString();b(c,'definition',f),b(c,'footnoteDefinition',f),h()}var b=c('../utilities/visit'),a=c('../utilities/position'),d=/^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/;e.exports=f},{'../utilities/position':61,'../utilities/visit':63}],8:[function(b,e,g){'use strict';function f(f,e,b,g){if(b=typeof b!=='string'||b==='consistent'?null:b,d[b]!==!0){e.fail('Invalid emphasis marker `'+b+"`: use either `'consistent'`, `'*'`, or `'_'`");return}c(f,'emphasis',function(c){var d=e.toString().charAt(a.start(c).offset);if(a.isGenerated(c))return;b?d!==b&&e.warn('Emphasis should use `'+b+'` as a marker',c):b=d}),g()}var c=b('../utilities/visit'),a=b('../utilities/position'),d={'*':!0,_:!0,null:!0};e.exports=f},{'../utilities/position':61,'../utilities/visit':63}],9:[function(b,f,h){'use strict';function g(j,g,b,k){var i=g.toString(),h=!1,f=[];typeof b==='object'&&!('length'in b)&&(h=Boolean(b.allowEmpty),b=b.flags),typeof b==='object'&&'length'in b&&(f=String(b).split(',')),c(j,'code',function(b){var c=i.slice(d(b).offset,e(b).offset);if(a.isGenerated(b))return;b.lang?f.length&&f.indexOf(b.lang)===-1&&g.warn('Invalid code-language flag',b):/^\ {0,3}([~`])\1{2,}/.test(c)&&!h&&g.warn('Missing code-language flag',b)}),k()}var c=b('../utilities/visit'),a=b('../utilities/position'),d=a.start,e=a.end;f.exports=g},{'../utilities/position':61,'../utilities/visit':63}],10:[function(c,e,g){'use strict';function f(g,e,c,h){var f=e.toString();if(c=typeof c!=='string'||c==='consistent'?null:c,b[c]!==!0){e.fail('Invalid fenced code marker `'+c+"`: use either `'consistent'`, `` '`' ``, or `'~'`");return}d(g,'code',function(g){var d=f.substr(a.start(g).offset,4);if(a.isGenerated(g))return;if(d=d.trimLeft().charAt(0),b[d]!==!0)return;c?d!==c&&e.warn('Fenced code should use '+c+' as a marker',g):c=d}),h()}var d=c('../utilities/visit'),a=c('../utilities/position'),b={'`':!0,'~':!0,null:!0};e.exports=f},{'../utilities/position':61,'../utilities/visit':63}],11:[function(c,a,d){'use strict';function b(e,c,a,d){var b=c.extension;a=typeof a==='string'?a:'md',b!==''&&b!==a&&c.warn('Invalid extension: use `'+a+'`'),d()}a.exports=b},{}],12:[function(b,e,g){'use strict';function f(e,f,h,g){var b=null;c(e,function(c){var e=d(c).line;if(c.type==='root'||a.isGenerated(c))return;c.type==='definition'?b!==null&&b>e&&f.warn('Move definitions to the end of the file (after the node at line `'+b+'`)',c):b===null&&(b=e)},!0),g()}var c=b('../utilities/visit'),a=b('../utilities/position'),d=a.start;e.exports=f},{'../utilities/position':61,'../utilities/visit':63}],13:[function(c,a,d){'use strict';function b(e,c,f,d){var a=c.toString(),b=a.length-1;b>0&&a.charAt(b)!=='\n'&&c.warn('Missing newline character at end of file'),d()}a.exports=b},{}],14:[function(a,d,f){'use strict';function e(a,d,f,e){b(a,'heading',function(a){return c.isGenerated(a)?null:(a.depth!==1&&d.warn('First heading level should be `1`',a),!1)}),e()}var b=a('../utilities/visit'),c=a('../utilities/position');d.exports=e},{'../utilities/position':61,'../utilities/visit':63}],15:[function(b,d,f){'use strict';function e(e,b,g,f){var d=b.toString();c(e,'break',function(c){var e=a.start(c).offset,f=a.end(c).offset;if(a.isGenerated(c))return;d.slice(e,f).length>3&&b.warn('Use two spaces for hard line breaks',c)}),f()}var c=b('../utilities/visit'),a=b('../utilities/position');d.exports=e},{'../utilities/position':61,'../utilities/visit':63}],16:[function(a,d,f){'use strict';function e(d,e,g,f){var a=null;b(d,'heading',function(b){var d=b.depth;if(c.isGenerated(b))return;a&&d>a+1&&e.warn('Heading levels should increment by one level at a time',b),a=d}),f()}var b=a('../utilities/visit'),c=a('../utilities/position');d.exports=e},{'../utilities/position':61,'../utilities/visit':63}],17:[function(a,f,h){'use strict';function g(f,g,a,h){a=e.indexOf(a)===-1?null:a,c(f,'heading',function(c){if(d.isGenerated(c))return;a?b(c,a)!==a&&g.warn('Headings should use '+a,c):a=b(c,a)}),h()}var c=a('../utilities/visit'),b=a('../utilities/heading-style'),d=a('../utilities/position'),e=['atx','atx-closed','setext'];f.exports=g},{'../utilities/heading-style':59,'../utilities/position':61,'../utilities/visit':63}],18:[function(a,b,c){'use strict';b.exports={'no-auto-link-without-protocol':a('./no-auto-link-without-protocol'),'no-literal-urls':a('./no-literal-urls'),'no-consecutive-blank-lines':a('./no-consecutive-blank-lines'),'no-missing-blank-lines':a('./no-missing-blank-lines'),'blockquote-indentation':a('./blockquote-indentation'),'no-blockquote-without-caret':a('./no-blockquote-without-caret'),'code-block-style':a('./code-block-style'),'definition-case':a('./definition-case'),'definition-spacing':a('./definition-spacing'),'no-emphasis-as-heading':a('./no-emphasis-as-heading'),'emphasis-marker':a('./emphasis-marker'),'fenced-code-flag':a('./fenced-code-flag'),'fenced-code-marker':a('./fenced-code-marker'),'file-extension':a('./file-extension'),'final-newline':a('./final-newline'),'no-file-name-articles':a('./no-file-name-articles'),'no-file-name-consecutive-dashes':a('./no-file-name-consecutive-dashes'),'no-file-name-irregular-characters':a('./no-file-name-irregular-characters'),'no-file-name-mixed-case':a('./no-file-name-mixed-case'),'no-file-name-outer-dashes':a('./no-file-name-outer-dashes'),'final-definition':a('./final-definition'),'hard-break-spaces':a('./hard-break-spaces'),'heading-increment':a('./heading-increment'),'no-heading-content-indent':a('./no-heading-content-indent'),'no-heading-indent':a('./no-heading-indent'),'first-heading-level':a('./first-heading-level'),'maximum-heading-length':a('./maximum-heading-length'),'no-heading-punctuation':a('./no-heading-punctuation'),'heading-style':a('./heading-style'),'no-multiple-toplevel-headings':a('./no-multiple-toplevel-headings'),'no-duplicate-headings':a('./no-duplicate-headings'),'no-duplicate-definitions':a('./no-duplicate-definitions'),'no-html':a('./no-html'),'no-inline-padding':a('./no-inline-padding'),'maximum-line-length':a('./maximum-line-length'),'link-title-style':a('./link-title-style'),'list-item-bullet-indent':a('./list-item-bullet-indent'),'list-item-content-indent':a('./list-item-content-indent'),'list-item-indent':a('./list-item-indent'),'list-item-spacing':a('./list-item-spacing'),'ordered-list-marker-style':a('./ordered-list-marker-style'),'ordered-list-marker-value':a('./ordered-list-marker-value'),'no-shortcut-reference-image':a('./no-shortcut-reference-image'),'no-shortcut-reference-link':a('./no-shortcut-reference-link'),'rule-style':a('./rule-style'),'no-shell-dollars':a('./no-shell-dollars'),'strong-marker':a('./strong-marker'),'no-table-indentation':a('./no-table-indentation'),'table-pipe-alignment':a('./table-pipe-alignment'),'table-cell-padding':a('./table-cell-padding'),'table-pipes':a('./table-pipes'),'no-tabs':a('./no-tabs'),'unordered-list-marker-style':a('./unordered-list-marker-style')}},{'./blockquote-indentation':4,'./code-block-style':5,'./definition-case':6,'./definition-spacing':7,'./emphasis-marker':8,'./fenced-code-flag':9,'./fenced-code-marker':10,'./file-extension':11,'./final-definition':12,'./final-newline':13,'./first-heading-level':14,'./hard-break-spaces':15,'./heading-increment':16,'./heading-style':17,'./link-title-style':19,'./list-item-bullet-indent':20,'./list-item-content-indent':21,'./list-item-indent':22,'./list-item-spacing':23,'./maximum-heading-length':24,'./maximum-line-length':25,'./no-auto-link-without-protocol':26,'./no-blockquote-without-caret':27,'./no-consecutive-blank-lines':28,'./no-duplicate-definitions':29,'./no-duplicate-headings':30,'./no-emphasis-as-heading':31,'./no-file-name-articles':32,'./no-file-name-consecutive-dashes':33,'./no-file-name-irregular-characters':34,'./no-file-name-mixed-case':35,'./no-file-name-outer-dashes':36,'./no-heading-content-indent':37,'./no-heading-indent':38,'./no-heading-punctuation':39,'./no-html':40,'./no-inline-padding':41,'./no-literal-urls':42,'./no-missing-blank-lines':43,'./no-multiple-toplevel-headings':44,'./no-shell-dollars':45,'./no-shortcut-reference-image':46,'./no-shortcut-reference-link':47,'./no-table-indentation':48,'./no-tabs':49,'./ordered-list-marker-style':50,'./ordered-list-marker-value':51,'./rule-style':52,'./strong-marker':53,'./table-cell-padding':54,'./table-pipe-alignment':55,'./table-pipes':56,'./unordered-list-marker-style':57}],19:[function(d,f,h){'use strict';function g(g,f,d,j){function h(h){var a=e(h).offset-1,g,j;if(b.isGenerated(h))return;h.type!=='definition'&&a--;while(a){if(g=i.charAt(a),!/\s/.test(g))break;a--}if(!(g in c))return;d?d!==g&&(j=f.offsetToPosition(a+1),f.warn(d===')'?'Titles should use `()` as a quote':'Titles should use `'+d+'` as a quote',j)):d=g}var i=f.toString();if(d=typeof d!=='string'||d==='consistent'?null:d,(d==='()'||d==='(')&&(d=')'),c[d]!==!0){f.fail('Invalid link title style marker `'+d+"`: use either `'consistent'`, `'\"'`, `'\\''`, or `'()'`");return}a(g,'link',h),a(g,'image',h),a(g,'definition',h),j()}var a=d('../utilities/visit'),b=d('../utilities/position'),c={'"':!0,"'":!0,')':!0,null:!0},e=b.end;f.exports=g},{'../utilities/position':61,'../utilities/visit':63}],20:[function(b,f,h){'use strict';function g(g,b,i,h){var f=b.toString();d(g,'list',function(d){var g=d.children;g.forEach(function(j){var i=j.children[0],g=a(j).offset,k=a(i).offset,h;if(c.isGenerated(d))return;h=f.slice(g,k).match(/^\s*/)[0].length,h!==0&&(g=a(i),b.warn('Incorrect indentation before bullet: remove '+h+' '+e('space',h),{line:g.line,column:g.column-h}))})}),h()}var d=b('../utilities/visit'),c=b('../utilities/position'),e=b('../utilities/plural'),a=c.start;f.exports=g},{'../utilities/plural':60,'../utilities/position':61,'../utilities/visit':63}],21:[function(a,f,h){'use strict';function g(g,a,i,h){var f=a.toString();d(g,'listItem',function(d){var g;d.children.forEach(function(l,n){var j=c(l),h=j.column,k,i,m;if(b.isGenerated(l))return;if(n===0){if(Boolean(d.checked)===d.checked){k=j.offset;while(f.charAt(k)!=='[')k--;h-=j.offset-k}g=h;return}h!==g&&(i=g-h,m=i>0?'add':'remove',i=Math.abs(i),a.warn('Don’t use mixed indentation for children, '+m+' '+i+' '+e('space',i),{line:c(l).line,column:h}))})}),h()}var d=a('../utilities/visit'),b=a('../utilities/position'),e=a('../utilities/plural'),c=b.start;f.exports=g},{'../utilities/plural':60,'../utilities/position':61,'../utilities/visit':63}],22:[function(b,g,i){'use strict';function h(i,g,b,j){var h=g.toString();if(b=typeof b!=='string'?'tab-size':b,f[b]!==!0){g.fail('Invalid list-item indent style `'+b+"`: use either `'tab-size'`, `'space'`, or `'mixed'`");return}d(i,'list',function(d){var f=d.children,i=d.ordered,j=d.start||1;if(c.isGenerated(d))return;f.forEach(function(r,s){var m=r.children[0],l=i?String(j+s).length+1:1,n=Math.ceil(l/4)*4,o=a(r).offset,p=a(m).offset,f,k,c,q;f=h.slice(o,p),f=f.replace(/\[[x ]?\]\s*$/i,''),b==='tab-size'?k=n:b==='space'?k=l+1:k=d.loose?n:l+1,f.length!==k&&(c=k-f.length,q=c>0?'add':'remove',c=Math.abs(c),g.warn('Incorrect list-item indent: '+q+' '+c+' '+e('space',c),a(m)))})}),j()}var d=b('../utilities/visit'),c=b('../utilities/position'),e=b('../utilities/plural'),a=c.start,f={'tab-size':!0,mixed:!0,space:!0};g.exports=h},{'../utilities/plural':60,'../utilities/position':61,'../utilities/visit':63}],23:[function(d,f,h){'use strict';function g(d,f,h,g){e(d,'list',function(g){var d=g.children,e=!0,h=b(g).column,i;if(a.isGenerated(g))return;d.forEach(function(h){var a=h.children,d=a[0],f=a[a.length-1],g=c(f).line-b(d).line>0;g&&(e=!1)}),i=e?'tight':'loose',d.forEach(function(g,k){var a=d[k+1],j=c(g).column>h;if(!a)return;j!==e&&f.warn('List item should be '+i+', isn’t',{start:c(g),end:b(a)})})}),g()}var e=d('../utilities/visit'),a=d('../utilities/position'),b=a.start,c=a.end;f.exports=g},{'../utilities/position':61,'../utilities/visit':63}],24:[function(a,e,g){'use strict';function f(e,f,a,g){a=isNaN(a)||typeof a!=='number'?60:a,b(e,'heading',function(b){if(d.isGenerated(b))return;c(b).length>a&&f.warn('Use headings shorter than `'+a+'`',b)}),g()}var b=a('../utilities/visit'),c=a('../utilities/to-string'),d=a('../utilities/position');e.exports=f},{'../utilities/position':61,'../utilities/to-string':62,'../utilities/visit':63}],25:[function(e,f,i){'use strict';function g(a){return a.type==='heading'||a.type==='table'||a.type==='code'}function h(l,n,i,p){function k(a,b){a--;while(++ae||h.columne&&n.warn('Line must be at most '+e+' characters',{line:f+1,column:j+1});p()}var c=e('../utilities/visit'),a=e('../utilities/position'),b=a.start,d=a.end;f.exports=h},{'../utilities/position':61,'../utilities/visit':63}],26:[function(d,j,k){'use strict';function i(a){return g.test(f(a))}function h(d,f,h,g){e(d,'link',function(d){var e=c(d.children[0]).column,g=b(d.children[d.children.length-1]).column,h=c(d).column,j=b(d).column;if(a.isGenerated(d))return;h===e-1&&j===g+1&&!i(d)&&f.warn('All automatic links must start with a protocol',d)}),g()}var e=d('../utilities/visit'),f=d('../utilities/to-string'),a=d('../utilities/position'),c=a.start,b=a.end,g=/^[a-z][a-z+.-]+:\/?/i;j.exports=h},{'../utilities/position':61,'../utilities/to-string':62,'../utilities/visit':63}],27:[function(b,d,f){'use strict';function e(f,b,h,g){var d=b.toString(),e=d.length;c(f,'blockquote',function(c){var g=a.start(c).line,f=c.position&&c.position.indent;if(a.isGenerated(c)||!f||!f.length)return;f.forEach(function(h,i){var a,c=g+i+1,f=b.positionToOffset({line:c,column:h})-1;while(++f')return;if(a!==' '&&a!==' ')break}b.warn('Missing caret in blockquote',{line:c,column:h})})}),g()}var c=b('../utilities/visit'),a=b('../utilities/position');d.exports=e},{'../utilities/position':61,'../utilities/visit':63}],28:[function(b,f,h){'use strict';function g(f,g,i,h){function b(e,b,f){var a=b.line-e.line,c=a>0?'before':'after';a=Math.abs(a)-f,a>0&&g.warn('Remove '+a+' '+d('line',a)+' '+c+' node',b)}c(f,function(d){var c=d.children;if(a.isGenerated(d))return;c&&c[0]&&(b(a.start(d),a.start(c[0]),0),c.forEach(function(f,h){var d=c[h-1],g=e;if(!d)return;(d.type==='list'&&f.type==='list'||f.type==='code'&&d.type==='list'&&!f.lang)&&g++,b(a.end(d),a.start(f),g)}),b(a.end(d),a.end(c[c.length-1]),1))}),h()}var c=b('../utilities/visit'),a=b('../utilities/position'),d=b('../utilities/plural'),e=2;f.exports=g},{'../utilities/plural':60,'../utilities/position':61,'../utilities/visit':63}],29:[function(c,d,f){'use strict';function e(d,f,h,g){function e(b){var d=c[b.identifier],e;if(a.isGenerated(b))return;d&&d.type&&(e=a.start(d),f.warn('Do not use definitions with the same identifier ('+e.line+':'+e.column+')',b)),c[b.identifier]=b}var c={};b(d,'definition',e),b(d,'footnoteDefinition',e),g()}var a=c('../utilities/position'),b=c('../utilities/visit');d.exports=e},{'../utilities/position':61,'../utilities/visit':63}],30:[function(a,e,g){'use strict';function f(e,f,h,g){var a={};c(e,'heading',function(c){var g=d(c).toUpperCase(),e=a[g],h;if(b.isGenerated(c))return;e&&e.type==='heading'&&(h=b.start(e),f.warn('Do not use headings with similar content ('+h.line+':'+h.column+')',c)),a[g]=c}),g()}var b=a('../utilities/position'),c=a('../utilities/visit'),d=a('../utilities/to-string');e.exports=f},{'../utilities/position':61,'../utilities/to-string':62,'../utilities/visit':63}],31:[function(a,e,g){'use strict';function f(a,e,g,f){b(a,'paragraph',function(b,j,k){var f=b.children,a=f[0],g=k.children[j-1],h=k.children[j+1],i;if(d.isGenerated(b))return;(!g||g.type!=='heading')&&h&&h.type==='paragraph'&&f.length===1&&(a.type==='emphasis'||a.type==='strong')&&(i=c(a),i.charAt(i.length-1)===':'&&e.warn('Don’t use emphasis to introduce a section, use a heading',b))}),f()}var b=a('../utilities/visit'),c=a('../utilities/to-string'),d=a('../utilities/position');e.exports=f},{'../utilities/position':61,'../utilities/to-string':62,'../utilities/visit':63}],32:[function(c,a,d){'use strict';function b(d,a,e,c){var b=a.filename&&a.filename.match(/^(the|an?)\b/i);b&&a.warn('Do not start file names with `'+b[0]+'`'),c()}a.exports=b},{}],33:[function(c,a,d){'use strict';function b(c,a,d,b){a.filename&&/-{2,}/.test(a.filename)&&a.warn('Do not use consecutive dashes in a file name'),b()}a.exports=b},{}],34:[function(c,a,d){'use strict';function b(d,a,e,c){var b=a.filename&&a.filename.match(/[^.a-zA-Z0-9-]/);b&&a.warn('Do not use `'+b[0]+'` in a file name'),c()}a.exports=b},{}],35:[function(c,a,d){'use strict';function b(d,b,e,c){var a=b.filename;a&&!(a===a.toLowerCase()||a===a.toUpperCase())&&b.warn('Do not mix casing in file names'),c()}a.exports=b},{}],36:[function(c,a,d){'use strict';function b(c,a,d,b){a.filename&&/^-|-$/.test(a.filename)&&a.warn('Do not use initial or final dashes in a file name'),b()}a.exports=b},{}],37:[function(c,i,j){'use strict';function h(i,c,k,j){var h=c.toString();f(i,'heading',function(i){var n=i.depth,k=i.children,l=g(i,'atx'),m,o,f,p,j;if(b.isGenerated(i))return;if(l==='atx'||l==='atx-closed'){m=a(i),j=m.offset;while(h.charAt(j)!=='#')j++;j=n+(j-m.offset),f=a(k[0]).column-m.column-1-j,f&&(p=f>0?'Remove':'Add',f=Math.abs(f),c.warn(p+' '+f+' '+e('space',f)+' before this heading’s content',a(k[0])))}l==='atx-closed'&&(o=d(k[k.length-1]),f=d(i).column-o.column-1-n,f&&c.warn('Remove '+f+' '+e('space',f)+' after this heading’s content',o))}),j()}var f=c('../utilities/visit'),g=c('../utilities/heading-style'),e=c('../utilities/plural'),b=c('../utilities/position'),a=b.start,d=b.end;i.exports=h},{'../utilities/heading-style':59,'../utilities/plural':60,'../utilities/position':61,'../utilities/visit':63}],38:[function(a,f,h){'use strict';function g(h,f,j,i){var a=f.toString(),g=a.length;c(h,'heading',function(l){var h=e(l),j=h.offset,i=j-1,k,c;if(b.isGenerated(l))return;while(++i1&&b.warn('Do not indent table rows',c)})}),e()}var c=b('../utilities/visit'),a=b('../utilities/position');d.exports=e},{'../utilities/position':61,'../utilities/visit':63}],49:[function(c,a,d){'use strict';function b(f,b,g,e){var c=b.toString(),a=-1,d=c.length;while(++ac)if(d(b[a],a)===!1)return!1;return!0}function d(i,a,d,f){var g,e,h;typeof a==='function'&&(f=d,d=a,a=null),g=f?c:b,h=function(a,b){return g(a,function(a,c){return e(a,c,b)})},e=function(b,e,f){var c;return(!a||b.type===a)&&(c=d(b,e||0,f||null)),b.children&&c!==!1?h(b.children,b):c},e(i)}a.exports=d},{}],64:[function(i,c,j){'use strict';function e(b,c){function a(e){c(e);var b=e.children,d=-1,f=b?b.length:0;while(++dc)return{line:b+1,column:(c-a[b-1]||0)+1};return{}}return b}function d(i,c){var d=String(c).split('\n'),g;if(!c||typeof c.contents!=='string')throw new Error('Missing `file` for mdast-range');d=b(d),g=h(d),c.offsetToPosition=f(d),c.positionToOffset=g,e(i,function(c){var b=c.position;b&&b.start&&a(b.start,g),b&&b.end&&a(b.end,g)})}function g(){return d}c.exports=g},{}],65:[function(n,h,m){'use strict';function f(a){return new RegExp('(\\s*'+'\\s*'+')')}function j(b,c){function a(b,e){var d=this||null;c(b,d,e),b.children&&b.children.forEach(a,b)}a(b)}function g(c){var a={};return c.replace(b,function(g,c,d,e,f){var b=d||e||f||'';return b==='true'||b===''?b=!0:b==='false'?b=!1:isNaN(b)||(b=Number(b)),a[c]=b,''}),a}function a(a,b){function e(e,i){var f=e.value,c,h;return e.type!=='html'?null:(c=f.match(d),!c||c[1].length!==f.length||c[2]!==a.name?null:(h={type:c[3]||'marker',attributes:c[4]||'',parameters:g(c[4]||''),node:e},b&&b(h,i),h))}var c=a.name,d=f(c);return e}function d(e,b){var c=b.onparse,d=a(b,function(a,b){a.type==='marker'&&c(a,b)});return function(){var a=e.apply(this,arguments);return d(a,this),a}}function e(e,b){var c=b.onstringify,d=a(b,function(a,b){a.type==='marker'&&c(a,b)});return function(a){return d(a,this),e.apply(this,arguments)}}function k(h){function l(l,m,j){var a=k(l),h=a&&a.type;d&&m===d&&(h==='start'&&e++,h==='end'&&e--,h==='end'&&e===0?(b=i(f,b,a,{start:j-b.length-1,end:j,parent:d}),b&&c.apply(d.children,[g,j+1].concat(b)),f=null,d=null,g=null,b=[]):b.push(l)),!d&&h==='start'&&(e=1,g=j,f=a,d=m)}var i=h.onrun,k=a(h),b=[],f=null,d=null,e=0,g;return function(a){j(a,l)}}function l(b,a){var c=b.Parser.prototype.blockTokenizers,f=b.Parser.prototype.inlineTokenizers,g=b.Compiler.prototype;return a.onparse&&(c.html=d(c.html,a),f.tag=d(f.tag,a)),a.onstringify&&(g.html=e(g.html,a)),a.onrun?k(a):null}function i(a){if(!(a&&a.name))throw new Error('Missing `name` in `options`');return function(b){return l(b,a)}}var c=[].splice,b=new RegExp('\\s*([-a-z09_]+)(?:=(?:"((?:\\\\[\\s\\S]|[^"])+)"|\'((?:\\\\[\\s\\S]|[^\'])+)\'|((?:\\\\[\\s\\S]|[^"\'\\s])+)))?\\s*','gi');h.exports=i},{}]},{},[1])(1)}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..56874ad --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "mdast-lint", + "version": "0.0.0", + "description": "Lint markdown with mdast", + "license": "MIT", + "keywords": [ + "markdown", + "lint", + "validate", + "mdast" + ], + "repository": { + "type": "git", + "url": "https://github.com/wooorm/mdast-lint.git" + }, + "author": { + "name": "Titus Wormer", + "email": "tituswormer@gmail.com" + }, + "dependencies": { + "mdast-range": "^0.4.0", + "mdast-zone": "^0.2.1" + }, + "files": [ + "index.js", + "lib/", + "LICENSE" + ], + "peerDependencies": { + "mdast": ">=0.21.0" + }, + "devDependencies": { + "browserify": "^10.0.0", + "dox": "^0.8.0", + "eslint": "^0.22.0", + "esmangle": "^1.0.0", + "istanbul": "^0.3.0", + "jscs": "^1.0.0", + "jscs-jsdoc": "^1.0.0", + "mdast": "^0.21.0", + "mdast-github": "^0.3.0", + "mdast-toc": "^0.4.1", + "mdast-usage": "^0.2.0", + "mocha": "^2.0.0" + }, + "scripts": { + "test-api": "mocha --check-leaks test/index.js", + "test-coveralls": "istanbul cover _mocha --report lcovonly -- --check-leaks test/index.js", + "test-coverage": "istanbul cover _mocha -- --check-leaks test/index.js", + "test-travis": "npm run test-coveralls", + "test": "npm run test-api", + "lint-api": "eslint index.js lib test", + "lint-style": "jscs --reporter inline index.js lib test", + "lint": "npm run lint-api && npm run lint-style", + "make": "npm run lint && npm run test-coverage", + "build-rules": "node script/build-rule-documentation.js", + "build-md": "mdast . LICENSE --output", + "build-bundle": "browserify index.js -s mdast > mdast.js", + "postbuild-bundle": "esmangle mdast.js > mdast.min.js", + "build": "npm run build-rules && npm run build-md && npm run build-bundle", + "prepublish": "npm run build" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..66630fa --- /dev/null +++ b/readme.md @@ -0,0 +1,173 @@ +# ![mdast-lint](http://i61.tinypic.com/2eeh36g.png) + +[![Build Status](https://img.shields.io/travis/wooorm/mdast-lint.svg?style=flat)](https://travis-ci.org/wooorm/mdast-lint) [![Coverage Status](https://img.shields.io/coveralls/wooorm/mdast-lint.svg?style=flat)](https://coveralls.io/r/wooorm/mdast-lint?branch=master) + +**mdast-lint** is a markdown code style linter. Another linter? Yes. +Ensuring the markdown you (and contributors) write is of great quality will +provide better rendering in all the different markdown parsers, and makes +sure less refactoring is needed afterwards. What is quality? That’s up to you, +but the defaults are sensible :ok_hand:. + +**mdast-lint** has lots of tests. Supports Node, io.js, and the browser. +100% coverage. 40+ rules. It’s build on [**mdast**](https://github.com/wooorm/mdast), +a powerful markdown processor powered by [plugins](https://github.com/wooorm/mdast/blob/master/doc/plugins.md) +(such as this one). + +## Table of Contents + +* [Installation](#installation) +* [Command line](#command-line) +* [Programmatic](#programmatic) +* [Rules](#rules) +* [Configuring mdast-lint](#configuring-mdast-lint) +* [Combining with other plug-ins](#combining-with-other-plug-ins) +* [Using mdast to fix your markdown](#using-mdast-to-fix-your-markdown) +* [License](#license) + +## Installation + +[npm](https://docs.npmjs.com/cli/install): + +```bash +npm install mdast-lint +``` + +**mdast-lint** is also available for bower, component, duo, and for AMD, +CommonJS, and globals. + +## Command line + +![](http://i60.tinypic.com/125gtn9.png "Example how mdast-lint looks on screen") + +Use mdast-lint together with mdast: + +```bash +npm install --global mdast mdast-lint +``` + +Let’s say `example.md` looks as follows: + +```md +* Hello + +- World +``` + +Then, to run **mdast-lint** on `example.md`: + +```bash +mdast -u mdast-lint example.md +# +# Yields: +# +# example.md +# 1:3 warning Incorrect list-item content indent: add 2 spaces list-item-indent +# 3:1 warning Invalid ordered list item marker: should be `*` unordered-list-marker-style +# +# ✖ 2 problems (0 errors, 2 warnings) +# +# * Hello +# +# +# * World +# +``` + +See [doc/rules.md](doc/rules.md) to see what those warnings are, and how to +turn them off. + +## Programmatic + +[doc/api.md](doc/api.md) describes how to use **mdast-lint**’s +programatic interface in JavaScript. + +## Rules + +[doc/rules.md](doc/rules.md) describes all available rules, what they check +for, examples of markdown they warn for, and how to fix their warnings. + +## Configuring mdast-lint + +**mdast-lint** is just an **mdast** plug-in. Meaning, you can opt to +configure using configuration files. Read more about these files +(`.mdastrc` or `package.json`) in [**mdast**’s docs](https://github.com/wooorm/mdast/blob/master/doc/mdastrc.5.md). + +An example `.mdastrc` file could look as follows: + +```json +{ + "plugins": { + "lint": { + "no-multiple-toplevel-headings": false, + "maximum-line-length": 79, + "emphasis-marker": "_", + "strong-marker": "*" + } + }, + "settings": { + "commonmark": true + } +} +``` + +Where the object at `plugins.lint` is a map of `ruleId`s and their values. +The object at `settings` determines how **mdast** parses (and compiles) +markdown code. Read more about the latter on [**mdast**’s readme](https://github.com/wooorm/mdast#mdastprocessvalue-options-done). + +In addition, you can also provide configuration comments to turn a rule +on or off inside a file (note that you cannot change what a setting, such as +`maximum-line-length`, you’re either enabling or disabling warnings). + +The following file will warn twice for the duplicate headings: + +```markdown +# Hello + +## Hello + +### Hello +``` + +The following file will warn once (the second heading is ignored, +but the third is re-enabled): + +```markdown +# Hello + + + +## Hello + + + +### Hello +``` + +## Combining with other plug-ins + +As noted above, **mdast-lint** is just an **mdast** plugin. Meaning, you +can use other plug-ins together with it. Such as [mdast-toc](https://github.com/wooorm/mdast-toc), +which will generate a table of contents for you. + +However, these plug-ins will generate new nodes in the syntax tree, nodes +which previously weren’t available: thus, they have no positional information, +and **mdast-lint** cannot warn you about them. + +Therefore, you need to do two things: + +* Process the files twice (this is similar to how LaTeX works); +* Ensure **mdast-lint** runs before the plug-in’s which generate content. + +## Using mdast to fix your markdown + +One of **mdast**’s cool parts is that it compiles to very clean, and highly +cross-vendor supported markdown. It’ll ensure list items use a single bullet, +emphasis and strong use a standard marker, and that your table fences are +aligned. + +**mdast** should be able to fix most of your styling issues automatically, +and I strongly suggest checking out how it can make your life easier :+1: + +## License + +[MIT](LICENSE) © [Titus Wormer](http://wooorm.com) diff --git a/screen-shot.png b/screen-shot.png new file mode 100644 index 0000000..18833fe Binary files /dev/null and b/screen-shot.png differ diff --git a/script/build-rule-documentation.js b/script/build-rule-documentation.js new file mode 100755 index 0000000..80404fd --- /dev/null +++ b/script/build-rule-documentation.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module build-rule-documentation + * @fileoverview Creates documentation for all exposed + * rules. + */ + +'use strict'; + +/* + * Dependencies. + */ + +var fs = require('fs'); +var path = require('path'); +var dox = require('dox'); +var mdast = require('mdast'); +var toc = require('mdast-toc'); +var rules = require('../lib/rules'); + +function find(tags, key) { + var value = null; + + tags.some(function (tag) { + if (tag && tag.type === key) { + value = tag; + + return true; + } + }); + + return value; +} + +var children = []; + +/* + * Add main heading. + */ + +children.push({ + 'type': 'heading', + 'depth': 1, + 'children': [{ + 'type': 'text', + 'value': 'List of Rules' + }] +}); + +/* + * Add main description. + */ + +children.push({ + 'type': 'paragraph', + 'children': [{ + 'type': 'text', + 'value': 'This document describes all available rules, what they\n' + + 'check for, examples of what they warn for, and how to\n' + + 'fix their warnings.' + }] +}); + +/* + * Add the table-of-contents heading. + */ + +children.push({ + 'type': 'heading', + 'depth': 2, + 'children': [{ + 'type': 'text', + 'value': 'Table of Contents' + }] +}); + +/* + * Add the rules heading. + */ + +children.push({ + 'type': 'heading', + 'depth': 2, + 'children': [{ + 'type': 'text', + 'value': 'Rules' + }] +}); + +/* + * Add a section on how to turn of rules. + */ + +children.push({ + 'type': 'paragraph', + 'children': [{ + 'type': 'text', + 'value': 'Remember that rules can always be turned off by\n' + + 'passing false. In addition, when reset is given, values can\n' + + 'be null or undefined in order to be ignored.' + }] +}); + +/* + * Add rules. + */ + +Object.keys(rules).sort().forEach(function (ruleId) { + var filePath = path.join('lib', 'rules', ruleId + '.js'); + var code = fs.readFileSync(filePath, 'utf-8'); + var tags = dox.parseComments(code)[0].tags; + var description = find(tags, 'fileoverview'); + var example = find(tags, 'example'); + + if (!description) { + throw new Error(ruleId + ' is missing a `@fileoverview`'); + } else { + description = description.string; + } + + if (example) { + example = example.string; + } + + children.push({ + 'type': 'heading', + 'depth': 3, + 'children': [{ + 'type': 'text', + 'value': ruleId + }] + }); + + if (example) { + children.push({ + 'type': 'code', + 'lang': 'md', + 'value': example + }); + } + + children = children.concat(mdast().parse(description).children); +}); + +/* + * Node. + */ + +var node = { + 'type': 'root', + 'children': children +}; + +/* + * Add toc. + */ + +mdast().use(toc).run(node); + +/* + * Write. + */ + +fs.writeFileSync('doc/rules.md', mdast().stringify(node)); diff --git a/test/clean.js b/test/clean.js new file mode 100644 index 0000000..e86bdfe --- /dev/null +++ b/test/clean.js @@ -0,0 +1,42 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Clean + * @fileoverview mdast plug-in used to remove positional + * information from mdast’s syntax tree. + * @todo Externalise into its own repository. + */ + +'use strict'; + +/* + * Dpendencies. + */ + +var visit = require('../lib/utilities/visit'); + +/** + * Delete the `position` key for each node. + * + * @param {Node} ast - Root node. + */ +function transformer(ast) { + visit(ast, function (node) { + node.position = undefined; + }); +} + +/** + * Return `transformer`. + * + * @return {Function} - See `transformer`. + */ +function attacher() { + return transformer; +} + +/* + * Expose. + */ + +module.exports = attacher; diff --git a/test/fixtures/-file-name-initial-dash.md b/test/fixtures/-file-name-initial-dash.md new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/blockquote-indentation-2.md b/test/fixtures/blockquote-indentation-2.md new file mode 100644 index 0000000..c4ff22f --- /dev/null +++ b/test/fixtures/blockquote-indentation-2.md @@ -0,0 +1,9 @@ +> Foo + + + +> Bar + + + +> Baz diff --git a/test/fixtures/blockquote-indentation-4.md b/test/fixtures/blockquote-indentation-4.md new file mode 100644 index 0000000..be22310 --- /dev/null +++ b/test/fixtures/blockquote-indentation-4.md @@ -0,0 +1,9 @@ +> Foo + + + +> Bar + + + +> Baz diff --git a/test/fixtures/code-style-fenced.md b/test/fixtures/code-style-fenced.md new file mode 100644 index 0000000..a25b7fb --- /dev/null +++ b/test/fixtures/code-style-fenced.md @@ -0,0 +1,11 @@ +Some fenced code block: + +``` +foo +``` + +And one with language flag: + +```barscript +bar +``` diff --git a/test/fixtures/code-style-indented.md b/test/fixtures/code-style-indented.md new file mode 100644 index 0000000..ebb7e7a --- /dev/null +++ b/test/fixtures/code-style-indented.md @@ -0,0 +1,7 @@ +Some indented code block: + + foo + +And another: + + bar diff --git a/test/fixtures/comments-disable.md b/test/fixtures/comments-disable.md new file mode 100644 index 0000000..a4993fb --- /dev/null +++ b/test/fixtures/comments-disable.md @@ -0,0 +1,7 @@ + + +alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november. + + + +alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november. diff --git a/test/fixtures/comments-duplicates.md b/test/fixtures/comments-duplicates.md new file mode 100644 index 0000000..9cadb72 --- /dev/null +++ b/test/fixtures/comments-duplicates.md @@ -0,0 +1,7 @@ + + +alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november. + + + +alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november. diff --git a/test/fixtures/comments-enable.md b/test/fixtures/comments-enable.md new file mode 100644 index 0000000..29a7330 --- /dev/null +++ b/test/fixtures/comments-enable.md @@ -0,0 +1,7 @@ + + +alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november. + + + +alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november. diff --git a/test/fixtures/comments-inline.md b/test/fixtures/comments-inline.md new file mode 100644 index 0000000..642aff0 --- /dev/null +++ b/test/fixtures/comments-inline.md @@ -0,0 +1 @@ + This is HTML. diff --git a/test/fixtures/comments-invalid-keyword.md b/test/fixtures/comments-invalid-keyword.md new file mode 100644 index 0000000..e840556 --- /dev/null +++ b/test/fixtures/comments-invalid-keyword.md @@ -0,0 +1,5 @@ +Intro. + + + +Outro. diff --git a/test/fixtures/comments-invalid-rule-id.md b/test/fixtures/comments-invalid-rule-id.md new file mode 100644 index 0000000..6d674c4 --- /dev/null +++ b/test/fixtures/comments-invalid-rule-id.md @@ -0,0 +1,5 @@ +Intro. + + + +Outro. diff --git a/test/fixtures/comments-none.md b/test/fixtures/comments-none.md new file mode 100644 index 0000000..24cda23 --- /dev/null +++ b/test/fixtures/comments-none.md @@ -0,0 +1 @@ +Things should not fail without warnings, nor comments. Alpha bravo charlie delta echo foxtrot. diff --git a/test/fixtures/definition-case-invalid.md b/test/fixtures/definition-case-invalid.md new file mode 100644 index 0000000..f051119 --- /dev/null +++ b/test/fixtures/definition-case-invalid.md @@ -0,0 +1,3 @@ +This document has definitions with improper spacing and casing. + +[Invalid]: http://example.com/favicon.ico "Example Domain" diff --git a/test/fixtures/definition-case-valid.md b/test/fixtures/definition-case-valid.md new file mode 100644 index 0000000..43f2fb6 --- /dev/null +++ b/test/fixtures/definition-case-valid.md @@ -0,0 +1,3 @@ +This document has definitions with proper spacing and casing. + +[valid]: http://example.com/favicon.ico "Example Domain" diff --git a/test/fixtures/definition-spacing-invalid.md b/test/fixtures/definition-spacing-invalid.md new file mode 100644 index 0000000..57b5743 --- /dev/null +++ b/test/fixtures/definition-spacing-invalid.md @@ -0,0 +1,3 @@ +This document has definitions with improper spacing and casing. + +[another invalid]: http://example.org/favicon.ico "Example Domain" diff --git a/test/fixtures/definition-spacing-valid.md b/test/fixtures/definition-spacing-valid.md new file mode 100644 index 0000000..b65290b --- /dev/null +++ b/test/fixtures/definition-spacing-valid.md @@ -0,0 +1,3 @@ +This document has definitions with proper spacing and casing. + +[another valid]: http://example.org/favicon.ico "Example Domain" diff --git a/test/fixtures/emphasis-marker-asterisk-underscore.md b/test/fixtures/emphasis-marker-asterisk-underscore.md new file mode 100644 index 0000000..256c356 --- /dev/null +++ b/test/fixtures/emphasis-marker-asterisk-underscore.md @@ -0,0 +1,3 @@ +*foo* + +_bar_ diff --git a/test/fixtures/emphasis-marker-asterisk.md b/test/fixtures/emphasis-marker-asterisk.md new file mode 100644 index 0000000..4ac70c5 --- /dev/null +++ b/test/fixtures/emphasis-marker-asterisk.md @@ -0,0 +1,3 @@ +*foo* + +*bar* diff --git a/test/fixtures/emphasis-marker-underscore-asterisk.md b/test/fixtures/emphasis-marker-underscore-asterisk.md new file mode 100644 index 0000000..5c58ea2 --- /dev/null +++ b/test/fixtures/emphasis-marker-underscore-asterisk.md @@ -0,0 +1,3 @@ +_foo_ + +*bar* diff --git a/test/fixtures/emphasis-marker-underscore.md b/test/fixtures/emphasis-marker-underscore.md new file mode 100644 index 0000000..6dd4f37 --- /dev/null +++ b/test/fixtures/emphasis-marker-underscore.md @@ -0,0 +1,3 @@ +_foo_ + +_bar_ diff --git a/test/fixtures/empty.md b/test/fixtures/empty.md new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/fenced-code-flag-invalid.md b/test/fixtures/fenced-code-flag-invalid.md new file mode 100644 index 0000000..23ae4a0 --- /dev/null +++ b/test/fixtures/fenced-code-flag-invalid.md @@ -0,0 +1,5 @@ +A missing code flag. + +``` +foo +``` diff --git a/test/fixtures/fenced-code-flag-unknown.md b/test/fixtures/fenced-code-flag-unknown.md new file mode 100644 index 0000000..eb098bd --- /dev/null +++ b/test/fixtures/fenced-code-flag-unknown.md @@ -0,0 +1,5 @@ +A missing code flag. + +```bar +foo +``` diff --git a/test/fixtures/fenced-code-flag-valid.md b/test/fixtures/fenced-code-flag-valid.md new file mode 100644 index 0000000..0a81845 --- /dev/null +++ b/test/fixtures/fenced-code-flag-valid.md @@ -0,0 +1,7 @@ +A nice code flag. + +```foo +foo +``` + + foo diff --git a/test/fixtures/fenced-code-marker-mismatched.md b/test/fixtures/fenced-code-marker-mismatched.md new file mode 100644 index 0000000..f73cd63 --- /dev/null +++ b/test/fixtures/fenced-code-marker-mismatched.md @@ -0,0 +1,9 @@ +```foo +bar(); +``` + +~~~ +baz(); +~~~ + + qux(); diff --git a/test/fixtures/fenced-code-marker-tick.md b/test/fixtures/fenced-code-marker-tick.md new file mode 100644 index 0000000..b1b9e70 --- /dev/null +++ b/test/fixtures/fenced-code-marker-tick.md @@ -0,0 +1,9 @@ +```foo +bar(); +``` + +``` +baz(); +``` + + qux(); diff --git a/test/fixtures/fenced-code-marker-tilde.md b/test/fixtures/fenced-code-marker-tilde.md new file mode 100644 index 0000000..3bc5419 --- /dev/null +++ b/test/fixtures/fenced-code-marker-tilde.md @@ -0,0 +1,9 @@ +~~~foo +bar(); +~~~ + +~~~ +baz(); +~~~ + + qux(); diff --git a/test/fixtures/file-extension-markdown.markdown b/test/fixtures/file-extension-markdown.markdown new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/file-extension-md.md b/test/fixtures/file-extension-md.md new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/file-name characters.md b/test/fixtures/file-name characters.md new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/file-name--consecutive-dashes.md b/test/fixtures/file-name--consecutive-dashes.md new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/file-name-Upper-case.md b/test/fixtures/file-name-Upper-case.md new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/file-name-final-dash-.md b/test/fixtures/file-name-final-dash-.md new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/file-without-extension b/test/fixtures/file-without-extension new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/final-definition-invalid.md b/test/fixtures/final-definition-invalid.md new file mode 100644 index 0000000..80e45ad --- /dev/null +++ b/test/fixtures/final-definition-invalid.md @@ -0,0 +1,7 @@ +[valid]: http://example.com/favicon.ico "Example.domain" + +This document has the definitions in some weird... + +[another]: http://example.org/favicon.ico "Example.domain" + +...places in the document. diff --git a/test/fixtures/final-definition-valid.md b/test/fixtures/final-definition-valid.md new file mode 100644 index 0000000..6ea6cd3 --- /dev/null +++ b/test/fixtures/final-definition-valid.md @@ -0,0 +1,4 @@ +This document has the definitions properly at the end of the document. + +[valid]: http://example.com/favicon.ico "Example.domain" +[another]: http://example.org/favicon.ico "Example.domain" diff --git a/test/fixtures/final-newline-invalid.md b/test/fixtures/final-newline-invalid.md new file mode 100644 index 0000000..ce85b46 --- /dev/null +++ b/test/fixtures/final-newline-invalid.md @@ -0,0 +1 @@ +Foo bar. \ No newline at end of file diff --git a/test/fixtures/final-newline-valid.md b/test/fixtures/final-newline-valid.md new file mode 100644 index 0000000..b7035dc --- /dev/null +++ b/test/fixtures/final-newline-valid.md @@ -0,0 +1 @@ +Foo bar. diff --git a/test/fixtures/first-heading-level-invalid.md b/test/fixtures/first-heading-level-invalid.md new file mode 100644 index 0000000..11df538 --- /dev/null +++ b/test/fixtures/first-heading-level-invalid.md @@ -0,0 +1 @@ +## Invalid diff --git a/test/fixtures/first-heading-level-valid.md b/test/fixtures/first-heading-level-valid.md new file mode 100644 index 0000000..ad0eb40 --- /dev/null +++ b/test/fixtures/first-heading-level-valid.md @@ -0,0 +1 @@ +# Valid diff --git a/test/fixtures/hard-break-spaces-invalid.md b/test/fixtures/hard-break-spaces-invalid.md new file mode 100644 index 0000000..1211398 --- /dev/null +++ b/test/fixtures/hard-break-spaces-invalid.md @@ -0,0 +1,5 @@ +Here’s one that uses too +much white space. + +And here’s a commonmark\ +break. diff --git a/test/fixtures/hard-break-spaces-valid.md b/test/fixtures/hard-break-spaces-valid.md new file mode 100644 index 0000000..85cedf0 --- /dev/null +++ b/test/fixtures/hard-break-spaces-valid.md @@ -0,0 +1,5 @@ +Here’s one that uses too +just enough white space. + +And here’s another +break. diff --git a/test/fixtures/heading-increment-invalid-blockquote.md b/test/fixtures/heading-increment-invalid-blockquote.md new file mode 100644 index 0000000..61905e0 --- /dev/null +++ b/test/fixtures/heading-increment-invalid-blockquote.md @@ -0,0 +1,3 @@ +> # Foo +> +> ### Bar diff --git a/test/fixtures/heading-increment-invalid-list.md b/test/fixtures/heading-increment-invalid-list.md new file mode 100644 index 0000000..2c057ed --- /dev/null +++ b/test/fixtures/heading-increment-invalid-list.md @@ -0,0 +1,3 @@ +* # Foo + + ### Bar diff --git a/test/fixtures/heading-increment-invalid.md b/test/fixtures/heading-increment-invalid.md new file mode 100644 index 0000000..4e75f6a --- /dev/null +++ b/test/fixtures/heading-increment-invalid.md @@ -0,0 +1,3 @@ +# Foo + +### Bar diff --git a/test/fixtures/heading-length-normal.md b/test/fixtures/heading-length-normal.md new file mode 100644 index 0000000..311271d --- /dev/null +++ b/test/fixtures/heading-length-normal.md @@ -0,0 +1 @@ +# Normal diff --git a/test/fixtures/heading-length-quite-short.md b/test/fixtures/heading-length-quite-short.md new file mode 100644 index 0000000..a4f04dd --- /dev/null +++ b/test/fixtures/heading-length-quite-short.md @@ -0,0 +1 @@ +# This heading is quite long diff --git a/test/fixtures/heading-length-too-long.md b/test/fixtures/heading-length-too-long.md new file mode 100644 index 0000000..222d8ac --- /dev/null +++ b/test/fixtures/heading-length-too-long.md @@ -0,0 +1 @@ +# This heading is longer than 60 characters longer longer longer diff --git a/test/fixtures/heading-nesting-initial.md b/test/fixtures/heading-nesting-initial.md new file mode 100644 index 0000000..11df538 --- /dev/null +++ b/test/fixtures/heading-nesting-initial.md @@ -0,0 +1 @@ +## Invalid diff --git a/test/fixtures/heading-style-atx-closed.md b/test/fixtures/heading-style-atx-closed.md new file mode 100644 index 0000000..528c349 --- /dev/null +++ b/test/fixtures/heading-style-atx-closed.md @@ -0,0 +1,11 @@ +# ATX-closed # + +## ATX-closed ## + +### ATX-closed ### + +#### ATX-closed #### + +##### ATX-closed ##### + +###### ATX-closed ###### diff --git a/test/fixtures/heading-style-atx.md b/test/fixtures/heading-style-atx.md new file mode 100644 index 0000000..3e6d7f6 --- /dev/null +++ b/test/fixtures/heading-style-atx.md @@ -0,0 +1,13 @@ +# ATX + +## ATX + +### ATX / Setext + +#### ATX / Setext + +##### ATX / Setext + +###### ATX / Setext + +Note that the missing spaces aren’t pretty: but they’re there to test against :) diff --git a/test/fixtures/heading-style-empty.md b/test/fixtures/heading-style-empty.md new file mode 100644 index 0000000..8bbfbec --- /dev/null +++ b/test/fixtures/heading-style-empty.md @@ -0,0 +1,11 @@ +# # + +## + +### ### + +#### + +##### ##### + +###### diff --git a/test/fixtures/heading-style-not-consistent.md b/test/fixtures/heading-style-not-consistent.md new file mode 100644 index 0000000..c161bee --- /dev/null +++ b/test/fixtures/heading-style-not-consistent.md @@ -0,0 +1,12 @@ +# ATX + +Setext +----- + +### ATX Closed ### + +#### ATX / Setext + +##### ATX Closed ###### + +###### ATX / Setext diff --git a/test/fixtures/heading-style-setext.md b/test/fixtures/heading-style-setext.md new file mode 100644 index 0000000..5ddb7cc --- /dev/null +++ b/test/fixtures/heading-style-setext.md @@ -0,0 +1,13 @@ +Setext +===== + +Setext +----- + +### ATX / Setext + +#### ATX / Setext + +##### ATX / Setext + +###### ATX / Setext diff --git a/test/fixtures/link-title-style-double.md b/test/fixtures/link-title-style-double.md new file mode 100644 index 0000000..21572ca --- /dev/null +++ b/test/fixtures/link-title-style-double.md @@ -0,0 +1,5 @@ +[foo bar baz]( "example") + +![foo bar baz]( "example") + +[foo bar baz]: "example" diff --git a/test/fixtures/link-title-style-missing.md b/test/fixtures/link-title-style-missing.md new file mode 100644 index 0000000..f0fc561 --- /dev/null +++ b/test/fixtures/link-title-style-missing.md @@ -0,0 +1,5 @@ +[foo bar baz]() + +![foo bar baz]() + +[foo bar baz]: diff --git a/test/fixtures/link-title-style-parentheses.md b/test/fixtures/link-title-style-parentheses.md new file mode 100644 index 0000000..33544d6 --- /dev/null +++ b/test/fixtures/link-title-style-parentheses.md @@ -0,0 +1,5 @@ +[foo bar baz]( (example)) + +![foo bar baz]( (example)) + +[foo bar baz]: (example) diff --git a/test/fixtures/link-title-style-single.md b/test/fixtures/link-title-style-single.md new file mode 100644 index 0000000..ff4c73d --- /dev/null +++ b/test/fixtures/link-title-style-single.md @@ -0,0 +1,5 @@ +[foo bar baz]( 'example') + +![foo bar baz]( 'example') + +[foo bar baz]: 'example' diff --git a/test/fixtures/link-title-style-trailing-white-space.md b/test/fixtures/link-title-style-trailing-white-space.md new file mode 100644 index 0000000..607f932 --- /dev/null +++ b/test/fixtures/link-title-style-trailing-white-space.md @@ -0,0 +1,5 @@ +[foo bar baz]( "example" ) + +![foo bar baz]( 'example' ) + +[foo bar baz]: (example) diff --git a/test/fixtures/list-item-bullet-indent-invalid.md b/test/fixtures/list-item-bullet-indent-invalid.md new file mode 100644 index 0000000..b8114ec --- /dev/null +++ b/test/fixtures/list-item-bullet-indent-invalid.md @@ -0,0 +1,4 @@ +Some text + + * List item + * List item diff --git a/test/fixtures/list-item-bullet-indent-valid.md b/test/fixtures/list-item-bullet-indent-valid.md new file mode 100644 index 0000000..6e4fc7e --- /dev/null +++ b/test/fixtures/list-item-bullet-indent-valid.md @@ -0,0 +1,4 @@ +Some text + +* List item +* List item diff --git a/test/fixtures/list-item-content-indent-invalid.md b/test/fixtures/list-item-content-indent-invalid.md new file mode 100644 index 0000000..f1ce5ba --- /dev/null +++ b/test/fixtures/list-item-content-indent-invalid.md @@ -0,0 +1,14 @@ +* List item + + * Nested list item indented by 3 spaces + + + +* List item + + * [x] with checked checkbox. + + + +1. Foo + 1. Bar diff --git a/test/fixtures/list-item-content-indent-valid.md b/test/fixtures/list-item-content-indent-valid.md new file mode 100644 index 0000000..03e613f --- /dev/null +++ b/test/fixtures/list-item-content-indent-valid.md @@ -0,0 +1,14 @@ +* List item + + * Nested list item indented by 4 spaces + + + +* List item + + * [x] with checked checkbox. + + + +1. Foo + 1. Bar diff --git a/test/fixtures/list-item-indent-mixed.md b/test/fixtures/list-item-indent-mixed.md new file mode 100644 index 0000000..1020465 --- /dev/null +++ b/test/fixtures/list-item-indent-mixed.md @@ -0,0 +1,50 @@ +Foo: + +- item 1 +- item 2 +- item 3 + +Bar: + +1. item 1 +2. item 2 +3. item 3 + +Baz: + +- item + 1 + +- item + 2 + +- item + 3 + +Qux: + +1. item + 1 + +2. item + 2 + +3. item + 3 + +And an extra test for numbers: + +9. foo +10. bar +11. baz + +...and loose: + +9. foo + 1 + +10. bar + 2 + +11. baz + 3 diff --git a/test/fixtures/list-item-indent-space.md b/test/fixtures/list-item-indent-space.md new file mode 100644 index 0000000..95d4b98 --- /dev/null +++ b/test/fixtures/list-item-indent-space.md @@ -0,0 +1,48 @@ +Foo: + +- item 1 +- item 2 +- item 3 + +Bar: + +1. item 1 +2. item 2 +3. item 3 + +Baz: + +- item + 1 + +- item + 2 + +- item + 3 + +1. item + 1 + +2. item + 2 + +3. item + 3 + +And an extra test for numbers: + +9. foo +10. bar +11. baz + +...and loose: + +9. foo + 1 + +10. bar + 2 + +11. baz + 3 diff --git a/test/fixtures/list-item-indent-tab-size.md b/test/fixtures/list-item-indent-tab-size.md new file mode 100644 index 0000000..8b46f74 --- /dev/null +++ b/test/fixtures/list-item-indent-tab-size.md @@ -0,0 +1,50 @@ +Foo: + +- item 1 +- item 2 +- item 3 + +Bar: + +1. item 1 +2. item 2 +3. item 3 + +Baz: + +- item + 1 + +- item + 2 + +- item + 3 + +Qux: + +1. item + 1 + +2. item + 2 + +3. item + 3 + +And an extra test for numbers + +9. foo +10. bar +11. baz + +...and loose: + +9. foo + 1 + +10. bar + 2 + +11. baz + 3 diff --git a/test/fixtures/list-item-marker-asterisk.md b/test/fixtures/list-item-marker-asterisk.md new file mode 100644 index 0000000..544f94c --- /dev/null +++ b/test/fixtures/list-item-marker-asterisk.md @@ -0,0 +1,10 @@ +* item 1 + + +* item 1 + + +> * item 1 + + +> * item 1 diff --git a/test/fixtures/list-item-marker-dash.md b/test/fixtures/list-item-marker-dash.md new file mode 100644 index 0000000..bea0b69 --- /dev/null +++ b/test/fixtures/list-item-marker-dash.md @@ -0,0 +1,5 @@ +- item 1 +- item 1 + +> - item 1 +> - item 1 diff --git a/test/fixtures/list-item-marker-dot.md b/test/fixtures/list-item-marker-dot.md new file mode 100644 index 0000000..8503fc9 --- /dev/null +++ b/test/fixtures/list-item-marker-dot.md @@ -0,0 +1,5 @@ +1. item 1 +1. item 1 + +> 1. item 1 +> 1. item 1 diff --git a/test/fixtures/list-item-marker-paren.md b/test/fixtures/list-item-marker-paren.md new file mode 100644 index 0000000..4db5fd1 --- /dev/null +++ b/test/fixtures/list-item-marker-paren.md @@ -0,0 +1,5 @@ +1) item 1 +1) item 1 + +> 1) item 1 +> 1) item 1 diff --git a/test/fixtures/list-item-marker-plus.md b/test/fixtures/list-item-marker-plus.md new file mode 100644 index 0000000..4287114 --- /dev/null +++ b/test/fixtures/list-item-marker-plus.md @@ -0,0 +1,10 @@ ++ item 1 + + ++ item 1 + + +> + item 1 + + +> + item 1 diff --git a/test/fixtures/list-item-spacing-loose-invalid.md b/test/fixtures/list-item-spacing-loose-invalid.md new file mode 100644 index 0000000..458c1dc --- /dev/null +++ b/test/fixtures/list-item-spacing-loose-invalid.md @@ -0,0 +1,4 @@ +- Wrapped + item +- item 2 +- item 3 diff --git a/test/fixtures/list-item-spacing-loose-valid.md b/test/fixtures/list-item-spacing-loose-valid.md new file mode 100644 index 0000000..ba65bff --- /dev/null +++ b/test/fixtures/list-item-spacing-loose-valid.md @@ -0,0 +1,6 @@ +- Wrapped + item + +- item 2 + +- item 3 diff --git a/test/fixtures/list-item-spacing-tight-invalid.md b/test/fixtures/list-item-spacing-tight-invalid.md new file mode 100644 index 0000000..550f5ae --- /dev/null +++ b/test/fixtures/list-item-spacing-tight-invalid.md @@ -0,0 +1,5 @@ +- item 1 + +- item 2 + +- item 3 diff --git a/test/fixtures/list-item-spacing-tight-valid.md b/test/fixtures/list-item-spacing-tight-valid.md new file mode 100644 index 0000000..b60d4ec --- /dev/null +++ b/test/fixtures/list-item-spacing-tight-valid.md @@ -0,0 +1,3 @@ +- item 1 +- item 2 +- item 3 diff --git a/test/fixtures/maximum-line-length-invalid.md b/test/fixtures/maximum-line-length-invalid.md new file mode 100644 index 0000000..a20f0e7 --- /dev/null +++ b/test/fixtures/maximum-line-length-invalid.md @@ -0,0 +1,7 @@ +This line is simply toooooooooooooooooooooooooooooooooooooooooooooooooooooo long. + +Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one. + +And this one is also very wrong: because the link starts aaaaaaafter the column: + + and such. diff --git a/test/fixtures/maximum-line-length-valid.md b/test/fixtures/maximum-line-length-valid.md new file mode 100644 index 0000000..67e9af1 --- /dev/null +++ b/test/fixtures/maximum-line-length-valid.md @@ -0,0 +1,16 @@ +This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo +long. + +This is also fine: + + + + + +| An | exception | is | line | length | in | long | tables | because | those | can’t | just | +| -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- | +| be | helped | | | | | | | | | | . | + +The following is also fine, because there is no white-space. + +. diff --git a/test/fixtures/no-auto-link-without-protocol-invalid.md b/test/fixtures/no-auto-link-without-protocol-invalid.md new file mode 100644 index 0000000..da92d7a --- /dev/null +++ b/test/fixtures/no-auto-link-without-protocol-invalid.md @@ -0,0 +1,12 @@ +No tool, including **mdast**, supports the below examples, except for the +e-mail. + + + + + + + + foo + +bar diff --git a/test/fixtures/no-auto-link-without-protocol-valid.md b/test/fixtures/no-auto-link-without-protocol-valid.md new file mode 100644 index 0000000..df77b18 --- /dev/null +++ b/test/fixtures/no-auto-link-without-protocol-valid.md @@ -0,0 +1,7 @@ +[file.html](file.html) + + + + + + diff --git a/test/fixtures/no-blockquote-without-caret-invalid.md b/test/fixtures/no-blockquote-without-caret-invalid.md new file mode 100644 index 0000000..77cb87e --- /dev/null +++ b/test/fixtures/no-blockquote-without-caret-invalid.md @@ -0,0 +1,17 @@ +* > This is a blockquote + > which is immediately followed by + + > this blockquote. Unfortunately + > In some parsers, these are treated as the same blockquote. + +> This is a blockquote +> which is immediately followed by + + +> this blockquote. Unfortunately +> In some parsers, these are treated as the same blockquote. + + + +> Fooo +bar diff --git a/test/fixtures/no-blockquote-without-caret-valid.md b/test/fixtures/no-blockquote-without-caret-valid.md new file mode 100644 index 0000000..a2b97f7 --- /dev/null +++ b/test/fixtures/no-blockquote-without-caret-valid.md @@ -0,0 +1,17 @@ +* > This is a blockquote + > which is immediately followed by + > + > this blockquote. Unfortunately + > In some parsers, these are treated as the same blockquote. + +> This is a blockquote +> which is immediately followed by +> +> +> this blockquote. Unfortunately +> In some parsers, these are treated as the same blockquote. + + + +> Fooo +> bar diff --git a/test/fixtures/no-consecutive-blank-lines-invalid.md b/test/fixtures/no-consecutive-blank-lines-invalid.md new file mode 100644 index 0000000..41b9261 --- /dev/null +++ b/test/fixtures/no-consecutive-blank-lines-invalid.md @@ -0,0 +1,22 @@ + +Foo + + +Bar + + +> Baz +> +> +> Qux + + +1. Quux + + + +1. Foo + + + + Bar diff --git a/test/fixtures/no-consecutive-blank-lines-valid.md b/test/fixtures/no-consecutive-blank-lines-valid.md new file mode 100644 index 0000000..c1a8aed --- /dev/null +++ b/test/fixtures/no-consecutive-blank-lines-valid.md @@ -0,0 +1,15 @@ +Foo + +Bar + +> Baz +> +> Qux + +1. Quux + + +1. Foo + + + Bar diff --git a/test/fixtures/no-duplicate-definitions-invalid.md b/test/fixtures/no-duplicate-definitions-invalid.md new file mode 100644 index 0000000..a26e6e1 --- /dev/null +++ b/test/fixtures/no-duplicate-definitions-invalid.md @@ -0,0 +1,2 @@ +[foo]: bar +[foo]: qux diff --git a/test/fixtures/no-duplicate-definitions-valid.md b/test/fixtures/no-duplicate-definitions-valid.md new file mode 100644 index 0000000..0bb5abb --- /dev/null +++ b/test/fixtures/no-duplicate-definitions-valid.md @@ -0,0 +1,2 @@ +[foo]: bar +[bar]: qux diff --git a/test/fixtures/no-duplicate-headings-invalid.md b/test/fixtures/no-duplicate-headings-invalid.md new file mode 100644 index 0000000..817d106 --- /dev/null +++ b/test/fixtures/no-duplicate-headings-invalid.md @@ -0,0 +1,8 @@ +# Fooo + +## Fooo + +![Bar](this://even-works-with.images/and/links) +--- + +## [Bar](http://bar.com) ## diff --git a/test/fixtures/no-duplicate-headings-valid.md b/test/fixtures/no-duplicate-headings-valid.md new file mode 100644 index 0000000..2491528 --- /dev/null +++ b/test/fixtures/no-duplicate-headings-valid.md @@ -0,0 +1,8 @@ +# toString + +## hasOwnProperty + +![Bar](this://even-works-with.images/and/links) +--- + +## [Baz](http://qux.com) ## diff --git a/test/fixtures/no-emphasis-as-heading-invalid.md b/test/fixtures/no-emphasis-as-heading-invalid.md new file mode 100644 index 0000000..78a6b28 --- /dev/null +++ b/test/fixtures/no-emphasis-as-heading-invalid.md @@ -0,0 +1,7 @@ +**How to make omelets:** + +Break an egg. + +*How to bake bread:* + +Open the flour sack. diff --git a/test/fixtures/no-emphasis-as-heading-valid.md b/test/fixtures/no-emphasis-as-heading-valid.md new file mode 100644 index 0000000..3bb504a --- /dev/null +++ b/test/fixtures/no-emphasis-as-heading-valid.md @@ -0,0 +1,23 @@ +# How to make omelets + +Break an egg. + +## How to bake bread + +Open the flour sack. + +*And this is valid* + +This is also valid: + + foo + +So is **this**: + +Foo. + +# This one’s also Ignored + +_Byline_: + +Foo. diff --git a/test/fixtures/no-heading-content-indent-invalid.md b/test/fixtures/no-heading-content-indent-invalid.md new file mode 100644 index 0000000..6225e23 --- /dev/null +++ b/test/fixtures/no-heading-content-indent-invalid.md @@ -0,0 +1,13 @@ +# Too much spacing + +## Even more spacing ## + +####Too much spacing right, too few left #### + + ## Even works for indented headings ## + +Foo +=== + +Bar +--- diff --git a/test/fixtures/no-heading-content-indent-valid.md b/test/fixtures/no-heading-content-indent-valid.md new file mode 100644 index 0000000..406770e --- /dev/null +++ b/test/fixtures/no-heading-content-indent-valid.md @@ -0,0 +1,13 @@ +# Too much spacing + +## Even more spacing ## + +#### Too much spacing right, too few left #### + + ## Even works for indented headings ## + +Foo +=== + +Bar +--- diff --git a/test/fixtures/no-heading-indent-invalid.md b/test/fixtures/no-heading-indent-invalid.md new file mode 100644 index 0000000..fe3a237 --- /dev/null +++ b/test/fixtures/no-heading-indent-invalid.md @@ -0,0 +1,9 @@ + # Hello world + + Foo +----- + + # Hello world # + + Bar +===== diff --git a/test/fixtures/no-heading-indent-valid.md b/test/fixtures/no-heading-indent-valid.md new file mode 100644 index 0000000..4f2be51 --- /dev/null +++ b/test/fixtures/no-heading-indent-valid.md @@ -0,0 +1,9 @@ +# Hello world + +Foo +----- + +# Hello world # + +Bar +===== diff --git a/test/fixtures/no-heading-punctuation-colon.md b/test/fixtures/no-heading-punctuation-colon.md new file mode 100644 index 0000000..7ccb4fd --- /dev/null +++ b/test/fixtures/no-heading-punctuation-colon.md @@ -0,0 +1 @@ +# Foo: diff --git a/test/fixtures/no-heading-punctuation-period.md b/test/fixtures/no-heading-punctuation-period.md new file mode 100644 index 0000000..351de83 --- /dev/null +++ b/test/fixtures/no-heading-punctuation-period.md @@ -0,0 +1 @@ +# Foo. diff --git a/test/fixtures/no-heading-punctuation-question.md b/test/fixtures/no-heading-punctuation-question.md new file mode 100644 index 0000000..d0777c6 --- /dev/null +++ b/test/fixtures/no-heading-punctuation-question.md @@ -0,0 +1 @@ +# Foo? diff --git a/test/fixtures/no-heading-punctuation-valid.md b/test/fixtures/no-heading-punctuation-valid.md new file mode 100644 index 0000000..7635c78 --- /dev/null +++ b/test/fixtures/no-heading-punctuation-valid.md @@ -0,0 +1 @@ +# Foo diff --git a/test/fixtures/no-html-invalid.md b/test/fixtures/no-html-invalid.md new file mode 100644 index 0000000..4987291 --- /dev/null +++ b/test/fixtures/no-html-invalid.md @@ -0,0 +1,3 @@ +

HTML

+ +Inline HTML might be a problem! diff --git a/test/fixtures/no-html-valid.md b/test/fixtures/no-html-valid.md new file mode 100644 index 0000000..9268e51 --- /dev/null +++ b/test/fixtures/no-html-valid.md @@ -0,0 +1,3 @@ +# HTML + +Inline HTML (HyperText Markup Language) might be a problem! diff --git a/test/fixtures/no-inline-padding-invalid.md b/test/fixtures/no-inline-padding-invalid.md new file mode 100644 index 0000000..dfc5652 --- /dev/null +++ b/test/fixtures/no-inline-padding-invalid.md @@ -0,0 +1,9 @@ +Emphasis: * foo*, and _bar _. + +Strong: **baz ** and __ qux__. + +Delete: ~~ quux~~ (this one isn’t parsed by mdast when padded). + +An image: ![ alpha](bravo.png). + +A link: [ charlie ](delta.html). diff --git a/test/fixtures/no-inline-padding-valid.md b/test/fixtures/no-inline-padding-valid.md new file mode 100644 index 0000000..e21b6ed --- /dev/null +++ b/test/fixtures/no-inline-padding-valid.md @@ -0,0 +1,9 @@ +Emphasis: *foo*, and _bar_. + +Strong: **baz** and __qux__. + +Delete: ~~quux~~ (this one isn’t parsed by mdast when padded). + +An image: ![alpha](bravo.png). + +A link: [charlie](delta.html). diff --git a/test/fixtures/no-literal-urls-invalid.md b/test/fixtures/no-literal-urls-invalid.md new file mode 100644 index 0000000..e74a1e0 --- /dev/null +++ b/test/fixtures/no-literal-urls-invalid.md @@ -0,0 +1,5 @@ +http://example.com + +foo@example.com which isn’t detected by mdast yet. + +mailto:foo@example.com which isn’t detected by mdast yet. diff --git a/test/fixtures/no-literal-urls-valid.md b/test/fixtures/no-literal-urls-valid.md new file mode 100644 index 0000000..058c91d --- /dev/null +++ b/test/fixtures/no-literal-urls-valid.md @@ -0,0 +1,5 @@ + + + which isn’t detected by mdast yet. + + which isn’t detected by mdast yet. diff --git a/test/fixtures/no-missing-blank-lines-invalid.md b/test/fixtures/no-missing-blank-lines-invalid.md new file mode 100644 index 0000000..d05210b --- /dev/null +++ b/test/fixtures/no-missing-blank-lines-invalid.md @@ -0,0 +1,19 @@ +--- +YAML: "front-matter" +--- +| A | table | with | +| --------- | ------- | ---- | +| incorrect | spacing | ... | +Foo bar baz +Heading +=== +* A list +1. Numbered list, followed by a rule: +--- +```text +fenced code +``` + indented code +A normal paragraph. +
foo
+> blockquote diff --git a/test/fixtures/no-missing-blank-lines-valid.md b/test/fixtures/no-missing-blank-lines-valid.md new file mode 100644 index 0000000..0ca01dc --- /dev/null +++ b/test/fixtures/no-missing-blank-lines-valid.md @@ -0,0 +1,30 @@ +--- +YAML: "front-matter" +--- + +| A | table | with | +| --------- | ------- | ---- | +| incorrect | spacing | ... | + +Foo bar baz + +Heading +=== + +* A list + +1. Numbered list, followed by a rule: + +--- + +```text +fenced code +``` + + indented code + +A normal paragraph. + +
foo
+ +> blockquote diff --git a/test/fixtures/no-multiple-toplevel-headings-invalid.md b/test/fixtures/no-multiple-toplevel-headings-invalid.md new file mode 100644 index 0000000..ed2c58c --- /dev/null +++ b/test/fixtures/no-multiple-toplevel-headings-invalid.md @@ -0,0 +1,7 @@ +# Invalid + +Another heading +--------------- + +Another +======= diff --git a/test/fixtures/no-multiple-toplevel-headings-valid.md b/test/fixtures/no-multiple-toplevel-headings-valid.md new file mode 100644 index 0000000..8751cf2 --- /dev/null +++ b/test/fixtures/no-multiple-toplevel-headings-valid.md @@ -0,0 +1,4 @@ +# Valid + +Another heading +--------------- diff --git a/test/fixtures/no-shell-dollars-invalid.md b/test/fixtures/no-shell-dollars-invalid.md new file mode 100644 index 0000000..aa04b3e --- /dev/null +++ b/test/fixtures/no-shell-dollars-invalid.md @@ -0,0 +1,36 @@ +An indented code block is ignored: + + $ echo a + $ echo a > file + +An unflagged fenced code block is also ignored: + +``` +$ echo a +$ echo a > file +``` + +Flagged fenced code blocks: + +```sh +$ echo a +$ echo a > file +``` + +```bash +$ echo a + +$ echo a > file +``` + +```command +$ echo a +$ echo a > file +``` + +Nested: + +> * Hello blocks without language are also ignored: +> +> $ echo a +> $ echo a > file diff --git a/test/fixtures/no-shell-dollars-valid.md b/test/fixtures/no-shell-dollars-valid.md new file mode 100644 index 0000000..b5f399f --- /dev/null +++ b/test/fixtures/no-shell-dollars-valid.md @@ -0,0 +1,50 @@ +An indented code block: + + $ echo a + a + $ echo a > file + +An unflagged fenced code block: + +``` +$ echo a +a +$ echo a > file +``` + +Empty: + +```bash +``` + +Flagged fenced code blocks: + +```sh +$ echo a +a +$ echo a > file +``` + +```bash +echo a +echo a > file +``` + +```command +$ echo a +a +$ echo a > file +``` + +```javascript +$ echo a +$ echo a > file +``` + +Nested: + +> * Hello +> +> $ echo a +> a +> $ echo a > file diff --git a/test/fixtures/no-shortcut-reference-image-invalid.md b/test/fixtures/no-shortcut-reference-image-invalid.md new file mode 100644 index 0000000..28534a2 --- /dev/null +++ b/test/fixtures/no-shortcut-reference-image-invalid.md @@ -0,0 +1,4 @@ +Here’s an ![valid] reference image, and here’s ![another]. + +[valid]: http://example.com/favicon.ico "Example Domain" +[another]: http://example.org/favicon.ico "Example Domain" diff --git a/test/fixtures/no-shortcut-reference-image-valid.md b/test/fixtures/no-shortcut-reference-image-valid.md new file mode 100644 index 0000000..30a2b15 --- /dev/null +++ b/test/fixtures/no-shortcut-reference-image-valid.md @@ -0,0 +1,4 @@ +Here’s an ![valid][] reference image, and here’s ![another][]. + +[valid]: http://example.com/favicon.ico "Example Domain" +[another]: http://example.org/favicon.ico "Example Domain" diff --git a/test/fixtures/no-shortcut-reference-link-invalid.md b/test/fixtures/no-shortcut-reference-link-invalid.md new file mode 100644 index 0000000..8567935 --- /dev/null +++ b/test/fixtures/no-shortcut-reference-link-invalid.md @@ -0,0 +1,4 @@ +Here’s an [invalid] reference link, and here’s [another]. + +[invalid]: http://example.com "Example Domain" +[another]: http://example.org "Example Domain" diff --git a/test/fixtures/no-shortcut-reference-link-valid.md b/test/fixtures/no-shortcut-reference-link-valid.md new file mode 100644 index 0000000..1aa5be4 --- /dev/null +++ b/test/fixtures/no-shortcut-reference-link-valid.md @@ -0,0 +1,4 @@ +Here’s an [valid][] reference link, and here’s [another][]. + +[valid]: http://example.com "Example Domain" +[another]: http://example.org "Example Domain" diff --git a/test/fixtures/no-table-indentation-invalid.md b/test/fixtures/no-table-indentation-invalid.md new file mode 100644 index 0000000..8574d68 --- /dev/null +++ b/test/fixtures/no-table-indentation-invalid.md @@ -0,0 +1,6 @@ +Did you know GitHub renders a table indented, by four spaces, as a table, not +code? + + | Foo | Bar | + | --- | --- | + | Baz | Qux | diff --git a/test/fixtures/no-table-indentation-valid.md b/test/fixtures/no-table-indentation-valid.md new file mode 100644 index 0000000..a2111e1 --- /dev/null +++ b/test/fixtures/no-table-indentation-valid.md @@ -0,0 +1,5 @@ +Here’s a proper table: + +| Foo | Bar | +| --- | --- | +| Baz | Qux | diff --git a/test/fixtures/no-tabs-invalid.md b/test/fixtures/no-tabs-invalid.md new file mode 100644 index 0000000..bf88104 --- /dev/null +++ b/test/fixtures/no-tabs-invalid.md @@ -0,0 +1,13 @@ + Here's one before a code block. + +Here's a tab: , and here is another: . + +And this is in `inline code`. + +> This is in a block quote. + +* And... + + 1. in a list. + +And this is a tab as the last character. diff --git a/test/fixtures/no-tabs-valid.md b/test/fixtures/no-tabs-valid.md new file mode 100644 index 0000000..a889560 --- /dev/null +++ b/test/fixtures/no-tabs-valid.md @@ -0,0 +1,11 @@ +Here's a paragraph without tabs. + + This is a very normal indented code block. + +This is in quite boring `inline code`. + +> This is a really standard block quote. + +* And... + + 1. This is just a list. diff --git a/test/fixtures/ordered-list-marker-value-one.md b/test/fixtures/ordered-list-marker-value-one.md new file mode 100644 index 0000000..0e14495 --- /dev/null +++ b/test/fixtures/ordered-list-marker-value-one.md @@ -0,0 +1,8 @@ +1. One; +1. Two; +1. Three. + + +1. One; +1. Two; +1. Three. diff --git a/test/fixtures/ordered-list-marker-value-ordered.md b/test/fixtures/ordered-list-marker-value-ordered.md new file mode 100644 index 0000000..454b2e0 --- /dev/null +++ b/test/fixtures/ordered-list-marker-value-ordered.md @@ -0,0 +1,8 @@ +1. One; +2. Two; +3. Three. + + +3. One; +4. Two; +5. Three. diff --git a/test/fixtures/ordered-list-marker-value-single.md b/test/fixtures/ordered-list-marker-value-single.md new file mode 100644 index 0000000..27cb5b2 --- /dev/null +++ b/test/fixtures/ordered-list-marker-value-single.md @@ -0,0 +1,8 @@ +1. One; +1. Two; +1. Three. + + +3. One; +3. Two; +3. Three. diff --git a/test/fixtures/rule-style-invalid.md b/test/fixtures/rule-style-invalid.md new file mode 100644 index 0000000..50569fe --- /dev/null +++ b/test/fixtures/rule-style-invalid.md @@ -0,0 +1,15 @@ +Foo + +* * * * + +Bar + +- - - - - + +Baz + +_ _ _ + +Qux + +**** diff --git a/test/fixtures/rule-style-valid.md b/test/fixtures/rule-style-valid.md new file mode 100644 index 0000000..65dd29a --- /dev/null +++ b/test/fixtures/rule-style-valid.md @@ -0,0 +1,15 @@ +Foo + +* * * * + +Bar + +* * * * + +Baz + +* * * * + +Qux + +* * * * diff --git a/test/fixtures/strong-marker-asterisk-underscore.md b/test/fixtures/strong-marker-asterisk-underscore.md new file mode 100644 index 0000000..3ec2b78 --- /dev/null +++ b/test/fixtures/strong-marker-asterisk-underscore.md @@ -0,0 +1,3 @@ +**foo** + +__bar__ diff --git a/test/fixtures/strong-marker-asterisk.md b/test/fixtures/strong-marker-asterisk.md new file mode 100644 index 0000000..81dfc73 --- /dev/null +++ b/test/fixtures/strong-marker-asterisk.md @@ -0,0 +1,3 @@ +**foo** + +**bar** diff --git a/test/fixtures/strong-marker-underscore-asterisk.md b/test/fixtures/strong-marker-underscore-asterisk.md new file mode 100644 index 0000000..5d911eb --- /dev/null +++ b/test/fixtures/strong-marker-underscore-asterisk.md @@ -0,0 +1,3 @@ +__foo__ + +**bar** diff --git a/test/fixtures/strong-marker-underscore.md b/test/fixtures/strong-marker-underscore.md new file mode 100644 index 0000000..9074ba7 --- /dev/null +++ b/test/fixtures/strong-marker-underscore.md @@ -0,0 +1,3 @@ +__foo__ + +__bar__ diff --git a/test/fixtures/table-cell-padding-compact-unaligned.md b/test/fixtures/table-cell-padding-compact-unaligned.md new file mode 100644 index 0000000..c57b037 --- /dev/null +++ b/test/fixtures/table-cell-padding-compact-unaligned.md @@ -0,0 +1,6 @@ +Here are some unaligned spaced tables. + +|foooo|fo|foooo|fo| +|-----|:-|:---:|-:| +|bar|baz|baaaaaar|bar| +|bar|baaaaaaaaaaz|bar| diff --git a/test/fixtures/table-cell-padding-compact.md b/test/fixtures/table-cell-padding-compact.md new file mode 100644 index 0000000..731fb22 --- /dev/null +++ b/test/fixtures/table-cell-padding-compact.md @@ -0,0 +1,11 @@ +Here are some properly non-spaced tables. + +|foooo|foooo|foooo|foooo| +|-----|:----|:---:|----:| +|bar |baz | bar | bar| + +So is this one (the longest cell receives preference, not the first): + +|foo |fuu | foo | foo| +|-----|:----|:---:|----:| +|baaar|baaar|baaar|baaar| diff --git a/test/fixtures/table-cell-padding-mixed.md b/test/fixtures/table-cell-padding-mixed.md new file mode 100644 index 0000000..9240ba8 --- /dev/null +++ b/test/fixtures/table-cell-padding-mixed.md @@ -0,0 +1,19 @@ +Here’s a weirdly padded table, which start compact. + +|foooo |foooo| fooo |foooo| +|------|:----|:----:|----:| +|bar |baz | bar | bar| + +|bar |baz | bar | bar| +|------|:----|:----:|----:| +|foooo |foooo| fooo |foooo| + +Here’s a weirdly padded table, which start padded. + +| foooo |foooo| fooo |foooo| +| ----- |:----|:----:|----:| +| bar |baz | bar | bar| + +| bar |baz | bar | bar| +| ----- |:----|:----:|----:| +| foooo |foooo| fooo |foooo| diff --git a/test/fixtures/table-cell-padding-padded-unaligned.md b/test/fixtures/table-cell-padding-padded-unaligned.md new file mode 100644 index 0000000..1e55ff1 --- /dev/null +++ b/test/fixtures/table-cell-padding-padded-unaligned.md @@ -0,0 +1,6 @@ +Here are some unaligned spaced tables. + +| foooo | fo | foooo | fo | +| ----- | :- | :---: | -: | +| bar | baz | baaaaaar | bar | +| bar | baaaaaaaaaaaaz | bar | diff --git a/test/fixtures/table-cell-padding-padded.md b/test/fixtures/table-cell-padding-padded.md new file mode 100644 index 0000000..28016a6 --- /dev/null +++ b/test/fixtures/table-cell-padding-padded.md @@ -0,0 +1,11 @@ +Here are some properly spaced tables. + +| foooo | foooo | foooo | foooo | +| ----- | :---- | :---: | ----: | +| bar | baz | bar | bar | + +So is this one (the longest cell receives preference, not the first): + +| foo | foo | foo | foo | +| ----- | :---- | :---: | ----: | +| baaar | baaaz | baaar | baaar | diff --git a/test/fixtures/table-pipe-alignment-invalid.md b/test/fixtures/table-pipe-alignment-invalid.md new file mode 100644 index 0000000..e8746ec --- /dev/null +++ b/test/fixtures/table-pipe-alignment-invalid.md @@ -0,0 +1,5 @@ +Unaligned fences (note that the alignment-row is not validated). + +| Foo | Bar | Baz | +| --- | ---- | ---- | +| Qu | Quux | foooo | diff --git a/test/fixtures/table-pipe-alignment-valid.md b/test/fixtures/table-pipe-alignment-valid.md new file mode 100644 index 0000000..870df4c --- /dev/null +++ b/test/fixtures/table-pipe-alignment-valid.md @@ -0,0 +1,5 @@ +Aligned fences (note the missing last cell in the last row). + +| Foooo | Bar | Baz | +| ----- | ----- | --- | +| Qux | Quux | diff --git a/test/fixtures/table-pipes-invalid.md b/test/fixtures/table-pipes-invalid.md new file mode 100644 index 0000000..73822cf --- /dev/null +++ b/test/fixtures/table-pipes-invalid.md @@ -0,0 +1,23 @@ +Without fences: + +Foo | Bar +--- | --- +Baz | Qux + +Without initial fence: + +Foo | Bar | +--- | --- | +Baz | Qux | + +Without final fence: + +| Foo | Bar +| --- | --- +| Baz | Qux + +Centered without fences: + +Foooooo | Baaaaar +:-----: | :-----: + Baz | Qux diff --git a/test/fixtures/table-pipes-valid.md b/test/fixtures/table-pipes-valid.md new file mode 100644 index 0000000..1a4273e --- /dev/null +++ b/test/fixtures/table-pipes-valid.md @@ -0,0 +1,11 @@ +With fences: + +| Foo | Bar | +| --- | --- | +| Baz | Qux | + +Centered with fences: + +| Foooooo | Baaaaar | +| :-----: | :-----: | +| Baz | Qux | diff --git a/test/fixtures/the-file-name.md b/test/fixtures/the-file-name.md new file mode 100644 index 0000000..e69de29 diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..c89c591 --- /dev/null +++ b/test/index.js @@ -0,0 +1,1704 @@ +/** + * @author Titus Wormer + * @copyright 2015 Titus Wormer. All rights reserved. + * @module Test + * @fileoverview Tests for mdast-lint. + */ + +'use strict'; + +/* eslint-env mocha */ + +var fs = require('fs'); +var path = require('path'); +var mdast = require('mdast'); +var File = require('mdast/lib/file'); +var assert = require('assert'); +var lint = require('..'); +var plural = require('../lib/utilities/plural'); +var clean = require('./clean'); + +/* + * Methods. + */ + +var read = fs.readFileSync; +var join = path.join; +var basename = path.basename; +var extname = path.extname; +var dirname = path.dirname; + +/** + * Create a `File` from a `filePath`. + * + * @param {string} filePath + * @return {File} + */ +function toFile(filePath) { + var extension = extname(filePath); + var directory = dirname(filePath); + var name = basename(filePath, extension); + + return new File({ + 'directory': directory, + 'filename': name, + 'extension': extension.slice(1), + 'contents': read(join('test', 'fixtures', filePath), 'utf-8') + }); +} + +/** + * Shortcut. + * + * @param {string} filePath + * @param {Object?} options - Passed to `mdast-lint` + * @param {Object?} settings - Passed to `mdast` + * @param {boolean?} shouldClean - Uses `clean` plugin, + * when truthy. + * @return {Array.} - Messages. + */ +function process(filePath, options, settings, shouldClean) { + var file = toFile(filePath); + var processor = mdast(); + + if (shouldClean) { + processor.use(clean); + } + + processor.use(lint, options); + + file.quiet = true; + + processor.process(file, settings, function (err) { + if (err) { + if (file.messages.indexOf(err) !== -1) { + return; + } + + throw err; + } + }); + + return file.messages; +} + +/* + * BDD-like helpers. + */ + +var currentRule = null; +var currentSetting = null; + +/** + * Describe a single mdast-lint rule. + * + * @param {string} ruleId - Rule to turn on when testing. + * @param {Function} description - Passed to `describe()`. + */ +function describeRule(ruleId, description) { + currentRule = ruleId; + + describe(ruleId, description); + + currentRule = null; +} + +/** + * Describe how a single mdast-lint rule should behave + * when given a certain `setting`. + * + * @param {*} setting - Passed to the rule. + * @param {Function} description - Passed to `describe()`. + */ +function describeSetting(setting, description) { + currentSetting = setting; + + describe(JSON.stringify(setting), description); + + currentSetting = null; +} + +/** + * Assert the messages triggered when running a bound rule + * with a bound setting on `filePath`, are the same as + * `messages`. Additionally accepts `settings` which are + * given to `mdast`. + * + * @param {string} filePath - Location of file in + * `test/fixtures/`. + * @param {Array.} messages - Assertions. + * @param {Object?} settings - Passed to `mdast`. + * @param {Object?} overwrite - Passed to `mdast-lint` + * instead of constructing based on BDD-like tests. + */ +function assertFile(filePath, messages, settings, overwrite) { + var n = -1; + var options; + var results; + var max; + var label; + + if (overwrite) { + options = overwrite; + } else { + /* + * Construct mdast-lint options. + */ + + options = {}; + options.reset = true; + options[currentRule] = currentSetting; + } + + /* + * Convert messages to strings. + */ + + results = process(filePath, options, settings).map(String); + + max = (results.length > messages.length ? results : messages).length; + + /* + * Create descriptive label for the test. + */ + + label = max === 0 ? 'shouldn’t warn' : + max === 1 ? 'should warn' : + 'should warn ' + max + ' ' + plural('time', max); + + /* + * Assert. + */ + + it(filePath + ' ' + label, function () { + while (++n < max) { + assert.strictEqual(messages[n], results[n]); + } + }); +} + +/* + * Basic tests. + */ + +describe('mdast-lint', function () { + it('should work without `options`', function () { + assert(process('file-extension-markdown.markdown').length === 1); + }); + + it('should ignore rules when set to `false`', function () { + assert(process('file-extension-markdown.markdown', { + 'file-extension': false + }).length === 0); + }); + + it('should ignore all rules when `reset: true`', function () { + assert(process('file-extension-markdown.markdown', { + 'reset': true + }).length === 0); + }); + + it('...except for explicitly turned on rules', function () { + assert(process('file-extension-markdown.markdown', { + 'reset': true, + 'file-extension': 'md' + }).length === 1); + }); +}); + +describe('Comments', function () { + describe('Disable and re-enable rules based on markers', function () { + assertFile('comments-disable.md', [ + 'comments-disable.md:7:89: Line must be at most 80 characters' + ], null, {}); + }); + + describe('Enable and re-disable rules based on markers', function () { + assertFile('comments-enable.md', [ + 'comments-enable.md:3:89: Line must be at most 80 characters' + ], null, { + 'reset': true + }); + }); + + describe('Inline comments', function () { + assertFile('comments-inline.md', [ + 'comments-inline.md:1:28-1:34: Do not use HTML in markdown', + 'comments-inline.md:1:46-1:53: Do not use HTML in markdown' + ], null, { + 'reset': true + }); + }); + + describe('Invalid comments', function () { + assertFile('comments-invalid-keyword.md', [ + 'comments-invalid-keyword.md:3:1-3:20: Unknown lint keyword `foo`: use either `\'enable\'` or `\'disable\'`' + ], null, { + 'reset': true + }); + }); + + describe('Invalid rules', function () { + assertFile('comments-invalid-rule-id.md', [ + 'comments-invalid-rule-id.md:3:1-3:23: Unknown rule: cannot enable `\'bar\'`' + ], null, { + 'reset': true + }); + }); + + describe('Without comments', function () { + assertFile('comments-none.md', [], null, { + 'reset': true + }); + }); + + describe('Duplicate enabling of rules', function () { + assertFile('comments-duplicates.md', [ + 'comments-duplicates.md:3:89: Line must be at most 80 characters', + 'comments-duplicates.md:7:89: Line must be at most 80 characters' + ], null, {}); + }); +}); + +/* + * Validate rules. + */ + +describe('Rules', function () { + describeRule('file-extension', function () { + describeSetting('md', function () { + assertFile('file-extension-markdown.markdown', [ + 'file-extension-markdown.markdown:1:1: Invalid extension: use `md`' + ]); + + assertFile('file-without-extension', []); + assertFile('file-extension-md.md', []); + }); + + describeSetting('markdown', function () { + assertFile('file-extension-markdown.markdown', []); + assertFile('file-without-extension', []); + + assertFile('file-extension-md.md', [ + 'file-extension-md.md:1:1: Invalid extension: use `markdown`' + ]); + }); + }); + + describeRule('no-file-name-articles', function () { + describeSetting(true, function () { + assertFile('the-file-name.md', [ + 'the-file-name.md:1:1: Do not start file names with `the`' + ]); + }); + }); + + describeRule('no-file-name-mixed-case', function () { + describeSetting(true, function () { + assertFile('file-name-Upper-case.md', [ + 'file-name-Upper-case.md:1:1: Do not mix casing in file names' + ]); + }); + }); + + describeRule('no-file-name-irregular-characters', function () { + describeSetting(true, function () { + assertFile('file-name characters.md', [ + 'file-name characters.md:1:1: Do not use ` ` in a file name' + ]); + }); + }); + + describeRule('no-file-name-consecutive-dashes', function () { + describeSetting(true, function () { + assertFile('file-name--consecutive-dashes.md', [ + 'file-name--consecutive-dashes.md:1:1: Do not use consecutive dashes in a file name' + ]); + }); + }); + + describeRule('no-file-name-consecutive-dashes', function () { + describeSetting(true, function () { + assertFile('file-name--consecutive-dashes.md', [ + 'file-name--consecutive-dashes.md:1:1: Do not use consecutive dashes in a file name' + ]); + }); + }); + + describeRule('no-file-name-outer-dashes', function () { + describeSetting(true, function () { + assertFile('-file-name-initial-dash.md', [ + '-file-name-initial-dash.md:1:1: Do not use initial or final dashes in a file name' + ]); + + assertFile('file-name-final-dash-.md', [ + 'file-name-final-dash-.md:1:1: Do not use initial or final dashes in a file name' + ]); + }); + }); + + describeRule('maximum-heading-length', function () { + describeSetting(true, function () { + assertFile('heading-length-too-long.md', [ + 'heading-length-too-long.md:1:1-1:65: Use headings shorter than `60`' + ]); + + assertFile('heading-length-normal.md', []); + assertFile('heading-length-quite-short.md', []); + }); + + describeSetting(20, function () { + assertFile('heading-length-too-long.md', [ + 'heading-length-too-long.md:1:1-1:65: Use headings shorter than `20`' + ]); + + assertFile('heading-length-quite-short.md', [ + 'heading-length-quite-short.md:1:1-1:29: Use headings shorter than `20`' + ]); + + assertFile('heading-length-normal.md', []); + }); + }); + + describeRule('first-heading-level', function () { + describeSetting(true, function () { + assertFile('first-heading-level-invalid.md', [ + 'first-heading-level-invalid.md:1:1-1:11: First heading level should be `1`' + ]); + + assertFile('first-heading-level-valid.md', []); + }); + }); + + describeRule('heading-increment', function () { + describeSetting(true, function () { + assertFile('heading-increment-invalid.md', [ + 'heading-increment-invalid.md:3:1-3:8: Heading levels should increment by one level at a time' + ]); + + assertFile('heading-increment-invalid-blockquote.md', [ + 'heading-increment-invalid-blockquote.md:3:3-3:10: Heading levels should increment by one level at a time' + ]); + + assertFile('heading-increment-invalid-list.md', [ + 'heading-increment-invalid-list.md:3:5-3:12: Heading levels should increment by one level at a time' + ]); + + assertFile('first-heading-level-invalid.md', []); + }); + }); + + describeRule('no-heading-punctuation', function () { + describeSetting(true, function () { + assertFile('no-heading-punctuation-period.md', [ + 'no-heading-punctuation-period.md:1:1-1:7: Don’t add a trailing `.` to headings' + ]); + + assertFile('no-heading-punctuation-colon.md', [ + 'no-heading-punctuation-colon.md:1:1-1:7: Don’t add a trailing `:` to headings' + ]); + + assertFile('no-heading-punctuation-question.md', [ + 'no-heading-punctuation-question.md:1:1-1:7: Don’t add a trailing `?` to headings' + ]); + + assertFile('no-heading-punctuation-valid.md', []); + }); + + describeSetting('o', function () { + assertFile('no-heading-punctuation-period.md', []); + assertFile('no-heading-punctuation-colon.md', []); + assertFile('no-heading-punctuation-question.md', []); + + assertFile('no-heading-punctuation-valid.md', [ + 'no-heading-punctuation-valid.md:1:1-1:6: Don’t add a trailing `o` to headings' + ]); + }); + }); + + describeRule('heading-style', function () { + describeSetting(true, function () { + assertFile('heading-style-atx.md', []); + assertFile('heading-style-atx-closed.md', []); + assertFile('heading-style-setext.md', []); + + assertFile('heading-style-not-consistent.md', [ + 'heading-style-not-consistent.md:3:1-4:6: Headings should use atx', + 'heading-style-not-consistent.md:6:1-6:19: Headings should use atx', + 'heading-style-not-consistent.md:10:1-10:24: Headings should use atx' + ]); + + assertFile('heading-style-empty.md', [ + 'heading-style-empty.md:3:1-3:3: Headings should use atx-closed', + 'heading-style-empty.md:7:1-7:5: Headings should use atx-closed', + 'heading-style-empty.md:11:1-11:7: Headings should use atx-closed' + ]); + }); + + describeSetting('atx', function () { + assertFile('heading-style-atx.md', []); + + assertFile('heading-style-atx-closed.md', [ + 'heading-style-atx-closed.md:1:1-1:15: Headings should use atx', + 'heading-style-atx-closed.md:3:1-3:17: Headings should use atx', + 'heading-style-atx-closed.md:5:1-5:19: Headings should use atx', + 'heading-style-atx-closed.md:7:1-7:21: Headings should use atx', + 'heading-style-atx-closed.md:9:1-9:23: Headings should use atx', + 'heading-style-atx-closed.md:11:1-11:25: Headings should use atx' + ]); + + assertFile('heading-style-setext.md', [ + 'heading-style-setext.md:1:1-2:6: Headings should use atx', + 'heading-style-setext.md:4:1-5:6: Headings should use atx' + ]); + + assertFile('heading-style-not-consistent.md', [ + 'heading-style-not-consistent.md:3:1-4:6: Headings should use atx', + 'heading-style-not-consistent.md:6:1-6:19: Headings should use atx', + 'heading-style-not-consistent.md:10:1-10:24: Headings should use atx' + ]); + + assertFile('heading-style-empty.md', [ + 'heading-style-empty.md:1:1-1:4: Headings should use atx', + 'heading-style-empty.md:5:1-5:8: Headings should use atx', + 'heading-style-empty.md:9:1-9:12: Headings should use atx' + ]); + }); + + describeSetting('atx-closed', function () { + assertFile('heading-style-atx.md', [ + 'heading-style-atx.md:1:1-1:6: Headings should use atx-closed', + 'heading-style-atx.md:3:1-3:7: Headings should use atx-closed', + 'heading-style-atx.md:5:1-5:17: Headings should use atx-closed', + 'heading-style-atx.md:7:1-7:18: Headings should use atx-closed', + 'heading-style-atx.md:9:1-9:19: Headings should use atx-closed', + 'heading-style-atx.md:11:1-11:20: Headings should use atx-closed' + ]); + + assertFile('heading-style-atx-closed.md', []); + + assertFile('heading-style-setext.md', [ + 'heading-style-setext.md:1:1-2:6: Headings should use atx-closed', + 'heading-style-setext.md:4:1-5:6: Headings should use atx-closed', + 'heading-style-setext.md:7:1-7:17: Headings should use atx-closed', + 'heading-style-setext.md:9:1-9:18: Headings should use atx-closed', + 'heading-style-setext.md:11:1-11:19: Headings should use atx-closed', + 'heading-style-setext.md:13:1-13:20: Headings should use atx-closed' + ]); + + assertFile('heading-style-not-consistent.md', [ + 'heading-style-not-consistent.md:1:1-1:6: Headings should use atx-closed', + 'heading-style-not-consistent.md:3:1-4:6: Headings should use atx-closed', + 'heading-style-not-consistent.md:8:1-8:18: Headings should use atx-closed', + 'heading-style-not-consistent.md:12:1-12:20: Headings should use atx-closed' + ]); + + assertFile('heading-style-empty.md', [ + 'heading-style-empty.md:3:1-3:3: Headings should use atx-closed', + 'heading-style-empty.md:7:1-7:5: Headings should use atx-closed', + 'heading-style-empty.md:11:1-11:7: Headings should use atx-closed' + ]); + }); + + describeSetting('setext', function () { + assertFile('heading-style-atx.md', [ + 'heading-style-atx.md:1:1-1:6: Headings should use setext', + 'heading-style-atx.md:3:1-3:7: Headings should use setext' + ]); + + assertFile('heading-style-atx-closed.md', [ + 'heading-style-atx-closed.md:1:1-1:15: Headings should use setext', + 'heading-style-atx-closed.md:3:1-3:17: Headings should use setext', + 'heading-style-atx-closed.md:5:1-5:19: Headings should use setext', + 'heading-style-atx-closed.md:7:1-7:21: Headings should use setext', + 'heading-style-atx-closed.md:9:1-9:23: Headings should use setext', + 'heading-style-atx-closed.md:11:1-11:25: Headings should use setext' + ]); + + assertFile('heading-style-setext.md', []); + + assertFile('heading-style-not-consistent.md', [ + 'heading-style-not-consistent.md:1:1-1:6: Headings should use setext', + 'heading-style-not-consistent.md:6:1-6:19: Headings should use setext', + 'heading-style-not-consistent.md:10:1-10:24: Headings should use setext' + ]); + + assertFile('heading-style-empty.md', [ + 'heading-style-empty.md:1:1-1:4: Headings should use setext', + 'heading-style-empty.md:3:1-3:3: Headings should use setext', + 'heading-style-empty.md:5:1-5:8: Headings should use setext', + 'heading-style-empty.md:9:1-9:12: Headings should use setext' + ]); + }); + }); + + describeRule('no-heading-indent', function () { + describeSetting(true, function () { + assertFile('no-heading-indent-invalid.md', [ + 'no-heading-indent-invalid.md:1:4: Remove 3 spaces before this heading', + 'no-heading-indent-invalid.md:3:2: Remove 1 space before this heading', + 'no-heading-indent-invalid.md:6:2: Remove 1 space before this heading', + 'no-heading-indent-invalid.md:8:4: Remove 3 spaces before this heading' + ]); + + assertFile('no-heading-indent-valid.md', []); + }); + }); + + describeRule('no-heading-content-indent', function () { + describeSetting(true, function () { + assertFile('no-heading-content-indent-invalid.md', [ + 'no-heading-content-indent-invalid.md:1:4: Remove 1 space before this heading’s content', + 'no-heading-content-indent-invalid.md:3:6: Remove 2 spaces before this heading’s content', + 'no-heading-content-indent-invalid.md:5:5: Add 1 space before this heading’s content', + 'no-heading-content-indent-invalid.md:5:41: Remove 1 space after this heading’s content', + 'no-heading-content-indent-invalid.md:7:7: Remove 1 space before this heading’s content', + 'no-heading-content-indent-invalid.md:7:39: Remove 1 space after this heading’s content' + ], { + 'pedantic': true + }); + + assertFile('no-heading-content-indent-valid.md', [], { + 'pedantic': true + }); + }); + }); + + describeRule('no-duplicate-headings', function () { + describeSetting(true, function () { + assertFile('no-duplicate-headings-invalid.md', [ + 'no-duplicate-headings-invalid.md:3:1-3:8: Do not use headings with similar content (1:1)', + 'no-duplicate-headings-invalid.md:8:1-8:28: Do not use headings with similar content (5:1)' + ]); + + assertFile('no-duplicate-headings-valid.md', []); + }); + }); + + describeRule('no-emphasis-as-heading', function () { + describeSetting(true, function () { + assertFile('no-emphasis-as-heading-invalid.md', [ + 'no-emphasis-as-heading-invalid.md:1:1-1:25: Don’t use emphasis to introduce a section, use a heading', + 'no-emphasis-as-heading-invalid.md:5:1-5:21: Don’t use emphasis to introduce a section, use a heading' + ]); + + assertFile('no-emphasis-as-heading-valid.md', []); + }); + }); + + describeRule('no-multiple-toplevel-headings', function () { + describeSetting(true, function () { + assertFile('no-multiple-toplevel-headings-invalid.md', [ + 'no-multiple-toplevel-headings-invalid.md:6:1-7:8: Don’t use multiple top level headings (6:1)' + ]); + + assertFile('no-multiple-toplevel-headings-valid.md', []); + }); + }); + + describeRule('no-literal-urls', function () { + describeSetting(true, function () { + assertFile('no-literal-urls-invalid.md', [ + 'no-literal-urls-invalid.md:1:1-1:19: Don’t use literal URLs without angle brackets' + ]); + + assertFile('no-literal-urls-valid.md', []); + }); + }); + + describeRule('no-auto-link-without-protocol', function () { + describeSetting(true, function () { + assertFile('no-auto-link-without-protocol-invalid.md', [ + 'no-auto-link-without-protocol-invalid.md:8:1-8:17: All automatic links must start with a protocol' + ]); + + assertFile('no-auto-link-without-protocol-valid.md', []); + }); + }); + + describeRule('no-consecutive-blank-lines', function () { + describeSetting(true, function () { + assertFile('no-consecutive-blank-lines-invalid.md', [ + 'no-consecutive-blank-lines-invalid.md:2:1: Remove 1 line before node', + 'no-consecutive-blank-lines-invalid.md:5:1: Remove 1 line before node', + 'no-consecutive-blank-lines-invalid.md:8:1: Remove 1 line before node', + 'no-consecutive-blank-lines-invalid.md:11:3: Remove 1 line before node', + 'no-consecutive-blank-lines-invalid.md:14:1: Remove 1 line before node', + 'no-consecutive-blank-lines-invalid.md:18:1: Remove 1 line before node', + 'no-consecutive-blank-lines-invalid.md:22:1: Remove 1 line before node' + ]); + + assertFile('no-consecutive-blank-lines-valid.md', []); + }); + }); + + describeRule('no-missing-blank-lines', function () { + describeSetting(true, function () { + assertFile('no-missing-blank-lines-invalid.md', [ + 'no-missing-blank-lines-invalid.md:4:1-6:31: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:7:1-7:12: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:8:1-9:4: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:10:1-10:11: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:11:1-11:39: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:12:1-12:4: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:13:1-15:4: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:16:1-16:18: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:17:1-17:20: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:18:1-18:15: Missing blank line before block node', + 'no-missing-blank-lines-invalid.md:19:1-19:13: Missing blank line before block node' + ]); + + assertFile('no-missing-blank-lines-valid.md', []); + }); + }); + + describeRule('blockquote-indentation', function () { + describeSetting(true, function () { + assertFile('blockquote-indentation-2.md', []); + assertFile('blockquote-indentation-4.md', []); + }); + + describeSetting(2, function () { + assertFile('blockquote-indentation-2.md', []); + + assertFile('blockquote-indentation-4.md', [ + 'blockquote-indentation-4.md:1:3: Remove 2 spaces between blockquote and content', + 'blockquote-indentation-4.md:5:3: Remove 2 spaces between blockquote and content', + 'blockquote-indentation-4.md:9:3: Remove 2 spaces between blockquote and content' + ]); + }); + + describeSetting(4, function () { + assertFile('blockquote-indentation-2.md', [ + 'blockquote-indentation-2.md:1:3: Add 2 spaces between blockquote and content', + 'blockquote-indentation-2.md:5:3: Add 2 spaces between blockquote and content', + 'blockquote-indentation-2.md:9:3: Add 2 spaces between blockquote and content' + ]); + + assertFile('blockquote-indentation-4.md', []); + }); + }); + + describeRule('emphasis-marker', function () { + describeSetting(true, function () { + assertFile('emphasis-marker-asterisk-underscore.md', [ + 'emphasis-marker-asterisk-underscore.md:3:1-3:6: Emphasis should use `*` as a marker' + ]); + + assertFile('emphasis-marker-underscore-asterisk.md', [ + 'emphasis-marker-underscore-asterisk.md:3:1-3:6: Emphasis should use `_` as a marker' + ]); + + assertFile('emphasis-marker-asterisk.md', []); + assertFile('emphasis-marker-underscore.md', []); + }); + + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid emphasis marker `~`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`' + ]); + }); + + describeSetting('*', function () { + assertFile('emphasis-marker-asterisk-underscore.md', [ + 'emphasis-marker-asterisk-underscore.md:3:1-3:6: Emphasis should use `*` as a marker' + ]); + + assertFile('emphasis-marker-underscore-asterisk.md', [ + 'emphasis-marker-underscore-asterisk.md:1:1-1:6: Emphasis should use `*` as a marker' + ]); + + assertFile('emphasis-marker-asterisk.md', []); + + assertFile('emphasis-marker-underscore.md', [ + 'emphasis-marker-underscore.md:1:1-1:6: Emphasis should use `*` as a marker', + 'emphasis-marker-underscore.md:3:1-3:6: Emphasis should use `*` as a marker' + ]); + }); + + describeSetting('_', function () { + assertFile('emphasis-marker-asterisk-underscore.md', [ + 'emphasis-marker-asterisk-underscore.md:1:1-1:6: Emphasis should use `_` as a marker' + ]); + + assertFile('emphasis-marker-underscore-asterisk.md', [ + 'emphasis-marker-underscore-asterisk.md:3:1-3:6: Emphasis should use `_` as a marker' + ]); + + assertFile('emphasis-marker-asterisk.md', [ + 'emphasis-marker-asterisk.md:1:1-1:6: Emphasis should use `_` as a marker', + 'emphasis-marker-asterisk.md:3:1-3:6: Emphasis should use `_` as a marker' + ]); + + assertFile('emphasis-marker-underscore.md', []); + }); + + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid emphasis marker `~`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`' + ]); + }); + }); + + describeRule('strong-marker', function () { + describeSetting(true, function () { + assertFile('strong-marker-asterisk-underscore.md', [ + 'strong-marker-asterisk-underscore.md:3:1-3:8: Strong should use `*` as a marker' + ]); + + assertFile('strong-marker-underscore-asterisk.md', [ + 'strong-marker-underscore-asterisk.md:3:1-3:8: Strong should use `_` as a marker' + ]); + + assertFile('strong-marker-asterisk.md', []); + assertFile('strong-marker-underscore.md', []); + }); + + describeSetting('*', function () { + assertFile('strong-marker-asterisk-underscore.md', [ + 'strong-marker-asterisk-underscore.md:3:1-3:8: Strong should use `*` as a marker' + ]); + + assertFile('strong-marker-underscore-asterisk.md', [ + 'strong-marker-underscore-asterisk.md:1:1-1:8: Strong should use `*` as a marker' + ]); + + assertFile('strong-marker-asterisk.md', []); + + assertFile('strong-marker-underscore.md', [ + 'strong-marker-underscore.md:1:1-1:8: Strong should use `*` as a marker', + 'strong-marker-underscore.md:3:1-3:8: Strong should use `*` as a marker' + ]); + }); + + describeSetting('_', function () { + assertFile('strong-marker-asterisk-underscore.md', [ + 'strong-marker-asterisk-underscore.md:1:1-1:8: Strong should use `_` as a marker' + ]); + + assertFile('strong-marker-underscore-asterisk.md', [ + 'strong-marker-underscore-asterisk.md:3:1-3:8: Strong should use `_` as a marker' + ]); + + assertFile('strong-marker-asterisk.md', [ + 'strong-marker-asterisk.md:1:1-1:8: Strong should use `_` as a marker', + 'strong-marker-asterisk.md:3:1-3:8: Strong should use `_` as a marker' + ]); + + assertFile('strong-marker-underscore.md', []); + }); + + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid strong marker `~`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`' + ]); + }); + }); + + describeRule('no-inline-padding', function () { + describeSetting(true, function () { + assertFile('no-inline-padding-invalid.md', [ + 'no-inline-padding-invalid.md:1:11-1:17: Don’t pad `emphasis` with inner spaces', + 'no-inline-padding-invalid.md:1:23-1:29: Don’t pad `emphasis` with inner spaces', + 'no-inline-padding-invalid.md:3:9-3:17: Don’t pad `strong` with inner spaces', + 'no-inline-padding-invalid.md:3:22-3:30: Don’t pad `strong` with inner spaces', + 'no-inline-padding-invalid.md:7:11-7:31: Don’t pad `image` with inner spaces', + 'no-inline-padding-invalid.md:9:9-9:32: Don’t pad `link` with inner spaces' + ]); + + assertFile('no-inline-padding-valid.md', []); + }); + }); + + describeRule('no-table-indentation', function () { + describeSetting(true, function () { + assertFile('no-table-indentation-invalid.md', [ + 'no-table-indentation-invalid.md:4:1-4:16: Do not indent table rows', + 'no-table-indentation-invalid.md:6:1-6:16: Do not indent table rows' + ]); + + assertFile('no-table-indentation-valid.md', []); + }); + }); + + describeRule('table-pipes', function () { + describeSetting(true, function () { + assertFile('table-pipes-invalid.md', [ + 'table-pipes-invalid.md:3:1: Missing initial pipe in table fence', + 'table-pipes-invalid.md:3:10: Missing final pipe in table fence', + 'table-pipes-invalid.md:5:1: Missing initial pipe in table fence', + 'table-pipes-invalid.md:5:10: Missing final pipe in table fence', + 'table-pipes-invalid.md:9:1: Missing initial pipe in table fence', + 'table-pipes-invalid.md:11:1: Missing initial pipe in table fence', + 'table-pipes-invalid.md:15:12: Missing final pipe in table fence', + 'table-pipes-invalid.md:17:12: Missing final pipe in table fence', + 'table-pipes-invalid.md:21:1: Missing initial pipe in table fence', + 'table-pipes-invalid.md:21:18: Missing final pipe in table fence', + 'table-pipes-invalid.md:23:1: Missing initial pipe in table fence', + 'table-pipes-invalid.md:23:16: Missing final pipe in table fence' + ]); + + assertFile('table-pipes-valid.md', []); + }); + }); + + describeRule('table-pipe-alignment', function () { + describeSetting(true, function () { + assertFile('table-pipe-alignment-invalid.md', [ + 'table-pipe-alignment-invalid.md:5:6-5:7: Misaligned table fence', + 'table-pipe-alignment-invalid.md:5:22-5:23: Misaligned table fence' + ]); + + assertFile('table-pipe-alignment-valid.md', []); + }); + }); + + describeRule('table-cell-padding', function () { + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid table-cell-padding style `~`' + ]); + }); + + describeSetting(true, function () { + assertFile('table-cell-padding-padded.md', []); + assertFile('table-cell-padding-compact.md', []); + assertFile('table-cell-padding-padded-unaligned.md', []); + assertFile('table-cell-padding-compact-unaligned.md', []); + + assertFile('table-cell-padding-mixed.md', [ + 'table-cell-padding-mixed.md:3:7: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:3:16: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:3:20: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:7:7: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:7:16: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:7:20: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:13:10: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:13:15: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:13:23: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:13:28: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:10: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:14: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:24: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:28: Cell should be padded, isn’t' + ]); + }); + + describeSetting('compact', function () { + assertFile('table-cell-padding-padded.md', [ + 'table-cell-padding-padded.md:3:3: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:3:8: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:3:11: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:3:16: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:3:19: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:3:24: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:3:27: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:3:32: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:3: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:8: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:11: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:16: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:19: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:24: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:27: Cell should be compact, isn’t', + 'table-cell-padding-padded.md:9:32: Cell should be compact, isn’t' + ]); + + assertFile('table-cell-padding-compact.md', []); + + assertFile('table-cell-padding-padded-unaligned.md', [ + 'table-cell-padding-padded-unaligned.md:3:3: Cell should be compact, isn’t', + 'table-cell-padding-padded-unaligned.md:3:8: Cell should be compact, isn’t', + 'table-cell-padding-padded-unaligned.md:3:11: Cell should be compact, isn’t', + 'table-cell-padding-padded-unaligned.md:3:13: Cell should be compact, isn’t', + 'table-cell-padding-padded-unaligned.md:3:16: Cell should be compact, isn’t', + 'table-cell-padding-padded-unaligned.md:3:21: Cell should be compact, isn’t', + 'table-cell-padding-padded-unaligned.md:3:24: Cell should be compact, isn’t', + 'table-cell-padding-padded-unaligned.md:3:26: Cell should be compact, isn’t' + ]); + + assertFile('table-cell-padding-compact-unaligned.md', []); + + assertFile('table-cell-padding-mixed.md', [ + 'table-cell-padding-mixed.md:3:7: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:3:16: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:3:20: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:7:7: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:7:16: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:7:20: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:13:3: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:13:8: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:13:17: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:13:21: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:17:3: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:17:8: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:17:17: Cell should be compact, isn’t', + 'table-cell-padding-mixed.md:17:21: Cell should be compact, isn’t' + ]); + }); + + describeSetting('padded', function () { + assertFile('table-cell-padding-padded.md', []); + + assertFile('table-cell-padding-compact.md', [ + 'table-cell-padding-compact.md:3:2: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:3:7: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:3:8: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:3:13: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:3:14: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:3:19: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:3:20: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:3:25: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:2: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:6: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:8: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:12: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:15: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:18: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:21: Cell should be padded, isn’t', + 'table-cell-padding-compact.md:9:25: Cell should be padded, isn’t' + ]); + + assertFile('table-cell-padding-padded-unaligned.md', []); + + assertFile('table-cell-padding-compact-unaligned.md', [ + 'table-cell-padding-compact-unaligned.md:3:2: Cell should be padded, isn’t', + 'table-cell-padding-compact-unaligned.md:3:7: Cell should be padded, isn’t', + 'table-cell-padding-compact-unaligned.md:3:8: Cell should be padded, isn’t', + 'table-cell-padding-compact-unaligned.md:3:10: Cell should be padded, isn’t', + 'table-cell-padding-compact-unaligned.md:3:11: Cell should be padded, isn’t', + 'table-cell-padding-compact-unaligned.md:3:16: Cell should be padded, isn’t', + 'table-cell-padding-compact-unaligned.md:3:17: Cell should be padded, isn’t', + 'table-cell-padding-compact-unaligned.md:3:19: Cell should be padded, isn’t' + ]); + + assertFile('table-cell-padding-mixed.md', [ + 'table-cell-padding-mixed.md:3:2: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:3:9: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:3:14: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:3:22: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:3:27: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:7:2: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:7:9: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:7:13: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:7:23: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:7:27: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:13:10: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:13:15: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:13:23: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:13:28: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:10: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:14: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:24: Cell should be padded, isn’t', + 'table-cell-padding-mixed.md:17:28: Cell should be padded, isn’t' + ]); + }); + }); + + describeRule('no-blockquote-without-caret', function () { + describeSetting(true, function () { + assertFile('no-blockquote-without-caret-invalid.md', [ + 'no-blockquote-without-caret-invalid.md:3:1: Missing caret in blockquote', + 'no-blockquote-without-caret-invalid.md:9:1: Missing caret in blockquote', + 'no-blockquote-without-caret-invalid.md:10:1: Missing caret in blockquote', + 'no-blockquote-without-caret-invalid.md:17:1: Missing caret in blockquote' + ]); + + assertFile('no-blockquote-without-caret-valid.md', []); + }); + }); + + describeRule('code-block-style', function () { + describeSetting(true, function () { + assertFile('code-style-indented.md', []); + assertFile('code-style-fenced.md', []); + }); + + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid code block style `~`: use either `\'consistent\'`, `\'fenced\'`, or `\'indented\'`' + ]); + }); + + describeSetting('indented', function () { + assertFile('code-style-indented.md', []); + + assertFile('code-style-fenced.md', [ + 'code-style-fenced.md:3:1-5:4: Code blocks should be indented', + 'code-style-fenced.md:9:1-11:4: Code blocks should be indented' + ]); + }); + + describeSetting('fenced', function () { + assertFile('code-style-indented.md', [ + 'code-style-indented.md:3:1-3:8: Code blocks should be fenced', + 'code-style-indented.md:7:1-7:8: Code blocks should be fenced' + ]); + + assertFile('code-style-fenced.md', []); + }); + }); + + describeRule('definition-case', function () { + describeSetting(true, function () { + assertFile('definition-case-invalid.md', [ + 'definition-case-invalid.md:3:1-3:59: Do not use uppper-case characters in definition labels' + ]); + + assertFile('definition-case-valid.md', []); + }); + }); + + describeRule('definition-spacing', function () { + describeSetting(true, function () { + assertFile('definition-spacing-invalid.md', [ + 'definition-spacing-invalid.md:3:1-3:68: Do not use consecutive white-space in definition labels' + ]); + + assertFile('definition-spacing-valid.md', []); + }); + }); + + describeRule('fenced-code-flag', function () { + describeSetting(true, function () { + assertFile('fenced-code-flag-invalid.md', [ + 'fenced-code-flag-invalid.md:3:1-5:4: Missing code-language flag' + ]); + + assertFile('fenced-code-flag-unknown.md', []); + assertFile('fenced-code-flag-valid.md', []); + }); + + describeSetting(['foo'], function () { + assertFile('fenced-code-flag-invalid.md', [ + 'fenced-code-flag-invalid.md:3:1-5:4: Missing code-language flag' + ]); + + assertFile('fenced-code-flag-unknown.md', [ + 'fenced-code-flag-unknown.md:3:1-5:4: Invalid code-language flag' + ]); + + assertFile('fenced-code-flag-valid.md', []); + }); + + describeSetting({ + 'allowEmpty': true, + 'flags': ['foo'] + }, function () { + assertFile('fenced-code-flag-invalid.md', []); + + assertFile('fenced-code-flag-unknown.md', [ + 'fenced-code-flag-unknown.md:3:1-5:4: Invalid code-language flag' + ]); + + assertFile('fenced-code-flag-valid.md', []); + }); + }); + + describeRule('final-definition', function () { + describeSetting(true, function () { + assertFile('final-definition-invalid.md', [ + 'final-definition-invalid.md:1:1-1:57: Move definitions to the end of the file (after the node at line `7`)', + 'final-definition-invalid.md:5:1-5:59: Move definitions to the end of the file (after the node at line `7`)' + ]); + + assertFile('final-definition-valid.md', []); + }); + }); + + describeRule('hard-break-spaces', function () { + describeSetting(true, function () { + assertFile('hard-break-spaces-invalid.md', [ + 'hard-break-spaces-invalid.md:1:25-2:1: Use two spaces for hard line breaks' + ]); + + assertFile('hard-break-spaces-valid.md', []); + }); + }); + + describeRule('no-html', function () { + describeSetting(true, function () { + assertFile('no-html-invalid.md', [ + 'no-html-invalid.md:1:1-1:14: Do not use HTML in markdown', + 'no-html-invalid.md:3:8-3:14: Do not use HTML in markdown', + 'no-html-invalid.md:3:18-3:25: Do not use HTML in markdown' + ]); + + assertFile('no-html-valid.md', []); + }); + }); + + describeRule('maximum-line-length', function () { + describeSetting(true, function () { + assertFile('maximum-line-length-invalid.md', [ + 'maximum-line-length-invalid.md:1:82: Line must be at most 80 characters', + 'maximum-line-length-invalid.md:3:86: Line must be at most 80 characters', + 'maximum-line-length-invalid.md:5:99: Line must be at most 80 characters', + 'maximum-line-length-invalid.md:7:97: Line must be at most 80 characters' + ]); + + assertFile('maximum-line-length-valid.md', []); + }); + + describeSetting(100, function () { + assertFile('maximum-line-length-invalid.md', []); + assertFile('maximum-line-length-valid.md', []); + }); + }); + + describeRule('list-item-bullet-indent', function () { + describeSetting(true, function () { + assertFile('list-item-bullet-indent-invalid.md', [ + 'list-item-bullet-indent-invalid.md:3:3: Incorrect indentation before bullet: remove 2 spaces', + 'list-item-bullet-indent-invalid.md:4:3: Incorrect indentation before bullet: remove 2 spaces' + ]); + + assertFile('list-item-bullet-indent-valid.md', []); + }); + }); + + describeRule('list-item-content-indent', function () { + describeSetting(true, function () { + assertFile('list-item-content-indent-invalid.md', [ + 'list-item-content-indent-invalid.md:3:4: Don’t use mixed indentation for children, add 1 space', + 'list-item-content-indent-invalid.md:9:4: Don’t use mixed indentation for children, add 1 space', + 'list-item-content-indent-invalid.md:14:5: Don’t use mixed indentation for children, remove 1 space' + ]); + + assertFile('list-item-content-indent-valid.md', []); + }); + }); + + describeRule('list-item-indent', function () { + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid list-item indent style `~`: use either `\'tab-size\'`, `\'space\'`, or `\'mixed\'`' + ]); + }); + + describeSetting('tab-size', function () { + assertFile('list-item-indent-tab-size.md', []); + + assertFile('list-item-indent-space.md', [ + 'list-item-indent-space.md:3:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:4:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:5:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:9:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:10:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:11:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:15:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:18:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:21:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:24:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:27:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:30:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:35:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:41:4: Incorrect list-item indent: add 1 space' + ]); + + assertFile('list-item-indent-mixed.md', [ + 'list-item-indent-mixed.md:3:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-mixed.md:4:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-mixed.md:5:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-mixed.md:9:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-mixed.md:10:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-mixed.md:11:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-mixed.md:37:4: Incorrect list-item indent: add 1 space' + ]); + }); + + describeSetting('space', function () { + assertFile('list-item-indent-tab-size.md', [ + 'list-item-indent-tab-size.md:3:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:4:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:5:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:9:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:10:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:11:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:15:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:18:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:21:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:26:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:29:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:32:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:37:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:43:5: Incorrect list-item indent: remove 1 space' + ]); + + assertFile('list-item-indent-space.md', []); + + assertFile('list-item-indent-mixed.md', [ + 'list-item-indent-mixed.md:15:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-mixed.md:18:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-mixed.md:21:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-mixed.md:26:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-mixed.md:29:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-mixed.md:32:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-mixed.md:43:5: Incorrect list-item indent: remove 1 space' + ]); + }); + + describeSetting('mixed', function () { + assertFile('list-item-indent-tab-size.md', [ + 'list-item-indent-tab-size.md:3:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:4:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:5:5: Incorrect list-item indent: remove 2 spaces', + 'list-item-indent-tab-size.md:9:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:10:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:11:5: Incorrect list-item indent: remove 1 space', + 'list-item-indent-tab-size.md:37:5: Incorrect list-item indent: remove 1 space' + ]); + + assertFile('list-item-indent-space.md', [ + 'list-item-indent-space.md:15:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:18:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:21:3: Incorrect list-item indent: add 2 spaces', + 'list-item-indent-space.md:24:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:27:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:30:4: Incorrect list-item indent: add 1 space', + 'list-item-indent-space.md:41:4: Incorrect list-item indent: add 1 space' + ]); + + assertFile('list-item-indent-mixed.md', []); + }); + }); + + describeRule('list-item-spacing', function () { + describeSetting(true, function () { + assertFile('list-item-spacing-tight-invalid.md', [ + 'list-item-spacing-tight-invalid.md:2:1-3:1: List item should be tight, isn’t', + 'list-item-spacing-tight-invalid.md:4:1-5:1: List item should be tight, isn’t' + ]); + + assertFile('list-item-spacing-loose-invalid.md', [ + 'list-item-spacing-loose-invalid.md:2:9-3:1: List item should be loose, isn’t', + 'list-item-spacing-loose-invalid.md:3:11-4:1: List item should be loose, isn’t' + ]); + + assertFile('list-item-spacing-tight-valid.md', []); + assertFile('list-item-spacing-loose-valid.md', []); + }); + }); + + describeRule('ordered-list-marker-value', function () { + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid ordered list-item marker value `~`: use either `\'ordered\'` or `\'one\'`' + ]); + }); + + describeSetting('ordered', function () { + assertFile('ordered-list-marker-value-ordered.md', []); + + assertFile('ordered-list-marker-value-one.md', [ + 'ordered-list-marker-value-one.md:2:1-2:9: Marker should be `2`, was `1`', + 'ordered-list-marker-value-one.md:3:1-3:11: Marker should be `3`, was `1`', + 'ordered-list-marker-value-one.md:7:1-7:9: Marker should be `2`, was `1`', + 'ordered-list-marker-value-one.md:8:1-8:11: Marker should be `3`, was `1`' + ]); + + assertFile('ordered-list-marker-value-single.md', [ + 'ordered-list-marker-value-single.md:2:1-2:9: Marker should be `2`, was `1`', + 'ordered-list-marker-value-single.md:3:1-3:11: Marker should be `3`, was `1`', + 'ordered-list-marker-value-single.md:7:1-7:9: Marker should be `4`, was `3`', + 'ordered-list-marker-value-single.md:8:1-8:11: Marker should be `5`, was `3`' + ]); + }); + + describeSetting('one', function () { + assertFile('ordered-list-marker-value-ordered.md', [ + 'ordered-list-marker-value-ordered.md:2:1-2:9: Marker should be `1`, was `2`', + 'ordered-list-marker-value-ordered.md:3:1-3:11: Marker should be `1`, was `3`', + 'ordered-list-marker-value-ordered.md:7:1-7:9: Marker should be `1`, was `4`', + 'ordered-list-marker-value-ordered.md:8:1-8:11: Marker should be `1`, was `5`' + ]); + + assertFile('ordered-list-marker-value-one.md', []); + + assertFile('ordered-list-marker-value-single.md', [ + 'ordered-list-marker-value-single.md:7:1-7:9: Marker should be `1`, was `3`', + 'ordered-list-marker-value-single.md:8:1-8:11: Marker should be `1`, was `3`' + ]); + }); + + describeSetting('single', function () { + assertFile('ordered-list-marker-value-ordered.md', [ + 'ordered-list-marker-value-ordered.md:2:1-2:9: Marker should be `1`, was `2`', + 'ordered-list-marker-value-ordered.md:3:1-3:11: Marker should be `1`, was `3`', + 'ordered-list-marker-value-ordered.md:7:1-7:9: Marker should be `3`, was `4`', + 'ordered-list-marker-value-ordered.md:8:1-8:11: Marker should be `3`, was `5`' + ]); + + assertFile('ordered-list-marker-value-one.md', []); + + assertFile('ordered-list-marker-value-single.md', []); + }); + }); + + describeRule('ordered-list-marker-style', function () { + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid ordered list-item marker style `~`: use either `\'.\'` or `\')\'`' + ]); + }); + + describeSetting(true, function () { + assertFile('list-item-marker-dot.md', [], { + 'commonmark': true + }); + + assertFile('list-item-marker-paren.md', [], { + 'commonmark': true + }); + }); + + describeSetting('.', function () { + assertFile('list-item-marker-dot.md', [], { + 'commonmark': true + }); + + assertFile('list-item-marker-paren.md', [ + 'list-item-marker-paren.md:1:1-1:10: Marker style should be `.`', + 'list-item-marker-paren.md:2:1-2:11: Marker style should be `.`', + 'list-item-marker-paren.md:4:3-4:14: Marker style should be `.`', + 'list-item-marker-paren.md:5:3-5:15: Marker style should be `.`' + ], { + 'commonmark': true + }); + }); + + describeSetting(')', function () { + assertFile('list-item-marker-dot.md', [ + 'list-item-marker-dot.md:1:1-1:10: Marker style should be `)`', + 'list-item-marker-dot.md:2:1-2:11: Marker style should be `)`', + 'list-item-marker-dot.md:4:3-4:14: Marker style should be `)`', + 'list-item-marker-dot.md:5:3-5:15: Marker style should be `)`' + ], { + 'commonmark': true + }); + + assertFile('list-item-marker-paren.md', [], { + 'commonmark': true + }); + }); + }); + + describeRule('unordered-list-marker-style', function () { + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid unordered list-item marker style `~`: use either `\'-\'`, `\'*\'`, or `\'+\'`' + ]); + }); + + describeSetting(true, function () { + assertFile('list-item-marker-dash.md', []); + assertFile('list-item-marker-asterisk.md', []); + assertFile('list-item-marker-plus.md', []); + }); + + describeSetting('-', function () { + assertFile('list-item-marker-dash.md', []); + + assertFile('list-item-marker-asterisk.md', [ + 'list-item-marker-asterisk.md:1:1-1:9: Marker style should be `-`', + 'list-item-marker-asterisk.md:4:1-4:11: Marker style should be `-`', + 'list-item-marker-asterisk.md:7:3-7:13: Marker style should be `-`', + 'list-item-marker-asterisk.md:10:3-10:15: Marker style should be `-`' + ]); + + assertFile('list-item-marker-plus.md', [ + 'list-item-marker-plus.md:1:1-1:9: Marker style should be `-`', + 'list-item-marker-plus.md:4:1-4:11: Marker style should be `-`', + 'list-item-marker-plus.md:7:3-7:13: Marker style should be `-`', + 'list-item-marker-plus.md:10:3-10:15: Marker style should be `-`' + ]); + }); + + describeSetting('*', function () { + assertFile('list-item-marker-dash.md', [ + 'list-item-marker-dash.md:1:1-1:9: Marker style should be `*`', + 'list-item-marker-dash.md:2:1-2:11: Marker style should be `*`', + 'list-item-marker-dash.md:4:3-4:13: Marker style should be `*`', + 'list-item-marker-dash.md:5:3-5:15: Marker style should be `*`' + ]); + + assertFile('list-item-marker-asterisk.md', []); + + assertFile('list-item-marker-plus.md', [ + 'list-item-marker-plus.md:1:1-1:9: Marker style should be `*`', + 'list-item-marker-plus.md:4:1-4:11: Marker style should be `*`', + 'list-item-marker-plus.md:7:3-7:13: Marker style should be `*`', + 'list-item-marker-plus.md:10:3-10:15: Marker style should be `*`' + ]); + }); + + describeSetting('+', function () { + assertFile('list-item-marker-dash.md', [ + 'list-item-marker-dash.md:1:1-1:9: Marker style should be `+`', + 'list-item-marker-dash.md:2:1-2:11: Marker style should be `+`', + 'list-item-marker-dash.md:4:3-4:13: Marker style should be `+`', + 'list-item-marker-dash.md:5:3-5:15: Marker style should be `+`' + ]); + + assertFile('list-item-marker-asterisk.md', [ + 'list-item-marker-asterisk.md:1:1-1:9: Marker style should be `+`', + 'list-item-marker-asterisk.md:4:1-4:11: Marker style should be `+`', + 'list-item-marker-asterisk.md:7:3-7:13: Marker style should be `+`', + 'list-item-marker-asterisk.md:10:3-10:15: Marker style should be `+`' + ]); + + assertFile('list-item-marker-plus.md', []); + }); + }); + + describeRule('no-tabs', function () { + describeSetting(true, function () { + assertFile('no-tabs-invalid.md', [ + 'no-tabs-invalid.md:1:1: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:3:14: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:3:37: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:5:23: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:7:2: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:9:2: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:11:1: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:11:4: Use spaces instead of hard-tabs', + 'no-tabs-invalid.md:13:41: Use spaces instead of hard-tabs' + ]); + + assertFile('no-tabs-valid.md', []); + }); + }); + + describeRule('no-shell-dollars', function () { + describeSetting(true, function () { + assertFile('no-shell-dollars-invalid.md', [ + 'no-shell-dollars-invalid.md:15:1-18:4: Do not use dollar signs before shell-commands', + 'no-shell-dollars-invalid.md:20:1-24:4: Do not use dollar signs before shell-commands', + 'no-shell-dollars-invalid.md:26:1-29:4: Do not use dollar signs before shell-commands' + ]); + + assertFile('no-shell-dollars-valid.md', []); + }); + }); + + describeRule('no-shortcut-reference-link', function () { + describeSetting(true, function () { + assertFile('no-shortcut-reference-link-invalid.md', [ + 'no-shortcut-reference-link-invalid.md:1:11-1:20: Use the trailing [] on reference links', + 'no-shortcut-reference-link-invalid.md:1:48-1:57: Use the trailing [] on reference links' + ]); + + assertFile('no-shortcut-reference-link-valid.md', []); + }); + }); + + describeRule('no-shortcut-reference-image', function () { + describeSetting(true, function () { + assertFile('no-shortcut-reference-image-invalid.md', [ + 'no-shortcut-reference-image-invalid.md:1:11-1:19: Use the trailing [] on reference images', + 'no-shortcut-reference-image-invalid.md:1:48-1:58: Use the trailing [] on reference images' + ]); + + assertFile('no-shortcut-reference-image-valid.md', []); + }); + }); + + describeRule('no-blockquote-without-caret', function () { + describeSetting(true, function () { + assertFile('no-blockquote-without-caret-invalid.md', [ + 'no-blockquote-without-caret-invalid.md:3:1: Missing caret in blockquote', + 'no-blockquote-without-caret-invalid.md:9:1: Missing caret in blockquote', + 'no-blockquote-without-caret-invalid.md:10:1: Missing caret in blockquote', + 'no-blockquote-without-caret-invalid.md:17:1: Missing caret in blockquote' + ]); + + assertFile('no-blockquote-without-caret-valid.md', []); + }); + }); + + describeRule('rule-style', function () { + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid preferred rule-style: provide a valid markdown rule, or `\'consistent\'`' + ]); + }); + + describeSetting(true, function () { + assertFile('rule-style-invalid.md', [ + 'rule-style-invalid.md:7:1-7:10: Horizontal rules should use `* * * *`', + 'rule-style-invalid.md:11:1-11:6: Horizontal rules should use `* * * *`', + 'rule-style-invalid.md:15:1-15:5: Horizontal rules should use `* * * *`' + ]); + + assertFile('rule-style-valid.md', []); + }); + }); + + describeRule('final-newline', function () { + describeSetting(true, function () { + assertFile('final-newline-invalid.md', [ + 'final-newline-invalid.md:1:1: Missing newline character at end of file' + ]); + + assertFile('final-newline-valid.md', []); + }); + }); + + describeRule('link-title-style', function () { + var settings = { + 'commonmark': true + }; + + describeSetting('~', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid link title style marker `~`: use either `\'consistent\'`, `\'"\'`, `\'\\\'\'`, or `\'()\'`' + ]); + }); + + describeSetting(true, function () { + assertFile('link-title-style-double.md', [], settings); + assertFile('link-title-style-single.md', [], settings); + assertFile('link-title-style-parentheses.md', [], settings); + assertFile('link-title-style-missing.md', [], settings); + + assertFile('link-title-style-trailing-white-space.md', [ + 'link-title-style-trailing-white-space.md:3:45: Titles should use `"` as a quote', + 'link-title-style-trailing-white-space.md:5:45: Titles should use `"` as a quote' + ], settings); + }); + + describeSetting('"', function () { + assertFile('link-title-style-double.md', [], settings); + assertFile('link-title-style-missing.md', [], settings); + + assertFile('link-title-style-single.md', [ + 'link-title-style-single.md:1:1: Titles should use `"` as a quote', + 'link-title-style-single.md:3:45: Titles should use `"` as a quote', + 'link-title-style-single.md:5:45: Titles should use `"` as a quote' + ], settings); + + assertFile('link-title-style-parentheses.md', [ + 'link-title-style-parentheses.md:1:1: Titles should use `"` as a quote', + 'link-title-style-parentheses.md:3:45: Titles should use `"` as a quote', + 'link-title-style-parentheses.md:5:45: Titles should use `"` as a quote' + ], settings); + }); + + describeSetting('\'', function () { + assertFile('link-title-style-single.md', [], settings); + assertFile('link-title-style-missing.md', [], settings); + + assertFile('link-title-style-double.md', [ + 'link-title-style-double.md:1:1: Titles should use `\'` as a quote', + 'link-title-style-double.md:3:45: Titles should use `\'` as a quote', + 'link-title-style-double.md:5:45: Titles should use `\'` as a quote' + ], settings); + + assertFile('link-title-style-parentheses.md', [ + 'link-title-style-parentheses.md:1:1: Titles should use `\'` as a quote', + 'link-title-style-parentheses.md:3:45: Titles should use `\'` as a quote', + 'link-title-style-parentheses.md:5:45: Titles should use `\'` as a quote' + ], settings); + }); + + describeSetting('()', function () { + assertFile('link-title-style-parentheses.md', [], settings); + assertFile('link-title-style-missing.md', [], settings); + + assertFile('link-title-style-double.md', [ + 'link-title-style-double.md:1:1: Titles should use `()` as a quote', + 'link-title-style-double.md:3:45: Titles should use `()` as a quote', + 'link-title-style-double.md:5:45: Titles should use `()` as a quote' + ], settings); + + assertFile('link-title-style-single.md', [ + 'link-title-style-single.md:1:1: Titles should use `()` as a quote', + 'link-title-style-single.md:3:45: Titles should use `()` as a quote', + 'link-title-style-single.md:5:45: Titles should use `()` as a quote' + ], settings); + }); + }); + + describeRule('no-duplicate-definitions', function () { + describeSetting(true, function () { + assertFile('no-duplicate-definitions-invalid.md', [ + 'no-duplicate-definitions-invalid.md:2:1-2:11: Do not use definitions with the same identifier (1:1)' + ]); + + assertFile('no-duplicate-definitions-valid.md', []); + }); + }); + + + describeRule('fenced-code-marker', function () { + describeSetting(true, function () { + assertFile('fenced-code-marker-tick.md', []); + assertFile('fenced-code-marker-tilde.md', []); + + assertFile('fenced-code-marker-mismatched.md', [ + 'fenced-code-marker-mismatched.md:5:1-7:4: Fenced code should use ` as a marker' + ]); + }); + + describeSetting('@', function () { + assertFile('empty.md', [ + 'empty.md:1:1: Invalid fenced code marker `@`: use either `\'consistent\'`, `` \'`\' ``, or `\'~\'`' + ]); + }); + + describeSetting('`', function () { + assertFile('fenced-code-marker-tick.md', []); + + assertFile('fenced-code-marker-tilde.md', [ + 'fenced-code-marker-tilde.md:1:1-3:4: Fenced code should use ` as a marker', + 'fenced-code-marker-tilde.md:5:1-7:4: Fenced code should use ` as a marker' + ]); + + assertFile('fenced-code-marker-mismatched.md', [ + 'fenced-code-marker-mismatched.md:5:1-7:4: Fenced code should use ` as a marker' + ]); + }); + + describeSetting('~', function () { + assertFile('fenced-code-marker-tick.md', [ + 'fenced-code-marker-tick.md:1:1-3:4: Fenced code should use ~ as a marker', + 'fenced-code-marker-tick.md:5:1-7:4: Fenced code should use ~ as a marker' + ]); + + assertFile('fenced-code-marker-tilde.md', []); + + assertFile('fenced-code-marker-mismatched.md', [ + 'fenced-code-marker-mismatched.md:1:1-3:4: Fenced code should use ~ as a marker' + ]); + }); + }); +}); + +/* + * Check that no warnings are created for generated + * nodes: nodes without positional information. + */ + +var nonnode = [ + 'no-file-name-articles', + 'no-tabs', + 'no-file-name-outer-dashes', + 'maximum-line-length', + 'final-newline', + 'no-file-name-mixed-case', + 'no-file-name-consecutive-dashes', + 'file-extension', + 'no-file-name-irregular-characters' +]; + +describe('mdast-lint with generated nodes', function () { + fs.readdirSync(join(__dirname, 'fixtures')).forEach(function (filePath) { + it(filePath, function () { + var messages = process(filePath, null, null, true); + + messages = messages.filter(function (message) { + return !message.ruleId || nonnode.indexOf(message.ruleId) === -1; + }); + }); + }); +});