UNPKG

tldr-lint

Version:

A linting tool to validate tldr pages

231 lines (205 loc) 7.52 kB
const fs = require('fs'); const path = require('path'); const parser = require('./tldr-parser.js').parser; const util = require('util'); const MAX_EXAMPLES = 8; module.exports.ERRORS = parser.ERRORS = { 'TLDR001': 'File should contain no leading whitespace', 'TLDR002': 'A single space should precede a sentence', 'TLDR003': 'Descriptions should start with a capital letter', 'TLDR004': 'Command descriptions should end in a period', 'TLDR005': 'Example descriptions should end in a colon with no trailing characters', 'TLDR006': 'Command name and description should be separated by an empty line', 'TLDR007': 'Example descriptions should be surrounded by empty lines', 'TLDR008': 'File should contain no trailing whitespace', 'TLDR009': 'Page should contain a newline at end of file', 'TLDR010': 'Only Unix-style line endings allowed', 'TLDR011': 'Page never contains more than a single empty line', 'TLDR012': 'Page should contain no tabs', 'TLDR013': 'Title should be alphanumeric with dashes, underscores, spaces or allowed characters', 'TLDR014': 'Page should contain no trailing whitespace', 'TLDR015': 'Example descriptions should start with a capital letter', 'TLDR016': 'Label for information link should be spelled exactly `More information: `', 'TLDR017': 'Information link should be surrounded with angle brackets', 'TLDR018': 'Page should only include a single information link', 'TLDR019': 'Page should only include a maximum of 8 examples', 'TLDR020': 'Label for additional notes should be spelled exactly `Note: `', 'TLDR021': 'Command example should not begin or end in whitespace', 'TLDR101': 'Command description probably not properly annotated', 'TLDR102': 'Example description probably not properly annotated', 'TLDR103': 'Command example is missing its closing backtick', 'TLDR104': 'Example descriptions should prefer infinitive tense (e.g. write) over present (e.g. writes) or gerund (e.g. writing)', 'TLDR105': 'There should be only one command per example', 'TLDR106': 'Page title should start with a hash (\'#\')', 'TLDR107': 'File name should end with .md extension', 'TLDR108': 'File name should not contain whitespace', 'TLDR109': 'File name should be lowercase', 'TLDR110': 'Command example should not be empty', 'TLDR111': 'File name should not contain any Windows-forbidden character' }; (function(parser) { // Prepares state for a single page. Should be called before a run. parser.init = function() { this.yy.errors = []; this.yy.page = { description: [], // can be multiple lines informationLink: [], examples: [] }; }; parser.finish = function() { }; parser.yy.ERRORS = parser.ERRORS; parser.yy.error = function(location, error) { if (!parser.ERRORS[error]) { throw new Error('Linter done goofed. \'' + error + '\' does not exist.'); } parser.yy.errors.push({ locinfo: location, code: error, description: parser.ERRORS[error] }); }; parser.yy.setTitle = function(title) { parser.yy.page.title = title; }; parser.yy.addDescription = function(description) { parser.yy.page.description.push(description); }; parser.yy.addInformationLink = function(url) { parser.yy.page.informationLink.push(url); }; parser.yy.addExample = function(description, commands) { parser.yy.page.examples.push({ description: description, commands: commands }); }; // parser.yy.parseError = function(error, hash) { // console.log(arguments); // }; parser.yy.createToken = function(token) { return { type: 'token', content: token }; }; parser.yy.createCommandText = function(text) { return { type: 'text', content: text }; }; parser.yy.initLexer = function(lexer) { lexer.pushState = function(key, condition) { if (!condition) { condition = { ctx: key, rules: lexer._currentRules() }; } lexer.conditions[key] = condition; lexer.conditionStack.push(key); }; lexer.checkNewline = function(nl, locinfo) { if (nl.match(/\r/)) { parser.yy.error(locinfo, 'TLDR010'); } }; lexer.checkTrailingWhitespace = function(nl, locinfo) { if (nl !== '') { parser.yy.error(locinfo, 'TLDR014'); } }; }; })(parser); const linter = module.exports; linter.parse = function(page) { parser.init(); parser.parse(page); parser.finish(); return parser.yy.page; }; linter.formatDescription = function(str) { return str[0].toUpperCase() + str.slice(1) + '.'; }; linter.formatExampleDescription = function(str) { return str[0].toUpperCase() + str.slice(1) + ':'; }; linter.format = function(parsedPage) { let str = ''; str += util.format('# %s', parsedPage.title); str += '\n\n'; parsedPage.description.forEach(function(line) { str += util.format('> %s', linter.formatDescription(line)); str += '\n'; }); parsedPage.informationLink.forEach(function(informationLink) { str += util.format('> More information: %s.', informationLink); str += '\n'; }); parsedPage.examples.forEach(function(example) { str += '\n'; str += util.format('- %s', linter.formatExampleDescription(example.description)); str += '\n\n'; example.commands.forEach(function(command) { str += '`'; command.forEach(function(textOrToken) { str += textOrToken.type === 'token' ? util.format('{{%s}}', textOrToken.content) : textOrToken.content; }); str += '`\n'; }); }); return str; }; linter.process = function(file, page, verbose, alsoFormat) { let success, result; try { linter.parse(page); success = true; } catch(err) { console.error(`${file}:`); console.error(err.toString()); success = false; } if (verbose) { console.log(parser.yy.page.description.length + ' line(s) of description'); console.log(parser.yy.page.examples.length + ' examples'); console.log(parser.yy.page.informationLink.length + ' link(s)'); } result = { page: parser.yy.page, errors: parser.yy.errors, success: success }; if (parser.yy.page.examples.length > MAX_EXAMPLES) { result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR019', 'description': this.ERRORS.TLDR019 }); } if (alsoFormat) result.formatted = linter.format(parser.yy.page); return result; }; linter.processFile = function(file, verbose, alsoFormat, ignoreErrors) { const result = linter.process(file, fs.readFileSync(file, 'utf8'), verbose, alsoFormat); if (path.extname(file) !== '.md') { result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR107', description: this.ERRORS.TLDR107 }); } if (RegExp(/\s/).test(path.basename(file))) { result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR108', description: this.ERRORS.TLDR108 }); } if (/[A-Z]/.test(path.basename(file))) { result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR109', description: this.ERRORS.TLDR109 }); } if (/[<>:"/\\|?*]/.test(path.basename(file))) { result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR111', description: this.ERRORS.TLDR111 }); } if (ignoreErrors) { ignoreErrors = ignoreErrors.split(',').map(function(val) { return val.trim(); }); result.errors = result.errors.filter(function(error) { return !ignoreErrors.includes(error.code); }); } return result; };