├── .gitignore ├── .travis.yml ├── cli.js ├── index.js ├── lib ├── codeBlockUtils.js ├── formatter.js └── linter.js ├── package.json ├── readme.md └── tests ├── fixtures ├── clean.md ├── cleanable.md └── dirty.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | - "14" 6 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const fs = require('fs') 5 | const globby = require('globby') 6 | const path = require('path') 7 | const program = require('commander') 8 | const standardMarkdown = require('./') 9 | 10 | let patterns = [ 11 | '**/*.md', 12 | '**/*.markdown', 13 | '!**/.git/**', 14 | '!**/coverage/**', 15 | '!**/dist/**', 16 | '!**/node_modules/**', 17 | '!**/vendor/**', 18 | '!*.min.js', 19 | '!bundle.js' 20 | ] 21 | 22 | let cwd 23 | 24 | program 25 | .version(require('./package.json').version) 26 | .arguments('[cwd] [patterns...]') 27 | .option('-f, --fix', 'Attempt to fix basic standard JS issues') 28 | .option('-v, --verbose', 'Verbose mode') 29 | .action(function (cwdValue, patternArgs) { 30 | if (cwdValue == null) return 31 | // If cwd is an actual path, set it to be the cwd 32 | // Otherwise interpret it as a glob pattern 33 | if (fs.existsSync(path.resolve(cwdValue)) && fs.lstatSync(path.resolve(cwdValue)).isDirectory()) { 34 | cwd = cwdValue 35 | } else { 36 | if (cwdValue) { 37 | patterns = [cwdValue].concat(patternArgs).concat(patterns.slice(2)) 38 | } 39 | } 40 | }) 41 | .parse(process.argv) 42 | 43 | cwd = cwd || process.cwd() 44 | 45 | // The files to run our command against 46 | const files = globby.sync(patterns, { cwd: cwd }).map(function (file) { 47 | return path.resolve(cwd, file) 48 | }) 49 | 50 | let afterLint = function () {} 51 | 52 | // Auto fix the files first if we were told to 53 | if (program.fix) { 54 | afterLint = function (result) { 55 | if (result.input !== result.output) { 56 | console.log('File has changed: ' + result.file) 57 | } 58 | fs.writeFileSync(result.file, result.output) 59 | } 60 | } 61 | 62 | // Lint the files 63 | standardMarkdown[program.fix ? 'formatFiles' : 'lintFiles'](files, function (err, results) { 64 | if (err) throw err 65 | 66 | // No errors 67 | if (results.every(function (result) { return result.errors.length === 0 })) { 68 | process.exit(0) 69 | } 70 | 71 | let lastFilePath 72 | let totalErrors = 0 73 | function pad (width, string, padding) { 74 | return (width <= string.length) ? string : pad(width, string + padding, padding) 75 | } 76 | 77 | results.forEach(afterLint) 78 | 79 | // Errors! 80 | results.forEach(function (result) { 81 | totalErrors += result.errors.length 82 | result.errors.forEach(function (error) { 83 | const filepath = path.relative(cwd, result.file) 84 | if (filepath !== lastFilePath) { 85 | console.log('\n ' + filepath) 86 | } 87 | lastFilePath = filepath 88 | console.log(' ' + pad(10, error.line + ':' + error.column + ': ', ' ') + error.message) 89 | }) 90 | }) 91 | 92 | console.log('\nThere are ' + totalErrors + ' errors in "' + cwd + '"') 93 | process.exit(1) 94 | }) 95 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const formatter = require('./lib/formatter') 4 | const linter = require('./lib/linter') 5 | 6 | module.exports = { 7 | formatFiles: formatter.formatFiles, 8 | formatText: formatter.formatText, 9 | lintFiles: linter.lintFiles, 10 | lintText: linter.lintText 11 | } 12 | -------------------------------------------------------------------------------- /lib/codeBlockUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const blockOpener = /^```(js|javascript)$/mg 4 | const blockCloser = /^```$/mg 5 | 6 | // Extract all code blocks in the file 7 | function extractCodeBlocks (text) { 8 | let currentBlock = '' 9 | const blocks = [] 10 | let insideBlock = false 11 | const lines = text.split('\n') 12 | let startLine 13 | 14 | lines.forEach(function (line, index) { 15 | const originalLine = line 16 | // standard doesn't like trailing whitespace 17 | line = line.replace(/\s*$/, '') 18 | 19 | if (blockOpener.test(line)) { 20 | insideBlock = true 21 | startLine = index 22 | } 23 | if (blockCloser.test(line)) insideBlock = false 24 | if (insideBlock) { 25 | currentBlock += originalLine + '\n' 26 | } 27 | if (!insideBlock) { 28 | if (currentBlock) { 29 | currentBlock = currentBlock.replace(/^```.*(\r\n?|\n)/g, '') 30 | blocks.push({ 31 | code: currentBlock, 32 | line: startLine 33 | }) 34 | currentBlock = '' 35 | } 36 | } 37 | }) 38 | 39 | return blocks 40 | } 41 | 42 | module.exports = { 43 | extractCodeBlocks: extractCodeBlocks 44 | } 45 | -------------------------------------------------------------------------------- /lib/formatter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const linter = require('./linter') 4 | 5 | const formatter = module.exports = {} 6 | 7 | formatter.formatFiles = function (files, standardOptions, done) { 8 | if (typeof standardOptions === 'function') { 9 | done = standardOptions 10 | standardOptions = {} 11 | } 12 | return linter.lintFiles(files, Object.assign({ fix: true }, standardOptions), done) 13 | } 14 | 15 | formatter.formatText = function (text, standardOptions, done) { 16 | if (typeof standardOptions === 'function') { 17 | done = standardOptions 18 | standardOptions = {} 19 | } 20 | return linter.lintText(text, Object.assign({ fix: true }, standardOptions), done) 21 | } 22 | -------------------------------------------------------------------------------- /lib/linter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const standard = require('standard') 5 | const ora = require('ora') 6 | const { extractCodeBlocks } = require('./codeBlockUtils') 7 | const disabledRules = [ 8 | 'no-labels', 9 | 'no-lone-blocks', 10 | 'no-undef', 11 | 'no-unused-expressions', 12 | 'no-unused-vars', 13 | 'node/no-callback-literal' 14 | ] 15 | const eslintDisable = '/* eslint-disable ' + disabledRules.join(', ') + ' */\n' 16 | const linter = module.exports = {} 17 | 18 | function removeParensWrappingOrphanedObject (block) { 19 | return block.replace( 20 | /^\(([{|[][\s\S]+[}|\]])\)$/mg, 21 | '$1' 22 | ) 23 | } 24 | 25 | function wrapOrphanObjectInParens (block) { 26 | return block.replace( 27 | /^([{|[][\s\S]+[}|\]])$/mg, 28 | '($1)' 29 | ) 30 | } 31 | 32 | linter.lintText = function (text, standardOptions, done) { 33 | let outputText = text 34 | 35 | const blocks = extractCodeBlocks(text) 36 | if (typeof standardOptions === 'function') { 37 | done = standardOptions 38 | standardOptions = {} 39 | } 40 | 41 | return Promise.all(blocks.map((block) => { 42 | const ignoredBlock = eslintDisable + wrapOrphanObjectInParens(block.code) 43 | return new Promise((resolve, reject) => { 44 | standard.lintText(ignoredBlock, standardOptions, (err, results) => { 45 | if (err) return reject(err) 46 | results.originalLine = block.line 47 | results.originalText = block 48 | return resolve(results) 49 | }) 50 | }) 51 | })).then((results) => { 52 | results = results.map((r) => { 53 | return r.results.map((res) => { 54 | if (res.output) { 55 | outputText = outputText.replace( 56 | r.originalText.code, 57 | removeParensWrappingOrphanedObject(res.output.replace(eslintDisable, '')) 58 | ) 59 | } 60 | return res.messages.map((message) => { 61 | // We added an extra line to the top of the "file" so we need to remove one here 62 | message.line += r.originalLine 63 | return message 64 | }) 65 | }) 66 | }) 67 | results = results.flat(Infinity) 68 | return done(null, results, outputText) 69 | }).catch((err) => { 70 | return done(err) 71 | }) 72 | } 73 | 74 | linter.lintFiles = function (files, standardOptions, done) { 75 | let index = 0 76 | const spinner = (files.length > 3) ? ora().start() : {} 77 | 78 | if (typeof standardOptions === 'function') { 79 | done = standardOptions 80 | standardOptions = {} 81 | } 82 | return Promise.all(files.map((file) => { 83 | spinner.text = 'Linting file ' + (++index) + ' of ' + files.length 84 | 85 | return new Promise((resolve, reject) => { 86 | linter.lintText(fs.readFileSync(file, 'utf8'), standardOptions, (err, errors, outputText) => { 87 | if (err) return reject(err) 88 | return resolve({ 89 | file: file, 90 | errors: errors, 91 | input: fs.readFileSync(file, 'utf8'), 92 | output: outputText 93 | }) 94 | }) 95 | }) 96 | })).then((results) => { 97 | console.log('\n') 98 | return done(null, results) 99 | }).catch((err) => { 100 | return done(err) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "standard-markdown", 3 | "version": "7.1.0", 4 | "description": "Test your Markdown files for Standard JavaScript Style™", 5 | "main": "index.js", 6 | "bin": { 7 | "standard-markdown": "cli.js" 8 | }, 9 | "scripts": { 10 | "test": "node tests/index.js | tap-spec && standard" 11 | }, 12 | "repository": "https://github.com/zeke/standard-markdown", 13 | "keywords": [ 14 | "standard", 15 | "lint", 16 | "linter", 17 | "markdown", 18 | "code", 19 | "snippet", 20 | "javascript" 21 | ], 22 | "author": "Zeke Sikelianos (http://zeke.sikelianos.com)", 23 | "license": "MIT", 24 | "dependencies": { 25 | "commander": "^4.1.0", 26 | "globby": "^11.0.0", 27 | "ora": "^4.0.3", 28 | "standard": "^16.0.3" 29 | }, 30 | "devDependencies": { 31 | "tap-spec": "^5.0.0", 32 | "tape": "^4.13.0" 33 | }, 34 | "engines": { 35 | "node": ">=11" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # standard-markdown [![Build Status](https://travis-ci.org/zeke/standard-markdown.svg?branch=master)](https://travis-ci.org/zeke/standard-markdown) 2 | 3 | Test your Markdown files for Standard JavaScript Style™ 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install standard-markdown --save 9 | ``` 10 | 11 | ## Usage 12 | ### Linting 13 | 14 | This module works just like `standard`, but instead of linting javascript files, it lints GitHub-Flavored `js` and `javascript` code blocks inside markdown files. 15 | 16 | Lint everything in the current directory: 17 | 18 | ```sh 19 | standard-markdown 20 | ``` 21 | 22 | Or lint some other directory: 23 | 24 | ```sh 25 | standard-markdown some/other/directory 26 | ``` 27 | 28 | All files with `.md` or `.markdown` extension are linted, and the following directories are ignored: 29 | 30 | - `.git` 31 | - `node_modules` 32 | - `vendor` 33 | 34 | If you want to specify which files to lint / which files to ignore you can use glob patterns 35 | 36 | ```sh 37 | # This will lint everything in some/directory except everything in some/directory/api 38 | standard-markdown some/directory **/*.md !api/**/*.md 39 | 40 | # You also don't need to specify CWD to use globs 41 | # This will only lint markdown file in the current directory 42 | standard-markdown *.md 43 | ``` 44 | 45 | ### Fixing 46 | 47 | This module also provides the ability to automatically fix common syntax issues like extra semicolons, bad whitespacing, etc. 48 | This functionality is provided by [standard](https://github.com/feross/standard#is-there-an-automatic-formatter). 49 | 50 | ```sh 51 | standard-markdown some/directory --fix 52 | ``` 53 | 54 | Once the module has attempted to fix all your issues it will run the linter on the generated files so you can see how much it fixed. 55 | 56 | ## Rules 57 | 58 | This module disables certain rules that were considered inappropriate for linting JS blocks: 59 | 60 | * [`no-labels`](http://eslint.org/docs/rules/no-labels) 61 | * [`no-lone-blocks`](http://eslint.org/docs/rules/no-lone-blocks) 62 | * [`no-undef`](http://eslint.org/docs/rules/no-undef) 63 | * [`no-unused-expressions`](http://eslint.org/docs/rules/no-unused-expressions) 64 | * [`no-unused-vars`](http://eslint.org/docs/rules/no-unused-vars) 65 | * [`standard/no-callback-literal`](https://github.com/xjamundx/eslint-plugin-standard#rules-explanations) 66 | 67 | See 68 | [#2](https://github.com/zeke/standard-markdown/issues/2), 69 | [#18](https://github.com/zeke/standard-markdown/issues/18), and 70 | [#19](https://github.com/zeke/standard-markdown/issues/19) 71 | for reasons. 72 | 73 | For more examples of what is and isn't allowed, see the 74 | [clean](/tests/fixtures/clean.md) and 75 | [dirty](/tests/fixtures/dirty.md) test fixtures. 76 | 77 | ## Tests 78 | 79 | ```sh 80 | npm install 81 | npm test 82 | ``` 83 | 84 | ## Dependencies 85 | 86 | - [commander](https://github.com/tj/commander.js): The complete solution for node.js command-line programs 87 | - [globby](https://github.com/sindresorhus/globby): Extends `glob` with support for multiple patterns and exposes a Promise API 88 | - [standard](https://github.com/feross/standard): JavaScript Standard Style 89 | 90 | ## Dev Dependencies 91 | 92 | - [tap-spec](https://github.com/scottcorgan/tap-spec): Formatted TAP output like Mocha's spec reporter 93 | - [tape](https://github.com/substack/tape): tap-producing test harness for node and browsers 94 | 95 | 96 | ## License 97 | 98 | MIT 99 | -------------------------------------------------------------------------------- /tests/fixtures/clean.md: -------------------------------------------------------------------------------- 1 | # The Clean Readme 2 | 3 | This is a markdown file with some javascript code blocks in it. 4 | 5 | ```js 6 | console.log('all good here!') 7 | ``` 8 | 9 | There are no linting errors in this file. 10 | 11 | ```javascript 12 | const wibble = 2 13 | console.log(wibble) 14 | ``` 15 | 16 | It should allow use of undefined variables 17 | 18 | ```javascript 19 | win.close() 20 | ``` 21 | 22 | It should allow creation of unused variables 23 | 24 | ```js 25 | // `BrowserWindow` is declared but not used 26 | const { BrowserWindow } = require('electron') 27 | ``` 28 | 29 | It should allow orphan objects: 30 | 31 | ```js 32 | { some: 'object' } 33 | ``` 34 | 35 | and this wrapping kind too: 36 | 37 | ```js 38 | { 39 | some: 'object', 40 | with: 'different whitespace and tabbing' 41 | } 42 | ``` 43 | 44 | and arrays: 45 | 46 | ```js 47 | [1, 2, 3] 48 | ``` 49 | 50 | and wrapped arrays: 51 | 52 | ```js 53 | [ 54 | 4, 55 | 5, 56 | 6 57 | ] 58 | ``` 59 | 60 | Electron docs have a bunch of non-node-style callbacks that don't have `err` as the first arg: 61 | 62 | ```javascript 63 | const { app } = require('electron') 64 | 65 | app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { 66 | if (url === 'https://github.com') { 67 | callback(true) 68 | } else { 69 | callback(false) 70 | } 71 | }) 72 | ``` 73 | -------------------------------------------------------------------------------- /tests/fixtures/cleanable.md: -------------------------------------------------------------------------------- 1 | # The Cleanable Readme 2 | 3 | This is a markdown file with some javascript code blocks in it. 4 | 5 | ```js 6 | var foo = 1; 7 | ``` 8 | 9 | Each block is parsed separately, to avoid linting errors about variable 10 | assignment. Notice that `var foo` occurs twice in this markdown file, 11 | but only once in each individual snippet. 12 | 13 | The following code block has a few issues: 14 | 15 | - semicolons 16 | - type-insensitive equality comparison 17 | - double-quoted string 18 | 19 | ```javascript 20 | var foo = 2; 21 | console.log("foo is two"); 22 | ``` 23 | 24 | This non-js code block should be ignored by the cleaner and the linter: 25 | 26 | ```sh 27 | echo i am a shell command 28 | ``` 29 | 30 | It should allow orphan objects: 31 | 32 | ```js 33 | {some: 'object'} 34 | ``` 35 | 36 | and this wrapping kind too: 37 | 38 | ```js 39 | { 40 | some: 'object', 41 | with: 'different whitespace and tabbing' 42 | } 43 | ``` 44 | 45 | and arrays: 46 | 47 | ```js 48 | [1,2,3] 49 | ``` 50 | 51 | and wrapped arrays: 52 | 53 | ```js 54 | [ 55 | 4, 56 | 5, 57 | 6, 58 | ] 59 | ``` 60 | -------------------------------------------------------------------------------- /tests/fixtures/dirty.md: -------------------------------------------------------------------------------- 1 | # The Dirty Readme 2 | 3 | This is a markdown file with some javascript code blocks in it. 4 | 5 | ```js 6 | var foo = 1; 7 | ``` 8 | 9 | Each block is parsed separately, to avoid linting errors about variable 10 | assignment. Notice that `var foo` occurs twice in this markdown file, 11 | but only once in each individual snippet. 12 | 13 | The following code block has a few issues: 14 | 15 | - semicolons 16 | - type-insensitive equality comparison 17 | - double-quoted string 18 | 19 | ```javascript 20 | var foo = 2; 21 | if (foo == 1) console.log("foo is one"); 22 | ``` 23 | 24 | This non-js code block should be ignored by the linter: 25 | 26 | ```sh 27 | echo i am a shell command 28 | ``` 29 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const standardMarkdown = require('../') 7 | const dirty = fs.readFileSync(path.join(__dirname, 'fixtures/dirty.md'), 'utf8') 8 | const clean = fs.readFileSync(path.join(__dirname, 'fixtures/clean.md'), 'utf8') 9 | const cleanable = fs.readFileSync(path.join(__dirname, 'fixtures/cleanable.md'), 'utf8') 10 | 11 | test('standardMarkdownFormat', function (t) { 12 | t.comment('cleaning the dirty fixture') 13 | 14 | standardMarkdown.formatText(cleanable, function (cleanErr, results, cleanText) { 15 | if (cleanErr) throw cleanErr 16 | 17 | t.equal(results.length, 0, 'should remove all linting errors from the cleanable fixture') 18 | t.equal(cleanable.split('\n').length, cleanText.split('\n').length, 'should keep the same number of lines') 19 | t.ok(!/```(js|javascript)\n\(([{|[][\s\S]+[}|\]])\)\n```/mgi.test(cleanText), 'should remove the magic parenthesis when formatting') 20 | 21 | t.end() 22 | }) 23 | }) 24 | 25 | test('standardMarkdown', function (t) { 26 | standardMarkdown.lintText(dirty, function (err, results) { 27 | if (err) throw err 28 | 29 | t.comment('dirty fixture') 30 | t.equal(results.length, 7, 'returns 7 linting errors') 31 | 32 | t.equal(results[0].message, 'Unexpected var, use let or const instead.') 33 | t.equal(results[0].line, 6, 'identifies correct line number in first block') 34 | 35 | t.equal(results[1].message, 'Extra semicolon.', 'finds errors in second block') 36 | t.equal(results[1].line, 6, 'identifies correct line number in second block') 37 | 38 | t.equal(results[3].message, 'Extra semicolon.', 'finds errors in second block') 39 | t.equal(results[3].line, 20, 'identifies correct line number in second block') 40 | 41 | t.comment('every error') 42 | t.ok(results.every(function (result) { 43 | return result.message.length 44 | }), 'has a `message` property') 45 | t.ok(results.every(function (result) { 46 | return result.line > 0 47 | }), 'has a `line` property') 48 | t.ok(results.every(function (result) { 49 | return result.column > 0 50 | }), 'has a `column` property') 51 | t.ok(results.every(function (result) { 52 | return result.severity > 0 53 | }), 'has a `severity` property') 54 | 55 | t.comment('clean fixture') 56 | standardMarkdown.lintText(clean, function (err, results) { 57 | if (err) throw err 58 | t.equal(results.length, 0, 'has no errors') 59 | t.end() 60 | }) 61 | }) 62 | }) 63 | --------------------------------------------------------------------------------