UNPKG

minimize

Version:
311 lines (275 loc) 8.62 kB
'use strict'; // // Required modules. // var list = require('./list'); // // Predefined parsing options. // var config = { empty: false, // remove(false) or retain(true) empty attributes cdata: false, // remove(false) or retain(true) CDATA from scripts comments: false, // remove(false) or retain(true) comments conditionals: false, // remove(false) or retain(true) ie conditional comments ssi: false, // remove(false) or retain(true) server side includes spare: false, // remove(false) or retain(true) redundant attributes quotes: false, // remove(false) or retain(true) quotes if not required loose: false, // remove(false) all or retain(true) one whitespace whitespace: false, // remove(false) or retain(true) whitespace in attributes dom: { // see https://github.com/fb55/htmlparser2 for all options xmlMode: false, // Disables the special behavior for script/style tags (false by default) lowerCaseAttributeNames: true, // call .toLowerCase for each attribute name (true if xmlMode is `false`) lowerCaseTags: true // call .toLowerCase for each tag name (true if xmlMode is `false`) } }; /** * Compact the value, replace multiple white spaces and newlines * with a single white space. * * @param {String} value Value to compact * @param {Boolean} keepNewlines Do not remove newlines * @return {String} Compacted data * @api private */ function compact(value, keepNewlines) { var check = keepNewlines ? / +/g : /\s+/g; return value.replace(check, ' '); } /** * Helper constructor. * * @Constructor * @param {Object} options * @api public */ function Helpers(options) { this.config = {}; for (var key in options || {}) { this.config[key] = options[key]; } this.ancestor = []; } /** * Wraps the attribute in quotes, or anything that needs them. * * @param {String} value * @return {String} * @api public */ Helpers.prototype.quote = function quote(value) { var delim = ~value.indexOf('"') ? "'" : '"'; // // Quote is only called if required so it's safe to return quotes on no value. // if (!value) return '""'; // // Always quote attributes having spaces, equal signs, single quotes // or ending in a slash. Attributes containing double quotes will be // single quoted. // return /[\s='"<>]+|\/$/.test(value) || this.config.quotes ? delim + value + delim : value; }; /** * Is an element inline or not. * * @param {Object} element * @return {Boolean} * @api private */ Helpers.prototype.isInline = function isInline(element) { return !!~list.inline.indexOf(element.name); }; /** * Create starting tag for element, if required an additional white space will * be added to retain flow of inline elements. * * @param {Object} element * @return {String} * @api public */ Helpers.prototype.tag = function tag(element) { // // Check if the current element requires structure, store for later reference. // if (this.structure(element)) this.ancestor.push(element); return '<' + element.name + this.attributes(element) + '>'; }; /** * Loop set of attributes belonging to an element. Surrounds attributes with * quotes if required, omits if not. * * @param {Object} element element containing attributes * @return {String} * @api public */ Helpers.prototype.attributes = function attributes(element) { var attr = element.attribs , self = this , name = element.name , value, bool, allowed; if (!attr || typeof attr !== 'object') return ''; return Object.keys(attr).reduce(function (result, key) { value = attr[key]; bool = ~list.redundant.indexOf(key); // // Is the attribute allowed on the HTML element? If so, allow special // treatment. If not, then just return the full attribute and its value. // allowed = Object.hasOwnProperty.call(list.attributes, key) && list.attributes[key]; allowed = allowed ? allowed === '*' || ~allowed.indexOf(name) : ~key.indexOf('data-'); // // Remove attributes that are empty, not boolean and no semantic value. // if (!self.config.empty && !/data|itemscope/.test(key) && !bool && !value && allowed) return result; // // Boolean attributes should be added sparse, also unset attributes // should remain unset if retained with `empty` option. // result = result + ' ' + key; if (!self.config.spare) { if (!value.length) return result; if (allowed && bool && (value === key || 'true' === value)) return result; } // // Return full attribute with value. // return result + '=' + self.quote(!self.config.whitespace ? compact(value).trim() : value); }, ''); }; /** * Proxy to render HTML. * * @param {Object} element * @return {String} * @api public */ Helpers.prototype.open = function open(element) { if (element.type in this) { return this[element.type](element); } }; /** * Provide closing tag for element if required. * * @param {Object} element * @return {String} * @api public */ Helpers.prototype.close = function close(element) { if (this.structure(element)) this.ancestor.pop(); return ~list.node.indexOf(element.type) && !~list.singular.indexOf(element.name) ? '</' + element.name + '>' : ''; }; /** * Check the script is actual script or abused for template/config. Scripts * without attribute type or type="text/javascript" are JS elements by default. * * @param {Object} element * @return {Boolean} * @api public */ Helpers.prototype.isJS = function isJS(element) { return (element.type === 'script' && (!element.attribs || !element.attribs.type)) || (element.type === 'script' && element.attribs.type === 'text/javascript'); }; /** * Check if the element is of type style. * * @param {Object} element * @return {Boolean} * @api public */ Helpers.prototype.isStyle = function isStyle(element) { return element.type === 'style'; }; /** * Check if an element needs to retain its internal structure, e.g. this goes * for elements like script, style, textarea or pre. * * @param {Object} element * @return {Boolean} * @api public */ Helpers.prototype.structure = function structure(element) { return element.type !== 'text' ? !!~list.structural.indexOf(element.name) || this.isJS(element) || this.isStyle(element) : false; }; /** * Return trimmed text, if text requires no structure new lines and spaces will * be replaced with a single white space. Any white space adjacent to an inline * element is replaced with a single space. * * @param {Object} element * @return {String} text * @api public */ Helpers.prototype.text = function text(element) { var ancestors = this.ancestor.length , content = element.data , next = element.next , prev = element.prev , space = this.config.loose ? ' ' : ''; // // Collapse space between text and inline elements, clobber space without // inline elements. // if (!ancestors) { content = compact(content.replace( ancestors ? /^[^\S\n]+/ : /^\s+/, prev && this.isInline(prev) ? ' ' : space ).replace( ancestors ? /[^\S\n]+$/ : /\s+$/, next && this.isInline(next) ? ' ' : space )); } // // Remove CDATA from scripts. // if (!this.config.cdata && ancestors && this.isJS(this.ancestor[ancestors - 1])) { content = content.replace(/\/*<!\[CDATA\[/g, '').replace(/\/*\]\]>/g, ''); } return content; }; /** * Returned parsed comment or empty string if config.comments = true. * * @param {Object} element * @return {String} comment * @api public */ Helpers.prototype.comment = function comment(element) { var cfg = this.config; function io() { return '<!--' + compact(element.data, true).trim() + '-->'; } if (cfg.ssi && ~element.data.indexOf('#')) return io(); if (cfg.conditionals && (~element.data.indexOf('[if') || ~element.data.indexOf('<![endif'))) return io(); if (cfg.comments) return io(); return ''; }; /** * Return parsed directive. * * @param {Object} element * @return {String} comment * @api public */ Helpers.prototype.directive = function directive(element) { return '<' + element.data + '>'; }; // // Define some proxies for easy external reference. // Helpers.prototype.script = Helpers.prototype.tag; Helpers.prototype.style = Helpers.prototype.tag; // // Create public proxies. // module.exports = Helpers;