import { TagsWithInnerContent, SelfClosingTags } from '@unhead/shared'; function encodeAttribute(value) { return String(value).replace(/"/g, """); } function propsToString(props) { let attrs = ""; for (const key in props) { if (!Object.prototype.hasOwnProperty.call(props, key)) { continue; } const value = props[key]; if (value !== false && value !== null) { attrs += value === true ? ` ${key}` : ` ${key}="${encodeAttribute(value)}"`; } } return attrs; } function ssrRenderTags(tags, options) { const schema = { htmlAttrs: {}, bodyAttrs: {}, tags: { head: "", bodyClose: "", bodyOpen: "" } }; const lineBreaks = !options?.omitLineBreaks ? "\n" : ""; for (const tag of tags) { if (Object.keys(tag.props).length === 0 && !tag.innerHTML && !tag.textContent) { continue; } if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") { Object.assign(schema[tag.tag], tag.props); continue; } const s = tagToString(tag); const tagPosition = tag.tagPosition || "head"; schema.tags[tagPosition] += schema.tags[tagPosition] ? `${lineBreaks}${s}` : s; } return { headTags: schema.tags.head, bodyTags: schema.tags.bodyClose, bodyTagsOpen: schema.tags.bodyOpen, htmlAttrs: propsToString(schema.htmlAttrs), bodyAttrs: propsToString(schema.bodyAttrs) }; } 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.has(tag.tag)) return SelfClosingTags.has(tag.tag) ? openTag : `${openTag}`; let content = String(tag.innerHTML || ""); if (tag.textContent) content = escapeHtml(String(tag.textContent)); return SelfClosingTags.has(tag.tag) ? openTag : `${openTag}${content}`; } async function renderSSRHead(head, options) { 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, options); const renderCtx = { tags: ctx.tags, html }; await head.hooks.callHook("ssr:rendered", renderCtx); return renderCtx.html; } export { escapeHtml, propsToString, renderSSRHead, ssrRenderTags, tagToString };