@karibsen/eponyme 0.7.0 → 0.8.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/README.md +59 -7
- package/dist/module.d.mts +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -0
- package/dist/runtime/components/fields/FieldRenderer.vue +11 -0
- package/dist/runtime/components/fields/RichTextField.vue +37 -1
- package/dist/runtime/components/fields/TagsField.d.vue.ts +21 -0
- package/dist/runtime/components/fields/TagsField.vue +55 -0
- package/dist/runtime/components/fields/TagsField.vue.d.ts +21 -0
- package/dist/runtime/components/ui/EPAlertDialog.d.vue.ts +37 -0
- package/dist/runtime/components/ui/EPAlertDialog.vue +66 -0
- package/dist/runtime/components/ui/EPAlertDialog.vue.d.ts +37 -0
- package/dist/runtime/components/ui/EPDialog.d.vue.ts +1 -0
- package/dist/runtime/components/ui/EPDialog.vue +6 -2
- package/dist/runtime/components/ui/EPDialog.vue.d.ts +1 -0
- package/dist/runtime/components/ui/EPTagsInput.d.vue.ts +27 -0
- package/dist/runtime/components/ui/EPTagsInput.vue +101 -0
- package/dist/runtime/components/ui/EPTagsInput.vue.d.ts +27 -0
- package/dist/runtime/composables/useEponymeCollection.d.ts +13 -1
- package/dist/runtime/composables/useEponymeCollection.js +29 -4
- package/dist/runtime/fields/index.d.ts +2 -0
- package/dist/runtime/fields/index.js +2 -1
- package/dist/runtime/fields/tags.d.ts +6 -0
- package/dist/runtime/fields/tags.js +3 -0
- package/dist/runtime/pages/EponymeIndexPage.vue +0 -3
- package/dist/runtime/server/api/eponyme-collections/[path].get.js +40 -1
- package/dist/runtime/server/plugins/eponyme-sync.js +11 -1
- package/dist/runtime/server/services/eponyme-form-store.js +2 -2
- package/dist/runtime/server/services/eponyme-store.d.ts +278 -30
- package/dist/runtime/server/services/eponyme-store.js +373 -71
- package/dist/runtime/server/utils/eponyme-reindex.d.ts +13 -0
- package/dist/runtime/server/utils/eponyme-reindex.js +4 -0
- package/dist/runtime/types/eponyme.d.ts +53 -2
- package/dist/runtime/types/field.d.ts +35 -2
- package/dist/runtime/utils/eponyme-entry-index.d.ts +39 -0
- package/dist/runtime/utils/eponyme-entry-index.js +51 -0
- package/dist/runtime/utils/get-field-default-value.js +2 -0
- package/dist/runtime/utils/normalize-eponyme-values.d.ts +13 -0
- package/dist/runtime/utils/{normalize-eponyme-phones.js → normalize-eponyme-values.js} +4 -2
- package/dist/runtime/utils/normalize-tags.d.ts +10 -0
- package/dist/runtime/utils/normalize-tags.js +17 -0
- package/dist/runtime/utils/validate-eponyme-data.js +16 -0
- package/dist/runtime/utils/variables.d.ts +4 -0
- package/dist/runtime/utils/variables.js +8 -1
- package/package.json +3 -2
- package/dist/runtime/utils/normalize-eponyme-phones.d.ts +0 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ArrayFieldDefinition, ArrayItemValue, BooleanFieldDefinition, CheckboxGroupFieldDefinition, EmailFieldDefinition, FieldDefinition, NumberFieldDefinition, PhoneFieldDefinition, RadioFieldDefinition, SectionFieldDefinition, SectionValue, SelectFieldDefinition, SlugFieldDefinition, StringFieldDefinition, TabFieldDefinition, TabsValue, TextareaFieldDefinition, UrlFieldDefinition, UrlValue } from './field.js';
|
|
1
|
+
import type { ArrayFieldDefinition, ArrayItemValue, BooleanFieldDefinition, CheckboxGroupFieldDefinition, EmailFieldDefinition, FieldDefinition, NumberFieldDefinition, PhoneFieldDefinition, RadioFieldDefinition, SectionFieldDefinition, SectionValue, SelectFieldDefinition, SlugFieldDefinition, StringFieldDefinition, TabFieldDefinition, TagsFieldDefinition, TagsValueOf, TabsValue, TextareaFieldDefinition, UrlFieldDefinition, UrlValue } from './field.js';
|
|
2
2
|
export type EponymeSchema = Record<string, FieldDefinition>;
|
|
3
3
|
export interface EponymeCollectionDefinitionBase {
|
|
4
4
|
__eponymeCollection: true;
|
|
@@ -65,7 +65,7 @@ export type FieldValue<T extends FieldDefinition> = T extends {
|
|
|
65
65
|
type: 'number';
|
|
66
66
|
} ? number : T extends {
|
|
67
67
|
type: 'boolean';
|
|
68
|
-
} ? boolean : T extends UrlFieldDefinition ? UrlValue : T extends ArrayFieldDefinition<infer Item> ? Array<ArrayItemValue<Item>> : T extends CheckboxGroupFieldDefinition<infer Value> ? Value[] : T extends SectionFieldDefinition<infer Section> ? SectionValue<Section> : T extends TabFieldDefinition<infer Tabs> ? TabsValue<Tabs> : string;
|
|
68
|
+
} ? boolean : T extends UrlFieldDefinition ? UrlValue : T extends ArrayFieldDefinition<infer Item> ? Array<ArrayItemValue<Item>> : T extends CheckboxGroupFieldDefinition<infer Value> ? Value[] : T extends TagsFieldDefinition<infer Options> ? Array<TagsValueOf<Options>> : T extends SectionFieldDefinition<infer Section> ? SectionValue<Section> : T extends TabFieldDefinition<infer Tabs> ? TabsValue<Tabs> : string;
|
|
69
69
|
export type EponymeData<T extends EponymeSchema> = {
|
|
70
70
|
[K in keyof T]: T[K] extends FieldDefinition ? FieldValue<T[K]> : never;
|
|
71
71
|
};
|
|
@@ -74,6 +74,57 @@ export type EponymeCollectionName<T extends EponymeConfig> = {
|
|
|
74
74
|
[K in keyof T & string]: T[K] extends EponymeCollectionDefinitionBase ? K : T[K] extends EponymeFormDefinitionBase ? never : T[K] extends EponymeSchema ? never : T[K] extends EponymeConfig ? `${K}/${EponymeCollectionName<T[K]>}` : never;
|
|
75
75
|
}[keyof T & string];
|
|
76
76
|
export type EponymeCollectionDataByName<T extends EponymeConfig, Name extends EponymeCollectionName<T>> = EponymeCollectionAtPath<T, Name> extends EponymeCollectionDefinition<infer Schema> ? EponymeData<Schema> : never;
|
|
77
|
+
/**
|
|
78
|
+
* Field types a `where` can filter on, mirroring the write path's `INDEXABLE_TYPES`.
|
|
79
|
+
*
|
|
80
|
+
* The two lists are separate because one runs on values and the other on types, but they
|
|
81
|
+
* describe the same rule: a filterable field is one whose stored form compares correctly as
|
|
82
|
+
* text. That is why `date` is here — validation pins it to `YYYY-MM-DD` — and `number` is
|
|
83
|
+
* not, since `'10'` sorts before `'9'`.
|
|
84
|
+
*/
|
|
85
|
+
type FilterableFieldType = 'tags' | 'checkboxGroup' | 'select' | 'radio' | 'boolean' | 'date';
|
|
86
|
+
/** A multi-valued field is filtered by one of its items, not by the whole list. */
|
|
87
|
+
type FilterOperand<T extends FieldDefinition> = FieldValue<T> extends readonly (infer Item)[] ? Item : FieldValue<T>;
|
|
88
|
+
/**
|
|
89
|
+
* Bounds, offered only where alphabetical order is the value's natural order — which is
|
|
90
|
+
* `field.date()`, pinned by validation to `YYYY-MM-DD`. On a tag or a select, comparing
|
|
91
|
+
* with `gte` would sort labels, which answers no question anyone asks.
|
|
92
|
+
*/
|
|
93
|
+
export interface EponymeFilterBounds {
|
|
94
|
+
gte?: string;
|
|
95
|
+
lte?: string;
|
|
96
|
+
gt?: string;
|
|
97
|
+
lt?: string;
|
|
98
|
+
}
|
|
99
|
+
type FilterOperators<T extends FieldDefinition> = {
|
|
100
|
+
/** Any of these, the same as passing the list directly. */
|
|
101
|
+
in?: readonly FilterOperand<T>[];
|
|
102
|
+
/** None of these. Combines with the positive operators to narrow then subtract. */
|
|
103
|
+
not?: FilterOperand<T> | readonly FilterOperand<T>[];
|
|
104
|
+
/**
|
|
105
|
+
* Substring of the stored value, case-insensitive.
|
|
106
|
+
*
|
|
107
|
+
* The only operator that cannot use the index's ordering — a leading wildcard scans it.
|
|
108
|
+
* That still reads a narrow table instead of every entry's content, but it does not
|
|
109
|
+
* scale the way the others do.
|
|
110
|
+
*/
|
|
111
|
+
contains?: string;
|
|
112
|
+
} & (T extends {
|
|
113
|
+
type: 'date';
|
|
114
|
+
} ? EponymeFilterBounds : unknown);
|
|
115
|
+
/** The values of one key are ORed; the keys of a `where` are ANDed. */
|
|
116
|
+
export type EponymeFieldFilter<T extends FieldDefinition> = FilterOperand<T> | readonly FilterOperand<T>[] | FilterOperators<T>;
|
|
117
|
+
/**
|
|
118
|
+
* The filter a schema accepts, keyed by field name. Nested fields are left out: a field
|
|
119
|
+
* inside a section, a tab or an array has no single key to name it by, which is the same
|
|
120
|
+
* reason `orderBy` cannot reach one.
|
|
121
|
+
*/
|
|
122
|
+
export type EponymeFilterInput<T extends EponymeSchema> = {
|
|
123
|
+
[K in keyof T as T[K] extends {
|
|
124
|
+
type: FilterableFieldType;
|
|
125
|
+
} ? K : never]?: T[K] extends FieldDefinition ? EponymeFieldFilter<T[K]> : never;
|
|
126
|
+
};
|
|
127
|
+
export type EponymeCollectionFilter<T extends EponymeConfig, Name extends EponymeCollectionName<T>> = EponymeCollectionAtPath<T, Name> extends EponymeCollectionDefinition<infer Schema> ? EponymeFilterInput<Schema> : never;
|
|
77
128
|
export type EponymeName<T extends EponymeConfig> = {
|
|
78
129
|
[K in keyof T & string]: T[K] extends EponymeCollectionDefinitionBase ? never : T[K] extends EponymeFormDefinitionBase ? never : T[K] extends EponymeSchema ? K : T[K] extends EponymeConfig ? `${K}/${EponymeName<T[K]>}` : never;
|
|
79
130
|
}[keyof T & string];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CountryCode } from 'libphonenumber-js/min';
|
|
2
|
-
export type EponymeFieldType = 'string' | 'slug' | 'email' | 'phone' | 'url' | 'textarea' | 'richText' | 'number' | 'boolean' | 'image' | 'select' | 'radio' | 'checkboxGroup' | 'date' | 'color' | 'array' | 'section' | 'tabs';
|
|
2
|
+
export type EponymeFieldType = 'string' | 'slug' | 'email' | 'phone' | 'url' | 'textarea' | 'richText' | 'number' | 'boolean' | 'image' | 'select' | 'radio' | 'checkboxGroup' | 'tags' | 'date' | 'color' | 'array' | 'section' | 'tabs';
|
|
3
3
|
export type FieldVisibilityCondition = {
|
|
4
4
|
field: string;
|
|
5
5
|
equals: unknown;
|
|
@@ -176,6 +176,39 @@ export interface CheckboxGroupFieldDefinition<T extends string = string> {
|
|
|
176
176
|
type: 'checkboxGroup';
|
|
177
177
|
options: CheckboxGroupFieldOptions<T>;
|
|
178
178
|
}
|
|
179
|
+
export interface TagsFieldOptions<T extends string = string> extends DefaultFieldOptions<T[]> {
|
|
180
|
+
/** Offered while typing. With `allowCustom` off, they are also the only accepted values. */
|
|
181
|
+
suggestions?: readonly T[];
|
|
182
|
+
/**
|
|
183
|
+
* Whether a tag outside `suggestions` may be entered.
|
|
184
|
+
*
|
|
185
|
+
* @default false
|
|
186
|
+
*/
|
|
187
|
+
allowCustom?: boolean;
|
|
188
|
+
minItems?: number;
|
|
189
|
+
maxItems?: number;
|
|
190
|
+
placeholder?: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Values a tags field may hold, read from the options as they were written: the suggestions
|
|
194
|
+
* themselves while the list is closed, any string once `allowCustom` opens it. A tag list is
|
|
195
|
+
* therefore never a mix of types.
|
|
196
|
+
*
|
|
197
|
+
* Read from the object rather than from a type parameter on purpose. A parameter carrying
|
|
198
|
+
* `allowCustom` is re-inferred as `boolean` when the config is checked against `EponymeConfig`,
|
|
199
|
+
* and a conditional on `boolean` distributes over `true | false`, collapsing the union of
|
|
200
|
+
* suggestions to plain `string`. The shape of the object survives that check — which is also
|
|
201
|
+
* how `field.select()` and `field.checkboxGroup()` keep their literals.
|
|
202
|
+
*/
|
|
203
|
+
export type TagsValueOf<O> = O extends {
|
|
204
|
+
allowCustom: true;
|
|
205
|
+
} ? string : O extends {
|
|
206
|
+
suggestions: readonly (infer S extends string)[];
|
|
207
|
+
} ? S : string;
|
|
208
|
+
export interface TagsFieldDefinition<O extends TagsFieldOptions = TagsFieldOptions> {
|
|
209
|
+
type: 'tags';
|
|
210
|
+
options: O;
|
|
211
|
+
}
|
|
179
212
|
export interface TodayDateDefault {
|
|
180
213
|
readonly __eponymeDefault: 'today';
|
|
181
214
|
}
|
|
@@ -200,7 +233,7 @@ export interface ColorFieldDefinition {
|
|
|
200
233
|
type: 'color';
|
|
201
234
|
options: ColorFieldOptions;
|
|
202
235
|
}
|
|
203
|
-
export type ArrayItemFieldDefinition = StringFieldDefinition | SlugFieldDefinition | EmailFieldDefinition | PhoneFieldDefinition | UrlFieldDefinition | TextareaFieldDefinition | RichTextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | ImageFieldDefinition | SelectFieldDefinition | RadioFieldDefinition | CheckboxGroupFieldDefinition | DateFieldDefinition | ColorFieldDefinition;
|
|
236
|
+
export type ArrayItemFieldDefinition = StringFieldDefinition | SlugFieldDefinition | EmailFieldDefinition | PhoneFieldDefinition | UrlFieldDefinition | TextareaFieldDefinition | RichTextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | ImageFieldDefinition | SelectFieldDefinition | RadioFieldDefinition | CheckboxGroupFieldDefinition | TagsFieldDefinition | DateFieldDefinition | ColorFieldDefinition;
|
|
204
237
|
export type ArrayItemSchema = Record<string, ArrayItemFieldDefinition>;
|
|
205
238
|
export type ArrayItemDefinition = ArrayItemFieldDefinition | ArrayItemSchema;
|
|
206
239
|
export type ArrayItemValue<T extends ArrayItemDefinition> = T extends NumberFieldDefinition ? number : T extends BooleanFieldDefinition ? boolean : T extends UrlFieldDefinition ? UrlValue : T extends CheckboxGroupFieldDefinition<infer Value> ? Value[] : T extends ArrayItemFieldDefinition ? string : T extends ArrayItemSchema ? {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EponymeSchema } from '../types/eponyme.js';
|
|
2
|
+
export interface EponymeIndexRow {
|
|
3
|
+
entryName: string;
|
|
4
|
+
version: 'draft' | 'published';
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
/** Rejecting a query on an unknown key beats silently returning the whole collection. */
|
|
9
|
+
export declare function eponymeIndexKeys(schema: EponymeSchema): string[];
|
|
10
|
+
export declare function isEponymeIndexKey(schema: EponymeSchema, key: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Deliberately not `schemaFingerprint`, which covers names and types only. That one is the
|
|
13
|
+
* import compatibility contract — widening it would start refusing imports over harmless
|
|
14
|
+
* edits. Here the opposite bias applies: `field.select()` losing an option, or
|
|
15
|
+
* `field.tags({ allowCustom })` being turned off, makes `reconcile` drop stored values, and
|
|
16
|
+
* `defaultValue` decides what an entry missing the field gets indexed with. All of that has
|
|
17
|
+
* to invalidate the index, and none of it changes a name or a type.
|
|
18
|
+
*
|
|
19
|
+
* `validate` and `visibleWhen` are functions, kept as their mere presence: a body cannot be
|
|
20
|
+
* hashed honestly, since a minifier would change it without changing behaviour. Editing a
|
|
21
|
+
* custom validator so that it rejects previously stored values therefore needs a manual
|
|
22
|
+
* `reindexEponymeEntries()`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function describeEponymeIndexSchema(schema: EponymeSchema): string;
|
|
25
|
+
/**
|
|
26
|
+
* Only top-level fields are covered, which is the same reach as `orderBy`: a field nested
|
|
27
|
+
* in a section, a tab or an array is not addressable by a single key, so neither sorting
|
|
28
|
+
* nor filtering can name one.
|
|
29
|
+
*
|
|
30
|
+
* Draft and published are indexed separately. The dashboard lists drafts and a public page
|
|
31
|
+
* lists published entries, and the two disagree on purpose — one index for both would make
|
|
32
|
+
* an unpublished tag leak into a public listing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildEponymeIndexRows(entryName: string, schema: EponymeSchema, versions: {
|
|
35
|
+
draft: Record<string, unknown>;
|
|
36
|
+
published: Record<string, unknown>;
|
|
37
|
+
}): EponymeIndexRow[];
|
|
38
|
+
/** The form both the write path and a query reduce a value to, so they always agree. */
|
|
39
|
+
export declare function foldEponymeIndexValue(value: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const INDEXABLE_TYPES = /* @__PURE__ */ new Set(["tags", "checkboxGroup", "select", "radio", "boolean", "date"]);
|
|
2
|
+
const INDEX_ALGORITHM = 1;
|
|
3
|
+
const COSMETIC_OPTION_KEYS = /* @__PURE__ */ new Set(["label", "description", "placeholder"]);
|
|
4
|
+
export function eponymeIndexKeys(schema) {
|
|
5
|
+
return Object.keys(schema).filter((name) => isIndexable(schema[name]));
|
|
6
|
+
}
|
|
7
|
+
export function isEponymeIndexKey(schema, key) {
|
|
8
|
+
return isIndexable(schema[key]);
|
|
9
|
+
}
|
|
10
|
+
function isIndexable(definition) {
|
|
11
|
+
return Boolean(definition && INDEXABLE_TYPES.has(definition.type));
|
|
12
|
+
}
|
|
13
|
+
export function describeEponymeIndexSchema(schema) {
|
|
14
|
+
const fields = Object.keys(schema).filter((name) => isIndexable(schema[name])).sort().map((name) => `${name}:${schema[name].type}:${stableDescribe(schema[name].options)}`);
|
|
15
|
+
return `v${INDEX_ALGORITHM}|${fields.join(",")}`;
|
|
16
|
+
}
|
|
17
|
+
function stableDescribe(value) {
|
|
18
|
+
if (typeof value === "function") return "fn";
|
|
19
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
20
|
+
if (Array.isArray(value)) return `[${value.map(stableDescribe).join(",")}]`;
|
|
21
|
+
const entries = Object.keys(value).filter((key) => !COSMETIC_OPTION_KEYS.has(key)).sort().map((key) => `${key}:${stableDescribe(value[key])}`);
|
|
22
|
+
return `{${entries.join(",")}}`;
|
|
23
|
+
}
|
|
24
|
+
export function buildEponymeIndexRows(entryName, schema, versions) {
|
|
25
|
+
const rows = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const version of ["draft", "published"]) {
|
|
27
|
+
const data = versions[version];
|
|
28
|
+
for (const [key, definition] of Object.entries(schema)) {
|
|
29
|
+
if (!isIndexable(definition)) continue;
|
|
30
|
+
for (const value of indexValues(data[key])) {
|
|
31
|
+
rows.set(`${version}\0${key}\0${value}`, { entryName, version, key, value });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...rows.values()];
|
|
36
|
+
}
|
|
37
|
+
function indexValues(value) {
|
|
38
|
+
if (value === void 0 || value === null || value === "") return [];
|
|
39
|
+
if (typeof value === "boolean") return [String(value)];
|
|
40
|
+
if (typeof value === "string") return [fold(value)];
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.flatMap((item) => typeof item === "string" && item.trim() ? [fold(item)] : []);
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
export function foldEponymeIndexValue(value) {
|
|
47
|
+
return fold(value);
|
|
48
|
+
}
|
|
49
|
+
function fold(value) {
|
|
50
|
+
return value.trim().toLocaleLowerCase();
|
|
51
|
+
}
|
|
@@ -17,6 +17,8 @@ export function getFieldDefaultValue(definition) {
|
|
|
17
17
|
return [];
|
|
18
18
|
case "checkboxGroup":
|
|
19
19
|
return [];
|
|
20
|
+
case "tags":
|
|
21
|
+
return [];
|
|
20
22
|
case "section":
|
|
21
23
|
return Object.fromEntries(
|
|
22
24
|
Object.entries(definition.options.fields).map(([name, field]) => [name, getFieldDefaultValue(field)])
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EponymeSchema } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rewrites every value that has a canonical form — a phone in E.164, a tag list deduplicated —
|
|
4
|
+
* wherever it sits in the schema.
|
|
5
|
+
*
|
|
6
|
+
* Applied on the server before a write, which is what makes a stored format a guarantee rather
|
|
7
|
+
* than a convention: the API accepts any JSON, so a client normalising on its own could always
|
|
8
|
+
* be bypassed. A value that cannot be understood is left as sent, for validation to reject.
|
|
9
|
+
*
|
|
10
|
+
* One walk for every field type that needs one. A second walker would mean a second place to
|
|
11
|
+
* forget a container when a field type is added.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeEponymeValues(schema: EponymeSchema, data: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isArrayItemFieldDefinition } from "./get-field-default-value.js";
|
|
2
2
|
import { toEponymePhoneValue } from "./normalize-phone.js";
|
|
3
|
-
|
|
3
|
+
import { normalizeEponymeTags } from "./normalize-tags.js";
|
|
4
|
+
export function normalizeEponymeValues(schema, data) {
|
|
4
5
|
const result = { ...data };
|
|
5
6
|
for (const [name, definition] of Object.entries(schema)) {
|
|
6
7
|
if (!(name in result)) continue;
|
|
@@ -10,6 +11,7 @@ export function normalizeEponymePhones(schema, data) {
|
|
|
10
11
|
}
|
|
11
12
|
function normalizeField(definition, value) {
|
|
12
13
|
if (definition.type === "phone") return toEponymePhoneValue(value, definition.options);
|
|
14
|
+
if (definition.type === "tags") return Array.isArray(value) ? normalizeEponymeTags(value, definition.options) : value;
|
|
13
15
|
if (definition.type === "section")
|
|
14
16
|
return normalizeNested(definition.options.fields, value);
|
|
15
17
|
if (definition.type === "tabs") {
|
|
@@ -31,7 +33,7 @@ function normalizeArrayItem(of, item) {
|
|
|
31
33
|
}
|
|
32
34
|
function normalizeNested(schema, value) {
|
|
33
35
|
if (!isRecord(value)) return value;
|
|
34
|
-
return
|
|
36
|
+
return normalizeEponymeValues(schema, value);
|
|
35
37
|
}
|
|
36
38
|
function isRecord(value) {
|
|
37
39
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TagsFieldOptions } from '../types/field.js';
|
|
2
|
+
/**
|
|
3
|
+
* The canonical form of a tag list, shared by the validation the server enforces, the editor
|
|
4
|
+
* field, and the write path that stores the value.
|
|
5
|
+
*
|
|
6
|
+
* A tag list is a taxonomy, so `Nuxt` and ` nuxt ` are the same tag and must not both exist.
|
|
7
|
+
* The first spelling wins, and a suggestion counts as a first spelling: typing `nuxt` next to a
|
|
8
|
+
* declared `Nuxt` yields `Nuxt`, which keeps a listing from splitting in two.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeEponymeTags(value: unknown, options?: TagsFieldOptions): string[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function normalizeEponymeTags(value, options = {}) {
|
|
2
|
+
if (!Array.isArray(value)) return [];
|
|
3
|
+
const suggestions = new Map((options.suggestions ?? []).map((tag) => [fold(tag), tag]));
|
|
4
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const item of value) {
|
|
6
|
+
if (typeof item !== "string") continue;
|
|
7
|
+
const tag = item.trim();
|
|
8
|
+
if (!tag) continue;
|
|
9
|
+
const key = fold(tag);
|
|
10
|
+
if (seen.has(key)) continue;
|
|
11
|
+
seen.set(key, suggestions.get(key) ?? tag);
|
|
12
|
+
}
|
|
13
|
+
return [...seen.values()];
|
|
14
|
+
}
|
|
15
|
+
function fold(tag) {
|
|
16
|
+
return tag.trim().toLocaleLowerCase();
|
|
17
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isArrayItemFieldDefinition } from "./get-field-default-value.js";
|
|
2
2
|
import { isFieldVisible } from "./is-field-visible.js";
|
|
3
3
|
import { normalizeEponymePhone } from "./normalize-phone.js";
|
|
4
|
+
import { normalizeEponymeTags } from "./normalize-tags.js";
|
|
4
5
|
const urlPattern = /^https?:\/\//i;
|
|
5
6
|
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
6
7
|
const colorPattern = /^#(?:[\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i;
|
|
@@ -71,6 +72,21 @@ function validateFieldRules(name, definition, value, errors, mode, _context) {
|
|
|
71
72
|
}
|
|
72
73
|
return;
|
|
73
74
|
}
|
|
75
|
+
if (definition.type === "tags") {
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
addError(errors, name, "Must be an array.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const tags = normalizeEponymeTags(value, definition.options);
|
|
81
|
+
if (mode === "publish" && options.required && tags.length === 0) addError(errors, name, "This field is required.");
|
|
82
|
+
if (mode === "publish" && definition.options.minItems !== void 0 && tags.length < definition.options.minItems) addError(errors, name, `Must contain at least ${definition.options.minItems} items.`);
|
|
83
|
+
if (definition.options.maxItems !== void 0 && tags.length > definition.options.maxItems) addError(errors, name, `Must contain at most ${definition.options.maxItems} items.`);
|
|
84
|
+
if (value.some((item) => typeof item !== "string" || !item.trim())) addError(errors, name, "Must contain only non-empty tags.");
|
|
85
|
+
const suggestions = definition.options.suggestions;
|
|
86
|
+
if (!definition.options.allowCustom && suggestions?.length && tags.some((tag) => !suggestions.includes(tag)))
|
|
87
|
+
addError(errors, name, "Must contain only available options.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
74
90
|
if (definition.type === "checkboxGroup") {
|
|
75
91
|
if (!Array.isArray(value)) {
|
|
76
92
|
addError(errors, name, "Must be an array.");
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { EponymeVariableDefinition, EponymeVariables } from '../types/variables.js';
|
|
2
|
+
export declare function findEponymeVariableRanges(text: string): Array<{
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
}>;
|
|
2
6
|
/** Always available, whatever the host application configures. */
|
|
3
7
|
export declare function builtinEponymeVariables(now?: Date): Record<string, EponymeVariableDefinition>;
|
|
4
8
|
/** Host variables win, so an application can override a built-in for its own wording. */
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
const
|
|
1
|
+
const VARIABLE_PATTERN_SOURCE = String.raw`\{\{\s*([A-Z_]\w*)\s*\}\}`;
|
|
2
|
+
const VARIABLE_PATTERN = new RegExp(VARIABLE_PATTERN_SOURCE, "gi");
|
|
3
|
+
export function findEponymeVariableRanges(text) {
|
|
4
|
+
return [...text.matchAll(new RegExp(VARIABLE_PATTERN_SOURCE, "gi"))].map((match) => ({
|
|
5
|
+
from: match.index,
|
|
6
|
+
to: match.index + match[0].length
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
2
9
|
export function builtinEponymeVariables(now = /* @__PURE__ */ new Date()) {
|
|
3
10
|
return {
|
|
4
11
|
currentYear: { label: "Current year", description: "The year at the time the page is served.", value: () => now.getFullYear() },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karibsen/eponyme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A lightweight, type-safe content manager for Nuxt.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,13 +60,14 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@iconify-json/mingcute": "^1.2.7",
|
|
63
|
-
"@karibsen/ui": "
|
|
63
|
+
"@karibsen/ui": "^1.0.1",
|
|
64
64
|
"@nuxt/icon": "^2.3.1",
|
|
65
65
|
"@nuxt/kit": "^4.4.8",
|
|
66
66
|
"@tailwindcss/vite": "^4.1.12",
|
|
67
67
|
"@tanstack/vue-table": "^8.21.3",
|
|
68
68
|
"@tiptap/extension-image": "^3.29.2",
|
|
69
69
|
"@tiptap/extension-placeholder": "^3.28.0",
|
|
70
|
+
"@tiptap/pm": "^3.28.0",
|
|
70
71
|
"@tiptap/starter-kit": "^3.28.0",
|
|
71
72
|
"@tiptap/vue-3": "^3.28.0",
|
|
72
73
|
"@vueuse/core": "^14.3.0",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { EponymeSchema } from '../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Rewrites every phone value in a payload to E.164, wherever it sits in the schema.
|
|
4
|
-
*
|
|
5
|
-
* Applied on the server before a write, which is what makes the stored format a guarantee
|
|
6
|
-
* rather than a convention: the API accepts any JSON, so a client normalising on its own could
|
|
7
|
-
* always be bypassed. Values that cannot be parsed are left as typed for validation to reject.
|
|
8
|
-
*/
|
|
9
|
-
export declare function normalizeEponymePhones(schema: EponymeSchema, data: Record<string, unknown>): Record<string, unknown>;
|