|
@@ -37,10 +37,15 @@ this.fontsMinifier = this.fontsMinifier || (() => {
|
|
|
const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
|
|
const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
|
|
|
const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
|
|
const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
|
|
|
const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
|
|
const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
|
|
|
|
|
+ const FONT_WEIGHTS = {
|
|
|
|
|
+ normal: 400,
|
|
|
|
|
+ bold: 700
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
process: (doc, secondPass) => {
|
|
process: (doc, secondPass) => {
|
|
|
const declaredFonts = new Set();
|
|
const declaredFonts = new Set();
|
|
|
|
|
+ const fontsDetails = new Map();
|
|
|
const usedFonts = [];
|
|
const usedFonts = [];
|
|
|
const stats = {
|
|
const stats = {
|
|
|
rules: {
|
|
rules: {
|
|
@@ -56,8 +61,9 @@ this.fontsMinifier = this.fontsMinifier || (() => {
|
|
|
if (style.sheet) {
|
|
if (style.sheet) {
|
|
|
const processedRules = style.sheet.cssRules.length;
|
|
const processedRules = style.sheet.cssRules.length;
|
|
|
stats.rules.processed += processedRules;
|
|
stats.rules.processed += processedRules;
|
|
|
- style.textContent = processRules(doc, style.sheet.cssRules, declaredFonts, usedFonts, stats, secondPass);
|
|
|
|
|
- stats.rules.discarded += processedRules - style.sheet.cssRules.length;
|
|
|
|
|
|
|
+ stats.rules.discarded += processedRules;
|
|
|
|
|
+ processRules(doc, style.sheet.cssRules, fontsDetails, declaredFonts, usedFonts, secondPass);
|
|
|
|
|
+ style.textContent = processFontFaceRules(style.sheet.cssRules, fontsDetails, "all", stats);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
doc.querySelectorAll("[style]").forEach(element => {
|
|
doc.querySelectorAll("[style]").forEach(element => {
|
|
@@ -74,120 +80,95 @@ this.fontsMinifier = this.fontsMinifier || (() => {
|
|
|
const filteredUsedFonts = new Set(usedFonts.map(fontNames => fontNames.find(fontName => declaredFonts.has(fontName))).filter(fontName => fontName));
|
|
const filteredUsedFonts = new Set(usedFonts.map(fontNames => fontNames.find(fontName => declaredFonts.has(fontName))).filter(fontName => fontName));
|
|
|
unusedFonts = Array.from(declaredFonts).filter(fontFamilyName => !filteredUsedFonts.has(fontFamilyName));
|
|
unusedFonts = Array.from(declaredFonts).filter(fontFamilyName => !filteredUsedFonts.has(fontFamilyName));
|
|
|
}
|
|
}
|
|
|
- if (unusedFonts.length) {
|
|
|
|
|
- doc.querySelectorAll("style").forEach(style => {
|
|
|
|
|
- if (style.sheet) {
|
|
|
|
|
- const processedRules = style.sheet.cssRules.length;
|
|
|
|
|
- style.textContent = deleteUnusedFonts(doc, style.sheet.cssRules, unusedFonts);
|
|
|
|
|
- stats.rules.discarded += processedRules - style.sheet.cssRules.length;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ doc.querySelectorAll("style").forEach(style => {
|
|
|
|
|
+ if (style.sheet) {
|
|
|
|
|
+ style.textContent = deleteUnusedFonts(doc, style.sheet.cssRules, unusedFonts);
|
|
|
|
|
+ stats.rules.discarded -= style.sheet.cssRules.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
return stats;
|
|
return stats;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- function processRules(doc, rules, declaredFonts, usedFonts, stats, secondPass) {
|
|
|
|
|
- let stylesheetContent = "";
|
|
|
|
|
|
|
+ function processRules(doc, rules, fontsDetails, declaredFonts, usedFonts, secondPass) {
|
|
|
if (rules) {
|
|
if (rules) {
|
|
|
Array.from(rules).forEach(rule => {
|
|
Array.from(rules).forEach(rule => {
|
|
|
if (rule.type == CSSRule.MEDIA_RULE) {
|
|
if (rule.type == CSSRule.MEDIA_RULE) {
|
|
|
- stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
|
|
|
|
|
- stylesheetContent += processRules(doc, rule.cssRules, declaredFonts, usedFonts, stats, secondPass);
|
|
|
|
|
- stylesheetContent += "}";
|
|
|
|
|
|
|
+ processRules(doc, rule.cssRules, fontsDetails, declaredFonts, usedFonts, secondPass);
|
|
|
} else if (rule.type == CSSRule.STYLE_RULE) {
|
|
} else if (rule.type == CSSRule.STYLE_RULE) {
|
|
|
if (rule.style && rule.style.fontFamily) {
|
|
if (rule.style && rule.style.fontFamily) {
|
|
|
const fontFamilyNames = rule.style.fontFamily.split(",").map(fontFamilyName => getFontFamilyName(fontFamilyName));
|
|
const fontFamilyNames = rule.style.fontFamily.split(",").map(fontFamilyName => getFontFamilyName(fontFamilyName));
|
|
|
usedFonts.push(fontFamilyNames);
|
|
usedFonts.push(fontFamilyNames);
|
|
|
}
|
|
}
|
|
|
- stylesheetContent += rule.cssText;
|
|
|
|
|
} else {
|
|
} else {
|
|
|
- let cssText = rule.cssText;
|
|
|
|
|
if (rule.type == CSSRule.FONT_FACE_RULE && rule.style) {
|
|
if (rule.type == CSSRule.FONT_FACE_RULE && rule.style) {
|
|
|
- const fontFamilyName = rule.style.getPropertyValue("font-family");
|
|
|
|
|
|
|
+ const fontFamilyName = getFontFamilyName(rule.style.getPropertyValue("font-family"));
|
|
|
if (fontFamilyName) {
|
|
if (fontFamilyName) {
|
|
|
- declaredFonts.add(getFontFamilyName(fontFamilyName));
|
|
|
|
|
|
|
+ declaredFonts.add(fontFamilyName);
|
|
|
}
|
|
}
|
|
|
- const src = rule.style.getPropertyValue("src");
|
|
|
|
|
- if (src) {
|
|
|
|
|
- const fontSources = src.match(REGEXP_URL_FUNCTION);
|
|
|
|
|
- if (fontSources) {
|
|
|
|
|
- if (secondPass || testUnicodeRange(doc, rule)) {
|
|
|
|
|
- cssText = processFontFaceRule(rule, fontSources, stats);
|
|
|
|
|
- } else {
|
|
|
|
|
- cssText = "";
|
|
|
|
|
|
|
+ if (secondPass || testUnicodeRange(doc, rule)) {
|
|
|
|
|
+ const fontKey = getFontKey(rule.style);
|
|
|
|
|
+ let fontInfo = fontsDetails.get(fontKey);
|
|
|
|
|
+ if (!fontInfo) {
|
|
|
|
|
+ fontInfo = new Set();
|
|
|
|
|
+ fontsDetails.set(fontKey, fontInfo);
|
|
|
|
|
+ }
|
|
|
|
|
+ const src = rule.style.getPropertyValue("src");
|
|
|
|
|
+ if (src) {
|
|
|
|
|
+ const fontSources = src.match(REGEXP_URL_FUNCTION);
|
|
|
|
|
+ if (fontSources) {
|
|
|
|
|
+ fontSources.forEach(source => fontInfo.add(source));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- stylesheetContent += cssText;
|
|
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
- return stylesheetContent;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function testUnicodeRange(doc, rule) {
|
|
|
|
|
- const unicodeRange = rule.style.getPropertyValue("unicode-range");
|
|
|
|
|
- const docContent = doc.body.outerText;
|
|
|
|
|
- if (unicodeRange) {
|
|
|
|
|
- const unicodeRanges = unicodeRange.split(REGEXP_COMMA);
|
|
|
|
|
- const result = unicodeRanges.filter(rangeValue => {
|
|
|
|
|
- const range = rangeValue.split(REGEXP_DASH);
|
|
|
|
|
- if (range.length == 2) {
|
|
|
|
|
- range[0] = transformRange(range[0]);
|
|
|
|
|
- const regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
|
|
|
|
|
- return (new RegExp(regExpString, "u")).test(docContent);
|
|
|
|
|
- }
|
|
|
|
|
- if (range.length == 1) {
|
|
|
|
|
- if (range[0].includes("?")) {
|
|
|
|
|
- const firstRange = transformRange(range[0]);
|
|
|
|
|
- const secondRange = firstRange;
|
|
|
|
|
- const regExpString = "[" + firstRange.replace(REGEXP_QUESTION_MARK, "0") + "-" + secondRange.replace(REGEXP_QUESTION_MARK, "F") + "]";
|
|
|
|
|
- return (new RegExp(regExpString, "u")).test(docContent);
|
|
|
|
|
-
|
|
|
|
|
- } else {
|
|
|
|
|
- const regExpString = "[" + transformRange(range[0]) + "]";
|
|
|
|
|
- return (new RegExp(regExpString, "u")).test(docContent);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ function processFontFaceRules(rules, fontsDetails, media, stats) {
|
|
|
|
|
+ let stylesheetContent = "";
|
|
|
|
|
+ Array.from(rules).forEach(rule => {
|
|
|
|
|
+ if (rule.type == CSSRule.MEDIA_RULE) {
|
|
|
|
|
+ stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + "{";
|
|
|
|
|
+ stylesheetContent += processFontFaceRules(rule.cssRules, fontsDetails, rule.media, stats);
|
|
|
|
|
+ stylesheetContent += "}";
|
|
|
|
|
+ } else if (rule.type == CSSRule.FONT_FACE_RULE && (media.includes("all") || media.includes("screen"))) {
|
|
|
|
|
+ const fontInfo = fontsDetails.get(getFontKey(rule.style));
|
|
|
|
|
+ if (fontInfo) {
|
|
|
|
|
+ fontsDetails.delete(getFontKey(rule.style));
|
|
|
|
|
+ stylesheetContent += "@font-face {" + processFontFaceRule(rule, fontInfo, stats) + "}";
|
|
|
}
|
|
}
|
|
|
- return true;
|
|
|
|
|
- });
|
|
|
|
|
- return result.length;
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- function transformRange(range) {
|
|
|
|
|
- range = range.replace(REGEXP_STARTS_U_PLUS, "");
|
|
|
|
|
- while (range.length < 6) {
|
|
|
|
|
- range = "0" + range;
|
|
|
|
|
- }
|
|
|
|
|
- return "\\u{" + range + "}";
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stylesheetContent += rule.cssText;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return stylesheetContent;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function processFontFaceRule(rule, fontSources, stats) {
|
|
|
|
|
- fontSources = fontSources.map(fontSrc => {
|
|
|
|
|
- const fontFormatMatch = fontSrc.match(REGEXP_FONT_FORMAT_VALUE);
|
|
|
|
|
|
|
+ function processFontFaceRule(rule, fontInfo, stats) {
|
|
|
|
|
+ let fontSources = Array.from(fontInfo).map(fontSource => {
|
|
|
|
|
+ const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
|
|
|
let fontFormat;
|
|
let fontFormat;
|
|
|
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
|
fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
|
|
fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
|
|
|
}
|
|
}
|
|
|
if (!fontFormat) {
|
|
if (!fontFormat) {
|
|
|
- const fontFormatMatch = fontSrc.match(REGEXP_URL_FUNCTION_WOFF);
|
|
|
|
|
|
|
+ const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
|
|
|
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
|
fontFormat = fontFormatMatch[1];
|
|
fontFormat = fontFormatMatch[1];
|
|
|
} else {
|
|
} else {
|
|
|
- const fontFormatMatch = fontSrc.match(REGEXP_URL_FUNCTION_WOFF_ALT);
|
|
|
|
|
|
|
+ const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
|
|
|
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
|
fontFormat = fontFormatMatch[1];
|
|
fontFormat = fontFormatMatch[1];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (!fontFormat) {
|
|
if (!fontFormat) {
|
|
|
- const urlMatch = fontSrc.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
|
|
|
|
|
- fontSrc.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
|
|
|
|
|
- fontSrc.match(REGEXP_URL_NO_QUOTES_FN);
|
|
|
|
|
|
|
+ const urlMatch = fontSource.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
|
|
|
|
|
+ fontSource.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
|
|
|
|
|
+ fontSource.match(REGEXP_URL_NO_QUOTES_FN);
|
|
|
const fontUrl = urlMatch && urlMatch[1];
|
|
const fontUrl = urlMatch && urlMatch[1];
|
|
|
if (fontUrl) {
|
|
if (fontUrl) {
|
|
|
const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
|
|
const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
|
|
@@ -196,7 +177,7 @@ this.fontsMinifier = this.fontsMinifier || (() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return { src: fontSrc.match(REGEXP_FONT_SRC)[1], format: fontFormat };
|
|
|
|
|
|
|
+ return { src: fontSource.match(REGEXP_FONT_SRC)[1], format: fontFormat };
|
|
|
});
|
|
});
|
|
|
const fontTest = (fontSource, format) => fontSource.format == format;
|
|
const fontTest = (fontSource, format) => fontSource.format == format;
|
|
|
const woff2FontFound = fontSources.find(fontSource => fontTest(fontSource, "woff2"));
|
|
const woff2FontFound = fontSources.find(fontSource => fontTest(fontSource, "woff2"));
|
|
@@ -212,15 +193,17 @@ this.fontsMinifier = this.fontsMinifier || (() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
stats.fonts.processed += stats.fonts.processed - fontSources.length;
|
|
stats.fonts.processed += stats.fonts.processed - fontSources.length;
|
|
|
- const cssStyles = [];
|
|
|
|
|
|
|
+ let cssText = "";
|
|
|
Array.from(rule.style).forEach(propertyName => {
|
|
Array.from(rule.style).forEach(propertyName => {
|
|
|
|
|
+ cssText += propertyName + ":";
|
|
|
if (propertyName == "src") {
|
|
if (propertyName == "src") {
|
|
|
- cssStyles.push("src:" + fontSources.map(fontSource => fontSource.src).join(","));
|
|
|
|
|
|
|
+ cssText += fontSources.map(fontSource => fontSource.src).join(",");
|
|
|
} else {
|
|
} else {
|
|
|
- cssStyles.push(propertyName + ":" + rule.style.getPropertyValue(propertyName));
|
|
|
|
|
|
|
+ cssText += rule.style.getPropertyValue(propertyName);
|
|
|
}
|
|
}
|
|
|
|
|
+ cssText += ";";
|
|
|
});
|
|
});
|
|
|
- return "@font-face{" + cssStyles.join(";") + "}";
|
|
|
|
|
|
|
+ return cssText;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function deleteUnusedFonts(doc, rules, unusedFonts) {
|
|
function deleteUnusedFonts(doc, rules, unusedFonts) {
|
|
@@ -240,6 +223,59 @@ this.fontsMinifier = this.fontsMinifier || (() => {
|
|
|
return stylesheetContent;
|
|
return stylesheetContent;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function testUnicodeRange(doc, rule) {
|
|
|
|
|
+ const unicodeRange = rule.style.getPropertyValue("unicode-range");
|
|
|
|
|
+ const docContent = doc.body.outerText;
|
|
|
|
|
+ if (unicodeRange) {
|
|
|
|
|
+ const unicodeRanges = unicodeRange.split(REGEXP_COMMA);
|
|
|
|
|
+ const result = unicodeRanges.filter(rangeValue => {
|
|
|
|
|
+ const range = rangeValue.split(REGEXP_DASH);
|
|
|
|
|
+ if (range.length == 2) {
|
|
|
|
|
+ range[0] = transformRange(range[0]);
|
|
|
|
|
+ const regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
|
|
|
|
|
+ return (new RegExp(regExpString, "u")).test(docContent);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (range.length == 1) {
|
|
|
|
|
+ if (range[0].includes("?")) {
|
|
|
|
|
+ const firstRange = transformRange(range[0]);
|
|
|
|
|
+ const secondRange = firstRange;
|
|
|
|
|
+ const regExpString = "[" + firstRange.replace(REGEXP_QUESTION_MARK, "0") + "-" + secondRange.replace(REGEXP_QUESTION_MARK, "F") + "]";
|
|
|
|
|
+ return (new RegExp(regExpString, "u")).test(docContent);
|
|
|
|
|
+
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const regExpString = "[" + transformRange(range[0]) + "]";
|
|
|
|
|
+ return (new RegExp(regExpString, "u")).test(docContent);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ });
|
|
|
|
|
+ return result.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function transformRange(range) {
|
|
|
|
|
+ range = range.replace(REGEXP_STARTS_U_PLUS, "");
|
|
|
|
|
+ while (range.length < 6) {
|
|
|
|
|
+ range = "0" + range;
|
|
|
|
|
+ }
|
|
|
|
|
+ return "\\u{" + range + "}";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getFontKey(style) {
|
|
|
|
|
+ return JSON.stringify([
|
|
|
|
|
+ getFontFamilyName(style.getPropertyValue("font-family")),
|
|
|
|
|
+ getFontWeight(style.getPropertyValue("font-weight")),
|
|
|
|
|
+ style.getPropertyValue("font-style"),
|
|
|
|
|
+ style.getPropertyValue("unicode-range"),
|
|
|
|
|
+ style.getPropertyValue("font-display"),
|
|
|
|
|
+ style.getPropertyValue("font-stretch"),
|
|
|
|
|
+ style.getPropertyValue("font-variant"),
|
|
|
|
|
+ style.getPropertyValue("font-feature-settings"),
|
|
|
|
|
+ style.getPropertyValue("font-variation-settings")
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function getFontFamilyName(fontFamilyName) {
|
|
function getFontFamilyName(fontFamilyName) {
|
|
|
fontFamilyName = fontFamilyName.toLowerCase().trim();
|
|
fontFamilyName = fontFamilyName.toLowerCase().trim();
|
|
|
if (fontFamilyName.match(REGEXP_SIMPLE_QUOTES_STRING)) {
|
|
if (fontFamilyName.match(REGEXP_SIMPLE_QUOTES_STRING)) {
|
|
@@ -250,4 +286,8 @@ this.fontsMinifier = this.fontsMinifier || (() => {
|
|
|
return fontFamilyName.trim();
|
|
return fontFamilyName.trim();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function getFontWeight(weight) {
|
|
|
|
|
+ return FONT_WEIGHTS[weight] || weight;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
})();
|
|
})();
|