index.cjs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  1. 'use strict';
  2. const node_fs = require('node:fs');
  3. const path = require('node:path');
  4. const postcss = require('postcss');
  5. const mediaParser = require('postcss-media-query-parser');
  6. const cssSelect = require('css-select');
  7. const cssWhat = require('css-what');
  8. const render = require('dom-serializer');
  9. const domhandler = require('domhandler');
  10. const htmlparser2 = require('htmlparser2');
  11. const pc = require('picocolors');
  12. function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
  13. const path__default = /*#__PURE__*/_interopDefaultCompat(path);
  14. const mediaParser__default = /*#__PURE__*/_interopDefaultCompat(mediaParser);
  15. const render__default = /*#__PURE__*/_interopDefaultCompat(render);
  16. const pc__default = /*#__PURE__*/_interopDefaultCompat(pc);
  17. function parseStylesheet(stylesheet) {
  18. return postcss.parse(stylesheet);
  19. }
  20. function serializeStylesheet(ast, options) {
  21. const cssParts = [];
  22. postcss.stringify(ast, (result, node, type) => {
  23. if (node?.type === "decl" && node.value.includes("</style>")) {
  24. return;
  25. }
  26. if (!options.compress) {
  27. cssParts.push(result);
  28. return;
  29. }
  30. if (node?.type === "comment")
  31. return;
  32. if (node?.type === "decl") {
  33. const prefix = node.prop + node.raws.between;
  34. cssParts.push(result.replace(prefix, prefix.trim()));
  35. return;
  36. }
  37. if (type === "start") {
  38. if (node?.type === "rule" && node.selectors) {
  39. if (node.selectors.length === 1) {
  40. cssParts.push(node.selectors[0] ?? "", "{");
  41. } else {
  42. cssParts.push(node.selectors.join(","), "{");
  43. }
  44. } else {
  45. cssParts.push(result.trim());
  46. }
  47. return;
  48. }
  49. if (type === "end" && result === "}" && node?.raws?.semicolon) {
  50. const lastItemIdx = cssParts.length - 2;
  51. if (lastItemIdx >= 0 && cssParts[lastItemIdx]) {
  52. cssParts[lastItemIdx] = cssParts[lastItemIdx].slice(0, -1);
  53. }
  54. }
  55. cssParts.push(result.trim());
  56. });
  57. return cssParts.join("");
  58. }
  59. function markOnly(predicate) {
  60. return (rule) => {
  61. const sel = "selectors" in rule ? rule.selectors : void 0;
  62. if (predicate(rule) === false) {
  63. rule.$$remove = true;
  64. }
  65. if ("selectors" in rule) {
  66. rule.$$markedSelectors = rule.selectors;
  67. rule.selectors = sel;
  68. }
  69. if (rule._other) {
  70. rule._other.$$markedSelectors = rule._other.selectors;
  71. }
  72. };
  73. }
  74. function applyMarkedSelectors(rule) {
  75. if (rule.$$markedSelectors) {
  76. rule.selectors = rule.$$markedSelectors;
  77. }
  78. if (rule._other) {
  79. applyMarkedSelectors(rule._other);
  80. }
  81. }
  82. function walkStyleRules(node, iterator) {
  83. if (!("nodes" in node)) {
  84. return;
  85. }
  86. node.nodes = node.nodes?.filter((rule) => {
  87. if (hasNestedRules(rule)) {
  88. walkStyleRules(rule, iterator);
  89. }
  90. rule._other = void 0;
  91. rule.filterSelectors = filterSelectors;
  92. return iterator(rule) !== false;
  93. });
  94. }
  95. function walkStyleRulesWithReverseMirror(node, node2, iterator) {
  96. if (!node2)
  97. return walkStyleRules(node, iterator);
  98. [node.nodes, node2.nodes] = splitFilter(
  99. node.nodes,
  100. node2.nodes,
  101. (rule, index, _rules, rules2) => {
  102. const rule2 = rules2?.[index];
  103. if (hasNestedRules(rule)) {
  104. walkStyleRulesWithReverseMirror(rule, rule2, iterator);
  105. }
  106. rule._other = rule2;
  107. rule.filterSelectors = filterSelectors;
  108. return iterator(rule) !== false;
  109. }
  110. );
  111. }
  112. function hasNestedRules(rule) {
  113. 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");
  114. }
  115. function splitFilter(a, b, predicate) {
  116. const aOut = [];
  117. const bOut = [];
  118. for (let index = 0; index < a.length; index++) {
  119. const item = a[index];
  120. if (predicate(item, index, a, b)) {
  121. aOut.push(item);
  122. } else {
  123. bOut.push(item);
  124. }
  125. }
  126. return [aOut, bOut];
  127. }
  128. function filterSelectors(predicate) {
  129. if (this._other) {
  130. const [a, b] = splitFilter(
  131. this.selectors,
  132. this._other.selectors,
  133. predicate
  134. );
  135. this.selectors = a;
  136. this._other.selectors = b;
  137. } else {
  138. this.selectors = this.selectors.filter(predicate);
  139. }
  140. }
  141. const MEDIA_TYPES = /* @__PURE__ */ new Set(["all", "print", "screen", "speech"]);
  142. const MEDIA_KEYWORDS = /* @__PURE__ */ new Set(["and", "not", ","]);
  143. const MEDIA_FEATURES = new Set(
  144. [
  145. "width",
  146. "aspect-ratio",
  147. "color",
  148. "color-index",
  149. "grid",
  150. "height",
  151. "monochrome",
  152. "orientation",
  153. "resolution",
  154. "scan"
  155. ].flatMap((feature) => [feature, `min-${feature}`, `max-${feature}`])
  156. );
  157. function validateMediaType(node) {
  158. const { type: nodeType, value: nodeValue } = node;
  159. if (nodeType === "media-type") {
  160. return MEDIA_TYPES.has(nodeValue);
  161. } else if (nodeType === "keyword") {
  162. return MEDIA_KEYWORDS.has(nodeValue);
  163. } else if (nodeType === "media-feature") {
  164. return MEDIA_FEATURES.has(nodeValue);
  165. }
  166. }
  167. function validateMediaQuery(query) {
  168. const mediaParserFn = "default" in mediaParser__default ? mediaParser__default.default : mediaParser__default;
  169. const mediaTree = mediaParserFn(query);
  170. const nodeTypes = /* @__PURE__ */ new Set(["media-type", "keyword", "media-feature"]);
  171. const stack = [mediaTree];
  172. while (stack.length > 0) {
  173. const node = stack.pop();
  174. if (nodeTypes.has(node.type) && !validateMediaType(node)) {
  175. return false;
  176. }
  177. if (node.nodes) {
  178. stack.push(...node.nodes);
  179. }
  180. }
  181. return true;
  182. }
  183. let classCache = null;
  184. let idCache = null;
  185. function buildCache(container) {
  186. classCache = /* @__PURE__ */ new Set();
  187. idCache = /* @__PURE__ */ new Set();
  188. const queue = [container];
  189. while (queue.length) {
  190. const node = queue.shift();
  191. if (node.hasAttribute?.("class")) {
  192. const classList = node.getAttribute("class").trim().split(" ");
  193. classList.forEach((cls) => {
  194. classCache.add(cls);
  195. });
  196. }
  197. if (node.hasAttribute?.("id")) {
  198. const id = node.getAttribute("id").trim();
  199. idCache.add(id);
  200. }
  201. if ("children" in node) {
  202. queue.push(...node.children.filter((child) => child.type === "tag"));
  203. }
  204. }
  205. }
  206. function createDocument(html) {
  207. const document = htmlparser2.parseDocument(html, { decodeEntities: false });
  208. extendDocument(document);
  209. extendElement(domhandler.Element.prototype);
  210. let beastiesContainer = document.querySelector("[data-beasties-container]");
  211. if (!beastiesContainer) {
  212. document.documentElement?.setAttribute("data-beasties-container", "");
  213. beastiesContainer = document.documentElement || document;
  214. }
  215. document.beastiesContainer = beastiesContainer;
  216. buildCache(beastiesContainer);
  217. return document;
  218. }
  219. function serializeDocument(document) {
  220. return render__default(document, { decodeEntities: false });
  221. }
  222. let extended = false;
  223. function extendElement(element) {
  224. if (extended) {
  225. return;
  226. }
  227. extended = true;
  228. Object.defineProperties(element, {
  229. nodeName: {
  230. get() {
  231. return this.tagName.toUpperCase();
  232. }
  233. },
  234. id: {
  235. get() {
  236. return this.getAttribute("id");
  237. },
  238. set(value) {
  239. this.setAttribute("id", value);
  240. }
  241. },
  242. className: {
  243. get() {
  244. return this.getAttribute("class");
  245. },
  246. set(value) {
  247. this.setAttribute("class", value);
  248. }
  249. },
  250. insertBefore: {
  251. value(child, referenceNode) {
  252. if (!referenceNode)
  253. return this.appendChild(child);
  254. htmlparser2.DomUtils.prepend(referenceNode, child);
  255. return child;
  256. }
  257. },
  258. appendChild: {
  259. value(child) {
  260. htmlparser2.DomUtils.appendChild(this, child);
  261. return child;
  262. }
  263. },
  264. removeChild: {
  265. value(child) {
  266. htmlparser2.DomUtils.removeElement(child);
  267. }
  268. },
  269. remove: {
  270. value() {
  271. htmlparser2.DomUtils.removeElement(this);
  272. }
  273. },
  274. textContent: {
  275. get() {
  276. return htmlparser2.DomUtils.getText(this);
  277. },
  278. set(text) {
  279. this.children = [];
  280. htmlparser2.DomUtils.appendChild(this, new domhandler.Text(text));
  281. }
  282. },
  283. setAttribute: {
  284. value(name, value) {
  285. if (this.attribs == null)
  286. this.attribs = {};
  287. if (value == null)
  288. value = "";
  289. this.attribs[name] = value;
  290. }
  291. },
  292. removeAttribute: {
  293. value(name) {
  294. if (this.attribs != null) {
  295. delete this.attribs[name];
  296. }
  297. }
  298. },
  299. getAttribute: {
  300. value(name) {
  301. return this.attribs != null && this.attribs[name];
  302. }
  303. },
  304. hasAttribute: {
  305. value(name) {
  306. return this.attribs != null && this.attribs[name] != null;
  307. }
  308. },
  309. getAttributeNode: {
  310. value(name) {
  311. const value = this.getAttribute(name);
  312. if (value != null)
  313. return { specified: true, value };
  314. }
  315. },
  316. exists: {
  317. value(sel) {
  318. return cachedQuerySelector(sel, this);
  319. }
  320. },
  321. querySelector: {
  322. value(sel) {
  323. return cssSelect.selectOne(sel, this);
  324. }
  325. },
  326. querySelectorAll: {
  327. value(sel) {
  328. return cssSelect.selectAll(sel, this);
  329. }
  330. }
  331. });
  332. }
  333. function extendDocument(document) {
  334. Object.defineProperties(document, {
  335. // document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE.
  336. // TODO: verify if these are needed for css-select
  337. nodeType: {
  338. get() {
  339. return 9;
  340. }
  341. },
  342. contentType: {
  343. get() {
  344. return "text/html";
  345. }
  346. },
  347. nodeName: {
  348. get() {
  349. return "#document";
  350. }
  351. },
  352. documentElement: {
  353. get() {
  354. return this.children.find(
  355. (child) => "tagName" in child && String(child.tagName).toLowerCase() === "html"
  356. );
  357. }
  358. },
  359. head: {
  360. get() {
  361. return this.querySelector("head");
  362. }
  363. },
  364. body: {
  365. get() {
  366. return this.querySelector("body");
  367. }
  368. },
  369. createElement: {
  370. value(name) {
  371. return new domhandler.Element(name, {});
  372. }
  373. },
  374. createTextNode: {
  375. value(text) {
  376. return new domhandler.Text(text);
  377. }
  378. },
  379. exists: {
  380. value(sel) {
  381. return cachedQuerySelector(sel, this);
  382. }
  383. },
  384. querySelector: {
  385. value(sel) {
  386. return cssSelect.selectOne(sel, this);
  387. }
  388. },
  389. querySelectorAll: {
  390. value(sel) {
  391. if (sel === ":root") {
  392. return this;
  393. }
  394. return cssSelect.selectAll(sel, this);
  395. }
  396. }
  397. });
  398. }
  399. const selectorTokensCache = /* @__PURE__ */ new Map();
  400. function cachedQuerySelector(sel, node) {
  401. let selectorTokens = selectorTokensCache.get(sel);
  402. if (selectorTokens === void 0) {
  403. selectorTokens = parseRelevantSelectors(sel);
  404. selectorTokensCache.set(sel, selectorTokens);
  405. }
  406. if (selectorTokens) {
  407. for (const token of selectorTokens) {
  408. if (token.name === "class") {
  409. return classCache.has(token.value);
  410. }
  411. if (token.name === "id") {
  412. return idCache.has(token.value);
  413. }
  414. }
  415. }
  416. return !!cssSelect.selectOne(sel, node);
  417. }
  418. function parseRelevantSelectors(sel) {
  419. const tokens = cssWhat.parse(sel);
  420. const relevantTokens = [];
  421. for (let i = 0; i < tokens.length; i++) {
  422. const tokenGroup = tokens[i];
  423. if (tokenGroup?.length !== 1) {
  424. continue;
  425. }
  426. const token = tokenGroup[0];
  427. if (token?.type === "attribute" && (token.name === "class" || token.name === "id")) {
  428. relevantTokens.push(token);
  429. }
  430. }
  431. return relevantTokens.length > 0 ? relevantTokens : null;
  432. }
  433. const LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "silent"];
  434. const defaultLogger = {
  435. trace(msg) {
  436. console.trace(msg);
  437. },
  438. debug(msg) {
  439. console.debug(msg);
  440. },
  441. warn(msg) {
  442. console.warn(pc__default.yellow(msg));
  443. },
  444. error(msg) {
  445. console.error(pc__default.bold(pc__default.red(msg)));
  446. },
  447. info(msg) {
  448. console.info(pc__default.bold(pc__default.blue(msg)));
  449. },
  450. silent() {
  451. }
  452. };
  453. function createLogger(logLevel) {
  454. const logLevelIdx = LOG_LEVELS.indexOf(logLevel);
  455. return LOG_LEVELS.reduce((logger, type, index) => {
  456. if (index >= logLevelIdx) {
  457. logger[type] = defaultLogger[type];
  458. } else {
  459. logger[type] = defaultLogger.silent;
  460. }
  461. return logger;
  462. }, {});
  463. }
  464. function isSubpath(basePath, currentPath) {
  465. return !path__default.relative(basePath, currentPath).startsWith("..");
  466. }
  467. var __defProp = Object.defineProperty;
  468. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  469. var __publicField = (obj, key, value) => {
  470. __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  471. return value;
  472. };
  473. var __accessCheck = (obj, member, msg) => {
  474. if (!member.has(obj))
  475. throw TypeError("Cannot " + msg);
  476. };
  477. var __privateGet = (obj, member, getter) => {
  478. __accessCheck(obj, member, "read from private field");
  479. return getter ? getter.call(obj) : member.get(obj);
  480. };
  481. var __privateAdd = (obj, member, value) => {
  482. if (member.has(obj))
  483. throw TypeError("Cannot add the same private member more than once");
  484. member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
  485. };
  486. var _selectorCache;
  487. const removePseudoClassesAndElementsPattern = /(?<!\\)::?[a-z-]+(?:\(.+\))?/gi;
  488. const removeTrailingCommasPattern = /\(\s*,|,\s*\)/g;
  489. class Beasties {
  490. constructor(options = {}) {
  491. __privateAdd(this, _selectorCache, /* @__PURE__ */ new Map());
  492. __publicField(this, "options");
  493. __publicField(this, "logger");
  494. __publicField(this, "fs");
  495. this.options = Object.assign({
  496. logLevel: "info",
  497. path: "",
  498. publicPath: "",
  499. reduceInlineStyles: true,
  500. pruneSource: false,
  501. additionalStylesheets: [],
  502. allowRules: []
  503. }, options);
  504. this.logger = this.options.logger || createLogger(this.options.logLevel);
  505. }
  506. /**
  507. * Read the contents of a file from the specified filesystem or disk
  508. */
  509. readFile(filename) {
  510. const fs = this.fs;
  511. return new Promise((resolve, reject) => {
  512. const callback = (err, data) => {
  513. if (err)
  514. reject(err);
  515. else
  516. resolve(data.toString());
  517. };
  518. if (fs && fs.readFile) {
  519. fs.readFile(filename, callback);
  520. } else {
  521. node_fs.readFile(filename, "utf-8", callback);
  522. }
  523. });
  524. }
  525. /**
  526. * Apply critical CSS processing to the html
  527. */
  528. async process(html) {
  529. const start = Date.now();
  530. const document = createDocument(html);
  531. if (this.options.additionalStylesheets.length > 0) {
  532. await this.embedAdditionalStylesheet(document);
  533. }
  534. if (this.options.external !== false) {
  535. const externalSheets = [...document.querySelectorAll('link[rel="stylesheet"]')];
  536. await Promise.all(
  537. externalSheets.map((link) => this.embedLinkedStylesheet(link, document))
  538. );
  539. }
  540. const styles = this.getAffectedStyleTags(document);
  541. for (const style of styles) {
  542. this.processStyle(style, document);
  543. }
  544. if (this.options.mergeStylesheets !== false && styles.length !== 0) {
  545. this.mergeStylesheets(document);
  546. }
  547. const output = serializeDocument(document);
  548. const end = Date.now();
  549. this.logger.info?.(`Time ${end - start}ms`);
  550. return output;
  551. }
  552. /**
  553. * Get the style tags that need processing
  554. */
  555. getAffectedStyleTags(document) {
  556. const styles = [...document.querySelectorAll("style")];
  557. if (this.options.reduceInlineStyles === false) {
  558. return styles.filter((style) => style.$$external);
  559. }
  560. return styles;
  561. }
  562. mergeStylesheets(document) {
  563. const styles = this.getAffectedStyleTags(document);
  564. if (styles.length === 0) {
  565. this.logger.warn?.(
  566. "Merging inline stylesheets into a single <style> tag skipped, no inline stylesheets to merge"
  567. );
  568. return;
  569. }
  570. const first = styles[0];
  571. let sheet = first.textContent;
  572. for (let i = 1; i < styles.length; i++) {
  573. const node = styles[i];
  574. sheet += node.textContent;
  575. node.remove();
  576. }
  577. first.textContent = sheet;
  578. }
  579. /**
  580. * Given href, find the corresponding CSS asset
  581. */
  582. async getCssAsset(href, _style) {
  583. const outputPath = this.options.path;
  584. const publicPath = this.options.publicPath;
  585. let normalizedPath = href.replace(/^\//, "");
  586. const pathPrefix = `${(publicPath || "").replace(/(^\/|\/$)/g, "")}/`;
  587. if (normalizedPath.startsWith(pathPrefix)) {
  588. normalizedPath = normalizedPath.substring(pathPrefix.length).replace(/^\//, "");
  589. }
  590. if (/^https?:\/\//.test(normalizedPath) || href.startsWith("//")) {
  591. return void 0;
  592. }
  593. const filename = path__default.resolve(outputPath, normalizedPath);
  594. if (!isSubpath(outputPath, filename)) {
  595. return void 0;
  596. }
  597. let sheet;
  598. try {
  599. sheet = await this.readFile(filename);
  600. } catch {
  601. this.logger.warn?.(`Unable to locate stylesheet: ${filename}`);
  602. }
  603. return sheet;
  604. }
  605. checkInlineThreshold(link, style, sheet) {
  606. if (this.options.inlineThreshold && sheet.length < this.options.inlineThreshold) {
  607. const href = style.$$name;
  608. style.$$reduce = false;
  609. this.logger.info?.(
  610. `\x1B[32mInlined all of ${href} (${sheet.length} was below the threshold of ${this.options.inlineThreshold})\x1B[39m`
  611. );
  612. link.remove();
  613. return true;
  614. }
  615. return false;
  616. }
  617. /**
  618. * Inline the stylesheets from options.additionalStylesheets (assuming it passes `options.filter`)
  619. */
  620. async embedAdditionalStylesheet(document) {
  621. const styleSheetsIncluded = [];
  622. const sources = await Promise.all(
  623. this.options.additionalStylesheets.map((cssFile) => {
  624. if (styleSheetsIncluded.includes(cssFile)) {
  625. return [];
  626. }
  627. styleSheetsIncluded.push(cssFile);
  628. const style = document.createElement("style");
  629. style.$$external = true;
  630. return this.getCssAsset(cssFile, style).then((sheet) => [sheet, style]);
  631. })
  632. );
  633. for (const [sheet, style] of sources) {
  634. if (sheet) {
  635. style.textContent = sheet;
  636. document.head.appendChild(style);
  637. }
  638. }
  639. }
  640. /**
  641. * Inline the target stylesheet referred to by a <link rel="stylesheet"> (assuming it passes `options.filter`)
  642. */
  643. async embedLinkedStylesheet(link, document) {
  644. const href = link.getAttribute("href");
  645. let media = link.getAttribute("media");
  646. if (media && !validateMediaQuery(media)) {
  647. media = void 0;
  648. }
  649. const preloadMode = this.options.preload;
  650. if (!href?.endsWith(".css")) {
  651. return void 0;
  652. }
  653. const style = document.createElement("style");
  654. style.$$external = true;
  655. const sheet = await this.getCssAsset(href, style);
  656. if (!sheet) {
  657. return;
  658. }
  659. style.textContent = sheet;
  660. style.$$name = href;
  661. style.$$links = [link];
  662. link.parentNode?.insertBefore(style, link);
  663. if (this.checkInlineThreshold(link, style, sheet)) {
  664. return;
  665. }
  666. let cssLoaderPreamble = "function $loadcss(u,m,l){(l=document.createElement('link')).rel='stylesheet';l.href=u;document.head.appendChild(l)}";
  667. const lazy = preloadMode === "js-lazy";
  668. if (lazy) {
  669. cssLoaderPreamble = cssLoaderPreamble.replace(
  670. "l.href",
  671. "l.media='print';l.onload=function(){l.media=m};l.href"
  672. );
  673. }
  674. if (preloadMode === false)
  675. return;
  676. let noscriptFallback = false;
  677. let updateLinkToPreload = false;
  678. const noscriptLink = link.cloneNode(false);
  679. if (preloadMode === "body") {
  680. document.body.appendChild(link);
  681. } else {
  682. if (preloadMode === "js" || preloadMode === "js-lazy") {
  683. const script = document.createElement("script");
  684. script.setAttribute("data-href", href);
  685. script.setAttribute("data-media", media || "all");
  686. const js = `${cssLoaderPreamble}$loadcss(document.currentScript.dataset.href,document.currentScript.dataset.media)`;
  687. script.textContent = js;
  688. link.parentNode.insertBefore(script, link.nextSibling);
  689. style.$$links.push(script);
  690. cssLoaderPreamble = "";
  691. noscriptFallback = true;
  692. updateLinkToPreload = true;
  693. } else if (preloadMode === "media") {
  694. link.setAttribute("media", "print");
  695. link.setAttribute("onload", `this.media='${media || "all"}'`);
  696. noscriptFallback = true;
  697. } else if (preloadMode === "swap-high") {
  698. link.setAttribute("rel", "alternate stylesheet preload");
  699. link.setAttribute("title", "styles");
  700. link.setAttribute("onload", `this.title='';this.rel='stylesheet'`);
  701. noscriptFallback = true;
  702. } else if (preloadMode === "swap") {
  703. link.setAttribute("onload", "this.rel='stylesheet'");
  704. noscriptFallback = true;
  705. } else {
  706. const bodyLink = link.cloneNode(false);
  707. bodyLink.removeAttribute("id");
  708. document.body.appendChild(bodyLink);
  709. updateLinkToPreload = true;
  710. }
  711. }
  712. if (this.options.noscriptFallback !== false && noscriptFallback && !href.includes("</noscript>")) {
  713. const noscript = document.createElement("noscript");
  714. noscriptLink.removeAttribute("id");
  715. noscript.appendChild(noscriptLink);
  716. link.parentNode.insertBefore(noscript, link.nextSibling);
  717. style.$$links.push(noscript);
  718. }
  719. if (updateLinkToPreload) {
  720. link.setAttribute("rel", "preload");
  721. link.setAttribute("as", "style");
  722. }
  723. }
  724. /**
  725. * Prune the source CSS files
  726. */
  727. pruneSource(style, before, sheetInverse) {
  728. const minSize = this.options.minimumExternalSize;
  729. const name = style.$$name;
  730. if (minSize && sheetInverse.length < minSize) {
  731. this.logger.info?.(
  732. `\x1B[32mInlined all of ${name} (non-critical external stylesheet would have been ${sheetInverse.length}b, which was below the threshold of ${minSize})\x1B[39m`
  733. );
  734. style.textContent = before;
  735. if (style.$$links) {
  736. for (const link of style.$$links) {
  737. const parent = link.parentNode;
  738. if (parent)
  739. parent.removeChild(link);
  740. }
  741. }
  742. return true;
  743. }
  744. return false;
  745. }
  746. /**
  747. * Parse the stylesheet within a <style> element, then reduce it to contain only rules used by the document.
  748. */
  749. processStyle(style, document) {
  750. if (style.$$reduce === false)
  751. return;
  752. const name = style.$$name ? style.$$name.replace(/^\//, "") : "inline CSS";
  753. const options = this.options;
  754. const beastiesContainer = document.beastiesContainer;
  755. let keyframesMode = options.keyframes ?? "critical";
  756. if (keyframesMode === true)
  757. keyframesMode = "all";
  758. if (keyframesMode === false)
  759. keyframesMode = "none";
  760. let sheet = style.textContent;
  761. const before = sheet;
  762. if (!sheet)
  763. return;
  764. const ast = parseStylesheet(sheet);
  765. const astInverse = options.pruneSource ? parseStylesheet(sheet) : null;
  766. let criticalFonts = "";
  767. const failedSelectors = [];
  768. const criticalKeyframeNames = /* @__PURE__ */ new Set();
  769. let includeNext = false;
  770. let includeAll = false;
  771. let excludeNext = false;
  772. let excludeAll = false;
  773. const shouldPreloadFonts = options.fonts === true || options.preloadFonts === true;
  774. const shouldInlineFonts = options.fonts !== false && options.inlineFonts === true;
  775. walkStyleRules(
  776. ast,
  777. markOnly((rule) => {
  778. if (rule.type === "comment") {
  779. const beastiesComment = rule.text.match(/^(?<!! )beasties:(.*)/);
  780. const command = beastiesComment && beastiesComment[1];
  781. if (command) {
  782. switch (command) {
  783. case "include":
  784. includeNext = true;
  785. break;
  786. case "exclude":
  787. excludeNext = true;
  788. break;
  789. case "include start":
  790. includeAll = true;
  791. break;
  792. case "include end":
  793. includeAll = false;
  794. break;
  795. case "exclude start":
  796. excludeAll = true;
  797. break;
  798. case "exclude end":
  799. excludeAll = false;
  800. break;
  801. }
  802. }
  803. }
  804. if (rule.type === "rule") {
  805. if (includeNext) {
  806. includeNext = false;
  807. return true;
  808. }
  809. if (excludeNext) {
  810. excludeNext = false;
  811. return false;
  812. }
  813. if (includeAll) {
  814. return true;
  815. }
  816. if (excludeAll) {
  817. return false;
  818. }
  819. rule.filterSelectors?.((sel) => {
  820. const isAllowedRule = options.allowRules.some((exp) => {
  821. if (exp instanceof RegExp) {
  822. return exp.test(sel);
  823. }
  824. return exp === sel;
  825. });
  826. if (isAllowedRule)
  827. return true;
  828. if (sel === ":root" || sel === "html" || sel === "body" || sel[0] === ":" && /^::?(?:before|after)$/.test(sel)) {
  829. return true;
  830. }
  831. sel = this.normalizeCssSelector(sel);
  832. if (!sel)
  833. return false;
  834. try {
  835. return beastiesContainer.exists(sel);
  836. } catch (e) {
  837. failedSelectors.push(`${sel} -> ${e.message || e.toString()}`);
  838. return false;
  839. }
  840. });
  841. if (!rule.selector) {
  842. return false;
  843. }
  844. if (rule.nodes) {
  845. for (const decl of rule.nodes) {
  846. if (!("prop" in decl)) {
  847. continue;
  848. }
  849. if (shouldInlineFonts && /\bfont(?:-family)?\b/i.test(decl.prop)) {
  850. criticalFonts += ` ${decl.value}`;
  851. }
  852. if (decl.prop === "animation" || decl.prop === "animation-name") {
  853. for (const name2 of decl.value.split(/\s+/)) {
  854. const nameTrimmed = name2.trim();
  855. if (nameTrimmed)
  856. criticalKeyframeNames.add(nameTrimmed);
  857. }
  858. }
  859. }
  860. }
  861. }
  862. if (rule.type === "atrule" && rule.name === "font-face")
  863. return;
  864. const hasRemainingRules = ("nodes" in rule && rule.nodes?.some((rule2) => !rule2.$$remove)) ?? true;
  865. return hasRemainingRules;
  866. })
  867. );
  868. if (failedSelectors.length !== 0) {
  869. this.logger.warn?.(
  870. `${failedSelectors.length} rules skipped due to selector errors:
  871. ${failedSelectors.join(
  872. "\n "
  873. )}`
  874. );
  875. }
  876. const preloadedFonts = /* @__PURE__ */ new Set();
  877. walkStyleRulesWithReverseMirror(ast, astInverse, (rule) => {
  878. if (rule.$$remove === true)
  879. return false;
  880. if ("selectors" in rule) {
  881. applyMarkedSelectors(rule);
  882. }
  883. if (rule.type === "atrule" && rule.name === "keyframes") {
  884. if (keyframesMode === "none")
  885. return false;
  886. if (keyframesMode === "all")
  887. return true;
  888. return criticalKeyframeNames.has(rule.params);
  889. }
  890. if (rule.type === "atrule" && rule.name === "font-face") {
  891. let family, src;
  892. if (rule.nodes) {
  893. for (const decl of rule.nodes) {
  894. if (!("prop" in decl)) {
  895. continue;
  896. }
  897. if (decl.prop === "src") {
  898. src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2];
  899. } else if (decl.prop === "font-family") {
  900. family = decl.value;
  901. }
  902. }
  903. if (src && shouldPreloadFonts && !preloadedFonts.has(src)) {
  904. preloadedFonts.add(src);
  905. const preload = document.createElement("link");
  906. preload.setAttribute("rel", "preload");
  907. preload.setAttribute("as", "font");
  908. preload.setAttribute("crossorigin", "anonymous");
  909. preload.setAttribute("href", src.trim());
  910. document.head.appendChild(preload);
  911. }
  912. }
  913. if (!shouldInlineFonts || !family || !src || !criticalFonts.includes(family)) {
  914. return false;
  915. }
  916. }
  917. });
  918. sheet = serializeStylesheet(ast, {
  919. compress: this.options.compress !== false
  920. });
  921. if (sheet.trim().length === 0) {
  922. if (style.parentNode) {
  923. style.remove();
  924. }
  925. return;
  926. }
  927. let afterText = "";
  928. let styleInlinedCompletely = false;
  929. if (options.pruneSource) {
  930. const sheetInverse = serializeStylesheet(astInverse, {
  931. compress: this.options.compress !== false
  932. });
  933. styleInlinedCompletely = this.pruneSource(style, before, sheetInverse);
  934. if (styleInlinedCompletely) {
  935. const percent2 = sheetInverse.length / before.length * 100;
  936. afterText = `, reducing non-inlined size ${percent2 | 0}% to ${formatSize(sheetInverse.length)}`;
  937. }
  938. }
  939. if (!styleInlinedCompletely) {
  940. style.textContent = sheet;
  941. }
  942. const percent = sheet.length / before.length * 100 | 0;
  943. this.logger.info?.(
  944. `\x1B[32mInlined ${formatSize(sheet.length)} (${percent}% of original ${formatSize(before.length)}) of ${name}${afterText}.\x1B[39m`
  945. );
  946. }
  947. normalizeCssSelector(sel) {
  948. let normalizedSelector = __privateGet(this, _selectorCache).get(sel);
  949. if (normalizedSelector !== void 0) {
  950. return normalizedSelector;
  951. }
  952. normalizedSelector = sel.replace(removePseudoClassesAndElementsPattern, "").replace(removeTrailingCommasPattern, (match) => match.includes("(") ? "(" : ")").trim();
  953. __privateGet(this, _selectorCache).set(sel, normalizedSelector);
  954. return normalizedSelector;
  955. }
  956. }
  957. _selectorCache = new WeakMap();
  958. function formatSize(size) {
  959. if (size <= 0) {
  960. return "0 bytes";
  961. }
  962. const abbreviations = ["bytes", "kB", "MB", "GB"];
  963. const index = Math.floor(Math.log(size) / Math.log(1024));
  964. const roundedSize = size / 1024 ** index;
  965. const fractionDigits = index === 0 ? 0 : 2;
  966. return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`;
  967. }
  968. module.exports = Beasties;