schema-components 0.0.0 → 1.0.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.md +526 -0
  4. package/dist/core/adapter.d.mts +19 -0
  5. package/dist/core/adapter.mjs +140 -0
  6. package/dist/core/errors.d.mts +2 -0
  7. package/dist/core/errors.mjs +74 -0
  8. package/dist/core/guards.d.mts +44 -0
  9. package/dist/core/guards.mjs +58 -0
  10. package/dist/core/renderer.d.mts +2 -0
  11. package/dist/core/renderer.mjs +71 -0
  12. package/dist/core/types.d.mts +3 -0
  13. package/dist/core/types.mjs +40 -0
  14. package/dist/core/walker.d.mts +14 -0
  15. package/dist/core/walker.mjs +366 -0
  16. package/dist/errors-DIKI2C78.d.mts +57 -0
  17. package/dist/html/a11y.d.mts +47 -0
  18. package/dist/html/a11y.mjs +81 -0
  19. package/dist/html/html.d.mts +135 -0
  20. package/dist/html/html.mjs +168 -0
  21. package/dist/html/renderToHtml.d.mts +32 -0
  22. package/dist/html/renderToHtml.mjs +352 -0
  23. package/dist/html/renderToHtmlStream.d.mts +58 -0
  24. package/dist/html/renderToHtmlStream.mjs +285 -0
  25. package/dist/html/styles.css +151 -0
  26. package/dist/openapi/components.d.mts +76 -0
  27. package/dist/openapi/components.mjs +223 -0
  28. package/dist/openapi/parser.d.mts +45 -0
  29. package/dist/openapi/parser.mjs +159 -0
  30. package/dist/react/SchemaComponent.d.mts +96 -0
  31. package/dist/react/SchemaComponent.mjs +283 -0
  32. package/dist/react/SchemaErrorBoundary.d.mts +26 -0
  33. package/dist/react/SchemaErrorBoundary.mjs +47 -0
  34. package/dist/react/headless.d.mts +13 -0
  35. package/dist/react/headless.mjs +163 -0
  36. package/dist/themes/shadcn.d.mts +6 -0
  37. package/dist/themes/shadcn.mjs +166 -0
  38. package/dist/types-BU0ETFHk.d.mts +326 -0
  39. package/package.json +113 -3
@@ -0,0 +1,168 @@
1
+ //#region src/html/html.ts
2
+ const VOID_ELEMENTS = new Set([
3
+ "area",
4
+ "base",
5
+ "br",
6
+ "col",
7
+ "embed",
8
+ "hr",
9
+ "img",
10
+ "input",
11
+ "link",
12
+ "meta",
13
+ "param",
14
+ "source",
15
+ "track",
16
+ "wbr"
17
+ ]);
18
+ /**
19
+ * Build an HTML element node.
20
+ *
21
+ * - Tag name is type-checked (must be a known HTML tag)
22
+ * - Attributes are collected as a record — callers get IntelliSense for
23
+ * common attributes but can also pass `aria-*`, `data-*` etc.
24
+ * - Children are flattened; `undefined`, `null`, and `false` are dropped.
25
+ * - For void elements (input, img, etc.), children are ignored.
26
+ *
27
+ * @param tag - HTML element tag name
28
+ * @param attrs - Optional attributes (class, id, aria-*, etc.)
29
+ * @param children - Child nodes (strings are escaped by the serialiser)
30
+ */
31
+ function h(tag, attrs, ...children) {
32
+ return {
33
+ tag,
34
+ attributes: attrs ?? {},
35
+ children: flattenChildren(children)
36
+ };
37
+ }
38
+ /**
39
+ * Create a text node. The value is NOT escaped — the serialiser handles it.
40
+ * Use this for dynamic text that must appear in the output.
41
+ */
42
+ function text(value) {
43
+ return { text: value };
44
+ }
45
+ /**
46
+ * Create a raw HTML node. The value is emitted verbatim — NOT escaped.
47
+ * Use for embedding already-serialised HTML (e.g. from child renderers).
48
+ * Never use for user-supplied data.
49
+ */
50
+ function raw(html) {
51
+ return { html };
52
+ }
53
+ function flattenChildren(nodes) {
54
+ const out = [];
55
+ for (const node of nodes) {
56
+ if (node === void 0 || node === null || node === false) continue;
57
+ if (typeof node === "string") out.push(node);
58
+ else if ("tag" in node) out.push(node);
59
+ else if ("html" in node) out.push(node);
60
+ else if ("text" in node) out.push(node);
61
+ }
62
+ return out;
63
+ }
64
+ /**
65
+ * Serialise an HTML node to a string.
66
+ *
67
+ * - Text content is automatically escaped
68
+ * - Void elements are self-closing
69
+ * - Boolean attributes render as just the name (`disabled`, `checked`)
70
+ * - `false`/`undefined` attribute values are omitted
71
+ *
72
+ * @param node - An HtmlElement, HtmlText, or string to serialise
73
+ * @returns HTML string
74
+ */
75
+ function serialize(node) {
76
+ if (node === void 0 || node === null || node === false) return "";
77
+ if (typeof node === "string") return escapeHtml(node);
78
+ if ("html" in node) return node.html;
79
+ if ("text" in node) return escapeHtml(node.text);
80
+ if (node.tag === "") return node.children.map((child) => serialize(child)).join("");
81
+ return serializeElement(node);
82
+ }
83
+ function serializeElement(el) {
84
+ const attrs = serializeAttributes(el.attributes);
85
+ if (VOID_ELEMENTS.has(el.tag)) return `<${el.tag}${attrs}>`;
86
+ if (el.children.length === 0) return `<${el.tag}${attrs}></${el.tag}>`;
87
+ const inner = el.children.map((child) => serialize(child)).join("");
88
+ return `<${el.tag}${attrs}>${inner}</${el.tag}>`;
89
+ }
90
+ function serializeAttributes(attrs) {
91
+ const parts = [];
92
+ for (const [key, value] of Object.entries(attrs)) {
93
+ if (value === void 0 || value === false) continue;
94
+ if (value === true) parts.push(` ${key}`);
95
+ else parts.push(` ${key}="${escapeHtml(String(value))}"`);
96
+ }
97
+ return parts.join("");
98
+ }
99
+ /**
100
+ * Serialise an HTML node to chunks, yielded at natural element boundaries.
101
+ *
102
+ * - Each top-level child element becomes its own chunk
103
+ * - Leaf text within an element stays with its parent
104
+ * - Void elements are single chunks
105
+ *
106
+ * This is used by the streaming renderer to produce incremental output.
107
+ *
108
+ * @param node - An HTML node to serialise
109
+ * @returns Iterable of HTML string chunks
110
+ */
111
+ function* serializeChunks(node) {
112
+ if (node === void 0 || node === null || node === false) return;
113
+ if (typeof node === "string") {
114
+ yield escapeHtml(node);
115
+ return;
116
+ }
117
+ if ("html" in node) {
118
+ yield node.html;
119
+ return;
120
+ }
121
+ if ("text" in node) {
122
+ yield escapeHtml(node.text);
123
+ return;
124
+ }
125
+ const attrs = serializeAttributes(node.attributes);
126
+ const tag = node.tag;
127
+ const open = `<${tag}${attrs}>`;
128
+ if (VOID_ELEMENTS.has(tag)) {
129
+ yield open;
130
+ return;
131
+ }
132
+ if (node.children.length === 0) {
133
+ yield `${open}</${tag}>`;
134
+ return;
135
+ }
136
+ yield open;
137
+ for (const child of node.children) if (typeof child === "string") yield escapeHtml(child);
138
+ else if ("html" in child) yield child.html;
139
+ else if ("text" in child) yield escapeHtml(child.text);
140
+ else yield* serializeChunks(child);
141
+ yield `</${tag}>`;
142
+ }
143
+ /**
144
+ * Escape a string for safe inclusion in HTML text content or attribute values.
145
+ */
146
+ function escapeHtml(str) {
147
+ return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
148
+ }
149
+ /**
150
+ * Create a fragment: children rendered sequentially with no wrapping element.
151
+ * Useful when a renderer needs to return multiple top-level nodes.
152
+ */
153
+ function fragment(...children) {
154
+ return h("", void 0, ...children);
155
+ }
156
+ /**
157
+ * Serialise a node, treating fragments (empty tag) as just their children.
158
+ */
159
+ function serializeFragment(node) {
160
+ if (node === void 0 || node === null || node === false) return "";
161
+ if (typeof node === "string") return escapeHtml(node);
162
+ if ("html" in node) return node.html;
163
+ if ("text" in node) return escapeHtml(node.text);
164
+ if (node.tag === "") return node.children.map((child) => serialize(child)).join("");
165
+ return serializeElement(node);
166
+ }
167
+ //#endregion
168
+ export { VOID_ELEMENTS, escapeHtml, fragment, h, raw, serialize, serializeAttributes, serializeChunks, serializeElement, serializeFragment, text };
@@ -0,0 +1,32 @@
1
+ import { C as HtmlResolver, S as HtmlRenderProps, m as SchemaMeta, x as HtmlRenderFunction } from "../types-BU0ETFHk.mjs";
2
+
3
+ //#region src/html/renderToHtml.d.ts
4
+ interface RenderToHtmlOptions {
5
+ /** The data value to render. */
6
+ value?: unknown;
7
+ /** For OpenAPI: a ref string like "#/components/schemas/User". */
8
+ ref?: string;
9
+ /** Per-field meta overrides. */
10
+ fields?: Record<string, unknown>;
11
+ /** Meta overrides applied to the root schema. */
12
+ meta?: SchemaMeta;
13
+ /** Force all fields read-only. */
14
+ readOnly?: boolean;
15
+ /** Force all fields as inputs. */
16
+ writeOnly?: boolean;
17
+ /** Root description. */
18
+ description?: string;
19
+ /** Custom HTML resolver. Falls back to defaultHtmlResolver. */
20
+ resolver?: HtmlResolver;
21
+ }
22
+ declare const defaultHtmlResolver: HtmlResolver;
23
+ /**
24
+ * Render a schema to an HTML string.
25
+ *
26
+ * @param schema - Zod schema, JSON Schema, or OpenAPI document
27
+ * @param options - Value, overrides, and resolver options
28
+ * @returns Semantic HTML string with `sc-` prefixed classes
29
+ */
30
+ declare function renderToHtml(schema: unknown, options?: RenderToHtmlOptions): string;
31
+ //#endregion
32
+ export { type HtmlRenderFunction, type HtmlRenderProps, type HtmlResolver, RenderToHtmlOptions, defaultHtmlResolver, renderToHtml };
@@ -0,0 +1,352 @@
1
+ import { normaliseSchema } from "../core/adapter.mjs";
2
+ import { getHtmlRenderFn, mergeHtmlResolvers } from "../core/renderer.mjs";
3
+ import { walk } from "../core/walker.mjs";
4
+ import { h, raw, serialize } from "./html.mjs";
5
+ import { ariaDescribedByAttrs, ariaLabelAttrs, ariaReadonlyAttrs, ariaRequiredAttrs, buildHintElement, buildInputId, requiredIndicator } from "./a11y.mjs";
6
+ //#region src/html/renderToHtml.ts
7
+ /**
8
+ * HTML renderer — produces semantic HTML from schemas using the typed `h()` builder.
9
+ *
10
+ * Framework-agnostic alternative to the React rendering pipeline.
11
+ * Uses the same walker and adapter (normalise → walk → render) but
12
+ * outputs HTML strings instead of ReactNode.
13
+ *
14
+ * All HTML construction goes through `h()` from `html.ts`, which gives
15
+ * compile-time tag/attribute checking and automatic escaping.
16
+ *
17
+ * Usage:
18
+ * import { renderToHtml } from "schema-components/html/renderToHtml";
19
+ * const html = renderToHtml(userSchema, { value: userData });
20
+ *
21
+ * Custom resolver:
22
+ * const html = renderToHtml(schema, {
23
+ * value,
24
+ * resolver: { string: (props) => h("b", {}, String(props.value)) },
25
+ * });
26
+ */
27
+ function renderStringHtml(props) {
28
+ if (props.readOnly) return serialize(renderStringReadOnly(props));
29
+ return serialize(renderStringEditable(props));
30
+ }
31
+ function renderStringReadOnly(props) {
32
+ const strValue = typeof props.value === "string" ? props.value : void 0;
33
+ if (strValue === void 0 || strValue.length === 0) return h("span", {
34
+ class: "sc-value sc-value--empty",
35
+ ...ariaReadonlyAttrs()
36
+ }, "—");
37
+ const format = props.constraints.format;
38
+ if (format === "email") return h("a", {
39
+ class: "sc-value",
40
+ href: `mailto:${strValue}`,
41
+ ...ariaReadonlyAttrs()
42
+ }, strValue);
43
+ if (format === "uri" || format === "url") return h("a", {
44
+ class: "sc-value",
45
+ href: strValue,
46
+ ...ariaReadonlyAttrs()
47
+ }, strValue);
48
+ return h("span", {
49
+ class: "sc-value",
50
+ ...ariaReadonlyAttrs()
51
+ }, strValue);
52
+ }
53
+ function renderStringEditable(props) {
54
+ const strValue = typeof props.value === "string" ? props.value : "";
55
+ const inputType = props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text";
56
+ const id = props.path;
57
+ const attrs = {
58
+ class: "sc-input",
59
+ id,
60
+ type: inputType,
61
+ name: id
62
+ };
63
+ if (!props.writeOnly) attrs.value = strValue;
64
+ if (typeof props.meta.description === "string") attrs.placeholder = props.meta.description;
65
+ if (props.constraints.minLength !== void 0) attrs.minlength = String(props.constraints.minLength);
66
+ if (props.constraints.maxLength !== void 0) attrs.maxlength = String(props.constraints.maxLength);
67
+ Object.assign(attrs, ariaRequiredAttrs(props.tree));
68
+ Object.assign(attrs, ariaDescribedByAttrs(id, props.constraints));
69
+ return h("input", attrs);
70
+ }
71
+ function renderNumberHtml(props) {
72
+ if (props.readOnly) return serialize(renderNumberReadOnly(props));
73
+ return serialize(renderNumberEditable(props));
74
+ }
75
+ function renderNumberReadOnly(props) {
76
+ if (typeof props.value !== "number") return h("span", {
77
+ class: "sc-value sc-value--empty",
78
+ ...ariaReadonlyAttrs()
79
+ }, "—");
80
+ return h("span", {
81
+ class: "sc-value",
82
+ ...ariaReadonlyAttrs()
83
+ }, props.value.toLocaleString());
84
+ }
85
+ function renderNumberEditable(props) {
86
+ const numValue = typeof props.value === "number" ? String(props.value) : "";
87
+ const id = props.path;
88
+ const attrs = {
89
+ class: "sc-input",
90
+ id,
91
+ type: "number",
92
+ name: id
93
+ };
94
+ if (!props.writeOnly) attrs.value = numValue;
95
+ if (props.constraints.minimum !== void 0) attrs.min = String(props.constraints.minimum);
96
+ if (props.constraints.maximum !== void 0) attrs.max = String(props.constraints.maximum);
97
+ Object.assign(attrs, ariaRequiredAttrs(props.tree));
98
+ Object.assign(attrs, ariaDescribedByAttrs(id, props.constraints));
99
+ return h("input", attrs);
100
+ }
101
+ function renderBooleanHtml(props) {
102
+ if (props.readOnly) return serialize(renderBooleanReadOnly(props));
103
+ return serialize(renderBooleanEditable(props));
104
+ }
105
+ function renderBooleanReadOnly(props) {
106
+ if (typeof props.value !== "boolean") return h("span", {
107
+ class: "sc-value sc-value--empty",
108
+ ...ariaReadonlyAttrs()
109
+ }, "—");
110
+ return h("span", {
111
+ class: "sc-value sc-value--boolean",
112
+ ...ariaReadonlyAttrs()
113
+ }, props.value ? "Yes" : "No");
114
+ }
115
+ function renderBooleanEditable(props) {
116
+ const id = props.path;
117
+ const attrs = {
118
+ class: "sc-input",
119
+ id,
120
+ type: "checkbox",
121
+ name: id
122
+ };
123
+ if (props.value === true) attrs.checked = true;
124
+ Object.assign(attrs, ariaRequiredAttrs(props.tree));
125
+ Object.assign(attrs, ariaLabelAttrs(props.meta.description));
126
+ return h("input", attrs);
127
+ }
128
+ function renderEnumHtml(props) {
129
+ if (props.readOnly) return serialize(renderEnumReadOnly(props));
130
+ return serialize(renderEnumEditable(props));
131
+ }
132
+ function renderEnumReadOnly(props) {
133
+ const enumValue = typeof props.value === "string" ? props.value : "";
134
+ if (enumValue.length === 0) return h("span", {
135
+ class: "sc-value sc-value--empty",
136
+ ...ariaReadonlyAttrs()
137
+ }, "—");
138
+ return h("span", {
139
+ class: "sc-value",
140
+ ...ariaReadonlyAttrs()
141
+ }, enumValue);
142
+ }
143
+ function renderEnumEditable(props) {
144
+ const enumValue = typeof props.value === "string" ? props.value : "";
145
+ const id = props.path;
146
+ const selectedValue = props.writeOnly ? "" : enumValue;
147
+ const optionNodes = [h("option", { value: "" }, "Select…"), ...(props.enumValues ?? []).map((v) => {
148
+ const attrs = { value: v };
149
+ if (v === selectedValue) attrs.selected = true;
150
+ return h("option", attrs, v);
151
+ })];
152
+ const selectAttrs = {
153
+ class: "sc-input",
154
+ id,
155
+ name: id
156
+ };
157
+ Object.assign(selectAttrs, ariaRequiredAttrs(props.tree));
158
+ return h("select", selectAttrs, ...optionNodes);
159
+ }
160
+ function renderObjectHtml(props) {
161
+ return serialize(renderObjectNode(props));
162
+ }
163
+ function renderObjectNode(props) {
164
+ const fields = props.fields;
165
+ if (fields === void 0) return "";
166
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
167
+ const obj = isRecord(props.value) ? props.value : {};
168
+ const descriptionText = typeof props.meta.description === "string" ? props.meta.description : void 0;
169
+ const legend = descriptionText !== void 0 ? h("legend", {}, descriptionText) : void 0;
170
+ if (props.readOnly) {
171
+ const children = [];
172
+ if (legend !== void 0) children.push(legend);
173
+ for (const [key, field] of Object.entries(fields)) {
174
+ const label = typeof field.meta.description === "string" ? field.meta.description : key;
175
+ const childValue = obj[key];
176
+ const childHtml = props.renderChild(field, childValue, key);
177
+ children.push(h("dt", { class: "sc-label" }, label));
178
+ children.push(h("dd", { class: "sc-value" }, raw(childHtml)));
179
+ }
180
+ const dlAttrs = { class: "sc-object" };
181
+ Object.assign(dlAttrs, ariaLabelAttrs(descriptionText));
182
+ return h("dl", dlAttrs, ...children);
183
+ }
184
+ const children = [];
185
+ if (legend !== void 0) children.push(legend);
186
+ for (const [key, field] of Object.entries(fields)) {
187
+ const label = typeof field.meta.description === "string" ? field.meta.description : key;
188
+ const fieldId = buildInputId(props.path, key);
189
+ const childValue = obj[key];
190
+ const childHtml = props.renderChild(field, childValue, key);
191
+ const required = requiredIndicator(field);
192
+ const labelContent = [label];
193
+ if (required !== void 0) labelContent.push(required);
194
+ const fieldChildren = [h("label", {
195
+ class: "sc-label",
196
+ for: fieldId
197
+ }, ...labelContent), raw(childHtml)];
198
+ const hint = buildHintElement(key, field.constraints);
199
+ if (hint !== void 0) fieldChildren.push(hint);
200
+ children.push(h("div", { class: "sc-field" }, ...fieldChildren));
201
+ }
202
+ const fieldsetAttrs = { class: "sc-object" };
203
+ Object.assign(fieldsetAttrs, ariaLabelAttrs(descriptionText));
204
+ return h("fieldset", fieldsetAttrs, ...children);
205
+ }
206
+ function renderArrayHtml(props) {
207
+ return serialize(renderArrayNode(props));
208
+ }
209
+ function renderArrayNode(props) {
210
+ const arr = Array.isArray(props.value) ? props.value : [];
211
+ const element = props.element;
212
+ if (element === void 0) return "";
213
+ const items = arr.map((item) => {
214
+ return h("li", { class: "sc-item" }, raw(props.renderChild(element, item)));
215
+ });
216
+ if (props.readOnly) return h("ul", { class: "sc-array" }, ...items);
217
+ return h("div", { class: "sc-array" }, ...arr.map((item) => {
218
+ return h("div", {}, raw(props.renderChild(element, item)));
219
+ }));
220
+ }
221
+ function renderRecordHtml(props) {
222
+ return serialize(renderRecordNode(props));
223
+ }
224
+ function renderRecordNode(props) {
225
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
226
+ const obj = isRecord(props.value) ? props.value : {};
227
+ const valueType = props.valueType;
228
+ if (valueType === void 0) return "";
229
+ const attrs = {
230
+ class: "sc-record",
231
+ role: "group"
232
+ };
233
+ if (props.readOnly) {
234
+ const children = [];
235
+ for (const [key, val] of Object.entries(obj)) {
236
+ const childHtml = props.renderChild(valueType, val, key);
237
+ children.push(h("dt", { class: "sc-label" }, key));
238
+ children.push(h("dd", { class: "sc-value" }, raw(childHtml)));
239
+ }
240
+ return h("dl", attrs, ...children);
241
+ }
242
+ const children = [];
243
+ for (const [key, val] of Object.entries(obj)) {
244
+ const childHtml = props.renderChild(valueType, val, key);
245
+ children.push(h("div", { class: "sc-field" }, h("label", { class: "sc-label" }, key), raw(childHtml)));
246
+ }
247
+ return h("div", attrs, ...children);
248
+ }
249
+ function renderLiteralHtml(props) {
250
+ const values = props.tree.literalValues;
251
+ if (values === void 0 || values.length === 0) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
252
+ return serialize(h("span", { class: "sc-value" }, values.map((v) => v === null ? "null" : String(v)).join(", ")));
253
+ }
254
+ function renderUnionHtml(props) {
255
+ const options = props.options;
256
+ if (options === void 0 || options.length === 0) {
257
+ if (props.value === void 0 || props.value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
258
+ return serialize(h("span", { class: "sc-value" }, JSON.stringify(props.value)));
259
+ }
260
+ const matched = matchUnionOption(options, props.value);
261
+ if (matched !== void 0) return props.renderChild(matched, props.value);
262
+ const firstOption = options[0];
263
+ if (firstOption !== void 0) return props.renderChild(firstOption, props.value);
264
+ return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
265
+ }
266
+ function renderUnknownHtml(props) {
267
+ if (props.readOnly) {
268
+ if (props.value === void 0 || props.value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
269
+ if (typeof props.value === "string") return serialize(h("span", { class: "sc-value" }, props.value));
270
+ return serialize(h("span", { class: "sc-value" }, JSON.stringify(props.value)));
271
+ }
272
+ const strValue = typeof props.value === "string" ? props.value : "";
273
+ const attrs = {
274
+ class: "sc-input",
275
+ type: "text",
276
+ name: props.path
277
+ };
278
+ if (!props.writeOnly) attrs.value = strValue;
279
+ return serialize(h("input", attrs));
280
+ }
281
+ function matchUnionOption(options, value) {
282
+ if (typeof value === "string") return options.find((o) => o.type === "string" || o.type === "enum");
283
+ if (typeof value === "number") return options.find((o) => o.type === "number");
284
+ if (typeof value === "boolean") return options.find((o) => o.type === "boolean");
285
+ if (Array.isArray(value)) return options.find((o) => o.type === "array");
286
+ if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
287
+ }
288
+ const defaultHtmlResolver = {
289
+ string: renderStringHtml,
290
+ number: renderNumberHtml,
291
+ boolean: renderBooleanHtml,
292
+ enum: renderEnumHtml,
293
+ object: renderObjectHtml,
294
+ array: renderArrayHtml,
295
+ record: renderRecordHtml,
296
+ literal: renderLiteralHtml,
297
+ union: renderUnionHtml,
298
+ unknown: renderUnknownHtml
299
+ };
300
+ /**
301
+ * Render a schema to an HTML string.
302
+ *
303
+ * @param schema - Zod schema, JSON Schema, or OpenAPI document
304
+ * @param options - Value, overrides, and resolver options
305
+ * @returns Semantic HTML string with `sc-` prefixed classes
306
+ */
307
+ function renderToHtml(schema, options = {}) {
308
+ const mergedMeta = { ...options.meta };
309
+ if (options.readOnly === true) mergedMeta.readOnly = true;
310
+ if (options.writeOnly === true) mergedMeta.writeOnly = true;
311
+ if (options.description !== void 0) mergedMeta.description = options.description;
312
+ const { jsonSchema, rootMeta, rootDocument } = normaliseSchema(schema, options.ref);
313
+ const tree = walk(jsonSchema, {
314
+ componentMeta: mergedMeta,
315
+ rootMeta,
316
+ fieldOverrides: options.fields,
317
+ rootDocument
318
+ });
319
+ const resolver = options.resolver ?? defaultHtmlResolver;
320
+ const renderChild = (childTree, childValue, pathSuffix) => {
321
+ return renderFieldHtml(childTree, childValue, resolver, pathSuffix ?? childTree.meta.description ?? "", renderChild);
322
+ };
323
+ return renderFieldHtml(tree, options.value, resolver, "", renderChild);
324
+ }
325
+ function renderFieldHtml(tree, value, resolver, path, renderChild) {
326
+ const mergedResolver = mergeHtmlResolvers(resolver, defaultHtmlResolver);
327
+ const renderFn = getHtmlRenderFn(tree.type, mergedResolver);
328
+ if (renderFn !== void 0) {
329
+ const props = {
330
+ value,
331
+ readOnly: tree.editability === "presentation",
332
+ writeOnly: tree.editability === "input",
333
+ meta: tree.meta,
334
+ constraints: tree.constraints,
335
+ path,
336
+ tree,
337
+ renderChild
338
+ };
339
+ if (tree.enumValues !== void 0) props.enumValues = tree.enumValues;
340
+ if (tree.element !== void 0) props.element = tree.element;
341
+ if (tree.fields !== void 0) props.fields = tree.fields;
342
+ if (tree.options !== void 0) props.options = tree.options;
343
+ if (tree.discriminator !== void 0) props.discriminator = tree.discriminator;
344
+ if (tree.keyType !== void 0) props.keyType = tree.keyType;
345
+ if (tree.valueType !== void 0) props.valueType = tree.valueType;
346
+ return renderFn(props);
347
+ }
348
+ if (value === void 0 || value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
349
+ return serialize(h("span", { class: "sc-value" }, typeof value === "string" ? value : JSON.stringify(value)));
350
+ }
351
+ //#endregion
352
+ export { defaultHtmlResolver, renderToHtml };
@@ -0,0 +1,58 @@
1
+ import { RenderToHtmlOptions } from "./renderToHtml.mjs";
2
+
3
+ //#region src/html/renderToHtmlStream.d.ts
4
+ /**
5
+ * Streaming HTML renderer — yields HTML chunks incrementally.
6
+ *
7
+ * Same rendering pipeline as `renderToHtml` but yields string fragments
8
+ * as each field/element is produced instead of building the entire string
9
+ * in memory. Use for server-side rendering where you want to start
10
+ * flushing the response before the full schema is rendered.
11
+ *
12
+ * Three output formats:
13
+ *
14
+ * - `renderToHtmlChunks(schema, options)` → sync `Iterable<string>`
15
+ * - `renderToHtmlStream(schema, options)` → async `AsyncIterable<string>`
16
+ * - `renderToHtmlReadable(schema, options)` → web `ReadableStream<string>`
17
+ *
18
+ * Chunk boundaries:
19
+ * - Object: opening tag, one chunk per field, closing tag
20
+ * - Array: opening tag, one chunk per item, closing tag
21
+ * - Record: opening tag, one chunk per entry, closing tag
22
+ * - Leaf types (string, number, boolean, enum, literal, unknown):
23
+ * rendered entirely as one chunk
24
+ *
25
+ * All HTML construction uses `h()` from `html.ts` — the streaming module
26
+ * manually yields the opening tag, then children, then the closing tag.
27
+ */
28
+ type StreamRenderOptions = RenderToHtmlOptions;
29
+ /**
30
+ * Render a schema to HTML string chunks, yielded incrementally.
31
+ *
32
+ * Each yielded chunk is a self-contained HTML fragment. Concatenating
33
+ * all chunks produces the same output as `renderToHtml`.
34
+ *
35
+ * @returns Sync iterable of HTML string chunks
36
+ */
37
+ declare function renderToHtmlChunks(schema: unknown, options?: StreamRenderOptions): Iterable<string, void, undefined>;
38
+ /**
39
+ * Render a schema to HTML string chunks asynchronously.
40
+ *
41
+ * Identical chunk boundaries to `renderToHtmlChunks` but yields via
42
+ * an async generator. Use with `for await...of` or pipe to a response.
43
+ *
44
+ * @returns Async iterable of HTML string chunks
45
+ */
46
+ declare function renderToHtmlStream(schema: unknown, options?: StreamRenderOptions): AsyncIterable<string, void, undefined>;
47
+ /**
48
+ * Render a schema to a web `ReadableStream<string>`.
49
+ *
50
+ * ```ts
51
+ * return new Response(renderToHtmlReadable(schema, { value }), {
52
+ * headers: { "Content-Type": "text/html" },
53
+ * });
54
+ * ```
55
+ */
56
+ declare function renderToHtmlReadable(schema: unknown, options?: StreamRenderOptions): ReadableStream<string>;
57
+ //#endregion
58
+ export { StreamRenderOptions, renderToHtmlChunks, renderToHtmlReadable, renderToHtmlStream };