UNPKG

diffable-html

Version:

Opinionated HTML formatter focused towards making HTML diffs readable.

252 lines (203 loc) 5.75 kB
'use strict'; var htmlparser2 = require('htmlparser2'); // https://www.w3.org/TR/html/syntax.html#writing-html-documents-elements var voidElements = [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr' ]; var format = function(html, ref) { if ( ref === void 0 ) ref = {}; var sortAttributes = ref.sortAttributes; if ( sortAttributes === void 0 ) sortAttributes = function (names) { return names; }; var elements = []; var indentSize = 2; var currentDepth = 0; var increaseCurrentDepth = function () { currentDepth++; }; var decreaseCurrentDepth = function () { currentDepth--; }; var getIndentation = function (size) { return ' '.repeat(size); }; var getIndentationForDepth = function (depth) { return getIndentation(indentSize * depth); }; var getCurrentIndentation = function () { return getIndentationForDepth(currentDepth); }; var getAttributeIndentation = function (tagName) { return getIndentation(indentSize * currentDepth - 1); }; var append = function (content) { elements.push(content); }; var appendLineBreak = function () { append('\n'); }; var appendIndentation = function (depth) { append(getIndentationForDepth(depth)); }; var appendCurrentIndentation = function () { append(getCurrentIndentation()); }; var appendOpeningTag = function (name) { append('<' + name); }; var appendClosingTagOnSameLine = function (closeWith) { if ( closeWith === void 0 ) closeWith = '>'; append(closeWith); }; var appendClosingTagOnNewLine = function (closeWith) { if ( closeWith === void 0 ) closeWith = '>'; appendLineBreak(); appendIndentation(currentDepth - 1); append(closeWith); }; var appendAttribute = function (name, value) { var attribute = ' ' + name; if (value.length > 0) { attribute += "=\"" + value + "\""; } append(attribute); }; var appendAttributeOnNewLine = function (name, value, tagName) { appendLineBreak(); append(getAttributeIndentation(tagName)); appendAttribute(name, value); }; var appendAttributes = function (attributes, tagName) { var names = sortAttributes(Object.keys(attributes)); if (names.length === 1) { appendAttribute(names[0], attributes[names[0]]); } if (names.length <= 1) { return; } for (var name of names) { appendAttributeOnNewLine(name, attributes[name], tagName); } }; var appendClosingTag = function (attributes, closeWith) { if (Object.keys(attributes).length <= 1) { appendClosingTagOnSameLine(closeWith); return; } appendClosingTagOnNewLine(closeWith); }; var render = function () { return elements.join(''); }; var isXmlDirective = function (name) { return name === '?xml'; }; var isVoidTagName = function (name) { return voidElements.indexOf(name) !== -1; }; // https://www.w3.org/TR/html52/infrastructure.html#space-characters // defines "space characters" to include SPACE, TAB, LF, FF, and CR. var trimText = function (text) { return text.replace(/^[ \t\n\f\r]+|[ \t\n\f\r]+$/g, ''); }; var extractAttributesFromString = function (content) { var attributes = {}; var pieces = content.split(/\s/); // Remove tag name. delete pieces[0]; pieces.forEach(function (element) { if (element.length === 0) { return; } if (element.indexOf('=') === -1) { attributes[element] = ''; } }); var attributesRegex = /(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|[>"']))+.)["']?/gim; var result; while ((result = attributesRegex.exec(content))) { attributes[result[1]] = result[2]; } return attributes; }; var parser = new htmlparser2.Parser( { onprocessinginstruction: function(name, data) { var closingTag = '>'; if (isXmlDirective(name)) { closingTag = '?>'; } appendLineBreak(); appendCurrentIndentation(); increaseCurrentDepth(); appendOpeningTag(name); var attributes = extractAttributesFromString(data); appendAttributes(attributes, name); appendClosingTag(attributes, closingTag); decreaseCurrentDepth(); }, onopentag: function(name, attributes) { appendLineBreak(); appendCurrentIndentation(); increaseCurrentDepth(); appendOpeningTag(name); appendAttributes(attributes, name); appendClosingTag(attributes, '>'); }, ontext: function(text) { var trimmed = trimText(text); if (trimmed.length === 0) { return; } appendLineBreak(); appendCurrentIndentation(); append(trimmed); }, onclosetag: function(tagname) { var isVoidTag = isVoidTagName(tagname); if (isVoidTagName(tagname) === false) { appendLineBreak(); } decreaseCurrentDepth(); if (isVoidTag === true) { return; } appendCurrentIndentation(); append(("</" + tagname + ">")); }, oncomment: function(data) { // Only display conditional comments. if (!data.startsWith('[')) { return; } appendLineBreak(); appendCurrentIndentation(); append('<!--'); append(data); append('-->'); }, }, { lowerCaseTags: false, recognizeSelfClosing: true, decodeEntities: false, } ); parser.write(html); parser.end(); appendLineBreak(); return render(); }; module.exports = format;