@vueuse/head
Advanced tools
Comparing version 1.0.0-rc.4 to 1.0.0-rc.5
@@ -6,3 +6,2 @@ import * as vue from 'vue'; | ||
import { MaybeComputedRef } from '@vueuse/shared'; | ||
import { RawHeadAugmentation } from '@zhead/schema-raw'; | ||
@@ -34,4 +33,2 @@ interface HandlesDuplicates { | ||
* | ||
* @deprecated This can only be used with `useHeadRaw`. | ||
* | ||
* Alias for children | ||
@@ -82,6 +79,14 @@ */ | ||
declare type ResolvedUseHeadInput<T extends MergeHead = {}> = Head$1<T & VueUseHeadSchema>; | ||
declare type UseHeadRawInput = MaybeComputedRef<ReactiveHead<RawHeadAugmentation & VueUseHeadSchema>>; | ||
interface HeadAttrs { | ||
[k: string]: any; | ||
} | ||
declare type HookBeforeDomUpdate = (() => Promise<void | boolean> | void | boolean); | ||
declare type HookTagsResolved = ((tags: HeadTag[]) => Promise<void> | void); | ||
declare type HookEntriesResolved = ((entries: HeadEntry[]) => Promise<void> | void); | ||
declare type HeadTagOptions = HeadEntryOptions & HandlesDuplicates & HasRenderPriority & RendersToBody & HasTextContent; | ||
interface HeadEntryOptions { | ||
raw?: boolean; | ||
safe?: boolean; | ||
resolved?: boolean; | ||
beforeTagRender?: (tag: HeadTag) => void; | ||
} | ||
@@ -96,10 +101,2 @@ interface HeadEntry<T extends MergeHead = {}> { | ||
declare type TagKeys = keyof Omit<HeadObjectPlain, 'titleTemplate'>; | ||
interface HeadAttrs { | ||
[k: string]: any; | ||
} | ||
declare type HookBeforeDomUpdate = (() => Promise<void | boolean> | void | boolean); | ||
declare type HookTagsResolved = ((tags: HeadTag[]) => Promise<void> | void); | ||
declare type HookEntriesResolved = ((entries: HeadEntry[]) => Promise<void> | void); | ||
declare type HeadTagOptions = HeadEntryOptions & HandlesDuplicates & HasRenderPriority & RendersToBody & HasTextContent; | ||
interface HeadTag { | ||
@@ -112,2 +109,3 @@ tag: TagKeys; | ||
entryId?: number; | ||
beforeTagRender?: (tag: HeadTag) => void; | ||
}; | ||
@@ -136,22 +134,3 @@ options?: HeadTagOptions; | ||
declare const escapeHtml: (s: string) => string; | ||
declare const escapeJS: (s: string) => string; | ||
/** | ||
* Attribute names must consist of one or more characters other than controls, U+0020 SPACE, U+0022 ("), U+0027 ('), | ||
* U+003E (>), U+002F (/), U+003D (=), and noncharacters. | ||
* | ||
* We strip them for the attribute name as they shouldn't exist even if encoded. | ||
* | ||
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 | ||
*/ | ||
declare const stringifyAttrName: (str: string) => string; | ||
/** | ||
* Double-quoted attribute value must not contain any literal U+0022 QUOTATION MARK characters ("). Including | ||
* < and > will cause HTML to be invalid. | ||
* | ||
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 | ||
*/ | ||
declare const stringifyAttrValue: (str: string) => string; | ||
declare const stringifyAttrs: (attributes: Record<string, any>, options?: HeadEntryOptions) => string; | ||
declare const propsToString: (props: HeadTag['props']) => string; | ||
declare const tagToString: (tag: HeadTag) => string; | ||
@@ -170,10 +149,2 @@ declare const resolveHeadEntries: (entries: HeadEntry[], force?: boolean) => HeadEntry<{}>[]; | ||
headEntries: HeadEntry<T>[]; | ||
/** | ||
* Backwards compatibility function to fetch the headTags. | ||
* | ||
* This function forces reactivity resolving and is not performant. | ||
* | ||
* @deprecated Use hooks. | ||
*/ | ||
headTags: HeadTag[]; | ||
addEntry: (entry: UseHeadInput<T>, options?: HeadEntryOptions) => HeadObjectApi<T>; | ||
@@ -196,2 +167,18 @@ addReactiveEntry: (objs: UseHeadInput<T>, options?: HeadEntryOptions) => () => void; | ||
Record<'resolved:tags', HookTagsResolved[]>; | ||
/** | ||
* Backwards compatibility function to fetch the headTags. | ||
* | ||
* This function forces reactivity resolving and is not performant. | ||
* | ||
* @deprecated Use hooks. | ||
*/ | ||
headTags: HeadTag[]; | ||
/** | ||
* Backwards compatibility function to add a head obj. | ||
* | ||
* Note: This will not support reactivity. Use `addReactiveEntry` instead. | ||
* | ||
* @deprecated Use addEntry | ||
*/ | ||
addHeadObjs: (entry: UseHeadInput<T>, options?: HeadEntryOptions) => HeadObjectApi<T>; | ||
} | ||
@@ -206,4 +193,4 @@ declare const IS_BROWSER: boolean; | ||
declare const useHead: <T extends MergeHead = {}>(headObj: UseHeadInput<T>) => void; | ||
declare const useHeadRaw: (headObj: UseHeadRawInput) => void; | ||
declare const useHeadSafe: <T extends MergeHead = {}>(headObj: UseHeadInput<T>) => void; | ||
export { HTMLResult, HandlesDuplicates, HasRenderPriority, HasTextContent, Head, HeadAttrs, HeadClient, HeadEntry, HeadEntryOptions, HeadObject, HeadObjectApi, HeadObjectPlain, HeadTag, HeadTagOptions, HookBeforeDomUpdate, HookEntriesResolved, HookTagsResolved, IS_BROWSER, Never, RendersToBody, ResolvedUseHeadInput, TagKeys, UseHeadInput, UseHeadRawInput, VueUseHeadSchema, createElement, createHead, escapeHtml, escapeJS, headInputToTags, injectHead, isEqualNode, renderHeadToString, resolveHeadEntries, resolveHeadEntriesToTags, resolveUnrefHeadInput, setAttrs, sortTags, stringifyAttrName, stringifyAttrValue, stringifyAttrs, tagDedupeKey, tagToString, updateElements, useHead, useHeadRaw }; | ||
export { HTMLResult, HandlesDuplicates, HasRenderPriority, HasTextContent, Head, HeadAttrs, HeadClient, HeadEntry, HeadEntryOptions, HeadObject, HeadObjectApi, HeadObjectPlain, HeadTag, HeadTagOptions, HookBeforeDomUpdate, HookEntriesResolved, HookTagsResolved, IS_BROWSER, Never, RendersToBody, ResolvedUseHeadInput, TagKeys, UseHeadInput, VueUseHeadSchema, createElement, createHead, headInputToTags, injectHead, isEqualNode, propsToString, renderHeadToString, resolveHeadEntries, resolveHeadEntriesToTags, resolveUnrefHeadInput, setAttrs, sortTags, tagDedupeKey, tagToString, updateElements, useHead, useHeadSafe }; |
@@ -27,7 +27,6 @@ "use strict"; | ||
createHead: () => createHead, | ||
escapeHtml: () => escapeHtml, | ||
escapeJS: () => escapeJS, | ||
headInputToTags: () => headInputToTags, | ||
injectHead: () => injectHead, | ||
isEqualNode: () => isEqualNode, | ||
propsToString: () => propsToString, | ||
renderHeadToString: () => renderHeadToString, | ||
@@ -39,5 +38,2 @@ resolveHeadEntries: () => resolveHeadEntries, | ||
sortTags: () => sortTags, | ||
stringifyAttrName: () => stringifyAttrName, | ||
stringifyAttrValue: () => stringifyAttrValue, | ||
stringifyAttrs: () => stringifyAttrs, | ||
tagDedupeKey: () => tagDedupeKey, | ||
@@ -47,3 +43,3 @@ tagToString: () => tagToString, | ||
useHead: () => useHead, | ||
useHeadRaw: () => useHeadRaw | ||
useHeadSafe: () => useHeadSafe | ||
}); | ||
@@ -64,3 +60,3 @@ module.exports = __toCommonJS(src_exports); | ||
// src/ssr/stringify-attrs.ts | ||
// src/encoding/index.ts | ||
var escapeHtml = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); | ||
@@ -84,23 +80,16 @@ var escapeJS = (s) => s.replace(/["'\\\n\r\u2028\u2029]/g, (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 = {}) => { | ||
var sanitiseAttrName = (str) => str.replace(/[\s"'><\/=]/g, "").replace(/[^a-zA-Z0-9_-]/g, ""); | ||
var sanitiseAttrValue = (str) => escapeJS( | ||
str.replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">") | ||
); | ||
// src/ssr/index.ts | ||
var propsToString = (props) => { | ||
const handledAttributes = []; | ||
for (const [key, value] of Object.entries(attributes)) { | ||
if (key === "children" || key === "innerHTML" || key === "textContent" || key === "key") | ||
continue; | ||
for (const [key, value] of Object.entries(props)) { | ||
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)}"`; | ||
} | ||
} | ||
let attribute = key; | ||
if (value !== true) | ||
attribute += `="${String(value).replace(/"/g, """)}"`; | ||
handledAttributes.push(attribute); | ||
@@ -110,13 +99,6 @@ } | ||
}; | ||
// src/ssr/index.ts | ||
var tagToString = (tag) => { | ||
var _a, _b; | ||
const body = ((_a = tag.options) == null ? void 0 : _a.body) ? ` ${BODY_TAG_ATTR_NAME}="true"` : ""; | ||
const attrs = stringifyAttrs(tag.props, tag.options); | ||
if (SELF_CLOSING_TAGS.includes(tag.tag)) | ||
return `<${tag.tag}${attrs}${body}>`; | ||
let children = tag.children || ""; | ||
children = ((_b = tag.options) == null ? void 0 : _b.raw) ? children : escapeJS(escapeHtml(children)); | ||
return `<${tag.tag}${attrs}${body}>${children}</${tag.tag}>`; | ||
const attrs = propsToString(tag.props); | ||
const openTag = `<${tag.tag}${attrs}>`; | ||
return SELF_CLOSING_TAGS.includes(tag.tag) ? openTag : `${openTag}${tag.children || ""}</${tag.tag}>`; | ||
}; | ||
@@ -131,3 +113,3 @@ var resolveHeadEntries = (entries, force) => { | ||
var renderHeadToString = async (head) => { | ||
var _a; | ||
var _a, _b; | ||
const headHtml = []; | ||
@@ -144,2 +126,4 @@ const bodyHtml = []; | ||
for (const tag of headTags) { | ||
if ((_a = tag.options) == null ? void 0 : _a.beforeTagRender) | ||
tag.options.beforeTagRender(tag); | ||
if (tag.tag === "title") { | ||
@@ -149,5 +133,5 @@ titleHtml = tagToString(tag); | ||
for (const k in tag.props) { | ||
attrs[tag.tag][stringifyAttrName(k)] = stringifyAttrValue(tag.props[k]); | ||
attrs[tag.tag][sanitiseAttrName(k)] = sanitiseAttrValue(tag.props[k]); | ||
} | ||
} else if ((_a = tag.options) == null ? void 0 : _a.body) { | ||
} else if ((_b = tag.options) == null ? void 0 : _b.body) { | ||
bodyHtml.push(tagToString(tag)); | ||
@@ -164,18 +148,12 @@ } else { | ||
get htmlAttrs() { | ||
return stringifyAttrs( | ||
{ | ||
...attrs.htmlAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.htmlAttrs).join(",") | ||
}, | ||
{ raw: true } | ||
); | ||
return propsToString({ | ||
...attrs.htmlAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.htmlAttrs).join(",") | ||
}); | ||
}, | ||
get bodyAttrs() { | ||
return stringifyAttrs( | ||
{ | ||
...attrs.bodyAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.bodyAttrs).join(",") | ||
}, | ||
{ raw: true } | ||
); | ||
return propsToString({ | ||
...attrs.bodyAttrs, | ||
[HEAD_ATTRS_KEY]: Object.keys(attrs.bodyAttrs).join(",") | ||
}); | ||
}, | ||
@@ -252,2 +230,3 @@ get bodyTags() { | ||
var resolveTag = (name, input, e) => { | ||
var _a; | ||
const tag = { | ||
@@ -282,2 +261,4 @@ tag: name, | ||
}); | ||
if ((_a = tag.options) == null ? void 0 : _a.body) | ||
input[BODY_TAG_ATTR_NAME] = true; | ||
tag.props = input; | ||
@@ -321,3 +302,2 @@ return tag; | ||
tags.forEach((tag, tagIdx) => { | ||
var _a; | ||
tag.runtime = tag.runtime || {}; | ||
@@ -331,8 +311,2 @@ tag.runtime.position = entryIndex * 1e4 + tagIdx; | ||
} | ||
if (!((_a = tag.options) == 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; | ||
@@ -384,3 +358,3 @@ }); | ||
var createElement = (tag, document) => { | ||
var _a, _b; | ||
var _a; | ||
const $el = document.createElement(tag.tag); | ||
@@ -391,7 +365,10 @@ Object.entries(tag.props).forEach(([k, v]) => { | ||
}); | ||
if ((_a = tag.options) == null ? void 0 : _a.body) { | ||
$el.setAttribute(BODY_TAG_ATTR_NAME, "true"); | ||
if (tag.children) { | ||
if ((_a = tag.options) == null ? void 0 : _a.safe) { | ||
if (tag.tag !== "script") | ||
$el.textContent = tag.children; | ||
} else { | ||
$el.innerHTML = tag.children; | ||
} | ||
} | ||
if (tag.children) | ||
$el[((_b = tag.options) == null ? void 0 : _b.raw) ? "innerHTML" : "textContent"] = tag.children; | ||
return $el; | ||
@@ -460,3 +437,3 @@ }; | ||
newElements.forEach((t) => { | ||
if (t.body === true) | ||
if (t.body) | ||
body.insertAdjacentElement("beforeend", t.element); | ||
@@ -559,3 +536,3 @@ else | ||
if (IS_BROWSER) { | ||
const cleanUp = head.addReactiveEntry(obj, { raw: true }); | ||
const cleanUp = head.addReactiveEntry(obj); | ||
(0, import_vue2.onBeforeUnmount)(() => { | ||
@@ -565,3 +542,3 @@ cleanUp(); | ||
} else { | ||
head.addEntry(obj, { raw: true }); | ||
head.addEntry(obj); | ||
} | ||
@@ -609,2 +586,5 @@ return () => { | ||
}, | ||
addHeadObjs(input, options) { | ||
return head.addEntry(input, options); | ||
}, | ||
addEntry(input, options = {}) { | ||
@@ -686,4 +666,26 @@ let resolved = false; | ||
}; | ||
var useHeadRaw = (headObj) => { | ||
_useHead(headObj, { raw: true }); | ||
var useHeadSafe = (headObj) => { | ||
_useHead( | ||
headObj, | ||
{ | ||
beforeTagRender: (tag) => { | ||
for (const p in tag.props) { | ||
const value = tag.props[p]; | ||
const key = sanitiseAttrName(p); | ||
delete tag.props[p]; | ||
if (!p.startsWith("on") && p !== "innerHTML") { | ||
if (p === "href" || p === "src") | ||
tag.props[key] = encodeURI(value); | ||
tag.props[key] = sanitiseAttrValue(value); | ||
} | ||
} | ||
if (tag.children) { | ||
if (tag.tag === "script") | ||
delete tag.children; | ||
else | ||
tag.children = escapeJS(escapeHtml(tag.children)); | ||
} | ||
} | ||
} | ||
); | ||
}; | ||
@@ -696,7 +698,6 @@ // Annotate the CommonJS export names for ESM import in node: | ||
createHead, | ||
escapeHtml, | ||
escapeJS, | ||
headInputToTags, | ||
injectHead, | ||
isEqualNode, | ||
propsToString, | ||
renderHeadToString, | ||
@@ -708,5 +709,2 @@ resolveHeadEntries, | ||
sortTags, | ||
stringifyAttrName, | ||
stringifyAttrValue, | ||
stringifyAttrs, | ||
tagDedupeKey, | ||
@@ -716,3 +714,3 @@ tagToString, | ||
useHead, | ||
useHeadRaw | ||
useHeadSafe | ||
}); |
{ | ||
"name": "@vueuse/head", | ||
"version": "1.0.0-rc.4", | ||
"version": "1.0.0-rc.5", | ||
"packageManager": "pnpm@7.5.0", | ||
@@ -5,0 +5,0 @@ "description": "Document head manager for Vue 3. SSR ready.", |
@@ -55,6 +55,4 @@ <h1 align='center'>@vueuse/head</h1> | ||
To provide inner content you should use the `textContent` attribute (previously `children` which is deprecated). | ||
To provide inner content you should use the `children` attribute. | ||
Note: All values provided to `useHead` will be encoded to avoid XSS injection. If you need to insert raw data use `useHeadRaw`. | ||
#### Example | ||
@@ -75,3 +73,3 @@ | ||
style: [ | ||
{ type: 'text/css', textContent: 'body { background: red; }' }, | ||
{ type: 'text/css', children: 'body { background: red; }' }, | ||
], | ||
@@ -107,11 +105,8 @@ script: [ | ||
### `useHeadRaw(head: MaybeComputedRef<HeadObject>)` | ||
### `useHeadSafe(head: MaybeComputedRef<HeadObject>)` | ||
Has the same functionality as `useHead` but does not encode values. This is useful for inserting raw data such as scripts | ||
and attribute events. | ||
Has the same functionality as `useHead` but encodes values to prevent XSS. This is useful for inserting untrusted data from third-parties. | ||
When inserting raw inner content you should use `innerHTML`. | ||
```ts | ||
useHeadRaw({ | ||
useHeadSafe({ | ||
bodyAttrs: { | ||
@@ -122,3 +117,3 @@ onfocus: 'alert("hello")', | ||
{ | ||
innerHTML: 'alert("hello world")', | ||
children: 'alert("hello world")', | ||
}, | ||
@@ -155,3 +150,3 @@ ], | ||
```ts | ||
useHeadRaw({ | ||
useHead({ | ||
script: [ | ||
@@ -199,5 +194,2 @@ { | ||
> :warning: Experimental feature | ||
> Only available when rendering SSR. | ||
To set the render priority of a tag you can use the `renderPriority` attribute: | ||
@@ -204,0 +196,0 @@ |
Sorry, the diff of this file is not supported yet
58233
1502
352