@pyreon/head 0.11.4 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -8
- package/lib/index.js.map +1 -1
- package/lib/provider.js.map +1 -1
- package/lib/ssr.js.map +1 -1
- package/lib/types/index.d.ts +4 -4
- package/lib/types/provider.d.ts +1 -1
- package/lib/types/use-head.d.ts +3 -3
- package/lib/use-head.js.map +1 -1
- package/package.json +18 -18
- package/src/context.ts +5 -5
- package/src/dom.ts +7 -7
- package/src/index.ts +5 -5
- package/src/provider.ts +5 -5
- package/src/ssr.ts +15 -15
- package/src/tests/head.test.ts +447 -445
- package/src/tests/setup.ts +1 -1
- package/src/use-head.ts +19 -19
package/src/dom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { HeadContextValue } from
|
|
1
|
+
import type { HeadContextValue } from './context'
|
|
2
2
|
|
|
3
|
-
const ATTR =
|
|
3
|
+
const ATTR = 'data-pyreon-head'
|
|
4
4
|
|
|
5
5
|
/** Tracks managed elements by key — avoids querySelectorAll on every sync */
|
|
6
6
|
const managedElements = new Map<string, Element>()
|
|
@@ -41,7 +41,7 @@ function createNewTag(tag: {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function syncDom(ctx: HeadContextValue): void {
|
|
44
|
-
if (typeof document ===
|
|
44
|
+
if (typeof document === 'undefined') return
|
|
45
45
|
|
|
46
46
|
const tags = ctx.resolve()
|
|
47
47
|
const titleTemplate = ctx.resolveTitleTemplate()
|
|
@@ -66,7 +66,7 @@ export function syncDom(ctx: HeadContextValue): void {
|
|
|
66
66
|
const kept = new Set<string>()
|
|
67
67
|
|
|
68
68
|
for (const tag of tags) {
|
|
69
|
-
if (tag.tag ===
|
|
69
|
+
if (tag.tag === 'title') {
|
|
70
70
|
document.title = applyTitleTemplate(String(tag.children), titleTemplate)
|
|
71
71
|
continue
|
|
72
72
|
}
|
|
@@ -117,7 +117,7 @@ function applyTitleTemplate(
|
|
|
117
117
|
template: string | ((t: string) => string) | undefined,
|
|
118
118
|
): string {
|
|
119
119
|
if (!template) return title
|
|
120
|
-
if (typeof template ===
|
|
120
|
+
if (typeof template === 'function') return template(title)
|
|
121
121
|
return template.replace(/%s/g, title)
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -126,7 +126,7 @@ function syncElementAttrs(el: Element, attrs: Record<string, string>): void {
|
|
|
126
126
|
// Remove previously managed attrs that are no longer present
|
|
127
127
|
const managed = el.getAttribute(`${ATTR}-attrs`)
|
|
128
128
|
if (managed) {
|
|
129
|
-
for (const name of managed.split(
|
|
129
|
+
for (const name of managed.split(',')) {
|
|
130
130
|
if (name && !(name in attrs)) el.removeAttribute(name)
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -136,7 +136,7 @@ function syncElementAttrs(el: Element, attrs: Record<string, string>): void {
|
|
|
136
136
|
if (el.getAttribute(k) !== v) el.setAttribute(k, v)
|
|
137
137
|
}
|
|
138
138
|
if (keys.length > 0) {
|
|
139
|
-
el.setAttribute(`${ATTR}-attrs`, keys.join(
|
|
139
|
+
el.setAttribute(`${ATTR}-attrs`, keys.join(','))
|
|
140
140
|
} else if (managed) {
|
|
141
141
|
el.removeAttribute(`${ATTR}-attrs`)
|
|
142
142
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,8 @@ export type {
|
|
|
8
8
|
ScriptTag,
|
|
9
9
|
StyleTag,
|
|
10
10
|
UseHeadInput,
|
|
11
|
-
} from
|
|
12
|
-
export { createHeadContext, HeadContext } from
|
|
13
|
-
export type { HeadProviderProps } from
|
|
14
|
-
export { HeadProvider } from
|
|
15
|
-
export { useHead } from
|
|
11
|
+
} from './context'
|
|
12
|
+
export { createHeadContext, HeadContext } from './context'
|
|
13
|
+
export type { HeadProviderProps } from './provider'
|
|
14
|
+
export { HeadProvider } from './provider'
|
|
15
|
+
export { useHead } from './use-head'
|
package/src/provider.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ComponentFn, Props, VNodeChild } from
|
|
2
|
-
import { provide } from
|
|
3
|
-
import type { HeadContextValue } from
|
|
4
|
-
import { createHeadContext, HeadContext } from
|
|
1
|
+
import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
|
|
2
|
+
import { provide } from '@pyreon/core'
|
|
3
|
+
import type { HeadContextValue } from './context'
|
|
4
|
+
import { createHeadContext, HeadContext } from './context'
|
|
5
5
|
|
|
6
6
|
export interface HeadProviderProps extends Props {
|
|
7
7
|
context?: HeadContextValue | undefined
|
|
@@ -27,5 +27,5 @@ export const HeadProvider: ComponentFn<HeadProviderProps> = (props) => {
|
|
|
27
27
|
provide(HeadContext, ctx)
|
|
28
28
|
|
|
29
29
|
const ch = props.children
|
|
30
|
-
return typeof ch ===
|
|
30
|
+
return typeof ch === 'function' ? (ch as () => VNodeChild)() : ch
|
|
31
31
|
}
|
package/src/ssr.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ComponentFn, VNode } from
|
|
2
|
-
import { h, pushContext } from
|
|
3
|
-
import { renderToString } from
|
|
4
|
-
import type { HeadTag } from
|
|
5
|
-
import { createHeadContext, HeadContext } from
|
|
1
|
+
import type { ComponentFn, VNode } from '@pyreon/core'
|
|
2
|
+
import { h, pushContext } from '@pyreon/core'
|
|
3
|
+
import { renderToString } from '@pyreon/runtime-server'
|
|
4
|
+
import type { HeadTag } from './context'
|
|
5
|
+
import { createHeadContext, HeadContext } from './context'
|
|
6
6
|
|
|
7
|
-
const VOID_TAGS = new Set([
|
|
7
|
+
const VOID_TAGS = new Set(['meta', 'link', 'base'])
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Render a Pyreon app to an HTML fragment + a serialized <head> string.
|
|
@@ -46,7 +46,7 @@ export async function renderWithHead(app: VNode): Promise<RenderWithHeadResult>
|
|
|
46
46
|
const head = ctx
|
|
47
47
|
.resolve()
|
|
48
48
|
.map((tag) => serializeTag(tag, titleTemplate))
|
|
49
|
-
.join(
|
|
49
|
+
.join('\n ')
|
|
50
50
|
return {
|
|
51
51
|
html,
|
|
52
52
|
head,
|
|
@@ -56,10 +56,10 @@ export async function renderWithHead(app: VNode): Promise<RenderWithHeadResult>
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function serializeTag(tag: HeadTag, titleTemplate?: string | ((title: string) => string)): string {
|
|
59
|
-
if (tag.tag ===
|
|
60
|
-
const raw = tag.children ||
|
|
59
|
+
if (tag.tag === 'title') {
|
|
60
|
+
const raw = tag.children || ''
|
|
61
61
|
const title = titleTemplate
|
|
62
|
-
? typeof titleTemplate ===
|
|
62
|
+
? typeof titleTemplate === 'function'
|
|
63
63
|
? titleTemplate(raw)
|
|
64
64
|
: titleTemplate.replace(/%s/g, raw)
|
|
65
65
|
: raw
|
|
@@ -69,20 +69,20 @@ function serializeTag(tag: HeadTag, titleTemplate?: string | ((title: string) =>
|
|
|
69
69
|
const attrs = props
|
|
70
70
|
? Object.entries(props)
|
|
71
71
|
.map(([k, v]) => `${k}="${esc(v)}"`)
|
|
72
|
-
.join(
|
|
73
|
-
:
|
|
72
|
+
.join(' ')
|
|
73
|
+
: ''
|
|
74
74
|
const open = attrs ? `<${tag.tag} ${attrs}` : `<${tag.tag}`
|
|
75
75
|
if (VOID_TAGS.has(tag.tag)) return `${open} />`
|
|
76
|
-
const content = tag.children ||
|
|
76
|
+
const content = tag.children || ''
|
|
77
77
|
// Escape sequences that could break out of script/style/noscript blocks:
|
|
78
78
|
// 1. Closing tags like </script> — use Unicode escape in the slash
|
|
79
79
|
// 2. HTML comment openers <!-- that could confuse parsers
|
|
80
|
-
const body = content.replace(/<\/(script|style|noscript)/gi,
|
|
80
|
+
const body = content.replace(/<\/(script|style|noscript)/gi, '<\\/$1').replace(/<!--/g, '<\\!--')
|
|
81
81
|
return `${open}>${body}</${tag.tag}>`
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const ESC_RE = /[&<>"]/g
|
|
85
|
-
const ESC_MAP: Record<string, string> = {
|
|
85
|
+
const ESC_MAP: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"' }
|
|
86
86
|
|
|
87
87
|
function esc(s: string): string {
|
|
88
88
|
return ESC_RE.test(s) ? s.replace(ESC_RE, (ch) => ESC_MAP[ch] as string) : s
|