mirror of
https://github.com/remarkjs/remark-lint.git
synced 2024-09-11 19:57:15 +03:00
Initial commit
This commit is contained in:
commit
721c363001
15
.editorconfig
Normal file
15
.editorconfig
Normal 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
0
.eslintignore
Normal file
9
.eslintrc
Normal file
9
.eslintrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"rules": {
|
||||
"quotes": [2, "single"]
|
||||
}
|
||||
}
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
*.log
|
||||
bower_components/
|
||||
build/
|
||||
components/
|
||||
node_modules/
|
||||
coverage/
|
||||
build.js
|
140
.jscs.json
Normal file
140
.jscs.json
Normal 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
7
.mdastignore
Normal file
@ -0,0 +1,7 @@
|
||||
# `node_modules/` is already ignored.
|
||||
|
||||
# Duo’s components include docs.
|
||||
components
|
||||
|
||||
# Do not process fixtures.
|
||||
test
|
10
.mdastrc
Normal file
10
.mdastrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"plugins": [
|
||||
"./",
|
||||
"github",
|
||||
"toc"
|
||||
],
|
||||
"settings": {
|
||||
"bullet": "*"
|
||||
}
|
||||
}
|
9
.travis.yml
Normal file
9
.travis.yml
Normal 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
21
LICENSE
Normal 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
37
bower.json
Normal 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
21
component.json
Normal 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
104
doc/api.md
Normal 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 mdast’s [warning API](https://github.com/wooorm/mdast/blob/master/doc/mdast.3.md#filewarnreason-position).
|
||||
|
||||
When processing a file, these warnings are available at `file.messages`, and
|
||||
look as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"fatal": false,
|
||||
"reason": "Marker style should be `*`",
|
||||
"file": "~/example.md",
|
||||
"line": 3,
|
||||
"column": 1,
|
||||
"ruleId": "unordered-list-marker-style"
|
||||
}
|
||||
```
|
||||
|
||||
These messages comply to two schema’s. First, they’re valid error objects,
|
||||
so they can be thrown (while still looking :ok_hand:, due to `Error#toString()`
|
||||
magic), secondly the `file` object itself can be passed to ESLint’s formatters:
|
||||
|
||||
```javascript
|
||||
var compact = require('eslint/lib/formatters/compact');
|
||||
var mdast = require('mdast');
|
||||
var lint = require('mdast-lint');
|
||||
|
||||
// For example purposes were processing `example.md` as seen above.
|
||||
mdast().use(lint).process(doc, function (err, res, file) {
|
||||
compact([file]);
|
||||
/**
|
||||
* line 3, col 1, Warning - Invalid ordered list item marker: should be `*`
|
||||
*
|
||||
* 1 problem
|
||||
*/
|
||||
});
|
||||
```
|
||||
|
||||
Each message contains the following properties:
|
||||
|
||||
* `fatal` (`boolean?`) — Always `false`;
|
||||
|
||||
* `reason` (`string`) — Warning message;
|
||||
|
||||
* `line` (`number`) — Starting line (1-based);
|
||||
|
||||
* `column` (`number`) — Starting column of exception (1-based).
|
||||
|
||||
* `file` (`string`) — File path of exception.
|
||||
|
||||
* `ruleId` (`string`) — Name of the rule which triggered this warning,
|
||||
useful when looking for how to turn the darn thing off.
|
45
doc/comparison/markdownlint.md
Normal file
45
doc/comparison/markdownlint.md
Normal 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 | Won’t warn when that’s your preferred, consistent style |
|
||||
| MD028 | no-blockquote-without-caret | |
|
||||
| MD029 | ordered-list-marker-value | markdownlint defaults to `one`, whereas mdast-lint defaults to `ordered` |
|
||||
| MD030 | list-item-indent | |
|
||||
| MD031 | no-missing-blank-lines | |
|
||||
| MD032 | no-missing-blank-lines | |
|
||||
| MD033 | no-html | |
|
||||
| MD034 | no-literal-urls | |
|
||||
| MD035 | rule-style | |
|
||||
| MD036 | emphasis-heading | mdast-lint only warns when the emphasis is followed by a colon, but that might change. |
|
||||
| MD037 | no-inline-padding | |
|
||||
| MD038 | no-inline-padding | |
|
||||
| MD039 | no-inline-padding | |
|
||||
| MD039 | no-inline-padding | |
|
||||
| MD040 | fenced-code-flag | |
|
1142
doc/rules.md
Normal file
1142
doc/rules.md
Normal file
File diff suppressed because it is too large
Load Diff
16
example.js
Normal file
16
example.js
Normal 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));
|
||||
});
|
72
lib/filter.js
Normal file
72
lib/filter.js
Normal 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 ruleId’s by enabled and disabled
|
||||
* ranges.
|
||||
* @todo Externalise into its own repository.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Sort all `file`s messages by line/column. Note that
|
||||
* this works as a plugin, and will also sort warnings
|
||||
* added by other plug-ins before `mdast-lint` was added.
|
||||
*
|
||||
* @param {Node} ast - Root node.
|
||||
* @param {File} file - Virtual file.
|
||||
*/
|
||||
function transformer(ast, file) {
|
||||
if (!file || !file.messages || !file.messages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
file.messages = file.messages.filter(function (message) {
|
||||
var ranges = file.lintRanges[message.ruleId];
|
||||
var index = ranges && ranges.length;
|
||||
var length = -1;
|
||||
var range;
|
||||
|
||||
if (!message.line) {
|
||||
message.line = 1;
|
||||
}
|
||||
|
||||
if (!message.column) {
|
||||
message.column = 1;
|
||||
}
|
||||
|
||||
while (--index > length) {
|
||||
range = ranges[index];
|
||||
|
||||
if (
|
||||
range.position.line < message.line ||
|
||||
(
|
||||
range.position.line === message.line &&
|
||||
range.position.column < message.column
|
||||
)
|
||||
) {
|
||||
return range.state === true;
|
||||
}
|
||||
}
|
||||
|
||||
/* xistanbul ignore next - Just to be safe */
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `transformer`.
|
||||
*
|
||||
* @return {Function} - See `transformer`.
|
||||
*/
|
||||
function attacher() {
|
||||
return transformer;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = attacher;
|
243
lib/index.js
Normal file
243
lib/index.js
Normal 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;
|
108
lib/rules/blockquote-indentation.js
Normal file
108
lib/rules/blockquote-indentation.js
Normal 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;
|
133
lib/rules/code-block-style.js
Normal file
133
lib/rules/code-block-style.js
Normal 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;
|
74
lib/rules/definition-case.js
Normal file
74
lib/rules/definition-case.js
Normal 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;
|
74
lib/rules/definition-spacing.js
Normal file
74
lib/rules/definition-spacing.js
Normal 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;
|
84
lib/rules/emphasis-marker.js
Normal file
84
lib/rules/emphasis-marker.js
Normal 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;
|
105
lib/rules/fenced-code-flag.js
Normal file
105
lib/rules/fenced-code-flag.js
Normal 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;
|
114
lib/rules/fenced-code-marker.js
Normal file
114
lib/rules/fenced-code-marker.js
Normal 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;
|
45
lib/rules/file-extension.js
Normal file
45
lib/rules/file-extension.js
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer. All rights reserved.
|
||||
* @module file-extension
|
||||
* @fileoverview
|
||||
* Warn when the document’s extension differs from the given preferred
|
||||
* extension.
|
||||
*
|
||||
* Does not warn when given documents have no file extensions (such as
|
||||
* `AUTHORS` or `LICENSE`).
|
||||
*
|
||||
* Options: `string`, default: `'md'` — Expected file extension.
|
||||
* @example
|
||||
* Invalid (when `'md'`): readme.mkd, readme.markdown, etc.
|
||||
* Valid (when `'md'`): readme, readme.md
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Check file extensions.
|
||||
*
|
||||
* @param {Node} ast - Root node.
|
||||
* @param {File} file - Virtual file.
|
||||
* @param {string?} [preferred='md'] - Expected file
|
||||
* extension.
|
||||
* @param {Function} done - Callback.
|
||||
*/
|
||||
function fileExtension(ast, file, preferred, done) {
|
||||
var ext = file.extension;
|
||||
|
||||
preferred = typeof preferred === 'string' ? preferred : 'md';
|
||||
|
||||
if (ext !== '' && ext !== preferred) {
|
||||
file.warn('Invalid extension: use `' + preferred + '`');
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = fileExtension;
|
75
lib/rules/final-definition.js
Normal file
75
lib/rules/final-definition.js
Normal 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;
|
38
lib/rules/final-newline.js
Normal file
38
lib/rules/final-newline.js
Normal 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;
|
52
lib/rules/first-heading-level.js
Normal file
52
lib/rules/first-heading-level.js
Normal 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;
|
60
lib/rules/hard-break-spaces.js
Normal file
60
lib/rules/hard-break-spaces.js
Normal 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;
|
63
lib/rules/heading-increment.js
Normal file
63
lib/rules/heading-increment.js
Normal 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;
|
98
lib/rules/heading-style.js
Normal file
98
lib/rules/heading-style.js
Normal 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
68
lib/rules/index.js
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer. All rights reserved.
|
||||
* @module Rules
|
||||
* @fileoverview Map of rule id’s to rules.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'no-auto-link-without-protocol': require('./no-auto-link-without-protocol'),
|
||||
'no-literal-urls': require('./no-literal-urls'),
|
||||
'no-consecutive-blank-lines': require('./no-consecutive-blank-lines'),
|
||||
'no-missing-blank-lines': require('./no-missing-blank-lines'),
|
||||
'blockquote-indentation': require('./blockquote-indentation'),
|
||||
'no-blockquote-without-caret': require('./no-blockquote-without-caret'),
|
||||
'code-block-style': require('./code-block-style'),
|
||||
'definition-case': require('./definition-case'),
|
||||
'definition-spacing': require('./definition-spacing'),
|
||||
'no-emphasis-as-heading': require('./no-emphasis-as-heading'),
|
||||
'emphasis-marker': require('./emphasis-marker'),
|
||||
'fenced-code-flag': require('./fenced-code-flag'),
|
||||
'fenced-code-marker': require('./fenced-code-marker'),
|
||||
'file-extension': require('./file-extension'),
|
||||
'final-newline': require('./final-newline'),
|
||||
'no-file-name-articles': require('./no-file-name-articles'),
|
||||
'no-file-name-consecutive-dashes': require('./no-file-name-consecutive-dashes'),
|
||||
'no-file-name-irregular-characters': require('./no-file-name-irregular-characters'),
|
||||
'no-file-name-mixed-case': require('./no-file-name-mixed-case'),
|
||||
'no-file-name-outer-dashes': require('./no-file-name-outer-dashes'),
|
||||
'final-definition': require('./final-definition'),
|
||||
'hard-break-spaces': require('./hard-break-spaces'),
|
||||
'heading-increment': require('./heading-increment'),
|
||||
'no-heading-content-indent': require('./no-heading-content-indent'),
|
||||
'no-heading-indent': require('./no-heading-indent'),
|
||||
'first-heading-level': require('./first-heading-level'),
|
||||
'maximum-heading-length': require('./maximum-heading-length'),
|
||||
'no-heading-punctuation': require('./no-heading-punctuation'),
|
||||
'heading-style': require('./heading-style'),
|
||||
'no-multiple-toplevel-headings': require('./no-multiple-toplevel-headings'),
|
||||
'no-duplicate-headings': require('./no-duplicate-headings'),
|
||||
'no-duplicate-definitions': require('./no-duplicate-definitions'),
|
||||
'no-html': require('./no-html'),
|
||||
'no-inline-padding': require('./no-inline-padding'),
|
||||
'maximum-line-length': require('./maximum-line-length'),
|
||||
'link-title-style': require('./link-title-style'),
|
||||
'list-item-bullet-indent': require('./list-item-bullet-indent'),
|
||||
'list-item-content-indent': require('./list-item-content-indent'),
|
||||
'list-item-indent': require('./list-item-indent'),
|
||||
'list-item-spacing': require('./list-item-spacing'),
|
||||
'ordered-list-marker-style': require('./ordered-list-marker-style'),
|
||||
'ordered-list-marker-value': require('./ordered-list-marker-value'),
|
||||
'no-shortcut-reference-image': require('./no-shortcut-reference-image'),
|
||||
'no-shortcut-reference-link': require('./no-shortcut-reference-link'),
|
||||
'rule-style': require('./rule-style'),
|
||||
'no-shell-dollars': require('./no-shell-dollars'),
|
||||
'strong-marker': require('./strong-marker'),
|
||||
'no-table-indentation': require('./no-table-indentation'),
|
||||
'table-pipe-alignment': require('./table-pipe-alignment'),
|
||||
'table-cell-padding': require('./table-cell-padding'),
|
||||
'table-pipes': require('./table-pipes'),
|
||||
'no-tabs': require('./no-tabs'),
|
||||
'unordered-list-marker-style': require('./unordered-list-marker-style')
|
||||
};
|
139
lib/rules/link-title-style.js
Normal file
139
lib/rules/link-title-style.js
Normal 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;
|
77
lib/rules/list-item-bullet-indent.js
Normal file
77
lib/rules/list-item-bullet-indent.js
Normal 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;
|
112
lib/rules/list-item-content-indent.js
Normal file
112
lib/rules/list-item-content-indent.js
Normal 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(
|
||||
'Don’t use mixed indentation for children, ' + word +
|
||||
' ' + diff + ' ' + plural('space', diff),
|
||||
{
|
||||
'line': start(item).line,
|
||||
'column': column
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = listItemContentIndent;
|
148
lib/rules/list-item-indent.js
Normal file
148
lib/rules/list-item-indent.js
Normal 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 item’s 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 item’s bullet and
|
||||
* its content violates a given style.
|
||||
*
|
||||
* @param {Node} ast - Root node.
|
||||
* @param {File} file - Virtual file.
|
||||
* @param {string?} [preferred='tab-size'] - Either
|
||||
* `'tab-size'`, `'space'`, or `'mixed'`, defaulting
|
||||
* to the first.
|
||||
* @param {Function} done - Callback.
|
||||
*/
|
||||
function listItemIndent(ast, file, preferred, done) {
|
||||
var contents = file.toString();
|
||||
|
||||
preferred = typeof preferred !== 'string' ? 'tab-size' : preferred;
|
||||
|
||||
if (STYLES[preferred] !== true) {
|
||||
file.fail('Invalid list-item indent style `' + preferred + '`: use either `\'tab-size\'`, `\'space\'`, or `\'mixed\'`');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
visit(ast, 'list', function (node) {
|
||||
var items = node.children;
|
||||
var isOrdered = node.ordered;
|
||||
var offset = node.start || 1;
|
||||
|
||||
if (position.isGenerated(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(function (item, index) {
|
||||
var head = item.children[0];
|
||||
var bulletSize = isOrdered ? String(offset + index).length + 1 : 1;
|
||||
var tab = Math.ceil(bulletSize / 4) * 4;
|
||||
var initial = start(item).offset;
|
||||
var final = start(head).offset;
|
||||
var marker;
|
||||
var shouldBe;
|
||||
var diff;
|
||||
var word;
|
||||
|
||||
marker = contents.slice(initial, final);
|
||||
|
||||
/*
|
||||
* Support checkboxes.
|
||||
*/
|
||||
|
||||
marker = marker.replace(/\[[x ]?\]\s*$/i, '');
|
||||
|
||||
if (preferred === 'tab-size') {
|
||||
shouldBe = tab;
|
||||
} else if (preferred === 'space') {
|
||||
shouldBe = bulletSize + 1;
|
||||
} else {
|
||||
shouldBe = node.loose ? tab : bulletSize + 1;
|
||||
}
|
||||
|
||||
if (marker.length !== shouldBe) {
|
||||
diff = shouldBe - marker.length;
|
||||
word = diff > 0 ? 'add' : 'remove';
|
||||
|
||||
diff = Math.abs(diff);
|
||||
|
||||
file.warn(
|
||||
'Incorrect list-item indent: ' + word +
|
||||
' ' + diff + ' ' + plural('space', diff),
|
||||
start(head)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = listItemIndent;
|
118
lib/rules/list-item-spacing.js
Normal file
118
lib/rules/list-item-spacing.js
Normal 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 + ', isn’t', {
|
||||
'start': end(item),
|
||||
'end': start(next)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = listItemSpacing;
|
59
lib/rules/maximum-heading-length.js
Normal file
59
lib/rules/maximum-heading-length.js
Normal 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;
|
178
lib/rules/maximum-line-length.js
Normal file
178
lib/rules/maximum-line-length.js
Normal 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
|
||||
* there’s white-space after it, they are not
|
||||
* whitelisted.
|
||||
*/
|
||||
|
||||
visit(ast, 'link', function (node, pos, parent) {
|
||||
var next = parent.children[pos + 1];
|
||||
var initial = start(node);
|
||||
var final = end(node);
|
||||
|
||||
/*
|
||||
* Nothing to whitelist when generated.
|
||||
*/
|
||||
|
||||
if (position.isGenerated(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* No whitelisting when starting after the border,
|
||||
* or ending before it.
|
||||
*/
|
||||
|
||||
if (initial.column > style || final.column < style) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* No whitelisting when there’s white-space after
|
||||
* the link.
|
||||
*/
|
||||
|
||||
if (
|
||||
next &&
|
||||
start(next).line === initial.line &&
|
||||
(!next.value || /^(.+?[ \t].+?)/.test(next.value))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
whitelist(initial.line - 1, final.line);
|
||||
});
|
||||
|
||||
/*
|
||||
* Iterate over every line, and warn for
|
||||
* violating lines.
|
||||
*/
|
||||
|
||||
while (++index < length) {
|
||||
lineLength = matrix[index].length;
|
||||
|
||||
if (lineLength > style) {
|
||||
file.warn('Line must be at most ' + style + ' characters', {
|
||||
'line': index + 1,
|
||||
'column': lineLength + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = maximumLineLength;
|
84
lib/rules/no-auto-link-without-protocol.js
Normal file
84
lib/rules/no-auto-link-without-protocol.js
Normal 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;
|
84
lib/rules/no-blockquote-without-caret.js
Normal file
84
lib/rules/no-blockquote-without-caret.js
Normal 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;
|
124
lib/rules/no-consecutive-blank-lines.js
Normal file
124
lib/rules/no-consecutive-blank-lines.js
Normal 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;
|
75
lib/rules/no-duplicate-definitions.js
Normal file
75
lib/rules/no-duplicate-definitions.js
Normal 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;
|
73
lib/rules/no-duplicate-headings.js
Normal file
73
lib/rules/no-duplicate-headings.js
Normal 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;
|
81
lib/rules/no-emphasis-as-heading.js
Normal file
81
lib/rules/no-emphasis-as-heading.js
Normal 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('Don’t use emphasis to introduce a section, use a heading', node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = noEmphasisAsHeading;
|
36
lib/rules/no-file-name-articles.js
Normal file
36
lib/rules/no-file-name-articles.js
Normal 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;
|
34
lib/rules/no-file-name-consecutive-dashes.js
Normal file
34
lib/rules/no-file-name-consecutive-dashes.js
Normal 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;
|
38
lib/rules/no-file-name-irregular-characters.js
Normal file
38
lib/rules/no-file-name-irregular-characters.js
Normal 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;
|
38
lib/rules/no-file-name-mixed-case.js
Normal file
38
lib/rules/no-file-name-mixed-case.js
Normal 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;
|
34
lib/rules/no-file-name-outer-dashes.js
Normal file
34
lib/rules/no-file-name-outer-dashes.js
Normal 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;
|
119
lib/rules/no-heading-content-indent.js
Normal file
119
lib/rules/no-heading-content-indent.js
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer. All rights reserved.
|
||||
* @module no-heading-content-indent
|
||||
* @fileoverview
|
||||
* Warn when a heading’s content is indented.
|
||||
* @example
|
||||
* <!-- 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 heading’s content',
|
||||
start(children[0])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Closed ATX-heading always must have a space
|
||||
* between their content and the final hashes,
|
||||
* thus, there is no `add x spaces`.
|
||||
*/
|
||||
|
||||
if (type === 'atx-closed') {
|
||||
final = end(children[children.length - 1]);
|
||||
diff = end(node).column - final.column - 1 - depth;
|
||||
|
||||
if (diff) {
|
||||
file.warn(
|
||||
'Remove ' + diff + ' ' + plural('space', diff) +
|
||||
' after this heading’s content',
|
||||
final
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = noHeadingContentIndent;
|
101
lib/rules/no-heading-indent.js
Normal file
101
lib/rules/no-heading-indent.js
Normal 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;
|
71
lib/rules/no-heading-punctuation.js
Normal file
71
lib/rules/no-heading-punctuation.js
Normal 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('Don’t add a trailing `' + value + '` to headings', node);
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = noHeadingPunctuation;
|
45
lib/rules/no-html.js
Normal file
45
lib/rules/no-html.js
Normal 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 doesn’t 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;
|
68
lib/rules/no-inline-padding.js
Normal file
68
lib/rules/no-inline-padding.js
Normal 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('Don’t pad `' + type + '` with inner spaces', node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = noInlinePadding;
|
62
lib/rules/no-literal-urls.js
Normal file
62
lib/rules/no-literal-urls.js
Normal 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('Don’t use literal URLs without angle brackets', node);
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = noLiteralURLs;
|
81
lib/rules/no-missing-blank-lines.js
Normal file
81
lib/rules/no-missing-blank-lines.js
Normal 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;
|
60
lib/rules/no-multiple-toplevel-headings.js
Normal file
60
lib/rules/no-multiple-toplevel-headings.js
Normal 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('Don’t use multiple top level headings (' + pos.line + ':' + pos.column + ')', node);
|
||||
}
|
||||
|
||||
topLevelheading = node;
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
module.exports = noMultipleToplevelHeadings;
|
101
lib/rules/no-shell-dollars.js
Normal file
101
lib/rules/no-shell-dollars.js
Normal 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;
|
54
lib/rules/no-shortcut-reference-image.js
Normal file
54
lib/rules/no-shortcut-reference-image.js
Normal 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;
|
54
lib/rules/no-shortcut-reference-link.js
Normal file
54
lib/rules/no-shortcut-reference-link.js
Normal 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;
|
60
lib/rules/no-table-indentation.js
Normal file
60
lib/rules/no-table-indentation.js
Normal 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
48
lib/rules/no-tabs.js
Normal 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;
|
116
lib/rules/ordered-list-marker-style.js
Normal file
116
lib/rules/ordered-list-marker-style.js
Normal 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;
|
156
lib/rules/ordered-list-marker-value.js
Normal file
156
lib/rules/ordered-list-marker-value.js
Normal 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
97
lib/rules/rule-style.js
Normal 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;
|
82
lib/rules/strong-marker.js
Normal file
82
lib/rules/strong-marker.js
Normal 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;
|
176
lib/rules/table-cell-padding.js
Normal file
176
lib/rules/table-cell-padding.js
Normal 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 + ', isn’t';
|
||||
|
||||
positions.forEach(function (diff, index) {
|
||||
if (diff !== style && diff !== undefined) {
|
||||
file.warn(warning, locations[index]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = tableCellPadding;
|
100
lib/rules/table-pipe-alignment.js
Normal file
100
lib/rules/table-pipe-alignment.js
Normal 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
75
lib/rules/table-pipes.js
Normal 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;
|
121
lib/rules/unordered-list-marker-style.js
Normal file
121
lib/rules/unordered-list-marker-style.js
Normal 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
44
lib/sort.js
Normal 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;
|
79
lib/utilities/heading-style.js
Normal file
79
lib/utilities/heading-style.js
Normal 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
33
lib/utilities/plural.js
Normal 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
65
lib/utilities/position.js
Normal 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;
|
42
lib/utilities/to-string.js
Normal file
42
lib/utilities/to-string.js
Normal 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
111
lib/utilities/visit.js
Normal 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
5
logo.svg
Normal 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 |
1
mdast.min.js
vendored
Normal file
1
mdast.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
63
package.json
Normal file
63
package.json
Normal 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
173
readme.md
Normal 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? That’s up to you,
|
||||
but the defaults are sensible :ok_hand:.
|
||||
|
||||
**mdast-lint** has lots of tests. Supports Node, io.js, and the browser.
|
||||
100% coverage. 40+ rules. It’s build on [**mdast**](https://github.com/wooorm/mdast),
|
||||
a powerful markdown processor powered by [plugins](https://github.com/wooorm/mdast/blob/master/doc/plugins.md)
|
||||
(such as this one).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Command line](#command-line)
|
||||
* [Programmatic](#programmatic)
|
||||
* [Rules](#rules)
|
||||
* [Configuring mdast-lint](#configuring-mdast-lint)
|
||||
* [Combining with other plug-ins](#combining-with-other-plug-ins)
|
||||
* [Using mdast to fix your markdown](#using-mdast-to-fix-your-markdown)
|
||||
* [License](#license)
|
||||
|
||||
## Installation
|
||||
|
||||
[npm](https://docs.npmjs.com/cli/install):
|
||||
|
||||
```bash
|
||||
npm install mdast-lint
|
||||
```
|
||||
|
||||
**mdast-lint** is also available for bower, component, duo, and for AMD,
|
||||
CommonJS, and globals.
|
||||
|
||||
## Command line
|
||||
|
||||
![](http://i60.tinypic.com/125gtn9.png "Example how mdast-lint looks on screen")
|
||||
|
||||
Use mdast-lint together with mdast:
|
||||
|
||||
```bash
|
||||
npm install --global mdast mdast-lint
|
||||
```
|
||||
|
||||
Let’s say `example.md` looks as follows:
|
||||
|
||||
```md
|
||||
* Hello
|
||||
|
||||
- World
|
||||
```
|
||||
|
||||
Then, to run **mdast-lint** on `example.md`:
|
||||
|
||||
```bash
|
||||
mdast -u mdast-lint example.md
|
||||
#
|
||||
# Yields:
|
||||
#
|
||||
# example.md
|
||||
# 1:3 warning Incorrect list-item content indent: add 2 spaces list-item-indent
|
||||
# 3:1 warning Invalid ordered list item marker: should be `*` unordered-list-marker-style
|
||||
#
|
||||
# ✖ 2 problems (0 errors, 2 warnings)
|
||||
#
|
||||
# * Hello
|
||||
#
|
||||
#
|
||||
# * World
|
||||
#
|
||||
```
|
||||
|
||||
See [doc/rules.md](doc/rules.md) to see what those warnings are, and how to
|
||||
turn them off.
|
||||
|
||||
## Programmatic
|
||||
|
||||
[doc/api.md](doc/api.md) describes how to use **mdast-lint**’s
|
||||
programatic interface in JavaScript.
|
||||
|
||||
## Rules
|
||||
|
||||
[doc/rules.md](doc/rules.md) describes all available rules, what they check
|
||||
for, examples of markdown they warn for, and how to fix their warnings.
|
||||
|
||||
## Configuring mdast-lint
|
||||
|
||||
**mdast-lint** is just an **mdast** plug-in. Meaning, you can opt to
|
||||
configure using configuration files. Read more about these files
|
||||
(`.mdastrc` or `package.json`) in [**mdast**’s docs](https://github.com/wooorm/mdast/blob/master/doc/mdastrc.5.md).
|
||||
|
||||
An example `.mdastrc` file could look as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"lint": {
|
||||
"no-multiple-toplevel-headings": false,
|
||||
"maximum-line-length": 79,
|
||||
"emphasis-marker": "_",
|
||||
"strong-marker": "*"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"commonmark": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where the object at `plugins.lint` is a map of `ruleId`s and their values.
|
||||
The object at `settings` determines how **mdast** parses (and compiles)
|
||||
markdown code. Read more about the latter on [**mdast**’s readme](https://github.com/wooorm/mdast#mdastprocessvalue-options-done).
|
||||
|
||||
In addition, you can also provide configuration comments to turn a rule
|
||||
on or off inside a file (note that you cannot change what a setting, such as
|
||||
`maximum-line-length`, you’re either enabling or disabling warnings).
|
||||
|
||||
The following file will warn twice for the duplicate headings:
|
||||
|
||||
```markdown
|
||||
# Hello
|
||||
|
||||
## Hello
|
||||
|
||||
### Hello
|
||||
```
|
||||
|
||||
The following file will warn once (the second heading is ignored,
|
||||
but the third is re-enabled):
|
||||
|
||||
```markdown
|
||||
# Hello
|
||||
|
||||
<!--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 weren’t available: thus, they have no positional information,
|
||||
and **mdast-lint** cannot warn you about them.
|
||||
|
||||
Therefore, you need to do two things:
|
||||
|
||||
* Process the files twice (this is similar to how LaTeX works);
|
||||
* Ensure **mdast-lint** runs before the plug-in’s which generate content.
|
||||
|
||||
## Using mdast to fix your markdown
|
||||
|
||||
One of **mdast**’s cool parts is that it compiles to very clean, and highly
|
||||
cross-vendor supported markdown. It’ll ensure list items use a single bullet,
|
||||
emphasis and strong use a standard marker, and that your table fences are
|
||||
aligned.
|
||||
|
||||
**mdast** should be able to fix most of your styling issues automatically,
|
||||
and I strongly suggest checking out how it can make your life easier :+1:
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE) © [Titus Wormer](http://wooorm.com)
|
BIN
screen-shot.png
Normal file
BIN
screen-shot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
166
script/build-rule-documentation.js
Executable file
166
script/build-rule-documentation.js
Executable 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
42
test/clean.js
Normal 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 mdast’s syntax tree.
|
||||
* @todo Externalise into its own repository.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* Dpendencies.
|
||||
*/
|
||||
|
||||
var visit = require('../lib/utilities/visit');
|
||||
|
||||
/**
|
||||
* Delete the `position` key for each node.
|
||||
*
|
||||
* @param {Node} ast - Root node.
|
||||
*/
|
||||
function transformer(ast) {
|
||||
visit(ast, function (node) {
|
||||
node.position = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `transformer`.
|
||||
*
|
||||
* @return {Function} - See `transformer`.
|
||||
*/
|
||||
function attacher() {
|
||||
return transformer;
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose.
|
||||
*/
|
||||
|
||||
module.exports = attacher;
|
0
test/fixtures/-file-name-initial-dash.md
vendored
Normal file
0
test/fixtures/-file-name-initial-dash.md
vendored
Normal file
9
test/fixtures/blockquote-indentation-2.md
vendored
Normal file
9
test/fixtures/blockquote-indentation-2.md
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
> Foo
|
||||
|
||||
<!-- -->
|
||||
|
||||
> Bar
|
||||
|
||||
<!-- -->
|
||||
|
||||
> Baz
|
9
test/fixtures/blockquote-indentation-4.md
vendored
Normal file
9
test/fixtures/blockquote-indentation-4.md
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
> Foo
|
||||
|
||||
<!-- -->
|
||||
|
||||
> Bar
|
||||
|
||||
<!-- -->
|
||||
|
||||
> Baz
|
11
test/fixtures/code-style-fenced.md
vendored
Normal file
11
test/fixtures/code-style-fenced.md
vendored
Normal 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
7
test/fixtures/code-style-indented.md
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
Some indented code block:
|
||||
|
||||
foo
|
||||
|
||||
And another:
|
||||
|
||||
bar
|
7
test/fixtures/comments-disable.md
vendored
Normal file
7
test/fixtures/comments-disable.md
vendored
Normal 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
7
test/fixtures/comments-duplicates.md
vendored
Normal 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
7
test/fixtures/comments-enable.md
vendored
Normal 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
1
test/fixtures/comments-inline.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!--lint enable no-html--> <span>This is HTML</span>.
|
5
test/fixtures/comments-invalid-keyword.md
vendored
Normal file
5
test/fixtures/comments-invalid-keyword.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
Intro.
|
||||
|
||||
<!--lint foo bar-->
|
||||
|
||||
Outro.
|
5
test/fixtures/comments-invalid-rule-id.md
vendored
Normal file
5
test/fixtures/comments-invalid-rule-id.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
Intro.
|
||||
|
||||
<!--lint enable bar-->
|
||||
|
||||
Outro.
|
1
test/fixtures/comments-none.md
vendored
Normal file
1
test/fixtures/comments-none.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
Things should not fail without warnings, nor comments. Alpha bravo charlie delta echo foxtrot.
|
3
test/fixtures/definition-case-invalid.md
vendored
Normal file
3
test/fixtures/definition-case-invalid.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
This document has definitions with improper spacing and casing.
|
||||
|
||||
[Invalid]: http://example.com/favicon.ico "Example Domain"
|
3
test/fixtures/definition-case-valid.md
vendored
Normal file
3
test/fixtures/definition-case-valid.md
vendored
Normal 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
Loading…
Reference in New Issue
Block a user