first commit
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
export { c as createHead } from './shared/unhead.oZ7pkTV1.mjs';
|
||||
export { c as createDomRenderer, r as renderDOMHead } from './shared/unhead.Bjbp1C8D.mjs';
|
||||
import './shared/unhead.D7HkBzZn.mjs';
|
||||
import './shared/unhead.CGPOfp5O.mjs';
|
||||
import './shared/unhead.Bm4Y6XQI.mjs';
|
||||
import 'hookable';
|
||||
import './shared/unhead.C5Bksi2B.mjs';
|
||||
import './shared/unhead.-hZVKou0.mjs';
|
||||
|
||||
function createDebouncedFn(callee, delayer) {
|
||||
let ctxId = 0;
|
||||
return () => {
|
||||
const delayFnCtxId = ++ctxId;
|
||||
delayer(() => {
|
||||
if (ctxId === delayFnCtxId) {
|
||||
callee();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { createDebouncedFn };
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { D as DeprecationsPlugin, P as PromisesPlugin, T as TemplateParamsPlugin, A as AliasSortingPlugin } from './shared/unhead.tQnRaXfX.mjs';
|
||||
import { c as createUnhead } from './shared/unhead.D7HkBzZn.mjs';
|
||||
import { c as createHead$1 } from './shared/unhead.oZ7pkTV1.mjs';
|
||||
import { c as createHead$2 } from './shared/unhead.Dssd7L5I.mjs';
|
||||
import './shared/unhead.CHEy9ana.mjs';
|
||||
import './shared/unhead.BGFxPGPQ.mjs';
|
||||
import './shared/unhead.CGPOfp5O.mjs';
|
||||
import './shared/unhead.Bm4Y6XQI.mjs';
|
||||
import 'hookable';
|
||||
import './shared/unhead.C5Bksi2B.mjs';
|
||||
import './shared/unhead.Bjbp1C8D.mjs';
|
||||
import './shared/unhead.-hZVKou0.mjs';
|
||||
|
||||
const legacyPlugins = [DeprecationsPlugin, PromisesPlugin, TemplateParamsPlugin, AliasSortingPlugin];
|
||||
const activeHead = { value: null };
|
||||
function getActiveHead() {
|
||||
return activeHead.value;
|
||||
}
|
||||
function createHead(options = {}) {
|
||||
return activeHead.value = createHead$1({
|
||||
...options,
|
||||
plugins: [...legacyPlugins, ...options.plugins || []]
|
||||
});
|
||||
}
|
||||
function createServerHead(options = {}) {
|
||||
return activeHead.value = createHead$2({
|
||||
...options,
|
||||
plugins: [...legacyPlugins, ...options.plugins || []]
|
||||
});
|
||||
}
|
||||
const createHeadCore = createUnhead;
|
||||
|
||||
export { DeprecationsPlugin, activeHead, createHead, createHeadCore, createServerHead, getActiveHead, legacyPlugins };
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
function minifyCSS(code) {
|
||||
if (!/[\s/]|;\}|0\.\d/.test(code))
|
||||
return code;
|
||||
let result = "";
|
||||
let last = "";
|
||||
let i = 0;
|
||||
let parenDepth = 0;
|
||||
const len = code.length;
|
||||
const append = (value) => {
|
||||
result += value;
|
||||
last = value;
|
||||
};
|
||||
while (i < len) {
|
||||
const ch = code[i];
|
||||
if (ch === "'" || ch === '"') {
|
||||
const quote = ch;
|
||||
append(ch);
|
||||
i++;
|
||||
while (i < len && code[i] !== quote) {
|
||||
if (code[i] === "\\" && i + 1 < len)
|
||||
append(code[i++]);
|
||||
append(code[i++]);
|
||||
}
|
||||
if (i < len)
|
||||
append(code[i++]);
|
||||
} else if (ch === "/" && code[i + 1] === "*") {
|
||||
i += 2;
|
||||
while (i < len && !(code[i] === "*" && code[i + 1] === "/"))
|
||||
i++;
|
||||
i += 2;
|
||||
} else if (ch === "(") {
|
||||
parenDepth++;
|
||||
append(ch);
|
||||
i++;
|
||||
} else if (ch === ")") {
|
||||
parenDepth = Math.max(0, parenDepth - 1);
|
||||
append(ch);
|
||||
i++;
|
||||
} else if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
||||
while (i < len && (code[i] === " " || code[i] === " " || code[i] === "\n" || code[i] === "\r"))
|
||||
i++;
|
||||
const next = code[i];
|
||||
if (next === "!")
|
||||
continue;
|
||||
if (parenDepth > 0) {
|
||||
if (last && next && !isCSSCalcPunctuation(last) && !isCSSCalcPunctuation(next))
|
||||
append(" ");
|
||||
} else if (last && next && !isCSSPunctuation(last) && !isCSSPunctuation(next)) {
|
||||
append(" ");
|
||||
}
|
||||
} else if (ch === ";") {
|
||||
let j = i + 1;
|
||||
while (j < len && (code[j] === " " || code[j] === " " || code[j] === "\n" || code[j] === "\r"))
|
||||
j++;
|
||||
if (code[j] === "}") {
|
||||
i++;
|
||||
} else {
|
||||
append(ch);
|
||||
i++;
|
||||
}
|
||||
} else if (ch === "0" && code[i + 1] === "." && code[i + 2] >= "0" && code[i + 2] <= "9") {
|
||||
if (last && last >= "0" && last <= "9") {
|
||||
append(ch);
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
append(ch);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
}
|
||||
function isCSSPunctuation(ch) {
|
||||
return ch === "{" || ch === "}" || ch === ";" || ch === ":" || ch === "," || ch === ">" || ch === "~" || ch === "+" || ch === "(" || ch === ")";
|
||||
}
|
||||
function isCSSCalcPunctuation(ch) {
|
||||
return ch === "*" || ch === "/" || ch === "(" || ch === ")" || ch === ",";
|
||||
}
|
||||
|
||||
function minifyJS(code) {
|
||||
if (!/[\s/]/.test(code))
|
||||
return code;
|
||||
let result = "";
|
||||
let last = "";
|
||||
let i = 0;
|
||||
const len = code.length;
|
||||
const append = (value) => {
|
||||
result += value;
|
||||
last = value;
|
||||
};
|
||||
while (i < len) {
|
||||
const ch = code[i];
|
||||
if (ch === "'" || ch === '"' || ch === "`") {
|
||||
const quote = ch;
|
||||
append(ch);
|
||||
i++;
|
||||
while (i < len && code[i] !== quote) {
|
||||
if (code[i] === "\\" && i + 1 < len) {
|
||||
append(code[i++]);
|
||||
}
|
||||
append(code[i++]);
|
||||
}
|
||||
if (i < len)
|
||||
append(code[i++]);
|
||||
} else if (ch === "/" && code[i + 1] === "/") {
|
||||
i += 2;
|
||||
while (i < len && code[i] !== "\n")
|
||||
i++;
|
||||
} else if (ch === "/" && code[i + 1] === "*") {
|
||||
i += 2;
|
||||
while (i < len && !(code[i] === "*" && code[i + 1] === "/"))
|
||||
i++;
|
||||
i += 2;
|
||||
} else if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
||||
let hasNewline = false;
|
||||
while (i < len && (code[i] === " " || code[i] === " " || code[i] === "\n" || code[i] === "\r")) {
|
||||
if (code[i] === "\n")
|
||||
hasNewline = true;
|
||||
i++;
|
||||
}
|
||||
const next = code[i];
|
||||
if (hasNewline && last && next && last !== "{" && last !== "}" && last !== ";" && next !== "}" && next !== ";")
|
||||
append("\n");
|
||||
else if (last && next && isIdentChar(last) && isIdentChar(next))
|
||||
append(" ");
|
||||
else if (last && next && (last === "+" && next === "+" || last === "-" && next === "-"))
|
||||
append(" ");
|
||||
} else {
|
||||
append(ch);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
}
|
||||
function isIdentChar(ch) {
|
||||
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_" || ch === "$";
|
||||
}
|
||||
|
||||
function isJsonWhitespace(char) {
|
||||
return char === " " || char === "\n" || char === "\r" || char === " ";
|
||||
}
|
||||
function minifyJSON(code) {
|
||||
let escaped = false;
|
||||
let inString = false;
|
||||
let segmentStart = 0;
|
||||
let chunks;
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
const char = code[i];
|
||||
if (inString) {
|
||||
if (escaped)
|
||||
escaped = false;
|
||||
else if (char === "\\")
|
||||
escaped = true;
|
||||
else if (char === '"')
|
||||
inString = false;
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
if (isJsonWhitespace(char)) {
|
||||
chunks ||= [];
|
||||
if (segmentStart < i)
|
||||
chunks.push(code.slice(segmentStart, i));
|
||||
segmentStart = i + 1;
|
||||
}
|
||||
}
|
||||
if (!chunks)
|
||||
return code;
|
||||
try {
|
||||
JSON.parse(code);
|
||||
} catch {
|
||||
return code;
|
||||
}
|
||||
if (segmentStart < code.length)
|
||||
chunks.push(code.slice(segmentStart));
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
export { minifyCSS, minifyJS, minifyJSON };
|
||||
+518
@@ -0,0 +1,518 @@
|
||||
import { h as hasOwn } from './shared/unhead.Bf_fPVYA.mjs';
|
||||
|
||||
const TAG_HTML = 0;
|
||||
const TAG_HEAD = 1;
|
||||
const TAG_TITLE = 4;
|
||||
const TAG_META = 5;
|
||||
const TAG_BODY = 44;
|
||||
const TAG_SCRIPT = 52;
|
||||
const TAG_STYLE = 53;
|
||||
const TAG_LINK = 54;
|
||||
const TAG_BASE = 56;
|
||||
const LT_CHAR = 60;
|
||||
const GT_CHAR = 62;
|
||||
const SLASH_CHAR = 47;
|
||||
const EQUALS_CHAR = 61;
|
||||
const QUOTE_CHAR = 34;
|
||||
const APOS_CHAR = 39;
|
||||
const EXCLAMATION_CHAR = 33;
|
||||
const BACKSLASH_CHAR = 92;
|
||||
const DASH_CHAR = 45;
|
||||
const TagIdMap = {
|
||||
html: TAG_HTML,
|
||||
head: TAG_HEAD,
|
||||
title: TAG_TITLE,
|
||||
meta: TAG_META,
|
||||
body: TAG_BODY,
|
||||
script: TAG_SCRIPT,
|
||||
style: TAG_STYLE,
|
||||
link: TAG_LINK,
|
||||
base: TAG_BASE
|
||||
};
|
||||
const HEAD_ELEMENTS_TO_EXTRACT = /* @__PURE__ */ new Set([TAG_TITLE, TAG_META, TAG_LINK, TAG_SCRIPT, TAG_STYLE, TAG_BASE]);
|
||||
function prepareTemplate(html) {
|
||||
const template = parseHtmlForIndexes(html);
|
||||
Object.freeze(template.input);
|
||||
Object.freeze(template.indexes);
|
||||
return Object.freeze(template);
|
||||
}
|
||||
function isWhitespace(charCode) {
|
||||
return charCode === 32 || charCode === 9 || charCode === 10 || charCode === 13;
|
||||
}
|
||||
function processCommentOrDoctype(htmlChunk, position) {
|
||||
let i = position;
|
||||
const chunkLength = htmlChunk.length;
|
||||
if (i + 3 < chunkLength && htmlChunk.charCodeAt(i + 2) === DASH_CHAR && htmlChunk.charCodeAt(i + 3) === DASH_CHAR) {
|
||||
i += 4;
|
||||
while (i < chunkLength - 2) {
|
||||
if (htmlChunk.charCodeAt(i) === DASH_CHAR && htmlChunk.charCodeAt(i + 1) === DASH_CHAR && htmlChunk.charCodeAt(i + 2) === GT_CHAR) {
|
||||
i += 3;
|
||||
return {
|
||||
complete: true,
|
||||
newPosition: i,
|
||||
remainingText: ""
|
||||
};
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return {
|
||||
complete: false,
|
||||
newPosition: position,
|
||||
remainingText: htmlChunk.substring(position)
|
||||
};
|
||||
} else {
|
||||
i += 2;
|
||||
while (i < chunkLength) {
|
||||
if (htmlChunk.charCodeAt(i) === GT_CHAR) {
|
||||
i++;
|
||||
return {
|
||||
complete: true,
|
||||
newPosition: i,
|
||||
remainingText: ""
|
||||
};
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return {
|
||||
complete: false,
|
||||
newPosition: i,
|
||||
remainingText: htmlChunk.substring(position, i)
|
||||
};
|
||||
}
|
||||
}
|
||||
function parseAttributes(attrStr) {
|
||||
if (!attrStr)
|
||||
return {};
|
||||
const result = {};
|
||||
const setAttr = (attrName, value) => {
|
||||
if (!hasOwn(result, attrName))
|
||||
result[attrName] = value;
|
||||
};
|
||||
const len = attrStr.length;
|
||||
let i = 0;
|
||||
const WHITESPACE = 0;
|
||||
const NAME = 1;
|
||||
const AFTER_NAME = 2;
|
||||
const BEFORE_VALUE = 3;
|
||||
const QUOTED_VALUE = 4;
|
||||
const UNQUOTED_VALUE = 5;
|
||||
let state = WHITESPACE;
|
||||
let nameStart = 0;
|
||||
let nameEnd = 0;
|
||||
let valueStart = 0;
|
||||
let quoteChar = 0;
|
||||
let name = "";
|
||||
while (i < len) {
|
||||
const charCode = attrStr.charCodeAt(i);
|
||||
const isSpace = isWhitespace(charCode);
|
||||
switch (state) {
|
||||
case WHITESPACE:
|
||||
if (!isSpace) {
|
||||
state = NAME;
|
||||
nameStart = i;
|
||||
nameEnd = 0;
|
||||
}
|
||||
break;
|
||||
case NAME:
|
||||
if (charCode === EQUALS_CHAR || isSpace) {
|
||||
nameEnd = i;
|
||||
name = attrStr.substring(nameStart, nameEnd).toLowerCase();
|
||||
state = charCode === EQUALS_CHAR ? BEFORE_VALUE : AFTER_NAME;
|
||||
}
|
||||
break;
|
||||
case AFTER_NAME:
|
||||
if (charCode === EQUALS_CHAR) {
|
||||
state = BEFORE_VALUE;
|
||||
} else if (!isSpace) {
|
||||
setAttr(name, "");
|
||||
state = NAME;
|
||||
nameStart = i;
|
||||
nameEnd = 0;
|
||||
}
|
||||
break;
|
||||
case BEFORE_VALUE:
|
||||
if (charCode === QUOTE_CHAR || charCode === APOS_CHAR) {
|
||||
quoteChar = charCode;
|
||||
state = QUOTED_VALUE;
|
||||
valueStart = i + 1;
|
||||
} else if (!isSpace) {
|
||||
state = UNQUOTED_VALUE;
|
||||
valueStart = i;
|
||||
}
|
||||
break;
|
||||
case QUOTED_VALUE:
|
||||
if (charCode === quoteChar) {
|
||||
setAttr(name, attrStr.substring(valueStart, i));
|
||||
state = WHITESPACE;
|
||||
}
|
||||
break;
|
||||
case UNQUOTED_VALUE:
|
||||
if (isSpace || charCode === GT_CHAR) {
|
||||
setAttr(name, attrStr.substring(valueStart, i));
|
||||
state = WHITESPACE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (state === QUOTED_VALUE || state === UNQUOTED_VALUE) {
|
||||
if (name) {
|
||||
setAttr(name, attrStr.substring(valueStart, i));
|
||||
}
|
||||
} else if (state === NAME || state === AFTER_NAME || state === BEFORE_VALUE) {
|
||||
nameEnd = nameEnd || i;
|
||||
const currentName = attrStr.substring(nameStart, nameEnd).toLowerCase();
|
||||
if (currentName) {
|
||||
setAttr(currentName, "");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function parseHtmlForIndexes(html) {
|
||||
const indexes = {
|
||||
htmlTagStart: html.indexOf("<html"),
|
||||
htmlTagEnd: -1,
|
||||
headTagEnd: html.indexOf("</head>"),
|
||||
bodyTagStart: html.indexOf("<body"),
|
||||
bodyTagEnd: -1,
|
||||
bodyCloseTagStart: html.indexOf("</body>")
|
||||
};
|
||||
if (indexes.htmlTagStart >= 0) {
|
||||
const htmlTagEndPos = html.indexOf(">", indexes.htmlTagStart);
|
||||
if (htmlTagEndPos >= 0) {
|
||||
indexes.htmlTagEnd = htmlTagEndPos + 1;
|
||||
}
|
||||
}
|
||||
if (indexes.bodyTagStart >= 0) {
|
||||
const bodyTagEndPos = html.indexOf(">", indexes.bodyTagStart);
|
||||
if (bodyTagEndPos >= 0) {
|
||||
indexes.bodyTagEnd = bodyTagEndPos + 1;
|
||||
}
|
||||
}
|
||||
return { html, input: {}, indexes };
|
||||
}
|
||||
function parseHtmlForUnheadExtraction(html) {
|
||||
const input = {};
|
||||
const htmlParts = [];
|
||||
let position = 0;
|
||||
let inHead = false;
|
||||
let foundBodyStart = false;
|
||||
let lastCopyPosition = 0;
|
||||
let currentOutputLength = 0;
|
||||
const indexes = {
|
||||
htmlTagStart: -1,
|
||||
htmlTagEnd: -1,
|
||||
headTagEnd: -1,
|
||||
bodyTagStart: -1,
|
||||
bodyTagEnd: -1,
|
||||
bodyCloseTagStart: -1
|
||||
};
|
||||
function copyAccumulatedText() {
|
||||
if (lastCopyPosition < position) {
|
||||
const textToAdd = html.substring(lastCopyPosition, position);
|
||||
htmlParts.push(textToAdd);
|
||||
currentOutputLength += textToAdd.length;
|
||||
lastCopyPosition = position;
|
||||
}
|
||||
}
|
||||
function addText(text) {
|
||||
htmlParts.push(text);
|
||||
currentOutputLength += text.length;
|
||||
}
|
||||
while (position < html.length && !foundBodyStart) {
|
||||
const currentCharCode = html.charCodeAt(position);
|
||||
if (currentCharCode !== LT_CHAR) {
|
||||
position++;
|
||||
continue;
|
||||
}
|
||||
if (position + 1 >= html.length) {
|
||||
copyAccumulatedText();
|
||||
addText(html[position]);
|
||||
break;
|
||||
}
|
||||
const nextCharCode = html.charCodeAt(position + 1);
|
||||
if (nextCharCode === EXCLAMATION_CHAR) {
|
||||
const result = processCommentOrDoctype(html, position);
|
||||
if (result.complete) {
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position, result.newPosition));
|
||||
position = result.newPosition;
|
||||
lastCopyPosition = position;
|
||||
} else {
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position));
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (nextCharCode === SLASH_CHAR) {
|
||||
let tagEnd2 = position + 2;
|
||||
while (tagEnd2 < html.length && html.charCodeAt(tagEnd2) !== GT_CHAR) {
|
||||
tagEnd2++;
|
||||
}
|
||||
if (tagEnd2 >= html.length) {
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position));
|
||||
break;
|
||||
}
|
||||
const tagName2 = html.substring(position + 2, tagEnd2).toLowerCase().trim();
|
||||
const tagId2 = TagIdMap[tagName2] ?? -1;
|
||||
if (tagId2 === TAG_HEAD) {
|
||||
inHead = false;
|
||||
copyAccumulatedText();
|
||||
const headCloseStart = currentOutputLength;
|
||||
addText(html.substring(position, tagEnd2 + 1));
|
||||
indexes.headTagEnd = headCloseStart;
|
||||
} else {
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position, tagEnd2 + 1));
|
||||
}
|
||||
position = tagEnd2 + 1;
|
||||
lastCopyPosition = position;
|
||||
continue;
|
||||
}
|
||||
const tagStart = position + 1;
|
||||
let tagNameEnd = tagStart;
|
||||
while (tagNameEnd < html.length) {
|
||||
const c = html.charCodeAt(tagNameEnd);
|
||||
if (isWhitespace(c) || c === SLASH_CHAR || c === GT_CHAR) {
|
||||
break;
|
||||
}
|
||||
tagNameEnd++;
|
||||
}
|
||||
if (tagNameEnd >= html.length) {
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position));
|
||||
break;
|
||||
}
|
||||
const tagName = html.substring(tagStart, tagNameEnd).toLowerCase();
|
||||
const tagId = TagIdMap[tagName] ?? -1;
|
||||
let tagEnd = tagNameEnd;
|
||||
let inQuote = false;
|
||||
let quoteChar = 0;
|
||||
let foundEnd = false;
|
||||
let isSelfClosing = false;
|
||||
while (tagEnd < html.length && !foundEnd) {
|
||||
const c = html.charCodeAt(tagEnd);
|
||||
if (inQuote) {
|
||||
if (c === quoteChar) {
|
||||
inQuote = false;
|
||||
}
|
||||
} else if (c === QUOTE_CHAR || c === APOS_CHAR) {
|
||||
inQuote = true;
|
||||
quoteChar = c;
|
||||
} else if (c === SLASH_CHAR && tagEnd + 1 < html.length && html.charCodeAt(tagEnd + 1) === GT_CHAR) {
|
||||
isSelfClosing = true;
|
||||
tagEnd += 2;
|
||||
foundEnd = true;
|
||||
continue;
|
||||
} else if (c === GT_CHAR) {
|
||||
tagEnd++;
|
||||
foundEnd = true;
|
||||
continue;
|
||||
}
|
||||
tagEnd++;
|
||||
}
|
||||
if (!foundEnd) {
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position));
|
||||
break;
|
||||
}
|
||||
const attributesStr = html.substring(tagNameEnd, tagEnd - (isSelfClosing ? 2 : 1)).trim();
|
||||
const attributes = parseAttributes(attributesStr);
|
||||
if (tagId === TAG_HTML) {
|
||||
copyAccumulatedText();
|
||||
indexes.htmlTagStart = currentOutputLength;
|
||||
if (Object.keys(attributes).length > 0) {
|
||||
input.htmlAttrs = attributes;
|
||||
addText(`<${tagName}>`);
|
||||
} else {
|
||||
addText(html.substring(position, tagEnd));
|
||||
}
|
||||
indexes.htmlTagEnd = currentOutputLength;
|
||||
} else if (tagId === TAG_BODY) {
|
||||
copyAccumulatedText();
|
||||
indexes.bodyTagStart = currentOutputLength;
|
||||
if (Object.keys(attributes).length > 0) {
|
||||
input.bodyAttrs = attributes;
|
||||
addText(`<${tagName}>`);
|
||||
} else {
|
||||
addText(html.substring(position, tagEnd));
|
||||
}
|
||||
indexes.bodyTagEnd = currentOutputLength;
|
||||
foundBodyStart = true;
|
||||
position = tagEnd;
|
||||
lastCopyPosition = position;
|
||||
break;
|
||||
} else if (tagId === TAG_HEAD) {
|
||||
inHead = true;
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position, tagEnd));
|
||||
} else if (inHead && HEAD_ELEMENTS_TO_EXTRACT.has(tagId)) {
|
||||
if (tagId === TAG_TITLE) {
|
||||
if (!isSelfClosing) {
|
||||
const titleEnd = findClosingTag(html, tagEnd, tagName);
|
||||
if (titleEnd !== -1) {
|
||||
const titleContent = html.substring(tagEnd, titleEnd).trim();
|
||||
if (titleContent && !input.title) {
|
||||
input.title = titleContent;
|
||||
}
|
||||
position = findTagEnd(html, titleEnd, tagName);
|
||||
lastCopyPosition = position;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (tagId === TAG_SCRIPT) {
|
||||
const scriptAttrs = { ...attributes };
|
||||
if (!isSelfClosing) {
|
||||
const scriptEnd = findClosingTag(html, tagEnd, tagName);
|
||||
if (scriptEnd !== -1) {
|
||||
const scriptContent = html.substring(tagEnd, scriptEnd);
|
||||
scriptAttrs.innerHTML = scriptContent || "";
|
||||
position = findTagEnd(html, scriptEnd, tagName);
|
||||
} else {
|
||||
scriptAttrs.innerHTML = "";
|
||||
position = tagEnd;
|
||||
}
|
||||
} else {
|
||||
scriptAttrs.innerHTML = "";
|
||||
position = tagEnd;
|
||||
}
|
||||
lastCopyPosition = position;
|
||||
(input.script ||= []).push(scriptAttrs);
|
||||
continue;
|
||||
} else if (tagId === TAG_STYLE) {
|
||||
const styleAttrs = { ...attributes };
|
||||
if (!isSelfClosing) {
|
||||
const styleEnd = findClosingTag(html, tagEnd, tagName);
|
||||
if (styleEnd !== -1) {
|
||||
const styleContent = html.substring(tagEnd, styleEnd);
|
||||
styleAttrs.textContent = styleContent || "";
|
||||
position = findTagEnd(html, styleEnd, tagName);
|
||||
} else {
|
||||
styleAttrs.textContent = "";
|
||||
position = tagEnd;
|
||||
}
|
||||
} else {
|
||||
styleAttrs.textContent = "";
|
||||
position = tagEnd;
|
||||
}
|
||||
lastCopyPosition = position;
|
||||
(input.style ||= []).push(styleAttrs);
|
||||
continue;
|
||||
} else if (tagId === TAG_META) {
|
||||
(input.meta ||= []).push(attributes);
|
||||
position = tagEnd;
|
||||
lastCopyPosition = position;
|
||||
continue;
|
||||
} else if (tagId === TAG_LINK) {
|
||||
(input.link ||= []).push(attributes);
|
||||
position = tagEnd;
|
||||
lastCopyPosition = position;
|
||||
continue;
|
||||
} else if (tagId === TAG_BASE && !input.base) {
|
||||
input.base = attributes;
|
||||
position = tagEnd;
|
||||
lastCopyPosition = position;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
copyAccumulatedText();
|
||||
addText(html.substring(position, tagEnd));
|
||||
}
|
||||
position = tagEnd;
|
||||
lastCopyPosition = position;
|
||||
}
|
||||
const remainingHtml = html.substring(position);
|
||||
const bodyCloseIndex = remainingHtml.indexOf("</body>");
|
||||
if (bodyCloseIndex !== -1) {
|
||||
indexes.bodyCloseTagStart = currentOutputLength + bodyCloseIndex;
|
||||
}
|
||||
copyAccumulatedText();
|
||||
addText(remainingHtml);
|
||||
return { html: htmlParts.join(""), input, indexes };
|
||||
}
|
||||
function findClosingTag(html, startPos, tagName) {
|
||||
const tagId = TagIdMap[tagName];
|
||||
const isScriptOrStyle = tagId === TAG_SCRIPT || tagId === TAG_STYLE;
|
||||
if (!isScriptOrStyle) {
|
||||
const closingTag2 = `</${tagName}`;
|
||||
const index = html.indexOf(closingTag2, startPos);
|
||||
return index === -1 ? -1 : index;
|
||||
}
|
||||
const closingTag = `</${tagName}`;
|
||||
let pos = startPos;
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let inBacktick = false;
|
||||
let lastCharWasBackslash = false;
|
||||
while (pos < html.length) {
|
||||
const currentCharCode = html.charCodeAt(pos);
|
||||
if (!lastCharWasBackslash) {
|
||||
if (currentCharCode === APOS_CHAR && !inDoubleQuote && !inBacktick) {
|
||||
inSingleQuote = !inSingleQuote;
|
||||
} else if (currentCharCode === QUOTE_CHAR && !inSingleQuote && !inBacktick) {
|
||||
inDoubleQuote = !inDoubleQuote;
|
||||
} else if (currentCharCode === 96 && !inSingleQuote && !inDoubleQuote) {
|
||||
inBacktick = !inBacktick;
|
||||
}
|
||||
}
|
||||
lastCharWasBackslash = currentCharCode === BACKSLASH_CHAR && !lastCharWasBackslash;
|
||||
const inQuotes = inSingleQuote || inDoubleQuote || inBacktick;
|
||||
if (!inQuotes && html.startsWith(closingTag, pos)) {
|
||||
const afterTagPos = pos + closingTag.length;
|
||||
if (afterTagPos >= html.length) {
|
||||
return pos;
|
||||
}
|
||||
const nextChar = html.charCodeAt(afterTagPos);
|
||||
if (nextChar === GT_CHAR || isWhitespace(nextChar)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function findTagEnd(html, closingTagStart, tagName) {
|
||||
let pos = closingTagStart + tagName.length + 2;
|
||||
while (pos < html.length && html.charCodeAt(pos) !== GT_CHAR) {
|
||||
pos++;
|
||||
}
|
||||
return pos < html.length ? pos + 1 : pos;
|
||||
}
|
||||
function applyHeadToHtml(template, headHtml) {
|
||||
const { html, indexes } = template;
|
||||
const parts = [];
|
||||
let lastIndex = 0;
|
||||
if (indexes.htmlTagStart >= 0) {
|
||||
parts.push(html.substring(lastIndex, indexes.htmlTagStart));
|
||||
parts.push(`<html${headHtml.htmlAttrs}>`);
|
||||
lastIndex = indexes.htmlTagEnd;
|
||||
}
|
||||
if (indexes.headTagEnd >= 0) {
|
||||
parts.push(html.substring(lastIndex, indexes.headTagEnd));
|
||||
parts.push(headHtml.headTags);
|
||||
parts.push("</head>");
|
||||
lastIndex = indexes.headTagEnd + 7;
|
||||
}
|
||||
if (indexes.bodyTagStart >= 0) {
|
||||
parts.push(html.substring(lastIndex, indexes.bodyTagStart));
|
||||
if (headHtml.bodyTagsOpen) {
|
||||
parts.push(`<body${headHtml.bodyAttrs}>
|
||||
${headHtml.bodyTagsOpen}`);
|
||||
} else {
|
||||
parts.push(`<body${headHtml.bodyAttrs}>`);
|
||||
}
|
||||
lastIndex = indexes.bodyTagEnd;
|
||||
}
|
||||
if (indexes.bodyCloseTagStart >= 0) {
|
||||
parts.push(html.substring(lastIndex, indexes.bodyCloseTagStart));
|
||||
parts.push(headHtml.bodyTags);
|
||||
parts.push(html.substring(indexes.bodyCloseTagStart));
|
||||
} else {
|
||||
parts.push(html.substring(lastIndex));
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
export { TagIdMap, applyHeadToHtml, parseAttributes, parseHtmlForIndexes, parseHtmlForUnheadExtraction, prepareTemplate };
|
||||
+746
@@ -0,0 +1,746 @@
|
||||
export { A as AliasSortingPlugin, D as DeprecationsPlugin, P as PromisesPlugin, T as TemplateParamsPlugin } from './shared/unhead.tQnRaXfX.mjs';
|
||||
import { d as defineHeadPlugin } from './shared/unhead.CHEy9ana.mjs';
|
||||
export { F as FlatMetaPlugin, S as SafeInputPlugin } from './shared/unhead.jEy9BmRq.mjs';
|
||||
import { h as hasContent } from './shared/unhead.CGPOfp5O.mjs';
|
||||
import { minifyJS, minifyCSS, minifyJSON } from './minify.mjs';
|
||||
import { u as unpackMeta } from './shared/unhead.BQWSw36o.mjs';
|
||||
import { t as tagInputFromRuntime, u as tagPredicates, i as inputShapeFromRuntime, b as inputShapePredicates, U as URL_META_KEYS, v as titleInputFromRuntime, h as headInputPredicates } from './shared/unhead.BEmVMkeA.mjs';
|
||||
import './shared/unhead.BGFxPGPQ.mjs';
|
||||
|
||||
const HTTP_SCHEME_RE = /^https?:\/\//i;
|
||||
const META_TRANSFORMABLE_URL = [
|
||||
"og:url",
|
||||
"og:image",
|
||||
"og:image:url",
|
||||
"og:image:secure_url",
|
||||
"twitter:image",
|
||||
"twitter:image:src",
|
||||
"og:video",
|
||||
"og:video:url",
|
||||
"og:video:secure_url",
|
||||
"og:audio",
|
||||
"og:audio:url",
|
||||
"og:audio:secure_url",
|
||||
"twitter:player",
|
||||
"twitter:player:stream"
|
||||
];
|
||||
const LINK_REL_RESOLVABLE = /* @__PURE__ */ new Set([
|
||||
"canonical",
|
||||
"next",
|
||||
"prev",
|
||||
"alternate",
|
||||
"author",
|
||||
"license",
|
||||
"help",
|
||||
"search",
|
||||
"pingback"
|
||||
]);
|
||||
const META_CANONICAL_URL = /* @__PURE__ */ new Set([
|
||||
"og:url"
|
||||
]);
|
||||
function CanonicalPlugin(options) {
|
||||
return (head) => {
|
||||
let host = options.canonicalHost || (!head.ssr ? window.location.origin : "");
|
||||
if (!HTTP_SCHEME_RE.test(host) && !host.startsWith("//")) {
|
||||
host = `https://${host}`;
|
||||
}
|
||||
host = new URL(host).origin;
|
||||
const whitelist = options.queryWhitelist !== void 0 ? options.queryWhitelist : [];
|
||||
function normalizeCanonicalUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(url, host);
|
||||
parsed.hash = "";
|
||||
if (whitelist !== false && parsed.search) {
|
||||
const filtered = new URLSearchParams();
|
||||
for (const key of whitelist) {
|
||||
if (parsed.searchParams.has(key)) {
|
||||
for (const value of parsed.searchParams.getAll(key)) {
|
||||
filtered.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
parsed.search = filtered.toString();
|
||||
}
|
||||
if (options.trailingSlash === true && !parsed.pathname.endsWith("/")) {
|
||||
parsed.pathname = `${parsed.pathname}/`;
|
||||
} else if (options.trailingSlash === false && parsed.pathname !== "/" && parsed.pathname.endsWith("/")) {
|
||||
parsed.pathname = parsed.pathname.slice(0, -1);
|
||||
}
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
function resolvePath(path) {
|
||||
if (options?.customResolver) {
|
||||
return options.customResolver(path);
|
||||
}
|
||||
if (HTTP_SCHEME_RE.test(path) || path.startsWith("//"))
|
||||
return path;
|
||||
try {
|
||||
return new URL(path, host).toString();
|
||||
} catch {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: "canonical",
|
||||
hooks: {
|
||||
"tags:resolve": (ctx) => {
|
||||
for (const tag of ctx.tags) {
|
||||
const metaKey = tag.props?.property || tag.props?.name;
|
||||
if (tag.tag === "meta" && META_TRANSFORMABLE_URL.includes(metaKey)) {
|
||||
tag.props.content = resolvePath(tag.props.content);
|
||||
if (META_CANONICAL_URL.has(metaKey)) {
|
||||
tag.props.content = normalizeCanonicalUrl(tag.props.content);
|
||||
}
|
||||
} else if (tag.tag === "link" && LINK_REL_RESOLVABLE.has(tag.props.rel)) {
|
||||
const isCanonical = tag.props.rel === "canonical";
|
||||
tag.props.href = resolvePath(tag.props.href);
|
||||
if (isCanonical) {
|
||||
tag.props.href = normalizeCanonicalUrl(tag.props.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function InferSeoMetaPlugin(options = {}) {
|
||||
return defineHeadPlugin((head) => {
|
||||
if (options.twitterCard !== false) {
|
||||
head.push({
|
||||
meta: [
|
||||
{
|
||||
name: "twitter:card",
|
||||
content: options.twitterCard || "summary_large_image",
|
||||
tagPriority: "low"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
head.push({
|
||||
meta: [
|
||||
// content is intentionally omitted - inferred from title/description by the hook below
|
||||
{
|
||||
"property": "og:title",
|
||||
"tagPriority": "low",
|
||||
"data-infer": ""
|
||||
},
|
||||
{
|
||||
"property": "og:description",
|
||||
"tagPriority": "low",
|
||||
"data-infer": ""
|
||||
}
|
||||
]
|
||||
});
|
||||
return {
|
||||
key: "infer-seo-meta",
|
||||
hooks: {
|
||||
"tags:beforeResolve": ({ tagMap }) => {
|
||||
let title = head._titleTemplate || head._title;
|
||||
const ogTitle = tagMap.get("meta:og:title");
|
||||
if (typeof ogTitle?.props["data-infer"] !== "undefined") {
|
||||
if (typeof title === "function") {
|
||||
title = title(head._title);
|
||||
}
|
||||
title = hasContent(title) ? String(title) : void 0;
|
||||
ogTitle.props.content = options.ogTitle ? options.ogTitle(title) : title || "";
|
||||
ogTitle.processTemplateParams = true;
|
||||
}
|
||||
const descriptionValue = tagMap.get("meta:description")?.props?.content;
|
||||
const description = hasContent(descriptionValue) ? String(descriptionValue) : void 0;
|
||||
const ogDescription = tagMap.get("meta:og:description");
|
||||
if (typeof ogDescription?.props["data-infer"] !== "undefined") {
|
||||
ogDescription.props.content = options.ogDescription ? options.ogDescription(description) : description || "";
|
||||
ogDescription.processTemplateParams = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, "infer-seo-meta");
|
||||
}
|
||||
|
||||
const JSON_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json", "speculationrules", "importmap"]);
|
||||
function MinifyPlugin(options) {
|
||||
const jsMinify = options?.js === false ? false : options?.js || minifyJS;
|
||||
const cssMinify = options?.css === false ? false : options?.css || minifyCSS;
|
||||
const jsonMinify = options?.json !== false;
|
||||
const omitLineBreaks = options?.omitLineBreaks === true;
|
||||
return {
|
||||
key: "minify",
|
||||
hooks: {
|
||||
"ssr:render": (ctx) => {
|
||||
if (omitLineBreaks)
|
||||
ctx.options.omitLineBreaks = true;
|
||||
for (const tag of ctx.tags) {
|
||||
const content = tag.innerHTML;
|
||||
if (!content || content.length < 20)
|
||||
continue;
|
||||
if (tag.tag === "script") {
|
||||
const type = tag.props?.type;
|
||||
if (type && JSON_TYPES.has(type)) {
|
||||
if (jsonMinify) {
|
||||
const min = minifyJSON(content);
|
||||
if (min.length < content.length)
|
||||
tag.innerHTML = min;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (jsMinify) {
|
||||
const min = jsMinify(content);
|
||||
if (min.length < content.length)
|
||||
tag.innerHTML = min;
|
||||
}
|
||||
} else if (tag.tag === "style" && cssMinify) {
|
||||
const min = cssMinify(content);
|
||||
if (min.length < content.length)
|
||||
tag.innerHTML = min;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const TEMPLATE_PARAM_RE = /%\w+(?:\.\w+)?%/;
|
||||
const AT_PREFIX_RE = /^at\s+/;
|
||||
const JAVASCRIPT_TYPES = /* @__PURE__ */ new Set([
|
||||
"application/ecmascript",
|
||||
"application/javascript",
|
||||
"application/x-ecmascript",
|
||||
"application/x-javascript",
|
||||
"text/ecmascript",
|
||||
"text/javascript",
|
||||
"text/javascript1.0",
|
||||
"text/javascript1.1",
|
||||
"text/javascript1.2",
|
||||
"text/javascript1.3",
|
||||
"text/javascript1.4",
|
||||
"text/javascript1.5",
|
||||
"text/jscript",
|
||||
"text/livescript",
|
||||
"text/x-ecmascript",
|
||||
"text/x-javascript"
|
||||
]);
|
||||
const SLACK_TWITTER_META_NAMES = /* @__PURE__ */ new Set([
|
||||
"twitter:data1",
|
||||
"twitter:data2",
|
||||
"twitter:label1",
|
||||
"twitter:label2"
|
||||
]);
|
||||
function isExecutableScript(type) {
|
||||
const value = typeof type === "boolean" ? "" : String(type ?? "");
|
||||
if (!value)
|
||||
return true;
|
||||
const normalized = value.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, "").toLowerCase();
|
||||
return normalized === "module" || JAVASCRIPT_TYPES.has(normalized);
|
||||
}
|
||||
const PREDICATE_SEVERITY = {
|
||||
"defer-on-module-script": "info",
|
||||
"deprecated-prop-body": "warn",
|
||||
"deprecated-prop-children": "warn",
|
||||
"deprecated-prop-hid-vmid": "warn",
|
||||
"empty-meta-content": "warn",
|
||||
"html-in-title": "warn",
|
||||
"invalid-input-shape": "warn",
|
||||
"non-absolute-canonical": "warn",
|
||||
"numeric-tag-priority": "info",
|
||||
"possible-typo": "warn",
|
||||
"preload-font-crossorigin": "warn",
|
||||
"preload-missing-as": "warn",
|
||||
"robots-conflict": "warn",
|
||||
"script-src-with-content": "warn",
|
||||
"streamed-tag-hidden-from-bots": "warn",
|
||||
"twitter-handle-missing-at": "warn",
|
||||
"viewport-user-scalable": "info"
|
||||
};
|
||||
function isAbsoluteUrl(url) {
|
||||
return url.startsWith("http://") || url.startsWith("https://");
|
||||
}
|
||||
function extractOrigin(url) {
|
||||
if (!isAbsoluteUrl(url))
|
||||
return void 0;
|
||||
const slash = url.indexOf("/", url.indexOf("//") + 2);
|
||||
return slash === -1 ? url : url.slice(0, slash);
|
||||
}
|
||||
function resolveSeverity(config, fallback) {
|
||||
if (config == null)
|
||||
return fallback;
|
||||
return Array.isArray(config) ? config[0] : config;
|
||||
}
|
||||
function resolveOptions(config, id, defaults) {
|
||||
const entry = config[id];
|
||||
if (Array.isArray(entry))
|
||||
return { ...defaults, ...entry[1] };
|
||||
return defaults;
|
||||
}
|
||||
function captureSource(root) {
|
||||
const stack = new Error("source").stack;
|
||||
if (!stack)
|
||||
return void 0;
|
||||
const lines = stack.split("\n");
|
||||
for (let i = 4; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line && !line.includes("node_modules") && !line.includes("unhead/src/")) {
|
||||
let source = line.replace(AT_PREFIX_RE, "");
|
||||
if (root) {
|
||||
const prefix = root.endsWith("/") ? root : `${root}/`;
|
||||
source = source.replace(prefix, "./");
|
||||
}
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
function createInputShapeObserver() {
|
||||
let remainingRootKeys;
|
||||
let resolvedValues;
|
||||
const resolver = (key, value) => {
|
||||
if (key === void 0) {
|
||||
if (value?.constructor === Object) {
|
||||
remainingRootKeys = new Set(Object.keys(value));
|
||||
resolvedValues = /* @__PURE__ */ new Map();
|
||||
} else {
|
||||
remainingRootKeys = void 0;
|
||||
resolvedValues = void 0;
|
||||
}
|
||||
} else if (remainingRootKeys?.delete(key)) {
|
||||
resolvedValues?.set(key, value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
resolver._static = true;
|
||||
return {
|
||||
resolver,
|
||||
take() {
|
||||
const input = resolvedValues ? Object.fromEntries(resolvedValues) : void 0;
|
||||
remainingRootKeys = void 0;
|
||||
resolvedValues = void 0;
|
||||
return input;
|
||||
}
|
||||
};
|
||||
}
|
||||
const BOT_HEAD_META_NAMES = /* @__PURE__ */ new Set(["description", "robots", "googlebot", "bingbot", "slurp", "keywords"]);
|
||||
const BOT_HEAD_META_EQUIVS = /* @__PURE__ */ new Set(["refresh", "content-language"]);
|
||||
const BOT_HEAD_LINK_RELS = /* @__PURE__ */ new Set(["canonical", "alternate", "amphtml", "prev", "next", "author", "license"]);
|
||||
const BOT_HEAD_META_PREFIX_RE = /^(?:og|twitter|article|book|profile|fb|al|music|video|place|product):/;
|
||||
const JSON_LD_TYPE_RE = /^[\t\n\f\r ]*application\/ld\+json[\t\n\f\r ]*(?:;|$)/i;
|
||||
const REL_SEPARATOR_RE = /[\t\n\f\r ]+/;
|
||||
const SAFE_BLOCKED_RELS = /* @__PURE__ */ new Set(["canonical", "modulepreload", "prerender", "preload", "prefetch", "dns-prefetch", "preconnect", "manifest", "pingback"]);
|
||||
function relTokens(value) {
|
||||
return String(value || "").toLowerCase().split(REL_SEPARATOR_RE);
|
||||
}
|
||||
function* expandPendingTag(tag) {
|
||||
if (tag._safe && tag.tag === "link" && relTokens(tag.props.rel).some((rel) => SAFE_BLOCKED_RELS.has(rel)))
|
||||
return;
|
||||
if (tag.props.body)
|
||||
tag.tagPosition = "bodyClose";
|
||||
if (tag.tag !== "_flatMeta") {
|
||||
yield tag;
|
||||
return;
|
||||
}
|
||||
for (const props of unpackMeta(tag.props))
|
||||
yield { ...tag, tag: "meta", props };
|
||||
}
|
||||
function isHiddenFromBots(tag, writesBodyTags) {
|
||||
const props = tag.props;
|
||||
if (tag.tag === "script")
|
||||
return !writesBodyTags && JSON_LD_TYPE_RE.test(String(props.type || ""));
|
||||
if (tag.tagPosition?.startsWith("body"))
|
||||
return false;
|
||||
switch (tag.tag) {
|
||||
case "title":
|
||||
case "titleTemplate":
|
||||
case "base":
|
||||
return true;
|
||||
case "meta": {
|
||||
if (BOT_HEAD_META_NAMES.has(String(props.name || "").toLowerCase()))
|
||||
return true;
|
||||
if (BOT_HEAD_META_EQUIVS.has(String(props["http-equiv"] || "").toLowerCase()))
|
||||
return true;
|
||||
return BOT_HEAD_META_PREFIX_RE.test(String(props.property || props.name || "").toLowerCase());
|
||||
}
|
||||
case "link":
|
||||
return relTokens(props.rel).some((rel) => BOT_HEAD_LINK_RELS.has(rel));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function describeTag(tag) {
|
||||
const props = tag.props;
|
||||
if (tag.tag === "meta")
|
||||
return `meta[${props.property ? `property="${props.property}"` : props["http-equiv"] ? `http-equiv="${props["http-equiv"]}"` : `name="${props.name}"`}]`;
|
||||
if (tag.tag === "link")
|
||||
return `link[rel="${props.rel}"]`;
|
||||
if (tag.tag === "script")
|
||||
return 'script[type="application/ld+json"]';
|
||||
return tag.tag;
|
||||
}
|
||||
function ValidatePlugin(options = {}) {
|
||||
const ruleConfig = options.rules || {};
|
||||
const root = options.root;
|
||||
const only = options.only && new Set(options.only);
|
||||
const pluginKey = options.key || "validate";
|
||||
function severityFor(id, fallback) {
|
||||
if (only && !only.has(id))
|
||||
return "off";
|
||||
return resolveSeverity(ruleConfig[id], fallback);
|
||||
}
|
||||
return defineHeadPlugin((head) => {
|
||||
const stacks = /* @__PURE__ */ new Map();
|
||||
const pendingInputDiagnostics = [];
|
||||
const inputShapeObserver = createInputShapeObserver();
|
||||
head.resolvedOptions.propResolvers = [
|
||||
...head.resolvedOptions.propResolvers || [],
|
||||
inputShapeObserver.resolver
|
||||
];
|
||||
const hooks = head.hooks;
|
||||
if (hooks) {
|
||||
const _callHook = hooks.callHook.bind(hooks);
|
||||
let warnedAsyncHook = false;
|
||||
hooks.callHook = (name, ...args) => {
|
||||
const result = _callHook(name, ...args);
|
||||
if (result?.then && !warnedAsyncHook && !only) {
|
||||
warnedAsyncHook = true;
|
||||
console.warn(`[unhead] promise ignored: ${name}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
const _push = head.push.bind(head);
|
||||
head.push = (input, opts) => {
|
||||
if (opts?.mode && severityFor("deprecated-option-mode", "warn") !== "off") {
|
||||
console.warn(`[unhead] "mode: '${opts.mode}'" option was removed in v3. Use the appropriate createHead import (unhead/client or unhead/server) instead.`);
|
||||
}
|
||||
const source = captureSource(root);
|
||||
const active = _push(input, opts);
|
||||
if (source)
|
||||
stacks.set(active._i, source);
|
||||
const _dispose = active.dispose;
|
||||
active.dispose = () => {
|
||||
stacks.delete(active._i);
|
||||
_dispose();
|
||||
};
|
||||
return active;
|
||||
};
|
||||
function dispatch(rules, mode) {
|
||||
const head_ = head;
|
||||
head_._validationRules = mode === "replace" ? rules : [...head_._validationRules || [], ...rules];
|
||||
if (!rules.length)
|
||||
return;
|
||||
if (options.onReport) {
|
||||
options.onReport(rules);
|
||||
return;
|
||||
}
|
||||
for (const rule of rules) {
|
||||
const loc = rule.source ? ` (${rule.source})` : "";
|
||||
console.warn(`[unhead] ${rule.message}${loc}`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: pluginKey,
|
||||
hooks: {
|
||||
"ssr:streamChunk": ({ tags }) => {
|
||||
const severity = severityFor("streamed-tag-hidden-from-bots", "warn");
|
||||
if (severity === "off")
|
||||
return;
|
||||
const rules = [];
|
||||
for (const pending of tags) {
|
||||
for (const tag of expandPendingTag(pending)) {
|
||||
if (!isHiddenFromBots(tag, !!head._stream?.writesBodyTags))
|
||||
continue;
|
||||
const entryIndex = tag._p != null ? tag._p >> 10 : void 0;
|
||||
rules.push({
|
||||
id: "streamed-tag-hidden-from-bots",
|
||||
message: `Bots will not see ${describeTag(tag)}. It arrived after the shell, so it ships as a client patch. Register it before the shell.`,
|
||||
severity,
|
||||
source: entryIndex != null ? stacks.get(entryIndex) : void 0,
|
||||
tag
|
||||
});
|
||||
}
|
||||
}
|
||||
dispatch(rules, "append");
|
||||
},
|
||||
"entries:normalize": ({ entry }) => {
|
||||
const input = inputShapeObserver.take();
|
||||
if (!input || input.constructor !== Object)
|
||||
return;
|
||||
const shape = inputShapeFromRuntime("head", input);
|
||||
for (const predicate of Object.values(inputShapePredicates)) {
|
||||
for (const diagnostic of predicate(shape))
|
||||
pendingInputDiagnostics.push({ diagnostic, entryIndex: entry._i });
|
||||
}
|
||||
},
|
||||
"tags:afterResolve": ({ tags }) => {
|
||||
const rules = [];
|
||||
function report(id, message, defaultSeverity, tag, inputEntryIndex) {
|
||||
const severity = severityFor(id, defaultSeverity);
|
||||
if (severity === "off")
|
||||
return;
|
||||
const entryIndex = inputEntryIndex ?? (tag?._p != null ? tag._p >> 10 : void 0);
|
||||
const source = entryIndex != null ? stacks.get(entryIndex) : void 0;
|
||||
rules.push({ id, message, severity, source, tag });
|
||||
}
|
||||
for (const { diagnostic, entryIndex } of pendingInputDiagnostics) {
|
||||
const severity = PREDICATE_SEVERITY[diagnostic.ruleId] ?? "warn";
|
||||
report(diagnostic.ruleId, diagnostic.message, severity, void 0, entryIndex);
|
||||
}
|
||||
pendingInputDiagnostics.length = 0;
|
||||
const metaByKey = /* @__PURE__ */ new Map();
|
||||
let canonicalHref;
|
||||
let hasOgTags = false;
|
||||
let hasTitle = false;
|
||||
let hasDescription = false;
|
||||
let isIndexable = true;
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "title")
|
||||
hasTitle = true;
|
||||
if (tag.tag === "meta") {
|
||||
const key = tag.props.property || (tag.props.name ? String(tag.props.name).toLowerCase() : void 0);
|
||||
if (key) {
|
||||
metaByKey.set(key, tag);
|
||||
if (key.startsWith("og:"))
|
||||
hasOgTags = true;
|
||||
if (key === "description")
|
||||
hasDescription = true;
|
||||
if (key === "robots" && String(tag.props.content ?? "").toLowerCase().includes("noindex"))
|
||||
isIndexable = false;
|
||||
}
|
||||
}
|
||||
if (tag.tag === "link" && tag.props.rel === "canonical")
|
||||
canonicalHref = tag.props.href;
|
||||
}
|
||||
function emitFromPredicates(diagnostics, tag) {
|
||||
for (const diag of diagnostics) {
|
||||
const sev = PREDICATE_SEVERITY[diag.ruleId] ?? "warn";
|
||||
report(diag.ruleId, diag.message, sev, tag);
|
||||
}
|
||||
}
|
||||
for (const tag of tags) {
|
||||
const { props } = tag;
|
||||
const metaKey = props.property || (props.name ? String(props.name).toLowerCase() : void 0);
|
||||
const tagInput = tagInputFromRuntime(tag);
|
||||
if (tagInput) {
|
||||
for (const predicate of Object.values(tagPredicates))
|
||||
emitFromPredicates(predicate(tagInput), tag);
|
||||
}
|
||||
if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
|
||||
const inputShape = inputShapeFromRuntime(tag.tag, tag.props);
|
||||
for (const predicate of Object.values(inputShapePredicates))
|
||||
emitFromPredicates(predicate(inputShape), tag);
|
||||
}
|
||||
if (tag.tag === "meta" && typeof metaKey === "string") {
|
||||
const normalizedMetaKey = metaKey.toLowerCase();
|
||||
if (normalizedMetaKey.startsWith("twitter:") && !SLACK_TWITTER_META_NAMES.has(normalizedMetaKey))
|
||||
report("deprecated-twitter-meta", `${normalizedMetaKey} is deprecated. Use Open Graph metadata instead.`, "warn", tag);
|
||||
}
|
||||
if (tag.tag === "meta" && metaKey && URL_META_KEYS.has(metaKey)) {
|
||||
const content = String(props.content ?? "");
|
||||
if (content && !isAbsoluteUrl(content))
|
||||
report("non-absolute-og-url", `${metaKey} should be an absolute URL, received "${content}".`, "warn", tag);
|
||||
}
|
||||
if (tag.tag === "meta" && metaKey) {
|
||||
const content = String(props.content ?? "");
|
||||
if (content && TEMPLATE_PARAM_RE.test(content))
|
||||
report("unresolved-template-param", `Unresolved template param in ${metaKey}: "${content}".`, "warn", tag);
|
||||
}
|
||||
if (tag.tag === "title") {
|
||||
const titleInput = titleInputFromRuntime(tag);
|
||||
if (titleInput) {
|
||||
for (const diag of headInputPredicates["no-html-in-title"](titleInput))
|
||||
report(diag.ruleId, diag.message, "warn", tag);
|
||||
}
|
||||
const text = String(tag.textContent ?? "");
|
||||
if (TEMPLATE_PARAM_RE.test(text))
|
||||
report("unresolved-template-param", `Unresolved template param in title: "${text}".`, "warn", tag);
|
||||
if (!text.trim())
|
||||
report("empty-title", `Title tag is empty. If using titleTemplate, ensure it produces output.`, "warn", tag);
|
||||
}
|
||||
if (tag.tag === "script" && props.src && !props.async && !props.defer && props.type !== "module" && (!tag.tagPosition || tag.tagPosition === "head"))
|
||||
report("render-blocking-script", `Script "${props.src}" is render-blocking. Add "async", "defer", or use type="module" to avoid blocking the critical rendering path.`, "warn", tag);
|
||||
if (tag.tag === "link" && props.rel === "preload" && props.fetchpriority === "low" && props.as !== "script")
|
||||
report("preload-fetchpriority-conflict", `This preload uses fetchpriority="low". Check whether the resource needs a higher priority.`, "off", tag);
|
||||
if (tag.tag === "style" && (tag.innerHTML || tag.textContent)) {
|
||||
const content = tag.innerHTML || tag.textContent || "";
|
||||
const sizeKB = new TextEncoder().encode(content).byteLength / 1024;
|
||||
const { maxKB: styleMaxKB } = resolveOptions(ruleConfig, "inline-style-size", { maxKB: 14 });
|
||||
if (sizeKB > styleMaxKB)
|
||||
report("inline-style-size", `Inline <style> is ${sizeKB.toFixed(1)}KB \u2014 exceeds ${styleMaxKB}KB critical CSS budget. Consider moving to an external stylesheet for cacheability.`, "info", tag);
|
||||
}
|
||||
if (tag.tag === "script" && isExecutableScript(props.type) && !props.src && (tag.innerHTML || tag.textContent)) {
|
||||
const content = tag.innerHTML || tag.textContent || "";
|
||||
const sizeKB = new TextEncoder().encode(content).byteLength / 1024;
|
||||
const { maxKB: scriptMaxKB } = resolveOptions(ruleConfig, "inline-script-size", { maxKB: 2 });
|
||||
if (sizeKB > scriptMaxKB)
|
||||
report("inline-script-size", `Inline <script> is ${sizeKB.toFixed(1)}KB \u2014 consider moving to an external file for cacheability.`, "info", tag);
|
||||
}
|
||||
}
|
||||
const ogUrl = metaByKey.get("og:url");
|
||||
if (canonicalHref && ogUrl?.props.content && canonicalHref !== ogUrl.props.content)
|
||||
report("canonical-og-url-mismatch", `Canonical URL "${canonicalHref}" differs from og:url "${ogUrl.props.content}".`, "warn", ogUrl);
|
||||
if (metaByKey.has("og:image") && (!metaByKey.has("og:image:width") || !metaByKey.has("og:image:height")))
|
||||
report("og-image-missing-dimensions", `og:image is set but og:image:width and/or og:image:height are missing \u2014 social platforms may not display the image.`, "warn", metaByKey.get("og:image"));
|
||||
if (hasOgTags) {
|
||||
if (!metaByKey.has("og:title"))
|
||||
report("og-missing-title", `Open Graph tags are present but og:title is missing.`, "warn");
|
||||
if (!metaByKey.has("og:description"))
|
||||
report("og-missing-description", `Open Graph tags are present but og:description is missing.`, "warn");
|
||||
}
|
||||
if (!hasTitle)
|
||||
report("missing-title", `Page is missing a <title> tag.`, "warn");
|
||||
if (!hasDescription && isIndexable)
|
||||
report("missing-description", `Page is missing a meta description and is indexable by search engines.`, "warn");
|
||||
const { max: maxPreloads } = resolveOptions(ruleConfig, "too-many-preloads", { max: 6 });
|
||||
const preloadCount = tags.filter((t) => t.tag === "link" && t.props.rel === "preload").length;
|
||||
if (preloadCount > maxPreloads)
|
||||
report("too-many-preloads", `Found ${preloadCount} preload links \u2014 more than ${maxPreloads} preloads compete for bandwidth and can hurt performance.`, "warn");
|
||||
const { max: maxPrefetches } = resolveOptions(ruleConfig, "too-many-prefetches", { max: 50 });
|
||||
const prefetchCount = tags.filter((t) => t.tag === "link" && t.props.rel === "prefetch").length;
|
||||
if (prefetchCount > maxPrefetches)
|
||||
report("too-many-prefetches", `Found ${prefetchCount} prefetch links. The configured maximum of ${maxPrefetches} is an advisory guardrail, not a standards limit. Large speculative fetch sets can consume bandwidth and cache capacity.`, "info");
|
||||
const { max: maxPreconnects } = resolveOptions(ruleConfig, "too-many-preconnects", { max: 4 });
|
||||
const preconnectCount = tags.filter((t) => t.tag === "link" && t.props.rel === "preconnect").length;
|
||||
if (preconnectCount > maxPreconnects)
|
||||
report("too-many-preconnects", `Found ${preconnectCount} preconnect links \u2014 each initiates a TCP+TLS handshake, more than ${maxPreconnects} compete for limited connections.`, "warn");
|
||||
const preconnectOrigins = /* @__PURE__ */ new Set();
|
||||
const dnsPrefetchTags = [];
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.href) {
|
||||
if (tag.props.rel === "preconnect")
|
||||
preconnectOrigins.add(tag.props.href);
|
||||
else if (tag.props.rel === "dns-prefetch")
|
||||
dnsPrefetchTags.push(tag);
|
||||
}
|
||||
}
|
||||
for (const tag of dnsPrefetchTags) {
|
||||
if (preconnectOrigins.has(tag.props.href))
|
||||
report("redundant-dns-prefetch", `dns-prefetch for "${tag.props.href}" is redundant \u2014 preconnect already includes DNS resolution.`, "info", tag);
|
||||
}
|
||||
const preloadScriptHrefs = /* @__PURE__ */ new Map();
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.rel === "preload" && tag.props.as === "script" && tag.props.href && tag.props.fetchpriority !== "low")
|
||||
preloadScriptHrefs.set(tag.props.href, tag);
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "script" && tag.props.src && (tag.props.async || tag.props.defer)) {
|
||||
const preloadTag = preloadScriptHrefs.get(tag.props.src);
|
||||
if (preloadTag) {
|
||||
const attr = tag.props.async ? "async" : "defer";
|
||||
report("preload-async-defer-conflict", `Script "${tag.props.src}" is preloaded with "${attr}". Check whether early fetching is intentional.`, "off", preloadTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
const preloadHrefs = /* @__PURE__ */ new Set();
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.rel === "preload" && tag.props.href)
|
||||
preloadHrefs.add(tag.props.href);
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.rel === "prefetch" && tag.props.href && preloadHrefs.has(tag.props.href))
|
||||
report("prefetch-preload-conflict", `"${tag.props.href}" has both preload and prefetch \u2014 use preload for current page resources, prefetch for future navigation.`, "warn", tag);
|
||||
}
|
||||
if (!head.plugins.has("template-params")) {
|
||||
const tpTag = tags.find((t) => t.tag === "templateParams");
|
||||
if (tpTag)
|
||||
report("missing-template-params-plugin", `templateParams are set but TemplateParamsPlugin is not registered. In v3, this plugin is opt-in. Add it to createHead({ plugins: [TemplateParamsPlugin] }).`, "warn", tpTag);
|
||||
}
|
||||
if (!head.plugins.has("aliasSorting")) {
|
||||
for (const tag of tags) {
|
||||
const p = typeof tag.tagPriority === "string" ? tag.tagPriority : "";
|
||||
if (p.startsWith("before:") || p.startsWith("after:")) {
|
||||
report("missing-alias-sorting-plugin", `Tag priority alias "${p}" requires AliasSortingPlugin. In v3, this plugin is opt-in. Add it to createHead({ plugins: [AliasSortingPlugin] }).`, "warn", tag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const { max: maxHighPriority } = resolveOptions(ruleConfig, "too-many-fetchpriority-high", { max: 2 });
|
||||
const highPriorityCount = tags.filter((t) => t.props.fetchpriority === "high").length;
|
||||
if (highPriorityCount > maxHighPriority)
|
||||
report("too-many-fetchpriority-high", `Found ${highPriorityCount} resources with fetchpriority="high". When everything is high priority, nothing is. Limit to ${maxHighPriority} for the signal to be effective.`, "warn");
|
||||
const resourceHintsSeen = /* @__PURE__ */ new Map();
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.href && (tag.props.rel === "preload" || tag.props.rel === "prefetch" || tag.props.rel === "preconnect")) {
|
||||
const crossoriginSuffix = tag.props.rel === "preconnect" && "crossorigin" in tag.props ? ":cors" : "";
|
||||
const key = `${tag.props.rel}:${tag.props.href}${crossoriginSuffix}`;
|
||||
if (resourceHintsSeen.has(key))
|
||||
report("duplicate-resource-hint", `Duplicate ${tag.props.rel} for "${tag.props.href}".`, "warn", tag);
|
||||
else
|
||||
resourceHintsSeen.set(key, tag);
|
||||
}
|
||||
}
|
||||
if (head.ssr) {
|
||||
const { maxPosition: charsetMaxPos } = resolveOptions(ruleConfig, "charset-not-early", { maxPosition: 3 });
|
||||
const headElementTags = /* @__PURE__ */ new Set(["title", "base", "meta", "link", "style", "script", "noscript"]);
|
||||
const sortedHeadTags = tags.filter((t) => headElementTags.has(t.tag) && (!t.tagPosition || t.tagPosition === "head")).sort((a, b) => (a._w ?? 100) === (b._w ?? 100) ? (a._p ?? 0) - (b._p ?? 0) : (a._w ?? 100) - (b._w ?? 100));
|
||||
let charsetTag;
|
||||
let charsetPosition = -1;
|
||||
for (let i = 0; i < sortedHeadTags.length; i++) {
|
||||
const tag = sortedHeadTags[i];
|
||||
if (tag.tag === "meta" && ("charset" in tag.props || tag.props["http-equiv"]?.toLowerCase() === "content-type")) {
|
||||
charsetTag = tag;
|
||||
charsetPosition = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (charsetTag && charsetPosition > charsetMaxPos)
|
||||
report("charset-not-early", `<meta charset> is at position ${charsetPosition} in <head>. It should be within the first ${charsetMaxPos} tags so the browser doesn't need to re-parse.`, "warn", charsetTag);
|
||||
}
|
||||
const moduleScriptSrcs = /* @__PURE__ */ new Set();
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "script" && tag.props.type === "module" && tag.props.src)
|
||||
moduleScriptSrcs.add(tag.props.src);
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.rel === "preload" && tag.props.as === "script" && tag.props.href && moduleScriptSrcs.has(tag.props.href))
|
||||
report("preload-not-modulepreload", `"${tag.props.href}" is a module script but uses rel="preload". Use rel="modulepreload" instead to also trigger module parsing.`, "warn", tag);
|
||||
}
|
||||
const corsOrigins = /* @__PURE__ */ new Set();
|
||||
const preconnectCorsOrigins = /* @__PURE__ */ new Set();
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.href && "crossorigin" in tag.props) {
|
||||
const origin = extractOrigin(tag.props.href);
|
||||
if (origin) {
|
||||
corsOrigins.add(origin);
|
||||
if (tag.props.rel === "preconnect")
|
||||
preconnectCorsOrigins.add(origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (tag.tag === "link" && tag.props.rel === "preconnect" && tag.props.href && !("crossorigin" in tag.props)) {
|
||||
const origin = extractOrigin(tag.props.href);
|
||||
if (origin && corsOrigins.has(origin) && !preconnectCorsOrigins.has(origin))
|
||||
report("preconnect-missing-crossorigin", `Preconnect to "${tag.props.href}" is missing "crossorigin" but CORS resources are loaded from this origin. Without it, the browser opens a separate connection for CORS requests.`, "warn", tag);
|
||||
}
|
||||
}
|
||||
const { maxBytes: crawlerMaxBytes } = resolveOptions(ruleConfig, "meta-beyond-1mb", { maxBytes: 1048576 });
|
||||
let byteOffset = 0;
|
||||
for (const tag of tags) {
|
||||
if (tag.tagPosition && tag.tagPosition !== "head")
|
||||
continue;
|
||||
const props = Object.entries(tag.props).filter(([, v]) => v !== false && v != null).map(([k, v]) => v === true || v === "" ? ` ${k}` : ` ${k}="${v}"`).join("");
|
||||
const content = tag.innerHTML || tag.textContent || "";
|
||||
const tagSize = `<${tag.tag}${props}>${content}</${tag.tag}>
|
||||
`.length;
|
||||
byteOffset += tagSize;
|
||||
if (byteOffset > crawlerMaxBytes && tag.tag === "meta") {
|
||||
const key = tag.props.property || tag.props.name || "unknown";
|
||||
report(
|
||||
"meta-beyond-1mb",
|
||||
`Meta tag "${key}" is rendered ~${(byteOffset / 1024).toFixed(0)}KB into <head>, beyond the ${(crawlerMaxBytes / 1048576).toFixed(0)}MB crawler parsing limit. Social crawlers (Facebook, Twitter) may not see it. Use \`tagPriority\` to promote it, or configure a custom \`tagWeight\` to reorder tags for bot requests.`,
|
||||
"warn",
|
||||
tag
|
||||
);
|
||||
}
|
||||
}
|
||||
dispatch(rules, "replace");
|
||||
}
|
||||
}
|
||||
};
|
||||
}, pluginKey);
|
||||
}
|
||||
|
||||
export { CanonicalPlugin, InferSeoMetaPlugin, MinifyPlugin, ValidatePlugin, defineHeadPlugin };
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { parseHtmlForUnheadExtraction, applyHeadToHtml, parseHtmlForIndexes } from './parser.mjs';
|
||||
export { prepareTemplate } from './parser.mjs';
|
||||
import { a as capoTagWeight } from './shared/unhead.Dssd7L5I.mjs';
|
||||
export { c as createHead, b as createServerRenderer, e as escapeHtml, p as propsToString, r as renderSSRHead, s as ssrRenderTags, t as tagToString } from './shared/unhead.Dssd7L5I.mjs';
|
||||
import { n as normalizeEntryToTags, d as dedupeKey, h as hashTag } from './shared/unhead.-hZVKou0.mjs';
|
||||
import './shared/unhead.Bf_fPVYA.mjs';
|
||||
import './shared/unhead.D7HkBzZn.mjs';
|
||||
import './shared/unhead.Bm4Y6XQI.mjs';
|
||||
import 'hookable';
|
||||
import './shared/unhead.CGPOfp5O.mjs';
|
||||
|
||||
let extractedTemplates;
|
||||
function cloneExtractedInput(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.map(cloneExtractedInput);
|
||||
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
||||
const clone = {};
|
||||
for (const key of Object.keys(value))
|
||||
clone[key] = cloneExtractedInput(value[key]);
|
||||
return clone;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function freezeExtractedInput(value) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value)
|
||||
freezeExtractedInput(item);
|
||||
} else if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
||||
for (const key of Object.keys(value))
|
||||
freezeExtractedInput(value[key]);
|
||||
}
|
||||
return value && typeof value === "object" ? Object.freeze(value) : value;
|
||||
}
|
||||
function hasHooks(head) {
|
||||
const hooks = head.hooks?._hooks || {};
|
||||
for (const key in hooks) {
|
||||
if (hooks[key]?.length)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function precomputeExtractedTags(input) {
|
||||
const tags = normalizeEntryToTags(input, []);
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tag = tags[i];
|
||||
tag._w = capoTagWeight(tag);
|
||||
tag._p = i;
|
||||
tag._d = dedupeKey(tag);
|
||||
if (!tag._d)
|
||||
tag._h = hashTag(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
function extractPreparedTemplate(prepared) {
|
||||
const cache = extractedTemplates ||= /* @__PURE__ */ new WeakMap();
|
||||
let extracted = cache.get(prepared);
|
||||
if (!extracted) {
|
||||
const template = parseHtmlForUnheadExtraction(prepared.html);
|
||||
freezeExtractedInput(template.input);
|
||||
Object.freeze(template.indexes);
|
||||
Object.freeze(template);
|
||||
const tags = precomputeExtractedTags(template.input);
|
||||
freezeExtractedInput(tags);
|
||||
extracted = Object.freeze({
|
||||
template,
|
||||
tags
|
||||
});
|
||||
cache.set(prepared, extracted);
|
||||
}
|
||||
return extracted;
|
||||
}
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function transformHtmlTemplate(head, html) {
|
||||
let cached;
|
||||
let template;
|
||||
if (typeof html === "string") {
|
||||
template = parseHtmlForUnheadExtraction(html);
|
||||
} else {
|
||||
cached = extractPreparedTemplate(html);
|
||||
template = cached.template;
|
||||
}
|
||||
const hooked = cached ? hasHooks(head) : false;
|
||||
const hasDynamicResolver = cached ? head.resolvedOptions.propResolvers?.some((resolver) => !resolver._static) || false : false;
|
||||
const input = cached && (hooked || hasDynamicResolver) ? cloneExtractedInput(template.input) : template.input;
|
||||
head.push(input, { _index: 0 });
|
||||
if (cached && !hooked && head.resolvedOptions._tagWeight === capoTagWeight && !hasDynamicResolver) {
|
||||
const entry = head.entries.get(0);
|
||||
if (entry)
|
||||
entry._precomputedTags = cached.tags;
|
||||
}
|
||||
return applyHeadToHtml(template, head.render());
|
||||
}
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function transformHtmlTemplateRaw(head, html) {
|
||||
const template = typeof html === "string" ? parseHtmlForIndexes(html) : html;
|
||||
return applyHeadToHtml(template, head.render());
|
||||
}
|
||||
|
||||
export { capoTagWeight, transformHtmlTemplate, transformHtmlTemplateRaw };
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
import { U as UniqueTags, c as TagsWithInnerContent, M as MetaTagsArrayable, i as isUnsafeKey, H as HasElementTags, T as TagConfigKeys, D as DupeableTags, d as UsesMergeStrategy, V as ValidHeadTags, h as hasContent } from './unhead.CGPOfp5O.mjs';
|
||||
import { a as callHook } from './unhead.Bm4Y6XQI.mjs';
|
||||
|
||||
const META_NOREWRITE_RE = /^(?:viewport|description|keywords|robots)$/;
|
||||
const META_KEY_ATTRS = ["name", "property", "http-equiv"];
|
||||
function isMetaArrayDupeKey(v) {
|
||||
const i = v.indexOf(":");
|
||||
if (i === -1)
|
||||
return false;
|
||||
const key = v.slice(i + 1);
|
||||
return MetaTagsArrayable.has(key) || key.startsWith("og:image:") || key.startsWith("og:video:") || key.startsWith("og:audio:") || key.startsWith("twitter:image:");
|
||||
}
|
||||
function dedupeKey(tag) {
|
||||
const { props, tag: t, key } = tag;
|
||||
if (UniqueTags.has(t))
|
||||
return t;
|
||||
if (t === "link") {
|
||||
if (props.rel === "canonical")
|
||||
return "canonical";
|
||||
if (props.rel === "alternate" && props.hreflang)
|
||||
return `alternate:${props.hreflang}`;
|
||||
}
|
||||
if (props.charset)
|
||||
return "charset";
|
||||
if (t === "meta") {
|
||||
for (const n of META_KEY_ATTRS) {
|
||||
const v = props[n];
|
||||
if (v !== void 0)
|
||||
return `meta:${v}${(typeof v !== "string" || !v.includes(":")) && !META_NOREWRITE_RE.test(v) && key ? `:key:${key}` : ""}`;
|
||||
}
|
||||
}
|
||||
if (key)
|
||||
return `${t}:key:${key}`;
|
||||
if (props.id)
|
||||
return `${t}:id:${props.id}`;
|
||||
if (t === "link" && props.rel && props.href)
|
||||
return `link:${props.rel}:${props.href}`;
|
||||
return TagsWithInnerContent.has(t) && (tag.textContent || tag.innerHTML) ? `${t}:content:${tag.textContent || tag.innerHTML}` : void 0;
|
||||
}
|
||||
function hashTag(tag) {
|
||||
const identity = tag._h || tag._d || tag.textContent || tag.innerHTML;
|
||||
if (identity)
|
||||
return identity;
|
||||
const keys = Object.keys(tag.props).sort();
|
||||
let hash = `${tag.tag}:`;
|
||||
let separator = "";
|
||||
for (const key of keys) {
|
||||
hash += `${separator}${key}:${String(tag.props[key])}`;
|
||||
separator = ",";
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
function walkResolver(val, resolve, key) {
|
||||
if (key === "_resolver")
|
||||
return val;
|
||||
if (typeof val === "function" && (!key || key !== "titleTemplate" && !key.startsWith("on")))
|
||||
val = val();
|
||||
const v = resolve ? resolve(key, val) : val;
|
||||
if (Array.isArray(v)) {
|
||||
let out;
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
const r = walkResolver(v[i], resolve);
|
||||
if (out) {
|
||||
out[i] = r;
|
||||
} else if (r !== v[i]) {
|
||||
out = v.slice(0, i);
|
||||
out[i] = r;
|
||||
}
|
||||
}
|
||||
return out || v;
|
||||
}
|
||||
if (v?.constructor === Object) {
|
||||
let next;
|
||||
for (const k in v) {
|
||||
const unsafe = isUnsafeKey(k);
|
||||
const r = unsafe ? void 0 : walkResolver(v[k], resolve, k);
|
||||
if (!next && (unsafe || r !== v[k])) {
|
||||
next = {};
|
||||
for (const pk in v) {
|
||||
if (pk === k)
|
||||
break;
|
||||
next[pk] = v[pk];
|
||||
}
|
||||
}
|
||||
if (next && !unsafe)
|
||||
next[k] = r;
|
||||
}
|
||||
return next || v;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const INVALID_ATTR_NAME_RE = /[\s"'<>/=\x00-\x1F\x7F]/;
|
||||
|
||||
function normalizeStyleClassProps(key, value) {
|
||||
const isStyle = key === "style";
|
||||
const store = isStyle ? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Set();
|
||||
const add = (v) => {
|
||||
if (!v)
|
||||
return;
|
||||
if (isStyle) {
|
||||
const i = v.indexOf(":");
|
||||
i > 0 && store.set(v.slice(0, i).trim(), v.slice(i + 1).trim());
|
||||
} else {
|
||||
v.split(" ").forEach((c) => c && store.add(c));
|
||||
}
|
||||
};
|
||||
if (typeof value === "string") {
|
||||
(isStyle ? value.split(";") : [value]).forEach(add);
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach(add);
|
||||
} else if (value && typeof value === "object") {
|
||||
for (const k in value) {
|
||||
const v = value[k];
|
||||
v && v !== "false" && (isStyle ? store.set(k.trim(), String(v)) : add(k));
|
||||
}
|
||||
}
|
||||
return store;
|
||||
}
|
||||
function normalizeProps(tag, input) {
|
||||
tag.props = tag.props || {};
|
||||
if (!input)
|
||||
return tag;
|
||||
if (tag.tag === "templateParams") {
|
||||
tag.props = input;
|
||||
return tag;
|
||||
}
|
||||
const isHtmlTag = HasElementTags.has(tag.tag) || tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs";
|
||||
for (const prop in input) {
|
||||
if (isUnsafeKey(prop))
|
||||
continue;
|
||||
const isData = prop.startsWith("data-");
|
||||
const isHtmlAttr = isHtmlTag && !TagConfigKeys.has(prop);
|
||||
const key = isHtmlAttr && !isData ? prop.toLowerCase() : prop;
|
||||
if (isHtmlAttr && (!key || INVALID_ATTR_NAME_RE.test(key)))
|
||||
continue;
|
||||
const value = input[prop];
|
||||
if (value === null) {
|
||||
tag.props[key] = null;
|
||||
} else if (prop === "class" || prop === "style") {
|
||||
tag.props[prop] = normalizeStyleClassProps(prop, value);
|
||||
} else if (TagConfigKeys.has(prop)) {
|
||||
if ((prop === "textContent" || prop === "innerHTML") && typeof value === "object") {
|
||||
const type = input.type || "application/json";
|
||||
if (type.endsWith("json") || type === "speculationrules" || type === "importmap") {
|
||||
tag.props.type = type;
|
||||
tag[prop] = JSON.stringify(value);
|
||||
}
|
||||
} else {
|
||||
tag[prop] = value;
|
||||
}
|
||||
} else if (value !== void 0) {
|
||||
const str = String(value);
|
||||
const isMeta = tag.tag === "meta" && key === "content";
|
||||
tag.props[key] = str === "true" || str === "" ? isData || isMeta ? str : true : !value && isData && str === "false" ? "false" : value;
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
function resolveHeadInput(input, propResolvers) {
|
||||
let resolve;
|
||||
if (propResolvers.length) {
|
||||
resolve = (key, val) => {
|
||||
for (let i = 0; i < propResolvers.length; i++)
|
||||
val = propResolvers[i](key, val);
|
||||
return val;
|
||||
};
|
||||
input = resolve(void 0, input);
|
||||
}
|
||||
return walkResolver(input, resolve);
|
||||
}
|
||||
function normalizeTag(tagName, _input) {
|
||||
const input = typeof _input === "object" && typeof _input !== "function" ? _input : { [tagName === "script" || tagName === "noscript" || tagName === "style" ? "innerHTML" : "textContent"]: _input };
|
||||
const tag = normalizeProps({ tag: tagName, props: {} }, input);
|
||||
if (tag.key && DupeableTags.has(tag.tag))
|
||||
tag.props["data-hid"] = tag._h = tag.key;
|
||||
if (tag.tag === "script" && typeof tag.innerHTML === "object") {
|
||||
tag.innerHTML = JSON.stringify(tag.innerHTML);
|
||||
tag.props.type = tag.props.type || "application/json";
|
||||
}
|
||||
if (Array.isArray(tag.props.content)) {
|
||||
const tags = [];
|
||||
for (const content of tag.props.content) {
|
||||
tags.push({ ...tag, props: { ...tag.props, content } });
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
function pushNormalizedTag(tags, tag) {
|
||||
if (Array.isArray(tag)) {
|
||||
for (const t of tag) tags.push(t);
|
||||
} else {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
function normalizeEntryToTags(input, propResolvers) {
|
||||
if (!input)
|
||||
return [];
|
||||
if (typeof input === "function")
|
||||
input = input();
|
||||
input = resolveHeadInput(input, propResolvers);
|
||||
const tags = [];
|
||||
for (const key in input) {
|
||||
const value = input[key];
|
||||
if (value !== void 0) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) pushNormalizedTag(tags, normalizeTag(key, v));
|
||||
} else {
|
||||
pushNormalizedTag(tags, normalizeTag(key, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
const LT_RE = /</g;
|
||||
const SCRIPT_END_RE = /<\/script/g;
|
||||
const sortTags = (a, b) => a._w === b._w ? a._p - b._p : a._w - b._w;
|
||||
const DEFAULT_TAG_WEIGHT = () => 100;
|
||||
function isEmptyProps(props) {
|
||||
for (const _ in props)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
const TAG_MUTATING_HOOK_RE = /^tags:|:render/;
|
||||
function syncEntryHookCache(head, hooks) {
|
||||
const count = (hooks["entries:resolve"]?.length || 0) + (hooks["entries:normalize"]?.length || 0);
|
||||
if (head._h !== count) {
|
||||
head._h = count;
|
||||
for (const entry of head.entries.values())
|
||||
delete entry._tags;
|
||||
}
|
||||
}
|
||||
function cloneTagsInPlace(tags) {
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const t = tags[i];
|
||||
const props = { ...t.props };
|
||||
if (props.class instanceof Set)
|
||||
props.class = new Set(props.class);
|
||||
if (props.style instanceof Map)
|
||||
props.style = new Map(props.style);
|
||||
tags[i] = { ...t, props };
|
||||
}
|
||||
}
|
||||
function valuesToTags(ctx, sortFlatMeta) {
|
||||
const tags = ctx.tags;
|
||||
let w = 0;
|
||||
for (const value of ctx.tagMap.values()) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const tag of value) tags[w++] = tag;
|
||||
} else {
|
||||
tags[w++] = value;
|
||||
}
|
||||
}
|
||||
tags.length = w;
|
||||
if (sortFlatMeta)
|
||||
tags.sort(sortTags);
|
||||
}
|
||||
function dedupeTags(ctx) {
|
||||
let hasFlatMeta = false;
|
||||
for (const next of ctx.tags.sort(sortTags)) {
|
||||
const k = next._d || hashTag(next);
|
||||
if (!k)
|
||||
continue;
|
||||
const prev = ctx.tagMap.get(k);
|
||||
if (!prev) {
|
||||
ctx.tagMap.set(k, next);
|
||||
continue;
|
||||
}
|
||||
const strategy = next.tagDuplicateStrategy || (UsesMergeStrategy.has(next.tag) ? "merge" : null) || (next.key && next.key === prev.key ? "merge" : null);
|
||||
if (strategy === "merge") {
|
||||
const props = { ...prev.props };
|
||||
for (const p in next.props) {
|
||||
props[p] = p === "style" ? new Map([...prev.props.style || /* @__PURE__ */ new Map(), ...next.props[p]]) : p === "class" ? /* @__PURE__ */ new Set([...prev.props.class || [], ...next.props[p]]) : next.props[p];
|
||||
}
|
||||
ctx.tagMap.set(k, { ...next, props });
|
||||
} else if (next._p >> 10 === prev._p >> 10 && next.tag === "meta" && isMetaArrayDupeKey(k)) {
|
||||
ctx.tagMap.set(k, Object.assign([...Array.isArray(prev) ? prev : [prev], next], next));
|
||||
hasFlatMeta = true;
|
||||
} else if (next._w === prev._w ? next._p > prev._p : next._w < prev._w) {
|
||||
ctx.tagMap.set(k, next);
|
||||
}
|
||||
}
|
||||
return hasFlatMeta;
|
||||
}
|
||||
function resolveTitleTemplate(ctx, head) {
|
||||
const title = ctx.tagMap.get("title");
|
||||
const tpl = ctx.tagMap.get("titleTemplate");
|
||||
head._title = title?.textContent;
|
||||
if (!tpl)
|
||||
return;
|
||||
const fn = tpl.textContent;
|
||||
head._titleTemplate = fn;
|
||||
if (!fn)
|
||||
return;
|
||||
let v = typeof fn === "function" ? fn(title?.textContent) : fn;
|
||||
if (typeof v === "string" && !head.plugins.has("template-params"))
|
||||
v = v.replace("%s", title?.textContent || "");
|
||||
if (title) {
|
||||
v === null ? ctx.tagMap.delete("title") : ctx.tagMap.set("title", { ...title, textContent: v });
|
||||
} else {
|
||||
ctx.tagMap.set("titleTemplate", { ...tpl, tag: "title", textContent: v });
|
||||
}
|
||||
}
|
||||
function sanitizeTagsInPlace(tags) {
|
||||
let w = 0;
|
||||
for (let t of tags) {
|
||||
const { innerHTML, tag, props } = t;
|
||||
if (!ValidHeadTags.has(tag) || isEmptyProps(props) && !hasContent(innerHTML) && !hasContent(t.textContent))
|
||||
continue;
|
||||
if (tag === "meta") {
|
||||
if (!hasContent(props.content) && !props["http-equiv"] && !props.charset)
|
||||
continue;
|
||||
}
|
||||
if (tag === "script" && (innerHTML || t.textContent)) {
|
||||
const type = String(props.type);
|
||||
const isJsonLike = type.endsWith("json") || type === "importmap" || type === "speculationrules";
|
||||
const escape = (content) => isJsonLike ? (typeof content === "string" ? content : JSON.stringify(content)).replace(LT_RE, "\\u003C") : typeof content === "string" ? content.replace(SCRIPT_END_RE, "<\\/script") : content;
|
||||
t = { ...t };
|
||||
if (innerHTML)
|
||||
t.innerHTML = escape(innerHTML);
|
||||
if (t.textContent)
|
||||
t.textContent = escape(t.textContent);
|
||||
t._d = dedupeKey(t);
|
||||
}
|
||||
tags[w++] = t;
|
||||
}
|
||||
tags.length = w;
|
||||
return tags;
|
||||
}
|
||||
function sanitizeTags(tags) {
|
||||
return sanitizeTagsInPlace([...tags]);
|
||||
}
|
||||
function resolveTags(head, options) {
|
||||
const weightFn = options?.tagWeight ?? head.resolvedOptions._tagWeight ?? DEFAULT_TAG_WEIGHT;
|
||||
const ctx = { tagMap: /* @__PURE__ */ new Map(), tags: [] };
|
||||
const hooks = head.hooks?._hooks || {};
|
||||
syncEntryHookCache(head, hooks);
|
||||
for (const e of head.entries.values()) {
|
||||
if (e._pending !== void 0) {
|
||||
e.input = e._pending;
|
||||
delete e._pending;
|
||||
delete e._tags;
|
||||
delete e._precomputedTags;
|
||||
}
|
||||
}
|
||||
let entries;
|
||||
if (hooks["entries:resolve"]?.length || hooks["entries:normalize"]?.length) {
|
||||
entries = [...head.entries.values()];
|
||||
if (hooks["entries:resolve"]?.length)
|
||||
callHook(head, "entries:resolve", { entries, ...ctx });
|
||||
}
|
||||
syncEntryHookCache(head, hooks);
|
||||
for (const e of entries || head.entries.values()) {
|
||||
let tags = e._tags;
|
||||
if (!tags) {
|
||||
if (e._precomputedTags && weightFn === head.resolvedOptions._tagWeight && !hooks["entries:normalize"]?.length && !hooks["entries:resolve"]?.length && (!e.options || isEmptyProps(e.options))) {
|
||||
tags = e._precomputedTags;
|
||||
} else {
|
||||
tags = normalizeEntryToTags(e.input, head.resolvedOptions.propResolvers || []);
|
||||
if (e.options && !isEmptyProps(e.options)) {
|
||||
for (const t of tags)
|
||||
Object.assign(t, e.options);
|
||||
}
|
||||
if (hooks["entries:normalize"]?.length) {
|
||||
const normalizeCtx = { tags, entry: e };
|
||||
callHook(head, "entries:normalize", normalizeCtx);
|
||||
tags = normalizeCtx.tags;
|
||||
}
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const t = tags[i];
|
||||
t._w = weightFn(t);
|
||||
t._p = (e._i << 10) + i;
|
||||
t._d = dedupeKey(t);
|
||||
if (!t._d)
|
||||
t._h = hashTag(t);
|
||||
}
|
||||
e._tags = tags;
|
||||
}
|
||||
}
|
||||
ctx.tags.push(...tags);
|
||||
}
|
||||
for (const name in hooks) {
|
||||
if (hooks[name]?.length && TAG_MUTATING_HOOK_RE.test(name)) {
|
||||
cloneTagsInPlace(ctx.tags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const hasFlatMeta = dedupeTags(ctx);
|
||||
resolveTitleTemplate(ctx, head);
|
||||
valuesToTags(ctx, hasFlatMeta);
|
||||
callHook(head, "tags:beforeResolve", ctx);
|
||||
callHook(head, "tags:resolve", ctx);
|
||||
callHook(head, "tags:afterResolve", ctx);
|
||||
return sanitizeTagsInPlace(ctx.tags);
|
||||
}
|
||||
|
||||
export { INVALID_ATTR_NAME_RE as I, dedupeTags as a, normalizeProps as b, resolveTitleTemplate as c, dedupeKey as d, resolveHeadInput as e, hashTag as h, isMetaArrayDupeKey as i, normalizeEntryToTags as n, resolveTags as r, sanitizeTags as s, walkResolver as w };
|
||||
+673
@@ -0,0 +1,673 @@
|
||||
const URL_META_KEYS = /* @__PURE__ */ new Set([
|
||||
"og:url",
|
||||
"og:image",
|
||||
"og:image:url",
|
||||
"og:image:secure_url",
|
||||
"og:video",
|
||||
"og:video:url",
|
||||
"og:video:secure_url",
|
||||
"og:audio",
|
||||
"og:audio:url",
|
||||
"og:audio:secure_url",
|
||||
"twitter:image",
|
||||
"twitter:image:src",
|
||||
"twitter:player",
|
||||
"twitter:player:stream",
|
||||
"payment:success_url"
|
||||
]);
|
||||
const KNOWN_META_PROPERTIES = /* @__PURE__ */ new Set([
|
||||
"article:author",
|
||||
"article:expiration_time",
|
||||
"article:modified_time",
|
||||
"article:published_time",
|
||||
"article:section",
|
||||
"article:tag",
|
||||
"book:author",
|
||||
"book:isbn",
|
||||
"book:release_date",
|
||||
"book:tag",
|
||||
"fb:app_id",
|
||||
"og:audio",
|
||||
"og:audio:secure_url",
|
||||
"og:audio:type",
|
||||
"og:audio:url",
|
||||
"og:description",
|
||||
"og:determiner",
|
||||
"og:image",
|
||||
"og:image:alt",
|
||||
"og:image:height",
|
||||
"og:image:secure_url",
|
||||
"og:image:type",
|
||||
"og:image:url",
|
||||
"og:image:width",
|
||||
"og:locale",
|
||||
"og:locale:alternate",
|
||||
"og:site_name",
|
||||
"og:title",
|
||||
"og:type",
|
||||
"og:url",
|
||||
"og:video",
|
||||
"og:video:alt",
|
||||
"og:video:duration",
|
||||
"og:video:height",
|
||||
"og:video:secure_url",
|
||||
"og:video:type",
|
||||
"og:video:url",
|
||||
"og:video:width",
|
||||
"payment:amount",
|
||||
"payment:currency",
|
||||
"payment:description",
|
||||
"payment:expires_at",
|
||||
"payment:id",
|
||||
"payment:status",
|
||||
"payment:success_url",
|
||||
"profile:first_name",
|
||||
"profile:gender",
|
||||
"profile:last_name",
|
||||
"profile:username"
|
||||
]);
|
||||
const KNOWN_META_NAMES = /* @__PURE__ */ new Set([
|
||||
"apple-itunes-app",
|
||||
"apple-mobile-web-app-capable",
|
||||
"apple-mobile-web-app-status-bar-style",
|
||||
"apple-mobile-web-app-title",
|
||||
"application-name",
|
||||
"author",
|
||||
"color-scheme",
|
||||
"creator",
|
||||
"description",
|
||||
"fb:app_id",
|
||||
"fediverse:creator",
|
||||
"format-detection",
|
||||
"generator",
|
||||
"google-site-verification",
|
||||
"google",
|
||||
"googlebot",
|
||||
"keywords",
|
||||
"mobile-web-app-capable",
|
||||
"msapplication-config",
|
||||
"msapplication-tilecolor",
|
||||
"msapplication-tileimage",
|
||||
"publisher",
|
||||
"rating",
|
||||
"referrer",
|
||||
"robots",
|
||||
"theme-color",
|
||||
"viewport",
|
||||
"twitter:app:id:googleplay",
|
||||
"twitter:app:id:ipad",
|
||||
"twitter:app:id:iphone",
|
||||
"twitter:app:name:googleplay",
|
||||
"twitter:app:name:ipad",
|
||||
"twitter:app:name:iphone",
|
||||
"twitter:app:url:googleplay",
|
||||
"twitter:app:url:ipad",
|
||||
"twitter:app:url:iphone",
|
||||
"twitter:card",
|
||||
"twitter:creator",
|
||||
"twitter:creator:id",
|
||||
"twitter:data:1",
|
||||
"twitter:data:2",
|
||||
"twitter:description",
|
||||
"twitter:image",
|
||||
"twitter:image:alt",
|
||||
"twitter:label:1",
|
||||
"twitter:label:2",
|
||||
"twitter:player",
|
||||
"twitter:player:height",
|
||||
"twitter:player:stream",
|
||||
"twitter:player:width",
|
||||
"twitter:site",
|
||||
"twitter:site:id",
|
||||
"twitter:title"
|
||||
]);
|
||||
const TAG_PRIORITY_ALIASES = ["critical", "high", "low"];
|
||||
const DEPRECATED_PROPS = {
|
||||
children: { replacement: "innerHTML", ruleId: "deprecated-prop-children" },
|
||||
hid: { replacement: "key", ruleId: "deprecated-prop-hid-vmid" },
|
||||
vmid: { replacement: "key", ruleId: "deprecated-prop-hid-vmid" },
|
||||
body: { replacement: "tagPosition: 'bodyClose'", ruleId: "deprecated-prop-body" }
|
||||
};
|
||||
|
||||
function levenshtein(a, b) {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const d = Array.from({ length: n + 1 }, (_, i) => i);
|
||||
for (let i = 1; i <= m; i++) {
|
||||
let prev = i - 1;
|
||||
d[0] = i;
|
||||
for (let j = 1; j <= n; j++) {
|
||||
const tmp = d[j];
|
||||
d[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, d[j], d[j - 1]);
|
||||
prev = tmp;
|
||||
}
|
||||
}
|
||||
return d[n];
|
||||
}
|
||||
function findClosestMatch(value, knownSet) {
|
||||
const threshold = value.length <= 8 ? 2 : 3;
|
||||
let best;
|
||||
let bestDist = threshold + 1;
|
||||
for (const known of knownSet) {
|
||||
if (Math.abs(known.length - value.length) > threshold)
|
||||
continue;
|
||||
const dist = levenshtein(value, known);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = known;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
const emptyMetaContent = (tag) => {
|
||||
if (tag.tagType !== "meta")
|
||||
return [];
|
||||
if (!tag.keys.has("content"))
|
||||
return [];
|
||||
if (tag.props.content !== "")
|
||||
return [];
|
||||
const key = typeof tag.props.name === "string" && tag.props.name || typeof tag.props.property === "string" && tag.props.property || "meta";
|
||||
const diag = {
|
||||
ruleId: "empty-meta-content",
|
||||
message: `Meta tag "${key}" has empty content.`,
|
||||
at: { kind: "prop", key: "content" }
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const RESOLVABLE_OBJECT = ["boolean", "function", "null", "object"];
|
||||
const RESOLVABLE_TAG_LIST = ["array", "boolean", "function", "null"];
|
||||
const RESOLVABLE_TITLE = ["boolean", "function", "null", "number", "object", "string"];
|
||||
const ATTRIBUTE_SCALAR = ["boolean", "function", "null", "number", "string"];
|
||||
const ATTRIBUTE_STRUCTURED = ["array", "boolean", "function", "null", "number", "object", "string"];
|
||||
const INPUT_SHAPE_FIELDS = {
|
||||
base: {
|
||||
canonical: "base",
|
||||
contexts: { head: RESOLVABLE_OBJECT }
|
||||
},
|
||||
bodyattrs: {
|
||||
canonical: "bodyAttrs",
|
||||
contexts: { head: RESOLVABLE_OBJECT }
|
||||
},
|
||||
class: {
|
||||
canonical: "class",
|
||||
contexts: {
|
||||
bodyAttrs: ATTRIBUTE_STRUCTURED,
|
||||
htmlAttrs: ATTRIBUTE_STRUCTURED
|
||||
}
|
||||
},
|
||||
htmlattrs: {
|
||||
canonical: "htmlAttrs",
|
||||
contexts: { head: RESOLVABLE_OBJECT }
|
||||
},
|
||||
link: {
|
||||
canonical: "link",
|
||||
contexts: { head: RESOLVABLE_TAG_LIST }
|
||||
},
|
||||
meta: {
|
||||
canonical: "meta",
|
||||
contexts: { head: RESOLVABLE_TAG_LIST }
|
||||
},
|
||||
noscript: {
|
||||
canonical: "noscript",
|
||||
contexts: { head: RESOLVABLE_TAG_LIST }
|
||||
},
|
||||
script: {
|
||||
canonical: "script",
|
||||
contexts: { head: RESOLVABLE_TAG_LIST }
|
||||
},
|
||||
style: {
|
||||
canonical: "style",
|
||||
contexts: {
|
||||
bodyAttrs: ATTRIBUTE_STRUCTURED,
|
||||
head: RESOLVABLE_TAG_LIST,
|
||||
htmlAttrs: ATTRIBUTE_STRUCTURED
|
||||
}
|
||||
},
|
||||
templateparams: {
|
||||
canonical: "templateParams",
|
||||
contexts: { head: ["object"] }
|
||||
},
|
||||
title: {
|
||||
canonical: "title",
|
||||
contexts: {
|
||||
bodyAttrs: ATTRIBUTE_SCALAR,
|
||||
head: RESOLVABLE_TITLE,
|
||||
htmlAttrs: ATTRIBUTE_SCALAR,
|
||||
seoMeta: RESOLVABLE_TITLE
|
||||
}
|
||||
},
|
||||
titletemplate: {
|
||||
canonical: "titleTemplate",
|
||||
contexts: {
|
||||
head: ["function", "null", "object", "string"],
|
||||
seoMeta: ["function", "null", "object", "string"]
|
||||
}
|
||||
}
|
||||
};
|
||||
function includes(kinds, kind) {
|
||||
return kinds?.includes(kind) === true;
|
||||
}
|
||||
function formatKinds(kinds) {
|
||||
return kinds.join(", ");
|
||||
}
|
||||
function invalidKnownHeadField(input, key, shape, kind) {
|
||||
const expected = shape.contexts[input.context];
|
||||
if (includes(expected, kind))
|
||||
return void 0;
|
||||
if ((input.context === "htmlAttrs" || input.context === "bodyAttrs") && kind === "null")
|
||||
return void 0;
|
||||
if (input.context === "head") {
|
||||
return {
|
||||
ruleId: "invalid-input-shape",
|
||||
message: `"${shape.canonical}" in a head input must be one of: ${formatKinds(expected || [])}. Received ${kind}.`,
|
||||
at: { kind: "prop-value", key }
|
||||
};
|
||||
}
|
||||
if (includes(shape.contexts.head, kind)) {
|
||||
return {
|
||||
ruleId: "invalid-input-shape",
|
||||
message: `"${shape.canonical}" has a head input shape but appears in ${input.context}. Move it to the top-level head input.`,
|
||||
at: { kind: "prop-value", key }
|
||||
};
|
||||
}
|
||||
}
|
||||
function invalidAttributeField(input, key, kind) {
|
||||
if (includes(ATTRIBUTE_SCALAR, kind))
|
||||
return void 0;
|
||||
return {
|
||||
ruleId: "invalid-input-shape",
|
||||
message: `"${key}" in ${input.context} must resolve to a scalar attribute value. Received ${kind}.`,
|
||||
at: { kind: "prop-value", key }
|
||||
};
|
||||
}
|
||||
function validateInputShape(input) {
|
||||
const diagnostics = [];
|
||||
const isAttributes = input.context === "htmlAttrs" || input.context === "bodyAttrs";
|
||||
for (const key of input.keys) {
|
||||
const kind = input.valueKinds.get(key) ?? "unknown";
|
||||
if (kind === "unknown")
|
||||
continue;
|
||||
const shape = INPUT_SHAPE_FIELDS[key.toLowerCase()];
|
||||
const knownDiagnostic = shape ? invalidKnownHeadField(input, key, shape, kind) : void 0;
|
||||
if (knownDiagnostic) {
|
||||
diagnostics.push(knownDiagnostic);
|
||||
continue;
|
||||
}
|
||||
if (isAttributes && !shape?.contexts[input.context]) {
|
||||
const attributeDiagnostic = invalidAttributeField(input, key, kind);
|
||||
if (attributeDiagnostic)
|
||||
diagnostics.push(attributeDiagnostic);
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
const noDeprecatedProps = (tag) => {
|
||||
const out = [];
|
||||
for (const key of tag.keys) {
|
||||
if (!(key in DEPRECATED_PROPS))
|
||||
continue;
|
||||
const { replacement } = DEPRECATED_PROPS[key];
|
||||
if (key === "body") {
|
||||
if (tag.props.body !== true)
|
||||
continue;
|
||||
const fix2 = tag.keys.has("tagPosition") ? void 0 : { type: "replace-prop", key: "body", newSource: `tagPosition: 'bodyClose'` };
|
||||
out.push({
|
||||
ruleId: "deprecated-prop-body",
|
||||
message: `"body" was removed in v3 of unhead. Use "${replacement}" instead.`,
|
||||
at: { kind: "prop", key },
|
||||
fix: fix2
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const newKey = key === "children" ? "innerHTML" : "key";
|
||||
const fix = tag.keys.has(newKey) ? void 0 : { type: "rename-prop", key, newKey };
|
||||
out.push({
|
||||
ruleId: key === "children" ? "deprecated-prop-children" : "deprecated-prop-hid-vmid",
|
||||
message: `"${key}" was removed in v3 of unhead. Use "${replacement}" instead.`,
|
||||
at: { kind: "prop", key },
|
||||
fix
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const HTML_CHARS_RE = /[<>]/;
|
||||
const noHtmlInTitle = (input) => {
|
||||
const title = input.props.title;
|
||||
if (typeof title !== "string" || !HTML_CHARS_RE.test(title))
|
||||
return [];
|
||||
const diag = {
|
||||
ruleId: "html-in-title",
|
||||
message: `Title contains HTML characters which will be escaped, not rendered: "${title}".`,
|
||||
at: { kind: "prop-value", key: "title" }
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const OG_PREFIX_RE = /^(?:og|article|book|profile|fb):/;
|
||||
const noUnknownMeta = (tag) => {
|
||||
if (tag.tagType !== "meta")
|
||||
return [];
|
||||
const out = [];
|
||||
const property = tag.props.property;
|
||||
if (typeof property === "string" && !KNOWN_META_PROPERTIES.has(property) && OG_PREFIX_RE.test(property)) {
|
||||
const suggestion = findClosestMatch(property, KNOWN_META_PROPERTIES);
|
||||
if (suggestion) {
|
||||
out.push({
|
||||
ruleId: "possible-typo",
|
||||
message: `Unknown meta property "${property}". Did you mean "${suggestion}"?`,
|
||||
at: { kind: "prop-value", key: "property" },
|
||||
fix: { type: "replace-prop-value", key: "property", newSource: `'${suggestion}'` }
|
||||
});
|
||||
}
|
||||
}
|
||||
const name = tag.props.name;
|
||||
if (typeof name === "string") {
|
||||
const lower = name.toLowerCase();
|
||||
if (!KNOWN_META_NAMES.has(lower) && (lower.startsWith("twitter:") || lower.startsWith("fediverse:") || !lower.includes(":"))) {
|
||||
const suggestion = findClosestMatch(lower, KNOWN_META_NAMES);
|
||||
if (suggestion) {
|
||||
out.push({
|
||||
ruleId: "possible-typo",
|
||||
message: `Unknown meta name "${name}". Did you mean "${suggestion}"?`,
|
||||
at: { kind: "prop-value", key: "name" },
|
||||
fix: { type: "replace-prop-value", key: "name", newSource: `'${suggestion}'` }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
function isAbsolute(url) {
|
||||
return url.startsWith("http://") || url.startsWith("https://");
|
||||
}
|
||||
const nonAbsoluteCanonical = (tag) => {
|
||||
if (tag.tagType !== "link")
|
||||
return [];
|
||||
if (tag.props.rel !== "canonical")
|
||||
return [];
|
||||
const href = tag.props.href;
|
||||
if (typeof href !== "string")
|
||||
return [];
|
||||
if (isAbsolute(href))
|
||||
return [];
|
||||
const diag = {
|
||||
ruleId: "non-absolute-canonical",
|
||||
message: `Canonical URL should be absolute, received "${href}".`,
|
||||
at: { kind: "prop-value", key: "href" }
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const ALIASES = ["critical", "high", "low"];
|
||||
const numericTagPriority = (tag) => {
|
||||
const value = tag.props.tagPriority;
|
||||
if (typeof value !== "number")
|
||||
return [];
|
||||
const diag = {
|
||||
ruleId: "numeric-tag-priority",
|
||||
message: `Numeric tagPriority (${value}) is brittle. Prefer an alias ('critical' | 'high' | 'low') or 'before:<key>' / 'after:<key>'.`,
|
||||
at: { kind: "prop-value", key: "tagPriority" },
|
||||
suggestions: ALIASES.map((alias) => ({
|
||||
message: `Replace with '${alias}'`,
|
||||
fix: { type: "replace-prop-value", key: "tagPriority", newSource: `'${alias}'` }
|
||||
}))
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const TAG_TO_HELPER = {
|
||||
link: "defineLink",
|
||||
script: "defineScript"
|
||||
};
|
||||
const preferDefineHelpers = (tag, ctx) => {
|
||||
if (!tag.inArray)
|
||||
return [];
|
||||
const helper = TAG_TO_HELPER[tag.tagType];
|
||||
if (!helper)
|
||||
return [];
|
||||
const localName = ctx?.importedHelpers?.get(helper);
|
||||
const imported = localName !== void 0;
|
||||
const fix = { type: "wrap-tag", wrapWith: localName ?? helper };
|
||||
const diag = {
|
||||
ruleId: "prefer-define-helpers",
|
||||
message: `Wrap this ${tag.tagType} entry in \`${helper}()\` so unhead can narrow its type.`,
|
||||
fix: imported ? fix : void 0,
|
||||
suggestions: imported ? void 0 : [{ message: `Wrap in \`${helper}()\` (you may need to import it).`, fix: { type: "wrap-tag", wrapWith: helper } }]
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const preloadMissingAs = (tag) => {
|
||||
if (tag.tagType !== "link")
|
||||
return [];
|
||||
if (tag.props.rel !== "preload")
|
||||
return [];
|
||||
if (tag.keys.has("as"))
|
||||
return [];
|
||||
const diag = {
|
||||
ruleId: "preload-missing-as",
|
||||
message: 'Preload link is missing the required "as" attribute.'
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
const preloadFontCrossorigin = (tag) => {
|
||||
if (tag.tagType !== "link")
|
||||
return [];
|
||||
if (tag.props.rel !== "preload")
|
||||
return [];
|
||||
if (tag.props.as !== "font")
|
||||
return [];
|
||||
if (tag.keys.has("crossorigin"))
|
||||
return [];
|
||||
const diag = {
|
||||
ruleId: "preload-font-crossorigin",
|
||||
message: 'Font preload requires "crossorigin" \u2014 without it the font will be fetched twice.',
|
||||
fix: { type: "insert-after-prop", afterKey: "as", insert: `, crossorigin: 'anonymous'` }
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const robotsConflict = (tag) => {
|
||||
if (tag.tagType !== "meta")
|
||||
return [];
|
||||
if (tag.props.name !== "robots")
|
||||
return [];
|
||||
const content = tag.props.content;
|
||||
if (typeof content !== "string")
|
||||
return [];
|
||||
const directives = content.toLowerCase().split(",").map((d) => d.trim());
|
||||
const out = [];
|
||||
if (directives.includes("index") && directives.includes("noindex")) {
|
||||
out.push({
|
||||
ruleId: "robots-conflict",
|
||||
message: 'Robots meta has conflicting "index" and "noindex" directives.'
|
||||
});
|
||||
}
|
||||
if (directives.includes("follow") && directives.includes("nofollow")) {
|
||||
out.push({
|
||||
ruleId: "robots-conflict",
|
||||
message: 'Robots meta has conflicting "follow" and "nofollow" directives.'
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const deferOnModuleScript = (tag) => {
|
||||
if (tag.tagType !== "script")
|
||||
return [];
|
||||
if (tag.props.type !== "module")
|
||||
return [];
|
||||
if (tag.props.defer !== true)
|
||||
return [];
|
||||
const diag = {
|
||||
ruleId: "defer-on-module-script",
|
||||
message: '"defer" is redundant on module scripts. Modules are deferred by default.',
|
||||
at: { kind: "prop", key: "defer" },
|
||||
fix: { type: "remove-prop", key: "defer" }
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
const scriptSrcWithContent = (tag) => {
|
||||
if (tag.tagType !== "script")
|
||||
return [];
|
||||
if (typeof tag.props.src !== "string")
|
||||
return [];
|
||||
const hasInner = tag.keys.has("innerHTML") && tag.props.innerHTML !== "" || tag.keys.has("textContent") && tag.props.textContent !== "";
|
||||
if (!hasInner)
|
||||
return [];
|
||||
const diag = {
|
||||
ruleId: "script-src-with-content",
|
||||
message: 'Script has both "src" and inline content. The browser will ignore the inline content.'
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const NUMERIC_RE = /^\d+$/;
|
||||
const twitterHandleMissingAt = (tag) => {
|
||||
if (tag.tagType !== "meta")
|
||||
return [];
|
||||
const name = tag.props.name;
|
||||
if (name !== "twitter:site" && name !== "twitter:creator")
|
||||
return [];
|
||||
const content = tag.props.content;
|
||||
if (typeof content !== "string")
|
||||
return [];
|
||||
if (content.startsWith("@") || NUMERIC_RE.test(content))
|
||||
return [];
|
||||
const fixedSource = JSON.stringify(`@${content}`);
|
||||
const diag = {
|
||||
ruleId: "twitter-handle-missing-at",
|
||||
message: `${name} should start with "@", received "${content}".`,
|
||||
at: { kind: "prop-value", key: "content" },
|
||||
fix: { type: "replace-prop-value", key: "content", newSource: fixedSource }
|
||||
};
|
||||
return [diag];
|
||||
};
|
||||
|
||||
const USER_SCALABLE_NO_RE = /user-scalable\s*=\s*(?:no|0|false)(?:[\s,;]|$)/i;
|
||||
const MAX_SCALE_RE = /maximum-scale\s*=\s*1(?:\.\d+)?(?:[\s,;]|$)/i;
|
||||
const viewportUserScalable = (tag) => {
|
||||
if (tag.tagType !== "meta")
|
||||
return [];
|
||||
if (tag.props.name !== "viewport")
|
||||
return [];
|
||||
const content = tag.props.content;
|
||||
if (typeof content !== "string")
|
||||
return [];
|
||||
const out = [];
|
||||
if (USER_SCALABLE_NO_RE.test(content)) {
|
||||
out.push({
|
||||
ruleId: "viewport-user-scalable",
|
||||
message: 'viewport "user-scalable=no" prevents zooming and harms accessibility.'
|
||||
});
|
||||
}
|
||||
if (MAX_SCALE_RE.test(content)) {
|
||||
out.push({
|
||||
ruleId: "viewport-user-scalable",
|
||||
message: 'viewport "maximum-scale=1" limits zooming and may harm accessibility.'
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const TAG_TYPES = /* @__PURE__ */ new Set(["meta", "link", "script", "noscript", "style"]);
|
||||
function valueKind(value) {
|
||||
if (value === null)
|
||||
return "null";
|
||||
if (Array.isArray(value))
|
||||
return "array";
|
||||
const kind = typeof value;
|
||||
if (kind === "boolean" || kind === "function" || kind === "number" || kind === "object" || kind === "string")
|
||||
return kind;
|
||||
return "unknown";
|
||||
}
|
||||
function inputShapeFromRuntime(context, input) {
|
||||
const keys = /* @__PURE__ */ new Set();
|
||||
const valueKinds = /* @__PURE__ */ new Map();
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
keys.add(key);
|
||||
valueKinds.set(key, valueKind(value));
|
||||
}
|
||||
return { context, keys, valueKinds };
|
||||
}
|
||||
function tagInputFromRuntime(tag) {
|
||||
if (!TAG_TYPES.has(tag.tag))
|
||||
return void 0;
|
||||
const props = {};
|
||||
const keys = /* @__PURE__ */ new Set();
|
||||
for (const [k, v] of Object.entries(tag.props)) {
|
||||
keys.add(k);
|
||||
if (v == null) {
|
||||
if (tag.tag === "meta" && k === "content")
|
||||
props[k] = "";
|
||||
continue;
|
||||
}
|
||||
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
||||
if (tag.tag === "meta" && k === "name" && typeof v === "string")
|
||||
props[k] = v.toLowerCase();
|
||||
else
|
||||
props[k] = v;
|
||||
} else {
|
||||
props[k] = String(v);
|
||||
}
|
||||
}
|
||||
if (tag.tag === "script" || tag.tag === "style" || tag.tag === "noscript") {
|
||||
if (tag.innerHTML != null && tag.innerHTML !== "") {
|
||||
keys.add("innerHTML");
|
||||
}
|
||||
if (tag.textContent != null && tag.textContent !== "") {
|
||||
keys.add("textContent");
|
||||
}
|
||||
}
|
||||
if (tag.tagPriority != null) {
|
||||
keys.add("tagPriority");
|
||||
if (typeof tag.tagPriority === "string" || typeof tag.tagPriority === "number")
|
||||
props.tagPriority = tag.tagPriority;
|
||||
}
|
||||
return {
|
||||
tagType: tag.tag,
|
||||
props,
|
||||
keys
|
||||
};
|
||||
}
|
||||
function titleInputFromRuntime(titleTag) {
|
||||
if (titleTag.tag !== "title")
|
||||
return void 0;
|
||||
const text = titleTag.textContent ?? titleTag.innerHTML ?? "";
|
||||
return {
|
||||
callee: "runtime",
|
||||
props: { title: text },
|
||||
keys: /* @__PURE__ */ new Set(["title"])
|
||||
};
|
||||
}
|
||||
|
||||
const tagPredicates = {
|
||||
"defer-on-module-script": deferOnModuleScript,
|
||||
"empty-meta-content": emptyMetaContent,
|
||||
"no-deprecated-props": noDeprecatedProps,
|
||||
"no-unknown-meta": noUnknownMeta,
|
||||
"non-absolute-canonical": nonAbsoluteCanonical,
|
||||
"numeric-tag-priority": numericTagPriority,
|
||||
"preload-font-crossorigin": preloadFontCrossorigin,
|
||||
"preload-missing-as": preloadMissingAs,
|
||||
"robots-conflict": robotsConflict,
|
||||
"script-src-with-content": scriptSrcWithContent,
|
||||
"twitter-handle-missing-at": twitterHandleMissingAt,
|
||||
"viewport-user-scalable": viewportUserScalable
|
||||
};
|
||||
const inputShapePredicates = {
|
||||
"invalid-input-shape": validateInputShape
|
||||
};
|
||||
const migrationTagPredicates = {
|
||||
"prefer-define-helpers": preferDefineHelpers
|
||||
};
|
||||
const headInputPredicates = {
|
||||
"no-html-in-title": noHtmlInTitle
|
||||
};
|
||||
|
||||
export { DEPRECATED_PROPS as D, INPUT_SHAPE_FIELDS as I, KNOWN_META_NAMES as K, TAG_PRIORITY_ALIASES as T, URL_META_KEYS as U, KNOWN_META_PROPERTIES as a, inputShapePredicates as b, noHtmlInTitle as c, deferOnModuleScript as d, emptyMetaContent as e, findClosestMatch as f, noUnknownMeta as g, headInputPredicates as h, inputShapeFromRuntime as i, nonAbsoluteCanonical as j, numericTagPriority as k, levenshtein as l, migrationTagPredicates as m, noDeprecatedProps as n, preloadFontCrossorigin as o, preferDefineHelpers as p, preloadMissingAs as q, robotsConflict as r, scriptSrcWithContent as s, tagInputFromRuntime as t, tagPredicates as u, titleInputFromRuntime as v, twitterHandleMissingAt as w, validateInputShape as x, viewportUserScalable as y };
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
const BACKSLASH_RE = /\\/g;
|
||||
const LT_RE = /</g;
|
||||
const DOUBLE_QUOTE_RE = /"/g;
|
||||
const TOKEN_RE = /%\w+(?:\.\w+)?/g;
|
||||
const SepSub = "%separator";
|
||||
function sub(p, token, isJson = false) {
|
||||
let val;
|
||||
if (token === "s" || token === "pageTitle") {
|
||||
val = p.pageTitle;
|
||||
} else if (token.includes(".")) {
|
||||
const dotIndex = token.indexOf(".");
|
||||
val = p[token.substring(0, dotIndex)]?.[token.substring(dotIndex + 1)];
|
||||
} else {
|
||||
val = p[token];
|
||||
}
|
||||
if (val !== void 0) {
|
||||
return isJson ? (val || "").replace(BACKSLASH_RE, "\\\\").replace(LT_RE, "\\u003C").replace(DOUBLE_QUOTE_RE, '\\"') : val || "";
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
function processTemplateParams(s, p, sep, isJson = false) {
|
||||
if (typeof s !== "string" || !s.includes("%"))
|
||||
return s;
|
||||
let decoded = s;
|
||||
try {
|
||||
decoded = decodeURI(s);
|
||||
} catch {
|
||||
}
|
||||
const tokens = decoded.match(TOKEN_RE);
|
||||
if (!tokens) {
|
||||
return s;
|
||||
}
|
||||
const hasSepSub = s.includes(SepSub);
|
||||
s = s.replace(TOKEN_RE, (token) => {
|
||||
if (token === SepSub || !tokens.includes(token)) {
|
||||
return token;
|
||||
}
|
||||
const re = sub(p, token.slice(1), isJson);
|
||||
return re !== void 0 ? re : token;
|
||||
}).trim();
|
||||
if (hasSepSub) {
|
||||
s = s.split(SepSub).map((part) => part.trim()).filter((part) => part !== "").join(sep ? ` ${sep} ` : " ");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export { processTemplateParams as p };
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
import { M as MetaTagsArrayable } from './unhead.CGPOfp5O.mjs';
|
||||
|
||||
const META_NAMESPACES = ["twitter", "fediverse"];
|
||||
const PROPERTY_NAMESPACES = ["og", "book", "article", "profile", "fb", "payment"];
|
||||
const MEDIA_KEYS = ["ogImage", "ogVideo", "ogAudio", "twitterImage"];
|
||||
const HTTP_EQUIV_KEYS = ["contentType", "defaultStyle", "xUaCompatible"];
|
||||
const META_ALIASES = {
|
||||
articleExpirationTime: "article:expiration_time",
|
||||
articleModifiedTime: "article:modified_time",
|
||||
articlePublishedTime: "article:published_time",
|
||||
bookReleaseDate: "book:release_date",
|
||||
fbAppId: "fb:app_id",
|
||||
ogAudioSecureUrl: "og:audio:secure_url",
|
||||
ogAudioUrl: "og:audio",
|
||||
ogImageSecureUrl: "og:image:secure_url",
|
||||
ogImageUrl: "og:image",
|
||||
ogSiteName: "og:site_name",
|
||||
ogVideoSecureUrl: "og:video:secure_url",
|
||||
ogVideoUrl: "og:video",
|
||||
paymentExpiresAt: "payment:expires_at",
|
||||
paymentSuccessUrl: "payment:success_url",
|
||||
profileFirstName: "profile:first_name",
|
||||
profileLastName: "profile:last_name",
|
||||
profileUsername: "profile:username",
|
||||
msapplicationConfig: "msapplication-Config",
|
||||
msapplicationTileColor: "msapplication-TileColor",
|
||||
msapplicationTileImage: "msapplication-TileImage"
|
||||
};
|
||||
const CAPS_RE = /([A-Z])/g;
|
||||
const OG_TWITTER_RE = /^(?:og|twitter)/;
|
||||
const MetaPackingSchema = {
|
||||
appleItunesApp: {
|
||||
unpack: {
|
||||
entrySeparator: ", ",
|
||||
resolve: ({ key, value }) => `${fixKeyCase(key)}=${value}`
|
||||
}
|
||||
},
|
||||
refresh: {
|
||||
metaKey: "http-equiv",
|
||||
unpack: {
|
||||
entrySeparator: ";",
|
||||
resolve: ({ key, value }) => key === "seconds" ? `${value}` : void 0
|
||||
}
|
||||
},
|
||||
robots: {
|
||||
unpack: {
|
||||
entrySeparator: ", ",
|
||||
resolve: ({ key, value }) => typeof value === "boolean" ? fixKeyCase(key) : `${fixKeyCase(key)}:${value}`
|
||||
}
|
||||
},
|
||||
contentSecurityPolicy: {
|
||||
metaKey: "http-equiv",
|
||||
unpack: {
|
||||
entrySeparator: "; ",
|
||||
resolve: ({ key, value }) => `${fixKeyCase(key)} ${value}`
|
||||
}
|
||||
},
|
||||
charset: {}
|
||||
};
|
||||
function fixKeyCase(key) {
|
||||
const updated = key.replace(CAPS_RE, "-$1").toLowerCase();
|
||||
const prefixIndex = updated.indexOf("-");
|
||||
return prefixIndex === -1 ? updated : META_NAMESPACES.includes(updated.slice(0, prefixIndex)) || PROPERTY_NAMESPACES.includes(updated.slice(0, prefixIndex)) ? key.replace(CAPS_RE, ":$1").toLowerCase() : updated;
|
||||
}
|
||||
function sanitizeObject(input) {
|
||||
return Object.fromEntries(Object.entries(input).filter(([k, v]) => String(v) !== "false" && k));
|
||||
}
|
||||
function transformObject(obj) {
|
||||
return Array.isArray(obj) ? obj.map(transformObject) : !obj || typeof obj !== "object" ? obj : Object.fromEntries(Object.entries(obj).map(([k, v]) => [fixKeyCase(k), transformObject(v)]));
|
||||
}
|
||||
function unpackToString(value, options = {}) {
|
||||
const { entrySeparator = "", keyValueSeparator = "", wrapValue, resolve } = options;
|
||||
return Object.entries(value).map(([key, val]) => {
|
||||
if (resolve) {
|
||||
const resolved = resolve({ key, value: val });
|
||||
if (resolved !== void 0)
|
||||
return resolved;
|
||||
}
|
||||
const processedVal = typeof val === "object" ? unpackToString(val, options) : typeof val === "number" ? val.toString() : typeof val === "string" && wrapValue ? `${wrapValue}${val.replace(new RegExp(wrapValue, "g"), `\\${wrapValue}`)}${wrapValue}` : val;
|
||||
return `${key}${keyValueSeparator}${processedVal}`;
|
||||
}).join(entrySeparator);
|
||||
}
|
||||
function handleObjectEntry(key, value) {
|
||||
const sanitizedValue = sanitizeObject(value);
|
||||
const fixedKey = fixKeyCase(key);
|
||||
const attr = resolveMetaKeyType(fixedKey);
|
||||
if (!MetaTagsArrayable.has(fixedKey)) {
|
||||
return [{ [attr]: fixedKey, ...sanitizedValue }];
|
||||
}
|
||||
const input = Object.fromEntries(
|
||||
Object.entries(sanitizedValue).map(([k, v]) => [`${key}${k === "url" ? "" : `${k[0].toUpperCase()}${k.slice(1)}`}`, v])
|
||||
);
|
||||
return unpackMeta(input || {}).sort((a, b) => (a[attr]?.length || 0) - (b[attr]?.length || 0));
|
||||
}
|
||||
function resolveMetaKeyType(key) {
|
||||
if (MetaPackingSchema[key]?.metaKey === "http-equiv" || HTTP_EQUIV_KEYS.includes(key)) {
|
||||
return "http-equiv";
|
||||
}
|
||||
const fixed = fixKeyCase(key);
|
||||
const colonIndex = fixed.indexOf(":");
|
||||
return colonIndex === -1 ? "name" : PROPERTY_NAMESPACES.includes(fixed.slice(0, colonIndex)) ? "property" : "name";
|
||||
}
|
||||
function resolveMetaKeyValue(key) {
|
||||
return META_ALIASES[key] || fixKeyCase(key);
|
||||
}
|
||||
function resolvePackedMetaObjectValue(value, key) {
|
||||
if (key === "refresh")
|
||||
return `${value.seconds};url=${value.url}`;
|
||||
return unpackToString(transformObject(value), {
|
||||
keyValueSeparator: "=",
|
||||
entrySeparator: ", ",
|
||||
resolve: ({ value: value2, key: key2 }) => value2 === null ? "" : typeof value2 === "boolean" ? key2 : void 0,
|
||||
...MetaPackingSchema[key]?.unpack
|
||||
});
|
||||
}
|
||||
function unpackMeta(input) {
|
||||
const extras = [];
|
||||
const primitives = {};
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (Array.isArray(value)) {
|
||||
if (key === "themeColor") {
|
||||
value.forEach((v) => {
|
||||
if (typeof v === "object" && v !== null) {
|
||||
extras.push({ name: "theme-color", ...v });
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const v of value) {
|
||||
if (typeof v === "object" && v !== null) {
|
||||
const urlProps = [];
|
||||
const otherProps = [];
|
||||
for (const [propKey, propValue] of Object.entries(v)) {
|
||||
const metaKey = `${key}${propKey === "url" ? "" : `:${propKey}`}`;
|
||||
const meta2 = unpackMeta({ [metaKey]: propValue });
|
||||
(propKey === "url" ? urlProps : otherProps).push(...meta2);
|
||||
}
|
||||
extras.push(...urlProps, ...otherProps);
|
||||
} else {
|
||||
extras.push(...typeof v === "string" ? unpackMeta({ [key]: v }) : handleObjectEntry(key, v));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "object" && value) {
|
||||
if (MEDIA_KEYS.includes(key)) {
|
||||
const prefix = key.startsWith("twitter") ? "twitter" : "og";
|
||||
const type = key.replace(OG_TWITTER_RE, "").toLowerCase();
|
||||
const metaKey = prefix === "twitter" ? "name" : "property";
|
||||
if (value.url) {
|
||||
extras.push({
|
||||
[metaKey]: `${prefix}:${type}`,
|
||||
content: value.url
|
||||
});
|
||||
}
|
||||
if (value.secureUrl) {
|
||||
extras.push({
|
||||
[metaKey]: `${prefix}:${type}:secure_url`,
|
||||
content: value.secureUrl
|
||||
});
|
||||
}
|
||||
for (const [propKey, propValue] of Object.entries(value)) {
|
||||
if (propKey !== "url" && propKey !== "secureUrl") {
|
||||
extras.push({
|
||||
[metaKey]: `${prefix}:${type}:${propKey}`,
|
||||
content: propValue
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (MetaTagsArrayable.has(fixKeyCase(key))) {
|
||||
extras.push(...handleObjectEntry(key, value));
|
||||
} else {
|
||||
primitives[key] = sanitizeObject(value);
|
||||
}
|
||||
} else {
|
||||
primitives[key] = value;
|
||||
}
|
||||
}
|
||||
const meta = Object.entries(primitives).map(([key, value]) => {
|
||||
if (key === "charset")
|
||||
return { charset: value === null ? "_null" : value };
|
||||
const metaKey = resolveMetaKeyType(key);
|
||||
const keyValue = resolveMetaKeyValue(key);
|
||||
const processedValue = value === null ? "_null" : typeof value === "object" ? resolvePackedMetaObjectValue(value, key) : typeof value === "number" ? value.toString() : value;
|
||||
return metaKey === "http-equiv" ? { "http-equiv": keyValue, "content": processedValue } : { [metaKey]: keyValue, content: processedValue };
|
||||
});
|
||||
return [...extras, ...meta].map(
|
||||
(m) => !("content" in m) ? m : m.content === "_null" ? { ...m, content: null } : m
|
||||
);
|
||||
}
|
||||
|
||||
export { resolveMetaKeyValue as a, resolvePackedMetaObjectValue as b, resolveMetaKeyType as r, unpackMeta as u };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
function hasOwn(object, key) {
|
||||
return hasOwnProperty.call(object, key);
|
||||
}
|
||||
|
||||
export { hasOwn as h };
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import { H as HasElementTags } from './unhead.CGPOfp5O.mjs';
|
||||
import { r as resolveTags, i as isMetaArrayDupeKey, b as normalizeProps, d as dedupeKey, h as hashTag } from './unhead.-hZVKou0.mjs';
|
||||
import { a as callHook } from './unhead.Bm4Y6XQI.mjs';
|
||||
|
||||
const WHITESPACE_RE = /\s+/;
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function createDomRenderer(options = {}) {
|
||||
return (head) => _renderDOMHead(head, options);
|
||||
}
|
||||
function renderDOMHead(head, options = {}) {
|
||||
return _renderDOMHead(head, options);
|
||||
}
|
||||
function hasPendingEntries(head) {
|
||||
for (const entry of head.entries.values()) {
|
||||
if (entry._pending !== void 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function createDomState(head, dom) {
|
||||
const state = { _d: dom, _t: dom.title, _e: /* @__PURE__ */ new Map([["htmlAttrs", dom.documentElement], ["bodyAttrs", dom.body]]), _p: {}, _s: {}, _l: /* @__PURE__ */ new Map() };
|
||||
for (const entry of head.entries.values()) {
|
||||
if (entry._o !== void 0) {
|
||||
const orig = entry._o;
|
||||
for (const t of ["bodyAttrs", "htmlAttrs"]) {
|
||||
const cls = orig[t]?.class;
|
||||
if (typeof cls === "string") {
|
||||
const $el = state._e.get(t);
|
||||
for (const c of cls.split(WHITESPACE_RE)) {
|
||||
if (c)
|
||||
state._p[`${t}:attr:class:${c}`] = () => $el.classList.remove(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
function _renderDOMHead(head, options = {}) {
|
||||
const dom = options.document || head.resolvedOptions.document;
|
||||
const activeState = head._dom;
|
||||
if (!dom || head._du || activeState && activeState._d === dom && !head.dirty && !hasPendingEntries(head))
|
||||
return false;
|
||||
const defaultView = dom.defaultView;
|
||||
head._du = true;
|
||||
let didRender = false;
|
||||
try {
|
||||
let track = function(key, fn, fresh) {
|
||||
renderState._s[key] = !fresh && previous[key] || fn;
|
||||
delete previous[key];
|
||||
}, trackEvent = function(id, k, ev, source, $el, target) {
|
||||
const key = `${id}:event:${k}`;
|
||||
const prev = renderState._l.get(key);
|
||||
if (prev && prev[0] === target && prev[1] === ev && prev[2] === source) {
|
||||
track(key, prev[4]);
|
||||
return;
|
||||
}
|
||||
prev?.[4]();
|
||||
const dk = `data-${k}`;
|
||||
const handler = ((e) => source.call($el, e));
|
||||
const cleanup = () => {
|
||||
target.removeEventListener(ev, handler);
|
||||
if ($el.getAttribute(dk) === "")
|
||||
$el.removeAttribute(dk);
|
||||
if (renderState._l.get(key)?.[3] === handler)
|
||||
renderState._l.delete(key);
|
||||
};
|
||||
target.addEventListener(ev, handler);
|
||||
renderState._l.set(key, [target, ev, source, handler, cleanup]);
|
||||
$el.setAttribute(dk, "");
|
||||
track(key, cleanup, true);
|
||||
}, trackCtx = function({ id, $el, tag }) {
|
||||
renderState._e.set(id, $el);
|
||||
if (!tag.tag.endsWith("Attrs")) {
|
||||
const text = tag.textContent;
|
||||
if (text != null && text !== "") {
|
||||
if (text !== $el.textContent)
|
||||
$el.textContent = text;
|
||||
track(`${id}:text`, () => {
|
||||
if ($el.textContent === text)
|
||||
$el.textContent = "";
|
||||
}, true);
|
||||
}
|
||||
const html = tag.innerHTML;
|
||||
if (html != null && html !== "") {
|
||||
if (html !== $el.innerHTML)
|
||||
$el.innerHTML = html;
|
||||
track(`${id}:html`, () => {
|
||||
if ($el.innerHTML === html)
|
||||
$el.innerHTML = "";
|
||||
}, true);
|
||||
}
|
||||
const elKey = `${id}:el`;
|
||||
track(elKey, previous[elKey] || (() => {
|
||||
$el?.remove();
|
||||
renderState._e.delete(id);
|
||||
}));
|
||||
}
|
||||
for (const k in tag.props) {
|
||||
const v = tag.props[k];
|
||||
if (k[0] === "o" && k[1] === "n" && typeof v === "function") {
|
||||
const ev = k.slice(2);
|
||||
if ($el?.dataset?.[`${k}fired`])
|
||||
v.call($el, new (defaultView?.Event || Event)(ev));
|
||||
trackEvent(id, k, ev, v, $el, tag.tag === "bodyAttrs" && defaultView ? defaultView : $el);
|
||||
continue;
|
||||
}
|
||||
const ck = `${id}:attr:${k}`;
|
||||
if (k === "class" && v) {
|
||||
for (const c of v) {
|
||||
const key = `${ck}:${c}`;
|
||||
track(key, previous[key] || (() => $el.classList.remove(c)));
|
||||
if (!$el.classList.contains(c))
|
||||
$el.classList.add(c);
|
||||
}
|
||||
} else if (k === "style" && v) {
|
||||
for (const [sk, sv] of v) {
|
||||
const key = `${ck}:${sk}`;
|
||||
track(key, previous[key] || (() => $el.style.removeProperty(sk)));
|
||||
$el.style.setProperty(sk, sv);
|
||||
}
|
||||
} else if (v !== false && v !== null) {
|
||||
if ($el.getAttribute(k) !== v)
|
||||
$el.setAttribute(k, v === true ? "" : String(v));
|
||||
track(ck, previous[ck] || (() => $el.removeAttribute(k)));
|
||||
}
|
||||
}
|
||||
};
|
||||
const beforeRenderCtx = { shouldRender: true, tags: [] };
|
||||
callHook(head, "dom:beforeRender", beforeRenderCtx);
|
||||
if (!beforeRenderCtx.shouldRender)
|
||||
return false;
|
||||
let state = activeState;
|
||||
if (state?._d !== dom) {
|
||||
if (state) {
|
||||
for (const k in state._s) state._s[k]();
|
||||
for (const k in state._p) state._p[k]();
|
||||
state._s = state._p = {};
|
||||
state._e.clear();
|
||||
state._l.clear();
|
||||
}
|
||||
state = void 0;
|
||||
}
|
||||
if (!state) {
|
||||
state = createDomState(head, dom);
|
||||
} else {
|
||||
state._p = state._s;
|
||||
}
|
||||
state._s = {};
|
||||
const renderState = state;
|
||||
const previous = renderState._p;
|
||||
const pending = [];
|
||||
const frag = {};
|
||||
head.dirty = false;
|
||||
const rawTags = resolveTags(head, options.tagWeight ? { tagWeight: options.tagWeight } : void 0);
|
||||
const tags = [];
|
||||
const dupeKeyCounter = {};
|
||||
for (const tag of rawTags) {
|
||||
const count = dupeKeyCounter[tag._d] || 0;
|
||||
const id = (count ? `${tag._d}:${count}` : tag._d) || tag._h;
|
||||
const ctx = { tag, id, shouldRender: true };
|
||||
if (tag.tag === "meta" && tag._d && isMetaArrayDupeKey(tag._d))
|
||||
dupeKeyCounter[tag._d] = count + 1;
|
||||
tags.push(ctx);
|
||||
if (tag.tag === "title") {
|
||||
dom.title = tag.textContent;
|
||||
track("title:", () => dom.title = renderState._t);
|
||||
continue;
|
||||
}
|
||||
ctx.$el = renderState._e.get(id);
|
||||
if (ctx.$el)
|
||||
trackCtx(ctx);
|
||||
else if (HasElementTags.has(tag.tag))
|
||||
pending.push(ctx);
|
||||
}
|
||||
if (pending.length) {
|
||||
const tracked = new Set(renderState._e.values());
|
||||
for (const el of [...dom.body.children, ...dom.head.children]) {
|
||||
const elTag = el.tagName.toLowerCase();
|
||||
if (!HasElementTags.has(elTag) || tracked.has(el))
|
||||
continue;
|
||||
const props = { innerHTML: el.innerHTML };
|
||||
for (const n of el.getAttributeNames())
|
||||
props[n] = el.getAttribute(n);
|
||||
const next = normalizeProps({ tag: elTag, props: {} }, props);
|
||||
next.key = el.getAttribute("data-hid") || void 0;
|
||||
const dedupe = dedupeKey(next) || hashTag(next);
|
||||
let k = dedupe;
|
||||
let c = 1;
|
||||
while (renderState._e.has(k))
|
||||
k = `${dedupe}:${c++}`;
|
||||
renderState._e.set(k, el);
|
||||
}
|
||||
}
|
||||
for (const ctx of pending) {
|
||||
const found = renderState._e.get(ctx.id);
|
||||
ctx.$el = found || dom.createElement(ctx.tag.tag);
|
||||
trackCtx(ctx);
|
||||
if (!found)
|
||||
(frag[ctx.tag.tagPosition || "head"] ??= dom.createDocumentFragment()).appendChild(ctx.$el);
|
||||
}
|
||||
if (frag.head)
|
||||
dom.head.appendChild(frag.head);
|
||||
if (frag.bodyOpen)
|
||||
dom.body.insertBefore(frag.bodyOpen, dom.body.firstChild);
|
||||
if (frag.bodyClose)
|
||||
dom.body.appendChild(frag.bodyClose);
|
||||
for (const k in previous)
|
||||
previous[k]();
|
||||
head._dom = renderState;
|
||||
didRender = true;
|
||||
callHook(head, "dom:rendered", { renders: tags });
|
||||
} catch (e) {
|
||||
head.dirty = true;
|
||||
throw e;
|
||||
} finally {
|
||||
head._du = false;
|
||||
}
|
||||
if (didRender && (head.dirty || hasPendingEntries(head)))
|
||||
_renderDOMHead(head, options);
|
||||
return didRender;
|
||||
}
|
||||
|
||||
export { createDomRenderer as c, renderDOMHead as r };
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { HookableCore } from 'hookable';
|
||||
|
||||
function createHooks(hooks) {
|
||||
const instance = new HookableCore();
|
||||
for (const key in hooks || {}) {
|
||||
instance.hook(key, hooks[key]);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
function callHook(head, hook, ctx) {
|
||||
const hooks = head.hooks?._hooks?.[hook];
|
||||
if (!hooks?.length)
|
||||
return;
|
||||
return head.hooks?.callHook(hook, ctx);
|
||||
}
|
||||
|
||||
export { callHook as a, createHooks as c };
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { r as registerPlugin } from './unhead.D7HkBzZn.mjs';
|
||||
|
||||
function createClientHeadAdapter(core, hooks, render) {
|
||||
const corePush = core.push;
|
||||
const head = core;
|
||||
head.ssr = false;
|
||||
head.hooks = hooks;
|
||||
head.dirty = !!head.dirty;
|
||||
head.use = (p) => registerPlugin(head, p);
|
||||
head.render = () => render(head);
|
||||
function notify() {
|
||||
hooks.callHook("entries:updated", head);
|
||||
if (!head._b)
|
||||
head.render();
|
||||
}
|
||||
head.invalidate = () => {
|
||||
for (const entry of head.entries.values())
|
||||
delete entry._tags;
|
||||
head.dirty = true;
|
||||
notify();
|
||||
};
|
||||
head.push = (input, entryOptions) => {
|
||||
const unhook = entryOptions?.onRendered ? hooks.hook("dom:rendered", entryOptions.onRendered) : void 0;
|
||||
const active = corePush(input, entryOptions);
|
||||
const entry = core.entries.get(active._i);
|
||||
if (entry)
|
||||
entry._o = input;
|
||||
head.dirty = true;
|
||||
notify();
|
||||
return {
|
||||
_i: active._i,
|
||||
patch(input2) {
|
||||
active.patch(input2);
|
||||
head.dirty = true;
|
||||
notify();
|
||||
},
|
||||
dispose() {
|
||||
unhook?.();
|
||||
if (core.entries.has(active._i)) {
|
||||
active.dispose();
|
||||
head.invalidate();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
return head;
|
||||
}
|
||||
|
||||
export { createClientHeadAdapter as c };
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
const SelfClosingTags = /* @__PURE__ */ new Set(["meta", "link", "base"]);
|
||||
const DupeableTags = /* @__PURE__ */ new Set(["link", "style", "script", "noscript"]);
|
||||
const TagsWithInnerContent = /* @__PURE__ */ new Set(["title", "titleTemplate", "script", "style", "noscript"]);
|
||||
const HasElementTags = /* @__PURE__ */ new Set(["base", "meta", "link", "style", "script", "noscript"]);
|
||||
const ValidHeadTags = /* @__PURE__ */ new Set(["title", "base", "htmlAttrs", "bodyAttrs", "meta", "link", "style", "script", "noscript"]);
|
||||
const UniqueTags = /* @__PURE__ */ new Set(["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs", "templateParams"]);
|
||||
const TagConfigKeys = /* @__PURE__ */ new Set(["key", "tagPosition", "tagPriority", "tagDuplicateStrategy", "innerHTML", "textContent", "processTemplateParams"]);
|
||||
const ScriptNetworkEvents = /* @__PURE__ */ new Set(["onload", "onerror"]);
|
||||
const UsesMergeStrategy = /* @__PURE__ */ new Set(["templateParams", "htmlAttrs", "bodyAttrs"]);
|
||||
const MetaTagsArrayable = /* @__PURE__ */ new Set([
|
||||
"theme-color",
|
||||
"google-site-verification",
|
||||
"author",
|
||||
"og:locale:alternate",
|
||||
"og:image",
|
||||
"og:video",
|
||||
"og:audio",
|
||||
"article:author",
|
||||
"article:tag",
|
||||
"book:author",
|
||||
"book:tag",
|
||||
"twitter:image"
|
||||
]);
|
||||
const TagPriorityAliases = { critical: -8, high: -1, low: 2 };
|
||||
const hasContent = (value) => typeof value === "number" ? Number.isFinite(value) : value;
|
||||
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function isUnsafeKey(key) {
|
||||
return key === "__proto__" || key === "constructor" || key === "prototype";
|
||||
}
|
||||
|
||||
export { DupeableTags as D, HasElementTags as H, MetaTagsArrayable as M, ScriptNetworkEvents as S, TagConfigKeys as T, UniqueTags as U, ValidHeadTags as V, SelfClosingTags as a, TagPriorityAliases as b, TagsWithInnerContent as c, UsesMergeStrategy as d, hasContent as h, isUnsafeKey as i };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
function defineHeadPlugin(plugin, key) {
|
||||
if (key && typeof plugin === "function")
|
||||
plugin.key = key;
|
||||
return plugin;
|
||||
}
|
||||
|
||||
export { defineHeadPlugin as d };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
const VALID_STREAM_KEY_RE = /^[$_a-z][$\w]*$/i;
|
||||
function parseStreamKey(streamKey) {
|
||||
if (typeof streamKey !== "string" || !VALID_STREAM_KEY_RE.test(streamKey)) {
|
||||
throw new Error(
|
||||
`[unhead] Invalid streamKey: must be a valid JavaScript identifier matching ${VALID_STREAM_KEY_RE}. Received: ${JSON.stringify(streamKey)}`
|
||||
);
|
||||
}
|
||||
return streamKey;
|
||||
}
|
||||
|
||||
export { parseStreamKey as p };
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
function registerPlugin(head, p) {
|
||||
if (typeof p === "function" && p.key && head.plugins.has(p.key))
|
||||
return;
|
||||
const plugin = typeof p === "function" ? p(head) : p;
|
||||
const key = plugin.key || String(head.plugins.size + 1);
|
||||
if (!head.plugins.get(key)) {
|
||||
head.plugins.set(key, plugin);
|
||||
for (const k in plugin.hooks || {})
|
||||
head.hooks?.hook(k, plugin.hooks[k]);
|
||||
}
|
||||
}
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function createUnhead(renderer, resolvedOptions = {}) {
|
||||
const ssr = !resolvedOptions.document;
|
||||
const entries = /* @__PURE__ */ new Map();
|
||||
const plugins = /* @__PURE__ */ new Map();
|
||||
const head = {
|
||||
_entryCount: 1,
|
||||
_h: 0,
|
||||
plugins,
|
||||
resolvedOptions,
|
||||
ssr,
|
||||
entries,
|
||||
// adapters decorate this same object; reserve the slot to keep its shape stable
|
||||
hooks: void 0,
|
||||
render: () => renderer(head),
|
||||
use: (p) => registerPlugin(head, p),
|
||||
push(input, _options) {
|
||||
const _i = _options?._index ?? head._entryCount++;
|
||||
const options = _options ? { ..._options } : {};
|
||||
delete options.head;
|
||||
delete options.onRendered;
|
||||
delete options._index;
|
||||
const entry = { _i, input, options };
|
||||
entries.set(_i, entry);
|
||||
const active = {
|
||||
_i,
|
||||
dispose() {
|
||||
entries.delete(_i);
|
||||
},
|
||||
patch(input2) {
|
||||
if (ssr) {
|
||||
entry.input = input2;
|
||||
delete entry._tags;
|
||||
} else {
|
||||
entry._pending = input2;
|
||||
}
|
||||
if (!entries.has(_i))
|
||||
entries.set(_i, entry);
|
||||
}
|
||||
};
|
||||
return active;
|
||||
}
|
||||
};
|
||||
resolvedOptions.init?.forEach((e) => e && head.push(e));
|
||||
return head;
|
||||
}
|
||||
|
||||
export { createUnhead as c, registerPlugin as r };
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import { c as createUnhead } from './unhead.D7HkBzZn.mjs';
|
||||
import { I as INVALID_ATTR_NAME_RE, r as resolveTags, n as normalizeEntryToTags, d as dedupeKey, h as hashTag } from './unhead.-hZVKou0.mjs';
|
||||
import { a as callHook, c as createHooks } from './unhead.Bm4Y6XQI.mjs';
|
||||
import { b as TagPriorityAliases, a as SelfClosingTags, c as TagsWithInnerContent } from './unhead.CGPOfp5O.mjs';
|
||||
|
||||
const isTruthy = (v) => v === "" || v === true;
|
||||
function capoTagWeight(tag) {
|
||||
if (typeof tag.tagPriority === "number")
|
||||
return tag.tagPriority;
|
||||
let weight = 100;
|
||||
const offset = TagPriorityAliases[tag.tagPriority] || 0;
|
||||
if (tag.tag === "base") {
|
||||
weight = -10;
|
||||
} else if (tag.tag === "title") {
|
||||
weight = 10;
|
||||
} else if (tag.tag === "meta") {
|
||||
weight = tag.props["http-equiv"] === "content-security-policy" ? -30 : tag.props.charset ? -20 : tag.props.name === "viewport" ? -15 : weight;
|
||||
} else if (tag.tag === "link" && tag.props.rel) {
|
||||
const rel = tag.props.rel;
|
||||
weight = rel === "preconnect" ? 20 : rel === "stylesheet" ? 60 : rel === "preload" || rel === "modulepreload" ? 70 : rel === "prefetch" || rel === "dns-prefetch" || rel === "prerender" ? 90 : weight;
|
||||
} else if (tag.tag === "script") {
|
||||
const type = typeof tag.props.type === "string" ? tag.props.type : "";
|
||||
const json = type.endsWith("json");
|
||||
if (type === "importmap")
|
||||
weight = 25;
|
||||
else if (type === "speculationrules")
|
||||
weight = 90;
|
||||
else if (isTruthy(tag.props.async))
|
||||
weight = 30;
|
||||
else if (tag.props.src && !isTruthy(tag.props.defer) && type !== "module" && !json || (tag.innerHTML || tag.textContent) && !json)
|
||||
weight = 50;
|
||||
else if (isTruthy(tag.props.defer) && tag.props.src || type === "module")
|
||||
weight = 80;
|
||||
} else if (tag.tag === "style") {
|
||||
weight = tag.innerHTML && /@import/.test(tag.innerHTML) ? 40 : 60;
|
||||
}
|
||||
return (weight || 100) + offset;
|
||||
}
|
||||
|
||||
const DOUBLE_QUOTE_RE = /"/g;
|
||||
function encodeAttribute(value) {
|
||||
const s = typeof value === "string" ? value : String(value);
|
||||
return s.includes('"') ? s.replace(DOUBLE_QUOTE_RE, """) : s;
|
||||
}
|
||||
function propsToString(props) {
|
||||
let attrs = "";
|
||||
for (const key in props) {
|
||||
if (!Object.hasOwn(props, key) || !key || INVALID_ATTR_NAME_RE.test(key))
|
||||
continue;
|
||||
let value = props[key];
|
||||
if (typeof value !== "string") {
|
||||
if (key === "class") {
|
||||
let out = "";
|
||||
for (const c of value) out += out ? ` ${c}` : c;
|
||||
value = out;
|
||||
} else if (key === "style") {
|
||||
let out = "";
|
||||
for (const [k, v] of value) out += out ? `;${k}:${v}` : `${k}:${v}`;
|
||||
value = out;
|
||||
}
|
||||
}
|
||||
if (value !== false && value !== null) {
|
||||
attrs += value === true ? ` ${key}` : ` ${key}="${encodeAttribute(value)}"`;
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
const ESCAPE_HTML_RE = /[&<>"'/]/g;
|
||||
const CLOSE_TAG_RE = {};
|
||||
const ESCAPE_HTML_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" };
|
||||
function escapeHtml(str) {
|
||||
return str.replace(ESCAPE_HTML_RE, (c) => ESCAPE_HTML_MAP[c]);
|
||||
}
|
||||
function tagToString(tag) {
|
||||
const attrs = propsToString(tag.props);
|
||||
const openTag = `<${tag.tag}${attrs}>`;
|
||||
if (SelfClosingTags.has(tag.tag))
|
||||
return openTag;
|
||||
if (!TagsWithInnerContent.has(tag.tag))
|
||||
return `${openTag}</${tag.tag}>`;
|
||||
let content = String(tag.textContent ?? tag.innerHTML ?? "");
|
||||
content = tag.tag === "title" ? escapeHtml(content) : content.replace(CLOSE_TAG_RE[tag.tag] ||= new RegExp(`</${tag.tag}`, "gi"), `<\\/${tag.tag}`);
|
||||
return `${openTag}${content}</${tag.tag}>`;
|
||||
}
|
||||
|
||||
function ssrRenderTags(tags, options) {
|
||||
const schema = { htmlAttrs: {}, bodyAttrs: {}, tags: { head: "", bodyClose: "", bodyOpen: "" } };
|
||||
const lineBreaks = !options?.omitLineBreaks ? "\n" : "";
|
||||
for (const tag of tags) {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function createServerRenderer(options = {}) {
|
||||
return (head) => {
|
||||
const beforeRenderCtx = { shouldRender: true };
|
||||
callHook(head, "ssr:beforeRender", beforeRenderCtx);
|
||||
if (!beforeRenderCtx.shouldRender)
|
||||
return ssrRenderTags([]);
|
||||
const ctx = {
|
||||
tags: options.resolvedTags || resolveTags(head, { tagWeight: options.tagWeight ?? capoTagWeight }),
|
||||
options: { ...options }
|
||||
};
|
||||
callHook(head, "ssr:render", ctx);
|
||||
const html = ssrRenderTags(ctx.tags, ctx.options);
|
||||
const renderCtx = { tags: ctx.tags, html };
|
||||
callHook(head, "ssr:rendered", renderCtx);
|
||||
return renderCtx.html;
|
||||
};
|
||||
}
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function renderSSRHead(head, options) {
|
||||
return (/* @__PURE__ */ createServerRenderer(options))(head);
|
||||
}
|
||||
|
||||
const DEFAULT_INIT = {
|
||||
htmlAttrs: {
|
||||
lang: "en"
|
||||
},
|
||||
meta: [
|
||||
{
|
||||
charset: "utf-8"
|
||||
},
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1"
|
||||
}
|
||||
]
|
||||
};
|
||||
const serverPropResolver = /* @__PURE__ */ Object.assign(
|
||||
(k, v) => {
|
||||
if (k && k.startsWith("on") && typeof v === "function") {
|
||||
return `this.dataset.${k}fired = true`;
|
||||
}
|
||||
return v;
|
||||
},
|
||||
{ _static: true }
|
||||
);
|
||||
let defaultInitTags;
|
||||
function getDefaultInitTags() {
|
||||
if (!defaultInitTags) {
|
||||
defaultInitTags = normalizeEntryToTags(DEFAULT_INIT, []);
|
||||
for (let i = 0; i < defaultInitTags.length; i++) {
|
||||
const t = defaultInitTags[i];
|
||||
t._w = capoTagWeight(t);
|
||||
t._p = (1 << 10) + i;
|
||||
t._d = dedupeKey(t);
|
||||
if (!t._d)
|
||||
t._h = hashTag(t);
|
||||
}
|
||||
}
|
||||
return defaultInitTags;
|
||||
}
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function createHead(options = {}) {
|
||||
const tagWeight = options.tagWeight || capoTagWeight;
|
||||
const core = createUnhead(createServerRenderer({ tagWeight, omitLineBreaks: options.omitLineBreaks }), {
|
||||
_tagWeight: tagWeight,
|
||||
// @ts-expect-error untyped
|
||||
document: false,
|
||||
experimentalStreamKey: options.experimentalStreamKey,
|
||||
propResolvers: [
|
||||
...options.propResolvers || [],
|
||||
serverPropResolver
|
||||
],
|
||||
init: [
|
||||
options.disableDefaults ? void 0 : DEFAULT_INIT,
|
||||
...options.init || []
|
||||
]
|
||||
});
|
||||
if (!options.disableDefaults && !options.tagWeight && !options.propResolvers?.some((r) => !r._static)) {
|
||||
const defaultEntry = core.entries.get(1);
|
||||
if (defaultEntry)
|
||||
defaultEntry._precomputedTags = getDefaultInitTags();
|
||||
}
|
||||
core.hooks = createHooks(options.hooks);
|
||||
options.plugins?.forEach((p) => core.use(p));
|
||||
return core;
|
||||
}
|
||||
|
||||
export { capoTagWeight as a, createServerRenderer as b, createHead as c, escapeHtml as e, propsToString as p, renderSSRHead as r, ssrRenderTags as s, tagToString as t };
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
import { u as unpackMeta } from './unhead.BQWSw36o.mjs';
|
||||
import { d as defineHeadPlugin } from './unhead.CHEy9ana.mjs';
|
||||
import { h as hasContent, i as isUnsafeKey } from './unhead.CGPOfp5O.mjs';
|
||||
|
||||
const FlatMetaPlugin = /* @__PURE__ */ defineHeadPlugin({
|
||||
key: "flatMeta",
|
||||
hooks: {
|
||||
"entries:normalize": (ctx) => {
|
||||
let hasFlatMeta = false;
|
||||
const tags = [];
|
||||
const tagsToAdd = [];
|
||||
for (const t of ctx.tags) {
|
||||
if (t.tag !== "_flatMeta") {
|
||||
tags.push(t);
|
||||
continue;
|
||||
}
|
||||
hasFlatMeta = true;
|
||||
for (const props of unpackMeta(t.props))
|
||||
tagsToAdd.push({ ...t, tag: "meta", props });
|
||||
}
|
||||
if (!hasFlatMeta)
|
||||
return;
|
||||
for (const tag of tagsToAdd) tags.push(tag);
|
||||
ctx.tags = tags;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const WhitelistAttributes = {
|
||||
htmlAttrs: /* @__PURE__ */ new Set(["class", "style", "lang", "dir"]),
|
||||
bodyAttrs: /* @__PURE__ */ new Set(["class", "style"]),
|
||||
meta: /* @__PURE__ */ new Set(["name", "property", "charset", "content", "media"]),
|
||||
noscript: /* @__PURE__ */ new Set([]),
|
||||
style: /* @__PURE__ */ new Set(["media", "nonce", "title", "blocking"]),
|
||||
script: /* @__PURE__ */ new Set(["type", "textContent", "nonce", "blocking"]),
|
||||
link: /* @__PURE__ */ new Set(["color", "crossorigin", "fetchpriority", "href", "hreflang", "imagesrcset", "imagesizes", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"])
|
||||
};
|
||||
const BlockedLinkRels = /* @__PURE__ */ new Set(["canonical", "modulepreload", "prerender", "preload", "prefetch", "dns-prefetch", "preconnect", "manifest", "pingback"]);
|
||||
const SafeDataAttrName = /^[a-z][a-z0-9-]*[a-z0-9]$/i;
|
||||
const AsciiWhitespace = /[\t\n\f\r ]+/;
|
||||
const HtmlEntityHex = /&#x([0-9a-f]+);?/gi;
|
||||
const HtmlEntityDec = /&#(\d+);?/g;
|
||||
const HtmlEntityNamed = /&(tab|newline|colon|semi|lpar|rpar|sol|bsol|comma|period|excl|num|dollar|percnt|amp|apos|ast|plus|lt|gt|equals|quest|at|lsqb|rsqb|lcub|rcub|vert|hat|grave|tilde|nbsp);?/gi;
|
||||
const ControlChars = /[\x00-\x20]+/g;
|
||||
const NamedEntityMap = {
|
||||
tab: " ",
|
||||
newline: "\n",
|
||||
colon: ":",
|
||||
semi: ";",
|
||||
lpar: "(",
|
||||
rpar: ")",
|
||||
sol: "/",
|
||||
bsol: "\\",
|
||||
comma: ",",
|
||||
period: ".",
|
||||
excl: "!",
|
||||
num: "#",
|
||||
dollar: "$",
|
||||
percnt: "%",
|
||||
amp: "&",
|
||||
apos: "'",
|
||||
ast: "*",
|
||||
plus: "+",
|
||||
lt: "<",
|
||||
gt: ">",
|
||||
equals: "=",
|
||||
quest: "?",
|
||||
at: "@",
|
||||
lsqb: "[",
|
||||
rsqb: "]",
|
||||
lcub: "{",
|
||||
rcub: "}",
|
||||
vert: "|",
|
||||
hat: "^",
|
||||
grave: "`",
|
||||
tilde: "~",
|
||||
nbsp: "\xA0"
|
||||
};
|
||||
function safeFromCodePoint(codePoint) {
|
||||
if (codePoint > 1114111 || codePoint < 0 || Number.isNaN(codePoint))
|
||||
return "";
|
||||
return String.fromCodePoint(codePoint);
|
||||
}
|
||||
function decodeHtmlEntities(str) {
|
||||
return str.replace(HtmlEntityHex, (_, hex) => safeFromCodePoint(Number.parseInt(hex, 16))).replace(HtmlEntityDec, (_, dec) => safeFromCodePoint(Number(dec))).replace(HtmlEntityNamed, (_, name) => NamedEntityMap[name.toLowerCase()] || "");
|
||||
}
|
||||
function hasDangerousProtocol(url) {
|
||||
const entityDecoded = decodeHtmlEntities(url);
|
||||
const cleaned = entityDecoded.replace(ControlChars, "");
|
||||
let decoded;
|
||||
try {
|
||||
decoded = decodeURIComponent(cleaned);
|
||||
} catch {
|
||||
decoded = cleaned;
|
||||
}
|
||||
const sanitized = decoded.replace(ControlChars, "");
|
||||
const lower = sanitized.toLowerCase();
|
||||
return lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:");
|
||||
}
|
||||
function stripProtoKeys(obj) {
|
||||
if (Array.isArray(obj))
|
||||
return obj.map(stripProtoKeys);
|
||||
if (obj && typeof obj === "object") {
|
||||
const clean = {};
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (isUnsafeKey(key))
|
||||
continue;
|
||||
clean[key] = stripProtoKeys(obj[key]);
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function acceptDataAttrs(value, allowId = true) {
|
||||
const next = {};
|
||||
if (!value)
|
||||
return next;
|
||||
const keys = Object.keys(value);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const isData = key.startsWith("data-");
|
||||
if ((isData || allowId && key === "id") && SafeDataAttrName.test(key))
|
||||
next[key] = value[key];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
function hasBlockedRel(rel) {
|
||||
const tokens = rel.split(AsciiWhitespace);
|
||||
return !tokens.some(Boolean) || tokens.some((token) => BlockedLinkRels.has(token.toLowerCase()));
|
||||
}
|
||||
function makeTagSafe(tag) {
|
||||
let next = {};
|
||||
const { tag: type, props: prev } = tag;
|
||||
switch (type) {
|
||||
// title: textContent is escaped in rendering (tagToString), no props needed
|
||||
case "title":
|
||||
break;
|
||||
// virtual tags, not rendered to HTML — but sanitize to prevent injection if rendered
|
||||
case "titleTemplate":
|
||||
case "templateParams":
|
||||
next = prev;
|
||||
break;
|
||||
case "htmlAttrs":
|
||||
case "bodyAttrs":
|
||||
WhitelistAttributes[type].forEach((attr) => {
|
||||
if (prev[attr]) {
|
||||
next[attr] = prev[attr];
|
||||
}
|
||||
});
|
||||
delete tag.innerHTML;
|
||||
delete tag.textContent;
|
||||
tag.props = { ...acceptDataAttrs(prev, false), ...next };
|
||||
return !Object.keys(tag.props).length ? false : tag;
|
||||
case "style":
|
||||
next = acceptDataAttrs(prev);
|
||||
WhitelistAttributes.style.forEach((key) => {
|
||||
if (prev[key]) {
|
||||
next[key] = prev[key];
|
||||
}
|
||||
});
|
||||
break;
|
||||
// meta is safe, except for http-equiv
|
||||
case "meta":
|
||||
WhitelistAttributes.meta.forEach((key) => {
|
||||
if (hasContent(prev[key])) {
|
||||
next[key] = prev[key];
|
||||
}
|
||||
});
|
||||
break;
|
||||
// link tags we block preloading, prerendering, prefetching, dns-prefetch, preconnect, manifest, etc
|
||||
case "link":
|
||||
WhitelistAttributes.link.forEach((key) => {
|
||||
const val = prev[key];
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
if (key === "rel" && (typeof val !== "string" || hasBlockedRel(val))) {
|
||||
return;
|
||||
}
|
||||
if (key === "href" || key === "imagesrcset") {
|
||||
if (typeof val !== "string") {
|
||||
return;
|
||||
}
|
||||
const urls = key === "imagesrcset" ? val.split(",").map((s) => s.trim()) : [val];
|
||||
if (urls.some((u) => hasDangerousProtocol(u))) {
|
||||
return;
|
||||
}
|
||||
next[key] = val;
|
||||
} else if (val) {
|
||||
next[key] = val;
|
||||
}
|
||||
});
|
||||
if (!next.href && !next.imagesrcset || !next.rel) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "noscript":
|
||||
WhitelistAttributes.noscript.forEach((key) => {
|
||||
if (prev[key]) {
|
||||
next[key] = prev[key];
|
||||
}
|
||||
});
|
||||
break;
|
||||
// we only allow JSON in scripts
|
||||
case "script":
|
||||
if (!tag.textContent || typeof prev.type !== "string" || !prev.type.endsWith("json")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const jsonVal = typeof tag.textContent === "string" ? JSON.parse(tag.textContent) : tag.textContent;
|
||||
tag.textContent = JSON.stringify(stripProtoKeys(jsonVal), null, 0);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
WhitelistAttributes.script.forEach((s) => {
|
||||
if (s !== "textContent" && prev[s]) {
|
||||
next[s] = prev[s];
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
delete tag.innerHTML;
|
||||
if (type !== "title" && type !== "script") {
|
||||
delete tag.textContent;
|
||||
}
|
||||
tag.props = { ...acceptDataAttrs(prev), ...next };
|
||||
if (!Object.keys(tag.props).length && !tag.tag.endsWith("Attrs") && !tag.textContent) {
|
||||
return false;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
const SafeInputPlugin = /* @__PURE__ */ defineHeadPlugin({
|
||||
key: "safe",
|
||||
hooks: {
|
||||
"entries:normalize": (ctx) => {
|
||||
if (ctx.entry.options?._safe) {
|
||||
ctx.tags = ctx.tags.reduce((acc, tag) => {
|
||||
const safeTag = makeTagSafe(tag);
|
||||
if (safeTag)
|
||||
acc.push(safeTag);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
},
|
||||
"tags:afterResolve": (ctx) => {
|
||||
ctx.tags = ctx.tags.reduce((acc, tag) => {
|
||||
if (!tag._safe) {
|
||||
acc.push(tag);
|
||||
return acc;
|
||||
}
|
||||
if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
|
||||
acc.push(tag);
|
||||
return acc;
|
||||
}
|
||||
const safeTag = makeTagSafe(tag);
|
||||
if (safeTag)
|
||||
acc.push(safeTag);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { FlatMetaPlugin as F, SafeInputPlugin as S };
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { c as createUnhead } from './unhead.D7HkBzZn.mjs';
|
||||
import { b as TagPriorityAliases } from './unhead.CGPOfp5O.mjs';
|
||||
import { c as createHooks } from './unhead.Bm4Y6XQI.mjs';
|
||||
import { c as createClientHeadAdapter } from './unhead.C5Bksi2B.mjs';
|
||||
import { c as createDomRenderer } from './unhead.Bjbp1C8D.mjs';
|
||||
|
||||
const tagWeight = (tag) => typeof tag.tagPriority === "number" ? tag.tagPriority : 100 + (TagPriorityAliases[tag.tagPriority] || 0);
|
||||
function createHead(options = {}) {
|
||||
options.document = options.document || (typeof window !== "undefined" ? document : void 0);
|
||||
const renderer = options.render || createDomRenderer({ document: options.document });
|
||||
const core = createUnhead(renderer, { document: options.document, propResolvers: options.propResolvers, _tagWeight: tagWeight, init: [] });
|
||||
const hooks = createHooks(options.hooks);
|
||||
const head = createClientHeadAdapter(core, hooks, renderer);
|
||||
options.plugins?.forEach((p) => head.use(p));
|
||||
options.init?.forEach((e) => e && head.push(e));
|
||||
return head;
|
||||
}
|
||||
|
||||
export { createHead as c };
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
import { d as defineHeadPlugin } from './unhead.CHEy9ana.mjs';
|
||||
import { p as processTemplateParams } from './unhead.BGFxPGPQ.mjs';
|
||||
|
||||
const sortTags = (a, b) => a._w === b._w ? a._p - b._p : a._w - b._w;
|
||||
const formatKey = (k) => !k.includes(":key") ? k.split(":").join(":key:") : k;
|
||||
const AliasSortingPlugin = /* @__PURE__ */ defineHeadPlugin({
|
||||
key: "aliasSorting",
|
||||
hooks: {
|
||||
"tags:resolve": (ctx) => {
|
||||
let m = false;
|
||||
for (const t of ctx.tags) {
|
||||
const p = t.tagPriority;
|
||||
if (!p)
|
||||
continue;
|
||||
const s = String(p);
|
||||
if (s.startsWith("before:")) {
|
||||
const k = formatKey(s.slice(7));
|
||||
const l = ctx.tagMap.get(k);
|
||||
if (l) {
|
||||
if (typeof l.tagPriority === "number")
|
||||
t.tagPriority = l.tagPriority;
|
||||
t._p = l._p - 1;
|
||||
m = true;
|
||||
}
|
||||
} else if (s.startsWith("after:")) {
|
||||
const k = formatKey(s.slice(6));
|
||||
const l = ctx.tagMap.get(k);
|
||||
if (l) {
|
||||
if (typeof l.tagPriority === "number")
|
||||
t.tagPriority = l.tagPriority;
|
||||
t._p = l._p + 1;
|
||||
m = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m)
|
||||
ctx.tags = ctx.tags.sort(sortTags);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const DeprecationsPlugin = /* @__PURE__ */ defineHeadPlugin({
|
||||
key: "deprecations",
|
||||
hooks: {
|
||||
"entries:normalize": ({ tags }) => {
|
||||
for (const tag of tags) {
|
||||
if (tag.props.children) {
|
||||
tag.innerHTML = tag.props.children;
|
||||
delete tag.props.children;
|
||||
}
|
||||
if (tag.props.hid) {
|
||||
tag.key = tag.props.hid;
|
||||
delete tag.props.hid;
|
||||
}
|
||||
if (tag.props.vmid) {
|
||||
tag.key = tag.props.vmid;
|
||||
delete tag.props.vmid;
|
||||
}
|
||||
if ("body" in tag.props) {
|
||||
if (tag.props.body) {
|
||||
tag.tagPosition = "bodyClose";
|
||||
}
|
||||
delete tag.props.body;
|
||||
}
|
||||
if (tag.props.renderPriority != null) {
|
||||
tag.tagPriority = tag.props.renderPriority;
|
||||
delete tag.props.renderPriority;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function isThenable(v) {
|
||||
return typeof v?.then === "function";
|
||||
}
|
||||
const maxSyncPrefix = 256;
|
||||
function walkArrayPromises(v, index) {
|
||||
if (index === v.length)
|
||||
return;
|
||||
if (index === maxSyncPrefix) {
|
||||
const values2 = new Array(v.length);
|
||||
let hasThenable = false;
|
||||
for (; index < v.length; index++) {
|
||||
const value2 = walkPromises(v[index]);
|
||||
values2[index] = value2;
|
||||
hasThenable ||= isThenable(value2);
|
||||
}
|
||||
return hasThenable ? values2 : void 0;
|
||||
}
|
||||
const value = walkPromises(v[index]);
|
||||
if (isThenable(value)) {
|
||||
const values2 = new Array(v.length);
|
||||
values2[index] = value;
|
||||
for (let rest = index + 1; rest < v.length; rest++)
|
||||
values2[rest] = walkPromises(v[rest]);
|
||||
return values2;
|
||||
}
|
||||
const values = walkArrayPromises(v, index + 1);
|
||||
if (values)
|
||||
values[index] = value;
|
||||
return values;
|
||||
}
|
||||
function walkObjectPromises(v, keys, index) {
|
||||
if (index === keys.length)
|
||||
return;
|
||||
if (index === maxSyncPrefix) {
|
||||
const values2 = new Array(keys.length);
|
||||
let hasThenable = false;
|
||||
for (; index < keys.length; index++) {
|
||||
const value2 = walkPromises(v[keys[index]]);
|
||||
values2[index] = value2;
|
||||
hasThenable ||= isThenable(value2);
|
||||
}
|
||||
return hasThenable ? values2 : void 0;
|
||||
}
|
||||
const value = walkPromises(v[keys[index]]);
|
||||
if (isThenable(value)) {
|
||||
const values2 = new Array(keys.length);
|
||||
values2[index] = value;
|
||||
for (let rest = index + 1; rest < keys.length; rest++)
|
||||
values2[rest] = walkPromises(v[keys[rest]]);
|
||||
return values2;
|
||||
}
|
||||
const values = walkObjectPromises(v, keys, index + 1);
|
||||
if (values)
|
||||
values[index] = value;
|
||||
return values;
|
||||
}
|
||||
function walkPromises(v) {
|
||||
if (typeof v === "function")
|
||||
return v;
|
||||
if (isThenable(v))
|
||||
return Promise.resolve(v).then(walkPromises);
|
||||
if (Array.isArray(v)) {
|
||||
const values = walkArrayPromises(v, 0);
|
||||
return values ? Promise.all(values) : v;
|
||||
}
|
||||
if (v?.constructor === Object) {
|
||||
const keys = Object.keys(v);
|
||||
const values = walkObjectPromises(v, keys, 0);
|
||||
if (values) {
|
||||
return Promise.all(values).then((resolved) => Object.fromEntries(
|
||||
keys.map((key, index) => [key, resolved[index]])
|
||||
));
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
const PromisesPlugin = /* @__PURE__ */ defineHeadPlugin((head) => {
|
||||
const pending = /* @__PURE__ */ new WeakMap();
|
||||
return {
|
||||
key: "promises",
|
||||
hooks: {
|
||||
"entries:resolve": (ctx) => {
|
||||
for (let index = ctx.entries.length - 1; index >= 0; index--) {
|
||||
const entry = ctx.entries[index];
|
||||
const input = entry.input;
|
||||
if (pending.get(entry) === input) {
|
||||
ctx.entries.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
const result = walkPromises(input);
|
||||
if (!isThenable(result)) {
|
||||
pending.delete(entry);
|
||||
continue;
|
||||
}
|
||||
pending.set(entry, input);
|
||||
ctx.entries.splice(index, 1);
|
||||
void Promise.resolve(result).then(
|
||||
(resolved) => {
|
||||
if (pending.get(entry) !== input)
|
||||
return;
|
||||
pending.delete(entry);
|
||||
entry.input = resolved;
|
||||
delete entry._tags;
|
||||
head.invalidate?.();
|
||||
},
|
||||
() => {
|
||||
if (pending.get(entry) === input)
|
||||
pending.delete(entry);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, "promises");
|
||||
|
||||
const SupportedAttrs = {
|
||||
meta: "content",
|
||||
link: "href",
|
||||
htmlAttrs: "lang"
|
||||
};
|
||||
const contentAttrs = ["innerHTML", "textContent"];
|
||||
function processIfNeeded(value, params, separator, isJson = false) {
|
||||
return typeof value === "string" && value.includes("%") ? processTemplateParams(value, params, separator, isJson) : value;
|
||||
}
|
||||
const TemplateParamsPlugin = /* @__PURE__ */ defineHeadPlugin((head) => {
|
||||
return {
|
||||
key: "template-params",
|
||||
hooks: {
|
||||
"tags:resolve": ({ tagMap, tags }) => {
|
||||
const params = tagMap.get("templateParams")?.props || {};
|
||||
const sep = params.separator || "|";
|
||||
delete params.separator;
|
||||
params.pageTitle = processIfNeeded(
|
||||
// find templateParams
|
||||
params.pageTitle || head._title || "",
|
||||
params,
|
||||
sep
|
||||
);
|
||||
for (const tag of tags) {
|
||||
if (tag.processTemplateParams === false) {
|
||||
continue;
|
||||
}
|
||||
const v = SupportedAttrs[tag.tag];
|
||||
if (v && typeof tag.props[v] === "string") {
|
||||
tag.props[v] = processIfNeeded(tag.props[v], params, sep);
|
||||
} else if (tag.processTemplateParams || tag.tag === "titleTemplate" || tag.tag === "title") {
|
||||
for (const p of contentAttrs) {
|
||||
if (typeof tag[p] === "string")
|
||||
tag[p] = processIfNeeded(tag[p], params, sep, tag.tag === "script" && typeof tag.props.type === "string" && tag.props.type.endsWith("json"));
|
||||
}
|
||||
}
|
||||
}
|
||||
head._templateParams = params;
|
||||
head._separator = sep;
|
||||
},
|
||||
"tags:afterResolve": ({ tagMap }) => {
|
||||
const title = tagMap.get("title");
|
||||
if (title?.textContent && title.processTemplateParams !== false) {
|
||||
title.textContent = processIfNeeded(title.textContent, head._templateParams, head._separator);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, "template-params");
|
||||
|
||||
export { AliasSortingPlugin as A, DeprecationsPlugin as D, PromisesPlugin as P, TemplateParamsPlugin as T };
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { c as createClientHeadAdapter } from '../shared/unhead.C5Bksi2B.mjs';
|
||||
import { c as createHooks } from '../shared/unhead.Bm4Y6XQI.mjs';
|
||||
import '../shared/unhead.D7HkBzZn.mjs';
|
||||
import 'hookable';
|
||||
|
||||
const DEFAULT_STREAM_KEY = "__unhead__";
|
||||
function createStreamableHead(options = {}) {
|
||||
const { streamKey = DEFAULT_STREAM_KEY, ...rest } = options;
|
||||
const win = typeof window !== "undefined" ? window : void 0;
|
||||
const streamQueue = win?.[streamKey];
|
||||
const core = streamQueue?._head;
|
||||
if (!core)
|
||||
return void 0;
|
||||
if (core._wrapped) {
|
||||
const wrapped = core;
|
||||
for (const name in rest.hooks || {})
|
||||
wrapped.hooks?.hook(name, rest.hooks[name]);
|
||||
(rest.plugins || []).forEach((p) => wrapped.use(p));
|
||||
rest.init?.forEach((e) => e && wrapped.push(e));
|
||||
return wrapped;
|
||||
}
|
||||
const coreRender = core.render;
|
||||
const hooks = createHooks(rest.hooks);
|
||||
const head = createClientHeadAdapter(core, hooks, () => coreRender());
|
||||
head._wrapped = true;
|
||||
(rest.plugins || []).forEach((p) => head.use(p));
|
||||
rest.init?.forEach((e) => e && head.push(e));
|
||||
if (streamQueue)
|
||||
streamQueue._head = head;
|
||||
return head;
|
||||
}
|
||||
|
||||
export { DEFAULT_STREAM_KEY, createStreamableHead };
|
||||
+2
File diff suppressed because one or more lines are too long
+446
@@ -0,0 +1,446 @@
|
||||
import { parseHtmlForIndexes, applyHeadToHtml } from '../parser.mjs';
|
||||
export { prepareTemplate } from '../parser.mjs';
|
||||
import { c as createHead } from '../shared/unhead.Dssd7L5I.mjs';
|
||||
import { e as resolveHeadInput, h as hashTag, n as normalizeEntryToTags, b as normalizeProps, d as dedupeKey } from '../shared/unhead.-hZVKou0.mjs';
|
||||
import { a as callHook } from '../shared/unhead.Bm4Y6XQI.mjs';
|
||||
import { DEFAULT_STREAM_KEY } from './client.mjs';
|
||||
import { p as parseStreamKey } from '../shared/unhead.C_7zwq8E.mjs';
|
||||
import '../shared/unhead.Bf_fPVYA.mjs';
|
||||
import '../shared/unhead.D7HkBzZn.mjs';
|
||||
import '../shared/unhead.CGPOfp5O.mjs';
|
||||
import 'hookable';
|
||||
import '../shared/unhead.C5Bksi2B.mjs';
|
||||
|
||||
const LT_RE = /</g;
|
||||
const GT_RE = />/g;
|
||||
const AMP_RE = /&/g;
|
||||
const SSR_OUTLET_RE = /<!--\s*(?:app-html|ssr-outlet)\s*-->/;
|
||||
let encoder;
|
||||
let preparedStreamingLayouts;
|
||||
// @__NO_SIDE_EFFECTS__
|
||||
function createStreamableHead(options = {}) {
|
||||
const { streamKey, writesBodyTags, nonce, ...rest } = options;
|
||||
const parsedStreamKey = streamKey === void 0 ? void 0 : parseStreamKey(streamKey);
|
||||
const head = createHead({
|
||||
...rest,
|
||||
experimentalStreamKey: parsedStreamKey
|
||||
});
|
||||
if (writesBodyTags)
|
||||
streamState(head).writesBodyTags = true;
|
||||
if (nonce)
|
||||
streamState(head).nonce = nonce;
|
||||
let resolveShellReady;
|
||||
const shellReady = new Promise((resolve) => {
|
||||
resolveShellReady = resolve;
|
||||
});
|
||||
return {
|
||||
head,
|
||||
onShellReady: () => resolveShellReady(),
|
||||
shellReady
|
||||
};
|
||||
}
|
||||
function getStreamKey(head) {
|
||||
const key = head.resolvedOptions.experimentalStreamKey || DEFAULT_STREAM_KEY;
|
||||
return parseStreamKey(key);
|
||||
}
|
||||
function createBootstrapScript(streamKey = DEFAULT_STREAM_KEY, nonce) {
|
||||
const parsedStreamKey = parseStreamKey(streamKey);
|
||||
const nonceAttr = renderNonceAttribute(nonce);
|
||||
return `<script${nonceAttr}>window.${parsedStreamKey}||(window.${parsedStreamKey}={_q:[],push(e){this._q.push(e)}})<\/script>`;
|
||||
}
|
||||
function renderNonceAttribute(nonce) {
|
||||
return nonce ? ` nonce="${nonce.replace(AMP_RE, "&").replace(/"/g, """)}"` : "";
|
||||
}
|
||||
function renderShell(head) {
|
||||
const result = head.render();
|
||||
rememberShellBodyTags(head);
|
||||
head.entries.clear();
|
||||
streamState(head).shellRendered = true;
|
||||
return result;
|
||||
}
|
||||
function renderSSRHeadShell(head, template) {
|
||||
const parsed = typeof template === "string" ? parseHtmlForIndexes(template) : template;
|
||||
const result = applyShellToTemplate(head, head.render(), parsed);
|
||||
rememberShellBodyTags(head);
|
||||
head.entries.clear();
|
||||
streamState(head).shellRendered = true;
|
||||
return result;
|
||||
}
|
||||
function applyShellToTemplate(head, ssr, parsed) {
|
||||
return applyHeadToHtml(parsed, {
|
||||
htmlAttrs: ssr.htmlAttrs,
|
||||
headTags: createBootstrapScript(getStreamKey(head), head._stream?.nonce) + ssr.headTags,
|
||||
bodyAttrs: ssr.bodyAttrs,
|
||||
bodyTagsOpen: ssr.bodyTagsOpen,
|
||||
bodyTags: ssr.bodyTags
|
||||
});
|
||||
}
|
||||
function normalizePendingTags(head) {
|
||||
const propResolvers = head.resolvedOptions.propResolvers || [];
|
||||
const tags = [];
|
||||
const entries = /* @__PURE__ */ new Map();
|
||||
for (const entry of head.entries.values()) {
|
||||
const resolved = resolveHeadInput(unwrapEntryInput(entry.input), propResolvers);
|
||||
entries.set(entry._i, { input: entry.input, resolved });
|
||||
let index = 0;
|
||||
for (const tag of normalizeEntryToTags(resolved, [])) {
|
||||
if (entry.options)
|
||||
Object.assign(tag, entry.options);
|
||||
tag._p = (entry._i << 10) + index++;
|
||||
if (tag.tag === "templateParams")
|
||||
tag.props = { ...tag.props };
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
return { tags, entries };
|
||||
}
|
||||
const JSON_LD_TYPE_RE = /\bld\+json\b/i;
|
||||
function streamState(head) {
|
||||
return head._stream ||= {};
|
||||
}
|
||||
function isStreamedBodyTag(tagName, tag, entryPosition) {
|
||||
if (!tag || typeof tag !== "object")
|
||||
return false;
|
||||
if (tagName === "noscript")
|
||||
return true;
|
||||
const position = tag.tagPosition ?? entryPosition;
|
||||
if (position === "bodyClose" || position === "bodyOpen")
|
||||
return true;
|
||||
const type = tag.type ?? tag.props?.type;
|
||||
return tagName === "script" && typeof type === "string" && JSON_LD_TYPE_RE.test(type);
|
||||
}
|
||||
function streamedBodyTagIdentity(tagName, tag) {
|
||||
const normalized = normalizeProps({ tag: tagName, props: {} }, tag);
|
||||
const content = hashTag(normalized);
|
||||
return { slot: dedupeKey(normalized) || content, content };
|
||||
}
|
||||
function unwrapEntryInput(input) {
|
||||
return typeof input === "function" ? input() : input;
|
||||
}
|
||||
function rememberShellBodyTags(head) {
|
||||
const state = streamState(head);
|
||||
let seen = state.seen;
|
||||
for (const entry of head.entries.values()) {
|
||||
const entryPosition = entry.options?.tagPosition;
|
||||
for (const tag of entry._tags || entry._precomputedTags || []) {
|
||||
if (!isStreamedBodyTag(tag.tag, tag, entryPosition))
|
||||
continue;
|
||||
seen ||= state.seen = /* @__PURE__ */ new Set();
|
||||
if (tag.textContent || tag.innerHTML)
|
||||
seen.add(String(tag.textContent || tag.innerHTML));
|
||||
seen.add(tag._d || tag._h || hashTag(tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
function hasStreamedBodyTags(input, entryPosition) {
|
||||
if (input && typeof input === "object") {
|
||||
for (const key in input) {
|
||||
const value = input[key];
|
||||
if (Array.isArray(value)) {
|
||||
for (const tag of value) {
|
||||
if (isStreamedBodyTag(key, tag, entryPosition))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function splitStreamedBodyTags(input, seen, entryPosition) {
|
||||
let patch;
|
||||
let bodyTags;
|
||||
for (const key in input) {
|
||||
const value = input[key];
|
||||
if (!Array.isArray(value) || !value.some((tag) => isStreamedBodyTag(key, tag, entryPosition)))
|
||||
continue;
|
||||
const rest = [];
|
||||
const carried = [];
|
||||
for (const tag of value) {
|
||||
if (!isStreamedBodyTag(key, tag, entryPosition)) {
|
||||
rest.push(tag);
|
||||
continue;
|
||||
}
|
||||
const id = streamedBodyTagIdentity(key, tag);
|
||||
if (seen.has(id.content))
|
||||
continue;
|
||||
if (seen.has(id.slot)) {
|
||||
rest.push(tag);
|
||||
continue;
|
||||
}
|
||||
seen.add(id.content);
|
||||
carried.push({ ...tag, tagPosition: "bodyClose" });
|
||||
}
|
||||
patch ||= { ...input };
|
||||
if (rest.length)
|
||||
patch[key] = rest;
|
||||
else
|
||||
delete patch[key];
|
||||
if (carried.length)
|
||||
(bodyTags ||= {})[key] = carried;
|
||||
}
|
||||
const hasPatch = Object.keys(patch).some((k) => patch[k] !== void 0);
|
||||
return { patch: hasPatch ? patch : void 0, bodyTags };
|
||||
}
|
||||
function renderStreamBodyTags(head) {
|
||||
const bodyTagInputs = streamState(head).bodyTags;
|
||||
if (!bodyTagInputs?.length) {
|
||||
return "";
|
||||
}
|
||||
const restore = new Map(head.entries);
|
||||
head.entries.clear();
|
||||
try {
|
||||
for (const input of bodyTagInputs)
|
||||
head.push(input);
|
||||
const bodyTags = head.render().bodyTags;
|
||||
streamState(head).bodyTags = void 0;
|
||||
return bodyTags;
|
||||
} finally {
|
||||
head.entries.clear();
|
||||
for (const [k, v] of restore)
|
||||
head.entries.set(k, v);
|
||||
}
|
||||
}
|
||||
function renderStreamEnd(head, parts) {
|
||||
const bodyTags = renderStreamBodyTags(head);
|
||||
if (!bodyTags)
|
||||
return parts.end;
|
||||
const at = parts.bodyTagsAt ?? parts.end.length;
|
||||
return parts.end.slice(0, at) + bodyTags + parts.end.slice(at);
|
||||
}
|
||||
function renderSSRHeadSuspenseChunk(head) {
|
||||
if (!head.entries.size)
|
||||
return "";
|
||||
const streamKey = getStreamKey(head);
|
||||
const propResolvers = head.resolvedOptions.propResolvers || [];
|
||||
let normalizedEntries;
|
||||
let serialized;
|
||||
let patchCount = 0;
|
||||
try {
|
||||
if (head.hooks?._hooks?.["ssr:streamChunk"]?.length) {
|
||||
const normalized = normalizePendingTags(head);
|
||||
normalizedEntries = normalized.entries;
|
||||
callHook(head, "ssr:streamChunk", { tags: normalized.tags });
|
||||
}
|
||||
const state = streamState(head);
|
||||
let nextSeen;
|
||||
const inputs = [];
|
||||
let bodyTags;
|
||||
for (const entry of head.entries.values()) {
|
||||
const normalized = normalizedEntries?.get(entry._i);
|
||||
const input = normalized && normalized.input === entry.input ? normalized.resolved : resolveHeadInput(unwrapEntryInput(entry.input), propResolvers);
|
||||
const entryPosition = entry.options?.tagPosition;
|
||||
if (!state.writesBodyTags || !hasStreamedBodyTags(input, entryPosition)) {
|
||||
inputs.push(input);
|
||||
continue;
|
||||
}
|
||||
const split = splitStreamedBodyTags(input, nextSeen ||= new Set(state.seen), entryPosition);
|
||||
if (split.patch)
|
||||
inputs.push(split.patch);
|
||||
if (split.bodyTags)
|
||||
(bodyTags ||= []).push(split.bodyTags);
|
||||
}
|
||||
serialized = safeJsonStringify(inputs);
|
||||
patchCount = inputs.length;
|
||||
if (nextSeen)
|
||||
state.seen = nextSeen;
|
||||
if (bodyTags)
|
||||
(state.bodyTags ||= []).push(...bodyTags);
|
||||
} catch (error) {
|
||||
for (const [key, entry] of head.entries) {
|
||||
try {
|
||||
safeJsonStringify(resolveHeadInput(unwrapEntryInput(entry.input), propResolvers));
|
||||
} catch {
|
||||
head.entries.delete(key);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
head.entries.clear();
|
||||
if (!patchCount)
|
||||
return "";
|
||||
return `window.${streamKey}.push(${serialized})`;
|
||||
}
|
||||
function safeJsonStringify(obj) {
|
||||
return JSON.stringify(obj).replace(LT_RE, "\\u003c").replace(GT_RE, "\\u003e").replace(AMP_RE, "\\u0026");
|
||||
}
|
||||
function wrapStream(head, stream, template, preRenderedState, options) {
|
||||
streamState(head).writesBodyTags = true;
|
||||
const flushChunk = options?.flushChunk ?? (() => {
|
||||
let chunk;
|
||||
try {
|
||||
chunk = renderSSRHeadSuspenseChunk(head);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
if (!chunk)
|
||||
return "";
|
||||
return `<script${renderNonceAttribute(head._stream?.nonce)}>window.${getStreamKey(head)}&&(${chunk});document.currentScript.remove()<\/script>`;
|
||||
});
|
||||
const enc = encoder ??= new TextEncoder();
|
||||
let reader;
|
||||
let parts;
|
||||
return new ReadableStream({
|
||||
// Async so a failure here rejects into an errored stream instead of
|
||||
// throwing synchronously out of the constructor. The reader is acquired
|
||||
// before rendering (and released if rendering fails) so a failure at
|
||||
// either step leaves `head.entries` intact and the upstream unlocked
|
||||
// for retry.
|
||||
async start(controller) {
|
||||
const activeReader = stream.getReader();
|
||||
let prepared;
|
||||
try {
|
||||
prepared = prepareStreamingTemplate(head, template, preRenderedState);
|
||||
} catch (error) {
|
||||
activeReader.releaseLock();
|
||||
throw error;
|
||||
}
|
||||
reader = activeReader;
|
||||
parts = prepared;
|
||||
controller.enqueue(enc.encode(prepared.shell));
|
||||
},
|
||||
// Read at most one upstream chunk per downstream request so backpressure
|
||||
// propagates instead of eagerly draining the app stream.
|
||||
async pull(controller) {
|
||||
const activeReader = reader;
|
||||
if (!activeReader)
|
||||
return;
|
||||
const result = await activeReader.read().then(
|
||||
(value) => ({ ok: true, value }),
|
||||
(error) => ({ ok: false, error })
|
||||
);
|
||||
if (activeReader !== reader)
|
||||
return;
|
||||
if (!result.ok) {
|
||||
reader = void 0;
|
||||
activeReader.releaseLock();
|
||||
controller.error(result.error);
|
||||
return;
|
||||
}
|
||||
if (result.value.done) {
|
||||
reader = void 0;
|
||||
activeReader.releaseLock();
|
||||
const extra2 = flushChunk?.();
|
||||
if (extra2)
|
||||
controller.enqueue(enc.encode(extra2));
|
||||
const closing = parts ? renderStreamEnd(head, parts) : "";
|
||||
if (closing)
|
||||
controller.enqueue(enc.encode(closing));
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(result.value.value);
|
||||
const extra = flushChunk?.();
|
||||
if (extra)
|
||||
controller.enqueue(enc.encode(extra));
|
||||
},
|
||||
async cancel(reason) {
|
||||
const activeReader = reader;
|
||||
reader = void 0;
|
||||
if (activeReader) {
|
||||
try {
|
||||
await activeReader.cancel(reason);
|
||||
} catch {
|
||||
}
|
||||
activeReader.releaseLock();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function createStreamingTemplateLayout(parsed) {
|
||||
const html = parsed.html;
|
||||
const bodyEnd = parsed.indexes.bodyTagEnd;
|
||||
const bodyCloseStart = parsed.indexes.bodyCloseTagStart;
|
||||
if (bodyEnd < 0 || bodyCloseStart < 0)
|
||||
return;
|
||||
const bodyInterior = html.substring(bodyEnd, bodyCloseStart);
|
||||
const markerMatch = bodyInterior.match(SSR_OUTLET_RE);
|
||||
let beforeStream;
|
||||
let afterStream;
|
||||
if (markerMatch) {
|
||||
beforeStream = bodyInterior.substring(0, markerMatch.index);
|
||||
afterStream = bodyInterior.substring(markerMatch.index + markerMatch[0].length);
|
||||
} else {
|
||||
beforeStream = "";
|
||||
afterStream = bodyInterior;
|
||||
}
|
||||
const shellPart = html.substring(0, bodyEnd) + beforeStream;
|
||||
const endPart = html.substring(bodyCloseStart);
|
||||
let shellTemplate;
|
||||
if (bodyCloseStart >= bodyEnd) {
|
||||
const shellLen = shellPart.length;
|
||||
const { htmlTagStart, headTagEnd, bodyTagStart } = parsed.indexes;
|
||||
const shellHtmlTagStart = htmlTagStart >= 0 && htmlTagStart + 5 <= shellLen ? htmlTagStart : -1;
|
||||
let shellHtmlTagEnd = -1;
|
||||
if (shellHtmlTagStart >= 0) {
|
||||
const gt = shellPart.indexOf(">", shellHtmlTagStart);
|
||||
shellHtmlTagEnd = gt >= 0 ? gt + 1 : shellLen + 7;
|
||||
}
|
||||
shellTemplate = {
|
||||
html: `${shellPart}</body></html>`,
|
||||
input: parsed.input,
|
||||
indexes: {
|
||||
htmlTagStart: shellHtmlTagStart,
|
||||
htmlTagEnd: shellHtmlTagEnd,
|
||||
headTagEnd: headTagEnd >= 0 && headTagEnd + 7 <= shellLen ? headTagEnd : -1,
|
||||
// <body> is always fully inside the prefix in this branch.
|
||||
bodyTagStart,
|
||||
bodyTagEnd: bodyEnd,
|
||||
bodyCloseTagStart: bodyCloseStart + 7 <= shellLen ? bodyCloseStart : shellLen
|
||||
}
|
||||
};
|
||||
} else {
|
||||
shellTemplate = parseHtmlForIndexes(`${shellPart}</body></html>`);
|
||||
}
|
||||
return {
|
||||
shellTemplate,
|
||||
endBeforeBodyTags: afterStream,
|
||||
endAfterBodyTags: endPart
|
||||
};
|
||||
}
|
||||
function getPreparedStreamingLayout(template) {
|
||||
const cache = preparedStreamingLayouts ||= /* @__PURE__ */ new WeakMap();
|
||||
let layout = cache.get(template);
|
||||
if (layout === void 0) {
|
||||
layout = createStreamingTemplateLayout(template) || null;
|
||||
if (layout) {
|
||||
Object.freeze(layout.shellTemplate.indexes);
|
||||
Object.freeze(layout.shellTemplate);
|
||||
Object.freeze(layout);
|
||||
}
|
||||
cache.set(template, layout);
|
||||
}
|
||||
return layout || void 0;
|
||||
}
|
||||
function prepareStreamingTemplate(head, template, preRenderedState) {
|
||||
const ssr = preRenderedState ?? head.render();
|
||||
const parsed = typeof template === "string" ? parseHtmlForIndexes(template) : template;
|
||||
const layout = typeof template === "string" ? createStreamingTemplateLayout(parsed) : getPreparedStreamingLayout(template);
|
||||
let parts;
|
||||
if (layout) {
|
||||
const shell = applyHeadToHtml(layout.shellTemplate, {
|
||||
htmlAttrs: ssr.htmlAttrs,
|
||||
headTags: createBootstrapScript(getStreamKey(head), head._stream?.nonce) + ssr.headTags,
|
||||
bodyAttrs: ssr.bodyAttrs,
|
||||
bodyTagsOpen: ssr.bodyTagsOpen,
|
||||
bodyTags: ""
|
||||
}).replace("</body></html>", "");
|
||||
parts = {
|
||||
shell,
|
||||
end: layout.endBeforeBodyTags + ssr.bodyTags + layout.endAfterBodyTags,
|
||||
bodyTagsAt: layout.endBeforeBodyTags.length
|
||||
};
|
||||
} else {
|
||||
parts = {
|
||||
shell: applyShellToTemplate(head, ssr, parsed),
|
||||
end: ""
|
||||
};
|
||||
}
|
||||
if (!preRenderedState) {
|
||||
rememberShellBodyTags(head);
|
||||
head.entries.clear();
|
||||
}
|
||||
streamState(head).shellRendered = true;
|
||||
return parts;
|
||||
}
|
||||
|
||||
export { createBootstrapScript, createStreamableHead, prepareStreamingTemplate, renderSSRHeadShell, renderSSRHeadSuspenseChunk, renderShell, renderStreamBodyTags, renderStreamEnd, wrapStream };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export { D as DupeableTags, H as HasElementTags, M as MetaTagsArrayable, S as ScriptNetworkEvents, a as SelfClosingTags, T as TagConfigKeys, b as TagPriorityAliases, c as TagsWithInnerContent, U as UniqueTags, d as UsesMergeStrategy, V as ValidHeadTags } from './shared/unhead.CGPOfp5O.mjs';
|
||||
export { d as dedupeKey, a as dedupeTags, h as hashTag, i as isMetaArrayDupeKey, n as normalizeEntryToTags, b as normalizeProps, r as resolveTags, c as resolveTitleTemplate, s as sanitizeTags, w as walkResolver } from './shared/unhead.-hZVKou0.mjs';
|
||||
export { h as hasOwn } from './shared/unhead.Bf_fPVYA.mjs';
|
||||
export { r as resolveMetaKeyType, a as resolveMetaKeyValue, b as resolvePackedMetaObjectValue, u as unpackMeta } from './shared/unhead.BQWSw36o.mjs';
|
||||
export { p as processTemplateParams } from './shared/unhead.BGFxPGPQ.mjs';
|
||||
import './shared/unhead.Bm4Y6XQI.mjs';
|
||||
import 'hookable';
|
||||
Reference in New Issue
Block a user