@dou-ui/form-builder 0.0.0-stage → 0.1.0-alpha.1

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/builder/canvas.js +695 -0
  4. package/dist/builder/canvas.js.map +1 -0
  5. package/dist/builder/completion.js +49 -0
  6. package/dist/builder/completion.js.map +1 -0
  7. package/dist/builder/condition-editor.js +6 -0
  8. package/dist/builder/condition-editor.js.map +1 -0
  9. package/dist/builder/condition-editor.vue_vue_type_script_setup_true_lang.js +410 -0
  10. package/dist/builder/condition-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
  11. package/dist/builder/condition.js +159 -0
  12. package/dist/builder/condition.js.map +1 -0
  13. package/dist/builder/context.js +477 -0
  14. package/dist/builder/context.js.map +1 -0
  15. package/dist/builder/core/commands.js +373 -0
  16. package/dist/builder/core/commands.js.map +1 -0
  17. package/dist/builder/core/history.js +76 -0
  18. package/dist/builder/core/history.js.map +1 -0
  19. package/dist/builder/core/transaction.js +298 -0
  20. package/dist/builder/core/transaction.js.map +1 -0
  21. package/dist/builder/drag.js +466 -0
  22. package/dist/builder/drag.js.map +1 -0
  23. package/dist/builder/expression-box.js +19 -0
  24. package/dist/builder/expression-box.js.map +1 -0
  25. package/dist/builder/expression-input.js +6 -0
  26. package/dist/builder/expression-input.js.map +1 -0
  27. package/dist/builder/expression-input.vue_vue_type_script_setup_true_lang.js +117 -0
  28. package/dist/builder/expression-input.vue_vue_type_script_setup_true_lang.js.map +1 -0
  29. package/dist/builder/extension.js +47 -0
  30. package/dist/builder/extension.js.map +1 -0
  31. package/dist/builder/form-builder.js +6 -0
  32. package/dist/builder/form-builder.js.map +1 -0
  33. package/dist/builder/form-builder.vue_vue_type_script_setup_true_lang.js +450 -0
  34. package/dist/builder/form-builder.vue_vue_type_script_setup_true_lang.js.map +1 -0
  35. package/dist/builder/index.js +22 -0
  36. package/dist/builder/index.js.map +1 -0
  37. package/dist/builder/inspector.js +6 -0
  38. package/dist/builder/inspector.js.map +1 -0
  39. package/dist/builder/inspector.vue_vue_type_script_setup_true_lang.js +442 -0
  40. package/dist/builder/inspector.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/builder/json-dialog.js +6 -0
  42. package/dist/builder/json-dialog.js.map +1 -0
  43. package/dist/builder/json-dialog.vue_vue_type_script_setup_true_lang.js +281 -0
  44. package/dist/builder/json-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  45. package/dist/builder/locale.js +1271 -0
  46. package/dist/builder/locale.js.map +1 -0
  47. package/dist/builder/logic-fields.js +18 -0
  48. package/dist/builder/logic-fields.js.map +1 -0
  49. package/dist/builder/logic.js +332 -0
  50. package/dist/builder/logic.js.map +1 -0
  51. package/dist/builder/menu.js +92 -0
  52. package/dist/builder/menu.js.map +1 -0
  53. package/dist/builder/outline-row.js +6 -0
  54. package/dist/builder/outline-row.js.map +1 -0
  55. package/dist/builder/outline-row.vue_vue_type_script_setup_true_lang.js +88 -0
  56. package/dist/builder/outline-row.vue_vue_type_script_setup_true_lang.js.map +1 -0
  57. package/dist/builder/outline.js +6 -0
  58. package/dist/builder/outline.js.map +1 -0
  59. package/dist/builder/outline.vue_vue_type_script_setup_true_lang.js +111 -0
  60. package/dist/builder/outline.vue_vue_type_script_setup_true_lang.js.map +1 -0
  61. package/dist/builder/palette.js +6 -0
  62. package/dist/builder/palette.js.map +1 -0
  63. package/dist/builder/palette.vue_vue_type_script_setup_true_lang.js +96 -0
  64. package/dist/builder/palette.vue_vue_type_script_setup_true_lang.js.map +1 -0
  65. package/dist/builder/preview.js +6 -0
  66. package/dist/builder/preview.js.map +1 -0
  67. package/dist/builder/preview.vue_vue_type_script_setup_true_lang.js +111 -0
  68. package/dist/builder/preview.vue_vue_type_script_setup_true_lang.js.map +1 -0
  69. package/dist/builder/settings-binding.js +57 -0
  70. package/dist/builder/settings-binding.js.map +1 -0
  71. package/dist/builder/settings.js +487 -0
  72. package/dist/builder/settings.js.map +1 -0
  73. package/dist/builder/span-field.js +104 -0
  74. package/dist/builder/span-field.js.map +1 -0
  75. package/dist/code/expression-input.js +6 -0
  76. package/dist/code/expression-input.js.map +1 -0
  77. package/dist/code/expression-input.vue_vue_type_script_setup_true_lang.js +88 -0
  78. package/dist/code/expression-input.vue_vue_type_script_setup_true_lang.js.map +1 -0
  79. package/dist/code/extension.js +202 -0
  80. package/dist/code/extension.js.map +1 -0
  81. package/dist/code/index.js +19 -0
  82. package/dist/code/index.js.map +1 -0
  83. package/dist/code/language.js +43 -0
  84. package/dist/code/language.js.map +1 -0
  85. package/dist/collaboration/index.js +382 -0
  86. package/dist/collaboration/index.js.map +1 -0
  87. package/dist/collaboration/order.js +88 -0
  88. package/dist/collaboration/order.js.map +1 -0
  89. package/dist/core/check.js +314 -0
  90. package/dist/core/check.js.map +1 -0
  91. package/dist/core/document.js +112 -0
  92. package/dist/core/document.js.map +1 -0
  93. package/dist/core/expression.js +854 -0
  94. package/dist/core/expression.js.map +1 -0
  95. package/dist/core/formulas.js +242 -0
  96. package/dist/core/formulas.js.map +1 -0
  97. package/dist/core/rules.js +83 -0
  98. package/dist/core/rules.js.map +1 -0
  99. package/dist/core/values.js +80 -0
  100. package/dist/core/values.js.map +1 -0
  101. package/dist/fields/builtin.js +167 -0
  102. package/dist/fields/builtin.js.map +1 -0
  103. package/dist/fields/components.js +155 -0
  104. package/dist/fields/components.js.map +1 -0
  105. package/dist/fields/define.js +44 -0
  106. package/dist/fields/define.js.map +1 -0
  107. package/dist/fields/format.js +125 -0
  108. package/dist/fields/format.js.map +1 -0
  109. package/dist/fields/options.js +54 -0
  110. package/dist/fields/options.js.map +1 -0
  111. package/dist/index.js +20 -0
  112. package/dist/locale.js +161 -0
  113. package/dist/locale.js.map +1 -0
  114. package/dist/renderer/containers.js +430 -0
  115. package/dist/renderer/containers.js.map +1 -0
  116. package/dist/renderer/context.js +21 -0
  117. package/dist/renderer/context.js.map +1 -0
  118. package/dist/renderer/form-renderer.js +6 -0
  119. package/dist/renderer/form-renderer.js.map +1 -0
  120. package/dist/renderer/form-renderer.vue_vue_type_script_setup_true_lang.js +272 -0
  121. package/dist/renderer/form-renderer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  122. package/dist/renderer/index.js +19 -0
  123. package/dist/renderer/index.js.map +1 -0
  124. package/dist/renderer/node-view.js +385 -0
  125. package/dist/renderer/node-view.js.map +1 -0
  126. package/dist/style.css +1 -0
  127. package/dist/style.d.ts +1 -0
  128. package/dist/style.js +27 -0
  129. package/dist/types/builder/canvas.d.ts +5 -0
  130. package/dist/types/builder/completion.d.ts +35 -0
  131. package/dist/types/builder/condition-editor.vue.d.ts +17 -0
  132. package/dist/types/builder/condition.d.ts +37 -0
  133. package/dist/types/builder/context.d.ts +149 -0
  134. package/dist/types/builder/core/commands.d.ts +74 -0
  135. package/dist/types/builder/core/history.d.ts +23 -0
  136. package/dist/types/builder/core/transaction.d.ts +108 -0
  137. package/dist/types/builder/drag.d.ts +52 -0
  138. package/dist/types/builder/expression-box.d.ts +8 -0
  139. package/dist/types/builder/expression-input.vue.d.ts +29 -0
  140. package/dist/types/builder/extension.d.ts +149 -0
  141. package/dist/types/builder/form-builder.vue.d.ts +24 -0
  142. package/dist/types/builder/index.d.ts +76 -0
  143. package/dist/types/builder/inspector.vue.d.ts +3 -0
  144. package/dist/types/builder/json-dialog.vue.d.ts +10 -0
  145. package/dist/types/builder/locale.d.ts +195 -0
  146. package/dist/types/builder/logic-fields.d.ts +2 -0
  147. package/dist/types/builder/logic.d.ts +57 -0
  148. package/dist/types/builder/menu.d.ts +23 -0
  149. package/dist/types/builder/outline-row.vue.d.ts +19 -0
  150. package/dist/types/builder/outline.vue.d.ts +3 -0
  151. package/dist/types/builder/palette.vue.d.ts +3 -0
  152. package/dist/types/builder/preview.vue.d.ts +3 -0
  153. package/dist/types/builder/settings-binding.d.ts +23 -0
  154. package/dist/types/builder/settings.d.ts +49 -0
  155. package/dist/types/builder/span-field.d.ts +4 -0
  156. package/dist/types/builder/types.d.ts +59 -0
  157. package/dist/types/code/expression-input.vue.d.ts +23 -0
  158. package/dist/types/code/extension.d.ts +45 -0
  159. package/dist/types/code/index.d.ts +12 -0
  160. package/dist/types/code/language.d.ts +6 -0
  161. package/dist/types/collaboration/index.d.ts +64 -0
  162. package/dist/types/collaboration/order.d.ts +20 -0
  163. package/dist/types/core/check.d.ts +68 -0
  164. package/dist/types/core/document.d.ts +36 -0
  165. package/dist/types/core/expression.d.ts +175 -0
  166. package/dist/types/core/formulas.d.ts +44 -0
  167. package/dist/types/core/rules.d.ts +23 -0
  168. package/dist/types/core/types.d.ts +211 -0
  169. package/dist/types/core/values.d.ts +22 -0
  170. package/dist/types/fields/builtin.d.ts +9 -0
  171. package/dist/types/fields/components.d.ts +177 -0
  172. package/dist/types/fields/define.d.ts +93 -0
  173. package/dist/types/fields/format.d.ts +76 -0
  174. package/dist/types/fields/options.d.ts +11 -0
  175. package/dist/types/index.d.ts +3 -0
  176. package/dist/types/locale.d.ts +45 -0
  177. package/dist/types/renderer/containers.d.ts +20 -0
  178. package/dist/types/renderer/context.d.ts +186 -0
  179. package/dist/types/renderer/form-renderer.vue.d.ts +25 -0
  180. package/dist/types/renderer/index.d.ts +81 -0
  181. package/dist/types/renderer/node-view.d.ts +56 -0
  182. package/package.json +88 -4
@@ -0,0 +1,314 @@
1
+ import { isContainer, isField, isList, isPage, isSteps, isTabs } from "./document.js";
2
+ import { checkExpression } from "./expression.js";
3
+ import { resolveFields } from "../fields/builtin.js";
4
+ import { analyseFormulas } from "./formulas.js";
5
+ // src/core/check.ts
6
+ var EXPRESSIONS = [
7
+ "visibleWhen",
8
+ "disabledWhen",
9
+ "requiredWhen",
10
+ "formula"
11
+ ];
12
+ /**
13
+ * The problems of a document, for an editor to point at, before it is
14
+ * saved or rendered: ids used twice, fields without a name or sharing one,
15
+ * types nobody registered, pages outside tabs and steps, expressions that
16
+ * do not parse or read fields that are not there, formulas that read
17
+ * each other, and rules naming a validator nobody gave.
18
+ */
19
+ function checkDocument(document, options = {}) {
20
+ const problems = [];
21
+ if (document.version !== 1) problems.push({
22
+ code: "version",
23
+ path: "version",
24
+ text: String(document.version),
25
+ message: `Version ${String(document.version)} is not known`
26
+ });
27
+ const types = resolveFields(options.fields);
28
+ const ids = /* @__PURE__ */ new Set();
29
+ const known = scopeNames(document.nodes);
30
+ /** Where each node is, by the node itself, so nodes that share an id each have their own. */
31
+ const paths = /* @__PURE__ */ new Map();
32
+ /** `itemFields`: the names an item of the list around the nodes has; none outside lists. */
33
+ function visit(nodes, parent, itemFields, at) {
34
+ for (const [index, node] of nodes.entries()) {
35
+ const path = `${at}[${index}]`;
36
+ paths.set(node, path);
37
+ if (ids.has(node.id)) problems.push({
38
+ code: "duplicate-id",
39
+ nodeId: node.id,
40
+ path,
41
+ text: node.id,
42
+ message: `The id "${node.id}" is used twice`
43
+ });
44
+ ids.add(node.id);
45
+ const pageParent = !!parent && (isTabs(parent) || isSteps(parent));
46
+ if (isPage(node) && !pageParent) problems.push({
47
+ code: "misplaced-page",
48
+ nodeId: node.id,
49
+ path,
50
+ text: node.id,
51
+ message: "A page belongs in tabs or steps"
52
+ });
53
+ if (pageParent && !isPage(node)) problems.push({
54
+ code: "not-a-page",
55
+ nodeId: node.id,
56
+ path,
57
+ text: node.id,
58
+ message: "Tabs and steps hold pages only"
59
+ });
60
+ if ((isTabs(node) || isSteps(node) || isList(node)) && !node.children?.length) problems.push({
61
+ code: "empty",
62
+ nodeId: node.id,
63
+ path,
64
+ text: node.id,
65
+ message: isList(node) ? "The list has no fields" : `The ${node.type} have no pages`
66
+ });
67
+ if (isList(node) && !node.name) problems.push({
68
+ code: "missing-name",
69
+ nodeId: node.id,
70
+ path,
71
+ text: node.id,
72
+ message: "The list has no name"
73
+ });
74
+ if (isField(node)) {
75
+ const type = types.get(node.type);
76
+ if (!type) problems.push({
77
+ code: "unknown-type",
78
+ nodeId: node.id,
79
+ path,
80
+ text: node.type,
81
+ message: `There is no field type "${node.type}"`
82
+ });
83
+ else if (type.kind !== "display" && !node.name) problems.push({
84
+ code: "missing-name",
85
+ nodeId: node.id,
86
+ path,
87
+ text: node.id,
88
+ message: "The field has no name"
89
+ });
90
+ }
91
+ const expressions = EXPRESSIONS.map((key) => [key, node[key]]);
92
+ if (isField(node) || isList(node)) (node.rules ?? []).forEach((rule, order) => {
93
+ expressions.push([`rules.${order}.check`, rule.check]);
94
+ if (rule.validator === void 0) return;
95
+ const rulePath = `${path}.rules[${order}]`;
96
+ if (rule.check) problems.push({
97
+ code: "check-and-validator",
98
+ nodeId: node.id,
99
+ path: rulePath,
100
+ text: rule.validator,
101
+ message: "A rule has both check and validator; only check runs"
102
+ });
103
+ else if (options.validators && !options.validators.includes(rule.validator)) problems.push({
104
+ code: "unknown-validator",
105
+ nodeId: node.id,
106
+ path: `${rulePath}.validator`,
107
+ text: rule.validator,
108
+ message: `There is no validator "${rule.validator}"`
109
+ });
110
+ });
111
+ if (isField(node) && node.source?.arg) expressions.push(["source.arg", node.source.arg]);
112
+ for (const [property, source] of expressions) {
113
+ if (!source) continue;
114
+ const found = checkExpression(source, {
115
+ fields: known,
116
+ itemFields,
117
+ functions: options.functions
118
+ });
119
+ for (const problem of found) problems.push({
120
+ code: "expression",
121
+ nodeId: node.id,
122
+ path: `${path}.${property.replace(/\.(\d+)/g, "[$1]")}`,
123
+ property,
124
+ text: source,
125
+ expression: problem,
126
+ message: `${property}: ${problem.message}`
127
+ });
128
+ }
129
+ if (isList(node)) visit(node.children ?? [], node, scopeNames(node.children ?? []), `${path}.children`);
130
+ else if (isContainer(node)) visit(node.children ?? [], node, itemFields, `${path}.children`);
131
+ }
132
+ }
133
+ visit(document.nodes, void 0, [], "nodes");
134
+ duplicateNames(document.nodes, problems, paths);
135
+ for (const cycle of analyseFormulas(document, options.functions).cycles) {
136
+ const text = cycle.map(({ label }) => label);
137
+ const node = cycle[0].node;
138
+ problems.push({
139
+ code: "formula-cycle",
140
+ nodeId: node.id,
141
+ path: paths.get(node) ?? "",
142
+ text: text.join(" → "),
143
+ message: `The formulas read each other: ${text.join(" -> ")}`
144
+ });
145
+ }
146
+ return problems;
147
+ }
148
+ var nameOf = (node) => isList(node) || isField(node) ? node.name : void 0;
149
+ /** The names of one scope: the document's, or a list item's. Groups and pages share their parent's. */
150
+ function scopeNames(nodes) {
151
+ const result = [];
152
+ for (const node of nodes) {
153
+ const name = nameOf(node);
154
+ if (name) result.push(name);
155
+ if (isContainer(node) && !isList(node)) result.push(...scopeNames(node.children ?? []));
156
+ }
157
+ return result;
158
+ }
159
+ /** Names used twice within one scope. */
160
+ function duplicateNames(nodes, problems, paths) {
161
+ const scan = (list, seen) => {
162
+ for (const node of list) {
163
+ const name = nameOf(node);
164
+ if (name) {
165
+ if (seen.has(name)) problems.push({
166
+ code: "duplicate-name",
167
+ nodeId: node.id,
168
+ path: paths.get(node) ?? "",
169
+ text: name,
170
+ message: `The name "${name}" is used twice`
171
+ });
172
+ seen.add(name);
173
+ }
174
+ if (isList(node)) scan(node.children ?? [], /* @__PURE__ */ new Set());
175
+ else if (isContainer(node)) scan(node.children ?? [], seen);
176
+ }
177
+ };
178
+ scan(nodes, /* @__PURE__ */ new Set());
179
+ }
180
+ /** Upgrades from an older version of the document, by the version they start from. */
181
+ var migrations = {};
182
+ /**
183
+ * The document in the current version (`1`). Documents of an older
184
+ * version are upgraded step by step; there are none yet, so for now this
185
+ * only checks the version. A document without a version is read as
186
+ * version 1, as the renderer does. Throws for anything that is not a
187
+ * document and for a version it does not know.
188
+ */
189
+ function migrateDocument(document) {
190
+ if (document === null || typeof document !== "object" || Array.isArray(document)) throw new Error("[@dou-ui/form-builder] a form document must be an object");
191
+ let current = document;
192
+ if (current.version === void 0) current = {
193
+ ...current,
194
+ version: 1
195
+ };
196
+ while (current.version !== 1) {
197
+ const step = typeof current.version === "number" ? migrations[current.version] : void 0;
198
+ if (!step) throw new Error(`[@dou-ui/form-builder] form document version ${String(current.version)} is not known`);
199
+ current = step(current);
200
+ }
201
+ return current;
202
+ }
203
+ var NODE_KEYS = {
204
+ id: "string",
205
+ type: "string",
206
+ name: "string",
207
+ label: "string",
208
+ help: "string",
209
+ title: "string",
210
+ description: "string",
211
+ addText: "string",
212
+ visibleWhen: "string",
213
+ disabledWhen: "string",
214
+ requiredWhen: "string",
215
+ formula: "string",
216
+ required: "boolean",
217
+ bordered: "boolean",
218
+ sortable: "boolean",
219
+ min: "number",
220
+ max: "number",
221
+ columns: "number",
222
+ span: "span",
223
+ props: "object",
224
+ source: "object",
225
+ rules: "array",
226
+ children: "array"
227
+ };
228
+ var RULE_KEYS = {
229
+ required: "boolean",
230
+ type: "string",
231
+ min: "number",
232
+ max: "number",
233
+ pattern: "string",
234
+ flags: "string",
235
+ check: "string",
236
+ validator: "string",
237
+ message: "string",
238
+ trigger: "string"
239
+ };
240
+ var DOCUMENT_KEYS = {
241
+ version: "number",
242
+ layout: "string",
243
+ labelAlign: "string",
244
+ labelWidth: "number",
245
+ size: "string",
246
+ requiredMark: "mark",
247
+ columns: "number",
248
+ nodes: "array"
249
+ };
250
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set([
251
+ "section",
252
+ "tabs",
253
+ "steps",
254
+ "page",
255
+ "list"
256
+ ]);
257
+ var SHAPE_LIMIT = 50;
258
+ var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
259
+ function fits(value, expected) {
260
+ switch (expected) {
261
+ case "object": return isObject(value);
262
+ case "array": return Array.isArray(value);
263
+ case "span": return typeof value === "number" && Number.isFinite(value) || isObject(value) && Object.values(value).every((each) => typeof each === "number");
264
+ case "mark": return typeof value === "boolean" || value === "optional";
265
+ case "number": return typeof value === "number" && Number.isFinite(value);
266
+ default: return typeof value === expected;
267
+ }
268
+ }
269
+ /**
270
+ * Where some data, e.g. JSON pasted into an editor, is not shaped like a
271
+ * form document: a key holding the wrong kind of value, or a node without
272
+ * its `id`, `type` or `children`. Problems name the path to the value, so
273
+ * they can be found in the text. Run `checkDocument` on the result for
274
+ * the problems of a document that is shaped right.
275
+ */
276
+ function checkDocumentShape(data) {
277
+ const problems = [];
278
+ const add = (path, code, expected) => {
279
+ if (problems.length >= SHAPE_LIMIT) return;
280
+ problems.push({
281
+ path,
282
+ code,
283
+ expected,
284
+ message: code === "required" ? `${path} is missing` : `${path || "The document"} should be ${expected === "span" ? "a number or an object of numbers" : expected === "mark" ? "a boolean or \"optional\"" : `a ${expected}`}`
285
+ });
286
+ };
287
+ const keys = (value, spec, path) => {
288
+ for (const [key, expected] of Object.entries(spec)) if (value[key] !== void 0 && !fits(value[key], expected)) add(`${path}.${key}`, "type", expected);
289
+ };
290
+ const node = (value, path) => {
291
+ if (!isObject(value)) return add(path, "type", "object");
292
+ keys(value, NODE_KEYS, path);
293
+ for (const key of ["id", "type"]) if (value[key] === void 0) add(`${path}.${key}`, "required", "string");
294
+ if (Array.isArray(value.rules)) value.rules.forEach((rule, index) => {
295
+ const at = `${path}.rules[${index}]`;
296
+ if (isObject(rule)) keys(rule, RULE_KEYS, at);
297
+ else add(at, "type", "object");
298
+ });
299
+ if (isObject(value.source) && typeof value.source.name !== "string") add(`${path}.source.name`, value.source.name === void 0 ? "required" : "type", "string");
300
+ if (typeof value.type === "string" && CONTAINER_TYPES.has(value.type) && value.children === void 0) add(`${path}.children`, "required", "array");
301
+ if (Array.isArray(value.children)) value.children.forEach((child, index) => node(child, `${path}.children[${index}]`));
302
+ };
303
+ if (!isObject(data)) {
304
+ add("", "type", "object");
305
+ return problems;
306
+ }
307
+ for (const [key, expected] of Object.entries(DOCUMENT_KEYS)) if (data[key] !== void 0 && !fits(data[key], expected)) add(key, "type", expected);
308
+ if (data.nodes === void 0) add("nodes", "required", "array");
309
+ if (Array.isArray(data.nodes)) data.nodes.forEach((each, index) => node(each, `nodes[${index}]`));
310
+ return problems;
311
+ }
312
+ export { checkDocument, checkDocumentShape, migrateDocument };
313
+
314
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","names":[],"sources":["../../src/core/check.ts"],"sourcesContent":["import { isContainer, isField, isList, isPage, isSteps, isTabs } from './document'\nimport { checkExpression } from './expression'\nimport type { ExpressionProblem } from './expression'\nimport { analyseFormulas } from './formulas'\nimport type { FormDocument, FormNode } from './types'\nimport { resolveFields } from '../fields/builtin'\nimport type { FormFieldType } from '../fields/define'\n\nexport interface DocumentProblem {\n code:\n | 'version'\n | 'duplicate-id'\n | 'missing-name'\n | 'duplicate-name'\n | 'unknown-type'\n | 'misplaced-page'\n | 'not-a-page'\n | 'empty'\n | 'expression'\n | 'formula-cycle'\n | 'unknown-validator'\n | 'check-and-validator'\n /** The node at fault; none for the document's own problems. */\n nodeId?: string\n /**\n * Where the problem is, as a path into the document, e.g.\n * `nodes[1].children[0]` or `nodes[2].rules[0].check`; `version` for the\n * version, and `''` for a problem of the whole.\n */\n path: string\n /** The property at fault, e.g. `visibleWhen`, for an expression. */\n property?: string\n /** The id, name, type or expression source at fault, for the editor's message. */\n text: string\n /** The expression's own problem, for `expression`. */\n expression?: ExpressionProblem\n /** An English description, for logs. */\n message: string\n}\n\nexport interface CheckDocumentOptions {\n /** Field types besides the built-in ones, as given to the renderer. */\n fields?: readonly FormFieldType<any>[]\n /** Functions the expressions can call besides the built-in ones. */\n functions?: readonly string[]\n /**\n * The names of the validators given to the renderer. Left out, a rule's\n * `validator` is not checked against them.\n */\n validators?: readonly string[]\n}\n\nconst EXPRESSIONS = ['visibleWhen', 'disabledWhen', 'requiredWhen', 'formula'] as const\n\n/**\n * The problems of a document, for an editor to point at, before it is\n * saved or rendered: ids used twice, fields without a name or sharing one,\n * types nobody registered, pages outside tabs and steps, expressions that\n * do not parse or read fields that are not there, formulas that read\n * each other, and rules naming a validator nobody gave.\n */\nexport function checkDocument(\n document: FormDocument,\n options: CheckDocumentOptions = {},\n): DocumentProblem[] {\n const problems: DocumentProblem[] = []\n if (document.version !== 1)\n problems.push({\n code: 'version',\n path: 'version',\n text: String(document.version),\n message: `Version ${String(document.version)} is not known`,\n })\n const types = resolveFields(options.fields)\n const ids = new Set<string>()\n const known = scopeNames(document.nodes)\n /** Where each node is, by the node itself, so nodes that share an id each have their own. */\n const paths = new Map<FormNode, string>()\n\n /** `itemFields`: the names an item of the list around the nodes has; none outside lists. */\n function visit(\n nodes: readonly FormNode[],\n parent: FormNode | undefined,\n itemFields: readonly string[],\n at: string,\n ) {\n for (const [index, node] of nodes.entries()) {\n const path = `${at}[${index}]`\n paths.set(node, path)\n if (ids.has(node.id))\n problems.push({\n code: 'duplicate-id',\n nodeId: node.id,\n path,\n text: node.id,\n message: `The id \"${node.id}\" is used twice`,\n })\n ids.add(node.id)\n\n const pageParent = !!parent && (isTabs(parent) || isSteps(parent))\n if (isPage(node) && !pageParent)\n problems.push({\n code: 'misplaced-page',\n nodeId: node.id,\n path,\n text: node.id,\n message: 'A page belongs in tabs or steps',\n })\n if (pageParent && !isPage(node))\n problems.push({\n code: 'not-a-page',\n nodeId: node.id,\n path,\n text: node.id,\n message: 'Tabs and steps hold pages only',\n })\n\n if ((isTabs(node) || isSteps(node) || isList(node)) && !node.children?.length)\n problems.push({\n code: 'empty',\n nodeId: node.id,\n path,\n text: node.id,\n message: isList(node) ? 'The list has no fields' : `The ${node.type} have no pages`,\n })\n if (isList(node) && !node.name)\n problems.push({\n code: 'missing-name',\n nodeId: node.id,\n path,\n text: node.id,\n message: 'The list has no name',\n })\n\n if (isField(node)) {\n const type = types.get(node.type)\n if (!type)\n problems.push({\n code: 'unknown-type',\n nodeId: node.id,\n path,\n text: node.type,\n message: `There is no field type \"${node.type}\"`,\n })\n else if (type.kind !== 'display' && !node.name)\n problems.push({\n code: 'missing-name',\n nodeId: node.id,\n path,\n text: node.id,\n message: 'The field has no name',\n })\n }\n\n const expressions: [string, string | undefined][] = EXPRESSIONS.map((key) => [\n key,\n (node as Partial<Record<(typeof EXPRESSIONS)[number], string>>)[key],\n ])\n if (isField(node) || isList(node))\n (node.rules ?? []).forEach((rule, order) => {\n expressions.push([`rules.${order}.check`, rule.check])\n if (rule.validator === undefined) return\n const rulePath = `${path}.rules[${order}]`\n if (rule.check)\n problems.push({\n code: 'check-and-validator',\n nodeId: node.id,\n path: rulePath,\n text: rule.validator,\n message: 'A rule has both check and validator; only check runs',\n })\n else if (options.validators && !options.validators.includes(rule.validator))\n problems.push({\n code: 'unknown-validator',\n nodeId: node.id,\n path: `${rulePath}.validator`,\n text: rule.validator,\n message: `There is no validator \"${rule.validator}\"`,\n })\n })\n if (isField(node) && node.source?.arg) expressions.push(['source.arg', node.source.arg])\n for (const [property, source] of expressions) {\n if (!source) continue\n const found = checkExpression(source, {\n fields: known,\n itemFields,\n functions: options.functions,\n })\n for (const problem of found)\n problems.push({\n code: 'expression',\n nodeId: node.id,\n path: `${path}.${property.replace(/\\.(\\d+)/g, '[$1]')}`,\n property,\n text: source,\n expression: problem,\n message: `${property}: ${problem.message}`,\n })\n }\n\n if (isList(node))\n visit(node.children ?? [], node, scopeNames(node.children ?? []), `${path}.children`)\n else if (isContainer(node)) visit(node.children ?? [], node, itemFields, `${path}.children`)\n }\n }\n visit(document.nodes, undefined, [], 'nodes')\n duplicateNames(document.nodes, problems, paths)\n\n for (const cycle of analyseFormulas(document, options.functions).cycles) {\n const text = cycle.map(({ label }) => label)\n const node = cycle[0]!.node\n problems.push({\n code: 'formula-cycle',\n nodeId: node.id,\n path: paths.get(node) ?? '',\n text: text.join(' → '),\n message: `The formulas read each other: ${text.join(' -> ')}`,\n })\n }\n return problems\n}\n\nconst nameOf = (node: FormNode) => (isList(node) || isField(node) ? node.name : undefined)\n\n/** The names of one scope: the document's, or a list item's. Groups and pages share their parent's. */\nfunction scopeNames(nodes: readonly FormNode[]): string[] {\n const result: string[] = []\n for (const node of nodes) {\n const name = nameOf(node)\n if (name) result.push(name)\n if (isContainer(node) && !isList(node)) result.push(...scopeNames(node.children ?? []))\n }\n return result\n}\n\n/** Names used twice within one scope. */\nfunction duplicateNames(\n nodes: readonly FormNode[],\n problems: DocumentProblem[],\n paths: ReadonlyMap<FormNode, string>,\n) {\n const scan = (list: readonly FormNode[], seen: Set<string>) => {\n for (const node of list) {\n const name = nameOf(node)\n if (name) {\n if (seen.has(name))\n problems.push({\n code: 'duplicate-name',\n nodeId: node.id,\n path: paths.get(node) ?? '',\n text: name,\n message: `The name \"${name}\" is used twice`,\n })\n seen.add(name)\n }\n if (isList(node)) scan(node.children ?? [], new Set())\n else if (isContainer(node)) scan(node.children ?? [], seen)\n }\n }\n scan(nodes, new Set())\n}\n\n/** Upgrades from an older version of the document, by the version they start from. */\nconst migrations: Record<number, (document: any) => any> = {}\n\n/**\n * The document in the current version (`1`). Documents of an older\n * version are upgraded step by step; there are none yet, so for now this\n * only checks the version. A document without a version is read as\n * version 1, as the renderer does. Throws for anything that is not a\n * document and for a version it does not know.\n */\nexport function migrateDocument(document: unknown): FormDocument {\n if (document === null || typeof document !== 'object' || Array.isArray(document))\n throw new Error('[@dou-ui/form-builder] a form document must be an object')\n let current = document as { version?: unknown } & Record<string, unknown>\n if (current.version === undefined) current = { ...current, version: 1 }\n while (current.version !== 1) {\n const step = typeof current.version === 'number' ? migrations[current.version] : undefined\n if (!step)\n throw new Error(\n `[@dou-ui/form-builder] form document version ${String(current.version)} is not known`,\n )\n current = step(current)\n }\n return current as unknown as FormDocument\n}\n\n/** A part of some data that is not shaped like a form document. */\nexport interface DocumentShapeProblem {\n /** Where, as a path into the data, e.g. `nodes[2].children[0].label`; `''` for the whole. */\n path: string\n /** `type`: the value is of another kind; `required`: it is missing. */\n code: 'type' | 'required'\n /** What the value should be. */\n expected: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'span' | 'mark'\n /** An English description, for logs. */\n message: string\n}\n\ntype Expected = DocumentShapeProblem['expected']\n\nconst NODE_KEYS: Record<string, Expected> = {\n id: 'string',\n type: 'string',\n name: 'string',\n label: 'string',\n help: 'string',\n title: 'string',\n description: 'string',\n addText: 'string',\n visibleWhen: 'string',\n disabledWhen: 'string',\n requiredWhen: 'string',\n formula: 'string',\n required: 'boolean',\n bordered: 'boolean',\n sortable: 'boolean',\n min: 'number',\n max: 'number',\n columns: 'number',\n span: 'span',\n props: 'object',\n source: 'object',\n rules: 'array',\n children: 'array',\n}\n\nconst RULE_KEYS: Record<string, Expected> = {\n required: 'boolean',\n type: 'string',\n min: 'number',\n max: 'number',\n pattern: 'string',\n flags: 'string',\n check: 'string',\n validator: 'string',\n message: 'string',\n trigger: 'string',\n}\n\nconst DOCUMENT_KEYS: Record<string, Expected> = {\n version: 'number',\n layout: 'string',\n labelAlign: 'string',\n labelWidth: 'number',\n size: 'string',\n requiredMark: 'mark',\n columns: 'number',\n nodes: 'array',\n}\n\nconst CONTAINER_TYPES = new Set(['section', 'tabs', 'steps', 'page', 'list'])\nconst SHAPE_LIMIT = 50\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === 'object' && !Array.isArray(value)\n\nfunction fits(value: unknown, expected: Expected): boolean {\n switch (expected) {\n case 'object':\n return isObject(value)\n case 'array':\n return Array.isArray(value)\n case 'span':\n return (\n (typeof value === 'number' && Number.isFinite(value)) ||\n (isObject(value) && Object.values(value).every((each) => typeof each === 'number'))\n )\n case 'mark':\n return typeof value === 'boolean' || value === 'optional'\n case 'number':\n return typeof value === 'number' && Number.isFinite(value)\n default:\n return typeof value === expected\n }\n}\n\n/**\n * Where some data, e.g. JSON pasted into an editor, is not shaped like a\n * form document: a key holding the wrong kind of value, or a node without\n * its `id`, `type` or `children`. Problems name the path to the value, so\n * they can be found in the text. Run `checkDocument` on the result for\n * the problems of a document that is shaped right.\n */\nexport function checkDocumentShape(data: unknown): DocumentShapeProblem[] {\n const problems: DocumentShapeProblem[] = []\n const add = (path: string, code: 'type' | 'required', expected: Expected) => {\n if (problems.length >= SHAPE_LIMIT) return\n problems.push({\n path,\n code,\n expected,\n message:\n code === 'required'\n ? `${path} is missing`\n : `${path || 'The document'} should be ${expected === 'span' ? 'a number or an object of numbers' : expected === 'mark' ? 'a boolean or \"optional\"' : `a ${expected}`}`,\n })\n }\n const keys = (value: Record<string, unknown>, spec: Record<string, Expected>, path: string) => {\n for (const [key, expected] of Object.entries(spec))\n if (value[key] !== undefined && !fits(value[key], expected))\n add(`${path}.${key}`, 'type', expected)\n }\n const node = (value: unknown, path: string) => {\n if (!isObject(value)) return add(path, 'type', 'object')\n keys(value, NODE_KEYS, path)\n for (const key of ['id', 'type'] as const)\n if (value[key] === undefined) add(`${path}.${key}`, 'required', 'string')\n if (Array.isArray(value.rules))\n value.rules.forEach((rule, index) => {\n const at = `${path}.rules[${index}]`\n if (isObject(rule)) keys(rule, RULE_KEYS, at)\n else add(at, 'type', 'object')\n })\n if (isObject(value.source) && typeof value.source.name !== 'string')\n add(`${path}.source.name`, value.source.name === undefined ? 'required' : 'type', 'string')\n const container = typeof value.type === 'string' && CONTAINER_TYPES.has(value.type)\n if (container && value.children === undefined) add(`${path}.children`, 'required', 'array')\n if (Array.isArray(value.children))\n value.children.forEach((child, index) => node(child, `${path}.children[${index}]`))\n }\n if (!isObject(data)) {\n add('', 'type', 'object')\n return problems\n }\n for (const [key, expected] of Object.entries(DOCUMENT_KEYS))\n if (data[key] !== undefined && !fits(data[key], expected)) add(key, 'type', expected)\n if (data.nodes === undefined) add('nodes', 'required', 'array')\n if (Array.isArray(data.nodes)) data.nodes.forEach((each, index) => node(each, `nodes[${index}]`))\n return problems\n}\n"],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ // src/core/document.ts
2
+ /** A document with nothing in it. */
3
+ function emptyDocument() {
4
+ return {
5
+ version: 1,
6
+ nodes: []
7
+ };
8
+ }
9
+ var warnedVersion = false;
10
+ /** Warn once, for the whole page, about a document of a version this package does not know. */
11
+ function checkVersion(document) {
12
+ if (document.version === 1 || warnedVersion) return;
13
+ warnedVersion = true;
14
+ console.warn(`[@dou-ui/form-builder] form document version ${String(document.version)} is not supported; it is read as version 1`);
15
+ }
16
+ var counter = 0;
17
+ /** A short id that is unique enough within one document, e.g. `f-lz3k9a-1`. */
18
+ function createId(prefix = "f") {
19
+ counter = (counter + 1) % 1679616;
20
+ return `${prefix}-${Date.now().toString(36)}-${counter.toString(36)}`;
21
+ }
22
+ var CONTAINERS = /* @__PURE__ */ new Set([
23
+ "section",
24
+ "tabs",
25
+ "steps",
26
+ "page",
27
+ "list"
28
+ ]);
29
+ var isSection = (node) => node.type === "section";
30
+ var isTabs = (node) => node.type === "tabs";
31
+ var isSteps = (node) => node.type === "steps";
32
+ var isPage = (node) => node.type === "page";
33
+ var isList = (node) => node.type === "list";
34
+ /** Whether a node holds other nodes: a section, tabs, steps, a page or a list. */
35
+ var isContainer = (node) => CONTAINERS.has(node.type);
36
+ /** Whether a node is a field, of a built-in or a registered type. */
37
+ var isField = (node) => !CONTAINERS.has(node.type);
38
+ /** Every node in document order, containers before their children, the fields of list items included. */
39
+ function* walkNodes(nodes) {
40
+ for (const node of nodes) {
41
+ yield node;
42
+ if (isContainer(node)) yield* walkNodes(node.children);
43
+ }
44
+ }
45
+ /**
46
+ * The nodes that write a value of their own, in document order: fields
47
+ * with a `name` and lists. Fields inside a list belong to its items, so
48
+ * they are not among them.
49
+ */
50
+ function valueNodes(nodes) {
51
+ const result = [];
52
+ const visit = (list) => {
53
+ for (const node of list) if (isList(node)) result.push(node);
54
+ else if (isContainer(node)) visit(node.children);
55
+ else if (node.name) result.push(node);
56
+ };
57
+ visit(nodes);
58
+ return result;
59
+ }
60
+ /** The fields that write a value, in document order, outside lists. */
61
+ function fieldNodes(document) {
62
+ return valueNodes(document.nodes).filter(isField);
63
+ }
64
+ /** The node with this id, anywhere in the document. */
65
+ function findNode(document, id) {
66
+ for (const node of walkNodes(document.nodes)) if (node.id === id) return node;
67
+ }
68
+ var keysOf = (path) => path.split(".");
69
+ /** The value at a dotted path, or `undefined` when part of it is missing. */
70
+ function getPath(model, path) {
71
+ let value = model;
72
+ for (const key of keysOf(path)) {
73
+ if (value === null || typeof value !== "object") return void 0;
74
+ value = value[key];
75
+ }
76
+ return value;
77
+ }
78
+ /** Write a value at a dotted path, creating the objects on the way. */
79
+ function setPath(model, path, value) {
80
+ const keys = keysOf(path);
81
+ const last = keys.pop();
82
+ let target = model;
83
+ for (const key of keys) {
84
+ let next = target[key];
85
+ if (next === null || typeof next !== "object") next = target[key] = {};
86
+ target = next;
87
+ }
88
+ target[last] = value;
89
+ }
90
+ /** Remove the value at a dotted path, if it is there. */
91
+ function unsetPath(model, path) {
92
+ const keys = keysOf(path);
93
+ const last = keys.pop();
94
+ let target = model;
95
+ for (const key of keys) {
96
+ if (target === null || typeof target !== "object") return;
97
+ target = target[key];
98
+ }
99
+ if (target !== null && typeof target === "object") delete target[last];
100
+ }
101
+ /** A deep copy of plain data, so a default is never shared between forms. */
102
+ function cloneValue(value) {
103
+ if (value === null || typeof value !== "object") return value;
104
+ if (Array.isArray(value)) return value.map(cloneValue);
105
+ if (value instanceof Date) return new Date(value);
106
+ const result = {};
107
+ for (const [key, item] of Object.entries(value)) result[key] = cloneValue(item);
108
+ return result;
109
+ }
110
+ export { checkVersion, cloneValue, createId, emptyDocument, fieldNodes, findNode, getPath, isContainer, isField, isList, isPage, isSection, isSteps, isTabs, setPath, unsetPath, valueNodes, walkNodes };
111
+
112
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","names":[],"sources":["../../src/core/document.ts"],"sourcesContent":["import type {\n FormContainerNode,\n FormDocument,\n FormFieldNode,\n FormListNode,\n FormNode,\n FormPageNode,\n FormSectionNode,\n FormStepsNode,\n FormTabsNode,\n} from './types'\n\n/** A document with nothing in it. */\nexport function emptyDocument(): FormDocument {\n return { version: 1, nodes: [] }\n}\n\nlet warnedVersion = false\n\n/** Warn once, for the whole page, about a document of a version this package does not know. */\nexport function checkVersion(document: FormDocument) {\n if (document.version === 1 || warnedVersion) return\n warnedVersion = true\n console.warn(\n `[@dou-ui/form-builder] form document version ${String(document.version)} is not supported; it is read as version 1`,\n )\n}\n\nlet counter = 0\n\n/** A short id that is unique enough within one document, e.g. `f-lz3k9a-1`. */\nexport function createId(prefix = 'f'): string {\n counter = (counter + 1) % 1679616\n return `${prefix}-${Date.now().toString(36)}-${counter.toString(36)}`\n}\n\nconst CONTAINERS = new Set(['section', 'tabs', 'steps', 'page', 'list'])\n\nexport const isSection = (node: FormNode): node is FormSectionNode => node.type === 'section'\nexport const isTabs = (node: FormNode): node is FormTabsNode => node.type === 'tabs'\nexport const isSteps = (node: FormNode): node is FormStepsNode => node.type === 'steps'\nexport const isPage = (node: FormNode): node is FormPageNode => node.type === 'page'\nexport const isList = (node: FormNode): node is FormListNode => node.type === 'list'\n/** Whether a node holds other nodes: a section, tabs, steps, a page or a list. */\nexport const isContainer = (node: FormNode): node is FormContainerNode => CONTAINERS.has(node.type)\n/** Whether a node is a field, of a built-in or a registered type. */\nexport const isField = (node: FormNode): node is FormFieldNode => !CONTAINERS.has(node.type)\n\n/** Every node in document order, containers before their children, the fields of list items included. */\nexport function* walkNodes(nodes: readonly FormNode[]): Generator<FormNode> {\n for (const node of nodes) {\n yield node\n if (isContainer(node)) yield* walkNodes(node.children)\n }\n}\n\n/**\n * The nodes that write a value of their own, in document order: fields\n * with a `name` and lists. Fields inside a list belong to its items, so\n * they are not among them.\n */\nexport function valueNodes(nodes: readonly FormNode[]): (FormFieldNode | FormListNode)[] {\n const result: (FormFieldNode | FormListNode)[] = []\n const visit = (list: readonly FormNode[]) => {\n for (const node of list) {\n if (isList(node)) result.push(node)\n else if (isContainer(node)) visit(node.children)\n else if (node.name) result.push(node)\n }\n }\n visit(nodes)\n return result\n}\n\n/** The fields that write a value, in document order, outside lists. */\nexport function fieldNodes(document: FormDocument): FormFieldNode[] {\n return valueNodes(document.nodes).filter(isField)\n}\n\n/** The node with this id, anywhere in the document. */\nexport function findNode(document: FormDocument, id: string): FormNode | undefined {\n for (const node of walkNodes(document.nodes)) if (node.id === id) return node\n return undefined\n}\n\nconst keysOf = (path: string) => path.split('.')\n\n/** The value at a dotted path, or `undefined` when part of it is missing. */\nexport function getPath(model: unknown, path: string): unknown {\n let value = model\n for (const key of keysOf(path)) {\n if (value === null || typeof value !== 'object') return undefined\n value = (value as Record<string, unknown>)[key]\n }\n return value\n}\n\n/** Write a value at a dotted path, creating the objects on the way. */\nexport function setPath(model: Record<string, unknown>, path: string, value: unknown) {\n const keys = keysOf(path)\n const last = keys.pop()!\n let target = model\n for (const key of keys) {\n let next = target[key]\n if (next === null || typeof next !== 'object') next = target[key] = {}\n target = next as Record<string, unknown>\n }\n target[last] = value\n}\n\n/** Remove the value at a dotted path, if it is there. */\nexport function unsetPath(model: Record<string, unknown>, path: string) {\n const keys = keysOf(path)\n const last = keys.pop()!\n let target: unknown = model\n for (const key of keys) {\n if (target === null || typeof target !== 'object') return\n target = (target as Record<string, unknown>)[key]\n }\n if (target !== null && typeof target === 'object')\n delete (target as Record<string, unknown>)[last]\n}\n\n/** A deep copy of plain data, so a default is never shared between forms. */\nexport function cloneValue<T>(value: T): T {\n if (value === null || typeof value !== 'object') return value\n if (Array.isArray(value)) return value.map(cloneValue) as T\n if (value instanceof Date) return new Date(value) as T\n const result: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) result[key] = cloneValue(item)\n return result as T\n}\n"],"mappings":";;AAaA,SAAgB,gBAA8B;CAC5C,OAAO;EAAE,SAAS;EAAG,OAAO,CAAC;CAAE;AACjC;AAEA,IAAI,gBAAgB;;AAGpB,SAAgB,aAAa,UAAwB;CACnD,IAAI,SAAS,YAAY,KAAK,eAAe;CAC7C,gBAAgB;CAChB,QAAQ,KACN,gDAAgD,OAAO,SAAS,OAAO,EAAE,2CAC3E;AACF;AAEA,IAAI,UAAU;;AAGd,SAAgB,SAAS,SAAS,KAAa;CAC7C,WAAW,UAAU,KAAK;CAC1B,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,QAAQ,SAAS,EAAE;AACpE;AAEA,IAAM,6BAAa,IAAI,IAAI;CAAC;CAAW;CAAQ;CAAS;CAAQ;AAAM,CAAC;AAEvE,IAAa,aAAa,SAA4C,KAAK,SAAS;AACpF,IAAa,UAAU,SAAyC,KAAK,SAAS;AAC9E,IAAa,WAAW,SAA0C,KAAK,SAAS;AAChF,IAAa,UAAU,SAAyC,KAAK,SAAS;AAC9E,IAAa,UAAU,SAAyC,KAAK,SAAS;;AAE9E,IAAa,eAAe,SAA8C,WAAW,IAAI,KAAK,IAAI;;AAElG,IAAa,WAAW,SAA0C,CAAC,WAAW,IAAI,KAAK,IAAI;;AAG3F,UAAiB,UAAU,OAAiD;CAC1E,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM;EACN,IAAI,YAAY,IAAI,GAAG,OAAO,UAAU,KAAK,QAAQ;CACvD;AACF;;;;;;AAOA,SAAgB,WAAW,OAA8D;CACvF,MAAM,SAA2C,CAAC;CAClD,MAAM,SAAS,SAA8B;EAC3C,KAAK,MAAM,QAAQ,MACjB,IAAI,OAAO,IAAI,GAAG,OAAO,KAAK,IAAI;OAC7B,IAAI,YAAY,IAAI,GAAG,MAAM,KAAK,QAAQ;OAC1C,IAAI,KAAK,MAAM,OAAO,KAAK,IAAI;CAExC;CACA,MAAM,KAAK;CACX,OAAO;AACT;;AAGA,SAAgB,WAAW,UAAyC;CAClE,OAAO,WAAW,SAAS,KAAK,CAAC,CAAC,OAAO,OAAO;AAClD;;AAGA,SAAgB,SAAS,UAAwB,IAAkC;CACjF,KAAK,MAAM,QAAQ,UAAU,SAAS,KAAK,GAAG,IAAI,KAAK,OAAO,IAAI,OAAO;AAE3E;AAEA,IAAM,UAAU,SAAiB,KAAK,MAAM,GAAG;;AAG/C,SAAgB,QAAQ,OAAgB,MAAuB;CAC7D,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,OAAO,IAAI,GAAG;EAC9B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO,KAAA;EACxD,QAAS,MAAkC;CAC7C;CACA,OAAO;AACT;;AAGA,SAAgB,QAAQ,OAAgC,MAAc,OAAgB;CACpF,MAAM,OAAO,OAAO,IAAI;CACxB,MAAM,OAAO,KAAK,IAAI;CACtB,IAAI,SAAS;CACb,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,OAAO,OAAO;EAClB,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO,OAAO,OAAO,CAAC;EACrE,SAAS;CACX;CACA,OAAO,QAAQ;AACjB;;AAGA,SAAgB,UAAU,OAAgC,MAAc;CACtE,MAAM,OAAO,OAAO,IAAI;CACxB,MAAM,OAAO,KAAK,IAAI;CACtB,IAAI,SAAkB;CACtB,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,WAAW,QAAQ,OAAO,WAAW,UAAU;EACnD,SAAU,OAAmC;CAC/C;CACA,IAAI,WAAW,QAAQ,OAAO,WAAW,UACvC,OAAQ,OAAmC;AAC/C;;AAGA,SAAgB,WAAc,OAAa;CACzC,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,IAAI,UAAU;CACrD,IAAI,iBAAiB,MAAM,OAAO,IAAI,KAAK,KAAK;CAChD,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,KAAK,GAAG,OAAO,OAAO,WAAW,IAAI;CAC9E,OAAO;AACT"}