'use strict'; const node_fs = require('node:fs'); const path = require('node:path'); const postcss = require('postcss'); const mediaParser = require('postcss-media-query-parser'); const cssSelect = require('css-select'); const cssWhat = require('css-what'); const render = require('dom-serializer'); const domhandler = require('domhandler'); const htmlparser2 = require('htmlparser2'); const pc = require('picocolors'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const path__default = /*#__PURE__*/_interopDefaultCompat(path); const mediaParser__default = /*#__PURE__*/_interopDefaultCompat(mediaParser); const render__default = /*#__PURE__*/_interopDefaultCompat(render); const pc__default = /*#__PURE__*/_interopDefaultCompat(pc); function parseStylesheet(stylesheet) { return postcss.parse(stylesheet); } function serializeStylesheet(ast, options) { let cssStr = ""; postcss.stringify(ast, (result, node, type) => { if (node?.type === "decl" && node.value.includes("")) { return; } if (!options.compress) { cssStr += result; return; } if (node?.type === "comment") return; if (node?.type === "decl") { const prefix = node.prop + node.raws.between; cssStr += result.replace(prefix, prefix.trim()); return; } if (type === "start") { if (node?.type === "rule" && node.selectors) { cssStr += `${node.selectors.join(",")}{`; } else { cssStr += result.replace(/\s\{$/, "{"); } return; } if (type === "end" && result === "}" && node?.raws?.semicolon) { cssStr = cssStr.slice(0, -1); } cssStr += result.trim(); }); return cssStr; } function markOnly(predicate) { return (rule) => { const sel = "selectors" in rule ? rule.selectors : void 0; if (predicate(rule) === false) { rule.$$remove = true; } if ("selectors" in rule) { rule.$$markedSelectors = rule.selectors; rule.selectors = sel; } if (rule._other) { rule._other.$$markedSelectors = rule._other.selectors; } }; } function applyMarkedSelectors(rule) { if (rule.$$markedSelectors) { rule.selectors = rule.$$markedSelectors; } if (rule._other) { applyMarkedSelectors(rule._other); } } function walkStyleRules(node, iterator) { if (!("nodes" in node)) { return; } node.nodes = node.nodes?.filter((rule) => { if (hasNestedRules(rule)) { walkStyleRules(rule, iterator); } rule._other = void 0; rule.filterSelectors = filterSelectors; return iterator(rule) !== false; }); } function walkStyleRulesWithReverseMirror(node, node2, iterator) { if (!node2) return walkStyleRules(node, iterator); [node.nodes, node2.nodes] = splitFilter( node.nodes, node2.nodes, (rule, index, _rules, rules2) => { const rule2 = rules2?.[index]; if (hasNestedRules(rule)) { walkStyleRulesWithReverseMirror(rule, rule2, iterator); } rule._other = rule2; rule.filterSelectors = filterSelectors; return iterator(rule) !== false; } ); } function hasNestedRules(rule) { return "nodes" in rule && !!rule.nodes?.length && (!("name" in rule) || rule.name !== "keyframes" && rule.name !== "-webkit-keyframes") && rule.nodes.some((n) => n.type === "rule" || n.type === "atrule"); } function splitFilter(a, b, predicate) { const aOut = []; const bOut = []; for (let index = 0; index < a.length; index++) { const item = a[index]; if (predicate(item, index, a, b)) { aOut.push(item); } else { bOut.push(item); } } return [aOut, bOut]; } function filterSelectors(predicate) { if (this._other) { const [a, b] = splitFilter( this.selectors, this._other.selectors, predicate ); this.selectors = a; this._other.selectors = b; } else { this.selectors = this.selectors.filter(predicate); } } const MEDIA_TYPES = /* @__PURE__ */ new Set(["all", "print", "screen", "speech"]); const MEDIA_KEYWORDS = /* @__PURE__ */ new Set(["and", "not", ","]); const MEDIA_FEATURES = new Set( [ "width", "aspect-ratio", "color", "color-index", "grid", "height", "monochrome", "orientation", "resolution", "scan" ].flatMap((feature) => [feature, `min-${feature}`, `max-${feature}`]) ); function validateMediaType(node) { const { type: nodeType, value: nodeValue } = node; if (nodeType === "media-type") { return MEDIA_TYPES.has(nodeValue); } else if (nodeType === "keyword") { return MEDIA_KEYWORDS.has(nodeValue); } else if (nodeType === "media-feature") { return MEDIA_FEATURES.has(nodeValue); } } function validateMediaQuery(query) { const mediaParserFn = "default" in mediaParser__default ? mediaParser__default.default : mediaParser__default; const mediaTree = mediaParserFn(query); const nodeTypes = /* @__PURE__ */ new Set(["media-type", "keyword", "media-feature"]); const stack = [mediaTree]; while (stack.length > 0) { const node = stack.pop(); if (nodeTypes.has(node.type) && !validateMediaType(node)) { return false; } if (node.nodes) { stack.push(...node.nodes); } } return true; } let classCache = null; let idCache = null; function buildCache(container) { classCache = /* @__PURE__ */ new Set(); idCache = /* @__PURE__ */ new Set(); const queue = [container]; while (queue.length) { const node = queue.shift(); if (node.hasAttribute?.("class")) { const classList = node.getAttribute("class").trim().split(" "); classList.forEach((cls) => { classCache.add(cls); }); } if (node.hasAttribute?.("id")) { const id = node.getAttribute("id").trim(); idCache.add(id); } if ("children" in node) { queue.push(...node.children.filter((child) => child.type === "tag")); } } } function createDocument(html) { const document = htmlparser2.parseDocument(html, { decodeEntities: false }); extendDocument(document); extendElement(domhandler.Element.prototype); let beastiesContainer = document.querySelector("[data-beasties-container]"); if (!beastiesContainer) { document.documentElement?.setAttribute("data-beasties-container", ""); beastiesContainer = document.documentElement || document; } document.beastiesContainer = beastiesContainer; buildCache(beastiesContainer); return document; } function serializeDocument(document) { return render__default(document, { decodeEntities: false }); } let extended = false; function extendElement(element) { if (extended) { return; } extended = true; Object.defineProperties(element, { nodeName: { get() { return this.tagName.toUpperCase(); } }, id: { get() { return this.getAttribute("id"); }, set(value) { this.setAttribue("id", value); } }, className: { get() { return this.getAttribute("class"); }, set(value) { this.setAttribute("class", value); } }, insertBefore: { value(child, referenceNode) { if (!referenceNode) return this.appendChild(child); htmlparser2.DomUtils.prepend(referenceNode, child); return child; } }, appendChild: { value(child) { htmlparser2.DomUtils.appendChild(this, child); return child; } }, removeChild: { value(child) { htmlparser2.DomUtils.removeElement(child); } }, remove: { value() { htmlparser2.DomUtils.removeElement(this); } }, textContent: { get() { return htmlparser2.DomUtils.getText(this); }, set(text) { this.children = []; htmlparser2.DomUtils.appendChild(this, new domhandler.Text(text)); } }, setAttribute: { value(name, value) { if (this.attribs == null) this.attribs = {}; if (value == null) value = ""; this.attribs[name] = value; } }, removeAttribute: { value(name) { if (this.attribs != null) { delete this.attribs[name]; } } }, getAttribute: { value(name) { return this.attribs != null && this.attribs[name]; } }, hasAttribute: { value(name) { return this.attribs != null && this.attribs[name] != null; } }, getAttributeNode: { value(name) { const value = this.getAttribute(name); if (value != null) return { specified: true, value }; } }, exists: { value(sel) { return cachedQuerySelector(sel, this); } }, querySelector: { value(sel) { return cssSelect.selectOne(sel, this); } }, querySelectorAll: { value(sel) { return cssSelect.selectAll(sel, this); } } }); } function extendDocument(document) { Object.defineProperties(document, { // document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE. // TODO: verify if these are needed for css-select nodeType: { get() { return 9; } }, contentType: { get() { return "text/html"; } }, nodeName: { get() { return "#document"; } }, documentElement: { get() { return this.children.find( (child) => "tagName" in child && String(child.tagName).toLowerCase() === "html" ); } }, head: { get() { return this.querySelector("head"); } }, body: { get() { return this.querySelector("body"); } }, createElement: { value(name) { return new domhandler.Element(name, {}); } }, createTextNode: { value(text) { return new domhandler.Text(text); } }, exists: { value(sel) { return cachedQuerySelector(sel, this); } }, querySelector: { value(sel) { return cssSelect.selectOne(sel, this); } }, querySelectorAll: { value(sel) { if (sel === ":root") { return this; } return cssSelect.selectAll(sel, this); } } }); } function cachedQuerySelector(sel, node) { const selectorTokens = cssWhat.parse(sel); for (const tokens of selectorTokens) { if (tokens.length === 1) { const token = tokens[0]; if (token.type === "attribute" && token.name === "class") { return classCache.has(token.value); } if (token.type === "attribute" && token.name === "id") { return idCache.has(token.value); } } } return !!cssSelect.selectOne(sel, node); } const LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "silent"]; const defaultLogger = { trace(msg) { console.trace(msg); }, debug(msg) { console.debug(msg); }, warn(msg) { console.warn(pc__default.yellow(msg)); }, error(msg) { console.error(pc__default.bold(pc__default.red(msg))); }, info(msg) { console.info(pc__default.bold(pc__default.blue(msg))); }, silent() { } }; function createLogger(logLevel) { const logLevelIdx = LOG_LEVELS.indexOf(logLevel); return LOG_LEVELS.reduce((logger, type, index) => { if (index >= logLevelIdx) { logger[type] = defaultLogger[type]; } else { logger[type] = defaultLogger.silent; } return logger; }, {}); } function isSubpath(basePath, currentPath) { return !path__default.relative(basePath, currentPath).startsWith(".."); } var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class Beasties { constructor(options = {}) { __publicField(this, "options"); __publicField(this, "logger"); __publicField(this, "fs"); this.options = Object.assign({ logLevel: "info", path: "", publicPath: "", reduceInlineStyles: true, pruneSource: false, additionalStylesheets: [], allowRules: [] }, options); this.logger = this.options.logger || createLogger(this.options.logLevel); } /** * Read the contents of a file from the specified filesystem or disk */ readFile(filename) { const fs = this.fs; return new Promise((resolve, reject) => { const callback = (err, data) => { if (err) reject(err); else resolve(data.toString()); }; if (fs && fs.readFile) { fs.readFile(filename, callback); } else { node_fs.readFile(filename, "utf-8", callback); } }); } /** * Apply critical CSS processing to the html */ async process(html) { const start = Date.now(); const document = createDocument(html); if (this.options.additionalStylesheets.length > 0) { this.embedAdditionalStylesheet(document); } if (this.options.external !== false) { const externalSheets = [].slice.call( document.querySelectorAll('link[rel="stylesheet"]') ); await Promise.all( externalSheets.map((link) => this.embedLinkedStylesheet(link, document)) ); } const styles = this.getAffectedStyleTags(document); await Promise.all( styles.map((style) => this.processStyle(style, document)) ); if (this.options.mergeStylesheets !== false && styles.length !== 0) { await this.mergeStylesheets(document); } const output = serializeDocument(document); const end = Date.now(); this.logger.info?.(`Time ${end - start}ms`); return output; } /** * Get the style tags that need processing */ getAffectedStyleTags(document) { const styles = [...document.querySelectorAll("style")]; if (this.options.reduceInlineStyles === false) { return styles.filter((style) => style.$$external); } return styles; } async mergeStylesheets(document) { const styles = this.getAffectedStyleTags(document); if (styles.length === 0) { this.logger.warn?.( "Merging inline stylesheets into a single