85 lines
2.5 KiB
JavaScript
85 lines
2.5 KiB
JavaScript
|
import { TagsWithInnerContent, SelfClosingTags } from '@unhead/shared';
|
||
|
|
||
|
function encodeAttribute(value) {
|
||
|
return String(value).replace(/"/g, """);
|
||
|
}
|
||
|
function propsToString(props) {
|
||
|
const attrs = [];
|
||
|
for (const [key, value] of Object.entries(props)) {
|
||
|
if (value !== false && value !== null)
|
||
|
attrs.push(value === true ? key : `${key}="${encodeAttribute(value)}"`);
|
||
|
}
|
||
|
return `${attrs.length > 0 ? " " : ""}${attrs.join(" ")}`;
|
||
|
}
|
||
|
|
||
|
function escapeHtml(str) {
|
||
|
return str.replace(/[&<>"'/]/g, (char) => {
|
||
|
switch (char) {
|
||
|
case "&":
|
||
|
return "&";
|
||
|
case "<":
|
||
|
return "<";
|
||
|
case ">":
|
||
|
return ">";
|
||
|
case '"':
|
||
|
return """;
|
||
|
case "'":
|
||
|
return "'";
|
||
|
case "/":
|
||
|
return "/";
|
||
|
default:
|
||
|
return char;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
function tagToString(tag) {
|
||
|
const attrs = propsToString(tag.props);
|
||
|
const openTag = `<${tag.tag}${attrs}>`;
|
||
|
if (!TagsWithInnerContent.includes(tag.tag))
|
||
|
return SelfClosingTags.includes(tag.tag) ? openTag : `${openTag}</${tag.tag}>`;
|
||
|
let content = String(tag.innerHTML || "");
|
||
|
if (tag.textContent)
|
||
|
content = escapeHtml(String(tag.textContent));
|
||
|
return SelfClosingTags.includes(tag.tag) ? openTag : `${openTag}${content}</${tag.tag}>`;
|
||
|
}
|
||
|
|
||
|
function ssrRenderTags(tags) {
|
||
|
const schema = { htmlAttrs: {}, bodyAttrs: {}, tags: { head: [], bodyClose: [], bodyOpen: [] } };
|
||
|
for (const tag of tags) {
|
||
|
if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
|
||
|
schema[tag.tag] = { ...schema[tag.tag], ...tag.props };
|
||
|
continue;
|
||
|
}
|
||
|
schema.tags[tag.tagPosition || "head"].push(tagToString(tag));
|
||
|
}
|
||
|
return {
|
||
|
headTags: schema.tags.head.join("\n"),
|
||
|
bodyTags: schema.tags.bodyClose.join("\n"),
|
||
|
bodyTagsOpen: schema.tags.bodyOpen.join("\n"),
|
||
|
htmlAttrs: propsToString(schema.htmlAttrs),
|
||
|
bodyAttrs: propsToString(schema.bodyAttrs)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
async function renderSSRHead(head) {
|
||
|
const beforeRenderCtx = { shouldRender: true };
|
||
|
await head.hooks.callHook("ssr:beforeRender", beforeRenderCtx);
|
||
|
if (!beforeRenderCtx.shouldRender) {
|
||
|
return {
|
||
|
headTags: "",
|
||
|
bodyTags: "",
|
||
|
bodyTagsOpen: "",
|
||
|
htmlAttrs: "",
|
||
|
bodyAttrs: ""
|
||
|
};
|
||
|
}
|
||
|
const ctx = { tags: await head.resolveTags() };
|
||
|
await head.hooks.callHook("ssr:render", ctx);
|
||
|
const html = ssrRenderTags(ctx.tags);
|
||
|
const renderCtx = { tags: ctx.tags, html };
|
||
|
await head.hooks.callHook("ssr:rendered", renderCtx);
|
||
|
return renderCtx.html;
|
||
|
}
|
||
|
|
||
|
export { escapeHtml, propsToString, renderSSRHead, ssrRenderTags, tagToString };
|