UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

134 lines (133 loc) • 4.26 kB
import { HTMLString, markHTMLString, unescapeHTML } from "../escape.js"; import { renderChild } from "./any.js"; import { renderTemplate } from "./astro/render-template.js"; import { chunkToString } from "./common.js"; import { isScriptInstruction } from "./instruction.js"; const slotString = Symbol.for("astro:slot-string"); export class SlotString extends HTMLString { instructions; /** * The slot's content as an ordered stream. Unlike `instructions` (which holds * position-independent instructions like head/hydration content), scripts are * kept inline here so they render at their original position instead of being * hoisted to the start of the slot output. */ chunks; [slotString]; constructor(content, instructions, chunks = []) { super(content); this.instructions = instructions; this.chunks = chunks; this[slotString] = true; } } export function isSlotString(str) { return !!str[slotString]; } /** * Collects instructions from a SlotString into the target array. * Returns the (possibly newly created) instructions array. */ export function mergeSlotInstructions(target, source) { if (source.instructions?.length) { target ??= []; target.push(...source.instructions); } return target; } export class SlotRenderInstance { result; slotted; constructor(result, slotted) { this.result = result; this.slotted = slotted; } /** Must be called at most once per instance. */ evaluate() { const { slotted } = this; return typeof slotted === "function" ? slotted(this.result) : slotted; } // Not `async`: that would return a promise even for sync slots, forcing the enclosing template to buffer. render(destination) { return renderChild(destination, this.evaluate()); } } export function renderSlot(result, slotted, fallback) { if (!slotted && fallback) { return renderSlot(result, fallback); } return new SlotRenderInstance(result, slotted); } export async function renderSlotToString(result, slotted, fallback) { let content = ""; let instructions = null; const chunks = []; const temporaryDestination = { write(chunk) { // if the chunk is already a SlotString, we concatenate if (chunk instanceof SlotString) { content += chunk; // Preserve nested content (including its scripts) in order. if (chunk.chunks.length) { chunks.push(...chunk.chunks); } instructions = mergeSlotInstructions(instructions, chunk); } else if (chunk instanceof Response) return; else if (typeof chunk === "object" && "type" in chunk && typeof chunk.type === "string") { // Scripts are position-sensitive, so keep them inline in the content // stream at their original location. Other instructions (head, // hydration, etc.) are position-independent and bubble up separately. if (isScriptInstruction(chunk)) { chunks.push(chunk); } else { if (instructions === null) { instructions = []; } instructions.push(chunk); } } else { const str = chunkToString(result, chunk); content += str; chunks.push(str); } } }; const renderInstance = renderSlot(result, slotted, fallback); await renderInstance.render(temporaryDestination); return markHTMLString(new SlotString(content, instructions, chunks)); } export async function renderSlots(result, slots = {}) { let slotInstructions = null; let children = {}; if (slots) { await Promise.all(Object.entries(slots).map(([key, value]) => renderSlotToString(result, value).then((output) => { if (output.instructions) { if (slotInstructions === null) { slotInstructions = []; } slotInstructions.push(...output.instructions); } // Framework/`.html` components inline slot content as an opaque // string, so the inline scripts kept in `chunks` would be lost. // Surface them here so they still render (deduplicated at output). if (output.chunks) { for (const part of output.chunks) { if (typeof part !== "string") { if (slotInstructions === null) { slotInstructions = []; } slotInstructions.push(part); } } } children[key] = output; }))); } return { slotInstructions, children }; } export function createSlotValueFromString(content) { return function() { return renderTemplate`${unescapeHTML(content)}`; }; }