diff --git a/README.md b/README.md index 0d6bdbcb..71e9eec6 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ playground for learning and exploring. * **[MD044](doc/Rules.md#md044)** *proper-names* - Proper names should have the correct capitalization * **[MD045](doc/Rules.md#md045)** *no-alt-text* - Images should have alternate text (alt text) * **[MD046](doc/Rules.md#md046)** *code-block-style* - Code block style +* **[MD047](doc/Rules.md#md047)** *single-trailing-newline* - Files should end with a single newline character See [Rules.md](doc/Rules.md) for more details. @@ -102,7 +103,7 @@ Tags group related rules and can be used to enable/disable multiple rules at onc * **accessibility** - MD045 * **atx** - MD018, MD019 * **atx_closed** - MD020, MD021 -* **blank_lines** - MD012, MD022, MD031, MD032 +* **blank_lines** - MD012, MD022, MD031, MD032, MD047 * **blockquote** - MD027, MD028 * **bullet** - MD004, MD005, MD006, MD007, MD032 * **code** - MD014, MD031, MD038, MD040, MD046 diff --git a/doc/Rules.md b/doc/Rules.md index 2b9a9334..a3ed4a7a 100644 --- a/doc/Rules.md +++ b/doc/Rules.md @@ -1576,3 +1576,30 @@ To fix violations of this rule, use a consistent style (either indenting or code The specified style can be specific (`fenced`, `indented`) or simply require that usage be consistent within the document (`consistent`). + + + +## MD047 - Files should end with a single newline character + +Tags: blank_lines + +Aliases: single-trailing-newline + +This rule is triggered when there is not a single newline character at the end of a file. + +Example that triggers the rule: + +```markdown +# Heading + +This file ends without a newline.[EOF] +``` + +To fix the violation, add a newline character to the end of the file: + +```markdown +# Heading + +This file ends with a newline. +[EOF] +``` diff --git a/lib/md047.js b/lib/md047.js new file mode 100644 index 00000000..5d44b3a7 --- /dev/null +++ b/lib/md047.js @@ -0,0 +1,18 @@ +// @ts-check + +"use strict"; + +const { addError, isBlankLine } = require("../helpers"); + +module.exports = { + "names": [ "MD047", "single-trailing-newline" ], + "description": "Files should end with a single newline character", + "tags": [ "blank_lines" ], + "function": function MD047(params, onError) { + const lastLineNumber = params.lines.length; + const lastLine = params.lines[lastLineNumber - 1]; + if (!isBlankLine(lastLine)) { + addError(onError, lastLineNumber); + } + } +}; diff --git a/lib/rules.js b/lib/rules.js index 8f78449d..67d3176f 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -49,7 +49,8 @@ const rules = [ require("./md043"), require("./md044"), require("./md045"), - require("./md046") + require("./md046"), + require("./md047") ]; rules.forEach((rule) => { const name = rule.names[0].toLowerCase(); diff --git a/schema/markdownlint-config-schema.json b/schema/markdownlint-config-schema.json index c494a8cc..46485e66 100644 --- a/schema/markdownlint-config-schema.json +++ b/schema/markdownlint-config-schema.json @@ -1297,6 +1297,16 @@ }, "additionalProperties": false }, + "MD047": { + "description": "MD047/single-trailing-newline - Files should end with a single newline character", + "type": "boolean", + "default": true + }, + "single-trailing-newline": { + "description": "MD047/single-trailing-newline - Files should end with a single newline character", + "type": "boolean", + "default": true + }, "headings": { "description": "headings - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043", "type": "boolean", @@ -1338,7 +1348,7 @@ "default": true }, "blank_lines": { - "description": "blank_lines - MD012, MD022, MD031, MD032", + "description": "blank_lines - MD012, MD022, MD031, MD032, MD047", "type": "boolean", "default": true }, diff --git a/test/atx_heading_spacing.md b/test/atx_heading_spacing.md index 58443724..e668fa62 100644 --- a/test/atx_heading_spacing.md +++ b/test/atx_heading_spacing.md @@ -2,4 +2,4 @@ ## Heading 2 {MD019} -## Heading 3 {MD019} \ No newline at end of file +## Heading 3 {MD019} diff --git a/test/break-all-the-rules.md b/test/break-all-the-rules.md index 16f76f73..378aea0f 100644 --- a/test/break-all-the-rules.md +++ b/test/break-all-the-rules.md @@ -78,4 +78,4 @@ code fence without language {MD040:73} {MD046:73} markdownLint {MD044} -![](image.jpg) {MD045} +![](image.jpg) {MD045} {MD047} \ No newline at end of file diff --git a/test/bulleted_list_4_space_indent.md b/test/bulleted_list_4_space_indent.md index 1c5e75db..8079d913 100644 --- a/test/bulleted_list_4_space_indent.md +++ b/test/bulleted_list_4_space_indent.md @@ -1,3 +1,3 @@ * Test X * Test Y {MD007} - * Test Z {MD007} \ No newline at end of file + * Test Z {MD007} diff --git a/test/bulleted_list_not_at_beginning_of_line.md b/test/bulleted_list_not_at_beginning_of_line.md index 84bd0ba9..3a735f1b 100644 --- a/test/bulleted_list_not_at_beginning_of_line.md +++ b/test/bulleted_list_not_at_beginning_of_line.md @@ -11,4 +11,4 @@ Some text Some more text * Item {MD006} - * Item \ No newline at end of file + * Item diff --git a/test/consecutive_blank_lines.md b/test/consecutive_blank_lines.md index 9d403503..95fde905 100644 --- a/test/consecutive_blank_lines.md +++ b/test/consecutive_blank_lines.md @@ -8,4 +8,4 @@ Some text {MD012:3} with two blank lines in it -Some more text \ No newline at end of file +Some more text diff --git a/test/consistent_bullet_styles_asterisk.md b/test/consistent_bullet_styles_asterisk.md index e8ccdee3..ced4b371 100644 --- a/test/consistent_bullet_styles_asterisk.md +++ b/test/consistent_bullet_styles_asterisk.md @@ -1,3 +1,3 @@ * Item * Item - * Item \ No newline at end of file + * Item diff --git a/test/consistent_bullet_styles_dash.md b/test/consistent_bullet_styles_dash.md index b6c50290..2d89e193 100644 --- a/test/consistent_bullet_styles_dash.md +++ b/test/consistent_bullet_styles_dash.md @@ -1,3 +1,3 @@ - Item - Item - - Item \ No newline at end of file + - Item diff --git a/test/consistent_bullet_styles_plus.md b/test/consistent_bullet_styles_plus.md index 7930238c..8f4f5208 100644 --- a/test/consistent_bullet_styles_plus.md +++ b/test/consistent_bullet_styles_plus.md @@ -1,3 +1,3 @@ + Item + Item - + Item \ No newline at end of file + + Item diff --git a/test/detailed-results-MD030-warning-message.md b/test/detailed-results-MD030-warning-message.md index b7760bd1..9f69121c 100644 --- a/test/detailed-results-MD030-warning-message.md +++ b/test/detailed-results-MD030-warning-message.md @@ -10,4 +10,4 @@ - a -1. a \ No newline at end of file +1. a diff --git a/test/detailed-results-MD041-MD050.md b/test/detailed-results-MD041-MD050.md index a6880a7e..41ceca0d 100644 --- a/test/detailed-results-MD041-MD050.md +++ b/test/detailed-results-MD041-MD050.md @@ -23,3 +23,5 @@ Fenced code ``` Indented code + +Missing newline character \ No newline at end of file diff --git a/test/detailed-results-MD041-MD050.results.json b/test/detailed-results-MD041-MD050.results.json index b864ee01..b7b80366 100644 --- a/test/detailed-results-MD041-MD050.results.json +++ b/test/detailed-results-MD041-MD050.results.json @@ -45,7 +45,7 @@ "errorRange": [25, 13] }, { - "lineNumber": 26, + "lineNumber": 27, "ruleNames": [ "MD043", "required-headings", "required-headers" ], "ruleDescription": "Required heading structure", "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md043", @@ -97,5 +97,14 @@ "errorDetail": "Expected: fenced; Actual: indented", "errorContext": null, "errorRange": null + }, + { + "lineNumber": 27, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md047", + "errorDetail": null, + "errorContext": null, + "errorRange": null } ] \ No newline at end of file diff --git a/test/fenced_code_without_blank_lines.md b/test/fenced_code_without_blank_lines.md index b4cf01a1..7a786d6c 100644 --- a/test/fenced_code_without_blank_lines.md +++ b/test/fenced_code_without_blank_lines.md @@ -38,5 +38,5 @@ code text ``` -code at end of file without newline +code at end of file without newline {MD047:42} ``` \ No newline at end of file diff --git a/test/first_heading_bad_atx.md b/test/first_heading_bad_atx.md index 37f21480..318140e3 100644 --- a/test/first_heading_bad_atx.md +++ b/test/first_heading_bad_atx.md @@ -1 +1 @@ -## Heading \ No newline at end of file +## Heading diff --git a/test/first_heading_bad_setext.md b/test/first_heading_bad_setext.md index c9ebf7c5..b9e54368 100644 --- a/test/first_heading_bad_setext.md +++ b/test/first_heading_bad_setext.md @@ -1,2 +1,2 @@ Heading -------- \ No newline at end of file +------- diff --git a/test/first_heading_good_atx.md b/test/first_heading_good_atx.md index d9b3ffed..7c1f8312 100644 --- a/test/first_heading_good_atx.md +++ b/test/first_heading_good_atx.md @@ -1 +1 @@ -# Heading \ No newline at end of file +# Heading diff --git a/test/first_heading_good_setext.md b/test/first_heading_good_setext.md index 46ccb11b..5c4577b4 100644 --- a/test/first_heading_good_setext.md +++ b/test/first_heading_good_setext.md @@ -1,2 +1,2 @@ Heading -======= \ No newline at end of file +======= diff --git a/test/heading_duplicate_content.md b/test/heading_duplicate_content.md index 662aaa6f..eeb83bde 100644 --- a/test/heading_duplicate_content.md +++ b/test/heading_duplicate_content.md @@ -8,4 +8,4 @@ ## Heading 3 -{MD024:5} {MD024:7} \ No newline at end of file +{MD024:5} {MD024:7} diff --git a/test/heading_multiple_toplevel.md b/test/heading_multiple_toplevel.md index fa6467aa..3ae67562 100644 --- a/test/heading_multiple_toplevel.md +++ b/test/heading_multiple_toplevel.md @@ -1,3 +1,3 @@ # Heading 1 -# Heading 2 {MD025} \ No newline at end of file +# Heading 2 {MD025} diff --git a/test/heading_mutliple_h1_no_toplevel.md b/test/heading_mutliple_h1_no_toplevel.md index a8453007..4ccb03a1 100644 --- a/test/heading_mutliple_h1_no_toplevel.md +++ b/test/heading_mutliple_h1_no_toplevel.md @@ -2,4 +2,4 @@ Some introductory text # Heading 1 -# Heading 2 \ No newline at end of file +# Heading 2 diff --git a/test/heading_trailing_punctuation.md b/test/heading_trailing_punctuation.md index 81d4fabf..3aedc267 100644 --- a/test/heading_trailing_punctuation.md +++ b/test/heading_trailing_punctuation.md @@ -8,4 +8,4 @@ ## Heading 5 {MD026}; -## Heading 6 {MD026}? \ No newline at end of file +## Heading 6 {MD026}? diff --git a/test/headings_bad.md b/test/headings_bad.md index 1fa4b013..dc5ba916 100644 --- a/test/headings_bad.md +++ b/test/headings_bad.md @@ -4,4 +4,4 @@ ## Heading 2 -#### Heading 4 {MD001} \ No newline at end of file +#### Heading 4 {MD001} diff --git a/test/headings_good.md b/test/headings_good.md index f2576043..c9aaa2be 100644 --- a/test/headings_good.md +++ b/test/headings_good.md @@ -2,4 +2,4 @@ ## Heading 2 -## Heading 3 \ No newline at end of file +## Heading 3 diff --git a/test/headings_surrounding_space_setext.md b/test/headings_surrounding_space_setext.md index 8aabebc6..b75cd0f9 100644 --- a/test/headings_surrounding_space_setext.md +++ b/test/headings_surrounding_space_setext.md @@ -12,4 +12,4 @@ Heading 4 Some text Heading 5 ---------- \ No newline at end of file +--------- diff --git a/test/inconsistent_bullet_indent_same_level.md b/test/inconsistent_bullet_indent_same_level.md index 4679c876..7851e6c8 100644 --- a/test/inconsistent_bullet_indent_same_level.md +++ b/test/inconsistent_bullet_indent_same_level.md @@ -1,4 +1,4 @@ * Item * Item {MD007} * Item {MD005} - * Item {MD007} \ No newline at end of file + * Item {MD007} diff --git a/test/inconsistent_bullet_styles_asterisk.md b/test/inconsistent_bullet_styles_asterisk.md index 9825cacb..795b16b9 100644 --- a/test/inconsistent_bullet_styles_asterisk.md +++ b/test/inconsistent_bullet_styles_asterisk.md @@ -1,3 +1,3 @@ * Item + Item {MD004} - - Item {MD004} \ No newline at end of file + - Item {MD004} diff --git a/test/inconsistent_bullet_styles_dash.md b/test/inconsistent_bullet_styles_dash.md index 4f3b756e..b9577773 100644 --- a/test/inconsistent_bullet_styles_dash.md +++ b/test/inconsistent_bullet_styles_dash.md @@ -1,3 +1,3 @@ - Item * Item {MD004} - + Item {MD004} \ No newline at end of file + + Item {MD004} diff --git a/test/inconsistent_bullet_styles_plus.md b/test/inconsistent_bullet_styles_plus.md index bd4799f8..e3e3ea5d 100644 --- a/test/inconsistent_bullet_styles_plus.md +++ b/test/inconsistent_bullet_styles_plus.md @@ -1,3 +1,3 @@ + Item * Item {MD004} - - Item {MD004} \ No newline at end of file + - Item {MD004} diff --git a/test/lists_without_blank_lines.md b/test/lists_without_blank_lines.md index 7f370d48..6e593efe 100644 --- a/test/lists_without_blank_lines.md +++ b/test/lists_without_blank_lines.md @@ -72,4 +72,4 @@ code text -* list (on last line without newline) \ No newline at end of file +* list (on last line without newline) {MD047} \ No newline at end of file diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index a96153b2..f564caa2 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -250,7 +250,7 @@ module.exports.resultFormattingV1 = function resultFormattingV1(test) { const options = { "strings": { "truncate": - "# Multiple spaces inside hashes on closed atx style heading #" + "# Multiple spaces inside hashes on closed atx style heading #\n" }, "files": [ "./test/atx_heading_spacing.md", @@ -352,7 +352,7 @@ module.exports.resultFormattingV2 = function resultFormattingV2(test) { const options = { "strings": { "truncate": - "# Multiple spaces inside hashes on closed atx style heading #" + "# Multiple spaces inside hashes on closed atx style heading #\n" }, "files": [ "./test/atx_heading_spacing.md", @@ -448,14 +448,14 @@ module.exports.stringInputLineEndings = function stringInputLineEndings(test) { test.expect(2); const options = { "strings": { - "cr": "One\rTwo\r#Three", - "lf": "One\nTwo\n#Three", - "crlf": "One\r\nTwo\r\n#Three", - "mixed": "One\rTwo\n#Three", - "crnel": "One\r\u0085Two\r\u0085#Three", - "snl": "One\u2424Two\u2424#Three", - "lsep": "One\u2028Two\u2028#Three", - "nel": "One\u0085Two\u0085#Three" + "cr": "One\rTwo\r#Three\n", + "lf": "One\nTwo\n#Three\n", + "crlf": "One\r\nTwo\r\n#Three\n", + "mixed": "One\rTwo\n#Three\n", + "crnel": "One\r\u0085Two\r\u0085#Three\n", + "snl": "One\u2424Two\u2424#Three\n", + "lsep": "One\u2028Two\u2028#Three\n", + "nel": "One\u0085Two\u0085#Three\n" }, "config": defaultConfig, "resultVersion": 0 @@ -811,7 +811,8 @@ module.exports.styleAll = function styleAll(test) { "MD041": [ 1 ], "MD042": [ 77 ], "MD045": [ 81 ], - "MD046": [ 49, 73 ] + "MD046": [ 49, 73 ], + "MD047": [ 81 ] } }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -851,7 +852,8 @@ module.exports.styleRelaxed = function styleRelaxed(test) { "MD036": [ 65 ], "MD042": [ 77 ], "MD045": [ 81 ], - "MD046": [ 49, 73 ] + "MD046": [ 49, 73 ], + "MD047": [ 81 ] } }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -917,7 +919,7 @@ module.exports.noInlineConfig = function noInlineConfig(test) { "", "", "", - "\tTab" + "\tTab\n" ].join("\n") }, "noInlineConfig": true, @@ -1093,7 +1095,7 @@ module.exports.missingStringValue = function missingStringValue(test) { }; module.exports.readme = function readme(test) { - test.expect(111); + test.expect(113); const tagToRules = {}; rules.forEach(function forRule(rule) { rule.tags.forEach(function forTag(tag) { @@ -1159,7 +1161,7 @@ module.exports.readme = function readme(test) { }; module.exports.doc = function doc(test) { - test.expect(320); + test.expect(327); fs.readFile("doc/Rules.md", helpers.utf8Encoding, function readFile(err, contents) { test.ifError(err); @@ -1859,7 +1861,7 @@ module.exports.configBadHybridSync = function configBadHybridSync(test) { module.exports.allBuiltInRulesHaveValidUrl = function allBuiltInRulesHaveValidUrl(test) { - test.expect(126); + test.expect(129); rules.forEach(function forRule(rule) { test.ok(rule.information); test.ok(Object.getPrototypeOf(rule.information) === URL.prototype); @@ -2216,7 +2218,7 @@ module.exports.customRulesNpmPackage = function customRulesNpmPackage(test) { const options = { "customRules": [ require("./rules/npm") ], "strings": { - "string": "# Text\n\n---\n\nText" + "string": "# Text\n\n---\n\nText\n" }, "resultVersion": 0 }; @@ -2515,7 +2517,7 @@ module.exports.customRulesOnErrorLazy = function customRulesOnErrorLazy(test) { } ], "strings": { - "string": "# Heading" + "string": "# Heading\n" } }; markdownlint(options, function callback(err, actualResult) { @@ -2602,7 +2604,7 @@ module.exports.markdownItPluginsSingle = test.expect(2); markdownlint({ "strings": { - "string": "# Heading\n\nText [ link ](https://example.com)" + "string": "# Heading\n\nText [ link ](https://example.com)\n" }, "markdownItPlugins": [ [ @@ -2627,7 +2629,7 @@ module.exports.markdownItPluginsMultiple = test.expect(4); markdownlint({ "strings": { - "string": "# Heading\n\nText H~2~0 text 29^th^ text" + "string": "# Heading\n\nText H~2~0 text 29^th^ text\n" }, "markdownItPlugins": [ [ pluginSub ], @@ -2680,7 +2682,7 @@ $$ 1 $$$$ 2 -$$` +$$\n` }, "markdownItPlugins": [ [ pluginKatex ] ], "resultVersion": 0 diff --git a/test/md022-line-number-out-of-range.json b/test/md022-line-number-out-of-range.json new file mode 100644 index 00000000..4970b843 --- /dev/null +++ b/test/md022-line-number-out-of-range.json @@ -0,0 +1,4 @@ +{ + "default": true, + "MD047": false +} diff --git a/test/mixed_heading_types_atx.md b/test/mixed_heading_types_atx.md index d94af2a6..b82f5462 100644 --- a/test/mixed_heading_types_atx.md +++ b/test/mixed_heading_types_atx.md @@ -3,4 +3,4 @@ ## Heading 2 {MD003} ## Heading 3 {MD003} ------------------ \ No newline at end of file +----------------- diff --git a/test/mixed_heading_types_atx_closed.md b/test/mixed_heading_types_atx_closed.md index cc29cf88..e786dd65 100644 --- a/test/mixed_heading_types_atx_closed.md +++ b/test/mixed_heading_types_atx_closed.md @@ -3,4 +3,4 @@ ## Heading 2 {MD003} Heading 3 {MD003} ------------------ \ No newline at end of file +----------------- diff --git a/test/mixed_heading_types_setext.md b/test/mixed_heading_types_setext.md index bcf67836..47515ac8 100644 --- a/test/mixed_heading_types_setext.md +++ b/test/mixed_heading_types_setext.md @@ -3,4 +3,4 @@ Heading 1 ## Heading 2 {MD003} -## Heading 3 {MD003} ## \ No newline at end of file +## Heading 3 {MD003} ## diff --git a/test/required-headings-missing-last.md b/test/required-headings-missing-last.md index dd4ceb8e..a9b30cda 100644 --- a/test/required-headings-missing-last.md +++ b/test/required-headings-missing-last.md @@ -4,4 +4,4 @@ One Two --- -{MD043} \ No newline at end of file +{MD043} {MD047} \ No newline at end of file diff --git a/test/whitespace_issues.md b/test/whitespace_issues.md index 8f4209eb..7c50dfb2 100644 --- a/test/whitespace_issues.md +++ b/test/whitespace_issues.md @@ -1,3 +1,3 @@ Some text {MD009} Some more text {MD010} -Some more text \ No newline at end of file +Some more text