@kubohiroya/turbowarp-extension-data-editor 0.1.0 → 0.2.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.
- package/README.md +30 -6
- package/dist/build-info.json +7 -2
- package/dist/descendants.js +11 -2
- package/dist/fixed-input-branches.d.ts +12 -0
- package/dist/fixed-input-branches.js +180 -0
- package/dist/fixed-input-editor.d.ts +39 -0
- package/dist/fixed-input-editor.js +538 -0
- package/dist/fixed-input-schema.d.ts +15 -0
- package/dist/fixed-input-schema.js +21 -0
- package/dist/fixed-input-snapshot.d.ts +7 -0
- package/dist/fixed-input-snapshot.js +180 -0
- package/dist/fixed-input-types.d.ts +138 -0
- package/dist/fixed-input-types.js +23 -0
- package/dist/fixed-input-view.d.ts +14 -0
- package/dist/fixed-input-view.js +168 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/json-value.d.ts +4 -0
- package/dist/json-value.js +12 -0
- package/dist/prepared-save.d.ts +5 -0
- package/dist/prepared-save.js +47 -0
- package/dist/prepared-types.d.ts +13 -0
- package/dist/prepared-types.js +1 -0
- package/dist/react.css +42 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.js +10 -1
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +98 -48
- package/dist/rjsf.d.ts +48 -1
- package/dist/rjsf.js +92 -8
- package/dist/schema-compose.d.ts +6 -0
- package/dist/schema-compose.js +103 -0
- package/dist/schema-engine.d.ts +18 -0
- package/dist/schema-engine.js +298 -0
- package/dist/schema-json.d.ts +10 -0
- package/dist/schema-json.js +68 -0
- package/dist/schema-types.d.ts +161 -0
- package/dist/schema-types.js +2 -0
- package/dist/schema-view.d.ts +7 -0
- package/dist/schema-view.js +102 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +87 -0
- package/dist/session.d.ts +3 -1
- package/dist/session.js +220 -9
- package/dist/source-document.d.ts +3 -0
- package/dist/source-document.js +415 -0
- package/dist/source-editor.d.ts +13 -0
- package/dist/source-editor.js +235 -0
- package/dist/source-schema-hints.d.ts +3 -0
- package/dist/source-schema-hints.js +13 -0
- package/dist/source-types.d.ts +47 -0
- package/dist/source-types.js +1 -0
- package/dist/source-view.d.ts +3 -0
- package/dist/source-view.js +318 -0
- package/dist/turbowarp/bridge.d.ts +2 -1
- package/dist/turbowarp/bridge.js +34 -5
- package/dist/turbowarp/fixed-fields.d.ts +82 -0
- package/dist/turbowarp/fixed-fields.js +311 -0
- package/dist/turbowarp/fixed-transaction.d.ts +18 -0
- package/dist/turbowarp/fixed-transaction.js +460 -0
- package/dist/turbowarp/host.d.ts +3 -8
- package/dist/turbowarp/host.js +68 -1
- package/dist/turbowarp.d.ts +4 -0
- package/dist/turbowarp.js +1 -1
- package/dist/types.d.ts +7 -4
- package/dist/view.js +4 -0
- package/dist/webpack4/index.js +24 -5
- package/dist/webpack4/metafile.json +9886 -8710
- package/dist/webpack4/react.js +11 -2
- package/dist/webpack4/react.js.map +2 -2
- package/dist/webpack4/rjsf.js +15205 -20951
- package/dist/webpack4/rjsf.js.map +4 -4
- package/dist/webpack4/schema.js +20 -0
- package/dist/webpack4/shared-75QIWLZY.js +8375 -0
- package/dist/webpack4/shared-75QIWLZY.js.map +7 -0
- package/dist/webpack4/shared-BKTRZUZH.js +42 -0
- package/dist/webpack4/{shared-KIFRVTSM.js.map → shared-BKTRZUZH.js.map} +1 -1
- package/dist/webpack4/shared-FYUILHED.js +1 -0
- package/dist/webpack4/shared-FYUILHED.js.map +7 -0
- package/dist/webpack4/shared-GQLPPRT2.js +210 -0
- package/dist/webpack4/shared-GQLPPRT2.js.map +7 -0
- package/dist/webpack4/shared-P2JBRZFP.js +2206 -0
- package/dist/webpack4/shared-P2JBRZFP.js.map +7 -0
- package/dist/webpack4/{shared-KIFRVTSM.js → shared-RN7GOT65.js} +2 -41
- package/dist/webpack4/shared-RN7GOT65.js.map +7 -0
- package/dist/webpack4/turbowarp.js +912 -9
- package/dist/webpack4/turbowarp.js.map +4 -4
- package/docs/README.md +1 -0
- package/docs/block-editor-e2e.md +98 -16
- package/docs/ci-success-gate.md +84 -0
- package/docs/editor/issue-12-public-validation/README.md +61 -0
- package/docs/editor/issue-12-public-validation/dark-invalid.png +0 -0
- package/docs/editor/issue-12-public-validation/dark-styles.json +173 -0
- package/docs/editor/issue-12-public-validation/dark-valid.png +0 -0
- package/docs/editor/issue-12-public-validation/deployment-target.json +5 -0
- package/docs/editor/issue-12-public-validation/deployment-wait.json +7 -0
- package/docs/editor/issue-12-public-validation/light-invalid.png +0 -0
- package/docs/editor/issue-12-public-validation/light-styles.json +173 -0
- package/docs/editor/issue-12-public-validation/light-valid.png +0 -0
- package/docs/editor/issue-12-public-validation/provenance.json +88 -0
- package/docs/editor/issue-12-public-validation/results.json +677 -0
- package/docs/editor/issue-12-public-validation/summary.json +206 -0
- package/docs/fixed-input-contracts.md +77 -0
- package/docs/github-pages.md +48 -41
- package/docs/package-distribution.md +2 -2
- package/docs/schema-stage1-browser.json +10 -0
- package/docs/schema-stage1-contracts.json +242 -0
- package/docs/schema-stage1-dependencies.json +202 -0
- package/docs/schema-stage1-git-artifact-20261018.ja.md +17 -0
- package/docs/schema-stage1-profile-compat.json +30 -0
- package/docs/schema-stage1-review-fixes-20261018.ja.md +36 -0
- package/docs/schema-stage1-verification.json +541 -0
- package/docs/schema-stage1.ja.md +80 -0
- package/docs/schema.md +43 -0
- package/docs/source-forms.ja.md +19 -0
- package/docs/stage-2-publication.ja.md +35 -0
- package/docs/template-fixture-separation.ja.md +43 -0
- package/package.json +18 -3
- package/webpack4.cjs +1 -1
- package/dist/webpack4/shared-CQB3NG2A.js +0 -1
- package/dist/webpack4/shared-WHOAW2VF.js +0 -957
- package/dist/webpack4/shared-WHOAW2VF.js.map +0 -7
- /package/dist/webpack4/{shared-CQB3NG2A.js.map → schema.js.map} +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument } from 'yaml';
|
|
2
|
+
import { snapshotJson } from './schema-json.js';
|
|
3
|
+
function fail() { throw new Error('Source cannot be edited without changing unedited data.'); }
|
|
4
|
+
function record(value) {
|
|
5
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function equal(left, right) {
|
|
8
|
+
if (left === right)
|
|
9
|
+
return true;
|
|
10
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
11
|
+
return left.length === right.length && left.every((value, index) => equal(value, right[index]));
|
|
12
|
+
}
|
|
13
|
+
if (record(left) && record(right)) {
|
|
14
|
+
const keys = Object.keys(left);
|
|
15
|
+
return keys.length === Object.keys(right).length && keys.every(key => Object.hasOwn(right, key) && equal(left[key], right[key]));
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function serialize(value) { return JSON.stringify(value); }
|
|
20
|
+
// snapshotJson creates fresh mutable arrays; its public result type is readonly.
|
|
21
|
+
function snapshot(value) { return snapshotJson(value); }
|
|
22
|
+
function apply(source, patches) {
|
|
23
|
+
const ordered = patches.map((patch, order) => ({ ...patch, order }));
|
|
24
|
+
ordered.sort((a, b) => b.start - a.start || b.end - a.end || b.order - a.order);
|
|
25
|
+
let boundary = source.length;
|
|
26
|
+
let result = source;
|
|
27
|
+
for (const patch of ordered) {
|
|
28
|
+
if (patch.start < 0 || patch.end < patch.start || patch.end > boundary)
|
|
29
|
+
fail();
|
|
30
|
+
result = result.slice(0, patch.start) + patch.text + result.slice(patch.end);
|
|
31
|
+
boundary = patch.start;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
/** Align equal array values first, including duplicates; pair edits between anchors. */
|
|
36
|
+
function alignSequence(original, edited) {
|
|
37
|
+
if ((original.length + 1) * (edited.length + 1) > 1000000)
|
|
38
|
+
fail();
|
|
39
|
+
const width = edited.length + 1;
|
|
40
|
+
const lengths = new Uint32Array((original.length + 1) * width);
|
|
41
|
+
for (let i = original.length - 1; i >= 0; i--) {
|
|
42
|
+
for (let j = edited.length - 1; j >= 0; j--) {
|
|
43
|
+
lengths[i * width + j] = equal(original[i], edited[j])
|
|
44
|
+
? 1 + lengths[(i + 1) * width + j + 1]
|
|
45
|
+
: Math.max(lengths[(i + 1) * width + j], lengths[i * width + j + 1]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const anchors = [];
|
|
49
|
+
let i = 0;
|
|
50
|
+
let j = 0;
|
|
51
|
+
while (i < original.length && j < edited.length) {
|
|
52
|
+
if (equal(original[i], edited[j])) {
|
|
53
|
+
anchors.push([i++, j++]);
|
|
54
|
+
}
|
|
55
|
+
else if (lengths[(i + 1) * width + j] >= lengths[i * width + j + 1])
|
|
56
|
+
i++;
|
|
57
|
+
else
|
|
58
|
+
j++;
|
|
59
|
+
}
|
|
60
|
+
anchors.push([original.length, edited.length]);
|
|
61
|
+
const result = [];
|
|
62
|
+
i = 0;
|
|
63
|
+
j = 0;
|
|
64
|
+
for (const [oldEnd, newEnd] of anchors) {
|
|
65
|
+
while (i < oldEnd && j < newEnd)
|
|
66
|
+
result.push({ index: i++, value: edited[j++] });
|
|
67
|
+
while (j < newEnd)
|
|
68
|
+
result.push({ value: edited[j++] });
|
|
69
|
+
i = oldEnd;
|
|
70
|
+
if (oldEnd < original.length) {
|
|
71
|
+
result.push({ index: i++, value: edited[j++] });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function sequence(original, edited) {
|
|
77
|
+
let prefix = 0;
|
|
78
|
+
while (prefix < original.length && prefix < edited.length && equal(original[prefix], edited[prefix]))
|
|
79
|
+
prefix++;
|
|
80
|
+
let oldEnd = original.length;
|
|
81
|
+
let newEnd = edited.length;
|
|
82
|
+
while (oldEnd > prefix && newEnd > prefix && equal(original[oldEnd - 1], edited[newEnd - 1])) {
|
|
83
|
+
oldEnd--;
|
|
84
|
+
newEnd--;
|
|
85
|
+
}
|
|
86
|
+
const result = original.slice(0, prefix).map((value, index) => ({ index, value }));
|
|
87
|
+
const middle = alignSequence(original.slice(prefix, oldEnd), edited.slice(prefix, newEnd));
|
|
88
|
+
for (const item of middle)
|
|
89
|
+
result.push('index' in item ? { ...item, index: item.index + prefix } : item);
|
|
90
|
+
for (let index = oldEnd; index < original.length; index++)
|
|
91
|
+
result.push({ index, value: original[index] });
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
/** JSON.parse remains the authority for accepted syntax; this only locates tokens. */
|
|
95
|
+
function jsonRanges(source) {
|
|
96
|
+
let offset = 0;
|
|
97
|
+
const whitespace = () => { var _a; while (/[\t\n\r ]/.test((_a = source[offset]) !== null && _a !== void 0 ? _a : '\0'))
|
|
98
|
+
offset++; };
|
|
99
|
+
const string = () => {
|
|
100
|
+
const start = offset++;
|
|
101
|
+
while (source[offset] !== '"') {
|
|
102
|
+
if (source[offset++] === '\\')
|
|
103
|
+
offset++;
|
|
104
|
+
}
|
|
105
|
+
offset++;
|
|
106
|
+
return JSON.parse(source.slice(start, offset));
|
|
107
|
+
};
|
|
108
|
+
const value = () => {
|
|
109
|
+
whitespace();
|
|
110
|
+
const start = offset;
|
|
111
|
+
const first = source[offset];
|
|
112
|
+
if (first === '{' || first === '[') {
|
|
113
|
+
offset++;
|
|
114
|
+
const entries = [];
|
|
115
|
+
const commas = [];
|
|
116
|
+
const close = first === '{' ? '}' : ']';
|
|
117
|
+
whitespace();
|
|
118
|
+
while (source[offset] !== close) {
|
|
119
|
+
const memberStart = offset;
|
|
120
|
+
let key;
|
|
121
|
+
if (first === '{') {
|
|
122
|
+
key = string();
|
|
123
|
+
whitespace();
|
|
124
|
+
offset++;
|
|
125
|
+
}
|
|
126
|
+
const child = value();
|
|
127
|
+
entries.push({ ...key === undefined ? {} : { key }, start: memberStart, end: child.end, node: child });
|
|
128
|
+
whitespace();
|
|
129
|
+
if (source[offset] !== ',')
|
|
130
|
+
break;
|
|
131
|
+
commas.push(offset++);
|
|
132
|
+
whitespace();
|
|
133
|
+
}
|
|
134
|
+
offset++;
|
|
135
|
+
return { start, end: offset, kind: first === '{' ? 'object' : 'array', entries, commas };
|
|
136
|
+
}
|
|
137
|
+
if (first === '"')
|
|
138
|
+
string();
|
|
139
|
+
else {
|
|
140
|
+
while (offset < source.length && !/[\t\n\r ,\]}]/.test(source[offset]))
|
|
141
|
+
offset++;
|
|
142
|
+
}
|
|
143
|
+
return { start, end: offset, kind: 'value' };
|
|
144
|
+
};
|
|
145
|
+
return value();
|
|
146
|
+
}
|
|
147
|
+
function delimited(entries, commas, end, items, patches, print, remove) {
|
|
148
|
+
const retained = new Set(items.flatMap(item => 'index' in item ? [item.index] : []));
|
|
149
|
+
for (let index = 0; index < entries.length; index++) {
|
|
150
|
+
if (!retained.has(index))
|
|
151
|
+
remove(entries[index].start, entries[index].end);
|
|
152
|
+
}
|
|
153
|
+
const indices = [...retained];
|
|
154
|
+
const keepCommas = new Set(indices.slice(0, -1).map(index => commas[index]));
|
|
155
|
+
for (const comma of commas)
|
|
156
|
+
if (!keepCommas.has(comma))
|
|
157
|
+
patches.push({ start: comma, end: comma + 1, text: '' });
|
|
158
|
+
let pending = [];
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
if (!('index' in item)) {
|
|
161
|
+
pending.push(print(item));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (pending.length) {
|
|
165
|
+
patches.push({ start: entries[item.index].start, end: entries[item.index].start, text: `${pending.join(',')},` });
|
|
166
|
+
pending = [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (pending.length)
|
|
170
|
+
patches.push({ start: end - 1, end: end - 1, text: `${retained.size ? ',' : ''}${pending.join(',')}` });
|
|
171
|
+
}
|
|
172
|
+
function patchJson(source, original, edited) {
|
|
173
|
+
const patches = [];
|
|
174
|
+
const visit = (node, before, after) => {
|
|
175
|
+
if (equal(before, after))
|
|
176
|
+
return;
|
|
177
|
+
if (record(before) && record(after) && node.kind === 'object') {
|
|
178
|
+
const entries = node.entries;
|
|
179
|
+
// JSON.parse exposes the last occurrence. Earlier duplicates stay byte-exact
|
|
180
|
+
// on updates; deleting a key removes every occurrence so none can reappear.
|
|
181
|
+
const last = new Map();
|
|
182
|
+
entries.forEach((entry, index) => last.set(entry.key, index));
|
|
183
|
+
const items = [];
|
|
184
|
+
for (let index = 0; index < entries.length; index++) {
|
|
185
|
+
const entry = entries[index];
|
|
186
|
+
if (Object.hasOwn(after, entry.key)) {
|
|
187
|
+
if (last.get(entry.key) === index)
|
|
188
|
+
visit(entry.node, before[entry.key], after[entry.key]);
|
|
189
|
+
items.push({ index, value: after[entry.key] });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const additions = Object.keys(after).filter(key => !Object.hasOwn(before, key));
|
|
193
|
+
if (items.length !== entries.length || additions.length) {
|
|
194
|
+
for (const key of additions)
|
|
195
|
+
items.push({ value: after[key] });
|
|
196
|
+
let addition = 0;
|
|
197
|
+
delimited(entries, node.commas, node.end, items, patches, item => `${JSON.stringify(additions[addition++])}:${serialize(item.value)}`, (start, end) => patches.push({ start, end, text: '' }));
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(before) && Array.isArray(after) && node.kind === 'array') {
|
|
202
|
+
const items = sequence(before, after);
|
|
203
|
+
for (const item of items)
|
|
204
|
+
if ('index' in item)
|
|
205
|
+
visit(node.entries[item.index].node, before[item.index], item.value);
|
|
206
|
+
if (items.length !== before.length || items.some((item, index) => !('index' in item) || item.index !== index)) {
|
|
207
|
+
delimited(node.entries, node.commas, node.end, items, patches, item => serialize(item.value), (start, end) => patches.push({ start, end, text: '' }));
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
patches.push({ start: node.start, end: node.end, text: serialize(after) });
|
|
212
|
+
};
|
|
213
|
+
visit(jsonRanges(source), original, edited);
|
|
214
|
+
return apply(source, patches);
|
|
215
|
+
}
|
|
216
|
+
function yamlRange(node) {
|
|
217
|
+
if (!(node === null || node === void 0 ? void 0 : node.range))
|
|
218
|
+
fail();
|
|
219
|
+
return node.range;
|
|
220
|
+
}
|
|
221
|
+
function checkYamlKeys(node) {
|
|
222
|
+
if (isMap(node)) {
|
|
223
|
+
const keys = new Set();
|
|
224
|
+
for (const pair of node.items) {
|
|
225
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== 'string' || keys.has(pair.key.value))
|
|
226
|
+
fail();
|
|
227
|
+
keys.add(pair.key.value);
|
|
228
|
+
checkYamlKeys(isNode(pair.value) ? pair.value : null);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (isSeq(node)) {
|
|
232
|
+
for (const item of node.items)
|
|
233
|
+
checkYamlKeys(isNode(item) ? item : null);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function yamlDocument(source) {
|
|
237
|
+
const document = parseDocument(source[0] === '\uFEFF' ? source.slice(1) : source, { keepSourceTokens: true, uniqueKeys: true, strict: true, version: '1.2', prettyErrors: false });
|
|
238
|
+
if (document.errors.length || document.warnings.length)
|
|
239
|
+
fail();
|
|
240
|
+
checkYamlKeys(document.contents);
|
|
241
|
+
return document;
|
|
242
|
+
}
|
|
243
|
+
function yamlValue(source) {
|
|
244
|
+
return snapshot(yamlDocument(source).toJS({ maxAliasCount: 100 }));
|
|
245
|
+
}
|
|
246
|
+
function patchYaml(source, original, edited) {
|
|
247
|
+
var _a;
|
|
248
|
+
// YAML 2.x mis-parses a BOM immediately followed by a block sequence. Keep
|
|
249
|
+
// the BOM outside the patch coordinate system and put its exact byte back.
|
|
250
|
+
if (source[0] === '\uFEFF')
|
|
251
|
+
return '\uFEFF' + patchYaml(source.slice(1), original, edited);
|
|
252
|
+
const document = yamlDocument(source);
|
|
253
|
+
const patches = [];
|
|
254
|
+
const comments = [];
|
|
255
|
+
const annotations = [];
|
|
256
|
+
// CST nodes are library-owned, never user-provided objects or iterators.
|
|
257
|
+
const tokens = (value) => {
|
|
258
|
+
if (value === null || typeof value !== 'object')
|
|
259
|
+
return;
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
for (const child of value)
|
|
262
|
+
tokens(child);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const token = value;
|
|
266
|
+
if (typeof token.offset === 'number' && typeof token.source === 'string') {
|
|
267
|
+
const span = [token.offset, token.offset + token.source.length];
|
|
268
|
+
if (token.type === 'comment')
|
|
269
|
+
comments.push(span);
|
|
270
|
+
if (token.type === 'anchor' || token.type === 'tag' || token.type === 'alias')
|
|
271
|
+
annotations.push(span);
|
|
272
|
+
}
|
|
273
|
+
for (const child of Object.values(value))
|
|
274
|
+
tokens(child);
|
|
275
|
+
};
|
|
276
|
+
tokens((_a = document.contents) === null || _a === void 0 ? void 0 : _a.srcToken);
|
|
277
|
+
const remove = (start, end) => {
|
|
278
|
+
if (comments.some(([a, b]) => a < end && b > start) || annotations.some(([a, b]) => a < end && b > start))
|
|
279
|
+
fail();
|
|
280
|
+
patches.push({ start, end, text: '' });
|
|
281
|
+
};
|
|
282
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
283
|
+
const trimEol = (end) => source[end - 1] === '\n' ? end - (source[end - 2] === '\r' ? 2 : 1) : end;
|
|
284
|
+
const lineStart = (offset) => source.lastIndexOf('\n', offset - 1) + 1;
|
|
285
|
+
const visit = (node, before, after) => {
|
|
286
|
+
var _a, _b;
|
|
287
|
+
if (equal(before, after))
|
|
288
|
+
return;
|
|
289
|
+
if (!node || isAlias(node) || ('anchor' in node && node.anchor) || node.tag)
|
|
290
|
+
fail();
|
|
291
|
+
const [start, end] = yamlRange(node);
|
|
292
|
+
if ((isMap(node) && record(before) && record(after)) || (isSeq(node) && Array.isArray(before) && Array.isArray(after))) {
|
|
293
|
+
const entries = [];
|
|
294
|
+
const items = [];
|
|
295
|
+
const additions = [];
|
|
296
|
+
if (isMap(node) && record(before) && record(after)) {
|
|
297
|
+
for (const pair of node.items) {
|
|
298
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== 'string')
|
|
299
|
+
fail();
|
|
300
|
+
const child = isNode(pair.value) ? pair.value : null;
|
|
301
|
+
entries.push({ start: yamlRange(pair.key)[0], end: child ? yamlRange(child)[1] : yamlRange(pair.key)[1], node: child, key: pair.key.value });
|
|
302
|
+
}
|
|
303
|
+
for (let index = 0; index < entries.length; index++) {
|
|
304
|
+
const entry = entries[index];
|
|
305
|
+
if (Object.hasOwn(after, entry.key)) {
|
|
306
|
+
visit(entry.node, before[entry.key], after[entry.key]);
|
|
307
|
+
items.push({ index, value: after[entry.key] });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const key of Object.keys(after))
|
|
311
|
+
if (!Object.hasOwn(before, key)) {
|
|
312
|
+
additions.push(key);
|
|
313
|
+
items.push({ value: after[key] });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (isSeq(node) && Array.isArray(before) && Array.isArray(after)) {
|
|
317
|
+
for (const child of node.items) {
|
|
318
|
+
if (!isNode(child))
|
|
319
|
+
fail();
|
|
320
|
+
const range = yamlRange(child);
|
|
321
|
+
entries.push({ start: range[0], end: range[1], node: child });
|
|
322
|
+
}
|
|
323
|
+
items.push(...sequence(before, after));
|
|
324
|
+
for (const item of items)
|
|
325
|
+
if ('index' in item)
|
|
326
|
+
visit(entries[item.index].node, before[item.index], item.value);
|
|
327
|
+
}
|
|
328
|
+
if (items.length === entries.length && items.every((item, index) => 'index' in item && item.index === index))
|
|
329
|
+
return;
|
|
330
|
+
let additionIndex = 0;
|
|
331
|
+
const print = (item) => isMap(node)
|
|
332
|
+
? `${JSON.stringify(additions[additionIndex++])}: ${serialize(item.value)}` : serialize(item.value);
|
|
333
|
+
const token = node.srcToken;
|
|
334
|
+
if (node.flow) {
|
|
335
|
+
if ((token === null || token === void 0 ? void 0 : token.type) !== 'flow-collection')
|
|
336
|
+
fail();
|
|
337
|
+
const commas = token.items.flatMap(item => item.start.filter(part => part.type === 'comma').map(part => part.offset));
|
|
338
|
+
commas.push(...token.end.filter(part => part.type === 'comma').map(part => part.offset));
|
|
339
|
+
if (commas.length < Math.max(0, entries.length - 1))
|
|
340
|
+
fail();
|
|
341
|
+
delimited(entries, commas, end, items, patches, print, remove);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
if ((token === null || token === void 0 ? void 0 : token.type) !== 'block-map' && (token === null || token === void 0 ? void 0 : token.type) !== 'block-seq')
|
|
345
|
+
fail();
|
|
346
|
+
const indent = ' '.repeat(token.indent);
|
|
347
|
+
if (token.type === 'block-seq') {
|
|
348
|
+
for (let index = 0; index < entries.length; index++) {
|
|
349
|
+
const marker = (_a = token.items[index]) === null || _a === void 0 ? void 0 : _a.start.find(part => part.type === 'seq-item-ind');
|
|
350
|
+
if (!marker)
|
|
351
|
+
fail();
|
|
352
|
+
entries[index].start = marker.offset;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const retained = new Set(items.flatMap(item => 'index' in item ? [item.index] : []));
|
|
356
|
+
for (let index = 0; index < entries.length; index++)
|
|
357
|
+
if (!retained.has(index))
|
|
358
|
+
remove(entries[index].start, trimEol(entries[index].end));
|
|
359
|
+
let pending = [];
|
|
360
|
+
for (const item of items) {
|
|
361
|
+
if (!('index' in item)) {
|
|
362
|
+
pending.push(`${indent}${isSeq(node) ? '- ' : ''}${print(item)}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (pending.length) {
|
|
366
|
+
const offset = lineStart(entries[item.index].start);
|
|
367
|
+
if (!/^ *$/.test(source.slice(offset, entries[item.index].start)))
|
|
368
|
+
fail();
|
|
369
|
+
patches.push({ start: offset, end: offset, text: pending.join(eol) + eol });
|
|
370
|
+
pending = [];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (pending.length) {
|
|
374
|
+
const atLineStart = end === 0 || source[end - 1] === '\n';
|
|
375
|
+
patches.push({ start: end, end, text: `${atLineStart ? '' : eol}${pending.join(eol)}${atLineStart ? eol : ''}` });
|
|
376
|
+
}
|
|
377
|
+
if (!items.length)
|
|
378
|
+
patches.push({ start, end: start, text: isMap(node) ? '{}' : '[]' });
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (annotations.some(([a, b]) => a < end && b > start) || comments.some(([a, b]) => a < end && b > start))
|
|
383
|
+
fail();
|
|
384
|
+
let text = serialize(after);
|
|
385
|
+
if (end > start && source[end - 1] === '\n')
|
|
386
|
+
text += source[end - 2] === '\r' ? '\r\n' : '\n';
|
|
387
|
+
if (start === end && /[:-]/.test((_b = source[start - 1]) !== null && _b !== void 0 ? _b : ''))
|
|
388
|
+
text = ` ${text}`;
|
|
389
|
+
if (start === end && source[start] === '#')
|
|
390
|
+
text += ' ';
|
|
391
|
+
patches.push({ start, end, text });
|
|
392
|
+
};
|
|
393
|
+
visit(document.contents, original, edited);
|
|
394
|
+
return apply(source, patches);
|
|
395
|
+
}
|
|
396
|
+
/** Patch only edited source ranges; unsupported lossless operations fail closed. */
|
|
397
|
+
export function patchSource(source, format, original, edited) {
|
|
398
|
+
if (typeof source !== 'string' || (format !== 'json' && format !== 'yaml'))
|
|
399
|
+
fail();
|
|
400
|
+
const before = snapshot(original);
|
|
401
|
+
const after = snapshot(edited);
|
|
402
|
+
// No-op (including edit-and-revert) must retain even an invalid raw document.
|
|
403
|
+
if (equal(before, after))
|
|
404
|
+
return source;
|
|
405
|
+
// Whitespace-only input is an explicit creation surface, not a parsed document.
|
|
406
|
+
if (/^[\t\r\n ]*$/.test(source))
|
|
407
|
+
return serialize(after);
|
|
408
|
+
const parse = format === 'json' ? (text) => snapshot(JSON.parse(text)) : yamlValue;
|
|
409
|
+
if (!equal(parse(source), before))
|
|
410
|
+
fail();
|
|
411
|
+
const result = format === 'json' ? patchJson(source, before, after) : patchYaml(source, before, after);
|
|
412
|
+
if (!equal(parse(result), after))
|
|
413
|
+
fail();
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CompiledSchema } from './schema-types.js';
|
|
2
|
+
import type { PreparedEditorDefinition } from './prepared-types.js';
|
|
3
|
+
import type { SourceBinding, SourceDocument, SourceEditorOptions, SourceVariant } from './source-types.js';
|
|
4
|
+
import type { JsonValue } from './types.js';
|
|
5
|
+
export interface CapturedBinding<C> extends SourceBinding<C> {
|
|
6
|
+
readonly validators: ReadonlyMap<string, CompiledSchema>;
|
|
7
|
+
readonly presentationVariants: readonly SourceVariant[];
|
|
8
|
+
}
|
|
9
|
+
export declare function createSourceCandidate(format: 'json' | 'yaml', data: JsonValue): string;
|
|
10
|
+
export declare function patchDecodedSource(document: SourceDocument, data: JsonValue, decode?: (source: string) => SourceDocument): string;
|
|
11
|
+
export declare function readSourceDocument<C>(binding: SourceBinding<C>, source: string): SourceDocument;
|
|
12
|
+
/** An additive path: legacy validate/createPatch deliberately refuse prepared definitions. */
|
|
13
|
+
export declare function createSourceEditor<C = null>(options: SourceEditorOptions<C>): PreparedEditorDefinition;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { compileSchemaBundle } from './schema-engine.js';
|
|
2
|
+
import { snapshotJson, isJsonRecord } from './schema-json.js';
|
|
3
|
+
import { stringify } from 'yaml';
|
|
4
|
+
import { sameData } from './codec.js';
|
|
5
|
+
import { patchSource } from './source-document.js';
|
|
6
|
+
import { prepareSourceVariant } from './source-schema-hints.js';
|
|
7
|
+
import { mountSourceView } from './source-view.js';
|
|
8
|
+
const keyFor = (format, variant) => JSON.stringify([format, variant]);
|
|
9
|
+
const issue = (code, path) => ({ message: `Source validation failed: ${code}.`, ...(path === undefined ? {} : { path }) });
|
|
10
|
+
function drain(value) {
|
|
11
|
+
try {
|
|
12
|
+
void Promise.prototype.then.call(value, () => undefined, () => undefined);
|
|
13
|
+
}
|
|
14
|
+
catch ( /* Synchronous callbacks never await arbitrary thenables. */_a) { /* Synchronous callbacks never await arbitrary thenables. */ }
|
|
15
|
+
}
|
|
16
|
+
export function createSourceCandidate(format, data) {
|
|
17
|
+
const value = snapshotJson(data);
|
|
18
|
+
return format === 'json' ? JSON.stringify(value, null, 2) : '---\n' + stringify(value, { version: '1.2' });
|
|
19
|
+
}
|
|
20
|
+
export function patchDecodedSource(document, data, decode) {
|
|
21
|
+
// Only an owner-accepted document reaches this path. Preserve outer whitespace
|
|
22
|
+
// accepted by that owner without broadening the strict standalone JSON codec.
|
|
23
|
+
if (document.format === 'json') {
|
|
24
|
+
const body = document.source.trim();
|
|
25
|
+
const start = document.source.length - document.source.trimStart().length;
|
|
26
|
+
return document.source.slice(0, start) + patchSource(body, 'json', document.data, data) + document.source.slice(start + body.length);
|
|
27
|
+
}
|
|
28
|
+
let candidate = patchSource(document.source, document.format, document.data, data);
|
|
29
|
+
if (decode && decode(candidate).format !== document.format) {
|
|
30
|
+
// Auto-detecting owners must still see YAML after a root collection becomes empty.
|
|
31
|
+
candidate = candidate.startsWith('\uFEFF') ? '\uFEFF---\n' + candidate.slice(1) : '---\n' + candidate;
|
|
32
|
+
}
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
export function readSourceDocument(binding, source) {
|
|
36
|
+
const output = binding.decode(source);
|
|
37
|
+
try {
|
|
38
|
+
const doc = snapshotJson(output);
|
|
39
|
+
if (!isJsonRecord(doc) || doc.source !== source || (doc.format !== 'json' && doc.format !== 'yaml') ||
|
|
40
|
+
typeof doc.variantId !== 'string' || !Object.hasOwn(doc, 'data'))
|
|
41
|
+
throw new TypeError();
|
|
42
|
+
return doc;
|
|
43
|
+
}
|
|
44
|
+
catch (_a) {
|
|
45
|
+
drain(output);
|
|
46
|
+
throw new TypeError('The source could not be decoded.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function entryFor(draft, argument) {
|
|
50
|
+
const entries = draft.sources;
|
|
51
|
+
const entry = isJsonRecord(entries) && Object.hasOwn(entries, argument) ? entries[argument] : undefined;
|
|
52
|
+
if (!isJsonRecord(entry) || typeof entry.source !== 'string' || (entry.mode !== 'source' && entry.mode !== 'form')) {
|
|
53
|
+
throw new TypeError('Invalid source draft.');
|
|
54
|
+
}
|
|
55
|
+
return entry;
|
|
56
|
+
}
|
|
57
|
+
/** An additive path: legacy validate/createPatch deliberately refuse prepared definitions. */
|
|
58
|
+
export function createSourceEditor(options) {
|
|
59
|
+
var _a;
|
|
60
|
+
const { apiVersion, extensionId, editorId, validationContext: provider } = options;
|
|
61
|
+
if (apiVersion !== 1 || typeof provider !== 'function' || !Array.isArray(options.bindings) || !options.bindings.length) {
|
|
62
|
+
throw new TypeError('Invalid source editor options.');
|
|
63
|
+
}
|
|
64
|
+
const inputBindings = options.bindings;
|
|
65
|
+
const bindings = [];
|
|
66
|
+
const names = new Set();
|
|
67
|
+
for (let i = 0; i < inputBindings.length; i++) {
|
|
68
|
+
const binding = inputBindings[i];
|
|
69
|
+
const { argument, label, decode, validateSource } = binding;
|
|
70
|
+
if (typeof argument !== 'string' || !argument || names.has(argument) || typeof decode !== 'function' ||
|
|
71
|
+
typeof validateSource !== 'function' || (label !== undefined && typeof label !== 'string'))
|
|
72
|
+
throw new TypeError('Invalid source binding.');
|
|
73
|
+
names.add(argument);
|
|
74
|
+
const variants = snapshotJson(binding.variants);
|
|
75
|
+
if (!Array.isArray(variants) || !variants.length)
|
|
76
|
+
throw new TypeError('Source variants are required.');
|
|
77
|
+
const sourceOnlyVariants = snapshotJson((_a = binding.sourceOnlyVariants) !== null && _a !== void 0 ? _a : []);
|
|
78
|
+
if (!Array.isArray(sourceOnlyVariants) || sourceOnlyVariants.some(v => !v || typeof v.variantId !== 'string' || (v.format !== 'json' && v.format !== 'yaml')))
|
|
79
|
+
throw new TypeError('Invalid source-only variants.');
|
|
80
|
+
const validators = new Map();
|
|
81
|
+
for (const variant of variants) {
|
|
82
|
+
if (!variant || typeof variant.variantId !== 'string' || (variant.format !== 'json' && variant.format !== 'yaml'))
|
|
83
|
+
throw new TypeError('Invalid source variant.');
|
|
84
|
+
const key = keyFor(variant.format, variant.variantId);
|
|
85
|
+
if (validators.has(key))
|
|
86
|
+
throw new TypeError('Duplicate source variant.');
|
|
87
|
+
const compiled = compileSchemaBundle(variant.schemaBundle);
|
|
88
|
+
if (compiled.kind !== 'compiled')
|
|
89
|
+
throw new TypeError(`Invalid source schema: ${compiled.diagnostics[0].code}.`);
|
|
90
|
+
validators.set(key, compiled.validator);
|
|
91
|
+
}
|
|
92
|
+
bindings.push(Object.freeze({ argument, ...(label === undefined ? {} : { label }), variants, decode, validateSource, sourceOnlyVariants, validators, presentationVariants: variants.map(prepareSourceVariant) }));
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
apiVersion, extensionId, editorId, opcodes: Object.freeze([...options.opcodes]),
|
|
96
|
+
editableArguments: Object.freeze([...names]), saveContract: 'prepared-source-v1',
|
|
97
|
+
read: snapshot => {
|
|
98
|
+
const sources = {};
|
|
99
|
+
for (const binding of bindings) {
|
|
100
|
+
const slot = snapshot.arguments[binding.argument];
|
|
101
|
+
if ((slot === null || slot === void 0 ? void 0 : slot.kind) !== 'literal' || typeof slot.value !== 'string')
|
|
102
|
+
throw new TypeError('Source editors require literal string arguments.');
|
|
103
|
+
let entry = { mode: 'source', source: slot.value };
|
|
104
|
+
try {
|
|
105
|
+
const doc = readSourceDocument(binding, slot.value);
|
|
106
|
+
if (binding.validators.has(keyFor(doc.format, doc.variantId)))
|
|
107
|
+
entry = { mode: 'form', source: slot.value, data: doc.data };
|
|
108
|
+
}
|
|
109
|
+
catch ( /* Invalid raw data stays available for explicit repair in Source. */_a) { /* Invalid raw data stays available for explicit repair in Source. */ }
|
|
110
|
+
Object.defineProperty(sources, binding.argument, { value: entry, enumerable: true });
|
|
111
|
+
}
|
|
112
|
+
return { sources };
|
|
113
|
+
},
|
|
114
|
+
validate: () => [issue('prepared-host-required')],
|
|
115
|
+
createPatch: () => { throw new TypeError('Prepared source editors require a compatible host.'); },
|
|
116
|
+
prepareSave: (draft, snapshot) => {
|
|
117
|
+
var _a;
|
|
118
|
+
const issues = [];
|
|
119
|
+
const changes = [];
|
|
120
|
+
let context;
|
|
121
|
+
let raw;
|
|
122
|
+
try {
|
|
123
|
+
raw = provider();
|
|
124
|
+
context = snapshotJson(raw);
|
|
125
|
+
if (!isJsonRecord(context) || typeof context.consumerId !== 'string' || !context.consumerId ||
|
|
126
|
+
typeof context.nowMs !== 'number' || !Number.isFinite(context.nowMs) || !Object.hasOwn(context, 'settings'))
|
|
127
|
+
throw new TypeError();
|
|
128
|
+
}
|
|
129
|
+
catch (_b) {
|
|
130
|
+
drain(raw);
|
|
131
|
+
return { kind: 'invalid', issues: [issue('owner-context-invalid')] };
|
|
132
|
+
}
|
|
133
|
+
// Freeze the fresh context so one binding cannot alter validation of another binding.
|
|
134
|
+
const freeze = (value) => {
|
|
135
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
136
|
+
Object.values(value).forEach(freeze);
|
|
137
|
+
Object.freeze(value);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
freeze(context);
|
|
141
|
+
for (const binding of bindings) {
|
|
142
|
+
const path = `/${binding.argument.replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
143
|
+
try {
|
|
144
|
+
const entry = entryFor(draft, binding.argument);
|
|
145
|
+
if (entry.invalidForm === true) {
|
|
146
|
+
issues.push(issue('form-value-invalid', path));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const slot = snapshot.arguments[binding.argument];
|
|
150
|
+
if ((slot === null || slot === void 0 ? void 0 : slot.kind) !== 'literal' || typeof slot.value !== 'string')
|
|
151
|
+
throw new TypeError();
|
|
152
|
+
let candidate = entry.source;
|
|
153
|
+
if (entry.mode === 'form') {
|
|
154
|
+
if (!Object.hasOwn(entry, 'data'))
|
|
155
|
+
throw new TypeError();
|
|
156
|
+
const data = snapshotJson(entry.data);
|
|
157
|
+
if (isJsonRecord(entry.creating)) {
|
|
158
|
+
const { format, variantId } = entry.creating;
|
|
159
|
+
if ((format !== 'json' && format !== 'yaml') || typeof variantId !== 'string' || !binding.validators.has(keyFor(format, variantId)))
|
|
160
|
+
throw new TypeError();
|
|
161
|
+
candidate = createSourceCandidate(format, data);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const original = readSourceDocument(binding, candidate);
|
|
165
|
+
candidate = sameData(original.data, data) ? candidate : patchDecodedSource(original, data, source => readSourceDocument(binding, source));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (entry.sourceEdited !== true && candidate !== slot.value) {
|
|
169
|
+
try {
|
|
170
|
+
const opened = readSourceDocument(binding, slot.value);
|
|
171
|
+
const final = readSourceDocument(binding, candidate);
|
|
172
|
+
if (opened.format === final.format && opened.variantId === final.variantId && sameData(opened.data, final.data))
|
|
173
|
+
candidate = slot.value;
|
|
174
|
+
}
|
|
175
|
+
catch ( /* A repaired original source is compared only when both versions decode. */_c) { /* A repaired original source is compared only when both versions decode. */ }
|
|
176
|
+
}
|
|
177
|
+
if (entry.mode === 'form' && !isJsonRecord(entry.creating)) {
|
|
178
|
+
const original = readSourceDocument(binding, entry.source);
|
|
179
|
+
const final = readSourceDocument(binding, candidate);
|
|
180
|
+
if (original.format !== final.format || original.variantId !== final.variantId) {
|
|
181
|
+
issues.push(issue('variant-change-requires-source', path));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Source-bound owner validation sees exactly the string committed below, once.
|
|
186
|
+
const rawResult = binding.validateSource(candidate, context);
|
|
187
|
+
let validation;
|
|
188
|
+
try {
|
|
189
|
+
validation = snapshotJson(rawResult);
|
|
190
|
+
}
|
|
191
|
+
catch (_d) {
|
|
192
|
+
drain(rawResult);
|
|
193
|
+
throw new TypeError();
|
|
194
|
+
}
|
|
195
|
+
if (!isJsonRecord(validation) || validation.source !== candidate || !isJsonRecord(validation.result) ||
|
|
196
|
+
!Array.isArray(validation.result.diagnostics))
|
|
197
|
+
throw new TypeError();
|
|
198
|
+
if (validation.result.kind !== 'valid') {
|
|
199
|
+
if (!['invalid', 'execution-error', 'schema-error'].includes(validation.result.kind))
|
|
200
|
+
throw new TypeError();
|
|
201
|
+
issues.push(issue('owner-input-invalid', path));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (validation.result.diagnostics.length)
|
|
205
|
+
throw new TypeError();
|
|
206
|
+
// Empty/no-op variants can be accepted by the owner without inventing a form variant.
|
|
207
|
+
const document = readSourceDocument(binding, candidate);
|
|
208
|
+
if (isJsonRecord(entry.creating) && (document.format !== entry.creating.format || document.variantId !== entry.creating.variantId)) {
|
|
209
|
+
issues.push(issue('variant-unrecognized', path));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const validator = binding.validators.get(keyFor(document.format, document.variantId));
|
|
213
|
+
if (validator) {
|
|
214
|
+
const result = validator.validate(document.data, { consumerId: context.consumerId });
|
|
215
|
+
if (result.kind !== 'valid') {
|
|
216
|
+
issues.push(issue('schema-data-invalid', path));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (entry.mode === 'form' || (candidate !== slot.value && !((_a = binding.sourceOnlyVariants) === null || _a === void 0 ? void 0 : _a.some(v => v.variantId === document.variantId && v.format === document.format)))) {
|
|
221
|
+
issues.push(issue('variant-unrecognized', path));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (candidate !== slot.value)
|
|
225
|
+
changes.push(Object.freeze({ argument: binding.argument, value: candidate }));
|
|
226
|
+
}
|
|
227
|
+
catch (_e) {
|
|
228
|
+
issues.push(issue('source-invalid', path));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return issues.length ? { kind: 'invalid', issues } : { kind: 'ready', changes };
|
|
232
|
+
},
|
|
233
|
+
mount: (container, context) => mountSourceView(container, context, bindings)
|
|
234
|
+
});
|
|
235
|
+
}
|