astro-favicons
Version:
An all-in-one favicon and PWA assets generator for Astro projects. It automates the creation of favicons, manifest, and supports hot reloading for efficient development. Powered by `astro-capo`, it keeps your ๏นค๐๐๐๐๏นฅ content well-organized and tidy.
192 lines (187 loc) โข 5.82 kB
JavaScript
import { html, opts } from 'virtual:astro-favicons';
import { defineMiddleware, sequence } from 'astro/middleware';
import { renderSync, parse, ELEMENT_NODE } from 'ultrahtml';
/**
* @author Nate Moore
* @license [Apache](src/middleware/capo/LICENSE)
* Adapted from https://github.com/rviscomi/capo.js/blob/a4d8902d300bc5207b3b588984a3b5e67bdc38b1/src/lib/rules.js
* Further modified by Nate Moore for the `astro-capo` project.
*
* Original code by Rick Viscomi.
*/
function has(value) {
return typeof value === "string";
}
function is(a, b) {
return a === b;
}
function any(a, b) {
return has(a) && b.includes(a.toLowerCase());
}
const ElementWeights = {
META: 10,
TITLE: 9,
PRECONNECT: 8,
ASYNC_SCRIPT: 7,
IMPORT_STYLES: 6,
SYNC_SCRIPT: 5,
SYNC_STYLES: 4,
PRELOAD: 3,
DEFER_SCRIPT: 2,
PREFETCH_PRERENDER: 1,
OTHER: 0
};
const ElementDetectors = {
META: isMeta,
TITLE: isTitle,
PRECONNECT: isPreconnect,
DEFER_SCRIPT: isDeferScript,
ASYNC_SCRIPT: isAsyncScript,
IMPORT_STYLES: isImportStyles,
SYNC_SCRIPT: isSyncScript,
SYNC_STYLES: isSyncStyles,
PRELOAD: isPreload,
PREFETCH_PRERENDER: isPrefetchPrerender
};
const META_HTTP_EQUIV_KEYWORDS = [
"accept-ch",
"content-security-policy",
"content-type",
"default-style",
"delegate-ch",
"origin-trial",
"x-dns-prefetch-control"
];
function isMeta(name, a) {
if (name === "base")
return true;
if (name !== "meta")
return false;
return has(a.charset) || is(a.name, "viewport") || any(a["http-equiv"], META_HTTP_EQUIV_KEYWORDS);
}
function isTitle(name) {
return name === "title";
}
function isPreconnect(name, { rel }) {
return name === "link" && is(rel, "preconnect");
}
function isAsyncScript(name, { src, async }) {
return name === "script" && has(src) && has(async);
}
function isImportStyles(name, a, children) {
const importRe = /@import/;
if (name === "style") {
return importRe.test(children);
}
return false;
}
function isSyncScript(name, { src, defer, async, type = "" }) {
if (name !== "script")
return false;
return !(has(src) && (has(defer) || has(async) || is(type, "module")) || type.includes("json"));
}
function isSyncStyles(name, { rel }) {
if (name === "style")
return true;
return name === "link" && is(rel, "stylesheet");
}
function isPreload(name, { rel }) {
return name === "link" && any(rel, ["preload", "modulepreload"]);
}
function isDeferScript(name, { src, defer, async, type }) {
if (name !== "script")
return false;
return has(src) && has(defer) || has(src) && is(type, "module") && !has(async);
}
function isPrefetchPrerender(name, { rel }) {
return name === "link" && any(rel, ["prefetch", "dns-prefetch", "prerender"]);
}
function getWeight(element) {
for (const [id, detector] of Object.entries(ElementDetectors)) {
const children = element.name === "style" && element.children.length > 0 ? renderSync(element) : "";
if (detector(element.name, element.attributes, children)) {
return ElementWeights[id];
}
}
return ElementWeights.OTHER;
}
function capo(html) {
const { headHtml, beforeHead, afterHead } = extractHeadContent(html);
if (!headHtml)
return html;
const headAst = parse(headHtml);
const headNode = headAst.children.find(
(node) => node.type === ELEMENT_NODE && node.name === "head"
);
if (!headNode)
return html;
const updatedHead = processHead(headNode);
const renderedHead = renderSync(updatedHead);
return `${beforeHead}${renderedHead}${afterHead}`;
}
function extractHeadContent(html) {
const headStart = html.indexOf("<head>");
const headEnd = html.indexOf("</head>") + "</head>".length;
if (headStart === -1 || headEnd === -1) {
return { headHtml: null, beforeHead: html, afterHead: "" };
}
const beforeHead = html.slice(0, headStart);
const headHtml = html.slice(headStart, headEnd);
const afterHead = html.slice(headEnd);
return { headHtml, beforeHead, afterHead };
}
function processHead(head) {
const weightedChildren = head.children.map((node) => {
if (node.type === ELEMENT_NODE) {
const weight = getWeight(node);
return [weight, node];
}
}).filter(Boolean);
const sortedChildren = weightedChildren.sort((a, b) => b[0] - a[0]).map(([_, element]) => element);
return { ...head, children: sortedChildren };
}
const useLocaleName = (locale) => {
if (!locale)
return opts.name;
const localized = opts.name_localized?.[locale];
if (!localized)
return opts.name;
return typeof localized === "string" ? localized : localized.value;
};
const localizedHTML = (locale) => {
const namePattern = /(name="(application-name|apple-mobile-web-app-title)")\scontent="[^"]*"/;
const tags = html.map(
(line) => line.replace(namePattern, `name="$2" content="${useLocaleName(locale)}"`)
).join("\n");
return tags;
};
const withCapo = defineMiddleware(async (ctx, next) => {
try {
if (html.length === 0)
throw "done";
const res = await next();
if (res.headers.get("X-Astro-Route-Type") !== "page") {
return res;
}
const doc = await res.text();
const headIndex = doc.indexOf("</head>");
const htmlSet = new Set(html);
const isInjected = [...htmlSet].some((line) => doc.includes(line));
if (headIndex === -1 || !opts.withCapo && isInjected)
throw "done";
const document = `${doc.slice(0, headIndex)}
${!isInjected ? localizedHTML(ctx.currentLocale) : ""}
${doc.slice(headIndex)}`;
return new Response(opts.withCapo ? capo(document) : document, {
status: res.status,
headers: res.headers
});
} catch (e) {
if (e !== "done") {
console.error("Error in withCapo middleware:", e);
}
return next();
}
});
const onRequest = sequence(withCapo);
export { localizedHTML, onRequest };