diff --git a/package.json b/package.json index 7eec7ec..1f98ccc 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,8 @@ "test/**", "tmp/**", "lib/**", - "*.{html,jpg}" + "*.{html,jpg}", + ".idea/**" ], "rules": { "pkg-main": [ diff --git a/readme.md b/readme.md index 1a3f377..1179b5f 100644 --- a/readme.md +++ b/readme.md @@ -162,13 +162,25 @@ Default: Default: '\n' Description: *As value is a string symbol which is added to the end of the row* + - **useExistingLineBreaks** + Type: `Boolean` + Default: false + Description: *Preserve existing line breaks. Don't add new line breaks (exception: adds missing line break at the start/end of a tag if it already has line break at the end/start). `blankLines` and `eol` are ignored if this option set to `true`* + + - **lowerAttributeName** + Type: `Boolean` + Default: true + Description: *Control case of attribute names* + - **eof** (*end of file*) Type: `String|Boolean` Default: '\n' Description: *As value is a string symbol which is added to the end of the file and will not adds if you specify a boolean value of `false`* ### `mini` -Type: `Object` +Type: `Object|Boolean(only false)` +Description: *Describe options for tidying up html output. Can be turned off with `false` value* + Default: - **removeAttribute** diff --git a/src/index.js b/src/index.js index 286c5b9..386be52 100644 --- a/src/index.js +++ b/src/index.js @@ -15,29 +15,91 @@ const optionsDefault = { } }; -const clean = tree => parser(render(tree)) - .filter(node => { - return typeof node === 'object' || (typeof node === 'string' && (node.trim().length !== 0 || /doctype/gi.test(node))); - }) - .map(node => { - if (Object.prototype.hasOwnProperty.call(node, 'content')) { - node.content = clean(node.content); +const horizontalWhitespace = /[\t ]+/g; +const verticalWhitespace = /[\r\n\v\f]+/g; + +const getEdgeWhitespace = (node) => { + let leftWhitespace; + let rightWhitespace; + let leftLinebreaks; + let rightLinebreaks; + + if (typeof node === 'string') { + const nodeTrimmed = node.trim(); + + if (nodeTrimmed.length === 0) { + leftWhitespace = node.replace(verticalWhitespace, ''); + rightWhitespace = node.replace(verticalWhitespace, ''); + leftLinebreaks = node.replace(horizontalWhitespace, ''); + rightLinebreaks = node.replace(horizontalWhitespace, ''); + } else { + leftWhitespace = node.replace(/\s+$/,'').replace(nodeTrimmed, ''); + rightWhitespace = node.replace(/^\s+/,'').replace(nodeTrimmed, ''); + leftLinebreaks = leftWhitespace.replace(horizontalWhitespace, ''); + rightLinebreaks = rightWhitespace.replace(horizontalWhitespace, ''); } - return typeof node === 'string' ? node.trim() : node; - }); + return {leftWhitespace, rightWhitespace, leftLinebreaks, rightLinebreaks}; + } else { + return false; + } + +}; + +const clean = (tree, options) => { + let previousNodeRightLinebreaks = ''; + + return parser(render(tree)) + .filter(node => { + return options.rules.useExistingLineBreaks + ? node + : typeof node === 'object' || (typeof node === 'string' && (node.trim().length !== 0 || /doctype/gi.test(node))); + }) + .map(node => { + if (Object.prototype.hasOwnProperty.call(node, 'content')) { + node.content = clean(node.content, options); + } + + if (typeof node === 'string') { + if (!options.rules.useExistingLineBreaks) { + return node.trim(); + } + + const nodeTrimmed = node.trim(); + const {leftWhitespace, rightWhitespace, leftLinebreaks, rightLinebreaks} = getEdgeWhitespace(node); + let nodeCleaned; -const parseConditional = tree => { + if (nodeTrimmed.length === 0) { + nodeCleaned = node.replace(horizontalWhitespace, '') || ' '; + } else { + nodeCleaned = + `${leftLinebreaks || (previousNodeRightLinebreaks ? '' : leftWhitespace.replace(horizontalWhitespace, ' '))}` + + `${nodeTrimmed.replace(horizontalWhitespace, ' ').replace(/([\r\n\v\f]+)[\t ]/g, '$1')}` + + `${rightLinebreaks || rightWhitespace.replace(horizontalWhitespace, ' ')}`; + + previousNodeRightLinebreaks = rightLinebreaks; + } + + return nodeCleaned; + } else { + previousNodeRightLinebreaks = ''; + + return node; + } + }); +}; + +const parseConditional = (tree, options) => { return tree.map(node => { if (typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, 'content')) { - node.content = parseConditional(node.content); + node.content = parseConditional(node.content, options); } if (typeof node === 'string' && //.test(node)) { const conditional = /^((?:<[^>]+>)?(?:)?)([\s\S]*?)()$/ .exec(node) .slice(1) - .map((node, index) => index === 1 ? {tag: 'conditional-content', content: clean(parser(node))} : node); + .map((node, index) => index === 1 ? {tag: 'conditional-content', content: clean(parser(node), options)} : node); return { tag: 'conditional', @@ -68,10 +130,10 @@ const renderConditional = tree => { }, []); }; -const indent = (tree, {rules: {indent, eol, blankLines}}) => { +const indent = (tree, {rules: {indent, eol, blankLines, useExistingLineBreaks}}) => { const indentString = typeof indent === 'number' ? ' '.repeat(indent) : '\t'; - const getIndent = level => `${eol}${indentString.repeat(level)}`; + const getIndent = level => `${useExistingLineBreaks ? '' : eol}${indentString.repeat(level)}`; const setIndent = (tree, level = 0) => tree.reduce((previousValue, node, index) => { if (typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, 'content')) { @@ -79,43 +141,77 @@ const indent = (tree, {rules: {indent, eol, blankLines}}) => { --level; } - if (tree.length === 1 && typeof tree[index] === 'string') { + if (useExistingLineBreaks) { + if (typeof node === 'string') { + node = node.replace(/([\r\n\v\f]+)/g, function (match, p1, offset, string) { + if ((index === tree.length - 1) && (offset + match.length === string.length)) { + --level; + } + + return `${p1}${getIndent(Math.max(level, 0))}` + }); + } + + if ( + level > 0 + && (index === tree.length - 1) + && typeof tree[0] === 'string' + && getEdgeWhitespace(tree[0]).leftLinebreaks + && ((typeof node === 'string' && node.trim() && !getEdgeWhitespace(node).rightLinebreaks) || typeof node === 'object') + ) { + return [ ...previousValue, node, `\n${getIndent(--level)}` ]; + } + + if ( + level > 0 + && (index === 0) + && typeof tree[tree.length - 1] === 'string' + && getEdgeWhitespace(tree[tree.length - 1]).rightLinebreaks + && ((typeof node === 'string' && node.trim() && !getEdgeWhitespace(node).leftLinebreaks) || typeof node === 'object') + ) { + return [ ...previousValue, `\n${getIndent(typeof node === 'string' ? ++level : level)}`, node]; + } + return [...previousValue, node]; - } + } else { + if (tree.length === 1 && typeof tree[index] === 'string') { + return [...previousValue, node]; + } - if (level === 0 && (tree.length - 1) === index && tree.length > 1) { - return [...previousValue, getIndent(level), node]; - } + if (level === 0 && (tree.length - 1) === index && tree.length > 1) { + return [...previousValue, getIndent(level), node]; + } - if (level === 0 && (tree.length - 1) === index && tree.length === 1) { - return [...previousValue, node]; - } + if (level === 0 && (tree.length - 1) === index && tree.length === 1) { + return [...previousValue, node]; + } - if (level === 0 && index === 0) { - return [...previousValue, node, blankLines]; - } + if (level === 0 && index === 0) { + return [...previousValue, node, blankLines]; + } - if (level === 0) { - return [...previousValue, getIndent(level), node, blankLines]; - } + if (level === 0) { + return [...previousValue, getIndent(level), node, blankLines]; + } - if ((tree.length - 1) === index) { - return [...previousValue, getIndent(level), node, getIndent(--level)]; - } + if ((tree.length - 1) === index) { + return [...previousValue, getIndent(level), node, getIndent(--level)]; + } - if (typeof node === 'string' && //.test(node)) { - return [...previousValue, getIndent(level), node, blankLines]; - } + if (typeof node === 'string' && //.test(node)) { + return [...previousValue, getIndent(level), node, blankLines]; + } - if (typeof node === 'string' && //.test(node)) { - return [...previousValue, getIndent(level), node]; - } + if (typeof node === 'string' && //.test(node)) { + return [...previousValue, getIndent(level), node]; + } - if (node.tag === false) { - return [...previousValue, ...node.content.slice(0, -1)]; - } + if (node.tag === false) { + return [...previousValue, ...node.content.slice(0, -1)]; + } - return [...previousValue, getIndent(level), node, blankLines]; + return [...previousValue, getIndent(level), node, blankLines]; + } }, []); return setIndent(tree); @@ -141,7 +237,7 @@ const attrsBoolean = (tree, {attrs: {boolean}}) => { return node; }); - return removeAttrValue(tree); + return boolean ? removeAttrValue(tree) : tree; }; const lowerElementName = (tree, {tags}) => { @@ -155,6 +251,7 @@ const lowerElementName = (tree, {tags}) => { if ( typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, 'tag') && + typeof node.tag === 'string' && tags.includes(node.tag.toLowerCase()) ) { node.tag = node.tag.toLowerCase(); @@ -166,7 +263,7 @@ const lowerElementName = (tree, {tags}) => { return bypass(tree); }; -const lowerAttributeName = tree => { +const lowerAttributeName = (tree, {rules: {lowerAttributeName}}) => { const bypass = tree => tree.map(node => { if (typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, 'content')) { node.content = bypass(node.content); @@ -182,7 +279,7 @@ const lowerAttributeName = tree => { return node; }); - return bypass(tree); + return lowerAttributeName ? bypass(tree) : tree; }; const eof = (tree, {rules: {eof}}) => eof ? [...tree, eof] : tree; @@ -213,7 +310,7 @@ const mini = (tree, {mini}) => { return node; }); - return bypass(tree); + return mini ? bypass(tree) : tree; }; const beautify = (tree, options) => [ diff --git a/src/rules.js b/src/rules.js index bfcbacb..0e9ee91 100644 --- a/src/rules.js +++ b/src/rules.js @@ -2,5 +2,7 @@ export default { indent: 2, blankLines: '\n', eof: '\n', - eol: '\n' + eol: '\n', + useExistingLineBreaks: false, + lowerAttributeName: true }; diff --git a/test/expected/output-with-line-breaks.html b/test/expected/output-with-line-breaks.html new file mode 100644 index 0000000..0d9e73d --- /dev/null +++ b/test/expected/output-with-line-breaks.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
A + Description of A +
BDescription of B
+ +
+

+ Some inline text +

+ +
+ A +
+ B +
+ D +
+ +
+ + +
+ + diff --git a/test/expected/output-with-unbalanced-line-breaks.html b/test/expected/output-with-unbalanced-line-breaks.html new file mode 100644 index 0000000..085daed --- /dev/null +++ b/test/expected/output-with-unbalanced-line-breaks.html @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/test/fixtures/input-with-line-breaks.html b/test/fixtures/input-with-line-breaks.html new file mode 100644 index 0000000..1ec07ea --- /dev/null +++ b/test/fixtures/input-with-line-breaks.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
A + Description of A +
BDescription of B
+ +
+

+ Some inline text +

+ +
+ A +
+ B +
+ D +
+ +
+ + +
+ + \ No newline at end of file diff --git a/test/fixtures/input-with-unbalanced-line-breaks.html b/test/fixtures/input-with-unbalanced-line-breaks.html new file mode 100644 index 0000000..76cbafe --- /dev/null +++ b/test/fixtures/input-with-unbalanced-line-breaks.html @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/test-plugin.js b/test/test-plugin.js index 3d1f3fd..5112abf 100644 --- a/test/test-plugin.js +++ b/test/test-plugin.js @@ -126,3 +126,15 @@ test('processing with plugin beautify and include should return equal html which const fixtures = (await processing(await read('test/fixtures/input-conditional-comment.html'), [require('posthtml-include')(), beautify()])).html; t.deepEqual(expected, fixtures); }); + +test('processing with plugin beautify should keep existing line breaks if instructed', async t => { + const expected = await read('test/expected/output-with-line-breaks.html'); + const fixtures = (await processing(await read('test/fixtures/input-with-line-breaks.html'), [beautify({rules: {useExistingLineBreaks: true}})])).html; + t.deepEqual(expected, fixtures); +}); + +test('processing with plugin beautify and `useExistingLineBreaks: true` should insert missing line break as a first/last child of a tag if there is a line break as a last/first child of this tag', async t => { + const expected = await read('test/expected/output-with-unbalanced-line-breaks.html'); + const fixtures = (await processing(await read('test/fixtures/input-with-unbalanced-line-breaks.html'), [beautify({rules: {useExistingLineBreaks: true}})])).html; + t.deepEqual(expected, fixtures); +});