form-cheatsheet/src/format.js

185 lines
4.8 KiB
JavaScript

// 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 = /<label.*?>(?:.*?)?(?:\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;