-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathutils.js
More file actions
193 lines (164 loc) · 6.58 KB
/
utils.js
File metadata and controls
193 lines (164 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { daFetch } from 'https://da.live/blocks/shared/utils.js';
function parsePath(path) {
const parts = path.split('.').flatMap((part) => {
// Handle consecutive indices like "[0][0]" first (before arrayMatch grabs "[0]" as key)
if (part.startsWith('[')) {
const indices = part.match(/\[(\d+)\]/g);
if (indices) return indices.map((i) => parseInt(i.slice(1, -1), 10));
}
const arrayMatch = part.match(/^(.+?)\[(\d+)\]$/);
if (arrayMatch) return [arrayMatch[1], parseInt(arrayMatch[2], 10)];
const indexMatch = part.match(/^\[(\d+)\]$/);
if (indexMatch) return [parseInt(indexMatch[1], 10)];
return part;
});
return parts;
}
export async function loadHtml(details) {
const resp = await daFetch(details.sourceUrl);
if (!resp.ok) return { error: 'Could not fetch doc' };
return { html: (await resp.text()) };
}
/**
* Gets a value from an object using a path string.
* @param {Object} obj - The object to read from
* @param {string} path - The path string (e.g. "data.items[0].name")
* @returns {*} - The value at the path, or undefined
*/
export function getValueByPath(obj, path) {
const parts = parsePath(path);
let current = obj;
for (const part of parts) {
if (current == null) return undefined;
current = current[part];
}
return current;
}
/**
* Removes an array item at the given path by splicing it from its parent array.
* @param {Object} obj - The object to modify
* @param {string} path - The path to the array item (e.g., "data.items.[0]" or "data.items[0]")
* @returns {boolean} - True if the item was removed, false otherwise
*/
export function removeArrayItemByPath(obj, path) {
const parts = parsePath(path);
if (parts.length < 2) return false;
// Path must end with an array index (e.g. "data.items.[0]" → index 0)
const lastPart = parts[parts.length - 1];
if (typeof lastPart !== 'number') return false;
// Navigate to the parent of the target array
const parentParts = parts.slice(0, -1);
let current = obj;
for (let i = 0; i < parentParts.length - 1; i += 1) {
const part = parentParts[i];
if (current == null || !(part in current)) return false;
// Step into each segment (e.g. obj → obj.data → obj.data.items)
current = current[part];
}
// Key of the array (e.g. "items")
const parentKey = parentParts[parentParts.length - 1];
if (current == null || !(parentKey in current)
|| !Array.isArray(current[parentKey])) return false;
const array = current[parentKey];
const index = lastPart;
if (index < 0 || index >= array.length) return false;
// Remove the item in-place
array.splice(index, 1);
return true;
}
/**
* Sets a value on an object using a path string.
* Supports dot notation and array indices.
* @param {Object} obj - The object to set the value on
* @param {string} path - The path string (e.g., "data.parent[0].child")
* @param {*} value - The value to set
* @example
* const obj = { data: { items: [{ name: 'test' }] } };
* setValueByPath(obj, 'data.items[0].name', 'updated');
* // obj.data.items[0].name is now 'updated'
*/
export function setValueByPath(obj, path, value) {
const parts = parsePath(path);
// Navigate to the parent of the final property
let current = obj;
for (let i = 0; i < parts.length - 1; i += 1) {
const part = parts[i];
if (!(part in current)) {
// Create missing intermediate objects/arrays
const nextPart = parts[i + 1];
current[part] = typeof nextPart === 'number' ? [] : {};
}
current = current[part];
}
// Set the final value
current[parts[parts.length - 1]] = value;
}
export function resolvePropSchema(localSchema, fullSchema) {
const { title } = localSchema;
if (localSchema.$ref) {
const path = localSchema.$ref.substring(2).split('/')[1];
// try local ref
let def = localSchema?.$defs?.[path];
// TODO: walk up the tree looking for the def
// try global ref
if (!def) def = fullSchema?.$defs?.[path];
if (def) {
if (!title) return def;
return { ...def, title };
}
}
// Normalize local props to the same format as referenced schema
return { title, properties: localSchema };
}
/**
* @param {*} key the key of the property
* @param {*} prop the current property being acted on
* @param {*} propSchema the schema that applies to the current property
* @param {*} fullSchema the full schema that applies to the form
* @param {*} path the full path to this property (e.g., "grand.parent[0].child")
*/
export function annotateProp(key, propData, propSchema, fullSchema, path = '', required = false) {
// Build the current path
const currentPath = path ? `${path}.${key}` : key;
// Will have schema.props
const resolvedSchema = resolvePropSchema(propSchema, fullSchema);
if (Array.isArray(propData)) {
const resolvedItemsSchema = resolvePropSchema(propSchema.items, fullSchema);
// It's possible that items do not have a title, let them inherit from the parent
resolvedItemsSchema.title ??= resolvedSchema.title;
const data = [];
// Loop through the actual data and match it to the item schema
propData.forEach((itemPropData, index) => {
if (propSchema.items.oneOf) {
// TODO: Support one of schemas
// propSchema.items.oneOf.forEach((oneOf) => {
// console.log(oneOf);
// const arrayPath = `${currentPath}[${index}]`;
// data.push(annotateProp(key, itemPropData, oneOf, fullSchema, arrayPath));
// });
} else {
data.push(annotateProp(`[${index}]`, itemPropData, propSchema.items, fullSchema, currentPath));
}
});
return { key, data, schema: resolvedSchema, path: currentPath, required };
}
if (typeof propData === 'object') {
// Loop through the data and match it to the item schema
// return as array to keep consistent with upper array
const data = Object.entries(propData).reduce((acc, [k, pD]) => {
const isRequired = resolvedSchema.required?.includes(k) ?? false;
if (resolvedSchema.properties[k]) {
const childSchema = resolvedSchema.properties[k];
acc.push(annotateProp(k, pD, childSchema, fullSchema, currentPath, isRequired));
}
// Look for sub-property schemas
if (resolvedSchema.properties.properties?.[k]) {
const subPropSchema = resolvedSchema.properties.properties[k];
acc.push(annotateProp(k, pD, subPropSchema, fullSchema, currentPath, isRequired));
}
return acc;
}, []);
return { key, data, schema: resolvedSchema, path: currentPath, required };
}
return { key, data: propData, schema: resolvedSchema, path: currentPath, required };
}