From 721c363001b06345d11f6763aed8e333c91e8e57 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 2 Jun 2015 08:34:14 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 15 + .eslintignore | 0 .eslintrc | 9 + .gitignore | 8 + .jscs.json | 140 + .mdastignore | 7 + .mdastrc | 10 + .travis.yml | 9 + LICENSE | 21 + bower.json | 37 + component.json | 21 + doc/api.md | 104 + doc/comparison/markdownlint.md | 45 + doc/rules.md | 1142 ++++ example.js | 16 + index.js | 3 + lib/filter.js | 72 + lib/index.js | 243 + lib/rules/blockquote-indentation.js | 108 + lib/rules/code-block-style.js | 133 + lib/rules/definition-case.js | 74 + lib/rules/definition-spacing.js | 74 + lib/rules/emphasis-marker.js | 84 + lib/rules/fenced-code-flag.js | 105 + lib/rules/fenced-code-marker.js | 114 + lib/rules/file-extension.js | 45 + lib/rules/final-definition.js | 75 + lib/rules/final-newline.js | 38 + lib/rules/first-heading-level.js | 52 + lib/rules/hard-break-spaces.js | 60 + lib/rules/heading-increment.js | 63 + lib/rules/heading-style.js | 98 + lib/rules/index.js | 68 + lib/rules/link-title-style.js | 139 + lib/rules/list-item-bullet-indent.js | 77 + lib/rules/list-item-content-indent.js | 112 + lib/rules/list-item-indent.js | 148 + lib/rules/list-item-spacing.js | 118 + lib/rules/maximum-heading-length.js | 59 + lib/rules/maximum-line-length.js | 178 + lib/rules/no-auto-link-without-protocol.js | 84 + lib/rules/no-blockquote-without-caret.js | 84 + lib/rules/no-consecutive-blank-lines.js | 124 + lib/rules/no-duplicate-definitions.js | 75 + lib/rules/no-duplicate-headings.js | 73 + lib/rules/no-emphasis-as-heading.js | 81 + lib/rules/no-file-name-articles.js | 36 + lib/rules/no-file-name-consecutive-dashes.js | 34 + .../no-file-name-irregular-characters.js | 38 + lib/rules/no-file-name-mixed-case.js | 38 + lib/rules/no-file-name-outer-dashes.js | 34 + lib/rules/no-heading-content-indent.js | 119 + lib/rules/no-heading-indent.js | 101 + lib/rules/no-heading-punctuation.js | 71 + lib/rules/no-html.js | 45 + lib/rules/no-inline-padding.js | 68 + lib/rules/no-literal-urls.js | 62 + lib/rules/no-missing-blank-lines.js | 81 + lib/rules/no-multiple-toplevel-headings.js | 60 + lib/rules/no-shell-dollars.js | 101 + lib/rules/no-shortcut-reference-image.js | 54 + lib/rules/no-shortcut-reference-link.js | 54 + lib/rules/no-table-indentation.js | 60 + lib/rules/no-tabs.js | 48 + lib/rules/ordered-list-marker-style.js | 116 + lib/rules/ordered-list-marker-value.js | 156 + lib/rules/rule-style.js | 97 + lib/rules/strong-marker.js | 82 + lib/rules/table-cell-padding.js | 176 + lib/rules/table-pipe-alignment.js | 100 + lib/rules/table-pipes.js | 75 + lib/rules/unordered-list-marker-style.js | 121 + lib/sort.js | 44 + lib/utilities/heading-style.js | 79 + lib/utilities/plural.js | 33 + lib/utilities/position.js | 65 + lib/utilities/to-string.js | 42 + lib/utilities/visit.js | 111 + logo.svg | 5 + mdast.js | 5949 +++++++++++++++++ mdast.min.js | 1 + package.json | 63 + readme.md | 173 + screen-shot.png | Bin 0 -> 64208 bytes script/build-rule-documentation.js | 166 + test/clean.js | 42 + test/fixtures/-file-name-initial-dash.md | 0 test/fixtures/blockquote-indentation-2.md | 9 + test/fixtures/blockquote-indentation-4.md | 9 + test/fixtures/code-style-fenced.md | 11 + test/fixtures/code-style-indented.md | 7 + test/fixtures/comments-disable.md | 7 + test/fixtures/comments-duplicates.md | 7 + test/fixtures/comments-enable.md | 7 + test/fixtures/comments-inline.md | 1 + test/fixtures/comments-invalid-keyword.md | 5 + test/fixtures/comments-invalid-rule-id.md | 5 + test/fixtures/comments-none.md | 1 + test/fixtures/definition-case-invalid.md | 3 + test/fixtures/definition-case-valid.md | 3 + test/fixtures/definition-spacing-invalid.md | 3 + test/fixtures/definition-spacing-valid.md | 3 + .../emphasis-marker-asterisk-underscore.md | 3 + test/fixtures/emphasis-marker-asterisk.md | 3 + .../emphasis-marker-underscore-asterisk.md | 3 + test/fixtures/emphasis-marker-underscore.md | 3 + test/fixtures/empty.md | 0 test/fixtures/fenced-code-flag-invalid.md | 5 + test/fixtures/fenced-code-flag-unknown.md | 5 + test/fixtures/fenced-code-flag-valid.md | 7 + .../fixtures/fenced-code-marker-mismatched.md | 9 + test/fixtures/fenced-code-marker-tick.md | 9 + test/fixtures/fenced-code-marker-tilde.md | 9 + .../fixtures/file-extension-markdown.markdown | 0 test/fixtures/file-extension-md.md | 0 test/fixtures/file-name characters.md | 0 .../fixtures/file-name--consecutive-dashes.md | 0 test/fixtures/file-name-Upper-case.md | 0 test/fixtures/file-name-final-dash-.md | 0 test/fixtures/file-without-extension | 0 test/fixtures/final-definition-invalid.md | 7 + test/fixtures/final-definition-valid.md | 4 + test/fixtures/final-newline-invalid.md | 1 + test/fixtures/final-newline-valid.md | 1 + test/fixtures/first-heading-level-invalid.md | 1 + test/fixtures/first-heading-level-valid.md | 1 + test/fixtures/hard-break-spaces-invalid.md | 5 + test/fixtures/hard-break-spaces-valid.md | 5 + .../heading-increment-invalid-blockquote.md | 3 + .../heading-increment-invalid-list.md | 3 + test/fixtures/heading-increment-invalid.md | 3 + test/fixtures/heading-length-normal.md | 1 + test/fixtures/heading-length-quite-short.md | 1 + test/fixtures/heading-length-too-long.md | 1 + test/fixtures/heading-nesting-initial.md | 1 + test/fixtures/heading-style-atx-closed.md | 11 + test/fixtures/heading-style-atx.md | 13 + test/fixtures/heading-style-empty.md | 11 + test/fixtures/heading-style-not-consistent.md | 12 + test/fixtures/heading-style-setext.md | 13 + test/fixtures/link-title-style-double.md | 5 + test/fixtures/link-title-style-missing.md | 5 + test/fixtures/link-title-style-parentheses.md | 5 + test/fixtures/link-title-style-single.md | 5 + .../link-title-style-trailing-white-space.md | 5 + .../list-item-bullet-indent-invalid.md | 4 + .../fixtures/list-item-bullet-indent-valid.md | 4 + .../list-item-content-indent-invalid.md | 14 + .../list-item-content-indent-valid.md | 14 + test/fixtures/list-item-indent-mixed.md | 50 + test/fixtures/list-item-indent-space.md | 48 + test/fixtures/list-item-indent-tab-size.md | 50 + test/fixtures/list-item-marker-asterisk.md | 10 + test/fixtures/list-item-marker-dash.md | 5 + test/fixtures/list-item-marker-dot.md | 5 + test/fixtures/list-item-marker-paren.md | 5 + test/fixtures/list-item-marker-plus.md | 10 + .../list-item-spacing-loose-invalid.md | 4 + .../fixtures/list-item-spacing-loose-valid.md | 6 + .../list-item-spacing-tight-invalid.md | 5 + .../fixtures/list-item-spacing-tight-valid.md | 3 + test/fixtures/maximum-line-length-invalid.md | 7 + test/fixtures/maximum-line-length-valid.md | 16 + .../no-auto-link-without-protocol-invalid.md | 12 + .../no-auto-link-without-protocol-valid.md | 7 + .../no-blockquote-without-caret-invalid.md | 17 + .../no-blockquote-without-caret-valid.md | 17 + .../no-consecutive-blank-lines-invalid.md | 22 + .../no-consecutive-blank-lines-valid.md | 15 + .../no-duplicate-definitions-invalid.md | 2 + .../no-duplicate-definitions-valid.md | 2 + .../fixtures/no-duplicate-headings-invalid.md | 8 + test/fixtures/no-duplicate-headings-valid.md | 8 + .../no-emphasis-as-heading-invalid.md | 7 + test/fixtures/no-emphasis-as-heading-valid.md | 23 + .../no-heading-content-indent-invalid.md | 13 + .../no-heading-content-indent-valid.md | 13 + test/fixtures/no-heading-indent-invalid.md | 9 + test/fixtures/no-heading-indent-valid.md | 9 + test/fixtures/no-heading-punctuation-colon.md | 1 + .../fixtures/no-heading-punctuation-period.md | 1 + .../no-heading-punctuation-question.md | 1 + test/fixtures/no-heading-punctuation-valid.md | 1 + test/fixtures/no-html-invalid.md | 3 + test/fixtures/no-html-valid.md | 3 + test/fixtures/no-inline-padding-invalid.md | 9 + test/fixtures/no-inline-padding-valid.md | 9 + test/fixtures/no-literal-urls-invalid.md | 5 + test/fixtures/no-literal-urls-valid.md | 5 + .../no-missing-blank-lines-invalid.md | 19 + test/fixtures/no-missing-blank-lines-valid.md | 30 + .../no-multiple-toplevel-headings-invalid.md | 7 + .../no-multiple-toplevel-headings-valid.md | 4 + test/fixtures/no-shell-dollars-invalid.md | 36 + test/fixtures/no-shell-dollars-valid.md | 50 + .../no-shortcut-reference-image-invalid.md | 4 + .../no-shortcut-reference-image-valid.md | 4 + .../no-shortcut-reference-link-invalid.md | 4 + .../no-shortcut-reference-link-valid.md | 4 + test/fixtures/no-table-indentation-invalid.md | 6 + test/fixtures/no-table-indentation-valid.md | 5 + test/fixtures/no-tabs-invalid.md | 13 + test/fixtures/no-tabs-valid.md | 11 + .../fixtures/ordered-list-marker-value-one.md | 8 + .../ordered-list-marker-value-ordered.md | 8 + .../ordered-list-marker-value-single.md | 8 + test/fixtures/rule-style-invalid.md | 15 + test/fixtures/rule-style-valid.md | 15 + .../strong-marker-asterisk-underscore.md | 3 + test/fixtures/strong-marker-asterisk.md | 3 + .../strong-marker-underscore-asterisk.md | 3 + test/fixtures/strong-marker-underscore.md | 3 + .../table-cell-padding-compact-unaligned.md | 6 + test/fixtures/table-cell-padding-compact.md | 11 + test/fixtures/table-cell-padding-mixed.md | 19 + .../table-cell-padding-padded-unaligned.md | 6 + test/fixtures/table-cell-padding-padded.md | 11 + test/fixtures/table-pipe-alignment-invalid.md | 5 + test/fixtures/table-pipe-alignment-valid.md | 5 + test/fixtures/table-pipes-invalid.md | 23 + test/fixtures/table-pipes-valid.md | 11 + test/fixtures/the-file-name.md | 0 test/index.js | 1704 +++++ 223 files changed, 16051 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .jscs.json create mode 100644 .mdastignore create mode 100644 .mdastrc create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 bower.json create mode 100644 component.json create mode 100644 doc/api.md create mode 100644 doc/comparison/markdownlint.md create mode 100644 doc/rules.md create mode 100644 example.js create mode 100644 index.js create mode 100644 lib/filter.js create mode 100644 lib/index.js create mode 100644 lib/rules/blockquote-indentation.js create mode 100644 lib/rules/code-block-style.js create mode 100644 lib/rules/definition-case.js create mode 100644 lib/rules/definition-spacing.js create mode 100644 lib/rules/emphasis-marker.js create mode 100644 lib/rules/fenced-code-flag.js create mode 100644 lib/rules/fenced-code-marker.js create mode 100644 lib/rules/file-extension.js create mode 100644 lib/rules/final-definition.js create mode 100644 lib/rules/final-newline.js create mode 100644 lib/rules/first-heading-level.js create mode 100644 lib/rules/hard-break-spaces.js create mode 100644 lib/rules/heading-increment.js create mode 100644 lib/rules/heading-style.js create mode 100644 lib/rules/index.js create mode 100644 lib/rules/link-title-style.js create mode 100644 lib/rules/list-item-bullet-indent.js create mode 100644 lib/rules/list-item-content-indent.js create mode 100644 lib/rules/list-item-indent.js create mode 100644 lib/rules/list-item-spacing.js create mode 100644 lib/rules/maximum-heading-length.js create mode 100644 lib/rules/maximum-line-length.js create mode 100644 lib/rules/no-auto-link-without-protocol.js create mode 100644 lib/rules/no-blockquote-without-caret.js create mode 100644 lib/rules/no-consecutive-blank-lines.js create mode 100644 lib/rules/no-duplicate-definitions.js create mode 100644 lib/rules/no-duplicate-headings.js create mode 100644 lib/rules/no-emphasis-as-heading.js create mode 100644 lib/rules/no-file-name-articles.js create mode 100644 lib/rules/no-file-name-consecutive-dashes.js create mode 100644 lib/rules/no-file-name-irregular-characters.js create mode 100644 lib/rules/no-file-name-mixed-case.js create mode 100644 lib/rules/no-file-name-outer-dashes.js create mode 100644 lib/rules/no-heading-content-indent.js create mode 100644 lib/rules/no-heading-indent.js create mode 100644 lib/rules/no-heading-punctuation.js create mode 100644 lib/rules/no-html.js create mode 100644 lib/rules/no-inline-padding.js create mode 100644 lib/rules/no-literal-urls.js create mode 100644 lib/rules/no-missing-blank-lines.js create mode 100644 lib/rules/no-multiple-toplevel-headings.js create mode 100644 lib/rules/no-shell-dollars.js create mode 100644 lib/rules/no-shortcut-reference-image.js create mode 100644 lib/rules/no-shortcut-reference-link.js create mode 100644 lib/rules/no-table-indentation.js create mode 100644 lib/rules/no-tabs.js create mode 100644 lib/rules/ordered-list-marker-style.js create mode 100644 lib/rules/ordered-list-marker-value.js create mode 100644 lib/rules/rule-style.js create mode 100644 lib/rules/strong-marker.js create mode 100644 lib/rules/table-cell-padding.js create mode 100644 lib/rules/table-pipe-alignment.js create mode 100644 lib/rules/table-pipes.js create mode 100644 lib/rules/unordered-list-marker-style.js create mode 100644 lib/sort.js create mode 100644 lib/utilities/heading-style.js create mode 100644 lib/utilities/plural.js create mode 100644 lib/utilities/position.js create mode 100644 lib/utilities/to-string.js create mode 100644 lib/utilities/visit.js create mode 100644 logo.svg create mode 100644 mdast.js create mode 100644 mdast.min.js create mode 100644 package.json create mode 100644 readme.md create mode 100644 screen-shot.png create mode 100755 script/build-rule-documentation.js create mode 100644 test/clean.js create mode 100644 test/fixtures/-file-name-initial-dash.md create mode 100644 test/fixtures/blockquote-indentation-2.md create mode 100644 test/fixtures/blockquote-indentation-4.md create mode 100644 test/fixtures/code-style-fenced.md create mode 100644 test/fixtures/code-style-indented.md create mode 100644 test/fixtures/comments-disable.md create mode 100644 test/fixtures/comments-duplicates.md create mode 100644 test/fixtures/comments-enable.md create mode 100644 test/fixtures/comments-inline.md create mode 100644 test/fixtures/comments-invalid-keyword.md create mode 100644 test/fixtures/comments-invalid-rule-id.md create mode 100644 test/fixtures/comments-none.md create mode 100644 test/fixtures/definition-case-invalid.md create mode 100644 test/fixtures/definition-case-valid.md create mode 100644 test/fixtures/definition-spacing-invalid.md create mode 100644 test/fixtures/definition-spacing-valid.md create mode 100644 test/fixtures/emphasis-marker-asterisk-underscore.md create mode 100644 test/fixtures/emphasis-marker-asterisk.md create mode 100644 test/fixtures/emphasis-marker-underscore-asterisk.md create mode 100644 test/fixtures/emphasis-marker-underscore.md create mode 100644 test/fixtures/empty.md create mode 100644 test/fixtures/fenced-code-flag-invalid.md create mode 100644 test/fixtures/fenced-code-flag-unknown.md create mode 100644 test/fixtures/fenced-code-flag-valid.md create mode 100644 test/fixtures/fenced-code-marker-mismatched.md create mode 100644 test/fixtures/fenced-code-marker-tick.md create mode 100644 test/fixtures/fenced-code-marker-tilde.md create mode 100644 test/fixtures/file-extension-markdown.markdown create mode 100644 test/fixtures/file-extension-md.md create mode 100644 test/fixtures/file-name characters.md create mode 100644 test/fixtures/file-name--consecutive-dashes.md create mode 100644 test/fixtures/file-name-Upper-case.md create mode 100644 test/fixtures/file-name-final-dash-.md create mode 100644 test/fixtures/file-without-extension create mode 100644 test/fixtures/final-definition-invalid.md create mode 100644 test/fixtures/final-definition-valid.md create mode 100644 test/fixtures/final-newline-invalid.md create mode 100644 test/fixtures/final-newline-valid.md create mode 100644 test/fixtures/first-heading-level-invalid.md create mode 100644 test/fixtures/first-heading-level-valid.md create mode 100644 test/fixtures/hard-break-spaces-invalid.md create mode 100644 test/fixtures/hard-break-spaces-valid.md create mode 100644 test/fixtures/heading-increment-invalid-blockquote.md create mode 100644 test/fixtures/heading-increment-invalid-list.md create mode 100644 test/fixtures/heading-increment-invalid.md create mode 100644 test/fixtures/heading-length-normal.md create mode 100644 test/fixtures/heading-length-quite-short.md create mode 100644 test/fixtures/heading-length-too-long.md create mode 100644 test/fixtures/heading-nesting-initial.md create mode 100644 test/fixtures/heading-style-atx-closed.md create mode 100644 test/fixtures/heading-style-atx.md create mode 100644 test/fixtures/heading-style-empty.md create mode 100644 test/fixtures/heading-style-not-consistent.md create mode 100644 test/fixtures/heading-style-setext.md create mode 100644 test/fixtures/link-title-style-double.md create mode 100644 test/fixtures/link-title-style-missing.md create mode 100644 test/fixtures/link-title-style-parentheses.md create mode 100644 test/fixtures/link-title-style-single.md create mode 100644 test/fixtures/link-title-style-trailing-white-space.md create mode 100644 test/fixtures/list-item-bullet-indent-invalid.md create mode 100644 test/fixtures/list-item-bullet-indent-valid.md create mode 100644 test/fixtures/list-item-content-indent-invalid.md create mode 100644 test/fixtures/list-item-content-indent-valid.md create mode 100644 test/fixtures/list-item-indent-mixed.md create mode 100644 test/fixtures/list-item-indent-space.md create mode 100644 test/fixtures/list-item-indent-tab-size.md create mode 100644 test/fixtures/list-item-marker-asterisk.md create mode 100644 test/fixtures/list-item-marker-dash.md create mode 100644 test/fixtures/list-item-marker-dot.md create mode 100644 test/fixtures/list-item-marker-paren.md create mode 100644 test/fixtures/list-item-marker-plus.md create mode 100644 test/fixtures/list-item-spacing-loose-invalid.md create mode 100644 test/fixtures/list-item-spacing-loose-valid.md create mode 100644 test/fixtures/list-item-spacing-tight-invalid.md create mode 100644 test/fixtures/list-item-spacing-tight-valid.md create mode 100644 test/fixtures/maximum-line-length-invalid.md create mode 100644 test/fixtures/maximum-line-length-valid.md create mode 100644 test/fixtures/no-auto-link-without-protocol-invalid.md create mode 100644 test/fixtures/no-auto-link-without-protocol-valid.md create mode 100644 test/fixtures/no-blockquote-without-caret-invalid.md create mode 100644 test/fixtures/no-blockquote-without-caret-valid.md create mode 100644 test/fixtures/no-consecutive-blank-lines-invalid.md create mode 100644 test/fixtures/no-consecutive-blank-lines-valid.md create mode 100644 test/fixtures/no-duplicate-definitions-invalid.md create mode 100644 test/fixtures/no-duplicate-definitions-valid.md create mode 100644 test/fixtures/no-duplicate-headings-invalid.md create mode 100644 test/fixtures/no-duplicate-headings-valid.md create mode 100644 test/fixtures/no-emphasis-as-heading-invalid.md create mode 100644 test/fixtures/no-emphasis-as-heading-valid.md create mode 100644 test/fixtures/no-heading-content-indent-invalid.md create mode 100644 test/fixtures/no-heading-content-indent-valid.md create mode 100644 test/fixtures/no-heading-indent-invalid.md create mode 100644 test/fixtures/no-heading-indent-valid.md create mode 100644 test/fixtures/no-heading-punctuation-colon.md create mode 100644 test/fixtures/no-heading-punctuation-period.md create mode 100644 test/fixtures/no-heading-punctuation-question.md create mode 100644 test/fixtures/no-heading-punctuation-valid.md create mode 100644 test/fixtures/no-html-invalid.md create mode 100644 test/fixtures/no-html-valid.md create mode 100644 test/fixtures/no-inline-padding-invalid.md create mode 100644 test/fixtures/no-inline-padding-valid.md create mode 100644 test/fixtures/no-literal-urls-invalid.md create mode 100644 test/fixtures/no-literal-urls-valid.md create mode 100644 test/fixtures/no-missing-blank-lines-invalid.md create mode 100644 test/fixtures/no-missing-blank-lines-valid.md create mode 100644 test/fixtures/no-multiple-toplevel-headings-invalid.md create mode 100644 test/fixtures/no-multiple-toplevel-headings-valid.md create mode 100644 test/fixtures/no-shell-dollars-invalid.md create mode 100644 test/fixtures/no-shell-dollars-valid.md create mode 100644 test/fixtures/no-shortcut-reference-image-invalid.md create mode 100644 test/fixtures/no-shortcut-reference-image-valid.md create mode 100644 test/fixtures/no-shortcut-reference-link-invalid.md create mode 100644 test/fixtures/no-shortcut-reference-link-valid.md create mode 100644 test/fixtures/no-table-indentation-invalid.md create mode 100644 test/fixtures/no-table-indentation-valid.md create mode 100644 test/fixtures/no-tabs-invalid.md create mode 100644 test/fixtures/no-tabs-valid.md create mode 100644 test/fixtures/ordered-list-marker-value-one.md create mode 100644 test/fixtures/ordered-list-marker-value-ordered.md create mode 100644 test/fixtures/ordered-list-marker-value-single.md create mode 100644 test/fixtures/rule-style-invalid.md create mode 100644 test/fixtures/rule-style-valid.md create mode 100644 test/fixtures/strong-marker-asterisk-underscore.md create mode 100644 test/fixtures/strong-marker-asterisk.md create mode 100644 test/fixtures/strong-marker-underscore-asterisk.md create mode 100644 test/fixtures/strong-marker-underscore.md create mode 100644 test/fixtures/table-cell-padding-compact-unaligned.md create mode 100644 test/fixtures/table-cell-padding-compact.md create mode 100644 test/fixtures/table-cell-padding-mixed.md create mode 100644 test/fixtures/table-cell-padding-padded-unaligned.md create mode 100644 test/fixtures/table-cell-padding-padded.md create mode 100644 test/fixtures/table-pipe-alignment-invalid.md create mode 100644 test/fixtures/table-pipe-alignment-valid.md create mode 100644 test/fixtures/table-pipes-invalid.md create mode 100644 test/fixtures/table-pipes-valid.md create mode 100644 test/fixtures/the-file-name.md create mode 100644 test/index.js 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 0000000000000000000000000000000000000000..18833feacd60cb81dd83c5b73809cde7197d0f3e GIT binary patch literal 64208 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

UdTQ@ztma|P_Gk0Bu9N+ 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 literal 0 HcmV?d00001 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; + }); + }); + }); +});