@pyreon/head 0.13.1 → 0.15.0
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/lib/analysis/index.js.html +1 -1
- package/lib/analysis/provider.js.html +1 -1
- package/lib/index.js +2 -1
- package/lib/provider.js +2 -1
- package/lib/types/index.d.ts +2 -2
- package/package.json +9 -7
- package/src/manifest.ts +142 -0
- package/src/provider.ts +6 -1
- package/src/tests/manifest-snapshot.test.ts +34 -0
- package/src/tests/native-marker.test.ts +9 -0
- package/lib/index.js.map +0 -1
- package/lib/provider.js.map +0 -1
- package/lib/ssr.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/provider.d.ts.map +0 -1
- package/lib/types/ssr.d.ts.map +0 -1
- package/lib/types/use-head.d.ts.map +0 -1
- package/lib/use-head.js.map +0 -1
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"198a0340-1","name":"context.ts"},{"uid":"198a0340-3","name":"provider.ts"},{"uid":"198a0340-5","name":"dom.ts"},{"uid":"198a0340-7","name":"use-head.ts"},{"uid":"198a0340-9","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"198a0340-1":{"renderedLength":1373,"gzipLength":509,"brotliLength":0,"metaUid":"198a0340-0"},"198a0340-3":{"renderedLength":681,"gzipLength":408,"brotliLength":0,"metaUid":"198a0340-2"},"198a0340-5":{"renderedLength":3401,"gzipLength":1293,"brotliLength":0,"metaUid":"198a0340-4"},"198a0340-7":{"renderedLength":2462,"gzipLength":1063,"brotliLength":0,"metaUid":"198a0340-6"},"198a0340-9":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"198a0340-8"}},"nodeMetas":{"198a0340-0":{"id":"/src/context.ts","moduleParts":{"index.js":"198a0340-1"},"imported":[{"uid":"198a0340-10"}],"importedBy":[{"uid":"198a0340-8"},{"uid":"198a0340-2"},{"uid":"198a0340-6"}]},"198a0340-2":{"id":"/src/provider.ts","moduleParts":{"index.js":"198a0340-3"},"imported":[{"uid":"198a0340-10"},{"uid":"198a0340-0"}],"importedBy":[{"uid":"198a0340-8"}]},"198a0340-4":{"id":"/src/dom.ts","moduleParts":{"index.js":"198a0340-5"},"imported":[],"importedBy":[{"uid":"198a0340-6"}]},"198a0340-6":{"id":"/src/use-head.ts","moduleParts":{"index.js":"198a0340-7"},"imported":[{"uid":"198a0340-10"},{"uid":"198a0340-11"},{"uid":"198a0340-0"},{"uid":"198a0340-4"}],"importedBy":[{"uid":"198a0340-8"}]},"198a0340-8":{"id":"/src/index.ts","moduleParts":{"index.js":"198a0340-9"},"imported":[{"uid":"198a0340-0"},{"uid":"198a0340-2"},{"uid":"198a0340-6"}],"importedBy":[],"isEntry":true},"198a0340-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"198a0340-0"},{"uid":"198a0340-2"},{"uid":"198a0340-6"}]},"198a0340-11":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"198a0340-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"provider.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"provider.js","children":[{"name":"src","children":[{"uid":"65ae058d-1","name":"context.ts"},{"uid":"65ae058d-3","name":"provider.ts"}]}]}],"isRoot":true},"nodeParts":{"65ae058d-1":{"renderedLength":1373,"gzipLength":509,"brotliLength":0,"metaUid":"65ae058d-0"},"65ae058d-3":{"renderedLength":681,"gzipLength":408,"brotliLength":0,"metaUid":"65ae058d-2"}},"nodeMetas":{"65ae058d-0":{"id":"/src/context.ts","moduleParts":{"provider.js":"65ae058d-1"},"imported":[{"uid":"65ae058d-4"}],"importedBy":[{"uid":"65ae058d-2"}]},"65ae058d-2":{"id":"/src/provider.ts","moduleParts":{"provider.js":"65ae058d-3"},"imported":[{"uid":"65ae058d-4"},{"uid":"65ae058d-0"}],"importedBy":[],"isEntry":true},"65ae058d-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"65ae058d-2"},{"uid":"65ae058d-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, onMount, onUnmount, provide, useContext } from "@pyreon/core";
|
|
1
|
+
import { createContext, nativeCompat, onMount, onUnmount, provide, useContext } from "@pyreon/core";
|
|
2
2
|
import { effect } from "@pyreon/reactivity";
|
|
3
3
|
|
|
4
4
|
//#region src/context.ts
|
|
@@ -79,6 +79,7 @@ const HeadProvider = (props) => {
|
|
|
79
79
|
const ch = props.children;
|
|
80
80
|
return typeof ch === "function" ? ch() : ch;
|
|
81
81
|
};
|
|
82
|
+
nativeCompat(HeadProvider);
|
|
82
83
|
|
|
83
84
|
//#endregion
|
|
84
85
|
//#region src/dom.ts
|
package/lib/provider.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, provide } from "@pyreon/core";
|
|
1
|
+
import { createContext, nativeCompat, provide } from "@pyreon/core";
|
|
2
2
|
|
|
3
3
|
//#region src/context.ts
|
|
4
4
|
function createHeadContext() {
|
|
@@ -78,6 +78,7 @@ const HeadProvider = (props) => {
|
|
|
78
78
|
const ch = props.children;
|
|
79
79
|
return typeof ch === "function" ? ch() : ch;
|
|
80
80
|
};
|
|
81
|
+
nativeCompat(HeadProvider);
|
|
81
82
|
|
|
82
83
|
//#endregion
|
|
83
84
|
export { HeadProvider };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as _pyreon_core0 from "@pyreon/core";
|
|
1
|
+
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
2
|
import { ComponentFn, Props, VNodeChild } from "@pyreon/core";
|
|
3
3
|
|
|
4
4
|
//#region src/context.d.ts
|
|
@@ -153,7 +153,7 @@ interface HeadContextValue {
|
|
|
153
153
|
resolveBodyAttrs(): Record<string, string>;
|
|
154
154
|
}
|
|
155
155
|
declare function createHeadContext(): HeadContextValue;
|
|
156
|
-
declare const HeadContext: _pyreon_core0.Context<HeadContextValue | null>;
|
|
156
|
+
declare const HeadContext: _$_pyreon_core0.Context<HeadContextValue | null>;
|
|
157
157
|
//#endregion
|
|
158
158
|
//#region src/provider.d.ts
|
|
159
159
|
interface HeadProviderProps extends Props {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/head",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Head tag management for Pyreon — works in SSR and CSR",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/head#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"lib",
|
|
17
|
+
"!lib/**/*.map",
|
|
17
18
|
"src",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|
|
@@ -58,18 +59,19 @@
|
|
|
58
59
|
"prepublishOnly": "bun run build"
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
|
-
"@pyreon/core": "^0.
|
|
62
|
-
"@pyreon/reactivity": "^0.
|
|
62
|
+
"@pyreon/core": "^0.15.0",
|
|
63
|
+
"@pyreon/reactivity": "^0.15.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@happy-dom/global-registrator": "^20.8.9",
|
|
66
|
-
"@pyreon/
|
|
67
|
-
"@pyreon/runtime-
|
|
68
|
-
"@pyreon/
|
|
67
|
+
"@pyreon/manifest": "0.13.1",
|
|
68
|
+
"@pyreon/runtime-dom": "^0.15.0",
|
|
69
|
+
"@pyreon/runtime-server": "^0.15.0",
|
|
70
|
+
"@pyreon/test-utils": "^0.13.2",
|
|
69
71
|
"@vitest/browser-playwright": "^4.1.4"
|
|
70
72
|
},
|
|
71
73
|
"peerDependencies": {
|
|
72
|
-
"@pyreon/runtime-server": "^0.
|
|
74
|
+
"@pyreon/runtime-server": "^0.15.0"
|
|
73
75
|
},
|
|
74
76
|
"peerDependenciesMeta": {
|
|
75
77
|
"@pyreon/runtime-server": {
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { defineManifest } from '@pyreon/manifest'
|
|
2
|
+
|
|
3
|
+
export default defineManifest({
|
|
4
|
+
name: '@pyreon/head',
|
|
5
|
+
title: 'Head Management',
|
|
6
|
+
tagline:
|
|
7
|
+
'Reactive `<head>` tag management — useHead(), HeadProvider, renderWithHead() for SSR',
|
|
8
|
+
description:
|
|
9
|
+
'Reactive head tag management for Pyreon — `useHead()` collects title, meta, link, script, style, noscript, base, jsonLd entries from any component in the tree (static or signal-driven). `HeadProvider` collects them on the client and syncs to the live `<head>` element; `renderWithHead()` collects them on the server and returns the serialized HTML alongside the rendered app.',
|
|
10
|
+
category: 'browser',
|
|
11
|
+
features: [
|
|
12
|
+
'useHead(input | () => input) — register head tags from any component',
|
|
13
|
+
'Reactive: pass a function to re-register on signal change',
|
|
14
|
+
'Title templates with %s placeholder or function form',
|
|
15
|
+
'HeadProvider for client-side DOM sync',
|
|
16
|
+
'renderWithHead() for SSR — returns html + head string',
|
|
17
|
+
'Keyed deduplication — innermost component wins per key',
|
|
18
|
+
'JSON-LD shorthand: `jsonLd: {...}` auto-wraps as `<script type="application/ld+json">`',
|
|
19
|
+
],
|
|
20
|
+
longExample: `import { useHead, HeadProvider } from '@pyreon/head'
|
|
21
|
+
import { renderWithHead } from '@pyreon/head'
|
|
22
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
23
|
+
|
|
24
|
+
// Static head tags from any component
|
|
25
|
+
function ProfilePage() {
|
|
26
|
+
useHead({
|
|
27
|
+
title: 'My Profile',
|
|
28
|
+
meta: [{ name: 'description', content: 'User profile page' }],
|
|
29
|
+
link: [{ rel: 'canonical', href: 'https://example.com/profile' }],
|
|
30
|
+
})
|
|
31
|
+
return <div>profile body</div>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Reactive head — pass a function so signal reads re-register on change
|
|
35
|
+
function ReactiveTitle() {
|
|
36
|
+
useHead(() => ({
|
|
37
|
+
title: \`\${username()} — Profile\`,
|
|
38
|
+
meta: [{ property: 'og:title', content: username() }],
|
|
39
|
+
}))
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Client setup
|
|
44
|
+
mount(
|
|
45
|
+
<HeadProvider>
|
|
46
|
+
<App />
|
|
47
|
+
</HeadProvider>,
|
|
48
|
+
document.getElementById('app')!,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Server setup — collects every useHead() call and serializes the head
|
|
52
|
+
const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)
|
|
53
|
+
const document = \`<!doctype html><html\${htmlAttrs}><head>\${head}</head><body\${bodyAttrs}>\${html}</body></html>\``,
|
|
54
|
+
api: [
|
|
55
|
+
{
|
|
56
|
+
name: 'useHead',
|
|
57
|
+
kind: 'hook',
|
|
58
|
+
signature: 'useHead(input: UseHeadInput | (() => UseHeadInput)): void',
|
|
59
|
+
summary:
|
|
60
|
+
'Register head tags from any component in the tree. Pass a static `UseHeadInput` object for one-shot registration, or a `() => UseHeadInput` thunk for reactive re-registration when signal reads inside the thunk change. Calling `useHead()` outside a `HeadProvider` ancestor (CSR) or `renderWithHead()` invocation (SSR) is a silent no-op — it does not throw.',
|
|
61
|
+
example: `// Static:
|
|
62
|
+
useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
|
|
63
|
+
|
|
64
|
+
// Reactive (updates when signals change):
|
|
65
|
+
useHead(() => ({
|
|
66
|
+
title: \`\${username()} — Profile\`,
|
|
67
|
+
meta: [{ property: "og:title", content: username() }]
|
|
68
|
+
}))`,
|
|
69
|
+
mistakes: [
|
|
70
|
+
'Using `${...}` in a `titleTemplate` string — the placeholder is `%s` (or pass a function form `(title) => …`)',
|
|
71
|
+
'Calling `useHead()` outside any `HeadProvider` / `renderWithHead()` boundary — silent no-op, the entries simply go nowhere',
|
|
72
|
+
'Wrapping the input in `computed()` instead of a thunk — pass a plain `() => ({...})` arrow; `useHead` registers its own effect',
|
|
73
|
+
'Expecting `</script>` inside an inline script body to render verbatim — the SSR escaper rewrites it as `<\\/script>` to prevent breaking out of the inline tag',
|
|
74
|
+
],
|
|
75
|
+
seeAlso: ['HeadProvider', 'renderWithHead'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'HeadProvider',
|
|
79
|
+
kind: 'component',
|
|
80
|
+
signature: '(props: HeadProviderProps) => VNodeChild',
|
|
81
|
+
summary:
|
|
82
|
+
'Client-side context provider that collects every `useHead()` call from descendants and syncs the resolved tags into the live `document.head` element. Mount once near the application root. Auto-creates a `HeadContextValue` when no `context` prop is passed; nested providers each own an independent context.',
|
|
83
|
+
example: `<HeadProvider>{children}</HeadProvider>
|
|
84
|
+
|
|
85
|
+
// Client-side setup:
|
|
86
|
+
mount(
|
|
87
|
+
<HeadProvider>
|
|
88
|
+
<App />
|
|
89
|
+
</HeadProvider>,
|
|
90
|
+
document.getElementById("app")!
|
|
91
|
+
)`,
|
|
92
|
+
mistakes: [
|
|
93
|
+
'Mounting two `HeadProvider` instances at sibling roots — each owns an independent context, so a `useHead()` deeper in tree A is invisible to tree B',
|
|
94
|
+
'Forgetting to mount `HeadProvider` and expecting `useHead()` to still update `document.head` — silent no-op outside a provider',
|
|
95
|
+
],
|
|
96
|
+
seeAlso: ['useHead', 'renderWithHead', 'createHeadContext'],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'renderWithHead',
|
|
100
|
+
kind: 'function',
|
|
101
|
+
signature:
|
|
102
|
+
'renderWithHead(app: VNode): Promise<{ html: string; head: string; htmlAttrs: string; bodyAttrs: string }>',
|
|
103
|
+
summary:
|
|
104
|
+
'SSR companion to `HeadProvider`. Renders the app to HTML via `renderToString` while collecting every `useHead()` call from the tree, then serializes the resolved tags into a single `head` string plus separate `htmlAttrs` / `bodyAttrs` strings. Async components that call `useHead()` in their body work — the renderer awaits suspended subtrees before serialization.',
|
|
105
|
+
example: `import { renderWithHead } from '@pyreon/head'
|
|
106
|
+
|
|
107
|
+
const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)
|
|
108
|
+
const doc = \`<!doctype html><html\${htmlAttrs}><head>\${head}</head><body\${bodyAttrs}>\${html}</body></html>\``,
|
|
109
|
+
mistakes: [
|
|
110
|
+
'Awaiting `renderWithHead` and then NOT splicing `head` into the `<head>` element — every `useHead()` call quietly disappears',
|
|
111
|
+
'Forgetting to interpolate `htmlAttrs` / `bodyAttrs` (the leading space is included in each string) — `htmlAttrs.lang` and `bodyAttrs.class` set via `useHead` won\\\'t reach the DOM',
|
|
112
|
+
],
|
|
113
|
+
seeAlso: ['useHead', 'HeadProvider'],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'createHeadContext',
|
|
117
|
+
kind: 'function',
|
|
118
|
+
signature: '() => HeadContextValue',
|
|
119
|
+
summary:
|
|
120
|
+
'Manual factory for a `HeadContextValue` — only needed when wiring up a custom SSR pipeline that bypasses `renderWithHead`, or when running multiple isolated head contexts in the same process. The value exposes `add` / `remove` / `resolve` / `resolveTitleTemplate` / `resolveHtmlAttrs` / `resolveBodyAttrs` for full programmatic control.',
|
|
121
|
+
example: `import { createHeadContext, HeadContext } from '@pyreon/head'
|
|
122
|
+
|
|
123
|
+
const ctx = createHeadContext()
|
|
124
|
+
provide(HeadContext, ctx)
|
|
125
|
+
// ... render tree that calls useHead() ...
|
|
126
|
+
const { tags, htmlAttrs, bodyAttrs } = ctx.resolve()`,
|
|
127
|
+
seeAlso: ['HeadProvider', 'renderWithHead'],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
gotchas: [
|
|
131
|
+
{
|
|
132
|
+
label: 'Key deduplication',
|
|
133
|
+
note:
|
|
134
|
+
'Tags with the same key replace each other (innermost wins). Meta keys: `name` → `property` → index. Link keys: `href + rel` → `rel` → index. Script keys: `src` → index. Style and noscript are unkeyed and always accumulated.',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
label: 'Inline script escaping',
|
|
138
|
+
note:
|
|
139
|
+
'Script / style / noscript bodies are not HTML-escaped, but the SSR serializer rewrites `</script>` / `</style>` / `</noscript>` and `<!--` to prevent breaking out of the wrapping tag. Inline JSON-LD via `jsonLd: {...}` auto-wraps in `<script type="application/ld+json">` and stringifies the value.',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
})
|
package/src/provider.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
|
|
2
|
-
import { provide } from '@pyreon/core'
|
|
2
|
+
import { nativeCompat, provide } from '@pyreon/core'
|
|
3
3
|
import type { HeadContextValue } from './context'
|
|
4
4
|
import { createHeadContext, HeadContext } from './context'
|
|
5
5
|
|
|
@@ -29,3 +29,8 @@ export const HeadProvider: ComponentFn<HeadProviderProps> = (props) => {
|
|
|
29
29
|
const ch = props.children
|
|
30
30
|
return typeof ch === 'function' ? (ch as () => VNodeChild)() : ch
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
// Mark as native — compat-mode jsx() runtimes skip wrapCompatComponent so
|
|
34
|
+
// HeadProvider's provide(HeadContext, ...) call runs inside Pyreon's setup
|
|
35
|
+
// frame, not the compat wrapper's runUntracked accessor.
|
|
36
|
+
nativeCompat(HeadProvider)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderApiReferenceEntries,
|
|
3
|
+
renderLlmsFullSection,
|
|
4
|
+
renderLlmsTxtLine,
|
|
5
|
+
} from '@pyreon/manifest'
|
|
6
|
+
import manifest from '../manifest'
|
|
7
|
+
|
|
8
|
+
// Structural snapshot — locks in the manifest's contract with the
|
|
9
|
+
// gen-docs pipeline without inline-snapshotting the full prose
|
|
10
|
+
// (which rots on every wording change). Pairs with the en-masse
|
|
11
|
+
// `gen-docs --check` CI gate and the api-reference region marker.
|
|
12
|
+
|
|
13
|
+
describe('gen-docs — head snapshot', () => {
|
|
14
|
+
it('renders a llms.txt bullet starting with the package prefix', () => {
|
|
15
|
+
const line = renderLlmsTxtLine(manifest)
|
|
16
|
+
expect(line.startsWith('- @pyreon/head —')).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('renders a llms-full.txt section with the right header', () => {
|
|
20
|
+
const section = renderLlmsFullSection(manifest)
|
|
21
|
+
expect(section.startsWith('## @pyreon/head —')).toBe(true)
|
|
22
|
+
expect(section).toContain('```typescript')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders MCP api-reference entries for every api[] item', () => {
|
|
26
|
+
const record = renderApiReferenceEntries(manifest)
|
|
27
|
+
expect(Object.keys(record).sort()).toEqual([
|
|
28
|
+
'head/HeadProvider',
|
|
29
|
+
'head/createHeadContext',
|
|
30
|
+
'head/renderWithHead',
|
|
31
|
+
'head/useHead',
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isNativeCompat } from '@pyreon/core'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { HeadProvider } from '../provider'
|
|
4
|
+
|
|
5
|
+
describe('native-compat marker — @pyreon/head', () => {
|
|
6
|
+
it('HeadProvider is marked native', () => {
|
|
7
|
+
expect(isNativeCompat(HeadProvider)).toBe(true)
|
|
8
|
+
})
|
|
9
|
+
})
|
package/lib/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/provider.ts","../src/dom.ts","../src/use-head.ts"],"sourcesContent":["import { createContext } from '@pyreon/core'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface HeadTag {\n /** HTML tag name */\n tag: 'title' | 'meta' | 'link' | 'script' | 'style' | 'base' | 'noscript'\n /**\n * Deduplication key. Tags with the same key replace each other;\n * innermost component (last added) wins.\n * Example: all components setting the page title use key \"title\".\n */\n key?: string\n /** HTML attributes for the tag */\n props?: Record<string, string>\n /** Text content — for <title>, <script>, <style>, <noscript> */\n children?: string\n}\n\n// ─── Strict tag types ────────────────────────────────────────────────────────\n\n/** Standard `<meta>` tag attributes. Catches typos like `{ naem: \"description\" }`. */\nexport interface MetaTag {\n /** Standard meta name (e.g. \"description\", \"viewport\", \"robots\") */\n name?: string\n /** Open Graph / social property (e.g. \"og:title\", \"twitter:card\") */\n property?: string\n /** HTTP equivalent header (e.g. \"refresh\", \"content-type\") */\n 'http-equiv'?: string\n /** Value associated with name, property, or http-equiv */\n content?: string\n /** Document character encoding (e.g. \"utf-8\") */\n charset?: string\n /** Schema.org itemprop */\n itemprop?: string\n /** Media condition for applicability (e.g. \"(prefers-color-scheme: dark)\") */\n media?: string\n}\n\n/** Standard `<link>` tag attributes. */\nexport interface LinkTag {\n /** Relationship to the current document (e.g. \"stylesheet\", \"icon\", \"canonical\") */\n rel?: string\n /** URL of the linked resource */\n href?: string\n /** Resource type hint for preloading (e.g. \"style\", \"script\", \"font\") */\n as?: string\n /** MIME type (e.g. \"text/css\", \"image/png\") */\n type?: string\n /** Media query for conditional loading */\n media?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Icon sizes (e.g. \"32x32\", \"any\") */\n sizes?: string\n /** Language of the linked resource */\n hreflang?: string\n /** Title for the link (used for alternate stylesheets) */\n title?: string\n /** Fetch priority hint */\n fetchpriority?: 'high' | 'low' | 'auto'\n /** Referrer policy */\n referrerpolicy?: string\n /** Image source set for preloading responsive images */\n imagesrcset?: string\n /** Image sizes for preloading responsive images */\n imagesizes?: string\n /** Disable the resource (for stylesheets) */\n disabled?: string\n /** Color for mask-icon */\n color?: string\n}\n\n/** Standard `<script>` tag attributes. */\nexport interface ScriptTag {\n /** External script URL */\n src?: string\n /** Script MIME type or module type (e.g. \"module\", \"importmap\") */\n type?: string\n /** Load asynchronously */\n async?: string\n /** Defer execution until document is parsed */\n defer?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Exclude from module-supporting browsers */\n nomodule?: string\n /** Referrer policy */\n referrerpolicy?: string\n /** Fetch priority hint */\n fetchpriority?: string\n /** Inline script content */\n children?: string\n}\n\n/** Standard `<style>` tag attributes. */\nexport interface StyleTag {\n /** Inline CSS content (required) */\n children: string\n /** Media query for conditional styles */\n media?: string\n /** Nonce for CSP */\n nonce?: string\n /** Title for alternate stylesheets */\n title?: string\n /** Render-blocking behavior */\n blocking?: string\n}\n\n/** Standard `<base>` tag attributes. */\nexport interface BaseTag {\n /** Base URL for relative URLs in the document */\n href?: string\n /** Default target for links and forms */\n target?: '_blank' | '_self' | '_parent' | '_top'\n}\n\nexport interface UseHeadInput {\n title?: string\n /**\n * Title template — use `%s` as a placeholder for the page title.\n * Applied to the resolved title after deduplication.\n * @example useHead({ titleTemplate: \"%s | My App\" })\n */\n titleTemplate?: string | ((title: string) => string)\n meta?: MetaTag[]\n link?: LinkTag[]\n script?: ScriptTag[]\n style?: StyleTag[]\n noscript?: { children: string }[]\n /** Convenience: emits a <script type=\"application/ld+json\"> tag with JSON.stringify'd content */\n jsonLd?: Record<string, unknown> | Record<string, unknown>[]\n base?: BaseTag\n /** Attributes to set on the <html> element (e.g. { lang: \"en\", dir: \"ltr\" }) */\n htmlAttrs?: Record<string, string>\n /** Attributes to set on the <body> element (e.g. { class: \"dark\" }) */\n bodyAttrs?: Record<string, string>\n}\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport interface HeadEntry {\n tags: HeadTag[]\n titleTemplate?: string | ((title: string) => string) | undefined\n htmlAttrs?: Record<string, string> | undefined\n bodyAttrs?: Record<string, string> | undefined\n}\n\nexport interface HeadContextValue {\n add(id: symbol, entry: HeadEntry): void\n remove(id: symbol): void\n /** Returns deduplicated tags — last-added entry wins per key */\n resolve(): HeadTag[]\n /** Returns the merged titleTemplate (last-added wins) */\n resolveTitleTemplate(): (string | ((title: string) => string)) | undefined\n /** Returns merged htmlAttrs (later entries override earlier) */\n resolveHtmlAttrs(): Record<string, string>\n /** Returns merged bodyAttrs (later entries override earlier) */\n resolveBodyAttrs(): Record<string, string>\n}\n\nexport function createHeadContext(): HeadContextValue {\n const map = new Map<symbol, HeadEntry>()\n\n // ── Cached resolve ───────────────────────────────────────────────────────\n let dirty = true\n let cachedTags: HeadTag[] = []\n let cachedTitleTemplate: (string | ((title: string) => string)) | undefined\n let cachedHtmlAttrs: Record<string, string> = {}\n let cachedBodyAttrs: Record<string, string> = {}\n\n function rebuild(): void {\n if (!dirty) return\n dirty = false\n\n const keyed = new Map<string, HeadTag>()\n const unkeyed: HeadTag[] = []\n let titleTemplate: (string | ((title: string) => string)) | undefined\n const htmlAttrs: Record<string, string> = {}\n const bodyAttrs: Record<string, string> = {}\n\n for (const entry of map.values()) {\n for (const tag of entry.tags) {\n if (tag.key) keyed.set(tag.key, tag)\n else unkeyed.push(tag)\n }\n if (entry.titleTemplate !== undefined) titleTemplate = entry.titleTemplate\n if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs)\n if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs)\n }\n\n cachedTags = [...keyed.values(), ...unkeyed]\n cachedTitleTemplate = titleTemplate\n cachedHtmlAttrs = htmlAttrs\n cachedBodyAttrs = bodyAttrs\n }\n\n return {\n add(id, entry) {\n map.set(id, entry)\n dirty = true\n },\n remove(id) {\n map.delete(id)\n dirty = true\n },\n resolve() {\n rebuild()\n return cachedTags\n },\n resolveTitleTemplate() {\n rebuild()\n return cachedTitleTemplate\n },\n resolveHtmlAttrs() {\n rebuild()\n return cachedHtmlAttrs\n },\n resolveBodyAttrs() {\n rebuild()\n return cachedBodyAttrs\n },\n }\n}\n\nexport const HeadContext = createContext<HeadContextValue | null>(null)\n","import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'\nimport { provide } from '@pyreon/core'\nimport type { HeadContextValue } from './context'\nimport { createHeadContext, HeadContext } from './context'\n\nexport interface HeadProviderProps extends Props {\n context?: HeadContextValue | undefined\n children?: VNodeChild\n}\n\n/**\n * Provides a HeadContextValue to all descendant components.\n * Wrap your app root with this to enable useHead() throughout the tree.\n *\n * If no `context` prop is passed, a new HeadContext is created automatically.\n *\n * @example\n * // Auto-create context:\n * <HeadProvider><App /></HeadProvider>\n *\n * // Explicit context (e.g. for SSR):\n * const headCtx = createHeadContext()\n * mount(h(HeadProvider, { context: headCtx }, h(App, null)), root)\n */\nexport const HeadProvider: ComponentFn<HeadProviderProps> = (props) => {\n const ctx = props.context ?? createHeadContext()\n provide(HeadContext, ctx)\n\n const ch = props.children\n return typeof ch === 'function' ? (ch as () => VNodeChild)() : ch\n}\n","import type { HeadContextValue } from './context'\n\nconst ATTR = 'data-pyreon-head'\n\n/** Tracks managed elements by key — avoids querySelectorAll on every sync */\nconst managedElements = new Map<string, Element>()\n\n/**\n * Sync the resolved head tags to the real DOM <head>.\n * Uses incremental diffing: matches existing elements by key, patches attributes\n * in-place, adds new elements, and removes stale ones.\n * Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.\n * No-op on the server (typeof document === \"undefined\").\n */\nfunction patchExistingTag(\n found: Element,\n tag: { props: Record<string, unknown>; children: string },\n kept: Set<string>,\n): void {\n kept.add(found.getAttribute(ATTR) as string)\n patchAttrs(found, tag.props as Record<string, string>)\n const content = String(tag.children)\n if (found.textContent !== content) found.textContent = content\n}\n\nfunction createNewTag(tag: {\n tag: string\n props: Record<string, unknown>\n children: string\n key: unknown\n}): void {\n const el = document.createElement(tag.tag)\n const key = tag.key as string\n el.setAttribute(ATTR, key)\n for (const [k, v] of Object.entries(tag.props as Record<string, string>)) {\n el.setAttribute(k, v)\n }\n if (tag.children) el.textContent = tag.children\n document.head.appendChild(el)\n managedElements.set(key, el)\n}\n\nexport function syncDom(ctx: HeadContextValue): void {\n if (typeof document === 'undefined') return\n\n const tags = ctx.resolve()\n const titleTemplate = ctx.resolveTitleTemplate()\n\n // Seed from DOM on first sync, or re-seed if DOM was reset (e.g. between tests)\n let needsSeed = managedElements.size === 0\n if (!needsSeed) {\n // Check if a tracked element is still in the DOM\n const sample = managedElements.values().next().value\n if (sample && !sample.isConnected) {\n managedElements.clear()\n needsSeed = true\n }\n }\n if (needsSeed) {\n const existing = document.head.querySelectorAll(`[${ATTR}]`)\n for (const el of existing) {\n managedElements.set(el.getAttribute(ATTR) as string, el)\n }\n }\n\n const kept = new Set<string>()\n\n for (const tag of tags) {\n if (tag.tag === 'title') {\n document.title = applyTitleTemplate(String(tag.children), titleTemplate)\n continue\n }\n\n const key = tag.key as string\n const found = managedElements.get(key)\n\n if (found && found.tagName.toLowerCase() === tag.tag) {\n patchExistingTag(found, tag as { props: Record<string, unknown>; children: string }, kept)\n } else {\n if (found) {\n found.remove()\n managedElements.delete(key)\n }\n createNewTag(\n tag as { tag: string; props: Record<string, unknown>; children: string; key: unknown },\n )\n kept.add(key)\n }\n }\n\n // Remove stale elements\n for (const [key, el] of managedElements) {\n if (!kept.has(key)) {\n el.remove()\n managedElements.delete(key)\n }\n }\n\n syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs())\n syncElementAttrs(document.body, ctx.resolveBodyAttrs())\n}\n\n/** Patch an element's attributes to match the desired props. */\nfunction patchAttrs(el: Element, props: Record<string, string>): void {\n for (let i = el.attributes.length - 1; i >= 0; i--) {\n const attr = el.attributes[i]\n if (!attr || attr.name === ATTR) continue\n if (!(attr.name in props)) el.removeAttribute(attr.name)\n }\n for (const [k, v] of Object.entries(props)) {\n if (el.getAttribute(k) !== v) el.setAttribute(k, v)\n }\n}\n\nfunction applyTitleTemplate(\n title: string,\n template: string | ((t: string) => string) | undefined,\n): string {\n if (!template) return title\n if (typeof template === 'function') return template(title)\n return template.replace(/%s/g, title)\n}\n\n/** Sync pyreon-managed attributes on <html> or <body>. */\nfunction syncElementAttrs(el: Element, attrs: Record<string, string>): void {\n // Remove previously managed attrs that are no longer present\n const managed = el.getAttribute(`${ATTR}-attrs`)\n if (managed) {\n for (const name of managed.split(',')) {\n if (name && !(name in attrs)) el.removeAttribute(name)\n }\n }\n const keys: string[] = []\n for (const [k, v] of Object.entries(attrs)) {\n keys.push(k)\n if (el.getAttribute(k) !== v) el.setAttribute(k, v)\n }\n if (keys.length > 0) {\n el.setAttribute(`${ATTR}-attrs`, keys.join(','))\n } else if (managed) {\n el.removeAttribute(`${ATTR}-attrs`)\n }\n}\n","import { onMount, onUnmount, useContext } from '@pyreon/core'\nimport { effect } from '@pyreon/reactivity'\nimport type { HeadEntry, HeadTag, UseHeadInput } from './context'\nimport { HeadContext } from './context'\nimport { syncDom } from './dom'\n\n/** Cast a strict tag interface to the internal props format, stripping undefined values */\nfunction toProps(obj: Record<string, string | undefined>): Record<string, string> {\n const result: Record<string, string> = {}\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined) result[k] = v\n }\n return result\n}\n\nfunction buildEntry(o: UseHeadInput): HeadEntry {\n const tags: HeadTag[] = []\n if (o.title != null) tags.push({ tag: 'title', key: 'title', children: o.title })\n o.meta?.forEach((m, i) => {\n tags.push({\n tag: 'meta',\n key: m.name ?? m.property ?? `meta-${i}`,\n props: toProps(m as Record<string, string | undefined>),\n })\n })\n o.link?.forEach((l, i) => {\n tags.push({\n tag: 'link',\n key: l.href ? `link-${l.rel || ''}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,\n props: toProps(l as Record<string, string | undefined>),\n })\n })\n o.script?.forEach((s, i) => {\n const { children, ...rest } = s\n tags.push({\n tag: 'script',\n key: s.src ?? `script-${i}`,\n props: toProps(rest as Record<string, string | undefined>),\n ...(children != null ? { children } : {}),\n })\n })\n o.style?.forEach((s, i) => {\n const { children, ...rest } = s\n tags.push({\n tag: 'style',\n key: `style-${i}`,\n props: toProps(rest as Record<string, string | undefined>),\n children,\n })\n })\n o.noscript?.forEach((ns, i) => {\n tags.push({ tag: 'noscript', key: `noscript-${i}`, children: ns.children })\n })\n if (o.jsonLd) {\n tags.push({\n tag: 'script',\n key: 'jsonld',\n props: { type: 'application/ld+json' },\n children: JSON.stringify(o.jsonLd),\n })\n }\n if (o.base)\n tags.push({\n tag: 'base',\n key: 'base',\n props: toProps(o.base as Record<string, string | undefined>),\n })\n return {\n tags,\n titleTemplate: o.titleTemplate,\n htmlAttrs: o.htmlAttrs,\n bodyAttrs: o.bodyAttrs,\n }\n}\n\n/**\n * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)\n * for the current component.\n *\n * Accepts a static object or a reactive getter:\n * useHead({ title: \"My Page\", meta: [{ name: \"description\", content: \"...\" }] })\n * useHead(() => ({ title: `${count()} items` })) // updates when signal changes\n *\n * Tags are deduplicated by key — innermost component wins.\n * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.\n */\nexport function useHead(input: UseHeadInput | (() => UseHeadInput)): void {\n const ctx = useContext(HeadContext)\n if (!ctx) return // no HeadProvider — silently no-op\n\n const id = Symbol()\n\n if (typeof input === 'function') {\n if (typeof document !== 'undefined') {\n // CSR: reactive — re-register whenever signals change\n effect(() => {\n ctx.add(id, buildEntry(input()))\n syncDom(ctx)\n })\n } else {\n // SSR: evaluate once synchronously (no effects on server)\n ctx.add(id, buildEntry(input()))\n }\n } else {\n ctx.add(id, buildEntry(input))\n onMount(() => {\n syncDom(ctx)\n })\n }\n\n onUnmount(() => {\n ctx.remove(id)\n syncDom(ctx)\n })\n}\n"],"mappings":"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"}
|
package/lib/provider.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","names":[],"sources":["../src/context.ts","../src/provider.ts"],"sourcesContent":["import { createContext } from '@pyreon/core'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface HeadTag {\n /** HTML tag name */\n tag: 'title' | 'meta' | 'link' | 'script' | 'style' | 'base' | 'noscript'\n /**\n * Deduplication key. Tags with the same key replace each other;\n * innermost component (last added) wins.\n * Example: all components setting the page title use key \"title\".\n */\n key?: string\n /** HTML attributes for the tag */\n props?: Record<string, string>\n /** Text content — for <title>, <script>, <style>, <noscript> */\n children?: string\n}\n\n// ─── Strict tag types ────────────────────────────────────────────────────────\n\n/** Standard `<meta>` tag attributes. Catches typos like `{ naem: \"description\" }`. */\nexport interface MetaTag {\n /** Standard meta name (e.g. \"description\", \"viewport\", \"robots\") */\n name?: string\n /** Open Graph / social property (e.g. \"og:title\", \"twitter:card\") */\n property?: string\n /** HTTP equivalent header (e.g. \"refresh\", \"content-type\") */\n 'http-equiv'?: string\n /** Value associated with name, property, or http-equiv */\n content?: string\n /** Document character encoding (e.g. \"utf-8\") */\n charset?: string\n /** Schema.org itemprop */\n itemprop?: string\n /** Media condition for applicability (e.g. \"(prefers-color-scheme: dark)\") */\n media?: string\n}\n\n/** Standard `<link>` tag attributes. */\nexport interface LinkTag {\n /** Relationship to the current document (e.g. \"stylesheet\", \"icon\", \"canonical\") */\n rel?: string\n /** URL of the linked resource */\n href?: string\n /** Resource type hint for preloading (e.g. \"style\", \"script\", \"font\") */\n as?: string\n /** MIME type (e.g. \"text/css\", \"image/png\") */\n type?: string\n /** Media query for conditional loading */\n media?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Icon sizes (e.g. \"32x32\", \"any\") */\n sizes?: string\n /** Language of the linked resource */\n hreflang?: string\n /** Title for the link (used for alternate stylesheets) */\n title?: string\n /** Fetch priority hint */\n fetchpriority?: 'high' | 'low' | 'auto'\n /** Referrer policy */\n referrerpolicy?: string\n /** Image source set for preloading responsive images */\n imagesrcset?: string\n /** Image sizes for preloading responsive images */\n imagesizes?: string\n /** Disable the resource (for stylesheets) */\n disabled?: string\n /** Color for mask-icon */\n color?: string\n}\n\n/** Standard `<script>` tag attributes. */\nexport interface ScriptTag {\n /** External script URL */\n src?: string\n /** Script MIME type or module type (e.g. \"module\", \"importmap\") */\n type?: string\n /** Load asynchronously */\n async?: string\n /** Defer execution until document is parsed */\n defer?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Exclude from module-supporting browsers */\n nomodule?: string\n /** Referrer policy */\n referrerpolicy?: string\n /** Fetch priority hint */\n fetchpriority?: string\n /** Inline script content */\n children?: string\n}\n\n/** Standard `<style>` tag attributes. */\nexport interface StyleTag {\n /** Inline CSS content (required) */\n children: string\n /** Media query for conditional styles */\n media?: string\n /** Nonce for CSP */\n nonce?: string\n /** Title for alternate stylesheets */\n title?: string\n /** Render-blocking behavior */\n blocking?: string\n}\n\n/** Standard `<base>` tag attributes. */\nexport interface BaseTag {\n /** Base URL for relative URLs in the document */\n href?: string\n /** Default target for links and forms */\n target?: '_blank' | '_self' | '_parent' | '_top'\n}\n\nexport interface UseHeadInput {\n title?: string\n /**\n * Title template — use `%s` as a placeholder for the page title.\n * Applied to the resolved title after deduplication.\n * @example useHead({ titleTemplate: \"%s | My App\" })\n */\n titleTemplate?: string | ((title: string) => string)\n meta?: MetaTag[]\n link?: LinkTag[]\n script?: ScriptTag[]\n style?: StyleTag[]\n noscript?: { children: string }[]\n /** Convenience: emits a <script type=\"application/ld+json\"> tag with JSON.stringify'd content */\n jsonLd?: Record<string, unknown> | Record<string, unknown>[]\n base?: BaseTag\n /** Attributes to set on the <html> element (e.g. { lang: \"en\", dir: \"ltr\" }) */\n htmlAttrs?: Record<string, string>\n /** Attributes to set on the <body> element (e.g. { class: \"dark\" }) */\n bodyAttrs?: Record<string, string>\n}\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport interface HeadEntry {\n tags: HeadTag[]\n titleTemplate?: string | ((title: string) => string) | undefined\n htmlAttrs?: Record<string, string> | undefined\n bodyAttrs?: Record<string, string> | undefined\n}\n\nexport interface HeadContextValue {\n add(id: symbol, entry: HeadEntry): void\n remove(id: symbol): void\n /** Returns deduplicated tags — last-added entry wins per key */\n resolve(): HeadTag[]\n /** Returns the merged titleTemplate (last-added wins) */\n resolveTitleTemplate(): (string | ((title: string) => string)) | undefined\n /** Returns merged htmlAttrs (later entries override earlier) */\n resolveHtmlAttrs(): Record<string, string>\n /** Returns merged bodyAttrs (later entries override earlier) */\n resolveBodyAttrs(): Record<string, string>\n}\n\nexport function createHeadContext(): HeadContextValue {\n const map = new Map<symbol, HeadEntry>()\n\n // ── Cached resolve ───────────────────────────────────────────────────────\n let dirty = true\n let cachedTags: HeadTag[] = []\n let cachedTitleTemplate: (string | ((title: string) => string)) | undefined\n let cachedHtmlAttrs: Record<string, string> = {}\n let cachedBodyAttrs: Record<string, string> = {}\n\n function rebuild(): void {\n if (!dirty) return\n dirty = false\n\n const keyed = new Map<string, HeadTag>()\n const unkeyed: HeadTag[] = []\n let titleTemplate: (string | ((title: string) => string)) | undefined\n const htmlAttrs: Record<string, string> = {}\n const bodyAttrs: Record<string, string> = {}\n\n for (const entry of map.values()) {\n for (const tag of entry.tags) {\n if (tag.key) keyed.set(tag.key, tag)\n else unkeyed.push(tag)\n }\n if (entry.titleTemplate !== undefined) titleTemplate = entry.titleTemplate\n if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs)\n if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs)\n }\n\n cachedTags = [...keyed.values(), ...unkeyed]\n cachedTitleTemplate = titleTemplate\n cachedHtmlAttrs = htmlAttrs\n cachedBodyAttrs = bodyAttrs\n }\n\n return {\n add(id, entry) {\n map.set(id, entry)\n dirty = true\n },\n remove(id) {\n map.delete(id)\n dirty = true\n },\n resolve() {\n rebuild()\n return cachedTags\n },\n resolveTitleTemplate() {\n rebuild()\n return cachedTitleTemplate\n },\n resolveHtmlAttrs() {\n rebuild()\n return cachedHtmlAttrs\n },\n resolveBodyAttrs() {\n rebuild()\n return cachedBodyAttrs\n },\n }\n}\n\nexport const HeadContext = createContext<HeadContextValue | null>(null)\n","import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'\nimport { provide } from '@pyreon/core'\nimport type { HeadContextValue } from './context'\nimport { createHeadContext, HeadContext } from './context'\n\nexport interface HeadProviderProps extends Props {\n context?: HeadContextValue | undefined\n children?: VNodeChild\n}\n\n/**\n * Provides a HeadContextValue to all descendant components.\n * Wrap your app root with this to enable useHead() throughout the tree.\n *\n * If no `context` prop is passed, a new HeadContext is created automatically.\n *\n * @example\n * // Auto-create context:\n * <HeadProvider><App /></HeadProvider>\n *\n * // Explicit context (e.g. for SSR):\n * const headCtx = createHeadContext()\n * mount(h(HeadProvider, { context: headCtx }, h(App, null)), root)\n */\nexport const HeadProvider: ComponentFn<HeadProviderProps> = (props) => {\n const ctx = props.context ?? createHeadContext()\n provide(HeadContext, ctx)\n\n const ch = props.children\n return typeof ch === 'function' ? (ch as () => VNodeChild)() : ch\n}\n"],"mappings":";;;AAqKA,SAAgB,oBAAsC;CACpD,MAAM,sBAAM,IAAI,KAAwB;CAGxC,IAAI,QAAQ;CACZ,IAAI,aAAwB,EAAE;CAC9B,IAAI;CACJ,IAAI,kBAA0C,EAAE;CAChD,IAAI,kBAA0C,EAAE;CAEhD,SAAS,UAAgB;AACvB,MAAI,CAAC,MAAO;AACZ,UAAQ;EAER,MAAM,wBAAQ,IAAI,KAAsB;EACxC,MAAM,UAAqB,EAAE;EAC7B,IAAI;EACJ,MAAM,YAAoC,EAAE;EAC5C,MAAM,YAAoC,EAAE;AAE5C,OAAK,MAAM,SAAS,IAAI,QAAQ,EAAE;AAChC,QAAK,MAAM,OAAO,MAAM,KACtB,KAAI,IAAI,IAAK,OAAM,IAAI,IAAI,KAAK,IAAI;OAC/B,SAAQ,KAAK,IAAI;AAExB,OAAI,MAAM,kBAAkB,OAAW,iBAAgB,MAAM;AAC7D,OAAI,MAAM,UAAW,QAAO,OAAO,WAAW,MAAM,UAAU;AAC9D,OAAI,MAAM,UAAW,QAAO,OAAO,WAAW,MAAM,UAAU;;AAGhE,eAAa,CAAC,GAAG,MAAM,QAAQ,EAAE,GAAG,QAAQ;AAC5C,wBAAsB;AACtB,oBAAkB;AAClB,oBAAkB;;AAGpB,QAAO;EACL,IAAI,IAAI,OAAO;AACb,OAAI,IAAI,IAAI,MAAM;AAClB,WAAQ;;EAEV,OAAO,IAAI;AACT,OAAI,OAAO,GAAG;AACd,WAAQ;;EAEV,UAAU;AACR,YAAS;AACT,UAAO;;EAET,uBAAuB;AACrB,YAAS;AACT,UAAO;;EAET,mBAAmB;AACjB,YAAS;AACT,UAAO;;EAET,mBAAmB;AACjB,YAAS;AACT,UAAO;;EAEV;;AAGH,MAAa,cAAc,cAAuC,KAAK;;;;;;;;;;;;;;;;;;AC7MvE,MAAa,gBAAgD,UAAU;AAErE,SAAQ,aADI,MAAM,WAAW,mBAAmB,CACvB;CAEzB,MAAM,KAAK,MAAM;AACjB,QAAO,OAAO,OAAO,aAAc,IAAyB,GAAG"}
|
package/lib/ssr.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ssr.js","names":[],"sources":["../src/context.ts","../src/ssr.ts"],"sourcesContent":["import { createContext } from '@pyreon/core'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface HeadTag {\n /** HTML tag name */\n tag: 'title' | 'meta' | 'link' | 'script' | 'style' | 'base' | 'noscript'\n /**\n * Deduplication key. Tags with the same key replace each other;\n * innermost component (last added) wins.\n * Example: all components setting the page title use key \"title\".\n */\n key?: string\n /** HTML attributes for the tag */\n props?: Record<string, string>\n /** Text content — for <title>, <script>, <style>, <noscript> */\n children?: string\n}\n\n// ─── Strict tag types ────────────────────────────────────────────────────────\n\n/** Standard `<meta>` tag attributes. Catches typos like `{ naem: \"description\" }`. */\nexport interface MetaTag {\n /** Standard meta name (e.g. \"description\", \"viewport\", \"robots\") */\n name?: string\n /** Open Graph / social property (e.g. \"og:title\", \"twitter:card\") */\n property?: string\n /** HTTP equivalent header (e.g. \"refresh\", \"content-type\") */\n 'http-equiv'?: string\n /** Value associated with name, property, or http-equiv */\n content?: string\n /** Document character encoding (e.g. \"utf-8\") */\n charset?: string\n /** Schema.org itemprop */\n itemprop?: string\n /** Media condition for applicability (e.g. \"(prefers-color-scheme: dark)\") */\n media?: string\n}\n\n/** Standard `<link>` tag attributes. */\nexport interface LinkTag {\n /** Relationship to the current document (e.g. \"stylesheet\", \"icon\", \"canonical\") */\n rel?: string\n /** URL of the linked resource */\n href?: string\n /** Resource type hint for preloading (e.g. \"style\", \"script\", \"font\") */\n as?: string\n /** MIME type (e.g. \"text/css\", \"image/png\") */\n type?: string\n /** Media query for conditional loading */\n media?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Icon sizes (e.g. \"32x32\", \"any\") */\n sizes?: string\n /** Language of the linked resource */\n hreflang?: string\n /** Title for the link (used for alternate stylesheets) */\n title?: string\n /** Fetch priority hint */\n fetchpriority?: 'high' | 'low' | 'auto'\n /** Referrer policy */\n referrerpolicy?: string\n /** Image source set for preloading responsive images */\n imagesrcset?: string\n /** Image sizes for preloading responsive images */\n imagesizes?: string\n /** Disable the resource (for stylesheets) */\n disabled?: string\n /** Color for mask-icon */\n color?: string\n}\n\n/** Standard `<script>` tag attributes. */\nexport interface ScriptTag {\n /** External script URL */\n src?: string\n /** Script MIME type or module type (e.g. \"module\", \"importmap\") */\n type?: string\n /** Load asynchronously */\n async?: string\n /** Defer execution until document is parsed */\n defer?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Exclude from module-supporting browsers */\n nomodule?: string\n /** Referrer policy */\n referrerpolicy?: string\n /** Fetch priority hint */\n fetchpriority?: string\n /** Inline script content */\n children?: string\n}\n\n/** Standard `<style>` tag attributes. */\nexport interface StyleTag {\n /** Inline CSS content (required) */\n children: string\n /** Media query for conditional styles */\n media?: string\n /** Nonce for CSP */\n nonce?: string\n /** Title for alternate stylesheets */\n title?: string\n /** Render-blocking behavior */\n blocking?: string\n}\n\n/** Standard `<base>` tag attributes. */\nexport interface BaseTag {\n /** Base URL for relative URLs in the document */\n href?: string\n /** Default target for links and forms */\n target?: '_blank' | '_self' | '_parent' | '_top'\n}\n\nexport interface UseHeadInput {\n title?: string\n /**\n * Title template — use `%s` as a placeholder for the page title.\n * Applied to the resolved title after deduplication.\n * @example useHead({ titleTemplate: \"%s | My App\" })\n */\n titleTemplate?: string | ((title: string) => string)\n meta?: MetaTag[]\n link?: LinkTag[]\n script?: ScriptTag[]\n style?: StyleTag[]\n noscript?: { children: string }[]\n /** Convenience: emits a <script type=\"application/ld+json\"> tag with JSON.stringify'd content */\n jsonLd?: Record<string, unknown> | Record<string, unknown>[]\n base?: BaseTag\n /** Attributes to set on the <html> element (e.g. { lang: \"en\", dir: \"ltr\" }) */\n htmlAttrs?: Record<string, string>\n /** Attributes to set on the <body> element (e.g. { class: \"dark\" }) */\n bodyAttrs?: Record<string, string>\n}\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport interface HeadEntry {\n tags: HeadTag[]\n titleTemplate?: string | ((title: string) => string) | undefined\n htmlAttrs?: Record<string, string> | undefined\n bodyAttrs?: Record<string, string> | undefined\n}\n\nexport interface HeadContextValue {\n add(id: symbol, entry: HeadEntry): void\n remove(id: symbol): void\n /** Returns deduplicated tags — last-added entry wins per key */\n resolve(): HeadTag[]\n /** Returns the merged titleTemplate (last-added wins) */\n resolveTitleTemplate(): (string | ((title: string) => string)) | undefined\n /** Returns merged htmlAttrs (later entries override earlier) */\n resolveHtmlAttrs(): Record<string, string>\n /** Returns merged bodyAttrs (later entries override earlier) */\n resolveBodyAttrs(): Record<string, string>\n}\n\nexport function createHeadContext(): HeadContextValue {\n const map = new Map<symbol, HeadEntry>()\n\n // ── Cached resolve ───────────────────────────────────────────────────────\n let dirty = true\n let cachedTags: HeadTag[] = []\n let cachedTitleTemplate: (string | ((title: string) => string)) | undefined\n let cachedHtmlAttrs: Record<string, string> = {}\n let cachedBodyAttrs: Record<string, string> = {}\n\n function rebuild(): void {\n if (!dirty) return\n dirty = false\n\n const keyed = new Map<string, HeadTag>()\n const unkeyed: HeadTag[] = []\n let titleTemplate: (string | ((title: string) => string)) | undefined\n const htmlAttrs: Record<string, string> = {}\n const bodyAttrs: Record<string, string> = {}\n\n for (const entry of map.values()) {\n for (const tag of entry.tags) {\n if (tag.key) keyed.set(tag.key, tag)\n else unkeyed.push(tag)\n }\n if (entry.titleTemplate !== undefined) titleTemplate = entry.titleTemplate\n if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs)\n if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs)\n }\n\n cachedTags = [...keyed.values(), ...unkeyed]\n cachedTitleTemplate = titleTemplate\n cachedHtmlAttrs = htmlAttrs\n cachedBodyAttrs = bodyAttrs\n }\n\n return {\n add(id, entry) {\n map.set(id, entry)\n dirty = true\n },\n remove(id) {\n map.delete(id)\n dirty = true\n },\n resolve() {\n rebuild()\n return cachedTags\n },\n resolveTitleTemplate() {\n rebuild()\n return cachedTitleTemplate\n },\n resolveHtmlAttrs() {\n rebuild()\n return cachedHtmlAttrs\n },\n resolveBodyAttrs() {\n rebuild()\n return cachedBodyAttrs\n },\n }\n}\n\nexport const HeadContext = createContext<HeadContextValue | null>(null)\n","import type { ComponentFn, VNode } from '@pyreon/core'\nimport { h, pushContext } from '@pyreon/core'\nimport { renderToString } from '@pyreon/runtime-server'\nimport type { HeadTag } from './context'\nimport { createHeadContext, HeadContext } from './context'\n\nconst VOID_TAGS = new Set(['meta', 'link', 'base'])\n\n/**\n * Render a Pyreon app to an HTML fragment + a serialized <head> string.\n *\n * The returned `head` string can be injected directly into your HTML template:\n *\n * @example\n * const { html, head } = await renderWithHead(h(App, null))\n * const page = `<!DOCTYPE html>\n * <html>\n * <head>\n * <meta charset=\"UTF-8\" />\n * ${head}\n * </head>\n * <body><div id=\"app\">${html}</div></body>\n * </html>`\n */\nexport interface RenderWithHeadResult {\n html: string\n head: string\n /** Attributes to set on the <html> element */\n htmlAttrs: Record<string, string>\n /** Attributes to set on the <body> element */\n bodyAttrs: Record<string, string>\n}\n\nexport async function renderWithHead(app: VNode): Promise<RenderWithHeadResult> {\n const ctx = createHeadContext()\n\n // HeadInjector runs inside renderToString's ALS scope, so pushContext reaches\n // the per-request context stack rather than the module-level fallback stack.\n function HeadInjector(): VNode {\n pushContext(new Map([[HeadContext.id, ctx]]))\n return app\n }\n\n const html = await renderToString(h(HeadInjector as ComponentFn, null))\n const titleTemplate = ctx.resolveTitleTemplate()\n const head = ctx\n .resolve()\n .map((tag) => serializeTag(tag, titleTemplate))\n .join('\\n ')\n return {\n html,\n head,\n htmlAttrs: ctx.resolveHtmlAttrs(),\n bodyAttrs: ctx.resolveBodyAttrs(),\n }\n}\n\nfunction serializeTag(tag: HeadTag, titleTemplate?: string | ((title: string) => string)): string {\n if (tag.tag === 'title') {\n const raw = tag.children || ''\n const title = titleTemplate\n ? typeof titleTemplate === 'function'\n ? titleTemplate(raw)\n : titleTemplate.replace(/%s/g, raw)\n : raw\n return `<title>${esc(title)}</title>`\n }\n const props = tag.props as Record<string, string> | undefined\n const attrs = props\n ? Object.entries(props)\n .map(([k, v]) => `${k}=\"${esc(v)}\"`)\n .join(' ')\n : ''\n const open = attrs ? `<${tag.tag} ${attrs}` : `<${tag.tag}`\n if (VOID_TAGS.has(tag.tag)) return `${open} />`\n const content = tag.children || ''\n // Escape sequences that could break out of script/style/noscript blocks:\n // 1. Closing tags like </script> — use Unicode escape in the slash\n // 2. HTML comment openers <!-- that could confuse parsers\n const body = content.replace(/<\\/(script|style|noscript)/gi, '<\\\\/$1').replace(/<!--/g, '<\\\\!--')\n return `${open}>${body}</${tag.tag}>`\n}\n\nconst ESC_RE = /[&<>\"]/g\nconst ESC_MAP: Record<string, string> = { '&': '&', '<': '<', '>': '>', '\"': '"' }\n\nfunction esc(s: string): string {\n return ESC_RE.test(s) ? s.replace(ESC_RE, (ch) => ESC_MAP[ch] as string) : s\n}\n"],"mappings":"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"}
|
package/lib/types/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/context.ts","../../../src/provider.ts","../../../src/use-head.ts"],"mappings":";;;;UAIiB,OAAA;;EAEf,GAAA;;AAFF;;;;EAQE,GAAA;EAAA;EAEA,KAAA,GAAQ,MAAA;EAAA;EAER,QAAA;AAAA;;UAMe,OAAA;EAAO;EAEtB,IAAA;EAFsB;EAItB,QAAA;EAAA;EAEA,YAAA;EAEA;EAAA,OAAA;EAIA;EAFA,OAAA;EAIK;EAFL,QAAA;EAMe;EAJf,KAAA;AAAA;;UAIe,OAAA;EAIf;EAFA,GAAA;EAMA;EAJA,IAAA;EAQA;EANA,EAAA;EAUA;EARA,IAAA;EAYA;EAVA,KAAA;EAcA;EAZA,WAAA;EAgBA;EAdA,SAAA;EAkBA;EAhBA,KAAA;EAgBK;EAdL,QAAA;EAkBwB;EAhBxB,KAAA;EAgBwB;EAdxB,aAAA;EAkBA;EAhBA,cAAA;EAoBA;EAlBA,WAAA;EAsBA;EApBA,UAAA;EAwBA;EAtBA,QAAA;EA0BA;EAxBA,KAAA;AAAA;AA4BF;AAAA,UAxBiB,SAAA;;EAEf,GAAA;EAwBA;EAtBA,IAAA;EA0BA;EAxBA,KAAA;EA4BA;EA1BA,KAAA;EA0BQ;EAxBR,WAAA;EA4BsB;EA1BtB,SAAA;EA4BA;EA1BA,QAAA;EA+Be;EA7Bf,cAAA;;EAEA,aAAA;EAoCO;EAlCP,QAAA;AAAA;;UAIe,QAAA;EAoCR;EAlCP,QAAA;EAsCY;EApCZ,KAAA;EAoCkB;EAlClB,KAAA;EAsBA;EApBA,KAAA;EAqBA;EAnBA,QAAA;AAAA;;UAIe,OAAA;EAiBN;EAfT,IAAA;EAgBQ;EAdR,MAAA;AAAA;AAAA,UAGe,YAAA;EACf,KAAA;EAamC;;;;;EAPnC,aAAA,cAA2B,KAAA;EAC3B,IAAA,GAAO,OAAA;EACP,IAAA,GAAO,OAAA;EACP,MAAA,GAAS,SAAA;EACT,KAAA,GAAQ,QAAA;EACR,QAAA;IAAa,QAAA;EAAA;EAeD;EAbZ,MAAA,GAAS,MAAA,oBAA0B,MAAA;EACnC,IAAA,GAAO,OAAA;EAaW;EAXlB,SAAA,GAAY,MAAA;EAQN;EANN,SAAA,GAAY,MAAA;AAAA;AAAA,UAKG,SAAA;EACf,IAAA,EAAM,OAAA;EACN,aAAA,cAA2B,KAAA;EAC3B,SAAA,GAAY,MAAA;EACZ,SAAA,GAAY,MAAA;AAAA;AAAA,UAGG,gBAAA;EACf,GAAA,CAAI,EAAA,UAAY,KAAA,EAAO,SAAA;EACvB,MAAA,CAAO,EAAA;EADgB;EAGvB,OAAA,IAAW,OAAA;EAIS;EAFpB,oBAAA,gBAAoC,KAAA;EAIV;EAF1B,gBAAA,IAAoB,MAAA;EAPpB;EASA,gBAAA,IAAoB,MAAA;AAAA;AAAA,iBAGN,iBAAA,CAAA,GAAqB,gBAAA;AAAA,cAgExB,WAAA,EAAW,aAAA,CAAA,OAAA,CAAA,gBAAA;;;UChOP,iBAAA,SAA0B,KAAA;EACzC,OAAA,GAAU,gBAAA;EACV,QAAA,GAAW,UAAA;AAAA;;;;;;;;;;ADeb;;;;;cCEa,YAAA,EAAc,WAAA,CAAY,iBAAA;;;;;;ADpBvC;;;;;;;;iBEkFgB,OAAA,CAAQ,KAAA,EAAO,YAAA,UAAsB,YAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"provider2.d.ts","names":[],"sources":["../../../src/context.ts","../../../src/provider.ts"],"mappings":";;;UAIiB,OAAA;;EAEf,GAAA;EAFe;;;;;EAQf,GAAA;EAEA;EAAA,KAAA,GAAQ,MAAA;EAER;EAAA,QAAA;AAAA;AAAA,UAiIe,SAAA;EACf,IAAA,EAAM,OAAA;EACN,aAAA,cAA2B,KAAA;EAC3B,SAAA,GAAY,MAAA;EACZ,SAAA,GAAY,MAAA;AAAA;AAAA,UAGG,gBAAA;EACf,GAAA,CAAI,EAAA,UAAY,KAAA,EAAO,SAAA;EACvB,MAAA,CAAO,EAAA;;EAEP,OAAA,IAAW,OAAA;;EAEX,oBAAA,gBAAoC,KAAA;;EAEpC,gBAAA,IAAoB,MAAA;;EAEpB,gBAAA,IAAoB,MAAA;AAAA;;;UC7JL,iBAAA,SAA0B,KAAA;EACzC,OAAA,GAAU,gBAAA;EACV,QAAA,GAAW,UAAA;AAAA;;;;;;;;;AD0Ib;;;;;;cCzHa,YAAA,EAAc,WAAA,CAAY,iBAAA"}
|
package/lib/types/ssr.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ssr2.d.ts","names":[],"sources":["../../../src/ssr.ts"],"mappings":";;;;;AAwBA;;;;;;;;;;;;AASA;;UATiB,oBAAA;EACf,IAAA;EACA,IAAA;EAOgD;EALhD,SAAA,EAAW,MAAA;EAK4C;EAHvD,SAAA,EAAW,MAAA;AAAA;AAAA,iBAGS,cAAA,CAAe,GAAA,EAAK,KAAA,GAAQ,OAAA,CAAQ,oBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-head2.d.ts","names":[],"sources":["../../../src/context.ts","../../../src/use-head.ts"],"mappings":";;UAsBiB,OAAA;EAkBA;EAhBf,IAAA;;EAEA,QAAA;EAgBA;EAdA,YAAA;EAkBA;EAhBA,OAAA;EAoBA;EAlBA,OAAA;EAsBA;EApBA,QAAA;EAwBA;EAtBA,KAAA;AAAA;;UAIe,OAAA;EA4Bf;EA1BA,GAAA;EA8BA;EA5BA,IAAA;EA4BK;EA1BL,EAAA;EA8BwB;EA5BxB,IAAA;EA4BwB;EA1BxB,KAAA;EA8BA;EA5BA,WAAA;EAgCA;EA9BA,SAAA;EAkCA;EAhCA,KAAA;EAoCA;EAlCA,QAAA;EAsCA;EApCA,KAAA;EAoCQ;EAlCR,aAAA;EAsCuB;EApCvB,cAAA;EAoCuB;EAlCvB,WAAA;EAsCA;EApCA,UAAA;EAwCA;EAtCA,QAAA;EAwCQ;EAtCR,KAAA;AAAA;;UAIe,SAAA;EAwCf;EAtCA,GAAA;EA2Ce;EAzCf,IAAA;;EAEA,KAAA;EAgDO;EA9CP,KAAA;EAgDQ;EA9CR,WAAA;EAiDmC;EA/CnC,SAAA;EAkDY;EAhDZ,QAAA;EAkDkB;EAhDlB,cAAA;EA8BA;EA5BA,aAAA;EAkC2B;EAhC3B,QAAA;AAAA;;UAIe,QAAA;EA+Bf;EA7BA,QAAA;EA8BA;EA5BA,KAAA;EA6BA;EA3BA,KAAA;EA6BA;EA3BA,KAAA;EA2BmC;EAzBnC,QAAA;AAAA;;UAIe,OAAA;EA0Bf;EAxBA,IAAA;EAwBkB;EAtBlB,MAAA;AAAA;AAAA,UAGe,YAAA;EACf,KAAA;ECpCc;;;;;ED0Cd,aAAA,cAA2B,KAAA;EAC3B,IAAA,GAAO,OAAA;EACP,IAAA,GAAO,OAAA;EACP,MAAA,GAAS,SAAA;EACT,KAAA,GAAQ,QAAA;EACR,QAAA;IAAa,QAAA;EAAA;;EAEb,MAAA,GAAS,MAAA,oBAA0B,MAAA;EACnC,IAAA,GAAO,OAAA;;EAEP,SAAA,GAAY,MAAA;;EAEZ,SAAA,GAAY,MAAA;AAAA;;;;;AAtHd;;;;;;;;;iBCgEgB,OAAA,CAAQ,KAAA,EAAO,YAAA,UAAsB,YAAA"}
|
package/lib/use-head.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-head.js","names":[],"sources":["../src/context.ts","../src/dom.ts","../src/use-head.ts"],"sourcesContent":["import { createContext } from '@pyreon/core'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface HeadTag {\n /** HTML tag name */\n tag: 'title' | 'meta' | 'link' | 'script' | 'style' | 'base' | 'noscript'\n /**\n * Deduplication key. Tags with the same key replace each other;\n * innermost component (last added) wins.\n * Example: all components setting the page title use key \"title\".\n */\n key?: string\n /** HTML attributes for the tag */\n props?: Record<string, string>\n /** Text content — for <title>, <script>, <style>, <noscript> */\n children?: string\n}\n\n// ─── Strict tag types ────────────────────────────────────────────────────────\n\n/** Standard `<meta>` tag attributes. Catches typos like `{ naem: \"description\" }`. */\nexport interface MetaTag {\n /** Standard meta name (e.g. \"description\", \"viewport\", \"robots\") */\n name?: string\n /** Open Graph / social property (e.g. \"og:title\", \"twitter:card\") */\n property?: string\n /** HTTP equivalent header (e.g. \"refresh\", \"content-type\") */\n 'http-equiv'?: string\n /** Value associated with name, property, or http-equiv */\n content?: string\n /** Document character encoding (e.g. \"utf-8\") */\n charset?: string\n /** Schema.org itemprop */\n itemprop?: string\n /** Media condition for applicability (e.g. \"(prefers-color-scheme: dark)\") */\n media?: string\n}\n\n/** Standard `<link>` tag attributes. */\nexport interface LinkTag {\n /** Relationship to the current document (e.g. \"stylesheet\", \"icon\", \"canonical\") */\n rel?: string\n /** URL of the linked resource */\n href?: string\n /** Resource type hint for preloading (e.g. \"style\", \"script\", \"font\") */\n as?: string\n /** MIME type (e.g. \"text/css\", \"image/png\") */\n type?: string\n /** Media query for conditional loading */\n media?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Icon sizes (e.g. \"32x32\", \"any\") */\n sizes?: string\n /** Language of the linked resource */\n hreflang?: string\n /** Title for the link (used for alternate stylesheets) */\n title?: string\n /** Fetch priority hint */\n fetchpriority?: 'high' | 'low' | 'auto'\n /** Referrer policy */\n referrerpolicy?: string\n /** Image source set for preloading responsive images */\n imagesrcset?: string\n /** Image sizes for preloading responsive images */\n imagesizes?: string\n /** Disable the resource (for stylesheets) */\n disabled?: string\n /** Color for mask-icon */\n color?: string\n}\n\n/** Standard `<script>` tag attributes. */\nexport interface ScriptTag {\n /** External script URL */\n src?: string\n /** Script MIME type or module type (e.g. \"module\", \"importmap\") */\n type?: string\n /** Load asynchronously */\n async?: string\n /** Defer execution until document is parsed */\n defer?: string\n /** CORS mode */\n crossorigin?: string\n /** Subresource integrity hash */\n integrity?: string\n /** Exclude from module-supporting browsers */\n nomodule?: string\n /** Referrer policy */\n referrerpolicy?: string\n /** Fetch priority hint */\n fetchpriority?: string\n /** Inline script content */\n children?: string\n}\n\n/** Standard `<style>` tag attributes. */\nexport interface StyleTag {\n /** Inline CSS content (required) */\n children: string\n /** Media query for conditional styles */\n media?: string\n /** Nonce for CSP */\n nonce?: string\n /** Title for alternate stylesheets */\n title?: string\n /** Render-blocking behavior */\n blocking?: string\n}\n\n/** Standard `<base>` tag attributes. */\nexport interface BaseTag {\n /** Base URL for relative URLs in the document */\n href?: string\n /** Default target for links and forms */\n target?: '_blank' | '_self' | '_parent' | '_top'\n}\n\nexport interface UseHeadInput {\n title?: string\n /**\n * Title template — use `%s` as a placeholder for the page title.\n * Applied to the resolved title after deduplication.\n * @example useHead({ titleTemplate: \"%s | My App\" })\n */\n titleTemplate?: string | ((title: string) => string)\n meta?: MetaTag[]\n link?: LinkTag[]\n script?: ScriptTag[]\n style?: StyleTag[]\n noscript?: { children: string }[]\n /** Convenience: emits a <script type=\"application/ld+json\"> tag with JSON.stringify'd content */\n jsonLd?: Record<string, unknown> | Record<string, unknown>[]\n base?: BaseTag\n /** Attributes to set on the <html> element (e.g. { lang: \"en\", dir: \"ltr\" }) */\n htmlAttrs?: Record<string, string>\n /** Attributes to set on the <body> element (e.g. { class: \"dark\" }) */\n bodyAttrs?: Record<string, string>\n}\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport interface HeadEntry {\n tags: HeadTag[]\n titleTemplate?: string | ((title: string) => string) | undefined\n htmlAttrs?: Record<string, string> | undefined\n bodyAttrs?: Record<string, string> | undefined\n}\n\nexport interface HeadContextValue {\n add(id: symbol, entry: HeadEntry): void\n remove(id: symbol): void\n /** Returns deduplicated tags — last-added entry wins per key */\n resolve(): HeadTag[]\n /** Returns the merged titleTemplate (last-added wins) */\n resolveTitleTemplate(): (string | ((title: string) => string)) | undefined\n /** Returns merged htmlAttrs (later entries override earlier) */\n resolveHtmlAttrs(): Record<string, string>\n /** Returns merged bodyAttrs (later entries override earlier) */\n resolveBodyAttrs(): Record<string, string>\n}\n\nexport function createHeadContext(): HeadContextValue {\n const map = new Map<symbol, HeadEntry>()\n\n // ── Cached resolve ───────────────────────────────────────────────────────\n let dirty = true\n let cachedTags: HeadTag[] = []\n let cachedTitleTemplate: (string | ((title: string) => string)) | undefined\n let cachedHtmlAttrs: Record<string, string> = {}\n let cachedBodyAttrs: Record<string, string> = {}\n\n function rebuild(): void {\n if (!dirty) return\n dirty = false\n\n const keyed = new Map<string, HeadTag>()\n const unkeyed: HeadTag[] = []\n let titleTemplate: (string | ((title: string) => string)) | undefined\n const htmlAttrs: Record<string, string> = {}\n const bodyAttrs: Record<string, string> = {}\n\n for (const entry of map.values()) {\n for (const tag of entry.tags) {\n if (tag.key) keyed.set(tag.key, tag)\n else unkeyed.push(tag)\n }\n if (entry.titleTemplate !== undefined) titleTemplate = entry.titleTemplate\n if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs)\n if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs)\n }\n\n cachedTags = [...keyed.values(), ...unkeyed]\n cachedTitleTemplate = titleTemplate\n cachedHtmlAttrs = htmlAttrs\n cachedBodyAttrs = bodyAttrs\n }\n\n return {\n add(id, entry) {\n map.set(id, entry)\n dirty = true\n },\n remove(id) {\n map.delete(id)\n dirty = true\n },\n resolve() {\n rebuild()\n return cachedTags\n },\n resolveTitleTemplate() {\n rebuild()\n return cachedTitleTemplate\n },\n resolveHtmlAttrs() {\n rebuild()\n return cachedHtmlAttrs\n },\n resolveBodyAttrs() {\n rebuild()\n return cachedBodyAttrs\n },\n }\n}\n\nexport const HeadContext = createContext<HeadContextValue | null>(null)\n","import type { HeadContextValue } from './context'\n\nconst ATTR = 'data-pyreon-head'\n\n/** Tracks managed elements by key — avoids querySelectorAll on every sync */\nconst managedElements = new Map<string, Element>()\n\n/**\n * Sync the resolved head tags to the real DOM <head>.\n * Uses incremental diffing: matches existing elements by key, patches attributes\n * in-place, adds new elements, and removes stale ones.\n * Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.\n * No-op on the server (typeof document === \"undefined\").\n */\nfunction patchExistingTag(\n found: Element,\n tag: { props: Record<string, unknown>; children: string },\n kept: Set<string>,\n): void {\n kept.add(found.getAttribute(ATTR) as string)\n patchAttrs(found, tag.props as Record<string, string>)\n const content = String(tag.children)\n if (found.textContent !== content) found.textContent = content\n}\n\nfunction createNewTag(tag: {\n tag: string\n props: Record<string, unknown>\n children: string\n key: unknown\n}): void {\n const el = document.createElement(tag.tag)\n const key = tag.key as string\n el.setAttribute(ATTR, key)\n for (const [k, v] of Object.entries(tag.props as Record<string, string>)) {\n el.setAttribute(k, v)\n }\n if (tag.children) el.textContent = tag.children\n document.head.appendChild(el)\n managedElements.set(key, el)\n}\n\nexport function syncDom(ctx: HeadContextValue): void {\n if (typeof document === 'undefined') return\n\n const tags = ctx.resolve()\n const titleTemplate = ctx.resolveTitleTemplate()\n\n // Seed from DOM on first sync, or re-seed if DOM was reset (e.g. between tests)\n let needsSeed = managedElements.size === 0\n if (!needsSeed) {\n // Check if a tracked element is still in the DOM\n const sample = managedElements.values().next().value\n if (sample && !sample.isConnected) {\n managedElements.clear()\n needsSeed = true\n }\n }\n if (needsSeed) {\n const existing = document.head.querySelectorAll(`[${ATTR}]`)\n for (const el of existing) {\n managedElements.set(el.getAttribute(ATTR) as string, el)\n }\n }\n\n const kept = new Set<string>()\n\n for (const tag of tags) {\n if (tag.tag === 'title') {\n document.title = applyTitleTemplate(String(tag.children), titleTemplate)\n continue\n }\n\n const key = tag.key as string\n const found = managedElements.get(key)\n\n if (found && found.tagName.toLowerCase() === tag.tag) {\n patchExistingTag(found, tag as { props: Record<string, unknown>; children: string }, kept)\n } else {\n if (found) {\n found.remove()\n managedElements.delete(key)\n }\n createNewTag(\n tag as { tag: string; props: Record<string, unknown>; children: string; key: unknown },\n )\n kept.add(key)\n }\n }\n\n // Remove stale elements\n for (const [key, el] of managedElements) {\n if (!kept.has(key)) {\n el.remove()\n managedElements.delete(key)\n }\n }\n\n syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs())\n syncElementAttrs(document.body, ctx.resolveBodyAttrs())\n}\n\n/** Patch an element's attributes to match the desired props. */\nfunction patchAttrs(el: Element, props: Record<string, string>): void {\n for (let i = el.attributes.length - 1; i >= 0; i--) {\n const attr = el.attributes[i]\n if (!attr || attr.name === ATTR) continue\n if (!(attr.name in props)) el.removeAttribute(attr.name)\n }\n for (const [k, v] of Object.entries(props)) {\n if (el.getAttribute(k) !== v) el.setAttribute(k, v)\n }\n}\n\nfunction applyTitleTemplate(\n title: string,\n template: string | ((t: string) => string) | undefined,\n): string {\n if (!template) return title\n if (typeof template === 'function') return template(title)\n return template.replace(/%s/g, title)\n}\n\n/** Sync pyreon-managed attributes on <html> or <body>. */\nfunction syncElementAttrs(el: Element, attrs: Record<string, string>): void {\n // Remove previously managed attrs that are no longer present\n const managed = el.getAttribute(`${ATTR}-attrs`)\n if (managed) {\n for (const name of managed.split(',')) {\n if (name && !(name in attrs)) el.removeAttribute(name)\n }\n }\n const keys: string[] = []\n for (const [k, v] of Object.entries(attrs)) {\n keys.push(k)\n if (el.getAttribute(k) !== v) el.setAttribute(k, v)\n }\n if (keys.length > 0) {\n el.setAttribute(`${ATTR}-attrs`, keys.join(','))\n } else if (managed) {\n el.removeAttribute(`${ATTR}-attrs`)\n }\n}\n","import { onMount, onUnmount, useContext } from '@pyreon/core'\nimport { effect } from '@pyreon/reactivity'\nimport type { HeadEntry, HeadTag, UseHeadInput } from './context'\nimport { HeadContext } from './context'\nimport { syncDom } from './dom'\n\n/** Cast a strict tag interface to the internal props format, stripping undefined values */\nfunction toProps(obj: Record<string, string | undefined>): Record<string, string> {\n const result: Record<string, string> = {}\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined) result[k] = v\n }\n return result\n}\n\nfunction buildEntry(o: UseHeadInput): HeadEntry {\n const tags: HeadTag[] = []\n if (o.title != null) tags.push({ tag: 'title', key: 'title', children: o.title })\n o.meta?.forEach((m, i) => {\n tags.push({\n tag: 'meta',\n key: m.name ?? m.property ?? `meta-${i}`,\n props: toProps(m as Record<string, string | undefined>),\n })\n })\n o.link?.forEach((l, i) => {\n tags.push({\n tag: 'link',\n key: l.href ? `link-${l.rel || ''}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,\n props: toProps(l as Record<string, string | undefined>),\n })\n })\n o.script?.forEach((s, i) => {\n const { children, ...rest } = s\n tags.push({\n tag: 'script',\n key: s.src ?? `script-${i}`,\n props: toProps(rest as Record<string, string | undefined>),\n ...(children != null ? { children } : {}),\n })\n })\n o.style?.forEach((s, i) => {\n const { children, ...rest } = s\n tags.push({\n tag: 'style',\n key: `style-${i}`,\n props: toProps(rest as Record<string, string | undefined>),\n children,\n })\n })\n o.noscript?.forEach((ns, i) => {\n tags.push({ tag: 'noscript', key: `noscript-${i}`, children: ns.children })\n })\n if (o.jsonLd) {\n tags.push({\n tag: 'script',\n key: 'jsonld',\n props: { type: 'application/ld+json' },\n children: JSON.stringify(o.jsonLd),\n })\n }\n if (o.base)\n tags.push({\n tag: 'base',\n key: 'base',\n props: toProps(o.base as Record<string, string | undefined>),\n })\n return {\n tags,\n titleTemplate: o.titleTemplate,\n htmlAttrs: o.htmlAttrs,\n bodyAttrs: o.bodyAttrs,\n }\n}\n\n/**\n * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)\n * for the current component.\n *\n * Accepts a static object or a reactive getter:\n * useHead({ title: \"My Page\", meta: [{ name: \"description\", content: \"...\" }] })\n * useHead(() => ({ title: `${count()} items` })) // updates when signal changes\n *\n * Tags are deduplicated by key — innermost component wins.\n * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.\n */\nexport function useHead(input: UseHeadInput | (() => UseHeadInput)): void {\n const ctx = useContext(HeadContext)\n if (!ctx) return // no HeadProvider — silently no-op\n\n const id = Symbol()\n\n if (typeof input === 'function') {\n if (typeof document !== 'undefined') {\n // CSR: reactive — re-register whenever signals change\n effect(() => {\n ctx.add(id, buildEntry(input()))\n syncDom(ctx)\n })\n } else {\n // SSR: evaluate once synchronously (no effects on server)\n ctx.add(id, buildEntry(input()))\n }\n } else {\n ctx.add(id, buildEntry(input))\n onMount(() => {\n syncDom(ctx)\n })\n }\n\n onUnmount(() => {\n ctx.remove(id)\n syncDom(ctx)\n })\n}\n"],"mappings":";;;;AAqOA,MAAa,cAAc,cAAuC,KAAK;;;;ACnOvE,MAAM,OAAO;;AAGb,MAAM,kCAAkB,IAAI,KAAsB;;;;;;;;AASlD,SAAS,iBACP,OACA,KACA,MACM;AACN,MAAK,IAAI,MAAM,aAAa,KAAK,CAAW;AAC5C,YAAW,OAAO,IAAI,MAAgC;CACtD,MAAM,UAAU,OAAO,IAAI,SAAS;AACpC,KAAI,MAAM,gBAAgB,QAAS,OAAM,cAAc;;AAGzD,SAAS,aAAa,KAKb;CACP,MAAM,KAAK,SAAS,cAAc,IAAI,IAAI;CAC1C,MAAM,MAAM,IAAI;AAChB,IAAG,aAAa,MAAM,IAAI;AAC1B,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,MAAgC,CACtE,IAAG,aAAa,GAAG,EAAE;AAEvB,KAAI,IAAI,SAAU,IAAG,cAAc,IAAI;AACvC,UAAS,KAAK,YAAY,GAAG;AAC7B,iBAAgB,IAAI,KAAK,GAAG;;AAG9B,SAAgB,QAAQ,KAA6B;AACnD,KAAI,OAAO,aAAa,YAAa;CAErC,MAAM,OAAO,IAAI,SAAS;CAC1B,MAAM,gBAAgB,IAAI,sBAAsB;CAGhD,IAAI,YAAY,gBAAgB,SAAS;AACzC,KAAI,CAAC,WAAW;EAEd,MAAM,SAAS,gBAAgB,QAAQ,CAAC,MAAM,CAAC;AAC/C,MAAI,UAAU,CAAC,OAAO,aAAa;AACjC,mBAAgB,OAAO;AACvB,eAAY;;;AAGhB,KAAI,WAAW;EACb,MAAM,WAAW,SAAS,KAAK,iBAAiB,IAAI,KAAK,GAAG;AAC5D,OAAK,MAAM,MAAM,SACf,iBAAgB,IAAI,GAAG,aAAa,KAAK,EAAY,GAAG;;CAI5D,MAAM,uBAAO,IAAI,KAAa;AAE9B,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,IAAI,QAAQ,SAAS;AACvB,YAAS,QAAQ,mBAAmB,OAAO,IAAI,SAAS,EAAE,cAAc;AACxE;;EAGF,MAAM,MAAM,IAAI;EAChB,MAAM,QAAQ,gBAAgB,IAAI,IAAI;AAEtC,MAAI,SAAS,MAAM,QAAQ,aAAa,KAAK,IAAI,IAC/C,kBAAiB,OAAO,KAA6D,KAAK;OACrF;AACL,OAAI,OAAO;AACT,UAAM,QAAQ;AACd,oBAAgB,OAAO,IAAI;;AAE7B,gBACE,IACD;AACD,QAAK,IAAI,IAAI;;;AAKjB,MAAK,MAAM,CAAC,KAAK,OAAO,gBACtB,KAAI,CAAC,KAAK,IAAI,IAAI,EAAE;AAClB,KAAG,QAAQ;AACX,kBAAgB,OAAO,IAAI;;AAI/B,kBAAiB,SAAS,iBAAiB,IAAI,kBAAkB,CAAC;AAClE,kBAAiB,SAAS,MAAM,IAAI,kBAAkB,CAAC;;;AAIzD,SAAS,WAAW,IAAa,OAAqC;AACpE,MAAK,IAAI,IAAI,GAAG,WAAW,SAAS,GAAG,KAAK,GAAG,KAAK;EAClD,MAAM,OAAO,GAAG,WAAW;AAC3B,MAAI,CAAC,QAAQ,KAAK,SAAS,KAAM;AACjC,MAAI,EAAE,KAAK,QAAQ,OAAQ,IAAG,gBAAgB,KAAK,KAAK;;AAE1D,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,CACxC,KAAI,GAAG,aAAa,EAAE,KAAK,EAAG,IAAG,aAAa,GAAG,EAAE;;AAIvD,SAAS,mBACP,OACA,UACQ;AACR,KAAI,CAAC,SAAU,QAAO;AACtB,KAAI,OAAO,aAAa,WAAY,QAAO,SAAS,MAAM;AAC1D,QAAO,SAAS,QAAQ,OAAO,MAAM;;;AAIvC,SAAS,iBAAiB,IAAa,OAAqC;CAE1E,MAAM,UAAU,GAAG,aAAa,GAAG,KAAK,QAAQ;AAChD,KAAI,SACF;OAAK,MAAM,QAAQ,QAAQ,MAAM,IAAI,CACnC,KAAI,QAAQ,EAAE,QAAQ,OAAQ,IAAG,gBAAgB,KAAK;;CAG1D,MAAM,OAAiB,EAAE;AACzB,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,OAAK,KAAK,EAAE;AACZ,MAAI,GAAG,aAAa,EAAE,KAAK,EAAG,IAAG,aAAa,GAAG,EAAE;;AAErD,KAAI,KAAK,SAAS,EAChB,IAAG,aAAa,GAAG,KAAK,SAAS,KAAK,KAAK,IAAI,CAAC;UACvC,QACT,IAAG,gBAAgB,GAAG,KAAK,QAAQ;;;;;;ACrIvC,SAAS,QAAQ,KAAiE;CAChF,MAAM,SAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,CACtC,KAAI,MAAM,OAAW,QAAO,KAAK;AAEnC,QAAO;;AAGT,SAAS,WAAW,GAA4B;CAC9C,MAAM,OAAkB,EAAE;AAC1B,KAAI,EAAE,SAAS,KAAM,MAAK,KAAK;EAAE,KAAK;EAAS,KAAK;EAAS,UAAU,EAAE;EAAO,CAAC;AACjF,GAAE,MAAM,SAAS,GAAG,MAAM;AACxB,OAAK,KAAK;GACR,KAAK;GACL,KAAK,EAAE,QAAQ,EAAE,YAAY,QAAQ;GACrC,OAAO,QAAQ,EAAwC;GACxD,CAAC;GACF;AACF,GAAE,MAAM,SAAS,GAAG,MAAM;AACxB,OAAK,KAAK;GACR,KAAK;GACL,KAAK,EAAE,OAAO,QAAQ,EAAE,OAAO,GAAG,GAAG,EAAE,SAAS,EAAE,MAAM,QAAQ,EAAE,QAAQ,QAAQ;GAClF,OAAO,QAAQ,EAAwC;GACxD,CAAC;GACF;AACF,GAAE,QAAQ,SAAS,GAAG,MAAM;EAC1B,MAAM,EAAE,UAAU,GAAG,SAAS;AAC9B,OAAK,KAAK;GACR,KAAK;GACL,KAAK,EAAE,OAAO,UAAU;GACxB,OAAO,QAAQ,KAA2C;GAC1D,GAAI,YAAY,OAAO,EAAE,UAAU,GAAG,EAAE;GACzC,CAAC;GACF;AACF,GAAE,OAAO,SAAS,GAAG,MAAM;EACzB,MAAM,EAAE,UAAU,GAAG,SAAS;AAC9B,OAAK,KAAK;GACR,KAAK;GACL,KAAK,SAAS;GACd,OAAO,QAAQ,KAA2C;GAC1D;GACD,CAAC;GACF;AACF,GAAE,UAAU,SAAS,IAAI,MAAM;AAC7B,OAAK,KAAK;GAAE,KAAK;GAAY,KAAK,YAAY;GAAK,UAAU,GAAG;GAAU,CAAC;GAC3E;AACF,KAAI,EAAE,OACJ,MAAK,KAAK;EACR,KAAK;EACL,KAAK;EACL,OAAO,EAAE,MAAM,uBAAuB;EACtC,UAAU,KAAK,UAAU,EAAE,OAAO;EACnC,CAAC;AAEJ,KAAI,EAAE,KACJ,MAAK,KAAK;EACR,KAAK;EACL,KAAK;EACL,OAAO,QAAQ,EAAE,KAA2C;EAC7D,CAAC;AACJ,QAAO;EACL;EACA,eAAe,EAAE;EACjB,WAAW,EAAE;EACb,WAAW,EAAE;EACd;;;;;;;;;;;;;AAcH,SAAgB,QAAQ,OAAkD;CACxE,MAAM,MAAM,WAAW,YAAY;AACnC,KAAI,CAAC,IAAK;CAEV,MAAM,KAAK,QAAQ;AAEnB,KAAI,OAAO,UAAU,WACnB,KAAI,OAAO,aAAa,YAEtB,cAAa;AACX,MAAI,IAAI,IAAI,WAAW,OAAO,CAAC,CAAC;AAChC,UAAQ,IAAI;GACZ;KAGF,KAAI,IAAI,IAAI,WAAW,OAAO,CAAC,CAAC;MAE7B;AACL,MAAI,IAAI,IAAI,WAAW,MAAM,CAAC;AAC9B,gBAAc;AACZ,WAAQ,IAAI;IACZ;;AAGJ,iBAAgB;AACd,MAAI,OAAO,GAAG;AACd,UAAQ,IAAI;GACZ"}
|