@json-layout/core 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.
- package/LICENSE +21 -0
- package/package.json +70 -0
- package/src/compile/index.js +136 -0
- package/src/compile/serialize.js +76 -0
- package/src/compile/skeleton-node.js +162 -0
- package/src/compile/skeleton-tree.js +28 -0
- package/src/compile/types.ts +50 -0
- package/src/compile/utils/resolve-refs.js +69 -0
- package/src/i18n/en.js +10 -0
- package/src/i18n/fr.js +10 -0
- package/src/i18n/index.js +8 -0
- package/src/i18n/types.ts +14 -0
- package/src/index.js +3 -0
- package/src/state/index.js +381 -0
- package/src/state/state-node.js +317 -0
- package/src/state/state-tree.js +59 -0
- package/src/state/types.ts +89 -0
- package/src/state/utils/display.js +132 -0
- package/src/state/utils/immutable.js +11 -0
- package/types/compile/index.d.ts +14 -0
- package/types/compile/index.d.ts.map +1 -0
- package/types/compile/serialize.d.ts +6 -0
- package/types/compile/serialize.d.ts.map +1 -0
- package/types/compile/skeleton-node.d.ts +14 -0
- package/types/compile/skeleton-node.d.ts.map +1 -0
- package/types/compile/skeleton-tree.d.ts +12 -0
- package/types/compile/skeleton-tree.d.ts.map +1 -0
- package/types/compile/types.d.ts +43 -0
- package/types/compile/types.d.ts.map +1 -0
- package/types/compile/utils/resolve-refs.d.ts +9 -0
- package/types/compile/utils/resolve-refs.d.ts.map +1 -0
- package/types/i18n/en.d.ts +3 -0
- package/types/i18n/en.d.ts.map +1 -0
- package/types/i18n/fr.d.ts +3 -0
- package/types/i18n/fr.d.ts.map +1 -0
- package/types/i18n/index.d.ts +3 -0
- package/types/i18n/index.d.ts.map +1 -0
- package/types/i18n/types.d.ts +13 -0
- package/types/i18n/types.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/state/index.d.ts +185 -0
- package/types/state/index.d.ts.map +1 -0
- package/types/state/state-node.d.ts +31 -0
- package/types/state/state-node.d.ts.map +1 -0
- package/types/state/state-tree.d.ts +13 -0
- package/types/state/state-tree.d.ts.map +1 -0
- package/types/state/types.d.ts +119 -0
- package/types/state/types.d.ts.map +1 -0
- package/types/state/utils/display.d.ts +51 -0
- package/types/state/utils/display.d.ts.map +1 -0
- package/types/state/utils/immutable.d.ts +8 -0
- package/types/state/utils/immutable.d.ts.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 json-layout
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@json-layout/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compilation and state management utilities for JSON Layout.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": {
|
|
9
|
+
"default": "./src/index.js",
|
|
10
|
+
"types": "./types/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"./src/compile/serialize": {
|
|
14
|
+
"import": {
|
|
15
|
+
"default": "./src/compile/serialize.js",
|
|
16
|
+
"types": "./types/compile/serialize.d.ts"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"types"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test:single": "node --no-experimental-fetch --test --test-only test/*.spec.js",
|
|
26
|
+
"test": "node --no-experimental-fetch --test test/*.spec.js",
|
|
27
|
+
"build": "rm -rf ./types && tsc -p tsconfig.build.json",
|
|
28
|
+
"lint": "eslint . --fix",
|
|
29
|
+
"watch:build": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
30
|
+
"watch:test": "node --no-experimental-fetch --test --watch test/*.spec.js"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/json-layout/json-layout.git"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"json",
|
|
38
|
+
"schema",
|
|
39
|
+
"forms",
|
|
40
|
+
"layout"
|
|
41
|
+
],
|
|
42
|
+
"author": "Alban Mouton <alban.mouton@gmail.com>",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/json-layout/json-layout/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/json-layout/json-layout#readme",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@json-layout/vocabulary": "^0.1.0",
|
|
50
|
+
"@types/markdown-it": "^13.0.1",
|
|
51
|
+
"ajv": "^8.12.0",
|
|
52
|
+
"ajv-errors": "^3.0.0",
|
|
53
|
+
"ajv-formats": "^2.1.1",
|
|
54
|
+
"ajv-i18n": "^4.2.0",
|
|
55
|
+
"debug": "^4.3.4",
|
|
56
|
+
"immer": "^10.0.3",
|
|
57
|
+
"magicast": "^0.3.0",
|
|
58
|
+
"markdown-it": "^13.0.2",
|
|
59
|
+
"mitt": "^3.0.1",
|
|
60
|
+
"recast": "^0.23.4",
|
|
61
|
+
"rfdc": "^1.3.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/debug": "^4.1.7",
|
|
65
|
+
"@types/node-fetch": "^2.6.5",
|
|
66
|
+
"json-schema-to-typescript": "^13.1.1",
|
|
67
|
+
"nock": "^13.3.3",
|
|
68
|
+
"node-fetch": "^2.7.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// compileStatic is meant to produce a serializable result
|
|
2
|
+
|
|
3
|
+
import ajvModule from 'ajv'
|
|
4
|
+
import rfdc from 'rfdc'
|
|
5
|
+
// import { Parser as ExprEvalParser } from 'expr-eval'
|
|
6
|
+
import addFormats from 'ajv-formats'
|
|
7
|
+
import ajvErrors from 'ajv-errors'
|
|
8
|
+
import ajvLocalizeModule from 'ajv-i18n'
|
|
9
|
+
import MarkdownIt from 'markdown-it'
|
|
10
|
+
import i18n from '../i18n/index.js'
|
|
11
|
+
import { makeSkeletonTree } from './skeleton-tree.js'
|
|
12
|
+
import { resolveRefs } from './utils/resolve-refs.js'
|
|
13
|
+
|
|
14
|
+
export { resolveRefs } from './utils/resolve-refs.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {import('./types.js').SkeletonTree} SkeletonTree
|
|
18
|
+
* @typedef {import('./types.js').SkeletonNode} SkeletonNode
|
|
19
|
+
* @typedef {import('./types.js').CompiledLayout} CompiledLayout
|
|
20
|
+
* @typedef {import('./types.js').CompileOptions} CompileOptions
|
|
21
|
+
* @typedef {import('./types.js').PartialCompileOptions} PartialCompileOptions
|
|
22
|
+
* @typedef {import('./types.js').CompiledExpression} CompiledExpression
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
const Ajv = /** @type {typeof ajvModule.default} */ (ajvModule)
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
const ajvLocalize = /** @type {typeof ajvLocalizeModule.default} */ (ajvLocalizeModule)
|
|
29
|
+
|
|
30
|
+
const expressionsParams = ['data', 'options', 'display']
|
|
31
|
+
|
|
32
|
+
const clone = rfdc()
|
|
33
|
+
// const exprEvalParser = new ExprEvalParser()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {PartialCompileOptions} partialOptions
|
|
37
|
+
* @returns {CompileOptions}
|
|
38
|
+
*/
|
|
39
|
+
const fillOptions = (partialOptions) => {
|
|
40
|
+
let ajv = partialOptions.ajv
|
|
41
|
+
if (!ajv) {
|
|
42
|
+
/** @type {import('ajv').Options} */
|
|
43
|
+
const ajvOpts = { strict: false, allErrors: true }
|
|
44
|
+
if (partialOptions.code) ajvOpts.code = { source: true, esm: true, lines: true }
|
|
45
|
+
const newAjv = new Ajv(ajvOpts)
|
|
46
|
+
addFormats.default(newAjv)
|
|
47
|
+
ajvErrors.default(newAjv)
|
|
48
|
+
ajv = newAjv
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let markdown = partialOptions.markdown
|
|
52
|
+
if (!markdown) {
|
|
53
|
+
const markdownIt = new MarkdownIt(partialOptions.markdownIt ?? {})
|
|
54
|
+
markdown = markdownIt.render.bind(markdownIt)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const locale = partialOptions.locale || 'en'
|
|
58
|
+
const messages = { ...i18n[locale] || i18n.en }
|
|
59
|
+
if (partialOptions.messages) Object.assign(messages, partialOptions.messages)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ajv,
|
|
63
|
+
code: false,
|
|
64
|
+
markdown,
|
|
65
|
+
...partialOptions,
|
|
66
|
+
locale,
|
|
67
|
+
messages
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {object} _schema
|
|
73
|
+
* @param {PartialCompileOptions} [partialOptions]
|
|
74
|
+
* @returns {CompiledLayout}
|
|
75
|
+
*/
|
|
76
|
+
export function compile (_schema, partialOptions = {}) {
|
|
77
|
+
const options = fillOptions(partialOptions)
|
|
78
|
+
|
|
79
|
+
const schema = /** @type {import('ajv').SchemaObject} */(clone(_schema))
|
|
80
|
+
|
|
81
|
+
schema.$id = schema.$id ?? '_jl'
|
|
82
|
+
resolveRefs(schema, options.ajv, options.locale)
|
|
83
|
+
|
|
84
|
+
/** @type {string[]} */
|
|
85
|
+
const validatePointers = []
|
|
86
|
+
/** @type {Record<string, import('@json-layout/vocabulary').NormalizedLayout>} */
|
|
87
|
+
const normalizedLayouts = {}
|
|
88
|
+
/** @type {import('@json-layout/vocabulary').Expression[]} */
|
|
89
|
+
const expressionsDefinitions = []
|
|
90
|
+
|
|
91
|
+
const skeletonTree = makeSkeletonTree(schema, options, validatePointers, normalizedLayouts, expressionsDefinitions, `${schema.$id}#`, 'main')
|
|
92
|
+
|
|
93
|
+
options.ajv.addSchema(schema)
|
|
94
|
+
|
|
95
|
+
const uriResolver = options.ajv.opts.uriResolver
|
|
96
|
+
/** @type {Record<string, import('ajv').ValidateFunction>} */
|
|
97
|
+
const validates = {}
|
|
98
|
+
for (const pointer of validatePointers) {
|
|
99
|
+
const fullPointer = uriResolver.resolve(schema.$id, pointer)
|
|
100
|
+
validates[pointer] = options.ajv.compile({ $ref: fullPointer })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @type {CompiledExpression[]} */
|
|
104
|
+
const expressions = []
|
|
105
|
+
|
|
106
|
+
for (const expression of expressionsDefinitions) {
|
|
107
|
+
/* if (expression.type === 'expr-eval') {
|
|
108
|
+
expressions.push(exprEvalParser.parse(expression.expr).toJSFunction(expressionsParams.join(',')))
|
|
109
|
+
} */
|
|
110
|
+
if (expression.type === 'js-fn') {
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
112
|
+
expressions.push(/** @type {CompiledExpression} */(new Function(...expressionsParams, expression.expr)))
|
|
113
|
+
}
|
|
114
|
+
if (expression.type === 'js-eval') {
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func, @typescript-eslint/restrict-plus-operands
|
|
116
|
+
expressions.push(/** @type {CompiledExpression} */(new Function(...expressionsParams, 'return (' + expression.expr + ')')))
|
|
117
|
+
}
|
|
118
|
+
if (expression.type === 'js-tpl') {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func, @typescript-eslint/restrict-plus-operands
|
|
120
|
+
expressions.push(/** @type {CompiledExpression} */(new Function(...expressionsParams, 'return `' + expression.expr + '`')))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
options,
|
|
126
|
+
schema,
|
|
127
|
+
skeletonTree,
|
|
128
|
+
validates,
|
|
129
|
+
normalizedLayouts,
|
|
130
|
+
expressions,
|
|
131
|
+
locale: options.locale,
|
|
132
|
+
messages: options.messages,
|
|
133
|
+
// @ts-ignore
|
|
134
|
+
localizeErrors: ajvLocalize[options.locale] || ajvLocalize.en
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// import Debug from 'debug'
|
|
2
|
+
import { ok } from 'assert/strict'
|
|
3
|
+
import standaloneCode from 'ajv/dist/standalone/index.js'
|
|
4
|
+
import { parseModule, generateCode, builders } from 'magicast'
|
|
5
|
+
import { parse, print } from 'recast'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('./index.js').CompiledLayout} compiledLayout
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function serialize (compiledLayout) {
|
|
12
|
+
ok(compiledLayout.schema)
|
|
13
|
+
ok(compiledLayout.options)
|
|
14
|
+
const ajv = compiledLayout.options.ajv
|
|
15
|
+
|
|
16
|
+
/** @type {Record<string, string>} */
|
|
17
|
+
const validatesExports = {}
|
|
18
|
+
let i = 0
|
|
19
|
+
for (const pointer of Object.keys(compiledLayout.validates)) {
|
|
20
|
+
const fullPointer = ajv.opts.uriResolver.resolve(/** @type {string} */(compiledLayout.schema.$id), pointer)
|
|
21
|
+
const exportKey = `export${i++}`
|
|
22
|
+
ajv.addSchema({ $id: exportKey, $ref: fullPointer })
|
|
23
|
+
validatesExports[exportKey] = exportKey
|
|
24
|
+
}
|
|
25
|
+
let code = standaloneCode.default(ajv, validatesExports)
|
|
26
|
+
|
|
27
|
+
code = code.replace('"use strict";', '')
|
|
28
|
+
|
|
29
|
+
// some internal imports to ajv are not translated to asm, we do it here
|
|
30
|
+
// cf https://github.com/ajv-validator/ajv-formats/pull/73
|
|
31
|
+
if (code.includes('require("ajv-formats/dist/formats")')) {
|
|
32
|
+
code = 'import { fullFormats } from "ajv-formats/dist/formats.js";\n' + code
|
|
33
|
+
code = code.replace(/require\("ajv-formats\/dist\/formats"\)\.fullFormats/g, 'fullFormats')
|
|
34
|
+
}
|
|
35
|
+
if (code.includes('require("ajv/dist/runtime/ucs2length")')) {
|
|
36
|
+
code = 'import ucs2length from "ajv/dist/runtime/ucs2length.js";\n' + code
|
|
37
|
+
code = code.replace(/require\("ajv\/dist\/runtime\/ucs2length"\)/g, 'ucs2length')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// importe only the current locale from ajv-i18n
|
|
41
|
+
code = `import localizeErrors from "ajv-i18n/localize/${compiledLayout.locale}/index.js";
|
|
42
|
+
export const exportLocalizeErrors = localizeErrors;\n` + code
|
|
43
|
+
|
|
44
|
+
i = 0
|
|
45
|
+
const expressionsNodes = []
|
|
46
|
+
for (const expression of compiledLayout.expressions) {
|
|
47
|
+
const fn = parse(expression.toString()).program.body[0]
|
|
48
|
+
fn.id = `expression${i++}`
|
|
49
|
+
code += `\n${print(fn)}\n`
|
|
50
|
+
expressionsNodes.push(builders.raw(fn.id))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ast = parseModule(code)
|
|
54
|
+
ast.exports.compiledLayout = {
|
|
55
|
+
skeletonTree: compiledLayout.skeletonTree,
|
|
56
|
+
normalizedLayouts: compiledLayout.normalizedLayouts,
|
|
57
|
+
validates: {},
|
|
58
|
+
expressions: expressionsNodes,
|
|
59
|
+
locale: compiledLayout.locale,
|
|
60
|
+
messages: compiledLayout.messages,
|
|
61
|
+
localizeErrors: ast.exports.exportLocalizeErrors
|
|
62
|
+
}
|
|
63
|
+
delete ast.exports.exportLocalizeErrors
|
|
64
|
+
|
|
65
|
+
i = 0
|
|
66
|
+
for (const pointer of Object.keys(compiledLayout.validates)) {
|
|
67
|
+
const exportKey = `export${i++}`
|
|
68
|
+
ast.exports.compiledLayout.validates[pointer] = ast.exports[exportKey]
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
70
|
+
delete ast.exports[exportKey]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const generatedCode = generateCode(ast).code.replace('export const compiledLayout = {', 'const compiledLayout = {')
|
|
74
|
+
|
|
75
|
+
return generatedCode
|
|
76
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// import Debug from 'debug'
|
|
2
|
+
import { normalizeLayoutFragment, isSwitchStruct, isGetItemsExpression, isSelectLayout, isGetItemsFetch } from '@json-layout/vocabulary'
|
|
3
|
+
import { makeSkeletonTree } from './skeleton-tree.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('@json-layout/vocabulary').Expression[]} expressions
|
|
7
|
+
* @param {import('@json-layout/vocabulary').Expression} expression
|
|
8
|
+
*/
|
|
9
|
+
const pushExpression = (expressions, expression) => {
|
|
10
|
+
const index = expressions.findIndex(e => e.type === expression.type && e.expr === expression.expr)
|
|
11
|
+
if (index !== -1) {
|
|
12
|
+
expression.ref = index
|
|
13
|
+
} else {
|
|
14
|
+
expression.ref = expressions.length
|
|
15
|
+
expressions.push(expression)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {any} schema
|
|
21
|
+
* @param {import('./index.js').CompileOptions} options
|
|
22
|
+
* @param {string[]} validates
|
|
23
|
+
* @param {Record<string, import('@json-layout/vocabulary').NormalizedLayout>} normalizedLayouts
|
|
24
|
+
* @param {import('@json-layout/vocabulary').Expression[]} expressions
|
|
25
|
+
* @param {string | number} key
|
|
26
|
+
* @param {string} pointer
|
|
27
|
+
* @param {string | null} parentPointer
|
|
28
|
+
* @param {boolean} required
|
|
29
|
+
* @returns {import('./types.js').SkeletonNode}
|
|
30
|
+
*/
|
|
31
|
+
export function makeSkeletonNode (
|
|
32
|
+
schema,
|
|
33
|
+
options,
|
|
34
|
+
validates,
|
|
35
|
+
normalizedLayouts,
|
|
36
|
+
expressions,
|
|
37
|
+
key,
|
|
38
|
+
pointer,
|
|
39
|
+
parentPointer,
|
|
40
|
+
required
|
|
41
|
+
) {
|
|
42
|
+
// consolidate schema
|
|
43
|
+
if (!schema.type && schema.properties) schema.type = 'object'
|
|
44
|
+
|
|
45
|
+
// improve on ajv error messages based on ajv-errors (https://ajv.js.org/packages/ajv-errors.html)
|
|
46
|
+
schema.errorMessage = schema.errorMessage ?? {}
|
|
47
|
+
/** @type {import('@json-layout/vocabulary').NormalizedLayout} */
|
|
48
|
+
const normalizedLayout = normalizedLayouts[pointer] ?? normalizeLayoutFragment(/** @type {import('@json-layout/vocabulary').SchemaFragment} */(schema), pointer, options.markdown)
|
|
49
|
+
normalizedLayouts[pointer] = normalizedLayout
|
|
50
|
+
|
|
51
|
+
const compObjects = isSwitchStruct(normalizedLayout) ? normalizedLayout.switch : [normalizedLayout]
|
|
52
|
+
for (const compObject of compObjects) {
|
|
53
|
+
if (schema.description && !compObject.help) compObject.help = schema.description
|
|
54
|
+
if (compObject.if) pushExpression(expressions, compObject.if)
|
|
55
|
+
if (isSelectLayout(compObject) && compObject.getItems) {
|
|
56
|
+
if (isGetItemsExpression(compObject.getItems)) pushExpression(expressions, compObject.getItems)
|
|
57
|
+
if (isGetItemsFetch(compObject.getItems)) pushExpression(expressions, compObject.getItems.url)
|
|
58
|
+
if (compObject.getItems.itemTitle) pushExpression(expressions, compObject.getItems.itemTitle)
|
|
59
|
+
if (compObject.getItems.itemKey) pushExpression(expressions, compObject.getItems.itemKey)
|
|
60
|
+
if (compObject.getItems.itemValue) pushExpression(expressions, compObject.getItems.itemValue)
|
|
61
|
+
if (compObject.getItems.itemsResults) pushExpression(expressions, compObject.getItems.itemsResults)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let defaultData
|
|
66
|
+
if (schema.const) defaultData = schema.const
|
|
67
|
+
else if (schema.default) defaultData = schema.default
|
|
68
|
+
if (required) {
|
|
69
|
+
if (schema.type === 'object') defaultData = {} // TODO: this is only true if property is required ?
|
|
70
|
+
if (schema.type === 'array') defaultData = []
|
|
71
|
+
if (schema.type === 'string' && !schema.format) defaultData = ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @type {import('./types.js').SkeletonNode} */
|
|
75
|
+
const node = { key: key ?? '', pointer, parentPointer, defaultData }
|
|
76
|
+
if (schema.const) node.const = schema.const
|
|
77
|
+
if (schema.type === 'object') {
|
|
78
|
+
if (schema.properties) {
|
|
79
|
+
node.children = node.children ?? []
|
|
80
|
+
for (const propertyKey of Object.keys(schema.properties)) {
|
|
81
|
+
node.children.push(makeSkeletonNode(
|
|
82
|
+
schema.properties[propertyKey],
|
|
83
|
+
options,
|
|
84
|
+
validates,
|
|
85
|
+
normalizedLayouts,
|
|
86
|
+
expressions,
|
|
87
|
+
propertyKey,
|
|
88
|
+
`${pointer}/properties/${propertyKey}`,
|
|
89
|
+
pointer,
|
|
90
|
+
schema.required?.includes(propertyKey)
|
|
91
|
+
))
|
|
92
|
+
if (schema?.required?.includes(propertyKey)) {
|
|
93
|
+
schema.errorMessage.required = schema.errorMessage.required ?? {}
|
|
94
|
+
schema.errorMessage.required[propertyKey] = options.messages.errorRequired
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (schema.allOf) {
|
|
99
|
+
node.children = node.children ?? []
|
|
100
|
+
for (let i = 0; i < schema.allOf.length; i++) {
|
|
101
|
+
node.children.push(makeSkeletonNode(
|
|
102
|
+
schema.allOf[i],
|
|
103
|
+
options,
|
|
104
|
+
validates,
|
|
105
|
+
normalizedLayouts,
|
|
106
|
+
expressions,
|
|
107
|
+
`$allOf-${i}`,
|
|
108
|
+
`${pointer}/allOf/${i}`,
|
|
109
|
+
pointer,
|
|
110
|
+
false
|
|
111
|
+
))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (schema.oneOf) {
|
|
115
|
+
const oneOfPointer = `${pointer}/oneOf`
|
|
116
|
+
normalizedLayouts[oneOfPointer] = normalizedLayouts[oneOfPointer] ?? normalizeLayoutFragment(schema, oneOfPointer, options.markdown, 'oneOf')
|
|
117
|
+
/** @type {import('./types.js').SkeletonTree[]} */
|
|
118
|
+
const childrenTrees = []
|
|
119
|
+
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
120
|
+
if (!schema.oneOf[i].type) schema.oneOf[i].type = schema.type
|
|
121
|
+
const title = schema.oneOf[i].title ?? `option ${i}`
|
|
122
|
+
delete schema.oneOf[i].title
|
|
123
|
+
childrenTrees.push(makeSkeletonTree(schema.oneOf[i], options, validates, normalizedLayouts, expressions, `${oneOfPointer}/${i}`, title))
|
|
124
|
+
}
|
|
125
|
+
node.children = node.children ?? []
|
|
126
|
+
node.children.push({ key: '$oneOf', pointer: `${pointer}/oneOf`, parentPointer: pointer, childrenTrees })
|
|
127
|
+
|
|
128
|
+
schema.errorMessage.oneOf = options.messages.errorOneOf
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (schema.type === 'array' && schema.items) {
|
|
133
|
+
if (Array.isArray(schema.items)) {
|
|
134
|
+
node.children = schema.items.map((/** @type {any} */ itemSchema, /** @type {number} */ i) => {
|
|
135
|
+
return makeSkeletonNode(
|
|
136
|
+
itemSchema,
|
|
137
|
+
options,
|
|
138
|
+
validates,
|
|
139
|
+
normalizedLayouts,
|
|
140
|
+
expressions,
|
|
141
|
+
i,
|
|
142
|
+
`${pointer}/items/${i}`,
|
|
143
|
+
pointer,
|
|
144
|
+
true
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
} else {
|
|
148
|
+
node.childrenTrees = [
|
|
149
|
+
makeSkeletonTree(
|
|
150
|
+
schema.items,
|
|
151
|
+
options,
|
|
152
|
+
validates,
|
|
153
|
+
normalizedLayouts,
|
|
154
|
+
expressions,
|
|
155
|
+
`${pointer}/items`,
|
|
156
|
+
schema.items.title
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return node
|
|
162
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// import Debug from 'debug'
|
|
2
|
+
import { makeSkeletonNode } from './skeleton-node.js'
|
|
3
|
+
|
|
4
|
+
// const debug = Debug('json-layout:compile-raw')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {any} schema
|
|
8
|
+
* @param {import('./index.js').CompileOptions} options
|
|
9
|
+
* @param {string[]} validates
|
|
10
|
+
* @param {Record<string, import('@json-layout/vocabulary').NormalizedLayout>} normalizedLayouts
|
|
11
|
+
* @param {import('@json-layout/vocabulary').Expression[]} expressions
|
|
12
|
+
* @param {string} pointer
|
|
13
|
+
* @param {string} title
|
|
14
|
+
* @returns {import('./types.js').SkeletonTree}
|
|
15
|
+
*/
|
|
16
|
+
export function makeSkeletonTree (
|
|
17
|
+
schema,
|
|
18
|
+
options,
|
|
19
|
+
validates,
|
|
20
|
+
normalizedLayouts,
|
|
21
|
+
expressions,
|
|
22
|
+
pointer,
|
|
23
|
+
title
|
|
24
|
+
) {
|
|
25
|
+
const root = makeSkeletonNode(schema, options, validates, normalizedLayouts, expressions, '', pointer, null, true)
|
|
26
|
+
validates.push(pointer)
|
|
27
|
+
return { title, root }
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type ajvModule from 'ajv'
|
|
2
|
+
import type MarkdownIt from 'markdown-it'
|
|
3
|
+
import { type NormalizedLayout, type StateNodeOptions } from '@json-layout/vocabulary'
|
|
4
|
+
import { type ValidateFunction, type SchemaObject } from 'ajv'
|
|
5
|
+
import { type Display } from '../state/utils/display.js'
|
|
6
|
+
import { type LocaleMessages } from '../i18n/types.js'
|
|
7
|
+
|
|
8
|
+
export type CompiledExpression = (data: any, options: StateNodeOptions, display: Display) => any
|
|
9
|
+
|
|
10
|
+
export interface CompileOptions {
|
|
11
|
+
ajv: ajvModule.default
|
|
12
|
+
code: boolean
|
|
13
|
+
markdown: (text: string) => string
|
|
14
|
+
markdownIt?: MarkdownIt.Options
|
|
15
|
+
locale: string
|
|
16
|
+
messages: LocaleMessages
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PartialCompileOptions = Partial<Omit<CompileOptions, 'messages'>> & { messages?: Partial<LocaleMessages> }
|
|
20
|
+
|
|
21
|
+
export interface CompiledLayout {
|
|
22
|
+
options?: CompileOptions
|
|
23
|
+
schema?: SchemaObject
|
|
24
|
+
skeletonTree: SkeletonTree
|
|
25
|
+
validates: Record<string, ValidateFunction>
|
|
26
|
+
normalizedLayouts: Record<string, NormalizedLayout>
|
|
27
|
+
expressions: CompiledExpression[]
|
|
28
|
+
locale: string
|
|
29
|
+
messages: LocaleMessages
|
|
30
|
+
localizeErrors: (errors: ajvModule.ErrorObject[]) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// a tree is a root node and a validation function
|
|
34
|
+
// it will be used to instantiate a StateLayoutTree with 1 validation context
|
|
35
|
+
export interface SkeletonTree {
|
|
36
|
+
title: string
|
|
37
|
+
root: SkeletonNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// a skeleton node is a light recursive structure
|
|
41
|
+
// at runtime each one will be instantiated as a StateNode with a value and an associated component instance
|
|
42
|
+
export interface SkeletonNode {
|
|
43
|
+
key: string | number
|
|
44
|
+
pointer: string
|
|
45
|
+
parentPointer: string | null
|
|
46
|
+
defaultData?: unknown
|
|
47
|
+
const?: unknown
|
|
48
|
+
children?: SkeletonNode[] // optional children in the case of arrays and object nodes
|
|
49
|
+
childrenTrees?: SkeletonTree[] // other trees that can be instantiated with separate validation (for example in the case of new array items of oneOfs, etc)
|
|
50
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import ajvModule from 'ajv'
|
|
2
|
+
|
|
3
|
+
const Ajv = ajvModule.default
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {Record<string, import('ajv').SchemaObject>} schemas
|
|
7
|
+
* @param {string} ref
|
|
8
|
+
* @param {ajvModule.default} ajv
|
|
9
|
+
* @returns {[any, string]}
|
|
10
|
+
*/
|
|
11
|
+
const getJSONRef = (schemas, ref, ajv) => {
|
|
12
|
+
const [schemaId, pointer] = ref.split('#')
|
|
13
|
+
schemas[schemaId] = schemas[schemaId] ?? (ajv.getSchema(schemaId)?.schema)
|
|
14
|
+
if (!schemas[schemaId]) throw new Error(`reference not found ${schemaId}`)
|
|
15
|
+
const pointerParts = pointer.split('/').filter(p => !!p)
|
|
16
|
+
const { value: fragment } = pointerParts.reduce((a, pointerPart) => {
|
|
17
|
+
a.path.push(pointerPart)
|
|
18
|
+
if (!(pointerPart in a.value)) throw new Error(`reference not found ${schemaId}#${a.path.join('/')}`)
|
|
19
|
+
a.value = a.value[pointerPart]
|
|
20
|
+
return a
|
|
21
|
+
}, { path: ['/'], value: schemas[schemaId] })
|
|
22
|
+
return [fragment, schemaId]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Record<string, import('ajv').SchemaObject>} schemas
|
|
27
|
+
* @param {import('ajv').SchemaObject} schemaFragment
|
|
28
|
+
* @param {string} schemaId
|
|
29
|
+
* @param {ajvModule.default} ajv
|
|
30
|
+
* @param {string} locale
|
|
31
|
+
* @returns {import('ajv').SchemaObject}
|
|
32
|
+
*/
|
|
33
|
+
const recurse = (schemas, schemaFragment, schemaId, ajv, locale = 'en') => {
|
|
34
|
+
for (const key of Object.keys(schemaFragment)) {
|
|
35
|
+
if (schemaFragment[key] && typeof schemaFragment[key] === 'object') {
|
|
36
|
+
if ('$ref' in schemaFragment[key]) {
|
|
37
|
+
const fullRef = ajv.opts.uriResolver.resolve(schemaId, schemaFragment[key].$ref).replace('~$locale~', locale)
|
|
38
|
+
const fullRefDefaultLocale = ajv.opts.uriResolver.resolve(schemaId, schemaFragment[key].$ref).replace('~$locale~', 'en')
|
|
39
|
+
let refFragment, refSchemaId
|
|
40
|
+
try {
|
|
41
|
+
[refFragment, refSchemaId] = getJSONRef(schemas, fullRef, ajv)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
[refFragment, refSchemaId] = getJSONRef(schemas, fullRefDefaultLocale, ajv)
|
|
44
|
+
}
|
|
45
|
+
if (typeof refFragment === 'object' && !Array.isArray(refFragment)) {
|
|
46
|
+
schemaFragment[key] = { ...refFragment, ...schemaFragment[key] }
|
|
47
|
+
delete schemaFragment[key].$ref
|
|
48
|
+
} else {
|
|
49
|
+
schemaFragment[key] = refFragment
|
|
50
|
+
}
|
|
51
|
+
recurse(schemas, schemaFragment[key], refSchemaId, ajv, locale)
|
|
52
|
+
} else {
|
|
53
|
+
recurse(schemas, schemaFragment[key], schemaId, ajv, locale)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return schemaFragment
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {import('ajv').SchemaObject} schema
|
|
62
|
+
* @param {ajvModule.default} ajv
|
|
63
|
+
* @param {string} locale
|
|
64
|
+
* @returns {import('ajv').SchemaObject}
|
|
65
|
+
*/
|
|
66
|
+
export function resolveRefs (schema, ajv, locale = 'en') {
|
|
67
|
+
if (!schema.$id) throw new Error('missing schema id')
|
|
68
|
+
return recurse({ [schema.$id]: schema }, schema, schema.$id, ajv ?? new Ajv(), locale)
|
|
69
|
+
}
|
package/src/i18n/en.js
ADDED
package/src/i18n/fr.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @type {import('./types.js').LocaleMessages} */
|
|
2
|
+
export default {
|
|
3
|
+
errorOneOf: 'choisissez une valeur',
|
|
4
|
+
errorRequired: 'information obligatoire',
|
|
5
|
+
addItem: 'Ajouter un élément',
|
|
6
|
+
delete: 'Supprimer',
|
|
7
|
+
edit: 'Éditer',
|
|
8
|
+
duplicate: 'Dupliquer',
|
|
9
|
+
sort: 'Trier'
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CompileOptionsMessages {
|
|
2
|
+
errorOneOf: string
|
|
3
|
+
errorRequired: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface StateOptionsMessages {
|
|
7
|
+
addItem: string
|
|
8
|
+
delete: string
|
|
9
|
+
edit: string
|
|
10
|
+
duplicate: string
|
|
11
|
+
sort: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LocaleMessages = CompileOptionsMessages & StateOptionsMessages
|
package/src/index.js
ADDED