Skip to content

Instantly share code, notes, and snippets.

@stipsan
Last active November 4, 2017 18:09
Show Gist options
  • Save stipsan/0d0578fe92bcdeb024a0af2a3e2a9e2a to your computer and use it in GitHub Desktop.
Save stipsan/0d0578fe92bcdeb024a0af2a3e2a9e2a to your computer and use it in GitHub Desktop.
html-cli with lint-staged support
#!/usr/bin/env node
'use strict';
const path = require('path');
const getStdin = require('get-stdin');
const globby = require('globby');
const entries = require('lodash/entries');
const kebabCase = require('lodash/kebabCase');
const snakeCase = require('lodash/snakeCase');
const meow = require('meow');
const sander = require('sander');
const html = require('js-beautify').html;
const cli = meow(`
Usage
$ html [flags] <input>
Options
--e4x, --jsx, -x Pass through JSX/E4X [false]
--editorconfig, -c Use .editorconfig for options [true]
--end-with-newline, -n Ensure newline at file end [true]
--eol, -e Carriage return character ["\\n"]
--indent-character, -i Indentation character [" "]
--indent-level, -l Initial indentation level [1]
--indent-size, -s Indentation size [2]
--max-preserve-newlines, -m Count of newlines to preserve per chunk [10]
--preserve-newlines, -p Preserve newlines [false]
Examples
$ html index.html # overwrites in place
$ html docs/**/*.html # overwrites in place
$ echo "<span>html</span>" | html
<span>
html
</span>
$ echo "<span>html</span>" > index.html && html < index.html
<span>
html
</span>
`);
const booleans = [
'jsx', 'editorconfig', 'end-with-newline',
'eol', 'preserve-newlines'
];
const strings = [
'eol', 'indent-character'
];
const numbers = [
'indent-level', 'indent-size', 'max-preserve-newlines'
];
main(cli.input, cli.flags)
.catch(err => {
if (err.managed) {
console.error(`${cli.help}\n\n ${err.message}`);
process.exit(1);
}
setTimeout(() => {
throw err;
});
});
// (input: Array<string>, flags: any) => Promise<void>
function main(input, raw) {
return Promise.resolve()
.then(() => {
const flags = getFlags(raw);
const out = input.length === 0 ?
(filename, content) => process.stdout.write(content) :
(filename, content) => sander.writeFile(filename, content);
const pretty = content => html(content, flags);
return {flags, out, pretty};
})
.then(context => {
return getContents(input)
.then(files => {
context.files = files;
return context;
});
})
.then(context => {
context.results = context.files.map(file => {
const [name, content] = file;
return [name, context.pretty(content, context.flags)];
});
return context;
})
.then(context => {
context.results.forEach(entry => context.out(...entry));
return context;
});
}
// (input: Array<string>) => Promise<Array[filename: null|string, content: string]>
function getContents(input) {
if (input.length === 0) {
return getStdin()
.then(stdin => {
if (!stdin) {
const error = new Error(`Either <input> or stdin is required.`);
error.managed = true;
throw error;
}
return [[null, stdin]];
});
}
return globby(input)
.then(files => Promise.all(
files
.filter(file => path.extname(file) === '.html')
.map(file => sander.readFile(file).then(contents => [file, contents.toString()]))
)
);
}
// (raw: any) => any
function getFlags(raw) {
const rawFlagEntries = entries(raw)
.map(entry => {
const [name, value] = entry;
return [kebabCase(name), value];
});
const violations = rawFlagEntries
.map(flag => {
const [flagName, flagValue] = flag;
if (booleans.includes(flagName)) {
return typeof flagValue === 'boolean' ?
null : [flagName, flagValue, 'boolean', typeof flagValue];
}
if (strings.includes(flagName)) {
return typeof flagValue === 'string' ?
null : [flagName, flagValue, 'string', typeof flagValue];
}
if (numbers.includes(flagName)) {
return typeof flagValue === 'number' ?
null : [flagName, flagValue, 'number', typeof flagValue];
}
return null;
})
.filter(Array.isArray);
if (violations.length > 0) {
const messages = violations
.map(violation => {
const [name, value, expected, actual] = violation;
return `Expected flag ${name} to be of type "${expected}". Received value "${value}" with type "${actual}".`;
});
const error = new Error(messages.join('\n'));
error.managed = true;
throw error;
}
return rawFlagEntries.reduce((flags, flag) => {
const [flagName, flagValue] = flag;
flags[snakeCase(flagName)] = flagValue;
return flags;
}, {});
}
{
"name": "html-cli",
"version": "1.0.0",
"description": "Pretty print html",
"license": "MIT",
"author": "Mario Nebl <[email protected]>",
"contributors": [
"Max Ogden <[email protected]> (http://maxogden.com)",
"Nochum Sossonko <[email protected]>",
"Einar Lielmanis <[email protected]>"
],
"scripts": {
"lint": "xo cli.js"
},
"xo": {
"esnext": true
},
"repository": {
"type": "git",
"url": "https://github.com/marionebl/html-cli.git"
},
"bin": {
"html": "cli.js"
},
"bugs": {
"url": "https://github.com/marionebl/html-cli"
},
"homepage": "https://github.com/marionebl/html-cli",
"dependencies": {
"get-stdin": "^5.0.1",
"globby": "^6.1.0",
"js-beautify": "^1.6.4",
"lodash": "^4.17.2",
"meow": "^3.7.0",
"sander": "^0.5.1"
},
"devDependencies": {
"xo": "^0.17.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment