+ * @property {'html'|'xhtml'|'closingTag'} [options.voidElements='xhtml'] How to handle void elements. html =
![]()
, xhtml =
![]()
, closingTag =
![]()
*/
/**
@@ -12,6 +16,29 @@ import { logger } from './helpers.js';
*/
export let DIFFABLE_OPTIONS_TYPE;
+// From https://developer.mozilla.org/en-US/docs/Glossary/Void_element
+const VOID_ELEMENTS = Object.freeze([
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'link',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr'
+]);
+
+const WHITESPACE_DEPENDENT_TAGS = Object.freeze([
+ 'a',
+ 'pre'
+]);
+
/**
* Uses Parse5 to create an AST from the markup. Loops over the AST to create a formatted HTML string.
*
@@ -25,40 +52,30 @@ export const diffableFormatter = function (markup, options) {
if (typeof(options.showEmptyAttributes) !== 'boolean') {
options.showEmptyAttributes = true;
}
+ if (!['html', 'xhtml', 'closingTag'].includes(options.voidElements)) {
+ options.voidElements = 'xhtml';
+ }
const astOptions = {
sourceCodeLocationInfo: true
};
const ast = parseFragment(markup, astOptions);
- // From https://developer.mozilla.org/en-US/docs/Glossary/Void_element
- const VOID_ELEMENTS = Object.freeze([
- 'area',
- 'base',
- 'br',
- 'col',
- 'embed',
- 'hr',
- 'img',
- 'input',
- 'link',
- 'meta',
- 'param',
- 'source',
- 'track',
- 'wbr'
- ]);
- const WHITESPACE_DEPENDENT_TAGS = Object.freeze([
- 'a',
- 'pre'
- ]);
-
let lastSeenTag = '';
+
+ /**
+ * Applies formatting to each DOM Node in the AST.
+ *
+ * @param {DefaultTreeAdapterMap["childNode"]} node Parse5 AST of a DOM node
+ * @param {number} indent The current indentation level for this DOM node in the AST loop
+ * @return {string} Formatted markup
+ */
const formatNode = (node, indent) => {
indent = indent || 0;
if (node.tagName) {
lastSeenTag = node.tagName;
}
+
const tagIsWhitespaceDependent = WHITESPACE_DEPENDENT_TAGS.includes(lastSeenTag);
const tagIsVoidElement = VOID_ELEMENTS.includes(lastSeenTag);
const hasChildren = node.childNodes && node.childNodes.length;
@@ -109,16 +126,25 @@ export const diffableFormatter = function (markup, options) {
return '\n' + ' '.repeat(indent) + '';
}
+ //
let result = '\n' + ' '.repeat(indent) + '<' + node.nodeName;
let endingAngleBracket = '>';
- if (tagIsVoidElement) {
+ if (
+ tagIsVoidElement &&
+ options.voidElements === 'xhtml'
+ ) {
endingAngleBracket = ' />';
}
// Add attributes
- if (node?.attrs?.length === 1) {
- let attr = node?.attrs[0];
+ if (
+ !node.tagName ||
+ !node.attrs.length
+ ) {
+ result = result + endingAngleBracket;
+ } else if (node.attrs?.length === 1) {
+ let attr = node.attrs[0];
if (
!attr.value &&
!options.showEmptyAttributes
@@ -127,7 +153,7 @@ export const diffableFormatter = function (markup, options) {
} else {
result = result + ' ' + attr.name + '="' + attr.value + '"' + endingAngleBracket;
}
- } else if (node?.attrs?.length) {
+ } else if (node.attrs?.length) {
node.attrs.forEach((attr) => {
if (
!attr.value &&
@@ -139,8 +165,6 @@ export const diffableFormatter = function (markup, options) {
}
});
result = result + '\n' + ' '.repeat(indent) + endingAngleBracket.trim();
- } else {
- result = result + endingAngleBracket;
}
// Process child nodes
@@ -150,12 +174,21 @@ export const diffableFormatter = function (markup, options) {
});
}
- if (!tagIsVoidElement) {
- if (tagIsWhitespaceDependent || !hasChildren) {
- result = result + '' + node.nodeName + '>';
- } else {
- result = result + '\n' + ' '.repeat(indent) + '' + node.nodeName + '>';
- }
+ // Add closing tag
+ if (
+ tagIsWhitespaceDependent ||
+ (
+ !tagIsVoidElement &&
+ !hasChildren
+ ) ||
+ (
+ tagIsVoidElement &&
+ options.voidElements === 'closingTag'
+ )
+ ) {
+ result = result + '' + node.nodeName + '>';
+ } else if (!tagIsVoidElement) {
+ result = result + '\n' + ' '.repeat(indent) + '' + node.nodeName + '>';
}
return result;
diff --git a/tests/unit/src/formatMarkup.test.js b/tests/unit/src/formatMarkup.test.js
index 521da3b..876cf68 100644
--- a/tests/unit/src/formatMarkup.test.js
+++ b/tests/unit/src/formatMarkup.test.js
@@ -31,6 +31,38 @@ const formattedMarkup = `
+`.trim();
+
describe('Format markup', () => {
const info = console.info;
@@ -216,4 +248,23 @@ describe('Format markup', () => {
`);
});
});
+
+ describe('Void elements', () => {
+ beforeEach(() => {
+ globalThis.vueSnapshots.formatter = 'diffable';
+ });
+
+ const voidElementTests = [
+ ['html', formattedMarkupVoidElementsWithHTML],
+ ['xhtml', formattedMarkupVoidElementsWithXHTML],
+ ['closingTag', formattedMarkupVoidElementsWithClosingTag]
+ ];
+
+ test.each(voidElementTests)('Formats void elements using mode "%s"', (mode, expected) => {
+ globalThis.vueSnapshots.formatting.voidElements = mode;
+
+ expect(formatMarkup(unformattedMarkupVoidElements))
+ .toEqual(expected);
+ });
+ });
});