Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

html2pug

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

html2pug - npm Package Compare versions

Comparing version 2.0.1 to 3.0.0

.eslintrc.yml

22

CHANGELOG.md

@@ -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

45

package.json
{
"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
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&apos;" 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)
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc