/* eslint-disable no-undef */ /** * * @param {StyleSheetList} styleSheets */ const ForeignHtmlRenderer = function (styleSheets) { /** * * @param {String} binStr */ const binaryStringToBase64 = binStr => new Promise(resolve => { const reader = new FileReader(); reader.readAsDataURL(binStr); reader.onloadend = function () { resolve(reader.result); }; }); function prepend(extension) { return window.location.origin + extension; } function CorsProxy(url) { return prepend('/corsproxy/') + encodeURIComponent(url); } /** * * @param {String} url * @returns {Promise} */ const getResourceAsBase64 = (webUrl, inurl) => new Promise(resolve => { const xhr = new XMLHttpRequest(); // const url = inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl; // const url = CorsProxy(inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl);// inurl.startsWith("http") ? CorsProxy(inurl) : inurl; let url = inurl; if (inurl.startsWith('/static')) { url = new URL(webUrl).origin + inurl; } else if (inurl.startsWith('/') && !inurl.startsWith('//')) { url = CorsProxy(new URL(webUrl).origin + inurl); } else if (!inurl.startsWith('http') && !inurl.startsWith('//')) { url = CorsProxy(`${webUrl}/${inurl}`); } else if (inurl.startsWith('https') && !inurl.startsWith(window.location.origin)) { url = CorsProxy(inurl); } xhr.open('GET', url); xhr.responseType = 'blob'; xhr.onreadystatechange = async function () { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { const resBase64 = await binaryStringToBase64(xhr.response); resolve({ resourceUrl: inurl, resourceBase64: resBase64, }); } else if (xhr.readyState === XMLHttpRequest.DONE) { console.log(`COULDN'T FIND: ${inurl.startsWith('/') ? webUrl + inurl : inurl}`); resolve({ resourceUrl: '', resourceBase64: inurl, }); } }; xhr.send(null); }); /** * * @param {String[]} urls * @returns {Promise} */ const getMultipleResourcesAsBase64 = (webUrl, urls) => { const promises = []; for (let i = 0; webUrl && i < urls.length; i += 1) { promises.push(getResourceAsBase64(webUrl, urls[i])); } return Promise.all(promises); }; /** * * @param {String} str * @param {Number} startIndex * @param {String} prefixToken * @param {String[]} suffixTokens * * @returns {String|null} */ const parseValue = function (str, startIndex, prefixToken, suffixTokens) { const idx = str.indexOf(prefixToken, startIndex); if (idx === -1) { return null; } let val = ''; for (let i = idx + prefixToken.length; i < str.length; i += 1) { if (suffixTokens.indexOf(str[i]) !== -1) { break; } val += str[i]; } return { foundAtIndex: idx, value: val, }; }; /** * * @param {String} str * @returns {String} */ const removeQuotes = function (str) { return str.replace(/["']/g, ''); }; /** * * @param {String} cssRuleStr * @returns {String[]} */ const getUrlsFromCssString = function (cssRuleStr, selector = 'url(', delimiters = [')'], mustEndWithQuote = false) { const urlsFound = []; let searchStartIndex = 0; // eslint-disable-next-line no-constant-condition while (true) { const url = parseValue(cssRuleStr, searchStartIndex, selector, delimiters); if (url === null) { break; } searchStartIndex = url.foundAtIndex + url.value.length + 1; if (!mustEndWithQuote || url.value[url.value.length - 1] === '"') { const unquoted = removeQuotes(url.value); if (unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") ) */ && unquoted !== 'http://' && unquoted !== 'https://') { if (unquoted) urlsFound.push(unquoted); } } } return urlsFound; }; /** * Extracts font-face URLs from CSS rules * @param {String} cssRuleStr * @returns {String[]} */ const getFontFaceUrlsFromCss = function (cssRuleStr) { const fontFaceUrls = []; // Find @font-face blocks const fontFaceBlocks = cssRuleStr.match(/@font-face\s*{[^}]*}/g) || []; fontFaceBlocks.forEach(block => { // Extract URLs from src properties const urls = block.match(/src\s*:\s*[^;]*/g) || []; urls.forEach(srcDeclaration => { // Find all url() references in the src declaration const fontUrls = getUrlsFromCssString(srcDeclaration); fontFaceUrls.push(...fontUrls); }); }); return fontFaceUrls; }; /** * * @param {String} html * @returns {String[]} */ const getImageUrlsFromFromHtml = function (html) { return getUrlsFromCssString(html, 'src=', [' ', '>', '\t'], true); }; const escapeRegExp = function (string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string }; /** * Create a fallback font-face rule for handling CORS errors * @returns {String} */ const createFallbackFontFaceRules = function () { return ` @font-face { font-family: 'CORS-fallback-serif'; src: local('Times New Roman'), local('Georgia'), serif; } @font-face { font-family: 'CORS-fallback-sans'; src: local('Arial'), local('Helvetica'), sans-serif; } /* Add fallback font handling */ [data-font-error] { font-family: 'CORS-fallback-sans', sans-serif !important; } [data-font-error="serif"] { font-family: 'CORS-fallback-serif', serif !important; } `; }; /** * Clean up and optimize CSS for better rendering * @param {String} cssStyles * @returns {String} */ const optimizeCssForRendering = function (cssStyles) { // Add fallback font-face rules const enhanced = cssStyles + createFallbackFontFaceRules(); // Replace problematic font-face declarations with proxied versions let optimized = enhanced.replace(/(url\(['"]?)(https?:\/\/[^)'"]+)(['"]?\))/gi, (match, prefix, url, suffix) => { // If it's a font file, proxy it if (url.match(/\.(woff2?|ttf|eot|otf)(\?.*)?$/i)) { return `${prefix}${CorsProxy(url)}${suffix}`; } return match; }); // Add error handling for fonts optimized += ` /* Suppress font CORS errors in console */ @supports (font-display: swap) { @font-face { font-display: swap !important; } } `; return optimized; }; /** * * @param {String} contentHtml * @param {Number} width * @param {Number} height * * @returns {Promise} */ const buildSvgDataUri = (webUrl, inputContentHtml, width, height, scroll, xoff) => { /* !! The problems !! * 1. CORS (not really an issue, expect perhaps for images, as this is a general security consideration to begin with) * 2. Platform won't wait for external assets to load (fonts, images, etc.) */ // copy styles let cssStyles = ''; const urlsFoundInCss = []; const fontUrlsInCss = []; for (let i = 0; i < styleSheets.length; i += 1) { try { const rules = styleSheets[i].cssRules; for (let j = 0; j < rules.length; j += 1) { const cssRuleStr = rules[j].cssText; urlsFoundInCss.push(...getUrlsFromCssString(cssRuleStr)); fontUrlsInCss.push(...getFontFaceUrlsFromCss(cssRuleStr)); cssStyles += cssRuleStr; } } catch (e) { /* empty */ } } // Optimize and enhance CSS cssStyles = optimizeCssForRendering(cssStyles); // const fetchedResourcesFromStylesheets = await getMultipleResourcesAsBase64(webUrl, urlsFoundInCss); // for (let i = 0; i < fetchedResourcesFromStylesheets.length; i++) { // const r = fetchedResourcesFromStylesheets[i]; // if (r.resourceUrl) { // cssStyles = cssStyles.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64); // } // } let contentHtml = inputContentHtml .replace(/]*>/g, '') // tags have a which has a srcset field of image refs. instead of converting each, just use the default of the picture .replace(/noscript/g, 'div') .replace(/
<\/div>/g, '') // when scripting isn't available (ie, rendering web pages here),