const Entities = require('html-entities').AllHtmlEntities; import { CssTypes, parse as cssParse, stringify as cssStringify } from '@adobe/css-tools'; import { dirname, basename } from 'path'; import parseHtmlAsync, { HtmlAttrs } from './utils/parseHtmlAsync'; const selfClosingElements = [ 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr', ]; const htmlentities = (s: string): string => { const output = (new Entities()).encode(s); return output.replace(/ /ig, '\t'); }; const attributesHtml = (attrs: HtmlAttrs) => { const output: string[] = []; for (const n in attrs) { if (!attrs.hasOwnProperty(n)) continue; output.push(`${n}="${htmlentities(attrs[n])}"`); } return output.join(' '); }; const attrValue = (attrs: HtmlAttrs, name: string): string => { if (!attrs[name]) return ''; return attrs[name]; }; const isSelfClosingTag = (tagName: string) => { return selfClosingElements.includes(tagName.toLowerCase()); }; export type FileApi = { exists(path: string): Promise; readFileText(path: string): Promise; readFileDataUri(path: string): Promise; }; // packToString should be able to run in React Native -- don't use fs-extra. const packToString = async (baseDir: string, inputFileText: string, fs: FileApi) => { const readFileDataUriSafe = async (path: string) => { try { return await fs.readFileDataUri(path); } catch (error) { // If the file path is invalid, the Datauri will throw an exception. // Instead, since we can just ignore that particular file. // Fixes https://github.com/laurent22/joplin/issues/8305 return ''; } }; const processCssContent = async (cssBaseDir: string, content: string) => { const o = cssParse(content, { silent: false, }); for (const rule of o.stylesheet.rules) { if (rule.type === 'font-face') { for (const declaration of rule.declarations) { if (declaration.type === CssTypes.comment) { continue; } if (declaration.property === 'src') { const replacements = new Map(); const replacementTasks: Promise[] = []; declaration.value.replace(/url\((.*?)\)/g, (match: string, url: string) => { if (replacements.has(url)) return match; replacements.set(url, match); replacementTasks.push((async () => { const cssFilePath = `${cssBaseDir}/${url}`; let replacement; if (await fs.exists(cssFilePath)) { replacement = `url(${await readFileDataUriSafe(cssFilePath)})`; } else { replacement = `url(${url})`; } replacements.set(url, replacement); })()); return match; }); await Promise.all(replacementTasks); declaration.value = declaration.value.replace(/url\((.*?)\)/g, (_match: string, url: string) => { return replacements.get(url); }); } } } } return cssStringify(o); }; const processLinkTag = async (_name: string, attrs: HtmlAttrs) => { const href = attrValue(attrs, 'href'); if (!href) return null; const filePath = `${baseDir}/${href}`; if (!await fs.exists(filePath)) return null; const content = await fs.readFileText(filePath); return ``; }; const processScriptTag = async (_name: string, attrs: HtmlAttrs) => { const src = attrValue(attrs, 'src'); if (!src) return null; const scriptFilePath = `${baseDir}/${src}`; let content = await fs.readFileText(scriptFilePath); // There's no simple way to insert arbitrary content in ` or `