index.mjs 29 KB

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