Skip to content

Instantly share code, notes, and snippets.

@sompylasar
Created April 9, 2018 23:51
Show Gist options
  • Save sompylasar/378b402991a1f407c538cc9a5c2372de to your computer and use it in GitHub Desktop.
Save sompylasar/378b402991a1f407c538cc9a5c2372de to your computer and use it in GitHub Desktop.
TODO.md generator
#!/usr/bin/env node
/* eslint-disable strict, no-console, prefer-arrow-callback, prefer-spread */
'use strict';
const glob = require('glob');
const leasot = require('leasot'); // eslint-disable-line import/no-extraneous-dependencies
const path = require('path');
const fs = require('fs');
const gitAdd = require('../_binUtils').gitAdd;
const handleError = require('../_binUtils').handleError;
const ROOT_DIR = require('../_binUtils').ROOT_DIR;
const REPO_BASE_URL = '';
const DEFAULT_USERNAME = 'sompylasar';
const TAGS = 'HACK, QUESTION, WORKAROUND, TODO, FIXME, XXX, REVIEW'.split(/\s*,\s*/);
if (false) { // eslint-disable-line no-constant-condition
TAGS.push.apply(TAGS, 'WARNING, NOTE, CHANGED, IDEA'.split(/\s*,\s*/));
}
const GLOB = '@(src|bin)/**/*';
const GLOB_IGNORE = [];
const OUTPUT = path.join(ROOT_DIR, 'TODO.md');
function processFile(file, next) {
fs.readFile(file, { encoding: 'utf8' }, function onFileRead(error, content) {
if (error) {
next(error);
return;
}
const contentString = content.toString();
const todosForFile = leasot.parse({
ext: path.extname(file),
content: contentString,
fileName: file,
customTags: TAGS,
});
const isInTests = ( path.basename(path.dirname(file)).indexOf('test') === 0 );
if ( isInTests ) {
const contentStringLines = contentString.split('\n');
contentStringLines.forEach((line) => {
const skippedTestRegexp = /\bit\.skip\((['"])(.+)(\1),.*$/;
const skippedTestMatch = skippedTestRegexp.exec(line);
if (skippedTestMatch) {
const todoEnableSkippedTest = {
file: file,
line: 0,
kind: 'TODO',
ref: '@' + DEFAULT_USERNAME,
text: 'Enable skipped test: ' + skippedTestMatch[2].replace(/\\(.)/g, '$1'),
};
todosForFile.push(todoEnableSkippedTest);
}
});
}
next(null, todosForFile);
});
}
function strcmp(left, right) {
return (left < right ? -1 : (left > right ? 1 : 0));
}
function multicmp(cmpArray) {
for (let ic = cmpArray.length, i = 0; i < ic; ++i) {
if (cmpArray[i] !== 0) {
return cmpArray[i];
}
}
return 0;
}
function compareTodos(left, right) {
const kindcmpres = ( TAGS.indexOf(left.kind) - TAGS.indexOf(right.kind) );
const filecmpres = strcmp(left.file, right.file);
const linecmpres = ( left.line - right.line );
const refcmpres = strcmp(left.ref, right.ref);
const textcmpres = strcmp(left.text, right.text);
const cmpArray = [
kindcmpres,
filecmpres,
linecmpres,
refcmpres,
textcmpres,
];
const multicmpres = multicmp(cmpArray);
return multicmpres;
}
function processAllFiles(files, next) {
const todosAll = [];
function nextFile(fileIndex) {
if (fileIndex >= files.length) {
todosAll.sort(compareTodos);
next(null, todosAll);
return;
}
processFile(files[fileIndex], function onFileProcessed(error, todosForFile) {
if (error) {
next(error);
return;
}
if (todosForFile) {
todosAll.push.apply(todosAll, todosForFile);
}
nextFile(fileIndex + 1);
});
}
nextFile(0);
}
function generateMarkdown(todosArray, next) {
const todosMarkdown = (todosArray.length <= 0 ? '_Congratulations, the code is clean of TODOs!_' : todosArray.map(function onMapItem(item) {
/*
item ~ {
file: '/absolute/path/to/file.js',
kind: 'TODO',
line: 2,
text: 'Make this work with `foo-bar`.',
ref: '@sompylasar'
}
*/
const fileRelative = path.relative(ROOT_DIR, item.file);
const text = item.text.replace(/^\s+|\s+$/g, '');
const usernames = (
item.ref.split(/[&|,;]+/g)
.map(function onUsernameTrim(username) {
username = username.replace(/(^\s+)|(\s+$)/g, '');
if (username === 'any') {
username = DEFAULT_USERNAME;
}
return username;
})
.filter(function onUsernameFilter(username) {
return !!username;
})
);
const line = (
' - ' +
'**' + item.kind.toUpperCase() + '**' +
' ' +
usernames.map(function onUsernameToMarkdown(username) {
if (/\s+/.test(username) || username.indexOf('@') > 0 || username.indexOf('+') >= 0) { return '**' + username + '**'; }
return '[**' + '@' + username.replace(/^[@]+/, '') + '**](https://github.com/' + username.replace(/^[@]+/, '') + ')';
}).join(';') +
': ' +
(text || '_(no comment)_') + ' \n' +
'<sup>– [' + fileRelative + '#L' + item.line + '](' + REPO_BASE_URL + fileRelative + '#L' + item.line + ')</sup>'
);
return line;
}).join('\n'));
const todosMarkdownWithHeading = [
'# TODOs',
'',
'> <sup>**Warning**: This file is auto-generated. Please do not edit it manually, use [npm run todo](bin/todo.js) to update it.</sup>',
'',
'_Found ' + todosArray.length + ' in `' + GLOB + '`, looked for ' +
TAGS.map(function onMapTag(tag) {
return (
'`' + tag.toUpperCase() + '` (' +
todosArray.filter(function onFilterByTag(item) {
return (item.kind.toUpperCase() === tag.toUpperCase());
}).length +
')'
);
}).join(', ') +
'._',
'',
todosMarkdown,
'',
].join('\n');
next(null, todosMarkdownWithHeading);
}
glob(GLOB, {
cwd: ROOT_DIR,
nodir: true,
realpath: true,
ignore: GLOB_IGNORE,
}, function onGlob(globError, filesFromGlob) {
if (globError) {
handleError(globError);
return;
}
const files = filesFromGlob.filter(function onFilesFilter(file) {
const relativePath = path.relative(ROOT_DIR, file);
return (
!/node_modules|bower_components|vendor/.test(relativePath)
&& leasot.isExtSupported(path.extname(relativePath))
);
});
console.log('Files found: ' + files.length);
processAllFiles(files, function onAllFilesProcessed(processAllFilesError, todosArray) {
if (processAllFilesError) {
handleError(processAllFilesError);
return;
}
console.log('TODOs found: ' + todosArray.length);
generateMarkdown(todosArray, function onMarkdownGenerated(generateMarkdownError, todosMarkdown) {
if (generateMarkdownError) {
handleError(generateMarkdownError);
return;
}
console.log('Markdown generated.');
fs.writeFile(OUTPUT, todosMarkdown, { encoding: 'utf8' }, function onFileWritten(writeFileError) {
if (writeFileError) {
handleError(writeFileError);
return;
}
console.log('Markdown written to file: ' + OUTPUT);
gitAdd(OUTPUT, function onGitAdded(gitAddError) {
if (gitAddError) {
handleError(gitAddError);
return;
}
console.log('File added to git stage: ' + OUTPUT);
});
});
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment