| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- /*
- * Copyright 2010-2020 Gildas Lormeau
- * contact : gildas.lormeau <at> gmail.com
- *
- * This file is part of SingleFile.
- *
- * The code in this file is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * (GNU AGPL) as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *
- * The code in this file is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
- * General Public License for more details.
- *
- * As additional permission under GNU AGPL version 3 section 7, you may
- * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
- * AGPL normally required by section 4, provided you include this license
- * notice and a URL through which recipients can access the Corresponding
- * Source.
- */
- /* global globalThis */
- import * as cssTree from "./../vendor/css-tree.js";
- import {
- normalizeFontFamily,
- getFontWeight
- } from "./../single-file-helper.js";
- const helper = {
- normalizeFontFamily,
- getFontWeight
- };
- const FontFace = globalThis.FontFace;
- const REGEXP_URL_SIMPLE_QUOTES_FN = /url\s*\(\s*'(.*?)'\s*\)/i;
- const REGEXP_URL_DOUBLE_QUOTES_FN = /url\s*\(\s*"(.*?)"\s*\)/i;
- const REGEXP_URL_NO_QUOTES_FN = /url\s*\(\s*(.*?)\s*\)/i;
- const REGEXP_URL_FUNCTION = /(url|local)\(.*?\)\s*(,|$)/g;
- const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
- const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
- const REGEXP_URL_FUNCTION_WOFF = /^url\(\s*["']?data:font\/(woff2?)/;
- const REGEXP_URL_FUNCTION_WOFF_ALT = /^url\(\s*["']?data:application\/x-font-(woff)/;
- const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
- const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
- const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
- const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
- const LOCAL_SOURCE = "local(";
- const MEDIA_ALL = "all";
- const FONT_STRETCHES = {
- "ultra-condensed": "50%",
- "extra-condensed": "62.5%",
- "condensed": "75%",
- "semi-condensed": "87.5%",
- "normal": "100%",
- "semi-expanded": "112.5%",
- "expanded": "125%",
- "extra-expanded": "150%",
- "ultra-expanded": "200%"
- };
- const FONT_MAX_LOAD_DELAY = 5000;
- export {
- process
- };
- async function process(doc, stylesheets, fontURLs, fontTests) {
- const fontsDetails = {
- fonts: new Map(),
- medias: new Map(),
- supports: new Map()
- };
- const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
- let sheetIndex = 0;
- stylesheets.forEach(stylesheetInfo => {
- const cssRules = stylesheetInfo.stylesheet.children;
- if (cssRules) {
- stats.rules.processed += cssRules.getSize();
- stats.rules.discarded += cssRules.getSize();
- if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != MEDIA_ALL) {
- const mediaFontsDetails = createFontsDetailsInfo();
- fontsDetails.medias.set("media-" + sheetIndex + "-" + stylesheetInfo.mediaText, mediaFontsDetails);
- getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails);
- } else {
- getFontsDetails(doc, cssRules, sheetIndex, fontsDetails);
- }
- }
- sheetIndex++;
- });
- processFontDetails(fontsDetails);
- await Promise.all([...stylesheets].map(async ([, stylesheetInfo], sheetIndex) => {
- const cssRules = stylesheetInfo.stylesheet.children;
- const media = stylesheetInfo.mediaText;
- if (cssRules) {
- if (media && media != MEDIA_ALL) {
- await processFontFaceRules(cssRules, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + media), fontURLs, fontTests, stats);
- } else {
- await processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontURLs, fontTests, stats);
- }
- stats.rules.discarded -= cssRules.getSize();
- }
- }));
- return stats;
- }
- function getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
- let mediaIndex = 0, supportsIndex = 0;
- cssRules.forEach(ruleData => {
- if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
- const mediaText = cssTree.generate(ruleData.prelude);
- const fontsDetails = createFontsDetailsInfo();
- mediaFontsDetails.medias.set("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, fontsDetails);
- mediaIndex++;
- getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
- } else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
- const supportsText = cssTree.generate(ruleData.prelude);
- const fontsDetails = createFontsDetailsInfo();
- mediaFontsDetails.supports.set("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText, fontsDetails);
- supportsIndex++;
- getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
- } else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && ruleData.block && ruleData.block.children) {
- const fontKey = getFontKey(ruleData);
- let fontInfo = mediaFontsDetails.fonts.get(fontKey);
- if (!fontInfo) {
- fontInfo = [];
- mediaFontsDetails.fonts.set(fontKey, fontInfo);
- }
- const src = getPropertyValue(ruleData, "src");
- if (src) {
- const fontSources = src.match(REGEXP_URL_FUNCTION);
- if (fontSources) {
- fontSources.forEach(source => fontInfo.unshift(source));
- }
- }
- }
- });
- }
- function processFontDetails(fontsDetails) {
- fontsDetails.fonts.forEach((fontInfo, fontKey) => {
- fontsDetails.fonts.set(fontKey, fontInfo.map(fontSource => {
- const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
- let fontFormat;
- 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];
- if (fontFormatMatch && fontFormatMatch[1]) {
- fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
- }
- if (!fontFormat) {
- const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
- if (fontFormatMatch && fontFormatMatch[1]) {
- fontFormat = fontFormatMatch[1];
- } else {
- const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
- if (fontFormatMatch && fontFormatMatch[1]) {
- fontFormat = fontFormatMatch[1];
- }
- }
- }
- if (!fontFormat && fontUrl) {
- const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
- if (fontFormatMatch && fontFormatMatch[1]) {
- fontFormat = fontFormatMatch[1];
- }
- }
- return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat };
- }));
- });
- fontsDetails.medias.forEach(mediaFontsDetails => processFontDetails(mediaFontsDetails));
- fontsDetails.supports.forEach(supportsFontsDetails => processFontDetails(supportsFontsDetails));
- }
- async function processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontURLs, fontTests, stats) {
- const removedRules = [];
- let mediaIndex = 0, supportsIndex = 0;
- for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
- const ruleData = cssRule.data;
- if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
- const mediaText = cssTree.generate(ruleData.prelude);
- await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText), fontURLs, fontTests, stats);
- mediaIndex++;
- } else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
- const supportsText = cssTree.generate(ruleData.prelude);
- await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.supports.get("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText), fontURLs, fontTests, stats);
- supportsIndex++;
- } else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
- const key = getFontKey(ruleData);
- const fontInfo = fontsDetails.fonts.get(key);
- if (fontInfo) {
- const processed = await processFontFaceRule(ruleData, fontInfo, fontURLs, fontTests, stats);
- if (processed) {
- fontsDetails.fonts.delete(key);
- }
- } else {
- removedRules.push(cssRule);
- }
- }
- }
- removedRules.forEach(cssRule => cssRules.remove(cssRule));
- }
- async function processFontFaceRule(ruleData, fontInfo, fontURLs, fontTests, stats) {
- const removedNodes = [];
- for (let node = ruleData.block.children.head; node; node = node.next) {
- if (node.data.property == "src") {
- removedNodes.push(node);
- }
- }
- removedNodes.pop();
- removedNodes.forEach(node => ruleData.block.children.remove(node));
- const srcDeclaration = ruleData.block.children.filter(node => node.property == "src").tail;
- if (srcDeclaration) {
- await Promise.all(fontInfo.map(async (source, sourceIndex) => {
- if (fontTests.has(source.src)) {
- source.valid = fontTests.get(source.src);
- } else {
- if (FontFace) {
- const fontFace = new FontFace("test-font", source.src);
- try {
- let timeout;
- await Promise.race([
- fontFace.load().then(() => fontFace.loaded).then(() => { source.valid = true; globalThis.clearTimeout(timeout); }),
- new Promise(resolve => timeout = globalThis.setTimeout(() => { source.valid = true; resolve(); }, FONT_MAX_LOAD_DELAY))
- ]);
- } catch (error) {
- const declarationFontURLs = fontURLs.get(srcDeclaration.data);
- if (declarationFontURLs) {
- const fontURL = declarationFontURLs[declarationFontURLs.length - sourceIndex - 1];
- if (fontURL) {
- const fontFace = new FontFace("test-font", "url(" + fontURL + ")");
- try {
- let timeout;
- await Promise.race([
- fontFace.load().then(() => fontFace.loaded).then(() => { source.valid = true; globalThis.clearTimeout(timeout); }),
- new Promise(resolve => timeout = globalThis.setTimeout(() => { source.valid = true; resolve(); }, FONT_MAX_LOAD_DELAY))
- ]);
- } catch (error) {
- // ignored
- }
- }
- } else {
- source.valid = true;
- }
- }
- } else {
- source.valid = true;
- }
- fontTests.set(source.src, source.valid);
- }
- }));
- const findSource = (fontFormat, testValidity) => fontInfo.find(source => !source.src.match(EMPTY_URL_SOURCE) && source.format == fontFormat && (!testValidity || source.valid));
- const filterSource = fontSource => fontInfo.filter(source => source == fontSource || source.src.startsWith(LOCAL_SOURCE));
- stats.fonts.processed += fontInfo.length;
- stats.fonts.discarded += fontInfo.length;
- const woffFontFound = findSource("woff2-variations", true) || findSource("woff2", true) || findSource("woff", true);
- if (woffFontFound) {
- fontInfo = filterSource(woffFontFound);
- } else {
- const ttfFontFound = findSource("truetype-variations", true) || findSource("truetype", true);
- if (ttfFontFound) {
- fontInfo = filterSource(ttfFontFound);
- } else {
- const otfFontFound = findSource("opentype") || findSource("embedded-opentype");
- if (otfFontFound) {
- fontInfo = filterSource(otfFontFound);
- } else {
- fontInfo = fontInfo.filter(source => !source.src.match(EMPTY_URL_SOURCE) && (source.valid) || source.src.startsWith(LOCAL_SOURCE));
- }
- }
- }
- stats.fonts.discarded -= fontInfo.length;
- fontInfo.reverse();
- try {
- srcDeclaration.data.value = cssTree.parse(fontInfo.map(fontSource => fontSource.src).join(","), { context: "value" });
- }
- catch (error) {
- // ignored
- }
- return true;
- } else {
- return false;
- }
- }
- function getPropertyValue(ruleData, propertyName) {
- let property;
- if (ruleData.block.children) {
- property = ruleData.block.children.filter(node => {
- try {
- return node.property == propertyName && !cssTree.generate(node.value).match(/\\9$/);
- } catch (error) {
- return node.property == propertyName;
- }
- }).tail;
- }
- if (property) {
- try {
- return cssTree.generate(property.data.value);
- } catch (error) {
- // ignored
- }
- }
- }
- function getFontKey(ruleData) {
- return JSON.stringify([
- helper.normalizeFontFamily(getPropertyValue(ruleData, "font-family")),
- helper.getFontWeight(getPropertyValue(ruleData, "font-weight") || "400"),
- getPropertyValue(ruleData, "font-style") || "normal",
- getPropertyValue(ruleData, "unicode-range"),
- getFontStretch(getPropertyValue(ruleData, "font-stretch")),
- getPropertyValue(ruleData, "font-variant") || "normal",
- getPropertyValue(ruleData, "font-feature-settings"),
- getPropertyValue(ruleData, "font-variation-settings")
- ]);
- }
- function getFontStretch(stretch) {
- return FONT_STRETCHES[stretch] || stretch;
- }
- function createFontsDetailsInfo() {
- return {
- fonts: new Map(),
- medias: new Map(),
- supports: new Map()
- };
- }
|