129 lines
4.9 KiB
Plaintext
129 lines
4.9 KiB
Plaintext
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|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i;
|
|
const htmlEnumAttributes = /^(?:contenteditable|draggable|spellcheck|value)$/i;
|
|
const svgEnumAttributes = /^(?:autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
|
|
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(/&/g, "&").replace(/"/g, """) : 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 (value === false) {
|
|
if (htmlEnumAttributes.test(key) || svgEnumAttributes.test(key)) {
|
|
return markHTMLString(` ${key}="false"`);
|
|
}
|
|
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 (value === true && (key.startsWith("data-") || htmlBooleanAttributes.test(key))) {
|
|
return markHTMLString(` ${key}`);
|
|
} else {
|
|
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}>`;
|
|
}
|
|
function renderToBufferDestination(bufferRenderFunction) {
|
|
const bufferChunks = [];
|
|
const bufferDestination = {
|
|
write: (chunk) => bufferChunks.push(chunk)
|
|
};
|
|
const renderPromise = bufferRenderFunction(bufferDestination);
|
|
return {
|
|
async renderToFinalDestination(destination) {
|
|
for (const chunk of bufferChunks) {
|
|
destination.write(chunk);
|
|
}
|
|
bufferDestination.write = (chunk) => destination.write(chunk);
|
|
await renderPromise;
|
|
}
|
|
};
|
|
}
|
|
export {
|
|
addAttribute,
|
|
defineScriptVars,
|
|
formatList,
|
|
internalSpreadAttributes,
|
|
renderElement,
|
|
renderToBufferDestination,
|
|
toAttributeString,
|
|
voidElementNames
|
|
};
|