// Make sure regex patterns are only parsed once const $whiteSpacePattern = /^\s+$/ig; const $inputTagPattern = /<(input)(.*?)>/ig; const $optionClosingTagPattern = />(?:\s+)?<\/option>/ig; const $htmlAttributeSpacingPattern = /(\s+)([^in\s][a-z_\-]+=(?:"(?:.*?)"|[^"'`=<>\s]+))/ig; const $labelWrapPattern = /(?:.*?)?(?:\s+)?(<.*?>)(?:.*?)?(?:\s+)?<\/label>/ig; const ELEMENT_NODE = 1; const TEXT_NODE = 3; class Format { /** * Format the display of an input element, and align the attributes * on separate lines * * @param {HTMLInputElement} formElement * @return {string} */ static input(formElement) { // Let's do some ugly DOM manipulation to make the output of the // highlighted have lined up attributes. // For the spacing, we replace normal whitespace characters with // {x} placeholders so the highlighter doesn't mangle them const formElementSpaces = '{s}'.repeat(formElement.tagName.length + 2); const formElementReplace = '{n}' + formElementSpaces; return formElement.outerHTML .trim() .replace($inputTagPattern, `<$1$2 />`) .replace($htmlAttributeSpacingPattern, formElementReplace + '$2'); } /** * Format the display of an option element * * @param {HTMLOptionElement} formElement * @return {string} */ static option(formElement) { let raw = Format.generic(formElement); if (Format.hasHtmlChildren(formElement) && formElement.childNodes.length > 0) { return raw; } raw = raw.replace($whiteSpacePattern, ''); return raw.replace($optionClosingTagPattern, ' />'); } /** * Format the display of elements without specific formatting methods * * @param {HTMLElement} formElement * @return {string} */ static generic(formElement) { return formElement.outerHTML.trim(); } /** * Format the current element * * @param {HTMLElement} formElement * @param {number} i * @return string */ static formatElement(formElement, i) { const elementPrefix = (i > 0) ? '{n}' + '{t}'.repeat(i) : ''; const formattingMethods = ['input', 'option']; // Attempt to indent text nodes if (formElement.nodeType === TEXT_NODE) { //alert('Attempting to format a text node'); return elementPrefix + formElement.nodeValue; } else if (formElement.nodeType === ELEMENT_NODE) { const tagName = formElement.nodeName.toLowerCase(); const formattingMethod = (formattingMethods.includes(tagName)) ? Format[tagName] : Format.generic; return elementPrefix + formattingMethod(formElement); } else if (formElement.nodeValue) { alert('What am I?'); return formElement.nodeValue; } console.error('Empty form element :('); console.error(formElement); } /** * Format a label-wrapped element * * @param {HTMLElement} formElement * @returns {string} */ static formatLabelWrappedElement(formElement) { return formElement.outerHTML + '{n}'; } /** * Check whether an element has an other elements as children * * @param {HTMLElement} element * @return {boolean} */ static hasHtmlChildren(element) { const numChildren = element.childNodes.length; for (let x = 0; x < numChildren; x++) { const node = element.childNodes.item(x); // Only count as a child if the node is an element if (node.nodeType === ELEMENT_NODE) { return true; } } return false; } /** * Recursively format the form elements for better alignment * * @param {HTMLElement} formElement * @param {number} level * @return {string} */ static formatHtml(formElement, level = 0) { const hasChildren = Format.hasHtmlChildren(formElement); const isLabelWrapped = Format.isLabelWrapped(formElement); let formattedHTML = (isLabelWrapped) ? Format.formatLabelWrappedElement(formElement) : Format.formatElement(formElement, level); // If there are no children, just return the formatted element if (!hasChildren) { return formattedHTML; } let children = Array.from(formElement.childNodes); const rawChildren = formElement.innerHTML; // Discard text nodes if they only contain whitespace children = children.filter(node => (!(node.nodeType === TEXT_NODE || $whiteSpacePattern.test(node.nodeValue)))); level++; let newChildrenHTML = children.reduce((prevHTML, node) => { return prevHTML + Format.formatHtml(node, level); }, ''); // Format those closing tags if (level > 0 && hasChildren) { newChildrenHTML += '{n}' + '{t}'.repeat(level - 1); } formattedHTML = formattedHTML.replace(rawChildren, newChildrenHTML); return formattedHTML; } /** * Check whether an element * * @param {Element|Element[]} labelElements * @returns {boolean} */ static isLabelWrapped(labelElements) { if (labelElements.length === 0) { return false; } const labelElement = (Array.isArray(labelElements)) ? labelElements[0] : labelElements; return $labelWrapPattern.test(labelElement.outerHTML); } } export default Format;