[79a0317] | 1 | 'use strict';
|
---|
| 2 |
|
---|
| 3 | var CleanCSS = require('clean-css');
|
---|
| 4 | var decode = require('he').decode;
|
---|
| 5 | var HTMLParser = require('./htmlparser').HTMLParser;
|
---|
| 6 | var endTag = require('./htmlparser').endTag;
|
---|
| 7 | var RelateUrl = require('relateurl');
|
---|
| 8 | var TokenChain = require('./tokenchain');
|
---|
| 9 | var Terser = require('terser');
|
---|
| 10 | var utils = require('./utils');
|
---|
| 11 |
|
---|
| 12 | function trimWhitespace(str) {
|
---|
| 13 | return str && str.replace(/^[ \n\r\t\f]+/, '').replace(/[ \n\r\t\f]+$/, '');
|
---|
| 14 | }
|
---|
| 15 |
|
---|
| 16 | function collapseWhitespaceAll(str) {
|
---|
| 17 | // Non-breaking space is specifically handled inside the replacer function here:
|
---|
| 18 | return str && str.replace(/[ \n\r\t\f\xA0]+/g, function(spaces) {
|
---|
| 19 | return spaces === '\t' ? '\t' : spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 ');
|
---|
| 20 | });
|
---|
| 21 | }
|
---|
| 22 |
|
---|
| 23 | function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
---|
| 24 | var lineBreakBefore = '', lineBreakAfter = '';
|
---|
| 25 |
|
---|
| 26 | if (options.preserveLineBreaks) {
|
---|
| 27 | str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function() {
|
---|
| 28 | lineBreakBefore = '\n';
|
---|
| 29 | return '';
|
---|
| 30 | }).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function() {
|
---|
| 31 | lineBreakAfter = '\n';
|
---|
| 32 | return '';
|
---|
| 33 | });
|
---|
| 34 | }
|
---|
| 35 |
|
---|
| 36 | if (trimLeft) {
|
---|
| 37 | // Non-breaking space is specifically handled inside the replacer function here:
|
---|
| 38 | str = str.replace(/^[ \n\r\t\f\xA0]+/, function(spaces) {
|
---|
| 39 | var conservative = !lineBreakBefore && options.conservativeCollapse;
|
---|
| 40 | if (conservative && spaces === '\t') {
|
---|
| 41 | return '\t';
|
---|
| 42 | }
|
---|
| 43 | return spaces.replace(/^[^\xA0]+/, '').replace(/(\xA0+)[^\xA0]+/g, '$1 ') || (conservative ? ' ' : '');
|
---|
| 44 | });
|
---|
| 45 | }
|
---|
| 46 |
|
---|
| 47 | if (trimRight) {
|
---|
| 48 | // Non-breaking space is specifically handled inside the replacer function here:
|
---|
| 49 | str = str.replace(/[ \n\r\t\f\xA0]+$/, function(spaces) {
|
---|
| 50 | var conservative = !lineBreakAfter && options.conservativeCollapse;
|
---|
| 51 | if (conservative && spaces === '\t') {
|
---|
| 52 | return '\t';
|
---|
| 53 | }
|
---|
| 54 | return spaces.replace(/[^\xA0]+(\xA0+)/g, ' $1').replace(/[^\xA0]+$/, '') || (conservative ? ' ' : '');
|
---|
| 55 | });
|
---|
| 56 | }
|
---|
| 57 |
|
---|
| 58 | if (collapseAll) {
|
---|
| 59 | // strip non space whitespace then compress spaces to one
|
---|
| 60 | str = collapseWhitespaceAll(str);
|
---|
| 61 | }
|
---|
| 62 |
|
---|
| 63 | return lineBreakBefore + str + lineBreakAfter;
|
---|
| 64 | }
|
---|
| 65 |
|
---|
| 66 | var createMapFromString = utils.createMapFromString;
|
---|
| 67 | // non-empty tags that will maintain whitespace around them
|
---|
| 68 | var inlineTags = createMapFromString('a,abbr,acronym,b,bdi,bdo,big,button,cite,code,del,dfn,em,font,i,ins,kbd,label,mark,math,nobr,object,q,rp,rt,rtc,ruby,s,samp,select,small,span,strike,strong,sub,sup,svg,textarea,time,tt,u,var');
|
---|
| 69 | // non-empty tags that will maintain whitespace within them
|
---|
| 70 | var inlineTextTags = createMapFromString('a,abbr,acronym,b,big,del,em,font,i,ins,kbd,mark,nobr,rp,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var');
|
---|
| 71 | // self-closing tags that will maintain whitespace around them
|
---|
| 72 | var selfClosingInlineTags = createMapFromString('comment,img,input,wbr');
|
---|
| 73 |
|
---|
| 74 | function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
|
---|
| 75 | var trimLeft = prevTag && !selfClosingInlineTags(prevTag);
|
---|
| 76 | if (trimLeft && !options.collapseInlineTagWhitespace) {
|
---|
| 77 | trimLeft = prevTag.charAt(0) === '/' ? !inlineTags(prevTag.slice(1)) : !inlineTextTags(prevTag);
|
---|
| 78 | }
|
---|
| 79 | var trimRight = nextTag && !selfClosingInlineTags(nextTag);
|
---|
| 80 | if (trimRight && !options.collapseInlineTagWhitespace) {
|
---|
| 81 | trimRight = nextTag.charAt(0) === '/' ? !inlineTextTags(nextTag.slice(1)) : !inlineTags(nextTag);
|
---|
| 82 | }
|
---|
| 83 | return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
---|
| 84 | }
|
---|
| 85 |
|
---|
| 86 | function isConditionalComment(text) {
|
---|
| 87 | return /^\[if\s[^\]]+]|\[endif]$/.test(text);
|
---|
| 88 | }
|
---|
| 89 |
|
---|
| 90 | function isIgnoredComment(text, options) {
|
---|
| 91 | for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
|
---|
| 92 | if (options.ignoreCustomComments[i].test(text)) {
|
---|
| 93 | return true;
|
---|
| 94 | }
|
---|
| 95 | }
|
---|
| 96 | return false;
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 | function isEventAttribute(attrName, options) {
|
---|
| 100 | var patterns = options.customEventAttributes;
|
---|
| 101 | if (patterns) {
|
---|
| 102 | for (var i = patterns.length; i--;) {
|
---|
| 103 | if (patterns[i].test(attrName)) {
|
---|
| 104 | return true;
|
---|
| 105 | }
|
---|
| 106 | }
|
---|
| 107 | return false;
|
---|
| 108 | }
|
---|
| 109 | return /^on[a-z]{3,}$/.test(attrName);
|
---|
| 110 | }
|
---|
| 111 |
|
---|
| 112 | function canRemoveAttributeQuotes(value) {
|
---|
| 113 | // https://mathiasbynens.be/notes/unquoted-attribute-values
|
---|
| 114 | return /^[^ \t\n\f\r"'`=<>]+$/.test(value);
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | function attributesInclude(attributes, attribute) {
|
---|
| 118 | for (var i = attributes.length; i--;) {
|
---|
| 119 | if (attributes[i].name.toLowerCase() === attribute) {
|
---|
| 120 | return true;
|
---|
| 121 | }
|
---|
| 122 | }
|
---|
| 123 | return false;
|
---|
| 124 | }
|
---|
| 125 |
|
---|
| 126 | function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
---|
| 127 | attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
---|
| 128 |
|
---|
| 129 | return (
|
---|
| 130 | tag === 'script' &&
|
---|
| 131 | attrName === 'language' &&
|
---|
| 132 | attrValue === 'javascript' ||
|
---|
| 133 |
|
---|
| 134 | tag === 'form' &&
|
---|
| 135 | attrName === 'method' &&
|
---|
| 136 | attrValue === 'get' ||
|
---|
| 137 |
|
---|
| 138 | tag === 'input' &&
|
---|
| 139 | attrName === 'type' &&
|
---|
| 140 | attrValue === 'text' ||
|
---|
| 141 |
|
---|
| 142 | tag === 'script' &&
|
---|
| 143 | attrName === 'charset' &&
|
---|
| 144 | !attributesInclude(attrs, 'src') ||
|
---|
| 145 |
|
---|
| 146 | tag === 'a' &&
|
---|
| 147 | attrName === 'name' &&
|
---|
| 148 | attributesInclude(attrs, 'id') ||
|
---|
| 149 |
|
---|
| 150 | tag === 'area' &&
|
---|
| 151 | attrName === 'shape' &&
|
---|
| 152 | attrValue === 'rect'
|
---|
| 153 | );
|
---|
| 154 | }
|
---|
| 155 |
|
---|
| 156 | // https://mathiasbynens.be/demo/javascript-mime-type
|
---|
| 157 | // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
|
---|
| 158 | var executableScriptsMimetypes = utils.createMap([
|
---|
| 159 | 'text/javascript',
|
---|
| 160 | 'text/ecmascript',
|
---|
| 161 | 'text/jscript',
|
---|
| 162 | 'application/javascript',
|
---|
| 163 | 'application/x-javascript',
|
---|
| 164 | 'application/ecmascript',
|
---|
| 165 | 'module'
|
---|
| 166 | ]);
|
---|
| 167 |
|
---|
| 168 | var keepScriptsMimetypes = utils.createMap([
|
---|
| 169 | 'module'
|
---|
| 170 | ]);
|
---|
| 171 |
|
---|
| 172 | function isScriptTypeAttribute(attrValue) {
|
---|
| 173 | attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
---|
| 174 | return attrValue === '' || executableScriptsMimetypes(attrValue);
|
---|
| 175 | }
|
---|
| 176 |
|
---|
| 177 | function keepScriptTypeAttribute(attrValue) {
|
---|
| 178 | attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
---|
| 179 | return keepScriptsMimetypes(attrValue);
|
---|
| 180 | }
|
---|
| 181 |
|
---|
| 182 | function isExecutableScript(tag, attrs) {
|
---|
| 183 | if (tag !== 'script') {
|
---|
| 184 | return false;
|
---|
| 185 | }
|
---|
| 186 | for (var i = 0, len = attrs.length; i < len; i++) {
|
---|
| 187 | var attrName = attrs[i].name.toLowerCase();
|
---|
| 188 | if (attrName === 'type') {
|
---|
| 189 | return isScriptTypeAttribute(attrs[i].value);
|
---|
| 190 | }
|
---|
| 191 | }
|
---|
| 192 | return true;
|
---|
| 193 | }
|
---|
| 194 |
|
---|
| 195 | function isStyleLinkTypeAttribute(attrValue) {
|
---|
| 196 | attrValue = trimWhitespace(attrValue).toLowerCase();
|
---|
| 197 | return attrValue === '' || attrValue === 'text/css';
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | function isStyleSheet(tag, attrs) {
|
---|
| 201 | if (tag !== 'style') {
|
---|
| 202 | return false;
|
---|
| 203 | }
|
---|
| 204 | for (var i = 0, len = attrs.length; i < len; i++) {
|
---|
| 205 | var attrName = attrs[i].name.toLowerCase();
|
---|
| 206 | if (attrName === 'type') {
|
---|
| 207 | return isStyleLinkTypeAttribute(attrs[i].value);
|
---|
| 208 | }
|
---|
| 209 | }
|
---|
| 210 | return true;
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | var isSimpleBoolean = createMapFromString('allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible');
|
---|
| 214 | var isBooleanValue = createMapFromString('true,false');
|
---|
| 215 |
|
---|
| 216 | function isBooleanAttribute(attrName, attrValue) {
|
---|
| 217 | return isSimpleBoolean(attrName) || attrName === 'draggable' && !isBooleanValue(attrValue);
|
---|
| 218 | }
|
---|
| 219 |
|
---|
| 220 | function isUriTypeAttribute(attrName, tag) {
|
---|
| 221 | return (
|
---|
| 222 | /^(?:a|area|link|base)$/.test(tag) && attrName === 'href' ||
|
---|
| 223 | tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName) ||
|
---|
| 224 | tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName) ||
|
---|
| 225 | tag === 'q' && attrName === 'cite' ||
|
---|
| 226 | tag === 'blockquote' && attrName === 'cite' ||
|
---|
| 227 | (tag === 'ins' || tag === 'del') && attrName === 'cite' ||
|
---|
| 228 | tag === 'form' && attrName === 'action' ||
|
---|
| 229 | tag === 'input' && (attrName === 'src' || attrName === 'usemap') ||
|
---|
| 230 | tag === 'head' && attrName === 'profile' ||
|
---|
| 231 | tag === 'script' && (attrName === 'src' || attrName === 'for')
|
---|
| 232 | );
|
---|
| 233 | }
|
---|
| 234 |
|
---|
| 235 | function isNumberTypeAttribute(attrName, tag) {
|
---|
| 236 | return (
|
---|
| 237 | /^(?:a|area|object|button)$/.test(tag) && attrName === 'tabindex' ||
|
---|
| 238 | tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex') ||
|
---|
| 239 | tag === 'select' && (attrName === 'size' || attrName === 'tabindex') ||
|
---|
| 240 | tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName) ||
|
---|
| 241 | tag === 'colgroup' && attrName === 'span' ||
|
---|
| 242 | tag === 'col' && attrName === 'span' ||
|
---|
| 243 | (tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan')
|
---|
| 244 | );
|
---|
| 245 | }
|
---|
| 246 |
|
---|
| 247 | function isLinkType(tag, attrs, value) {
|
---|
| 248 | if (tag !== 'link') {
|
---|
| 249 | return false;
|
---|
| 250 | }
|
---|
| 251 | for (var i = 0, len = attrs.length; i < len; i++) {
|
---|
| 252 | if (attrs[i].name === 'rel' && attrs[i].value === value) {
|
---|
| 253 | return true;
|
---|
| 254 | }
|
---|
| 255 | }
|
---|
| 256 | }
|
---|
| 257 |
|
---|
| 258 | function isMediaQuery(tag, attrs, attrName) {
|
---|
| 259 | return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
|
---|
| 260 | }
|
---|
| 261 |
|
---|
| 262 | var srcsetTags = createMapFromString('img,source');
|
---|
| 263 |
|
---|
| 264 | function isSrcset(attrName, tag) {
|
---|
| 265 | return attrName === 'srcset' && srcsetTags(tag);
|
---|
| 266 | }
|
---|
| 267 |
|
---|
| 268 | async function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
|
---|
| 269 | if (isEventAttribute(attrName, options)) {
|
---|
| 270 | attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
---|
| 271 | return await options.minifyJS(attrValue, true);
|
---|
| 272 | }
|
---|
| 273 | else if (attrName === 'class') {
|
---|
| 274 | attrValue = trimWhitespace(attrValue);
|
---|
| 275 | if (options.sortClassName) {
|
---|
| 276 | attrValue = options.sortClassName(attrValue);
|
---|
| 277 | }
|
---|
| 278 | else {
|
---|
| 279 | attrValue = collapseWhitespaceAll(attrValue);
|
---|
| 280 | }
|
---|
| 281 | return attrValue;
|
---|
| 282 | }
|
---|
| 283 | else if (isUriTypeAttribute(attrName, tag)) {
|
---|
| 284 | attrValue = trimWhitespace(attrValue);
|
---|
| 285 | return isLinkType(tag, attrs, 'canonical') ? attrValue : options.minifyURLs(attrValue);
|
---|
| 286 | }
|
---|
| 287 | else if (isNumberTypeAttribute(attrName, tag)) {
|
---|
| 288 | return trimWhitespace(attrValue);
|
---|
| 289 | }
|
---|
| 290 | else if (attrName === 'style') {
|
---|
| 291 | attrValue = trimWhitespace(attrValue);
|
---|
| 292 | if (attrValue) {
|
---|
| 293 | if (/;$/.test(attrValue) && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
---|
| 294 | attrValue = attrValue.replace(/\s*;$/, ';');
|
---|
| 295 | }
|
---|
| 296 | attrValue = options.minifyCSS(attrValue, 'inline');
|
---|
| 297 | }
|
---|
| 298 | return attrValue;
|
---|
| 299 | }
|
---|
| 300 | else if (isSrcset(attrName, tag)) {
|
---|
| 301 | // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
---|
| 302 | attrValue = trimWhitespace(attrValue).split(/\s+,\s*|\s*,\s+/).map(function(candidate) {
|
---|
| 303 | var url = candidate;
|
---|
| 304 | var descriptor = '';
|
---|
| 305 | var match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
---|
| 306 | if (match) {
|
---|
| 307 | url = url.slice(0, -match[0].length);
|
---|
| 308 | var num = +match[1].slice(0, -1);
|
---|
| 309 | var suffix = match[1].slice(-1);
|
---|
| 310 | if (num !== 1 || suffix !== 'x') {
|
---|
| 311 | descriptor = ' ' + num + suffix;
|
---|
| 312 | }
|
---|
| 313 | }
|
---|
| 314 | return options.minifyURLs(url) + descriptor;
|
---|
| 315 | }).join(', ');
|
---|
| 316 | }
|
---|
| 317 | else if (isMetaViewport(tag, attrs) && attrName === 'content') {
|
---|
| 318 | attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function(numString) {
|
---|
| 319 | // "0.90000" -> "0.9"
|
---|
| 320 | // "1.0" -> "1"
|
---|
| 321 | // "1.0001" -> "1.0001" (unchanged)
|
---|
| 322 | return (+numString).toString();
|
---|
| 323 | });
|
---|
| 324 | }
|
---|
| 325 | else if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
---|
| 326 | return collapseWhitespaceAll(attrValue);
|
---|
| 327 | }
|
---|
| 328 | else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
---|
| 329 | attrValue = trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
|
---|
| 330 | }
|
---|
| 331 | else if (tag === 'script' && attrName === 'type') {
|
---|
| 332 | attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
|
---|
| 333 | }
|
---|
| 334 | else if (isMediaQuery(tag, attrs, attrName)) {
|
---|
| 335 | attrValue = trimWhitespace(attrValue);
|
---|
| 336 | return options.minifyCSS(attrValue, 'media');
|
---|
| 337 | }
|
---|
| 338 | return attrValue;
|
---|
| 339 | }
|
---|
| 340 |
|
---|
| 341 | function isMetaViewport(tag, attrs) {
|
---|
| 342 | if (tag !== 'meta') {
|
---|
| 343 | return false;
|
---|
| 344 | }
|
---|
| 345 | for (var i = 0, len = attrs.length; i < len; i++) {
|
---|
| 346 | if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
---|
| 347 | return true;
|
---|
| 348 | }
|
---|
| 349 | }
|
---|
| 350 | }
|
---|
| 351 |
|
---|
| 352 | function isContentSecurityPolicy(tag, attrs) {
|
---|
| 353 | if (tag !== 'meta') {
|
---|
| 354 | return false;
|
---|
| 355 | }
|
---|
| 356 | for (var i = 0, len = attrs.length; i < len; i++) {
|
---|
| 357 | if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') {
|
---|
| 358 | return true;
|
---|
| 359 | }
|
---|
| 360 | }
|
---|
| 361 | }
|
---|
| 362 |
|
---|
| 363 | function ignoreCSS(id) {
|
---|
| 364 | return '/* clean-css ignore:start */' + id + '/* clean-css ignore:end */';
|
---|
| 365 | }
|
---|
| 366 |
|
---|
| 367 | // Wrap CSS declarations for CleanCSS > 3.x
|
---|
| 368 | // See https://github.com/jakubpawlowicz/clean-css/issues/418
|
---|
| 369 | function wrapCSS(text, type) {
|
---|
| 370 | switch (type) {
|
---|
| 371 | case 'inline':
|
---|
| 372 | return '*{' + text + '}';
|
---|
| 373 | case 'media':
|
---|
| 374 | return '@media ' + text + '{a{top:0}}';
|
---|
| 375 | default:
|
---|
| 376 | return text;
|
---|
| 377 | }
|
---|
| 378 | }
|
---|
| 379 |
|
---|
| 380 | function unwrapCSS(text, type) {
|
---|
| 381 | var matches;
|
---|
| 382 | switch (type) {
|
---|
| 383 | case 'inline':
|
---|
| 384 | matches = text.match(/^\*\{([\s\S]*)\}$/);
|
---|
| 385 | break;
|
---|
| 386 | case 'media':
|
---|
| 387 | matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
|
---|
| 388 | break;
|
---|
| 389 | }
|
---|
| 390 | return matches ? matches[1] : text;
|
---|
| 391 | }
|
---|
| 392 |
|
---|
| 393 | async function cleanConditionalComment(comment, options) {
|
---|
| 394 | return options.processConditionalComments ? await utils.replaceAsync(comment, /^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, async function(match, prefix, text, suffix) {
|
---|
| 395 | return prefix + await minify(text, options, true) + suffix;
|
---|
| 396 | }) : comment;
|
---|
| 397 | }
|
---|
| 398 |
|
---|
| 399 | async function processScript(text, options, currentAttrs) {
|
---|
| 400 | for (var i = 0, len = currentAttrs.length; i < len; i++) {
|
---|
| 401 | if (currentAttrs[i].name.toLowerCase() === 'type' &&
|
---|
| 402 | options.processScripts.indexOf(currentAttrs[i].value) > -1) {
|
---|
| 403 | return await minify(text, options);
|
---|
| 404 | }
|
---|
| 405 | }
|
---|
| 406 | return text;
|
---|
| 407 | }
|
---|
| 408 |
|
---|
| 409 | // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
---|
| 410 | // with the following deviations:
|
---|
| 411 | // - retain <body> if followed by <noscript>
|
---|
| 412 | // - </rb>, </rt>, </rtc>, </rp> & </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
|
---|
| 413 | // - retain all tags which are adjacent to non-standard HTML tags
|
---|
| 414 | var optionalStartTags = createMapFromString('html,head,body,colgroup,tbody');
|
---|
| 415 | var optionalEndTags = createMapFromString('html,head,body,li,dt,dd,p,rb,rt,rtc,rp,optgroup,option,colgroup,caption,thead,tbody,tfoot,tr,td,th');
|
---|
| 416 | var headerTags = createMapFromString('meta,link,script,style,template,noscript');
|
---|
| 417 | var descriptionTags = createMapFromString('dt,dd');
|
---|
| 418 | var pBlockTags = createMapFromString('address,article,aside,blockquote,details,div,dl,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,main,menu,nav,ol,p,pre,section,table,ul');
|
---|
| 419 | var pInlineTags = createMapFromString('a,audio,del,ins,map,noscript,video');
|
---|
| 420 | var rubyTags = createMapFromString('rb,rt,rtc,rp');
|
---|
| 421 | var rtcTag = createMapFromString('rb,rtc,rp');
|
---|
| 422 | var optionTag = createMapFromString('option,optgroup');
|
---|
| 423 | var tableContentTags = createMapFromString('tbody,tfoot');
|
---|
| 424 | var tableSectionTags = createMapFromString('thead,tbody,tfoot');
|
---|
| 425 | var cellTags = createMapFromString('td,th');
|
---|
| 426 | var topLevelTags = createMapFromString('html,head,body');
|
---|
| 427 | var compactTags = createMapFromString('html,body');
|
---|
| 428 | var looseTags = createMapFromString('head,colgroup,caption');
|
---|
| 429 | var trailingTags = createMapFromString('dt,thead');
|
---|
| 430 | var htmlTags = createMapFromString('a,abbr,acronym,address,applet,area,article,aside,audio,b,base,basefont,bdi,bdo,bgsound,big,blink,blockquote,body,br,button,canvas,caption,center,cite,code,col,colgroup,command,content,data,datalist,dd,del,details,dfn,dialog,dir,div,dl,dt,element,em,embed,fieldset,figcaption,figure,font,footer,form,frame,frameset,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,image,img,input,ins,isindex,kbd,keygen,label,legend,li,link,listing,main,map,mark,marquee,menu,menuitem,meta,meter,multicol,nav,nobr,noembed,noframes,noscript,object,ol,optgroup,option,output,p,param,picture,plaintext,pre,progress,q,rb,rp,rt,rtc,ruby,s,samp,script,section,select,shadow,small,source,spacer,span,strike,strong,style,sub,summary,sup,table,tbody,td,template,textarea,tfoot,th,thead,time,title,tr,track,tt,u,ul,var,video,wbr,xmp');
|
---|
| 431 |
|
---|
| 432 | function canRemoveParentTag(optionalStartTag, tag) {
|
---|
| 433 | switch (optionalStartTag) {
|
---|
| 434 | case 'html':
|
---|
| 435 | case 'head':
|
---|
| 436 | return true;
|
---|
| 437 | case 'body':
|
---|
| 438 | return !headerTags(tag);
|
---|
| 439 | case 'colgroup':
|
---|
| 440 | return tag === 'col';
|
---|
| 441 | case 'tbody':
|
---|
| 442 | return tag === 'tr';
|
---|
| 443 | }
|
---|
| 444 | return false;
|
---|
| 445 | }
|
---|
| 446 |
|
---|
| 447 | function isStartTagMandatory(optionalEndTag, tag) {
|
---|
| 448 | switch (tag) {
|
---|
| 449 | case 'colgroup':
|
---|
| 450 | return optionalEndTag === 'colgroup';
|
---|
| 451 | case 'tbody':
|
---|
| 452 | return tableSectionTags(optionalEndTag);
|
---|
| 453 | }
|
---|
| 454 | return false;
|
---|
| 455 | }
|
---|
| 456 |
|
---|
| 457 | function canRemovePrecedingTag(optionalEndTag, tag) {
|
---|
| 458 | switch (optionalEndTag) {
|
---|
| 459 | case 'html':
|
---|
| 460 | case 'head':
|
---|
| 461 | case 'body':
|
---|
| 462 | case 'colgroup':
|
---|
| 463 | case 'caption':
|
---|
| 464 | return true;
|
---|
| 465 | case 'li':
|
---|
| 466 | case 'optgroup':
|
---|
| 467 | case 'tr':
|
---|
| 468 | return tag === optionalEndTag;
|
---|
| 469 | case 'dt':
|
---|
| 470 | case 'dd':
|
---|
| 471 | return descriptionTags(tag);
|
---|
| 472 | case 'p':
|
---|
| 473 | return pBlockTags(tag);
|
---|
| 474 | case 'rb':
|
---|
| 475 | case 'rt':
|
---|
| 476 | case 'rp':
|
---|
| 477 | return rubyTags(tag);
|
---|
| 478 | case 'rtc':
|
---|
| 479 | return rtcTag(tag);
|
---|
| 480 | case 'option':
|
---|
| 481 | return optionTag(tag);
|
---|
| 482 | case 'thead':
|
---|
| 483 | case 'tbody':
|
---|
| 484 | return tableContentTags(tag);
|
---|
| 485 | case 'tfoot':
|
---|
| 486 | return tag === 'tbody';
|
---|
| 487 | case 'td':
|
---|
| 488 | case 'th':
|
---|
| 489 | return cellTags(tag);
|
---|
| 490 | }
|
---|
| 491 | return false;
|
---|
| 492 | }
|
---|
| 493 |
|
---|
| 494 | var reEmptyAttribute = new RegExp(
|
---|
| 495 | '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
|
---|
| 496 | '?:down|up|over|move|out)|key(?:press|down|up)))$');
|
---|
| 497 |
|
---|
| 498 | function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
---|
| 499 | var isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
|
---|
| 500 | if (!isValueEmpty) {
|
---|
| 501 | return false;
|
---|
| 502 | }
|
---|
| 503 | if (typeof options.removeEmptyAttributes === 'function') {
|
---|
| 504 | return options.removeEmptyAttributes(attrName, tag);
|
---|
| 505 | }
|
---|
| 506 | return tag === 'input' && attrName === 'value' || reEmptyAttribute.test(attrName);
|
---|
| 507 | }
|
---|
| 508 |
|
---|
| 509 | function hasAttrName(name, attrs) {
|
---|
| 510 | for (var i = attrs.length - 1; i >= 0; i--) {
|
---|
| 511 | if (attrs[i].name === name) {
|
---|
| 512 | return true;
|
---|
| 513 | }
|
---|
| 514 | }
|
---|
| 515 | return false;
|
---|
| 516 | }
|
---|
| 517 |
|
---|
| 518 | function canRemoveElement(tag, attrs) {
|
---|
| 519 | switch (tag) {
|
---|
| 520 | case 'textarea':
|
---|
| 521 | return false;
|
---|
| 522 | case 'audio':
|
---|
| 523 | case 'script':
|
---|
| 524 | case 'video':
|
---|
| 525 | if (hasAttrName('src', attrs)) {
|
---|
| 526 | return false;
|
---|
| 527 | }
|
---|
| 528 | break;
|
---|
| 529 | case 'iframe':
|
---|
| 530 | if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
|
---|
| 531 | return false;
|
---|
| 532 | }
|
---|
| 533 | break;
|
---|
| 534 | case 'object':
|
---|
| 535 | if (hasAttrName('data', attrs)) {
|
---|
| 536 | return false;
|
---|
| 537 | }
|
---|
| 538 | break;
|
---|
| 539 | case 'applet':
|
---|
| 540 | if (hasAttrName('code', attrs)) {
|
---|
| 541 | return false;
|
---|
| 542 | }
|
---|
| 543 | break;
|
---|
| 544 | }
|
---|
| 545 | return true;
|
---|
| 546 | }
|
---|
| 547 |
|
---|
| 548 | function canCollapseWhitespace(tag) {
|
---|
| 549 | return !/^(?:script|style|pre|textarea)$/.test(tag);
|
---|
| 550 | }
|
---|
| 551 |
|
---|
| 552 | function canTrimWhitespace(tag) {
|
---|
| 553 | return !/^(?:pre|textarea)$/.test(tag);
|
---|
| 554 | }
|
---|
| 555 |
|
---|
| 556 | async function normalizeAttr(attr, attrs, tag, options) {
|
---|
| 557 | var attrName = options.name(attr.name),
|
---|
| 558 | attrValue = attr.value;
|
---|
| 559 |
|
---|
| 560 | if (options.decodeEntities && attrValue) {
|
---|
| 561 | attrValue = decode(attrValue, { isAttributeValue: true });
|
---|
| 562 | }
|
---|
| 563 |
|
---|
| 564 | if (options.removeRedundantAttributes &&
|
---|
| 565 | isAttributeRedundant(tag, attrName, attrValue, attrs) ||
|
---|
| 566 | options.removeScriptTypeAttributes && tag === 'script' &&
|
---|
| 567 | attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue) ||
|
---|
| 568 | options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
---|
| 569 | attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) {
|
---|
| 570 | return;
|
---|
| 571 | }
|
---|
| 572 |
|
---|
| 573 | if (attrValue) {
|
---|
| 574 | attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs);
|
---|
| 575 | }
|
---|
| 576 |
|
---|
| 577 | if (options.removeEmptyAttributes &&
|
---|
| 578 | canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
---|
| 579 | return;
|
---|
| 580 | }
|
---|
| 581 |
|
---|
| 582 | if (options.decodeEntities && attrValue) {
|
---|
| 583 | attrValue = attrValue.replace(/&(#?[0-9a-zA-Z]+;)/g, '&$1');
|
---|
| 584 | }
|
---|
| 585 |
|
---|
| 586 | return {
|
---|
| 587 | attr: attr,
|
---|
| 588 | name: attrName,
|
---|
| 589 | value: attrValue
|
---|
| 590 | };
|
---|
| 591 | }
|
---|
| 592 |
|
---|
| 593 | function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
---|
| 594 | var attrName = normalized.name,
|
---|
| 595 | attrValue = normalized.value,
|
---|
| 596 | attr = normalized.attr,
|
---|
| 597 | attrQuote = attr.quote,
|
---|
| 598 | attrFragment,
|
---|
| 599 | emittedAttrValue;
|
---|
| 600 |
|
---|
| 601 | if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
|
---|
| 602 | ~attrValue.indexOf(uidAttr) || !canRemoveAttributeQuotes(attrValue))) {
|
---|
| 603 | if (!options.preventAttributesEscaping) {
|
---|
| 604 | if (typeof options.quoteCharacter === 'undefined') {
|
---|
| 605 | var apos = (attrValue.match(/'/g) || []).length;
|
---|
| 606 | var quot = (attrValue.match(/"/g) || []).length;
|
---|
| 607 | attrQuote = apos < quot ? '\'' : '"';
|
---|
| 608 | }
|
---|
| 609 | else {
|
---|
| 610 | attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
---|
| 611 | }
|
---|
| 612 | if (attrQuote === '"') {
|
---|
| 613 | attrValue = attrValue.replace(/"/g, '"');
|
---|
| 614 | }
|
---|
| 615 | else {
|
---|
| 616 | attrValue = attrValue.replace(/'/g, ''');
|
---|
| 617 | }
|
---|
| 618 | }
|
---|
| 619 | emittedAttrValue = attrQuote + attrValue + attrQuote;
|
---|
| 620 | if (!isLast && !options.removeTagWhitespace) {
|
---|
| 621 | emittedAttrValue += ' ';
|
---|
| 622 | }
|
---|
| 623 | }
|
---|
| 624 | // make sure trailing slash is not interpreted as HTML self-closing tag
|
---|
| 625 | else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
|
---|
| 626 | emittedAttrValue = attrValue;
|
---|
| 627 | }
|
---|
| 628 | else {
|
---|
| 629 | emittedAttrValue = attrValue + ' ';
|
---|
| 630 | }
|
---|
| 631 |
|
---|
| 632 | if (typeof attrValue === 'undefined' || options.collapseBooleanAttributes &&
|
---|
| 633 | isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase())) {
|
---|
| 634 | attrFragment = attrName;
|
---|
| 635 | if (!isLast) {
|
---|
| 636 | attrFragment += ' ';
|
---|
| 637 | }
|
---|
| 638 | }
|
---|
| 639 | else {
|
---|
| 640 | attrFragment = attrName + attr.customAssign + emittedAttrValue;
|
---|
| 641 | }
|
---|
| 642 |
|
---|
| 643 | return attr.customOpen + attrFragment + attr.customClose;
|
---|
| 644 | }
|
---|
| 645 |
|
---|
| 646 | function identity(value) {
|
---|
| 647 | return value;
|
---|
| 648 | }
|
---|
| 649 |
|
---|
| 650 | function processOptions(values) {
|
---|
| 651 | var options = {
|
---|
| 652 | name: function(name) {
|
---|
| 653 | return name.toLowerCase();
|
---|
| 654 | },
|
---|
| 655 | canCollapseWhitespace: canCollapseWhitespace,
|
---|
| 656 | canTrimWhitespace: canTrimWhitespace,
|
---|
| 657 | html5: true,
|
---|
| 658 | ignoreCustomComments: [
|
---|
| 659 | /^!/,
|
---|
| 660 | /^\s*#/
|
---|
| 661 | ],
|
---|
| 662 | ignoreCustomFragments: [
|
---|
| 663 | /<%[\s\S]*?%>/,
|
---|
| 664 | /<\?[\s\S]*?\?>/
|
---|
| 665 | ],
|
---|
| 666 | includeAutoGeneratedTags: true,
|
---|
| 667 | log: identity,
|
---|
| 668 | minifyCSS: identity,
|
---|
| 669 | minifyJS: identity,
|
---|
| 670 | minifyURLs: identity
|
---|
| 671 | };
|
---|
| 672 | Object.keys(values).forEach(function(key) {
|
---|
| 673 | var value = values[key];
|
---|
| 674 | if (key === 'caseSensitive') {
|
---|
| 675 | if (value) {
|
---|
| 676 | options.name = identity;
|
---|
| 677 | }
|
---|
| 678 | }
|
---|
| 679 | else if (key === 'log') {
|
---|
| 680 | if (typeof value === 'function') {
|
---|
| 681 | options.log = value;
|
---|
| 682 | }
|
---|
| 683 | }
|
---|
| 684 | else if (key === 'minifyCSS' && typeof value !== 'function') {
|
---|
| 685 | if (!value) {
|
---|
| 686 | return;
|
---|
| 687 | }
|
---|
| 688 | if (typeof value !== 'object') {
|
---|
| 689 | value = {};
|
---|
| 690 | }
|
---|
| 691 | options.minifyCSS = function(text, type) {
|
---|
| 692 | text = text.replace(/(url\s*\(\s*)("|'|)(.*?)\2(\s*\))/ig, function(match, prefix, quote, url, suffix) {
|
---|
| 693 | return prefix + quote + options.minifyURLs(url) + quote + suffix;
|
---|
| 694 | });
|
---|
| 695 | var cleanCssOutput = new CleanCSS(value).minify(wrapCSS(text, type));
|
---|
| 696 | if (cleanCssOutput.errors.length > 0) {
|
---|
| 697 | cleanCssOutput.errors.forEach(options.log);
|
---|
| 698 | return text;
|
---|
| 699 | }
|
---|
| 700 | return unwrapCSS(cleanCssOutput.styles, type);
|
---|
| 701 | };
|
---|
| 702 | }
|
---|
| 703 | else if (key === 'minifyJS' && typeof value !== 'function') {
|
---|
| 704 | if (!value) {
|
---|
| 705 | return;
|
---|
| 706 | }
|
---|
| 707 | if (typeof value !== 'object') {
|
---|
| 708 | value = {};
|
---|
| 709 | }
|
---|
| 710 | (value.parse || (value.parse = {})).bare_returns = false;
|
---|
| 711 | options.minifyJS = async function(text, inline) {
|
---|
| 712 | var start = text.match(/^\s*<!--.*/);
|
---|
| 713 | var code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
---|
| 714 | value.parse.bare_returns = inline;
|
---|
| 715 | try {
|
---|
| 716 | const result = await Terser.minify(code, value);
|
---|
| 717 | return result.code.replace(/;$/, '');
|
---|
| 718 | }
|
---|
| 719 | catch (error) {
|
---|
| 720 | options.log(error);
|
---|
| 721 | return text;
|
---|
| 722 | }
|
---|
| 723 | };
|
---|
| 724 | }
|
---|
| 725 | else if (key === 'minifyURLs' && typeof value !== 'function') {
|
---|
| 726 | if (!value) {
|
---|
| 727 | return;
|
---|
| 728 | }
|
---|
| 729 | if (typeof value === 'string') {
|
---|
| 730 | value = { site: value };
|
---|
| 731 | }
|
---|
| 732 | else if (typeof value !== 'object') {
|
---|
| 733 | value = {};
|
---|
| 734 | }
|
---|
| 735 | options.minifyURLs = function(text) {
|
---|
| 736 | try {
|
---|
| 737 | return RelateUrl.relate(text, value);
|
---|
| 738 | }
|
---|
| 739 | catch (err) {
|
---|
| 740 | options.log(err);
|
---|
| 741 | return text;
|
---|
| 742 | }
|
---|
| 743 | };
|
---|
| 744 | }
|
---|
| 745 | else {
|
---|
| 746 | options[key] = value;
|
---|
| 747 | }
|
---|
| 748 | });
|
---|
| 749 | return options;
|
---|
| 750 | }
|
---|
| 751 |
|
---|
| 752 | function uniqueId(value) {
|
---|
| 753 | var id;
|
---|
| 754 | do {
|
---|
| 755 | id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
|
---|
| 756 | } while (~value.indexOf(id));
|
---|
| 757 | return id;
|
---|
| 758 | }
|
---|
| 759 |
|
---|
| 760 | var specialContentTags = createMapFromString('script,style');
|
---|
| 761 |
|
---|
| 762 | async function createSortFns(value, options, uidIgnore, uidAttr) {
|
---|
| 763 | var attrChains = options.sortAttributes && Object.create(null);
|
---|
| 764 | var classChain = options.sortClassName && new TokenChain();
|
---|
| 765 |
|
---|
| 766 | function attrNames(attrs) {
|
---|
| 767 | return attrs.map(function(attr) {
|
---|
| 768 | return options.name(attr.name);
|
---|
| 769 | });
|
---|
| 770 | }
|
---|
| 771 |
|
---|
| 772 | function shouldSkipUID(token, uid) {
|
---|
| 773 | return !uid || token.indexOf(uid) === -1;
|
---|
| 774 | }
|
---|
| 775 |
|
---|
| 776 | function shouldSkipUIDs(token) {
|
---|
| 777 | return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr);
|
---|
| 778 | }
|
---|
| 779 |
|
---|
| 780 | async function scan(input) {
|
---|
| 781 | var currentTag, currentType;
|
---|
| 782 | const parser = new HTMLParser(input, {
|
---|
| 783 | start: function(tag, attrs) {
|
---|
| 784 | if (attrChains) {
|
---|
| 785 | if (!attrChains[tag]) {
|
---|
| 786 | attrChains[tag] = new TokenChain();
|
---|
| 787 | }
|
---|
| 788 | attrChains[tag].add(attrNames(attrs).filter(shouldSkipUIDs));
|
---|
| 789 | }
|
---|
| 790 | for (var i = 0, len = attrs.length; i < len; i++) {
|
---|
| 791 | var attr = attrs[i];
|
---|
| 792 | if (classChain && attr.value && options.name(attr.name) === 'class') {
|
---|
| 793 | classChain.add(trimWhitespace(attr.value).split(/[ \t\n\f\r]+/).filter(shouldSkipUIDs));
|
---|
| 794 | }
|
---|
| 795 | else if (options.processScripts && attr.name.toLowerCase() === 'type') {
|
---|
| 796 | currentTag = tag;
|
---|
| 797 | currentType = attr.value;
|
---|
| 798 | }
|
---|
| 799 | }
|
---|
| 800 | },
|
---|
| 801 | end: function() {
|
---|
| 802 | currentTag = '';
|
---|
| 803 | },
|
---|
| 804 | chars: async function(text) {
|
---|
| 805 | if (options.processScripts && specialContentTags(currentTag) &&
|
---|
| 806 | options.processScripts.indexOf(currentType) > -1) {
|
---|
| 807 | await scan(text);
|
---|
| 808 | }
|
---|
| 809 | }
|
---|
| 810 | });
|
---|
| 811 |
|
---|
| 812 | await parser.parse();
|
---|
| 813 | }
|
---|
| 814 |
|
---|
| 815 | var log = options.log;
|
---|
| 816 | options.log = identity;
|
---|
| 817 | options.sortAttributes = false;
|
---|
| 818 | options.sortClassName = false;
|
---|
| 819 | await scan(await minify(value, options));
|
---|
| 820 | options.log = log;
|
---|
| 821 | if (attrChains) {
|
---|
| 822 | var attrSorters = Object.create(null);
|
---|
| 823 | for (var tag in attrChains) {
|
---|
| 824 | attrSorters[tag] = attrChains[tag].createSorter();
|
---|
| 825 | }
|
---|
| 826 | options.sortAttributes = function(tag, attrs) {
|
---|
| 827 | var sorter = attrSorters[tag];
|
---|
| 828 | if (sorter) {
|
---|
| 829 | var attrMap = Object.create(null);
|
---|
| 830 | var names = attrNames(attrs);
|
---|
| 831 | names.forEach(function(name, index) {
|
---|
| 832 | (attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
---|
| 833 | });
|
---|
| 834 | sorter.sort(names).forEach(function(name, index) {
|
---|
| 835 | attrs[index] = attrMap[name].shift();
|
---|
| 836 | });
|
---|
| 837 | }
|
---|
| 838 | };
|
---|
| 839 | }
|
---|
| 840 | if (classChain) {
|
---|
| 841 | var sorter = classChain.createSorter();
|
---|
| 842 | options.sortClassName = function(value) {
|
---|
| 843 | return sorter.sort(value.split(/[ \n\f\r]+/)).join(' ');
|
---|
| 844 | };
|
---|
| 845 | }
|
---|
| 846 | }
|
---|
| 847 |
|
---|
| 848 | async function minify(value, options, partialMarkup) {
|
---|
| 849 | if (options.collapseWhitespace) {
|
---|
| 850 | value = collapseWhitespace(value, options, true, true);
|
---|
| 851 | }
|
---|
| 852 |
|
---|
| 853 | var buffer = [],
|
---|
| 854 | charsPrevTag,
|
---|
| 855 | currentChars = '',
|
---|
| 856 | hasChars,
|
---|
| 857 | currentTag = '',
|
---|
| 858 | currentAttrs = [],
|
---|
| 859 | stackNoTrimWhitespace = [],
|
---|
| 860 | stackNoCollapseWhitespace = [],
|
---|
| 861 | optionalStartTag = '',
|
---|
| 862 | optionalEndTag = '',
|
---|
| 863 | ignoredMarkupChunks = [],
|
---|
| 864 | ignoredCustomMarkupChunks = [],
|
---|
| 865 | uidIgnore,
|
---|
| 866 | uidAttr,
|
---|
| 867 | uidPattern;
|
---|
| 868 |
|
---|
| 869 | // temporarily replace ignored chunks with comments,
|
---|
| 870 | // so that we don't have to worry what's there.
|
---|
| 871 | // for all we care there might be
|
---|
| 872 | // completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
|
---|
| 873 | value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function(match, group1) {
|
---|
| 874 | if (!uidIgnore) {
|
---|
| 875 | uidIgnore = uniqueId(value);
|
---|
| 876 | var pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
---|
| 877 | if (options.ignoreCustomComments) {
|
---|
| 878 | options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
---|
| 879 | }
|
---|
| 880 | else {
|
---|
| 881 | options.ignoreCustomComments = [];
|
---|
| 882 | }
|
---|
| 883 | options.ignoreCustomComments.push(pattern);
|
---|
| 884 | }
|
---|
| 885 | var token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
|
---|
| 886 | ignoredMarkupChunks.push(group1);
|
---|
| 887 | return token;
|
---|
| 888 | });
|
---|
| 889 |
|
---|
| 890 | var customFragments = options.ignoreCustomFragments.map(function(re) {
|
---|
| 891 | return re.source;
|
---|
| 892 | });
|
---|
| 893 | if (customFragments.length) {
|
---|
| 894 | var reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g');
|
---|
| 895 | // temporarily replace custom ignored fragments with unique attributes
|
---|
| 896 | value = value.replace(reCustomIgnore, function(match) {
|
---|
| 897 | if (!uidAttr) {
|
---|
| 898 | uidAttr = uniqueId(value);
|
---|
| 899 | uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
---|
| 900 | if (options.minifyCSS) {
|
---|
| 901 | options.minifyCSS = (function(fn) {
|
---|
| 902 | return function(text, type) {
|
---|
| 903 | text = text.replace(uidPattern, function(match, prefix, index) {
|
---|
| 904 | var chunks = ignoredCustomMarkupChunks[+index];
|
---|
| 905 | return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
---|
| 906 | });
|
---|
| 907 | var ids = [];
|
---|
| 908 | new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function(warning) {
|
---|
| 909 | var match = uidPattern.exec(warning);
|
---|
| 910 | if (match) {
|
---|
| 911 | var id = uidAttr + match[2] + uidAttr;
|
---|
| 912 | text = text.replace(id, ignoreCSS(id));
|
---|
| 913 | ids.push(id);
|
---|
| 914 | }
|
---|
| 915 | });
|
---|
| 916 | text = fn(text, type);
|
---|
| 917 | ids.forEach(function(id) {
|
---|
| 918 | text = text.replace(ignoreCSS(id), id);
|
---|
| 919 | });
|
---|
| 920 | return text;
|
---|
| 921 | };
|
---|
| 922 | })(options.minifyCSS);
|
---|
| 923 | }
|
---|
| 924 | if (options.minifyJS) {
|
---|
| 925 | options.minifyJS = (function(fn) {
|
---|
| 926 | return function(text, type) {
|
---|
| 927 | return fn(text.replace(uidPattern, function(match, prefix, index) {
|
---|
| 928 | var chunks = ignoredCustomMarkupChunks[+index];
|
---|
| 929 | return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
---|
| 930 | }), type);
|
---|
| 931 | };
|
---|
| 932 | })(options.minifyJS);
|
---|
| 933 | }
|
---|
| 934 | }
|
---|
| 935 | var token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
|
---|
| 936 | ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
|
---|
| 937 | return '\t' + token + '\t';
|
---|
| 938 | });
|
---|
| 939 | }
|
---|
| 940 |
|
---|
| 941 | if (options.sortAttributes && typeof options.sortAttributes !== 'function' ||
|
---|
| 942 | options.sortClassName && typeof options.sortClassName !== 'function') {
|
---|
| 943 | await createSortFns(value, options, uidIgnore, uidAttr);
|
---|
| 944 | }
|
---|
| 945 |
|
---|
| 946 | function _canCollapseWhitespace(tag, attrs) {
|
---|
| 947 | return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
---|
| 948 | }
|
---|
| 949 |
|
---|
| 950 | function _canTrimWhitespace(tag, attrs) {
|
---|
| 951 | return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
---|
| 952 | }
|
---|
| 953 |
|
---|
| 954 | function removeStartTag() {
|
---|
| 955 | var index = buffer.length - 1;
|
---|
| 956 | while (index > 0 && !/^<[^/!]/.test(buffer[index])) {
|
---|
| 957 | index--;
|
---|
| 958 | }
|
---|
| 959 | buffer.length = Math.max(0, index);
|
---|
| 960 | }
|
---|
| 961 |
|
---|
| 962 | function removeEndTag() {
|
---|
| 963 | var index = buffer.length - 1;
|
---|
| 964 | while (index > 0 && !/^<\//.test(buffer[index])) {
|
---|
| 965 | index--;
|
---|
| 966 | }
|
---|
| 967 | buffer.length = Math.max(0, index);
|
---|
| 968 | }
|
---|
| 969 |
|
---|
| 970 | // look for trailing whitespaces, bypass any inline tags
|
---|
| 971 | function trimTrailingWhitespace(index, nextTag) {
|
---|
| 972 | for (var endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
|
---|
| 973 | var str = buffer[index];
|
---|
| 974 | var match = str.match(/^<\/([\w:-]+)>$/);
|
---|
| 975 | if (match) {
|
---|
| 976 | endTag = match[1];
|
---|
| 977 | }
|
---|
| 978 | else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options))) {
|
---|
| 979 | break;
|
---|
| 980 | }
|
---|
| 981 | }
|
---|
| 982 | }
|
---|
| 983 |
|
---|
| 984 | // look for trailing whitespaces from previously processed text
|
---|
| 985 | // which may not be trimmed due to a following comment or an empty
|
---|
| 986 | // element which has now been removed
|
---|
| 987 | function squashTrailingWhitespace(nextTag) {
|
---|
| 988 | var charsIndex = buffer.length - 1;
|
---|
| 989 | if (buffer.length > 1) {
|
---|
| 990 | var item = buffer[buffer.length - 1];
|
---|
| 991 | if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
|
---|
| 992 | charsIndex--;
|
---|
| 993 | }
|
---|
| 994 | }
|
---|
| 995 | trimTrailingWhitespace(charsIndex, nextTag);
|
---|
| 996 | }
|
---|
| 997 |
|
---|
| 998 | const parser = new HTMLParser(value, {
|
---|
| 999 | partialMarkup: partialMarkup,
|
---|
| 1000 | continueOnParseError: options.continueOnParseError,
|
---|
| 1001 | customAttrAssign: options.customAttrAssign,
|
---|
| 1002 | customAttrSurround: options.customAttrSurround,
|
---|
| 1003 | html5: options.html5,
|
---|
| 1004 |
|
---|
| 1005 | start: async function(tag, attrs, unary, unarySlash, autoGenerated) {
|
---|
| 1006 | if (tag.toLowerCase() === 'svg') {
|
---|
| 1007 | options = Object.create(options);
|
---|
| 1008 | options.caseSensitive = true;
|
---|
| 1009 | options.keepClosingSlash = true;
|
---|
| 1010 | options.name = identity;
|
---|
| 1011 | }
|
---|
| 1012 | tag = options.name(tag);
|
---|
| 1013 | currentTag = tag;
|
---|
| 1014 | charsPrevTag = tag;
|
---|
| 1015 | if (!inlineTextTags(tag)) {
|
---|
| 1016 | currentChars = '';
|
---|
| 1017 | }
|
---|
| 1018 | hasChars = false;
|
---|
| 1019 | currentAttrs = attrs;
|
---|
| 1020 |
|
---|
| 1021 | var optional = options.removeOptionalTags;
|
---|
| 1022 | if (optional) {
|
---|
| 1023 | var htmlTag = htmlTags(tag);
|
---|
| 1024 | // <html> may be omitted if first thing inside is not comment
|
---|
| 1025 | // <head> may be omitted if first thing inside is an element
|
---|
| 1026 | // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
|
---|
| 1027 | // <colgroup> may be omitted if first thing inside is <col>
|
---|
| 1028 | // <tbody> may be omitted if first thing inside is <tr>
|
---|
| 1029 | if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
---|
| 1030 | removeStartTag();
|
---|
| 1031 | }
|
---|
| 1032 | optionalStartTag = '';
|
---|
| 1033 | // end-tag-followed-by-start-tag omission rules
|
---|
| 1034 | if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
|
---|
| 1035 | removeEndTag();
|
---|
| 1036 | // <colgroup> cannot be omitted if preceding </colgroup> is omitted
|
---|
| 1037 | // <tbody> cannot be omitted if preceding </tbody>, </thead> or </tfoot> is omitted
|
---|
| 1038 | optional = !isStartTagMandatory(optionalEndTag, tag);
|
---|
| 1039 | }
|
---|
| 1040 | optionalEndTag = '';
|
---|
| 1041 | }
|
---|
| 1042 |
|
---|
| 1043 | // set whitespace flags for nested tags (eg. <code> within a <pre>)
|
---|
| 1044 | if (options.collapseWhitespace) {
|
---|
| 1045 | if (!stackNoTrimWhitespace.length) {
|
---|
| 1046 | squashTrailingWhitespace(tag);
|
---|
| 1047 | }
|
---|
| 1048 | if (!unary) {
|
---|
| 1049 | if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
|
---|
| 1050 | stackNoTrimWhitespace.push(tag);
|
---|
| 1051 | }
|
---|
| 1052 | if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
|
---|
| 1053 | stackNoCollapseWhitespace.push(tag);
|
---|
| 1054 | }
|
---|
| 1055 | }
|
---|
| 1056 | }
|
---|
| 1057 |
|
---|
| 1058 | var openTag = '<' + tag;
|
---|
| 1059 | var hasUnarySlash = unarySlash && options.keepClosingSlash;
|
---|
| 1060 |
|
---|
| 1061 | buffer.push(openTag);
|
---|
| 1062 |
|
---|
| 1063 | if (options.sortAttributes) {
|
---|
| 1064 | options.sortAttributes(tag, attrs);
|
---|
| 1065 | }
|
---|
| 1066 |
|
---|
| 1067 | var parts = [];
|
---|
| 1068 | for (var i = attrs.length, isLast = true; --i >= 0;) {
|
---|
| 1069 | var normalized = await normalizeAttr(attrs[i], attrs, tag, options);
|
---|
| 1070 | if (normalized) {
|
---|
| 1071 | parts.unshift(buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr));
|
---|
| 1072 | isLast = false;
|
---|
| 1073 | }
|
---|
| 1074 | }
|
---|
| 1075 | if (parts.length > 0) {
|
---|
| 1076 | buffer.push(' ');
|
---|
| 1077 | buffer.push.apply(buffer, parts);
|
---|
| 1078 | }
|
---|
| 1079 | // start tag must never be omitted if it has any attributes
|
---|
| 1080 | else if (optional && optionalStartTags(tag)) {
|
---|
| 1081 | optionalStartTag = tag;
|
---|
| 1082 | }
|
---|
| 1083 |
|
---|
| 1084 | buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
|
---|
| 1085 |
|
---|
| 1086 | if (autoGenerated && !options.includeAutoGeneratedTags) {
|
---|
| 1087 | removeStartTag();
|
---|
| 1088 | optionalStartTag = '';
|
---|
| 1089 | }
|
---|
| 1090 | },
|
---|
| 1091 | end: function(tag, attrs, autoGenerated) {
|
---|
| 1092 | if (tag.toLowerCase() === 'svg') {
|
---|
| 1093 | options = Object.getPrototypeOf(options);
|
---|
| 1094 | }
|
---|
| 1095 | tag = options.name(tag);
|
---|
| 1096 |
|
---|
| 1097 | // check if current tag is in a whitespace stack
|
---|
| 1098 | if (options.collapseWhitespace) {
|
---|
| 1099 | if (stackNoTrimWhitespace.length) {
|
---|
| 1100 | if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
---|
| 1101 | stackNoTrimWhitespace.pop();
|
---|
| 1102 | }
|
---|
| 1103 | }
|
---|
| 1104 | else {
|
---|
| 1105 | squashTrailingWhitespace('/' + tag);
|
---|
| 1106 | }
|
---|
| 1107 | if (stackNoCollapseWhitespace.length &&
|
---|
| 1108 | tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
---|
| 1109 | stackNoCollapseWhitespace.pop();
|
---|
| 1110 | }
|
---|
| 1111 | }
|
---|
| 1112 |
|
---|
| 1113 | var isElementEmpty = false;
|
---|
| 1114 | if (tag === currentTag) {
|
---|
| 1115 | currentTag = '';
|
---|
| 1116 | isElementEmpty = !hasChars;
|
---|
| 1117 | }
|
---|
| 1118 |
|
---|
| 1119 | if (options.removeOptionalTags) {
|
---|
| 1120 | // <html>, <head> or <body> may be omitted if the element is empty
|
---|
| 1121 | if (isElementEmpty && topLevelTags(optionalStartTag)) {
|
---|
| 1122 | removeStartTag();
|
---|
| 1123 | }
|
---|
| 1124 | optionalStartTag = '';
|
---|
| 1125 | // </html> or </body> may be omitted if not followed by comment
|
---|
| 1126 | // </head> may be omitted if not followed by space or comment
|
---|
| 1127 | // </p> may be omitted if no more content in non-</a> parent
|
---|
| 1128 | // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
|
---|
| 1129 | if (htmlTags(tag) && optionalEndTag && !trailingTags(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags(tag))) {
|
---|
| 1130 | removeEndTag();
|
---|
| 1131 | }
|
---|
| 1132 | optionalEndTag = optionalEndTags(tag) ? tag : '';
|
---|
| 1133 | }
|
---|
| 1134 |
|
---|
| 1135 | if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
|
---|
| 1136 | // remove last "element" from buffer
|
---|
| 1137 | removeStartTag();
|
---|
| 1138 | optionalStartTag = '';
|
---|
| 1139 | optionalEndTag = '';
|
---|
| 1140 | }
|
---|
| 1141 | else {
|
---|
| 1142 | if (autoGenerated && !options.includeAutoGeneratedTags) {
|
---|
| 1143 | optionalEndTag = '';
|
---|
| 1144 | }
|
---|
| 1145 | else {
|
---|
| 1146 | buffer.push('</' + tag + '>');
|
---|
| 1147 | }
|
---|
| 1148 | charsPrevTag = '/' + tag;
|
---|
| 1149 | if (!inlineTags(tag)) {
|
---|
| 1150 | currentChars = '';
|
---|
| 1151 | }
|
---|
| 1152 | else if (isElementEmpty) {
|
---|
| 1153 | currentChars += '|';
|
---|
| 1154 | }
|
---|
| 1155 | }
|
---|
| 1156 | },
|
---|
| 1157 | chars: async function(text, prevTag, nextTag) {
|
---|
| 1158 | prevTag = prevTag === '' ? 'comment' : prevTag;
|
---|
| 1159 | nextTag = nextTag === '' ? 'comment' : nextTag;
|
---|
| 1160 | if (options.decodeEntities && text && !specialContentTags(currentTag)) {
|
---|
| 1161 | text = decode(text);
|
---|
| 1162 | }
|
---|
| 1163 | if (options.collapseWhitespace) {
|
---|
| 1164 | if (!stackNoTrimWhitespace.length) {
|
---|
| 1165 | if (prevTag === 'comment') {
|
---|
| 1166 | var prevComment = buffer[buffer.length - 1];
|
---|
| 1167 | if (prevComment.indexOf(uidIgnore) === -1) {
|
---|
| 1168 | if (!prevComment) {
|
---|
| 1169 | prevTag = charsPrevTag;
|
---|
| 1170 | }
|
---|
| 1171 | if (buffer.length > 1 && (!prevComment || !options.conservativeCollapse && / $/.test(currentChars))) {
|
---|
| 1172 | var charsIndex = buffer.length - 2;
|
---|
| 1173 | buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function(trailingSpaces) {
|
---|
| 1174 | text = trailingSpaces + text;
|
---|
| 1175 | return '';
|
---|
| 1176 | });
|
---|
| 1177 | }
|
---|
| 1178 | }
|
---|
| 1179 | }
|
---|
| 1180 | if (prevTag) {
|
---|
| 1181 | if (prevTag === '/nobr' || prevTag === 'wbr') {
|
---|
| 1182 | if (/^\s/.test(text)) {
|
---|
| 1183 | var tagIndex = buffer.length - 1;
|
---|
| 1184 | while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
|
---|
| 1185 | tagIndex--;
|
---|
| 1186 | }
|
---|
| 1187 | trimTrailingWhitespace(tagIndex - 1, 'br');
|
---|
| 1188 | }
|
---|
| 1189 | }
|
---|
| 1190 | else if (inlineTextTags(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
---|
| 1191 | text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
---|
| 1192 | }
|
---|
| 1193 | }
|
---|
| 1194 | if (prevTag || nextTag) {
|
---|
| 1195 | text = collapseWhitespaceSmart(text, prevTag, nextTag, options);
|
---|
| 1196 | }
|
---|
| 1197 | else {
|
---|
| 1198 | text = collapseWhitespace(text, options, true, true);
|
---|
| 1199 | }
|
---|
| 1200 | if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
|
---|
| 1201 | trimTrailingWhitespace(buffer.length - 1, nextTag);
|
---|
| 1202 | }
|
---|
| 1203 | }
|
---|
| 1204 | if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
|
---|
| 1205 | text = collapseWhitespace(text, options, false, false, true);
|
---|
| 1206 | }
|
---|
| 1207 | }
|
---|
| 1208 | if (options.processScripts && specialContentTags(currentTag)) {
|
---|
| 1209 | text = await processScript(text, options, currentAttrs);
|
---|
| 1210 | }
|
---|
| 1211 | if (isExecutableScript(currentTag, currentAttrs)) {
|
---|
| 1212 | text = await options.minifyJS(text);
|
---|
| 1213 | }
|
---|
| 1214 | if (isStyleSheet(currentTag, currentAttrs)) {
|
---|
| 1215 | text = options.minifyCSS(text);
|
---|
| 1216 | }
|
---|
| 1217 | if (options.removeOptionalTags && text) {
|
---|
| 1218 | // <html> may be omitted if first thing inside is not comment
|
---|
| 1219 | // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
|
---|
| 1220 | if (optionalStartTag === 'html' || optionalStartTag === 'body' && !/^\s/.test(text)) {
|
---|
| 1221 | removeStartTag();
|
---|
| 1222 | }
|
---|
| 1223 | optionalStartTag = '';
|
---|
| 1224 | // </html> or </body> may be omitted if not followed by comment
|
---|
| 1225 | // </head>, </colgroup> or </caption> may be omitted if not followed by space or comment
|
---|
| 1226 | if (compactTags(optionalEndTag) || looseTags(optionalEndTag) && !/^\s/.test(text)) {
|
---|
| 1227 | removeEndTag();
|
---|
| 1228 | }
|
---|
| 1229 | optionalEndTag = '';
|
---|
| 1230 | }
|
---|
| 1231 | charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
---|
| 1232 | if (options.decodeEntities && text && !specialContentTags(currentTag)) {
|
---|
| 1233 | // Escape any `&` symbols that start either:
|
---|
| 1234 | // 1) a legacy named character reference (i.e. one that doesn't end with `;`)
|
---|
| 1235 | // 2) or any other character reference (i.e. one that does end with `;`)
|
---|
| 1236 | // Note that `&` can be escaped as `&`, without the semi-colon.
|
---|
| 1237 | // https://mathiasbynens.be/notes/ambiguous-ampersands
|
---|
| 1238 | text = text.replace(/&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g, '&$1').replace(/</g, '<');
|
---|
| 1239 | }
|
---|
| 1240 | if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
---|
| 1241 | text = text.replace(uidPattern, function(match, prefix, index) {
|
---|
| 1242 | return ignoredCustomMarkupChunks[+index][0];
|
---|
| 1243 | });
|
---|
| 1244 | }
|
---|
| 1245 | currentChars += text;
|
---|
| 1246 | if (text) {
|
---|
| 1247 | hasChars = true;
|
---|
| 1248 | }
|
---|
| 1249 | buffer.push(text);
|
---|
| 1250 | },
|
---|
| 1251 | comment: async function(text, nonStandard) {
|
---|
| 1252 | var prefix = nonStandard ? '<!' : '<!--';
|
---|
| 1253 | var suffix = nonStandard ? '>' : '-->';
|
---|
| 1254 | if (isConditionalComment(text)) {
|
---|
| 1255 | text = prefix + await cleanConditionalComment(text, options) + suffix;
|
---|
| 1256 | }
|
---|
| 1257 | else if (options.removeComments) {
|
---|
| 1258 | if (isIgnoredComment(text, options)) {
|
---|
| 1259 | text = '<!--' + text + '-->';
|
---|
| 1260 | }
|
---|
| 1261 | else {
|
---|
| 1262 | text = '';
|
---|
| 1263 | }
|
---|
| 1264 | }
|
---|
| 1265 | else {
|
---|
| 1266 | text = prefix + text + suffix;
|
---|
| 1267 | }
|
---|
| 1268 | if (options.removeOptionalTags && text) {
|
---|
| 1269 | // preceding comments suppress tag omissions
|
---|
| 1270 | optionalStartTag = '';
|
---|
| 1271 | optionalEndTag = '';
|
---|
| 1272 | }
|
---|
| 1273 | buffer.push(text);
|
---|
| 1274 | },
|
---|
| 1275 | doctype: function(doctype) {
|
---|
| 1276 | buffer.push(options.useShortDoctype ? '<!doctype' +
|
---|
| 1277 | (options.removeTagWhitespace ? '' : ' ') + 'html>' :
|
---|
| 1278 | collapseWhitespaceAll(doctype));
|
---|
| 1279 | }
|
---|
| 1280 | });
|
---|
| 1281 |
|
---|
| 1282 | await parser.parse();
|
---|
| 1283 |
|
---|
| 1284 | if (options.removeOptionalTags) {
|
---|
| 1285 | // <html> may be omitted if first thing inside is not comment
|
---|
| 1286 | // <head> or <body> may be omitted if empty
|
---|
| 1287 | if (topLevelTags(optionalStartTag)) {
|
---|
| 1288 | removeStartTag();
|
---|
| 1289 | }
|
---|
| 1290 | // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
|
---|
| 1291 | if (optionalEndTag && !trailingTags(optionalEndTag)) {
|
---|
| 1292 | removeEndTag();
|
---|
| 1293 | }
|
---|
| 1294 | }
|
---|
| 1295 | if (options.collapseWhitespace) {
|
---|
| 1296 | squashTrailingWhitespace('br');
|
---|
| 1297 | }
|
---|
| 1298 |
|
---|
| 1299 | return joinResultSegments(buffer, options, uidPattern ? function(str) {
|
---|
| 1300 | return str.replace(uidPattern, function(match, prefix, index, suffix) {
|
---|
| 1301 | var chunk = ignoredCustomMarkupChunks[+index][0];
|
---|
| 1302 | if (options.collapseWhitespace) {
|
---|
| 1303 | if (prefix !== '\t') {
|
---|
| 1304 | chunk = prefix + chunk;
|
---|
| 1305 | }
|
---|
| 1306 | if (suffix !== '\t') {
|
---|
| 1307 | chunk += suffix;
|
---|
| 1308 | }
|
---|
| 1309 | return collapseWhitespace(chunk, {
|
---|
| 1310 | preserveLineBreaks: options.preserveLineBreaks,
|
---|
| 1311 | conservativeCollapse: !options.trimCustomFragments
|
---|
| 1312 | }, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk));
|
---|
| 1313 | }
|
---|
| 1314 | return chunk;
|
---|
| 1315 | });
|
---|
| 1316 | } : identity, uidIgnore ? function(str) {
|
---|
| 1317 | return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function(match, index) {
|
---|
| 1318 | return ignoredMarkupChunks[+index];
|
---|
| 1319 | });
|
---|
| 1320 | } : identity);
|
---|
| 1321 | }
|
---|
| 1322 |
|
---|
| 1323 | function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
---|
| 1324 | var str;
|
---|
| 1325 | var maxLineLength = options.maxLineLength;
|
---|
| 1326 | var noNewlinesBeforeTagClose = options.noNewlinesBeforeTagClose;
|
---|
| 1327 |
|
---|
| 1328 | if (maxLineLength) {
|
---|
| 1329 | var line = '', lines = [];
|
---|
| 1330 | while (results.length) {
|
---|
| 1331 | var len = line.length;
|
---|
| 1332 | var end = results[0].indexOf('\n');
|
---|
| 1333 | var isClosingTag = Boolean(results[0].match(endTag));
|
---|
| 1334 | var shouldKeepSameLine = noNewlinesBeforeTagClose && isClosingTag;
|
---|
| 1335 | if (end < 0) {
|
---|
| 1336 | line += restoreIgnore(restoreCustom(results.shift()));
|
---|
| 1337 | }
|
---|
| 1338 | else {
|
---|
| 1339 | line += restoreIgnore(restoreCustom(results[0].slice(0, end)));
|
---|
| 1340 | results[0] = results[0].slice(end + 1);
|
---|
| 1341 | }
|
---|
| 1342 | if (len > 0 && line.length > maxLineLength && !shouldKeepSameLine) {
|
---|
| 1343 | lines.push(line.slice(0, len));
|
---|
| 1344 | line = line.slice(len);
|
---|
| 1345 | }
|
---|
| 1346 | else if (end >= 0) {
|
---|
| 1347 | lines.push(line);
|
---|
| 1348 | line = '';
|
---|
| 1349 | }
|
---|
| 1350 | }
|
---|
| 1351 | if (line) {
|
---|
| 1352 | lines.push(line);
|
---|
| 1353 | }
|
---|
| 1354 | str = lines.join('\n');
|
---|
| 1355 | }
|
---|
| 1356 | else {
|
---|
| 1357 | str = restoreIgnore(restoreCustom(results.join('')));
|
---|
| 1358 | }
|
---|
| 1359 | return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
|
---|
| 1360 | }
|
---|
| 1361 |
|
---|
| 1362 | exports.minify = async function(value, options) {
|
---|
| 1363 | var start = Date.now();
|
---|
| 1364 | options = processOptions(options || {});
|
---|
| 1365 | var result = await minify(value, options);
|
---|
| 1366 | options.log('minified in: ' + (Date.now() - start) + 'ms');
|
---|
| 1367 | return result;
|
---|
| 1368 | };
|
---|