@json-layout/vocabulary 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +54 -0
  3. package/src/index.js +4 -0
  4. package/src/layout-keyword/index.js +62 -0
  5. package/src/layout-keyword/schema.js +367 -0
  6. package/src/layout-keyword/schema.json +209 -0
  7. package/src/layout-keyword/types.ts +109 -0
  8. package/src/layout-keyword/validate.js +2 -0
  9. package/src/normalize.js +313 -0
  10. package/src/normalized-layout/index.js +105 -0
  11. package/src/normalized-layout/schema.js +824 -0
  12. package/src/normalized-layout/schema.json +413 -0
  13. package/src/normalized-layout/types.ts +243 -0
  14. package/src/normalized-layout/validate.js +3 -0
  15. package/src/types.ts +19 -0
  16. package/types/index.d.ts +5 -0
  17. package/types/index.d.ts.map +1 -0
  18. package/types/layout-keyword/index.d.ts +42 -0
  19. package/types/layout-keyword/index.d.ts.map +1 -0
  20. package/types/layout-keyword/schema.d.ts +279 -0
  21. package/types/layout-keyword/schema.d.ts.map +1 -0
  22. package/types/layout-keyword/types.d.ts +90 -0
  23. package/types/layout-keyword/types.d.ts.map +1 -0
  24. package/types/layout-keyword/validate.d.ts +24 -0
  25. package/types/layout-keyword/validate.d.ts.map +1 -0
  26. package/types/normalize.d.ts +18 -0
  27. package/types/normalize.d.ts.map +1 -0
  28. package/types/normalized-layout/index.d.ts +96 -0
  29. package/types/normalized-layout/index.d.ts.map +1 -0
  30. package/types/normalized-layout/schema.d.ts +626 -0
  31. package/types/normalized-layout/schema.d.ts.map +1 -0
  32. package/types/normalized-layout/types.d.ts +221 -0
  33. package/types/normalized-layout/types.d.ts.map +1 -0
  34. package/types/normalized-layout/validate.d.ts +23 -0
  35. package/types/normalized-layout/validate.d.ts.map +1 -0
  36. package/types/types.d.ts +19 -0
  37. package/types/types.d.ts.map +1 -0
@@ -0,0 +1,313 @@
1
+ import { validateLayoutKeyword, isComponentName, isPartialCompObject, isPartialChildren, isPartialSwitch, isPartialGetItemsExpr, isPartialGetItemsObj, isPartialSlotMarkdown, isPartialGetItemsFetch } from './layout-keyword/index.js'
2
+ import { validateNormalizedLayout, normalizedLayoutSchema, isSectionLayout, isCompositeLayout } from './index.js'
3
+
4
+ /**
5
+ * @typedef {import('./index.js').Child} Child
6
+ * @typedef {import('./index.js').Children} Children
7
+ * @typedef {import('./index.js').CompObject} CompObject
8
+ * @typedef {import('./index.js').Expression} Expression
9
+ * @typedef {import('./index.js').NormalizedLayout} NormalizedLayout
10
+ * @typedef {import('./index.js').LayoutKeyword} LayoutKeyword
11
+ * @typedef {import('./index.js').PartialChildren} PartialChildren
12
+ * @typedef {import('./index.js').PartialCompObject} PartialCompObject
13
+ * @typedef {import("./types.js").SchemaFragment} SchemaFragment
14
+ */
15
+
16
+ // export type Markdown = (src: string) => string
17
+
18
+ // this navice implementation is sufficient here, not used very often
19
+ const clone = (/** @type {any} */ obj) => JSON.parse(JSON.stringify(obj))
20
+
21
+ /**
22
+ * @param {import('./types.js').SchemaFragment} schemaFragment
23
+ * @returns {Children}
24
+ */
25
+ function getDefaultChildren (schemaFragment) {
26
+ /** @type {Children} */
27
+ const children = []
28
+ if (schemaFragment.type === 'object') {
29
+ if (schemaFragment.properties) {
30
+ for (const key of Object.keys(schemaFragment.properties)) {
31
+ children.push({ key })
32
+ }
33
+ }
34
+ if (schemaFragment.allOf?.length) {
35
+ for (let i = 0; i < schemaFragment.allOf.length; i++) {
36
+ children.push({ key: `$allOf-${i}` })
37
+ }
38
+ }
39
+ if (schemaFragment.oneOf) {
40
+ children.push({ key: '$oneOf' })
41
+ }
42
+ }
43
+ if (schemaFragment.type === 'array' && Array.isArray(schemaFragment.items)) {
44
+ for (let i = 0; i < schemaFragment.items.length; i++) {
45
+ children.push({ key: i })
46
+ }
47
+ }
48
+ return children
49
+ }
50
+
51
+ /**
52
+ * @param {Children} defaultChildren
53
+ * @param {PartialChildren} [partialChildren]
54
+ * @returns {Children}
55
+ */
56
+ function getChildren (defaultChildren, partialChildren) {
57
+ if (!partialChildren) return defaultChildren
58
+ let compI = 0
59
+ return partialChildren.map(partialChild => {
60
+ if (typeof partialChild === 'string') { // simple string/key referencing a known child
61
+ const matchingDefaultChild = defaultChildren.find(c => c.key === partialChild)
62
+ if (!matchingDefaultChild) throw new Error(`child unknown ${partialChild}`)
63
+ return matchingDefaultChild
64
+ } else {
65
+ if (typeof partialChild.cols === 'number') partialChild.cols = { sm: partialChild.cols }
66
+ if (typeof partialChild.cols === 'object' && partialChild.cols.xs === undefined) partialChild.cols.xs = 12
67
+ if (partialChild.key) { // object referencing known child and overwriting cols
68
+ const matchingDefaultChild = defaultChildren.find(c => c.key === partialChild.key)
69
+ if (!matchingDefaultChild) throw new Error(`child unknown ${partialChild.key}`)
70
+ return /** @type {Child} */ (partialChild)
71
+ } else { // a composite component definition, not directly related to a known child
72
+ const child = partialChild
73
+ if (partialChild.children) {
74
+ if (!partialChild.comp) child.comp = 'section'
75
+ child.children = getChildren(defaultChildren, partialChild.children)
76
+ }
77
+ if (!('key' in partialChild)) {
78
+ child.key = `$comp-${compI}`
79
+ compI++
80
+ }
81
+ return /** @type {Child} */ (child)
82
+ }
83
+ }
84
+ })
85
+ }
86
+
87
+ /**
88
+ * @param {SchemaFragment} schemaFragment
89
+ * @param {string} schemaPath
90
+ * @returns {import('./index.js').CompObject}
91
+ */
92
+ function getDefaultCompObject (schemaFragment, schemaPath) {
93
+ const key = schemaPath.slice(schemaPath.lastIndexOf('/') + 1)
94
+ if ('const' in schemaFragment) return { comp: 'none' }
95
+ if (!schemaFragment.type) return { comp: 'none' }
96
+ if (schemaFragment.type === 'object') {
97
+ return { comp: 'section', title: schemaFragment.title ?? null, children: getDefaultChildren(schemaFragment) }
98
+ }
99
+ if (schemaFragment.type === 'array') {
100
+ if (!schemaFragment.items) return { comp: 'none' }
101
+ if (Array.isArray(schemaFragment.items)) {
102
+ return { comp: 'section', title: schemaFragment.title ?? null, children: getDefaultChildren(schemaFragment) } // tuples
103
+ }
104
+ return {
105
+ comp: 'list',
106
+ title: schemaFragment.title ?? key,
107
+ listEditMode: schemaFragment.items.type === 'object' ? 'inline-single' : 'inline',
108
+ listActions: ['add', 'edit', 'delete', 'duplicate', 'sort']
109
+ }
110
+ }
111
+ const hasSimpleType = ['string', 'integer', 'number'].includes(schemaFragment.type)
112
+ if (schemaFragment.enum && hasSimpleType) {
113
+ return {
114
+ comp: 'select',
115
+ label: schemaFragment.title ?? key,
116
+ items: schemaFragment.enum.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
117
+ }
118
+ }
119
+ if (schemaFragment.oneOf && !(schemaFragment.oneOf.find((/** @type {any} */ oneOfItem) => !('const' in oneOfItem))) && hasSimpleType) {
120
+ return {
121
+ comp: 'select',
122
+ label: schemaFragment.title ?? key,
123
+ items: schemaFragment.oneOf.map((/** @type {{ const: string; title: any; }} */ oneOfItem) => ({ key: oneOfItem.const + '', title: (oneOfItem.title ?? oneOfItem.const) + '', value: oneOfItem.const }))
124
+ }
125
+ }
126
+ if (schemaFragment.type === 'string') {
127
+ if (schemaFragment.format) {
128
+ /** @type {CompObject | null} */
129
+ let formatCompObject = null
130
+ if (schemaFragment.format === 'date') formatCompObject = { comp: 'date-picker', label: schemaFragment.title ?? key, format: 'date' }
131
+ if (schemaFragment.format === 'date-time') formatCompObject = { comp: 'date-time-picker', label: schemaFragment.title ?? key }
132
+ if (schemaFragment.format === 'time') formatCompObject = { comp: 'time-picker', label: schemaFragment.title ?? key }
133
+ if (formatCompObject) {
134
+ if ('formatMinimum' in schemaFragment) formatCompObject.min = schemaFragment.formatMinimum
135
+ if ('formatMaximum' in schemaFragment) formatCompObject.max = schemaFragment.formatMaximum
136
+ return formatCompObject
137
+ }
138
+ }
139
+ return { comp: 'text-field', label: schemaFragment.title ?? key }
140
+ }
141
+ if (schemaFragment.type === 'integer' || schemaFragment.type === 'number') {
142
+ /** @type {import('./index.js').NumberField} */
143
+ const compObject = { comp: 'number-field', label: schemaFragment.title ?? key }
144
+ if (schemaFragment.type === 'integer') compObject.step = 1
145
+ if ('minimum' in schemaFragment) compObject.min = schemaFragment.minimum
146
+ if ('maximum' in schemaFragment) compObject.max = schemaFragment.maximum
147
+ return compObject
148
+ }
149
+ if (schemaFragment.type === 'boolean') return { comp: 'checkbox', label: schemaFragment.title ?? key }
150
+ console.warn(`failed to calculate default layout for schema ${schemaPath}`, schemaFragment)
151
+ return { comp: 'none' }
152
+ }
153
+
154
+ /**
155
+ * @param {LayoutKeyword} layoutKeyword
156
+ * @returns {PartialCompObject | null}
157
+ */
158
+ function getPartialCompObject (layoutKeyword) {
159
+ if (isPartialCompObject(layoutKeyword)) return { ...layoutKeyword }
160
+ else if (isComponentName(layoutKeyword)) return { comp: layoutKeyword }
161
+ else if (isPartialChildren(layoutKeyword)) return { children: layoutKeyword }
162
+ return null
163
+ }
164
+
165
+ /**
166
+ * @param {import("./index.js").PartialExpression} expression
167
+ * @param {Expression['type']} defaultType
168
+ * @returns {Expression}
169
+ */
170
+ function normalizeExpression (expression, defaultType = 'js-eval') {
171
+ if (typeof expression === 'string') return { type: defaultType, expr: expression }
172
+ else return { ...expression, type: expression.type ?? defaultType }
173
+ }
174
+
175
+ /**
176
+ * @param {LayoutKeyword} layoutKeyword
177
+ * @param {CompObject} defaultCompObject
178
+ * @param {SchemaFragment} schemaFragment
179
+ * @param {(text: string) => string} markdown
180
+ * @returns {CompObject}
181
+ */
182
+ function getCompObject (layoutKeyword, defaultCompObject, schemaFragment, markdown) {
183
+ const partial = getPartialCompObject(layoutKeyword)
184
+ if (!partial) return defaultCompObject
185
+
186
+ if (partial.if) partial.if = normalizeExpression(partial.if)
187
+ if (partial.getItems && isPartialGetItemsExpr(partial.getItems)) partial.getItems = normalizeExpression(partial.getItems)
188
+ if (partial.getItems && isPartialGetItemsObj(partial.getItems)) {
189
+ if (schemaFragment.type === 'object') partial.getItems.returnObjects = true
190
+ if (partial.getItems.itemTitle) partial.getItems.itemTitle = normalizeExpression(partial.getItems.itemTitle)
191
+ if (partial.getItems.itemKey) partial.getItems.itemKey = normalizeExpression(partial.getItems.itemKey)
192
+ if (partial.getItems.itemValue) partial.getItems.itemValue = normalizeExpression(partial.getItems.itemValue)
193
+ if (partial.getItems.itemsResults) partial.getItems.itemsResults = normalizeExpression(partial.getItems.itemsResults)
194
+ }
195
+ if (partial.getItems && isPartialGetItemsFetch(partial.getItems)) {
196
+ partial.getItems.url = normalizeExpression(partial.getItems.url, 'js-tpl')
197
+ }
198
+
199
+ if (partial.items) {
200
+ partial.items = partial.items.map((/** @type {import('./index.js').PartialSelectItem} */ item) => {
201
+ if (['string', 'integer', 'number'].includes(typeof item)) {
202
+ return { title: item + '', key: item + '', value: item }
203
+ } else if (typeof item === 'object') {
204
+ return {
205
+ key: (item.key ?? item.value) + '',
206
+ title: (item.title ?? item.key ?? item.value) + '',
207
+ value: item.value ?? item.key
208
+ }
209
+ } else {
210
+ throw new Error(`bad item for select: ${JSON.stringify(item)}`)
211
+ }
212
+ })
213
+ }
214
+
215
+ if (!partial.comp && (partial.items ?? partial.getItems)) {
216
+ partial.comp = 'select'
217
+ }
218
+
219
+ if (partial.comp === 'date-picker' && schemaFragment.format === 'date-time') {
220
+ partial.format = 'date-time'
221
+ }
222
+
223
+ if (partial.slots) {
224
+ for (const [name, slot] of Object.entries(partial.slots)) {
225
+ if (typeof slot === 'string') {
226
+ if (['before', 'after'].includes(name)) {
227
+ partial.slots[name] = { markdown: slot }
228
+ } else {
229
+ partial.slots[name] = { name: slot }
230
+ }
231
+ }
232
+ const slotObj = partial.slots[name]
233
+ if (isPartialSlotMarkdown(slotObj)) {
234
+ slotObj.markdown = markdown(slotObj.markdown).trim()
235
+ }
236
+ }
237
+ }
238
+
239
+ /** @type {any} */
240
+ const compObject = {}
241
+ if (partial.comp && defaultCompObject.comp !== partial.comp) {
242
+ const compProperties = normalizedLayoutSchema.$defs[partial.comp]?.properties
243
+ if (typeof compProperties === 'object') {
244
+ for (const key of Object.keys(compProperties).concat(['if', 'help', 'cols', 'props', 'slots', 'options'])) {
245
+ if (key in defaultCompObject) compObject[key] = defaultCompObject[key]
246
+ if (key in partial) compObject[key] = partial[key]
247
+ }
248
+ }
249
+ } else {
250
+ Object.assign(compObject, defaultCompObject, partial)
251
+ }
252
+
253
+ if (isSectionLayout(defaultCompObject) && isCompositeLayout(compObject)) {
254
+ compObject.children = getChildren((defaultCompObject).children, partial.children)
255
+ }
256
+
257
+ if (compObject.description && !compObject.help) compObject.help = compObject.description
258
+ if (compObject.help) compObject.help = markdown(compObject.help).trim()
259
+
260
+ if (typeof compObject.cols === 'number') compObject.cols = { xs: compObject.cols }
261
+ if (typeof compObject.cols === 'object' && compObject.cols.xs === undefined) compObject.cols.xs = 12
262
+
263
+ return compObject
264
+ }
265
+
266
+ /**
267
+ * @param {LayoutKeyword} layoutKeyword
268
+ * @param {CompObject} defaultCompObject
269
+ * @param {SchemaFragment} schemaFragment
270
+ * @param {(text: string) => string} markdown
271
+ * @returns {NormalizedLayout}
272
+ */
273
+ function getNormalizedLayout (layoutKeyword, defaultCompObject, schemaFragment, markdown) {
274
+ if (isPartialSwitch(layoutKeyword)) {
275
+ const switchCases = layoutKeyword.switch.map(layout => getCompObject(layout, clone(defaultCompObject), schemaFragment, markdown))
276
+ if (!switchCases.find(switchCase => !switchCase.if)) switchCases.push(clone(defaultCompObject))
277
+ return { switch: switchCases }
278
+ } else {
279
+ return getCompObject(layoutKeyword, defaultCompObject, schemaFragment, markdown)
280
+ }
281
+ }
282
+
283
+ /**
284
+ * @param {SchemaFragment} schemaFragment
285
+ * @param {string} schemaPath
286
+ * @param {(text: string) => string} markdown
287
+ * @param {'oneOf'} [arrayChild]
288
+ * @returns {NormalizedLayout}
289
+ */
290
+ export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown = (src) => src, arrayChild) {
291
+ let layoutKeyword
292
+ /** @type {CompObject} */
293
+ let defaultCompObject
294
+ if (arrayChild === 'oneOf') {
295
+ layoutKeyword = schemaFragment.oneOfLayout ?? {}
296
+ defaultCompObject = { comp: 'one-of-select' } // TODO: default label ?
297
+ } else {
298
+ layoutKeyword = schemaFragment.layout ?? {}
299
+ defaultCompObject = getDefaultCompObject(schemaFragment, schemaPath)
300
+ }
301
+ if (!validateLayoutKeyword(layoutKeyword)) {
302
+ console.error(`layout keyword validation errors at path ${schemaPath}`, layoutKeyword, validateLayoutKeyword.errors)
303
+ return defaultCompObject
304
+ // throw new Error(`invalid layout keyword at path ${schemaPath}`, { cause: validateLayoutKeyword.errors })
305
+ }
306
+ const normalizedLayout = getNormalizedLayout(layoutKeyword, defaultCompObject, schemaFragment, markdown)
307
+ if (!validateNormalizedLayout(normalizedLayout)) {
308
+ console.error(`normalized layout validation errors at path ${schemaPath}`, normalizedLayout, validateNormalizedLayout.errors)
309
+ return defaultCompObject
310
+ // throw new Error(`invalid layout at path ${schemaPath}`, { cause: validateNormalizedLayout.errors })
311
+ }
312
+ return normalizedLayout
313
+ }
@@ -0,0 +1,105 @@
1
+ import validate from './validate.js'
2
+ import schema from './schema.js'
3
+
4
+ /**
5
+ * @typedef {import('./types.js').NormalizedLayout} NormalizedLayout
6
+ * @typedef {import('./types.js').SwitchStruct} SwitchStruct
7
+ * @typedef {import('./types.js').CompObject} CompObject
8
+ * @typedef {import('./types.js').Section} Section
9
+ * @typedef {import('./types.js').GetItems} GetItems
10
+ * @typedef {import('./types.js').Expression} Expression
11
+ * @typedef {import('./types.js').Select} Select
12
+ * @typedef {import('./types.js').SelectItem} SelectItem
13
+ * @typedef {import('./types.js').SelectItems} SelectItems
14
+ * @typedef {import('./types.js').GetItemsFetch} GetItemsFetch
15
+ * @typedef {import('./types.js').TextField} TextField
16
+ * @typedef {import('./types.js').Textarea} Textarea
17
+ * @typedef {import('./types.js').NumberField} NumberField
18
+ * @typedef {import('./types.js').Slider} Slider
19
+ * @typedef {import('./types.js').Checkbox} Checkbox
20
+ * @typedef {import('./types.js').Switch} Switch
21
+ * @typedef {import('./types.js').DatePicker} DatePicker
22
+ * @typedef {import('./types.js').TimePicker} TimePicker
23
+ * @typedef {import('./types.js').DateTimePicker} DateTimePicker
24
+ * @typedef {import('./types.js').ColorPicker} ColorPicker
25
+ * @typedef {import('./types.js').OneOfSelect} OneOfSelect
26
+ * @typedef {import('./types.js').Child} Child
27
+ * @typedef {import('./types.js').Children} Children
28
+ * @typedef {import('./types.js').CompositeCompObject} CompositeCompObject
29
+ * @typedef {import('./types.js').Tabs} Tabs
30
+ * @typedef {import('./types.js').VerticalTabs} VerticalTabs
31
+ * @typedef {import('./types.js').ExpansionPanels} ExpansionPanels
32
+ * @typedef {import('./types.js').List} List
33
+ * @typedef {import('./types.js').Cols} Cols
34
+ * @typedef {import('./types.js').ColsObj} ColsObj
35
+ * @typedef {import('./types.js').StateNodeOptions} StateNodeOptions
36
+ * @typedef {import('./types.js').Slot} Slot
37
+ * @typedef {{ errors: any, (layoutKeyword: any): layoutKeyword is NormalizedLayout }} ValidateNormalizedLayout
38
+ */
39
+
40
+ export const /** @type {ValidateNormalizedLayout} */ validateNormalizedLayout = /** @type {any} */ (validate)
41
+
42
+ export const normalizedLayoutSchema = /** @type {any} */ (schema)
43
+
44
+ /** @type {(layout: NormalizedLayout) => layout is SwitchStruct} */
45
+ export function isSwitchStruct (layout) {
46
+ return typeof layout === 'object' && 'switch' in layout
47
+ }
48
+
49
+ /** @type {(layout: NormalizedLayout) => layout is CompObject} */
50
+ export function isCompObject (layout) {
51
+ return !isSwitchStruct(layout)
52
+ }
53
+
54
+ /** @type {(child: Child) => child is Child & CompositeCompObject} */
55
+ export function childIsCompObject (child) {
56
+ return !!child.comp
57
+ }
58
+
59
+ /** @type {(layout: CompObject) => layout is Section} */
60
+ export function isSectionLayout (layout) {
61
+ return layout.comp === 'section'
62
+ }
63
+
64
+ /** @type {(layout: CompObject) => layout is CompositeCompObject} */
65
+ export function isCompositeLayout (layout) {
66
+ return ['section', 'tabs', 'vertical-tabs', 'expansion-panels'].includes(layout.comp)
67
+ }
68
+
69
+ /** @type {(layout: CompObject) => layout is TextField} */
70
+ export function isTextFieldLayout (layout) {
71
+ return layout.comp === 'text-field'
72
+ }
73
+
74
+ /** @type {(layout: CompObject) => layout is Select} */
75
+ export function isSelectLayout (layout) {
76
+ return layout.comp === 'select'
77
+ }
78
+
79
+ /** @type {(getItems: GetItems) => getItems is Expression} */
80
+ export function isGetItemsExpression (getItems) {
81
+ return !!getItems.expr
82
+ }
83
+
84
+ /** @type {(getItems: GetItems) => getItems is GetItemsFetch} */
85
+ export function isGetItemsFetch (getItems) {
86
+ return !!getItems.url
87
+ }
88
+
89
+ /** @type {(slot: Slot) => slot is {text: string}} */
90
+ export function isTextSlot (slot) {
91
+ // @ts-ignore
92
+ return !!slot.text
93
+ }
94
+
95
+ /** @type {(slot: Slot) => slot is {markdown: string}} */
96
+ export function isMarkdownSlot (slot) {
97
+ // @ts-ignore
98
+ return !!slot.markdown
99
+ }
100
+
101
+ /** @type {(slot: Slot) => slot is {name: string}} */
102
+ export function isNameSlot (slot) {
103
+ // @ts-ignore
104
+ return !!slot.name
105
+ }