@glw907/cairn-cms 0.68.0 → 0.76.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/CHANGELOG.md +82 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +5 -5
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.js +3 -4
- package/dist/content/fields.d.ts +49 -1
- package/dist/content/fields.js +11 -0
- package/dist/content/fieldset.d.ts +31 -10
- package/dist/content/fieldset.js +262 -109
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +2 -5
- package/dist/delivery/public-routes.js +15 -1
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +16 -12
- package/dist/index.js +7 -8
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +34 -34
- package/dist/render/registry.js +26 -5
- package/dist/render/rehype-dispatch.d.ts +4 -4
- package/dist/render/rehype-dispatch.js +36 -11
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.js +1 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +5 -1
- package/src/lib/ambient.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +9 -8
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +3 -4
- package/src/lib/content/fields.ts +52 -1
- package/src/lib/content/fieldset.ts +291 -128
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +17 -3
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +38 -23
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/pipeline.ts +1 -7
- package/src/lib/render/registry.ts +58 -39
- package/src/lib/render/rehype-dispatch.ts +45 -10
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +1 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -85
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -163
- package/src/lib/content/validate.ts +0 -90
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// cairn-cms: the concept schema primitive (schema-source-of-truth design). One field
|
|
2
|
-
// declaration yields a plain-data field projection for the editor form, a generated validator,
|
|
3
|
-
// and an inferred frontmatter type. Plan 1 builds the additive primitive; the adapter-contract
|
|
4
|
-
// cutover and the typed reads are Plan 2.
|
|
5
|
-
import { compilePattern, dateBoundsError, patternError, stringLengthError } from './field-rules.js';
|
|
6
|
-
import type { FrontmatterField, ImageValue, ValidationResult } from './types.js';
|
|
7
|
-
import { validateFields } from './validate.js';
|
|
8
|
-
|
|
9
|
-
/** The validate input the cairn adapter takes: the raw frontmatter and the body. */
|
|
10
|
-
export interface StandardInput {
|
|
11
|
-
frontmatter: Record<string, unknown>;
|
|
12
|
-
body: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A minimal local copy of the Standard Schema v1 interface (https://standardschema.dev), so the
|
|
17
|
-
* schema is a drop-in where the ecosystem accepts a validator, with no runtime dependency.
|
|
18
|
-
*/
|
|
19
|
-
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
20
|
-
readonly '~standard': {
|
|
21
|
-
readonly version: 1;
|
|
22
|
-
readonly vendor: string;
|
|
23
|
-
readonly validate: (value: unknown) => StandardResult<Output>;
|
|
24
|
-
readonly types?: { readonly input: Input; readonly output: Output };
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
type StandardResult<Output> =
|
|
28
|
-
| { readonly value: Output; readonly issues?: undefined }
|
|
29
|
-
| { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray<PropertyKey> }> };
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Map one field descriptor to the TS type of its normalized value. text, textarea, and date
|
|
33
|
-
* normalize to a string; a closed-vocabulary `tags` field to the option-union array; an `image`
|
|
34
|
-
* field to its nested object.
|
|
35
|
-
*/
|
|
36
|
-
type FieldValue<K extends FrontmatterField> = K extends { type: 'boolean' }
|
|
37
|
-
? boolean
|
|
38
|
-
: K extends { type: 'image' }
|
|
39
|
-
? ImageValue
|
|
40
|
-
: K extends { type: 'tags'; options: readonly (infer O extends string)[] }
|
|
41
|
-
? O[]
|
|
42
|
-
: K extends { type: 'tags' | 'freetags' }
|
|
43
|
-
? string[]
|
|
44
|
-
: string;
|
|
45
|
-
|
|
46
|
-
/** Flatten an intersection into a single readable object type. */
|
|
47
|
-
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The normalized frontmatter type inferred from a field tuple. A field declared
|
|
51
|
-
* `required: true` is a required key; every other field is optional.
|
|
52
|
-
*/
|
|
53
|
-
export type InferFields<F extends readonly FrontmatterField[]> = Prettify<
|
|
54
|
-
{ [K in F[number] as K extends { required: true } ? K['name'] : never]: FieldValue<K> } & {
|
|
55
|
-
[K in F[number] as K extends { required: true } ? never : K['name']]?: FieldValue<K>;
|
|
56
|
-
}
|
|
57
|
-
>;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* A concept's schema: the plain-data field projection, the generated validator, and the
|
|
61
|
-
* Standard Schema conformance property.
|
|
62
|
-
*/
|
|
63
|
-
export interface ConceptSchema<F extends readonly FrontmatterField[] = readonly FrontmatterField[]> {
|
|
64
|
-
/** The declared fields as plain serializable data, for the editor form. */
|
|
65
|
-
readonly fields: FrontmatterField[];
|
|
66
|
-
/** Validate raw frontmatter, returning field-keyed errors or the normalized data. */
|
|
67
|
-
validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
|
|
68
|
-
/** Standard Schema v1 conformance, for ecosystem interop. A thin adapter over `validate`. */
|
|
69
|
-
readonly '~standard': StandardSchemaV1<StandardInput, InferFields<F>>['~standard'];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Extract the inferred frontmatter type from a `ConceptSchema`. */
|
|
73
|
-
export type Infer<S> = S extends ConceptSchema<infer F> ? InferFields<F> : never;
|
|
74
|
-
|
|
75
|
-
// Enforce the declarative per-field rules on an already-coerced value. Rules run only on a
|
|
76
|
-
// present, non-empty string value, so an absent optional field is never flagged. The first
|
|
77
|
-
// failing rule per field wins, so the author sees one clear message at a time.
|
|
78
|
-
function applyRules(field: FrontmatterField, value: unknown, errors: Record<string, string>, patterns: Map<string, RegExp>): void {
|
|
79
|
-
if (typeof value !== 'string' || value === '') return;
|
|
80
|
-
if (field.type === 'text' || field.type === 'textarea') {
|
|
81
|
-
const lengthError = stringLengthError(value, field, field.label);
|
|
82
|
-
if (lengthError != null) errors[field.name] = lengthError;
|
|
83
|
-
else {
|
|
84
|
-
const formatError = patternError(value, patterns.get(field.name), field.label);
|
|
85
|
-
if (formatError != null) errors[field.name] = formatError;
|
|
86
|
-
}
|
|
87
|
-
} else if (field.type === 'date') {
|
|
88
|
-
const boundsError = dateBoundsError(value, field, field.label);
|
|
89
|
-
if (boundsError != null) errors[field.name] = boundsError;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Options for `defineFields`. `refine` runs after the per-field rules pass, for cross-field and
|
|
95
|
-
* body-dependent checks. It is validation-only: it returns field-keyed errors to merge, or
|
|
96
|
-
* nothing, and never transforms the data.
|
|
97
|
-
*/
|
|
98
|
-
export interface DefineFieldsOptions<F extends readonly FrontmatterField[]> {
|
|
99
|
-
refine?: (data: InferFields<F>, body: string) => Record<string, string> | undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Compile each declared text/textarea pattern once, so a malformed pattern fails loudly at
|
|
103
|
-
// declaration (a site config error) instead of throwing from inside validate() on every save.
|
|
104
|
-
function compilePatterns(fields: FrontmatterField[]): Map<string, RegExp> {
|
|
105
|
-
const compiled = new Map<string, RegExp>();
|
|
106
|
-
for (const field of fields) {
|
|
107
|
-
if ((field.type === 'text' || field.type === 'textarea') && field.pattern != null) {
|
|
108
|
-
compiled.set(field.name, compilePattern(field.pattern, field.name));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return compiled;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// True when an image field feeds the social card: an explicit `seo: true`, or the back-compat
|
|
115
|
-
// default that the field named `image` is the SEO image. The SEO unify (Task 4) reads this flag.
|
|
116
|
-
function isSeoImage(field: FrontmatterField): boolean {
|
|
117
|
-
return field.type === 'image' && (field.seo === true || (field.seo === undefined && field.name === 'image'));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// A concept declares at most one SEO image field, so the social card is unambiguous. More than one
|
|
121
|
-
// is a site config error: a hero named `cover` plus an explicit `seo` on another, or two explicit
|
|
122
|
-
// `seo` fields. Fail loudly at declaration rather than emit a silent or wrong og:image.
|
|
123
|
-
function checkSeoImageFields(fields: FrontmatterField[]): void {
|
|
124
|
-
const seo = fields.filter(isSeoImage);
|
|
125
|
-
if (seo.length > 1) {
|
|
126
|
-
const names = seo.map((field) => `"${field.name}"`).join(', ');
|
|
127
|
-
throw new Error(
|
|
128
|
-
`cairn: a concept declares at most one SEO image field, but found ${seo.length} (${names}). ` +
|
|
129
|
-
'Set seo: false on all but one, or rename the extra image fields so only one feeds the social card.',
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Declare a concept's fields once. Returns the schema's faces derived from that one declaration. */
|
|
135
|
-
export function defineFields<const F extends readonly FrontmatterField[]>(
|
|
136
|
-
fields: F,
|
|
137
|
-
options: DefineFieldsOptions<F> = {},
|
|
138
|
-
): ConceptSchema<F> {
|
|
139
|
-
const list = [...fields] as FrontmatterField[];
|
|
140
|
-
const patterns = compilePatterns(list);
|
|
141
|
-
checkSeoImageFields(list);
|
|
142
|
-
const validate = (frontmatter: Record<string, unknown>, body: string): ValidationResult => {
|
|
143
|
-
const base = validateFields(list, frontmatter);
|
|
144
|
-
if (!base.ok) return base;
|
|
145
|
-
const errors: Record<string, string> = {};
|
|
146
|
-
for (const field of list) applyRules(field, base.data[field.name], errors, patterns);
|
|
147
|
-
if (Object.keys(errors).length > 0) return { ok: false, errors };
|
|
148
|
-
const refined = options.refine?.(base.data as InferFields<F>, body);
|
|
149
|
-
return refined && Object.keys(refined).length > 0 ? { ok: false, errors: refined } : base;
|
|
150
|
-
};
|
|
151
|
-
const standard: StandardSchemaV1<StandardInput, InferFields<F>>['~standard'] = {
|
|
152
|
-
version: 1,
|
|
153
|
-
vendor: 'cairn',
|
|
154
|
-
validate: (value) => {
|
|
155
|
-
const { frontmatter = {}, body = '' } = (value ?? {}) as Partial<StandardInput>;
|
|
156
|
-
const result = validate(frontmatter ?? {}, body ?? '');
|
|
157
|
-
return result.ok
|
|
158
|
-
? { value: result.data as InferFields<F> }
|
|
159
|
-
: { issues: Object.entries(result.errors).map(([field, message]) => ({ message, path: [field] })) };
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
return { fields: list, validate, '~standard': standard };
|
|
163
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// cairn-cms: the field-driven baseline validator. A site's `validate` calls this for the
|
|
2
|
-
// required-and-coerce baseline, then layers any bespoke rules on top, so the per-site
|
|
3
|
-
// validator stays thin (engine-fat rule). Saving runs the concept's validator on the
|
|
4
|
-
// server before any commit; invalid input bounces to the form (spec §7.4).
|
|
5
|
-
import type { FrontmatterField, ImageValue, ValidationResult } from './types.js';
|
|
6
|
-
import { dateInputValue, isCalendarDate } from './frontmatter.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Validate raw frontmatter against a field list. Required text and date fields must be
|
|
10
|
-
* non-empty; required tag fields must be non-empty lists. A present boolean coerces to `true`
|
|
11
|
-
* and an unchecked one is omitted; a present tag field coerces to a string array and an empty
|
|
12
|
-
* one is omitted, so validated data carries no key for an absent tag field (`tags` or `freetags`).
|
|
13
|
-
* The delivery read model
|
|
14
|
-
* (`ContentSummary.tags`) fills that case with an empty array; the two layers differ on purpose.
|
|
15
|
-
* An empty optional text or date field is omitted, so the normalized data
|
|
16
|
-
* carries only meaningful values and committed frontmatter stays minimal. Returns the
|
|
17
|
-
* normalized data, or field-keyed errors when any required field is empty.
|
|
18
|
-
*
|
|
19
|
-
* Frontmatter may arrive from the edit form (all string values) or from `parseMarkdown`,
|
|
20
|
-
* where gray-matter turns an unquoted YAML date into a JS `Date`. The `date` case coerces a
|
|
21
|
-
* `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
|
|
22
|
-
*/
|
|
23
|
-
export function validateFields(
|
|
24
|
-
fields: FrontmatterField[],
|
|
25
|
-
frontmatter: Record<string, unknown>,
|
|
26
|
-
): ValidationResult {
|
|
27
|
-
const data: Record<string, unknown> = {};
|
|
28
|
-
const errors: Record<string, string> = {};
|
|
29
|
-
for (const field of fields) {
|
|
30
|
-
const value = frontmatter[field.name];
|
|
31
|
-
switch (field.type) {
|
|
32
|
-
case 'boolean':
|
|
33
|
-
// Absent or unchecked means false; omit it so a published file carries no draft: false noise.
|
|
34
|
-
if (value === true) data[field.name] = true;
|
|
35
|
-
break;
|
|
36
|
-
case 'tags':
|
|
37
|
-
case 'freetags': {
|
|
38
|
-
const list = Array.isArray(value) ? value.map(String) : [];
|
|
39
|
-
if (field.required && list.length === 0) errors[field.name] = `${field.label} is required`;
|
|
40
|
-
else if (field.type === 'tags') {
|
|
41
|
-
const unknown = list.find((tag) => !field.options.includes(tag));
|
|
42
|
-
if (unknown !== undefined) errors[field.name] = `${field.label} contains an unknown value: ${unknown}`;
|
|
43
|
-
}
|
|
44
|
-
if (list.length > 0) data[field.name] = list;
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
case 'image': {
|
|
48
|
-
// A hero is the nested object { src, alt, caption }. Normalize a well-formed value (default
|
|
49
|
-
// a missing alt to empty, since alt is debt and never a save block), and drop the key when
|
|
50
|
-
// src is empty or absent. A malformed value (a string, or an object without a string src)
|
|
51
|
-
// drops the key rather than throwing, so a hand-edit never breaks a save.
|
|
52
|
-
let src = '';
|
|
53
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
54
|
-
const obj = value as Record<string, unknown>;
|
|
55
|
-
src = typeof obj.src === 'string' ? obj.src.trim() : '';
|
|
56
|
-
if (src !== '') {
|
|
57
|
-
const normalized: ImageValue = {
|
|
58
|
-
src,
|
|
59
|
-
alt: typeof obj.alt === 'string' ? obj.alt : '',
|
|
60
|
-
};
|
|
61
|
-
const caption = typeof obj.caption === 'string' ? obj.caption.trim() : '';
|
|
62
|
-
if (caption !== '') normalized.caption = caption;
|
|
63
|
-
// An explicit decorative choice carries through; it is never required and never a save
|
|
64
|
-
// block. A missing or non-boolean value drops the key, like a blank caption.
|
|
65
|
-
if (obj.decorative === true) normalized.decorative = true;
|
|
66
|
-
data[field.name] = normalized;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// A required image needs a src (the presence check), like the other arms; alt is never
|
|
70
|
-
// required, since alt is debt. The inferred type makes a required image non-optional, so the
|
|
71
|
-
// validator must enforce it or a save could omit it against the type.
|
|
72
|
-
if (field.required && src === '') errors[field.name] = `${field.label} is required`;
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
case 'date': {
|
|
76
|
-
const text = value instanceof Date ? dateInputValue(value) : typeof value === 'string' ? value.trim() : '';
|
|
77
|
-
if (field.required && text === '') errors[field.name] = `${field.label} is required`;
|
|
78
|
-
else if (text !== '' && !isCalendarDate(text)) errors[field.name] = `${field.label} must be a valid date (YYYY-MM-DD)`;
|
|
79
|
-
if (text !== '') data[field.name] = text;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
default: {
|
|
83
|
-
const text = typeof value === 'string' ? value.trim() : '';
|
|
84
|
-
if (field.required && text === '') errors[field.name] = `${field.label} is required`;
|
|
85
|
-
if (text !== '') data[field.name] = text;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, data };
|
|
90
|
-
}
|