Initial commit

This commit is contained in:
Titus Wormer 2015-06-02 08:34:14 +02:00
commit 721c363001
223 changed files with 16051 additions and 0 deletions

15
.editorconfig Normal file
View File

@ -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

0
.eslintignore Normal file
View File

9
.eslintrc Normal file
View File

@ -0,0 +1,9 @@
{
"env": {
"node": true,
"browser": true
},
"rules": {
"quotes": [2, "single"]
}
}

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
*.log
bower_components/
build/
components/
node_modules/
coverage/
build.js

140
.jscs.json Normal file
View File

@ -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
}
}

7
.mdastignore Normal file
View File

@ -0,0 +1,7 @@
# `node_modules/` is already ignored.
# Duos components include docs.
components
# Do not process fixtures.
test

10
.mdastrc Normal file
View File

@ -0,0 +1,10 @@
{
"plugins": [
"./",
"github",
"toc"
],
"settings": {
"bullet": "*"
}
}

9
.travis.yml Normal file
View File

@ -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

21
LICENSE Normal file
View File

@ -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.

37
bower.json Normal file
View File

@ -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 <tituswormer@gmail.com>"
],
"ignore": [
".*",
"*.log",
"*.png",
"*.svg",
"*.md",
"build/",
"components/",
"coverage/",
"node_modules/",
"script/",
"test/",
"build.js",
"example.js",
"index.js",
"component.json",
"package.json"
]
}

21
component.json Normal file
View File

@ -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"
]
}

104
doc/api.md Normal file
View File

@ -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 mdasts [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 schemas. First, theyre 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 ESLints 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.

View File

@ -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 | Wont warn when thats 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 | |

1142
doc/rules.md Normal file

File diff suppressed because it is too large Load Diff

16
example.js Normal file
View File

@ -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));
});

3
index.js Normal file
View File

@ -0,0 +1,3 @@
'use strict';
module.exports = require('./lib');

72
lib/filter.js Normal file
View File

@ -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 ruleIds 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;

243
lib/index.js Normal file
View File

@ -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;

View File

@ -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
* <!-- Valid, when set to `4`, invalid when set to `2` -->
* > Hello
* ...
* > World
*
* <!-- Valid, when set to `2`, invalid when set to `4` -->
* > Hello
* ...
* > World
*
* <!-- Always invalid -->
* > 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;

View File

@ -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
* <!-- Valid, when set to `indented` or `consistent`, invalid when set to `fenced` -->
* Hello
*
* ...
*
* World
*
* <!-- Valid, when set to `fenced` or `consistent`, invalid when set to `indented` -->
* ```
* Hello
* ```
* ...
* ```bar
* World
* ```
*
* <!-- Always invalid -->
* 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;

View File

@ -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
* <!-- Valid -->
* [example] http://example.com "Example Domain"
*
* <!-- Invalid -->
* ![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;

View File

@ -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
* <!-- Valid -->
* [example domain] http://example.com "Example Domain"
*
* <!-- Invalid -->
* ![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;

View File

@ -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
* <!-- Valid when set to `consistent` or `*` -->
* *foo*
* *bar*
*
* <!-- Valid when set to `consistent` or `_` -->
* _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;

View File

@ -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.<string>` 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
* <!-- Valid: -->
* ```hello
* world();
* ```
*
* <!-- Valid: -->
* Hello
*
* <!-- Invalid: -->
* ```
* world();
* ```
*
* <!-- Valid when given `{allowEmpty: true}`: -->
* ```
* world();
* ```
*
* <!-- Invalid when given `["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.<string>} [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;

View File

@ -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
* <!-- Valid by default and `` '`' ``: -->
* ```foo
* bar();
* ```
*
* ```
* baz();
* ```
*
* <!-- Valid by default and `'~'`: -->
* ~~~foo
* bar();
* ~~~
*
* ~~~
* baz();
* ~~~
*
* <!-- Always invalid: -->
* ~~~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;

View File

@ -0,0 +1,45 @@
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer. All rights reserved.
* @module file-extension
* @fileoverview
* Warn when the documents 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;

View File

@ -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
* <!-- Valid: -->
* ...
*
* [example] http://example.com "Example Domain"
*
* <!-- Invalid: -->
* ...
*
* [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;

View File

@ -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;

View File

@ -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
* <!-- Valid: -->
* # Foo
*
* ## Bar
*
* <!-- Invalid: -->
* ## 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;

View File

@ -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
* <!-- Note: the middle-dots represent spaces -->
*
* <!-- Valid: -->
* Lorem ipsum··
* dolor sit amet
*
* <!-- Invalid: -->
* 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;

View File

@ -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
* <!-- Valid: -->
* # Foo
*
* ## Bar
*
* <!-- Invalid: -->
* # 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;

View File

@ -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
* <!-- Valid when `consistent` or `atx` -->
* # Foo
*
* ## Bar
*
* ### Baz
*
* <!-- Valid when `consistent` or `atx-closed` -->
* # Foo #
*
* ## Bar #
*
* ### Baz ###
*
* <!-- Valid when `consistent` or `setext` -->
* Foo
* ===
*
* Bar
* ---
*
* ### Baz
*
* <!-- Invalid -->
* 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;

68
lib/rules/index.js Normal file
View File

@ -0,0 +1,68 @@
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer. All rights reserved.
* @module Rules
* @fileoverview Map of rule ids 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')
};

View File

@ -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
* <!-- Valid when `consistent` or `"` -->
* [Example](http://example.com "Example Domain")
* [Example](http://example.com "Example Domain")
*
* <!-- Valid when `consistent` or `'` -->
* [Example](http://example.com 'Example Domain')
* [Example](http://example.com 'Example Domain')
*
* <!-- Valid when `consistent` or `()` -->
* [Example](http://example.com (Example Domain))
* [Example](http://example.com (Example Domain))
*
* <!-- Always invalid -->
* [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;

View File

@ -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
* <!-- Valid -->
* * List item
* * List item
*
* <!-- Invalid -->
* * 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;

View File

@ -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
* <!-- Valid -->
* * List item
*
* * Nested list item indented by 4 spaces
*
* <!-- Invalid -->
* * 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(
'Dont use mixed indentation for children, ' + word +
' ' + diff + ' ' + plural('space', diff),
{
'line': start(item).line,
'column': column
}
);
}
});
});
done();
}
/*
* Expose.
*/
module.exports = listItemContentIndent;

View File

@ -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 items bullet and its content
* violates a given style.
*
* Options: `string`, either `'tab-size'`, `'mixed'`, or `'space'`,
* default: `'tab-size'`.
* @example
* <!-- Valid when `tab-size` -->
* * List
* item.
*
* 11. List
* item.
*
* <!-- Valid when `mixed` -->
* * List item.
*
* 11. List item
*
* * List
* item.
*
* 11. List
* item.
*
* <!-- Valid when `space` -->
* * 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 items 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;

View File

@ -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
* <!-- Valid: -->
* - Wrapped
* item
*
* - item 2
*
* - item 3
*
* <!-- Valid: -->
* - item 1
* - item 2
* - item 3
*
* <!-- Invalid: -->
* - Wrapped
* item
* - item 2
* - item 3
*
* <!-- Invalid: -->
* - 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 + ', isnt', {
'start': end(item),
'end': start(next)
});
}
});
});
done();
}
/*
* Expose.
*/
module.exports = listItemSpacing;

View File

@ -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
* <!-- Valid, when set to `40` -->
* # Alpha bravo charlie delta echo
* # ![Alpha bravo charlie delta echo](http://example.com/nato.png)
*
* <!-- Invalid, when set to `40` -->
* # 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;

View File

@ -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
* <!-- Valid, when set to `40` -->
* 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 |
*
* <!-- Invalid, when set to `40` -->
* 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
* theres 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 theres 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;

View File

@ -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
* <!-- Valid: -->
* <http://www.example.com>
* <mailto:foo@bar.com>
*
* <!-- Invalid: -->
* <www.example.com>
* <foo@bar.com>
*/
'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;

View File

@ -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
* <!-- Valid: -->
* > Foo...
* >
* > ...Bar.
*
* <!-- Invalid: -->
* > 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;

View File

@ -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
* <!-- Valid: -->
* Foo...
*
* ...Bar.
*
* <!-- Invalid: -->
* 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;

View File

@ -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
* <!-- Valid: -->
* [foo]: bar
* [baz]: qux
*
* <!-- Invalid: -->
* [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;

View File

@ -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
* <!-- Valid: -->
* # Foo
*
* ## Bar
*
* <!-- Invalid: -->
* # 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;

View File

@ -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
* <!-- Valid: -->
* # Foo:
*
* Bar.
*
* <!-- Invalid: -->
* *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('Dont use emphasis to introduce a section, use a heading', node);
}
}
});
done();
}
/*
* Expose.
*/
module.exports = noEmphasisAsHeading;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,119 @@
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer. All rights reserved.
* @module no-heading-content-indent
* @fileoverview
* Warn when a headings content is indented.
* @example
* <!-- Note: the middle-dots represent spaces -->
* <!-- Invalid: -->
* #··Foo
*
* ## Bar··##
*
* ##··Baz
*
* <!-- Valid: -->
* #·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 headings 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 headings content',
final
);
}
}
});
done();
}
/*
* Expose.
*/
module.exports = noHeadingContentIndent;

View File

@ -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
* <!-- Note: the middle-dots represent spaces -->
* <!-- Invalid: -->
* ···# Hello world
*
* ·Foo
* -----
*
* ·# Hello world #
*
* ···Bar
* =====
*
* <!-- Valid: -->
* # 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;

View File

@ -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
* <!-- Invalid: -->
* # Hello:
*
* # Hello?
*
* # Hello!
*
* # Hello,
*
* # Hello;
*
* <!-- Valid: -->
* # 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('Dont add a trailing `' + value + '` to headings', node);
}
});
done();
}
/*
* Expose.
*/
module.exports = noHeadingPunctuation;

45
lib/rules/no-html.js Normal file
View File

@ -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 doesnt have native comments.
* @example
* <!-- Invalid: -->
* <h1>Hello</h1>
*
* <!-- Valid: -->
* # 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*<!--/.test(node.value)) {
file.warn('Do not use HTML in markdown', node);
}
});
done();
}
module.exports = html;

View File

@ -0,0 +1,68 @@
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer. All rights reserved.
* @module no-inline-padding
* @fileoverview
* Warn when inline nodes are padded with spaces between markers and
* content.
*
* Warns for emphasis, strong, delete, image, and link.
* @example
* <!-- Invalid: -->
* * Hello *, [ world ](http://foo.bar/baz)
*
* <!-- Valid: -->
* *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('Dont pad `' + type + '` with inner spaces', node);
}
}
});
done();
}
/*
* Expose.
*/
module.exports = noInlinePadding;

View File

@ -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
* <!-- Invalid: -->
* http://foo.bar/baz
*
* <!-- Valid: -->
* <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('Dont use literal URLs without angle brackets', node);
}
});
done();
}
/*
* Expose.
*/
module.exports = noLiteralURLs;

View File

@ -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
* <!-- Invalid: -->
* # Foo
* ## Bar
*
* <!-- Valid: -->
* # 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;

View File

@ -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
* <!-- Invalid: -->
* # Foo
*
* # Bar
*
* <!-- Valid: -->
* # 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('Dont use multiple top level headings (' + pos.line + ':' + pos.column + ')', node);
}
topLevelheading = node;
}
});
done();
}
module.exports = noMultipleToplevelHeadings;

View File

@ -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
* <!-- Invalid: -->
* ```bash
* $ echo a
* $ echo a > file
* ```
*
* <!-- Valid: -->
* ```sh
* echo a
* echo a > file
* ```
*
* <!-- Also valid: -->
* ```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;

View File

@ -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
* <!-- Invalid: -->
* ![foo]
*
* [foo]: http://foo.bar/baz.png
*
* <!-- Valid: -->
* ![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;

View File

@ -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
* <!-- Invalid: -->
* [foo]
*
* [foo]: http://foo.bar/baz
*
* <!-- Valid: -->
* [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;

View File

@ -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
* <!-- Invalid: -->
* | A | B |
* | ----- | ----- |
* | Alpha | Bravo |
*
* <!-- Valid: -->
* | 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;

48
lib/rules/no-tabs.js Normal file
View File

@ -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
* <!-- Note: the double guillemet (`»`) and middle-dots represent a tab -->
* <!-- Invalid: -->
* Foo»Bar
*
* »···Foo
*
* <!-- Valid: -->
* 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;

View File

@ -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
* <!-- Valid when set to `consistent` or `.` -->
* 1. Foo
*
* 2. Bar
*
* <!-- Valid when set to `consistent` or `)` -->
* 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;

View File

@ -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
* <!-- Valid when set to `one`: -->
* 1. Foo
* 1. Bar
* 1. Baz
*
* 1. Alpha
* 1. Bravo
* 1. Charlie
*
* <!-- Valid when set to `single`: -->
* 1. Foo
* 1. Bar
* 1. Baz
*
* 3. Alpha
* 3. Bravo
* 3. Charlie
*
* <!-- Valid when set to `ordered`: -->
* 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;

97
lib/rules/rule-style.js Normal file
View File

@ -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
* <!-- Valid when set to `consistent` or `* * *`: -->
* * * *
*
* * * *
*
* <!-- Valid when set to `consistent` or `_______`: -->
* _______
*
* _______
*/
'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;

View File

@ -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
* <!-- Valid when set to `consistent` or `*` -->
* **foo**
* **bar**
*
* <!-- Valid when set to `consistent` or `_` -->
* __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;

View File

@ -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
* <!-- Valid when set to `consistent` or `padded` -->
* | A | B |
* | ----- | ----- |
* | Alpha | Bravo |
*
* <!-- Valid when set to `consistent` or `compact` -->
* |A |B |
* |-----|-----|
* |Alpha|Bravo|
*
* <!-- Invalid: -->
* | 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 + ', isnt';
positions.forEach(function (diff, index) {
if (diff !== style && diff !== undefined) {
file.warn(warning, locations[index]);
}
});
});
done();
}
/*
* Expose.
*/
module.exports = tableCellPadding;

View File

@ -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
* <!-- Valid: -->
* | A | B |
* | ----- | ----- |
* | Alpha | Bravo |
*
* <!-- Invalid: -->
* | 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;

75
lib/rules/table-pipes.js Normal file
View File

@ -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
* <!-- Valid: -->
* | A | B |
* | ----- | ----- |
* | Alpha | Bravo |
*
* <!-- Invalid: -->
* 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;

View File

@ -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
* <!-- Valid when set to `consistent` or `-` -->
* - Foo
* - Bar
*
* <!-- Valid when set to `consistent` or `*` -->
* * Foo
* * Bar
*
* <!-- Valid when set to `consistent` or `+` -->
* + Foo
* + Bar
*
* <!-- Never valid: -->
* + 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;

44
lib/sort.js Normal file
View File

@ -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;

View File

@ -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;

33
lib/utilities/plural.js Normal file
View File

@ -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;

65
lib/utilities/position.js Normal file
View File

@ -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;

View File

@ -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;

111
lib/utilities/visit.js Normal file
View File

@ -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;

5
logo.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="200">
<text x="-4" y="128" font-family="helvetica-neue, helvetica neue, helvetica" font-weight="100" font-size="88">
<tspan fill="#000">MDAST-</tspan><tspan fill="#e05d44">LINT</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 268 B

5949
mdast.js Normal file

File diff suppressed because it is too large Load Diff

1
mdast.min.js vendored Normal file

File diff suppressed because one or more lines are too long

63
package.json Normal file
View File

@ -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"
}
}

173
readme.md Normal file
View File

@ -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? Thats 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. Its 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
```
Lets 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`, youre 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
<!--lint disable no-duplicate-headings-->
## Hello
<!--lint enable no-duplicate-headings-->
### 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 werent 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-ins 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. Itll 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)

BIN
screen-shot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -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));

42
test/clean.js Normal file
View File

@ -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 mdasts 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;

View File

View File

@ -0,0 +1,9 @@
> Foo
<!-- -->
> Bar
<!-- -->
> Baz

View File

@ -0,0 +1,9 @@
> Foo
<!-- -->
> Bar
<!-- -->
> Baz

11
test/fixtures/code-style-fenced.md vendored Normal file
View File

@ -0,0 +1,11 @@
Some fenced code block:
```
foo
```
And one with language flag:
```barscript
bar
```

7
test/fixtures/code-style-indented.md vendored Normal file
View File

@ -0,0 +1,7 @@
Some indented code block:
foo
And another:
bar

7
test/fixtures/comments-disable.md vendored Normal file
View File

@ -0,0 +1,7 @@
<!--lint disable maximum-line-length-->
alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november.
<!--lint enable maximum-line-length-->
alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november.

7
test/fixtures/comments-duplicates.md vendored Normal file
View File

@ -0,0 +1,7 @@
<!--lint enable maximum-line-length-->
alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november.
<!--lint enable maximum-line-length-->
alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november.

7
test/fixtures/comments-enable.md vendored Normal file
View File

@ -0,0 +1,7 @@
<!--lint enable maximum-line-length-->
alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november.
<!--lint disable maximum-line-length-->
alpha bravo charlie delta echo foxtrot golf hotel india julliet kilo lima mike november.

1
test/fixtures/comments-inline.md vendored Normal file
View File

@ -0,0 +1 @@
<!--lint enable no-html--> <span>This is HTML</span>.

View File

@ -0,0 +1,5 @@
Intro.
<!--lint foo bar-->
Outro.

View File

@ -0,0 +1,5 @@
Intro.
<!--lint enable bar-->
Outro.

1
test/fixtures/comments-none.md vendored Normal file
View File

@ -0,0 +1 @@
Things should not fail without warnings, nor comments. Alpha bravo charlie delta echo foxtrot.

View File

@ -0,0 +1,3 @@
This document has definitions with improper spacing and casing.
[Invalid]: http://example.com/favicon.ico "Example Domain"

View File

@ -0,0 +1,3 @@
This document has definitions with proper spacing and casing.
[valid]: http://example.com/favicon.ico "Example Domain"

Some files were not shown because too many files have changed in this diff Show More