@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
package/dist/content/fieldset.js
CHANGED
|
@@ -1,34 +1,10 @@
|
|
|
1
|
-
import { dateInputValue, isCalendarDate } from './frontmatter.js';
|
|
1
|
+
import { datetimeInputValue, dateInputValue, isCalendarDate, referenceIdsFromValue } from './frontmatter.js';
|
|
2
2
|
import { compilePattern, dateBoundsError, patternError, stringLengthError } from './field-rules.js';
|
|
3
|
+
import { isValidId } from './ids.js';
|
|
3
4
|
/** Accept any URL using http or https with a non-empty rest, mirroring the conservative form check. */
|
|
4
5
|
const URL_RE = /^https?:\/\/\S+$/;
|
|
5
6
|
/** Accept a single address conservatively: exactly one at-sign and a dotted domain, nothing more. */
|
|
6
7
|
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
7
|
-
// Coerce one image value to the stored `{ src, alt, caption?, decorative? }` shape, ported from
|
|
8
|
-
// validate.ts. Default a missing alt to empty (alt is debt, never a save block), trim and drop a
|
|
9
|
-
// blank caption, keep decorative only when an explicit true, and drop the whole key when src is empty.
|
|
10
|
-
// A required image with an empty src is the one error this arm raises.
|
|
11
|
-
function coerceImage(field, key, value, data, errors) {
|
|
12
|
-
let src = '';
|
|
13
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
14
|
-
const obj = value;
|
|
15
|
-
src = typeof obj.src === 'string' ? obj.src.trim() : '';
|
|
16
|
-
if (src !== '') {
|
|
17
|
-
const normalized = {
|
|
18
|
-
src,
|
|
19
|
-
alt: typeof obj.alt === 'string' ? obj.alt : '',
|
|
20
|
-
};
|
|
21
|
-
const caption = typeof obj.caption === 'string' ? obj.caption.trim() : '';
|
|
22
|
-
if (caption !== '')
|
|
23
|
-
normalized.caption = caption;
|
|
24
|
-
if (obj.decorative === true)
|
|
25
|
-
normalized.decorative = true;
|
|
26
|
-
data[key] = normalized;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (field.required && src === '')
|
|
30
|
-
errors[key] = `${field.label} is required`;
|
|
31
|
-
}
|
|
32
8
|
// Coerce a raw value to the trimmed string the empty check and constraints run on. A parsed value may
|
|
33
9
|
// arrive from parseMarkdown, not only a form string: a Date on a date or datetime field, a JS number on
|
|
34
10
|
// a number field. A finite 0 coerces to '0', never read as empty, since 0 is a real number a YAML scalar
|
|
@@ -37,30 +13,88 @@ function coerceToText(type, value) {
|
|
|
37
13
|
if (type === 'date' && value instanceof Date)
|
|
38
14
|
return dateInputValue(value);
|
|
39
15
|
if (type === 'datetime' && value instanceof Date)
|
|
40
|
-
return value
|
|
16
|
+
return datetimeInputValue(value);
|
|
41
17
|
if (type === 'number' && typeof value === 'number' && Number.isFinite(value))
|
|
42
18
|
return String(value);
|
|
43
19
|
if (typeof value === 'string')
|
|
44
20
|
return value.trim();
|
|
45
21
|
return '';
|
|
46
22
|
}
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
23
|
+
// Build the structural key for a path by dropping numeric (row-index) segments, so a nested text
|
|
24
|
+
// field's compiled pattern is found regardless of which row it sits in: ['faq', 2, 'code'] -> 'faq.code'.
|
|
25
|
+
function structuralKey(path) {
|
|
26
|
+
return path.filter((seg) => typeof seg === 'string').join('.');
|
|
27
|
+
}
|
|
28
|
+
// Validate one descriptor against its raw value and return its outcome. Empty or absent is
|
|
29
|
+
// "not provided" and is read BEFORE type coercion, uniformly: a required field returns an issue, an
|
|
30
|
+
// optional field drops (no value, no issue). Only a non-empty value is coerced. boolean is the
|
|
31
|
+
// exception: true stores true, anything else omits the value. number relies on the empty-first drop so
|
|
32
|
+
// an empty optional number never becomes Number('') === 0. A container (object, array) recurses one
|
|
33
|
+
// level, appending the leaf key or element index to `path` for each nested issue.
|
|
34
|
+
function validateField(path, field, value, patterns) {
|
|
35
|
+
const label = field.label ?? '';
|
|
36
|
+
// object: validate each leaf one level down, assembling a nested object value and concatenating
|
|
37
|
+
// issues with the leaf key appended to the path. An empty (all-leaves-dropped) object omits the
|
|
38
|
+
// value; a required empty object is an error on the object's own path.
|
|
39
|
+
if (field.type === 'object') {
|
|
40
|
+
const obj = {};
|
|
41
|
+
const issues = [];
|
|
42
|
+
const raw = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
43
|
+
for (const [leafKey, leaf] of Object.entries(field.fields)) {
|
|
44
|
+
const outcome = validateField([...path, leafKey], leaf, raw[leafKey], patterns);
|
|
45
|
+
issues.push(...outcome.issues);
|
|
46
|
+
if ('value' in outcome)
|
|
47
|
+
obj[leafKey] = outcome.value;
|
|
48
|
+
}
|
|
49
|
+
if (issues.length > 0)
|
|
50
|
+
return { issues };
|
|
51
|
+
if (Object.keys(obj).length === 0) {
|
|
52
|
+
return field.required ? { issues: [{ path, message: `${label} is required` }] } : { issues: [] };
|
|
53
|
+
}
|
|
54
|
+
return { value: obj, issues };
|
|
55
|
+
}
|
|
56
|
+
// array: a reference item keeps the shipped id-list path; any other item recurses per element with
|
|
57
|
+
// the element index appended to the path. A required empty list errors on the array's own path.
|
|
58
|
+
if (field.type === 'array') {
|
|
59
|
+
if (field.item.type === 'reference') {
|
|
60
|
+
// array(reference): coerceToText returns '' for an array, so the empty-first drop below would
|
|
61
|
+
// silently lose an optional list or falsely error a required one. The canonicalizer coerces a
|
|
62
|
+
// lone scalar to one element and a Date element to its id. Each element must pass isValidId (the
|
|
63
|
+
// item's reference rule this phase); a required empty list errors; the value is set only when the
|
|
64
|
+
// list is non-empty.
|
|
65
|
+
const list = referenceIdsFromValue(value);
|
|
66
|
+
if (field.required && list.length === 0)
|
|
67
|
+
return { issues: [{ path, message: `${label} is required` }] };
|
|
68
|
+
const invalid = list.find((id) => !isValidId(id));
|
|
69
|
+
if (invalid !== undefined)
|
|
70
|
+
return { issues: [{ path, message: `${label} is not a valid reference` }] };
|
|
71
|
+
return list.length > 0 ? { value: list, issues: [] } : { issues: [] };
|
|
72
|
+
}
|
|
73
|
+
const elements = Array.isArray(value) ? value : [];
|
|
74
|
+
const out = [];
|
|
75
|
+
const issues = [];
|
|
76
|
+
elements.forEach((element, i) => {
|
|
77
|
+
const outcome = validateField([...path, i], field.item, element, patterns);
|
|
78
|
+
issues.push(...outcome.issues);
|
|
79
|
+
if ('value' in outcome)
|
|
80
|
+
out.push(outcome.value);
|
|
81
|
+
});
|
|
82
|
+
if (issues.length > 0)
|
|
83
|
+
return { issues };
|
|
84
|
+
if (out.length === 0) {
|
|
85
|
+
return field.required ? { issues: [{ path, message: `${label} is required` }] } : { issues: [] };
|
|
86
|
+
}
|
|
87
|
+
return { value: out, issues };
|
|
88
|
+
}
|
|
89
|
+
// boolean: presence is the value; an unchecked or absent box omits the value (no draft: false noise).
|
|
54
90
|
if (field.type === 'boolean') {
|
|
55
|
-
|
|
56
|
-
data[key] = true;
|
|
57
|
-
return;
|
|
91
|
+
return value === true ? { value: true, issues: [] } : { issues: [] };
|
|
58
92
|
}
|
|
59
93
|
// multiselect: a string array; drop empties, reject an unknown value when options is closed. An empty
|
|
60
|
-
// list omits the
|
|
61
|
-
// non-empty scalar (a single tag a YAML scalar carries) coerces to a single-element list, rather
|
|
62
|
-
// dropping to [] and reading as "required" while present. An empty string or a
|
|
63
|
-
// stays the empty list.
|
|
94
|
+
// list omits the value (a required empty errors); the array path is the one non-string coercion. A
|
|
95
|
+
// lone non-empty scalar (a single tag a YAML scalar carries) coerces to a single-element list, rather
|
|
96
|
+
// than dropping to [] and reading as "required" while present. An empty string or a
|
|
97
|
+
// non-string-non-array stays the empty list.
|
|
64
98
|
if (field.type === 'multiselect') {
|
|
65
99
|
let raw;
|
|
66
100
|
if (Array.isArray(value))
|
|
@@ -71,103 +105,176 @@ function validateField(key, field, value, data, errors, patterns) {
|
|
|
71
105
|
raw = [];
|
|
72
106
|
const list = raw.map((v) => v.trim()).filter((v) => v !== '');
|
|
73
107
|
if (field.required && list.length === 0) {
|
|
74
|
-
|
|
75
|
-
return;
|
|
108
|
+
return { issues: [{ path, message: `${label} is required` }] };
|
|
76
109
|
}
|
|
77
110
|
const { options } = field;
|
|
78
111
|
if (options) {
|
|
79
112
|
const unknown = list.find((v) => !options.includes(v));
|
|
80
113
|
if (unknown !== undefined) {
|
|
81
|
-
|
|
82
|
-
return;
|
|
114
|
+
return { issues: [{ path, message: `${label} contains an unknown value: ${unknown}` }] };
|
|
83
115
|
}
|
|
84
116
|
}
|
|
85
|
-
|
|
86
|
-
data[key] = list;
|
|
87
|
-
return;
|
|
117
|
+
return list.length > 0 ? { value: list, issues: [] } : { issues: [] };
|
|
88
118
|
}
|
|
89
|
-
// image: the nested object arm, dropping the
|
|
119
|
+
// image: the nested object arm, dropping the value on empty src. Default a missing alt to empty (alt
|
|
120
|
+
// is debt, never a save block), trim and drop a blank caption, keep decorative only when an explicit
|
|
121
|
+
// true. A required image with an empty src is the one error this arm raises.
|
|
90
122
|
if (field.type === 'image') {
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
let src = '';
|
|
124
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
125
|
+
const obj = value;
|
|
126
|
+
src = typeof obj.src === 'string' ? obj.src.trim() : '';
|
|
127
|
+
if (src !== '') {
|
|
128
|
+
const normalized = {
|
|
129
|
+
src,
|
|
130
|
+
alt: typeof obj.alt === 'string' ? obj.alt : '',
|
|
131
|
+
};
|
|
132
|
+
const caption = typeof obj.caption === 'string' ? obj.caption.trim() : '';
|
|
133
|
+
if (caption !== '')
|
|
134
|
+
normalized.caption = caption;
|
|
135
|
+
if (obj.decorative === true)
|
|
136
|
+
normalized.decorative = true;
|
|
137
|
+
return { value: normalized, issues: [] };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return field.required && src === '' ? { issues: [{ path, message: `${label} is required` }] } : { issues: [] };
|
|
93
141
|
}
|
|
94
142
|
// Every other type is "not provided when empty" first, then coerced. `coerceToText` turns a parsed
|
|
95
143
|
// value into its string form BEFORE the empty check, so a real parsed value (a Date on a date or
|
|
96
144
|
// datetime field, a number on a number field) is not read as empty.
|
|
97
145
|
const text = coerceToText(field.type, value);
|
|
98
146
|
if (text === '') {
|
|
99
|
-
|
|
100
|
-
errors[key] = `${field.label} is required`;
|
|
101
|
-
return;
|
|
147
|
+
return field.required ? { issues: [{ path, message: `${label} is required` }] } : { issues: [] };
|
|
102
148
|
}
|
|
149
|
+
const key = structuralKey(path);
|
|
103
150
|
switch (field.type) {
|
|
104
151
|
case 'number': {
|
|
105
152
|
const n = Number(text);
|
|
106
153
|
// Reject NaN and the non-finite values Number() yields for "Infinity"/"1e400", which an
|
|
107
154
|
// isNaN check alone would pass through and commit as a YAML .inf scalar.
|
|
108
155
|
if (!Number.isFinite(n))
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
data[key] = n;
|
|
118
|
-
break;
|
|
156
|
+
return { issues: [{ path, message: `${label} must be a number` }] };
|
|
157
|
+
if (field.integer && !Number.isInteger(n))
|
|
158
|
+
return { issues: [{ path, message: `${label} must be a whole number` }] };
|
|
159
|
+
if (field.min != null && n < field.min)
|
|
160
|
+
return { issues: [{ path, message: `${label} must be at least ${field.min}` }] };
|
|
161
|
+
if (field.max != null && n > field.max)
|
|
162
|
+
return { issues: [{ path, message: `${label} must be at most ${field.max}` }] };
|
|
163
|
+
return { value: n, issues: [] };
|
|
119
164
|
}
|
|
120
165
|
case 'select': {
|
|
121
166
|
if (!field.options.includes(text))
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
data[key] = text;
|
|
125
|
-
break;
|
|
167
|
+
return { issues: [{ path, message: `${label} contains an unknown value: ${text}` }] };
|
|
168
|
+
return { value: text, issues: [] };
|
|
126
169
|
}
|
|
127
170
|
case 'url': {
|
|
128
171
|
if (!URL_RE.test(text))
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
data[key] = text;
|
|
132
|
-
break;
|
|
172
|
+
return { issues: [{ path, message: `${label} is not a valid URL` }] };
|
|
173
|
+
return { value: text, issues: [] };
|
|
133
174
|
}
|
|
134
175
|
case 'email': {
|
|
135
176
|
if (!EMAIL_RE.test(text))
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
data[key] = text;
|
|
139
|
-
break;
|
|
177
|
+
return { issues: [{ path, message: `${label} is not a valid email address` }] };
|
|
178
|
+
return { value: text, issues: [] };
|
|
140
179
|
}
|
|
141
180
|
case 'date': {
|
|
142
|
-
if (!isCalendarDate(text))
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
if (!isCalendarDate(text))
|
|
182
|
+
return { issues: [{ path, message: `${label} must be a valid date (YYYY-MM-DD)` }] };
|
|
183
|
+
const boundsError = dateBoundsError(text, field, label);
|
|
184
|
+
if (boundsError != null)
|
|
185
|
+
return { issues: [{ path, message: boundsError }] };
|
|
186
|
+
return { value: text, issues: [] };
|
|
187
|
+
}
|
|
188
|
+
case 'reference': {
|
|
189
|
+
// A scalar edge: the empty-first drop above already handled an absent optional, so a non-empty
|
|
190
|
+
// value must be a valid id. An invalid token is a corrupted edge, not a coercible value.
|
|
191
|
+
if (!isValidId(text))
|
|
192
|
+
return { issues: [{ path, message: `${label} is not a valid reference` }] };
|
|
193
|
+
return { value: text, issues: [] };
|
|
153
194
|
}
|
|
154
195
|
default: {
|
|
155
196
|
// text, textarea, datetime: a trimmed non-empty string. text and textarea also enforce the
|
|
156
197
|
// string-length and pattern constraints (v1 parity); datetime stays a plain string for now,
|
|
157
198
|
// since its bounds are out of scope this pass and v1 has no datetime equivalent to match.
|
|
158
199
|
if (field.type === 'text' || field.type === 'textarea') {
|
|
159
|
-
const lengthError = stringLengthError(text, field,
|
|
160
|
-
if (lengthError != null)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
200
|
+
const lengthError = stringLengthError(text, field, label);
|
|
201
|
+
if (lengthError != null)
|
|
202
|
+
return { issues: [{ path, message: lengthError }] };
|
|
203
|
+
const formatError = patternError(text, patterns.get(key), label);
|
|
204
|
+
if (formatError != null)
|
|
205
|
+
return { issues: [{ path, message: formatError }] };
|
|
206
|
+
}
|
|
207
|
+
return { value: text, issues: [] };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// At most one image field may feed the social card, so the og:image is unambiguous. A v2 fieldset
|
|
212
|
+
// marks that field with an explicit `seo: true`; there is no field-name default, since the record key
|
|
213
|
+
// is arbitrary. Two seo images is a site config error, so fail loudly at declaration (v1 parity).
|
|
214
|
+
// The delivery seo reader resolves the social card off a hardcoded top-level key list, so a nested
|
|
215
|
+
// seo image cannot resolve at delivery; this phase forbids seo: true inside any container and defers
|
|
216
|
+
// nested seo to the pass that generalizes delivery seo resolution.
|
|
217
|
+
function checkSeoImageFields(record) {
|
|
218
|
+
const seo = [];
|
|
219
|
+
for (const [key, field] of Object.entries(record)) {
|
|
220
|
+
if (field.type === 'image' && field.seo === true)
|
|
221
|
+
seo.push(`"${key}"`);
|
|
222
|
+
else if (field.type === 'object') {
|
|
223
|
+
for (const [leafKey, leaf] of Object.entries(field.fields)) {
|
|
224
|
+
if (leaf.type === 'image' && leaf.seo === true) {
|
|
225
|
+
throw new Error(`cairn: the image "${key}.${leafKey}" sets seo: true, but a nested seo image is not supported this phase. Put the social-card image at the top level.`);
|
|
168
226
|
}
|
|
169
227
|
}
|
|
170
|
-
|
|
228
|
+
}
|
|
229
|
+
else if (field.type === 'array') {
|
|
230
|
+
const item = field.item;
|
|
231
|
+
const nested = (item.type === 'image' && item.seo === true)
|
|
232
|
+
|| (item.type === 'object' && Object.values(item.fields).some((l) => l.type === 'image' && l.seo === true));
|
|
233
|
+
if (nested) {
|
|
234
|
+
throw new Error(`cairn: the array field "${key}" declares an seo image, but an array would mean one social card per row. Put seo: true on a top-level image.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (seo.length > 1) {
|
|
239
|
+
throw new Error(`cairn: a concept declares at most one SEO image field, but found ${seo.length} (${seo.join(', ')}). Set seo: false on all but one.`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// A leaf is any non-container descriptor. A container (object, array) may hold leaves one level deep only.
|
|
243
|
+
function isLeaf(field) {
|
|
244
|
+
return field.type !== 'object' && field.type !== 'array';
|
|
245
|
+
}
|
|
246
|
+
// Enforce the one-level nesting cap, the no-reference-in-object deferral, and the no-dot-in-key rule, all
|
|
247
|
+
// loudly at declaration. A deeper nesting, a nested reference, or a dotted key would otherwise mis-save or
|
|
248
|
+
// mis-decode at the edge, so fail here.
|
|
249
|
+
function checkContainerNesting(record) {
|
|
250
|
+
const checkKey = (k, where) => {
|
|
251
|
+
if (k.includes('.'))
|
|
252
|
+
throw new Error(`cairn: ${where} "${k}" must not contain a dot; field keys address the nested form by dotted path.`);
|
|
253
|
+
};
|
|
254
|
+
const checkObjectLeaves = (fieldsRecord, where) => {
|
|
255
|
+
for (const [k, leaf] of Object.entries(fieldsRecord)) {
|
|
256
|
+
checkKey(k, where);
|
|
257
|
+
if (!isLeaf(leaf)) {
|
|
258
|
+
throw new Error(`cairn: ${where} "${k}" must be a leaf field; containers nest one level only.`);
|
|
259
|
+
}
|
|
260
|
+
if (leaf.type === 'reference') {
|
|
261
|
+
throw new Error(`cairn: ${where} "${k}" is a reference; a reference inside an object is not supported this phase. Model it as the parent's own concept, or use a top-level array(reference).`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
for (const [key, field] of Object.entries(record)) {
|
|
266
|
+
checkKey(key, 'the field');
|
|
267
|
+
if (field.type === 'object') {
|
|
268
|
+
checkObjectLeaves(field.fields, `the object field "${key}" sub-field`);
|
|
269
|
+
}
|
|
270
|
+
else if (field.type === 'array') {
|
|
271
|
+
const item = field.item;
|
|
272
|
+
if (item.type === 'object') {
|
|
273
|
+
checkObjectLeaves(item.fields, `the array field "${key}" row sub-field`);
|
|
274
|
+
}
|
|
275
|
+
else if (!isLeaf(item)) {
|
|
276
|
+
throw new Error(`cairn: the array field "${key}" item must be a leaf or a flat object; an array of arrays is not allowed.`);
|
|
277
|
+
}
|
|
171
278
|
}
|
|
172
279
|
}
|
|
173
280
|
}
|
|
@@ -177,24 +284,70 @@ function validateField(key, field, value, data, errors, patterns) {
|
|
|
177
284
|
* and the Standard Schema conformance property whose issues map each error to a single-segment path.
|
|
178
285
|
*/
|
|
179
286
|
export function fieldset(record, options = {}) {
|
|
287
|
+
checkSeoImageFields(record);
|
|
288
|
+
checkContainerNesting(record);
|
|
289
|
+
for (const key of Object.keys(options.behavior ?? {})) {
|
|
290
|
+
if (!(key in record))
|
|
291
|
+
throw new Error(`cairn: behavior names "${key}", which is not a declared field.`);
|
|
292
|
+
}
|
|
180
293
|
// Compile each text/textarea pattern once at construction, so a malformed pattern fails loudly here
|
|
181
|
-
// (mirroring v1's compilePatterns) rather than on every save. Keyed by
|
|
294
|
+
// (mirroring v1's compilePatterns) rather than on every save. Keyed by the structural path
|
|
295
|
+
// ('faq.code', 'address.zip') so a nested leaf's compiled pattern is found regardless of row index,
|
|
296
|
+
// recursing one level into an object and an array(object).
|
|
182
297
|
const patterns = new Map();
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
298
|
+
const compilePatternsIn = (rec, prefix) => {
|
|
299
|
+
for (const [k, f] of Object.entries(rec)) {
|
|
300
|
+
if ((f.type === 'text' || f.type === 'textarea') && f.pattern != null) {
|
|
301
|
+
patterns.set([...prefix, k].join('.'), compilePattern(f.pattern, f.label));
|
|
302
|
+
}
|
|
303
|
+
else if (f.type === 'object') {
|
|
304
|
+
compilePatternsIn(f.fields, [...prefix, k]);
|
|
305
|
+
}
|
|
306
|
+
else if (f.type === 'array' && f.item.type === 'object') {
|
|
307
|
+
compilePatternsIn(f.item.fields, [...prefix, k]);
|
|
308
|
+
}
|
|
309
|
+
else if (f.type === 'array' && (f.item.type === 'text' || f.item.type === 'textarea') && f.item.pattern != null) {
|
|
310
|
+
patterns.set([...prefix, k].join('.'), compilePattern(f.item.pattern, f.item.label));
|
|
311
|
+
}
|
|
186
312
|
}
|
|
187
|
-
}
|
|
313
|
+
};
|
|
314
|
+
compilePatternsIn(record, []);
|
|
188
315
|
const validate = (frontmatter, body) => {
|
|
189
316
|
const data = {};
|
|
190
|
-
const
|
|
317
|
+
const issues = [];
|
|
191
318
|
for (const [key, field] of Object.entries(record)) {
|
|
192
|
-
validateField(key, field, frontmatter[key],
|
|
319
|
+
const outcome = validateField([key], field, frontmatter[key], patterns);
|
|
320
|
+
issues.push(...outcome.issues);
|
|
321
|
+
if ('value' in outcome)
|
|
322
|
+
data[key] = outcome.value;
|
|
323
|
+
if (outcome.issues.length === 0 && options.behavior?.[key]?.validate) {
|
|
324
|
+
let message = null;
|
|
325
|
+
try {
|
|
326
|
+
message = options.behavior[key].validate('value' in outcome ? outcome.value : undefined, frontmatter);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
console.warn(`cairn: behavior.validate for field "${key}" threw; treating it as valid.`, err);
|
|
330
|
+
}
|
|
331
|
+
if (typeof message === 'string')
|
|
332
|
+
issues.push({ path: [key], message });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (issues.length > 0) {
|
|
336
|
+
// Back-compat: derive the flat errors map from the located issues, keying each top-level field by
|
|
337
|
+
// the first message that mentions it, so a consumer reading `errors[fieldName]` still works.
|
|
338
|
+
const errors = {};
|
|
339
|
+
for (const issue of issues) {
|
|
340
|
+
const top = String(issue.path[0]);
|
|
341
|
+
if (!(top in errors))
|
|
342
|
+
errors[top] = issue.message;
|
|
343
|
+
}
|
|
344
|
+
return { ok: false, errors, issues };
|
|
193
345
|
}
|
|
194
|
-
if (Object.keys(errors).length > 0)
|
|
195
|
-
return { ok: false, errors };
|
|
196
346
|
const refined = options.refine?.(data, body);
|
|
197
|
-
|
|
347
|
+
if (refined && Object.keys(refined).length > 0) {
|
|
348
|
+
return { ok: false, errors: refined, issues: Object.entries(refined).map(([k, m]) => ({ path: [k], message: m })) };
|
|
349
|
+
}
|
|
350
|
+
return { ok: true, data };
|
|
198
351
|
};
|
|
199
352
|
const standard = {
|
|
200
353
|
version: 1,
|
|
@@ -204,10 +357,10 @@ export function fieldset(record, options = {}) {
|
|
|
204
357
|
const result = validate(frontmatter ?? {}, body ?? '');
|
|
205
358
|
return result.ok
|
|
206
359
|
? { value: result.data }
|
|
207
|
-
: { issues: Object.entries(result.errors).map(([key, message]) => ({ message, path: [key] })) };
|
|
360
|
+
: { issues: result.issues ?? Object.entries(result.errors).map(([key, message]) => ({ message, path: [key] })) };
|
|
208
361
|
},
|
|
209
362
|
};
|
|
210
|
-
return { fields: record, behavior: {}, validate, '~standard': standard };
|
|
363
|
+
return { fields: record, behavior: options.behavior ?? {}, validate, '~standard': standard };
|
|
211
364
|
}
|
|
212
365
|
/**
|
|
213
366
|
* Resolve each descriptor's `default` to a form-initial value, so a fresh entry opens prefilled. The
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The split of fmBlock into its lines, each with its block-relative start and end offsets (the end
|
|
3
|
+
* is the index of the trailing newline, or the block length for the last line). Block offsets are
|
|
4
|
+
* already absolute since the frontmatter leads the document.
|
|
5
|
+
*/
|
|
6
|
+
export interface FmLine {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
|
|
12
|
+
* both fences and the trailing newline (empty when there is none); `body` is everything after it.
|
|
13
|
+
* The block leads the document, so a frontmatter offset is already absolute and a body offset needs
|
|
14
|
+
* `fmBlock.length` added. Shared by every arm so they agree on the boundary.
|
|
15
|
+
*/
|
|
16
|
+
export declare function splitFrontmatter(markdown: string): {
|
|
17
|
+
fmBlock: string;
|
|
18
|
+
body: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
|
|
22
|
+
* re-scanning the block per call.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fmLines(fmBlock: string): FmLine[];
|
|
25
|
+
/**
|
|
26
|
+
* The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
|
|
27
|
+
* line `^<key>:` at indent 0 through the last line before the next top-level key (or the document
|
|
28
|
+
* end). A flow-style value (`key: { ... }` all on one line) yields a single-line range. Returns null
|
|
29
|
+
* when the key has no top-level line, which a malformed or non-canonical block can cause. Scoping the
|
|
30
|
+
* per-key search to this range is what lets two image fields that share one hash, or an image field
|
|
31
|
+
* whose hash also appears in a sibling text value, resolve to distinct, correct spans.
|
|
32
|
+
*/
|
|
33
|
+
export declare function frontmatterKeyRange(lines: FmLine[], fmBlock: string, key: string): [number, number] | null;
|
|
34
|
+
/**
|
|
35
|
+
* Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
|
|
36
|
+
* matched literally, so its characters must not act as metacharacters.
|
|
37
|
+
*/
|
|
38
|
+
export declare function escapeForRegExp(literal: string): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// cairn-cms: the shared frontmatter-region helpers. A byte-preserving rewriter (media-rewrite's
|
|
2
|
+
// `repointMediaRef`/`fillAltForHash`, references' `rewriteFrontmatterReference`) splices a
|
|
3
|
+
// frontmatter value by source offset rather than round-tripping through gray-matter (which reformats
|
|
4
|
+
// YAML and is not byte stable). These helpers locate the `---` fenced block, split it into lines with
|
|
5
|
+
// absolute offsets, and find a top-level key's inclusive line range, so every such rewriter agrees on
|
|
6
|
+
// the boundary, the CRLF handling, and the colon-anchored key scan.
|
|
7
|
+
/**
|
|
8
|
+
* Split a leading frontmatter block off the markdown. `fmBlock` is the `---` fenced block including
|
|
9
|
+
* both fences and the trailing newline (empty when there is none); `body` is everything after it.
|
|
10
|
+
* The block leads the document, so a frontmatter offset is already absolute and a body offset needs
|
|
11
|
+
* `fmBlock.length` added. Shared by every arm so they agree on the boundary.
|
|
12
|
+
*/
|
|
13
|
+
export function splitFrontmatter(markdown) {
|
|
14
|
+
const m = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
15
|
+
const fmBlock = m ? m[0] : '';
|
|
16
|
+
return { fmBlock, body: markdown.slice(fmBlock.length) };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Split fmBlock into lines once, so the locator helpers walk a shared structure instead of
|
|
20
|
+
* re-scanning the block per call.
|
|
21
|
+
*/
|
|
22
|
+
export function fmLines(fmBlock) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
let pos = 0;
|
|
25
|
+
while (pos <= fmBlock.length) {
|
|
26
|
+
const nl = fmBlock.indexOf('\n', pos);
|
|
27
|
+
const end = nl === -1 ? fmBlock.length : nl;
|
|
28
|
+
lines.push({ start: pos, end });
|
|
29
|
+
if (nl === -1)
|
|
30
|
+
break;
|
|
31
|
+
pos = nl + 1;
|
|
32
|
+
}
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The inclusive line-index range `[lo, hi]` of the block-style mapping a top-level key opens: the
|
|
37
|
+
* line `^<key>:` at indent 0 through the last line before the next top-level key (or the document
|
|
38
|
+
* end). A flow-style value (`key: { ... }` all on one line) yields a single-line range. Returns null
|
|
39
|
+
* when the key has no top-level line, which a malformed or non-canonical block can cause. Scoping the
|
|
40
|
+
* per-key search to this range is what lets two image fields that share one hash, or an image field
|
|
41
|
+
* whose hash also appears in a sibling text value, resolve to distinct, correct spans.
|
|
42
|
+
*/
|
|
43
|
+
export function frontmatterKeyRange(lines, fmBlock, key) {
|
|
44
|
+
const opener = new RegExp(`^${escapeForRegExp(key)}:`);
|
|
45
|
+
const topLevelKey = /^[^\s#][^:]*:/;
|
|
46
|
+
const isBoundary = (i) => {
|
|
47
|
+
const text = fmBlock.slice(lines[i].start, lines[i].end);
|
|
48
|
+
// A new top-level key or the closing `---` fence ends the current key's block.
|
|
49
|
+
return topLevelKey.test(text) || text === '---';
|
|
50
|
+
};
|
|
51
|
+
let lo = -1;
|
|
52
|
+
for (let i = 1; i < lines.length - 1; i += 1) {
|
|
53
|
+
// Skip the leading `---` fence (line 0) and the trailing empty line after the closing fence.
|
|
54
|
+
if (opener.test(fmBlock.slice(lines[i].start, lines[i].end))) {
|
|
55
|
+
lo = i;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (lo === -1)
|
|
60
|
+
return null;
|
|
61
|
+
let hi = lo;
|
|
62
|
+
for (let i = lo + 1; i < lines.length - 1; i += 1) {
|
|
63
|
+
if (isBoundary(i))
|
|
64
|
+
break;
|
|
65
|
+
hi = i;
|
|
66
|
+
}
|
|
67
|
+
return [lo, hi];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Escape a literal string for safe interpolation into a RegExp source. A key name or an indent is
|
|
71
|
+
* matched literally, so its characters must not act as metacharacters.
|
|
72
|
+
*/
|
|
73
|
+
export function escapeForRegExp(literal) {
|
|
74
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NamedField } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* True when a multiselect field is a closed checkbox group: it declares an options vocabulary and is
|
|
4
|
+
* not author-extendable. The save decoder and the editor render arm both call this, so the
|
|
5
|
+
* closed-versus-open multiselect decision can never drift between decode and display.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isClosedMultiselect(field: {
|
|
8
|
+
type: string;
|
|
9
|
+
options?: readonly string[];
|
|
10
|
+
creatable?: boolean;
|
|
11
|
+
}): boolean;
|
|
2
12
|
/** Decode submitted form data into raw frontmatter, one rule per field type. */
|
|
3
|
-
export declare function frontmatterFromForm(fields:
|
|
13
|
+
export declare function frontmatterFromForm(fields: NamedField[], form: FormData): Record<string, unknown>;
|
|
4
14
|
/**
|
|
5
15
|
* Coerce a frontmatter date value to the `YYYY-MM-DD` an `<input type="date">` wants.
|
|
6
16
|
* gray-matter parses an unquoted YAML date into a JS Date, so a string-only read would
|
|
@@ -8,6 +18,14 @@ export declare function frontmatterFromForm(fields: FrontmatterField[], form: Fo
|
|
|
8
18
|
* slicing the ISO string avoids a local-timezone shift.
|
|
9
19
|
*/
|
|
10
20
|
export declare function dateInputValue(value: unknown): string;
|
|
21
|
+
/**
|
|
22
|
+
* Coerce a frontmatter datetime value to the naive-local, minute-precision `YYYY-MM-DDTHH:mm` an
|
|
23
|
+
* `<input type="datetime-local">` wants. A datetime is round-tripped as TEXT, so a stored value is
|
|
24
|
+
* already this string; the `Date` branch is the fallback for a value gray-matter parsed into a JS
|
|
25
|
+
* `Date` from an unquoted full-ISO scalar. UTC getters read the value back as it was written,
|
|
26
|
+
* avoiding a local-timezone shift.
|
|
27
|
+
*/
|
|
28
|
+
export declare function datetimeInputValue(value: unknown): string;
|
|
11
29
|
/**
|
|
12
30
|
* True when `s` is a canonical zero-padded `YYYY-MM-DD` string naming a real calendar date.
|
|
13
31
|
* Rejects a wrong format, an impossible month or day, and a JS date-rollover such as
|
|
@@ -16,6 +34,21 @@ export declare function dateInputValue(value: unknown): string;
|
|
|
16
34
|
* `dateInputValue` emit, so a value outside it is a hand-edit or odd-YAML error.
|
|
17
35
|
*/
|
|
18
36
|
export declare function isCalendarDate(s: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Canonicalize one raw reference value to its target id. A Date (a YAML-parsed unquoted date-shaped id)
|
|
39
|
+
* becomes its UTC-sliced `YYYY-MM-DD` string, so a stored id never depends on the reader's timezone; a
|
|
40
|
+
* string is trimmed; anything else is the empty string. `validate`, `extractReferenceEdges`, and
|
|
41
|
+
* `formValues` all read through this, so a hand-edited or rewriter-emitted value canonicalizes identically.
|
|
42
|
+
*/
|
|
43
|
+
export declare function referenceIdFromValue(value: unknown): string;
|
|
44
|
+
/**
|
|
45
|
+
* Canonicalize a raw `array(reference)` value to its list of target ids. An array maps each element
|
|
46
|
+
* through `referenceIdFromValue`; a lone non-empty scalar is one element (a single id a YAML scalar
|
|
47
|
+
* carries, never dropped to an empty list); anything else is empty. Empty ids are dropped.
|
|
48
|
+
*/
|
|
49
|
+
export declare function referenceIdsFromValue(value: unknown): string[];
|
|
50
|
+
/** Coerce parsed frontmatter to the form-ready values the editor inputs expect, one rule per field type. */
|
|
51
|
+
export declare function formValues(fields: NamedField[], frontmatter: Record<string, unknown>): Record<string, unknown>;
|
|
19
52
|
/** Reassemble a markdown file from frontmatter and body for committing. */
|
|
20
53
|
export declare function serializeMarkdown(frontmatter: object, body: string): string;
|
|
21
54
|
/** Parse a markdown file into its frontmatter and body: the read-side inverse of serialize. */
|