@vueuse/head
Advanced tools
Comparing version 0.9.7 to 1.0.0-rc.1
@@ -31,2 +31,8 @@ import * as vue from 'vue'; | ||
/** | ||
* Text content of the tag. | ||
* | ||
* @deprecated This can only be used with `useHeadRaw`. | ||
*/ | ||
innerHTML?: string; | ||
/** | ||
* Sets the textContent of an element. | ||
@@ -53,4 +59,2 @@ * | ||
* All other tags have a default priority of 10: <meta>, <script>, <link>, <style>, etc | ||
* | ||
* @warn Experimental feature. Only available when rendering SSR | ||
*/ | ||
@@ -75,15 +79,15 @@ renderPriority?: number; | ||
declare type UseHeadInput<T extends MergeHead = {}> = MaybeComputedRef<HeadObject<T>>; | ||
declare type ResolvedUseHeadInput<T extends MergeHead = {}> = Head$1<T & VueUseHeadSchema>; | ||
declare type UseHeadRawInput = MaybeComputedRef<ReactiveHead<RawHeadAugmentation & VueUseHeadSchema>>; | ||
interface HeadEntryOptions { | ||
raw?: boolean; | ||
resolved?: boolean; | ||
} | ||
interface HeadEntry<T extends MergeHead = {}> { | ||
options?: HeadEntryOptions; | ||
input: UseHeadInput<T>; | ||
id?: number; | ||
options: HeadEntryOptions; | ||
tags: HeadTag[]; | ||
input: ResolvedUseHeadInput<T>; | ||
resolved: boolean; | ||
id: number; | ||
} | ||
interface ResolvedHeadEntry<T extends MergeHead = {}> { | ||
options?: HeadEntryOptions; | ||
input: Head$1<T & VueUseHeadSchema>; | ||
} | ||
declare type TagKeys = keyof Omit<HeadObjectPlain, 'titleTemplate'>; | ||
@@ -94,17 +98,18 @@ | ||
} | ||
declare type HookBeforeDomUpdate = ((tags: Record<string, HeadTag[]>) => void | boolean)[]; | ||
declare type HookTagsResolved = ((tags: HeadTag[]) => void)[]; | ||
declare type HookBeforeDomUpdate = ((tags: HeadTag[]) => Promise<void | boolean> | void | boolean)[]; | ||
declare type HookTagsResolved = ((tags: HeadTag[]) => Promise<void> | void)[]; | ||
declare type HeadTagRuntime = HeadEntryOptions & HandlesDuplicates & HasRenderPriority & RendersToBody & HasTextContent & { | ||
position: number; | ||
entryId: number; | ||
}; | ||
interface HeadTag { | ||
tag: TagKeys; | ||
props: HandlesDuplicates & HasRenderPriority & RendersToBody & HasTextContent & { | ||
props: { | ||
[k: string]: any; | ||
}; | ||
_options?: HeadEntryOptions; | ||
_position?: number; | ||
_runtime: HeadTagRuntime; | ||
} | ||
interface DomUpdateCtx { | ||
title: string | undefined; | ||
htmlAttrs: HeadAttrs; | ||
bodyAttrs: HeadAttrs; | ||
actualTags: Record<string, HeadTag[]>; | ||
interface HeadObjectApi<T extends MergeHead = {}> { | ||
update: (resolvedInput: ResolvedUseHeadInput<T>) => void; | ||
remove: () => void; | ||
} | ||
@@ -120,5 +125,3 @@ interface HTMLResult { | ||
declare const createElement: (tag: string, attrs: { | ||
[k: string]: any; | ||
}, document: Document) => HTMLElement; | ||
declare const createElement: (tag: HeadTag, document: Document) => HTMLElement; | ||
@@ -151,16 +154,16 @@ declare const updateElements: (document: Document | undefined, type: string, tags: HeadTag[]) => void; | ||
declare const tagToString: (tag: HeadTag) => string; | ||
declare const renderHeadToString: (head: HeadClient) => HTMLResult; | ||
declare const resolveHeadEntry: (entries: HeadEntry[], force?: boolean) => HeadEntry<{}>[]; | ||
declare const renderHeadToString: <T extends MergeHead = {}>(head: HeadClient<T>) => Promise<HTMLResult>; | ||
declare const sortTags: (aTag: HeadTag, bTag: HeadTag) => number; | ||
declare const tagDedupeKey: <T extends HeadTag>(tag: T) => string | false; | ||
declare function resolveHeadEntry<T extends MergeHead = {}>(obj: HeadEntry<T>): ResolvedHeadEntry; | ||
declare const tagDedupeKey: <T extends HeadTag>(tag: T) => string | number; | ||
declare function resolveUnrefHeadInput<T extends MergeHead = {}>(ref: UseHeadInput<T>): ResolvedUseHeadInput<T>; | ||
declare const headInputToTags: (e: HeadEntry) => HeadTag[]; | ||
declare const resolveHeadEntriesToTags: (entries: HeadEntry[]) => HeadTag[]; | ||
interface HeadClient<T extends MergeHead = {}> { | ||
install: (app: App) => void; | ||
headTags: HeadTag[]; | ||
addHeadObjs: (objs: UseHeadInput<T>, options?: HeadEntryOptions) => () => void; | ||
/** | ||
* @deprecated Use the return function from `addHeadObjs` | ||
*/ | ||
removeHeadObjs: (objs: UseHeadInput<T>) => void; | ||
headEntries: HeadEntry<T>[]; | ||
addEntry: (entry: UseHeadInput<T>, options?: HeadEntryOptions) => HeadObjectApi<T>; | ||
addReactiveEntry: (objs: UseHeadInput<T>, options?: HeadEntryOptions) => () => void; | ||
updateDOM: (document?: Document, force?: boolean) => void; | ||
@@ -181,2 +184,3 @@ /** | ||
} | ||
declare const IS_BROWSER: boolean; | ||
/** | ||
@@ -187,6 +191,6 @@ * Inject the head manager instance | ||
declare const injectHead: <T extends MergeHead = {}>() => HeadClient<T>; | ||
declare const createHead: <T extends MergeHead = {}>(initHeadObject?: UseHeadInput<T> | undefined) => HeadClient<T>; | ||
declare const createHead: <T extends MergeHead = {}>(initHeadObject?: ResolvedUseHeadInput<T> | undefined) => HeadClient<T>; | ||
declare const useHead: <T extends MergeHead = {}>(headObj: UseHeadInput<T>) => void; | ||
declare const useHeadRaw: (headObj: UseHeadRawInput) => void; | ||
export { DomUpdateCtx, HTMLResult, HandlesDuplicates, HasRenderPriority, HasTextContent, Head, HeadAttrs, HeadClient, HeadEntry, HeadEntryOptions, HeadObject, HeadObjectPlain, HeadTag, HookBeforeDomUpdate, HookTagsResolved, Never, RendersToBody, ResolvedHeadEntry, TagKeys, UseHeadInput, UseHeadRawInput, VueUseHeadSchema, createElement, createHead, escapeHtml, escapeJS, injectHead, isEqualNode, renderHeadToString, resolveHeadEntry, setAttrs, sortTags, stringifyAttrName, stringifyAttrValue, stringifyAttrs, tagDedupeKey, tagToString, updateElements, useHead, useHeadRaw }; | ||
export { HTMLResult, HandlesDuplicates, HasRenderPriority, HasTextContent, Head, HeadAttrs, HeadClient, HeadEntry, HeadEntryOptions, HeadObject, HeadObjectApi, HeadObjectPlain, HeadTag, HeadTagRuntime, HookBeforeDomUpdate, HookTagsResolved, IS_BROWSER, Never, RendersToBody, ResolvedUseHeadInput, TagKeys, UseHeadInput, UseHeadRawInput, VueUseHeadSchema, createElement, createHead, escapeHtml, escapeJS, headInputToTags, injectHead, isEqualNode, renderHeadToString, resolveHeadEntriesToTags, resolveHeadEntry, resolveUnrefHeadInput, setAttrs, sortTags, stringifyAttrName, stringifyAttrValue, stringifyAttrs, tagDedupeKey, tagToString, updateElements, useHead, useHeadRaw }; |
@@ -24,2 +24,3 @@ "use strict"; | ||
Head: () => Head, | ||
IS_BROWSER: () => IS_BROWSER, | ||
createElement: () => createElement, | ||
@@ -29,6 +30,9 @@ createHead: () => createHead, | ||
escapeJS: () => escapeJS, | ||
headInputToTags: () => headInputToTags, | ||
injectHead: () => injectHead, | ||
isEqualNode: () => isEqualNode, | ||
renderHeadToString: () => renderHeadToString, | ||
resolveHeadEntriesToTags: () => resolveHeadEntriesToTags, | ||
resolveHeadEntry: () => resolveHeadEntry, | ||
resolveUnrefHeadInput: () => resolveUnrefHeadInput, | ||
setAttrs: () => setAttrs, | ||
@@ -58,6 +62,127 @@ sortTags: () => sortTags, | ||
var import_vue = require("vue"); | ||
// src/ssr/stringify-attrs.ts | ||
var escapeHtml = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); | ||
var escapeJS = (s) => s.replace(/["'\\\n\r\u2028\u2029]/g, (character) => { | ||
switch (character) { | ||
case '"': | ||
case "'": | ||
case "\\": | ||
return `\\${character}`; | ||
case "\n": | ||
return "\\n"; | ||
case "\r": | ||
return "\\r"; | ||
case "\u2028": | ||
return "\\u2028"; | ||
case "\u2029": | ||
return "\\u2029"; | ||
} | ||
return character; | ||
}); | ||
var stringifyAttrName = (str) => str.replace(/[\s"'><\/=]/g, "").replace(/[^a-zA-Z0-9_-]/g, ""); | ||
var stringifyAttrValue = (str) => escapeJS(str.replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">")); | ||
var stringifyAttrs = (attributes, options = {}) => { | ||
const handledAttributes = []; | ||
for (const [key, value] of Object.entries(attributes)) { | ||
if (key === "children" || key === "innerHTML" || key === "textContent" || key === "key") | ||
continue; | ||
if (value === false || value == null) | ||
continue; | ||
let attribute = stringifyAttrName(key); | ||
if (value !== true) { | ||
const val = String(value); | ||
if (options.raw) { | ||
attribute += `="${val}"`; | ||
} else { | ||
if (attribute === "href" || attribute === "src") | ||
attribute += `="${stringifyAttrValue(encodeURI(val))}"`; | ||
else | ||
attribute += `="${stringifyAttrValue(val)}"`; | ||
} | ||
} | ||
handledAttributes.push(attribute); | ||
} | ||
return handledAttributes.length > 0 ? ` ${handledAttributes.join(" ")}` : ""; | ||
}; | ||
// src/ssr/index.ts | ||
var tagToString = (tag) => { | ||
var _a; | ||
const body = tag._runtime.body ? ` ${BODY_TAG_ATTR_NAME}="true"` : ""; | ||
const attrs = stringifyAttrs(tag.props, tag._runtime); | ||
if (SELF_CLOSING_TAGS.includes(tag.tag)) | ||
return `<${tag.tag}${attrs}${body}>`; | ||
let innerContent = ""; | ||
if (((_a = tag._runtime) == null ? void 0 : _a.raw) && tag._runtime.innerHTML) | ||
innerContent = tag._runtime.innerHTML; | ||
if (!innerContent && tag._runtime.textContent) | ||
innerContent = escapeJS(escapeHtml(tag._runtime.textContent)); | ||
if (!innerContent && tag._runtime.children) | ||
innerContent = escapeJS(escapeHtml(tag._runtime.children)); | ||
return `<${tag.tag}${attrs}${body}>${innerContent}</${tag.tag}>`; | ||
}; | ||
var resolveHeadEntry = (entries, force) => { | ||
return entries.map((e) => { | ||
if (e.input && (force || !e.resolved)) | ||
e.input = resolveUnrefHeadInput(e.input); | ||
return e; | ||
}); | ||
}; | ||
var renderHeadToString = async (head) => { | ||
const headHtml = []; | ||
const bodyHtml = []; | ||
let titleHtml = ""; | ||
const attrs = { htmlAttrs: {}, bodyAttrs: {} }; | ||
const resolvedEntries = resolveHeadEntry(head.headEntries, true); | ||
const headTags = resolveHeadEntriesToTags(resolvedEntries); | ||
for (const h in head.hookTagsResolved) | ||
await head.hookTagsResolved[h](headTags); | ||
for (const tag of headTags) { | ||
if (tag.tag === "title") { | ||
titleHtml = tagToString(tag); | ||
} else if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") { | ||
for (const k in tag.props) { | ||
attrs[tag.tag][stringifyAttrName(k)] = stringifyAttrValue(tag.props[k]); | ||
} | ||
} else if (tag._runtime.body) { | ||
bodyHtml.push(tagToString(tag)); | ||
} else { | ||
headHtml.push(tagToString(tag)); | ||
} | ||
} | ||
headHtml.push(`<meta name="${HEAD_COUNT_KEY}" content="${headHtml.length}">`); | ||
return { | ||
get headTags() { | ||
return titleHtml + headHtml.join(""); | ||
}, | ||
get htmlAttrs() { | ||
return stringifyAttrs( | ||
{ | ||
...attrs.htmlAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.htmlAttrs).join(",") | ||
}, | ||
{ raw: true } | ||
); | ||
}, | ||
get bodyAttrs() { | ||
return stringifyAttrs( | ||
{ | ||
...attrs.bodyAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.bodyAttrs).join(",") | ||
}, | ||
{ raw: true } | ||
); | ||
}, | ||
get bodyTags() { | ||
return bodyHtml.join(""); | ||
} | ||
}; | ||
}; | ||
// src/utils.ts | ||
var sortTags = (aTag, bTag) => { | ||
const tagWeight = (tag) => { | ||
if (tag.props.renderPriority) | ||
return tag.props.renderPriority; | ||
if (tag._runtime.renderPriority) | ||
return tag._runtime.renderPriority; | ||
switch (tag.tag) { | ||
@@ -79,5 +204,3 @@ case "base": | ||
var tagDedupeKey = (tag) => { | ||
if (!["meta", "base", "script", "link", "title"].includes(tag.tag)) | ||
return false; | ||
const { props, tag: tagName } = tag; | ||
const { props, tag: tagName, _runtime } = tag; | ||
if (tagName === "base" || tagName === "title") | ||
@@ -89,21 +212,22 @@ return tagName; | ||
return "charset"; | ||
const name = ["key", "id", "name", "property", "http-equiv"]; | ||
if (_runtime.key) | ||
return `${tagName}:${_runtime.key}`; | ||
const name = ["id"]; | ||
if (tagName === "meta") | ||
name.push(...["name", "property", "http-equiv"]); | ||
for (const n of name) { | ||
let value; | ||
if (typeof props.getAttribute === "function" && props.hasAttribute(n)) | ||
value = props.getAttribute(n); | ||
else | ||
value = props[n]; | ||
if (value !== void 0) { | ||
return `${tagName}-${n}-${value}`; | ||
if (typeof props[n] !== "undefined") { | ||
return `${tagName}:${n}:${props[n]}`; | ||
} | ||
} | ||
return false; | ||
return tag._runtime.position; | ||
}; | ||
function resolveUnrefDeeply(ref2) { | ||
function resolveUnrefHeadInput(ref2) { | ||
const root = (0, import_shared.resolveUnref)(ref2); | ||
if (!ref2 || !root) | ||
if (!ref2 || !root) { | ||
return root; | ||
if (Array.isArray(root)) | ||
return root.map(resolveUnrefDeeply); | ||
} | ||
if (Array.isArray(root)) { | ||
return root.map(resolveUnrefHeadInput); | ||
} | ||
if (typeof root === "object") { | ||
@@ -116,3 +240,3 @@ return Object.fromEntries( | ||
key, | ||
resolveUnrefDeeply(value) | ||
resolveUnrefHeadInput(value) | ||
]; | ||
@@ -124,8 +248,84 @@ }) | ||
} | ||
function resolveHeadEntry(obj) { | ||
return { | ||
...obj, | ||
input: resolveUnrefDeeply(obj.input) | ||
var resolveTag = (name, input, e) => { | ||
const tag = { | ||
tag: name, | ||
props: [], | ||
_runtime: { | ||
entryId: e.id, | ||
position: 0 | ||
} | ||
}; | ||
} | ||
["hid", "vmid"].forEach((key) => { | ||
if (input[key]) { | ||
tag._runtime.key = input[key]; | ||
delete input[key]; | ||
} | ||
}); | ||
tag._runtime = { | ||
...tag._runtime, | ||
...e.options | ||
}; | ||
["body", "renderPriority", "key", "children", "innerHTML", "textContent"].forEach((key) => { | ||
if (typeof input[key] !== "undefined") { | ||
tag._runtime[key] = input[key]; | ||
delete input[key]; | ||
} | ||
}); | ||
tag.props = input; | ||
return tag; | ||
}; | ||
var headInputToTags = (e) => { | ||
return Object.entries(e.input).filter(([k, v]) => typeof v !== "undefined" && v !== null && k !== "titleTemplate").map(([key, value]) => { | ||
return (Array.isArray(value) ? value : [value]).map((props) => { | ||
switch (key) { | ||
case "title": | ||
return resolveTag(key, { textContent: props }, e); | ||
case "base": | ||
case "meta": | ||
case "link": | ||
case "style": | ||
case "script": | ||
case "noscript": | ||
case "htmlAttrs": | ||
case "bodyAttrs": | ||
return resolveTag(key, props, e); | ||
default: | ||
return false; | ||
} | ||
}); | ||
}).flat().filter((v) => !!v); | ||
}; | ||
var renderTitleTemplate = (template, title) => { | ||
if (template == null) | ||
return ""; | ||
if (typeof template === "function") | ||
return template(title); | ||
return template.replace("%s", title ?? ""); | ||
}; | ||
var resolveHeadEntriesToTags = (entries) => { | ||
const deduping = {}; | ||
const resolvedEntries = resolveHeadEntry(entries); | ||
const titleTemplate = resolvedEntries.map((i) => i.input.titleTemplate).reverse().find((i) => i != null); | ||
resolvedEntries.forEach((entry, entryIndex) => { | ||
const tags = headInputToTags(entry); | ||
tags.forEach((tag, tagIdx) => { | ||
var _a; | ||
tag._runtime.position = entryIndex * 1e4 + tagIdx; | ||
if (titleTemplate && tag.tag === "title") { | ||
tag._runtime.textContent = renderTitleTemplate( | ||
titleTemplate, | ||
tag._runtime.textContent | ||
); | ||
} | ||
if (!((_a = tag._runtime) == null ? void 0 : _a.raw)) { | ||
for (const k in tag.props) { | ||
if (k.startsWith("on") || k === "innerHTML") | ||
delete tag.props[k]; | ||
} | ||
} | ||
deduping[tagDedupeKey(tag)] = tag; | ||
}); | ||
}); | ||
return Object.values(deduping).sort((a, b) => a._runtime.position - b._runtime.position).sort(sortTags); | ||
}; | ||
@@ -171,20 +371,16 @@ // src/dom/utils.ts | ||
// src/dom/create-element.ts | ||
var createElement = (tag, attrs, document) => { | ||
const el = document.createElement(tag); | ||
for (const key of Object.keys(attrs)) { | ||
if (key === "body" && attrs.body === true) { | ||
el.setAttribute(BODY_TAG_ATTR_NAME, "true"); | ||
} else { | ||
const value = attrs[key]; | ||
if (key === "renderPriority" || key === "key" || value === false) | ||
continue; | ||
if (key === "children" || key === "textContent") | ||
el.textContent = value; | ||
else if (key === "innerHTML") | ||
el.innerHTML = value; | ||
else | ||
el.setAttribute(key, value); | ||
} | ||
var createElement = (tag, document) => { | ||
const $el = document.createElement(tag.tag); | ||
Object.entries(tag.props).forEach(([k, v]) => { | ||
if (v !== false) | ||
$el.setAttribute(k, v); | ||
}); | ||
if (tag._runtime.body === true) { | ||
$el.setAttribute(BODY_TAG_ATTR_NAME, "true"); | ||
} | ||
return el; | ||
if (tag._runtime.raw && tag._runtime.innerHTML) | ||
$el.innerHTML = tag._runtime.innerHTML; | ||
else | ||
$el.textContent = tag._runtime.textContent || tag._runtime.children || ""; | ||
return $el; | ||
}; | ||
@@ -220,4 +416,4 @@ | ||
let newElements = tags.map((tag) => ({ | ||
element: createElement(tag.tag, tag.props, document), | ||
body: tag.props.body ?? false | ||
element: createElement(tag, document), | ||
body: tag._runtime.body ?? false | ||
})); | ||
@@ -262,14 +458,33 @@ newElements = newElements.filter((newEl) => { | ||
// src/dom/update-dom.ts | ||
var updateDOM = ({ domCtx, document, previousTags }) => { | ||
var updateDOM = async (head, previousTags, document) => { | ||
const tags = {}; | ||
if (!document) | ||
document = window.document; | ||
if (domCtx.title !== void 0) | ||
document.title = domCtx.title; | ||
setAttrs(document.documentElement, domCtx.htmlAttrs); | ||
setAttrs(document.body, domCtx.bodyAttrs); | ||
const tags = /* @__PURE__ */ new Set([...Object.keys(domCtx.actualTags), ...previousTags]); | ||
for (const tag of tags) | ||
updateElements(document, tag, domCtx.actualTags[tag] || []); | ||
const headTags = resolveHeadEntriesToTags(head.headEntries); | ||
for (const h in head.hookTagsResolved) | ||
await head.hookTagsResolved[h](headTags); | ||
for (const k in head.hookBeforeDomUpdate) { | ||
if (await head.hookBeforeDomUpdate[k](headTags) === false) | ||
return; | ||
} | ||
for (const tag of headTags) { | ||
switch (tag.tag) { | ||
case "title": | ||
if (typeof tag._runtime.textContent !== "undefined") | ||
document.title = tag._runtime.textContent; | ||
break; | ||
case "htmlAttrs": | ||
case "bodyAttrs": | ||
setAttrs(document[tag.tag === "htmlAttrs" ? "documentElement" : "body"], tag.props); | ||
break; | ||
default: | ||
tags[tag.tag] = tags[tag.tag] || []; | ||
tags[tag.tag].push(tag); | ||
} | ||
} | ||
const tagKeys = /* @__PURE__ */ new Set([...Object.keys(tags), ...previousTags]); | ||
for (const tag of tagKeys) | ||
updateElements(document, tag, tags[tag] || []); | ||
previousTags.clear(); | ||
Object.keys(domCtx.actualTags).forEach((i) => previousTags.add(i)); | ||
Object.keys(tags).forEach((i) => previousTags.add(i)); | ||
}; | ||
@@ -283,10 +498,11 @@ | ||
return; | ||
const props = { | ||
...node.props, | ||
children: Array.isArray(node.children) ? node.children[0].children : node.children | ||
}; | ||
const props = node.props || {}; | ||
if (node.children) { | ||
const k = type === "script" ? "innerHTML" : "textContent"; | ||
props[k] = Array.isArray(node.children) ? node.children[0].children : node.children; | ||
} | ||
if (Array.isArray(obj[type])) | ||
obj[type].push(props); | ||
else if (type === "title") | ||
obj.title = props.children; | ||
obj.title = props.textContent; | ||
else | ||
@@ -321,9 +537,11 @@ obj[type] = props; | ||
const head = injectHead(); | ||
let obj; | ||
(0, import_vue2.onBeforeUnmount)(() => { | ||
if (obj) { | ||
head.removeHeadObjs(obj); | ||
head.updateDOM(); | ||
} | ||
}); | ||
const obj = (0, import_vue2.ref)({}); | ||
if (IS_BROWSER) { | ||
const cleanUp = head.addReactiveEntry(obj, { raw: true }); | ||
(0, import_vue2.onBeforeUnmount)(() => { | ||
cleanUp(); | ||
}); | ||
} else { | ||
head.addEntry(obj, { raw: true }); | ||
} | ||
return () => { | ||
@@ -333,8 +551,3 @@ (0, import_vue2.watchEffect)(() => { | ||
return; | ||
if (obj) | ||
head.removeHeadObjs(obj); | ||
obj = (0, import_vue2.ref)(vnodesToHeadObj(slots.default())); | ||
head.addHeadObjs(obj); | ||
if (typeof window !== "undefined") | ||
head.updateDOM(); | ||
obj.value = vnodesToHeadObj(slots.default()); | ||
}); | ||
@@ -346,120 +559,4 @@ return null; | ||
// src/ssr/stringify-attrs.ts | ||
var escapeHtml = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); | ||
var escapeJS = (s) => s.replace(/["'\\\n\r\u2028\u2029]/g, (character) => { | ||
switch (character) { | ||
case '"': | ||
case "'": | ||
case "\\": | ||
return `\\${character}`; | ||
case "\n": | ||
return "\\n"; | ||
case "\r": | ||
return "\\r"; | ||
case "\u2028": | ||
return "\\u2028"; | ||
case "\u2029": | ||
return "\\u2029"; | ||
} | ||
return character; | ||
}); | ||
var stringifyAttrName = (str) => str.replace(/[\s"'><\/=]/g, "").replace(/[^a-zA-Z0-9_-]/g, ""); | ||
var stringifyAttrValue = (str) => escapeJS(str.replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">")); | ||
var stringifyAttrs = (attributes, options = {}) => { | ||
const handledAttributes = []; | ||
for (const [key, value] of Object.entries(attributes)) { | ||
if (key === "children" || key === "innerHTML" || key === "textContent" || key === "key") | ||
continue; | ||
if (value === false || value == null) | ||
continue; | ||
let attribute = stringifyAttrName(key); | ||
if (value !== true) { | ||
const val = String(value); | ||
if (options.raw) { | ||
attribute += `="${val}"`; | ||
} else { | ||
if (attribute === "href" || attribute === "src") | ||
attribute += `="${stringifyAttrValue(encodeURI(val))}"`; | ||
else | ||
attribute += `="${stringifyAttrValue(val)}"`; | ||
} | ||
} | ||
handledAttributes.push(attribute); | ||
} | ||
return handledAttributes.length > 0 ? ` ${handledAttributes.join(" ")}` : ""; | ||
}; | ||
// src/ssr/index.ts | ||
var tagToString = (tag) => { | ||
var _a; | ||
let isBodyTag = false; | ||
if (tag.props.body) { | ||
isBodyTag = true; | ||
delete tag.props.body; | ||
} | ||
if (tag.props.renderPriority) | ||
delete tag.props.renderPriority; | ||
const attrs = stringifyAttrs(tag.props, tag._options); | ||
if (SELF_CLOSING_TAGS.includes(tag.tag)) { | ||
return `<${tag.tag}${attrs}${isBodyTag ? ` ${BODY_TAG_ATTR_NAME}="true"` : ""}>`; | ||
} | ||
let innerContent = ""; | ||
if (((_a = tag._options) == null ? void 0 : _a.raw) && tag.props.innerHTML) | ||
innerContent = tag.props.innerHTML; | ||
if (!innerContent && tag.props.textContent) | ||
innerContent = escapeJS(escapeHtml(tag.props.textContent)); | ||
if (!innerContent && tag.props.children) | ||
innerContent = escapeJS(escapeHtml(tag.props.children)); | ||
return `<${tag.tag}${attrs}${isBodyTag ? ` ${BODY_TAG_ATTR_NAME}="true"` : ""}>${innerContent}</${tag.tag}>`; | ||
}; | ||
var renderHeadToString = (head) => { | ||
var _a; | ||
const tags = []; | ||
const bodyTags = []; | ||
let titleTag = ""; | ||
const attrs = { htmlAttrs: {}, bodyAttrs: {} }; | ||
for (const tag of head.headTags.sort(sortTags)) { | ||
if (tag.tag === "title") { | ||
titleTag = tagToString(tag); | ||
} else if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") { | ||
for (const k in tag.props) { | ||
const keyName = stringifyAttrName(k); | ||
attrs[tag.tag][keyName] = ((_a = tag._options) == null ? void 0 : _a.raw) ? tag.props[keyName] : tag.props[stringifyAttrValue(keyName)]; | ||
} | ||
} else if (tag.props.body) { | ||
bodyTags.push(tagToString(tag)); | ||
} else { | ||
tags.push(tagToString(tag)); | ||
} | ||
} | ||
tags.push(`<meta name="${HEAD_COUNT_KEY}" content="${tags.length}">`); | ||
return { | ||
get headTags() { | ||
return titleTag + tags.join(""); | ||
}, | ||
get htmlAttrs() { | ||
return stringifyAttrs( | ||
{ | ||
...attrs.htmlAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.htmlAttrs).join(",") | ||
}, | ||
{ raw: true } | ||
); | ||
}, | ||
get bodyAttrs() { | ||
return stringifyAttrs( | ||
{ | ||
...attrs.bodyAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.bodyAttrs).join(",") | ||
}, | ||
{ raw: true } | ||
); | ||
}, | ||
get bodyTags() { | ||
return bodyTags.join(""); | ||
} | ||
}; | ||
}; | ||
// src/index.ts | ||
var IS_BROWSER = typeof window !== "undefined"; | ||
var injectHead = () => { | ||
@@ -471,73 +568,9 @@ const head = (0, import_vue3.inject)(PROVIDE_KEY); | ||
}; | ||
var acceptFields = [ | ||
"title", | ||
"meta", | ||
"link", | ||
"base", | ||
"style", | ||
"script", | ||
"noscript", | ||
"htmlAttrs", | ||
"bodyAttrs" | ||
]; | ||
var renderTitleTemplate = (template, title) => { | ||
if (template == null) | ||
return ""; | ||
if (typeof template === "function") | ||
return template(title); | ||
return template.replace("%s", title ?? ""); | ||
}; | ||
var headObjToTags = (obj) => { | ||
const tags = []; | ||
const keys = Object.keys(obj); | ||
const convertLegacyKey = (value) => { | ||
if (value.hid) { | ||
value.key = value.hid; | ||
delete value.hid; | ||
} | ||
if (value.vmid) { | ||
value.key = value.vmid; | ||
delete value.vmid; | ||
} | ||
return value; | ||
}; | ||
for (const key of keys) { | ||
if (obj[key] == null) | ||
continue; | ||
switch (key) { | ||
case "title": | ||
tags.push({ tag: key, props: { textContent: obj[key] } }); | ||
break; | ||
case "titleTemplate": | ||
break; | ||
case "base": | ||
tags.push({ tag: key, props: { key: "default", ...obj[key] } }); | ||
break; | ||
default: | ||
if (acceptFields.includes(key)) { | ||
const value = obj[key]; | ||
if (Array.isArray(value)) { | ||
value.forEach((item) => { | ||
const props = convertLegacyKey(item); | ||
tags.push({ tag: key, props }); | ||
}); | ||
} else if (value) { | ||
tags.push({ tag: key, props: convertLegacyKey(value) }); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
return tags; | ||
}; | ||
var createHead = (initHeadObject) => { | ||
let allHeadObjs = []; | ||
let entries = []; | ||
let entryId = 0; | ||
const previousTags = /* @__PURE__ */ new Set(); | ||
let headObjId = 0; | ||
const hookBeforeDomUpdate = []; | ||
const hookTagsResolved = []; | ||
if (initHeadObject) | ||
allHeadObjs.push({ input: initHeadObject }); | ||
let domUpdateTick = null; | ||
let domCtx; | ||
const head = { | ||
@@ -550,102 +583,74 @@ install(app) { | ||
hookTagsResolved, | ||
get headTags() { | ||
const deduped = []; | ||
const deduping = {}; | ||
const resolvedHeadObjs = allHeadObjs.map(resolveHeadEntry); | ||
const titleTemplate = resolvedHeadObjs.map((i) => i.input.titleTemplate).reverse().find((i) => i != null); | ||
resolvedHeadObjs.forEach((objs, headObjectIdx) => { | ||
const tags2 = headObjToTags(objs.input); | ||
tags2.forEach((tag, tagIdx) => { | ||
var _a; | ||
tag._position = headObjectIdx * 1e4 + tagIdx; | ||
if (tag._options) | ||
delete tag._options; | ||
if (objs.options) | ||
tag._options = objs.options; | ||
if (titleTemplate && tag.tag === "title") { | ||
tag.props.textContent = renderTitleTemplate( | ||
titleTemplate, | ||
tag.props.textContent | ||
); | ||
} | ||
if (!((_a = tag._options) == null ? void 0 : _a.raw)) { | ||
for (const k in tag.props) { | ||
if (k.startsWith("on")) { | ||
console.warn("[@vueuse/head] Warning, you must use `useHeadRaw` to set event listeners. See https://github.com/vueuse/head/pull/118", tag); | ||
delete tag.props[k]; | ||
} | ||
} | ||
if (tag.props.innerHTML) { | ||
console.warn("[@vueuse/head] Warning, you must use `useHeadRaw` to use `innerHTML`", tag); | ||
delete tag.props.innerHTML; | ||
} | ||
} | ||
const dedupeKey = tagDedupeKey(tag); | ||
if (dedupeKey) | ||
deduping[dedupeKey] = tag; | ||
else | ||
deduped.push(tag); | ||
}); | ||
}); | ||
deduped.push(...Object.values(deduping)); | ||
const tags = deduped.sort((a, b) => a._position - b._position); | ||
head.hookTagsResolved.forEach((fn) => fn(tags)); | ||
return tags; | ||
get headEntries() { | ||
return entries; | ||
}, | ||
addHeadObjs(objs, options) { | ||
const entry = { input: objs, options, id: headObjId++ }; | ||
allHeadObjs.push(entry); | ||
return () => { | ||
allHeadObjs = allHeadObjs.filter((_objs) => _objs.id !== entry.id); | ||
addEntry(input, options = {}) { | ||
let resolved = false; | ||
if (options == null ? void 0 : options.resolved) { | ||
resolved = true; | ||
delete options.resolved; | ||
} | ||
const entry = { | ||
id: entryId++, | ||
options, | ||
resolved, | ||
input | ||
}; | ||
entries.push(entry); | ||
return { | ||
remove() { | ||
entries = entries.filter((_objs) => _objs.id !== entry.id); | ||
}, | ||
update(updatedInput) { | ||
entries = entries.map((e) => { | ||
if (e.id === entry.id) | ||
e.input = updatedInput; | ||
return e; | ||
}); | ||
} | ||
}; | ||
}, | ||
removeHeadObjs(objs) { | ||
allHeadObjs = allHeadObjs.filter((_objs) => _objs.input !== objs); | ||
}, | ||
updateDOM: (document, force) => { | ||
domCtx = { | ||
title: void 0, | ||
htmlAttrs: {}, | ||
bodyAttrs: {}, | ||
actualTags: {} | ||
}; | ||
for (const tag of head.headTags.sort(sortTags)) { | ||
if (tag.tag === "title") { | ||
domCtx.title = tag.props.textContent; | ||
continue; | ||
} | ||
if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") { | ||
Object.assign(domCtx[tag.tag], tag.props); | ||
continue; | ||
} | ||
domCtx.actualTags[tag.tag] = domCtx.actualTags[tag.tag] || []; | ||
domCtx.actualTags[tag.tag].push(tag); | ||
} | ||
async updateDOM(document, force) { | ||
const doDomUpdate = () => { | ||
domUpdateTick = null; | ||
for (const k in head.hookBeforeDomUpdate) { | ||
if (head.hookBeforeDomUpdate[k](domCtx.actualTags) === false) | ||
return; | ||
return updateDOM(head, previousTags, document); | ||
}; | ||
if (force) | ||
return doDomUpdate(); | ||
return domUpdateTick = domUpdateTick || new Promise((resolve) => (0, import_vue3.nextTick)(() => resolve(doDomUpdate()))); | ||
}, | ||
addReactiveEntry(input, options = {}) { | ||
let entrySideEffect = null; | ||
const cleanUpWatch = (0, import_vue3.watchEffect)(() => { | ||
const resolvedInput = resolveUnrefHeadInput(input); | ||
if (entrySideEffect === null) { | ||
entrySideEffect = head.addEntry( | ||
resolvedInput, | ||
{ ...options, resolved: true } | ||
); | ||
} else { | ||
entrySideEffect.update(resolvedInput); | ||
} | ||
updateDOM({ domCtx, document, previousTags }); | ||
if (IS_BROWSER) | ||
head.updateDOM(); | ||
}); | ||
return () => { | ||
cleanUpWatch(); | ||
if (entrySideEffect) | ||
entrySideEffect.remove(); | ||
}; | ||
if (force) { | ||
doDomUpdate(); | ||
return; | ||
} | ||
domUpdateTick = domUpdateTick || (0, import_vue3.nextTick)(() => doDomUpdate()); | ||
} | ||
}; | ||
if (initHeadObject) | ||
head.addEntry(initHeadObject); | ||
return head; | ||
}; | ||
var IS_BROWSER = typeof window !== "undefined"; | ||
var _useHead = (headObj, options = {}) => { | ||
const head = injectHead(); | ||
const removeHeadObjs = head.addHeadObjs(headObj, options); | ||
if (IS_BROWSER) { | ||
(0, import_vue3.watchEffect)(() => { | ||
head.updateDOM(); | ||
}); | ||
if (!IS_BROWSER) { | ||
head.addEntry(headObj, options); | ||
} else { | ||
const cleanUp = head.addReactiveEntry(headObj, options); | ||
(0, import_vue3.onBeforeUnmount)(() => { | ||
removeHeadObjs(); | ||
cleanUp(); | ||
head.updateDOM(); | ||
@@ -664,2 +669,3 @@ }); | ||
Head, | ||
IS_BROWSER, | ||
createElement, | ||
@@ -669,6 +675,9 @@ createHead, | ||
escapeJS, | ||
headInputToTags, | ||
injectHead, | ||
isEqualNode, | ||
renderHeadToString, | ||
resolveHeadEntriesToTags, | ||
resolveHeadEntry, | ||
resolveUnrefHeadInput, | ||
setAttrs, | ||
@@ -675,0 +684,0 @@ sortTags, |
{ | ||
"name": "@vueuse/head", | ||
"version": "0.9.7", | ||
"version": "1.0.0-rc.1", | ||
"packageManager": "pnpm@7.5.0", | ||
@@ -58,3 +58,3 @@ "description": "Document head manager for Vue 3. SSR ready.", | ||
"cheerio": "1.0.0-rc.12", | ||
"eslint": "^8.24.0", | ||
"eslint": "^8.25.0", | ||
"execa": "^6.1.0", | ||
@@ -67,9 +67,9 @@ "get-port-please": "^2.6.1", | ||
"nuxt": "3.0.0-rc.11", | ||
"pathe": "^0.3.8", | ||
"playwright": "^1.26.1", | ||
"pathe": "^0.3.9", | ||
"playwright": "^1.27.0", | ||
"simple-git-hooks": "^2.8.0", | ||
"tsup": "^6.2.3", | ||
"typescript": "^4.8.4", | ||
"vite": "^3.1.4", | ||
"vitest": "^0.23.4", | ||
"vite": "^3.1.6", | ||
"vitest": "^0.24.0", | ||
"vue": "^3.2.40", | ||
@@ -76,0 +76,0 @@ "vue-router": "^4.1.5" |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
58134
1464