astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
166 lines (164 loc) • 5.78 kB
JavaScript
import { clsx } from "clsx";
import { HTMLString, markHTMLString } from "../escape.js";
const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
const htmlBooleanAttributes = /^(?:allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|inert|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i;
const AMPERSAND_REGEX = /&/g;
const DOUBLE_QUOTE_REGEX = /"/g;
const STATIC_DIRECTIVES = /* @__PURE__ */ new Set(["set:html", "set:text"]);
const toIdent = (k) => k.trim().replace(/(?!^)\b\w|\s+|\W+/g, (match, index) => {
if (/\W/.test(match)) return "";
return index === 0 ? match : match.toUpperCase();
});
const toAttributeString = (value, shouldEscape = true) => shouldEscape ? String(value).replace(AMPERSAND_REGEX, "&").replace(DOUBLE_QUOTE_REGEX, """) : value;
const kebab = (k) => k.toLowerCase() === k ? k : k.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
const toStyleString = (obj) => Object.entries(obj).filter(([_, v]) => typeof v === "string" && v.trim() || typeof v === "number").map(([k, v]) => {
if (k[0] !== "-" && k[1] !== "-") return `${kebab(k)}:${v}`;
return `${k}:${v}`;
}).join(";");
function defineScriptVars(vars) {
let output = "";
for (const [key, value] of Object.entries(vars)) {
output += `const ${toIdent(key)} = ${JSON.stringify(value)?.replace(
/<\/script>/g,
"\\x3C/script>"
)};
`;
}
return markHTMLString(output);
}
function formatList(values) {
if (values.length === 1) {
return values[0];
}
return `${values.slice(0, -1).join(", ")} or ${values[values.length - 1]}`;
}
function addAttribute(value, key, shouldEscape = true) {
if (value == null) {
return "";
}
if (STATIC_DIRECTIVES.has(key)) {
console.warn(`[astro] The "${key}" directive cannot be applied dynamically at runtime. It will not be rendered as an attribute.
Make sure to use the static attribute syntax (\`${key}={value}\`) instead of the dynamic spread syntax (\`{...{ "${key}": value }}\`).`);
return "";
}
if (key === "class:list") {
const listValue = toAttributeString(clsx(value), shouldEscape);
if (listValue === "") {
return "";
}
return markHTMLString(` ${key.slice(0, -5)}="${listValue}"`);
}
if (key === "style" && !(value instanceof HTMLString)) {
if (Array.isArray(value) && value.length === 2) {
return markHTMLString(
` ${key}="${toAttributeString(`${toStyleString(value[0])};${value[1]}`, shouldEscape)}"`
);
}
if (typeof value === "object") {
return markHTMLString(` ${key}="${toAttributeString(toStyleString(value), shouldEscape)}"`);
}
}
if (key === "className") {
return markHTMLString(` class="${toAttributeString(value, shouldEscape)}"`);
}
if (typeof value === "string" && value.includes("&") && isHttpUrl(value)) {
return markHTMLString(` ${key}="${toAttributeString(value, false)}"`);
}
if (htmlBooleanAttributes.test(key)) {
return markHTMLString(value ? ` ${key}` : "");
}
if (value === "") {
return markHTMLString(` ${key}`);
}
return markHTMLString(` ${key}="${toAttributeString(value, shouldEscape)}"`);
}
function internalSpreadAttributes(values, shouldEscape = true) {
let output = "";
for (const [key, value] of Object.entries(values)) {
output += addAttribute(value, key, shouldEscape);
}
return markHTMLString(output);
}
function renderElement(name, { props: _props, children = "" }, shouldEscape = true) {
const { lang: _, "data-astro-id": astroId, "define:vars": defineVars, ...props } = _props;
if (defineVars) {
if (name === "style") {
delete props["is:global"];
delete props["is:scoped"];
}
if (name === "script") {
delete props.hoist;
children = defineScriptVars(defineVars) + "\n" + children;
}
}
if ((children == null || children == "") && voidElementNames.test(name)) {
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>`;
}
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
}
const noop = () => {
};
class BufferedRenderer {
chunks = [];
renderPromise;
destination;
constructor(bufferRenderFunction) {
this.renderPromise = bufferRenderFunction(this);
Promise.resolve(this.renderPromise).catch(noop);
}
write(chunk) {
if (this.destination) {
this.destination.write(chunk);
} else {
this.chunks.push(chunk);
}
}
async renderToFinalDestination(destination) {
for (const chunk of this.chunks) {
destination.write(chunk);
}
this.destination = destination;
await this.renderPromise;
}
}
function renderToBufferDestination(bufferRenderFunction) {
const renderer = new BufferedRenderer(bufferRenderFunction);
return renderer;
}
const isNode = typeof process !== "undefined" && Object.prototype.toString.call(process) === "[object process]";
const isDeno = typeof Deno !== "undefined";
function promiseWithResolvers() {
let resolve, reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return {
promise,
resolve,
reject
};
}
const VALID_PROTOCOLS = ["http:", "https:"];
function isHttpUrl(url) {
try {
const parsedUrl = new URL(url);
return VALID_PROTOCOLS.includes(parsedUrl.protocol);
} catch {
return false;
}
}
export {
addAttribute,
defineScriptVars,
formatList,
internalSpreadAttributes,
isDeno,
isNode,
promiseWithResolvers,
renderElement,
renderToBufferDestination,
toAttributeString,
toStyleString,
voidElementNames
};