@json-render/core 0.4.3 → 0.4.4

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/dist/index.mjs CHANGED
@@ -28,17 +28,27 @@ function resolveDynamicValue(value, dataModel) {
28
28
  }
29
29
  return value;
30
30
  }
31
+ function unescapeJsonPointer(token) {
32
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
33
+ }
34
+ function parseJsonPointer(path) {
35
+ const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/");
36
+ return raw.map(unescapeJsonPointer);
37
+ }
31
38
  function getByPath(obj, path) {
32
39
  if (!path || path === "/") {
33
40
  return obj;
34
41
  }
35
- const segments = path.startsWith("/") ? path.slice(1).split("/") : path.split("/");
42
+ const segments = parseJsonPointer(path);
36
43
  let current = obj;
37
44
  for (const segment of segments) {
38
45
  if (current === null || current === void 0) {
39
46
  return void 0;
40
47
  }
41
- if (typeof current === "object") {
48
+ if (Array.isArray(current)) {
49
+ const index = parseInt(segment, 10);
50
+ current = current[index];
51
+ } else if (typeof current === "object") {
42
52
  current = current[segment];
43
53
  } else {
44
54
  return void 0;
@@ -50,13 +60,13 @@ function isNumericIndex(str) {
50
60
  return /^\d+$/.test(str);
51
61
  }
52
62
  function setByPath(obj, path, value) {
53
- const segments = path.startsWith("/") ? path.slice(1).split("/") : path.split("/");
63
+ const segments = parseJsonPointer(path);
54
64
  if (segments.length === 0) return;
55
65
  let current = obj;
56
66
  for (let i = 0; i < segments.length - 1; i++) {
57
67
  const segment = segments[i];
58
68
  const nextSegment = segments[i + 1];
59
- const nextIsNumeric = nextSegment !== void 0 && isNumericIndex(nextSegment);
69
+ const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-");
60
70
  if (Array.isArray(current)) {
61
71
  const index = parseInt(segment, 10);
62
72
  if (current[index] === void 0 || typeof current[index] !== "object") {
@@ -72,12 +82,95 @@ function setByPath(obj, path, value) {
72
82
  }
73
83
  const lastSegment = segments[segments.length - 1];
74
84
  if (Array.isArray(current)) {
75
- const index = parseInt(lastSegment, 10);
76
- current[index] = value;
85
+ if (lastSegment === "-") {
86
+ current.push(value);
87
+ } else {
88
+ const index = parseInt(lastSegment, 10);
89
+ current[index] = value;
90
+ }
91
+ } else {
92
+ current[lastSegment] = value;
93
+ }
94
+ }
95
+ function addByPath(obj, path, value) {
96
+ const segments = parseJsonPointer(path);
97
+ if (segments.length === 0) return;
98
+ let current = obj;
99
+ for (let i = 0; i < segments.length - 1; i++) {
100
+ const segment = segments[i];
101
+ const nextSegment = segments[i + 1];
102
+ const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-");
103
+ if (Array.isArray(current)) {
104
+ const index = parseInt(segment, 10);
105
+ if (current[index] === void 0 || typeof current[index] !== "object") {
106
+ current[index] = nextIsNumeric ? [] : {};
107
+ }
108
+ current = current[index];
109
+ } else {
110
+ if (!(segment in current) || typeof current[segment] !== "object") {
111
+ current[segment] = nextIsNumeric ? [] : {};
112
+ }
113
+ current = current[segment];
114
+ }
115
+ }
116
+ const lastSegment = segments[segments.length - 1];
117
+ if (Array.isArray(current)) {
118
+ if (lastSegment === "-") {
119
+ current.push(value);
120
+ } else {
121
+ const index = parseInt(lastSegment, 10);
122
+ current.splice(index, 0, value);
123
+ }
77
124
  } else {
78
125
  current[lastSegment] = value;
79
126
  }
80
127
  }
128
+ function removeByPath(obj, path) {
129
+ const segments = parseJsonPointer(path);
130
+ if (segments.length === 0) return;
131
+ let current = obj;
132
+ for (let i = 0; i < segments.length - 1; i++) {
133
+ const segment = segments[i];
134
+ if (Array.isArray(current)) {
135
+ const index = parseInt(segment, 10);
136
+ if (current[index] === void 0 || typeof current[index] !== "object") {
137
+ return;
138
+ }
139
+ current = current[index];
140
+ } else {
141
+ if (!(segment in current) || typeof current[segment] !== "object") {
142
+ return;
143
+ }
144
+ current = current[segment];
145
+ }
146
+ }
147
+ const lastSegment = segments[segments.length - 1];
148
+ if (Array.isArray(current)) {
149
+ const index = parseInt(lastSegment, 10);
150
+ if (index >= 0 && index < current.length) {
151
+ current.splice(index, 1);
152
+ }
153
+ } else {
154
+ delete current[lastSegment];
155
+ }
156
+ }
157
+ function deepEqual(a, b) {
158
+ if (a === b) return true;
159
+ if (a === null || b === null) return false;
160
+ if (typeof a !== typeof b) return false;
161
+ if (typeof a !== "object") return false;
162
+ if (Array.isArray(a)) {
163
+ if (!Array.isArray(b)) return false;
164
+ if (a.length !== b.length) return false;
165
+ return a.every((item, i) => deepEqual(item, b[i]));
166
+ }
167
+ const aObj = a;
168
+ const bObj = b;
169
+ const aKeys = Object.keys(aObj);
170
+ const bKeys = Object.keys(bObj);
171
+ if (aKeys.length !== bKeys.length) return false;
172
+ return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
173
+ }
81
174
  function findFormValue(fieldName, params, data) {
82
175
  if (params?.[fieldName] !== void 0) {
83
176
  const val = params[fieldName];
@@ -126,10 +219,38 @@ function parseSpecStreamLine(line) {
126
219
  }
127
220
  }
128
221
  function applySpecStreamPatch(obj, patch) {
129
- if (patch.op === "set" || patch.op === "add" || patch.op === "replace") {
130
- setByPath(obj, patch.path, patch.value);
131
- } else if (patch.op === "remove") {
132
- setByPath(obj, patch.path, void 0);
222
+ switch (patch.op) {
223
+ case "add":
224
+ addByPath(obj, patch.path, patch.value);
225
+ break;
226
+ case "replace":
227
+ setByPath(obj, patch.path, patch.value);
228
+ break;
229
+ case "remove":
230
+ removeByPath(obj, patch.path);
231
+ break;
232
+ case "move": {
233
+ if (!patch.from) break;
234
+ const moveValue = getByPath(obj, patch.from);
235
+ removeByPath(obj, patch.from);
236
+ addByPath(obj, patch.path, moveValue);
237
+ break;
238
+ }
239
+ case "copy": {
240
+ if (!patch.from) break;
241
+ const copyValue = getByPath(obj, patch.from);
242
+ addByPath(obj, patch.path, copyValue);
243
+ break;
244
+ }
245
+ case "test": {
246
+ const actual = getByPath(obj, patch.path);
247
+ if (!deepEqual(actual, patch.value)) {
248
+ throw new Error(
249
+ `Test operation failed: value at "${patch.path}" does not match`
250
+ );
251
+ }
252
+ break;
253
+ }
133
254
  }
134
255
  return obj;
135
256
  }
@@ -872,10 +993,10 @@ function generatePrompt(catalog, options) {
872
993
  lines.push("");
873
994
  lines.push("Example output (each line is a separate JSON object):");
874
995
  lines.push("");
875
- lines.push(`{"op":"set","path":"/root","value":"card-1"}
876
- {"op":"set","path":"/elements/card-1","value":{"key":"card-1","type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"],"parentKey":""}}
877
- {"op":"set","path":"/elements/metric-1","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[],"parentKey":"card-1"}}
878
- {"op":"set","path":"/elements/chart-1","value":{"key":"chart-1","type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[],"parentKey":"card-1"}}`);
996
+ lines.push(`{"op":"add","path":"/root","value":"card-1"}
997
+ {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"]}}
998
+ {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[]}}
999
+ {"op":"add","path":"/elements/chart-1","value":{"type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[]}}`);
879
1000
  lines.push("");
880
1001
  const components = catalog.data.components;
881
1002
  if (components) {
@@ -902,12 +1023,11 @@ function generatePrompt(catalog, options) {
902
1023
  lines.push("RULES:");
903
1024
  const baseRules = [
904
1025
  "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
905
- 'First line sets root: {"op":"set","path":"/root","value":"<root-key>"}',
906
- 'Then add each element: {"op":"set","path":"/elements/<key>","value":{...}}',
1026
+ 'First line sets root: {"op":"add","path":"/root","value":"<root-key>"}',
1027
+ 'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
907
1028
  "ONLY use components listed above",
908
- "Each element value needs: key, type, props, children (array of child keys), parentKey",
909
- "Use unique keys (e.g., 'header', 'metric-1', 'chart-revenue')",
910
- "Root element's parentKey is empty string, children reference their parent's key"
1029
+ "Each element value needs: type, props, children (array of child keys)",
1030
+ "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
911
1031
  ];
912
1032
  const allRules = [...baseRules, ...customRules];
913
1033
  allRules.forEach((rule, i) => {
@@ -1063,22 +1183,18 @@ function createCatalog(config) {
1063
1183
  const componentSchemas = componentNames.map((componentName) => {
1064
1184
  const def = components[componentName];
1065
1185
  return z6.object({
1066
- key: z6.string(),
1067
1186
  type: z6.literal(componentName),
1068
1187
  props: def.props,
1069
1188
  children: z6.array(z6.string()).optional(),
1070
- parentKey: z6.string().nullable().optional(),
1071
1189
  visible: VisibilityConditionSchema.optional()
1072
1190
  });
1073
1191
  });
1074
1192
  let elementSchema;
1075
1193
  if (componentSchemas.length === 0) {
1076
1194
  elementSchema = z6.object({
1077
- key: z6.string(),
1078
1195
  type: z6.string(),
1079
1196
  props: z6.record(z6.string(), z6.unknown()),
1080
1197
  children: z6.array(z6.string()).optional(),
1081
- parentKey: z6.string().nullable().optional(),
1082
1198
  visible: VisibilityConditionSchema.optional()
1083
1199
  });
1084
1200
  } else if (componentSchemas.length === 1) {
@@ -1299,21 +1415,21 @@ function generateSystemPrompt(catalog, options = {}) {
1299
1415
  }
1300
1416
  lines.push("");
1301
1417
  }
1302
- lines.push("OUTPUT FORMAT (JSONL):");
1303
- lines.push('{"op":"set","path":"/root","value":"element-key"}');
1418
+ lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
1419
+ lines.push('{"op":"add","path":"/root","value":"element-key"}');
1304
1420
  lines.push(
1305
- '{"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}'
1421
+ '{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}'
1306
1422
  );
1307
1423
  lines.push('{"op":"remove","path":"/elements/key"}');
1308
1424
  lines.push("");
1309
1425
  lines.push("RULES:");
1310
1426
  const baseRules = [
1311
- "First line sets /root to root element key",
1312
- "Add elements with /elements/{key}",
1427
+ 'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
1428
+ 'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
1313
1429
  "Remove elements with op:remove - also update the parent's children array to exclude the removed key",
1314
1430
  "Children array contains string keys, not objects",
1315
1431
  "Parent first, then children",
1316
- "Each element needs: key, type, props",
1432
+ "Each element needs: type, props",
1317
1433
  "ONLY use props listed above - never invent new props"
1318
1434
  ];
1319
1435
  const allRules = [...baseRules, ...customRules];
@@ -1343,6 +1459,7 @@ export {
1343
1459
  ValidationConfigSchema,
1344
1460
  VisibilityConditionSchema,
1345
1461
  action,
1462
+ addByPath,
1346
1463
  applySpecStreamPatch,
1347
1464
  builtInValidationFunctions,
1348
1465
  check,
@@ -1360,6 +1477,7 @@ export {
1360
1477
  getByPath,
1361
1478
  interpolateString,
1362
1479
  parseSpecStreamLine,
1480
+ removeByPath,
1363
1481
  resolveAction,
1364
1482
  resolveDynamicValue,
1365
1483
  runValidation,