1060 lines
31 KiB
JavaScript
1060 lines
31 KiB
JavaScript
import { createHooks } from 'hookable';
|
|
import { unref, isRef, version, getCurrentInstance, inject, nextTick, ref, watchEffect, watch, onBeforeUnmount } from 'vue';
|
|
|
|
const TagsWithInnerContent = ["script", "style", "noscript"];
|
|
const HasElementTags$1 = [
|
|
"base",
|
|
"meta",
|
|
"link",
|
|
"style",
|
|
"script",
|
|
"noscript"
|
|
];
|
|
|
|
const UniqueTags$1 = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
|
|
function tagDedupeKey$1(tag, fn) {
|
|
const { props, tag: tagName } = tag;
|
|
if (UniqueTags$1.includes(tagName))
|
|
return tagName;
|
|
if (tagName === "link" && props.rel === "canonical")
|
|
return "canonical";
|
|
if (props.charset)
|
|
return "charset";
|
|
const name = ["id"];
|
|
if (tagName === "meta")
|
|
name.push(...["name", "property", "http-equiv"]);
|
|
for (const n of name) {
|
|
if (typeof props[n] !== "undefined") {
|
|
const val = String(props[n]);
|
|
if (fn && !fn(val))
|
|
return false;
|
|
return `${tagName}:${n}:${val}`;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const setAttrs = (ctx, markSideEffect) => {
|
|
const { tag, $el } = ctx;
|
|
if (!$el)
|
|
return;
|
|
Object.entries(tag.props).forEach(([k, value]) => {
|
|
value = String(value);
|
|
const attrSdeKey = `attr:${k}`;
|
|
if (k === "class") {
|
|
if (!value)
|
|
return;
|
|
for (const c of value.split(" ")) {
|
|
const classSdeKey = `${attrSdeKey}:${c}`;
|
|
if (markSideEffect)
|
|
markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
|
|
if (!$el.classList.contains(c))
|
|
$el.classList.add(c);
|
|
}
|
|
return;
|
|
}
|
|
if (markSideEffect && !k.startsWith("data-h-"))
|
|
markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
|
|
if ($el.getAttribute(k) !== value)
|
|
$el.setAttribute(k, value);
|
|
});
|
|
if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
|
|
$el.innerHTML = tag.children || "";
|
|
};
|
|
|
|
function hashCode(s) {
|
|
let h = 9;
|
|
for (let i = 0; i < s.length; )
|
|
h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
|
|
return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
|
|
}
|
|
|
|
async function renderDOMHead(head, options = {}) {
|
|
const ctx = { shouldRender: true };
|
|
await head.hooks.callHook("dom:beforeRender", ctx);
|
|
if (!ctx.shouldRender)
|
|
return;
|
|
const dom = options.document || window.document;
|
|
const staleSideEffects = head._popSideEffectQueue();
|
|
head.headEntries().map((entry) => entry._sde).forEach((sde) => {
|
|
Object.entries(sde).forEach(([key, fn]) => {
|
|
staleSideEffects[key] = fn;
|
|
});
|
|
});
|
|
const preRenderTag = async (tag) => {
|
|
const entry = head.headEntries().find((e) => e._i === tag._e);
|
|
const renderCtx = {
|
|
renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
|
|
$el: null,
|
|
shouldRender: true,
|
|
tag,
|
|
entry,
|
|
staleSideEffects
|
|
};
|
|
await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
|
|
return renderCtx;
|
|
};
|
|
const renders = [];
|
|
const pendingRenders = {
|
|
body: [],
|
|
head: []
|
|
};
|
|
const markSideEffect = (ctx2, key, fn) => {
|
|
key = `${ctx2.renderId}:${key}`;
|
|
if (ctx2.entry)
|
|
ctx2.entry._sde[key] = fn;
|
|
delete staleSideEffects[key];
|
|
};
|
|
const markEl = (ctx2) => {
|
|
head._elMap[ctx2.renderId] = ctx2.$el;
|
|
renders.push(ctx2);
|
|
markSideEffect(ctx2, "el", () => {
|
|
ctx2.$el?.remove();
|
|
delete head._elMap[ctx2.renderId];
|
|
});
|
|
};
|
|
for (const t of await head.resolveTags()) {
|
|
const ctx2 = await preRenderTag(t);
|
|
if (!ctx2.shouldRender)
|
|
continue;
|
|
const { tag } = ctx2;
|
|
if (tag.tag === "title") {
|
|
dom.title = tag.children || "";
|
|
renders.push(ctx2);
|
|
continue;
|
|
}
|
|
if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
|
|
ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
|
|
setAttrs(ctx2, markSideEffect);
|
|
renders.push(ctx2);
|
|
continue;
|
|
}
|
|
ctx2.$el = head._elMap[ctx2.renderId];
|
|
if (!ctx2.$el && tag._hash) {
|
|
ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
|
|
}
|
|
if (ctx2.$el) {
|
|
if (ctx2.tag._d)
|
|
setAttrs(ctx2);
|
|
markEl(ctx2);
|
|
continue;
|
|
}
|
|
ctx2.$el = dom.createElement(tag.tag);
|
|
setAttrs(ctx2);
|
|
pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
|
|
}
|
|
Object.entries(pendingRenders).forEach(([pos, queue]) => {
|
|
if (!queue.length)
|
|
return;
|
|
for (const $el of [...dom[pos].children].reverse()) {
|
|
const elTag = $el.tagName.toLowerCase();
|
|
if (!HasElementTags$1.includes(elTag))
|
|
continue;
|
|
const dedupeKey = tagDedupeKey$1({
|
|
tag: elTag,
|
|
props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
|
|
});
|
|
const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode(ctx2.$el)));
|
|
if (matchIdx !== -1) {
|
|
const ctx2 = queue[matchIdx];
|
|
ctx2.$el = $el;
|
|
setAttrs(ctx2);
|
|
markEl(ctx2);
|
|
delete queue[matchIdx];
|
|
}
|
|
}
|
|
queue.forEach((ctx2) => {
|
|
if (!ctx2.$el)
|
|
return;
|
|
switch (ctx2.tag.tagPosition) {
|
|
case "bodyClose":
|
|
dom.body.appendChild(ctx2.$el);
|
|
break;
|
|
case "bodyOpen":
|
|
dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
|
|
break;
|
|
case "head":
|
|
default:
|
|
dom.head.appendChild(ctx2.$el);
|
|
break;
|
|
}
|
|
markEl(ctx2);
|
|
});
|
|
});
|
|
for (const ctx2 of renders)
|
|
await head.hooks.callHook("dom:renderTag", ctx2);
|
|
Object.values(staleSideEffects).forEach((fn) => fn());
|
|
}
|
|
let domUpdatePromise = null;
|
|
async function debouncedRenderDOMHead(head, options = {}) {
|
|
function doDomUpdate() {
|
|
domUpdatePromise = null;
|
|
return renderDOMHead(head, options);
|
|
}
|
|
const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
|
|
return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
|
|
}
|
|
|
|
const index = {
|
|
__proto__: null,
|
|
debouncedRenderDOMHead: debouncedRenderDOMHead,
|
|
get domUpdatePromise () { return domUpdatePromise; },
|
|
hashCode: hashCode,
|
|
renderDOMHead: renderDOMHead
|
|
};
|
|
|
|
const ValidHeadTags = [
|
|
"title",
|
|
"titleTemplate",
|
|
"base",
|
|
"htmlAttrs",
|
|
"bodyAttrs",
|
|
"meta",
|
|
"link",
|
|
"style",
|
|
"script",
|
|
"noscript"
|
|
];
|
|
const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy"];
|
|
|
|
async function normaliseTag(tagName, input) {
|
|
const tag = { tag: tagName, props: {} };
|
|
if (tagName === "title" || tagName === "titleTemplate") {
|
|
tag.children = input instanceof Promise ? await input : input;
|
|
return tag;
|
|
}
|
|
tag.props = await normaliseProps({ ...input });
|
|
["children", "innerHtml", "innerHTML"].forEach((key) => {
|
|
if (typeof tag.props[key] !== "undefined") {
|
|
tag.children = tag.props[key];
|
|
if (typeof tag.children === "object")
|
|
tag.children = JSON.stringify(tag.children);
|
|
delete tag.props[key];
|
|
}
|
|
});
|
|
Object.keys(tag.props).filter((k) => TagConfigKeys.includes(k)).forEach((k) => {
|
|
tag[k] = tag.props[k];
|
|
delete tag.props[k];
|
|
});
|
|
if (typeof tag.props.class === "object" && !Array.isArray(tag.props.class)) {
|
|
tag.props.class = Object.keys(tag.props.class).filter((k) => tag.props.class[k]);
|
|
}
|
|
if (Array.isArray(tag.props.class))
|
|
tag.props.class = tag.props.class.join(" ");
|
|
if (tag.props.content && Array.isArray(tag.props.content)) {
|
|
return tag.props.content.map((v, i) => {
|
|
const newTag = { ...tag, props: { ...tag.props } };
|
|
newTag.props.content = v;
|
|
newTag.key = `${tag.props.name || tag.props.property}:${i}`;
|
|
return newTag;
|
|
});
|
|
}
|
|
return tag;
|
|
}
|
|
async function normaliseProps(props) {
|
|
for (const k of Object.keys(props)) {
|
|
if (props[k] instanceof Promise) {
|
|
props[k] = await props[k];
|
|
}
|
|
if (String(props[k]) === "true") {
|
|
props[k] = "";
|
|
} else if (String(props[k]) === "false") {
|
|
delete props[k];
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
|
|
const tagWeight = (tag) => {
|
|
if (typeof tag.tagPriority === "number")
|
|
return tag.tagPriority;
|
|
switch (tag.tagPriority) {
|
|
case "critical":
|
|
return 2;
|
|
case "high":
|
|
return 9;
|
|
case "low":
|
|
return 12;
|
|
}
|
|
switch (tag.tag) {
|
|
case "base":
|
|
return -1;
|
|
case "title":
|
|
return 1;
|
|
case "meta":
|
|
if (tag.props.charset)
|
|
return -2;
|
|
if (tag.props["http-equiv"] === "content-security-policy")
|
|
return 0;
|
|
return 10;
|
|
default:
|
|
return 10;
|
|
}
|
|
};
|
|
const sortTags = (aTag, bTag) => {
|
|
return tagWeight(aTag) - tagWeight(bTag);
|
|
};
|
|
|
|
const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
|
|
function tagDedupeKey(tag, fn) {
|
|
const { props, tag: tagName } = tag;
|
|
if (UniqueTags.includes(tagName))
|
|
return tagName;
|
|
if (tagName === "link" && props.rel === "canonical")
|
|
return "canonical";
|
|
if (props.charset)
|
|
return "charset";
|
|
const name = ["id"];
|
|
if (tagName === "meta")
|
|
name.push(...["name", "property", "http-equiv"]);
|
|
for (const n of name) {
|
|
if (typeof props[n] !== "undefined") {
|
|
const val = String(props[n]);
|
|
if (fn && !fn(val))
|
|
return false;
|
|
return `${tagName}:${n}:${val}`;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const renderTitleTemplate = (template, title) => {
|
|
if (template == null)
|
|
return title || null;
|
|
if (typeof template === "function")
|
|
return template(title);
|
|
return template.replace("%s", title ?? "");
|
|
};
|
|
function resolveTitleTemplateFromTags(tags) {
|
|
let titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
|
|
const titleIdx = tags.findIndex((i) => i.tag === "title");
|
|
if (titleIdx !== -1 && titleTemplateIdx !== -1) {
|
|
const newTitle = renderTitleTemplate(
|
|
tags[titleTemplateIdx].children,
|
|
tags[titleIdx].children
|
|
);
|
|
if (newTitle !== null) {
|
|
tags[titleIdx].children = newTitle || tags[titleIdx].children;
|
|
} else {
|
|
delete tags[titleIdx];
|
|
}
|
|
} else if (titleTemplateIdx !== -1) {
|
|
const newTitle = renderTitleTemplate(
|
|
tags[titleTemplateIdx].children
|
|
);
|
|
if (newTitle !== null) {
|
|
tags[titleTemplateIdx].children = newTitle;
|
|
tags[titleTemplateIdx].tag = "title";
|
|
titleTemplateIdx = -1;
|
|
}
|
|
}
|
|
if (titleTemplateIdx !== -1) {
|
|
delete tags[titleTemplateIdx];
|
|
}
|
|
return tags.filter(Boolean);
|
|
}
|
|
|
|
const DedupesTagsPlugin = (options) => {
|
|
options = options || {};
|
|
const dedupeKeys = options.dedupeKeys || ["hid", "vmid", "key"];
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"tag:normalise": function({ tag }) {
|
|
dedupeKeys.forEach((key) => {
|
|
if (tag.props[key]) {
|
|
tag.key = tag.props[key];
|
|
delete tag.props[key];
|
|
}
|
|
});
|
|
const dedupe = tag.key ? `${tag.tag}:${tag.key}` : tagDedupeKey(tag);
|
|
if (dedupe)
|
|
tag._d = dedupe;
|
|
},
|
|
"tags:resolve": function(ctx) {
|
|
const deduping = {};
|
|
ctx.tags.forEach((tag) => {
|
|
let dedupeKey = tag._d || tag._p;
|
|
const dupedTag = deduping[dedupeKey];
|
|
if (dupedTag) {
|
|
let strategy = tag?.tagDuplicateStrategy;
|
|
if (!strategy && (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs"))
|
|
strategy = "merge";
|
|
if (strategy === "merge") {
|
|
const oldProps = dupedTag.props;
|
|
["class", "style"].forEach((key) => {
|
|
if (tag.props[key] && oldProps[key]) {
|
|
if (key === "style" && !oldProps[key].endsWith(";"))
|
|
oldProps[key] += ";";
|
|
tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
|
|
}
|
|
});
|
|
deduping[dedupeKey].props = {
|
|
...oldProps,
|
|
...tag.props
|
|
};
|
|
return;
|
|
} else if (tag._e === dupedTag._e) {
|
|
dedupeKey = tag._d = `${dedupeKey}:${tag._p}`;
|
|
}
|
|
const propCount = Object.keys(tag.props).length;
|
|
if ((propCount === 0 || propCount === 1 && typeof tag.props["data-h-key"] !== "undefined") && !tag.children) {
|
|
delete deduping[dedupeKey];
|
|
return;
|
|
}
|
|
}
|
|
deduping[dedupeKey] = tag;
|
|
});
|
|
ctx.tags = Object.values(deduping);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const SortTagsPlugin = () => {
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"tags:resolve": (ctx) => {
|
|
const tagIndexForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
|
|
for (const tag of ctx.tags) {
|
|
if (!tag.tagPriority || typeof tag.tagPriority === "number")
|
|
continue;
|
|
const modifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
|
|
for (const { prefix, offset } of modifiers) {
|
|
if (tag.tagPriority.startsWith(prefix)) {
|
|
const key = tag.tagPriority.replace(prefix, "");
|
|
const index = tagIndexForKey(key);
|
|
if (typeof index !== "undefined")
|
|
tag._p = index + offset;
|
|
}
|
|
}
|
|
}
|
|
ctx.tags.sort((a, b) => a._p - b._p).sort(sortTags);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const TitleTemplatePlugin = () => {
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"tags:resolve": (ctx) => {
|
|
ctx.tags = resolveTitleTemplateFromTags(ctx.tags);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const DeprecatedTagAttrPlugin = () => {
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"tag:normalise": function({ tag }) {
|
|
if (typeof tag.props.body !== "undefined") {
|
|
tag.tagPosition = "bodyClose";
|
|
delete tag.props.body;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const IsBrowser$1 = typeof window !== "undefined";
|
|
|
|
const ProvideTagHashPlugin = () => {
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"tag:normalise": (ctx) => {
|
|
const { tag, entry } = ctx;
|
|
const isDynamic = typeof tag.props._dynamic !== "undefined";
|
|
if (!HasElementTags.includes(tag.tag) || !tag.key)
|
|
return;
|
|
tag._hash = hashCode(JSON.stringify({ tag: tag.tag, key: tag.key }));
|
|
if (IsBrowser$1 || getActiveHead()?.resolvedOptions?.document)
|
|
return;
|
|
if (entry._m === "server" || isDynamic) {
|
|
tag.props[`data-h-${tag._hash}`] = "";
|
|
}
|
|
},
|
|
"tags:resolve": (ctx) => {
|
|
ctx.tags = ctx.tags.map((t) => {
|
|
delete t.props._dynamic;
|
|
return t;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const PatchDomOnEntryUpdatesPlugin = (options) => {
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"entries:updated": function(head) {
|
|
if (typeof options?.document === "undefined" && typeof window === "undefined")
|
|
return;
|
|
let delayFn = options?.delayFn;
|
|
if (!delayFn && typeof requestAnimationFrame !== "undefined")
|
|
delayFn = requestAnimationFrame;
|
|
Promise.resolve().then(function () { return index; }).then(({ debouncedRenderDOMHead }) => {
|
|
debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
|
|
});
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const EventHandlersPlugin = () => {
|
|
const stripEventHandlers = (mode, tag) => {
|
|
const props = {};
|
|
const eventHandlers = {};
|
|
Object.entries(tag.props).forEach(([key, value]) => {
|
|
if (key.startsWith("on") && typeof value === "function")
|
|
eventHandlers[key] = value;
|
|
else
|
|
props[key] = value;
|
|
});
|
|
let delayedSrc;
|
|
if (mode === "dom" && tag.tag === "script" && typeof props.src === "string" && typeof eventHandlers.onload !== "undefined") {
|
|
delayedSrc = props.src;
|
|
delete props.src;
|
|
}
|
|
return { props, eventHandlers, delayedSrc };
|
|
};
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"ssr:render": function(ctx) {
|
|
ctx.tags = ctx.tags.map((tag) => {
|
|
tag.props = stripEventHandlers("ssr", tag).props;
|
|
return tag;
|
|
});
|
|
},
|
|
"dom:beforeRenderTag": function(ctx) {
|
|
const { props, eventHandlers, delayedSrc } = stripEventHandlers("dom", ctx.tag);
|
|
if (!Object.keys(eventHandlers).length)
|
|
return;
|
|
ctx.tag.props = props;
|
|
ctx.tag._eventHandlers = eventHandlers;
|
|
ctx.tag._delayedSrc = delayedSrc;
|
|
},
|
|
"dom:renderTag": function(ctx) {
|
|
const $el = ctx.$el;
|
|
if (!ctx.tag._eventHandlers || !$el)
|
|
return;
|
|
const $eventListenerTarget = ctx.tag.tag === "bodyAttrs" && typeof window !== "undefined" ? window : $el;
|
|
Object.entries(ctx.tag._eventHandlers).forEach(([k, value]) => {
|
|
const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
|
|
const eventName = k.slice(2).toLowerCase();
|
|
const eventDedupeKey = `data-h-${eventName}`;
|
|
delete ctx.staleSideEffects[sdeKey];
|
|
if ($el.hasAttribute(eventDedupeKey))
|
|
return;
|
|
const handler = value;
|
|
$el.setAttribute(eventDedupeKey, "");
|
|
$eventListenerTarget.addEventListener(eventName, handler);
|
|
if (ctx.entry) {
|
|
ctx.entry._sde[sdeKey] = () => {
|
|
$eventListenerTarget.removeEventListener(eventName, handler);
|
|
$el.removeAttribute(eventDedupeKey);
|
|
};
|
|
}
|
|
});
|
|
if (ctx.tag._delayedSrc) {
|
|
$el.setAttribute("src", ctx.tag._delayedSrc);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
function asArray$1(value) {
|
|
return Array.isArray(value) ? value : [value];
|
|
}
|
|
const HasElementTags = [
|
|
"base",
|
|
"meta",
|
|
"link",
|
|
"style",
|
|
"script",
|
|
"noscript"
|
|
];
|
|
|
|
let activeHead;
|
|
const setActiveHead = (head) => activeHead = head;
|
|
const getActiveHead = () => activeHead;
|
|
|
|
const TagEntityBits = 10;
|
|
|
|
async function normaliseEntryTags(e) {
|
|
const tagPromises = [];
|
|
Object.entries(e.resolvedInput || e.input).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).forEach(([k, value]) => {
|
|
const v = asArray$1(value);
|
|
tagPromises.push(...v.map((props) => normaliseTag(k, props)).flat());
|
|
});
|
|
return (await Promise.all(tagPromises)).flat().map((t, i) => {
|
|
t._e = e._i;
|
|
t._p = (e._i << TagEntityBits) + i;
|
|
return t;
|
|
});
|
|
}
|
|
|
|
const CorePlugins = () => [
|
|
DedupesTagsPlugin(),
|
|
SortTagsPlugin(),
|
|
TitleTemplatePlugin(),
|
|
ProvideTagHashPlugin(),
|
|
EventHandlersPlugin(),
|
|
DeprecatedTagAttrPlugin()
|
|
];
|
|
const DOMPlugins = (options = {}) => [
|
|
PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
|
|
];
|
|
function createHead$1(options = {}) {
|
|
const head = createHeadCore({
|
|
...options,
|
|
plugins: [...DOMPlugins(options), ...options?.plugins || []]
|
|
});
|
|
setActiveHead(head);
|
|
return head;
|
|
}
|
|
function createHeadCore(options = {}) {
|
|
let entries = [];
|
|
let _sde = {};
|
|
let _eid = 0;
|
|
const hooks = createHooks();
|
|
if (options?.hooks)
|
|
hooks.addHooks(options.hooks);
|
|
options.plugins = [
|
|
...CorePlugins(),
|
|
...options?.plugins || []
|
|
];
|
|
options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
|
|
const updated = () => hooks.callHook("entries:updated", head);
|
|
const head = {
|
|
resolvedOptions: options,
|
|
headEntries() {
|
|
return entries;
|
|
},
|
|
get hooks() {
|
|
return hooks;
|
|
},
|
|
use(plugin) {
|
|
if (plugin.hooks)
|
|
hooks.addHooks(plugin.hooks);
|
|
},
|
|
push(input, options2) {
|
|
const activeEntry = {
|
|
_i: _eid++,
|
|
input,
|
|
_sde: {}
|
|
};
|
|
if (options2?.mode)
|
|
activeEntry._m = options2?.mode;
|
|
entries.push(activeEntry);
|
|
updated();
|
|
return {
|
|
dispose() {
|
|
entries = entries.filter((e) => {
|
|
if (e._i !== activeEntry._i)
|
|
return true;
|
|
_sde = { ..._sde, ...e._sde || {} };
|
|
e._sde = {};
|
|
updated();
|
|
return false;
|
|
});
|
|
},
|
|
patch(input2) {
|
|
entries = entries.map((e) => {
|
|
if (e._i === activeEntry._i) {
|
|
activeEntry.input = e.input = input2;
|
|
updated();
|
|
}
|
|
return e;
|
|
});
|
|
}
|
|
};
|
|
},
|
|
async resolveTags() {
|
|
const resolveCtx = { tags: [], entries: [...entries] };
|
|
await hooks.callHook("entries:resolve", resolveCtx);
|
|
for (const entry of resolveCtx.entries) {
|
|
for (const tag of await normaliseEntryTags(entry)) {
|
|
const tagCtx = { tag, entry };
|
|
await hooks.callHook("tag:normalise", tagCtx);
|
|
resolveCtx.tags.push(tagCtx.tag);
|
|
}
|
|
}
|
|
await hooks.callHook("tags:resolve", resolveCtx);
|
|
return resolveCtx.tags;
|
|
},
|
|
_elMap: {},
|
|
_popSideEffectQueue() {
|
|
const sde = { ..._sde };
|
|
_sde = {};
|
|
return sde;
|
|
}
|
|
};
|
|
head.hooks.callHook("init", head);
|
|
return head;
|
|
}
|
|
|
|
function defineHeadPlugin(plugin) {
|
|
return plugin;
|
|
}
|
|
const composableNames = [
|
|
"useHead",
|
|
"useTagTitle",
|
|
"useTagBase",
|
|
"useTagMeta",
|
|
"useTagMetaFlat",
|
|
"useSeoMeta",
|
|
"useTagLink",
|
|
"useTagScript",
|
|
"useTagStyle",
|
|
"useTagNoscript",
|
|
"useHtmlAttrs",
|
|
"useBodyAttrs",
|
|
"useTitleTemplate",
|
|
"useServerHead",
|
|
"useServerTagTitle",
|
|
"useServerTagBase",
|
|
"useServerTagMeta",
|
|
"useServerTagMetaFlat",
|
|
"useServerTagLink",
|
|
"useServerTagScript",
|
|
"useServerTagStyle",
|
|
"useServerTagNoscript",
|
|
"useServerHtmlAttrs",
|
|
"useServerBodyAttrs",
|
|
"useServerTitleTemplate"
|
|
];
|
|
|
|
function resolveUnref(r) {
|
|
return typeof r === "function" ? r() : unref(r);
|
|
}
|
|
function resolveUnrefHeadInput(ref, lastKey = "") {
|
|
if (ref instanceof Promise)
|
|
return ref;
|
|
const root = resolveUnref(ref);
|
|
if (!ref || !root)
|
|
return root;
|
|
if (Array.isArray(root))
|
|
return root.map((r) => resolveUnrefHeadInput(r, lastKey));
|
|
if (typeof root === "object") {
|
|
let dynamic = false;
|
|
const unrefdObj = Object.fromEntries(
|
|
Object.entries(root).map(([k, v]) => {
|
|
if (k === "titleTemplate" || k.startsWith("on"))
|
|
return [k, unref(v)];
|
|
if (typeof v === "function" || isRef(v))
|
|
dynamic = true;
|
|
return [k, resolveUnrefHeadInput(v, k)];
|
|
})
|
|
);
|
|
if (dynamic && HasElementTags.includes(String(lastKey)))
|
|
unrefdObj._dynamic = true;
|
|
return unrefdObj;
|
|
}
|
|
return root;
|
|
}
|
|
function asArray(value) {
|
|
return Array.isArray(value) ? value : [value];
|
|
}
|
|
|
|
const Vue3 = version.startsWith("3");
|
|
const IsBrowser = typeof window !== "undefined";
|
|
|
|
const headSymbol = "usehead";
|
|
function injectHead() {
|
|
return getCurrentInstance() && inject(headSymbol) || getActiveHead();
|
|
}
|
|
function createHead(options = {}) {
|
|
const head = createHead$1({
|
|
...options,
|
|
domDelayFn: (fn) => setTimeout(() => nextTick(() => fn()), 10),
|
|
plugins: [
|
|
VueReactiveUseHeadPlugin(),
|
|
...options?.plugins || []
|
|
]
|
|
});
|
|
const vuePlugin = {
|
|
install(app) {
|
|
if (Vue3) {
|
|
app.config.globalProperties.$unhead = head;
|
|
app.provide(headSymbol, head);
|
|
}
|
|
}
|
|
};
|
|
head.install = vuePlugin.install;
|
|
return head;
|
|
}
|
|
|
|
const VueHeadMixin = {
|
|
created() {
|
|
const instance = getCurrentInstance();
|
|
if (!instance)
|
|
return;
|
|
const options = instance.type;
|
|
if (!options || !("head" in options))
|
|
return;
|
|
const source = typeof options.head === "function" ? () => options.head.call(instance.proxy) : options.head;
|
|
useHead(source);
|
|
}
|
|
};
|
|
|
|
const VueReactiveUseHeadPlugin = () => {
|
|
return defineHeadPlugin({
|
|
hooks: {
|
|
"entries:resolve": function(ctx) {
|
|
for (const entry of ctx.entries)
|
|
entry.resolvedInput = resolveUnrefHeadInput(entry.input);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const Vue2ProvideUnheadPlugin = function(_Vue, head) {
|
|
_Vue.mixin({
|
|
beforeCreate() {
|
|
const options = this.$options;
|
|
const origProvide = options.provide;
|
|
options.provide = function() {
|
|
let origProvideResult;
|
|
if (typeof origProvide === "function")
|
|
origProvideResult = origProvide.call(this);
|
|
else
|
|
origProvideResult = origProvide || {};
|
|
return {
|
|
...origProvideResult,
|
|
[headSymbol]: head
|
|
};
|
|
};
|
|
}
|
|
});
|
|
};
|
|
|
|
function unpackToArray(input, options) {
|
|
const unpacked = [];
|
|
const kFn = options.resolveKeyData || ((ctx) => ctx.key);
|
|
const vFn = options.resolveValueData || ((ctx) => ctx.value);
|
|
for (const [k, v] of Object.entries(input)) {
|
|
unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
|
|
const ctx = { key: k, value: i };
|
|
const val = vFn(ctx);
|
|
if (typeof val === "object")
|
|
return unpackToArray(val, options);
|
|
if (Array.isArray(val))
|
|
return val;
|
|
return {
|
|
[typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
|
|
[typeof options.value === "function" ? options.value(ctx) : options.value]: val
|
|
};
|
|
}).flat());
|
|
}
|
|
return unpacked;
|
|
}
|
|
|
|
function unpackToString(value, options) {
|
|
return Object.entries(value).map(([key, value2]) => {
|
|
if (typeof value2 === "object")
|
|
value2 = unpackToString(value2, options);
|
|
if (options.resolve) {
|
|
const resolved = options.resolve({ key, value: value2 });
|
|
if (resolved)
|
|
return resolved;
|
|
}
|
|
if (typeof value2 === "number")
|
|
value2 = value2.toString();
|
|
if (typeof value2 === "string" && options.wrapValue) {
|
|
value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
|
|
value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
|
|
}
|
|
return `${key}${options.keyValueSeparator || ""}${value2}`;
|
|
}).join(options.entrySeparator || "");
|
|
}
|
|
|
|
const MetaPackingSchema = {
|
|
robots: {
|
|
unpack: {
|
|
keyValueSeparator: ":"
|
|
}
|
|
},
|
|
contentSecurityPolicy: {
|
|
unpack: {
|
|
keyValueSeparator: " ",
|
|
entrySeparator: "; "
|
|
},
|
|
metaKey: "http-equiv"
|
|
},
|
|
fbAppId: {
|
|
keyValue: "fb:app_id",
|
|
metaKey: "property"
|
|
},
|
|
msapplicationTileImage: {
|
|
keyValue: "msapplication-TileImage"
|
|
},
|
|
msapplicationTileColor: {
|
|
keyValue: "msapplication-TileColor"
|
|
},
|
|
msapplicationConfig: {
|
|
keyValue: "msapplication-Config"
|
|
},
|
|
charset: {
|
|
metaKey: "charset"
|
|
},
|
|
contentType: {
|
|
metaKey: "http-equiv"
|
|
},
|
|
defaultStyle: {
|
|
metaKey: "http-equiv"
|
|
},
|
|
xUaCompatible: {
|
|
metaKey: "http-equiv"
|
|
},
|
|
refresh: {
|
|
metaKey: "http-equiv"
|
|
}
|
|
};
|
|
function resolveMetaKeyType(key) {
|
|
return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
|
|
}
|
|
|
|
function unpackMeta(input) {
|
|
const meta = unpackToArray(input, {
|
|
key({ key }) {
|
|
return resolveMetaKeyType(key);
|
|
},
|
|
value({ key }) {
|
|
return key === "charset" ? "charset" : "content";
|
|
},
|
|
resolveKeyData({ key }) {
|
|
return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
|
|
},
|
|
resolveValueData({ value, key }) {
|
|
if (value === null)
|
|
return "_null";
|
|
if (typeof value === "object") {
|
|
const definition = MetaPackingSchema[key];
|
|
if (key === "refresh")
|
|
return `${value.seconds};url=${value.url}`;
|
|
return unpackToString(
|
|
changeKeyCasingDeep(value),
|
|
{
|
|
entrySeparator: ", ",
|
|
keyValueSeparator: "=",
|
|
resolve({ value: value2, key: key2 }) {
|
|
if (value2 === null)
|
|
return "";
|
|
if (typeof value2 === "boolean")
|
|
return `${key2}`;
|
|
},
|
|
...definition?.unpack
|
|
}
|
|
);
|
|
}
|
|
return typeof value === "number" ? value.toString() : value;
|
|
}
|
|
});
|
|
return meta.filter((v) => typeof v.content === "undefined" || v.content !== "_null");
|
|
}
|
|
|
|
const PropertyPrefixKeys = /^(og|twitter|fb)/;
|
|
function fixKeyCase(key) {
|
|
key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
if (PropertyPrefixKeys.test(key)) {
|
|
key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
|
|
}
|
|
return key;
|
|
}
|
|
function changeKeyCasingDeep(input) {
|
|
if (Array.isArray(input)) {
|
|
return input.map((entry) => changeKeyCasingDeep(entry));
|
|
}
|
|
if (typeof input !== "object" || Array.isArray(input))
|
|
return input;
|
|
const output = {};
|
|
for (const [key, value] of Object.entries(input))
|
|
output[fixKeyCase(key)] = changeKeyCasingDeep(value);
|
|
return output;
|
|
}
|
|
|
|
function clientUseHead(input, options = {}) {
|
|
const head = injectHead();
|
|
const resolvedInput = ref({});
|
|
watchEffect(() => {
|
|
resolvedInput.value = resolveUnrefHeadInput(input);
|
|
});
|
|
const entry = head.push(resolvedInput.value, options);
|
|
watch(resolvedInput, (e) => entry.patch(e));
|
|
const vm = getCurrentInstance();
|
|
if (vm) {
|
|
onBeforeUnmount(() => {
|
|
entry.dispose();
|
|
});
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
function serverUseHead(input, options = {}) {
|
|
const head = injectHead();
|
|
return head.push(input, options);
|
|
}
|
|
|
|
function useServerHead(input, options = {}) {
|
|
return useHead(input, { ...options, mode: "server" });
|
|
}
|
|
const useServerTagTitle = (title) => useServerHead({ title });
|
|
const useServerTitleTemplate = (titleTemplate) => useServerHead({ titleTemplate });
|
|
const useServerTagMeta = (meta) => useServerHead({ meta: asArray(meta) });
|
|
const useServerTagMetaFlat = (meta) => {
|
|
const input = ref({});
|
|
watchEffect(() => {
|
|
input.value = unpackMeta(resolveUnrefHeadInput(meta));
|
|
});
|
|
return useServerHead({ meta: input });
|
|
};
|
|
const useServerTagLink = (link) => useServerHead({ link: asArray(link) });
|
|
const useServerTagScript = (script) => useServerHead({ script: asArray(script) });
|
|
const useServerTagStyle = (style) => useServerHead({ style: asArray(style) });
|
|
const useServerTagNoscript = (noscript) => useServerHead({ noscript: asArray(noscript) });
|
|
const useServerTagBase = (base) => useServerHead({ base });
|
|
const useServerHtmlAttrs = (attrs) => useServerHead({ htmlAttrs: attrs });
|
|
const useServerBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
|
|
|
|
function useHead(input, options = {}) {
|
|
const head = injectHead();
|
|
if (head) {
|
|
const isBrowser = IsBrowser || !!head.resolvedOptions?.document;
|
|
if (options.mode === "server" && isBrowser || options.mode === "client" && !isBrowser)
|
|
return;
|
|
return isBrowser ? clientUseHead(input, options) : serverUseHead(input, options);
|
|
}
|
|
}
|
|
const useTagTitle = (title) => useHead({ title });
|
|
const useTitleTemplate = (titleTemplate) => useHead({ titleTemplate });
|
|
const useTagMeta = (meta) => useHead({ meta: asArray(meta) });
|
|
const useTagMetaFlat = (meta) => {
|
|
const input = ref({});
|
|
watchEffect(() => {
|
|
input.value = unpackMeta(resolveUnrefHeadInput(meta));
|
|
});
|
|
return useHead({ meta: input });
|
|
};
|
|
const useSeoMeta = useTagMetaFlat;
|
|
const useTagLink = (link) => useHead({ link: asArray(link) });
|
|
const useTagScript = (script) => useHead({ script: asArray(script) });
|
|
const useTagStyle = (style) => useHead({ style: asArray(style) });
|
|
const useTagNoscript = (noscript) => useHead({ noscript: asArray(noscript) });
|
|
const useTagBase = (base) => useHead({ base });
|
|
const useHtmlAttrs = (attrs) => useHead({ htmlAttrs: attrs });
|
|
const useBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
|
|
|
|
const coreComposableNames = [
|
|
"injectHead"
|
|
];
|
|
const unheadVueComposablesImports = [
|
|
{
|
|
from: "@unhead/vue",
|
|
imports: [...coreComposableNames, ...composableNames]
|
|
}
|
|
];
|
|
|
|
export { Vue2ProvideUnheadPlugin, VueHeadMixin, VueReactiveUseHeadPlugin, asArray, createHead, createHeadCore, headSymbol, injectHead, resolveUnrefHeadInput, unheadVueComposablesImports, useBodyAttrs, useHead, useHtmlAttrs, useSeoMeta, useServerBodyAttrs, useServerHead, useServerHtmlAttrs, useServerTagBase, useServerTagLink, useServerTagMeta, useServerTagMetaFlat, useServerTagNoscript, useServerTagScript, useServerTagStyle, useServerTagTitle, useServerTitleTemplate, useTagBase, useTagLink, useTagMeta, useTagMetaFlat, useTagNoscript, useTagScript, useTagStyle, useTagTitle, useTitleTemplate };
|