181 lines
6.8 KiB
JavaScript
181 lines
6.8 KiB
JavaScript
|
import { toArray } from './util';
|
||
|
import { fetchAsDataURL } from './dataurl';
|
||
|
import { shouldEmbed, embedResources } from './embed-resources';
|
||
|
const cssFetchCache = {};
|
||
|
async function fetchCSS(url) {
|
||
|
let cache = cssFetchCache[url];
|
||
|
if (cache != null) {
|
||
|
return cache;
|
||
|
}
|
||
|
const res = await fetch(url);
|
||
|
const cssText = await res.text();
|
||
|
cache = { url, cssText };
|
||
|
cssFetchCache[url] = cache;
|
||
|
return cache;
|
||
|
}
|
||
|
async function embedFonts(data, options) {
|
||
|
let cssText = data.cssText;
|
||
|
const regexUrl = /url\(["']?([^"')]+)["']?\)/g;
|
||
|
const fontLocs = cssText.match(/url\([^)]+\)/g) || [];
|
||
|
const loadFonts = fontLocs.map(async (loc) => {
|
||
|
let url = loc.replace(regexUrl, '$1');
|
||
|
if (!url.startsWith('https://')) {
|
||
|
url = new URL(url, data.url).href;
|
||
|
}
|
||
|
return fetchAsDataURL(url, options.fetchRequestInit, ({ result }) => {
|
||
|
cssText = cssText.replace(loc, `url(${result})`);
|
||
|
return [loc, result];
|
||
|
});
|
||
|
});
|
||
|
return Promise.all(loadFonts).then(() => cssText);
|
||
|
}
|
||
|
function parseCSS(source) {
|
||
|
if (source == null) {
|
||
|
return [];
|
||
|
}
|
||
|
const result = [];
|
||
|
const commentsRegex = /(\/\*[\s\S]*?\*\/)/gi;
|
||
|
// strip out comments
|
||
|
let cssText = source.replace(commentsRegex, '');
|
||
|
// eslint-disable-next-line prefer-regex-literals
|
||
|
const keyframesRegex = new RegExp('((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})', 'gi');
|
||
|
// eslint-disable-next-line no-constant-condition
|
||
|
while (true) {
|
||
|
const matches = keyframesRegex.exec(cssText);
|
||
|
if (matches === null) {
|
||
|
break;
|
||
|
}
|
||
|
result.push(matches[0]);
|
||
|
}
|
||
|
cssText = cssText.replace(keyframesRegex, '');
|
||
|
const importRegex = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi;
|
||
|
// to match css & media queries together
|
||
|
const combinedCSSRegex = '((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]' +
|
||
|
'*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})';
|
||
|
// unified regex
|
||
|
const unifiedRegex = new RegExp(combinedCSSRegex, 'gi');
|
||
|
// eslint-disable-next-line no-constant-condition
|
||
|
while (true) {
|
||
|
let matches = importRegex.exec(cssText);
|
||
|
if (matches === null) {
|
||
|
matches = unifiedRegex.exec(cssText);
|
||
|
if (matches === null) {
|
||
|
break;
|
||
|
}
|
||
|
else {
|
||
|
importRegex.lastIndex = unifiedRegex.lastIndex;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
unifiedRegex.lastIndex = importRegex.lastIndex;
|
||
|
}
|
||
|
result.push(matches[0]);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
async function getCSSRules(styleSheets, options) {
|
||
|
const ret = [];
|
||
|
const deferreds = [];
|
||
|
// First loop inlines imports
|
||
|
styleSheets.forEach((sheet) => {
|
||
|
if ('cssRules' in sheet) {
|
||
|
try {
|
||
|
toArray(sheet.cssRules || []).forEach((item, index) => {
|
||
|
if (item.type === CSSRule.IMPORT_RULE) {
|
||
|
let importIndex = index + 1;
|
||
|
const url = item.href;
|
||
|
const deferred = fetchCSS(url)
|
||
|
.then((metadata) => embedFonts(metadata, options))
|
||
|
.then((cssText) => parseCSS(cssText).forEach((rule) => {
|
||
|
try {
|
||
|
sheet.insertRule(rule, rule.startsWith('@import')
|
||
|
? (importIndex += 1)
|
||
|
: sheet.cssRules.length);
|
||
|
}
|
||
|
catch (error) {
|
||
|
console.error('Error inserting rule from remote css', {
|
||
|
rule,
|
||
|
error,
|
||
|
});
|
||
|
}
|
||
|
}))
|
||
|
.catch((e) => {
|
||
|
console.error('Error loading remote css', e.toString());
|
||
|
});
|
||
|
deferreds.push(deferred);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
catch (e) {
|
||
|
const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0];
|
||
|
if (sheet.href != null) {
|
||
|
deferreds.push(fetchCSS(sheet.href)
|
||
|
.then((metadata) => embedFonts(metadata, options))
|
||
|
.then((cssText) => parseCSS(cssText).forEach((rule) => {
|
||
|
inline.insertRule(rule, sheet.cssRules.length);
|
||
|
}))
|
||
|
.catch((err) => {
|
||
|
console.error('Error loading remote stylesheet', err);
|
||
|
}));
|
||
|
}
|
||
|
console.error('Error inlining remote css file', e);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
return Promise.all(deferreds).then(() => {
|
||
|
// Second loop parses rules
|
||
|
styleSheets.forEach((sheet) => {
|
||
|
if ('cssRules' in sheet) {
|
||
|
try {
|
||
|
toArray(sheet.cssRules || []).forEach((item) => {
|
||
|
ret.push(item);
|
||
|
});
|
||
|
}
|
||
|
catch (e) {
|
||
|
console.error(`Error while reading CSS rules from ${sheet.href}`, e);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
return ret;
|
||
|
});
|
||
|
}
|
||
|
function getWebFontRules(cssRules) {
|
||
|
return cssRules
|
||
|
.filter((rule) => rule.type === CSSRule.FONT_FACE_RULE)
|
||
|
.filter((rule) => shouldEmbed(rule.style.getPropertyValue('src')));
|
||
|
}
|
||
|
async function parseWebFontRules(node, options) {
|
||
|
if (node.ownerDocument == null) {
|
||
|
throw new Error('Provided element is not within a Document');
|
||
|
}
|
||
|
const styleSheets = toArray(node.ownerDocument.styleSheets);
|
||
|
const cssRules = await getCSSRules(styleSheets, options);
|
||
|
return getWebFontRules(cssRules);
|
||
|
}
|
||
|
export async function getWebFontCSS(node, options) {
|
||
|
const rules = await parseWebFontRules(node, options);
|
||
|
const cssTexts = await Promise.all(rules.map((rule) => {
|
||
|
const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null;
|
||
|
return embedResources(rule.cssText, baseUrl, options);
|
||
|
}));
|
||
|
return cssTexts.join('\n');
|
||
|
}
|
||
|
export async function embedWebFonts(clonedNode, options) {
|
||
|
const cssText = options.fontEmbedCSS != null
|
||
|
? options.fontEmbedCSS
|
||
|
: options.skipFonts
|
||
|
? null
|
||
|
: await getWebFontCSS(clonedNode, options);
|
||
|
if (cssText) {
|
||
|
const styleNode = document.createElement('style');
|
||
|
const sytleContent = document.createTextNode(cssText);
|
||
|
styleNode.appendChild(sytleContent);
|
||
|
if (clonedNode.firstChild) {
|
||
|
clonedNode.insertBefore(styleNode, clonedNode.firstChild);
|
||
|
}
|
||
|
else {
|
||
|
clonedNode.appendChild(styleNode);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
//# sourceMappingURL=embed-webfonts.js.map
|