Comparing version 2.0.1 to 3.0.0
@@ -7,2 +7,22 @@ # Changelog | ||
## [3.0.0] - 2019-04-07 | ||
### Changed | ||
- Option `tabs` to `useTabs` (**BREAKING CHANGE**) | ||
- Option `fragment` to `isFragment` (**BREAKING CHANGE**) | ||
- `preserveLineBreaks` default is `true` | ||
### Added | ||
- Multiline element values (`<script>`, `<pre>` etc.) | ||
- Extend method to merge options with defaults | ||
- Prettier + ESLint | ||
- Shorthand for boolean/empty attributes | ||
- Option `useCommas` for attribute separator | ||
### Removed | ||
- Standard code style | ||
### Fixed | ||
- Replaced single quotes in attributes with an escaped single quote | ||
- Vulnerabilities in dependencies | ||
## [2.0.1] - 2017-08-13 | ||
@@ -15,3 +35,3 @@ ### Changed | ||
### Changed | ||
- CLI invocation syntax | ||
- CLI invocation syntax (**BREAKING CHANGE**) | ||
@@ -18,0 +38,0 @@ ### Added |
{ | ||
"name": "html2pug", | ||
"version": "2.0.1", | ||
"version": "3.0.0", | ||
"description": "Converts HTML to Pug", | ||
@@ -10,29 +10,26 @@ "main": "src/index.js", | ||
"dependencies": { | ||
"get-stdin": "^5.0.1", | ||
"html-minifier": "^3.5.2", | ||
"parse5": "^2.1.5", | ||
"get-stdin": "^6.0.0", | ||
"html-minifier": "^4.0.0", | ||
"parse5": "^5.1.0", | ||
"yargs": "^8.0.2" | ||
}, | ||
"devDependencies": { | ||
"ava": "^0.21.0", | ||
"eslint": "^4.1.1", | ||
"eslint-config-standard": "^10.2.1", | ||
"eslint-plugin-import": "^2.6.1", | ||
"eslint-plugin-node": "^5.1.0", | ||
"eslint-plugin-promise": "^3.5.0", | ||
"eslint-plugin-standard": "^3.0.1", | ||
"husky": "^0.14.2", | ||
"lint-staged": "^4.0.0", | ||
"prettier-standard": "^6.0.0" | ||
"ava": "^1.4.1", | ||
"eslint": "^5.16.0", | ||
"eslint-config-airbnb-base": "^13.1.0", | ||
"eslint-config-prettier": "^4.1.0", | ||
"eslint-plugin-import": "^2.16.0", | ||
"eslint-plugin-prettier": "^3.0.1", | ||
"husky": "^1.3.1", | ||
"lint-staged": "^8.1.5", | ||
"prettier": "^1.16.4" | ||
}, | ||
"scripts": { | ||
"test": "ava", | ||
"standard": "prettier-standard 'src/**/*.js'", | ||
"lint": "eslint src", | ||
"precommit": "lint-staged", | ||
"prepush": "npm run lint" | ||
"test": "ava test.js", | ||
"lint": "eslint .", | ||
"lint:fix": "eslint . --fix" | ||
}, | ||
"lint-staged": { | ||
"*.js": [ | ||
"prettier-standard", | ||
"npm run lint:fix", | ||
"git add" | ||
@@ -57,3 +54,9 @@ ] | ||
}, | ||
"homepage": "https://github.com/izolate/html2pug#readme" | ||
"homepage": "https://github.com/izolate/html2pug#readme", | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "lint-staged", | ||
"pre-push": "npm run lint" | ||
} | ||
} | ||
} |
@@ -15,3 +15,3 @@ # html2pug | ||
<h1 class="title">Hello World!</h1> | ||
</header> | ||
</div> | ||
</body> | ||
@@ -37,3 +37,3 @@ </html> | ||
```bash | ||
npm i -g html2pug | ||
npm install -g html2pug | ||
``` | ||
@@ -44,9 +44,13 @@ | ||
### CLI | ||
Accept input from a file and write to stdout: | ||
Accept input from a file or stdin and write to stdout: | ||
```bash | ||
# choose a file | ||
html2pug < example.html | ||
# use pipe | ||
echo '<h1>foo</h1>' | html2pug -f | ||
``` | ||
Or write to a file: | ||
Write output to a file: | ||
```bash | ||
@@ -64,3 +68,3 @@ html2pug < example.html > example.pug | ||
const html = '<header><h1 class="title">Hello World!</h1></header>' | ||
const pug = html2pug(html, { tabs: true }) | ||
const pug = html2pug(html, { useTabs: true }) | ||
``` | ||
@@ -72,3 +76,4 @@ | ||
--- | --- | --- | --- | ||
tabs | Boolean | `false` | Use tabs instead of spaces | ||
fragment | Boolean | `false` | Wrap in enclosing `<html>` and `<body>` tags | ||
useTabs | Boolean | `false` | Use tabs instead of spaces | ||
useCommas | Boolean | `true` | Use commas to separate node attributes, or a space if false | ||
isFragment | Boolean | `false` | Wraps result in enclosing `<html>` and `<body>` tags if false |
#!/usr/bin/env node | ||
'use strict' | ||
const { version } = require('../package.json') | ||
const getStdin = require('get-stdin') | ||
const { argv } = require('yargs') | ||
const html2pug = require('./') | ||
const argv = require('yargs').argv | ||
const { version } = require('../package.json') | ||
@@ -16,6 +14,6 @@ /** | ||
' Options:\n', | ||
` -f, --fragment Don't wrap output in <html>/<body> tags`, | ||
` -t, --tabs Use tabs instead of spaces`, | ||
` -h, --help Show this page`, | ||
` -v, --version Show version\n`, | ||
" -f, --fragment Don't wrap output in <html>/<body> tags", | ||
' -t, --tabs Use tabs instead of spaces', | ||
' -h, --help Show this page', | ||
' -v, --version Show version\n', | ||
' Examples:\n', | ||
@@ -25,3 +23,3 @@ ' # Accept input from file and write to stdout', | ||
' # Or write to a file', | ||
' $ html2pug < example.html > example.pug \n' | ||
' $ html2pug < example.html > example.pug \n', | ||
].join('\n') | ||
@@ -32,3 +30,4 @@ | ||
*/ | ||
async function main ({ fragment, needsHelp, showVersion, tabs }) { | ||
async function main({ isFragment, needsHelp, showVersion, useTabs }) { | ||
/* eslint-disable no-console */ | ||
const stdin = await getStdin() | ||
@@ -44,4 +43,6 @@ | ||
const pug = html2pug(stdin, { tabs, fragment }) | ||
console.log(pug) | ||
const pug = html2pug(stdin, { isFragment, useTabs }) | ||
return console.log(pug) | ||
/* eslint-enable no-console */ | ||
} | ||
@@ -53,6 +54,6 @@ | ||
main({ | ||
fragment: !!(argv.fragment || argv.f), | ||
isFragment: !!(argv.fragment || argv.f), | ||
needsHelp: !!(argv.help || argv.h), | ||
showVersion: !!(argv.version || argv.v), | ||
tabs: !!(argv.tabs || argv.t) | ||
useTabs: !!(argv.tabs || argv.t), | ||
}) |
@@ -1,35 +0,30 @@ | ||
'use strict' | ||
const { minify } = require('html-minifier') | ||
const { parse, parseFragment } = require('parse5') | ||
const Parser = require('./parser') | ||
const Pugify = require('./parser') | ||
module.exports = ( | ||
sourceHtml, | ||
{ | ||
tabs = false, | ||
fragment = false, | ||
caseSensitive = true, | ||
removeEmptyAttributes = true, | ||
collapseWhitespace = true, | ||
collapseBooleanAttributes = true, | ||
collapseInlineTagWhitespace = true | ||
} = {} | ||
) => { | ||
const defaultOptions = { | ||
// html2pug options | ||
isFragment: false, | ||
useTabs: false, | ||
useCommas: true, | ||
// html-minifier options | ||
caseSensitive: true, | ||
removeEmptyAttributes: true, | ||
collapseWhitespace: true, | ||
collapseBooleanAttributes: true, | ||
preserveLineBreaks: true, | ||
} | ||
module.exports = (sourceHtml, options = {}) => { | ||
// Minify source HTML | ||
const html = minify(sourceHtml, { | ||
removeEmptyAttributes, | ||
collapseWhitespace, | ||
collapseBooleanAttributes, | ||
collapseInlineTagWhitespace, | ||
caseSensitive | ||
}) | ||
const opts = { ...defaultOptions, ...options } | ||
const html = minify(sourceHtml, opts) | ||
// Parse minified HTML | ||
const parser = new Parser({ | ||
root: fragment ? parseFragment(html) : parse(html), | ||
tabs | ||
}) | ||
const { isFragment, useTabs, useCommas } = opts | ||
return parser.parse() | ||
// Parse HTML and convert to Pug | ||
const documentRoot = isFragment ? parseFragment(html) : parse(html) | ||
const pugify = new Pugify(documentRoot, { useTabs, useCommas }) | ||
return pugify.parse() | ||
} |
@@ -1,20 +0,33 @@ | ||
const { | ||
isDocumentTypeNode, | ||
isTextNode, | ||
isElementNode, | ||
isCommentNode | ||
} = require('parse5').treeAdapters.default | ||
const DOCUMENT_TYPE_NODE = '#documentType' | ||
const TEXT_NODE = '#text' | ||
const DIV_NODE = 'div' | ||
const COMMENT_NODE = '#comment' | ||
const COMMENT_NODE_PUG = '//' | ||
const hasSingleTextNodeChild = node => { | ||
return ( | ||
node.childNodes && | ||
node.childNodes.length === 1 && | ||
node.childNodes[0].nodeName === TEXT_NODE | ||
) | ||
} | ||
class Parser { | ||
constructor ({ root, tabs = false }) { | ||
constructor(root, options = {}) { | ||
this.pug = '' | ||
this.root = root | ||
this.tabs = tabs | ||
this.pug = '' | ||
const { useTabs, useCommas } = options | ||
// Tabs or spaces? | ||
this.indentType = useTabs ? '\t' : ' ' | ||
// Comma separate attributes? | ||
this.separatorType = useCommas ? ', ' : ' ' | ||
} | ||
get indent () { | ||
return this.tabs ? '\t' : ' ' | ||
getIndent(level = 0) { | ||
return this.indentType.repeat(level) | ||
} | ||
parse () { | ||
parse() { | ||
const walk = this.walk(this.root.childNodes, 0) | ||
@@ -37,3 +50,3 @@ let it | ||
*/ | ||
* walk (tree, level) { | ||
*walk(tree, level) { | ||
if (!tree) { | ||
@@ -46,3 +59,6 @@ return | ||
this.pug += `\n${this.parseNode(node, level)}` | ||
const newline = this.parseNode(node, level) | ||
if (newline) { | ||
this.pug += `\n${newline}` | ||
} | ||
@@ -52,5 +68,5 @@ if ( | ||
node.childNodes.length > 0 && | ||
!this.isUniqueTextNode(node) | ||
!hasSingleTextNodeChild(node) | ||
) { | ||
yield * this.walk(node.childNodes, level + 1) | ||
yield* this.walk(node.childNodes, level + 1) | ||
} | ||
@@ -60,68 +76,43 @@ } | ||
parseComment (node, indent) { | ||
const comment = node.data.split('\n') | ||
/* | ||
* Returns a Pug node name with all attributes set in parentheses. | ||
*/ | ||
getNodeWithAttributes(node) { | ||
const { tagName, attrs } = node | ||
const attributes = [] | ||
let pugNode = tagName | ||
// Differentiate single line to multi-line comments | ||
if (comment.length > 1) { | ||
const multiLine = comment.map(line => `${indent} ${line}`).join('\n') | ||
return `${indent}//${multiLine}` | ||
} else { | ||
return `${indent}//${comment}` | ||
if (!attrs) { | ||
return pugNode | ||
} | ||
} | ||
parseNode (node, level) { | ||
const indent = this.indent.repeat(level) | ||
// Add CSS selectors to pug node and append any element attributes to it | ||
for (const attr of attrs) { | ||
const { name, value } = attr | ||
if (isDocumentTypeNode(node)) { | ||
return `${indent}doctype html` | ||
} | ||
if (isTextNode(node)) { | ||
return `${indent}| ${node.value}` | ||
} else if (isCommentNode(node)) { | ||
return this.parseComment(node, indent) | ||
} else if (isElementNode(node)) { | ||
let line = `${indent}${this.setAttributes(node)}` | ||
if (this.isUniqueTextNode(node)) { | ||
line += ` ${node.childNodes[0].value}` | ||
// Remove div tag if a selector is present (shorthand) | ||
// e.g. div#form() -> #form() | ||
const hasSelector = name === 'id' || name === 'class' | ||
if (tagName === DIV_NODE && hasSelector) { | ||
pugNode = pugNode.replace(DIV_NODE, '') | ||
} | ||
return line | ||
} else { | ||
return node | ||
} | ||
} | ||
setAttributes (node) { | ||
const { tagName, attrs: attributes } = node | ||
const attributeList = [] | ||
let pugNode = tagName | ||
attributes.forEach(({ name, value }) => { | ||
let shorten = false | ||
switch (name) { | ||
case 'id': | ||
shorten = true | ||
pugNode += `#${value}` | ||
break | ||
case 'class': | ||
shorten = true | ||
pugNode += `.${value.split(' ').join('.')}` | ||
break | ||
default: | ||
attributeList.push(`${name}='${value}'`) | ||
default: { | ||
// Add escaped single quotes (\') to attribute values | ||
const val = value.replace(/'/g, "\\'") | ||
attributes.push(val ? `${name}='${val}'` : name) | ||
break | ||
} | ||
} | ||
} | ||
// Remove div tagName | ||
if (tagName === 'div' && shorten) { | ||
pugNode = pugNode.replace('div', '') | ||
} | ||
}) | ||
if (attributeList.length) { | ||
pugNode += `(${attributeList.join(', ')})` | ||
if (attributes.length) { | ||
pugNode += `(${attributes.join(this.separatorType)})` | ||
} | ||
@@ -132,7 +123,99 @@ | ||
isUniqueTextNode (node) { | ||
return node.childNodes.length === 1 && isTextNode(node.childNodes[0]) | ||
/** | ||
* formatPugNode applies the correct indent for the current line, | ||
* and formats the value as either as a single or multiline string. | ||
* | ||
* @param {String} node - The pug node (e.g. header(class='foo')) | ||
* @param {String} value - The node's value | ||
* @param {Number} level - Current tree level to generate indent | ||
* @param {String} blockChar - The character used to denote a multiline value | ||
*/ | ||
formatPugNode(node, value = '', level, blockChar = '.') { | ||
const indent = this.getIndent(level) | ||
const result = `${indent}${node}` | ||
const lines = value.split('\n') | ||
// Create an inline node | ||
if (lines.length <= 1) { | ||
return value.length ? `${result} ${value}` : result | ||
} | ||
// Create a multiline node | ||
const indentChild = this.getIndent(level + 1) | ||
const multiline = lines.map(line => `${indentChild}${line}`).join('\n') | ||
return `${result}${blockChar}\n${multiline}` | ||
} | ||
/** | ||
* createDoctype formats a #documentType element | ||
*/ | ||
createDoctype(node, level) { | ||
const indent = this.getIndent(level) | ||
return `${indent}doctype html` | ||
} | ||
/** | ||
* createComment formats a #comment element. | ||
* | ||
* Block comments in Pug don't require the dot '.' character. | ||
*/ | ||
createComment(node, level) { | ||
return this.formatPugNode(COMMENT_NODE_PUG, node.data, level, '') | ||
} | ||
/** | ||
* createText formats a #text element. | ||
* | ||
* A #text element containing only line breaks (\n) indicates | ||
* unnecessary whitespace between elements that should be removed. | ||
* | ||
* Actual text in a single #text element has no significant | ||
* whitespace and should be treated as inline text. | ||
*/ | ||
createText(node, level) { | ||
const { value } = node | ||
const indent = this.getIndent(level) | ||
// Omit line breaks between HTML elements | ||
if (/^[\n]+$/.test(value)) { | ||
return false | ||
} | ||
return `${indent}| ${value}` | ||
} | ||
/** | ||
* createElement formats a generic HTML element. | ||
*/ | ||
createElement(node, level) { | ||
const pugNode = this.getNodeWithAttributes(node) | ||
const value = hasSingleTextNodeChild(node) | ||
? node.childNodes[0].value | ||
: node.value | ||
return this.formatPugNode(pugNode, value, level) | ||
} | ||
parseNode(node, level) { | ||
const { nodeName } = node | ||
switch (nodeName) { | ||
case DOCUMENT_TYPE_NODE: | ||
return this.createDoctype(node, level) | ||
case COMMENT_NODE: | ||
return this.createComment(node, level) | ||
case TEXT_NODE: | ||
return this.createText(node, level) | ||
default: | ||
return this.createElement(node, level) | ||
} | ||
} | ||
} | ||
module.exports = Parser |
164
test.js
import test from 'ava' | ||
import html2pug from './src' | ||
const html = `<!doctype html> | ||
test('transforms html document to pug with default options', t => { | ||
const html = `<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<title>Hello World!</title> | ||
</head> | ||
<body> | ||
<div id="content"> | ||
<h1 class="accents">â, é, ï, õ, ù</h1> | ||
<body data-page="home"> | ||
<header id="nav"> | ||
<h1 class="heading">Hello, world!</h1> | ||
</header> | ||
@@ -16,11 +18,11 @@ </body> | ||
const pug = `doctype html | ||
const pug = `doctype html | ||
html(lang='en') | ||
head | ||
meta(charset='utf-8') | ||
title Hello World! | ||
body | ||
#content | ||
h1.accents â, é, ï, õ, ù` | ||
body(data-page='home') | ||
header#nav | ||
h1.heading Hello, world!` | ||
test('Pug', t => { | ||
const generated = html2pug(html) | ||
@@ -30,15 +32,147 @@ t.is(generated, pug) | ||
test('Fragment', t => { | ||
const generated = html2pug('<h1>Hello World!</h1>', { fragment: true }) | ||
test('result contains no outer html element when isFragment is truthy', t => { | ||
const generated = html2pug('<h1>Hello World!</h1>', { isFragment: true }) | ||
t.falsy(generated.startsWith('html')) | ||
}) | ||
test('Tabs', t => { | ||
test('respects whitespace within elements', t => { | ||
const html = `<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<style type="text/css"> | ||
* { | ||
margin: 0; | ||
padding: 0; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<script type="text/javascript"> | ||
$(document).ready(function() { | ||
console.log('ready') | ||
}) | ||
</script> | ||
</body> | ||
</html>` | ||
const pug = `doctype html | ||
html(lang='en') | ||
head | ||
style(type='text/css'). | ||
* { | ||
margin: 0; | ||
padding: 0; | ||
} | ||
body | ||
script(type='text/javascript'). | ||
$(document).ready(function() { | ||
console.log('ready') | ||
}) | ||
` | ||
const generated = html2pug(html) | ||
t.is(generated, pug) | ||
}) | ||
test('creates multiline block when linebreaks are present', t => { | ||
const html = '<textarea>multi\nline\nstring</textarea>' | ||
const pug = `textarea. | ||
multi | ||
line | ||
string` | ||
const generated = html2pug(html, { isFragment: true }) | ||
t.is(generated, pug) | ||
}) | ||
test('uses div tag shorthand when id/class is present', t => { | ||
const html = "<div id='foo' class='bar'>baz</div>" | ||
const pug = '#foo.bar baz' | ||
const generated = html2pug(html, { isFragment: true }) | ||
t.is(generated, pug) | ||
}) | ||
test('removes whitespace between HTML elements', t => { | ||
const html = `<ul class="list"> | ||
<li>one</li> | ||
<li>two</li> | ||
<li>three</li> | ||
<li>four</li> | ||
</ul>` | ||
const pug = `ul.list | ||
li one | ||
li two | ||
li three | ||
li four` | ||
const generated = html2pug(html, { isFragment: true }) | ||
t.is(generated, pug) | ||
}) | ||
test('does not fail on unicode characters', t => { | ||
const generated = html2pug('<h1 class="accents">â, é, ï, õ, ù</h1>', { | ||
isFragment: true, | ||
}) | ||
const expected = 'h1.accents â, é, ï, õ, ù' | ||
t.is(generated, expected) | ||
}) | ||
test('uses tabs when useTabs is truthy', t => { | ||
const generated = html2pug('<div><span>Tabs!</span></div>', { | ||
fragment: true, | ||
tabs: true | ||
isFragment: true, | ||
useTabs: true, | ||
}) | ||
const expected = 'div\n\tspan Tabs!' | ||
const expected = 'div\n\tspan Tabs!' | ||
t.is(generated, expected) | ||
}) | ||
test('uses a comma to separate attributes', t => { | ||
const generated = html2pug('<input type="text" name="foo" />', { | ||
isFragment: true, | ||
}) | ||
const expected = "input(type='text', name='foo')" | ||
t.is(generated, expected) | ||
}) | ||
test('uses a space to separate attributes', t => { | ||
const generated = html2pug('<input type="text" name="foo" />', { | ||
isFragment: true, | ||
useCommas: false, | ||
}) | ||
const expected = "input(type='text' name='foo')" | ||
t.is(generated, expected) | ||
}) | ||
test('single quotes in attribute values are escaped', t => { | ||
const generated = html2pug( | ||
`<button aria-label="closin'" onclick="window.alert('bye')">close</button>`, | ||
{ | ||
isFragment: true, | ||
} | ||
) | ||
const expected = `button(aria-label='closin\\'', onclick='window.alert(\\'bye\\')') close` | ||
t.is(generated, expected) | ||
}) | ||
test('collapses boolean attributes', t => { | ||
const generated = html2pug( | ||
`<input type="text" name="foo" disabled="disabled" readonly="readonly" />`, | ||
{ isFragment: true } | ||
) | ||
const expected = `input(type='text', name='foo', disabled, readonly)` | ||
t.is(generated, expected) | ||
}) |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
17676
9
14
393
75
1
+ Addedcommander@2.20.3(transitive)
+ Addedget-stdin@6.0.0(transitive)
+ Addedhtml-minifier@4.0.0(transitive)
+ Addedparse5@5.1.1(transitive)
+ Addeduglify-js@3.19.3(transitive)
- Removedcommander@2.17.12.19.0(transitive)
- Removedget-stdin@5.0.1(transitive)
- Removedhtml-minifier@3.5.21(transitive)
- Removedparse5@2.2.3(transitive)
- Removeduglify-js@3.4.10(transitive)
Updatedget-stdin@^6.0.0
Updatedhtml-minifier@^4.0.0
Updatedparse5@^5.1.0