@unitninety/site-kit 0.0.0-stage → 1.6.1

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.
Files changed (204) hide show
  1. package/README.md +12 -2
  2. package/package.json +79 -4
  3. package/src/bridge/DraftBlocks.tsx +70 -0
  4. package/src/bridge/PreviewCommitBoundary.tsx +19 -0
  5. package/src/bridge/U90Bridge.tsx +54 -0
  6. package/src/bridge/caret.ts +317 -0
  7. package/src/bridge/client.tsx +66 -0
  8. package/src/bridge/createBridge.ts +1010 -0
  9. package/src/bridge/hooks.tsx +56 -0
  10. package/src/bridge/index.ts +9 -0
  11. package/src/bridge/lexical.ts +177 -0
  12. package/src/bridge/lexicalDom.ts +212 -0
  13. package/src/bridge/preserveBlockMoves.ts +88 -0
  14. package/src/bridge/preview.ts +179 -0
  15. package/src/bridge/protocol.ts +271 -0
  16. package/src/bridge/registry.ts +27 -0
  17. package/src/bridge/styles.ts +137 -0
  18. package/src/components/DateText.tsx +9 -0
  19. package/src/components/File.tsx +9 -0
  20. package/src/components/Image.tsx +30 -0
  21. package/src/components/Link.tsx +24 -0
  22. package/src/components/NumberText.tsx +9 -0
  23. package/src/components/RichText.tsx +47 -0
  24. package/src/components/Text.tsx +15 -0
  25. package/src/components/Video.tsx +12 -0
  26. package/src/components/VideoPlayback.tsx +19 -0
  27. package/src/components/draft.ts +9 -0
  28. package/src/components/imageProps.ts +26 -0
  29. package/src/components/index.ts +12 -0
  30. package/src/components/videoProps.ts +6 -0
  31. package/src/elements/containers/group.ts +33 -0
  32. package/src/elements/containers/list.ts +31 -0
  33. package/src/elements/containers/zone.ts +41 -0
  34. package/src/elements/declaration.ts +118 -0
  35. package/src/elements/define.ts +195 -0
  36. package/src/elements/ids.ts +50 -0
  37. package/src/elements/index.ts +35 -0
  38. package/src/elements/jsonSchema.ts +36 -0
  39. package/src/elements/leaf.ts +98 -0
  40. package/src/elements/leaves/boolean.ts +30 -0
  41. package/src/elements/leaves/choice.ts +53 -0
  42. package/src/elements/leaves/date.ts +39 -0
  43. package/src/elements/leaves/file.ts +56 -0
  44. package/src/elements/leaves/image.ts +160 -0
  45. package/src/elements/leaves/link.ts +149 -0
  46. package/src/elements/leaves/number.ts +52 -0
  47. package/src/elements/leaves/query.ts +101 -0
  48. package/src/elements/leaves/relation.ts +69 -0
  49. package/src/elements/leaves/rich-text.ts +155 -0
  50. package/src/elements/leaves/selection.ts +40 -0
  51. package/src/elements/leaves/single.ts +29 -0
  52. package/src/elements/leaves/text.ts +103 -0
  53. package/src/elements/leaves/video.ts +108 -0
  54. package/src/elements/ops/expand.ts +94 -0
  55. package/src/elements/ops/overlay.ts +59 -0
  56. package/src/elements/ops/refs.ts +45 -0
  57. package/src/elements/ops/seed.ts +185 -0
  58. package/src/elements/ops/text.ts +20 -0
  59. package/src/elements/ops/translatable.ts +19 -0
  60. package/src/elements/ops/upgrade.ts +43 -0
  61. package/src/elements/ops/validate.ts +56 -0
  62. package/src/elements/order.ts +10 -0
  63. package/src/elements/path.ts +43 -0
  64. package/src/elements/prefix.ts +2 -0
  65. package/src/elements/registry.ts +48 -0
  66. package/src/elements/tokens.ts +14 -0
  67. package/src/elements/walk.ts +42 -0
  68. package/src/features/client/AskAiProvider.tsx +551 -0
  69. package/src/features/client/AskAiWidget.tsx +222 -0
  70. package/src/features/client/ConsentRuntime.tsx +95 -0
  71. package/src/features/client/FeedbackProvider.tsx +231 -0
  72. package/src/features/client/FeedbackWidget.tsx +101 -0
  73. package/src/features/client/PageReporter.tsx +26 -0
  74. package/src/features/client/README.md +131 -0
  75. package/src/features/client/SiteFeatures.tsx +250 -0
  76. package/src/features/client/askAiRecovery.ts +92 -0
  77. package/src/features/client/askAiStream.ts +59 -0
  78. package/src/features/client/context.tsx +24 -0
  79. package/src/features/client/index.ts +24 -0
  80. package/src/features/client/overlay.tsx +129 -0
  81. package/src/features/client/styles.css +337 -0
  82. package/src/features/client/transport.ts +310 -0
  83. package/src/features/client/useAskAI.ts +45 -0
  84. package/src/features/client/useConsent.ts +15 -0
  85. package/src/features/client/useFeedback.ts +33 -0
  86. package/src/features/client/useSiteSearch.ts +50 -0
  87. package/src/features/client/verification.ts +141 -0
  88. package/src/features/consent/README.md +70 -0
  89. package/src/features/consent/cookieScript.ts +53 -0
  90. package/src/features/consent/custom.ts +89 -0
  91. package/src/features/consent/defaults.ts +40 -0
  92. package/src/features/consent/drivers.ts +233 -0
  93. package/src/features/consent/loader.ts +248 -0
  94. package/src/features/consent/registry.ts +398 -0
  95. package/src/features/contracts/askAi.ts +166 -0
  96. package/src/features/contracts/askAiSettings.ts +42 -0
  97. package/src/features/contracts/base.ts +107 -0
  98. package/src/features/contracts/consent.ts +150 -0
  99. package/src/features/contracts/feedback.ts +22 -0
  100. package/src/features/contracts/feedbackSettings.ts +49 -0
  101. package/src/features/contracts/index.ts +61 -0
  102. package/src/features/contracts/knowledge.ts +99 -0
  103. package/src/features/contracts/knowledgePolicy.ts +249 -0
  104. package/src/features/contracts/search.ts +24 -0
  105. package/src/features/contracts/verification.ts +52 -0
  106. package/src/features/contracts/verificationConfig.ts +9 -0
  107. package/src/features/server/ConsentDefaults.tsx +15 -0
  108. package/src/features/server/README.md +185 -0
  109. package/src/features/server/gateway.ts +318 -0
  110. package/src/features/server/http.ts +179 -0
  111. package/src/features/server/index.ts +6 -0
  112. package/src/features/server/session.ts +23 -0
  113. package/src/features/server/verification.ts +39 -0
  114. package/src/features/server/verifyVisitor.ts +69 -0
  115. package/src/languages/config.d.mts +5 -0
  116. package/src/languages/config.mjs +53 -0
  117. package/src/languages/decide.ts +72 -0
  118. package/src/languages/index.ts +4 -0
  119. package/src/languages/proxy.ts +108 -0
  120. package/src/languages/routing.ts +93 -0
  121. package/src/languages/wire.ts +19 -0
  122. package/src/legacy/bridge.tsx +1987 -0
  123. package/src/legacy/caret.ts +246 -0
  124. package/src/legacy/cli.mjs +189 -0
  125. package/src/legacy/content.ts +993 -0
  126. package/src/legacy/contract.ts +664 -0
  127. package/src/legacy/defineBlocks.ts +1602 -0
  128. package/src/legacy/defineSite.ts +623 -0
  129. package/src/legacy/defineSite.types.ts +270 -0
  130. package/src/legacy/draft.tsx +128 -0
  131. package/src/legacy/image.ts +433 -0
  132. package/src/legacy/index.ts +209 -0
  133. package/src/legacy/language-switcher.tsx +58 -0
  134. package/src/legacy/languageHints.ts +153 -0
  135. package/src/legacy/languages/fields.ts +131 -0
  136. package/src/legacy/languages/hash.ts +98 -0
  137. package/src/legacy/languages/health.ts +97 -0
  138. package/src/legacy/languages/identity.ts +247 -0
  139. package/src/legacy/languages/links.ts +81 -0
  140. package/src/legacy/languages/resolution.ts +56 -0
  141. package/src/legacy/languages/routing.ts +169 -0
  142. package/src/legacy/languages/runtime.ts +216 -0
  143. package/src/legacy/languages/seo.ts +95 -0
  144. package/src/legacy/languages/settings.ts +211 -0
  145. package/src/legacy/languages/types.ts +85 -0
  146. package/src/legacy/languages.ts +8 -0
  147. package/src/legacy/lexical.ts +170 -0
  148. package/src/legacy/lexicalDom.ts +166 -0
  149. package/src/legacy/media.ts +272 -0
  150. package/src/legacy/nav.ts +176 -0
  151. package/src/legacy/preview.ts +79 -0
  152. package/src/legacy/redirects.ts +474 -0
  153. package/src/legacy/richText.tsx +104 -0
  154. package/src/legacy/routes.ts +414 -0
  155. package/src/legacy/seo.ts +327 -0
  156. package/src/legacy/shape.ts +412 -0
  157. package/src/legacy/uploads.ts +233 -0
  158. package/src/legacy/verification.ts +42 -0
  159. package/src/preview-lock/contracts.ts +42 -0
  160. package/src/preview-lock/index.ts +3 -0
  161. package/src/preview-lock/screen.tsx +244 -0
  162. package/src/preview-lock/server.ts +416 -0
  163. package/src/redirects/config.d.mts +5 -0
  164. package/src/redirects/config.mjs +22 -0
  165. package/src/redirects/index.ts +3 -0
  166. package/src/redirects/location.ts +31 -0
  167. package/src/redirects/match.ts +199 -0
  168. package/src/redirects/proxy.ts +91 -0
  169. package/src/redirects/wire.ts +30 -0
  170. package/src/seo/jsonLd.ts +70 -0
  171. package/src/seo/metadata.ts +127 -0
  172. package/src/seo/robots.ts +14 -0
  173. package/src/seo/sitemap.ts +22 -0
  174. package/src/seo/title.ts +22 -0
  175. package/src/server/adapters.ts +73 -0
  176. package/src/server/createU90.ts +96 -0
  177. package/src/server/handlers.ts +164 -0
  178. package/src/server/index.ts +6 -0
  179. package/src/server/read.ts +207 -0
  180. package/src/server/render.tsx +46 -0
  181. package/src/server/routes.tsx +171 -0
  182. package/src/site/assemble.ts +109 -0
  183. package/src/site/defineSite.ts +132 -0
  184. package/src/site/documents.ts +18 -0
  185. package/src/site/fields.ts +59 -0
  186. package/src/site/index.ts +14 -0
  187. package/src/site/replies.ts +118 -0
  188. package/src/site/store.ts +45 -0
  189. package/src/site/types.ts +75 -0
  190. package/src/site/version.ts +1 -0
  191. package/src/site/wire.ts +125 -0
  192. package/src/switcher/LanguageSwitcher.tsx +42 -0
  193. package/src/test-preview/README.md +90 -0
  194. package/src/test-preview/binding.ts +54 -0
  195. package/src/test-preview/bridge.ts +16 -0
  196. package/src/test-preview/guard.ts +186 -0
  197. package/src/test-preview/media.ts +23 -0
  198. package/src/test-preview/protocol.d.mts +67 -0
  199. package/src/test-preview/protocol.mjs +222 -0
  200. package/src/test-preview/runtime.ts +168 -0
  201. package/src/test-preview/server.ts +10 -0
  202. package/src/testing/fake.ts +148 -0
  203. package/src/testing/index.ts +2 -0
  204. package/src/testing/seed.ts +2 -0
@@ -0,0 +1,43 @@
1
+ import type { DocumentDecl, ElementEntry, Schema } from '../define'
2
+ import type { ElementId } from '../ids'
3
+ import { CONTRACTS, CURRENT_CONTRACT, elements } from '../registry'
4
+ import { walkDocument } from '../walk'
5
+
6
+ export class UpgradeError extends Error {
7
+ constructor(public path: string, public from: number, public to: number, public cause: unknown) {
8
+ super(`Upgrading ${path} from v${from} to v${to} failed: ${cause instanceof Error ? cause.message : String(cause)}`)
9
+ this.name = 'UpgradeError'
10
+ }
11
+ }
12
+ export type UpgradeOptions = { registry?: Record<ElementId, ElementEntry>; contracts?: Record<number, Record<ElementId, number>>; current?: number }
13
+
14
+ /**
15
+ * Runs each leaf's upgrade chain from the element version recorded for `fromContract` up to the
16
+ * registry's current version, writes the results back, and stamps `_contract`. Reads run this on
17
+ * the fly; writes store the result. v1 ships no upgrades, so this is a stamp unless a registry with
18
+ * newer versions is passed (as the tests do).
19
+ */
20
+ export function upgrade(schema: Schema, docDecl: DocumentDecl, doc: any, at: string, fromContract: number, opts: UpgradeOptions = {}): unknown {
21
+ const registry = opts.registry ?? (elements as Record<ElementId, ElementEntry>)
22
+ const contracts = opts.contracts ?? CONTRACTS
23
+ const current = opts.current ?? CURRENT_CONTRACT
24
+ const versions = contracts[fromContract]
25
+ if (!versions) throw new Error(`Unknown contract version ${fromContract}.`)
26
+ walkDocument(schema, docDecl, doc, {
27
+ leaf: ({ decl, value, at: here, set }) => {
28
+ if (value === undefined) return
29
+ const entry = registry[decl.element]
30
+ let v = versions[decl.element] ?? 1
31
+ let upgraded: unknown = value
32
+ while (v < entry.version) {
33
+ const step = entry.upgrade[v]
34
+ if (!step) throw new UpgradeError(here, v, v + 1, new Error(`${decl.element} has no upgrade from v${v}.`))
35
+ try { upgraded = step(upgraded) } catch (cause) { throw new UpgradeError(here, v, v + 1, cause) }
36
+ v++
37
+ }
38
+ if (upgraded !== value) set(upgraded)
39
+ },
40
+ }, at)
41
+ doc._contract = current
42
+ return doc
43
+ }
@@ -0,0 +1,56 @@
1
+ import { SLOT, type DocumentDecl, type Issue, type LeafIssue, type Mode, type Schema } from '../define'
2
+ import { hasStoredShape, validateLeaf } from '../leaf'
3
+ import { path } from '../path'
4
+ import { walkDocument } from '../walk'
5
+ import { record } from '../containers/group'
6
+
7
+ export type ValidateOptions = { mode: Mode; previous?: unknown }
8
+
9
+ const valueAtPointer = (doc: unknown, pointer: string): unknown =>
10
+ pointer === '' ? doc : pointer.slice(1).split('/').reduce<any>((n, seg) => (n == null ? undefined : n[seg.replace(/~1/g, '/').replace(/~0/g, '~')]), doc)
11
+ const dig = (v: unknown, sub: string) => sub.split('.').reduce<any>((n, k) => (n == null ? undefined : n[k]), v)
12
+ const slots = (v: unknown) => (Array.isArray(v) ? v.filter((x) => record(x) && x._block === SLOT).length : 0)
13
+
14
+ export function validate(schema: Schema, docDecl: DocumentDecl, doc: unknown, at: string, opts: ValidateOptions): Issue[] {
15
+ const issues: Issue[] = []
16
+ const push = (base: string, i: LeafIssue) => issues.push({ path: path.child(base, ...i.path), code: i.code, message: i.message, severity: i.severity ?? 'error' })
17
+ const ids = new Map<string, string>()
18
+ const layoutRoot = docDecl.kind === 'layout' ? path.child(at, 'blocks') : null
19
+ const root = (p: string, code: string, message: string): Issue[] => [{ path: p, code, message, severity: 'error' }]
20
+ if (!record(doc)) return root(at, 'invalid_type', 'A stored document is an object.')
21
+ if (docDecl.kind === 'entry' && !Object.hasOwn(schema.collections, docDecl.collection)) return root(at, 'unknown-collection', `No collection named "${docDecl.collection}".`)
22
+ if (docDecl.kind === 'single' && !Object.hasOwn(schema.singles, docDecl.key)) return root(at, 'unknown-single', `No Single named "${docDecl.key}".`)
23
+ if (docDecl.kind === 'entry' && doc.fields !== undefined && !record(doc.fields)) return root(path.child(at, 'fields'), 'invalid_type', 'An entry stores its fields as an object.')
24
+ walkDocument(schema, docDecl, doc, {
25
+ enter: ({ container, decl, value, at: here }) => {
26
+ container.enter(decl, value, schema).forEach((i) => push(here, i))
27
+ if (container.id === 'u90/zone' && slots(value) !== (here === layoutRoot ? 1 : 0))
28
+ push(here, { path: [], code: 'slot', message: here === layoutRoot ? 'A layout holds exactly one SLOT.' : 'Only a layout may hold a SLOT.' })
29
+ if (Array.isArray(value))
30
+ for (const item of value)
31
+ if (record(item) && typeof item._id === 'string') {
32
+ if (ids.has(item._id)) push(here, { path: [], code: 'duplicate-id', message: `The id "${item._id}" is already used at ${ids.get(item._id)}.` })
33
+ else ids.set(item._id, here)
34
+ }
35
+ },
36
+ leaf: ({ entry, decl, value, at: here }) => {
37
+ validateLeaf(entry, decl, value, opts.mode).forEach((i) => push(here, i))
38
+ if (decl.lock?.length && opts.previous !== undefined) {
39
+ const pointer = path.toPointer(opts.previous, here)
40
+ const before = pointer === null ? undefined : valueAtPointer(opts.previous, pointer)
41
+ if (before !== undefined)
42
+ for (const sub of decl.lock)
43
+ if (JSON.stringify(dig(before, sub)) !== JSON.stringify(dig(value, sub)))
44
+ push(here, { path: sub.split('.'), code: 'locked', message: 'This part is set by the site and cannot be changed here.' })
45
+ }
46
+ if (value !== undefined && value !== null) {
47
+ const o = entry.options.safeParse(decl.options ?? {})
48
+ if (o.success && hasStoredShape(entry, o.data, value))
49
+ for (const ref of entry.refs(value, o.data, here))
50
+ if (ref.kind === 'single' && !Object.hasOwn(schema.singles, ref.key))
51
+ push(here, { path: [], code: 'unknown-single', message: `No Single named "${ref.key}".`, severity: 'warning' })
52
+ }
53
+ },
54
+ }, at)
55
+ return issues
56
+ }
@@ -0,0 +1,10 @@
1
+ /** A record's keys in declared order: the listed keys that exist, then any others in their stored order. */
2
+ export function orderedKeys(record: Record<string, unknown>, order?: readonly string[]): string[] {
3
+ const keys = Object.keys(record)
4
+ if (!order?.length) return keys
5
+ const listed = order.filter((k, i) => Object.hasOwn(record, k) && order.indexOf(k) === i)
6
+ return [...listed, ...keys.filter((k) => !listed.includes(k))]
7
+ }
8
+ export function orderedEntries<T>(record: Record<string, T>, order?: readonly string[]): [string, T][] {
9
+ return orderedKeys(record, order).map((k) => [k, record[k]])
10
+ }
@@ -0,0 +1,43 @@
1
+ export { ID_RE, newId } from './ids'
2
+ const KINDS = ['page', 'entry', 'layout', 'single'] as const
3
+ export type DocKind = (typeof KINDS)[number]
4
+ export type Path = string
5
+
6
+ const doc = (kind: DocKind, id: string | number): Path => `${kind}:${id}`
7
+ const child = (p: Path, ...segments: (string | number)[]): Path =>
8
+ segments.length ? `${p}/${segments.join('/')}` : p
9
+ const parse = (p: Path): { kind: DocKind; id: string; segments: string[] } => {
10
+ const m = /^(page|entry|layout|single):([^/]+)(?:\/(.*))?$/.exec(p)
11
+ if (!m) throw new Error(`Not a u90 path: ${p}`)
12
+ return { kind: m[1] as DocKind, id: m[2], segments: m[3] ? m[3].split('/') : [] }
13
+ }
14
+ /**
15
+ * JSON Pointer (RFC 6901) of a path inside its document. Arrays are addressed by `_id`,
16
+ * and a leaf list item resolves to its `_value`. Returns null when a segment does not exist.
17
+ */
18
+ const toPointer = (document: unknown, p: Path): string | null => {
19
+ const { kind, segments } = parse(p)
20
+ // A Single document is `{ value }`; its leaf sits at the document path itself.
21
+ let node: any = kind === 'single' ? (document as any)?.value : document
22
+ let pointer = kind === 'single' ? '/value' : ''
23
+ for (const seg of segments) {
24
+ if (Array.isArray(node)) {
25
+ const i = node.findIndex((item) => item && typeof item === 'object' && item._id === seg)
26
+ if (i < 0) return null
27
+ node = node[i]
28
+ pointer += `/${i}`
29
+ if (node && typeof node === 'object' && '_value' in node) {
30
+ node = node._value
31
+ pointer += '/_value'
32
+ }
33
+ } else if (node && typeof node === 'object' && seg in node) {
34
+ node = node[seg]
35
+ pointer += `/${seg.replace(/~/g, '~0').replace(/\//g, '~1')}`
36
+ } else return null
37
+ }
38
+ return pointer
39
+ }
40
+ export const path = { doc, child, parse, toPointer }
41
+
42
+ /** A leaf's key in an overlay: its path inside the document, `blocks/<blockId>/headline`; '' for a Single's own leaf. */
43
+ export const overlayKey = (at: Path): string => parse(at).segments.join('/')
@@ -0,0 +1,2 @@
1
+ /** `route` under a language's address prefix (`/fr`): the home page is the prefix itself. It imports nothing, so the languages proxy can load it without the element registry. */
2
+ export const prefixedRoute = (prefix: string, route: string): string => (route === '/' ? prefix : `${prefix}${route}`)
@@ -0,0 +1,48 @@
1
+ import type { ContainerEntry, ElementEntry, FieldDecl } from './define'
2
+ import { ELEMENT_IDS, type ContainerId, type ElementId } from './ids'
3
+ import { boolean } from './leaves/boolean'
4
+ import { choice } from './leaves/choice'
5
+ import { date } from './leaves/date'
6
+ import { file } from './leaves/file'
7
+ import { image } from './leaves/image'
8
+ import { link } from './leaves/link'
9
+ import { number } from './leaves/number'
10
+ import { query } from './leaves/query'
11
+ import { relation } from './leaves/relation'
12
+ import { richText } from './leaves/rich-text'
13
+ import { selection } from './leaves/selection'
14
+ import { single } from './leaves/single'
15
+ import { text } from './leaves/text'
16
+ import { video } from './leaves/video'
17
+ import { group } from './containers/group'
18
+ import { list } from './containers/list'
19
+ import { zone } from './containers/zone'
20
+
21
+ /** Every element. `satisfies` fails the build if an id has no entry. */
22
+ export const elements = {
23
+ 'u90/text': text,
24
+ 'u90/rich-text': richText,
25
+ 'u90/image': image,
26
+ 'u90/video': video,
27
+ 'u90/file': file,
28
+ 'u90/link': link,
29
+ 'u90/date': date,
30
+ 'u90/number': number,
31
+ 'u90/boolean': boolean,
32
+ 'u90/choice': choice,
33
+ 'u90/relation': relation,
34
+ 'u90/selection': selection,
35
+ 'u90/query': query,
36
+ 'u90/single': single,
37
+ } satisfies Record<ElementId, ElementEntry>
38
+
39
+ export const getElement = (id: ElementId): ElementEntry => elements[id] as ElementEntry
40
+
41
+ export const CURRENT_CONTRACT = 1
42
+ /** Which element version each contract version was written with. Add a row when any element's version changes. */
43
+ export const CONTRACTS: Record<number, Record<ElementId, number>> = {
44
+ 1: Object.fromEntries(ELEMENT_IDS.map((id) => [id, 1])) as Record<ElementId, number>,
45
+ }
46
+
47
+ export const containers = { 'u90/group': group, 'u90/list': list, 'u90/zone': zone } satisfies Record<ContainerId, ContainerEntry<any>>
48
+ export const getContainer = (id: ContainerId): ContainerEntry<FieldDecl> => containers[id] as unknown as ContainerEntry<FieldDecl>
@@ -0,0 +1,14 @@
1
+ /** `{key}` inside copy points at a Single. Keys are lower-case identifiers. */
2
+ export const TOKEN = /\{([a-z][a-zA-Z0-9_-]*)\}/g
3
+ export const tokenKeys = (s: string): string[] => [...s.matchAll(TOKEN)].map((m) => m[1])
4
+ /** The text a Single's read value contributes inline: Text gives `text`, Link gives `label`. */
5
+ export const textOfRead = (read: unknown): string | undefined => {
6
+ if (!read || typeof read !== 'object') return undefined
7
+ const r = read as Record<string, unknown>
8
+ if (typeof r.text === 'string') return r.text
9
+ if (typeof r.label === 'string') return r.label
10
+ return undefined
11
+ }
12
+ /** Unknown keys stay literal; `validate` reports them as `unknown-single` warnings. */
13
+ export const resolveTokens = (s: string, single: (key: string) => unknown): string =>
14
+ s.replace(TOKEN, (match, key: string) => textOfRead(single(key)) ?? match)
@@ -0,0 +1,42 @@
1
+ import { isLeaf, type ContainerEntry, type DocumentDecl, type ElementEntry, type FieldDecl, type GroupDecl, type LeafDecl, type ListDecl, type Schema, type ZoneDecl } from './define'
2
+ import { path } from './path'
3
+ import { getContainer, getElement } from './registry'
4
+
5
+ export type LeafNode = { entry: ElementEntry; decl: LeafDecl; value: unknown; at: string; set(v: unknown): void }
6
+ export type ContainerNode = { container: ContainerEntry<FieldDecl>; decl: GroupDecl | ListDecl | ZoneDecl; value: unknown; at: string; set(v: unknown): void }
7
+ export type Visitor = { leaf?(n: LeafNode): void; enter?(n: ContainerNode): void; exit?(n: ContainerNode): void }
8
+ const noop = () => {}
9
+
10
+ /** The only traversal. Depth first; containers are entered, their children walked, then exited. */
11
+ export function walk(schema: Schema, decl: FieldDecl, value: unknown, visitor: Visitor, at: string, set: (v: unknown) => void = noop): void {
12
+ if (isLeaf(decl)) {
13
+ visitor.leaf?.({ entry: getElement(decl.element), decl, value, at, set })
14
+ return
15
+ }
16
+ const container = getContainer(decl.container)
17
+ const node: ContainerNode = { container, decl, value, at, set }
18
+ visitor.enter?.(node)
19
+ for (const child of container.children(decl, value, schema)) walk(schema, child.decl, child.value, visitor, path.child(at, child.key), child.set)
20
+ visitor.exit?.(node)
21
+ }
22
+
23
+ /** Stored document shapes: page/layout `{ blocks }`, entry `{ fields, blocks? }`, single `{ value }`. */
24
+ export function walkDocument(schema: Schema, docDecl: DocumentDecl, doc: any, visitor: Visitor, at: string): void {
25
+ switch (docDecl.kind) {
26
+ case 'page':
27
+ case 'layout':
28
+ return walk(schema, docDecl.blocks, doc?.blocks, visitor, path.child(at, 'blocks'), (v) => { doc.blocks = v })
29
+ case 'entry': {
30
+ const collection = Object.hasOwn(schema.collections, docDecl.collection) ? schema.collections[docDecl.collection] : undefined
31
+ if (!collection) throw new Error(`No collection named "${docDecl.collection}".`)
32
+ walk(schema, { container: 'u90/group', fields: collection.fields, order: collection.order }, doc?.fields, visitor, path.child(at, 'fields'), (v) => { doc.fields = v })
33
+ if (collection.template) walk(schema, collection.template, doc?.blocks, visitor, path.child(at, 'blocks'), (v) => { doc.blocks = v })
34
+ return
35
+ }
36
+ case 'single': {
37
+ const single = Object.hasOwn(schema.singles, docDecl.key) ? schema.singles[docDecl.key] : undefined
38
+ if (!single) throw new Error(`No Single named "${docDecl.key}".`)
39
+ return walk(schema, single.field, doc?.value, visitor, at, (v) => { doc.value = v })
40
+ }
41
+ }
42
+ }