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
JavaScript
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)}`;
};
}