html-minifier-next 4.7.1 → 4.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -19
- package/cli.js +4 -3
- package/dist/htmlminifier.cjs +1884 -1660
- package/dist/htmlminifier.esm.bundle.js +33820 -33531
- package/dist/types/htmlminifier.d.ts +29 -3
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +1877 -1662
- package/src/htmlparser.js +10 -1
package/dist/htmlminifier.cjs
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var lightningcss = require('lightningcss');
|
|
6
5
|
var entities = require('entities');
|
|
7
6
|
var RelateURL = require('relateurl');
|
|
8
|
-
var terser = require('terser');
|
|
9
7
|
|
|
10
8
|
async function replaceAsync(str, regex, asyncFn) {
|
|
11
9
|
const promises = [];
|
|
@@ -86,6 +84,14 @@ const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base
|
|
|
86
84
|
|
|
87
85
|
const reCache = {};
|
|
88
86
|
|
|
87
|
+
// Pre-compiled regexes for common special elements (`script`, `style`, `noscript`)
|
|
88
|
+
// These are used frequently and pre-compiling them avoids regex creation overhead
|
|
89
|
+
const preCompiledStackedTags = {
|
|
90
|
+
'script': /([\s\S]*?)<\/script[^>]*>/i,
|
|
91
|
+
'style': /([\s\S]*?)<\/style[^>]*>/i,
|
|
92
|
+
'noscript': /([\s\S]*?)<\/noscript[^>]*>/i
|
|
93
|
+
};
|
|
94
|
+
|
|
89
95
|
function attrForHandler(handler) {
|
|
90
96
|
let pattern = singleAttrIdentifier.source +
|
|
91
97
|
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
@@ -234,7 +240,8 @@ class HTMLParser {
|
|
|
234
240
|
prevTag = '';
|
|
235
241
|
} else {
|
|
236
242
|
const stackedTag = lastTag.toLowerCase();
|
|
237
|
-
|
|
243
|
+
// Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
|
|
244
|
+
const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)</' + stackedTag + '[^>]*>', 'i'));
|
|
238
245
|
|
|
239
246
|
html = await replaceAsync(html, reStackedTag, async (_, text) => {
|
|
240
247
|
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
|
|
@@ -704,1668 +711,37 @@ function getPresetNames() {
|
|
|
704
711
|
return Object.keys(presets);
|
|
705
712
|
}
|
|
706
713
|
|
|
707
|
-
//
|
|
708
|
-
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
709
|
-
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
710
|
-
const RE_ALL_WS_NBSP = /[ \n\r\t\f\xA0]+/g;
|
|
711
|
-
const RE_NBSP_LEADING_GROUP = /(^|\xA0+)[^\xA0]+/g;
|
|
712
|
-
const RE_NBSP_LEAD_GROUP = /(\xA0+)[^\xA0]+/g;
|
|
713
|
-
const RE_NBSP_TRAILING_GROUP = /[^\xA0]+(\xA0+)/g;
|
|
714
|
-
const RE_NBSP_TRAILING_STRIP = /[^\xA0]+$/;
|
|
715
|
-
const RE_CONDITIONAL_COMMENT = /^\[if\s[^\]]+]|\[endif]$/;
|
|
716
|
-
const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
717
|
-
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
718
|
-
const RE_TRAILING_SEMICOLON = /;$/;
|
|
719
|
-
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
720
|
-
|
|
721
|
-
// Tiny stable stringify for options signatures (sorted keys, shallow, nested objects)
|
|
722
|
-
function stableStringify(obj) {
|
|
723
|
-
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
724
|
-
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
725
|
-
const keys = Object.keys(obj).sort();
|
|
726
|
-
let out = '{';
|
|
727
|
-
for (let i = 0; i < keys.length; i++) {
|
|
728
|
-
const k = keys[i];
|
|
729
|
-
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
730
|
-
}
|
|
731
|
-
return out + '}';
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Minimal LRU cache for strings and promises
|
|
735
|
-
class LRU {
|
|
736
|
-
constructor(limit = 200) {
|
|
737
|
-
this.limit = limit;
|
|
738
|
-
this.map = new Map();
|
|
739
|
-
}
|
|
740
|
-
get(key) {
|
|
741
|
-
const v = this.map.get(key);
|
|
742
|
-
if (v !== undefined) {
|
|
743
|
-
this.map.delete(key);
|
|
744
|
-
this.map.set(key, v);
|
|
745
|
-
}
|
|
746
|
-
return v;
|
|
747
|
-
}
|
|
748
|
-
set(key, value) {
|
|
749
|
-
if (this.map.has(key)) this.map.delete(key);
|
|
750
|
-
this.map.set(key, value);
|
|
751
|
-
if (this.map.size > this.limit) {
|
|
752
|
-
const first = this.map.keys().next().value;
|
|
753
|
-
this.map.delete(first);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
delete(key) { this.map.delete(key); }
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Per-process caches
|
|
760
|
-
const jsMinifyCache = new LRU(200);
|
|
761
|
-
const cssMinifyCache = new LRU(200);
|
|
762
|
-
|
|
763
|
-
const trimWhitespace = str => {
|
|
764
|
-
if (!str) return str;
|
|
765
|
-
// Fast path: if no whitespace at start or end, return early
|
|
766
|
-
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
767
|
-
return str;
|
|
768
|
-
}
|
|
769
|
-
return str.replace(RE_WS_START, '').replace(RE_WS_END, '');
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
function collapseWhitespaceAll(str) {
|
|
773
|
-
if (!str) return str;
|
|
774
|
-
// Fast path: if there are no common whitespace characters, return early
|
|
775
|
-
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
776
|
-
return str;
|
|
777
|
-
}
|
|
778
|
-
// Non-breaking space is specifically handled inside the replacer function here:
|
|
779
|
-
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
780
|
-
return spaces === '\t' ? '\t' : spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
785
|
-
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
786
|
-
|
|
787
|
-
if (!str) return str;
|
|
788
|
-
|
|
789
|
-
if (options.preserveLineBreaks) {
|
|
790
|
-
str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
|
|
791
|
-
lineBreakBefore = '\n';
|
|
792
|
-
return '';
|
|
793
|
-
}).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function () {
|
|
794
|
-
lineBreakAfter = '\n';
|
|
795
|
-
return '';
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
if (trimLeft) {
|
|
800
|
-
// Non-breaking space is specifically handled inside the replacer function here:
|
|
801
|
-
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
802
|
-
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
803
|
-
if (conservative && spaces === '\t') {
|
|
804
|
-
return '\t';
|
|
805
|
-
}
|
|
806
|
-
return spaces.replace(/^[^\xA0]+/, '').replace(RE_NBSP_LEAD_GROUP, '$1 ') || (conservative ? ' ' : '');
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (trimRight) {
|
|
811
|
-
// Non-breaking space is specifically handled inside the replacer function here:
|
|
812
|
-
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
813
|
-
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
814
|
-
if (conservative && spaces === '\t') {
|
|
815
|
-
return '\t';
|
|
816
|
-
}
|
|
817
|
-
return spaces.replace(RE_NBSP_TRAILING_GROUP, ' $1').replace(RE_NBSP_TRAILING_STRIP, '') || (conservative ? ' ' : '');
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (collapseAll) {
|
|
822
|
-
// Strip non-space whitespace then compress spaces to one
|
|
823
|
-
str = collapseWhitespaceAll(str);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
return lineBreakBefore + str + lineBreakAfter;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Non-empty elements that will maintain whitespace around them
|
|
830
|
-
const inlineElementsToKeepWhitespaceAround = ['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr'];
|
|
831
|
-
// Non-empty elements that will maintain whitespace within them
|
|
832
|
-
const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
|
|
833
|
-
// Elements that will always maintain whitespace around them
|
|
834
|
-
const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
|
|
835
|
-
|
|
836
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
837
|
-
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
838
|
-
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
839
|
-
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
840
|
-
}
|
|
841
|
-
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
842
|
-
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
843
|
-
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
844
|
-
}
|
|
845
|
-
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function isConditionalComment(text) {
|
|
849
|
-
return RE_CONDITIONAL_COMMENT.test(text);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function isIgnoredComment(text, options) {
|
|
853
|
-
for (let i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
|
|
854
|
-
if (options.ignoreCustomComments[i].test(text)) {
|
|
855
|
-
return true;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
return false;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function isEventAttribute(attrName, options) {
|
|
862
|
-
const patterns = options.customEventAttributes;
|
|
863
|
-
if (patterns) {
|
|
864
|
-
for (let i = patterns.length; i--;) {
|
|
865
|
-
if (patterns[i].test(attrName)) {
|
|
866
|
-
return true;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function canRemoveAttributeQuotes(value) {
|
|
875
|
-
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
876
|
-
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
877
|
-
}
|
|
714
|
+
// Lazy-load heavy dependencies only when needed
|
|
878
715
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
716
|
+
let lightningCSSPromise;
|
|
717
|
+
async function getLightningCSS() {
|
|
718
|
+
if (!lightningCSSPromise) {
|
|
719
|
+
lightningCSSPromise = import('lightningcss').then(m => m.transform);
|
|
884
720
|
}
|
|
885
|
-
return
|
|
721
|
+
return lightningCSSPromise;
|
|
886
722
|
}
|
|
887
723
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
loading: 'eager',
|
|
893
|
-
popovertargetaction: 'toggle'
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
// Tag-specific default attribute values
|
|
897
|
-
const tagDefaults = {
|
|
898
|
-
area: { shape: 'rect' },
|
|
899
|
-
button: { type: 'submit' },
|
|
900
|
-
form: {
|
|
901
|
-
enctype: 'application/x-www-form-urlencoded',
|
|
902
|
-
method: 'get'
|
|
903
|
-
},
|
|
904
|
-
html: { dir: 'ltr' },
|
|
905
|
-
img: { decoding: 'auto' },
|
|
906
|
-
input: {
|
|
907
|
-
colorspace: 'limited-srgb',
|
|
908
|
-
type: 'text'
|
|
909
|
-
},
|
|
910
|
-
marquee: {
|
|
911
|
-
behavior: 'scroll',
|
|
912
|
-
direction: 'left'
|
|
913
|
-
},
|
|
914
|
-
style: { media: 'all' },
|
|
915
|
-
textarea: { wrap: 'soft' },
|
|
916
|
-
track: { kind: 'subtitles' }
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
920
|
-
attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
|
921
|
-
|
|
922
|
-
// Legacy attributes
|
|
923
|
-
if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
|
|
924
|
-
return true;
|
|
925
|
-
}
|
|
926
|
-
if (tag === 'script' && attrName === 'charset' && !attributesInclude(attrs, 'src')) {
|
|
927
|
-
return true;
|
|
928
|
-
}
|
|
929
|
-
if (tag === 'a' && attrName === 'name' && attributesInclude(attrs, 'id')) {
|
|
930
|
-
return true;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// Check general defaults
|
|
934
|
-
if (generalDefaults[attrName] === attrValue) {
|
|
935
|
-
return true;
|
|
724
|
+
let terserPromise;
|
|
725
|
+
async function getTerser() {
|
|
726
|
+
if (!terserPromise) {
|
|
727
|
+
terserPromise = import('terser').then(m => m.minify);
|
|
936
728
|
}
|
|
937
|
-
|
|
938
|
-
// Check tag-specific defaults
|
|
939
|
-
return tagDefaults[tag]?.[attrName] === attrValue;
|
|
729
|
+
return terserPromise;
|
|
940
730
|
}
|
|
941
731
|
|
|
942
|
-
//
|
|
943
|
-
// https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
|
|
944
|
-
const executableScriptsMimetypes = new Set([
|
|
945
|
-
'text/javascript',
|
|
946
|
-
'text/ecmascript',
|
|
947
|
-
'text/jscript',
|
|
948
|
-
'application/javascript',
|
|
949
|
-
'application/x-javascript',
|
|
950
|
-
'application/ecmascript',
|
|
951
|
-
'module'
|
|
952
|
-
]);
|
|
732
|
+
// Type definitions
|
|
953
733
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
return keepScriptsMimetypes.has(attrValue);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function isExecutableScript(tag, attrs) {
|
|
969
|
-
if (tag !== 'script') {
|
|
970
|
-
return false;
|
|
971
|
-
}
|
|
972
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
973
|
-
const attrName = attrs[i].name.toLowerCase();
|
|
974
|
-
if (attrName === 'type') {
|
|
975
|
-
return isScriptTypeAttribute(attrs[i].value);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return true;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function isStyleLinkTypeAttribute(attrValue = '') {
|
|
982
|
-
attrValue = trimWhitespace(attrValue).toLowerCase();
|
|
983
|
-
return attrValue === '' || attrValue === 'text/css';
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function isStyleSheet(tag, attrs) {
|
|
987
|
-
if (tag !== 'style') {
|
|
988
|
-
return false;
|
|
989
|
-
}
|
|
990
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
991
|
-
const attrName = attrs[i].name.toLowerCase();
|
|
992
|
-
if (attrName === 'type') {
|
|
993
|
-
return isStyleLinkTypeAttribute(attrs[i].value);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return true;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const isSimpleBoolean = new Set(['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']);
|
|
1000
|
-
const isBooleanValue = new Set(['true', 'false']);
|
|
1001
|
-
|
|
1002
|
-
function isBooleanAttribute(attrName, attrValue) {
|
|
1003
|
-
return isSimpleBoolean.has(attrName) || (attrName === 'draggable' && !isBooleanValue.has(attrValue));
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function isUriTypeAttribute(attrName, tag) {
|
|
1007
|
-
return (
|
|
1008
|
-
(/^(?:a|area|link|base)$/.test(tag) && attrName === 'href') ||
|
|
1009
|
-
(tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
|
|
1010
|
-
(tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
|
|
1011
|
-
(tag === 'q' && attrName === 'cite') ||
|
|
1012
|
-
(tag === 'blockquote' && attrName === 'cite') ||
|
|
1013
|
-
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
|
1014
|
-
(tag === 'form' && attrName === 'action') ||
|
|
1015
|
-
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
|
1016
|
-
(tag === 'head' && attrName === 'profile') ||
|
|
1017
|
-
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function isNumberTypeAttribute(attrName, tag) {
|
|
1022
|
-
return (
|
|
1023
|
-
(/^(?:a|area|object|button)$/.test(tag) && attrName === 'tabindex') ||
|
|
1024
|
-
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
|
1025
|
-
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
|
1026
|
-
(tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
|
|
1027
|
-
(tag === 'colgroup' && attrName === 'span') ||
|
|
1028
|
-
(tag === 'col' && attrName === 'span') ||
|
|
1029
|
-
((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function isLinkType(tag, attrs, value) {
|
|
1034
|
-
if (tag !== 'link') return false;
|
|
1035
|
-
const needle = String(value).toLowerCase();
|
|
1036
|
-
for (let i = 0; i < attrs.length; i++) {
|
|
1037
|
-
if (attrs[i].name.toLowerCase() === 'rel') {
|
|
1038
|
-
const tokens = String(attrs[i].value).toLowerCase().split(/\s+/);
|
|
1039
|
-
if (tokens.includes(needle)) return true;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
return false;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
function isMediaQuery(tag, attrs, attrName) {
|
|
1046
|
-
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
const srcsetTags = new Set(['img', 'source']);
|
|
1050
|
-
|
|
1051
|
-
function isSrcset(attrName, tag) {
|
|
1052
|
-
return attrName === 'srcset' && srcsetTags.has(tag);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
1056
|
-
if (isEventAttribute(attrName, options)) {
|
|
1057
|
-
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
1058
|
-
return options.minifyJS(attrValue, true);
|
|
1059
|
-
} else if (attrName === 'class') {
|
|
1060
|
-
attrValue = trimWhitespace(attrValue);
|
|
1061
|
-
if (options.sortClassName) {
|
|
1062
|
-
attrValue = options.sortClassName(attrValue);
|
|
1063
|
-
} else {
|
|
1064
|
-
attrValue = collapseWhitespaceAll(attrValue);
|
|
1065
|
-
}
|
|
1066
|
-
return attrValue;
|
|
1067
|
-
} else if (isUriTypeAttribute(attrName, tag)) {
|
|
1068
|
-
attrValue = trimWhitespace(attrValue);
|
|
1069
|
-
if (isLinkType(tag, attrs, 'canonical')) {
|
|
1070
|
-
return attrValue;
|
|
1071
|
-
}
|
|
1072
|
-
try {
|
|
1073
|
-
const out = await options.minifyURLs(attrValue);
|
|
1074
|
-
return typeof out === 'string' ? out : attrValue;
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
if (!options.continueOnMinifyError) {
|
|
1077
|
-
throw err;
|
|
1078
|
-
}
|
|
1079
|
-
options.log && options.log(err);
|
|
1080
|
-
return attrValue;
|
|
1081
|
-
}
|
|
1082
|
-
} else if (isNumberTypeAttribute(attrName, tag)) {
|
|
1083
|
-
return trimWhitespace(attrValue);
|
|
1084
|
-
} else if (attrName === 'style') {
|
|
1085
|
-
attrValue = trimWhitespace(attrValue);
|
|
1086
|
-
if (attrValue) {
|
|
1087
|
-
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
1088
|
-
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
1089
|
-
}
|
|
1090
|
-
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
1091
|
-
}
|
|
1092
|
-
return attrValue;
|
|
1093
|
-
} else if (isSrcset(attrName, tag)) {
|
|
1094
|
-
// https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
|
1095
|
-
attrValue = (await Promise.all(trimWhitespace(attrValue).split(/\s+,\s*|\s*,\s+/).map(async function (candidate) {
|
|
1096
|
-
let url = candidate;
|
|
1097
|
-
let descriptor = '';
|
|
1098
|
-
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
1099
|
-
if (match) {
|
|
1100
|
-
url = url.slice(0, -match[0].length);
|
|
1101
|
-
const num = +match[1].slice(0, -1);
|
|
1102
|
-
const suffix = match[1].slice(-1);
|
|
1103
|
-
if (num !== 1 || suffix !== 'x') {
|
|
1104
|
-
descriptor = ' ' + num + suffix;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
try {
|
|
1108
|
-
const out = await options.minifyURLs(url);
|
|
1109
|
-
return (typeof out === 'string' ? out : url) + descriptor;
|
|
1110
|
-
} catch (err) {
|
|
1111
|
-
if (!options.continueOnMinifyError) {
|
|
1112
|
-
throw err;
|
|
1113
|
-
}
|
|
1114
|
-
options.log && options.log(err);
|
|
1115
|
-
return url + descriptor;
|
|
1116
|
-
}
|
|
1117
|
-
}))).join(', ');
|
|
1118
|
-
} else if (isMetaViewport(tag, attrs) && attrName === 'content') {
|
|
1119
|
-
attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function (numString) {
|
|
1120
|
-
// "0.90000" -> "0.9"
|
|
1121
|
-
// "1.0" -> "1"
|
|
1122
|
-
// "1.0001" -> "1.0001" (unchanged)
|
|
1123
|
-
return (+numString).toString();
|
|
1124
|
-
});
|
|
1125
|
-
} else if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
|
1126
|
-
return collapseWhitespaceAll(attrValue);
|
|
1127
|
-
} else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
|
1128
|
-
attrValue = trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
|
|
1129
|
-
} else if (tag === 'script' && attrName === 'type') {
|
|
1130
|
-
attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
|
|
1131
|
-
} else if (isMediaQuery(tag, attrs, attrName)) {
|
|
1132
|
-
attrValue = trimWhitespace(attrValue);
|
|
1133
|
-
return options.minifyCSS(attrValue, 'media');
|
|
1134
|
-
} else if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
1135
|
-
// Recursively minify HTML content within srcdoc attribute
|
|
1136
|
-
// Fast-path: skip if nothing would change
|
|
1137
|
-
if (!shouldMinifyInnerHTML(options)) {
|
|
1138
|
-
return attrValue;
|
|
1139
|
-
}
|
|
1140
|
-
return minifyHTMLSelf(attrValue, options, true);
|
|
1141
|
-
}
|
|
1142
|
-
return attrValue;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function isMetaViewport(tag, attrs) {
|
|
1146
|
-
if (tag !== 'meta') {
|
|
1147
|
-
return false;
|
|
1148
|
-
}
|
|
1149
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1150
|
-
if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
|
1151
|
-
return true;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
function isContentSecurityPolicy(tag, attrs) {
|
|
1157
|
-
if (tag !== 'meta') {
|
|
1158
|
-
return false;
|
|
1159
|
-
}
|
|
1160
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1161
|
-
if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') {
|
|
1162
|
-
return true;
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// Wrap CSS declarations for inline styles and media queries
|
|
1168
|
-
// This ensures proper context for CSS minification
|
|
1169
|
-
function wrapCSS(text, type) {
|
|
1170
|
-
switch (type) {
|
|
1171
|
-
case 'inline':
|
|
1172
|
-
return '*{' + text + '}';
|
|
1173
|
-
case 'media':
|
|
1174
|
-
return '@media ' + text + '{a{top:0}}';
|
|
1175
|
-
default:
|
|
1176
|
-
return text;
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
function unwrapCSS(text, type) {
|
|
1181
|
-
let matches;
|
|
1182
|
-
switch (type) {
|
|
1183
|
-
case 'inline':
|
|
1184
|
-
matches = text.match(/^\*\{([\s\S]*)\}$/);
|
|
1185
|
-
break;
|
|
1186
|
-
case 'media':
|
|
1187
|
-
matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
|
|
1188
|
-
break;
|
|
1189
|
-
}
|
|
1190
|
-
return matches ? matches[1] : text;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
async function cleanConditionalComment(comment, options) {
|
|
1194
|
-
return options.processConditionalComments
|
|
1195
|
-
? await replaceAsync(comment, /^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, async function (match, prefix, text, suffix) {
|
|
1196
|
-
return prefix + await minifyHTML(text, options, true) + suffix;
|
|
1197
|
-
})
|
|
1198
|
-
: comment;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const jsonScriptTypes = new Set([
|
|
1202
|
-
'application/json',
|
|
1203
|
-
'application/ld+json',
|
|
1204
|
-
'application/manifest+json',
|
|
1205
|
-
'application/vnd.geo+json',
|
|
1206
|
-
'importmap',
|
|
1207
|
-
'speculationrules',
|
|
1208
|
-
]);
|
|
1209
|
-
|
|
1210
|
-
function minifyJson(text, options) {
|
|
1211
|
-
try {
|
|
1212
|
-
return JSON.stringify(JSON.parse(text));
|
|
1213
|
-
}
|
|
1214
|
-
catch (err) {
|
|
1215
|
-
if (!options.continueOnMinifyError) {
|
|
1216
|
-
throw err;
|
|
1217
|
-
}
|
|
1218
|
-
options.log && options.log(err);
|
|
1219
|
-
return text;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function hasJsonScriptType(attrs) {
|
|
1224
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1225
|
-
const attrName = attrs[i].name.toLowerCase();
|
|
1226
|
-
if (attrName === 'type') {
|
|
1227
|
-
const attrValue = trimWhitespace((attrs[i].value || '').split(/;/, 2)[0]).toLowerCase();
|
|
1228
|
-
if (jsonScriptTypes.has(attrValue)) {
|
|
1229
|
-
return true;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
return false;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
async function processScript(text, options, currentAttrs) {
|
|
1237
|
-
for (let i = 0, len = currentAttrs.length; i < len; i++) {
|
|
1238
|
-
const attrName = currentAttrs[i].name.toLowerCase();
|
|
1239
|
-
if (attrName === 'type') {
|
|
1240
|
-
const rawValue = currentAttrs[i].value;
|
|
1241
|
-
const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0]).toLowerCase();
|
|
1242
|
-
// Minify JSON script types automatically
|
|
1243
|
-
if (jsonScriptTypes.has(normalizedValue)) {
|
|
1244
|
-
return minifyJson(text, options);
|
|
1245
|
-
}
|
|
1246
|
-
// Process custom script types if specified
|
|
1247
|
-
if (options.processScripts && options.processScripts.indexOf(rawValue) > -1) {
|
|
1248
|
-
return await minifyHTML(text, options);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
return text;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
|
|
1256
|
-
// - retain <body> if followed by <noscript>
|
|
1257
|
-
// - <rb>, <rt>, <rtc>, <rp> follow HTML Ruby Markup Extensions draft (https://www.w3.org/TR/html-ruby-extensions/)
|
|
1258
|
-
// - retain all tags which are adjacent to non-standard HTML tags
|
|
1259
|
-
const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
|
|
1260
|
-
const optionalEndTags = new Set(['html', 'head', 'body', 'li', 'dt', 'dd', 'p', 'rb', 'rt', 'rtc', 'rp', 'optgroup', 'option', 'colgroup', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']);
|
|
1261
|
-
const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
|
|
1262
|
-
const descriptionTags = new Set(['dt', 'dd']);
|
|
1263
|
-
const pBlockTags = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'search', 'section', 'table', 'ul']);
|
|
1264
|
-
const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
|
|
1265
|
-
const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // </rb>, </rt>, </rp> can be omitted if followed by <rb>, <rt>, <rtc>, or <rp>
|
|
1266
|
-
const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // </rtc> can be omitted if followed by <rb> or <rtc> (not <rt> or <rp>)
|
|
1267
|
-
const optionTag = new Set(['option', 'optgroup']);
|
|
1268
|
-
const tableContentTags = new Set(['tbody', 'tfoot']);
|
|
1269
|
-
const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
|
|
1270
|
-
const cellTags = new Set(['td', 'th']);
|
|
1271
|
-
const topLevelTags = new Set(['html', 'head', 'body']);
|
|
1272
|
-
const compactTags = new Set(['html', 'body']);
|
|
1273
|
-
const looseTags = new Set(['head', 'colgroup', 'caption']);
|
|
1274
|
-
const trailingTags = new Set(['dt', 'thead']);
|
|
1275
|
-
const htmlTags = new Set(['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', 'search', 'section', 'select', 'selectedcontent', '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']);
|
|
1276
|
-
|
|
1277
|
-
function canRemoveParentTag(optionalStartTag, tag) {
|
|
1278
|
-
switch (optionalStartTag) {
|
|
1279
|
-
case 'html':
|
|
1280
|
-
case 'head':
|
|
1281
|
-
return true;
|
|
1282
|
-
case 'body':
|
|
1283
|
-
return !headerTags.has(tag);
|
|
1284
|
-
case 'colgroup':
|
|
1285
|
-
return tag === 'col';
|
|
1286
|
-
case 'tbody':
|
|
1287
|
-
return tag === 'tr';
|
|
1288
|
-
}
|
|
1289
|
-
return false;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
function isStartTagMandatory(optionalEndTag, tag) {
|
|
1293
|
-
switch (tag) {
|
|
1294
|
-
case 'colgroup':
|
|
1295
|
-
return optionalEndTag === 'colgroup';
|
|
1296
|
-
case 'tbody':
|
|
1297
|
-
return tableSectionTags.has(optionalEndTag);
|
|
1298
|
-
}
|
|
1299
|
-
return false;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
1303
|
-
switch (optionalEndTag) {
|
|
1304
|
-
case 'html':
|
|
1305
|
-
case 'head':
|
|
1306
|
-
case 'body':
|
|
1307
|
-
case 'colgroup':
|
|
1308
|
-
case 'caption':
|
|
1309
|
-
return true;
|
|
1310
|
-
case 'li':
|
|
1311
|
-
case 'optgroup':
|
|
1312
|
-
case 'tr':
|
|
1313
|
-
return tag === optionalEndTag;
|
|
1314
|
-
case 'dt':
|
|
1315
|
-
case 'dd':
|
|
1316
|
-
return descriptionTags.has(tag);
|
|
1317
|
-
case 'p':
|
|
1318
|
-
return pBlockTags.has(tag);
|
|
1319
|
-
case 'rb':
|
|
1320
|
-
case 'rt':
|
|
1321
|
-
case 'rp':
|
|
1322
|
-
return rubyEndTagOmission.has(tag);
|
|
1323
|
-
case 'rtc':
|
|
1324
|
-
return rubyRtcEndTagOmission.has(tag);
|
|
1325
|
-
case 'option':
|
|
1326
|
-
return optionTag.has(tag);
|
|
1327
|
-
case 'thead':
|
|
1328
|
-
case 'tbody':
|
|
1329
|
-
return tableContentTags.has(tag);
|
|
1330
|
-
case 'tfoot':
|
|
1331
|
-
return tag === 'tbody';
|
|
1332
|
-
case 'td':
|
|
1333
|
-
case 'th':
|
|
1334
|
-
return cellTags.has(tag);
|
|
1335
|
-
}
|
|
1336
|
-
return false;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
const reEmptyAttribute = new RegExp(
|
|
1340
|
-
'^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
|
|
1341
|
-
'?:down|up|over|move|out)|key(?:press|down|up)))$');
|
|
1342
|
-
|
|
1343
|
-
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
1344
|
-
const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
|
|
1345
|
-
if (!isValueEmpty) {
|
|
1346
|
-
return false;
|
|
1347
|
-
}
|
|
1348
|
-
if (typeof options.removeEmptyAttributes === 'function') {
|
|
1349
|
-
return options.removeEmptyAttributes(attrName, tag);
|
|
1350
|
-
}
|
|
1351
|
-
return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
function hasAttrName(name, attrs) {
|
|
1355
|
-
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
1356
|
-
if (attrs[i].name === name) {
|
|
1357
|
-
return true;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
return false;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
function canRemoveElement(tag, attrs) {
|
|
1364
|
-
switch (tag) {
|
|
1365
|
-
case 'textarea':
|
|
1366
|
-
return false;
|
|
1367
|
-
case 'audio':
|
|
1368
|
-
case 'script':
|
|
1369
|
-
case 'video':
|
|
1370
|
-
if (hasAttrName('src', attrs)) {
|
|
1371
|
-
return false;
|
|
1372
|
-
}
|
|
1373
|
-
break;
|
|
1374
|
-
case 'iframe':
|
|
1375
|
-
if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
|
|
1376
|
-
return false;
|
|
1377
|
-
}
|
|
1378
|
-
break;
|
|
1379
|
-
case 'object':
|
|
1380
|
-
if (hasAttrName('data', attrs)) {
|
|
1381
|
-
return false;
|
|
1382
|
-
}
|
|
1383
|
-
break;
|
|
1384
|
-
case 'applet':
|
|
1385
|
-
if (hasAttrName('code', attrs)) {
|
|
1386
|
-
return false;
|
|
1387
|
-
}
|
|
1388
|
-
break;
|
|
1389
|
-
}
|
|
1390
|
-
return true;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
function canCollapseWhitespace(tag) {
|
|
1394
|
-
return !/^(?:script|style|pre|textarea)$/.test(tag);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
function canTrimWhitespace(tag) {
|
|
1398
|
-
return !/^(?:pre|textarea)$/.test(tag);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
async function normalizeAttr(attr, attrs, tag, options) {
|
|
1402
|
-
const attrName = options.name(attr.name);
|
|
1403
|
-
let attrValue = attr.value;
|
|
1404
|
-
|
|
1405
|
-
if (options.decodeEntities && attrValue) {
|
|
1406
|
-
// Fast path: only decode when entities are present
|
|
1407
|
-
if (attrValue.indexOf('&') !== -1) {
|
|
1408
|
-
attrValue = entities.decodeHTMLStrict(attrValue);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
if ((options.removeRedundantAttributes &&
|
|
1413
|
-
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
1414
|
-
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
1415
|
-
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
1416
|
-
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
1417
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
if (attrValue) {
|
|
1422
|
-
attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
if (options.removeEmptyAttributes &&
|
|
1426
|
-
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
1431
|
-
attrValue = attrValue.replace(RE_AMP_ENTITY, '&$1');
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
return {
|
|
1435
|
-
attr,
|
|
1436
|
-
name: attrName,
|
|
1437
|
-
value: attrValue
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
1442
|
-
const attrName = normalized.name;
|
|
1443
|
-
let attrValue = normalized.value;
|
|
1444
|
-
const attr = normalized.attr;
|
|
1445
|
-
let attrQuote = attr.quote;
|
|
1446
|
-
let attrFragment;
|
|
1447
|
-
let emittedAttrValue;
|
|
1448
|
-
|
|
1449
|
-
if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
|
|
1450
|
-
~attrValue.indexOf(uidAttr) || !canRemoveAttributeQuotes(attrValue))) {
|
|
1451
|
-
if (!options.preventAttributesEscaping) {
|
|
1452
|
-
if (typeof options.quoteCharacter === 'undefined') {
|
|
1453
|
-
const apos = (attrValue.match(/'/g) || []).length;
|
|
1454
|
-
const quot = (attrValue.match(/"/g) || []).length;
|
|
1455
|
-
attrQuote = apos < quot ? '\'' : '"';
|
|
1456
|
-
} else {
|
|
1457
|
-
attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
|
1458
|
-
}
|
|
1459
|
-
if (attrQuote === '"') {
|
|
1460
|
-
attrValue = attrValue.replace(/"/g, '"');
|
|
1461
|
-
} else {
|
|
1462
|
-
attrValue = attrValue.replace(/'/g, ''');
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
1466
|
-
if (!isLast && !options.removeTagWhitespace) {
|
|
1467
|
-
emittedAttrValue += ' ';
|
|
1468
|
-
}
|
|
1469
|
-
} else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
|
|
1470
|
-
// Make sure trailing slash is not interpreted as HTML self-closing tag
|
|
1471
|
-
emittedAttrValue = attrValue;
|
|
1472
|
-
} else {
|
|
1473
|
-
emittedAttrValue = attrValue + ' ';
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
|
|
1477
|
-
isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase()))) {
|
|
1478
|
-
attrFragment = attrName;
|
|
1479
|
-
if (!isLast) {
|
|
1480
|
-
attrFragment += ' ';
|
|
1481
|
-
}
|
|
1482
|
-
} else {
|
|
1483
|
-
attrFragment = attrName + attr.customAssign + emittedAttrValue;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
return attr.customOpen + attrFragment + attr.customClose;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
function identity(value) {
|
|
1490
|
-
return value;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
function identityAsync(value) {
|
|
1494
|
-
return Promise.resolve(value);
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
function shouldMinifyInnerHTML(options) {
|
|
1498
|
-
return Boolean(
|
|
1499
|
-
options.collapseWhitespace ||
|
|
1500
|
-
options.removeComments ||
|
|
1501
|
-
options.removeOptionalTags ||
|
|
1502
|
-
options.minifyJS !== identity ||
|
|
1503
|
-
options.minifyCSS !== identityAsync ||
|
|
1504
|
-
options.minifyURLs !== identity
|
|
1505
|
-
);
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
const processOptions = (inputOptions) => {
|
|
1509
|
-
const options = {
|
|
1510
|
-
name: function (name) {
|
|
1511
|
-
return name.toLowerCase();
|
|
1512
|
-
},
|
|
1513
|
-
canCollapseWhitespace,
|
|
1514
|
-
canTrimWhitespace,
|
|
1515
|
-
continueOnMinifyError: true,
|
|
1516
|
-
html5: true,
|
|
1517
|
-
ignoreCustomComments: [
|
|
1518
|
-
/^!/,
|
|
1519
|
-
/^\s*#/
|
|
1520
|
-
],
|
|
1521
|
-
ignoreCustomFragments: [
|
|
1522
|
-
/<%[\s\S]*?%>/,
|
|
1523
|
-
/<\?[\s\S]*?\?>/
|
|
1524
|
-
],
|
|
1525
|
-
includeAutoGeneratedTags: true,
|
|
1526
|
-
log: identity,
|
|
1527
|
-
minifyCSS: identityAsync,
|
|
1528
|
-
minifyJS: identity,
|
|
1529
|
-
minifyURLs: identity
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
Object.keys(inputOptions).forEach(function (key) {
|
|
1533
|
-
const option = inputOptions[key];
|
|
1534
|
-
|
|
1535
|
-
if (key === 'caseSensitive') {
|
|
1536
|
-
if (option) {
|
|
1537
|
-
options.name = identity;
|
|
1538
|
-
}
|
|
1539
|
-
} else if (key === 'log') {
|
|
1540
|
-
if (typeof option === 'function') {
|
|
1541
|
-
options.log = option;
|
|
1542
|
-
}
|
|
1543
|
-
} else if (key === 'minifyCSS' && typeof option !== 'function') {
|
|
1544
|
-
if (!option) {
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
1549
|
-
|
|
1550
|
-
options.minifyCSS = async function (text, type) {
|
|
1551
|
-
// Fast path: nothing to minify
|
|
1552
|
-
if (!text || !text.trim()) {
|
|
1553
|
-
return text;
|
|
1554
|
-
}
|
|
1555
|
-
text = await replaceAsync(
|
|
1556
|
-
text,
|
|
1557
|
-
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
1558
|
-
async function (match, prefix, dq, sq, unq, suffix) {
|
|
1559
|
-
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
1560
|
-
const url = dq ?? sq ?? unq ?? '';
|
|
1561
|
-
try {
|
|
1562
|
-
const out = await options.minifyURLs(url);
|
|
1563
|
-
return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
|
|
1564
|
-
} catch (err) {
|
|
1565
|
-
if (!options.continueOnMinifyError) {
|
|
1566
|
-
throw err;
|
|
1567
|
-
}
|
|
1568
|
-
options.log && options.log(err);
|
|
1569
|
-
return match;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
);
|
|
1573
|
-
// Cache key: wrapped content, type, options signature
|
|
1574
|
-
const inputCSS = wrapCSS(text, type);
|
|
1575
|
-
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
1576
|
-
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
1577
|
-
const cssKey = inputCSS.length > 2048
|
|
1578
|
-
? (inputCSS.length + '|' + inputCSS.slice(0, 50) + inputCSS.slice(-50) + '|' + type + '|' + cssSig)
|
|
1579
|
-
: (inputCSS + '|' + type + '|' + cssSig);
|
|
1580
|
-
|
|
1581
|
-
try {
|
|
1582
|
-
const cached = cssMinifyCache.get(cssKey);
|
|
1583
|
-
if (cached) {
|
|
1584
|
-
return cached;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
const result = lightningcss.transform({
|
|
1588
|
-
filename: 'input.css',
|
|
1589
|
-
code: Buffer.from(inputCSS),
|
|
1590
|
-
minify: true,
|
|
1591
|
-
errorRecovery: !!options.continueOnMinifyError,
|
|
1592
|
-
...lightningCssOptions
|
|
1593
|
-
});
|
|
1594
|
-
|
|
1595
|
-
const outputCSS = unwrapCSS(result.code.toString(), type);
|
|
1596
|
-
|
|
1597
|
-
// If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
|
|
1598
|
-
// This preserves:
|
|
1599
|
-
// 1. Template code like `<?php ?>`, `<%= %>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
|
|
1600
|
-
// 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
|
|
1601
|
-
// CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
|
|
1602
|
-
const isCDATA = text.includes('<![CDATA[');
|
|
1603
|
-
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
1604
|
-
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
1605
|
-
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
1606
|
-
|
|
1607
|
-
// Preserve if output is empty and input had template syntax or UIDs
|
|
1608
|
-
// This catches cases where Lightning CSS removed content that should be preserved
|
|
1609
|
-
const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
1610
|
-
|
|
1611
|
-
cssMinifyCache.set(cssKey, finalOutput);
|
|
1612
|
-
return finalOutput;
|
|
1613
|
-
} catch (err) {
|
|
1614
|
-
cssMinifyCache.delete(cssKey);
|
|
1615
|
-
if (!options.continueOnMinifyError) {
|
|
1616
|
-
throw err;
|
|
1617
|
-
}
|
|
1618
|
-
options.log && options.log(err);
|
|
1619
|
-
return text;
|
|
1620
|
-
}
|
|
1621
|
-
};
|
|
1622
|
-
} else if (key === 'minifyJS' && typeof option !== 'function') {
|
|
1623
|
-
if (!option) {
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
const terserOptions = typeof option === 'object' ? option : {};
|
|
1628
|
-
|
|
1629
|
-
terserOptions.parse = {
|
|
1630
|
-
...terserOptions.parse,
|
|
1631
|
-
bare_returns: false
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
|
-
options.minifyJS = async function (text, inline) {
|
|
1635
|
-
const start = text.match(/^\s*<!--.*/);
|
|
1636
|
-
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
1637
|
-
|
|
1638
|
-
terserOptions.parse.bare_returns = inline;
|
|
1639
|
-
|
|
1640
|
-
let jsKey;
|
|
1641
|
-
try {
|
|
1642
|
-
// Fast path: avoid invoking Terser for empty/whitespace-only content
|
|
1643
|
-
if (!code || !code.trim()) {
|
|
1644
|
-
return '';
|
|
1645
|
-
}
|
|
1646
|
-
// Cache key: content, inline, options signature (subset)
|
|
1647
|
-
const terserSig = stableStringify({
|
|
1648
|
-
compress: terserOptions.compress,
|
|
1649
|
-
mangle: terserOptions.mangle,
|
|
1650
|
-
ecma: terserOptions.ecma,
|
|
1651
|
-
toplevel: terserOptions.toplevel,
|
|
1652
|
-
module: terserOptions.module,
|
|
1653
|
-
keep_fnames: terserOptions.keep_fnames,
|
|
1654
|
-
format: terserOptions.format,
|
|
1655
|
-
cont: !!options.continueOnMinifyError,
|
|
1656
|
-
});
|
|
1657
|
-
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
1658
|
-
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
1659
|
-
const cached = jsMinifyCache.get(jsKey);
|
|
1660
|
-
if (cached) {
|
|
1661
|
-
return await cached;
|
|
1662
|
-
}
|
|
1663
|
-
const inFlight = (async () => {
|
|
1664
|
-
const result = await terser.minify(code, terserOptions);
|
|
1665
|
-
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
1666
|
-
})();
|
|
1667
|
-
jsMinifyCache.set(jsKey, inFlight);
|
|
1668
|
-
const resolved = await inFlight;
|
|
1669
|
-
jsMinifyCache.set(jsKey, resolved);
|
|
1670
|
-
return resolved;
|
|
1671
|
-
} catch (err) {
|
|
1672
|
-
if (jsKey) jsMinifyCache.delete(jsKey);
|
|
1673
|
-
if (!options.continueOnMinifyError) {
|
|
1674
|
-
throw err;
|
|
1675
|
-
}
|
|
1676
|
-
options.log && options.log(err);
|
|
1677
|
-
return text;
|
|
1678
|
-
}
|
|
1679
|
-
};
|
|
1680
|
-
} else if (key === 'minifyURLs' && typeof option !== 'function') {
|
|
1681
|
-
if (!option) {
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
let relateUrlOptions = option;
|
|
1686
|
-
|
|
1687
|
-
if (typeof option === 'string') {
|
|
1688
|
-
relateUrlOptions = { site: option };
|
|
1689
|
-
} else if (typeof option !== 'object') {
|
|
1690
|
-
relateUrlOptions = {};
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
options.minifyURLs = function (text) {
|
|
1694
|
-
try {
|
|
1695
|
-
return RelateURL.relate(text, relateUrlOptions);
|
|
1696
|
-
} catch (err) {
|
|
1697
|
-
if (!options.continueOnMinifyError) {
|
|
1698
|
-
throw err;
|
|
1699
|
-
}
|
|
1700
|
-
options.log && options.log(err);
|
|
1701
|
-
return text;
|
|
1702
|
-
}
|
|
1703
|
-
};
|
|
1704
|
-
} else {
|
|
1705
|
-
options[key] = option;
|
|
1706
|
-
}
|
|
1707
|
-
});
|
|
1708
|
-
return options;
|
|
1709
|
-
};
|
|
1710
|
-
|
|
1711
|
-
function uniqueId(value) {
|
|
1712
|
-
let id;
|
|
1713
|
-
do {
|
|
1714
|
-
id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
|
|
1715
|
-
} while (~value.indexOf(id));
|
|
1716
|
-
return id;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
const specialContentTags = new Set(['script', 'style']);
|
|
1720
|
-
|
|
1721
|
-
async function createSortFns(value, options, uidIgnore, uidAttr) {
|
|
1722
|
-
const attrChains = options.sortAttributes && Object.create(null);
|
|
1723
|
-
const classChain = options.sortClassName && new TokenChain();
|
|
1724
|
-
|
|
1725
|
-
function attrNames(attrs) {
|
|
1726
|
-
return attrs.map(function (attr) {
|
|
1727
|
-
return options.name(attr.name);
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
function shouldSkipUID(token, uid) {
|
|
1732
|
-
return !uid || token.indexOf(uid) === -1;
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
function shouldSkipUIDs(token) {
|
|
1736
|
-
return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
async function scan(input) {
|
|
1740
|
-
let currentTag, currentType;
|
|
1741
|
-
const parser = new HTMLParser(input, {
|
|
1742
|
-
start: function (tag, attrs) {
|
|
1743
|
-
if (attrChains) {
|
|
1744
|
-
if (!attrChains[tag]) {
|
|
1745
|
-
attrChains[tag] = new TokenChain();
|
|
1746
|
-
}
|
|
1747
|
-
attrChains[tag].add(attrNames(attrs).filter(shouldSkipUIDs));
|
|
1748
|
-
}
|
|
1749
|
-
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1750
|
-
const attr = attrs[i];
|
|
1751
|
-
if (classChain && attr.value && options.name(attr.name) === 'class') {
|
|
1752
|
-
classChain.add(trimWhitespace(attr.value).split(/[ \t\n\f\r]+/).filter(shouldSkipUIDs));
|
|
1753
|
-
} else if (options.processScripts && attr.name.toLowerCase() === 'type') {
|
|
1754
|
-
currentTag = tag;
|
|
1755
|
-
currentType = attr.value;
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
},
|
|
1759
|
-
end: function () {
|
|
1760
|
-
currentTag = '';
|
|
1761
|
-
},
|
|
1762
|
-
chars: async function (text) {
|
|
1763
|
-
// Only recursively scan HTML content, not JSON-LD or other non-HTML script types
|
|
1764
|
-
// `scan()` is for analyzing HTML attribute order, not for parsing JSON
|
|
1765
|
-
if (options.processScripts && specialContentTags.has(currentTag) &&
|
|
1766
|
-
options.processScripts.indexOf(currentType) > -1 &&
|
|
1767
|
-
currentType === 'text/html') {
|
|
1768
|
-
await scan(text);
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
});
|
|
1772
|
-
|
|
1773
|
-
await parser.parse();
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
const log = options.log;
|
|
1777
|
-
options.log = identity;
|
|
1778
|
-
options.sortAttributes = false;
|
|
1779
|
-
options.sortClassName = false;
|
|
1780
|
-
const firstPassOutput = await minifyHTML(value, options);
|
|
1781
|
-
await scan(firstPassOutput);
|
|
1782
|
-
options.log = log;
|
|
1783
|
-
if (attrChains) {
|
|
1784
|
-
const attrSorters = Object.create(null);
|
|
1785
|
-
for (const tag in attrChains) {
|
|
1786
|
-
attrSorters[tag] = attrChains[tag].createSorter();
|
|
1787
|
-
}
|
|
1788
|
-
options.sortAttributes = function (tag, attrs) {
|
|
1789
|
-
const sorter = attrSorters[tag];
|
|
1790
|
-
if (sorter) {
|
|
1791
|
-
const attrMap = Object.create(null);
|
|
1792
|
-
const names = attrNames(attrs);
|
|
1793
|
-
names.forEach(function (name, index) {
|
|
1794
|
-
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
1795
|
-
});
|
|
1796
|
-
sorter.sort(names).forEach(function (name, index) {
|
|
1797
|
-
attrs[index] = attrMap[name].shift();
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
};
|
|
1801
|
-
}
|
|
1802
|
-
if (classChain) {
|
|
1803
|
-
const sorter = classChain.createSorter();
|
|
1804
|
-
options.sortClassName = function (value) {
|
|
1805
|
-
return sorter.sort(value.split(/[ \n\f\r]+/)).join(' ');
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
async function minifyHTML(value, options, partialMarkup) {
|
|
1811
|
-
// Check input length limitation to prevent ReDoS attacks
|
|
1812
|
-
if (options.maxInputLength && value.length > options.maxInputLength) {
|
|
1813
|
-
throw new Error(`Input length (${value.length}) exceeds maximum allowed length (${options.maxInputLength})`);
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
if (options.collapseWhitespace) {
|
|
1817
|
-
value = collapseWhitespace(value, options, true, true);
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
const buffer = [];
|
|
1821
|
-
let charsPrevTag;
|
|
1822
|
-
let currentChars = '';
|
|
1823
|
-
let hasChars;
|
|
1824
|
-
let currentTag = '';
|
|
1825
|
-
let currentAttrs = [];
|
|
1826
|
-
const stackNoTrimWhitespace = [];
|
|
1827
|
-
const stackNoCollapseWhitespace = [];
|
|
1828
|
-
let optionalStartTag = '';
|
|
1829
|
-
let optionalEndTag = '';
|
|
1830
|
-
const ignoredMarkupChunks = [];
|
|
1831
|
-
const ignoredCustomMarkupChunks = [];
|
|
1832
|
-
let uidIgnore;
|
|
1833
|
-
let uidAttr;
|
|
1834
|
-
let uidPattern;
|
|
1835
|
-
// Create inline tags/text sets with custom elements
|
|
1836
|
-
const customElementsInput = options.inlineCustomElements ?? [];
|
|
1837
|
-
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
1838
|
-
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
1839
|
-
const inlineTextSet = new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements]);
|
|
1840
|
-
const inlineElements = new Set([...inlineElementsToKeepWhitespaceAround, ...normalizedCustomElements]);
|
|
1841
|
-
|
|
1842
|
-
// Temporarily replace ignored chunks with comments,
|
|
1843
|
-
// so that we don’t have to worry what’s there.
|
|
1844
|
-
// For all we care there might be
|
|
1845
|
-
// completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
|
|
1846
|
-
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
1847
|
-
if (!uidIgnore) {
|
|
1848
|
-
uidIgnore = uniqueId(value);
|
|
1849
|
-
const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
|
1850
|
-
if (options.ignoreCustomComments) {
|
|
1851
|
-
options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
|
1852
|
-
} else {
|
|
1853
|
-
options.ignoreCustomComments = [];
|
|
1854
|
-
}
|
|
1855
|
-
options.ignoreCustomComments.push(pattern);
|
|
1856
|
-
}
|
|
1857
|
-
const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
|
|
1858
|
-
ignoredMarkupChunks.push(group1);
|
|
1859
|
-
return token;
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
const customFragments = options.ignoreCustomFragments.map(function (re) {
|
|
1863
|
-
return re.source;
|
|
1864
|
-
});
|
|
1865
|
-
if (customFragments.length) {
|
|
1866
|
-
// Warn about potential ReDoS if custom fragments use unlimited quantifiers
|
|
1867
|
-
for (let i = 0; i < customFragments.length; i++) {
|
|
1868
|
-
if (/[*+]/.test(customFragments[i])) {
|
|
1869
|
-
options.log('Warning: Custom fragment contains unlimited quantifiers (* or +) which may cause ReDoS vulnerability');
|
|
1870
|
-
break;
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
// Safe approach: Use bounded quantifiers instead of unlimited ones to prevent ReDoS
|
|
1875
|
-
const maxQuantifier = options.customFragmentQuantifierLimit || 200;
|
|
1876
|
-
const whitespacePattern = `\\s{0,${maxQuantifier}}`;
|
|
1877
|
-
|
|
1878
|
-
// Use bounded quantifiers to prevent ReDoS - this approach prevents exponential backtracking
|
|
1879
|
-
const reCustomIgnore = new RegExp(
|
|
1880
|
-
whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
|
|
1881
|
-
'g'
|
|
1882
|
-
);
|
|
1883
|
-
// Temporarily replace custom ignored fragments with unique attributes
|
|
1884
|
-
value = value.replace(reCustomIgnore, function (match) {
|
|
1885
|
-
if (!uidAttr) {
|
|
1886
|
-
uidAttr = uniqueId(value);
|
|
1887
|
-
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
1888
|
-
|
|
1889
|
-
if (options.minifyCSS) {
|
|
1890
|
-
options.minifyCSS = (function (fn) {
|
|
1891
|
-
return function (text, type) {
|
|
1892
|
-
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
1893
|
-
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1894
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
|
-
return fn(text, type);
|
|
1898
|
-
};
|
|
1899
|
-
})(options.minifyCSS);
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
if (options.minifyJS) {
|
|
1903
|
-
options.minifyJS = (function (fn) {
|
|
1904
|
-
return function (text, type) {
|
|
1905
|
-
return fn(text.replace(uidPattern, function (match, prefix, index) {
|
|
1906
|
-
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1907
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
1908
|
-
}), type);
|
|
1909
|
-
};
|
|
1910
|
-
})(options.minifyJS);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
|
|
1915
|
-
ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
|
|
1916
|
-
return '\t' + token + '\t';
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
1921
|
-
(options.sortClassName && typeof options.sortClassName !== 'function')) {
|
|
1922
|
-
await createSortFns(value, options, uidIgnore, uidAttr);
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
function _canCollapseWhitespace(tag, attrs) {
|
|
1926
|
-
return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
function _canTrimWhitespace(tag, attrs) {
|
|
1930
|
-
return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function removeStartTag() {
|
|
1934
|
-
let index = buffer.length - 1;
|
|
1935
|
-
while (index > 0 && !/^<[^/!]/.test(buffer[index])) {
|
|
1936
|
-
index--;
|
|
1937
|
-
}
|
|
1938
|
-
buffer.length = Math.max(0, index);
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
function removeEndTag() {
|
|
1942
|
-
let index = buffer.length - 1;
|
|
1943
|
-
while (index > 0 && !/^<\//.test(buffer[index])) {
|
|
1944
|
-
index--;
|
|
1945
|
-
}
|
|
1946
|
-
buffer.length = Math.max(0, index);
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// Look for trailing whitespaces, bypass any inline tags
|
|
1950
|
-
function trimTrailingWhitespace(index, nextTag) {
|
|
1951
|
-
for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
|
|
1952
|
-
const str = buffer[index];
|
|
1953
|
-
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
1954
|
-
if (match) {
|
|
1955
|
-
endTag = match[1];
|
|
1956
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
|
|
1957
|
-
break;
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// Look for trailing whitespaces from previously processed text
|
|
1963
|
-
// which may not be trimmed due to a following comment or an empty
|
|
1964
|
-
// element which has now been removed
|
|
1965
|
-
function squashTrailingWhitespace(nextTag) {
|
|
1966
|
-
let charsIndex = buffer.length - 1;
|
|
1967
|
-
if (buffer.length > 1) {
|
|
1968
|
-
const item = buffer[buffer.length - 1];
|
|
1969
|
-
if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
|
|
1970
|
-
charsIndex--;
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
trimTrailingWhitespace(charsIndex, nextTag);
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
const parser = new HTMLParser(value, {
|
|
1977
|
-
partialMarkup: partialMarkup ?? options.partialMarkup,
|
|
1978
|
-
continueOnParseError: options.continueOnParseError,
|
|
1979
|
-
customAttrAssign: options.customAttrAssign,
|
|
1980
|
-
customAttrSurround: options.customAttrSurround,
|
|
1981
|
-
html5: options.html5,
|
|
1982
|
-
|
|
1983
|
-
start: async function (tag, attrs, unary, unarySlash, autoGenerated) {
|
|
1984
|
-
if (tag.toLowerCase() === 'svg') {
|
|
1985
|
-
options = Object.create(options);
|
|
1986
|
-
options.caseSensitive = true;
|
|
1987
|
-
options.keepClosingSlash = true;
|
|
1988
|
-
options.name = identity;
|
|
1989
|
-
}
|
|
1990
|
-
tag = options.name(tag);
|
|
1991
|
-
currentTag = tag;
|
|
1992
|
-
charsPrevTag = tag;
|
|
1993
|
-
if (!inlineTextSet.has(tag)) {
|
|
1994
|
-
currentChars = '';
|
|
1995
|
-
}
|
|
1996
|
-
hasChars = false;
|
|
1997
|
-
currentAttrs = attrs;
|
|
1998
|
-
|
|
1999
|
-
let optional = options.removeOptionalTags;
|
|
2000
|
-
if (optional) {
|
|
2001
|
-
const htmlTag = htmlTags.has(tag);
|
|
2002
|
-
// <html> may be omitted if first thing inside is not a comment
|
|
2003
|
-
// <head> may be omitted if first thing inside is an element
|
|
2004
|
-
// <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
|
|
2005
|
-
// <colgroup> may be omitted if first thing inside is <col>
|
|
2006
|
-
// <tbody> may be omitted if first thing inside is <tr>
|
|
2007
|
-
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
2008
|
-
removeStartTag();
|
|
2009
|
-
}
|
|
2010
|
-
optionalStartTag = '';
|
|
2011
|
-
// End-tag-followed-by-start-tag omission rules
|
|
2012
|
-
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
|
|
2013
|
-
removeEndTag();
|
|
2014
|
-
// <colgroup> cannot be omitted if preceding </colgroup> is omitted
|
|
2015
|
-
// <tbody> cannot be omitted if preceding </tbody>, </thead> or </tfoot> is omitted
|
|
2016
|
-
optional = !isStartTagMandatory(optionalEndTag, tag);
|
|
2017
|
-
}
|
|
2018
|
-
optionalEndTag = '';
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
// Set whitespace flags for nested tags (e.g., <code> within a <pre>)
|
|
2022
|
-
if (options.collapseWhitespace) {
|
|
2023
|
-
if (!stackNoTrimWhitespace.length) {
|
|
2024
|
-
squashTrailingWhitespace(tag);
|
|
2025
|
-
}
|
|
2026
|
-
if (!unary) {
|
|
2027
|
-
if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
2028
|
-
stackNoTrimWhitespace.push(tag);
|
|
2029
|
-
}
|
|
2030
|
-
if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
2031
|
-
stackNoCollapseWhitespace.push(tag);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
const openTag = '<' + tag;
|
|
2037
|
-
const hasUnarySlash = unarySlash && options.keepClosingSlash;
|
|
2038
|
-
|
|
2039
|
-
buffer.push(openTag);
|
|
2040
|
-
|
|
2041
|
-
if (options.sortAttributes) {
|
|
2042
|
-
options.sortAttributes(tag, attrs);
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
const parts = [];
|
|
2046
|
-
for (let i = attrs.length, isLast = true; --i >= 0;) {
|
|
2047
|
-
const normalized = await normalizeAttr(attrs[i], attrs, tag, options);
|
|
2048
|
-
if (normalized) {
|
|
2049
|
-
parts.unshift(buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr));
|
|
2050
|
-
isLast = false;
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
if (parts.length > 0) {
|
|
2054
|
-
buffer.push(' ');
|
|
2055
|
-
buffer.push.apply(buffer, parts);
|
|
2056
|
-
} else if (optional && optionalStartTags.has(tag)) {
|
|
2057
|
-
// Start tag must never be omitted if it has any attributes
|
|
2058
|
-
optionalStartTag = tag;
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
|
|
2062
|
-
|
|
2063
|
-
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
2064
|
-
removeStartTag();
|
|
2065
|
-
optionalStartTag = '';
|
|
2066
|
-
}
|
|
2067
|
-
},
|
|
2068
|
-
end: function (tag, attrs, autoGenerated) {
|
|
2069
|
-
if (tag.toLowerCase() === 'svg') {
|
|
2070
|
-
options = Object.getPrototypeOf(options);
|
|
2071
|
-
}
|
|
2072
|
-
tag = options.name(tag);
|
|
2073
|
-
|
|
2074
|
-
// Check if current tag is in a whitespace stack
|
|
2075
|
-
if (options.collapseWhitespace) {
|
|
2076
|
-
if (stackNoTrimWhitespace.length) {
|
|
2077
|
-
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
2078
|
-
stackNoTrimWhitespace.pop();
|
|
2079
|
-
}
|
|
2080
|
-
} else {
|
|
2081
|
-
squashTrailingWhitespace('/' + tag);
|
|
2082
|
-
}
|
|
2083
|
-
if (stackNoCollapseWhitespace.length &&
|
|
2084
|
-
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
2085
|
-
stackNoCollapseWhitespace.pop();
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
let isElementEmpty = false;
|
|
2090
|
-
if (tag === currentTag) {
|
|
2091
|
-
currentTag = '';
|
|
2092
|
-
isElementEmpty = !hasChars;
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
if (options.removeOptionalTags) {
|
|
2096
|
-
// <html>, <head> or <body> may be omitted if the element is empty
|
|
2097
|
-
if (isElementEmpty && topLevelTags.has(optionalStartTag)) {
|
|
2098
|
-
removeStartTag();
|
|
2099
|
-
}
|
|
2100
|
-
optionalStartTag = '';
|
|
2101
|
-
// </html> or </body> may be omitted if not followed by comment
|
|
2102
|
-
// </head> may be omitted if not followed by space or comment
|
|
2103
|
-
// </p> may be omitted if no more content in non-</a> parent
|
|
2104
|
-
// except for </dt> or </thead>, end tags may be omitted if no more content in parent element
|
|
2105
|
-
if (htmlTags.has(tag) && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
|
|
2106
|
-
removeEndTag();
|
|
2107
|
-
}
|
|
2108
|
-
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
|
|
2112
|
-
// Remove last “element” from buffer
|
|
2113
|
-
removeStartTag();
|
|
2114
|
-
optionalStartTag = '';
|
|
2115
|
-
optionalEndTag = '';
|
|
2116
|
-
} else {
|
|
2117
|
-
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
2118
|
-
optionalEndTag = '';
|
|
2119
|
-
} else {
|
|
2120
|
-
buffer.push('</' + tag + '>');
|
|
2121
|
-
}
|
|
2122
|
-
charsPrevTag = '/' + tag;
|
|
2123
|
-
if (!inlineElements.has(tag)) {
|
|
2124
|
-
currentChars = '';
|
|
2125
|
-
} else if (isElementEmpty) {
|
|
2126
|
-
currentChars += '|';
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
},
|
|
2130
|
-
chars: async function (text, prevTag, nextTag) {
|
|
2131
|
-
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
2132
|
-
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
2133
|
-
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
2134
|
-
if (text.indexOf('&') !== -1) {
|
|
2135
|
-
text = entities.decodeHTML(text);
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
if (options.collapseWhitespace) {
|
|
2139
|
-
if (!stackNoTrimWhitespace.length) {
|
|
2140
|
-
if (prevTag === 'comment') {
|
|
2141
|
-
const prevComment = buffer[buffer.length - 1];
|
|
2142
|
-
if (prevComment.indexOf(uidIgnore) === -1) {
|
|
2143
|
-
if (!prevComment) {
|
|
2144
|
-
prevTag = charsPrevTag;
|
|
2145
|
-
}
|
|
2146
|
-
if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
|
|
2147
|
-
const charsIndex = buffer.length - 2;
|
|
2148
|
-
buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
|
|
2149
|
-
text = trailingSpaces + text;
|
|
2150
|
-
return '';
|
|
2151
|
-
});
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
if (prevTag) {
|
|
2156
|
-
if (prevTag === '/nobr' || prevTag === 'wbr') {
|
|
2157
|
-
if (/^\s/.test(text)) {
|
|
2158
|
-
let tagIndex = buffer.length - 1;
|
|
2159
|
-
while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
|
|
2160
|
-
tagIndex--;
|
|
2161
|
-
}
|
|
2162
|
-
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
2163
|
-
}
|
|
2164
|
-
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
2165
|
-
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
if (prevTag || nextTag) {
|
|
2169
|
-
text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
|
|
2170
|
-
} else {
|
|
2171
|
-
text = collapseWhitespace(text, options, true, true);
|
|
2172
|
-
}
|
|
2173
|
-
if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
|
|
2174
|
-
trimTrailingWhitespace(buffer.length - 1, nextTag);
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
|
|
2178
|
-
text = collapseWhitespace(text, options, false, false, true);
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
if (specialContentTags.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
|
|
2182
|
-
text = await processScript(text, options, currentAttrs);
|
|
2183
|
-
}
|
|
2184
|
-
if (isExecutableScript(currentTag, currentAttrs)) {
|
|
2185
|
-
text = await options.minifyJS(text);
|
|
2186
|
-
}
|
|
2187
|
-
if (isStyleSheet(currentTag, currentAttrs)) {
|
|
2188
|
-
text = await options.minifyCSS(text);
|
|
2189
|
-
}
|
|
2190
|
-
if (options.removeOptionalTags && text) {
|
|
2191
|
-
// <html> may be omitted if first thing inside is not a comment
|
|
2192
|
-
// <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
|
|
2193
|
-
if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
|
|
2194
|
-
removeStartTag();
|
|
2195
|
-
}
|
|
2196
|
-
optionalStartTag = '';
|
|
2197
|
-
// </html> or </body> may be omitted if not followed by comment
|
|
2198
|
-
// </head>, </colgroup> or </caption> may be omitted if not followed by space or comment
|
|
2199
|
-
if (compactTags.has(optionalEndTag) || (looseTags.has(optionalEndTag) && !/^\s/.test(text))) {
|
|
2200
|
-
removeEndTag();
|
|
2201
|
-
}
|
|
2202
|
-
// Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
|
|
2203
|
-
if (!/^\s+$/.test(text) || !options.collapseWhitespace || options.conservativeCollapse) {
|
|
2204
|
-
optionalEndTag = '';
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
2208
|
-
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
2209
|
-
// Escape any `&` symbols that start either:
|
|
2210
|
-
// 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
|
|
2211
|
-
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
2212
|
-
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
2213
|
-
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
2214
|
-
if (text.indexOf('&') !== -1) {
|
|
2215
|
-
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');
|
|
2216
|
-
}
|
|
2217
|
-
if (text.indexOf('<') !== -1) {
|
|
2218
|
-
text = text.replace(/</g, '<');
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
2222
|
-
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
2223
|
-
return ignoredCustomMarkupChunks[+index][0];
|
|
2224
|
-
});
|
|
2225
|
-
}
|
|
2226
|
-
currentChars += text;
|
|
2227
|
-
if (text) {
|
|
2228
|
-
hasChars = true;
|
|
2229
|
-
}
|
|
2230
|
-
buffer.push(text);
|
|
2231
|
-
},
|
|
2232
|
-
comment: async function (text, nonStandard) {
|
|
2233
|
-
const prefix = nonStandard ? '<!' : '<!--';
|
|
2234
|
-
const suffix = nonStandard ? '>' : '-->';
|
|
2235
|
-
if (isConditionalComment(text)) {
|
|
2236
|
-
text = prefix + await cleanConditionalComment(text, options) + suffix;
|
|
2237
|
-
} else if (options.removeComments) {
|
|
2238
|
-
if (isIgnoredComment(text, options)) {
|
|
2239
|
-
text = '<!--' + text + '-->';
|
|
2240
|
-
} else {
|
|
2241
|
-
text = '';
|
|
2242
|
-
}
|
|
2243
|
-
} else {
|
|
2244
|
-
text = prefix + text + suffix;
|
|
2245
|
-
}
|
|
2246
|
-
if (options.removeOptionalTags && text) {
|
|
2247
|
-
// Preceding comments suppress tag omissions
|
|
2248
|
-
optionalStartTag = '';
|
|
2249
|
-
optionalEndTag = '';
|
|
2250
|
-
}
|
|
2251
|
-
buffer.push(text);
|
|
2252
|
-
},
|
|
2253
|
-
doctype: function (doctype) {
|
|
2254
|
-
buffer.push(options.useShortDoctype
|
|
2255
|
-
? '<!doctype' +
|
|
2256
|
-
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
2257
|
-
: collapseWhitespaceAll(doctype));
|
|
2258
|
-
}
|
|
2259
|
-
});
|
|
2260
|
-
|
|
2261
|
-
await parser.parse();
|
|
2262
|
-
|
|
2263
|
-
if (options.removeOptionalTags) {
|
|
2264
|
-
// <html> may be omitted if first thing inside is not a comment
|
|
2265
|
-
// <head> or <body> may be omitted if empty
|
|
2266
|
-
if (topLevelTags.has(optionalStartTag)) {
|
|
2267
|
-
removeStartTag();
|
|
2268
|
-
}
|
|
2269
|
-
// except for </dt> or </thead>, end tags may be omitted if no more content in parent element
|
|
2270
|
-
if (optionalEndTag && !trailingTags.has(optionalEndTag)) {
|
|
2271
|
-
removeEndTag();
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
if (options.collapseWhitespace) {
|
|
2275
|
-
squashTrailingWhitespace('br');
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
return joinResultSegments(buffer, options, uidPattern
|
|
2279
|
-
? function (str) {
|
|
2280
|
-
return str.replace(uidPattern, function (match, prefix, index, suffix) {
|
|
2281
|
-
let chunk = ignoredCustomMarkupChunks[+index][0];
|
|
2282
|
-
if (options.collapseWhitespace) {
|
|
2283
|
-
if (prefix !== '\t') {
|
|
2284
|
-
chunk = prefix + chunk;
|
|
2285
|
-
}
|
|
2286
|
-
if (suffix !== '\t') {
|
|
2287
|
-
chunk += suffix;
|
|
2288
|
-
}
|
|
2289
|
-
return collapseWhitespace(chunk, {
|
|
2290
|
-
preserveLineBreaks: options.preserveLineBreaks,
|
|
2291
|
-
conservativeCollapse: !options.trimCustomFragments
|
|
2292
|
-
}, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk));
|
|
2293
|
-
}
|
|
2294
|
-
return chunk;
|
|
2295
|
-
});
|
|
2296
|
-
}
|
|
2297
|
-
: identity, uidIgnore
|
|
2298
|
-
? function (str) {
|
|
2299
|
-
return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (match, index) {
|
|
2300
|
-
return ignoredMarkupChunks[+index];
|
|
2301
|
-
});
|
|
2302
|
-
}
|
|
2303
|
-
: identity);
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
2307
|
-
let str;
|
|
2308
|
-
const maxLineLength = options.maxLineLength;
|
|
2309
|
-
const noNewlinesBeforeTagClose = options.noNewlinesBeforeTagClose;
|
|
2310
|
-
|
|
2311
|
-
if (maxLineLength) {
|
|
2312
|
-
let line = ''; const lines = [];
|
|
2313
|
-
while (results.length) {
|
|
2314
|
-
const len = line.length;
|
|
2315
|
-
const end = results[0].indexOf('\n');
|
|
2316
|
-
const isClosingTag = Boolean(results[0].match(endTag));
|
|
2317
|
-
const shouldKeepSameLine = noNewlinesBeforeTagClose && isClosingTag;
|
|
2318
|
-
|
|
2319
|
-
if (end < 0) {
|
|
2320
|
-
line += restoreIgnore(restoreCustom(results.shift()));
|
|
2321
|
-
} else {
|
|
2322
|
-
line += restoreIgnore(restoreCustom(results[0].slice(0, end)));
|
|
2323
|
-
results[0] = results[0].slice(end + 1);
|
|
2324
|
-
}
|
|
2325
|
-
if (len > 0 && line.length > maxLineLength && !shouldKeepSameLine) {
|
|
2326
|
-
lines.push(line.slice(0, len));
|
|
2327
|
-
line = line.slice(len);
|
|
2328
|
-
} else if (end >= 0) {
|
|
2329
|
-
lines.push(line);
|
|
2330
|
-
line = '';
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
if (line) {
|
|
2334
|
-
lines.push(line);
|
|
2335
|
-
}
|
|
2336
|
-
str = lines.join('\n');
|
|
2337
|
-
} else {
|
|
2338
|
-
str = restoreIgnore(restoreCustom(results.join('')));
|
|
2339
|
-
}
|
|
2340
|
-
return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
/**
|
|
2344
|
-
* @param {string} value
|
|
2345
|
-
* @param {MinifierOptions} [options]
|
|
2346
|
-
* @returns {Promise<string>}
|
|
2347
|
-
*/
|
|
2348
|
-
const minify = async function (value, options) {
|
|
2349
|
-
const start = Date.now();
|
|
2350
|
-
options = processOptions(options || {});
|
|
2351
|
-
const result = await minifyHTML(value, options);
|
|
2352
|
-
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
2353
|
-
return result;
|
|
2354
|
-
};
|
|
2355
|
-
|
|
2356
|
-
var htmlminifier = { minify, presets, getPreset, getPresetNames };
|
|
2357
|
-
|
|
2358
|
-
/**
|
|
2359
|
-
* @typedef {Object} HTMLAttribute
|
|
2360
|
-
* Representation of an attribute from the HTML parser.
|
|
2361
|
-
*
|
|
2362
|
-
* @prop {string} name
|
|
2363
|
-
* @prop {string} [value]
|
|
2364
|
-
* @prop {string} [quote]
|
|
2365
|
-
* @prop {string} [customAssign]
|
|
2366
|
-
* @prop {string} [customOpen]
|
|
2367
|
-
* @prop {string} [customClose]
|
|
2368
|
-
*/
|
|
734
|
+
/**
|
|
735
|
+
* @typedef {Object} HTMLAttribute
|
|
736
|
+
* Representation of an attribute from the HTML parser.
|
|
737
|
+
*
|
|
738
|
+
* @prop {string} name
|
|
739
|
+
* @prop {string} [value]
|
|
740
|
+
* @prop {string} [quote]
|
|
741
|
+
* @prop {string} [customAssign]
|
|
742
|
+
* @prop {string} [customOpen]
|
|
743
|
+
* @prop {string} [customClose]
|
|
744
|
+
*/
|
|
2369
745
|
|
|
2370
746
|
/**
|
|
2371
747
|
* @typedef {Object} MinifierOptions
|
|
@@ -2393,7 +769,7 @@ var htmlminifier = { minify, presets, getPreset, getPresetNames };
|
|
|
2393
769
|
*
|
|
2394
770
|
* @prop {boolean} [collapseBooleanAttributes]
|
|
2395
771
|
* Collapse boolean attributes to their name only (for example
|
|
2396
|
-
* `disabled="disabled"`
|
|
772
|
+
* `disabled="disabled"` → `disabled`).
|
|
2397
773
|
* See also: https://perfectionkills.com/experimenting-with-html-minifier/#collapse_boolean_attributes
|
|
2398
774
|
*
|
|
2399
775
|
* Default: `false`
|
|
@@ -2637,6 +1013,31 @@ var htmlminifier = { minify, presets, getPreset, getPresetNames };
|
|
|
2637
1013
|
*
|
|
2638
1014
|
* Default: `false`
|
|
2639
1015
|
*
|
|
1016
|
+
* @prop {string[]} [removeEmptyElementsExcept]
|
|
1017
|
+
* Specifies empty elements to preserve when `removeEmptyElements` is enabled.
|
|
1018
|
+
* Has no effect unless `removeEmptyElements: true`.
|
|
1019
|
+
*
|
|
1020
|
+
* Accepts tag names or HTML-like element specifications:
|
|
1021
|
+
*
|
|
1022
|
+
* * Tag name only: `["td", "span"]`—preserves all empty elements of these types
|
|
1023
|
+
* * With valued attributes: `["<span aria-hidden='true'>"]`—preserves only when attribute values match
|
|
1024
|
+
* * With boolean attributes: `["<input disabled>"]`—preserves only when boolean attribute is present
|
|
1025
|
+
* * Mixed: `["<button type='button' disabled>"]`—all specified attributes must match
|
|
1026
|
+
*
|
|
1027
|
+
* Attribute matching:
|
|
1028
|
+
*
|
|
1029
|
+
* * All specified attributes must be present and match (valued attributes must have exact values)
|
|
1030
|
+
* * Additional attributes on the element are allowed
|
|
1031
|
+
* * Attribute name matching respects the `caseSensitive` option
|
|
1032
|
+
* * Supports double quotes, single quotes, and unquoted attribute values in specifications
|
|
1033
|
+
*
|
|
1034
|
+
* Limitations:
|
|
1035
|
+
*
|
|
1036
|
+
* * Self-closing syntax (e.g., `["<span/>"]`) is not supported; use `["span"]` instead
|
|
1037
|
+
* * Definitions containing `>` within quoted attribute values (e.g., `["<span title='a>b'>"]`) are not supported
|
|
1038
|
+
*
|
|
1039
|
+
* Default: `[]`
|
|
1040
|
+
*
|
|
2640
1041
|
* @prop {boolean} [removeOptionalTags]
|
|
2641
1042
|
* Drop optional start/end tags where the HTML specification permits it
|
|
2642
1043
|
* (for example `</li>`, optional `<html>` etc.).
|
|
@@ -2645,7 +1046,7 @@ var htmlminifier = { minify, presets, getPreset, getPresetNames };
|
|
|
2645
1046
|
* Default: `false`
|
|
2646
1047
|
*
|
|
2647
1048
|
* @prop {boolean} [removeRedundantAttributes]
|
|
2648
|
-
* Remove attributes that are redundant because they match the element
|
|
1049
|
+
* Remove attributes that are redundant because they match the element’s
|
|
2649
1050
|
* default values (for example `<button type="submit">`).
|
|
2650
1051
|
* See also: https://perfectionkills.com/experimenting-with-html-minifier/#remove_redundant_attributes
|
|
2651
1052
|
*
|
|
@@ -2664,7 +1065,7 @@ var htmlminifier = { minify, presets, getPreset, getPresetNames };
|
|
|
2664
1065
|
* Default: `false`
|
|
2665
1066
|
*
|
|
2666
1067
|
* @prop {boolean} [removeTagWhitespace]
|
|
2667
|
-
* **Note that this will
|
|
1068
|
+
* **Note that this will result in invalid HTML!**
|
|
2668
1069
|
*
|
|
2669
1070
|
* When true, extra whitespace between tag name and attributes (or before
|
|
2670
1071
|
* the closing bracket) will be removed where possible. Affects output spacing
|
|
@@ -2702,6 +1103,1829 @@ var htmlminifier = { minify, presets, getPreset, getPresetNames };
|
|
|
2702
1103
|
* Default: `false`
|
|
2703
1104
|
*/
|
|
2704
1105
|
|
|
1106
|
+
// Hoisted, reusable RegExp patterns and tiny helpers to avoid repeated allocations in hot paths
|
|
1107
|
+
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
1108
|
+
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
1109
|
+
const RE_ALL_WS_NBSP = /[ \n\r\t\f\xA0]+/g;
|
|
1110
|
+
const RE_NBSP_LEADING_GROUP = /(^|\xA0+)[^\xA0]+/g;
|
|
1111
|
+
const RE_NBSP_LEAD_GROUP = /(\xA0+)[^\xA0]+/g;
|
|
1112
|
+
const RE_NBSP_TRAILING_GROUP = /[^\xA0]+(\xA0+)/g;
|
|
1113
|
+
const RE_NBSP_TRAILING_STRIP = /[^\xA0]+$/;
|
|
1114
|
+
const RE_CONDITIONAL_COMMENT = /^\[if\s[^\]]+]|\[endif]$/;
|
|
1115
|
+
const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
1116
|
+
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
1117
|
+
const RE_TRAILING_SEMICOLON = /;$/;
|
|
1118
|
+
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
1119
|
+
|
|
1120
|
+
// Tiny stable stringify for options signatures (sorted keys, shallow, nested objects)
|
|
1121
|
+
function stableStringify(obj) {
|
|
1122
|
+
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
1123
|
+
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
1124
|
+
const keys = Object.keys(obj).sort();
|
|
1125
|
+
let out = '{';
|
|
1126
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1127
|
+
const k = keys[i];
|
|
1128
|
+
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
1129
|
+
}
|
|
1130
|
+
return out + '}';
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Minimal LRU cache for strings and promises
|
|
1134
|
+
class LRU {
|
|
1135
|
+
constructor(limit = 200) {
|
|
1136
|
+
this.limit = limit;
|
|
1137
|
+
this.map = new Map();
|
|
1138
|
+
}
|
|
1139
|
+
get(key) {
|
|
1140
|
+
const v = this.map.get(key);
|
|
1141
|
+
if (v !== undefined) {
|
|
1142
|
+
this.map.delete(key);
|
|
1143
|
+
this.map.set(key, v);
|
|
1144
|
+
}
|
|
1145
|
+
return v;
|
|
1146
|
+
}
|
|
1147
|
+
set(key, value) {
|
|
1148
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
1149
|
+
this.map.set(key, value);
|
|
1150
|
+
if (this.map.size > this.limit) {
|
|
1151
|
+
const first = this.map.keys().next().value;
|
|
1152
|
+
this.map.delete(first);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
delete(key) { this.map.delete(key); }
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Per-process caches
|
|
1159
|
+
const jsMinifyCache = new LRU(200);
|
|
1160
|
+
const cssMinifyCache = new LRU(200);
|
|
1161
|
+
|
|
1162
|
+
const trimWhitespace = str => {
|
|
1163
|
+
if (!str) return str;
|
|
1164
|
+
// Fast path: if no whitespace at start or end, return early
|
|
1165
|
+
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
1166
|
+
return str;
|
|
1167
|
+
}
|
|
1168
|
+
return str.replace(RE_WS_START, '').replace(RE_WS_END, '');
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
function collapseWhitespaceAll(str) {
|
|
1172
|
+
if (!str) return str;
|
|
1173
|
+
// Fast path: if there are no common whitespace characters, return early
|
|
1174
|
+
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
1175
|
+
return str;
|
|
1176
|
+
}
|
|
1177
|
+
// Non-breaking space is specifically handled inside the replacer function here:
|
|
1178
|
+
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
1179
|
+
return spaces === '\t' ? '\t' : spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
1184
|
+
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
1185
|
+
|
|
1186
|
+
if (!str) return str;
|
|
1187
|
+
|
|
1188
|
+
if (options.preserveLineBreaks) {
|
|
1189
|
+
str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
|
|
1190
|
+
lineBreakBefore = '\n';
|
|
1191
|
+
return '';
|
|
1192
|
+
}).replace(/[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*$/, function () {
|
|
1193
|
+
lineBreakAfter = '\n';
|
|
1194
|
+
return '';
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (trimLeft) {
|
|
1199
|
+
// Non-breaking space is specifically handled inside the replacer function here:
|
|
1200
|
+
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
1201
|
+
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
1202
|
+
if (conservative && spaces === '\t') {
|
|
1203
|
+
return '\t';
|
|
1204
|
+
}
|
|
1205
|
+
return spaces.replace(/^[^\xA0]+/, '').replace(RE_NBSP_LEAD_GROUP, '$1 ') || (conservative ? ' ' : '');
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (trimRight) {
|
|
1210
|
+
// Non-breaking space is specifically handled inside the replacer function here:
|
|
1211
|
+
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
1212
|
+
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
1213
|
+
if (conservative && spaces === '\t') {
|
|
1214
|
+
return '\t';
|
|
1215
|
+
}
|
|
1216
|
+
return spaces.replace(RE_NBSP_TRAILING_GROUP, ' $1').replace(RE_NBSP_TRAILING_STRIP, '') || (conservative ? ' ' : '');
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (collapseAll) {
|
|
1221
|
+
// Strip non-space whitespace then compress spaces to one
|
|
1222
|
+
str = collapseWhitespaceAll(str);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return lineBreakBefore + str + lineBreakAfter;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Non-empty elements that will maintain whitespace around them
|
|
1229
|
+
const inlineElementsToKeepWhitespaceAround = new Set(['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr']);
|
|
1230
|
+
// Non-empty elements that will maintain whitespace within them
|
|
1231
|
+
const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
|
|
1232
|
+
// Elements that will always maintain whitespace around them
|
|
1233
|
+
const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
|
|
1234
|
+
|
|
1235
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
1236
|
+
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
1237
|
+
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
1238
|
+
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
1239
|
+
}
|
|
1240
|
+
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
1241
|
+
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
1242
|
+
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
1243
|
+
}
|
|
1244
|
+
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function isConditionalComment(text) {
|
|
1248
|
+
return RE_CONDITIONAL_COMMENT.test(text);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function isIgnoredComment(text, options) {
|
|
1252
|
+
for (let i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
|
|
1253
|
+
if (options.ignoreCustomComments[i].test(text)) {
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function isEventAttribute(attrName, options) {
|
|
1261
|
+
const patterns = options.customEventAttributes;
|
|
1262
|
+
if (patterns) {
|
|
1263
|
+
for (let i = patterns.length; i--;) {
|
|
1264
|
+
if (patterns[i].test(attrName)) {
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function canRemoveAttributeQuotes(value) {
|
|
1274
|
+
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
1275
|
+
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function attributesInclude(attributes, attribute) {
|
|
1279
|
+
for (let i = attributes.length; i--;) {
|
|
1280
|
+
if (attributes[i].name.toLowerCase() === attribute) {
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Default attribute values (could apply to any element)
|
|
1288
|
+
const generalDefaults = {
|
|
1289
|
+
autocorrect: 'on',
|
|
1290
|
+
fetchpriority: 'auto',
|
|
1291
|
+
loading: 'eager',
|
|
1292
|
+
popovertargetaction: 'toggle'
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
// Tag-specific default attribute values
|
|
1296
|
+
const tagDefaults = {
|
|
1297
|
+
area: { shape: 'rect' },
|
|
1298
|
+
button: { type: 'submit' },
|
|
1299
|
+
form: {
|
|
1300
|
+
enctype: 'application/x-www-form-urlencoded',
|
|
1301
|
+
method: 'get'
|
|
1302
|
+
},
|
|
1303
|
+
html: { dir: 'ltr' },
|
|
1304
|
+
img: { decoding: 'auto' },
|
|
1305
|
+
input: {
|
|
1306
|
+
colorspace: 'limited-srgb',
|
|
1307
|
+
type: 'text'
|
|
1308
|
+
},
|
|
1309
|
+
marquee: {
|
|
1310
|
+
behavior: 'scroll',
|
|
1311
|
+
direction: 'left'
|
|
1312
|
+
},
|
|
1313
|
+
style: { media: 'all' },
|
|
1314
|
+
textarea: { wrap: 'soft' },
|
|
1315
|
+
track: { kind: 'subtitles' }
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
1319
|
+
attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
|
1320
|
+
|
|
1321
|
+
// Legacy attributes
|
|
1322
|
+
if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
if (tag === 'script' && attrName === 'charset' && !attributesInclude(attrs, 'src')) {
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
if (tag === 'a' && attrName === 'name' && attributesInclude(attrs, 'id')) {
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Check general defaults
|
|
1333
|
+
if (generalDefaults[attrName] === attrValue) {
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Check tag-specific defaults
|
|
1338
|
+
return tagDefaults[tag]?.[attrName] === attrValue;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// https://mathiasbynens.be/demo/javascript-mime-type
|
|
1342
|
+
// https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
|
|
1343
|
+
const executableScriptsMimetypes = new Set([
|
|
1344
|
+
'text/javascript',
|
|
1345
|
+
'text/ecmascript',
|
|
1346
|
+
'text/jscript',
|
|
1347
|
+
'application/javascript',
|
|
1348
|
+
'application/x-javascript',
|
|
1349
|
+
'application/ecmascript',
|
|
1350
|
+
'module'
|
|
1351
|
+
]);
|
|
1352
|
+
|
|
1353
|
+
const keepScriptsMimetypes = new Set([
|
|
1354
|
+
'module'
|
|
1355
|
+
]);
|
|
1356
|
+
|
|
1357
|
+
function isScriptTypeAttribute(attrValue = '') {
|
|
1358
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
1359
|
+
return attrValue === '' || executableScriptsMimetypes.has(attrValue);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function keepScriptTypeAttribute(attrValue = '') {
|
|
1363
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
1364
|
+
return keepScriptsMimetypes.has(attrValue);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function isExecutableScript(tag, attrs) {
|
|
1368
|
+
if (tag !== 'script') {
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1372
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
1373
|
+
if (attrName === 'type') {
|
|
1374
|
+
return isScriptTypeAttribute(attrs[i].value);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function isStyleLinkTypeAttribute(attrValue = '') {
|
|
1381
|
+
attrValue = trimWhitespace(attrValue).toLowerCase();
|
|
1382
|
+
return attrValue === '' || attrValue === 'text/css';
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function isStyleSheet(tag, attrs) {
|
|
1386
|
+
if (tag !== 'style') {
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1390
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
1391
|
+
if (attrName === 'type') {
|
|
1392
|
+
return isStyleLinkTypeAttribute(attrs[i].value);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const isSimpleBoolean = new Set(['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']);
|
|
1399
|
+
const isBooleanValue = new Set(['true', 'false']);
|
|
1400
|
+
|
|
1401
|
+
function isBooleanAttribute(attrName, attrValue) {
|
|
1402
|
+
return isSimpleBoolean.has(attrName) || (attrName === 'draggable' && !isBooleanValue.has(attrValue));
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function isUriTypeAttribute(attrName, tag) {
|
|
1406
|
+
return (
|
|
1407
|
+
(/^(?:a|area|link|base)$/.test(tag) && attrName === 'href') ||
|
|
1408
|
+
(tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
|
|
1409
|
+
(tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
|
|
1410
|
+
(tag === 'q' && attrName === 'cite') ||
|
|
1411
|
+
(tag === 'blockquote' && attrName === 'cite') ||
|
|
1412
|
+
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
|
1413
|
+
(tag === 'form' && attrName === 'action') ||
|
|
1414
|
+
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
|
1415
|
+
(tag === 'head' && attrName === 'profile') ||
|
|
1416
|
+
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function isNumberTypeAttribute(attrName, tag) {
|
|
1421
|
+
return (
|
|
1422
|
+
(/^(?:a|area|object|button)$/.test(tag) && attrName === 'tabindex') ||
|
|
1423
|
+
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
|
1424
|
+
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
|
1425
|
+
(tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
|
|
1426
|
+
(tag === 'colgroup' && attrName === 'span') ||
|
|
1427
|
+
(tag === 'col' && attrName === 'span') ||
|
|
1428
|
+
((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function isLinkType(tag, attrs, value) {
|
|
1433
|
+
if (tag !== 'link') return false;
|
|
1434
|
+
const needle = String(value).toLowerCase();
|
|
1435
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
1436
|
+
if (attrs[i].name.toLowerCase() === 'rel') {
|
|
1437
|
+
const tokens = String(attrs[i].value).toLowerCase().split(/\s+/);
|
|
1438
|
+
if (tokens.includes(needle)) return true;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function isMediaQuery(tag, attrs, attrName) {
|
|
1445
|
+
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const srcsetTags = new Set(['img', 'source']);
|
|
1449
|
+
|
|
1450
|
+
function isSrcset(attrName, tag) {
|
|
1451
|
+
return attrName === 'srcset' && srcsetTags.has(tag);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
1455
|
+
if (isEventAttribute(attrName, options)) {
|
|
1456
|
+
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
1457
|
+
return options.minifyJS(attrValue, true);
|
|
1458
|
+
} else if (attrName === 'class') {
|
|
1459
|
+
attrValue = trimWhitespace(attrValue);
|
|
1460
|
+
if (options.sortClassName) {
|
|
1461
|
+
attrValue = options.sortClassName(attrValue);
|
|
1462
|
+
} else {
|
|
1463
|
+
attrValue = collapseWhitespaceAll(attrValue);
|
|
1464
|
+
}
|
|
1465
|
+
return attrValue;
|
|
1466
|
+
} else if (isUriTypeAttribute(attrName, tag)) {
|
|
1467
|
+
attrValue = trimWhitespace(attrValue);
|
|
1468
|
+
if (isLinkType(tag, attrs, 'canonical')) {
|
|
1469
|
+
return attrValue;
|
|
1470
|
+
}
|
|
1471
|
+
try {
|
|
1472
|
+
const out = await options.minifyURLs(attrValue);
|
|
1473
|
+
return typeof out === 'string' ? out : attrValue;
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
if (!options.continueOnMinifyError) {
|
|
1476
|
+
throw err;
|
|
1477
|
+
}
|
|
1478
|
+
options.log && options.log(err);
|
|
1479
|
+
return attrValue;
|
|
1480
|
+
}
|
|
1481
|
+
} else if (isNumberTypeAttribute(attrName, tag)) {
|
|
1482
|
+
return trimWhitespace(attrValue);
|
|
1483
|
+
} else if (attrName === 'style') {
|
|
1484
|
+
attrValue = trimWhitespace(attrValue);
|
|
1485
|
+
if (attrValue) {
|
|
1486
|
+
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
1487
|
+
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
1488
|
+
}
|
|
1489
|
+
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
1490
|
+
}
|
|
1491
|
+
return attrValue;
|
|
1492
|
+
} else if (isSrcset(attrName, tag)) {
|
|
1493
|
+
// https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
|
1494
|
+
attrValue = (await Promise.all(trimWhitespace(attrValue).split(/\s+,\s*|\s*,\s+/).map(async function (candidate) {
|
|
1495
|
+
let url = candidate;
|
|
1496
|
+
let descriptor = '';
|
|
1497
|
+
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
1498
|
+
if (match) {
|
|
1499
|
+
url = url.slice(0, -match[0].length);
|
|
1500
|
+
const num = +match[1].slice(0, -1);
|
|
1501
|
+
const suffix = match[1].slice(-1);
|
|
1502
|
+
if (num !== 1 || suffix !== 'x') {
|
|
1503
|
+
descriptor = ' ' + num + suffix;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
try {
|
|
1507
|
+
const out = await options.minifyURLs(url);
|
|
1508
|
+
return (typeof out === 'string' ? out : url) + descriptor;
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
if (!options.continueOnMinifyError) {
|
|
1511
|
+
throw err;
|
|
1512
|
+
}
|
|
1513
|
+
options.log && options.log(err);
|
|
1514
|
+
return url + descriptor;
|
|
1515
|
+
}
|
|
1516
|
+
}))).join(', ');
|
|
1517
|
+
} else if (isMetaViewport(tag, attrs) && attrName === 'content') {
|
|
1518
|
+
attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function (numString) {
|
|
1519
|
+
// “0.90000” → “0.9”
|
|
1520
|
+
// “1.0” → “1”
|
|
1521
|
+
// “1.0001” → “1.0001” (unchanged)
|
|
1522
|
+
return (+numString).toString();
|
|
1523
|
+
});
|
|
1524
|
+
} else if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
|
1525
|
+
return collapseWhitespaceAll(attrValue);
|
|
1526
|
+
} else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
|
1527
|
+
attrValue = trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
|
|
1528
|
+
} else if (tag === 'script' && attrName === 'type') {
|
|
1529
|
+
attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
|
|
1530
|
+
} else if (isMediaQuery(tag, attrs, attrName)) {
|
|
1531
|
+
attrValue = trimWhitespace(attrValue);
|
|
1532
|
+
return options.minifyCSS(attrValue, 'media');
|
|
1533
|
+
} else if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
1534
|
+
// Recursively minify HTML content within srcdoc attribute
|
|
1535
|
+
// Fast-path: skip if nothing would change
|
|
1536
|
+
if (!shouldMinifyInnerHTML(options)) {
|
|
1537
|
+
return attrValue;
|
|
1538
|
+
}
|
|
1539
|
+
return minifyHTMLSelf(attrValue, options, true);
|
|
1540
|
+
}
|
|
1541
|
+
return attrValue;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function isMetaViewport(tag, attrs) {
|
|
1545
|
+
if (tag !== 'meta') {
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1549
|
+
if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function isContentSecurityPolicy(tag, attrs) {
|
|
1556
|
+
if (tag !== 'meta') {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1560
|
+
if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') {
|
|
1561
|
+
return true;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Wrap CSS declarations for inline styles and media queries
|
|
1567
|
+
// This ensures proper context for CSS minification
|
|
1568
|
+
function wrapCSS(text, type) {
|
|
1569
|
+
switch (type) {
|
|
1570
|
+
case 'inline':
|
|
1571
|
+
return '*{' + text + '}';
|
|
1572
|
+
case 'media':
|
|
1573
|
+
return '@media ' + text + '{a{top:0}}';
|
|
1574
|
+
default:
|
|
1575
|
+
return text;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function unwrapCSS(text, type) {
|
|
1580
|
+
let matches;
|
|
1581
|
+
switch (type) {
|
|
1582
|
+
case 'inline':
|
|
1583
|
+
matches = text.match(/^\*\{([\s\S]*)\}$/);
|
|
1584
|
+
break;
|
|
1585
|
+
case 'media':
|
|
1586
|
+
matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
return matches ? matches[1] : text;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
async function cleanConditionalComment(comment, options) {
|
|
1593
|
+
return options.processConditionalComments
|
|
1594
|
+
? await replaceAsync(comment, /^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, async function (match, prefix, text, suffix) {
|
|
1595
|
+
return prefix + await minifyHTML(text, options, true) + suffix;
|
|
1596
|
+
})
|
|
1597
|
+
: comment;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const jsonScriptTypes = new Set([
|
|
1601
|
+
'application/json',
|
|
1602
|
+
'application/ld+json',
|
|
1603
|
+
'application/manifest+json',
|
|
1604
|
+
'application/vnd.geo+json',
|
|
1605
|
+
'importmap',
|
|
1606
|
+
'speculationrules',
|
|
1607
|
+
]);
|
|
1608
|
+
|
|
1609
|
+
function minifyJson(text, options) {
|
|
1610
|
+
try {
|
|
1611
|
+
return JSON.stringify(JSON.parse(text));
|
|
1612
|
+
}
|
|
1613
|
+
catch (err) {
|
|
1614
|
+
if (!options.continueOnMinifyError) {
|
|
1615
|
+
throw err;
|
|
1616
|
+
}
|
|
1617
|
+
options.log && options.log(err);
|
|
1618
|
+
return text;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function hasJsonScriptType(attrs) {
|
|
1623
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
1624
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
1625
|
+
if (attrName === 'type') {
|
|
1626
|
+
const attrValue = trimWhitespace((attrs[i].value || '').split(/;/, 2)[0]).toLowerCase();
|
|
1627
|
+
if (jsonScriptTypes.has(attrValue)) {
|
|
1628
|
+
return true;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
async function processScript(text, options, currentAttrs) {
|
|
1636
|
+
for (let i = 0, len = currentAttrs.length; i < len; i++) {
|
|
1637
|
+
const attrName = currentAttrs[i].name.toLowerCase();
|
|
1638
|
+
if (attrName === 'type') {
|
|
1639
|
+
const rawValue = currentAttrs[i].value;
|
|
1640
|
+
const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0]).toLowerCase();
|
|
1641
|
+
// Minify JSON script types automatically
|
|
1642
|
+
if (jsonScriptTypes.has(normalizedValue)) {
|
|
1643
|
+
return minifyJson(text, options);
|
|
1644
|
+
}
|
|
1645
|
+
// Process custom script types if specified
|
|
1646
|
+
if (options.processScripts && options.processScripts.indexOf(rawValue) > -1) {
|
|
1647
|
+
return await minifyHTML(text, options);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return text;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
|
|
1655
|
+
// - retain `<body>` if followed by `<noscript>`
|
|
1656
|
+
// - `<rb>`, `<rt>`, `<rtc>`, `<rp>` follow HTML Ruby Markup Extensions draft (https://www.w3.org/TR/html-ruby-extensions/)
|
|
1657
|
+
// - retain all tags which are adjacent to non-standard HTML tags
|
|
1658
|
+
const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
|
|
1659
|
+
const optionalEndTags = new Set(['html', 'head', 'body', 'li', 'dt', 'dd', 'p', 'rb', 'rt', 'rtc', 'rp', 'optgroup', 'option', 'colgroup', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']);
|
|
1660
|
+
const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
|
|
1661
|
+
const descriptionTags = new Set(['dt', 'dd']);
|
|
1662
|
+
const pBlockTags = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'search', 'section', 'table', 'ul']);
|
|
1663
|
+
const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
|
|
1664
|
+
const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
|
|
1665
|
+
const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
|
|
1666
|
+
const optionTag = new Set(['option', 'optgroup']);
|
|
1667
|
+
const tableContentTags = new Set(['tbody', 'tfoot']);
|
|
1668
|
+
const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
|
|
1669
|
+
const cellTags = new Set(['td', 'th']);
|
|
1670
|
+
const topLevelTags = new Set(['html', 'head', 'body']);
|
|
1671
|
+
const compactTags = new Set(['html', 'body']);
|
|
1672
|
+
const looseTags = new Set(['head', 'colgroup', 'caption']);
|
|
1673
|
+
const trailingTags = new Set(['dt', 'thead']);
|
|
1674
|
+
const htmlTags = new Set(['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', 'search', 'section', 'select', 'selectedcontent', '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']);
|
|
1675
|
+
|
|
1676
|
+
function canRemoveParentTag(optionalStartTag, tag) {
|
|
1677
|
+
switch (optionalStartTag) {
|
|
1678
|
+
case 'html':
|
|
1679
|
+
case 'head':
|
|
1680
|
+
return true;
|
|
1681
|
+
case 'body':
|
|
1682
|
+
return !headerTags.has(tag);
|
|
1683
|
+
case 'colgroup':
|
|
1684
|
+
return tag === 'col';
|
|
1685
|
+
case 'tbody':
|
|
1686
|
+
return tag === 'tr';
|
|
1687
|
+
}
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function isStartTagMandatory(optionalEndTag, tag) {
|
|
1692
|
+
switch (tag) {
|
|
1693
|
+
case 'colgroup':
|
|
1694
|
+
return optionalEndTag === 'colgroup';
|
|
1695
|
+
case 'tbody':
|
|
1696
|
+
return tableSectionTags.has(optionalEndTag);
|
|
1697
|
+
}
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
1702
|
+
switch (optionalEndTag) {
|
|
1703
|
+
case 'html':
|
|
1704
|
+
case 'head':
|
|
1705
|
+
case 'body':
|
|
1706
|
+
case 'colgroup':
|
|
1707
|
+
case 'caption':
|
|
1708
|
+
return true;
|
|
1709
|
+
case 'li':
|
|
1710
|
+
case 'optgroup':
|
|
1711
|
+
case 'tr':
|
|
1712
|
+
return tag === optionalEndTag;
|
|
1713
|
+
case 'dt':
|
|
1714
|
+
case 'dd':
|
|
1715
|
+
return descriptionTags.has(tag);
|
|
1716
|
+
case 'p':
|
|
1717
|
+
return pBlockTags.has(tag);
|
|
1718
|
+
case 'rb':
|
|
1719
|
+
case 'rt':
|
|
1720
|
+
case 'rp':
|
|
1721
|
+
return rubyEndTagOmission.has(tag);
|
|
1722
|
+
case 'rtc':
|
|
1723
|
+
return rubyRtcEndTagOmission.has(tag);
|
|
1724
|
+
case 'option':
|
|
1725
|
+
return optionTag.has(tag);
|
|
1726
|
+
case 'thead':
|
|
1727
|
+
case 'tbody':
|
|
1728
|
+
return tableContentTags.has(tag);
|
|
1729
|
+
case 'tfoot':
|
|
1730
|
+
return tag === 'tbody';
|
|
1731
|
+
case 'td':
|
|
1732
|
+
case 'th':
|
|
1733
|
+
return cellTags.has(tag);
|
|
1734
|
+
}
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const reEmptyAttribute = new RegExp(
|
|
1739
|
+
'^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
|
|
1740
|
+
'?:down|up|over|move|out)|key(?:press|down|up)))$');
|
|
1741
|
+
|
|
1742
|
+
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
1743
|
+
const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
|
|
1744
|
+
if (!isValueEmpty) {
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
if (typeof options.removeEmptyAttributes === 'function') {
|
|
1748
|
+
return options.removeEmptyAttributes(attrName, tag);
|
|
1749
|
+
}
|
|
1750
|
+
return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function hasAttrName(name, attrs) {
|
|
1754
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
1755
|
+
if (attrs[i].name === name) {
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function canRemoveElement(tag, attrs) {
|
|
1763
|
+
switch (tag) {
|
|
1764
|
+
case 'textarea':
|
|
1765
|
+
return false;
|
|
1766
|
+
case 'audio':
|
|
1767
|
+
case 'script':
|
|
1768
|
+
case 'video':
|
|
1769
|
+
if (hasAttrName('src', attrs)) {
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
break;
|
|
1773
|
+
case 'iframe':
|
|
1774
|
+
if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
|
|
1775
|
+
return false;
|
|
1776
|
+
}
|
|
1777
|
+
break;
|
|
1778
|
+
case 'object':
|
|
1779
|
+
if (hasAttrName('data', attrs)) {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
break;
|
|
1783
|
+
case 'applet':
|
|
1784
|
+
if (hasAttrName('code', attrs)) {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* @param {string} str - Tag name or HTML-like element spec (e.g., “td” or “<span aria-hidden='true'>”)
|
|
1794
|
+
* @param {MinifierOptions} options - Options object for name normalization
|
|
1795
|
+
* @returns {{tag: string, attrs: Object.<string, string|undefined>|null}|null} Parsed spec or null if invalid
|
|
1796
|
+
*/
|
|
1797
|
+
function parseElementSpec(str, options) {
|
|
1798
|
+
if (typeof str !== 'string') {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const trimmed = str.trim();
|
|
1803
|
+
if (!trimmed) {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// Simple tag name: “td”
|
|
1808
|
+
if (!/[<>]/.test(trimmed)) {
|
|
1809
|
+
return { tag: options.name(trimmed), attrs: null };
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// HTML-like markup: “<span aria-hidden='true'>” or “<td></td>”
|
|
1813
|
+
// Extract opening tag using regex
|
|
1814
|
+
const match = trimmed.match(/^<([a-zA-Z][\w:-]*)((?:\s+[^>]*)?)>/);
|
|
1815
|
+
if (!match) {
|
|
1816
|
+
return null;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
const tag = options.name(match[1]);
|
|
1820
|
+
const attrString = match[2];
|
|
1821
|
+
|
|
1822
|
+
if (!attrString.trim()) {
|
|
1823
|
+
return { tag, attrs: null };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Parse attributes from string
|
|
1827
|
+
const attrs = {};
|
|
1828
|
+
const attrRegex = /([a-zA-Z][\w:-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>/]+)))?/g;
|
|
1829
|
+
let attrMatch;
|
|
1830
|
+
|
|
1831
|
+
while ((attrMatch = attrRegex.exec(attrString))) {
|
|
1832
|
+
const attrName = options.name(attrMatch[1]);
|
|
1833
|
+
const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
|
|
1834
|
+
// Boolean attributes have no value (undefined)
|
|
1835
|
+
attrs[attrName] = attrValue;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
return {
|
|
1839
|
+
tag,
|
|
1840
|
+
attrs: Object.keys(attrs).length > 0 ? attrs : null
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/**
|
|
1845
|
+
* @param {string[]} input - Array of element specifications from `removeEmptyElementsExcept` option
|
|
1846
|
+
* @param {MinifierOptions} options - Options object for parsing
|
|
1847
|
+
* @returns {Array<{tag: string, attrs: Object.<string, string|undefined>|null}>} Array of parsed element specs
|
|
1848
|
+
*/
|
|
1849
|
+
function parseRemoveEmptyElementsExcept(input, options) {
|
|
1850
|
+
if (!Array.isArray(input)) {
|
|
1851
|
+
return [];
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
return input.map(item => {
|
|
1855
|
+
if (typeof item === 'string') {
|
|
1856
|
+
const spec = parseElementSpec(item, options);
|
|
1857
|
+
if (!spec && options.log) {
|
|
1858
|
+
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: "' + item + '"');
|
|
1859
|
+
}
|
|
1860
|
+
return spec;
|
|
1861
|
+
}
|
|
1862
|
+
if (options.log) {
|
|
1863
|
+
options.log('Warning: “removeEmptyElementsExcept” specification must be a string, received: ' + typeof item);
|
|
1864
|
+
}
|
|
1865
|
+
return null;
|
|
1866
|
+
}).filter(Boolean);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* @param {string} tag - Element tag name
|
|
1871
|
+
* @param {HTMLAttribute[]} attrs - Array of element attributes
|
|
1872
|
+
* @param {Array<{tag: string, attrs: Object.<string, string|undefined>|null}>} preserveList - Parsed preserve specs
|
|
1873
|
+
* @returns {boolean} True if the empty element should be preserved
|
|
1874
|
+
*/
|
|
1875
|
+
function shouldPreserveEmptyElement(tag, attrs, preserveList) {
|
|
1876
|
+
for (const spec of preserveList) {
|
|
1877
|
+
// Tag name must match
|
|
1878
|
+
if (spec.tag !== tag) {
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// If no attributes specified in spec, tag match is enough
|
|
1883
|
+
if (!spec.attrs) {
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Check if all specified attributes match
|
|
1888
|
+
const allAttrsMatch = Object.entries(spec.attrs).every(([name, value]) => {
|
|
1889
|
+
const attr = attrs.find(a => a.name === name);
|
|
1890
|
+
if (!attr) {
|
|
1891
|
+
return false; // Attribute not present
|
|
1892
|
+
}
|
|
1893
|
+
// Boolean attribute in spec (undefined value) matches if attribute is present
|
|
1894
|
+
if (value === undefined) {
|
|
1895
|
+
return true;
|
|
1896
|
+
}
|
|
1897
|
+
// Valued attribute must match exactly
|
|
1898
|
+
return attr.value === value;
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
if (allAttrsMatch) {
|
|
1902
|
+
return true;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function canCollapseWhitespace(tag) {
|
|
1910
|
+
return !/^(?:script|style|pre|textarea)$/.test(tag);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function canTrimWhitespace(tag) {
|
|
1914
|
+
return !/^(?:pre|textarea)$/.test(tag);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
async function normalizeAttr(attr, attrs, tag, options) {
|
|
1918
|
+
const attrName = options.name(attr.name);
|
|
1919
|
+
let attrValue = attr.value;
|
|
1920
|
+
|
|
1921
|
+
if (options.decodeEntities && attrValue) {
|
|
1922
|
+
// Fast path: only decode when entities are present
|
|
1923
|
+
if (attrValue.indexOf('&') !== -1) {
|
|
1924
|
+
attrValue = entities.decodeHTMLStrict(attrValue);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if ((options.removeRedundantAttributes &&
|
|
1929
|
+
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
1930
|
+
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
1931
|
+
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
1932
|
+
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
1933
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (attrValue) {
|
|
1938
|
+
attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (options.removeEmptyAttributes &&
|
|
1942
|
+
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
1947
|
+
attrValue = attrValue.replace(RE_AMP_ENTITY, '&$1');
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
return {
|
|
1951
|
+
attr,
|
|
1952
|
+
name: attrName,
|
|
1953
|
+
value: attrValue
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
1958
|
+
const attrName = normalized.name;
|
|
1959
|
+
let attrValue = normalized.value;
|
|
1960
|
+
const attr = normalized.attr;
|
|
1961
|
+
let attrQuote = attr.quote;
|
|
1962
|
+
let attrFragment;
|
|
1963
|
+
let emittedAttrValue;
|
|
1964
|
+
|
|
1965
|
+
if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
|
|
1966
|
+
~attrValue.indexOf(uidAttr) || !canRemoveAttributeQuotes(attrValue))) {
|
|
1967
|
+
if (!options.preventAttributesEscaping) {
|
|
1968
|
+
if (typeof options.quoteCharacter === 'undefined') {
|
|
1969
|
+
// Count quotes in a single pass instead of two regex operations
|
|
1970
|
+
let apos = 0, quot = 0;
|
|
1971
|
+
for (let i = 0; i < attrValue.length; i++) {
|
|
1972
|
+
if (attrValue[i] === "'") apos++;
|
|
1973
|
+
else if (attrValue[i] === '"') quot++;
|
|
1974
|
+
}
|
|
1975
|
+
attrQuote = apos < quot ? '\'' : '"';
|
|
1976
|
+
} else {
|
|
1977
|
+
attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
|
1978
|
+
}
|
|
1979
|
+
if (attrQuote === '"') {
|
|
1980
|
+
attrValue = attrValue.replace(/"/g, '"');
|
|
1981
|
+
} else {
|
|
1982
|
+
attrValue = attrValue.replace(/'/g, ''');
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
1986
|
+
if (!isLast && !options.removeTagWhitespace) {
|
|
1987
|
+
emittedAttrValue += ' ';
|
|
1988
|
+
}
|
|
1989
|
+
} else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
|
|
1990
|
+
// Make sure trailing slash is not interpreted as HTML self-closing tag
|
|
1991
|
+
emittedAttrValue = attrValue;
|
|
1992
|
+
} else {
|
|
1993
|
+
emittedAttrValue = attrValue + ' ';
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
|
|
1997
|
+
isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase()))) {
|
|
1998
|
+
attrFragment = attrName;
|
|
1999
|
+
if (!isLast) {
|
|
2000
|
+
attrFragment += ' ';
|
|
2001
|
+
}
|
|
2002
|
+
} else {
|
|
2003
|
+
attrFragment = attrName + attr.customAssign + emittedAttrValue;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
return attr.customOpen + attrFragment + attr.customClose;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function identity(value) {
|
|
2010
|
+
return value;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function identityAsync(value) {
|
|
2014
|
+
return Promise.resolve(value);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
function shouldMinifyInnerHTML(options) {
|
|
2018
|
+
return Boolean(
|
|
2019
|
+
options.collapseWhitespace ||
|
|
2020
|
+
options.removeComments ||
|
|
2021
|
+
options.removeOptionalTags ||
|
|
2022
|
+
options.minifyJS !== identity ||
|
|
2023
|
+
options.minifyCSS !== identityAsync ||
|
|
2024
|
+
options.minifyURLs !== identity
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* @param {Partial<MinifierOptions>} inputOptions - User-provided options
|
|
2030
|
+
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
2031
|
+
*/
|
|
2032
|
+
const processOptions = (inputOptions) => {
|
|
2033
|
+
const options = {
|
|
2034
|
+
name: function (name) {
|
|
2035
|
+
return name.toLowerCase();
|
|
2036
|
+
},
|
|
2037
|
+
canCollapseWhitespace,
|
|
2038
|
+
canTrimWhitespace,
|
|
2039
|
+
continueOnMinifyError: true,
|
|
2040
|
+
html5: true,
|
|
2041
|
+
ignoreCustomComments: [
|
|
2042
|
+
/^!/,
|
|
2043
|
+
/^\s*#/
|
|
2044
|
+
],
|
|
2045
|
+
ignoreCustomFragments: [
|
|
2046
|
+
/<%[\s\S]*?%>/,
|
|
2047
|
+
/<\?[\s\S]*?\?>/
|
|
2048
|
+
],
|
|
2049
|
+
includeAutoGeneratedTags: true,
|
|
2050
|
+
log: identity,
|
|
2051
|
+
minifyCSS: identityAsync,
|
|
2052
|
+
minifyJS: identity,
|
|
2053
|
+
minifyURLs: identity
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
Object.keys(inputOptions).forEach(function (key) {
|
|
2057
|
+
const option = inputOptions[key];
|
|
2058
|
+
|
|
2059
|
+
if (key === 'caseSensitive') {
|
|
2060
|
+
if (option) {
|
|
2061
|
+
options.name = identity;
|
|
2062
|
+
}
|
|
2063
|
+
} else if (key === 'log') {
|
|
2064
|
+
if (typeof option === 'function') {
|
|
2065
|
+
options.log = option;
|
|
2066
|
+
}
|
|
2067
|
+
} else if (key === 'minifyCSS' && typeof option !== 'function') {
|
|
2068
|
+
if (!option) {
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
2073
|
+
|
|
2074
|
+
options.minifyCSS = async function (text, type) {
|
|
2075
|
+
// Fast path: nothing to minify
|
|
2076
|
+
if (!text || !text.trim()) {
|
|
2077
|
+
return text;
|
|
2078
|
+
}
|
|
2079
|
+
text = await replaceAsync(
|
|
2080
|
+
text,
|
|
2081
|
+
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
2082
|
+
async function (match, prefix, dq, sq, unq, suffix) {
|
|
2083
|
+
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
2084
|
+
const url = dq ?? sq ?? unq ?? '';
|
|
2085
|
+
try {
|
|
2086
|
+
const out = await options.minifyURLs(url);
|
|
2087
|
+
return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
if (!options.continueOnMinifyError) {
|
|
2090
|
+
throw err;
|
|
2091
|
+
}
|
|
2092
|
+
options.log && options.log(err);
|
|
2093
|
+
return match;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
);
|
|
2097
|
+
// Cache key: wrapped content, type, options signature
|
|
2098
|
+
const inputCSS = wrapCSS(text, type);
|
|
2099
|
+
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
2100
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
2101
|
+
const cssKey = inputCSS.length > 2048
|
|
2102
|
+
? (inputCSS.length + '|' + inputCSS.slice(0, 50) + inputCSS.slice(-50) + '|' + type + '|' + cssSig)
|
|
2103
|
+
: (inputCSS + '|' + type + '|' + cssSig);
|
|
2104
|
+
|
|
2105
|
+
try {
|
|
2106
|
+
const cached = cssMinifyCache.get(cssKey);
|
|
2107
|
+
if (cached) {
|
|
2108
|
+
return cached;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
const transformCSS = await getLightningCSS();
|
|
2112
|
+
const result = transformCSS({
|
|
2113
|
+
filename: 'input.css',
|
|
2114
|
+
code: Buffer.from(inputCSS),
|
|
2115
|
+
minify: true,
|
|
2116
|
+
errorRecovery: !!options.continueOnMinifyError,
|
|
2117
|
+
...lightningCssOptions
|
|
2118
|
+
});
|
|
2119
|
+
|
|
2120
|
+
const outputCSS = unwrapCSS(result.code.toString(), type);
|
|
2121
|
+
|
|
2122
|
+
// If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
|
|
2123
|
+
// This preserves:
|
|
2124
|
+
// 1. Template code like `<?php ?>`, `<%= %>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
|
|
2125
|
+
// 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
|
|
2126
|
+
// CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
|
|
2127
|
+
const isCDATA = text.includes('<![CDATA[');
|
|
2128
|
+
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
2129
|
+
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
2130
|
+
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
2131
|
+
|
|
2132
|
+
// Preserve if output is empty and input had template syntax or UIDs
|
|
2133
|
+
// This catches cases where Lightning CSS removed content that should be preserved
|
|
2134
|
+
const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
2135
|
+
|
|
2136
|
+
cssMinifyCache.set(cssKey, finalOutput);
|
|
2137
|
+
return finalOutput;
|
|
2138
|
+
} catch (err) {
|
|
2139
|
+
cssMinifyCache.delete(cssKey);
|
|
2140
|
+
if (!options.continueOnMinifyError) {
|
|
2141
|
+
throw err;
|
|
2142
|
+
}
|
|
2143
|
+
options.log && options.log(err);
|
|
2144
|
+
return text;
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
} else if (key === 'minifyJS' && typeof option !== 'function') {
|
|
2148
|
+
if (!option) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const terserOptions = typeof option === 'object' ? option : {};
|
|
2153
|
+
|
|
2154
|
+
terserOptions.parse = {
|
|
2155
|
+
...terserOptions.parse,
|
|
2156
|
+
bare_returns: false
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
options.minifyJS = async function (text, inline) {
|
|
2160
|
+
const start = text.match(/^\s*<!--.*/);
|
|
2161
|
+
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
2162
|
+
|
|
2163
|
+
terserOptions.parse.bare_returns = inline;
|
|
2164
|
+
|
|
2165
|
+
let jsKey;
|
|
2166
|
+
try {
|
|
2167
|
+
// Fast path: avoid invoking Terser for empty/whitespace-only content
|
|
2168
|
+
if (!code || !code.trim()) {
|
|
2169
|
+
return '';
|
|
2170
|
+
}
|
|
2171
|
+
// Cache key: content, inline, options signature (subset)
|
|
2172
|
+
const terserSig = stableStringify({
|
|
2173
|
+
compress: terserOptions.compress,
|
|
2174
|
+
mangle: terserOptions.mangle,
|
|
2175
|
+
ecma: terserOptions.ecma,
|
|
2176
|
+
toplevel: terserOptions.toplevel,
|
|
2177
|
+
module: terserOptions.module,
|
|
2178
|
+
keep_fnames: terserOptions.keep_fnames,
|
|
2179
|
+
format: terserOptions.format,
|
|
2180
|
+
cont: !!options.continueOnMinifyError,
|
|
2181
|
+
});
|
|
2182
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
2183
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
2184
|
+
const cached = jsMinifyCache.get(jsKey);
|
|
2185
|
+
if (cached) {
|
|
2186
|
+
return await cached;
|
|
2187
|
+
}
|
|
2188
|
+
const inFlight = (async () => {
|
|
2189
|
+
const terser = await getTerser();
|
|
2190
|
+
const result = await terser(code, terserOptions);
|
|
2191
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
2192
|
+
})();
|
|
2193
|
+
jsMinifyCache.set(jsKey, inFlight);
|
|
2194
|
+
const resolved = await inFlight;
|
|
2195
|
+
jsMinifyCache.set(jsKey, resolved);
|
|
2196
|
+
return resolved;
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
if (jsKey) jsMinifyCache.delete(jsKey);
|
|
2199
|
+
if (!options.continueOnMinifyError) {
|
|
2200
|
+
throw err;
|
|
2201
|
+
}
|
|
2202
|
+
options.log && options.log(err);
|
|
2203
|
+
return text;
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
} else if (key === 'minifyURLs' && typeof option !== 'function') {
|
|
2207
|
+
if (!option) {
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
let relateUrlOptions = option;
|
|
2212
|
+
|
|
2213
|
+
if (typeof option === 'string') {
|
|
2214
|
+
relateUrlOptions = { site: option };
|
|
2215
|
+
} else if (typeof option !== 'object') {
|
|
2216
|
+
relateUrlOptions = {};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
options.minifyURLs = function (text) {
|
|
2220
|
+
try {
|
|
2221
|
+
return RelateURL.relate(text, relateUrlOptions);
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
if (!options.continueOnMinifyError) {
|
|
2224
|
+
throw err;
|
|
2225
|
+
}
|
|
2226
|
+
options.log && options.log(err);
|
|
2227
|
+
return text;
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
} else {
|
|
2231
|
+
options[key] = option;
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2234
|
+
return options;
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
function uniqueId(value) {
|
|
2238
|
+
let id;
|
|
2239
|
+
do {
|
|
2240
|
+
id = Math.random().toString(36).replace(/^0\.[0-9]*/, '');
|
|
2241
|
+
} while (~value.indexOf(id));
|
|
2242
|
+
return id;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
const specialContentTags = new Set(['script', 'style']);
|
|
2246
|
+
|
|
2247
|
+
async function createSortFns(value, options, uidIgnore, uidAttr) {
|
|
2248
|
+
const attrChains = options.sortAttributes && Object.create(null);
|
|
2249
|
+
const classChain = options.sortClassName && new TokenChain();
|
|
2250
|
+
|
|
2251
|
+
function attrNames(attrs) {
|
|
2252
|
+
return attrs.map(function (attr) {
|
|
2253
|
+
return options.name(attr.name);
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
function shouldSkipUID(token, uid) {
|
|
2258
|
+
return !uid || token.indexOf(uid) === -1;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
function shouldSkipUIDs(token) {
|
|
2262
|
+
return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
async function scan(input) {
|
|
2266
|
+
let currentTag, currentType;
|
|
2267
|
+
const parser = new HTMLParser(input, {
|
|
2268
|
+
start: function (tag, attrs) {
|
|
2269
|
+
if (attrChains) {
|
|
2270
|
+
if (!attrChains[tag]) {
|
|
2271
|
+
attrChains[tag] = new TokenChain();
|
|
2272
|
+
}
|
|
2273
|
+
attrChains[tag].add(attrNames(attrs).filter(shouldSkipUIDs));
|
|
2274
|
+
}
|
|
2275
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
2276
|
+
const attr = attrs[i];
|
|
2277
|
+
if (classChain && attr.value && options.name(attr.name) === 'class') {
|
|
2278
|
+
classChain.add(trimWhitespace(attr.value).split(/[ \t\n\f\r]+/).filter(shouldSkipUIDs));
|
|
2279
|
+
} else if (options.processScripts && attr.name.toLowerCase() === 'type') {
|
|
2280
|
+
currentTag = tag;
|
|
2281
|
+
currentType = attr.value;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
},
|
|
2285
|
+
end: function () {
|
|
2286
|
+
currentTag = '';
|
|
2287
|
+
},
|
|
2288
|
+
chars: async function (text) {
|
|
2289
|
+
// Only recursively scan HTML content, not JSON-LD or other non-HTML script types
|
|
2290
|
+
// `scan()` is for analyzing HTML attribute order, not for parsing JSON
|
|
2291
|
+
if (options.processScripts && specialContentTags.has(currentTag) &&
|
|
2292
|
+
options.processScripts.indexOf(currentType) > -1 &&
|
|
2293
|
+
currentType === 'text/html') {
|
|
2294
|
+
await scan(text);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2299
|
+
await parser.parse();
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
const log = options.log;
|
|
2303
|
+
options.log = identity;
|
|
2304
|
+
options.sortAttributes = false;
|
|
2305
|
+
options.sortClassName = false;
|
|
2306
|
+
const firstPassOutput = await minifyHTML(value, options);
|
|
2307
|
+
await scan(firstPassOutput);
|
|
2308
|
+
options.log = log;
|
|
2309
|
+
if (attrChains) {
|
|
2310
|
+
const attrSorters = Object.create(null);
|
|
2311
|
+
for (const tag in attrChains) {
|
|
2312
|
+
attrSorters[tag] = attrChains[tag].createSorter();
|
|
2313
|
+
}
|
|
2314
|
+
options.sortAttributes = function (tag, attrs) {
|
|
2315
|
+
const sorter = attrSorters[tag];
|
|
2316
|
+
if (sorter) {
|
|
2317
|
+
const attrMap = Object.create(null);
|
|
2318
|
+
const names = attrNames(attrs);
|
|
2319
|
+
names.forEach(function (name, index) {
|
|
2320
|
+
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
2321
|
+
});
|
|
2322
|
+
sorter.sort(names).forEach(function (name, index) {
|
|
2323
|
+
attrs[index] = attrMap[name].shift();
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
if (classChain) {
|
|
2329
|
+
const sorter = classChain.createSorter();
|
|
2330
|
+
options.sortClassName = function (value) {
|
|
2331
|
+
return sorter.sort(value.split(/[ \n\f\r]+/)).join(' ');
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
/**
|
|
2337
|
+
* @param {string} value - HTML content to minify
|
|
2338
|
+
* @param {MinifierOptions} options - Normalized minification options
|
|
2339
|
+
* @param {boolean} [partialMarkup] - Whether treating input as partial markup
|
|
2340
|
+
* @returns {Promise<string>} Minified HTML
|
|
2341
|
+
*/
|
|
2342
|
+
async function minifyHTML(value, options, partialMarkup) {
|
|
2343
|
+
// Check input length limitation to prevent ReDoS attacks
|
|
2344
|
+
if (options.maxInputLength && value.length > options.maxInputLength) {
|
|
2345
|
+
throw new Error(`Input length (${value.length}) exceeds maximum allowed length (${options.maxInputLength})`);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
if (options.collapseWhitespace) {
|
|
2349
|
+
value = collapseWhitespace(value, options, true, true);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
const buffer = [];
|
|
2353
|
+
let charsPrevTag;
|
|
2354
|
+
let currentChars = '';
|
|
2355
|
+
let hasChars;
|
|
2356
|
+
let currentTag = '';
|
|
2357
|
+
let currentAttrs = [];
|
|
2358
|
+
const stackNoTrimWhitespace = [];
|
|
2359
|
+
const stackNoCollapseWhitespace = [];
|
|
2360
|
+
let optionalStartTag = '';
|
|
2361
|
+
let optionalEndTag = '';
|
|
2362
|
+
const ignoredMarkupChunks = [];
|
|
2363
|
+
const ignoredCustomMarkupChunks = [];
|
|
2364
|
+
let uidIgnore;
|
|
2365
|
+
let uidAttr;
|
|
2366
|
+
let uidPattern;
|
|
2367
|
+
// Create inline tags/text sets with custom elements
|
|
2368
|
+
const customElementsInput = options.inlineCustomElements ?? [];
|
|
2369
|
+
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
2370
|
+
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
2371
|
+
// Fast path: reuse base Sets if no custom elements
|
|
2372
|
+
const inlineTextSet = normalizedCustomElements.length
|
|
2373
|
+
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
2374
|
+
: inlineElementsToKeepWhitespaceWithin;
|
|
2375
|
+
const inlineElements = normalizedCustomElements.length
|
|
2376
|
+
? new Set([...inlineElementsToKeepWhitespaceAround, ...normalizedCustomElements])
|
|
2377
|
+
: inlineElementsToKeepWhitespaceAround;
|
|
2378
|
+
|
|
2379
|
+
// Parse `removeEmptyElementsExcept` option
|
|
2380
|
+
let removeEmptyElementsExcept;
|
|
2381
|
+
if (options.removeEmptyElementsExcept && !Array.isArray(options.removeEmptyElementsExcept)) {
|
|
2382
|
+
if (options.log) {
|
|
2383
|
+
options.log('Warning: “removeEmptyElementsExcept” option must be an array, received: ' + typeof options.removeEmptyElementsExcept);
|
|
2384
|
+
}
|
|
2385
|
+
removeEmptyElementsExcept = [];
|
|
2386
|
+
} else {
|
|
2387
|
+
removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept, options) || [];
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// Temporarily replace ignored chunks with comments,
|
|
2391
|
+
// so that we don’t have to worry what’s there.
|
|
2392
|
+
// For all we care there might be
|
|
2393
|
+
// completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
|
|
2394
|
+
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
2395
|
+
if (!uidIgnore) {
|
|
2396
|
+
uidIgnore = uniqueId(value);
|
|
2397
|
+
const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
|
2398
|
+
if (options.ignoreCustomComments) {
|
|
2399
|
+
options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
|
2400
|
+
} else {
|
|
2401
|
+
options.ignoreCustomComments = [];
|
|
2402
|
+
}
|
|
2403
|
+
options.ignoreCustomComments.push(pattern);
|
|
2404
|
+
}
|
|
2405
|
+
const token = '<!--' + uidIgnore + ignoredMarkupChunks.length + '-->';
|
|
2406
|
+
ignoredMarkupChunks.push(group1);
|
|
2407
|
+
return token;
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
const customFragments = options.ignoreCustomFragments.map(function (re) {
|
|
2411
|
+
return re.source;
|
|
2412
|
+
});
|
|
2413
|
+
if (customFragments.length) {
|
|
2414
|
+
// Warn about potential ReDoS if custom fragments use unlimited quantifiers
|
|
2415
|
+
for (let i = 0; i < customFragments.length; i++) {
|
|
2416
|
+
if (/[*+]/.test(customFragments[i])) {
|
|
2417
|
+
options.log('Warning: Custom fragment contains unlimited quantifiers (“*” or “+”) which may cause ReDoS vulnerability');
|
|
2418
|
+
break;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// Safe approach: Use bounded quantifiers instead of unlimited ones to prevent ReDoS
|
|
2423
|
+
const maxQuantifier = options.customFragmentQuantifierLimit || 200;
|
|
2424
|
+
const whitespacePattern = `\\s{0,${maxQuantifier}}`;
|
|
2425
|
+
|
|
2426
|
+
// Use bounded quantifiers to prevent ReDoS—this approach prevents exponential backtracking
|
|
2427
|
+
const reCustomIgnore = new RegExp(
|
|
2428
|
+
whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
|
|
2429
|
+
'g'
|
|
2430
|
+
);
|
|
2431
|
+
// Temporarily replace custom ignored fragments with unique attributes
|
|
2432
|
+
value = value.replace(reCustomIgnore, function (match) {
|
|
2433
|
+
if (!uidAttr) {
|
|
2434
|
+
uidAttr = uniqueId(value);
|
|
2435
|
+
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
2436
|
+
|
|
2437
|
+
if (options.minifyCSS) {
|
|
2438
|
+
options.minifyCSS = (function (fn) {
|
|
2439
|
+
return function (text, type) {
|
|
2440
|
+
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
2441
|
+
const chunks = ignoredCustomMarkupChunks[+index];
|
|
2442
|
+
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
2443
|
+
});
|
|
2444
|
+
|
|
2445
|
+
return fn(text, type);
|
|
2446
|
+
};
|
|
2447
|
+
})(options.minifyCSS);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
if (options.minifyJS) {
|
|
2451
|
+
options.minifyJS = (function (fn) {
|
|
2452
|
+
return function (text, type) {
|
|
2453
|
+
return fn(text.replace(uidPattern, function (match, prefix, index) {
|
|
2454
|
+
const chunks = ignoredCustomMarkupChunks[+index];
|
|
2455
|
+
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
2456
|
+
}), type);
|
|
2457
|
+
};
|
|
2458
|
+
})(options.minifyJS);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
|
|
2463
|
+
ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
|
|
2464
|
+
return '\t' + token + '\t';
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
2469
|
+
(options.sortClassName && typeof options.sortClassName !== 'function')) {
|
|
2470
|
+
await createSortFns(value, options, uidIgnore, uidAttr);
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function _canCollapseWhitespace(tag, attrs) {
|
|
2474
|
+
return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
function _canTrimWhitespace(tag, attrs) {
|
|
2478
|
+
return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function removeStartTag() {
|
|
2482
|
+
let index = buffer.length - 1;
|
|
2483
|
+
while (index > 0 && !/^<[^/!]/.test(buffer[index])) {
|
|
2484
|
+
index--;
|
|
2485
|
+
}
|
|
2486
|
+
buffer.length = Math.max(0, index);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
function removeEndTag() {
|
|
2490
|
+
let index = buffer.length - 1;
|
|
2491
|
+
while (index > 0 && !/^<\//.test(buffer[index])) {
|
|
2492
|
+
index--;
|
|
2493
|
+
}
|
|
2494
|
+
buffer.length = Math.max(0, index);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// Look for trailing whitespaces, bypass any inline tags
|
|
2498
|
+
function trimTrailingWhitespace(index, nextTag) {
|
|
2499
|
+
for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
|
|
2500
|
+
const str = buffer[index];
|
|
2501
|
+
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
2502
|
+
if (match) {
|
|
2503
|
+
endTag = match[1];
|
|
2504
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
// Look for trailing whitespaces from previously processed text
|
|
2511
|
+
// which may not be trimmed due to a following comment or an empty
|
|
2512
|
+
// element which has now been removed
|
|
2513
|
+
function squashTrailingWhitespace(nextTag) {
|
|
2514
|
+
let charsIndex = buffer.length - 1;
|
|
2515
|
+
if (buffer.length > 1) {
|
|
2516
|
+
const item = buffer[buffer.length - 1];
|
|
2517
|
+
if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
|
|
2518
|
+
charsIndex--;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
trimTrailingWhitespace(charsIndex, nextTag);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
const parser = new HTMLParser(value, {
|
|
2525
|
+
partialMarkup: partialMarkup ?? options.partialMarkup,
|
|
2526
|
+
continueOnParseError: options.continueOnParseError,
|
|
2527
|
+
customAttrAssign: options.customAttrAssign,
|
|
2528
|
+
customAttrSurround: options.customAttrSurround,
|
|
2529
|
+
html5: options.html5,
|
|
2530
|
+
|
|
2531
|
+
start: async function (tag, attrs, unary, unarySlash, autoGenerated) {
|
|
2532
|
+
if (tag.toLowerCase() === 'svg') {
|
|
2533
|
+
options = Object.create(options);
|
|
2534
|
+
options.caseSensitive = true;
|
|
2535
|
+
options.keepClosingSlash = true;
|
|
2536
|
+
options.name = identity;
|
|
2537
|
+
}
|
|
2538
|
+
tag = options.name(tag);
|
|
2539
|
+
currentTag = tag;
|
|
2540
|
+
charsPrevTag = tag;
|
|
2541
|
+
if (!inlineTextSet.has(tag)) {
|
|
2542
|
+
currentChars = '';
|
|
2543
|
+
}
|
|
2544
|
+
hasChars = false;
|
|
2545
|
+
currentAttrs = attrs;
|
|
2546
|
+
|
|
2547
|
+
let optional = options.removeOptionalTags;
|
|
2548
|
+
if (optional) {
|
|
2549
|
+
const htmlTag = htmlTags.has(tag);
|
|
2550
|
+
// `<html>` may be omitted if first thing inside is not a comment
|
|
2551
|
+
// `<head>` may be omitted if first thing inside is an element
|
|
2552
|
+
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, <`style>`, or `<template>`
|
|
2553
|
+
// `<colgroup>` may be omitted if first thing inside is `<col>`
|
|
2554
|
+
// `<tbody>` may be omitted if first thing inside is `<tr>`
|
|
2555
|
+
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
2556
|
+
removeStartTag();
|
|
2557
|
+
}
|
|
2558
|
+
optionalStartTag = '';
|
|
2559
|
+
// End-tag-followed-by-start-tag omission rules
|
|
2560
|
+
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
|
|
2561
|
+
removeEndTag();
|
|
2562
|
+
// `<colgroup>` cannot be omitted if preceding `</colgroup>` is omitted
|
|
2563
|
+
// `<tbody>` cannot be omitted if preceding `</tbody>`, `</thead>`, or `</tfoot>` is omitted
|
|
2564
|
+
optional = !isStartTagMandatory(optionalEndTag, tag);
|
|
2565
|
+
}
|
|
2566
|
+
optionalEndTag = '';
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// Set whitespace flags for nested tags (e.g., <code> within a <pre>)
|
|
2570
|
+
if (options.collapseWhitespace) {
|
|
2571
|
+
if (!stackNoTrimWhitespace.length) {
|
|
2572
|
+
squashTrailingWhitespace(tag);
|
|
2573
|
+
}
|
|
2574
|
+
if (!unary) {
|
|
2575
|
+
if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
2576
|
+
stackNoTrimWhitespace.push(tag);
|
|
2577
|
+
}
|
|
2578
|
+
if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
2579
|
+
stackNoCollapseWhitespace.push(tag);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
const openTag = '<' + tag;
|
|
2585
|
+
const hasUnarySlash = unarySlash && options.keepClosingSlash;
|
|
2586
|
+
|
|
2587
|
+
buffer.push(openTag);
|
|
2588
|
+
|
|
2589
|
+
if (options.sortAttributes) {
|
|
2590
|
+
options.sortAttributes(tag, attrs);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
const parts = [];
|
|
2594
|
+
for (let i = attrs.length, isLast = true; --i >= 0;) {
|
|
2595
|
+
const normalized = await normalizeAttr(attrs[i], attrs, tag, options);
|
|
2596
|
+
if (normalized) {
|
|
2597
|
+
parts.push(buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr));
|
|
2598
|
+
isLast = false;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
parts.reverse();
|
|
2602
|
+
if (parts.length > 0) {
|
|
2603
|
+
buffer.push(' ');
|
|
2604
|
+
buffer.push.apply(buffer, parts);
|
|
2605
|
+
} else if (optional && optionalStartTags.has(tag)) {
|
|
2606
|
+
// Start tag must never be omitted if it has any attributes
|
|
2607
|
+
optionalStartTag = tag;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
|
|
2611
|
+
|
|
2612
|
+
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
2613
|
+
removeStartTag();
|
|
2614
|
+
optionalStartTag = '';
|
|
2615
|
+
}
|
|
2616
|
+
},
|
|
2617
|
+
end: function (tag, attrs, autoGenerated) {
|
|
2618
|
+
if (tag.toLowerCase() === 'svg') {
|
|
2619
|
+
options = Object.getPrototypeOf(options);
|
|
2620
|
+
}
|
|
2621
|
+
tag = options.name(tag);
|
|
2622
|
+
|
|
2623
|
+
// Check if current tag is in a whitespace stack
|
|
2624
|
+
if (options.collapseWhitespace) {
|
|
2625
|
+
if (stackNoTrimWhitespace.length) {
|
|
2626
|
+
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
2627
|
+
stackNoTrimWhitespace.pop();
|
|
2628
|
+
}
|
|
2629
|
+
} else {
|
|
2630
|
+
squashTrailingWhitespace('/' + tag);
|
|
2631
|
+
}
|
|
2632
|
+
if (stackNoCollapseWhitespace.length &&
|
|
2633
|
+
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
2634
|
+
stackNoCollapseWhitespace.pop();
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
let isElementEmpty = false;
|
|
2639
|
+
if (tag === currentTag) {
|
|
2640
|
+
currentTag = '';
|
|
2641
|
+
isElementEmpty = !hasChars;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
if (options.removeOptionalTags) {
|
|
2645
|
+
// `<html>`, `<head>` or `<body>` may be omitted if the element is empty
|
|
2646
|
+
if (isElementEmpty && topLevelTags.has(optionalStartTag)) {
|
|
2647
|
+
removeStartTag();
|
|
2648
|
+
}
|
|
2649
|
+
optionalStartTag = '';
|
|
2650
|
+
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
2651
|
+
// `</head>` may be omitted if not followed by space or comment
|
|
2652
|
+
// `</p>` may be omitted if no more content in non-`</a>` parent
|
|
2653
|
+
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
2654
|
+
if (htmlTags.has(tag) && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
|
|
2655
|
+
removeEndTag();
|
|
2656
|
+
}
|
|
2657
|
+
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
|
|
2661
|
+
let preserve = false;
|
|
2662
|
+
if (removeEmptyElementsExcept.length) {
|
|
2663
|
+
// Normalize attribute names for comparison with specs
|
|
2664
|
+
const normalizedAttrs = attrs.map(attr => ({ ...attr, name: options.name(attr.name) }));
|
|
2665
|
+
preserve = shouldPreserveEmptyElement(tag, normalizedAttrs, removeEmptyElementsExcept);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
if (!preserve) {
|
|
2669
|
+
// Remove last “element” from buffer
|
|
2670
|
+
removeStartTag();
|
|
2671
|
+
optionalStartTag = '';
|
|
2672
|
+
optionalEndTag = '';
|
|
2673
|
+
} else {
|
|
2674
|
+
// Preserve the element—add closing tag
|
|
2675
|
+
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
2676
|
+
optionalEndTag = '';
|
|
2677
|
+
} else {
|
|
2678
|
+
buffer.push('</' + tag + '>');
|
|
2679
|
+
}
|
|
2680
|
+
charsPrevTag = '/' + tag;
|
|
2681
|
+
if (!inlineElements.has(tag)) {
|
|
2682
|
+
currentChars = '';
|
|
2683
|
+
} else if (isElementEmpty) {
|
|
2684
|
+
currentChars += '|';
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
} else {
|
|
2688
|
+
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
2689
|
+
optionalEndTag = '';
|
|
2690
|
+
} else {
|
|
2691
|
+
buffer.push('</' + tag + '>');
|
|
2692
|
+
}
|
|
2693
|
+
charsPrevTag = '/' + tag;
|
|
2694
|
+
if (!inlineElements.has(tag)) {
|
|
2695
|
+
currentChars = '';
|
|
2696
|
+
} else if (isElementEmpty) {
|
|
2697
|
+
currentChars += '|';
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
},
|
|
2701
|
+
chars: async function (text, prevTag, nextTag) {
|
|
2702
|
+
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
2703
|
+
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
2704
|
+
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
2705
|
+
if (text.indexOf('&') !== -1) {
|
|
2706
|
+
text = entities.decodeHTML(text);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (options.collapseWhitespace) {
|
|
2710
|
+
if (!stackNoTrimWhitespace.length) {
|
|
2711
|
+
if (prevTag === 'comment') {
|
|
2712
|
+
const prevComment = buffer[buffer.length - 1];
|
|
2713
|
+
if (prevComment.indexOf(uidIgnore) === -1) {
|
|
2714
|
+
if (!prevComment) {
|
|
2715
|
+
prevTag = charsPrevTag;
|
|
2716
|
+
}
|
|
2717
|
+
if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
|
|
2718
|
+
const charsIndex = buffer.length - 2;
|
|
2719
|
+
buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
|
|
2720
|
+
text = trailingSpaces + text;
|
|
2721
|
+
return '';
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
if (prevTag) {
|
|
2727
|
+
if (prevTag === '/nobr' || prevTag === 'wbr') {
|
|
2728
|
+
if (/^\s/.test(text)) {
|
|
2729
|
+
let tagIndex = buffer.length - 1;
|
|
2730
|
+
while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
|
|
2731
|
+
tagIndex--;
|
|
2732
|
+
}
|
|
2733
|
+
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
2734
|
+
}
|
|
2735
|
+
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
2736
|
+
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
if (prevTag || nextTag) {
|
|
2740
|
+
text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
|
|
2741
|
+
} else {
|
|
2742
|
+
text = collapseWhitespace(text, options, true, true);
|
|
2743
|
+
}
|
|
2744
|
+
if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
|
|
2745
|
+
trimTrailingWhitespace(buffer.length - 1, nextTag);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
if (!stackNoCollapseWhitespace.length && nextTag !== 'html' && !(prevTag && nextTag)) {
|
|
2749
|
+
text = collapseWhitespace(text, options, false, false, true);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
if (specialContentTags.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
|
|
2753
|
+
text = await processScript(text, options, currentAttrs);
|
|
2754
|
+
}
|
|
2755
|
+
if (isExecutableScript(currentTag, currentAttrs)) {
|
|
2756
|
+
text = await options.minifyJS(text);
|
|
2757
|
+
}
|
|
2758
|
+
if (isStyleSheet(currentTag, currentAttrs)) {
|
|
2759
|
+
text = await options.minifyCSS(text);
|
|
2760
|
+
}
|
|
2761
|
+
if (options.removeOptionalTags && text) {
|
|
2762
|
+
// `<html>` may be omitted if first thing inside is not a comment
|
|
2763
|
+
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
|
|
2764
|
+
if (optionalStartTag === 'html' || (optionalStartTag === 'body' && !/^\s/.test(text))) {
|
|
2765
|
+
removeStartTag();
|
|
2766
|
+
}
|
|
2767
|
+
optionalStartTag = '';
|
|
2768
|
+
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
2769
|
+
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
2770
|
+
if (compactTags.has(optionalEndTag) || (looseTags.has(optionalEndTag) && !/^\s/.test(text))) {
|
|
2771
|
+
removeEndTag();
|
|
2772
|
+
}
|
|
2773
|
+
// Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
|
|
2774
|
+
if (!/^\s+$/.test(text) || !options.collapseWhitespace || options.conservativeCollapse) {
|
|
2775
|
+
optionalEndTag = '';
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
2779
|
+
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
2780
|
+
// Escape any `&` symbols that start either:
|
|
2781
|
+
// 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
|
|
2782
|
+
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
2783
|
+
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
2784
|
+
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
2785
|
+
if (text.indexOf('&') !== -1) {
|
|
2786
|
+
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');
|
|
2787
|
+
}
|
|
2788
|
+
if (text.indexOf('<') !== -1) {
|
|
2789
|
+
text = text.replace(/</g, '<');
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
2793
|
+
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
2794
|
+
return ignoredCustomMarkupChunks[+index][0];
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
currentChars += text;
|
|
2798
|
+
if (text) {
|
|
2799
|
+
hasChars = true;
|
|
2800
|
+
}
|
|
2801
|
+
buffer.push(text);
|
|
2802
|
+
},
|
|
2803
|
+
comment: async function (text, nonStandard) {
|
|
2804
|
+
const prefix = nonStandard ? '<!' : '<!--';
|
|
2805
|
+
const suffix = nonStandard ? '>' : '-->';
|
|
2806
|
+
if (isConditionalComment(text)) {
|
|
2807
|
+
text = prefix + await cleanConditionalComment(text, options) + suffix;
|
|
2808
|
+
} else if (options.removeComments) {
|
|
2809
|
+
if (isIgnoredComment(text, options)) {
|
|
2810
|
+
text = '<!--' + text + '-->';
|
|
2811
|
+
} else {
|
|
2812
|
+
text = '';
|
|
2813
|
+
}
|
|
2814
|
+
} else {
|
|
2815
|
+
text = prefix + text + suffix;
|
|
2816
|
+
}
|
|
2817
|
+
if (options.removeOptionalTags && text) {
|
|
2818
|
+
// Preceding comments suppress tag omissions
|
|
2819
|
+
optionalStartTag = '';
|
|
2820
|
+
optionalEndTag = '';
|
|
2821
|
+
}
|
|
2822
|
+
buffer.push(text);
|
|
2823
|
+
},
|
|
2824
|
+
doctype: function (doctype) {
|
|
2825
|
+
buffer.push(options.useShortDoctype
|
|
2826
|
+
? '<!doctype' +
|
|
2827
|
+
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
2828
|
+
: collapseWhitespaceAll(doctype));
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
|
|
2832
|
+
await parser.parse();
|
|
2833
|
+
|
|
2834
|
+
if (options.removeOptionalTags) {
|
|
2835
|
+
// `<html>` may be omitted if first thing inside is not a comment
|
|
2836
|
+
// `<head>` or `<body>` may be omitted if empty
|
|
2837
|
+
if (topLevelTags.has(optionalStartTag)) {
|
|
2838
|
+
removeStartTag();
|
|
2839
|
+
}
|
|
2840
|
+
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
2841
|
+
if (optionalEndTag && !trailingTags.has(optionalEndTag)) {
|
|
2842
|
+
removeEndTag();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
if (options.collapseWhitespace) {
|
|
2846
|
+
squashTrailingWhitespace('br');
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
return joinResultSegments(buffer, options, uidPattern
|
|
2850
|
+
? function (str) {
|
|
2851
|
+
return str.replace(uidPattern, function (match, prefix, index, suffix) {
|
|
2852
|
+
let chunk = ignoredCustomMarkupChunks[+index][0];
|
|
2853
|
+
if (options.collapseWhitespace) {
|
|
2854
|
+
if (prefix !== '\t') {
|
|
2855
|
+
chunk = prefix + chunk;
|
|
2856
|
+
}
|
|
2857
|
+
if (suffix !== '\t') {
|
|
2858
|
+
chunk += suffix;
|
|
2859
|
+
}
|
|
2860
|
+
return collapseWhitespace(chunk, {
|
|
2861
|
+
preserveLineBreaks: options.preserveLineBreaks,
|
|
2862
|
+
conservativeCollapse: !options.trimCustomFragments
|
|
2863
|
+
}, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk));
|
|
2864
|
+
}
|
|
2865
|
+
return chunk;
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
: identity, uidIgnore
|
|
2869
|
+
? function (str) {
|
|
2870
|
+
return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (match, index) {
|
|
2871
|
+
return ignoredMarkupChunks[+index];
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
: identity);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
2878
|
+
let str;
|
|
2879
|
+
const maxLineLength = options.maxLineLength;
|
|
2880
|
+
const noNewlinesBeforeTagClose = options.noNewlinesBeforeTagClose;
|
|
2881
|
+
|
|
2882
|
+
if (maxLineLength) {
|
|
2883
|
+
let line = ''; const lines = [];
|
|
2884
|
+
while (results.length) {
|
|
2885
|
+
const len = line.length;
|
|
2886
|
+
const end = results[0].indexOf('\n');
|
|
2887
|
+
const isClosingTag = Boolean(results[0].match(endTag));
|
|
2888
|
+
const shouldKeepSameLine = noNewlinesBeforeTagClose && isClosingTag;
|
|
2889
|
+
|
|
2890
|
+
if (end < 0) {
|
|
2891
|
+
line += restoreIgnore(restoreCustom(results.shift()));
|
|
2892
|
+
} else {
|
|
2893
|
+
line += restoreIgnore(restoreCustom(results[0].slice(0, end)));
|
|
2894
|
+
results[0] = results[0].slice(end + 1);
|
|
2895
|
+
}
|
|
2896
|
+
if (len > 0 && line.length > maxLineLength && !shouldKeepSameLine) {
|
|
2897
|
+
lines.push(line.slice(0, len));
|
|
2898
|
+
line = line.slice(len);
|
|
2899
|
+
} else if (end >= 0) {
|
|
2900
|
+
lines.push(line);
|
|
2901
|
+
line = '';
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
if (line) {
|
|
2905
|
+
lines.push(line);
|
|
2906
|
+
}
|
|
2907
|
+
str = lines.join('\n');
|
|
2908
|
+
} else {
|
|
2909
|
+
str = restoreIgnore(restoreCustom(results.join('')));
|
|
2910
|
+
}
|
|
2911
|
+
return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
/**
|
|
2915
|
+
* @param {string} value
|
|
2916
|
+
* @param {MinifierOptions} [options]
|
|
2917
|
+
* @returns {Promise<string>}
|
|
2918
|
+
*/
|
|
2919
|
+
const minify = async function (value, options) {
|
|
2920
|
+
const start = Date.now();
|
|
2921
|
+
options = processOptions(options || {});
|
|
2922
|
+
const result = await minifyHTML(value, options);
|
|
2923
|
+
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
2924
|
+
return result;
|
|
2925
|
+
};
|
|
2926
|
+
|
|
2927
|
+
var htmlminifier = { minify, presets, getPreset, getPresetNames };
|
|
2928
|
+
|
|
2705
2929
|
exports.default = htmlminifier;
|
|
2706
2930
|
exports.getPreset = getPreset;
|
|
2707
2931
|
exports.getPresetNames = getPresetNames;
|