remark-lint
Advanced tools
Comparing version 4.0.2 to 4.1.0
@@ -5,2 +5,11 @@ <!--remark setext--> | ||
4.1.0 / 2016-08-02 | ||
================== | ||
* Add better external rule support, and refactor module ([`8dedd19`](https://github.com/wooorm/remark-lint/commit/8dedd19)) | ||
* Refactor code-style to use `xo` ([`60907ad`](https://github.com/wooorm/remark-lint/commit/60907ad)) | ||
* Add support for rules as any trough ware ([`d272ff2`](https://github.com/wooorm/remark-lint/commit/d272ff2)) | ||
* Add `load-plugin` to load external rules ([`31f82b2`](https://github.com/wooorm/remark-lint/commit/31f82b2)) | ||
* Update dependencies ([`5a70271`](https://github.com/wooorm/remark-lint/commit/5a70271)) | ||
4.0.2 / 2016-07-06 | ||
@@ -128,66 +137,1 @@ ================== | ||
================== | ||
* Update dependencies ([`4936d35`](https://github.com/wooorm/remark-lint/commit/4936d35)) | ||
0.4.5 / 2015-08-08 | ||
================== | ||
* Fix typo in definition-case warning ([`490ac16`](https://github.com/wooorm/remark-lint/commit/490ac16)) | ||
0.4.4 / 2015-08-05 | ||
================== | ||
* Remove maximum-line-length warnings on definitions ([`1119ca8`](https://github.com/wooorm/remark-lint/commit/1119ca8)) | ||
* Add `linter-markdown` to `readme.md` ([`7de8847`](https://github.com/wooorm/remark-lint/commit/7de8847)) | ||
0.4.3 / 2015-08-04 | ||
================== | ||
* Fix block-quotes without children ([`a9aaff7`](https://github.com/wooorm/remark-lint/commit/a9aaff7)) | ||
* Update mdast dev-dependency ([`34df79c`](https://github.com/wooorm/remark-lint/commit/34df79c)) | ||
* Add vfile as a dev-dependency ([`84d1ce3`](https://github.com/wooorm/remark-lint/commit/84d1ce3)) | ||
0.4.2 / 2015-07-12 | ||
================== | ||
* Remove peer-dependencies ([`8d70fcf`](https://github.com/wooorm/remark-lint/commit/8d70fcf)) | ||
0.4.1 / 2015-07-05 | ||
================== | ||
* Remove component support ([`58d7e6b`](https://github.com/wooorm/remark-lint/commit/58d7e6b)) | ||
* Refactor to externalise `lib/utilities/` ([`eb78529`](https://github.com/wooorm/remark-lint/commit/eb78529)) | ||
0.4.0 / 2015-06-29 | ||
================== | ||
* Add gap support ([`136e760`](https://github.com/wooorm/remark-lint/commit/136e760)) | ||
* Update mdast ([`2d122e4`](https://github.com/wooorm/remark-lint/commit/2d122e4)) | ||
0.3.0 / 2015-06-20 | ||
================== | ||
* Add checkbox-content-indent rule ([`7b55519`](https://github.com/wooorm/remark-lint/commit/7b55519)) | ||
* Fix dot-files from being read as fixtures ([`ecbec2c`](https://github.com/wooorm/remark-lint/commit/ecbec2c)) | ||
* Add checkbox-character-style rule ([`7ed4579`](https://github.com/wooorm/remark-lint/commit/7ed4579)) | ||
* Fix tests for invalid position given my mdast-range ([`55d1128`](https://github.com/wooorm/remark-lint/commit/55d1128)) | ||
* Add missing jsdoc comment ([`63b83b9`](https://github.com/wooorm/remark-lint/commit/63b83b9)) | ||
* Update eslint ([`a3b0829`](https://github.com/wooorm/remark-lint/commit/a3b0829)) | ||
* Update mdast, mdast-yaml-config ([`52bac04`](https://github.com/wooorm/remark-lint/commit/52bac04)) | ||
0.2.0 / 2015-06-13 | ||
================== | ||
* Remove mdast-usage, add mdast-yaml-config as dependencies ([`053674f`](https://github.com/wooorm/remark-lint/commit/053674f)) | ||
* Add images to blacklist for maximum-line-length ([`ba6d270`](https://github.com/wooorm/remark-lint/commit/ba6d270)) | ||
* Refactor to use rawgit references to images in `readme.md` ([`3f6344c`](https://github.com/wooorm/remark-lint/commit/3f6344c)) | ||
* Add support for external rules ([`5162a09`](https://github.com/wooorm/remark-lint/commit/5162a09)) | ||
* Refactor additional, fileless, rules support ([`6d2ba65`](https://github.com/wooorm/remark-lint/commit/6d2ba65)) | ||
* Adds support for automatic doc generation for file-less rules ([`29965a3`](https://github.com/wooorm/remark-lint/commit/29965a3)) | ||
* Add `reset` docs to rule generation script ([`77b8bfd`](https://github.com/wooorm/remark-lint/commit/77b8bfd)) | ||
* Adds `reset` rule to docs ([`90a5f8a`](https://github.com/wooorm/remark-lint/commit/90a5f8a)) | ||
* Update wording re rules in `readme.md` ([`00d9ba4`](https://github.com/wooorm/remark-lint/commit/00d9ba4)) | ||
* Update rule count in `readme.md` ([`f937cf4`](https://github.com/wooorm/remark-lint/commit/f937cf4)) | ||
0.1.0 / 2015-06-11 | ||
================== |
365
lib/index.js
@@ -14,162 +14,94 @@ /** | ||
/* | ||
* Constants. | ||
*/ | ||
var SOURCE = 'remark-lint'; | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var decamelize = require('decamelize'); | ||
var sort = require('vfile-sort'); | ||
var control = require('remark-message-control'); | ||
var loadPlugin = require('load-plugin'); | ||
var trough = require('trough'); | ||
var wrapped = require('wrapped'); | ||
var internals = require('./rules'); | ||
var npmPrefix = require('npm-prefix')(); | ||
/* | ||
* Needed for plug-in resolving. | ||
*/ | ||
/* Expose. */ | ||
module.exports = lint; | ||
var path = require('path'); | ||
var fs = require('fs'); | ||
var exists = fs && fs.existsSync; | ||
var resolve = path && path.resolve; | ||
var isWindows; | ||
var isElectron; | ||
var isGlobal; | ||
var globals; | ||
var cwd; | ||
/* Constants. */ | ||
var SOURCE = 'remark-lint'; | ||
var MODULES = 'node_modules'; | ||
/* istanbul ignore else */ | ||
if (typeof global !== 'undefined') { | ||
/* global global */ | ||
cwd = global.process.cwd(); | ||
/* Detect whether we’re running as a globally installed package. */ | ||
isWindows = global.process.platform === 'win32'; | ||
isElectron = global.process.versions.electron !== undefined; | ||
isGlobal = isElectron || global.process.argv[1].indexOf(npmPrefix) === 0; | ||
/* istanbul ignore next */ | ||
globals = resolve(npmPrefix, isWindows ? '' : 'lib', MODULES); | ||
} | ||
/** | ||
* Factory to create a plugin from a rule. | ||
* 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 | ||
* attachFactory('foo', console.log, false)() // null | ||
* attachFactory('foo', console.log, {})() // plugin | ||
* var processor = lint(remark, { | ||
* html: false // Ignore HTML warnings. | ||
* }); | ||
* | ||
* @param {string} id - Identifier. | ||
* @param {Function} rule - Rule | ||
* @param {*} options - Options for respective rule. | ||
* @return {Function} - See `attach` below. | ||
* @param {Remark} remark - Host object. | ||
* @param {Object?} options - Hash of rule names mapping to | ||
* rule options. | ||
*/ | ||
function attachFactory(id, rule, options) { | ||
/** | ||
* Attach the rule to a remark instance, unless `false` | ||
* is passed as an option. | ||
* | ||
* @return {Function?} - See `plugin` below. | ||
*/ | ||
function attach() { | ||
/** | ||
* Attach the rule to a remark 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) { | ||
var scope = file.namespace('remark-lint'); | ||
function lint(remark, options) { | ||
var settings = decamelizeSettings(options || {}); | ||
var rules = loadExternals(settings.external); | ||
var reset = options && options.reset; | ||
var enable = []; | ||
var disable = []; | ||
var known = []; | ||
var pipeline = trough(); | ||
var setting; | ||
var id; | ||
/* | ||
* Track new messages per file. | ||
*/ | ||
/* Add each rule. */ | ||
for (id in rules) { | ||
setting = settings[id]; | ||
if (scope.index === undefined || scope.index === null) { | ||
scope.index = file.messages.length; | ||
} | ||
known.push(id); | ||
/** | ||
* Add `ruleId` to each new message. | ||
* | ||
* @param {Error?} err - Optional failure. | ||
*/ | ||
function done(err) { | ||
var messages = file.messages; | ||
if (setting != null) { | ||
/* Pass turned on rules `undefined`. */ | ||
if (reset && setting === true) { | ||
setting = undefined; | ||
} | ||
while (scope.index < messages.length) { | ||
messages[scope.index].ruleId = id; | ||
messages[scope.index].source = SOURCE; | ||
scope.index++; | ||
} | ||
next(err); | ||
} | ||
/* | ||
* Invoke `rule`, with `options` | ||
*/ | ||
rule(ast, file, options, done); | ||
} | ||
return plugin; | ||
if (setting === false) { | ||
setting = undefined; | ||
disable.push(id); | ||
} else { | ||
enable.push(id); | ||
} | ||
} | ||
return attach; | ||
} | ||
pipeline.use(ruleFactory(id, rules[id], setting)); | ||
} | ||
/** | ||
* Require an external. Checks, in this order: | ||
* | ||
* - `$cwd/$pathlike`; | ||
* - `$cwd/$pathlike.js`; | ||
* - `$cwd/node_modules/$pathlike`; | ||
* - `$pathlike`. | ||
* | ||
* Where `$cwd` is the current working directory. | ||
* | ||
* When using a globally installed executable, the | ||
* following are also included: | ||
* | ||
* - `$globals/$pathlike`. | ||
* | ||
* Where `$globals` is the directory of globally installed | ||
* npm packages. | ||
* | ||
* @example | ||
* var plugin = findPlugin('foo'); | ||
* | ||
* @throws {Error} - Fails when `pathlike` cannot be | ||
* resolved. | ||
* @param {string} pathlike - Reference to external. | ||
* @return {Object} - Result of `require`ing external. | ||
*/ | ||
function loadExternal(pathlike) { | ||
var local = resolve(cwd, pathlike); | ||
var current = resolve(cwd, 'node_modules', pathlike); | ||
var globalPath = resolve(globals, pathlike); | ||
var plugin; | ||
/* Run all rules. */ | ||
remark.use(function () { | ||
return function (node, file, next) { | ||
pipeline.run(node, file, next); | ||
}; | ||
}); | ||
if (exists(local) || exists(local + '.js')) { | ||
plugin = local; | ||
/* istanbul ignore else - for globals */ | ||
} else if (exists(current)) { | ||
plugin = current; | ||
} else if (isGlobal && exists(globalPath)) { | ||
plugin = globalPath; | ||
} else { | ||
plugin = pathlike; | ||
} | ||
/* Allow comments to toggle messages. */ | ||
remark.use(control, { | ||
name: 'lint', | ||
source: SOURCE, | ||
reset: reset, | ||
known: known, | ||
enable: enable, | ||
disable: disable | ||
}); | ||
return require(plugin); | ||
/** | ||
* Transformer to sort messages. | ||
* | ||
* @param {Node} node - Syntax tree. | ||
* @param {VFile} file - Virtual file. | ||
*/ | ||
return function (node, file) { | ||
sort(file); | ||
}; | ||
} | ||
@@ -191,126 +123,81 @@ | ||
function loadExternals(externals) { | ||
var index = -1; | ||
var rules = {}; | ||
var external; | ||
var ruleId; | ||
var mapping = externals ? externals.concat() : []; | ||
var length; | ||
var index = -1; | ||
var rules = {}; | ||
var external; | ||
var ruleId; | ||
var mapping = externals ? externals.concat() : []; | ||
var length; | ||
mapping.push(internals); | ||
length = mapping.length; | ||
mapping.push(internals); | ||
length = mapping.length; | ||
while (++index < length) { | ||
external = mapping[index]; | ||
while (++index < length) { | ||
external = mapping[index]; | ||
if (typeof external === 'string') { | ||
external = loadExternal(external); | ||
} | ||
for (ruleId in external) { | ||
rules[ruleId] = external[ruleId]; | ||
} | ||
if (typeof external === 'string') { | ||
external = loadPlugin(external, { | ||
prefix: 'remark-lint-' | ||
}); | ||
} | ||
return rules; | ||
} | ||
/** | ||
* Helper to ensure ruleId’s are dash-cased instead of | ||
* camel-cased. | ||
* | ||
* @param {Object} source - Original settings. | ||
* @return {Object} - Dash-cased settings. | ||
*/ | ||
function decamelizeSettings(source) { | ||
var result = {}; | ||
var key; | ||
for (key in source) { | ||
result[decamelize(key, '-')] = source[key]; | ||
for (ruleId in external) { | ||
rules[ruleId] = external[ruleId]; | ||
} | ||
} | ||
return result; | ||
return rules; | ||
} | ||
/** | ||
* Lint attacher. | ||
* Factory to create a plugin from a rule. | ||
* | ||
* 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(remark, { | ||
* 'html': false // Ignore HTML warnings. | ||
* }); | ||
* attachFactory('foo', console.log, false)() // null | ||
* attachFactory('foo', console.log, {})() // plugin | ||
* | ||
* @param {Remark} remark - Host object. | ||
* @param {Object?} options - Hash of rule names mapping to | ||
* rule options. | ||
* @param {string} id - Identifier. | ||
* @param {Function} rule - Rule | ||
* @param {*} options - Options for respective rule. | ||
* @return {Function} - Trough ware. | ||
*/ | ||
function lint(remark, options) { | ||
var settings = decamelizeSettings(options || {}); | ||
var rules = loadExternals(settings.external); | ||
var reset = options && options.reset; | ||
var enable = []; | ||
var disable = []; | ||
var known = []; | ||
var setting; | ||
var id; | ||
function ruleFactory(id, rule, options) { | ||
var fn = wrapped(rule); | ||
/* | ||
* Add each rule as a seperate plugin. | ||
*/ | ||
return function (ast, file, next) { | ||
var scope = file.namespace('remark-lint'); | ||
for (id in rules) { | ||
setting = settings[id]; | ||
/* Track new messages per file. */ | ||
scope.index = file.messages.length; | ||
known.push(id); | ||
fn(ast, file, options, function (err) { | ||
var messages = file.messages; | ||
if (!(setting === null || setting === undefined)) { | ||
/* Pass turned on rules `undefined`. */ | ||
if (reset && setting === true) { | ||
setting = undefined; | ||
} | ||
while (scope.index < messages.length) { | ||
messages[scope.index].ruleId = id; | ||
messages[scope.index].source = SOURCE; | ||
if (setting === false) { | ||
setting = undefined; | ||
disable.push(id); | ||
} else { | ||
enable.push(id); | ||
} | ||
} | ||
scope.index++; | ||
} | ||
remark.use(attachFactory(id, rules[id], setting)); | ||
} | ||
/* | ||
* Allow comments to toggle messages. | ||
*/ | ||
remark.use(control, { | ||
'name': 'lint', | ||
'source': SOURCE, | ||
'reset': reset, | ||
'known': known, | ||
'enable': enable, | ||
'disable': disable | ||
next(err); | ||
}); | ||
/** | ||
* Transformer sort messages. | ||
* | ||
* @param {Node} node - Syntax tree. | ||
* @param {VFile} file - Virtual file. | ||
*/ | ||
return function (node, file) { | ||
sort(file); | ||
}; | ||
}; | ||
} | ||
/* | ||
* Expose. | ||
/** | ||
* Helper to ensure ruleId’s are dash-cased instead of | ||
* camel-cased. | ||
* | ||
* @param {Object} source - Original settings. | ||
* @return {Object} - Dash-cased settings. | ||
*/ | ||
function decamelizeSettings(source) { | ||
var result = {}; | ||
var key; | ||
module.exports = lint; | ||
for (key in source) { | ||
result[decamelize(key, '-')] = source[key]; | ||
} | ||
return result; | ||
} |
@@ -13,17 +13,39 @@ /** | ||
* and will warn when other blockquotes use a different indentation. | ||
* @example | ||
* <!-- Valid, when set to `4`, invalid when set to `2` --> | ||
* | ||
* @example {"name": "valid.md", "setting": 4} | ||
* | ||
* <!--This file is also valid by default--> | ||
* | ||
* > Hello | ||
* ... | ||
* | ||
* Paragraph. | ||
* | ||
* > World | ||
* | ||
* <!-- Valid, when set to `2`, invalid when set to `4` --> | ||
* @example {"name": "valid.md", "setting": 2} | ||
* | ||
* <!--This file is also valid by default--> | ||
* | ||
* > Hello | ||
* ... | ||
* | ||
* Paragraph. | ||
* | ||
* > World | ||
* | ||
* <!-- Always invalid --> | ||
* > Hello | ||
* ... | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* > Hello | ||
* | ||
* Paragraph. | ||
* | ||
* > World | ||
* | ||
* Paragraph. | ||
* | ||
* > World | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 5:3: Remove 1 space between blockquote and content | ||
* 9:3: Add 1 space between blockquote and content | ||
*/ | ||
@@ -33,32 +55,12 @@ | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = blockquoteIndentation; | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var toString = require('mdast-util-to-string'); | ||
var plural = require('plur'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/** | ||
* Get the indent of a blockquote. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @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 | ||
@@ -73,42 +75,51 @@ * indentation. | ||
* indentation. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function blockquoteIndentation(ast, file, preferred, done) { | ||
preferred = isNaN(preferred) || typeof preferred !== 'number' ? null : preferred; | ||
function blockquoteIndentation(ast, file, preferred) { | ||
preferred = isNaN(preferred) || typeof preferred !== 'number' ? null : preferred; | ||
visit(ast, 'blockquote', function (node) { | ||
var indent; | ||
var diff; | ||
var word; | ||
visit(ast, 'blockquote', function (node) { | ||
var indent; | ||
var diff; | ||
var word; | ||
if (position.generated(node) || !node.children.length) { | ||
return; | ||
} | ||
if (position.generated(node) || !node.children.length) { | ||
return; | ||
} | ||
if (preferred) { | ||
indent = check(node); | ||
diff = preferred - indent; | ||
word = diff > 0 ? 'Add' : 'Remove'; | ||
if (preferred) { | ||
indent = check(node); | ||
diff = preferred - indent; | ||
word = diff > 0 ? 'Add' : 'Remove'; | ||
diff = Math.abs(diff); | ||
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(); | ||
if (diff !== 0) { | ||
file.warn( | ||
word + ' ' + diff + ' ' + plural('space', diff) + | ||
' between blockquote and content', | ||
position.start(node.children[0]) | ||
); | ||
} | ||
} else { | ||
preferred = check(node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
/** | ||
* Get the indent of a blockquote. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @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(/^ +/); | ||
module.exports = blockquoteIndentation; | ||
if (padding) { | ||
indentation += padding[0].length; | ||
} | ||
return indentation; | ||
} |
@@ -15,32 +15,57 @@ /** | ||
* | ||
* ```json | ||
* { | ||
* "checked": "x", | ||
* "unchecked": " " | ||
* } | ||
* ```js | ||
* {checked: 'x', unchecked: ' '} | ||
* ``` | ||
* @example | ||
* <!-- Note: the double guillemet (`»`) and middle-dots represent a tab --> | ||
* | ||
* <!-- Valid by default, `'consistent'`, or `{'checked': 'x'}` --> | ||
* @example {"name": "valid.md", "setting": {"checked": "x"}} | ||
* | ||
* <!--This file is also valid by default--> | ||
* | ||
* - [x] List item | ||
* - [x] List item | ||
* | ||
* <!-- Valid by default, `'consistent'`, or `{'checked': 'X'}` --> | ||
* @example {"name": "valid.md", "setting": {"checked": "X"}} | ||
* | ||
* <!--This file is also valid by default--> | ||
* | ||
* - [X] List item | ||
* - [X] List item | ||
* | ||
* <!-- Valid by default, `'consistent'`, or `{'unchecked': ' '}` --> | ||
* @example {"name": "valid.md", "setting": {"unchecked": " "}} | ||
* | ||
* <!--This file is also valid by default--> | ||
* | ||
* - [ ] List item | ||
* - [ ] List item | ||
* - [ ]·· | ||
* - [ ] | ||
* | ||
* <!-- Valid by default, `'consistent'`, or `{'unchecked': '»'}` --> | ||
* - [»···] List item | ||
* - [»···] List item | ||
* @example {"name": "valid.md", "setting": {"unchecked": "\t"}} | ||
* | ||
* <!-- Always invalid --> | ||
* <!--Also valid by default (note: `»` represents `\t`)--> | ||
* | ||
* - [»] List item | ||
* - [»] List item | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!--Note: `»` represents `\t`--> | ||
* | ||
* - [x] List item | ||
* - [X] List item | ||
* - [ ] List item | ||
* - [»···] List item | ||
* - [»] List item | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 4:4-4:5: Checked checkboxes should use `x` as a marker | ||
* 6:4-6:5: Unchecked checkboxes should use ` ` as a marker | ||
* | ||
* @example {"setting": {"unchecked": "!"}, "name": "invalid.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid unchecked checkbox marker `!`: use either `'\t'`, or `' '` | ||
* | ||
* @example {"setting": {"checked": "!"}, "name": "invalid.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid checked checkbox marker `!`: use either `'x'`, or `'X'` | ||
*/ | ||
@@ -50,29 +75,18 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var vfileLocation = require('vfile-location'); | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = checkboxCharacterStyle; | ||
/* Methods. */ | ||
var start = position.start; | ||
var end = position.end; | ||
var CHECKED = { | ||
'x': true, | ||
'X': true | ||
}; | ||
/* Constants. */ | ||
var CHECKED = {x: true, X: true}; | ||
var UNCHECKED = {' ': true, '\t': true}; | ||
var UNCHECKED = { | ||
' ': true, | ||
' ': true | ||
}; | ||
/** | ||
@@ -87,105 +101,87 @@ * Warn when list item checkboxes violate a given style. | ||
* or `' '` (space) or `'\t'` (tab) for unchecked. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function checkboxCharacterStyle(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
function checkboxCharacterStyle(ast, file, preferred) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
if (preferred === 'consistent' || typeof preferred !== 'object') { | ||
preferred = {}; | ||
} | ||
if (preferred === 'consistent' || typeof preferred !== 'object') { | ||
preferred = {}; | ||
} | ||
if (!preferred.unchecked) { | ||
preferred.unchecked = null; | ||
} | ||
if (!preferred.unchecked) { | ||
preferred.unchecked = null; | ||
} | ||
if (!preferred.checked) { | ||
preferred.checked = null; | ||
} | ||
if (!preferred.checked) { | ||
preferred.checked = null; | ||
} | ||
if ( | ||
preferred.unchecked !== null && | ||
UNCHECKED[preferred.unchecked] !== true | ||
) { | ||
file.fail( | ||
'Invalid unchecked checkbox marker `' + | ||
preferred.unchecked + | ||
'`: use either `\'\\t\'`, or `\' \'`' | ||
); | ||
} | ||
if ( | ||
preferred.unchecked !== null && | ||
UNCHECKED[preferred.unchecked] !== true | ||
) { | ||
file.fail( | ||
'Invalid unchecked checkbox marker `' + | ||
preferred.unchecked + | ||
'`: use either `\'\\t\'`, or `\' \'`' | ||
); | ||
} | ||
if ( | ||
preferred.checked !== null && | ||
CHECKED[preferred.checked] !== true | ||
) { | ||
file.fail( | ||
'Invalid checked checkbox marker `' + | ||
preferred.checked + | ||
'`: use either `\'x\'`, or `\'X\'`' | ||
); | ||
} | ||
visit(ast, 'listItem', function (node) { | ||
var type; | ||
var initial; | ||
var final; | ||
var stop; | ||
var value; | ||
var style; | ||
var character; | ||
/* Exit early for items without checkbox. */ | ||
if ( | ||
preferred.checked !== null && | ||
CHECKED[preferred.checked] !== true | ||
node.checked !== Boolean(node.checked) || | ||
position.generated(node) | ||
) { | ||
file.fail( | ||
'Invalid checked checkbox marker `' + | ||
preferred.checked + | ||
'`: use either `\'x\'`, or `\'X\'`' | ||
); | ||
return; | ||
} | ||
visit(ast, 'listItem', function (node) { | ||
var type; | ||
var initial; | ||
var final; | ||
var stop; | ||
var value; | ||
var style; | ||
var character; | ||
type = node.checked ? 'checked' : 'unchecked'; | ||
/* | ||
* Exit early for items without checkbox. | ||
*/ | ||
initial = start(node).offset; | ||
final = (node.children.length ? start(node.children[0]) : end(node)).offset; | ||
if ( | ||
node.checked !== Boolean(node.checked) || | ||
position.generated(node) | ||
) { | ||
return; | ||
} | ||
/* For a checkbox to be parsed, it must be followed | ||
* by a white space. */ | ||
value = contents.slice(initial, final).trimRight().slice(0, -1); | ||
type = node.checked ? 'checked' : 'unchecked'; | ||
/* The checkbox character is behind a square | ||
* bracket. */ | ||
character = value.charAt(value.length - 1); | ||
style = preferred[type]; | ||
initial = start(node).offset; | ||
final = (node.children.length ? start(node.children[0]) : end(node)).offset; | ||
if (style === null) { | ||
preferred[type] = character; | ||
} else if (character !== style) { | ||
stop = initial + value.length; | ||
/* | ||
* For a checkbox to be parsed, it must be followed | ||
* by a white space. | ||
*/ | ||
value = contents.slice(initial, final).trimRight().slice(0, -1); | ||
/* | ||
* The checkbox character is behind a square | ||
* bracket. | ||
*/ | ||
character = value.charAt(value.length - 1); | ||
style = preferred[type]; | ||
if (style === null) { | ||
preferred[type] = character; | ||
} else if (character !== style) { | ||
stop = initial + value.length; | ||
file.warn( | ||
type.charAt(0).toUpperCase() + type.slice(1) + | ||
' checkboxes should use `' + style + '` as a marker', | ||
{ | ||
'start': location.toPosition(stop - 1), | ||
'end': location.toPosition(stop) | ||
} | ||
); | ||
file.warn( | ||
type.charAt(0).toUpperCase() + type.slice(1) + | ||
' checkboxes should use `' + style + '` as a marker', | ||
{ | ||
start: location.toPosition(stop - 1), | ||
end: location.toPosition(stop) | ||
} | ||
}); | ||
done(); | ||
); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = checkboxCharacterStyle; |
@@ -8,4 +8,5 @@ /** | ||
* Warn when list item checkboxes are followed by too much white-space. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* - [ ] List item | ||
@@ -16,3 +17,4 @@ * + [x] List item | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* - [ ] List item | ||
@@ -22,2 +24,8 @@ * + [x] List item | ||
* - [ ] List item | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 2:7-2:8: Checkboxes should be followed by a single character | ||
* 3:7-3:9: Checkboxes should be followed by a single character | ||
* 4:7-4:10: Checkboxes should be followed by a single character | ||
*/ | ||
@@ -27,16 +35,11 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var vfileLocation = require('vfile-location'); | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = checkboxContentIndent; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -51,57 +54,42 @@ var end = position.end; | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function checkboxContentIndent(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
function checkboxContentIndent(ast, file) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
visit(ast, 'listItem', function (node) { | ||
var initial; | ||
var final; | ||
var value; | ||
visit(ast, 'listItem', function (node) { | ||
var initial; | ||
var final; | ||
var value; | ||
/* | ||
* Exit early for items without checkbox. | ||
*/ | ||
/* Exit early for items without checkbox. */ | ||
if ( | ||
node.checked !== Boolean(node.checked) || | ||
position.generated(node) | ||
) { | ||
return; | ||
} | ||
if ( | ||
node.checked !== Boolean(node.checked) || | ||
position.generated(node) | ||
) { | ||
return; | ||
} | ||
initial = start(node).offset; | ||
final = (node.children.length ? start(node.children[0]) : end(node)).offset; | ||
initial = start(node).offset; | ||
final = (node.children.length ? start(node.children[0]) : end(node)).offset; | ||
while (/[^\S\n]/.test(contents.charAt(final))) { | ||
final++; | ||
} | ||
while (/[^\S\n]/.test(contents.charAt(final))) { | ||
final++; | ||
} | ||
/* For a checkbox to be parsed, it must be followed | ||
* by a white space. */ | ||
value = contents.slice(initial, final); | ||
/* | ||
* For a checkbox to be parsed, it must be followed | ||
* by a white space. | ||
*/ | ||
value = value.slice(value.indexOf(']') + 1); | ||
value = contents.slice(initial, final); | ||
if (value.length === 1) { | ||
return; | ||
} | ||
value = value.slice(value.indexOf(']') + 1); | ||
if (value.length === 1) { | ||
return; | ||
} | ||
file.warn('Checkboxes should be followed by a single character', { | ||
'start': location.toPosition(final - value.length + 1), | ||
'end': location.toPosition(final) | ||
}); | ||
file.warn('Checkboxes should be followed by a single character', { | ||
start: location.toPosition(final - value.length + 1), | ||
end: location.toPosition(final) | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = checkboxContentIndent; |
@@ -15,25 +15,76 @@ /** | ||
* style. | ||
* @example | ||
* <!-- Valid, when set to `indented` or `consistent`, invalid when set to `fenced` --> | ||
* Hello | ||
* | ||
* ... | ||
* @example {"setting": "indented", "name": "valid.md"} | ||
* | ||
* World | ||
* <!-- This is also valid when `'consistent'` --> | ||
* | ||
* <!-- Valid, when set to `fenced` or `consistent`, invalid when set to `indented` --> | ||
* alpha(); | ||
* | ||
* Paragraph. | ||
* | ||
* bravo(); | ||
* | ||
* @example {"setting": "indented", "name": "invalid.md", "label": "input"} | ||
* | ||
* ``` | ||
* Hello | ||
* alpha(); | ||
* ``` | ||
* ... | ||
* ```bar | ||
* World | ||
* | ||
* Paragraph. | ||
* | ||
* ``` | ||
* bravo(); | ||
* ``` | ||
* | ||
* <!-- Always invalid --> | ||
* Hello | ||
* ... | ||
* @example {"setting": "indented", "name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-3:4: Code blocks should be indented | ||
* 7:1-9:4: Code blocks should be indented | ||
* | ||
* @example {"setting": "fenced", "name": "valid.md"} | ||
* | ||
* <!-- This is also valid when `'consistent'` --> | ||
* | ||
* ``` | ||
* World | ||
* ``` | ||
* alpha(); | ||
* ``` | ||
* | ||
* Paragraph. | ||
* | ||
* ``` | ||
* bravo(); | ||
* ``` | ||
* | ||
* @example {"setting": "fenced", "name": "invalid.md", "label": "input"} | ||
* | ||
* alpha(); | ||
* | ||
* Paragraph. | ||
* | ||
* bravo(); | ||
* | ||
* @example {"setting": "fenced", "name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:13: Code blocks should be fenced | ||
* 5:1-5:13: Code blocks should be fenced | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- This is always invalid --> | ||
* | ||
* alpha(); | ||
* | ||
* Paragraph. | ||
* | ||
* ``` | ||
* bravo(); | ||
* ``` | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 7:1-9:4: Code blocks should be indented | ||
* | ||
* @example {"setting": "invalid", "name": "invalid.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid code block style `invalid`: use either `'consistent'`, `'fenced'`, or `'indented'` | ||
*/ | ||
@@ -43,26 +94,18 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = codeBlockStyle; | ||
/* Methods. */ | ||
var start = position.start; | ||
var end = position.end; | ||
/* | ||
* Valid styles. | ||
*/ | ||
/* Valid styles. */ | ||
var STYLES = { | ||
'null': true, | ||
'fenced': true, | ||
'indented': true | ||
null: true, | ||
fenced: true, | ||
indented: true | ||
}; | ||
@@ -79,61 +122,53 @@ | ||
* `'fenced'` or `'indented'`. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function codeBlockStyle(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function codeBlockStyle(ast, file, preferred) { | ||
var contents = file.toString(); | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
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\'`'); | ||
done(); | ||
return; | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid code block style `' + preferred + '`: use either `\'consistent\'`, `\'fenced\'`, or `\'indented\'`'); | ||
return; | ||
} | ||
visit(ast, 'code', function (node) { | ||
var current = check(node); | ||
if (!current) { | ||
return; | ||
} | ||
/** | ||
* Get the style of `node`. | ||
* | ||
* @param {Node} node - Node. | ||
* @return {string?} - `'fenced'`, `'indented'`, or | ||
* `null`. | ||
*/ | ||
function check(node) { | ||
var initial = start(node).offset; | ||
var final = end(node).offset; | ||
if (!preferred) { | ||
preferred = current; | ||
} else if (preferred !== current) { | ||
file.warn('Code blocks should be ' + preferred, node); | ||
} | ||
}); | ||
if (position.generated(node)) { | ||
return null; | ||
} | ||
return; | ||
if ( | ||
node.lang || | ||
/^\s*([~`])\1{2,}/.test(contents.slice(initial, final)) | ||
) { | ||
return 'fenced'; | ||
} | ||
/** | ||
* Get the style of `node`. | ||
* | ||
* @param {Node} node - Node. | ||
* @return {string?} - `'fenced'`, `'indented'`, or | ||
* `null`. | ||
*/ | ||
function check(node) { | ||
var initial = start(node).offset; | ||
var final = end(node).offset; | ||
return 'indented'; | ||
if (position.generated(node)) { | ||
return null; | ||
} | ||
visit(ast, 'code', function (node) { | ||
var current = check(node); | ||
if ( | ||
node.lang || | ||
/^\s*([~`])\1{2,}/.test(contents.slice(initial, final)) | ||
) { | ||
return 'fenced'; | ||
} | ||
if (!current) { | ||
return; | ||
} | ||
if (!preferred) { | ||
preferred = current; | ||
} else if (preferred !== current) { | ||
file.warn('Code blocks should be ' + preferred, node); | ||
} | ||
}); | ||
done(); | ||
return 'indented'; | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = codeBlockStyle; |
@@ -8,8 +8,14 @@ /** | ||
* 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" | ||
* @example {"name": "valid.md"} | ||
* | ||
* [example]: http://example.com "Example Domain" | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* [Example]: http://example.com "Example Domain" | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:47: Do not use upper-case characters in definition labels | ||
*/ | ||
@@ -19,15 +25,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Expressions. | ||
*/ | ||
/* Expose. */ | ||
module.exports = definitionCase; | ||
/* Expressions. */ | ||
var LABEL = /^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/; | ||
@@ -41,40 +42,32 @@ | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function definitionCase(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function definitionCase(ast, file) { | ||
var contents = file.toString(); | ||
/** | ||
* Validate a node, either a normal definition or | ||
* a footnote definition. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var start = position.start(node).offset; | ||
var end = position.end(node).offset; | ||
var label; | ||
visit(ast, 'definition', validate); | ||
visit(ast, 'footnoteDefinition', validate); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
return; | ||
label = contents.slice(start, end).match(LABEL)[1]; | ||
/** | ||
* Validate a node, either a normal definition or | ||
* a footnote definition. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var start = position.start(node).offset; | ||
var end = position.end(node).offset; | ||
var label; | ||
if (label !== label.toLowerCase()) { | ||
file.warn('Do not use upper-case characters in definition labels', node); | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'definition', validate); | ||
visit(ast, 'footnoteDefinition', validate); | ||
label = contents.slice(start, end).match(LABEL)[1]; | ||
done(); | ||
if (label !== label.toLowerCase()) { | ||
file.warn('Do not use upper-case characters in definition labels', node); | ||
} | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = definitionCase; |
@@ -8,8 +8,14 @@ /** | ||
* 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" | ||
* @example {"name": "valid.md"} | ||
* | ||
* [example domain]: http://example.com "Example Domain" | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* [example domain]: http://example.com "Example Domain" | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:57: Do not use consecutive white-space in definition labels | ||
*/ | ||
@@ -19,15 +25,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Expressions. | ||
*/ | ||
/* Expose. */ | ||
module.exports = definitionSpacing; | ||
/* Expressions. */ | ||
var LABEL = /^\s*\[((?:\\[\s\S]|[^\[\]])+)\]/; | ||
@@ -41,40 +42,32 @@ | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function definitionSpacing(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function definitionSpacing(ast, file) { | ||
var contents = file.toString(); | ||
/** | ||
* Validate a node, either a normal definition or | ||
* a footnote definition. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var start = position.start(node).offset; | ||
var end = position.end(node).offset; | ||
var label; | ||
visit(ast, 'definition', validate); | ||
visit(ast, 'footnoteDefinition', validate); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
return; | ||
label = contents.slice(start, end).match(LABEL)[1]; | ||
/** | ||
* Validate a node, either a normal definition or | ||
* a footnote definition. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var start = position.start(node).offset; | ||
var end = position.end(node).offset; | ||
var label; | ||
if (/[ \t\n]{2,}/.test(label)) { | ||
file.warn('Do not use consecutive white-space in definition labels', node); | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'definition', validate); | ||
visit(ast, 'footnoteDefinition', validate); | ||
label = contents.slice(start, end).match(LABEL)[1]; | ||
done(); | ||
if (/[ \t\n]{2,}/.test(label)) { | ||
file.warn('Do not use consecutive white-space in definition labels', node); | ||
} | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = definitionSpacing; |
@@ -15,10 +15,41 @@ /** | ||
* style. | ||
* @example | ||
* <!-- Valid when set to `consistent` or `*` --> | ||
* | ||
* @example {"setting": "*", "name": "valid.md"} | ||
* | ||
* *foo* | ||
* *bar* | ||
* | ||
* <!-- Valid when set to `consistent` or `_` --> | ||
* @example {"setting": "*", "name": "invalid.md", "label": "input"} | ||
* | ||
* _foo_ | ||
* | ||
* @example {"setting": "*", "name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:6: Emphasis should use `*` as a marker | ||
* | ||
* @example {"setting": "_", "name": "valid.md"} | ||
* | ||
* _foo_ | ||
* | ||
* @example {"setting": "_", "name": "invalid.md", "label": "input"} | ||
* | ||
* *foo* | ||
* | ||
* @example {"setting": "_", "name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:6: Emphasis should use `_` as a marker | ||
* | ||
* @example {"setting": "consistent", "name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- This is never valid --> | ||
* | ||
* *foo* | ||
* _bar_ | ||
* | ||
* @example {"setting": "consistent", "name": "invalid.md", "label": "output"} | ||
* | ||
* 4:1-4:6: Emphasis should use `*` as a marker | ||
* | ||
* @example {"setting": "invalid", "name": "invalid.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid emphasis marker `invalid`: use either `'consistent'`, `'*'`, or `'_'` | ||
*/ | ||
@@ -30,17 +61,14 @@ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Map of valid markers. | ||
*/ | ||
/* Expose. */ | ||
module.exports = emphasisMarker; | ||
/* Map of valid markers. */ | ||
var MARKERS = { | ||
'*': true, | ||
'_': true, | ||
'null': true | ||
'*': true, | ||
'_': true, | ||
'null': true | ||
}; | ||
@@ -55,37 +83,26 @@ | ||
* marker, either `'*'` or `'_'`, or `'consistent'`. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function emphasisMarker(ast, file, preferred, done) { | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
function emphasisMarker(ast, file, preferred) { | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
if (MARKERS[preferred] !== true) { | ||
file.fail('Invalid emphasis marker `' + preferred + '`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`'); | ||
done(); | ||
if (MARKERS[preferred] !== true) { | ||
file.fail('Invalid emphasis marker `' + preferred + '`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`'); | ||
return; | ||
} | ||
return; | ||
visit(ast, 'emphasis', function (node) { | ||
var marker = file.toString().charAt(position.start(node).offset); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'emphasis', function (node) { | ||
var marker = file.toString().charAt(position.start(node).offset); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (preferred) { | ||
if (marker !== preferred) { | ||
file.warn('Emphasis should use `' + preferred + '` as a marker', node); | ||
} | ||
} else { | ||
preferred = marker; | ||
} | ||
}); | ||
done(); | ||
if (preferred) { | ||
if (marker !== preferred) { | ||
file.warn('Emphasis should use `' + preferred + '` as a marker', node); | ||
} | ||
} else { | ||
preferred = marker; | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = emphasisMarker; |
@@ -18,25 +18,50 @@ /** | ||
* languge flags. | ||
* @example | ||
* <!-- Valid: --> | ||
* ```hello | ||
* world(); | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* ```alpha | ||
* bravo(); | ||
* ``` | ||
* | ||
* <!-- Valid: --> | ||
* Hello | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- Invalid: --> | ||
* ``` | ||
* world(); | ||
* alpha(); | ||
* ``` | ||
* | ||
* <!-- Valid when given `{allowEmpty: true}`: --> | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-3:4: Missing code-language flag | ||
* | ||
* @example {"name": "valid.md", "setting": {"allowEmpty": true}} | ||
* | ||
* ``` | ||
* world(); | ||
* alpha(); | ||
* ``` | ||
* | ||
* <!-- Invalid when given `["world"]`: --> | ||
* ```hello | ||
* world(); | ||
* @example {"name": "invalid.md", "setting": {"allowEmpty": false}, "label": "input"} | ||
* | ||
* ``` | ||
* alpha(); | ||
* ``` | ||
* | ||
* @example {"name": "invalid.md", "setting": {"allowEmpty": false}, "label": "output"} | ||
* | ||
* 1:1-3:4: Missing code-language flag | ||
* | ||
* @example {"name": "valid.md", "setting": ["alpha"]} | ||
* | ||
* ```alpha | ||
* bravo(); | ||
* ``` | ||
* | ||
* @example {"name": "invalid.md", "setting": ["charlie"], "label": "input"} | ||
* | ||
* ```alpha | ||
* bravo(); | ||
* ``` | ||
* | ||
* @example {"name": "invalid.md", "setting": ["charlie"], "label": "output"} | ||
* | ||
* 1:1-3:4: Invalid code-language flag | ||
*/ | ||
@@ -46,15 +71,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = fencedCodeFlag; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -70,42 +90,33 @@ var end = position.end; | ||
* of flags deemed valid. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function fencedCodeFlag(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
var allowEmpty = false; | ||
var flags = []; | ||
function fencedCodeFlag(ast, file, preferred) { | ||
var contents = file.toString(); | ||
var allowEmpty = false; | ||
var flags = []; | ||
if (typeof preferred === 'object' && !('length' in preferred)) { | ||
allowEmpty = Boolean(preferred.allowEmpty); | ||
if (typeof preferred === 'object' && !('length' in preferred)) { | ||
allowEmpty = Boolean(preferred.allowEmpty); | ||
preferred = preferred.flags; | ||
} | ||
preferred = preferred.flags; | ||
} | ||
if (typeof preferred === 'object' && 'length' in preferred) { | ||
flags = String(preferred).split(','); | ||
} | ||
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); | ||
visit(ast, 'code', function (node) { | ||
var value = contents.slice(start(node).offset, end(node).offset); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(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(); | ||
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); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = fencedCodeFlag; |
@@ -14,29 +14,46 @@ /** | ||
* different style. | ||
* @example | ||
* <!-- Valid by default and `` '`' ``: --> | ||
* ```foo | ||
* bar(); | ||
* | ||
* @example {"name": "valid.md", "setting": "`"} | ||
* | ||
* <!-- This is also valid by default. --> | ||
* | ||
* ```alpha | ||
* bravo(); | ||
* ``` | ||
* | ||
* ``` | ||
* baz(); | ||
* charlie(); | ||
* ``` | ||
* | ||
* <!-- Valid by default and `'~'`: --> | ||
* ~~~foo | ||
* bar(); | ||
* @example {"name": "valid.md", "setting": "~"} | ||
* | ||
* <!-- This is also valid by default. --> | ||
* | ||
* ~~~alpha | ||
* bravo(); | ||
* ~~~ | ||
* | ||
* ~~~ | ||
* baz(); | ||
* charlie(); | ||
* ~~~ | ||
* | ||
* <!-- Always invalid: --> | ||
* ~~~foo | ||
* bar(); | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- This is always invalid. --> | ||
* | ||
* ```alpha | ||
* bravo(); | ||
* ``` | ||
* | ||
* ~~~ | ||
* charlie(); | ||
* ~~~ | ||
* | ||
* ``` | ||
* baz(); | ||
* ``` | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 7:1-9:4: Fenced code should use ` as a marker | ||
* | ||
* @example {"name": "invalid.md", "setting": "!", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid fenced code marker `!`: use either `'consistent'`, `` '`' ``, or `'~'` | ||
*/ | ||
@@ -46,19 +63,14 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Map of valid markers. | ||
*/ | ||
/* Expose. */ | ||
module.exports = fencedCodeMarker; | ||
/* Map of valid markers. */ | ||
var MARKERS = { | ||
'`': true, | ||
'~': true, | ||
'null': true | ||
'`': true, | ||
'~': true, | ||
'null': true | ||
}; | ||
@@ -73,49 +85,35 @@ | ||
* marker, either `` '`' `` or `~`, or `'consistent'`. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function fencedCodeMarker(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function fencedCodeMarker(ast, file, preferred) { | ||
var contents = file.toString(); | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
if (MARKERS[preferred] !== true) { | ||
file.fail('Invalid fenced code marker `' + preferred + '`: use either `\'consistent\'`, `` \'\`\' ``, or `\'~\'`'); | ||
done(); | ||
if (MARKERS[preferred] !== true) { | ||
file.fail('Invalid fenced code marker `' + preferred + '`: use either `\'consistent\'`, `` \'`\' ``, or `\'~\'`'); | ||
return; | ||
} | ||
return; | ||
visit(ast, 'code', function (node) { | ||
var marker = contents.substr(position.start(node).offset, 4); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'code', function (node) { | ||
var marker = contents.substr(position.start(node).offset, 4); | ||
marker = marker.trimLeft().charAt(0); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
/* Ignore unfenced code blocks. */ | ||
if (MARKERS[marker] !== true) { | ||
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(); | ||
if (preferred) { | ||
if (marker !== preferred) { | ||
file.warn('Fenced code should use ' + preferred + ' as a marker', node); | ||
} | ||
} else { | ||
preferred = marker; | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = fencedCodeMarker; |
@@ -14,5 +14,12 @@ /** | ||
* Options: `string`, default: `'md'` — Expected file extension. | ||
* @example | ||
* Invalid (when `'md'`): readme.mkd, readme.markdown, etc. | ||
* Valid (when `'md'`): readme, readme.md | ||
* | ||
* @example {"name": "readme.md"} | ||
* | ||
* @example {"name": "readme"} | ||
* | ||
* @example {"name": "readme.mkd", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid extension: use `md` | ||
* | ||
* @example {"name": "readme.mkd", "setting": "mkd"} | ||
*/ | ||
@@ -22,3 +29,4 @@ | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = fileExtension; | ||
@@ -32,20 +40,11 @@ /** | ||
* extension. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function fileExtension(ast, file, preferred, done) { | ||
var ext = file.extension; | ||
function fileExtension(ast, file, preferred) { | ||
var ext = file.extension; | ||
preferred = typeof preferred === 'string' ? preferred : 'md'; | ||
preferred = typeof preferred === 'string' ? preferred : 'md'; | ||
if (ext !== '' && ext !== preferred) { | ||
file.warn('Invalid extension: use `' + preferred + '`'); | ||
} | ||
done(); | ||
if (ext !== '' && ext !== preferred) { | ||
file.warn('Invalid extension: use `' + preferred + '`'); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = fileExtension; |
@@ -8,14 +8,20 @@ /** | ||
* Warn when definitions are not placed at the end of the file. | ||
* @example | ||
* <!-- Valid: --> | ||
* ... | ||
* | ||
* [example] http://example.com "Example Domain" | ||
* @example {"name": "valid.md"} | ||
* | ||
* <!-- Invalid: --> | ||
* ... | ||
* Paragraph. | ||
* | ||
* [example] http://example.com "Example Domain" | ||
* [example]: http://example.com "Example Domain" | ||
* | ||
* A trailing paragraph. | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* Paragraph. | ||
* | ||
* [example]: http://example.com "Example Domain" | ||
* | ||
* Another paragraph. | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:1-3:47: Move definitions to the end of the file (after the node at line `5`) | ||
*/ | ||
@@ -25,15 +31,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = finalDefinition; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -47,35 +48,22 @@ | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function finalDefinition(ast, file, preferred, done) { | ||
var last = null; | ||
function finalDefinition(ast, file) { | ||
var last = null; | ||
visit(ast, function (node) { | ||
var line = start(node).line; | ||
visit(ast, function (node) { | ||
var line = start(node).line; | ||
/* | ||
* Ignore generated nodes. | ||
*/ | ||
/* Ignore generated nodes. */ | ||
if (node.type === 'root' || position.generated(node)) { | ||
return; | ||
} | ||
if (node.type === 'root' || position.generated(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(); | ||
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); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = finalDefinition; |
@@ -15,3 +15,4 @@ /** | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = finalNewline; | ||
@@ -24,20 +25,10 @@ /** | ||
* @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; | ||
function finalNewline(ast, file) { | ||
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(); | ||
if (last > -1 && contents.charAt(last) !== '\n') { | ||
file.warn('Missing newline character at end of file'); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = finalNewline; |
@@ -10,12 +10,38 @@ /** | ||
* Options: `number`, default: `1`. | ||
* @example | ||
* <!-- Valid, when set to `1` --> | ||
* # Foo | ||
* | ||
* ## Bar | ||
* @example {"name": "valid.md", "setting": 1} | ||
* | ||
* <!-- Invalid, when set to `1` --> | ||
* ## Foo | ||
* <!-- Also valid by default. --> | ||
* | ||
* # Bar | ||
* # Alpha | ||
* | ||
* Paragraph. | ||
* | ||
* @example {"name": "invalid.md", "setting": 1, "label": "input"} | ||
* | ||
* <!-- Also invalid by default. --> | ||
* | ||
* ## Bravo | ||
* | ||
* Paragraph. | ||
* | ||
* @example {"name": "invalid.md", "setting": 1, "label": "output"} | ||
* | ||
* 3:1-3:9: First heading level should be `1` | ||
* | ||
* @example {"name": "valid.md", "setting": 2} | ||
* | ||
* ## Bravo | ||
* | ||
* Paragraph. | ||
* | ||
* @example {"name": "invalid.md", "setting": 2, "label": "input"} | ||
* | ||
* # Bravo | ||
* | ||
* Paragraph. | ||
* | ||
* @example {"name": "invalid.md", "setting": 2, "label": "output"} | ||
* | ||
* 1:1-1:8: First heading level should be `2` | ||
*/ | ||
@@ -25,11 +51,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = firstHeadingLevel; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -41,22 +65,17 @@ * Warn when the first heading has a level other than a specified value. | ||
* @param {number?} [preferred=1] - First heading level. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function firstHeadingLevel(ast, file, preferred, done) { | ||
var style = preferred && preferred !== true ? preferred : 1; | ||
function firstHeadingLevel(ast, file, preferred) { | ||
var style = preferred && preferred !== true ? preferred : 1; | ||
visit(ast, 'heading', function (node) { | ||
if (position.generated(node)) { | ||
return null; | ||
} | ||
visit(ast, 'heading', function (node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (node.depth !== style) { | ||
file.warn('First heading level should be `' + style + '`', node); | ||
} | ||
if (node.depth !== style) { | ||
file.warn('First heading level should be `' + style + '`', node); | ||
} | ||
return false; | ||
}); | ||
done(); | ||
return false; | ||
}); | ||
} | ||
module.exports = firstHeadingLevel; |
@@ -8,12 +8,20 @@ /** | ||
* Warn when too many spaces are used to create a hard break. | ||
* @example | ||
* <!-- Note: the middle-dots represent spaces --> | ||
* | ||
* <!-- Valid: --> | ||
* @example {"name": "valid.md"} | ||
* | ||
* <!--Note: `·` represents ` `--> | ||
* | ||
* Lorem ipsum·· | ||
* dolor sit amet | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!--Note: `·` represents ` `--> | ||
* | ||
* Lorem ipsum··· | ||
* dolor sit amet. | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:12-4:1: Use two spaces for hard line breaks | ||
*/ | ||
@@ -25,9 +33,9 @@ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* Expose. */ | ||
module.exports = hardBreakSpaces; | ||
/** | ||
@@ -39,31 +47,21 @@ * Warn when too many spaces are used to create a | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function hardBreakSpaces(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function hardBreakSpaces(ast, file) { | ||
var contents = file.toString(); | ||
visit(ast, 'break', function (node) { | ||
var start = position.start(node).offset; | ||
var end = position.end(node).offset; | ||
var value; | ||
visit(ast, 'break', function (node) { | ||
var start = position.start(node).offset; | ||
var end = position.end(node).offset; | ||
var value; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
value = contents.slice(start, end).split('\n', 1)[0].replace(/\r$/, ''); | ||
value = contents.slice(start, end).split('\n', 1)[0].replace(/\r$/, ''); | ||
if (value.length > 2) { | ||
file.warn('Use two spaces for hard line breaks', node); | ||
} | ||
}); | ||
done(); | ||
if (value.length > 2) { | ||
file.warn('Use two spaces for hard line breaks', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = hardBreakSpaces; |
@@ -8,12 +8,18 @@ /** | ||
* Warn when headings increment with more than 1 level at a time. | ||
* @example | ||
* <!-- Valid: --> | ||
* # Foo | ||
* | ||
* ## Bar | ||
* @example {"name": "valid.md"} | ||
* | ||
* <!-- Invalid: --> | ||
* # Foo | ||
* # Alpha | ||
* | ||
* ### Bar | ||
* ## Bravo | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* # Charlie | ||
* | ||
* ### Delta | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:1-3:10: Heading levels should increment by one level at a time | ||
*/ | ||
@@ -23,11 +29,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = headingIncrement; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -41,29 +45,19 @@ * Warn when headings increment with more than 1 level at | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function headingIncrement(ast, file, preferred, done) { | ||
var prev = null; | ||
function headingIncrement(ast, file) { | ||
var prev = null; | ||
visit(ast, 'heading', function (node) { | ||
var depth = node.depth; | ||
visit(ast, 'heading', function (node) { | ||
var depth = node.depth; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (prev && depth > prev + 1) { | ||
file.warn('Heading levels should increment by one level at a time', node); | ||
} | ||
if (prev && depth > prev + 1) { | ||
file.warn('Heading levels should increment by one level at a time', node); | ||
} | ||
prev = depth; | ||
}); | ||
done(); | ||
prev = depth; | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = headingIncrement; |
@@ -15,33 +15,50 @@ /** | ||
* style. | ||
* @example | ||
* <!-- Valid when `consistent` or `atx` --> | ||
* # Foo | ||
* | ||
* ## Bar | ||
* @example {"name": "valid.md", "setting": "atx"} | ||
* | ||
* ### Baz | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* <!-- Valid when `consistent` or `atx-closed` --> | ||
* # Foo # | ||
* # Alpha | ||
* | ||
* ## Bar # | ||
* ## Bravo | ||
* | ||
* ### Baz ### | ||
* ### Charlie | ||
* | ||
* <!-- Valid when `consistent` or `setext` --> | ||
* Foo | ||
* === | ||
* @example {"name": "valid.md", "setting": "atx-closed"} | ||
* | ||
* Bar | ||
* --- | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* ### Baz | ||
* # Delta ## | ||
* | ||
* <!-- Invalid --> | ||
* Foo | ||
* === | ||
* ## Echo ## | ||
* | ||
* ## Bar | ||
* ### Foxtrot ### | ||
* | ||
* ### Baz ### | ||
* @example {"name": "valid.md", "setting": "setext"} | ||
* | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* Golf | ||
* ==== | ||
* | ||
* Hotel | ||
* ----- | ||
* | ||
* ### India | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!--Always invalid.--> | ||
* | ||
* Juliett | ||
* ======= | ||
* | ||
* ## Kilo | ||
* | ||
* ### Lima ### | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 6:1-6:8: Headings should use setext | ||
* 8:1-8:13: Headings should use setext | ||
*/ | ||
@@ -53,14 +70,11 @@ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var style = require('mdast-util-heading-style'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Types. | ||
*/ | ||
/* Expose. */ | ||
module.exports = headingStyle; | ||
/* Types. */ | ||
var TYPES = ['atx', 'atx-closed', 'setext']; | ||
@@ -79,26 +93,18 @@ | ||
*/ | ||
function headingStyle(ast, file, preferred, done) { | ||
preferred = TYPES.indexOf(preferred) === -1 ? null : preferred; | ||
function headingStyle(ast, file, preferred) { | ||
preferred = TYPES.indexOf(preferred) === -1 ? null : preferred; | ||
visit(ast, 'heading', function (node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'heading', function (node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (preferred) { | ||
if (style(node, preferred) !== preferred) { | ||
file.warn('Headings should use ' + preferred, node); | ||
} | ||
} else { | ||
preferred = style(node, preferred); | ||
} | ||
}); | ||
done(); | ||
if (preferred) { | ||
if (style(node, preferred) !== preferred) { | ||
file.warn('Headings should use ' + preferred, node); | ||
} | ||
} else { | ||
preferred = style(node, preferred); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = headingStyle; |
@@ -15,19 +15,50 @@ /** | ||
* style. | ||
* @example | ||
* <!-- Valid when `consistent` or `"` --> | ||
* | ||
* @example {"name": "valid.md", "setting": "\""} | ||
* | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* [Example](http://example.com "Example Domain") | ||
* [Example](http://example.com "Example Domain") | ||
* | ||
* <!-- Valid when `consistent` or `'` --> | ||
* @example {"name": "valid.md", "setting": "'"} | ||
* | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* [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)) | ||
* @example {"name": "valid.md", "setting": "()"} | ||
* | ||
* <!-- Always invalid --> | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* [Example](http://example.com (Example Domain) ) | ||
* [Example](http://example.com (Example Domain) ) | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!--Always invalid--> | ||
* | ||
* [Example](http://example.com "Example Domain") | ||
* [Example](http://example.com#without-title) | ||
* [Example](http://example.com 'Example Domain') | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 5:46: Titles should use `"` as a quote | ||
* | ||
* @example {"name": "invalid.md", "label": "input", "setting": "()"} | ||
* | ||
* <!--Always invalid--> | ||
* | ||
* [Example](http://example.com (Example Domain)) | ||
* [Example](http://example.com 'Example Domain') | ||
* | ||
* @example {"name": "invalid.md", "label": "output", "setting": "()"} | ||
* | ||
* 4:46: Titles should use `()` as a quote | ||
* | ||
* @example {"name": "invalid.md", "setting": ".", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid link title style marker `.`: use either `'consistent'`, `'"'`, `'\''`, or `'()'` | ||
*/ | ||
@@ -37,29 +68,21 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var vfileLocation = require('vfile-location'); | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Map of valid markers. | ||
*/ | ||
/* Expose. */ | ||
module.exports = linkTitleStyle; | ||
/* Methods. */ | ||
var end = position.end; | ||
/* Map of valid markers. */ | ||
var MARKERS = { | ||
'"': true, | ||
'\'': true, | ||
')': true, | ||
'null': true | ||
'"': true, | ||
'\'': true, | ||
')': true, | ||
'null': true | ||
}; | ||
/* | ||
* Methods. | ||
*/ | ||
var end = position.end; | ||
/** | ||
@@ -72,76 +95,64 @@ * Warn for fenced code blocks without language flag. | ||
* marker, either `'"'`, `'\''`, `'()'`, or `'consistent'`. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function linkTitleStyle(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
function linkTitleStyle(ast, file, preferred) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
if (preferred === '()' || preferred === '(') { | ||
preferred = ')'; | ||
} | ||
if (preferred === '()' || preferred === '(') { | ||
preferred = ')'; | ||
} | ||
if (MARKERS[preferred] !== true) { | ||
file.fail('Invalid link title style marker `' + preferred + '`: use either `\'consistent\'`, `\'"\'`, `\'\\\'\'`, or `\'()\'`'); | ||
done(); | ||
if (MARKERS[preferred] !== true) { | ||
file.fail('Invalid link title style marker `' + preferred + '`: use either `\'consistent\'`, `\'"\'`, `\'\\\'\'`, or `\'()\'`'); | ||
return; | ||
} | ||
return; | ||
} | ||
visit(ast, 'link', validate); | ||
visit(ast, 'image', validate); | ||
visit(ast, 'definition', validate); | ||
/** | ||
* Validate a single node. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var last = end(node).offset - 1; | ||
var character; | ||
var pos; | ||
return; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
/** | ||
* Validate a single node. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var last = end(node).offset - 1; | ||
var character; | ||
var pos; | ||
if (node.type !== 'definition') { | ||
last--; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
while (last) { | ||
character = contents.charAt(last); | ||
if (node.type !== 'definition') { | ||
last--; | ||
} | ||
if (/\s/.test(character)) { | ||
last--; | ||
} else { | ||
break; | ||
} | ||
} | ||
while (last) { | ||
character = contents.charAt(last); | ||
/* | ||
* Not a title. | ||
*/ | ||
if (/\s/.test(character)) { | ||
last--; | ||
} else { | ||
break; | ||
} | ||
} | ||
if (!(character in MARKERS)) { | ||
return; | ||
} | ||
/* Not a title. */ | ||
if (!(character in MARKERS)) { | ||
return; | ||
} | ||
if (!preferred) { | ||
preferred = character; | ||
} else if (preferred !== character) { | ||
pos = location.toPosition(last + 1); | ||
file.warn('Titles should use `' + (preferred === ')' ? '()' : preferred) + '` as a quote', pos); | ||
} | ||
if (!preferred) { | ||
preferred = character; | ||
} else if (preferred !== character) { | ||
pos = location.toPosition(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; |
@@ -8,10 +8,21 @@ /** | ||
* Warn when list item bullets are indented. | ||
* @example | ||
* <!-- Valid --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* Paragraph. | ||
* | ||
* * List item | ||
* * List item | ||
* | ||
* <!-- Invalid --> | ||
* * List item | ||
* * List item | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* Paragraph. | ||
* | ||
* * List item | ||
* * List item | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:3: Incorrect indentation before bullet: remove 1 space | ||
* 4:3: Incorrect indentation before bullet: remove 1 space | ||
*/ | ||
@@ -21,16 +32,11 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
var plural = require('plur'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = listItemBulletIndent; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -43,41 +49,31 @@ | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function listItemBulletIndent(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function listItemBulletIndent(ast, file) { | ||
var contents = file.toString(); | ||
visit(ast, 'list', function (node) { | ||
var items = node.children; | ||
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; | ||
items.forEach(function (item) { | ||
var head = item.children[0]; | ||
var initial = start(item).offset; | ||
var final = start(head).offset; | ||
var indent; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
indent = contents.slice(initial, final).match(/^\s*/)[0].length; | ||
indent = contents.slice(initial, final).match(/^\s*/)[0].length; | ||
if (indent !== 0) { | ||
initial = start(head); | ||
if (indent !== 0) { | ||
initial = start(head); | ||
file.warn('Incorrect indentation before bullet: remove ' + indent + ' ' + plural('space', indent), { | ||
'line': initial.line, | ||
'column': initial.column - indent | ||
}); | ||
} | ||
file.warn('Incorrect indentation before bullet: remove ' + indent + ' ' + plural('space', indent), { | ||
line: initial.line, | ||
column: initial.column - indent | ||
}); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = listItemBulletIndent; |
@@ -8,12 +8,16 @@ /** | ||
* Warn when the content of a list item has mixed indentation. | ||
* @example | ||
* <!-- Valid --> | ||
* * List item | ||
* | ||
* * Nested list item indented by 4 spaces | ||
* @example {"name": "valid.md"} | ||
* | ||
* <!-- Invalid --> | ||
* * List item | ||
* 1. [x] Alpha | ||
* 1. Bravo | ||
* | ||
* * Nested list item indented by 3 spaces | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* 1. [x] Charlie | ||
* 1. Delta | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 2:5: Don’t use mixed indentation for children, remove 1 space | ||
*/ | ||
@@ -23,16 +27,11 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
var plural = require('plur'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = listItemContentIndent; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -46,79 +45,60 @@ | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function listItemContentIndent(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function listItemContentIndent(ast, file) { | ||
var contents = file.toString(); | ||
visit(ast, 'listItem', function (node) { | ||
var style; | ||
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; | ||
node.children.forEach(function (item, index) { | ||
var begin = start(item); | ||
var column = begin.column; | ||
var char; | ||
var diff; | ||
var word; | ||
if (position.generated(item)) { | ||
return; | ||
} | ||
if (position.generated(item)) { | ||
return; | ||
} | ||
/* | ||
* Get indentation for the first child. | ||
* Only the first item can have a checkbox, | ||
* so here we remove that from the column. | ||
*/ | ||
/* 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 there’s a checkbox before the content, | ||
* look backwards to find the start of that | ||
* checkbox. */ | ||
if (Boolean(node.checked) === node.checked) { | ||
char = begin.offset - 1; | ||
if (index === 0) { | ||
/* | ||
* If there’s a checkbox before the content, | ||
* look backwards to find the start of that | ||
* checkbox. | ||
*/ | ||
while (contents.charAt(char) !== '[') { | ||
char--; | ||
} | ||
if (Boolean(node.checked) === node.checked) { | ||
char = begin.offset - 1; | ||
column -= begin.offset - char; | ||
} | ||
while (contents.charAt(char) !== '[') { | ||
char--; | ||
} | ||
style = column; | ||
column -= begin.offset - char; | ||
} | ||
return; | ||
} | ||
style = column; | ||
/* Warn for violating children. */ | ||
if (column !== style) { | ||
diff = style - column; | ||
word = diff > 0 ? 'add' : 'remove'; | ||
return; | ||
} | ||
diff = Math.abs(diff); | ||
/* | ||
* 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 | ||
} | ||
); | ||
} | ||
}); | ||
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; |
@@ -12,31 +12,56 @@ /** | ||
* default: `'tab-size'`. | ||
* @example | ||
* <!-- Valid when `tab-size` --> | ||
* | ||
* @example {"name": "valid.md", "setting": "tab-size"} | ||
* | ||
* * List | ||
* item. | ||
* | ||
* Paragraph. | ||
* | ||
* 11. List | ||
* item. | ||
* | ||
* <!-- Valid when `mixed` --> | ||
* Paragraph. | ||
* | ||
* * List | ||
* item. | ||
* | ||
* * List | ||
* item. | ||
* | ||
* @example {"name": "valid.md", "setting": "mixed"} | ||
* | ||
* * List item. | ||
* | ||
* Paragraph. | ||
* | ||
* 11. List item | ||
* | ||
* Paragraph. | ||
* | ||
* * List | ||
* item. | ||
* | ||
* 11. List | ||
* * List | ||
* item. | ||
* | ||
* <!-- Valid when `space` --> | ||
* @example {"name": "valid.md", "setting": "space"} | ||
* | ||
* * List item. | ||
* | ||
* Paragraph. | ||
* | ||
* 11. List item | ||
* | ||
* Paragraph. | ||
* | ||
* * List | ||
* item. | ||
* | ||
* 11. List | ||
* item. | ||
* * List | ||
* item. | ||
* | ||
* @example {"name": "invalid.md", "setting": "invalid", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid list-item indent style `invalid`: use either `'tab-size'`, `'space'`, or `'mixed'` | ||
*/ | ||
@@ -48,24 +73,18 @@ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
var plural = require('plur'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = listItemIndent; | ||
/* Methods. */ | ||
var start = position.start; | ||
/* | ||
* Styles. | ||
*/ | ||
/* Styles. */ | ||
var STYLES = { | ||
'tab-size': true, | ||
'mixed': true, | ||
'space': true | ||
'tab-size': true, | ||
'mixed': true, | ||
'space': true | ||
}; | ||
@@ -84,73 +103,60 @@ | ||
*/ | ||
function listItemIndent(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function listItemIndent(ast, file, preferred) { | ||
var contents = file.toString(); | ||
preferred = typeof preferred !== 'string' ? 'tab-size' : preferred; | ||
preferred = typeof preferred === 'string' ? preferred : 'tab-size'; | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid list-item indent style `' + preferred + '`: use either `\'tab-size\'`, `\'space\'`, or `\'mixed\'`'); | ||
done(); | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid list-item indent style `' + preferred + '`: use either `\'tab-size\'`, `\'space\'`, or `\'mixed\'`'); | ||
return; | ||
} | ||
return; | ||
visit(ast, 'list', function (node) { | ||
var items = node.children; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
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 bulletSize; | ||
var tab; | ||
var marker; | ||
var shouldBe; | ||
var diff; | ||
var word; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
marker = contents.slice(initial, final); | ||
items.forEach(function (item) { | ||
var head = item.children[0]; | ||
var initial = start(item).offset; | ||
var final = start(head).offset; | ||
var bulletSize; | ||
var tab; | ||
var marker; | ||
var shouldBe; | ||
var diff; | ||
var word; | ||
/* Support checkboxes. */ | ||
marker = marker.replace(/\[[x ]?\]\s*$/i, ''); | ||
marker = contents.slice(initial, final); | ||
bulletSize = marker.trimRight().length; | ||
tab = Math.ceil(bulletSize / 4) * 4; | ||
/* | ||
* Support checkboxes. | ||
*/ | ||
if (preferred === 'tab-size') { | ||
shouldBe = tab; | ||
} else if (preferred === 'space') { | ||
shouldBe = bulletSize + 1; | ||
} else { | ||
shouldBe = node.loose ? tab : bulletSize + 1; | ||
} | ||
marker = marker.replace(/\[[x ]?\]\s*$/i, ''); | ||
if (marker.length !== shouldBe) { | ||
diff = shouldBe - marker.length; | ||
word = diff > 0 ? 'add' : 'remove'; | ||
bulletSize = marker.trimRight().length; | ||
tab = Math.ceil(bulletSize / 4) * 4; | ||
diff = Math.abs(diff); | ||
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) | ||
); | ||
} | ||
}); | ||
file.warn( | ||
'Incorrect list-item indent: ' + word + | ||
' ' + diff + ' ' + plural('space', diff), | ||
start(head) | ||
); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = listItemIndent; |
@@ -9,4 +9,13 @@ /** | ||
* when it should be loose, and vice versa. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* A tight list: | ||
* | ||
* - item 1 | ||
* - item 2 | ||
* - item 3 | ||
* | ||
* A loose list: | ||
* | ||
* - Wrapped | ||
@@ -19,8 +28,6 @@ * item | ||
* | ||
* <!-- Valid: --> | ||
* - item 1 | ||
* - item 2 | ||
* - item 3 | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- Invalid: --> | ||
* A tight list: | ||
* | ||
* - Wrapped | ||
@@ -31,3 +38,4 @@ * item | ||
* | ||
* <!-- Invalid: --> | ||
* A loose list: | ||
* | ||
* - item 1 | ||
@@ -38,2 +46,9 @@ * | ||
* - item 3 | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 4:9-5:1: Missing new line after list item | ||
* 5:11-6:1: Missing new line after list item | ||
* 11:1-12:1: Extraneous new line after list item | ||
* 13:1-14:1: Extraneous new line after list item | ||
*/ | ||
@@ -43,15 +58,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = listItemSpacing; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -66,69 +76,53 @@ var end = position.end; | ||
* @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; | ||
function listItemSpacing(ast, file) { | ||
visit(ast, 'list', function (node) { | ||
var items = node.children; | ||
var isTightList = true; | ||
var indent = start(node).column; | ||
var type; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(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; | ||
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; | ||
} | ||
}); | ||
if (isLoose) { | ||
isTightList = false; | ||
} | ||
}); | ||
type = isTightList ? 'tight' : 'loose'; | ||
type = isTightList ? 'tight' : 'loose'; | ||
items.forEach(function (item, index) { | ||
var next = items[index + 1]; | ||
var isTight = end(item).column > indent; | ||
items.forEach(function (item, index) { | ||
var next = items[index + 1]; | ||
var isTight = end(item).column > indent; | ||
/* | ||
* Ignore last. | ||
*/ | ||
/* Ignore last. */ | ||
if (!next) { | ||
return; | ||
} | ||
if (!next) { | ||
return; | ||
} | ||
/* | ||
* Check if the list item's state does (not) | ||
* match the list's state. | ||
*/ | ||
if (isTight !== isTightList) { | ||
if (type === 'loose') { | ||
file.warn('Missing new line after list item', { | ||
'start': end(item), | ||
'end': start(next) | ||
}); | ||
} else { | ||
file.warn('Extraneous new line after list item', { | ||
'start': end(item), | ||
'end': start(next) | ||
}); | ||
} | ||
} | ||
}); | ||
/* Check if the list item's state does (not) | ||
* match the list's state. */ | ||
if (isTight !== isTightList) { | ||
if (type === 'loose') { | ||
file.warn('Missing new line after list item', { | ||
start: end(item), | ||
end: start(next) | ||
}); | ||
} else { | ||
file.warn('Extraneous new line after list item', { | ||
start: end(item), | ||
end: start(next) | ||
}); | ||
} | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = listItemSpacing; |
@@ -12,9 +12,16 @@ /** | ||
* 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 | ||
* @example {"name": "valid.md"} | ||
* | ||
* # Alpha bravo charlie delta echo foxtrot golf hotel | ||
* | ||
* # ![Alpha bravo charlie delta echo foxtrot golf hotel](http://example.com/nato.png) | ||
* | ||
* @example {"name": "invalid.md", "setting": 40, "label": "input"} | ||
* | ||
* # Alpha bravo charlie delta echo foxtrot golf hotel | ||
* | ||
* @example {"name": "invalid.md", "setting": 40, "label": "output"} | ||
* | ||
* 1:1-1:52: Use headings shorter than `40` | ||
*/ | ||
@@ -24,12 +31,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var toString = require('mdast-util-to-string'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* Expose. */ | ||
module.exports = maximumHeadingLength; | ||
/** | ||
@@ -44,22 +49,14 @@ * Warn when headings are too long. | ||
*/ | ||
function maximumHeadingLength(ast, file, preferred, done) { | ||
preferred = isNaN(preferred) || typeof preferred !== 'number' ? 60 : preferred; | ||
function maximumHeadingLength(ast, file, preferred) { | ||
preferred = isNaN(preferred) || typeof preferred !== 'number' ? 60 : preferred; | ||
visit(ast, 'heading', function (node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'heading', function (node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (toString(node).length > preferred) { | ||
file.warn('Use headings shorter than `' + preferred + '`', node); | ||
} | ||
}); | ||
done(); | ||
if (toString(node).length > preferred) { | ||
file.warn('Use headings shorter than `' + preferred + '`', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = maximumHeadingLength; |
@@ -13,20 +13,46 @@ /** | ||
* code, link, images, and definitions. | ||
* @example | ||
* <!-- Valid, when set to `40` --> | ||
* Alpha bravo charlie delta echo. | ||
* | ||
* Alpha bravo charlie delta echo [foxtrot](./foxtrot.html). | ||
* @example {"name": "valid.md", "setting": 80, "config": {"positionless": true}} | ||
* | ||
* # Alpha bravo charlie delta echo foxtrot golf hotel. | ||
* This line is simply not toooooooooooooooooooooooooooooooooooooooooooo | ||
* long. | ||
* | ||
* # Alpha bravo charlie delta echo foxtrot golf hotel. | ||
* This is also fine: <http://this-long-url-with-a-long-domain.co.uk/a-long-path?query=variables> | ||
* | ||
* | A | B | C | D | E | F | F | H | | ||
* | ----- | ----- | ------- | ----- | ---- | ------- | ---- | ----- | | ||
* | Alpha | bravo | charlie | delta | echo | foxtrot | golf | hotel | | ||
* <http://this-link-is-fine.com> | ||
* | ||
* <!-- Invalid, when set to `40` --> | ||
* Alpha bravo charlie delta echo foxtrot golf. | ||
* [foo](http://this-long-url-with-a-long-domain-is-valid.co.uk/a-long-path?query=variables) | ||
* | ||
* Alpha bravo charlie delta echo [foxtrot](./foxtrot.html) golf. | ||
* <http://this-long-url-with-a-long-domain-is-valid.co.uk/a-long-path?query=variables> | ||
* | ||
* ![foo](http://this-long-url-with-a-long-domain-is-valid.co.uk/a-long-path?query=variables) | ||
* | ||
* | An | exception | is | line | length | in | long | tables | because | those | can’t | just | | ||
* | -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- | | ||
* | be | helped | | | | | | | | | | . | | ||
* | ||
* The following is also fine, because there is no white-space. | ||
* | ||
* <http://this-long-url-with-a-long-domain-is-invalid.co.uk/a-long-path?query=variables>. | ||
* | ||
* In addition, definitions are also fine: | ||
* | ||
* [foo]: <http://this-long-url-with-a-long-domain-is-invalid.co.uk/a-long-path?query=variables> | ||
* | ||
* @example {"name": "invalid.md", "setting": 80, "label": "input", "config": {"positionless": true}} | ||
* | ||
* This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo | ||
* long. | ||
* | ||
* Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one. | ||
* | ||
* And this one is also very wrong: because the link starts aaaaaaafter the column: <http://line.com> | ||
* | ||
* <http://this-long-url-with-a-long-domain-is-invalid.co.uk/a-long-path?query=variables> and such. | ||
* | ||
* @example {"name": "invalid.md", "setting": 80, "label": "output", "config": {"positionless": true}} | ||
* | ||
* 4:86: Line must be at most 80 characters | ||
* 6:99: Line must be at most 80 characters | ||
* 8:97: Line must be at most 80 characters | ||
*/ | ||
@@ -36,15 +62,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = maximumLineLength; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -54,17 +75,2 @@ var end = position.end; | ||
/** | ||
* Check if `node` is applicable, as in, if it should be | ||
* ignored. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @return {boolean} - Whether or not `node` should be | ||
* ignored. | ||
*/ | ||
function isIgnored(node) { | ||
return node.type === 'heading' || | ||
node.type === 'table' || | ||
node.type === 'code' || | ||
node.type === 'definition'; | ||
} | ||
/** | ||
* Warn when lines are too long. This rule is forgiving | ||
@@ -77,116 +83,109 @@ * about lines which cannot be wrapped, such as code, | ||
* @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; | ||
function maximumLineLength(ast, file, preferred) { | ||
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 - Start. | ||
* @param {number} final - End. | ||
*/ | ||
function whitelist(initial, final) { | ||
initial--; | ||
/* Next, white list nodes which cannot be wrapped. */ | ||
visit(ast, function (node) { | ||
var applicable = isIgnored(node); | ||
var initial = applicable && start(node).line; | ||
var final = applicable && end(node).line; | ||
while (++initial < final) { | ||
matrix[initial] = ''; | ||
} | ||
if (!applicable || position.generated(node)) { | ||
return; | ||
} | ||
/* | ||
* Next, white list nodes which cannot be wrapped. | ||
*/ | ||
whitelist(initial - 1, final); | ||
}); | ||
visit(ast, function (node) { | ||
var applicable = isIgnored(node); | ||
var initial = applicable && start(node).line; | ||
var final = applicable && end(node).line; | ||
visit(ast, 'link', validateLink); | ||
visit(ast, 'image', validateLink); | ||
if (!applicable || position.generated(node)) { | ||
return; | ||
} | ||
/* Iterate over every line, and warn for | ||
* violating lines. */ | ||
while (++index < length) { | ||
lineLength = matrix[index].length; | ||
whitelist(initial - 1, final); | ||
}); | ||
if (lineLength > style) { | ||
file.warn('Line must be at most ' + style + ' characters', { | ||
line: index + 1, | ||
column: lineLength + 1 | ||
}); | ||
} | ||
} | ||
/** | ||
* 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. | ||
* | ||
* @param {Node} node - Node. | ||
* @param {number} pos - Position of `node` in `parent`. | ||
* @param {Node} parent - Parent of `node`. | ||
*/ | ||
function validateLink(node, pos, parent) { | ||
var next = parent.children[pos + 1]; | ||
var initial = start(node); | ||
var final = end(node); | ||
return; | ||
/* | ||
* Nothing to whitelist when generated. | ||
*/ | ||
/** | ||
* Whitelist from `initial` to `final`, zero-based. | ||
* | ||
* @param {number} initial - Start. | ||
* @param {number} final - End. | ||
*/ | ||
function whitelist(initial, final) { | ||
initial--; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
while (++initial < final) { | ||
matrix[initial] = ''; | ||
} | ||
} | ||
/* | ||
* No whitelisting when starting after the border, | ||
* or ending before it. | ||
*/ | ||
/** | ||
* 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. | ||
* | ||
* @param {Node} node - Node. | ||
* @param {number} pos - Position of `node` in `parent`. | ||
* @param {Node} parent - Parent of `node`. | ||
*/ | ||
function validateLink(node, pos, parent) { | ||
var next = parent.children[pos + 1]; | ||
var initial = start(node); | ||
var final = end(node); | ||
if (initial.column > style || final.column < style) { | ||
return; | ||
} | ||
/* Nothing to whitelist when generated. */ | ||
if (position.generated(node)) { | ||
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); | ||
/* No whitelisting when starting after the border, | ||
* or ending before it. */ | ||
if (initial.column > style || final.column < style) { | ||
return; | ||
} | ||
visit(ast, 'link', validateLink); | ||
visit(ast, 'image', validateLink); | ||
/* | ||
* 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 | ||
}); | ||
} | ||
/* No whitelisting when there’s white-space after | ||
* the link. */ | ||
if ( | ||
next && | ||
start(next).line === initial.line && | ||
(!next.value || /^(.+?[ \t].+?)/.test(next.value)) | ||
) { | ||
return; | ||
} | ||
done(); | ||
whitelist(initial.line - 1, final.line); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
/** | ||
* Check if `node` is applicable, as in, if it should be | ||
* ignored. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @return {boolean} - Whether or not `node` should be | ||
* ignored. | ||
*/ | ||
module.exports = maximumLineLength; | ||
function isIgnored(node) { | ||
return node.type === 'heading' || | ||
node.type === 'table' || | ||
node.type === 'code' || | ||
node.type === 'definition'; | ||
} |
@@ -8,10 +8,16 @@ /** | ||
* Warn for angle-bracketed links without protocol. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* <http://www.example.com> | ||
* <mailto:foo@bar.com> | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <www.example.com> | ||
* <foo@bar.com> | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 2:1-2:14: All automatic links must start with a protocol | ||
*/ | ||
@@ -23,14 +29,11 @@ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var toString = require('mdast-util-to-string'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noAutoLinkWithoutProtocol; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -49,12 +52,2 @@ var end = position.end; | ||
/** | ||
* Assert `node`s reference starts with a protocol. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @return {boolean} - Whether `node` has a protocol. | ||
*/ | ||
function hasProtocol(node) { | ||
return PROTOCOL.test(toString(node)); | ||
} | ||
/** | ||
* Warn for angle-bracketed links without protocol. | ||
@@ -64,28 +57,28 @@ * | ||
* @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; | ||
function noAutoLinkWithoutProtocol(ast, file) { | ||
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.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (initial === head - 1 && final === tail + 1 && !hasProtocol(node)) { | ||
file.warn('All automatic links must start with a protocol', node); | ||
} | ||
}); | ||
done(); | ||
if (initial === head - 1 && final === tail + 1 && !hasProtocol(node)) { | ||
file.warn('All automatic links must start with a protocol', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
/** | ||
* Assert `node`s reference starts with a protocol. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @return {boolean} - Whether `node` has a protocol. | ||
*/ | ||
module.exports = noAutoLinkWithoutProtocol; | ||
function hasProtocol(node) { | ||
return PROTOCOL.test(toString(node)); | ||
} |
@@ -8,4 +8,5 @@ /** | ||
* Warn when blank lines without carets are found in a blockquote. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* > Foo... | ||
@@ -15,6 +16,11 @@ * > | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* > Foo... | ||
* | ||
* > ...Bar. | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 2:1: Missing caret in blockquote | ||
*/ | ||
@@ -24,12 +30,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var vfileLocation = require('vfile-location'); | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* Expose. */ | ||
module.exports = noBlockquoteWithoutCaret; | ||
/** | ||
@@ -41,53 +45,43 @@ * Warn when blank lines without carets are found in a | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noBlockquoteWithoutCaret(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
var last = contents.length; | ||
function noBlockquoteWithoutCaret(ast, file) { | ||
var contents = file.toString(); | ||
var location = vfileLocation(file); | ||
var last = contents.length; | ||
visit(ast, 'blockquote', function (node) { | ||
var start = position.start(node).line; | ||
var indent = node.position && node.position.indent; | ||
visit(ast, 'blockquote', function (node) { | ||
var start = position.start(node).line; | ||
var indent = node.position && node.position.indent; | ||
if (position.generated(node) || !indent || !indent.length) { | ||
return; | ||
} | ||
if (position.generated(node) || !indent || !indent.length) { | ||
return; | ||
} | ||
indent.forEach(function (column, n) { | ||
var character; | ||
var line = start + n + 1; | ||
var offset = location.toOffset({ | ||
'line': line, | ||
'column': column | ||
}) - 1; | ||
indent.forEach(function (column, n) { | ||
var character; | ||
var line = start + n + 1; | ||
var offset = location.toOffset({ | ||
line: line, | ||
column: column | ||
}) - 1; | ||
while (++offset < last) { | ||
character = contents.charAt(offset); | ||
while (++offset < last) { | ||
character = contents.charAt(offset); | ||
if (character === '>') { | ||
return; | ||
} | ||
if (character === '>') { | ||
return; | ||
} | ||
/* istanbul ignore else - just for safety */ | ||
if (character !== ' ' && character !== '\t') { | ||
break; | ||
} | ||
} | ||
/* istanbul ignore else - just for safety */ | ||
if (character !== ' ' && character !== '\t') { | ||
break; | ||
} | ||
} | ||
file.warn('Missing caret in blockquote', { | ||
'line': line, | ||
'column': column | ||
}); | ||
}); | ||
file.warn('Missing caret in blockquote', { | ||
line: line, | ||
column: column | ||
}); | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noBlockquoteWithoutCaret; |
@@ -9,4 +9,5 @@ /** | ||
* needed between a list and indented code, and two lists. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* Foo... | ||
@@ -16,3 +17,13 @@ * | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "valid-for-code.md"} | ||
* | ||
* Paragraph. | ||
* | ||
* * List | ||
* | ||
* | ||
* bravo(); | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* Foo... | ||
@@ -22,2 +33,6 @@ * | ||
* ...Bar. | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 4:1: Remove 1 line before node | ||
*/ | ||
@@ -29,14 +44,11 @@ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
var plural = require('plur'); | ||
/* | ||
* Constants. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noConsecutiveBlankLines; | ||
/* Constants. */ | ||
var MAX = 2; | ||
@@ -51,91 +63,67 @@ | ||
* @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 exceeds `max`. | ||
* | ||
* @param {Position} start - Initial. | ||
* @param {Position} end - Final. | ||
* @param {number} max - Threshold. | ||
*/ | ||
function compare(start, end, max) { | ||
var diff = end.line - start.line; | ||
var word = diff > 0 ? 'before' : 'after'; | ||
function noConsecutiveBlankLines(ast, file) { | ||
visit(ast, function (node) { | ||
var children = node.children; | ||
var head = children && children[0]; | ||
var tail = children && children[children.length - 1]; | ||
diff = Math.abs(diff) - max; | ||
if (diff > 0) { | ||
file.warn('Remove ' + diff + ' ' + plural('line', diff) + ' ' + word + ' node', end); | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, function (node) { | ||
var children = node.children; | ||
var head = children && children[0]; | ||
var tail = children && children[children.length - 1]; | ||
if (head && !position.generated(head)) { | ||
/* Compare parent and first child. */ | ||
compare(position.start(node), position.start(head), 0); | ||
if (position.generated(node)) { | ||
return; | ||
/* Compare between each child. */ | ||
children.forEach(function (child, index) { | ||
var prev = children[index - 1]; | ||
var max = MAX; | ||
if ( | ||
!prev || | ||
position.generated(prev) || | ||
position.generated(child) | ||
) { | ||
return; | ||
} | ||
if (head && !position.generated(head)) { | ||
/* | ||
* Compare parent and first child. | ||
*/ | ||
if ( | ||
(prev.type === 'list' && child.type === 'list') || | ||
(child.type === 'code' && prev.type === 'list' && !child.lang) | ||
) { | ||
max++; | ||
} | ||
compare(position.start(node), position.start(head), 0); | ||
compare(position.end(prev), position.start(child), max); | ||
}); | ||
/* | ||
* Compare between each child. | ||
*/ | ||
/* Compare parent and last child. */ | ||
if (tail !== head && !position.generated(tail)) { | ||
compare(position.end(node), position.end(tail), 1); | ||
} | ||
} | ||
}); | ||
children.forEach(function (child, index) { | ||
var prev = children[index - 1]; | ||
var max = MAX; | ||
return; | ||
if ( | ||
!prev || | ||
position.generated(prev) || | ||
position.generated(child) | ||
) { | ||
return; | ||
} | ||
/** | ||
* Compare the difference between `start` and `end`, | ||
* and warn when that difference exceeds `max`. | ||
* | ||
* @param {Position} start - Initial. | ||
* @param {Position} end - Final. | ||
* @param {number} max - Threshold. | ||
*/ | ||
function compare(start, end, max) { | ||
var diff = end.line - start.line; | ||
var word = diff > 0 ? 'before' : 'after'; | ||
if ( | ||
( | ||
prev.type === 'list' && | ||
child.type === 'list' | ||
) || | ||
( | ||
child.type === 'code' && | ||
prev.type === 'list' && | ||
!child.lang | ||
) | ||
) { | ||
max++; | ||
} | ||
diff = Math.abs(diff) - max; | ||
compare(position.end(prev), position.start(child), max); | ||
}); | ||
/* | ||
* Compare parent and last child. | ||
*/ | ||
if (tail !== head && !position.generated(tail)) { | ||
compare(position.end(node), position.end(tail), 1); | ||
} | ||
} | ||
}); | ||
done(); | ||
if (diff > 0) { | ||
file.warn('Remove ' + diff + ' ' + plural('line', diff) + ' ' + word + ' node', end); | ||
} | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noConsecutiveBlankLines; |
@@ -8,10 +8,16 @@ /** | ||
* Warn when duplicate definitions are found. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* [foo]: bar | ||
* [baz]: qux | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* [foo]: bar | ||
* [foo]: qux | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 2:1-2:11: Do not use definitions with the same identifier (1:1) | ||
*/ | ||
@@ -21,11 +27,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var position = require('unist-util-position'); | ||
var visit = require('unist-util-visit'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noDuplicateDefinitions; | ||
var position = require('mdast-util-position'); | ||
var visit = require('unist-util-visit'); | ||
/** | ||
@@ -38,44 +42,36 @@ * Warn when definitions with equal content are found. | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noDuplicateDefinitions(ast, file, preferred, done) { | ||
var map = {}; | ||
function noDuplicateDefinitions(ast, file) { | ||
var map = {}; | ||
/** | ||
* Check `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var duplicate = map[node.identifier]; | ||
var pos; | ||
visit(ast, 'definition', validate); | ||
visit(ast, 'footnoteDefinition', validate); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
return; | ||
if (duplicate && duplicate.type) { | ||
pos = position.start(duplicate); | ||
/** | ||
* Check `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function validate(node) { | ||
var duplicate = map[node.identifier]; | ||
var pos; | ||
file.warn( | ||
'Do not use definitions with the same identifier (' + | ||
pos.line + ':' + pos.column + ')', | ||
node | ||
); | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
map[node.identifier] = node; | ||
if (duplicate && duplicate.type) { | ||
pos = position.start(duplicate); | ||
file.warn( | ||
'Do not use definitions with the same identifier (' + | ||
pos.line + ':' + pos.column + ')', | ||
node | ||
); | ||
} | ||
visit(ast, 'definition', validate); | ||
visit(ast, 'footnoteDefinition', validate); | ||
done(); | ||
map[node.identifier] = node; | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noDuplicateDefinitions; |
@@ -8,4 +8,5 @@ /** | ||
* Warn when duplicate headings are found. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* # Foo | ||
@@ -15,3 +16,4 @@ * | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* # Foo | ||
@@ -22,2 +24,7 @@ * | ||
* ## [Foo](http://foo.com/bar) | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:1-3:7: Do not use headings with similar content (1:1) | ||
* 5:1-5:29: Do not use headings with similar content (3:1) | ||
*/ | ||
@@ -29,10 +36,10 @@ | ||
/* | ||
* Dependencies. | ||
*/ | ||
var position = require('mdast-util-position'); | ||
/* Dependencies. */ | ||
var position = require('unist-util-position'); | ||
var visit = require('unist-util-visit'); | ||
var toString = require('mdast-util-to-string'); | ||
/* Expose. */ | ||
module.exports = noDuplicateHeadings; | ||
/** | ||
@@ -45,37 +52,27 @@ * Warn when headings with equal content are found. | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noDuplicateHeadings(ast, file, preferred, done) { | ||
var map = {}; | ||
function noDuplicateHeadings(ast, file) { | ||
var map = {}; | ||
visit(ast, 'heading', function (node) { | ||
var value = toString(node).toUpperCase(); | ||
var duplicate = map[value]; | ||
var pos; | ||
visit(ast, 'heading', function (node) { | ||
var value = toString(node).toUpperCase(); | ||
var duplicate = map[value]; | ||
var pos; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (duplicate && duplicate.type === 'heading') { | ||
pos = position.start(duplicate); | ||
if (duplicate && duplicate.type === 'heading') { | ||
pos = position.start(duplicate); | ||
file.warn( | ||
'Do not use headings with similar content (' + | ||
pos.line + ':' + pos.column + ')', | ||
node | ||
); | ||
} | ||
file.warn( | ||
'Do not use headings with similar content (' + | ||
pos.line + ':' + pos.column + ')', | ||
node | ||
); | ||
} | ||
map[value] = node; | ||
}); | ||
done(); | ||
map[value] = node; | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noDuplicateHeadings; |
@@ -9,4 +9,5 @@ /** | ||
* a paragraph. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* # Foo | ||
@@ -16,6 +17,11 @@ * | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* *Foo* | ||
* | ||
* Bar. | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:6: Don’t use emphasis to introduce a section, use a heading | ||
*/ | ||
@@ -25,11 +31,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noEmphasisAsHeading; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -41,34 +45,24 @@ * Warn when a section (a new paragraph) is introduced | ||
* @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]; | ||
function noEmphasisAsHeading(ast, file) { | ||
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]; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if ( | ||
(!prev || prev.type !== 'heading') && | ||
next && | ||
next.type === 'paragraph' && | ||
children.length === 1 && | ||
(child.type === 'emphasis' || child.type === 'strong') | ||
) { | ||
file.warn('Don’t use emphasis to introduce a section, use a heading', node); | ||
} | ||
}); | ||
done(); | ||
if ( | ||
(!prev || prev.type !== 'heading') && | ||
next && | ||
next.type === 'paragraph' && | ||
children.length === 1 && | ||
(child.type === 'emphasis' || child.type === 'strong') | ||
) { | ||
file.warn('Don’t use emphasis to introduce a section, use a heading', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noEmphasisAsHeading; |
@@ -8,5 +8,16 @@ /** | ||
* Warn when file name start with an article. | ||
* @example | ||
* Valid: article.md | ||
* Invalid: an-article.md, a-article.md, , the-article.md | ||
* | ||
* @example {"name": "title.md"} | ||
* | ||
* @example {"name": "a-title.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not start file names with `a` | ||
* | ||
* @example {"name": "the-title.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not start file names with `the` | ||
* | ||
* @example {"name": "an-article.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not start file names with `an` | ||
*/ | ||
@@ -16,3 +27,4 @@ | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = noFileNameArticles; | ||
@@ -24,19 +36,9 @@ /** | ||
* @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); | ||
function noFileNameArticles(ast, file) { | ||
var match = file.filename && file.filename.match(/^(the|an?)\b/i); | ||
if (match) { | ||
file.warn('Do not start file names with `' + match[0] + '`'); | ||
} | ||
done(); | ||
if (match) { | ||
file.warn('Do not start file names with `' + match[0] + '`'); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noFileNameArticles; |
@@ -8,5 +8,8 @@ /** | ||
* Warn when file names contain consecutive dashes. | ||
* @example | ||
* Invalid: docs/plug--ins.md | ||
* Valid: docs/plug-ins.md | ||
* | ||
* @example {"name": "plug-ins.md"} | ||
* | ||
* @example {"name": "plug--ins.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not use consecutive dashes in a file name | ||
*/ | ||
@@ -16,3 +19,4 @@ | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = noFileNameConsecutiveDashes; | ||
@@ -24,17 +28,7 @@ /** | ||
* @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(); | ||
function noFileNameConsecutiveDashes(ast, file) { | ||
if (file.filename && /-{2,}/.test(file.filename)) { | ||
file.warn('Do not use consecutive dashes in a file name'); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noFileNameConsecutiveDashes; |
@@ -17,5 +17,18 @@ /** | ||
* warning. | ||
* @example | ||
* Invalid: plug_ins.md, plug ins.md. | ||
* Valid: plug-ins.md, plugins.md. | ||
* | ||
* @example {"name": "plug-ins.md"} | ||
* | ||
* @example {"name": "plugins.md"} | ||
* | ||
* @example {"name": "plug_ins.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not use `_` in a file name | ||
* | ||
* @example {"name": "README.md", "label": "output", "setting": "\\.a-z0-9", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not use `R` in a file name | ||
* | ||
* @example {"name": "plug ins.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not use ` ` in a file name | ||
*/ | ||
@@ -25,3 +38,4 @@ | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = noFileNameIrregularCharacters; | ||
@@ -34,26 +48,20 @@ /** | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
* @param {string|RegExp} preferred - RegExp, or string of | ||
* characters (in which case it’s wrapped in | ||
* `new RegExp('[^' + preferred + ']')`), that matches | ||
* characters which should not be allowed. | ||
*/ | ||
function noFileNameIrregularCharacters(ast, file, preferred, done) { | ||
var expression = preferred || /[^\\.a-zA-Z0-9-]/; | ||
var match; | ||
function noFileNameIrregularCharacters(ast, file, preferred) { | ||
var expression = preferred || /[^\\\.a-zA-Z0-9-]/; | ||
var match; | ||
if (typeof expression === 'string') { | ||
expression = new RegExp('[^' + expression + ']'); | ||
} | ||
if (typeof expression === 'string') { | ||
expression = new RegExp('[^' + expression + ']'); | ||
} | ||
match = file.filename && file.filename.match(expression); | ||
match = file.filename && file.filename.match(expression); | ||
if (match) { | ||
file.warn('Do not use `' + match[0] + '` in a file name'); | ||
} | ||
done(); | ||
if (match) { | ||
file.warn('Do not use `' + match[0] + '` in a file name'); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noFileNameIrregularCharacters; |
@@ -9,5 +9,10 @@ /** | ||
* characters. | ||
* @example | ||
* Invalid: Readme.md | ||
* Valid: README.md, readme.md | ||
* | ||
* @example {"name": "README.md"} | ||
* | ||
* @example {"name": "readme.md"} | ||
* | ||
* @example {"name": "Readme.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not mix casing in file names | ||
*/ | ||
@@ -17,3 +22,4 @@ | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = noFileNameMixedCase; | ||
@@ -26,19 +32,9 @@ /** | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noFileNameMixedCase(ast, file, preferred, done) { | ||
var name = file.filename; | ||
function noFileNameMixedCase(ast, file) { | ||
var name = file.filename; | ||
if (name && !(name === name.toLowerCase() || name === name.toUpperCase())) { | ||
file.warn('Do not mix casing in file names'); | ||
} | ||
done(); | ||
if (name && !(name === name.toLowerCase() || name === name.toUpperCase())) { | ||
file.warn('Do not mix casing in file names'); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noFileNameMixedCase; |
@@ -8,5 +8,12 @@ /** | ||
* Warn when file names contain initial or final dashes. | ||
* @example | ||
* Invalid: -readme.md, readme-.md | ||
* Valid: readme.md | ||
* | ||
* @example {"name": "readme.md"} | ||
* | ||
* @example {"name": "-readme.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not use initial or final dashes in a file name | ||
* | ||
* @example {"name": "readme-.md", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Do not use initial or final dashes in a file name | ||
*/ | ||
@@ -16,3 +23,4 @@ | ||
/* eslint-env commonjs */ | ||
/* Expose. */ | ||
module.exports = noFileNameOuterDashes; | ||
@@ -24,17 +32,7 @@ /** | ||
* @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(); | ||
function noFileNameOuterDashes(ast, file) { | ||
if (file.filename && /^-|-$/.test(file.filename)) { | ||
file.warn('Do not use initial or final dashes in a file name'); | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noFileNameOuterDashes; |
@@ -8,5 +8,17 @@ /** | ||
* Warn when a heading’s content is indented. | ||
* @example | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* <!-- Note: the middle-dots represent spaces --> | ||
* <!-- Invalid: --> | ||
* | ||
* #·Foo | ||
* | ||
* ## Bar·## | ||
* | ||
* ##·Baz | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- Note: the middle-dots represent spaces --> | ||
* | ||
* #··Foo | ||
@@ -18,8 +30,22 @@ * | ||
* | ||
* <!-- Valid: --> | ||
* #·Foo | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* ## Bar·## | ||
* 3:4: Remove 1 space before this heading’s content | ||
* 5:7: Remove 1 space after this heading’s content | ||
* 7:7: Remove 1 space before this heading’s content | ||
* | ||
* ##·Baz | ||
* @example {"name": "empty-heading.md"} | ||
* | ||
* #·· | ||
* | ||
* @example {"name": "tight.md", "config":{"pedantic":true}, "label": "input"} | ||
* | ||
* In pedantic mode, headings without spacing can also be detected: | ||
* | ||
* ##No spacing left, too much right··## | ||
* | ||
* @example {"name": "tight.md", "label": "output"} | ||
* | ||
* 3:3: Add 1 space before this heading’s content | ||
* 3:34: Remove 1 space after this heading’s content | ||
*/ | ||
@@ -29,17 +55,12 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var style = require('mdast-util-heading-style'); | ||
var plural = require('plur'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noHeadingContentIndent; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -55,91 +76,81 @@ var end = position.end; | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noHeadingContentIndent(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function noHeadingContentIndent(ast, file) { | ||
var contents = file.toString(); | ||
visit(ast, 'heading', function (node) { | ||
var depth = node.depth; | ||
var children = node.children; | ||
var type = style(node, 'atx'); | ||
var head; | ||
var initial; | ||
var final; | ||
var diff; | ||
var word; | ||
var index; | ||
var char; | ||
visit(ast, 'heading', function (node) { | ||
var depth = node.depth; | ||
var children = node.children; | ||
var type = style(node, 'atx'); | ||
var head; | ||
var initial; | ||
var final; | ||
var diff; | ||
var word; | ||
var index; | ||
var char; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (type === 'atx' || type === 'atx-closed') { | ||
initial = start(node); | ||
index = initial.offset; | ||
char = contents.charAt(index); | ||
if (type === 'atx' || type === 'atx-closed') { | ||
initial = start(node); | ||
index = initial.offset; | ||
char = contents.charAt(index); | ||
while (char && char !== '#') { | ||
index++; | ||
char = contents.charAt(index); | ||
} | ||
while (char && char !== '#') { | ||
index++; | ||
char = contents.charAt(index); | ||
} | ||
/* CR/LF bug: wooorm/remark#195. */ | ||
if (!char) { | ||
return; | ||
} | ||
/* istanbul ignore if - CR/LF bug: wooorm/remark#195. */ | ||
if (!char) { | ||
return; | ||
} | ||
index = depth + (index - initial.offset); | ||
head = start(children[0]).column; | ||
index = depth + (index - initial.offset); | ||
head = start(children[0]).column; | ||
/* | ||
* Ignore empty headings. | ||
*/ | ||
/* | ||
* Ignore empty headings. | ||
*/ | ||
if (!head) { | ||
return; | ||
} | ||
if (!head) { | ||
return; | ||
} | ||
diff = head - initial.column - 1 - index; | ||
diff = head - initial.column - 1 - index; | ||
if (diff) { | ||
word = diff > 0 ? 'Remove' : 'Add'; | ||
diff = Math.abs(diff); | ||
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]) | ||
); | ||
} | ||
} | ||
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`. | ||
*/ | ||
/* | ||
* 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 (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(); | ||
if (diff) { | ||
file.warn( | ||
'Remove ' + diff + ' ' + plural('space', diff) + | ||
' after this heading’s content', | ||
final | ||
); | ||
} | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noHeadingContentIndent; |
@@ -8,5 +8,21 @@ /** | ||
* Warn when a heading is indented. | ||
* @example | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* <!-- Note: the middle-dots represent spaces --> | ||
* <!-- Invalid: --> | ||
* | ||
* #·Hello world | ||
* | ||
* Foo | ||
* ----- | ||
* | ||
* #·Hello world·# | ||
* | ||
* Bar | ||
* ===== | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- Note: the middle-dots represent spaces --> | ||
* | ||
* ···# Hello world | ||
@@ -22,12 +38,8 @@ * | ||
* | ||
* <!-- Valid: --> | ||
* # Hello world | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* Foo | ||
* ----- | ||
* | ||
* # Hello world # | ||
* | ||
* Bar | ||
* ===== | ||
* 3:4: Remove 3 spaces before this heading | ||
* 5:2: Remove 1 space before this heading | ||
* 8:2: Remove 1 space before this heading | ||
* 10:4: Remove 3 spaces before this heading | ||
*/ | ||
@@ -37,16 +49,11 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var plural = require('plur'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noHeadingIndent; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -60,49 +67,39 @@ | ||
* @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; | ||
function noHeadingIndent(ast, file) { | ||
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; | ||
visit(ast, 'heading', function (node) { | ||
var initial = start(node); | ||
var begin = initial.offset; | ||
var index = begin - 1; | ||
var character; | ||
var diff; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
while (++index < length) { | ||
character = contents.charAt(index); | ||
while (++index < length) { | ||
character = contents.charAt(index); | ||
if (character !== ' ' && character !== '\t') { | ||
break; | ||
} | ||
} | ||
if (character !== ' ' && character !== '\t') { | ||
break; | ||
} | ||
} | ||
diff = index - begin; | ||
diff = index - begin; | ||
if (diff) { | ||
file.warn( | ||
'Remove ' + diff + ' ' + plural('space', diff) + | ||
' before this heading', | ||
{ | ||
'line': initial.line, | ||
'column': initial.column + diff | ||
} | ||
); | ||
if (diff) { | ||
file.warn( | ||
'Remove ' + diff + ' ' + plural('space', diff) + | ||
' before this heading', | ||
{ | ||
line: initial.line, | ||
column: initial.column + diff | ||
} | ||
}); | ||
done(); | ||
); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noHeadingIndent; |
@@ -14,4 +14,13 @@ /** | ||
* be careful for escapes and dashes. | ||
* @example | ||
* <!-- Invalid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* # Hello | ||
* | ||
* @example {"name": "valid.md", "setting": ",;:!?"} | ||
* | ||
* # Hello... | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* # Hello: | ||
@@ -27,4 +36,9 @@ * | ||
* | ||
* <!-- Valid: --> | ||
* # Hello | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:9: Don’t add a trailing `:` to headings | ||
* 3:1-3:9: Don’t add a trailing `?` to headings | ||
* 5:1-5:9: Don’t add a trailing `!` to headings | ||
* 7:1-7:9: Don’t add a trailing `,` to headings | ||
* 9:1-9:9: Don’t add a trailing `;` to headings | ||
*/ | ||
@@ -34,12 +48,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
var toString = require('mdast-util-to-string'); | ||
/* Expose. */ | ||
module.exports = noHeadingPunctuation; | ||
/** | ||
@@ -51,28 +63,19 @@ * Warn when headings end in some characters. | ||
* @param {string?} [preferred='\\.,;:!?'] - Group of characters. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noHeadingPunctuation(ast, file, preferred, done) { | ||
preferred = typeof preferred === 'string' ? preferred : '\\.,;:!?'; | ||
function noHeadingPunctuation(ast, file, preferred) { | ||
preferred = typeof preferred === 'string' ? preferred : '\\.,;:!?'; | ||
visit(ast, 'heading', function (node) { | ||
var value = toString(node); | ||
visit(ast, 'heading', function (node) { | ||
var value = toString(node); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
value = value.charAt(value.length - 1); | ||
value = value.charAt(value.length - 1); | ||
if (new RegExp('[' + preferred + ']').test(value)) { | ||
file.warn('Don’t add a trailing `' + value + '` to headings', node); | ||
} | ||
}); | ||
done(); | ||
if (new RegExp('[' + preferred + ']').test(value)) { | ||
file.warn('Don’t add a trailing `' + value + '` to headings', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noHeadingPunctuation; |
@@ -11,8 +11,16 @@ /** | ||
* because markdown doesn’t have native comments. | ||
* @example | ||
* <!-- Invalid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* # Hello | ||
* | ||
* <!--Comments are also OK--> | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <h1>Hello</h1> | ||
* | ||
* <!-- Valid: --> | ||
* # Hello | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:15: Do not use HTML in markdown | ||
*/ | ||
@@ -22,11 +30,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = html; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -41,11 +47,9 @@ * Warn when HTML nodes are used. | ||
function html(ast, file, preferred, done) { | ||
visit(ast, 'html', function (node) { | ||
if (!position.generated(node) && !/^\s*<!--/.test(node.value)) { | ||
file.warn('Do not use HTML in markdown', node); | ||
} | ||
}); | ||
visit(ast, 'html', function (node) { | ||
if (!position.generated(node) && !/^\s*<!--/.test(node.value)) { | ||
file.warn('Do not use HTML in markdown', node); | ||
} | ||
}); | ||
done(); | ||
done(); | ||
} | ||
module.exports = html; |
@@ -11,8 +11,16 @@ /** | ||
* Warns for emphasis, strong, delete, image, and link. | ||
* @example | ||
* <!-- Invalid: --> | ||
* * Hello *, [ world ](http://foo.bar/baz) | ||
* | ||
* <!-- Valid: --> | ||
* *Hello*, [world](http://foo.bar/baz) | ||
* @example {"name": "valid.md"} | ||
* | ||
* Alpha, *bravo*, _charlie_, [delta](http://echo.fox/trot) | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* Alpha, * bravo *, _ charlie _, [ delta ](http://echo.fox/trot) | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:8-1:17: Don’t pad `emphasis` with inner spaces | ||
* 1:19-1:30: Don’t pad `emphasis` with inner spaces | ||
* 1:32-1:63: Don’t pad `link` with inner spaces | ||
*/ | ||
@@ -22,12 +30,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
var toString = require('mdast-util-to-string'); | ||
/* Expose. */ | ||
module.exports = noInlinePadding; | ||
/** | ||
@@ -39,36 +45,26 @@ * Warn when inline nodes are padded with spaces between | ||
* @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; | ||
function noInlinePadding(ast, file) { | ||
visit(ast, function (node) { | ||
var type = node.type; | ||
var contents; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if ( | ||
type === 'emphasis' || | ||
type === 'strong' || | ||
type === 'delete' || | ||
type === 'image' || | ||
type === 'link' | ||
) { | ||
contents = toString(node); | ||
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(); | ||
if (contents.charAt(0) === ' ' || contents.charAt(contents.length - 1) === ' ') { | ||
file.warn('Don’t pad `' + type + '` with inner spaces', node); | ||
} | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noInlinePadding; |
@@ -8,8 +8,17 @@ /** | ||
* Warn when URLs without angle-brackets are used. | ||
* @example | ||
* <!-- Invalid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* <http://foo.bar/baz> | ||
* <mailto:qux@quux.com> | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* http://foo.bar/baz | ||
* | ||
* <!-- Valid: --> | ||
* <http://foo.bar/baz> | ||
* mailto:qux@quux.com | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:19: Don’t use literal URLs without angle brackets | ||
*/ | ||
@@ -19,23 +28,15 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var toString = require('mdast-util-to-string'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noLiteralURLs; | ||
/* Methods. */ | ||
var start = position.start; | ||
var end = position.end; | ||
/* | ||
* Constants. | ||
*/ | ||
/* Constants. */ | ||
var MAILTO = 'mailto:'; | ||
@@ -48,33 +49,23 @@ | ||
* @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; | ||
var value = toString(node); | ||
function noLiteralURLs(ast, file) { | ||
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; | ||
var value = toString(node); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if ( | ||
initial === head && | ||
final === tail && | ||
(value === node.url || value == MAILTO + node.url) | ||
) { | ||
file.warn('Don’t use literal URLs without angle brackets', node); | ||
} | ||
}); | ||
done(); | ||
if ( | ||
initial === head && | ||
final === tail && | ||
(node.url === MAILTO + value || node.url === value) | ||
) { | ||
file.warn('Don’t use literal URLs without angle brackets', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noLiteralURLs; |
@@ -8,11 +8,17 @@ /** | ||
* Warn for missing blank lines before a block node. | ||
* @example | ||
* <!-- Invalid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* # Foo | ||
* | ||
* ## Bar | ||
* | ||
* <!-- Valid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* # Foo | ||
* ## Bar | ||
* | ||
* ## Bar | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 2:1-2:7: Missing blank line before block node | ||
*/ | ||
@@ -22,30 +28,8 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/** | ||
* Check if `node` is an applicable block-level node. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @return {boolean} - Whether or not `node` is applicable. | ||
*/ | ||
function isApplicable(node) { | ||
return [ | ||
'paragraph', | ||
'blockquote', | ||
'heading', | ||
'code', | ||
'yaml', | ||
'html', | ||
'list', | ||
'table', | ||
'thematicBreak' | ||
].indexOf(node.type) !== -1; | ||
} | ||
/* Expose. */ | ||
module.exports = noMissingBlankLines; | ||
@@ -58,30 +42,40 @@ /** | ||
* @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]; | ||
function noMissingBlankLines(ast, file) { | ||
visit(ast, function (node, index, parent) { | ||
var next = parent && parent.children[index + 1]; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(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(); | ||
if ( | ||
next && | ||
isApplicable(node) && | ||
isApplicable(next) && | ||
position.start(next).line === position.end(node).line + 1 | ||
) { | ||
file.warn('Missing blank line before block node', next); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
/** | ||
* Check if `node` is an applicable block-level node. | ||
* | ||
* @param {Node} node - Node to test. | ||
* @return {boolean} - Whether or not `node` is applicable. | ||
*/ | ||
module.exports = noMissingBlankLines; | ||
function isApplicable(node) { | ||
return [ | ||
'paragraph', | ||
'blockquote', | ||
'heading', | ||
'code', | ||
'yaml', | ||
'html', | ||
'list', | ||
'table', | ||
'thematicBreak' | ||
].indexOf(node.type) !== -1; | ||
} |
@@ -10,12 +10,18 @@ /** | ||
* Options: `number`, default: `1`. | ||
* @example | ||
* <!-- Invalid, when set to `1` --> | ||
* | ||
* @example {"name": "valid.md", "setting": 1} | ||
* | ||
* # Foo | ||
* | ||
* # Bar | ||
* ## Bar | ||
* | ||
* <!-- Valid, when set to `1` --> | ||
* @example {"name": "invalid.md", "setting": 1, "label": "input"} | ||
* | ||
* # Foo | ||
* | ||
* ## Bar | ||
* # Bar | ||
* | ||
* @example {"name": "invalid.md", "setting": 1, "label": "output"} | ||
* | ||
* 3:1-3:6: Don’t use multiple top level headings (3:1) | ||
*/ | ||
@@ -25,11 +31,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noMultipleToplevelHeadings; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -43,27 +47,23 @@ * Warn when multiple top-level headings are used. | ||
*/ | ||
function noMultipleToplevelHeadings(ast, file, preferred, done) { | ||
var style = preferred && preferred !== true ? preferred : 1; | ||
var topLevelheading = false; | ||
function noMultipleToplevelHeadings(ast, file, preferred) { | ||
var style = preferred ? preferred : 1; | ||
var topLevelheading = false; | ||
visit(ast, 'heading', function (node) { | ||
var pos; | ||
visit(ast, 'heading', function (node) { | ||
var pos; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (node.depth === style) { | ||
if (topLevelheading) { | ||
pos = position.start(node); | ||
if (node.depth === style) { | ||
if (topLevelheading) { | ||
pos = position.start(node); | ||
file.warn('Don’t use multiple top level headings (' + pos.line + ':' + pos.column + ')', node); | ||
} | ||
file.warn('Don’t use multiple top level headings (' + pos.line + ':' + pos.column + ')', node); | ||
} | ||
topLevelheading = node; | ||
} | ||
}); | ||
done(); | ||
topLevelheading = node; | ||
} | ||
}); | ||
} | ||
module.exports = noMultipleToplevelHeadings; |
@@ -11,10 +11,5 @@ /** | ||
* flag. | ||
* @example | ||
* <!-- Invalid: --> | ||
* ```bash | ||
* $ echo a | ||
* $ echo a > file | ||
* ``` | ||
* | ||
* <!-- Valid: --> | ||
* @example {"name": "valid.md"} | ||
* | ||
* ```sh | ||
@@ -25,3 +20,2 @@ * echo a | ||
* | ||
* <!-- Also valid: --> | ||
* ```zsh | ||
@@ -32,2 +26,13 @@ * $ echo a | ||
* ``` | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* ```bash | ||
* $ echo a | ||
* $ echo a > file | ||
* ``` | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-4:4: Do not use dollar signs before shell-commands | ||
*/ | ||
@@ -37,29 +42,24 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noShellDollars; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
* List of shell script file extensions (also used as code | ||
/* 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 | ||
*/ | ||
* https://github.com/github/linguist/blob/5bf8cf5/lib/ | ||
* linguist/languages.yml#L3002. */ | ||
var flags = [ | ||
'sh', | ||
'bash', | ||
'bats', | ||
'cgi', | ||
'command', | ||
'fcgi', | ||
'ksh', | ||
'tmux', | ||
'tool', | ||
'zsh' | ||
'sh', | ||
'bash', | ||
'bats', | ||
'cgi', | ||
'command', | ||
'fcgi', | ||
'ksh', | ||
'tmux', | ||
'tool', | ||
'zsh' | ||
]; | ||
@@ -72,37 +72,24 @@ | ||
* @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; | ||
function noShellDollars(ast, file) { | ||
visit(ast, 'code', function (node) { | ||
var language = node.lang; | ||
var value = node.value; | ||
var warn; | ||
if (!language || position.generated(node)) { | ||
return; | ||
} | ||
if (!language || position.generated(node)) { | ||
return; | ||
} | ||
/* | ||
* Check both known shell-code and unknown code. | ||
*/ | ||
/* 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 (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(); | ||
if (warn) { | ||
file.warn('Do not use dollar signs before shell-commands', node); | ||
} | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noShellDollars; |
@@ -8,4 +8,11 @@ /** | ||
* Warn when shortcut reference images are used. | ||
* @example | ||
* <!-- Invalid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* ![foo][] | ||
* | ||
* [foo]: http://foo.bar/baz.png | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* ![foo] | ||
@@ -15,6 +22,5 @@ * | ||
* | ||
* <!-- Valid: --> | ||
* ![foo][] | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* [foo]: http://foo.bar/baz.png | ||
* 1:1-1:7: Use the trailing [] on reference images | ||
*/ | ||
@@ -24,11 +30,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noShortcutReferenceImage; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -39,23 +43,13 @@ * Warn when shortcut reference images are used. | ||
* @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.generated(node)) { | ||
return; | ||
} | ||
function noShortcutReferenceImage(ast, file) { | ||
visit(ast, 'imageReference', function (node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (node.referenceType === 'shortcut') { | ||
file.warn('Use the trailing [] on reference images', node); | ||
} | ||
}); | ||
done(); | ||
if (node.referenceType === 'shortcut') { | ||
file.warn('Use the trailing [] on reference images', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noShortcutReferenceImage; |
@@ -8,4 +8,11 @@ /** | ||
* Warn when shortcut reference links are used. | ||
* @example | ||
* <!-- Invalid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* [foo][] | ||
* | ||
* [foo]: http://foo.bar/baz | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* [foo] | ||
@@ -15,6 +22,5 @@ * | ||
* | ||
* <!-- Valid: --> | ||
* [foo][] | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* [foo]: http://foo.bar/baz | ||
* 1:1-1:6: Use the trailing [] on reference links | ||
*/ | ||
@@ -24,11 +30,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noShortcutReferenceLink; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -39,23 +43,13 @@ * Warn when shortcut reference links are used. | ||
* @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.generated(node)) { | ||
return; | ||
} | ||
function noShortcutReferenceLink(ast, file) { | ||
visit(ast, 'linkReference', function (node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (node.referenceType === 'shortcut') { | ||
file.warn('Use the trailing [] on reference links', node); | ||
} | ||
}); | ||
done(); | ||
if (node.referenceType === 'shortcut') { | ||
file.warn('Use the trailing [] on reference links', node); | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noShortcutReferenceLink; |
@@ -8,12 +8,23 @@ /** | ||
* Warn when tables are indented. | ||
* @example | ||
* <!-- Invalid: --> | ||
* | A | B | | ||
* | ----- | ----- | | ||
* | Alpha | Bravo | | ||
* | ||
* <!-- Valid: --> | ||
* @example {"name": "valid.md"} | ||
* | ||
* Paragraph. | ||
* | ||
* | A | B | | ||
* | ----- | ----- | | ||
* | Alpha | Bravo | | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* Paragraph. | ||
* | ||
* | A | B | | ||
* | ----- | ----- | | ||
* | Alpha | Bravo | | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:1-3:21: Do not indent table rows | ||
* 5:1-5:21: Do not indent table rows | ||
*/ | ||
@@ -23,11 +34,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noTableIndentation; | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
/** | ||
@@ -38,29 +47,19 @@ * Warn when a table has a too much indentation. | ||
* @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(); | ||
function noTableIndentation(ast, file) { | ||
visit(ast, 'table', function (node) { | ||
var contents = file.toString(); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
node.children.forEach(function (row) { | ||
var fence = contents.slice(position.start(row).offset, position.start(row.children[0]).offset); | ||
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); | ||
} | ||
}); | ||
if (fence.indexOf('|') > 1) { | ||
file.warn('Do not indent table rows', row); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noTableIndentation; |
@@ -8,13 +8,38 @@ /** | ||
* Warn when hard-tabs instead of spaces | ||
* @example | ||
* <!-- Note: the double guillemet (`»`) and middle-dots represent a tab --> | ||
* <!-- Invalid: --> | ||
* Foo»Bar | ||
* | ||
* »···Foo | ||
* @example {"name": "valid.md"} | ||
* | ||
* <!-- Valid: --> | ||
* Foo Bar | ||
* | ||
* Foo | ||
* | ||
* @example {"name": "invalid.md", "label": "input", "config": {"positionless": true}} | ||
* | ||
* <!-- Note: the guillemets represent tabs --> | ||
* | ||
* »Here's one before a code block. | ||
* | ||
* Here's a tab:», and here is another:». | ||
* | ||
* And this is in `inline»code`. | ||
* | ||
* >»This is in a block quote. | ||
* | ||
* *»And... | ||
* | ||
* »1.»in a list. | ||
* | ||
* And this is a tab as the last character.» | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:1: Use spaces instead of hard-tabs | ||
* 5:14: Use spaces instead of hard-tabs | ||
* 5:37: Use spaces instead of hard-tabs | ||
* 7:23: Use spaces instead of hard-tabs | ||
* 9:2: Use spaces instead of hard-tabs | ||
* 11:2: Use spaces instead of hard-tabs | ||
* 13:1: Use spaces instead of hard-tabs | ||
* 13:4: Use spaces instead of hard-tabs | ||
* 15:41: Use spaces instead of hard-tabs | ||
*/ | ||
@@ -24,10 +49,8 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var vfileLocation = require('vfile-location'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noTabs; | ||
var vfileLocation = require('vfile-location'); | ||
/** | ||
@@ -42,20 +65,14 @@ * Warn when hard-tabs instead of spaces are used. | ||
function noTabs(ast, file, preferred, done) { | ||
var content = file.toString(); | ||
var location = vfileLocation(file); | ||
var index = -1; | ||
var length = content.length; | ||
var content = file.toString(); | ||
var location = vfileLocation(file); | ||
var index = -1; | ||
var length = content.length; | ||
while (++index < length) { | ||
if (content.charAt(index) === '\t') { | ||
file.warn('Use spaces instead of hard-tabs', location.toPosition(index)); | ||
} | ||
while (++index < length) { | ||
if (content.charAt(index) === '\t') { | ||
file.warn('Use spaces instead of hard-tabs', location.toPosition(index)); | ||
} | ||
} | ||
done(); | ||
done(); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noTabs; |
@@ -5,7 +5,8 @@ /** | ||
* @license MIT | ||
* @module no-unused-definitions | ||
* @module no-undefined-references | ||
* @fileoverview | ||
* Warn when references to undefined definitions are found. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* [foo][] | ||
@@ -15,4 +16,9 @@ * | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* [bar][] | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:8: Found reference to undefined definition | ||
*/ | ||
@@ -22,11 +28,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var position = require('unist-util-position'); | ||
var visit = require('unist-util-visit'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noUnusedDefinitions; | ||
var position = require('mdast-util-position'); | ||
var visit = require('unist-util-visit'); | ||
/** | ||
@@ -37,50 +41,42 @@ * Warn when references to undefined definitions are found. | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noUnusedDefinitions(ast, file, preferred, done) { | ||
var map = {}; | ||
function noUnusedDefinitions(ast, file) { | ||
var map = {}; | ||
/** | ||
* Check `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function mark(node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'definition', mark); | ||
visit(ast, 'footnoteDefinition', mark); | ||
map[node.identifier.toUpperCase()] = true; | ||
} | ||
visit(ast, 'imageReference', find); | ||
visit(ast, 'linkReference', find); | ||
visit(ast, 'footnoteReference', find); | ||
/** | ||
* Mark `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function find(node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
return; | ||
if (!map[node.identifier.toUpperCase()]) { | ||
file.warn('Found reference to undefined definition', node); | ||
} | ||
/** | ||
* Check `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function mark(node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'definition', mark); | ||
visit(ast, 'footnoteDefinition', mark); | ||
map[node.identifier.toUpperCase()] = true; | ||
} | ||
visit(ast, 'imageReference', find); | ||
visit(ast, 'linkReference', find); | ||
visit(ast, 'footnoteReference', find); | ||
/** | ||
* Mark `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function find(node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
done(); | ||
if (!map[node.identifier.toUpperCase()]) { | ||
file.warn('Found reference to undefined definition', node); | ||
} | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noUnusedDefinitions; |
@@ -8,4 +8,5 @@ /** | ||
* Warn when unused definitions are found. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* [foo][] | ||
@@ -15,4 +16,9 @@ * | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* [bar]: https://example.com | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1-1:27: Found unused definition | ||
*/ | ||
@@ -22,11 +28,9 @@ | ||
/* eslint-env commonjs */ | ||
/* Dependencies. */ | ||
var position = require('unist-util-position'); | ||
var visit = require('unist-util-visit'); | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Expose. */ | ||
module.exports = noUnusedDefinitions; | ||
var position = require('mdast-util-position'); | ||
var visit = require('unist-util-visit'); | ||
/** | ||
@@ -37,60 +41,52 @@ * Warn when unused definitions are found. | ||
* @param {File} file - Virtual file. | ||
* @param {*} preferred - Ignored. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function noUnusedDefinitions(ast, file, preferred, done) { | ||
var map = {}; | ||
var identifier; | ||
function noUnusedDefinitions(ast, file) { | ||
var map = {}; | ||
var identifier; | ||
/** | ||
* Check `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function find(node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'definition', find); | ||
visit(ast, 'footnoteDefinition', find); | ||
map[node.identifier.toUpperCase()] = { | ||
'node': node, | ||
'used': false | ||
}; | ||
visit(ast, 'imageReference', mark); | ||
visit(ast, 'linkReference', mark); | ||
visit(ast, 'footnoteReference', mark); | ||
for (identifier in map) { | ||
if (!map[identifier].used) { | ||
file.warn('Found unused definition', map[identifier].node); | ||
} | ||
} | ||
/** | ||
* Mark `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function mark(node) { | ||
var info = map[node.identifier.toUpperCase()]; | ||
return; | ||
if (position.generated(node) || !info) { | ||
return; | ||
} | ||
info.used = true; | ||
/** | ||
* Check `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function find(node) { | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'definition', find); | ||
visit(ast, 'footnoteDefinition', find); | ||
map[node.identifier.toUpperCase()] = { | ||
node: node, | ||
used: false | ||
}; | ||
} | ||
visit(ast, 'imageReference', mark); | ||
visit(ast, 'linkReference', mark); | ||
visit(ast, 'footnoteReference', mark); | ||
/** | ||
* Mark `node`. | ||
* | ||
* @param {Node} node - Node. | ||
*/ | ||
function mark(node) { | ||
var info = map[node.identifier.toUpperCase()]; | ||
for (identifier in map) { | ||
if (!map[identifier].used) { | ||
file.warn('Found unused definition', map[identifier].node); | ||
} | ||
if (position.generated(node) || !info) { | ||
return; | ||
} | ||
done(); | ||
info.used = true; | ||
} | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = noUnusedDefinitions; |
@@ -18,4 +18,7 @@ /** | ||
* style. | ||
* @example | ||
* <!-- Valid when set to `consistent` or `.` --> | ||
* | ||
* @example {"name": "valid.md", "setting": "."} | ||
* | ||
* <!-- This is also valid when `consistent`. --> | ||
* | ||
* 1. Foo | ||
@@ -25,6 +28,24 @@ * | ||
* | ||
* <!-- Valid when set to `consistent` or `)` --> | ||
* @example {"name": "valid.md", "setting": ")", "config": {"commonmark": true}} | ||
* | ||
* <!-- This is also valid when `consistent`. | ||
* But it does require commonmark. --> | ||
* | ||
* 1) Foo | ||
* | ||
* 2) Bar | ||
* | ||
* @example {"name": "invalid.md", "label": "input", "config": {"commonmark": true}} | ||
* | ||
* 1. Foo | ||
* | ||
* 2) Bar | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:1-3:8: Marker style should be `.` | ||
* | ||
* @example {"name": "invalid.md", "label": "output", "setting": "!", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid ordered list-item marker style `!`: use either `'.'` or `')'` | ||
*/ | ||
@@ -34,25 +55,17 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = orderedListMarkerStyle; | ||
/* Methods. */ | ||
var start = position.start; | ||
/* | ||
* Valid styles. | ||
*/ | ||
/* Valid styles. */ | ||
var STYLES = { | ||
')': true, | ||
'.': true, | ||
'null': true | ||
')': true, | ||
'.': true, | ||
'null': true | ||
}; | ||
@@ -69,56 +82,42 @@ | ||
* first found style. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function orderedListMarkerStyle(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function orderedListMarkerStyle(ast, file, preferred) { | ||
var contents = file.toString(); | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid ordered list-item marker style `' + preferred + '`: use either `\'.\'` or `\')\'`'); | ||
done(); | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid ordered list-item marker style `' + preferred + '`: use either `\'.\'` or `\')\'`'); | ||
return; | ||
} | ||
return; | ||
visit(ast, 'list', function (node) { | ||
var items = node.children; | ||
if (!node.ordered) { | ||
return; | ||
} | ||
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 marker; | ||
if (!node.ordered) { | ||
return; | ||
} | ||
if (position.generated(item)) { | ||
return; | ||
} | ||
items.forEach(function (item) { | ||
var head = item.children[0]; | ||
var initial = start(item).offset; | ||
var final = start(head).offset; | ||
var marker; | ||
marker = contents.slice(initial, final).replace(/\s|\d/g, ''); | ||
if (position.generated(item)) { | ||
return; | ||
} | ||
/* Support checkboxes. */ | ||
marker = marker.replace(/\[[x ]?\]\s*$/i, ''); | ||
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); | ||
} | ||
}); | ||
if (!preferred) { | ||
preferred = marker; | ||
} else if (marker !== preferred) { | ||
file.warn('Marker style should be `' + preferred + '`', item); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = orderedListMarkerStyle; |
@@ -17,4 +17,5 @@ /** | ||
* should always be `1`. | ||
* @example | ||
* <!-- Valid when set to `one`: --> | ||
* | ||
* @example {"name": "valid.md", "setting": "one"} | ||
* | ||
* 1. Foo | ||
@@ -24,2 +25,4 @@ * 1. Bar | ||
* | ||
* Paragraph. | ||
* | ||
* 1. Alpha | ||
@@ -29,3 +32,4 @@ * 1. Bravo | ||
* | ||
* <!-- Valid when set to `single`: --> | ||
* @example {"name": "valid.md", "setting": "single"} | ||
* | ||
* 1. Foo | ||
@@ -35,2 +39,4 @@ * 1. Bar | ||
* | ||
* Paragraph. | ||
* | ||
* 3. Alpha | ||
@@ -40,3 +46,4 @@ * 3. Bravo | ||
* | ||
* <!-- Valid when set to `ordered`: --> | ||
* @example {"name": "valid.md", "setting": "ordered"} | ||
* | ||
* 1. Foo | ||
@@ -46,5 +53,29 @@ * 2. Bar | ||
* | ||
* Paragraph. | ||
* | ||
* 3. Alpha | ||
* 4. Bravo | ||
* 5. Charlie | ||
* | ||
* @example {"name": "invalid.md", "setting": "one", "label": "input"} | ||
* | ||
* 1. Foo | ||
* 2. Bar | ||
* | ||
* @example {"name": "invalid.md", "setting": "one", "label": "output"} | ||
* | ||
* 2:1-2:8: Marker should be `1`, was `2` | ||
* | ||
* @example {"name": "invalid.md", "setting": "ordered", "label": "input"} | ||
* | ||
* 1. Foo | ||
* 1. Bar | ||
* | ||
* @example {"name": "invalid.md", "setting": "ordered", "label": "output"} | ||
* | ||
* 2:1-2:8: Marker should be `2`, was `1` | ||
* | ||
* @example {"name": "invalid.md", "setting": "invalid", "label": "output", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid ordered list-item marker value `invalid`: use either `'ordered'` or `'one'` | ||
*/ | ||
@@ -54,25 +85,17 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = orderedListMarkerValue; | ||
/* Methods. */ | ||
var start = position.start; | ||
/* | ||
* Valid styles. | ||
*/ | ||
/* Valid styles. */ | ||
var STYLES = { | ||
'ordered': true, | ||
'single': true, | ||
'one': true | ||
ordered: true, | ||
single: true, | ||
one: true | ||
}; | ||
@@ -89,80 +112,54 @@ | ||
* defaulting to the latter. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function orderedListMarkerValue(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function orderedListMarkerValue(ast, file, preferred) { | ||
var contents = file.toString(); | ||
preferred = typeof preferred !== 'string' ? 'ordered' : preferred; | ||
preferred = typeof preferred === 'string' ? preferred : 'ordered'; | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid ordered list-item marker value `' + preferred + '`: use either `\'ordered\'` or `\'one\'`'); | ||
done(); | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid ordered list-item marker value `' + preferred + '`: use either `\'ordered\'` or `\'one\'`'); | ||
return; | ||
} | ||
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; | ||
} | ||
visit(ast, 'list', function (node) { | ||
var items = node.children; | ||
var shouldBe = (preferred === 'one' ? 1 : node.start) || 1; | ||
items.forEach(function (item, index) { | ||
var head = item.children[0]; | ||
var initial = start(item).offset; | ||
var final = start(head).offset; | ||
var marker; | ||
/* | ||
* Ignore unordered lists. | ||
*/ | ||
/* Ignore first list item. */ | ||
if (index === 0) { | ||
return; | ||
} | ||
if (!node.ordered) { | ||
return; | ||
} | ||
/* Increase the expected line number when in | ||
* `ordered` mode. */ | ||
if (preferred === 'ordered') { | ||
shouldBe++; | ||
} | ||
items.forEach(function (item, index) { | ||
var head = item.children[0]; | ||
var initial = start(item).offset; | ||
var final = start(head).offset; | ||
var marker; | ||
/* Ignore generated nodes. */ | ||
if (position.generated(item)) { | ||
return; | ||
} | ||
/* | ||
* Ignore first list item. | ||
*/ | ||
marker = contents.slice(initial, final).replace(/[\s\.\)]/g, ''); | ||
if (index === 0) { | ||
return; | ||
} | ||
/* Support checkboxes. */ | ||
marker = Number(marker.replace(/\[[x ]?\]\s*$/i, '')); | ||
/* | ||
* Increase the expected line number when in | ||
* `ordered` mode. | ||
*/ | ||
if (preferred === 'ordered') { | ||
shouldBe++; | ||
} | ||
/* | ||
* Ignore generated nodes. | ||
*/ | ||
if (position.generated(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); | ||
} | ||
}); | ||
if (marker !== shouldBe) { | ||
file.warn('Marker should be `' + shouldBe + '`, was `' + marker + '`', item); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = orderedListMarkerValue; |
@@ -13,4 +13,7 @@ /** | ||
* default: `'consistent'`. | ||
* @example | ||
* <!-- Valid when set to `consistent` or `* * *`: --> | ||
* | ||
* @example {"name": "valid.md", "setting": "* * *"} | ||
* | ||
* <!-- This is also valid when `consistent`. --> | ||
* | ||
* * * * | ||
@@ -20,6 +23,25 @@ * | ||
* | ||
* <!-- Valid when set to `consistent` or `_______`: --> | ||
* @example {"name": "valid.md", "setting": "_______"} | ||
* | ||
* <!-- This is also valid when `consistent`. --> | ||
* | ||
* _______ | ||
* | ||
* _______ | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!-- Always invalid. --> | ||
* | ||
* *** | ||
* | ||
* * * * | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 5:1-5:6: Rules should use `***` | ||
* | ||
* @example {"name": "invalid.md", "label": "output", "setting": "!!!", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid preferred rule-style: provide a valid markdown rule, or `'consistent'` | ||
*/ | ||
@@ -29,15 +51,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = ruleStyle; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -47,13 +64,2 @@ var end = position.end; | ||
/** | ||
* Warn when a given style is invalid. | ||
* | ||
* @param {*} style - `*`, `_`, ` ` (space), or `-`. | ||
* @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 | ||
@@ -66,43 +72,43 @@ * detected style. | ||
* horizontal rule, defaulting to the first found style. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function ruleStyle(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function ruleStyle(ast, file, preferred) { | ||
var contents = file.toString(); | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
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\'`'); | ||
done(); | ||
if (validateRuleStyle(preferred) !== true) { | ||
file.fail('Invalid preferred rule-style: provide a valid markdown rule, or `\'consistent\'`'); | ||
return; | ||
} | ||
return; | ||
visit(ast, 'thematicBreak', function (node) { | ||
var initial = start(node).offset; | ||
var final = end(node).offset; | ||
var hr; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'thematicBreak', function (node) { | ||
var initial = start(node).offset; | ||
var final = end(node).offset; | ||
var hr; | ||
hr = contents.slice(initial, final); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
hr = contents.slice(initial, final); | ||
if (preferred) { | ||
if (hr !== preferred) { | ||
file.warn('Rules should use `' + preferred + '`', node); | ||
} | ||
} else { | ||
preferred = hr; | ||
} | ||
}); | ||
done(); | ||
if (preferred) { | ||
if (hr !== preferred) { | ||
file.warn('Rules should use `' + preferred + '`', node); | ||
} | ||
} else { | ||
preferred = hr; | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
/** | ||
* Warn when a given style is invalid. | ||
* | ||
* @param {*} style - `*`, `_`, ` ` (space), or `-`. | ||
* @return {boolean} - Whether or not `style` is a | ||
* valid rule style. | ||
*/ | ||
module.exports = ruleStyle; | ||
function validateRuleStyle(style) { | ||
return style === null || !/[^-_* ]/.test(style); | ||
} |
@@ -5,3 +5,3 @@ /** | ||
* @license MIT | ||
* @module blockquote-indentation | ||
* @module strong-marker | ||
* @fileoverview | ||
@@ -16,10 +16,30 @@ * Warn for violating strong markers. | ||
* style. | ||
* @example | ||
* <!-- Valid when set to `consistent` or `*` --> | ||
* **foo** | ||
* **bar** | ||
* | ||
* <!-- Valid when set to `consistent` or `_` --> | ||
* __foo__ | ||
* __bar__ | ||
* @example {"name": "valid.md"} | ||
* | ||
* **foo** and **bar**. | ||
* | ||
* @example {"name": "also-valid.md"} | ||
* | ||
* __foo__ and __bar__. | ||
* | ||
* @example {"name": "valid.md", "setting": "*"} | ||
* | ||
* **foo**. | ||
* | ||
* @example {"name": "valid.md", "setting": "_"} | ||
* | ||
* __foo__. | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* **foo** and __bar__. | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:13-1:20: Strong should use `*` as a marker | ||
* | ||
* @example {"name": "invalid.md", "label": "output", "setting": "!", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid strong marker `!`: use either `'consistent'`, `'*'`, or `'_'` | ||
*/ | ||
@@ -29,19 +49,14 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Map of valid markers. | ||
*/ | ||
/* Expose. */ | ||
module.exports = strongMarker; | ||
/* Map of valid markers. */ | ||
var MARKERS = { | ||
'*': true, | ||
'_': true, | ||
'null': true | ||
'*': true, | ||
'_': true, | ||
'null': true | ||
}; | ||
@@ -56,34 +71,26 @@ | ||
* marker, either `"*"` or `"_"`, or `"consistent"`. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function strongMarker(ast, file, preferred, done) { | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
function strongMarker(ast, file, preferred) { | ||
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 (MARKERS[preferred] !== true) { | ||
file.fail('Invalid strong marker `' + preferred + '`: use either `\'consistent\'`, `\'*\'`, or `\'_\'`'); | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
visit(ast, 'strong', function (node) { | ||
var marker = file.toString().charAt(position.start(node).offset); | ||
if (preferred) { | ||
if (marker !== preferred) { | ||
file.warn('Strong should use `' + preferred + '` as a marker', node); | ||
} | ||
} else { | ||
preferred = marker; | ||
} | ||
}); | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
done(); | ||
if (preferred) { | ||
if (marker !== preferred) { | ||
file.warn('Strong should use `' + preferred + '` as a marker', node); | ||
} | ||
} else { | ||
preferred = marker; | ||
} | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = strongMarker; |
@@ -15,4 +15,7 @@ /** | ||
* style. | ||
* @example | ||
* <!-- Valid when set to `consistent` or `padded` --> | ||
* | ||
* @example {"name": "valid.md", "setting": "padded"} | ||
* | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* | A | B | | ||
@@ -22,3 +25,6 @@ * | ----- | ----- | | ||
* | ||
* <!-- Valid when set to `consistent` or `compact` --> | ||
* @example {"name": "valid.md", "setting": "compact"} | ||
* | ||
* <!--Also valid when `consistent`--> | ||
* | ||
* |A |B | | ||
@@ -28,6 +34,19 @@ * |-----|-----| | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* <!--Always invalid--> | ||
* | ||
* | A | B | | ||
* | -----| -----| | ||
* | Alpha| Bravo| | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:5: Cell should be padded, isn’t | ||
* 3:9: Cell should be padded, isn’t | ||
* 3:16: Cell should be padded, isn’t | ||
* | ||
* @example {"name": "invalid.md", "label": "output", "setting": "invalid", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid table-cell-padding style `invalid` | ||
*/ | ||
@@ -37,26 +56,20 @@ | ||
/* eslint-env commonjs */ | ||
/* eslint-disable max-params */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = tableCellPadding; | ||
/* Methods. */ | ||
var start = position.start; | ||
var end = position.end; | ||
/* | ||
* Valid styles. | ||
*/ | ||
/* Valid styles. */ | ||
var STYLES = { | ||
'null': true, | ||
'padded': true, | ||
'compact': true | ||
null: true, | ||
padded: true, | ||
compact: true | ||
}; | ||
@@ -73,119 +86,111 @@ | ||
* first found style. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function tableCellPadding(ast, file, preferred, done) { | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
function tableCellPadding(ast, file, preferred) { | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid table-cell-padding style `' + preferred + '`'); | ||
} | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid table-cell-padding style `' + preferred + '`'); | ||
return; | ||
} | ||
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; | ||
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.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(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). | ||
* | ||
* @param {number} initial - Starting index. | ||
* @param {number} final - Closing index. | ||
* @param {Node} cell - Table cell. | ||
* @param {Node?} next - Following cell. | ||
* @param {number} index - Position of `cell` in | ||
* its parent. | ||
*/ | ||
function check(initial, final, cell, next, index) { | ||
var fence = contents.slice(initial, final); | ||
var pos = fence.indexOf('|'); | ||
/** | ||
* 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). | ||
* | ||
* @param {number} initial - Starting index. | ||
* @param {number} final - Closing index. | ||
* @param {Node} cell - Table cell. | ||
* @param {Node?} next - Following cell. | ||
* @param {number} index - Position of `cell` in | ||
* its parent. | ||
*/ | ||
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 ( | ||
cell && | ||
pos !== -1 && | ||
(ends[index] === undefined || pos < ends[index]) | ||
) { | ||
ends[index] = pos; | ||
} | ||
if (next && pos !== -1) { | ||
pos = fence.length - pos - 1; | ||
if (next && pos !== -1) { | ||
pos = fence.length - pos - 1; | ||
if (starts[index + 1] === undefined || pos < starts[index + 1]) { | ||
starts[index + 1] = pos; | ||
} | ||
} | ||
if (starts[index + 1] === undefined || pos < starts[index + 1]) { | ||
starts[index + 1] = pos; | ||
} | ||
} | ||
} | ||
children.forEach(function (row) { | ||
var cells = row.children; | ||
children.forEach(function (row) { | ||
var cells = row.children; | ||
check(start(row).offset, start(cells[0]).offset, null, cells[0], -1); | ||
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; | ||
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); | ||
}); | ||
}); | ||
check(end(cell).offset, final, cell, next, index); | ||
}); | ||
}); | ||
positions = starts.concat(ends); | ||
positions = starts.concat(ends); | ||
style = preferred === 'padded' ? 1 : preferred === 'compact' ? 0 : null; | ||
if (preferred === 'padded') { | ||
style = 1; | ||
} else if (preferred === 'compact') { | ||
style = 0; | ||
} else { | ||
style = 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`. | ||
*/ | ||
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); | ||
style = Math.min(pos, 1); | ||
return true; | ||
}); | ||
} | ||
return true; | ||
}); | ||
} | ||
locations = children[0].children.map(function (cell) { | ||
return start(cell); | ||
}).concat(children[0].children.map(function (cell) { | ||
return end(cell); | ||
})); | ||
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'; | ||
type = style === 1 ? 'padded' : 'compact'; | ||
warning = 'Cell should be ' + type + ', isn’t'; | ||
positions.forEach(function (diff, index) { | ||
if (diff !== style && diff !== undefined && diff !== null) { | ||
file.warn(warning, locations[index]); | ||
} | ||
}); | ||
positions.forEach(function (diff, index) { | ||
if (diff !== style && diff !== undefined && diff !== null) { | ||
file.warn(warning, locations[index]); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = tableCellPadding; |
@@ -8,4 +8,5 @@ /** | ||
* Warn when table pipes are not aligned. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* | A | B | | ||
@@ -15,6 +16,12 @@ * | ----- | ----- | | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* | A | B | | ||
* | -- | -- | | ||
* | Alpha | Bravo | | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 3:9-3:10: Misaligned table fence | ||
* 3:17-3:18: Misaligned table fence | ||
*/ | ||
@@ -24,15 +31,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = tablePipeAlignment; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -46,67 +48,57 @@ var end = position.end; | ||
* @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; | ||
function tablePipeAlignment(ast, file) { | ||
visit(ast, 'table', function (node) { | ||
var contents = file.toString(); | ||
var indices = []; | ||
var offset; | ||
var line; | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
if (position.generated(node)) { | ||
return; | ||
} | ||
/** | ||
* Check all pipes after each column are at | ||
* aligned. | ||
* | ||
* @param {number} initial - Starting index. | ||
* @param {number} final - Closing index. | ||
* @param {number} index - Position of cell in | ||
* its parent. | ||
*/ | ||
function check(initial, final, index) { | ||
var pos = initial + contents.slice(initial, final).indexOf('|') - offset + 1; | ||
/** | ||
* Check all pipes after each column are at | ||
* aligned. | ||
* | ||
* @param {number} initial - Starting index. | ||
* @param {number} final - Closing index. | ||
* @param {number} index - Position of cell in | ||
* its parent. | ||
*/ | ||
function check(initial, final, index) { | ||
var pos = initial + contents.slice(initial, final).indexOf('|') - offset + 1; | ||
if (indices[index] === undefined || indices[index] === null) { | ||
indices[index] = pos; | ||
} else if (pos !== indices[index]) { | ||
file.warn('Misaligned table fence', { | ||
'start': { | ||
'line': line, | ||
'column': pos | ||
}, | ||
'end': { | ||
'line': line, | ||
'column': pos + 1 | ||
} | ||
}); | ||
} | ||
} | ||
if (indices[index] === undefined || indices[index] === null) { | ||
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; | ||
node.children.forEach(function (row) { | ||
var cells = row.children; | ||
line = start(row).line; | ||
offset = start(row).offset; | ||
line = start(row).line; | ||
offset = start(row).offset; | ||
check(start(row).offset, start(cells[0]).offset, 0); | ||
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; | ||
row.children.forEach(function (cell, index) { | ||
var next = start(cells[index + 1]).offset || end(row).offset; | ||
check(end(cell).offset, next, index + 1); | ||
}); | ||
}); | ||
check(end(cell).offset, next, index + 1); | ||
}); | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = tablePipeAlignment; |
@@ -8,4 +8,5 @@ /** | ||
* Warn when table rows are not fenced with pipes. | ||
* @example | ||
* <!-- Valid: --> | ||
* | ||
* @example {"name": "valid.md"} | ||
* | ||
* | A | B | | ||
@@ -15,6 +16,14 @@ * | ----- | ----- | | ||
* | ||
* <!-- Invalid: --> | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* A | B | ||
* ----- | ----- | ||
* Alpha | Bravo | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 1:1: Missing initial pipe in table fence | ||
* 1:10: Missing final pipe in table fence | ||
* 3:1: Missing initial pipe in table fence | ||
* 3:14: Missing final pipe in table fence | ||
*/ | ||
@@ -24,15 +33,10 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = tablePipes; | ||
/* Methods. */ | ||
var start = position.start; | ||
@@ -46,37 +50,27 @@ var end = position.end; | ||
* @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(); | ||
function tablePipes(ast, file) { | ||
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); | ||
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.generated(row)) { | ||
return; | ||
} | ||
if (position.generated(row)) { | ||
return; | ||
} | ||
if (initial.indexOf('|') === -1) { | ||
file.warn('Missing initial pipe in table fence', start(row)); | ||
} | ||
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)); | ||
} | ||
}); | ||
if (final.indexOf('|') === -1) { | ||
file.warn('Missing final pipe in table fence', end(row)); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = tablePipes; |
@@ -16,18 +16,29 @@ /** | ||
* style. | ||
* @example | ||
* <!-- Valid when set to `consistent` or `-` --> | ||
* - Foo | ||
* - Bar | ||
* | ||
* <!-- Valid when set to `consistent` or `*` --> | ||
* * Foo | ||
* * Bar | ||
* @example {"name": "valid.md", "setting": "*"} | ||
* | ||
* <!-- Valid when set to `consistent` or `+` --> | ||
* + Foo | ||
* + Bar | ||
* * Foo | ||
* | ||
* <!-- Never valid: --> | ||
* + Foo | ||
* - Bar | ||
* @example {"name": "valid.md", "setting": "-"} | ||
* | ||
* - Foo | ||
* | ||
* @example {"name": "valid.md", "setting": "+"} | ||
* | ||
* + Foo | ||
* | ||
* @example {"name": "invalid.md", "label": "input"} | ||
* | ||
* * Foo | ||
* - Bar | ||
* + Baz | ||
* | ||
* @example {"name": "invalid.md", "label": "output"} | ||
* | ||
* 2:1-2:6: Marker style should be `*` | ||
* 3:1-3:6: Marker style should be `*` | ||
* | ||
* @example {"name": "invalid.md", "label": "output", "setting": "!", "config": {"positionless": true}} | ||
* | ||
* 1:1: Invalid unordered list-item marker style `!`: use either `'-'`, `'*'`, or `'+'` | ||
*/ | ||
@@ -37,26 +48,18 @@ | ||
/* eslint-env commonjs */ | ||
/* | ||
* Dependencies. | ||
*/ | ||
/* Dependencies. */ | ||
var visit = require('unist-util-visit'); | ||
var position = require('mdast-util-position'); | ||
var position = require('unist-util-position'); | ||
/* | ||
* Methods. | ||
*/ | ||
/* Expose. */ | ||
module.exports = unorderedListMarkerStyle; | ||
/* Methods. */ | ||
var start = position.start; | ||
/* | ||
* Valid styles. | ||
*/ | ||
/* Valid styles. */ | ||
var STYLES = { | ||
'-': true, | ||
'*': true, | ||
'+': true, | ||
'null': true | ||
'-': true, | ||
'*': true, | ||
'+': true, | ||
'null': true | ||
}; | ||
@@ -73,56 +76,42 @@ | ||
* defaulting to the first found style. | ||
* @param {Function} done - Callback. | ||
*/ | ||
function unorderedListMarkerStyle(ast, file, preferred, done) { | ||
var contents = file.toString(); | ||
function unorderedListMarkerStyle(ast, file, preferred) { | ||
var contents = file.toString(); | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
preferred = typeof preferred !== 'string' || preferred === 'consistent' ? null : preferred; | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid unordered list-item marker style `' + preferred + '`: use either `\'-\'`, `\'*\'`, or `\'+\'`'); | ||
done(); | ||
if (STYLES[preferred] !== true) { | ||
file.fail('Invalid unordered list-item marker style `' + preferred + '`: use either `\'-\'`, `\'*\'`, or `\'+\'`'); | ||
return; | ||
} | ||
return; | ||
visit(ast, 'list', function (node) { | ||
var items = node.children; | ||
if (node.ordered) { | ||
return; | ||
} | ||
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 marker; | ||
if (node.ordered) { | ||
return; | ||
} | ||
if (position.generated(item)) { | ||
return; | ||
} | ||
items.forEach(function (item) { | ||
var head = item.children[0]; | ||
var initial = start(item).offset; | ||
var final = start(head).offset; | ||
var marker; | ||
marker = contents.slice(initial, final).replace(/\s/g, ''); | ||
if (position.generated(item)) { | ||
return; | ||
} | ||
/* Support checkboxes. */ | ||
marker = marker.replace(/\[[x ]?\]\s*$/i, ''); | ||
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); | ||
} | ||
}); | ||
if (!preferred) { | ||
preferred = marker; | ||
} else if (marker !== preferred) { | ||
file.warn('Marker style should be `' + preferred + '`', item); | ||
} | ||
}); | ||
done(); | ||
}); | ||
} | ||
/* | ||
* Expose. | ||
*/ | ||
module.exports = unorderedListMarkerStyle; |
{ | ||
"name": "remark-lint", | ||
"version": "4.0.2", | ||
"version": "4.1.0", | ||
"description": "Lint markdown with remark", | ||
@@ -12,22 +12,30 @@ "license": "MIT", | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/wooorm/remark-lint.git" | ||
}, | ||
"repository": "https://github.com/wooorm/remark-lint", | ||
"bugs": "https://github.com/wooorm/remark-lint/issues", | ||
"author": "Titus Wormer <tituswormer@gmail.com> (http://wooorm.com)", | ||
"contributors": [ | ||
"Titus Wormer <tituswormer@gmail.com> (http://wooorm.com)" | ||
"Titus Wormer <tituswormer@gmail.com> (http://wooorm.com)", | ||
"Stephan Schneider <stephanschndr@gmail.com>", | ||
"Ben Balter <ben.balter@github.com>", | ||
"Danny Arnold <despair.blue@gmail.com>", | ||
"Tony Brix <tony@brix.ninja>", | ||
"Michael Mior <michael.mior@gmail.com>", | ||
"Patrick Gilday <pcgilday@gmail.com>", | ||
"Yoshua Wuyts <yoshuawuyts@gmail.com>", | ||
"YJ Yang <chcokr@gmail.com>", | ||
"Burak Yiğit Kaya <ben@byk.im>" | ||
], | ||
"dependencies": { | ||
"decamelize": "^1.0.0", | ||
"load-plugin": "^1.1.1", | ||
"mdast-util-heading-style": "^1.0.0", | ||
"mdast-util-position": "^1.0.0", | ||
"mdast-util-to-string": "^1.0.0", | ||
"npm-prefix": "^1.1.1", | ||
"plur": "^2.0.0", | ||
"remark-message-control": "^2.0.0", | ||
"trough": "^1.0.0", | ||
"unist-util-position": "^2.0.1", | ||
"unist-util-visit": "^1.0.0", | ||
"vfile-location": "^2.0.0", | ||
"vfile-sort": "^1.0.0" | ||
"vfile-sort": "^1.0.0", | ||
"wrapped": "^1.0.1" | ||
}, | ||
@@ -40,8 +48,6 @@ "files": [ | ||
"browserify": "^13.0.0", | ||
"chalk": "^1.1.3", | ||
"dox": "^0.8.0", | ||
"eslint": "^2.0.0", | ||
"esmangle": "^1.0.0", | ||
"istanbul": "^0.4.0", | ||
"jscs": "^3.0.0", | ||
"jscs-jsdoc": "^2.0.0", | ||
"nyc": "^7.1.0", | ||
"remark": "^5.0.0", | ||
@@ -51,20 +57,62 @@ "remark-cli": "^1.0.0", | ||
"remark-github": "^5.0.0", | ||
"remark-lint-no-url-trailing-slash": "^2.0.0", | ||
"remark-toc": "^3.0.0", | ||
"remark-validate-links": "^4.0.0", | ||
"mocha": "^2.0.0", | ||
"vfile": "^1.0.0" | ||
"strip-indent": "^1.0.0", | ||
"tape": "^4.6.0", | ||
"to-vfile": "^1.0.0", | ||
"trim": "0.0.1", | ||
"unist-builder": "^1.0.2", | ||
"unist-util-remove-position": "^1.1.0", | ||
"xo": "^0.16.0" | ||
}, | ||
"scripts": { | ||
"build-rules": "node script/build-rule-documentation.js", | ||
"build-index": "node script/build-index.js", | ||
"build-rules": "node script/build-docs.js", | ||
"build-md": "remark . --quiet --frail", | ||
"build-bundle": "browserify index.js --bare -s remarkLint > remark-lint.js", | ||
"build-mangle": "esmangle remark-lint.js > remark-lint.min.js", | ||
"build": "npm run build-rules && npm run build-md && npm run build-bundle && npm run build-mangle", | ||
"lint-api": "eslint .", | ||
"lint-style": "jscs . --reporter inline", | ||
"lint": "npm run lint-api && npm run lint-style", | ||
"test-api": "mocha --check-leaks test/index.js", | ||
"test-coverage": "istanbul cover _mocha -- test/index.js", | ||
"build": "npm run build-md && npm run build-index && npm run build-rules && npm run build-bundle && npm run build-mangle", | ||
"lint": "xo", | ||
"test-api": "tape test/index.js", | ||
"test-coverage": "nyc --reporter lcov tape test/index.js", | ||
"test": "npm run build && npm run lint && npm run test-coverage" | ||
}, | ||
"nyc": { | ||
"check-coverage": true, | ||
"lines": 100, | ||
"functions": 100, | ||
"branches": 100 | ||
}, | ||
"xo": { | ||
"space": true, | ||
"rules": { | ||
"guard-for-in": "off", | ||
"no-eq-null": "off", | ||
"eqeqeq": "off" | ||
}, | ||
"ignores": [ | ||
"remark-lint.js", | ||
"remark-lint.min.js" | ||
] | ||
}, | ||
"remarkConfig": { | ||
"output": true, | ||
"plugins": { | ||
"comment-config": null, | ||
"github": null, | ||
"toc": { | ||
"tight": true, | ||
"maxDepth": 2 | ||
}, | ||
"./": { | ||
"no-missing-blank-lines": false, | ||
"list-item-spacing": false | ||
}, | ||
"validate-links": null | ||
}, | ||
"settings": { | ||
"bullet": "*" | ||
} | ||
} | ||
} |
@@ -7,4 +7,2 @@ # ![remark-lint][logo] | ||
<!--lint disable list-item-spacing--> | ||
**remark-lint** is a markdown code style linter. Another linter? Yes. | ||
@@ -24,3 +22,2 @@ Ensuring the markdown you (and contributors) write is of great quality will | ||
* [Programmatic](#programmatic) | ||
* [remark.use(lint\[, options\])](#remarkuselint-options) | ||
* [Rules](#rules) | ||
@@ -98,3 +95,2 @@ * [Configuring remark-lint](#configuring-remark-lint) | ||
```txt | ||
<stdin> | ||
1:1 warning Missing newline character at end of file final-newline | ||
@@ -115,10 +111,14 @@ 1:1-1:16 warning First heading level should be `1` first-heading-level | ||
```js | ||
{ | ||
file: '~/example.md', | ||
{ [1:1-1:16: First heading level should be `1`] | ||
message: 'First heading level should be `1`', | ||
name: '1:1-1:16', | ||
file: '', | ||
reason: 'First heading level should be `1`', | ||
line: 1, | ||
column: 1, | ||
location: Position { start: [Object], end: [Object] }, | ||
location: { | ||
start: { line: 1, column: 1, offset: 0 }, | ||
end: { line: 1, column: 16, offset: 15 } }, | ||
fatal: false, | ||
ruleId: 'first-heading-level', | ||
fatal: false, | ||
source: 'remark-lint' } | ||
@@ -220,2 +220,8 @@ ``` | ||
If you want to run all of **remark** from **Atom**, use | ||
[**linter-remark**][linter-remark]. | ||
To run **remark**, optionally with **remark-lint** from **Gulp**, use | ||
[**gulp-remark**][gulp-remark]. | ||
I’m very interested in more integrations. Let me know if I can help. | ||
@@ -225,2 +231,6 @@ | ||
External rules can be loaded through the [`external` setting][external]. | ||
Learn how to create and use external rules in [`doc/external.md`][doc-external]. | ||
<!-- | ||
@@ -258,9 +268,9 @@ This list is ordered based on the name without prefix, so | ||
[build-badge]: https://img.shields.io/travis/wooorm/remark-inline-links.svg | ||
[build-badge]: https://img.shields.io/travis/wooorm/remark-lint.svg | ||
[build-status]: https://travis-ci.org/wooorm/remark-inline-links | ||
[build-status]: https://travis-ci.org/wooorm/remark-lint | ||
[coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/remark-inline-links.svg | ||
[coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/remark-lint.svg | ||
[coverage-status]: https://codecov.io/github/wooorm/remark-inline-links | ||
[coverage-status]: https://codecov.io/github/wooorm/remark-lint | ||
@@ -271,3 +281,3 @@ [chat-badge]: https://img.shields.io/gitter/room/wooorm/remark.svg | ||
[releases]: https://github.com/wooorm/remark-inline-links/releases | ||
[releases]: https://github.com/wooorm/remark-lint/releases | ||
@@ -301,1 +311,9 @@ [license]: LICENSE | ||
[vfile-message]: https://github.com/wooorm/vfile#vfilemessage | ||
[linter-remark]: https://github.com/wooorm/linter-remark | ||
[gulp-remark]: https://github.com/denysdovhan/gulp-remark | ||
[external]: doc/rules.md#external | ||
[doc-external]: doc/external.md |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
147176
4822
311
1
12
19
+ Addedload-plugin@^1.1.1
+ Addedtrough@^1.0.0
+ Addedunist-util-position@^2.0.1
+ Addedwrapped@^1.0.1
+ Addedarray-unique@0.2.1(transitive)
+ Addedco@3.1.0(transitive)
+ Addedfind-root@1.1.0(transitive)
+ Addedload-plugin@1.1.1(transitive)
+ Addedsliced@1.0.1(transitive)
+ Addedtrough@1.0.5(transitive)
+ Addedunist-util-position@2.0.1(transitive)
+ Addedwrapped@1.0.1(transitive)
- Removedmdast-util-position@^1.0.0
- Removednpm-prefix@^1.1.1
- Removedmdast-util-position@1.0.2(transitive)