@rogatio/cli 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/dist/editor/fonts/OFL-HankenGrotesk.txt +94 -0
- package/dist/editor/fonts/OFL-JetBrainsMono.txt +93 -0
- package/dist/editor/fonts/hanken-grotesk-400.woff2 +0 -0
- package/dist/editor/fonts/hanken-grotesk-500.woff2 +0 -0
- package/dist/editor/fonts/hanken-grotesk-700.woff2 +0 -0
- package/dist/editor/fonts/jetbrains-mono-400.woff2 +0 -0
- package/dist/editor/fonts/jetbrains-mono-700.woff2 +0 -0
- package/dist/editor/index.css +565 -0
- package/dist/editor/index.js +3822 -0
- package/dist/node/index.js +1642 -142
- package/package.json +5 -4
|
@@ -0,0 +1,3822 @@
|
|
|
1
|
+
// packages/schema/dist/browser/index.js
|
|
2
|
+
var LIMITS = Object.freeze({
|
|
3
|
+
maxGroups: 64,
|
|
4
|
+
maxRulesPerGroup: 256,
|
|
5
|
+
maxRulesPerProject: 4096,
|
|
6
|
+
maxOriginsPerScope: 32,
|
|
7
|
+
maxIdLength: 64,
|
|
8
|
+
maxLabelLength: 100,
|
|
9
|
+
maxDescriptionLength: 1e3,
|
|
10
|
+
maxUrlRegexLength: 2048,
|
|
11
|
+
maxResourceTypesPerRule: 16,
|
|
12
|
+
minPriority: 1,
|
|
13
|
+
maxPriority: 1e3,
|
|
14
|
+
maxRedirectDestinationLength: 2048,
|
|
15
|
+
maxCaptureGroups: 9,
|
|
16
|
+
maxQueryParamsPerRule: 64,
|
|
17
|
+
maxQueryNameLength: 256,
|
|
18
|
+
maxQueryValueLength: 2048,
|
|
19
|
+
maxHeaderNameLength: 256,
|
|
20
|
+
maxHeaderValueLength: 4096,
|
|
21
|
+
minMockStatus: 200,
|
|
22
|
+
maxMockStatus: 599,
|
|
23
|
+
maxMockHeadersPerRule: 32,
|
|
24
|
+
maxMockHeaderNameLength: 256,
|
|
25
|
+
maxMockHeaderValueLength: 4096,
|
|
26
|
+
maxMockInlineBodyLength: 65536,
|
|
27
|
+
maxMockDelayMs: 3e4,
|
|
28
|
+
maxMockFilePathLength: 2048,
|
|
29
|
+
maxResponseBodyReplacements: 64,
|
|
30
|
+
maxResponseBodyPatternLength: 2048,
|
|
31
|
+
maxResponseBodyReplacementLength: 4096,
|
|
32
|
+
maxRequestBodyBytes: 4 * 1024 * 1024,
|
|
33
|
+
maxRequestBodyPatternLength: 2048,
|
|
34
|
+
maxRequestBodyReplacementLength: 4096,
|
|
35
|
+
maxRequestBodyOperations: 32,
|
|
36
|
+
maxLocalOrigins: 32
|
|
37
|
+
});
|
|
38
|
+
function countCapturingGroups(urlRegex) {
|
|
39
|
+
let count = 0;
|
|
40
|
+
let index = 0;
|
|
41
|
+
const length = urlRegex.length;
|
|
42
|
+
while (index < length) {
|
|
43
|
+
const char = urlRegex[index];
|
|
44
|
+
if (char === "\\") {
|
|
45
|
+
index += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === "(") {
|
|
49
|
+
if (urlRegex[index + 1] === "?") {
|
|
50
|
+
index = skipBalancedGroup(urlRegex, index);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
count += 1;
|
|
54
|
+
}
|
|
55
|
+
index += 1;
|
|
56
|
+
}
|
|
57
|
+
return count;
|
|
58
|
+
}
|
|
59
|
+
function skipBalancedGroup(source, start) {
|
|
60
|
+
let depth = 0;
|
|
61
|
+
let index = start;
|
|
62
|
+
const length = source.length;
|
|
63
|
+
while (index < length) {
|
|
64
|
+
const char = source[index];
|
|
65
|
+
if (char === "\\") {
|
|
66
|
+
index += 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (char === "(") {
|
|
70
|
+
depth += 1;
|
|
71
|
+
} else if (char === ")") {
|
|
72
|
+
depth -= 1;
|
|
73
|
+
if (depth === 0) return index;
|
|
74
|
+
}
|
|
75
|
+
index += 1;
|
|
76
|
+
}
|
|
77
|
+
return length;
|
|
78
|
+
}
|
|
79
|
+
function validateRedirectDestination(destination, urlRegex) {
|
|
80
|
+
const issues = [];
|
|
81
|
+
if (typeof destination !== "string" || destination.length === 0) {
|
|
82
|
+
issues.push({
|
|
83
|
+
code: "schema.required",
|
|
84
|
+
message: "Redirect destination must be a non-empty string."
|
|
85
|
+
});
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
if (destination.length > LIMITS.maxRedirectDestinationLength) {
|
|
89
|
+
issues.push({
|
|
90
|
+
code: "schema.out-of-range",
|
|
91
|
+
message: `Redirect destination must be at most ${LIMITS.maxRedirectDestinationLength} characters.`
|
|
92
|
+
});
|
|
93
|
+
return issues;
|
|
94
|
+
}
|
|
95
|
+
let url;
|
|
96
|
+
try {
|
|
97
|
+
url = new URL(destination);
|
|
98
|
+
} catch {
|
|
99
|
+
issues.push({
|
|
100
|
+
code: "schema.invalid-format",
|
|
101
|
+
message: "Redirect destination must be an absolute URL."
|
|
102
|
+
});
|
|
103
|
+
return issues;
|
|
104
|
+
}
|
|
105
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
106
|
+
issues.push({
|
|
107
|
+
code: "schema.invalid-value",
|
|
108
|
+
message: "Redirect destination must use the http or https scheme."
|
|
109
|
+
});
|
|
110
|
+
return issues;
|
|
111
|
+
}
|
|
112
|
+
if (url.username.length > 0 || url.password.length > 0) {
|
|
113
|
+
issues.push({
|
|
114
|
+
code: "schema.invalid-value",
|
|
115
|
+
message: "Redirect destination must not contain credentials."
|
|
116
|
+
});
|
|
117
|
+
return issues;
|
|
118
|
+
}
|
|
119
|
+
if (url.hostname.length === 0 || url.hostname.includes("*")) {
|
|
120
|
+
issues.push({
|
|
121
|
+
code: "schema.invalid-format",
|
|
122
|
+
message: "Redirect destination must have a valid host."
|
|
123
|
+
});
|
|
124
|
+
return issues;
|
|
125
|
+
}
|
|
126
|
+
const groups = countCapturingGroups(urlRegex);
|
|
127
|
+
const backreference = /\\([1-9])/g;
|
|
128
|
+
let match = backreference.exec(destination);
|
|
129
|
+
while (match !== null) {
|
|
130
|
+
const referenced = Number(match[1]);
|
|
131
|
+
if (referenced > groups) {
|
|
132
|
+
issues.push({
|
|
133
|
+
code: "schema.invalid-value",
|
|
134
|
+
message: `Redirect destination references capture group ${referenced} but the URL pattern defines ${groups}.`
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
match = backreference.exec(destination);
|
|
138
|
+
}
|
|
139
|
+
return issues;
|
|
140
|
+
}
|
|
141
|
+
function hasControl(value) {
|
|
142
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
143
|
+
const code = value.charCodeAt(index);
|
|
144
|
+
if (code <= 31 || code === 127) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
var FORBIDDEN_REQUEST_HEADERS = Object.freeze([
|
|
149
|
+
"accept-charset",
|
|
150
|
+
"accept-encoding",
|
|
151
|
+
"access-control-request-headers",
|
|
152
|
+
"access-control-request-method",
|
|
153
|
+
"connection",
|
|
154
|
+
"content-length",
|
|
155
|
+
"cookie",
|
|
156
|
+
"cookie2",
|
|
157
|
+
"date",
|
|
158
|
+
"dnt",
|
|
159
|
+
"expect",
|
|
160
|
+
"host",
|
|
161
|
+
"keep-alive",
|
|
162
|
+
"origin",
|
|
163
|
+
"proxy-authenticate",
|
|
164
|
+
"proxy-authorization",
|
|
165
|
+
"te",
|
|
166
|
+
"trailer",
|
|
167
|
+
"transfer-encoding",
|
|
168
|
+
"upgrade",
|
|
169
|
+
"via"
|
|
170
|
+
]);
|
|
171
|
+
var FORBIDDEN_RESPONSE_HEADERS = Object.freeze([
|
|
172
|
+
"connection",
|
|
173
|
+
"content-encoding",
|
|
174
|
+
"content-length",
|
|
175
|
+
"date",
|
|
176
|
+
"keep-alive",
|
|
177
|
+
"proxy-authenticate",
|
|
178
|
+
"proxy-authorization",
|
|
179
|
+
"set-cookie",
|
|
180
|
+
"set-cookie2",
|
|
181
|
+
"te",
|
|
182
|
+
"trailer",
|
|
183
|
+
"transfer-encoding",
|
|
184
|
+
"upgrade",
|
|
185
|
+
"via"
|
|
186
|
+
]);
|
|
187
|
+
var FORBIDDEN_REQUEST_PREFIXES = Object.freeze(["proxy-", "sec-"]);
|
|
188
|
+
var RESOURCE_TYPES = Object.freeze([
|
|
189
|
+
"main_frame",
|
|
190
|
+
"sub_frame",
|
|
191
|
+
"stylesheet",
|
|
192
|
+
"script",
|
|
193
|
+
"image",
|
|
194
|
+
"font",
|
|
195
|
+
"object",
|
|
196
|
+
"media",
|
|
197
|
+
"xmlhttprequest",
|
|
198
|
+
"ping",
|
|
199
|
+
"csp_report",
|
|
200
|
+
"websocket",
|
|
201
|
+
"webtransport",
|
|
202
|
+
"webbundle",
|
|
203
|
+
"other"
|
|
204
|
+
]);
|
|
205
|
+
var HTTP_METHODS = Object.freeze([
|
|
206
|
+
"GET",
|
|
207
|
+
"POST",
|
|
208
|
+
"PUT",
|
|
209
|
+
"PATCH",
|
|
210
|
+
"DELETE",
|
|
211
|
+
"HEAD",
|
|
212
|
+
"OPTIONS",
|
|
213
|
+
"CONNECT",
|
|
214
|
+
"TRACE"
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
// packages/editor/src/rule-types/mock.ts
|
|
218
|
+
function isRecord(value) {
|
|
219
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
220
|
+
}
|
|
221
|
+
function asMockAction(value) {
|
|
222
|
+
if (!isRecord(value) || typeof value.status !== "number") return void 0;
|
|
223
|
+
const action = value;
|
|
224
|
+
if (action.headers !== void 0 && !Array.isArray(action.headers)) {
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
return action;
|
|
228
|
+
}
|
|
229
|
+
function stable(diagnostics) {
|
|
230
|
+
return [...diagnostics].sort(
|
|
231
|
+
(a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
function hasControlOrColon(value) {
|
|
235
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
236
|
+
const code = value.charCodeAt(index);
|
|
237
|
+
if (code <= 31 || code === 127 || code === 58) return true;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function createMockRuleType() {
|
|
242
|
+
return {
|
|
243
|
+
id: "mock",
|
|
244
|
+
label: "Mock response",
|
|
245
|
+
actionField: "mock",
|
|
246
|
+
matches(rule) {
|
|
247
|
+
const type = rule.type;
|
|
248
|
+
if (type === "mock") return true;
|
|
249
|
+
if (type !== void 0) return false;
|
|
250
|
+
return asMockAction(rule.mock) !== void 0;
|
|
251
|
+
},
|
|
252
|
+
validate(rule, rulePath) {
|
|
253
|
+
const mock = asMockAction(rule.mock);
|
|
254
|
+
if (mock === void 0) return [];
|
|
255
|
+
const diagnostics = [];
|
|
256
|
+
const status = mock.status;
|
|
257
|
+
if (!Number.isInteger(status) || status < LIMITS.minMockStatus || status > LIMITS.maxMockStatus) {
|
|
258
|
+
diagnostics.push({
|
|
259
|
+
code: "editor.mock-status-range",
|
|
260
|
+
severity: "error",
|
|
261
|
+
path: `${rulePath}/mock/status`,
|
|
262
|
+
message: `Mock status must be an integer between ${LIMITS.minMockStatus} and ${LIMITS.maxMockStatus}.`
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const hasBody = typeof mock.body === "string";
|
|
266
|
+
const hasFile = typeof mock.file === "string";
|
|
267
|
+
if (hasBody === hasFile) {
|
|
268
|
+
diagnostics.push({
|
|
269
|
+
code: "editor.mock-body-source",
|
|
270
|
+
severity: "error",
|
|
271
|
+
path: `${rulePath}/mock`,
|
|
272
|
+
message: "A mock rule must set exactly one of body or file."
|
|
273
|
+
});
|
|
274
|
+
} else if (hasBody) {
|
|
275
|
+
const body = mock.body;
|
|
276
|
+
if (body.length > LIMITS.maxMockInlineBodyLength) {
|
|
277
|
+
diagnostics.push({
|
|
278
|
+
code: "editor.mock-body-too-long",
|
|
279
|
+
severity: "error",
|
|
280
|
+
path: `${rulePath}/mock/body`,
|
|
281
|
+
message: `Mock inline body must be at most ${LIMITS.maxMockInlineBodyLength} characters.`
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
} else if (hasFile) {
|
|
285
|
+
const file = mock.file;
|
|
286
|
+
if (file.length === 0 || file.length > LIMITS.maxMockFilePathLength) {
|
|
287
|
+
diagnostics.push({
|
|
288
|
+
code: "editor.mock-file-path",
|
|
289
|
+
severity: "error",
|
|
290
|
+
path: `${rulePath}/mock/file`,
|
|
291
|
+
message: `Mock file path must be 1-${LIMITS.maxMockFilePathLength} characters.`
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (mock.delayMs !== void 0 && (!Number.isInteger(mock.delayMs) || mock.delayMs < 0 || mock.delayMs > LIMITS.maxMockDelayMs)) {
|
|
296
|
+
diagnostics.push({
|
|
297
|
+
code: "editor.mock-delay-range",
|
|
298
|
+
severity: "error",
|
|
299
|
+
path: `${rulePath}/mock/delayMs`,
|
|
300
|
+
message: `Mock delay must be an integer between 0 and ${LIMITS.maxMockDelayMs} milliseconds.`
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (mock.headers !== void 0) {
|
|
304
|
+
if (mock.headers.length > LIMITS.maxMockHeadersPerRule) {
|
|
305
|
+
diagnostics.push({
|
|
306
|
+
code: "editor.mock-too-many-headers",
|
|
307
|
+
severity: "error",
|
|
308
|
+
path: `${rulePath}/mock/headers`,
|
|
309
|
+
message: `A mock rule may define at most ${LIMITS.maxMockHeadersPerRule} headers.`
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
mock.headers.forEach((header, index) => {
|
|
313
|
+
const namePath = `${rulePath}/mock/headers/${index}/name`;
|
|
314
|
+
const valuePath = `${rulePath}/mock/headers/${index}/value`;
|
|
315
|
+
if (typeof header?.name !== "string" || header.name.length === 0 || header.name.length > LIMITS.maxMockHeaderNameLength) {
|
|
316
|
+
diagnostics.push({
|
|
317
|
+
code: "editor.mock-header-name",
|
|
318
|
+
severity: "error",
|
|
319
|
+
path: namePath,
|
|
320
|
+
message: `Mock header name must be 1-${LIMITS.maxMockHeaderNameLength} characters.`
|
|
321
|
+
});
|
|
322
|
+
} else if (hasControlOrColon(header.name)) {
|
|
323
|
+
diagnostics.push({
|
|
324
|
+
code: "editor.mock-header-name",
|
|
325
|
+
severity: "error",
|
|
326
|
+
path: namePath,
|
|
327
|
+
message: "Mock header name must not contain control characters or a colon."
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (typeof header?.value !== "string" || header.value.length > LIMITS.maxMockHeaderValueLength) {
|
|
331
|
+
diagnostics.push({
|
|
332
|
+
code: "editor.mock-header-value",
|
|
333
|
+
severity: "error",
|
|
334
|
+
path: valuePath,
|
|
335
|
+
message: `Mock header value must be at most ${LIMITS.maxMockHeaderValueLength} characters.`
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return stable(diagnostics);
|
|
341
|
+
},
|
|
342
|
+
mount(context) {
|
|
343
|
+
const { document, container } = context;
|
|
344
|
+
const getCurrent = () => asMockAction(context.getField("mock"));
|
|
345
|
+
const setAction = (next) => {
|
|
346
|
+
context.setField("mock", next);
|
|
347
|
+
};
|
|
348
|
+
const render = () => {
|
|
349
|
+
const current = getCurrent();
|
|
350
|
+
container.replaceChildren();
|
|
351
|
+
if (current === void 0) return;
|
|
352
|
+
const statusLabel = document.createElement("label");
|
|
353
|
+
statusLabel.textContent = "Status";
|
|
354
|
+
const statusInput = document.createElement("input");
|
|
355
|
+
statusInput.type = "number";
|
|
356
|
+
statusInput.min = String(LIMITS.minMockStatus);
|
|
357
|
+
statusInput.max = String(LIMITS.maxMockStatus);
|
|
358
|
+
statusInput.step = "1";
|
|
359
|
+
statusInput.value = String(current.status);
|
|
360
|
+
statusInput.dataset.editorField = "true";
|
|
361
|
+
statusInput.addEventListener("input", () => {
|
|
362
|
+
const c = getCurrent();
|
|
363
|
+
if (c === void 0) return;
|
|
364
|
+
setAction({ ...c, status: Number(statusInput.value) });
|
|
365
|
+
});
|
|
366
|
+
context.registerControl("/mock/status", statusInput);
|
|
367
|
+
statusLabel.append(statusInput);
|
|
368
|
+
const delayLabel = document.createElement("label");
|
|
369
|
+
delayLabel.textContent = "Delay (ms)";
|
|
370
|
+
const delayInput = document.createElement("input");
|
|
371
|
+
delayInput.type = "number";
|
|
372
|
+
delayInput.min = "0";
|
|
373
|
+
delayInput.max = String(LIMITS.maxMockDelayMs);
|
|
374
|
+
delayInput.step = "1";
|
|
375
|
+
delayInput.value = current.delayMs === void 0 ? "" : String(current.delayMs);
|
|
376
|
+
delayInput.dataset.editorField = "true";
|
|
377
|
+
delayInput.addEventListener("input", () => {
|
|
378
|
+
const c = getCurrent();
|
|
379
|
+
if (c === void 0) return;
|
|
380
|
+
const raw = delayInput.value;
|
|
381
|
+
const next = raw === "" ? { ...c, delayMs: void 0 } : { ...c, delayMs: Number(raw) };
|
|
382
|
+
setAction(next);
|
|
383
|
+
});
|
|
384
|
+
context.registerControl("/mock/delayMs", delayInput);
|
|
385
|
+
delayLabel.append(delayInput);
|
|
386
|
+
const sourceLabel = document.createElement("label");
|
|
387
|
+
sourceLabel.textContent = "Body source";
|
|
388
|
+
const sourceSelect = document.createElement("select");
|
|
389
|
+
sourceSelect.dataset.editorField = "true";
|
|
390
|
+
const bodyOption = document.createElement("option");
|
|
391
|
+
bodyOption.value = "body";
|
|
392
|
+
bodyOption.textContent = "Inline body";
|
|
393
|
+
const fileOption = document.createElement("option");
|
|
394
|
+
fileOption.value = "file";
|
|
395
|
+
fileOption.textContent = "File snapshot";
|
|
396
|
+
sourceSelect.append(bodyOption, fileOption);
|
|
397
|
+
sourceSelect.value = typeof current.file === "string" ? "file" : "body";
|
|
398
|
+
context.registerControl("/mock/body-source", sourceSelect);
|
|
399
|
+
const bodyArea = document.createElement("textarea");
|
|
400
|
+
bodyArea.value = typeof current.body === "string" ? current.body : "";
|
|
401
|
+
bodyArea.dataset.editorField = "true";
|
|
402
|
+
bodyArea.addEventListener("input", () => {
|
|
403
|
+
const c = getCurrent();
|
|
404
|
+
if (c === void 0) return;
|
|
405
|
+
setAction({ ...c, body: bodyArea.value, file: void 0 });
|
|
406
|
+
});
|
|
407
|
+
context.registerControl("/mock/body", bodyArea);
|
|
408
|
+
const fileLabel = document.createElement("label");
|
|
409
|
+
fileLabel.textContent = "File path";
|
|
410
|
+
const fileInput = document.createElement("input");
|
|
411
|
+
fileInput.type = "text";
|
|
412
|
+
fileInput.value = typeof current.file === "string" ? current.file : "";
|
|
413
|
+
fileInput.dataset.editorField = "true";
|
|
414
|
+
fileInput.addEventListener("input", () => {
|
|
415
|
+
const c = getCurrent();
|
|
416
|
+
if (c === void 0) return;
|
|
417
|
+
setAction({ ...c, body: void 0, file: fileInput.value });
|
|
418
|
+
});
|
|
419
|
+
context.registerControl("/mock/file", fileInput);
|
|
420
|
+
fileLabel.append(fileInput);
|
|
421
|
+
const syncSource = () => {
|
|
422
|
+
const useFile = sourceSelect.value === "file";
|
|
423
|
+
bodyArea.style.display = useFile ? "none" : "";
|
|
424
|
+
fileLabel.style.display = useFile ? "" : "none";
|
|
425
|
+
};
|
|
426
|
+
sourceSelect.addEventListener("change", syncSource);
|
|
427
|
+
syncSource();
|
|
428
|
+
const headersFieldset = document.createElement("fieldset");
|
|
429
|
+
const headersLegend = document.createElement("legend");
|
|
430
|
+
headersLegend.textContent = "Response headers";
|
|
431
|
+
headersFieldset.append(headersLegend);
|
|
432
|
+
const renderHeaders = () => {
|
|
433
|
+
const c = getCurrent();
|
|
434
|
+
if (c === void 0) return;
|
|
435
|
+
headersFieldset.querySelectorAll("[data-mock-header-row]").forEach((row) => {
|
|
436
|
+
row.remove();
|
|
437
|
+
});
|
|
438
|
+
const headers = c.headers ?? [];
|
|
439
|
+
headers.forEach((header, index) => {
|
|
440
|
+
const row = document.createElement("div");
|
|
441
|
+
row.dataset.mockHeaderRow = String(index);
|
|
442
|
+
const nameLabel = document.createElement("label");
|
|
443
|
+
nameLabel.textContent = "Name";
|
|
444
|
+
const nameInput = document.createElement("input");
|
|
445
|
+
nameInput.type = "text";
|
|
446
|
+
nameInput.value = header.name;
|
|
447
|
+
nameInput.dataset.editorField = "true";
|
|
448
|
+
nameInput.addEventListener("input", () => {
|
|
449
|
+
const cc = getCurrent();
|
|
450
|
+
if (cc === void 0) return;
|
|
451
|
+
const next = (cc.headers ?? []).map(
|
|
452
|
+
(h, i) => i === index ? { ...h, name: nameInput.value } : h
|
|
453
|
+
);
|
|
454
|
+
setAction({ ...cc, headers: next });
|
|
455
|
+
});
|
|
456
|
+
context.registerControl(`/mock/headers/${index}/name`, nameInput);
|
|
457
|
+
nameLabel.append(nameInput);
|
|
458
|
+
const valueLabel = document.createElement("label");
|
|
459
|
+
valueLabel.textContent = "Value";
|
|
460
|
+
const valueInput = document.createElement("input");
|
|
461
|
+
valueInput.type = "text";
|
|
462
|
+
valueInput.value = header.value;
|
|
463
|
+
valueInput.dataset.editorField = "true";
|
|
464
|
+
valueInput.addEventListener("input", () => {
|
|
465
|
+
const cc = getCurrent();
|
|
466
|
+
if (cc === void 0) return;
|
|
467
|
+
const next = (cc.headers ?? []).map(
|
|
468
|
+
(h, i) => i === index ? { ...h, value: valueInput.value } : h
|
|
469
|
+
);
|
|
470
|
+
setAction({ ...cc, headers: next });
|
|
471
|
+
});
|
|
472
|
+
context.registerControl(`/mock/headers/${index}/value`, valueInput);
|
|
473
|
+
valueLabel.append(valueInput);
|
|
474
|
+
const remove = document.createElement("button");
|
|
475
|
+
remove.type = "button";
|
|
476
|
+
remove.textContent = "Remove";
|
|
477
|
+
remove.dataset.editorField = "true";
|
|
478
|
+
remove.addEventListener("click", () => {
|
|
479
|
+
const cc = getCurrent();
|
|
480
|
+
if (cc === void 0) return;
|
|
481
|
+
const next = (cc.headers ?? []).filter((_, i) => i !== index);
|
|
482
|
+
setAction({ ...cc, headers: next });
|
|
483
|
+
});
|
|
484
|
+
row.append(nameLabel, valueLabel, remove);
|
|
485
|
+
headersFieldset.append(row);
|
|
486
|
+
});
|
|
487
|
+
const add = document.createElement("button");
|
|
488
|
+
add.type = "button";
|
|
489
|
+
add.textContent = "Add header";
|
|
490
|
+
add.dataset.editorField = "true";
|
|
491
|
+
add.dataset.mockAddHeader = "true";
|
|
492
|
+
add.addEventListener("click", () => {
|
|
493
|
+
const cc = getCurrent();
|
|
494
|
+
if (cc === void 0) return;
|
|
495
|
+
setAction({
|
|
496
|
+
...cc,
|
|
497
|
+
headers: [...cc.headers ?? [], { name: "", value: "" }]
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
headersFieldset.append(add);
|
|
501
|
+
};
|
|
502
|
+
renderHeaders();
|
|
503
|
+
container.append(
|
|
504
|
+
statusLabel,
|
|
505
|
+
delayLabel,
|
|
506
|
+
sourceLabel,
|
|
507
|
+
bodyArea,
|
|
508
|
+
fileLabel,
|
|
509
|
+
headersFieldset
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
render();
|
|
513
|
+
return { destroy() {
|
|
514
|
+
} };
|
|
515
|
+
},
|
|
516
|
+
defaultAction() {
|
|
517
|
+
return { status: 200, body: "" };
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// packages/editor/src/rule-types/query.ts
|
|
523
|
+
var MAX_QUERY_NAME_LENGTH = 256;
|
|
524
|
+
var MAX_QUERY_VALUE_LENGTH = 2048;
|
|
525
|
+
var MAX_QUERY_PARAMS = 64;
|
|
526
|
+
function isRecord2(value) {
|
|
527
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
528
|
+
}
|
|
529
|
+
function asQueryAction(value) {
|
|
530
|
+
if (!isRecord2(value)) return void 0;
|
|
531
|
+
if (value.type !== "query") return void 0;
|
|
532
|
+
const params = value.params;
|
|
533
|
+
if (!Array.isArray(params)) return void 0;
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function stable2(diagnostics) {
|
|
537
|
+
return [...diagnostics].sort(
|
|
538
|
+
(a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
var queryRuleType = {
|
|
542
|
+
id: "query",
|
|
543
|
+
label: "Query parameters",
|
|
544
|
+
matches(rule) {
|
|
545
|
+
return asQueryAction(rule.action) !== void 0;
|
|
546
|
+
},
|
|
547
|
+
validate(rule, rulePath) {
|
|
548
|
+
const action = asQueryAction(rule.action);
|
|
549
|
+
if (action === void 0) return [];
|
|
550
|
+
const diagnostics = [];
|
|
551
|
+
if (!Array.isArray(action.params) || action.params.length === 0) {
|
|
552
|
+
diagnostics.push({
|
|
553
|
+
code: "editor.query-params-required",
|
|
554
|
+
severity: "error",
|
|
555
|
+
path: `${rulePath}/action/params`,
|
|
556
|
+
message: "Query rules need at least one parameter."
|
|
557
|
+
});
|
|
558
|
+
return stable2(diagnostics);
|
|
559
|
+
}
|
|
560
|
+
if (action.params.length > MAX_QUERY_PARAMS) {
|
|
561
|
+
diagnostics.push({
|
|
562
|
+
code: "editor.query-too-many-params",
|
|
563
|
+
severity: "error",
|
|
564
|
+
path: `${rulePath}/action/params`,
|
|
565
|
+
message: `A query rule may define at most ${MAX_QUERY_PARAMS} parameters.`
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
const seen = /* @__PURE__ */ new Set();
|
|
569
|
+
action.params.forEach((param, index) => {
|
|
570
|
+
const namePath = `${rulePath}/action/params/${index}/name`;
|
|
571
|
+
const valuePath = `${rulePath}/action/params/${index}/value`;
|
|
572
|
+
if (typeof param?.name !== "string" || param.name.length === 0) {
|
|
573
|
+
diagnostics.push({
|
|
574
|
+
code: "editor.query-param-name-required",
|
|
575
|
+
severity: "error",
|
|
576
|
+
path: namePath,
|
|
577
|
+
message: "Query parameter name is required."
|
|
578
|
+
});
|
|
579
|
+
} else if (param.name.length > MAX_QUERY_NAME_LENGTH) {
|
|
580
|
+
diagnostics.push({
|
|
581
|
+
code: "editor.query-param-name-too-long",
|
|
582
|
+
severity: "error",
|
|
583
|
+
path: namePath,
|
|
584
|
+
message: "Query parameter name is too long."
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (typeof param?.value !== "string" || param.value.length === 0) {
|
|
588
|
+
diagnostics.push({
|
|
589
|
+
code: "editor.query-param-value-required",
|
|
590
|
+
severity: "error",
|
|
591
|
+
path: valuePath,
|
|
592
|
+
message: "Query parameter value is required."
|
|
593
|
+
});
|
|
594
|
+
} else if (param.value.length > MAX_QUERY_VALUE_LENGTH) {
|
|
595
|
+
diagnostics.push({
|
|
596
|
+
code: "editor.query-param-value-too-long",
|
|
597
|
+
severity: "error",
|
|
598
|
+
path: valuePath,
|
|
599
|
+
message: "Query parameter value is too long."
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
if (typeof param?.name === "string" && seen.has(param.name)) {
|
|
603
|
+
diagnostics.push({
|
|
604
|
+
code: "editor.query-duplicate-param",
|
|
605
|
+
severity: "error",
|
|
606
|
+
path: namePath,
|
|
607
|
+
message: "Query parameter names must be unique within a rule."
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (typeof param?.name === "string") seen.add(param.name);
|
|
611
|
+
});
|
|
612
|
+
return stable2(diagnostics);
|
|
613
|
+
},
|
|
614
|
+
mount(context) {
|
|
615
|
+
const { document, container } = context;
|
|
616
|
+
const getCurrent = () => asQueryAction(context.getField("action"));
|
|
617
|
+
const setAction = (next) => {
|
|
618
|
+
context.setField("action", next);
|
|
619
|
+
};
|
|
620
|
+
const render = () => {
|
|
621
|
+
const current = getCurrent();
|
|
622
|
+
container.replaceChildren();
|
|
623
|
+
if (current === void 0) return;
|
|
624
|
+
current.params.forEach((param, index) => {
|
|
625
|
+
const row = document.createElement("div");
|
|
626
|
+
row.dataset.queryParamRow = String(index);
|
|
627
|
+
const nameLabel = document.createElement("label");
|
|
628
|
+
nameLabel.textContent = "Name";
|
|
629
|
+
const nameInput = document.createElement("input");
|
|
630
|
+
nameInput.type = "text";
|
|
631
|
+
nameInput.value = param.name;
|
|
632
|
+
nameInput.dataset.editorField = "true";
|
|
633
|
+
nameInput.addEventListener("input", () => {
|
|
634
|
+
const c = getCurrent();
|
|
635
|
+
if (c === void 0) return;
|
|
636
|
+
const params = c.params.map(
|
|
637
|
+
(p, i) => i === index ? { ...p, name: nameInput.value } : p
|
|
638
|
+
);
|
|
639
|
+
setAction({ type: "query", params });
|
|
640
|
+
});
|
|
641
|
+
context.registerControl(`/action/params/${index}/name`, nameInput);
|
|
642
|
+
nameLabel.append(nameInput);
|
|
643
|
+
const valueLabel = document.createElement("label");
|
|
644
|
+
valueLabel.textContent = "Value";
|
|
645
|
+
const valueInput = document.createElement("input");
|
|
646
|
+
valueInput.type = "text";
|
|
647
|
+
valueInput.value = param.value;
|
|
648
|
+
valueInput.dataset.editorField = "true";
|
|
649
|
+
valueInput.addEventListener("input", () => {
|
|
650
|
+
const c = getCurrent();
|
|
651
|
+
if (c === void 0) return;
|
|
652
|
+
const params = c.params.map(
|
|
653
|
+
(p, i) => i === index ? { ...p, value: valueInput.value } : p
|
|
654
|
+
);
|
|
655
|
+
setAction({ type: "query", params });
|
|
656
|
+
});
|
|
657
|
+
context.registerControl(`/action/params/${index}/value`, valueInput);
|
|
658
|
+
valueLabel.append(valueInput);
|
|
659
|
+
const remove = document.createElement("button");
|
|
660
|
+
remove.type = "button";
|
|
661
|
+
remove.textContent = "Remove";
|
|
662
|
+
remove.dataset.editorField = "true";
|
|
663
|
+
remove.addEventListener("click", () => {
|
|
664
|
+
const c = getCurrent();
|
|
665
|
+
if (c === void 0) return;
|
|
666
|
+
const params = c.params.filter((_, i) => i !== index);
|
|
667
|
+
setAction({ type: "query", params });
|
|
668
|
+
});
|
|
669
|
+
row.append(nameLabel, valueLabel, remove);
|
|
670
|
+
container.append(row);
|
|
671
|
+
});
|
|
672
|
+
const add = document.createElement("button");
|
|
673
|
+
add.type = "button";
|
|
674
|
+
add.textContent = "Add parameter";
|
|
675
|
+
add.dataset.editorField = "true";
|
|
676
|
+
add.addEventListener("click", () => {
|
|
677
|
+
const c = getCurrent();
|
|
678
|
+
if (c === void 0) return;
|
|
679
|
+
setAction({
|
|
680
|
+
type: "query",
|
|
681
|
+
params: [...c.params, { name: "", value: "" }]
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
container.append(add);
|
|
685
|
+
};
|
|
686
|
+
render();
|
|
687
|
+
return { destroy() {
|
|
688
|
+
} };
|
|
689
|
+
},
|
|
690
|
+
defaultAction() {
|
|
691
|
+
return { type: "query", params: [{ name: "", value: "" }] };
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// packages/editor/src/rule-types/request-body.ts
|
|
696
|
+
function isRecord3(value) {
|
|
697
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
698
|
+
}
|
|
699
|
+
function requestBodyOf(value) {
|
|
700
|
+
if (!isRecord3(value) || !("mode" in value)) return void 0;
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
function hasLoneSurrogate(value) {
|
|
704
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
705
|
+
const code = value.charCodeAt(i);
|
|
706
|
+
if (code >= 55296 && code <= 56319) {
|
|
707
|
+
if (i + 1 >= value.length) return true;
|
|
708
|
+
const next = value.charCodeAt(i + 1);
|
|
709
|
+
if (next < 56320 || next > 57343) return true;
|
|
710
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
711
|
+
if (i === 0) return true;
|
|
712
|
+
const prev = value.charCodeAt(i - 1);
|
|
713
|
+
if (prev < 55296 || prev > 56319) return true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
function stable3(values) {
|
|
719
|
+
return [...values].sort(
|
|
720
|
+
(left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code)
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
function createRequestBodyRuleType() {
|
|
724
|
+
return {
|
|
725
|
+
id: "request-body",
|
|
726
|
+
label: "Request body",
|
|
727
|
+
actionField: "requestBody",
|
|
728
|
+
matches(rule) {
|
|
729
|
+
return rule.type === "request-body" || requestBodyOf(rule.requestBody) !== void 0;
|
|
730
|
+
},
|
|
731
|
+
validate(rule, rulePath) {
|
|
732
|
+
const action = requestBodyOf(rule.requestBody);
|
|
733
|
+
if (action === void 0) return [];
|
|
734
|
+
const diagnostics = [];
|
|
735
|
+
if (action.mode !== "replace" && action.mode !== "regex") {
|
|
736
|
+
diagnostics.push({
|
|
737
|
+
code: "editor.request-body-mode",
|
|
738
|
+
severity: "error",
|
|
739
|
+
path: `${rulePath}/requestBody/mode`,
|
|
740
|
+
message: 'requestBody.mode must be "replace" or "regex"'
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (action.mode === "replace") {
|
|
744
|
+
if (typeof action.body !== "string") {
|
|
745
|
+
diagnostics.push({
|
|
746
|
+
code: "editor.request-body-replace-body",
|
|
747
|
+
severity: "error",
|
|
748
|
+
path: `${rulePath}/requestBody/body`,
|
|
749
|
+
message: "Replace mode requires a body string."
|
|
750
|
+
});
|
|
751
|
+
} else if (action.body.length > LIMITS.maxRequestBodyBytes) {
|
|
752
|
+
diagnostics.push({
|
|
753
|
+
code: "editor.request-body-replace-body",
|
|
754
|
+
severity: "error",
|
|
755
|
+
path: `${rulePath}/requestBody/body`,
|
|
756
|
+
message: `Replace body exceeds the maximum size of ${LIMITS.maxRequestBodyBytes} bytes.`
|
|
757
|
+
});
|
|
758
|
+
} else if (hasLoneSurrogate(action.body)) {
|
|
759
|
+
diagnostics.push({
|
|
760
|
+
code: "editor.request-body-lone-surrogate",
|
|
761
|
+
severity: "error",
|
|
762
|
+
path: `${rulePath}/requestBody/body`,
|
|
763
|
+
message: "Replace body must not contain lone UTF-16 surrogates."
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (action.mode === "regex") {
|
|
768
|
+
if (typeof action.pattern !== "string" || action.pattern.length === 0) {
|
|
769
|
+
diagnostics.push({
|
|
770
|
+
code: "editor.request-body-pattern",
|
|
771
|
+
severity: "error",
|
|
772
|
+
path: `${rulePath}/requestBody/pattern`,
|
|
773
|
+
message: "Regex mode requires a non-empty pattern string."
|
|
774
|
+
});
|
|
775
|
+
} else if (action.pattern.length > LIMITS.maxRequestBodyPatternLength) {
|
|
776
|
+
diagnostics.push({
|
|
777
|
+
code: "editor.request-body-pattern",
|
|
778
|
+
severity: "error",
|
|
779
|
+
path: `${rulePath}/requestBody/pattern`,
|
|
780
|
+
message: `Regex pattern exceeds the maximum length of ${LIMITS.maxRequestBodyPatternLength} characters.`
|
|
781
|
+
});
|
|
782
|
+
} else if (hasLoneSurrogate(action.pattern)) {
|
|
783
|
+
diagnostics.push({
|
|
784
|
+
code: "editor.request-body-lone-surrogate",
|
|
785
|
+
severity: "error",
|
|
786
|
+
path: `${rulePath}/requestBody/pattern`,
|
|
787
|
+
message: "Regex pattern must not contain lone UTF-16 surrogates."
|
|
788
|
+
});
|
|
789
|
+
} else {
|
|
790
|
+
try {
|
|
791
|
+
new RegExp(action.pattern, "u");
|
|
792
|
+
} catch {
|
|
793
|
+
diagnostics.push({
|
|
794
|
+
code: "editor.request-body-pattern",
|
|
795
|
+
severity: "error",
|
|
796
|
+
path: `${rulePath}/requestBody/pattern`,
|
|
797
|
+
message: "Regex pattern must be a valid regular expression."
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (typeof action.replacement !== "string" || action.replacement.length > LIMITS.maxRequestBodyReplacementLength) {
|
|
802
|
+
diagnostics.push({
|
|
803
|
+
code: "editor.request-body-replacement",
|
|
804
|
+
severity: "error",
|
|
805
|
+
path: `${rulePath}/requestBody/replacement`,
|
|
806
|
+
message: `Regex replacement exceeds the maximum length of ${LIMITS.maxRequestBodyReplacementLength} characters.`
|
|
807
|
+
});
|
|
808
|
+
} else if (hasLoneSurrogate(action.replacement)) {
|
|
809
|
+
diagnostics.push({
|
|
810
|
+
code: "editor.request-body-lone-surrogate",
|
|
811
|
+
severity: "error",
|
|
812
|
+
path: `${rulePath}/requestBody/replacement`,
|
|
813
|
+
message: "Regex replacement must not contain lone UTF-16 surrogates."
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (rule.method !== "POST" && rule.method !== "PUT" && rule.method !== "PATCH") {
|
|
818
|
+
diagnostics.push({
|
|
819
|
+
code: "editor.request-body-method",
|
|
820
|
+
severity: "error",
|
|
821
|
+
path: `${rulePath}/method`,
|
|
822
|
+
message: 'Request-body rules require method "POST", "PUT", or "PATCH".'
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
const resourceTypes = rule.resourceTypes;
|
|
826
|
+
if (!Array.isArray(resourceTypes) || resourceTypes.length !== 1 || resourceTypes[0] !== "xmlhttprequest") {
|
|
827
|
+
diagnostics.push({
|
|
828
|
+
code: "editor.request-body-resource-types",
|
|
829
|
+
severity: "error",
|
|
830
|
+
path: `${rulePath}/resourceTypes`,
|
|
831
|
+
message: 'Request-body rules require exactly one resource type: "xmlhttprequest".'
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
return stable3(diagnostics);
|
|
835
|
+
},
|
|
836
|
+
mount(context) {
|
|
837
|
+
const { document, container } = context;
|
|
838
|
+
const render = () => {
|
|
839
|
+
const current = requestBodyOf(context.getField("requestBody"));
|
|
840
|
+
container.replaceChildren();
|
|
841
|
+
if (current === void 0) return;
|
|
842
|
+
const modeSelect = document.createElement("select");
|
|
843
|
+
modeSelect.value = current.mode;
|
|
844
|
+
const replaceOption = document.createElement("option");
|
|
845
|
+
replaceOption.value = "replace";
|
|
846
|
+
replaceOption.textContent = "Replace body";
|
|
847
|
+
const regexOption = document.createElement("option");
|
|
848
|
+
regexOption.value = "regex";
|
|
849
|
+
regexOption.textContent = "Regex replace";
|
|
850
|
+
modeSelect.append(replaceOption, regexOption);
|
|
851
|
+
modeSelect.addEventListener("change", () => {
|
|
852
|
+
const next = { ...current, mode: modeSelect.value };
|
|
853
|
+
if (modeSelect.value === "replace") {
|
|
854
|
+
delete next.pattern;
|
|
855
|
+
delete next.replacement;
|
|
856
|
+
} else {
|
|
857
|
+
delete next.body;
|
|
858
|
+
}
|
|
859
|
+
context.setField("requestBody", next);
|
|
860
|
+
render();
|
|
861
|
+
});
|
|
862
|
+
context.registerControl("/requestBody/mode", modeSelect);
|
|
863
|
+
container.append(modeSelect);
|
|
864
|
+
if (current.mode === "replace") {
|
|
865
|
+
const bodyInput = document.createElement("textarea");
|
|
866
|
+
bodyInput.value = current.body ?? "";
|
|
867
|
+
bodyInput.placeholder = "Replacement body";
|
|
868
|
+
bodyInput.addEventListener("input", () => {
|
|
869
|
+
const next = { ...current, body: bodyInput.value };
|
|
870
|
+
context.setField("requestBody", next);
|
|
871
|
+
});
|
|
872
|
+
context.registerControl("/requestBody/body", bodyInput);
|
|
873
|
+
container.append(bodyInput);
|
|
874
|
+
} else {
|
|
875
|
+
const patternInput = document.createElement("input");
|
|
876
|
+
patternInput.type = "text";
|
|
877
|
+
patternInput.value = current.pattern ?? "";
|
|
878
|
+
patternInput.placeholder = "Regex pattern";
|
|
879
|
+
patternInput.addEventListener("input", () => {
|
|
880
|
+
const next = { ...current, pattern: patternInput.value };
|
|
881
|
+
context.setField("requestBody", next);
|
|
882
|
+
});
|
|
883
|
+
context.registerControl("/requestBody/pattern", patternInput);
|
|
884
|
+
container.append(patternInput);
|
|
885
|
+
const replacementInput = document.createElement("input");
|
|
886
|
+
replacementInput.type = "text";
|
|
887
|
+
replacementInput.value = current.replacement ?? "";
|
|
888
|
+
replacementInput.placeholder = "Replacement";
|
|
889
|
+
replacementInput.addEventListener("input", () => {
|
|
890
|
+
const next = { ...current, replacement: replacementInput.value };
|
|
891
|
+
context.setField("requestBody", next);
|
|
892
|
+
});
|
|
893
|
+
context.registerControl("/requestBody/replacement", replacementInput);
|
|
894
|
+
container.append(replacementInput);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
render();
|
|
898
|
+
return { destroy() {
|
|
899
|
+
} };
|
|
900
|
+
},
|
|
901
|
+
defaultAction() {
|
|
902
|
+
return { mode: "replace", body: "" };
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// packages/editor/src/rule-types/response-body.ts
|
|
908
|
+
function isRecord4(value) {
|
|
909
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
910
|
+
}
|
|
911
|
+
function replacementsOf(value) {
|
|
912
|
+
if (!isRecord4(value) || !Array.isArray(value.replacements)) return void 0;
|
|
913
|
+
return value.replacements;
|
|
914
|
+
}
|
|
915
|
+
function stable4(values) {
|
|
916
|
+
return [...values].sort(
|
|
917
|
+
(left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code)
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
function createResponseBodyRuleType() {
|
|
921
|
+
return {
|
|
922
|
+
id: "response-body",
|
|
923
|
+
label: "Response body rewrite",
|
|
924
|
+
actionField: "responseBody",
|
|
925
|
+
matches(rule) {
|
|
926
|
+
return rule.type === "response-body" || replacementsOf(rule.responseBody) !== void 0;
|
|
927
|
+
},
|
|
928
|
+
validate(rule, rulePath) {
|
|
929
|
+
const replacements = replacementsOf(rule.responseBody);
|
|
930
|
+
if (replacements === void 0) return [];
|
|
931
|
+
const diagnostics = [];
|
|
932
|
+
if (replacements.length === 0 || replacements.length > LIMITS.maxResponseBodyReplacements) {
|
|
933
|
+
diagnostics.push({
|
|
934
|
+
code: "editor.response-body-replacements",
|
|
935
|
+
severity: "error",
|
|
936
|
+
path: `${rulePath}/responseBody/replacements`,
|
|
937
|
+
message: `Response-body rules need 1-${LIMITS.maxResponseBodyReplacements} replacements.`
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
replacements.forEach((entry, index) => {
|
|
941
|
+
const patternPath = `${rulePath}/responseBody/replacements/${index}/pattern`;
|
|
942
|
+
const replacementPath = `${rulePath}/responseBody/replacements/${index}/replacement`;
|
|
943
|
+
try {
|
|
944
|
+
if (typeof entry?.pattern !== "string" || entry.pattern.length === 0 || entry.pattern.length > LIMITS.maxResponseBodyPatternLength)
|
|
945
|
+
throw new Error();
|
|
946
|
+
new RegExp(entry.pattern, "u");
|
|
947
|
+
} catch {
|
|
948
|
+
diagnostics.push({
|
|
949
|
+
code: "editor.response-body-pattern",
|
|
950
|
+
severity: "error",
|
|
951
|
+
path: patternPath,
|
|
952
|
+
message: "Replacement pattern must be a valid bounded regular expression."
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (typeof entry?.replacement !== "string" || entry.replacement.length > LIMITS.maxResponseBodyReplacementLength) {
|
|
956
|
+
diagnostics.push({
|
|
957
|
+
code: "editor.response-body-replacement",
|
|
958
|
+
severity: "error",
|
|
959
|
+
path: replacementPath,
|
|
960
|
+
message: "Replacement text exceeds the permitted size."
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
return stable4(diagnostics);
|
|
965
|
+
},
|
|
966
|
+
mount(context) {
|
|
967
|
+
const { document, container } = context;
|
|
968
|
+
const render = () => {
|
|
969
|
+
const current = replacementsOf(context.getField("responseBody"));
|
|
970
|
+
container.replaceChildren();
|
|
971
|
+
if (current === void 0) return;
|
|
972
|
+
current.forEach((entry, index) => {
|
|
973
|
+
const row = document.createElement("div");
|
|
974
|
+
row.dataset.responseBodyReplacement = String(index);
|
|
975
|
+
const pattern = document.createElement("input");
|
|
976
|
+
pattern.type = "text";
|
|
977
|
+
pattern.value = entry.pattern;
|
|
978
|
+
pattern.placeholder = "Pattern";
|
|
979
|
+
pattern.addEventListener("input", () => {
|
|
980
|
+
const next = [
|
|
981
|
+
...replacementsOf(context.getField("responseBody")) ?? []
|
|
982
|
+
];
|
|
983
|
+
next[index] = { ...next[index], pattern: pattern.value };
|
|
984
|
+
context.setField("responseBody", { replacements: next });
|
|
985
|
+
});
|
|
986
|
+
context.registerControl(
|
|
987
|
+
`/responseBody/replacements/${index}/pattern`,
|
|
988
|
+
pattern
|
|
989
|
+
);
|
|
990
|
+
const replacement = document.createElement("input");
|
|
991
|
+
replacement.type = "text";
|
|
992
|
+
replacement.value = entry.replacement;
|
|
993
|
+
replacement.placeholder = "Replacement";
|
|
994
|
+
replacement.addEventListener("input", () => {
|
|
995
|
+
const next = [
|
|
996
|
+
...replacementsOf(context.getField("responseBody")) ?? []
|
|
997
|
+
];
|
|
998
|
+
next[index] = { ...next[index], replacement: replacement.value };
|
|
999
|
+
context.setField("responseBody", { replacements: next });
|
|
1000
|
+
});
|
|
1001
|
+
context.registerControl(
|
|
1002
|
+
`/responseBody/replacements/${index}/replacement`,
|
|
1003
|
+
replacement
|
|
1004
|
+
);
|
|
1005
|
+
const remove = document.createElement("button");
|
|
1006
|
+
remove.type = "button";
|
|
1007
|
+
remove.textContent = "Remove replacement";
|
|
1008
|
+
remove.addEventListener("click", () => {
|
|
1009
|
+
const next = (replacementsOf(context.getField("responseBody")) ?? []).filter((_, itemIndex) => itemIndex !== index);
|
|
1010
|
+
context.setField("responseBody", { replacements: next });
|
|
1011
|
+
render();
|
|
1012
|
+
});
|
|
1013
|
+
row.append(pattern, replacement, remove);
|
|
1014
|
+
container.append(row);
|
|
1015
|
+
});
|
|
1016
|
+
const add = document.createElement("button");
|
|
1017
|
+
add.type = "button";
|
|
1018
|
+
add.textContent = "Add replacement";
|
|
1019
|
+
add.addEventListener("click", () => {
|
|
1020
|
+
const next = [
|
|
1021
|
+
...replacementsOf(context.getField("responseBody")) ?? [],
|
|
1022
|
+
{ pattern: "", replacement: "" }
|
|
1023
|
+
];
|
|
1024
|
+
context.setField("responseBody", { replacements: next });
|
|
1025
|
+
render();
|
|
1026
|
+
});
|
|
1027
|
+
container.append(add);
|
|
1028
|
+
};
|
|
1029
|
+
render();
|
|
1030
|
+
return { destroy() {
|
|
1031
|
+
} };
|
|
1032
|
+
},
|
|
1033
|
+
defaultAction() {
|
|
1034
|
+
return { replacements: [{ pattern: "", replacement: "" }] };
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// packages/editor/src/rule-types/index.ts
|
|
1040
|
+
var builtInRuleTypes = Object.freeze([
|
|
1041
|
+
queryRuleType,
|
|
1042
|
+
createMockRuleType(),
|
|
1043
|
+
createResponseBodyRuleType(),
|
|
1044
|
+
createRequestBodyRuleType()
|
|
1045
|
+
]);
|
|
1046
|
+
|
|
1047
|
+
// packages/editor/src/types.ts
|
|
1048
|
+
var EditorInitializationError = class extends Error {
|
|
1049
|
+
diagnostics;
|
|
1050
|
+
constructor(diagnostics) {
|
|
1051
|
+
super("Rogatio editor could not initialize");
|
|
1052
|
+
this.name = "EditorInitializationError";
|
|
1053
|
+
this.diagnostics = diagnostics.map((diagnostic2) => ({ ...diagnostic2 }));
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// packages/editor/src/url.ts
|
|
1058
|
+
var F2_MAX_URL_REGEX_LENGTH = 2048;
|
|
1059
|
+
var REGEX_META_CHARACTERS = /[.*+?^${}()|[\]\\]/g;
|
|
1060
|
+
function urlToExactRegex(value) {
|
|
1061
|
+
if (typeof value !== "string" || value.length === 0 || value.trim() !== value || hasControl(value)) {
|
|
1062
|
+
return { ok: false, code: "editor.invalid-url" };
|
|
1063
|
+
}
|
|
1064
|
+
let url;
|
|
1065
|
+
try {
|
|
1066
|
+
url = new URL(value);
|
|
1067
|
+
} catch {
|
|
1068
|
+
return { ok: false, code: "editor.invalid-url" };
|
|
1069
|
+
}
|
|
1070
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.origin === "null" || url.username.length > 0 || url.password.length > 0 || url.hash.length > 0) {
|
|
1071
|
+
return { ok: false, code: "editor.invalid-url" };
|
|
1072
|
+
}
|
|
1073
|
+
const escaped = url.href.replace(REGEX_META_CHARACTERS, "\\$&");
|
|
1074
|
+
const source = `^${escaped}$`;
|
|
1075
|
+
if (source.length > F2_MAX_URL_REGEX_LENGTH) {
|
|
1076
|
+
return { ok: false, code: "editor.url-too-long" };
|
|
1077
|
+
}
|
|
1078
|
+
return { ok: true, source };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// packages/editor/src/editor.ts
|
|
1082
|
+
var RESOURCE_TYPES2 = [
|
|
1083
|
+
"main_frame",
|
|
1084
|
+
"sub_frame",
|
|
1085
|
+
"stylesheet",
|
|
1086
|
+
"script",
|
|
1087
|
+
"image",
|
|
1088
|
+
"font",
|
|
1089
|
+
"object",
|
|
1090
|
+
"media",
|
|
1091
|
+
"xmlhttprequest",
|
|
1092
|
+
"ping",
|
|
1093
|
+
"csp_report",
|
|
1094
|
+
"websocket",
|
|
1095
|
+
"webtransport",
|
|
1096
|
+
"webbundle",
|
|
1097
|
+
"other"
|
|
1098
|
+
];
|
|
1099
|
+
var HTTP_METHODS2 = [
|
|
1100
|
+
"GET",
|
|
1101
|
+
"POST",
|
|
1102
|
+
"PUT",
|
|
1103
|
+
"PATCH",
|
|
1104
|
+
"DELETE",
|
|
1105
|
+
"HEAD",
|
|
1106
|
+
"OPTIONS",
|
|
1107
|
+
"CONNECT",
|
|
1108
|
+
"TRACE"
|
|
1109
|
+
];
|
|
1110
|
+
var COMMON_RULE_FIELDS = /* @__PURE__ */ new Set([
|
|
1111
|
+
"id",
|
|
1112
|
+
"name",
|
|
1113
|
+
"urlRegex",
|
|
1114
|
+
"origins",
|
|
1115
|
+
"resourceTypes",
|
|
1116
|
+
"priority",
|
|
1117
|
+
"method",
|
|
1118
|
+
"type"
|
|
1119
|
+
]);
|
|
1120
|
+
var FORBIDDEN_EXTENSION_FIELDS = /* @__PURE__ */ new Set([
|
|
1121
|
+
"__proto__",
|
|
1122
|
+
"constructor",
|
|
1123
|
+
"prototype"
|
|
1124
|
+
]);
|
|
1125
|
+
var MAX_SNAPSHOT_ARRAY_LENGTH = 4096;
|
|
1126
|
+
var F2_MAX_URL_REGEX_LENGTH2 = 2048;
|
|
1127
|
+
var editorInstanceCount = 0;
|
|
1128
|
+
function isRecord5(value) {
|
|
1129
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1130
|
+
}
|
|
1131
|
+
function snapshotOwnData(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
1132
|
+
if (value === null || typeof value !== "object") {
|
|
1133
|
+
return { valid: true, value };
|
|
1134
|
+
}
|
|
1135
|
+
if (ancestors.has(value)) return { valid: false };
|
|
1136
|
+
ancestors.add(value);
|
|
1137
|
+
try {
|
|
1138
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
1139
|
+
return { valid: false };
|
|
1140
|
+
}
|
|
1141
|
+
if (Array.isArray(value)) {
|
|
1142
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
1143
|
+
if (lengthDescriptor === void 0 || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || lengthDescriptor.value > MAX_SNAPSHOT_ARRAY_LENGTH) {
|
|
1144
|
+
return { valid: false };
|
|
1145
|
+
}
|
|
1146
|
+
const length = lengthDescriptor.value;
|
|
1147
|
+
for (const propertyName of Object.getOwnPropertyNames(value)) {
|
|
1148
|
+
if (propertyName === "length") continue;
|
|
1149
|
+
const index = Number(propertyName);
|
|
1150
|
+
if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== propertyName) {
|
|
1151
|
+
return { valid: false };
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const snapshot2 = new Array(length);
|
|
1155
|
+
for (let index = 0; index < length; index += 1) {
|
|
1156
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
1157
|
+
value,
|
|
1158
|
+
String(index)
|
|
1159
|
+
);
|
|
1160
|
+
if (descriptor === void 0 || !("value" in descriptor) || !descriptor.enumerable) {
|
|
1161
|
+
return { valid: false };
|
|
1162
|
+
}
|
|
1163
|
+
const child = snapshotOwnData(descriptor.value, ancestors);
|
|
1164
|
+
if (!child.valid) return child;
|
|
1165
|
+
snapshot2[index] = child.value;
|
|
1166
|
+
}
|
|
1167
|
+
return { valid: true, value: snapshot2 };
|
|
1168
|
+
}
|
|
1169
|
+
const snapshot = {};
|
|
1170
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
1171
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1172
|
+
if (descriptor === void 0 || !("value" in descriptor) || !descriptor.enumerable) {
|
|
1173
|
+
return { valid: false };
|
|
1174
|
+
}
|
|
1175
|
+
const child = snapshotOwnData(descriptor.value, ancestors);
|
|
1176
|
+
if (!child.valid) return child;
|
|
1177
|
+
Object.defineProperty(snapshot, key, {
|
|
1178
|
+
configurable: true,
|
|
1179
|
+
enumerable: true,
|
|
1180
|
+
value: child.value,
|
|
1181
|
+
writable: true
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
return { valid: true, value: snapshot };
|
|
1185
|
+
} catch {
|
|
1186
|
+
return { valid: false };
|
|
1187
|
+
} finally {
|
|
1188
|
+
ancestors.delete(value);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function cloneSnapshot(value) {
|
|
1192
|
+
const result = snapshotOwnData(value);
|
|
1193
|
+
if (!result.valid) throw new Error("editor snapshot invariant failed");
|
|
1194
|
+
return result.value;
|
|
1195
|
+
}
|
|
1196
|
+
function freezeSnapshot(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1197
|
+
if (value === null || typeof value !== "object") return value;
|
|
1198
|
+
if (seen.has(value)) return value;
|
|
1199
|
+
seen.add(value);
|
|
1200
|
+
Object.freeze(value);
|
|
1201
|
+
for (const key of Object.keys(value)) {
|
|
1202
|
+
freezeSnapshot(value[key], seen);
|
|
1203
|
+
}
|
|
1204
|
+
return value;
|
|
1205
|
+
}
|
|
1206
|
+
function asDraftProject(value) {
|
|
1207
|
+
if (!isRecord5(value) || !Array.isArray(value.groups)) return void 0;
|
|
1208
|
+
for (const group of value.groups) {
|
|
1209
|
+
if (!isRecord5(group) || !Array.isArray(group.rules)) return void 0;
|
|
1210
|
+
for (const rule of group.rules) {
|
|
1211
|
+
if (!isRecord5(rule)) return void 0;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return value;
|
|
1215
|
+
}
|
|
1216
|
+
function encodePointerSegment(value) {
|
|
1217
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
1218
|
+
}
|
|
1219
|
+
function decodePointer(path) {
|
|
1220
|
+
if (path === "") return [];
|
|
1221
|
+
if (!path.startsWith("/")) return void 0;
|
|
1222
|
+
return path.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
1223
|
+
}
|
|
1224
|
+
function pointer(...segments) {
|
|
1225
|
+
return segments.length === 0 ? "" : `/${segments.map((segment) => encodePointerSegment(String(segment))).join("/")}`;
|
|
1226
|
+
}
|
|
1227
|
+
function arrayIndex(value) {
|
|
1228
|
+
if (!/^(0|[1-9][0-9]*)$/u.test(value)) return void 0;
|
|
1229
|
+
const index = Number(value);
|
|
1230
|
+
return Number.isSafeInteger(index) ? index : void 0;
|
|
1231
|
+
}
|
|
1232
|
+
function valueAtPath(root, path) {
|
|
1233
|
+
const segments = decodePointer(path);
|
|
1234
|
+
if (!segments) return void 0;
|
|
1235
|
+
let current = root;
|
|
1236
|
+
for (const segment of segments) {
|
|
1237
|
+
if (Array.isArray(current)) {
|
|
1238
|
+
const index = arrayIndex(segment);
|
|
1239
|
+
if (index === void 0 || !Object.hasOwn(current, index))
|
|
1240
|
+
return void 0;
|
|
1241
|
+
current = current[index];
|
|
1242
|
+
} else if (isRecord5(current) && Object.hasOwn(current, segment)) {
|
|
1243
|
+
current = current[segment];
|
|
1244
|
+
} else {
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return current;
|
|
1249
|
+
}
|
|
1250
|
+
function setValueAtPath(root, path, value) {
|
|
1251
|
+
const segments = decodePointer(path);
|
|
1252
|
+
if (!segments || segments.length === 0) return false;
|
|
1253
|
+
let current = root;
|
|
1254
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
1255
|
+
const segment = segments[index];
|
|
1256
|
+
if (Array.isArray(current)) {
|
|
1257
|
+
const childIndex = arrayIndex(segment);
|
|
1258
|
+
if (childIndex === void 0 || !Object.hasOwn(current, childIndex)) {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
current = current[childIndex];
|
|
1262
|
+
} else if (isRecord5(current) && Object.hasOwn(current, segment)) {
|
|
1263
|
+
current = current[segment];
|
|
1264
|
+
} else {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const finalSegment = segments[segments.length - 1];
|
|
1269
|
+
if (Array.isArray(current)) {
|
|
1270
|
+
const index = arrayIndex(finalSegment);
|
|
1271
|
+
if (index === void 0 || !Object.hasOwn(current, index)) return false;
|
|
1272
|
+
if (Object.is(current[index], value)) return false;
|
|
1273
|
+
current[index] = value;
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
if (!isRecord5(current)) return false;
|
|
1277
|
+
if (!Object.hasOwn(current, finalSegment) && finalSegment !== "description" && finalSegment !== "method" && finalSegment !== "type" && finalSegment !== "action" && finalSegment !== "redirect" && finalSegment !== "mock") {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
if (Object.is(current[finalSegment], value)) return false;
|
|
1281
|
+
Object.defineProperty(current, finalSegment, {
|
|
1282
|
+
configurable: true,
|
|
1283
|
+
enumerable: true,
|
|
1284
|
+
value,
|
|
1285
|
+
writable: true
|
|
1286
|
+
});
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
function deleteValueAtPath(root, path) {
|
|
1290
|
+
const segments = decodePointer(path);
|
|
1291
|
+
if (!segments || segments.length === 0) return false;
|
|
1292
|
+
const parentPath = pointer(...segments.slice(0, -1));
|
|
1293
|
+
const parent = valueAtPath(root, parentPath);
|
|
1294
|
+
const key = segments[segments.length - 1];
|
|
1295
|
+
if (!isRecord5(parent) || !Object.hasOwn(parent, key)) return false;
|
|
1296
|
+
return delete parent[key];
|
|
1297
|
+
}
|
|
1298
|
+
function safeText(value, fallback = "") {
|
|
1299
|
+
return typeof value === "string" ? value : fallback;
|
|
1300
|
+
}
|
|
1301
|
+
function diagnostic(code, path, message) {
|
|
1302
|
+
return { code, severity: "error", path, message };
|
|
1303
|
+
}
|
|
1304
|
+
function stableDiagnostics(diagnostics) {
|
|
1305
|
+
const compareCodeUnits = (left, right) => {
|
|
1306
|
+
if (left < right) return -1;
|
|
1307
|
+
if (left > right) return 1;
|
|
1308
|
+
return 0;
|
|
1309
|
+
};
|
|
1310
|
+
return diagnostics.map((value) => ({
|
|
1311
|
+
code: value.code,
|
|
1312
|
+
severity: "error",
|
|
1313
|
+
path: value.path,
|
|
1314
|
+
message: value.message
|
|
1315
|
+
})).sort(
|
|
1316
|
+
(left, right) => compareCodeUnits(left.path, right.path) || compareCodeUnits(left.code, right.code) || compareCodeUnits(left.message, right.message)
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
function normalizeDiagnostics(value) {
|
|
1320
|
+
if (!Array.isArray(value)) {
|
|
1321
|
+
return [
|
|
1322
|
+
diagnostic(
|
|
1323
|
+
"editor.validation-failed",
|
|
1324
|
+
"",
|
|
1325
|
+
"Project validation could not be completed."
|
|
1326
|
+
)
|
|
1327
|
+
];
|
|
1328
|
+
}
|
|
1329
|
+
const diagnostics = [];
|
|
1330
|
+
for (const item of value) {
|
|
1331
|
+
if (!isRecord5(item)) continue;
|
|
1332
|
+
const code = item.code;
|
|
1333
|
+
const path = item.path;
|
|
1334
|
+
const message = item.message;
|
|
1335
|
+
if (typeof code === "string" && typeof path === "string" && typeof message === "string") {
|
|
1336
|
+
diagnostics.push(diagnostic(code, path, message));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return stableDiagnostics(diagnostics);
|
|
1340
|
+
}
|
|
1341
|
+
function isValidExtensionName(name) {
|
|
1342
|
+
return name.length > 0 && !COMMON_RULE_FIELDS.has(name) && !FORBIDDEN_EXTENSION_FIELDS.has(name) && !hasControl(name);
|
|
1343
|
+
}
|
|
1344
|
+
var ACTION_FIELDS = ["redirect", "action", "mock"];
|
|
1345
|
+
function clearActionFields(rule, keep) {
|
|
1346
|
+
if (!isRecord5(rule)) return false;
|
|
1347
|
+
let changed = false;
|
|
1348
|
+
for (const field of ACTION_FIELDS) {
|
|
1349
|
+
if (field === keep) continue;
|
|
1350
|
+
if (Object.hasOwn(rule, field)) {
|
|
1351
|
+
delete rule[field];
|
|
1352
|
+
changed = true;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return changed;
|
|
1356
|
+
}
|
|
1357
|
+
function extensionFieldParent(rule, name) {
|
|
1358
|
+
const segments = name.split(".");
|
|
1359
|
+
if (segments.length === 0) return void 0;
|
|
1360
|
+
let current = rule;
|
|
1361
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
1362
|
+
if (!isRecord5(current)) return void 0;
|
|
1363
|
+
const next = current[segments[index]];
|
|
1364
|
+
if (!isRecord5(next)) {
|
|
1365
|
+
const created = {};
|
|
1366
|
+
Object.defineProperty(current, segments[index], {
|
|
1367
|
+
configurable: true,
|
|
1368
|
+
enumerable: true,
|
|
1369
|
+
value: created,
|
|
1370
|
+
writable: true
|
|
1371
|
+
});
|
|
1372
|
+
current = created;
|
|
1373
|
+
} else {
|
|
1374
|
+
current = next;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (!isRecord5(current)) return void 0;
|
|
1378
|
+
return { parent: current, key: segments[segments.length - 1] };
|
|
1379
|
+
}
|
|
1380
|
+
function toSearchText(value) {
|
|
1381
|
+
return typeof value === "string" || typeof value === "number" ? String(value).normalize("NFKC").toLowerCase() : "";
|
|
1382
|
+
}
|
|
1383
|
+
function displayName(value, fallback) {
|
|
1384
|
+
const text = safeText(value, fallback);
|
|
1385
|
+
return text.length > 0 ? text : fallback;
|
|
1386
|
+
}
|
|
1387
|
+
function isHTMLElement(value) {
|
|
1388
|
+
return value !== null && typeof value === "object" && value.nodeType === 1 && typeof value.appendChild === "function";
|
|
1389
|
+
}
|
|
1390
|
+
function normalizeExtensions(value) {
|
|
1391
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1392
|
+
const merged = [...builtInRuleTypes];
|
|
1393
|
+
for (const extension of builtInRuleTypes) ids.add(extension.id);
|
|
1394
|
+
if (value === void 0) return Object.freeze(merged);
|
|
1395
|
+
if (!Array.isArray(value)) {
|
|
1396
|
+
throw new EditorInitializationError([
|
|
1397
|
+
diagnostic(
|
|
1398
|
+
"editor.extension-registration",
|
|
1399
|
+
"",
|
|
1400
|
+
"Rule-type extensions are invalid."
|
|
1401
|
+
)
|
|
1402
|
+
]);
|
|
1403
|
+
}
|
|
1404
|
+
const passedIds = /* @__PURE__ */ new Set();
|
|
1405
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1406
|
+
const extension = value[index];
|
|
1407
|
+
if (!extension || typeof extension.id !== "string" || extension.id.length === 0 || typeof extension.label !== "string" || extension.label.length === 0 || typeof extension.matches !== "function" || typeof extension.mount !== "function" || typeof extension.validate !== "function" || passedIds.has(extension.id)) {
|
|
1408
|
+
throw new EditorInitializationError([
|
|
1409
|
+
diagnostic(
|
|
1410
|
+
"editor.extension-registration",
|
|
1411
|
+
`/ruleTypes/${index}`,
|
|
1412
|
+
"Rule-type extension registration is invalid or duplicated."
|
|
1413
|
+
)
|
|
1414
|
+
]);
|
|
1415
|
+
}
|
|
1416
|
+
passedIds.add(extension.id);
|
|
1417
|
+
const existing = merged.findIndex((e) => e.id === extension.id);
|
|
1418
|
+
if (existing >= 0) merged[existing] = extension;
|
|
1419
|
+
else merged.push(extension);
|
|
1420
|
+
ids.add(extension.id);
|
|
1421
|
+
}
|
|
1422
|
+
return Object.freeze(merged);
|
|
1423
|
+
}
|
|
1424
|
+
var EditorControllerImpl = class {
|
|
1425
|
+
root;
|
|
1426
|
+
document;
|
|
1427
|
+
options;
|
|
1428
|
+
extensions;
|
|
1429
|
+
instanceId;
|
|
1430
|
+
host;
|
|
1431
|
+
rail;
|
|
1432
|
+
main;
|
|
1433
|
+
header;
|
|
1434
|
+
status;
|
|
1435
|
+
summary;
|
|
1436
|
+
commandBar;
|
|
1437
|
+
form;
|
|
1438
|
+
searchResults;
|
|
1439
|
+
draft;
|
|
1440
|
+
committed;
|
|
1441
|
+
revision = 0;
|
|
1442
|
+
route = { kind: "project" };
|
|
1443
|
+
searchQuery = "";
|
|
1444
|
+
errors = [];
|
|
1445
|
+
conversionDiagnostics = /* @__PURE__ */ new Map();
|
|
1446
|
+
extensionErrors = /* @__PURE__ */ new Map();
|
|
1447
|
+
urlInputs = /* @__PURE__ */ new Map();
|
|
1448
|
+
controls = /* @__PURE__ */ new Map();
|
|
1449
|
+
extensionControls = /* @__PURE__ */ new Map();
|
|
1450
|
+
extensionCleanups = [];
|
|
1451
|
+
confirmation;
|
|
1452
|
+
focusRequest;
|
|
1453
|
+
saving = false;
|
|
1454
|
+
destroyed = false;
|
|
1455
|
+
composing = false;
|
|
1456
|
+
statusMessage = "";
|
|
1457
|
+
controlNumber = 0;
|
|
1458
|
+
previousFocus;
|
|
1459
|
+
testUrls = "";
|
|
1460
|
+
testMethod = "";
|
|
1461
|
+
testResourceType = "";
|
|
1462
|
+
testMaxCases = "256";
|
|
1463
|
+
testResult = void 0;
|
|
1464
|
+
testRunning = false;
|
|
1465
|
+
testRequestId = 0;
|
|
1466
|
+
constructor(options, initial, extensions) {
|
|
1467
|
+
this.root = options.root;
|
|
1468
|
+
this.document = options.root.ownerDocument;
|
|
1469
|
+
this.options = options;
|
|
1470
|
+
this.extensions = extensions;
|
|
1471
|
+
this.instanceId = `rogatio-editor-${++editorInstanceCount}`;
|
|
1472
|
+
this.draft = initial;
|
|
1473
|
+
this.committed = cloneSnapshot(initial);
|
|
1474
|
+
const initialDiagnostics = this.collectDiagnostics(this.draft);
|
|
1475
|
+
if (initialDiagnostics.length > 0) {
|
|
1476
|
+
throw new EditorInitializationError(initialDiagnostics);
|
|
1477
|
+
}
|
|
1478
|
+
this.host = this.document.createElement("div");
|
|
1479
|
+
this.host.className = "rogatio-editor";
|
|
1480
|
+
this.host.dataset.rogatioEditor = "true";
|
|
1481
|
+
const layout = this.document.createElement("div");
|
|
1482
|
+
layout.dataset.editorLayout = "true";
|
|
1483
|
+
this.rail = this.document.createElement("nav");
|
|
1484
|
+
this.rail.dataset.desktopRouteRail = "true";
|
|
1485
|
+
this.rail.setAttribute("aria-label", "Project sections");
|
|
1486
|
+
this.main = this.document.createElement("main");
|
|
1487
|
+
this.main.dataset.editorMain = "true";
|
|
1488
|
+
this.header = this.document.createElement("header");
|
|
1489
|
+
this.header.dataset.editorHeader = "true";
|
|
1490
|
+
this.status = this.document.createElement("p");
|
|
1491
|
+
this.status.dataset.editorStatus = "true";
|
|
1492
|
+
this.status.setAttribute("role", "status");
|
|
1493
|
+
this.status.setAttribute("aria-live", "polite");
|
|
1494
|
+
this.summary = this.document.createElement("section");
|
|
1495
|
+
this.summary.dataset.editorSummary = "true";
|
|
1496
|
+
this.summary.setAttribute("role", "alert");
|
|
1497
|
+
this.commandBar = this.document.createElement("div");
|
|
1498
|
+
this.commandBar.dataset.editorCommandBar = "true";
|
|
1499
|
+
this.commandBar.setAttribute("role", "toolbar");
|
|
1500
|
+
this.commandBar.setAttribute("aria-label", "Editor commands");
|
|
1501
|
+
this.form = this.document.createElement("form");
|
|
1502
|
+
this.form.dataset.editorForm = "true";
|
|
1503
|
+
this.form.noValidate = true;
|
|
1504
|
+
this.searchResults = this.document.createElement("section");
|
|
1505
|
+
this.searchResults.id = "rogatio-search-results";
|
|
1506
|
+
this.searchResults.dataset.searchResults = "true";
|
|
1507
|
+
this.host.addEventListener("click", (event) => {
|
|
1508
|
+
if (!this.searchQuery) return;
|
|
1509
|
+
const target = event.target;
|
|
1510
|
+
if (target instanceof Element && target.closest("[data-search-wrap]"))
|
|
1511
|
+
return;
|
|
1512
|
+
this.searchQuery = "";
|
|
1513
|
+
this.render();
|
|
1514
|
+
});
|
|
1515
|
+
this.main.append(
|
|
1516
|
+
this.header,
|
|
1517
|
+
this.status,
|
|
1518
|
+
this.summary,
|
|
1519
|
+
this.commandBar,
|
|
1520
|
+
this.form
|
|
1521
|
+
);
|
|
1522
|
+
layout.append(this.rail, this.main);
|
|
1523
|
+
this.host.append(layout);
|
|
1524
|
+
this.root.append(this.host);
|
|
1525
|
+
this.host.addEventListener("click", this.handleClick);
|
|
1526
|
+
this.host.addEventListener("input", this.handleInput);
|
|
1527
|
+
this.host.addEventListener("change", this.handleChange);
|
|
1528
|
+
this.host.addEventListener("submit", this.handleSubmit);
|
|
1529
|
+
this.host.addEventListener("keydown", this.handleKeydown);
|
|
1530
|
+
this.host.addEventListener("compositionstart", this.handleCompositionStart);
|
|
1531
|
+
this.host.addEventListener("compositionend", this.handleCompositionEnd);
|
|
1532
|
+
this.render();
|
|
1533
|
+
}
|
|
1534
|
+
getDraft() {
|
|
1535
|
+
return cloneSnapshot(this.draft);
|
|
1536
|
+
}
|
|
1537
|
+
isDirty() {
|
|
1538
|
+
return JSON.stringify(this.draft) !== JSON.stringify(this.committed);
|
|
1539
|
+
}
|
|
1540
|
+
validate() {
|
|
1541
|
+
if (this.destroyed) return [];
|
|
1542
|
+
this.errors = this.validateCurrent();
|
|
1543
|
+
this.statusMessage = this.errors.length === 0 ? "Project is valid." : `${this.errors.length} validation error${this.errors.length === 1 ? "" : "s"} found.`;
|
|
1544
|
+
this.focusRequest = this.errors[0]?.path;
|
|
1545
|
+
this.render();
|
|
1546
|
+
return this.errors.map((value) => ({ ...value }));
|
|
1547
|
+
}
|
|
1548
|
+
destroy() {
|
|
1549
|
+
if (this.destroyed) return;
|
|
1550
|
+
this.destroyed = true;
|
|
1551
|
+
this.cleanupExtensions();
|
|
1552
|
+
this.host.removeEventListener("click", this.handleClick);
|
|
1553
|
+
this.host.removeEventListener("input", this.handleInput);
|
|
1554
|
+
this.host.removeEventListener("change", this.handleChange);
|
|
1555
|
+
this.host.removeEventListener("submit", this.handleSubmit);
|
|
1556
|
+
this.host.removeEventListener("keydown", this.handleKeydown);
|
|
1557
|
+
this.host.removeEventListener(
|
|
1558
|
+
"compositionstart",
|
|
1559
|
+
this.handleCompositionStart
|
|
1560
|
+
);
|
|
1561
|
+
this.host.removeEventListener("compositionend", this.handleCompositionEnd);
|
|
1562
|
+
this.host.remove();
|
|
1563
|
+
}
|
|
1564
|
+
handleClick = (event) => {
|
|
1565
|
+
if (this.destroyed) return;
|
|
1566
|
+
const target = event.target;
|
|
1567
|
+
if (!(target instanceof Element)) return;
|
|
1568
|
+
const element = target.closest(
|
|
1569
|
+
"[data-command], [data-route], [data-search-result], [data-error-path]"
|
|
1570
|
+
);
|
|
1571
|
+
if (!element) return;
|
|
1572
|
+
if (element.dataset.route !== void 0) {
|
|
1573
|
+
this.navigate(element.dataset.route, element.dataset.groupId);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (element.dataset.searchResult !== void 0) {
|
|
1577
|
+
this.navigateToSearchResult(element.dataset.searchResult);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (element.dataset.errorPath !== void 0) {
|
|
1581
|
+
this.navigateToPath(element.dataset.errorPath);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const command = element.dataset.command;
|
|
1585
|
+
if (!command) return;
|
|
1586
|
+
this.dispatchCommand(command, element);
|
|
1587
|
+
};
|
|
1588
|
+
handleInput = (event) => {
|
|
1589
|
+
if (this.destroyed || this.saving) return;
|
|
1590
|
+
const target = event.target;
|
|
1591
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (target.dataset.urlSource !== void 0) {
|
|
1595
|
+
const ruleId = target.dataset.ruleId;
|
|
1596
|
+
if (ruleId) this.urlInputs.set(ruleId, target.value);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (target.dataset.search !== void 0) {
|
|
1600
|
+
this.searchQuery = target.value;
|
|
1601
|
+
this.render();
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (target.dataset.testUrls !== void 0) {
|
|
1605
|
+
this.testUrls = target.value;
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (target.dataset.testMaxCases !== void 0) {
|
|
1609
|
+
this.testMaxCases = target.value;
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const path = target.dataset.path;
|
|
1613
|
+
if (!path || target.type === "checkbox" || this.extensionControls.has(path))
|
|
1614
|
+
return;
|
|
1615
|
+
if (this.updateCommonField(path, target.value)) {
|
|
1616
|
+
if (!this.composing) this.render();
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
handleChange = (event) => {
|
|
1620
|
+
if (this.destroyed || this.saving) return;
|
|
1621
|
+
const target = event.target;
|
|
1622
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (target.dataset.mobileRoute !== void 0 && target instanceof HTMLSelectElement) {
|
|
1626
|
+
const selectedOption = target.options[target.selectedIndex];
|
|
1627
|
+
const groupId = selectedOption?.dataset.groupId;
|
|
1628
|
+
this.navigate(target.value, groupId);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (target instanceof HTMLInputElement && target.dataset.resourcePath !== void 0) {
|
|
1632
|
+
this.updateResourceTypes(
|
|
1633
|
+
target.dataset.resourcePath,
|
|
1634
|
+
target.dataset.resourceType ?? "",
|
|
1635
|
+
target.checked
|
|
1636
|
+
);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (target instanceof HTMLSelectElement && target.dataset.ruleTypeSelect !== void 0) {
|
|
1640
|
+
const ruleTypePath = target.dataset.ruleTypePath ?? "";
|
|
1641
|
+
if (ruleTypePath) this.setRuleType(ruleTypePath, target.value);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (target instanceof HTMLSelectElement && target.dataset.testMethod !== void 0) {
|
|
1645
|
+
this.testMethod = target.value || "";
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (target instanceof HTMLSelectElement && target.dataset.testResourceType !== void 0) {
|
|
1649
|
+
this.testResourceType = target.value || "";
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const path = target.dataset.path;
|
|
1653
|
+
if (!path || this.extensionControls.has(path)) return;
|
|
1654
|
+
if (this.updateCommonField(path, target.value)) this.render();
|
|
1655
|
+
};
|
|
1656
|
+
handleSubmit = (event) => {
|
|
1657
|
+
event.preventDefault();
|
|
1658
|
+
this.dispatchCommand("save", this.form);
|
|
1659
|
+
};
|
|
1660
|
+
handleKeydown = (event) => {
|
|
1661
|
+
if (event.key === "Escape" && this.confirmation) {
|
|
1662
|
+
event.preventDefault();
|
|
1663
|
+
this.confirmation = void 0;
|
|
1664
|
+
this.statusMessage = "No changes were discarded.";
|
|
1665
|
+
this.render();
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
handleCompositionStart = () => {
|
|
1669
|
+
this.composing = true;
|
|
1670
|
+
};
|
|
1671
|
+
handleCompositionEnd = (event) => {
|
|
1672
|
+
this.composing = false;
|
|
1673
|
+
const target = event.target;
|
|
1674
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
|
1675
|
+
const path = target.dataset.path;
|
|
1676
|
+
if (path && this.updateCommonField(path, target.value)) this.render();
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
dispatchCommand(command, element) {
|
|
1680
|
+
if (command === "validate") {
|
|
1681
|
+
this.validate();
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (command === "save") {
|
|
1685
|
+
void this.saveDraft();
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (command === "cancel") {
|
|
1689
|
+
this.requestCancel();
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (command === "confirm-cancel") {
|
|
1693
|
+
this.discardChanges();
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
if (command === "cancel-confirmation") {
|
|
1697
|
+
this.confirmation = void 0;
|
|
1698
|
+
this.statusMessage = "No changes were discarded.";
|
|
1699
|
+
this.render();
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (command === "confirm-remove") {
|
|
1703
|
+
this.confirmRemoval();
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
if (command === "remove-confirmation") {
|
|
1707
|
+
this.confirmation = void 0;
|
|
1708
|
+
this.statusMessage = "Removal cancelled.";
|
|
1709
|
+
this.render();
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (command === "add-group") {
|
|
1713
|
+
this.addGroup();
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if (command === "add-rule") {
|
|
1717
|
+
const groupId = element.dataset.groupId ?? this.currentGroupId();
|
|
1718
|
+
if (groupId) this.addRule(groupId);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (command === "move-group-up" || command === "move-group-down") {
|
|
1722
|
+
const groupId = element.dataset.groupId;
|
|
1723
|
+
if (groupId) this.moveGroup(groupId, command.endsWith("up") ? -1 : 1);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (command === "move-rule-up" || command === "move-rule-down") {
|
|
1727
|
+
const groupId = element.dataset.groupId;
|
|
1728
|
+
const ruleId = element.dataset.ruleId;
|
|
1729
|
+
if (groupId && ruleId) {
|
|
1730
|
+
this.moveRule(groupId, ruleId, command.endsWith("up") ? -1 : 1);
|
|
1731
|
+
}
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if (command === "remove-group") {
|
|
1735
|
+
const groupId = element.dataset.groupId;
|
|
1736
|
+
if (groupId) this.requestRemoveGroup(groupId);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (command === "remove-rule") {
|
|
1740
|
+
const groupId = element.dataset.groupId;
|
|
1741
|
+
const ruleId = element.dataset.ruleId;
|
|
1742
|
+
if (groupId && ruleId) this.requestRemoveRule(groupId, ruleId);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
if (command === "add-group-origin") {
|
|
1746
|
+
const groupId = element.dataset.groupId;
|
|
1747
|
+
if (groupId) this.addGroupOrigin(groupId);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (command === "remove-group-origin") {
|
|
1751
|
+
const groupId = element.dataset.groupId;
|
|
1752
|
+
const index = Number(element.dataset.index);
|
|
1753
|
+
if (groupId && Number.isSafeInteger(index))
|
|
1754
|
+
this.removeGroupOrigin(groupId, index);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (command === "add-rule-origin") {
|
|
1758
|
+
const groupId = element.dataset.groupId;
|
|
1759
|
+
const ruleId = element.dataset.ruleId;
|
|
1760
|
+
if (groupId && ruleId) this.addRuleOrigin(groupId, ruleId);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (command === "remove-rule-origin") {
|
|
1764
|
+
const groupId = element.dataset.groupId;
|
|
1765
|
+
const ruleId = element.dataset.ruleId;
|
|
1766
|
+
const index = Number(element.dataset.index);
|
|
1767
|
+
if (groupId && ruleId && Number.isSafeInteger(index)) {
|
|
1768
|
+
this.removeRuleOrigin(groupId, ruleId, index);
|
|
1769
|
+
}
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (command === "convert-url") {
|
|
1773
|
+
const groupId = element.dataset.groupId;
|
|
1774
|
+
const ruleId = element.dataset.ruleId;
|
|
1775
|
+
if (groupId && ruleId) this.convertUrl(groupId, ruleId);
|
|
1776
|
+
}
|
|
1777
|
+
if (command === "test:run") {
|
|
1778
|
+
void this.runTest();
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
collectDiagnostics(value) {
|
|
1783
|
+
let hostDiagnostics;
|
|
1784
|
+
try {
|
|
1785
|
+
const input = cloneSnapshot(value);
|
|
1786
|
+
hostDiagnostics = this.options.validate(input);
|
|
1787
|
+
} catch {
|
|
1788
|
+
return [
|
|
1789
|
+
diagnostic(
|
|
1790
|
+
"editor.validation-failed",
|
|
1791
|
+
"",
|
|
1792
|
+
"Project validation could not be completed."
|
|
1793
|
+
)
|
|
1794
|
+
];
|
|
1795
|
+
}
|
|
1796
|
+
const diagnostics = normalizeDiagnostics(hostDiagnostics);
|
|
1797
|
+
const project = asDraftProject(value);
|
|
1798
|
+
if (!project) return diagnostics;
|
|
1799
|
+
this.extensionErrors.clear();
|
|
1800
|
+
for (let groupIndex = 0; groupIndex < project.groups.length; groupIndex += 1) {
|
|
1801
|
+
const group = project.groups[groupIndex];
|
|
1802
|
+
for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex += 1) {
|
|
1803
|
+
const rule = group.rules[ruleIndex];
|
|
1804
|
+
const rulePath = pointer("groups", groupIndex, "rules", ruleIndex);
|
|
1805
|
+
const match = this.findExtension(rule, rulePath);
|
|
1806
|
+
if (match.error) {
|
|
1807
|
+
diagnostics.push(match.error);
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (!match.extension) continue;
|
|
1811
|
+
try {
|
|
1812
|
+
const ruleSnapshot = freezeSnapshot(
|
|
1813
|
+
cloneSnapshot(rule)
|
|
1814
|
+
);
|
|
1815
|
+
const extensionDiagnostics = normalizeDiagnostics(
|
|
1816
|
+
match.extension.validate(ruleSnapshot, rulePath)
|
|
1817
|
+
);
|
|
1818
|
+
for (const extensionDiagnostic of extensionDiagnostics) {
|
|
1819
|
+
diagnostics.push({
|
|
1820
|
+
...extensionDiagnostic,
|
|
1821
|
+
path: this.extensionDiagnosticPath(
|
|
1822
|
+
extensionDiagnostic.path,
|
|
1823
|
+
rulePath
|
|
1824
|
+
)
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
} catch {
|
|
1828
|
+
diagnostics.push(
|
|
1829
|
+
diagnostic(
|
|
1830
|
+
"editor.extension-failed",
|
|
1831
|
+
rulePath,
|
|
1832
|
+
"An additional rule field could not be validated."
|
|
1833
|
+
)
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return stableDiagnostics(diagnostics);
|
|
1839
|
+
}
|
|
1840
|
+
validateCurrent() {
|
|
1841
|
+
const diagnostics = this.collectDiagnostics(this.draft);
|
|
1842
|
+
diagnostics.push(...this.conversionDiagnostics.values());
|
|
1843
|
+
return stableDiagnostics(diagnostics);
|
|
1844
|
+
}
|
|
1845
|
+
findExtension(rule, rulePath) {
|
|
1846
|
+
const matches = [];
|
|
1847
|
+
let snapshot;
|
|
1848
|
+
try {
|
|
1849
|
+
snapshot = freezeSnapshot(
|
|
1850
|
+
cloneSnapshot(rule)
|
|
1851
|
+
);
|
|
1852
|
+
} catch {
|
|
1853
|
+
return {
|
|
1854
|
+
error: diagnostic(
|
|
1855
|
+
"editor.extension-failed",
|
|
1856
|
+
rulePath,
|
|
1857
|
+
"An additional rule field could not be read safely."
|
|
1858
|
+
)
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
for (const extension of this.extensions) {
|
|
1862
|
+
try {
|
|
1863
|
+
if (extension.matches(snapshot)) matches.push(extension);
|
|
1864
|
+
} catch {
|
|
1865
|
+
return {
|
|
1866
|
+
error: diagnostic(
|
|
1867
|
+
"editor.extension-failed",
|
|
1868
|
+
rulePath,
|
|
1869
|
+
"An additional rule field could not be identified."
|
|
1870
|
+
)
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (matches.length > 1) {
|
|
1875
|
+
return {
|
|
1876
|
+
error: diagnostic(
|
|
1877
|
+
"editor.extension-ambiguous",
|
|
1878
|
+
rulePath,
|
|
1879
|
+
"More than one additional rule field set matches this rule."
|
|
1880
|
+
)
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
return { extension: matches[0] };
|
|
1884
|
+
}
|
|
1885
|
+
extensionDiagnosticPath(path, rulePath) {
|
|
1886
|
+
if (path === "") return rulePath;
|
|
1887
|
+
return path.startsWith(rulePath) ? path : `${rulePath}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1888
|
+
}
|
|
1889
|
+
updateCommonField(path, rawValue) {
|
|
1890
|
+
if (this.saving) return false;
|
|
1891
|
+
let value = rawValue;
|
|
1892
|
+
const segments = decodePointer(path);
|
|
1893
|
+
const finalSegment = segments?.at(-1);
|
|
1894
|
+
if (finalSegment === "priority") {
|
|
1895
|
+
value = rawValue === "" ? "" : Number(rawValue);
|
|
1896
|
+
} else if (finalSegment === "method") {
|
|
1897
|
+
if (rawValue === "") {
|
|
1898
|
+
const changed2 = deleteValueAtPath(this.draft, path);
|
|
1899
|
+
if (changed2) this.markChanged();
|
|
1900
|
+
return changed2;
|
|
1901
|
+
}
|
|
1902
|
+
value = rawValue;
|
|
1903
|
+
} else if (finalSegment === "description" && rawValue === "") {
|
|
1904
|
+
const changed2 = deleteValueAtPath(this.draft, path);
|
|
1905
|
+
if (changed2) this.markChanged();
|
|
1906
|
+
return changed2;
|
|
1907
|
+
} else if (finalSegment === "type") {
|
|
1908
|
+
const ruleContainerPath = pointer(...segments?.slice(0, -1) ?? []);
|
|
1909
|
+
if (rawValue === "") {
|
|
1910
|
+
const changedType = deleteValueAtPath(this.draft, path);
|
|
1911
|
+
const ruleContainer = valueAtPath(this.draft, ruleContainerPath);
|
|
1912
|
+
const cleared = clearActionFields(ruleContainer);
|
|
1913
|
+
if (changedType || cleared) this.markChanged();
|
|
1914
|
+
return changedType || cleared;
|
|
1915
|
+
}
|
|
1916
|
+
const changed2 = setValueAtPath(this.draft, path, rawValue);
|
|
1917
|
+
if (changed2) {
|
|
1918
|
+
const ruleContainer = valueAtPath(this.draft, ruleContainerPath);
|
|
1919
|
+
clearActionFields(
|
|
1920
|
+
ruleContainer,
|
|
1921
|
+
rawValue === "redirect" || rawValue === "mock" ? rawValue : void 0
|
|
1922
|
+
);
|
|
1923
|
+
this.markChanged();
|
|
1924
|
+
}
|
|
1925
|
+
return changed2;
|
|
1926
|
+
}
|
|
1927
|
+
const changed = setValueAtPath(this.draft, path, value);
|
|
1928
|
+
if (changed) this.markChanged();
|
|
1929
|
+
return changed;
|
|
1930
|
+
}
|
|
1931
|
+
updateResourceTypes(path, resourceType, checked) {
|
|
1932
|
+
const values = valueAtPath(this.draft, path);
|
|
1933
|
+
if (!Array.isArray(values) || !RESOURCE_TYPES2.includes(resourceType)) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const index = values.indexOf(resourceType);
|
|
1937
|
+
if (checked && index === -1) values.push(resourceType);
|
|
1938
|
+
if (!checked && index !== -1) values.splice(index, 1);
|
|
1939
|
+
if (checked && index === -1 || !checked && index !== -1) {
|
|
1940
|
+
this.markChanged();
|
|
1941
|
+
this.render();
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
setRuleType(rulePath, typeId) {
|
|
1945
|
+
if (this.saving) return;
|
|
1946
|
+
const ruleContainer = valueAtPath(this.draft, rulePath);
|
|
1947
|
+
if (typeId === "") {
|
|
1948
|
+
const changedType2 = deleteValueAtPath(this.draft, `${rulePath}/type`);
|
|
1949
|
+
const cleared2 = clearActionFields(ruleContainer);
|
|
1950
|
+
if (changedType2 || cleared2) {
|
|
1951
|
+
this.markChanged();
|
|
1952
|
+
this.render();
|
|
1953
|
+
}
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const extension = this.extensions.find((entry) => entry.id === typeId);
|
|
1957
|
+
if (!extension?.defaultAction) return;
|
|
1958
|
+
const actionField = extension.actionField ?? "action";
|
|
1959
|
+
const changedType = setValueAtPath(this.draft, `${rulePath}/type`, typeId);
|
|
1960
|
+
const changedAction = setValueAtPath(
|
|
1961
|
+
this.draft,
|
|
1962
|
+
`${rulePath}/${actionField}`,
|
|
1963
|
+
extension.defaultAction()
|
|
1964
|
+
);
|
|
1965
|
+
const cleared = clearActionFields(ruleContainer, actionField);
|
|
1966
|
+
if (changedType || changedAction || cleared) {
|
|
1967
|
+
this.markChanged();
|
|
1968
|
+
this.render();
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
markChanged() {
|
|
1972
|
+
this.revision += 1;
|
|
1973
|
+
this.errors = [];
|
|
1974
|
+
this.conversionDiagnostics.clear();
|
|
1975
|
+
this.extensionErrors.clear();
|
|
1976
|
+
this.statusMessage = "";
|
|
1977
|
+
}
|
|
1978
|
+
allIds() {
|
|
1979
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1980
|
+
for (const group of this.draft.groups) {
|
|
1981
|
+
if (typeof group.id === "string") ids.add(group.id);
|
|
1982
|
+
for (const rule of group.rules) {
|
|
1983
|
+
if (typeof rule.id === "string") ids.add(rule.id);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return ids;
|
|
1987
|
+
}
|
|
1988
|
+
nextId(prefix) {
|
|
1989
|
+
const ids = this.allIds();
|
|
1990
|
+
let candidate = prefix;
|
|
1991
|
+
let suffix = 2;
|
|
1992
|
+
while (ids.has(candidate)) candidate = `${prefix}-${suffix++}`;
|
|
1993
|
+
return candidate;
|
|
1994
|
+
}
|
|
1995
|
+
addGroup() {
|
|
1996
|
+
if (this.saving) return;
|
|
1997
|
+
const groupId = this.nextId("group-new");
|
|
1998
|
+
this.draft.groups.push({
|
|
1999
|
+
id: groupId,
|
|
2000
|
+
name: "New group",
|
|
2001
|
+
origins: [],
|
|
2002
|
+
rules: []
|
|
2003
|
+
});
|
|
2004
|
+
this.markChanged();
|
|
2005
|
+
this.route = { kind: "group", groupId };
|
|
2006
|
+
this.focusRequest = pointer("groups", this.draft.groups.length - 1, "name");
|
|
2007
|
+
this.statusMessage = "Group added.";
|
|
2008
|
+
this.render();
|
|
2009
|
+
}
|
|
2010
|
+
addRule(groupId) {
|
|
2011
|
+
if (this.saving) return;
|
|
2012
|
+
const group = this.groupById(groupId);
|
|
2013
|
+
if (!group) return;
|
|
2014
|
+
const ruleId = this.nextId("rule-new");
|
|
2015
|
+
group.rules.push({
|
|
2016
|
+
id: ruleId,
|
|
2017
|
+
name: "New rule",
|
|
2018
|
+
urlRegex: "",
|
|
2019
|
+
origins: [],
|
|
2020
|
+
resourceTypes: ["main_frame"],
|
|
2021
|
+
priority: 100
|
|
2022
|
+
});
|
|
2023
|
+
this.markChanged();
|
|
2024
|
+
this.route = { kind: "group", groupId };
|
|
2025
|
+
const groupIndex = this.groupIndex(groupId);
|
|
2026
|
+
this.focusRequest = pointer(
|
|
2027
|
+
"groups",
|
|
2028
|
+
groupIndex,
|
|
2029
|
+
"rules",
|
|
2030
|
+
group.rules.length - 1,
|
|
2031
|
+
"name"
|
|
2032
|
+
);
|
|
2033
|
+
this.statusMessage = "Rule added.";
|
|
2034
|
+
this.render();
|
|
2035
|
+
}
|
|
2036
|
+
moveGroup(groupId, delta) {
|
|
2037
|
+
if (this.saving) return;
|
|
2038
|
+
const index = this.groupIndex(groupId);
|
|
2039
|
+
const next = index + delta;
|
|
2040
|
+
if (index < 0 || next < 0 || next >= this.draft.groups.length) return;
|
|
2041
|
+
const [group] = this.draft.groups.splice(index, 1);
|
|
2042
|
+
this.draft.groups.splice(next, 0, group);
|
|
2043
|
+
this.markChanged();
|
|
2044
|
+
this.focusRequest = pointer("groups", next, "name");
|
|
2045
|
+
this.statusMessage = `Group moved to position ${next + 1} of ${this.draft.groups.length}.`;
|
|
2046
|
+
this.render();
|
|
2047
|
+
}
|
|
2048
|
+
moveRule(groupId, ruleId, delta) {
|
|
2049
|
+
if (this.saving) return;
|
|
2050
|
+
const group = this.groupById(groupId);
|
|
2051
|
+
if (!group) return;
|
|
2052
|
+
const index = this.ruleIndex(group, ruleId);
|
|
2053
|
+
const next = index + delta;
|
|
2054
|
+
if (index < 0 || next < 0 || next >= group.rules.length) return;
|
|
2055
|
+
const [rule] = group.rules.splice(index, 1);
|
|
2056
|
+
group.rules.splice(next, 0, rule);
|
|
2057
|
+
this.markChanged();
|
|
2058
|
+
const groupIndex = this.groupIndex(groupId);
|
|
2059
|
+
this.focusRequest = pointer("groups", groupIndex, "rules", next, "name");
|
|
2060
|
+
this.statusMessage = `Rule moved to position ${next + 1} of ${group.rules.length}.`;
|
|
2061
|
+
this.render();
|
|
2062
|
+
}
|
|
2063
|
+
requestRemoveGroup(groupId) {
|
|
2064
|
+
if (this.saving) return;
|
|
2065
|
+
const group = this.groupById(groupId);
|
|
2066
|
+
if (!group) return;
|
|
2067
|
+
this.confirmation = {
|
|
2068
|
+
kind: "remove-group",
|
|
2069
|
+
groupId,
|
|
2070
|
+
name: displayName(group.name, "Unnamed group")
|
|
2071
|
+
};
|
|
2072
|
+
this.focusRequest = "confirm-cancel";
|
|
2073
|
+
this.render();
|
|
2074
|
+
}
|
|
2075
|
+
requestRemoveRule(groupId, ruleId) {
|
|
2076
|
+
if (this.saving) return;
|
|
2077
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
2078
|
+
if (!rule) return;
|
|
2079
|
+
this.confirmation = {
|
|
2080
|
+
kind: "remove-rule",
|
|
2081
|
+
groupId,
|
|
2082
|
+
ruleId,
|
|
2083
|
+
name: displayName(rule.name, "Unnamed rule")
|
|
2084
|
+
};
|
|
2085
|
+
this.focusRequest = "confirm-cancel";
|
|
2086
|
+
this.render();
|
|
2087
|
+
}
|
|
2088
|
+
confirmRemoval() {
|
|
2089
|
+
if (!this.confirmation || this.confirmation.kind === "cancel") return;
|
|
2090
|
+
const confirmation = this.confirmation;
|
|
2091
|
+
this.confirmation = void 0;
|
|
2092
|
+
if (confirmation.kind === "remove-group") {
|
|
2093
|
+
const index = this.groupIndex(confirmation.groupId);
|
|
2094
|
+
if (index < 0) return;
|
|
2095
|
+
this.draft.groups.splice(index, 1);
|
|
2096
|
+
this.markChanged();
|
|
2097
|
+
if (this.route.kind === "group" && this.route.groupId === confirmation.groupId) {
|
|
2098
|
+
this.route = { kind: "project" };
|
|
2099
|
+
}
|
|
2100
|
+
this.statusMessage = `Group ${confirmation.name} removed.`;
|
|
2101
|
+
} else {
|
|
2102
|
+
const group = this.groupById(confirmation.groupId);
|
|
2103
|
+
if (!group) return;
|
|
2104
|
+
const index = this.ruleIndex(group, confirmation.ruleId);
|
|
2105
|
+
if (index < 0) return;
|
|
2106
|
+
group.rules.splice(index, 1);
|
|
2107
|
+
this.markChanged();
|
|
2108
|
+
this.statusMessage = `Rule ${confirmation.name} removed.`;
|
|
2109
|
+
}
|
|
2110
|
+
this.render();
|
|
2111
|
+
}
|
|
2112
|
+
requestCancel() {
|
|
2113
|
+
if (this.saving) return;
|
|
2114
|
+
if (!this.isDirty()) {
|
|
2115
|
+
this.statusMessage = "No changes to discard.";
|
|
2116
|
+
try {
|
|
2117
|
+
this.options.onCancel?.();
|
|
2118
|
+
} catch {
|
|
2119
|
+
this.statusMessage = "The host could not complete cancel.";
|
|
2120
|
+
}
|
|
2121
|
+
this.render();
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
this.confirmation = { kind: "cancel" };
|
|
2125
|
+
this.focusRequest = "confirm-cancel";
|
|
2126
|
+
this.render();
|
|
2127
|
+
}
|
|
2128
|
+
discardChanges() {
|
|
2129
|
+
if (this.saving || !this.confirmation || this.confirmation.kind !== "cancel") {
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
this.draft = cloneSnapshot(this.committed);
|
|
2133
|
+
this.revision += 1;
|
|
2134
|
+
this.errors = [];
|
|
2135
|
+
this.conversionDiagnostics.clear();
|
|
2136
|
+
this.confirmation = void 0;
|
|
2137
|
+
this.statusMessage = "Changes discarded.";
|
|
2138
|
+
try {
|
|
2139
|
+
this.options.onCancel?.();
|
|
2140
|
+
} catch {
|
|
2141
|
+
this.statusMessage = "The host could not complete cancel.";
|
|
2142
|
+
}
|
|
2143
|
+
this.render();
|
|
2144
|
+
}
|
|
2145
|
+
async saveDraft() {
|
|
2146
|
+
if (this.destroyed || this.saving || !this.isDirty()) return;
|
|
2147
|
+
const validation = this.validateCurrent();
|
|
2148
|
+
if (validation.length > 0) {
|
|
2149
|
+
this.errors = validation;
|
|
2150
|
+
this.statusMessage = `${validation.length} validation error${validation.length === 1 ? "" : "s"} found.`;
|
|
2151
|
+
this.focusRequest = validation[0]?.path;
|
|
2152
|
+
this.render();
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
const revision = this.revision;
|
|
2156
|
+
const snapshot = cloneSnapshot(this.draft);
|
|
2157
|
+
this.saving = true;
|
|
2158
|
+
this.statusMessage = "Saving...";
|
|
2159
|
+
this.render();
|
|
2160
|
+
let result;
|
|
2161
|
+
try {
|
|
2162
|
+
result = await this.options.save(
|
|
2163
|
+
cloneSnapshot(snapshot)
|
|
2164
|
+
);
|
|
2165
|
+
} catch {
|
|
2166
|
+
result = {
|
|
2167
|
+
ok: false,
|
|
2168
|
+
code: "editor.save-failed",
|
|
2169
|
+
message: "The host could not save the project."
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
if (this.destroyed) return;
|
|
2173
|
+
this.saving = false;
|
|
2174
|
+
if (revision !== this.revision) {
|
|
2175
|
+
this.statusMessage = "The save result was stale; current edits were kept.";
|
|
2176
|
+
this.render();
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (isSaveSuccess(result)) {
|
|
2180
|
+
this.committed = cloneSnapshot(snapshot);
|
|
2181
|
+
this.errors = [];
|
|
2182
|
+
this.statusMessage = "Saved";
|
|
2183
|
+
this.render();
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const failure = saveFailureDiagnostic(result);
|
|
2187
|
+
this.errors = [failure];
|
|
2188
|
+
this.statusMessage = failure.message;
|
|
2189
|
+
this.focusRequest = failure.path;
|
|
2190
|
+
this.render();
|
|
2191
|
+
}
|
|
2192
|
+
addGroupOrigin(groupId) {
|
|
2193
|
+
const group = this.groupById(groupId);
|
|
2194
|
+
if (!group || this.saving) return;
|
|
2195
|
+
group.origins.push("");
|
|
2196
|
+
this.markChanged();
|
|
2197
|
+
this.focusRequest = pointer(
|
|
2198
|
+
"groups",
|
|
2199
|
+
this.groupIndex(groupId),
|
|
2200
|
+
"origins",
|
|
2201
|
+
group.origins.length - 1
|
|
2202
|
+
);
|
|
2203
|
+
this.render();
|
|
2204
|
+
}
|
|
2205
|
+
removeGroupOrigin(groupId, index) {
|
|
2206
|
+
const group = this.groupById(groupId);
|
|
2207
|
+
if (!group || this.saving || index < 0 || index >= group.origins.length)
|
|
2208
|
+
return;
|
|
2209
|
+
group.origins.splice(index, 1);
|
|
2210
|
+
this.markChanged();
|
|
2211
|
+
this.render();
|
|
2212
|
+
}
|
|
2213
|
+
addRuleOrigin(groupId, ruleId) {
|
|
2214
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
2215
|
+
if (!rule || this.saving) return;
|
|
2216
|
+
rule.origins.push("");
|
|
2217
|
+
this.markChanged();
|
|
2218
|
+
this.focusRequest = pointer(
|
|
2219
|
+
"groups",
|
|
2220
|
+
this.groupIndex(groupId),
|
|
2221
|
+
"rules",
|
|
2222
|
+
this.ruleIndex(this.groupById(groupId), ruleId),
|
|
2223
|
+
"origins",
|
|
2224
|
+
rule.origins.length - 1
|
|
2225
|
+
);
|
|
2226
|
+
this.render();
|
|
2227
|
+
}
|
|
2228
|
+
removeRuleOrigin(groupId, ruleId, index) {
|
|
2229
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
2230
|
+
if (!rule || this.saving || index < 0 || index >= rule.origins.length)
|
|
2231
|
+
return;
|
|
2232
|
+
rule.origins.splice(index, 1);
|
|
2233
|
+
this.markChanged();
|
|
2234
|
+
this.render();
|
|
2235
|
+
}
|
|
2236
|
+
convertUrl(groupId, ruleId) {
|
|
2237
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
2238
|
+
if (!rule || this.saving) return;
|
|
2239
|
+
const input = this.host.querySelector(
|
|
2240
|
+
`[data-url-source][data-rule-id="${CSS.escape(ruleId)}"]`
|
|
2241
|
+
);
|
|
2242
|
+
const value = input?.value ?? this.urlInputs.get(ruleId) ?? "";
|
|
2243
|
+
this.urlInputs.set(ruleId, value);
|
|
2244
|
+
const result = urlToExactRegex(value);
|
|
2245
|
+
const rulePath = pointer(
|
|
2246
|
+
"groups",
|
|
2247
|
+
this.groupIndex(groupId),
|
|
2248
|
+
"rules",
|
|
2249
|
+
this.ruleIndex(this.groupById(groupId), ruleId)
|
|
2250
|
+
);
|
|
2251
|
+
const path = `${rulePath}/urlRegex`;
|
|
2252
|
+
if (!result.ok) {
|
|
2253
|
+
this.conversionDiagnostics.set(
|
|
2254
|
+
ruleId,
|
|
2255
|
+
diagnostic(
|
|
2256
|
+
result.code,
|
|
2257
|
+
path,
|
|
2258
|
+
result.code === "editor.url-too-long" ? "The exact URL regular expression is too long." : "Enter a valid request URL without credentials or fragments."
|
|
2259
|
+
)
|
|
2260
|
+
);
|
|
2261
|
+
this.errors = [...this.conversionDiagnostics.values()];
|
|
2262
|
+
this.statusMessage = "The URL could not be converted.";
|
|
2263
|
+
this.focusRequest = path;
|
|
2264
|
+
this.render();
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (setValueAtPath(this.draft, path, result.source)) this.markChanged();
|
|
2268
|
+
this.statusMessage = "Exact URL regular expression created.";
|
|
2269
|
+
this.focusRequest = path;
|
|
2270
|
+
this.render();
|
|
2271
|
+
}
|
|
2272
|
+
async runTest() {
|
|
2273
|
+
if (!this.options.dryRun) {
|
|
2274
|
+
this.statusMessage = "Dry-run is not configured for this editor instance.";
|
|
2275
|
+
this.render();
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const urlsText = this.testUrls.trim();
|
|
2279
|
+
if (!urlsText) {
|
|
2280
|
+
this.statusMessage = "Please enter at least one URL to test.";
|
|
2281
|
+
this.render();
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
const lines = urlsText.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2285
|
+
const cases = lines.map((url) => ({
|
|
2286
|
+
url,
|
|
2287
|
+
method: this.testMethod === "" ? void 0 : this.testMethod,
|
|
2288
|
+
resourceType: this.testResourceType === "" ? void 0 : this.testResourceType
|
|
2289
|
+
}));
|
|
2290
|
+
const maxCasesRaw = Number.parseInt(this.testMaxCases, 10);
|
|
2291
|
+
const maxCases = Number.isSafeInteger(maxCasesRaw) && maxCasesRaw > 0 ? maxCasesRaw : void 0;
|
|
2292
|
+
const requestId = ++this.testRequestId;
|
|
2293
|
+
this.testRunning = true;
|
|
2294
|
+
this.statusMessage = "Running dry-run...";
|
|
2295
|
+
this.render();
|
|
2296
|
+
try {
|
|
2297
|
+
const result = await this.options.dryRun(
|
|
2298
|
+
this.getDraft(),
|
|
2299
|
+
cases,
|
|
2300
|
+
maxCases === void 0 ? void 0 : { maxCases }
|
|
2301
|
+
);
|
|
2302
|
+
if (requestId !== this.testRequestId) return;
|
|
2303
|
+
this.testResult = result;
|
|
2304
|
+
this.testRunning = false;
|
|
2305
|
+
const matched = result.summary.matchedUrlCount;
|
|
2306
|
+
const total = result.summary.urlCount;
|
|
2307
|
+
this.statusMessage = `Test complete: ${matched}/${total} URLs matched at least one rule.`;
|
|
2308
|
+
} catch (e) {
|
|
2309
|
+
if (requestId !== this.testRequestId) return;
|
|
2310
|
+
this.testRunning = false;
|
|
2311
|
+
const message = e instanceof Error ? e.message : "Test failed";
|
|
2312
|
+
this.statusMessage = `Test error: ${message}`;
|
|
2313
|
+
}
|
|
2314
|
+
this.render();
|
|
2315
|
+
}
|
|
2316
|
+
renderTestResults(result) {
|
|
2317
|
+
const resultsSection = this.host.querySelector(
|
|
2318
|
+
"[data-test-results]"
|
|
2319
|
+
);
|
|
2320
|
+
if (!resultsSection) return;
|
|
2321
|
+
resultsSection.replaceChildren();
|
|
2322
|
+
const heading = this.document.createElement("h3");
|
|
2323
|
+
heading.textContent = "Results";
|
|
2324
|
+
resultsSection.append(heading);
|
|
2325
|
+
if (result.errors.length > 0) {
|
|
2326
|
+
const errorsList = this.document.createElement("ul");
|
|
2327
|
+
errorsList.dataset.testErrors = "true";
|
|
2328
|
+
for (const err of result.errors) {
|
|
2329
|
+
const item = this.document.createElement("li");
|
|
2330
|
+
item.textContent = `${err.code}: ${err.message}${err.index !== void 0 ? ` (case ${err.index})` : ""}`;
|
|
2331
|
+
errorsList.append(item);
|
|
2332
|
+
}
|
|
2333
|
+
resultsSection.append(errorsList);
|
|
2334
|
+
}
|
|
2335
|
+
if (result.results.length === 0 && result.errors.length === 0) {
|
|
2336
|
+
const empty = this.document.createElement("p");
|
|
2337
|
+
empty.dataset.testEmpty = "true";
|
|
2338
|
+
empty.textContent = "No valid URLs to test.";
|
|
2339
|
+
resultsSection.append(empty);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
for (const urlResult of result.results) {
|
|
2343
|
+
const card = this.document.createElement("article");
|
|
2344
|
+
card.dataset.testResultCard = "true";
|
|
2345
|
+
const matched = urlResult.matchedRuleCount > 0;
|
|
2346
|
+
card.dataset.matched = matched ? "true" : "false";
|
|
2347
|
+
const urlHeader = this.document.createElement("div");
|
|
2348
|
+
urlHeader.dataset.testResultHeader = "true";
|
|
2349
|
+
const urlText = this.document.createElement("strong");
|
|
2350
|
+
urlText.textContent = urlResult.url;
|
|
2351
|
+
const urlBadge = this.document.createElement("span");
|
|
2352
|
+
urlBadge.dataset.testBadge = "true";
|
|
2353
|
+
urlBadge.dataset.variant = matched ? "matched" : "unmatched";
|
|
2354
|
+
urlBadge.textContent = matched ? "MATCHED" : "NO MATCH";
|
|
2355
|
+
urlHeader.append(urlText, urlBadge);
|
|
2356
|
+
card.append(urlHeader);
|
|
2357
|
+
for (const rule of urlResult.rules) {
|
|
2358
|
+
const ruleDiv = this.document.createElement("div");
|
|
2359
|
+
ruleDiv.dataset.testRule = "true";
|
|
2360
|
+
const ruleHeader = this.document.createElement("div");
|
|
2361
|
+
ruleHeader.dataset.testRuleHeader = "true";
|
|
2362
|
+
const ruleName = this.document.createElement("strong");
|
|
2363
|
+
ruleName.textContent = `${rule.groupId}/${rule.ruleId}`;
|
|
2364
|
+
const ruleBadge = this.document.createElement("span");
|
|
2365
|
+
ruleBadge.dataset.testBadge = "true";
|
|
2366
|
+
ruleBadge.dataset.variant = rule.matched ? "matched" : "unmatched";
|
|
2367
|
+
ruleBadge.textContent = rule.matched ? "\u2713 MATCHED" : "\u2717 NOT MATCHED";
|
|
2368
|
+
ruleHeader.append(ruleName, ruleBadge);
|
|
2369
|
+
ruleDiv.append(ruleHeader);
|
|
2370
|
+
const dims = [
|
|
2371
|
+
{ label: "urlRegex", dim: rule.urlRegex },
|
|
2372
|
+
{ label: "effectiveOrigin", dim: rule.effectiveOrigin },
|
|
2373
|
+
{ label: "method", dim: rule.method },
|
|
2374
|
+
{ label: "resourceType", dim: rule.resourceType }
|
|
2375
|
+
];
|
|
2376
|
+
for (const { label, dim } of dims) {
|
|
2377
|
+
const dimDiv = this.document.createElement("div");
|
|
2378
|
+
dimDiv.dataset.testDimension = "true";
|
|
2379
|
+
const badge = this.document.createElement("span");
|
|
2380
|
+
badge.dataset.testBadge = "true";
|
|
2381
|
+
badge.dataset.variant = dim.state === "matched" ? "matched" : dim.state === "unmatched" ? "unmatched" : "na";
|
|
2382
|
+
badge.textContent = dim.state.toUpperCase();
|
|
2383
|
+
dimDiv.append(
|
|
2384
|
+
badge,
|
|
2385
|
+
this.document.createTextNode(`${label}: ${dim.detail}`)
|
|
2386
|
+
);
|
|
2387
|
+
ruleDiv.append(dimDiv);
|
|
2388
|
+
}
|
|
2389
|
+
if (rule.actionPreview) {
|
|
2390
|
+
const apDiv = this.document.createElement("div");
|
|
2391
|
+
apDiv.dataset.testActionPreview = "true";
|
|
2392
|
+
apDiv.textContent = `Action preview: ${rule.actionPreview.kind} - ${rule.actionPreview.summary}`;
|
|
2393
|
+
ruleDiv.append(apDiv);
|
|
2394
|
+
}
|
|
2395
|
+
card.append(ruleDiv);
|
|
2396
|
+
}
|
|
2397
|
+
resultsSection.append(card);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
navigate(route, groupId) {
|
|
2401
|
+
if (route === "project") {
|
|
2402
|
+
this.route = { kind: "project" };
|
|
2403
|
+
} else if (route === "test") {
|
|
2404
|
+
this.route = { kind: "test" };
|
|
2405
|
+
} else if (route === "group" && groupId && this.groupById(groupId)) {
|
|
2406
|
+
this.route = { kind: "group", groupId };
|
|
2407
|
+
} else {
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
this.testRequestId += 1;
|
|
2411
|
+
this.statusMessage = "";
|
|
2412
|
+
this.render();
|
|
2413
|
+
}
|
|
2414
|
+
navigateToGroup(groupId) {
|
|
2415
|
+
const groupIds = new Set(
|
|
2416
|
+
this.draft.groups.map((group) => typeof group.id === "string" ? group.id : "").filter((id) => id.length > 0)
|
|
2417
|
+
);
|
|
2418
|
+
const resolved = resolveGroupRoute(groupId, groupIds);
|
|
2419
|
+
if (resolved.kind === "group") {
|
|
2420
|
+
this.route = { kind: "group", groupId: resolved.groupId };
|
|
2421
|
+
} else {
|
|
2422
|
+
this.route = { kind: "project" };
|
|
2423
|
+
}
|
|
2424
|
+
this.testRequestId += 1;
|
|
2425
|
+
this.statusMessage = "";
|
|
2426
|
+
this.render();
|
|
2427
|
+
}
|
|
2428
|
+
navigateToSearchResult(path) {
|
|
2429
|
+
const segments = decodePointer(path);
|
|
2430
|
+
if (segments?.[0] !== "groups") {
|
|
2431
|
+
this.route = { kind: "project" };
|
|
2432
|
+
this.searchQuery = "";
|
|
2433
|
+
this.render();
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const groupIndex = arrayIndex(segments[1] ?? "");
|
|
2437
|
+
const group = groupIndex === void 0 ? void 0 : this.draft.groups[groupIndex];
|
|
2438
|
+
if (!group || typeof group.id !== "string") return;
|
|
2439
|
+
const ruleIndex = arrayIndex(segments[3] ?? "");
|
|
2440
|
+
const rule = ruleIndex === void 0 ? void 0 : group.rules[ruleIndex];
|
|
2441
|
+
this.route = { kind: "group", groupId: group.id };
|
|
2442
|
+
this.searchQuery = "";
|
|
2443
|
+
this.statusMessage = "Jumped to rule.";
|
|
2444
|
+
this.render();
|
|
2445
|
+
if (rule && typeof rule.id === "string") {
|
|
2446
|
+
const anchor = `rogatio-rule-${safeText(group.id)}-${safeText(rule.id)}`;
|
|
2447
|
+
const card = this.document.getElementById(anchor);
|
|
2448
|
+
if (card) {
|
|
2449
|
+
card.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
2450
|
+
card.focus({ preventScroll: true });
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
navigateToPath(path) {
|
|
2455
|
+
const segments = decodePointer(path);
|
|
2456
|
+
if (segments?.[0] !== "groups") {
|
|
2457
|
+
this.route = { kind: "project" };
|
|
2458
|
+
this.focusRequest = path;
|
|
2459
|
+
this.render();
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
const groupIndex = arrayIndex(segments[1] ?? "");
|
|
2463
|
+
const group = groupIndex === void 0 ? void 0 : this.draft.groups[groupIndex];
|
|
2464
|
+
if (!group || typeof group.id !== "string") return;
|
|
2465
|
+
this.route = { kind: "group", groupId: group.id };
|
|
2466
|
+
this.focusRequest = path;
|
|
2467
|
+
this.render();
|
|
2468
|
+
}
|
|
2469
|
+
currentGroupId() {
|
|
2470
|
+
return this.route.kind === "group" ? this.route.groupId : void 0;
|
|
2471
|
+
}
|
|
2472
|
+
groupIndex(groupId) {
|
|
2473
|
+
for (let index = 0; index < this.draft.groups.length; index += 1) {
|
|
2474
|
+
if (this.draft.groups[index].id === groupId) return index;
|
|
2475
|
+
}
|
|
2476
|
+
return -1;
|
|
2477
|
+
}
|
|
2478
|
+
groupById(groupId) {
|
|
2479
|
+
const index = this.groupIndex(groupId);
|
|
2480
|
+
return index === -1 ? void 0 : this.draft.groups[index];
|
|
2481
|
+
}
|
|
2482
|
+
ruleIndex(group, ruleId) {
|
|
2483
|
+
for (let index = 0; index < group.rules.length; index += 1) {
|
|
2484
|
+
if (group.rules[index].id === ruleId) return index;
|
|
2485
|
+
}
|
|
2486
|
+
return -1;
|
|
2487
|
+
}
|
|
2488
|
+
ruleById(groupId, ruleId) {
|
|
2489
|
+
const group = this.groupById(groupId);
|
|
2490
|
+
if (!group) return void 0;
|
|
2491
|
+
const index = this.ruleIndex(group, ruleId);
|
|
2492
|
+
return index === -1 ? void 0 : group.rules[index];
|
|
2493
|
+
}
|
|
2494
|
+
render() {
|
|
2495
|
+
if (this.destroyed) return;
|
|
2496
|
+
this.previousFocus = this.captureFocus();
|
|
2497
|
+
this.cleanupExtensions();
|
|
2498
|
+
this.controlNumber = 0;
|
|
2499
|
+
this.controls.clear();
|
|
2500
|
+
this.extensionControls.clear();
|
|
2501
|
+
if (this.route.kind === "group" && !this.groupById(this.route.groupId)) {
|
|
2502
|
+
this.route = { kind: "project" };
|
|
2503
|
+
}
|
|
2504
|
+
this.renderHeader();
|
|
2505
|
+
this.renderRail();
|
|
2506
|
+
this.renderCommandBar();
|
|
2507
|
+
this.form.replaceChildren();
|
|
2508
|
+
if (this.route.kind === "project") {
|
|
2509
|
+
this.renderProject();
|
|
2510
|
+
} else if (this.route.kind === "test") {
|
|
2511
|
+
this.renderTest();
|
|
2512
|
+
} else {
|
|
2513
|
+
this.renderGroup(this.route.groupId);
|
|
2514
|
+
}
|
|
2515
|
+
this.decorateExtensionControls();
|
|
2516
|
+
this.renderSummary();
|
|
2517
|
+
this.renderSearchResults();
|
|
2518
|
+
this.renderConfirmation();
|
|
2519
|
+
this.restoreFocus();
|
|
2520
|
+
this.focusRequest = void 0;
|
|
2521
|
+
}
|
|
2522
|
+
renderHeader() {
|
|
2523
|
+
this.header.replaceChildren();
|
|
2524
|
+
const titleBlock = this.document.createElement("div");
|
|
2525
|
+
const title = this.document.createElement("h1");
|
|
2526
|
+
title.textContent = displayName(this.draft.name, "Project");
|
|
2527
|
+
const dirty = this.document.createElement("p");
|
|
2528
|
+
dirty.dataset.dirtyState = "true";
|
|
2529
|
+
dirty.textContent = this.isDirty() ? "Unsaved changes" : "All changes saved";
|
|
2530
|
+
titleBlock.append(title, dirty);
|
|
2531
|
+
const mobileNav = this.document.createElement("label");
|
|
2532
|
+
mobileNav.dataset.mobileRouteNav = "true";
|
|
2533
|
+
mobileNav.textContent = "Project section";
|
|
2534
|
+
const select = this.document.createElement("select");
|
|
2535
|
+
select.dataset.mobileRoute = "true";
|
|
2536
|
+
select.dataset.editorKey = "mobile-route";
|
|
2537
|
+
const projectOption = this.document.createElement("option");
|
|
2538
|
+
projectOption.value = "project";
|
|
2539
|
+
projectOption.textContent = "Project";
|
|
2540
|
+
select.append(projectOption);
|
|
2541
|
+
for (const group of this.draft.groups) {
|
|
2542
|
+
const option = this.document.createElement("option");
|
|
2543
|
+
option.value = "group";
|
|
2544
|
+
option.dataset.groupId = safeText(group.id);
|
|
2545
|
+
option.textContent = displayName(group.name, "Unnamed group");
|
|
2546
|
+
select.append(option);
|
|
2547
|
+
}
|
|
2548
|
+
const testOption = this.document.createElement("option");
|
|
2549
|
+
testOption.value = "test";
|
|
2550
|
+
testOption.textContent = "Test rules";
|
|
2551
|
+
select.append(testOption);
|
|
2552
|
+
if (this.route.kind === "project") {
|
|
2553
|
+
select.value = "project";
|
|
2554
|
+
} else if (this.route.kind === "test") {
|
|
2555
|
+
select.value = "test";
|
|
2556
|
+
} else {
|
|
2557
|
+
const groupId = this.route.groupId;
|
|
2558
|
+
const options = Array.from(select.options);
|
|
2559
|
+
const option = options.find((value) => value.dataset.groupId === groupId);
|
|
2560
|
+
if (option) select.value = "group";
|
|
2561
|
+
}
|
|
2562
|
+
mobileNav.append(select);
|
|
2563
|
+
this.header.append(titleBlock, mobileNav);
|
|
2564
|
+
this.status.textContent = this.statusMessage;
|
|
2565
|
+
this.status.setAttribute("aria-busy", this.saving ? "true" : "false");
|
|
2566
|
+
}
|
|
2567
|
+
renderRail() {
|
|
2568
|
+
this.rail.replaceChildren();
|
|
2569
|
+
const heading = this.document.createElement("h2");
|
|
2570
|
+
heading.dataset.editorVisuallyHidden = "true";
|
|
2571
|
+
heading.textContent = "Project sections";
|
|
2572
|
+
this.rail.append(heading);
|
|
2573
|
+
const project = this.createButton("Project", "route:project");
|
|
2574
|
+
project.dataset.route = "project";
|
|
2575
|
+
project.dataset.editorKey = "route:project";
|
|
2576
|
+
if (this.route.kind === "project")
|
|
2577
|
+
project.setAttribute("aria-current", "page");
|
|
2578
|
+
this.rail.append(project);
|
|
2579
|
+
for (const group of this.draft.groups) {
|
|
2580
|
+
const groupId = safeText(group.id);
|
|
2581
|
+
const name = displayName(group.name, "Unnamed group");
|
|
2582
|
+
const button = this.createButton(name, `route:group:${groupId}`);
|
|
2583
|
+
button.dataset.route = "group";
|
|
2584
|
+
button.dataset.groupId = groupId;
|
|
2585
|
+
button.dataset.editorKey = `route:group:${groupId}`;
|
|
2586
|
+
if (this.route.kind === "group" && this.route.groupId === groupId) {
|
|
2587
|
+
button.setAttribute("aria-current", "page");
|
|
2588
|
+
}
|
|
2589
|
+
this.rail.append(button);
|
|
2590
|
+
}
|
|
2591
|
+
const testBtn = this.createButton("Test rules", "route:test");
|
|
2592
|
+
testBtn.dataset.route = "test";
|
|
2593
|
+
testBtn.dataset.editorKey = "route:test";
|
|
2594
|
+
if (this.route.kind === "test")
|
|
2595
|
+
testBtn.setAttribute("aria-current", "page");
|
|
2596
|
+
this.rail.append(testBtn);
|
|
2597
|
+
const searchWrap = this.document.createElement("div");
|
|
2598
|
+
searchWrap.dataset.searchWrap = "true";
|
|
2599
|
+
const searchLabel = this.document.createElement("label");
|
|
2600
|
+
searchLabel.dataset.searchLabel = "true";
|
|
2601
|
+
searchLabel.textContent = "Search project";
|
|
2602
|
+
const search = this.document.createElement("input");
|
|
2603
|
+
search.type = "search";
|
|
2604
|
+
search.value = this.searchQuery;
|
|
2605
|
+
search.dataset.search = "true";
|
|
2606
|
+
search.dataset.editorKey = "search";
|
|
2607
|
+
search.setAttribute("aria-label", "Search rules by name");
|
|
2608
|
+
search.setAttribute(
|
|
2609
|
+
"aria-expanded",
|
|
2610
|
+
this.searchQuery.trim().length > 0 ? "true" : "false"
|
|
2611
|
+
);
|
|
2612
|
+
search.setAttribute("aria-controls", "rogatio-search-results");
|
|
2613
|
+
searchLabel.append(search);
|
|
2614
|
+
searchWrap.append(searchLabel, this.searchResults);
|
|
2615
|
+
this.rail.append(searchWrap);
|
|
2616
|
+
}
|
|
2617
|
+
renderCommandBar() {
|
|
2618
|
+
this.commandBar.replaceChildren();
|
|
2619
|
+
this.commandBar.setAttribute("aria-busy", this.saving ? "true" : "false");
|
|
2620
|
+
this.commandBar.append(
|
|
2621
|
+
this.createCommandButton("Validate", "validate", this.saving),
|
|
2622
|
+
this.createCommandButton("Save", "save", this.saving || !this.isDirty()),
|
|
2623
|
+
this.createCommandButton("Cancel", "cancel", this.saving)
|
|
2624
|
+
);
|
|
2625
|
+
if (this.route.kind === "project") {
|
|
2626
|
+
this.commandBar.append(
|
|
2627
|
+
this.createCommandButton("Add group", "add-group", this.saving)
|
|
2628
|
+
);
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (this.route.kind === "test") {
|
|
2632
|
+
this.commandBar.append(
|
|
2633
|
+
this.createCommandButton("Run test", "test:run", this.saving)
|
|
2634
|
+
);
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
const group = this.groupById(this.route.groupId);
|
|
2638
|
+
if (!group) return;
|
|
2639
|
+
const index = this.groupIndex(this.route.groupId);
|
|
2640
|
+
const name = displayName(group.name, "Unnamed group");
|
|
2641
|
+
this.commandBar.append(
|
|
2642
|
+
this.createCommandButton("Add rule", "add-rule", this.saving, {
|
|
2643
|
+
groupId: this.route.groupId
|
|
2644
|
+
}),
|
|
2645
|
+
this.createCommandButton(
|
|
2646
|
+
`Move group ${name} up`,
|
|
2647
|
+
"move-group-up",
|
|
2648
|
+
this.saving || index <= 0,
|
|
2649
|
+
{ groupId: this.route.groupId }
|
|
2650
|
+
),
|
|
2651
|
+
this.createCommandButton(
|
|
2652
|
+
`Move group ${name} down`,
|
|
2653
|
+
"move-group-down",
|
|
2654
|
+
this.saving || index >= this.draft.groups.length - 1,
|
|
2655
|
+
{ groupId: this.route.groupId }
|
|
2656
|
+
),
|
|
2657
|
+
this.createCommandButton(
|
|
2658
|
+
`Remove group ${name}`,
|
|
2659
|
+
"remove-group",
|
|
2660
|
+
this.saving,
|
|
2661
|
+
{
|
|
2662
|
+
groupId: this.route.groupId
|
|
2663
|
+
}
|
|
2664
|
+
)
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
renderProject() {
|
|
2668
|
+
const heading = this.document.createElement("h2");
|
|
2669
|
+
heading.textContent = "Project";
|
|
2670
|
+
this.form.append(heading);
|
|
2671
|
+
const fields = this.document.createElement("fieldset");
|
|
2672
|
+
const legend = this.document.createElement("legend");
|
|
2673
|
+
legend.textContent = "Project details";
|
|
2674
|
+
fields.append(legend);
|
|
2675
|
+
const fieldGrid = this.document.createElement("div");
|
|
2676
|
+
fieldGrid.dataset.editorFields = "true";
|
|
2677
|
+
const name = this.document.createElement("input");
|
|
2678
|
+
name.type = "text";
|
|
2679
|
+
name.maxLength = 100;
|
|
2680
|
+
name.value = safeText(this.draft.name);
|
|
2681
|
+
this.renderField(fieldGrid, "Project name", "/name", name);
|
|
2682
|
+
const description = this.document.createElement("textarea");
|
|
2683
|
+
description.maxLength = 1e3;
|
|
2684
|
+
description.value = safeText(this.draft.description);
|
|
2685
|
+
this.renderField(
|
|
2686
|
+
fieldGrid,
|
|
2687
|
+
"Project description",
|
|
2688
|
+
"/description",
|
|
2689
|
+
description
|
|
2690
|
+
);
|
|
2691
|
+
fields.append(fieldGrid);
|
|
2692
|
+
this.form.append(fields);
|
|
2693
|
+
const groups = this.document.createElement("section");
|
|
2694
|
+
const groupsHeading = this.document.createElement("h2");
|
|
2695
|
+
groupsHeading.textContent = "Groups";
|
|
2696
|
+
groups.append(groupsHeading);
|
|
2697
|
+
if (this.draft.groups.length === 0) {
|
|
2698
|
+
const empty = this.document.createElement("p");
|
|
2699
|
+
empty.textContent = "No groups yet.";
|
|
2700
|
+
groups.append(empty);
|
|
2701
|
+
} else {
|
|
2702
|
+
const list = this.document.createElement("ul");
|
|
2703
|
+
for (const group of this.draft.groups) {
|
|
2704
|
+
const item = this.document.createElement("li");
|
|
2705
|
+
const button = this.createButton(
|
|
2706
|
+
`Edit group ${displayName(group.name, "Unnamed group")}`,
|
|
2707
|
+
`route:group:${safeText(group.id)}`
|
|
2708
|
+
);
|
|
2709
|
+
button.dataset.route = "group";
|
|
2710
|
+
button.dataset.groupId = safeText(group.id);
|
|
2711
|
+
item.append(button);
|
|
2712
|
+
list.append(item);
|
|
2713
|
+
}
|
|
2714
|
+
groups.append(list);
|
|
2715
|
+
}
|
|
2716
|
+
this.form.append(groups);
|
|
2717
|
+
}
|
|
2718
|
+
renderGroup(groupId) {
|
|
2719
|
+
const group = this.groupById(groupId);
|
|
2720
|
+
if (!group) return;
|
|
2721
|
+
const groupIndex = this.groupIndex(groupId);
|
|
2722
|
+
const heading = this.document.createElement("h2");
|
|
2723
|
+
heading.textContent = displayName(group.name, "Unnamed group");
|
|
2724
|
+
this.form.append(heading);
|
|
2725
|
+
const settings = this.document.createElement("fieldset");
|
|
2726
|
+
const legend = this.document.createElement("legend");
|
|
2727
|
+
legend.textContent = "Group details";
|
|
2728
|
+
settings.append(legend);
|
|
2729
|
+
const fields = this.document.createElement("div");
|
|
2730
|
+
fields.dataset.editorFields = "true";
|
|
2731
|
+
const id = this.document.createElement("input");
|
|
2732
|
+
id.type = "text";
|
|
2733
|
+
id.maxLength = 64;
|
|
2734
|
+
id.value = safeText(group.id);
|
|
2735
|
+
this.renderField(
|
|
2736
|
+
fields,
|
|
2737
|
+
"Group ID",
|
|
2738
|
+
pointer("groups", groupIndex, "id"),
|
|
2739
|
+
id
|
|
2740
|
+
);
|
|
2741
|
+
const name = this.document.createElement("input");
|
|
2742
|
+
name.type = "text";
|
|
2743
|
+
name.maxLength = 100;
|
|
2744
|
+
name.value = safeText(group.name);
|
|
2745
|
+
this.renderField(
|
|
2746
|
+
fields,
|
|
2747
|
+
"Group name",
|
|
2748
|
+
pointer("groups", groupIndex, "name"),
|
|
2749
|
+
name
|
|
2750
|
+
);
|
|
2751
|
+
settings.append(fields);
|
|
2752
|
+
this.form.append(settings);
|
|
2753
|
+
this.renderOrigins(
|
|
2754
|
+
this.form,
|
|
2755
|
+
"Group origins",
|
|
2756
|
+
group.origins,
|
|
2757
|
+
"group",
|
|
2758
|
+
groupId,
|
|
2759
|
+
void 0,
|
|
2760
|
+
groupIndex
|
|
2761
|
+
);
|
|
2762
|
+
const rulesSection = this.document.createElement("section");
|
|
2763
|
+
const rulesHeading = this.document.createElement("h2");
|
|
2764
|
+
rulesHeading.textContent = "Rules";
|
|
2765
|
+
rulesSection.append(rulesHeading);
|
|
2766
|
+
const list = this.document.createElement("div");
|
|
2767
|
+
list.dataset.ruleList = groupId;
|
|
2768
|
+
for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex += 1) {
|
|
2769
|
+
list.append(
|
|
2770
|
+
this.renderRule(group, group.rules[ruleIndex], groupIndex, ruleIndex)
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
if (group.rules.length === 0) {
|
|
2774
|
+
const empty = this.document.createElement("p");
|
|
2775
|
+
empty.textContent = "No rules yet.";
|
|
2776
|
+
list.append(empty);
|
|
2777
|
+
}
|
|
2778
|
+
rulesSection.append(list);
|
|
2779
|
+
this.form.append(rulesSection);
|
|
2780
|
+
}
|
|
2781
|
+
renderTest() {
|
|
2782
|
+
const heading = this.document.createElement("h2");
|
|
2783
|
+
heading.textContent = "Test rules (dry-run)";
|
|
2784
|
+
this.form.append(heading);
|
|
2785
|
+
const description = this.document.createElement("p");
|
|
2786
|
+
description.dataset.testDescription = "true";
|
|
2787
|
+
description.textContent = "Run offline dry-run tests against the current project. Enter test cases (one URL per line) and optional method/resource type defaults. No network requests are made.";
|
|
2788
|
+
this.form.append(description);
|
|
2789
|
+
const panel = this.document.createElement("div");
|
|
2790
|
+
panel.dataset.testPanel = "true";
|
|
2791
|
+
const urlsFieldset = this.document.createElement("fieldset");
|
|
2792
|
+
const urlsLegend = this.document.createElement("legend");
|
|
2793
|
+
urlsLegend.textContent = "Test URLs";
|
|
2794
|
+
urlsFieldset.append(urlsLegend);
|
|
2795
|
+
const urlsField = this.document.createElement("div");
|
|
2796
|
+
urlsField.dataset.editorField = "true";
|
|
2797
|
+
const urlsLabel = this.document.createElement("label");
|
|
2798
|
+
urlsLabel.textContent = "One test URL per line";
|
|
2799
|
+
const urlsTextarea = this.document.createElement("textarea");
|
|
2800
|
+
urlsTextarea.rows = 8;
|
|
2801
|
+
urlsTextarea.placeholder = "https://example.com/page\nhttps://example.com/script.js\nhttps://other.com/";
|
|
2802
|
+
urlsTextarea.value = this.testUrls;
|
|
2803
|
+
urlsTextarea.dataset.testUrls = "true";
|
|
2804
|
+
urlsLabel.append(urlsTextarea);
|
|
2805
|
+
urlsField.append(urlsLabel);
|
|
2806
|
+
urlsFieldset.append(urlsField);
|
|
2807
|
+
panel.append(urlsFieldset);
|
|
2808
|
+
const defaultsFieldset = this.document.createElement("fieldset");
|
|
2809
|
+
const defaultsLegend = this.document.createElement("legend");
|
|
2810
|
+
defaultsLegend.textContent = "Defaults (applied to all URLs without explicit values)";
|
|
2811
|
+
defaultsFieldset.append(defaultsLegend);
|
|
2812
|
+
const defaultsGrid = this.document.createElement("div");
|
|
2813
|
+
defaultsGrid.dataset.testDefaults = "true";
|
|
2814
|
+
const methodField = this.document.createElement("div");
|
|
2815
|
+
methodField.dataset.editorField = "true";
|
|
2816
|
+
const methodLabel = this.document.createElement("label");
|
|
2817
|
+
methodLabel.textContent = "Default HTTP method";
|
|
2818
|
+
const methodSelect = this.document.createElement("select");
|
|
2819
|
+
methodSelect.dataset.testMethod = "true";
|
|
2820
|
+
methodSelect.value = this.testMethod;
|
|
2821
|
+
const methodEmpty = this.document.createElement("option");
|
|
2822
|
+
methodEmpty.value = "";
|
|
2823
|
+
methodEmpty.textContent = "(none \u2014 not-applicable)";
|
|
2824
|
+
methodSelect.append(methodEmpty);
|
|
2825
|
+
for (const m of HTTP_METHODS2) {
|
|
2826
|
+
const opt = this.document.createElement("option");
|
|
2827
|
+
opt.value = m;
|
|
2828
|
+
opt.textContent = m;
|
|
2829
|
+
methodSelect.append(opt);
|
|
2830
|
+
}
|
|
2831
|
+
methodLabel.append(methodSelect);
|
|
2832
|
+
methodField.append(methodLabel);
|
|
2833
|
+
const rtField = this.document.createElement("div");
|
|
2834
|
+
rtField.dataset.editorField = "true";
|
|
2835
|
+
const rtLabel = this.document.createElement("label");
|
|
2836
|
+
rtLabel.textContent = "Default resource type";
|
|
2837
|
+
const rtSelect = this.document.createElement("select");
|
|
2838
|
+
rtSelect.dataset.testResourceType = "true";
|
|
2839
|
+
rtSelect.value = this.testResourceType;
|
|
2840
|
+
const rtEmpty = this.document.createElement("option");
|
|
2841
|
+
rtEmpty.value = "";
|
|
2842
|
+
rtEmpty.textContent = "(none \u2014 not-applicable)";
|
|
2843
|
+
rtSelect.append(rtEmpty);
|
|
2844
|
+
for (const rt of RESOURCE_TYPES2) {
|
|
2845
|
+
const opt = this.document.createElement("option");
|
|
2846
|
+
opt.value = rt;
|
|
2847
|
+
opt.textContent = rt;
|
|
2848
|
+
rtSelect.append(opt);
|
|
2849
|
+
}
|
|
2850
|
+
rtLabel.append(rtSelect);
|
|
2851
|
+
rtField.append(rtLabel);
|
|
2852
|
+
defaultsGrid.append(methodField, rtField);
|
|
2853
|
+
defaultsFieldset.append(defaultsGrid);
|
|
2854
|
+
panel.append(defaultsFieldset);
|
|
2855
|
+
const maxCasesField = this.document.createElement("div");
|
|
2856
|
+
maxCasesField.dataset.editorField = "true";
|
|
2857
|
+
const maxCasesLabel = this.document.createElement("label");
|
|
2858
|
+
maxCasesLabel.textContent = "Max test cases";
|
|
2859
|
+
const maxCasesInput = this.document.createElement("input");
|
|
2860
|
+
maxCasesInput.type = "number";
|
|
2861
|
+
maxCasesInput.min = "1";
|
|
2862
|
+
maxCasesInput.max = "10000";
|
|
2863
|
+
maxCasesInput.value = this.testMaxCases;
|
|
2864
|
+
maxCasesInput.style.width = "8rem";
|
|
2865
|
+
maxCasesInput.dataset.testMaxCases = "true";
|
|
2866
|
+
maxCasesLabel.append(maxCasesInput);
|
|
2867
|
+
maxCasesField.append(maxCasesLabel);
|
|
2868
|
+
panel.append(maxCasesField);
|
|
2869
|
+
const runBtn = this.createButton("Run test", "test:run");
|
|
2870
|
+
runBtn.type = "button";
|
|
2871
|
+
runBtn.dataset.testRun = "true";
|
|
2872
|
+
runBtn.disabled = this.saving || this.testRunning;
|
|
2873
|
+
panel.append(runBtn);
|
|
2874
|
+
this.form.append(panel);
|
|
2875
|
+
const resultsSection = this.document.createElement("section");
|
|
2876
|
+
resultsSection.dataset.testResults = "true";
|
|
2877
|
+
this.form.append(resultsSection);
|
|
2878
|
+
if (this.testResult) {
|
|
2879
|
+
this.renderTestResults(this.testResult);
|
|
2880
|
+
} else if (this.testRunning) {
|
|
2881
|
+
const pending = this.document.createElement("p");
|
|
2882
|
+
pending.dataset.testPending = "true";
|
|
2883
|
+
pending.textContent = "Running dry-run...";
|
|
2884
|
+
resultsSection.append(pending);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
renderRule(group, rule, groupIndex, ruleIndex) {
|
|
2888
|
+
const groupId = safeText(group.id);
|
|
2889
|
+
const ruleId = safeText(rule.id);
|
|
2890
|
+
const ruleName = displayName(rule.name, "Unnamed rule");
|
|
2891
|
+
const rulePath = pointer("groups", groupIndex, "rules", ruleIndex);
|
|
2892
|
+
const card = this.document.createElement("article");
|
|
2893
|
+
card.dataset.ruleCard = "true";
|
|
2894
|
+
card.dataset.ruleId = ruleId;
|
|
2895
|
+
card.id = `rogatio-rule-${groupId}-${ruleId}`;
|
|
2896
|
+
card.tabIndex = -1;
|
|
2897
|
+
const heading = this.document.createElement("h3");
|
|
2898
|
+
heading.textContent = ruleName;
|
|
2899
|
+
card.append(heading);
|
|
2900
|
+
const fields = this.document.createElement("fieldset");
|
|
2901
|
+
const legend = this.document.createElement("legend");
|
|
2902
|
+
legend.textContent = "Common rule matcher";
|
|
2903
|
+
fields.append(legend);
|
|
2904
|
+
const grid = this.document.createElement("div");
|
|
2905
|
+
grid.dataset.editorFields = "true";
|
|
2906
|
+
const id = this.document.createElement("input");
|
|
2907
|
+
id.type = "text";
|
|
2908
|
+
id.maxLength = 64;
|
|
2909
|
+
id.value = safeText(rule.id);
|
|
2910
|
+
this.renderField(grid, `Rule ID for ${ruleName}`, `${rulePath}/id`, id);
|
|
2911
|
+
const name = this.document.createElement("input");
|
|
2912
|
+
name.type = "text";
|
|
2913
|
+
name.maxLength = 100;
|
|
2914
|
+
name.value = safeText(rule.name);
|
|
2915
|
+
this.renderField(
|
|
2916
|
+
grid,
|
|
2917
|
+
`Rule name for ${ruleName}`,
|
|
2918
|
+
`${rulePath}/name`,
|
|
2919
|
+
name
|
|
2920
|
+
);
|
|
2921
|
+
const regex = this.document.createElement("textarea");
|
|
2922
|
+
regex.maxLength = F2_MAX_URL_REGEX_LENGTH2;
|
|
2923
|
+
regex.value = safeText(rule.urlRegex);
|
|
2924
|
+
this.renderField(
|
|
2925
|
+
grid,
|
|
2926
|
+
`URL regular expression for ${ruleName}`,
|
|
2927
|
+
`${rulePath}/urlRegex`,
|
|
2928
|
+
regex
|
|
2929
|
+
);
|
|
2930
|
+
const urlRow = this.document.createElement("div");
|
|
2931
|
+
urlRow.dataset.editorUrlRow = "true";
|
|
2932
|
+
const urlLabel = this.document.createElement("label");
|
|
2933
|
+
urlLabel.textContent = `URL to match exactly for ${ruleName}`;
|
|
2934
|
+
const urlInput = this.document.createElement("input");
|
|
2935
|
+
urlInput.type = "url";
|
|
2936
|
+
urlInput.dataset.urlSource = "true";
|
|
2937
|
+
urlInput.dataset.ruleId = ruleId;
|
|
2938
|
+
urlInput.dataset.editorKey = `url-source:${ruleId}`;
|
|
2939
|
+
urlInput.value = this.urlInputs.get(ruleId) ?? "";
|
|
2940
|
+
urlInput.disabled = this.saving;
|
|
2941
|
+
urlLabel.append(urlInput);
|
|
2942
|
+
const convert = this.createCommandButton(
|
|
2943
|
+
`Convert URL to exact regex for ${ruleName}`,
|
|
2944
|
+
"convert-url",
|
|
2945
|
+
this.saving,
|
|
2946
|
+
{ groupId, ruleId }
|
|
2947
|
+
);
|
|
2948
|
+
urlRow.append(urlLabel, convert);
|
|
2949
|
+
grid.append(urlRow);
|
|
2950
|
+
fields.append(grid);
|
|
2951
|
+
card.append(fields);
|
|
2952
|
+
this.renderOrigins(
|
|
2953
|
+
card,
|
|
2954
|
+
"Rule origins",
|
|
2955
|
+
rule.origins,
|
|
2956
|
+
"rule",
|
|
2957
|
+
groupId,
|
|
2958
|
+
ruleId,
|
|
2959
|
+
groupIndex,
|
|
2960
|
+
ruleIndex
|
|
2961
|
+
);
|
|
2962
|
+
this.renderResourceTypes(card, rule, rulePath, ruleName);
|
|
2963
|
+
const matcherFields = this.document.createElement("fieldset");
|
|
2964
|
+
const matcherLegend = this.document.createElement("legend");
|
|
2965
|
+
matcherLegend.textContent = "Request constraints";
|
|
2966
|
+
matcherFields.append(matcherLegend);
|
|
2967
|
+
const matcherGrid = this.document.createElement("div");
|
|
2968
|
+
matcherGrid.dataset.editorFields = "true";
|
|
2969
|
+
const priority = this.document.createElement("input");
|
|
2970
|
+
priority.type = "number";
|
|
2971
|
+
priority.min = "1";
|
|
2972
|
+
priority.max = "1000";
|
|
2973
|
+
priority.step = "1";
|
|
2974
|
+
priority.value = typeof rule.priority === "number" && Number.isFinite(rule.priority) ? String(rule.priority) : safeText(rule.priority);
|
|
2975
|
+
this.renderField(
|
|
2976
|
+
matcherGrid,
|
|
2977
|
+
`Priority for ${ruleName}`,
|
|
2978
|
+
`${rulePath}/priority`,
|
|
2979
|
+
priority
|
|
2980
|
+
);
|
|
2981
|
+
const method = this.document.createElement("select");
|
|
2982
|
+
const anyMethod = this.document.createElement("option");
|
|
2983
|
+
anyMethod.value = "";
|
|
2984
|
+
anyMethod.textContent = "Any method";
|
|
2985
|
+
method.append(anyMethod);
|
|
2986
|
+
for (const value of HTTP_METHODS2) {
|
|
2987
|
+
const option = this.document.createElement("option");
|
|
2988
|
+
option.value = value;
|
|
2989
|
+
option.textContent = value;
|
|
2990
|
+
method.append(option);
|
|
2991
|
+
}
|
|
2992
|
+
method.value = safeText(rule.method);
|
|
2993
|
+
this.renderField(
|
|
2994
|
+
matcherGrid,
|
|
2995
|
+
`Method for ${ruleName}`,
|
|
2996
|
+
`${rulePath}/method`,
|
|
2997
|
+
method
|
|
2998
|
+
);
|
|
2999
|
+
matcherFields.append(matcherGrid);
|
|
3000
|
+
card.append(matcherFields);
|
|
3001
|
+
if (this.extensions.length > 0) {
|
|
3002
|
+
const currentType = this.extensions.find(
|
|
3003
|
+
(e) => e.matches(rule)
|
|
3004
|
+
)?.id ?? safeText(rule.type);
|
|
3005
|
+
const typeFieldset = this.document.createElement("fieldset");
|
|
3006
|
+
const typeLegend = this.document.createElement("legend");
|
|
3007
|
+
typeLegend.textContent = `Rule type for ${ruleName}`;
|
|
3008
|
+
const typeField = this.document.createElement("div");
|
|
3009
|
+
typeField.dataset.editorField = "true";
|
|
3010
|
+
const typeLabel = this.document.createElement("label");
|
|
3011
|
+
typeLabel.textContent = "Rule type";
|
|
3012
|
+
const typeSelect = this.document.createElement("select");
|
|
3013
|
+
typeSelect.dataset.ruleTypeSelect = "true";
|
|
3014
|
+
typeSelect.dataset.ruleTypePath = rulePath;
|
|
3015
|
+
typeSelect.disabled = this.saving;
|
|
3016
|
+
const noOption = this.document.createElement("option");
|
|
3017
|
+
noOption.value = "";
|
|
3018
|
+
noOption.textContent = "No action (choose a rule type)";
|
|
3019
|
+
typeSelect.append(noOption);
|
|
3020
|
+
for (const extension of this.extensions) {
|
|
3021
|
+
const option = this.document.createElement("option");
|
|
3022
|
+
option.value = extension.id;
|
|
3023
|
+
option.textContent = extension.label;
|
|
3024
|
+
typeSelect.append(option);
|
|
3025
|
+
}
|
|
3026
|
+
typeSelect.value = currentType ?? "";
|
|
3027
|
+
typeLabel.append(typeSelect);
|
|
3028
|
+
typeField.append(typeLabel);
|
|
3029
|
+
typeFieldset.append(typeLegend, typeField);
|
|
3030
|
+
card.append(typeFieldset);
|
|
3031
|
+
}
|
|
3032
|
+
const match = this.findExtension(rule, rulePath);
|
|
3033
|
+
if (match.extension)
|
|
3034
|
+
this.mountExtension(card, match.extension, groupId, ruleId, rulePath);
|
|
3035
|
+
if (match.error) {
|
|
3036
|
+
const extensionError = this.document.createElement("p");
|
|
3037
|
+
extensionError.dataset.extensionError = "true";
|
|
3038
|
+
extensionError.textContent = match.error.message;
|
|
3039
|
+
card.append(extensionError);
|
|
3040
|
+
}
|
|
3041
|
+
const actions = this.document.createElement("div");
|
|
3042
|
+
actions.dataset.ruleActions = "true";
|
|
3043
|
+
actions.append(
|
|
3044
|
+
this.createCommandButton(
|
|
3045
|
+
`Move rule ${ruleName} up`,
|
|
3046
|
+
"move-rule-up",
|
|
3047
|
+
this.saving || ruleIndex <= 0,
|
|
3048
|
+
{ groupId, ruleId }
|
|
3049
|
+
),
|
|
3050
|
+
this.createCommandButton(
|
|
3051
|
+
`Move rule ${ruleName} down`,
|
|
3052
|
+
"move-rule-down",
|
|
3053
|
+
this.saving || ruleIndex >= group.rules.length - 1,
|
|
3054
|
+
{ groupId, ruleId }
|
|
3055
|
+
),
|
|
3056
|
+
this.createCommandButton(
|
|
3057
|
+
`Remove rule ${ruleName}`,
|
|
3058
|
+
"remove-rule",
|
|
3059
|
+
this.saving,
|
|
3060
|
+
{
|
|
3061
|
+
groupId,
|
|
3062
|
+
ruleId
|
|
3063
|
+
}
|
|
3064
|
+
)
|
|
3065
|
+
);
|
|
3066
|
+
card.append(actions);
|
|
3067
|
+
return card;
|
|
3068
|
+
}
|
|
3069
|
+
renderOrigins(parent, label, values, owner, groupId, ruleId, groupIndex, ruleIndex) {
|
|
3070
|
+
const fieldset = this.document.createElement("fieldset");
|
|
3071
|
+
const legend = this.document.createElement("legend");
|
|
3072
|
+
legend.textContent = label;
|
|
3073
|
+
fieldset.append(legend);
|
|
3074
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
3075
|
+
const row = this.document.createElement("div");
|
|
3076
|
+
row.dataset.editorOriginRow = "true";
|
|
3077
|
+
const path = owner === "group" ? pointer("groups", groupIndex, "origins", index) : pointer(
|
|
3078
|
+
"groups",
|
|
3079
|
+
groupIndex,
|
|
3080
|
+
"rules",
|
|
3081
|
+
ruleIndex ?? 0,
|
|
3082
|
+
"origins",
|
|
3083
|
+
index
|
|
3084
|
+
);
|
|
3085
|
+
const input = this.document.createElement("input");
|
|
3086
|
+
input.type = "text";
|
|
3087
|
+
input.maxLength = 2048;
|
|
3088
|
+
input.value = safeText(values[index]);
|
|
3089
|
+
input.disabled = this.saving;
|
|
3090
|
+
this.renderField(row, `Origin ${index + 1}`, path, input);
|
|
3091
|
+
const remove = this.createCommandButton(
|
|
3092
|
+
`Remove ${owner} origin ${index + 1}`,
|
|
3093
|
+
owner === "group" ? "remove-group-origin" : "remove-rule-origin",
|
|
3094
|
+
this.saving,
|
|
3095
|
+
{ groupId, ruleId, index: String(index) }
|
|
3096
|
+
);
|
|
3097
|
+
row.append(remove);
|
|
3098
|
+
fieldset.append(row);
|
|
3099
|
+
}
|
|
3100
|
+
fieldset.append(
|
|
3101
|
+
this.createCommandButton(
|
|
3102
|
+
"Add origin",
|
|
3103
|
+
owner === "group" ? "add-group-origin" : "add-rule-origin",
|
|
3104
|
+
this.saving,
|
|
3105
|
+
{ groupId, ruleId }
|
|
3106
|
+
)
|
|
3107
|
+
);
|
|
3108
|
+
parent.append(fieldset);
|
|
3109
|
+
}
|
|
3110
|
+
renderResourceTypes(parent, rule, rulePath, ruleName) {
|
|
3111
|
+
const fieldset = this.document.createElement("fieldset");
|
|
3112
|
+
const legend = this.document.createElement("legend");
|
|
3113
|
+
legend.textContent = `Resource types for ${ruleName}`;
|
|
3114
|
+
fieldset.append(legend);
|
|
3115
|
+
const checks = this.document.createElement("div");
|
|
3116
|
+
checks.dataset.editorChecks = "true";
|
|
3117
|
+
for (const resourceType of RESOURCE_TYPES2) {
|
|
3118
|
+
const label = this.document.createElement("label");
|
|
3119
|
+
const input = this.document.createElement("input");
|
|
3120
|
+
input.type = "checkbox";
|
|
3121
|
+
input.dataset.resourcePath = `${rulePath}/resourceTypes`;
|
|
3122
|
+
input.dataset.resourceType = resourceType;
|
|
3123
|
+
input.checked = rule.resourceTypes.includes(resourceType);
|
|
3124
|
+
input.disabled = this.saving;
|
|
3125
|
+
label.append(input, this.document.createTextNode(resourceType));
|
|
3126
|
+
checks.append(label);
|
|
3127
|
+
}
|
|
3128
|
+
fieldset.append(checks);
|
|
3129
|
+
parent.append(fieldset);
|
|
3130
|
+
}
|
|
3131
|
+
mountExtension(parent, extension, groupId, ruleId, rulePath) {
|
|
3132
|
+
const fieldset = this.document.createElement("fieldset");
|
|
3133
|
+
const legend = this.document.createElement("legend");
|
|
3134
|
+
legend.textContent = extension.label;
|
|
3135
|
+
fieldset.append(legend);
|
|
3136
|
+
const container = this.document.createElement("div");
|
|
3137
|
+
container.dataset.extensionFields = extension.id;
|
|
3138
|
+
fieldset.append(container);
|
|
3139
|
+
parent.append(fieldset);
|
|
3140
|
+
const context = {
|
|
3141
|
+
document: this.document,
|
|
3142
|
+
container,
|
|
3143
|
+
rulePath,
|
|
3144
|
+
getField: (name) => {
|
|
3145
|
+
if (!isValidExtensionName(name)) return void 0;
|
|
3146
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
3147
|
+
if (!rule) return void 0;
|
|
3148
|
+
const resolved = extensionFieldParent(rule, name);
|
|
3149
|
+
if (!resolved) return void 0;
|
|
3150
|
+
const value = resolved.parent[resolved.key];
|
|
3151
|
+
return value === void 0 ? void 0 : cloneSnapshot(value);
|
|
3152
|
+
},
|
|
3153
|
+
setField: (name, value) => {
|
|
3154
|
+
if (!isValidExtensionName(name)) {
|
|
3155
|
+
this.extensionErrors.set(
|
|
3156
|
+
rulePath,
|
|
3157
|
+
diagnostic(
|
|
3158
|
+
"editor.extension-field",
|
|
3159
|
+
rulePath,
|
|
3160
|
+
"An additional rule field attempted to change a common field."
|
|
3161
|
+
)
|
|
3162
|
+
);
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const snapshot = snapshotOwnData(value);
|
|
3166
|
+
if (!snapshot.valid) {
|
|
3167
|
+
this.extensionErrors.set(
|
|
3168
|
+
rulePath,
|
|
3169
|
+
diagnostic(
|
|
3170
|
+
"editor.extension-field",
|
|
3171
|
+
rulePath,
|
|
3172
|
+
"An additional rule field contained invalid data."
|
|
3173
|
+
)
|
|
3174
|
+
);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
3178
|
+
if (!rule || this.saving) return;
|
|
3179
|
+
const resolved = extensionFieldParent(rule, name);
|
|
3180
|
+
if (!resolved) {
|
|
3181
|
+
this.extensionErrors.set(
|
|
3182
|
+
rulePath,
|
|
3183
|
+
diagnostic(
|
|
3184
|
+
"editor.extension-field",
|
|
3185
|
+
rulePath,
|
|
3186
|
+
"An additional rule field could not be resolved."
|
|
3187
|
+
)
|
|
3188
|
+
);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
if (!Object.is(resolved.parent[resolved.key], snapshot.value)) {
|
|
3192
|
+
Object.defineProperty(resolved.parent, resolved.key, {
|
|
3193
|
+
configurable: true,
|
|
3194
|
+
enumerable: true,
|
|
3195
|
+
value: snapshot.value,
|
|
3196
|
+
writable: true
|
|
3197
|
+
});
|
|
3198
|
+
this.markChanged();
|
|
3199
|
+
this.render();
|
|
3200
|
+
}
|
|
3201
|
+
},
|
|
3202
|
+
deleteField: (name) => {
|
|
3203
|
+
if (!isValidExtensionName(name)) return;
|
|
3204
|
+
const rule = this.ruleById(groupId, ruleId);
|
|
3205
|
+
if (rule && Object.hasOwn(rule, name) && !this.saving) {
|
|
3206
|
+
delete rule[name];
|
|
3207
|
+
this.markChanged();
|
|
3208
|
+
this.render();
|
|
3209
|
+
}
|
|
3210
|
+
},
|
|
3211
|
+
registerControl: (fieldPath, control) => {
|
|
3212
|
+
const firstSegment = decodePointer(fieldPath)?.[0] ?? "";
|
|
3213
|
+
if (!fieldPath.startsWith("/") || !isHTMLElement(control) || control.ownerDocument !== this.document || !isValidExtensionName(firstSegment)) {
|
|
3214
|
+
this.extensionErrors.set(
|
|
3215
|
+
rulePath,
|
|
3216
|
+
diagnostic(
|
|
3217
|
+
"editor.extension-control",
|
|
3218
|
+
rulePath,
|
|
3219
|
+
"An additional rule field registered an invalid control."
|
|
3220
|
+
)
|
|
3221
|
+
);
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
const absolutePath = `${rulePath}${fieldPath}`;
|
|
3225
|
+
control.dataset.path = absolutePath;
|
|
3226
|
+
this.extensionControls.set(absolutePath, control);
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
try {
|
|
3230
|
+
const mount = extension.mount(context);
|
|
3231
|
+
if (!mount || typeof mount.destroy !== "function") {
|
|
3232
|
+
throw new Error("invalid extension mount");
|
|
3233
|
+
}
|
|
3234
|
+
this.extensionCleanups.push(() => {
|
|
3235
|
+
try {
|
|
3236
|
+
mount.destroy();
|
|
3237
|
+
} catch {
|
|
3238
|
+
this.extensionErrors.set(
|
|
3239
|
+
rulePath,
|
|
3240
|
+
diagnostic(
|
|
3241
|
+
"editor.extension-failed",
|
|
3242
|
+
rulePath,
|
|
3243
|
+
"An additional rule field could not be cleaned up."
|
|
3244
|
+
)
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
});
|
|
3248
|
+
} catch {
|
|
3249
|
+
this.extensionErrors.set(
|
|
3250
|
+
rulePath,
|
|
3251
|
+
diagnostic(
|
|
3252
|
+
"editor.extension-failed",
|
|
3253
|
+
rulePath,
|
|
3254
|
+
"An additional rule field could not be rendered."
|
|
3255
|
+
)
|
|
3256
|
+
);
|
|
3257
|
+
const message = this.document.createElement("p");
|
|
3258
|
+
message.textContent = "Additional rule fields are unavailable.";
|
|
3259
|
+
fieldset.append(message);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
cleanupExtensions() {
|
|
3263
|
+
const cleanups = this.extensionCleanups;
|
|
3264
|
+
this.extensionCleanups = [];
|
|
3265
|
+
for (const cleanup of cleanups) cleanup();
|
|
3266
|
+
}
|
|
3267
|
+
decorateExtensionControls() {
|
|
3268
|
+
for (const [path, control] of this.extensionControls) {
|
|
3269
|
+
const id = this.controlId(path);
|
|
3270
|
+
control.id = id;
|
|
3271
|
+
control.dataset.editorKey = path;
|
|
3272
|
+
const fieldErrors = this.errors.filter((value) => value.path === path);
|
|
3273
|
+
if (fieldErrors.length === 0) {
|
|
3274
|
+
control.removeAttribute("aria-invalid");
|
|
3275
|
+
continue;
|
|
3276
|
+
}
|
|
3277
|
+
control.setAttribute("aria-invalid", "true");
|
|
3278
|
+
const error = this.document.createElement("div");
|
|
3279
|
+
error.id = `${id}-error`;
|
|
3280
|
+
error.dataset.editorFieldError = "true";
|
|
3281
|
+
error.textContent = fieldErrors.map((value) => value.message).join(" ");
|
|
3282
|
+
control.setAttribute("aria-describedby", error.id);
|
|
3283
|
+
control.parentElement?.append(error);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
renderField(parent, labelText, path, control) {
|
|
3287
|
+
const field = this.document.createElement("div");
|
|
3288
|
+
field.dataset.editorField = "true";
|
|
3289
|
+
const label = this.document.createElement("label");
|
|
3290
|
+
const id = this.controlId(path);
|
|
3291
|
+
label.htmlFor = id;
|
|
3292
|
+
label.textContent = labelText;
|
|
3293
|
+
control.id = id;
|
|
3294
|
+
control.dataset.path = path;
|
|
3295
|
+
control.dataset.editorKey = path;
|
|
3296
|
+
control.disabled = this.saving;
|
|
3297
|
+
const errors = this.errors.filter((value) => value.path === path);
|
|
3298
|
+
if (errors.length > 0) {
|
|
3299
|
+
control.setAttribute("aria-invalid", "true");
|
|
3300
|
+
const error = this.document.createElement("div");
|
|
3301
|
+
error.id = `${id}-error`;
|
|
3302
|
+
error.dataset.editorFieldError = "true";
|
|
3303
|
+
error.textContent = errors.map((value) => value.message).join(" ");
|
|
3304
|
+
control.setAttribute("aria-describedby", error.id);
|
|
3305
|
+
field.append(label, control, error);
|
|
3306
|
+
} else {
|
|
3307
|
+
control.removeAttribute("aria-invalid");
|
|
3308
|
+
field.append(label, control);
|
|
3309
|
+
}
|
|
3310
|
+
this.controls.set(path, control);
|
|
3311
|
+
parent.append(field);
|
|
3312
|
+
}
|
|
3313
|
+
renderSummary() {
|
|
3314
|
+
this.summary.replaceChildren();
|
|
3315
|
+
this.summary.hidden = this.errors.length === 0;
|
|
3316
|
+
if (this.errors.length === 0) return;
|
|
3317
|
+
const heading = this.document.createElement("h2");
|
|
3318
|
+
heading.textContent = "Fix these errors before saving";
|
|
3319
|
+
const list = this.document.createElement("ul");
|
|
3320
|
+
for (const error of this.errors) {
|
|
3321
|
+
const item = this.document.createElement("li");
|
|
3322
|
+
const button = this.document.createElement("button");
|
|
3323
|
+
button.type = "button";
|
|
3324
|
+
button.dataset.errorPath = error.path;
|
|
3325
|
+
button.textContent = `${error.message} (${error.path || "project"})`;
|
|
3326
|
+
item.append(button);
|
|
3327
|
+
list.append(item);
|
|
3328
|
+
}
|
|
3329
|
+
this.summary.append(heading, list);
|
|
3330
|
+
}
|
|
3331
|
+
renderSearchResults() {
|
|
3332
|
+
const root = this.searchResults;
|
|
3333
|
+
root.replaceChildren();
|
|
3334
|
+
root.hidden = this.searchQuery.trim().length === 0;
|
|
3335
|
+
if (this.searchQuery.trim().length === 0) return;
|
|
3336
|
+
root.setAttribute("role", "listbox");
|
|
3337
|
+
root.setAttribute("aria-label", "Rule search results");
|
|
3338
|
+
const results = this.searchResultsFor(this.searchQuery);
|
|
3339
|
+
const count = this.document.createElement("p");
|
|
3340
|
+
count.dataset.searchCount = "true";
|
|
3341
|
+
count.textContent = `${results.length} rule${results.length === 1 ? "" : "s"} matching \u201C${this.searchQuery.trim()}\u201D.`;
|
|
3342
|
+
const list = this.document.createElement("ul");
|
|
3343
|
+
for (const result of results) {
|
|
3344
|
+
const item = this.document.createElement("li");
|
|
3345
|
+
item.setAttribute("role", "option");
|
|
3346
|
+
const button = this.document.createElement("button");
|
|
3347
|
+
button.type = "button";
|
|
3348
|
+
button.dataset.searchResult = result.path;
|
|
3349
|
+
button.textContent = result.label;
|
|
3350
|
+
item.append(button);
|
|
3351
|
+
list.append(item);
|
|
3352
|
+
}
|
|
3353
|
+
root.append(count, list);
|
|
3354
|
+
if (results.length === 0) {
|
|
3355
|
+
const empty = this.document.createElement("p");
|
|
3356
|
+
empty.textContent = "No rules match that name.";
|
|
3357
|
+
root.append(empty);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
searchResultsFor(query) {
|
|
3361
|
+
const needle = toSearchText(query);
|
|
3362
|
+
if (!needle) return [];
|
|
3363
|
+
const results = [];
|
|
3364
|
+
for (let groupIndex = 0; groupIndex < this.draft.groups.length; groupIndex += 1) {
|
|
3365
|
+
const group = this.draft.groups[groupIndex];
|
|
3366
|
+
for (let ruleIndex = 0; ruleIndex < group.rules.length; ruleIndex += 1) {
|
|
3367
|
+
const rule = group.rules[ruleIndex];
|
|
3368
|
+
if (!toSearchText(rule.name).includes(needle)) continue;
|
|
3369
|
+
results.push({
|
|
3370
|
+
path: pointer("groups", groupIndex, "rules", ruleIndex),
|
|
3371
|
+
label: `Rule: ${displayName(rule.name, "Unnamed rule")} \xB7 ${displayName(group.name, "Unnamed group")}`
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
return results;
|
|
3376
|
+
}
|
|
3377
|
+
renderConfirmation() {
|
|
3378
|
+
const existing = this.host.querySelector("[data-editor-confirmation]");
|
|
3379
|
+
existing?.remove();
|
|
3380
|
+
if (!this.confirmation) return;
|
|
3381
|
+
const overlay = this.document.createElement("div");
|
|
3382
|
+
overlay.dataset.editorConfirmation = "true";
|
|
3383
|
+
overlay.setAttribute("role", "presentation");
|
|
3384
|
+
const dialog = this.document.createElement("section");
|
|
3385
|
+
dialog.dataset.editorDialog = "true";
|
|
3386
|
+
dialog.setAttribute("role", "alertdialog");
|
|
3387
|
+
dialog.setAttribute("aria-modal", "true");
|
|
3388
|
+
const title = this.document.createElement("h2");
|
|
3389
|
+
title.id = `${this.instanceId}-confirmation-title`;
|
|
3390
|
+
const message = this.document.createElement("p");
|
|
3391
|
+
const actions = this.document.createElement("div");
|
|
3392
|
+
actions.dataset.editorDialogActions = "true";
|
|
3393
|
+
const cancel = this.document.createElement("button");
|
|
3394
|
+
cancel.type = "button";
|
|
3395
|
+
cancel.dataset.command = this.confirmation.kind === "cancel" ? "cancel-confirmation" : "remove-confirmation";
|
|
3396
|
+
cancel.dataset.editorKey = "confirm-cancel";
|
|
3397
|
+
cancel.textContent = this.confirmation.kind === "cancel" ? "Keep editing" : "Cancel removal";
|
|
3398
|
+
const confirm = this.document.createElement("button");
|
|
3399
|
+
confirm.type = "button";
|
|
3400
|
+
confirm.dataset.command = this.confirmation.kind === "cancel" ? "confirm-cancel" : "confirm-remove";
|
|
3401
|
+
confirm.dataset.editorKey = "confirm-action";
|
|
3402
|
+
if (this.confirmation.kind === "cancel") {
|
|
3403
|
+
title.textContent = "Discard unsaved changes?";
|
|
3404
|
+
message.textContent = "Your current edits will be replaced by the last saved project.";
|
|
3405
|
+
confirm.textContent = "Discard changes";
|
|
3406
|
+
} else if (this.confirmation.kind === "remove-group") {
|
|
3407
|
+
title.textContent = "Remove group?";
|
|
3408
|
+
message.textContent = `Remove group ${this.confirmation.name} and its rules?`;
|
|
3409
|
+
confirm.textContent = "Remove group";
|
|
3410
|
+
} else {
|
|
3411
|
+
title.textContent = "Remove rule?";
|
|
3412
|
+
message.textContent = `Remove rule ${this.confirmation.name}?`;
|
|
3413
|
+
confirm.textContent = "Remove rule";
|
|
3414
|
+
}
|
|
3415
|
+
dialog.setAttribute("aria-labelledby", title.id);
|
|
3416
|
+
actions.append(cancel, confirm);
|
|
3417
|
+
dialog.append(title, message, actions);
|
|
3418
|
+
overlay.append(dialog);
|
|
3419
|
+
this.host.append(overlay);
|
|
3420
|
+
}
|
|
3421
|
+
createButton(label, key) {
|
|
3422
|
+
const button = this.document.createElement("button");
|
|
3423
|
+
button.type = "button";
|
|
3424
|
+
button.textContent = label;
|
|
3425
|
+
button.dataset.editorKey = `button:${key}`;
|
|
3426
|
+
return button;
|
|
3427
|
+
}
|
|
3428
|
+
createCommandButton(label, command, disabled, data = {}) {
|
|
3429
|
+
const button = this.createButton(
|
|
3430
|
+
label,
|
|
3431
|
+
`command:${command}:${Object.values(data).join(":")}`
|
|
3432
|
+
);
|
|
3433
|
+
button.dataset.command = command;
|
|
3434
|
+
button.disabled = disabled;
|
|
3435
|
+
for (const [key, value] of Object.entries(data)) {
|
|
3436
|
+
if (value !== void 0) button.dataset[key] = value;
|
|
3437
|
+
}
|
|
3438
|
+
return button;
|
|
3439
|
+
}
|
|
3440
|
+
controlId(path) {
|
|
3441
|
+
const existing = Array.from(this.controls.entries()).find(
|
|
3442
|
+
([key]) => key === path
|
|
3443
|
+
)?.[1].id;
|
|
3444
|
+
if (existing) return existing;
|
|
3445
|
+
return `${this.instanceId}-control-${++this.controlNumber}`;
|
|
3446
|
+
}
|
|
3447
|
+
captureFocus() {
|
|
3448
|
+
const active = this.document.activeElement;
|
|
3449
|
+
if (!(active instanceof HTMLElement) || !this.host.contains(active))
|
|
3450
|
+
return void 0;
|
|
3451
|
+
const key = active.dataset.editorKey;
|
|
3452
|
+
if (!key) return void 0;
|
|
3453
|
+
const selectionStart = active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? active.selectionStart : void 0;
|
|
3454
|
+
const selectionEnd = active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? active.selectionEnd : void 0;
|
|
3455
|
+
return { key, selectionStart, selectionEnd };
|
|
3456
|
+
}
|
|
3457
|
+
restoreFocus() {
|
|
3458
|
+
const requested = this.focusRequest;
|
|
3459
|
+
let key = requested;
|
|
3460
|
+
if (requested && this.controls.has(requested)) key = requested;
|
|
3461
|
+
if (!key && this.previousFocus) key = this.previousFocus.key;
|
|
3462
|
+
if (!key) return;
|
|
3463
|
+
let control = this.controls.get(key);
|
|
3464
|
+
if (!control) {
|
|
3465
|
+
const candidates = this.host.querySelectorAll("[data-editor-key]");
|
|
3466
|
+
for (const candidate of candidates) {
|
|
3467
|
+
if (candidate.dataset.editorKey === key) {
|
|
3468
|
+
control = candidate;
|
|
3469
|
+
break;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
if (!control || control.hidden || control.getAttribute("aria-hidden") === "true")
|
|
3474
|
+
return;
|
|
3475
|
+
control.focus();
|
|
3476
|
+
const focus = this.previousFocus;
|
|
3477
|
+
if (focus && (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement)) {
|
|
3478
|
+
if (focus.selectionStart !== void 0 && focus.selectionStart !== null) {
|
|
3479
|
+
try {
|
|
3480
|
+
control.setSelectionRange(
|
|
3481
|
+
focus.selectionStart,
|
|
3482
|
+
focus.selectionEnd ?? focus.selectionStart
|
|
3483
|
+
);
|
|
3484
|
+
} catch {
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
};
|
|
3490
|
+
function isSaveSuccess(value) {
|
|
3491
|
+
return isRecord5(value) && value.ok === true;
|
|
3492
|
+
}
|
|
3493
|
+
function saveFailureDiagnostic(value) {
|
|
3494
|
+
if (!isRecord5(value)) {
|
|
3495
|
+
return diagnostic(
|
|
3496
|
+
"editor.save-failed",
|
|
3497
|
+
"",
|
|
3498
|
+
"The host could not save the project."
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
3501
|
+
const code = typeof value.code === "string" ? value.code : "editor.save-failed";
|
|
3502
|
+
const path = typeof value.path === "string" ? value.path : "";
|
|
3503
|
+
const message = typeof value.message === "string" && value.message.length > 0 ? value.message : "The host could not save the project.";
|
|
3504
|
+
return diagnostic(code, path, message);
|
|
3505
|
+
}
|
|
3506
|
+
function resolveGroupRoute(groupId, groupIds) {
|
|
3507
|
+
if (typeof groupId === "string" && groupId.length > 0 && groupIds.has(groupId)) {
|
|
3508
|
+
return { kind: "group", groupId };
|
|
3509
|
+
}
|
|
3510
|
+
return { kind: "project" };
|
|
3511
|
+
}
|
|
3512
|
+
function createEditor(options) {
|
|
3513
|
+
if (!options || !isHTMLElement(options.root)) {
|
|
3514
|
+
throw new EditorInitializationError([
|
|
3515
|
+
diagnostic("editor.invalid-root", "", "The editor root is invalid.")
|
|
3516
|
+
]);
|
|
3517
|
+
}
|
|
3518
|
+
if (typeof options.validate !== "function" || typeof options.save !== "function") {
|
|
3519
|
+
throw new EditorInitializationError([
|
|
3520
|
+
diagnostic(
|
|
3521
|
+
"editor.invalid-host",
|
|
3522
|
+
"",
|
|
3523
|
+
"The editor requires validation and save host functions."
|
|
3524
|
+
)
|
|
3525
|
+
]);
|
|
3526
|
+
}
|
|
3527
|
+
const snapshot = snapshotOwnData(options.initialProject);
|
|
3528
|
+
const project = snapshot.valid ? asDraftProject(snapshot.value) : void 0;
|
|
3529
|
+
if (!snapshot.valid || !project) {
|
|
3530
|
+
throw new EditorInitializationError([
|
|
3531
|
+
diagnostic(
|
|
3532
|
+
"editor.invalid-initial-project",
|
|
3533
|
+
"",
|
|
3534
|
+
"The initial project is invalid or unsafe."
|
|
3535
|
+
)
|
|
3536
|
+
]);
|
|
3537
|
+
}
|
|
3538
|
+
const extensions = normalizeExtensions(options.ruleTypes);
|
|
3539
|
+
return new EditorControllerImpl(options, project, extensions);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
// packages/editor/src/rule-types/header.ts
|
|
3543
|
+
var FORBIDDEN_REQUEST_HEADERS2 = Object.freeze([
|
|
3544
|
+
"accept-charset",
|
|
3545
|
+
"accept-encoding",
|
|
3546
|
+
"access-control-request-headers",
|
|
3547
|
+
"access-control-request-method",
|
|
3548
|
+
"connection",
|
|
3549
|
+
"content-length",
|
|
3550
|
+
"cookie",
|
|
3551
|
+
"cookie2",
|
|
3552
|
+
"date",
|
|
3553
|
+
"dnt",
|
|
3554
|
+
"expect",
|
|
3555
|
+
"host",
|
|
3556
|
+
"keep-alive",
|
|
3557
|
+
"origin",
|
|
3558
|
+
"proxy-authenticate",
|
|
3559
|
+
"proxy-authorization",
|
|
3560
|
+
"te",
|
|
3561
|
+
"trailer",
|
|
3562
|
+
"transfer-encoding",
|
|
3563
|
+
"upgrade",
|
|
3564
|
+
"via"
|
|
3565
|
+
]);
|
|
3566
|
+
var FORBIDDEN_RESPONSE_HEADERS2 = Object.freeze([
|
|
3567
|
+
"connection",
|
|
3568
|
+
"content-encoding",
|
|
3569
|
+
"content-length",
|
|
3570
|
+
"date",
|
|
3571
|
+
"keep-alive",
|
|
3572
|
+
"proxy-authenticate",
|
|
3573
|
+
"proxy-authorization",
|
|
3574
|
+
"set-cookie",
|
|
3575
|
+
"set-cookie2",
|
|
3576
|
+
"te",
|
|
3577
|
+
"trailer",
|
|
3578
|
+
"transfer-encoding",
|
|
3579
|
+
"upgrade",
|
|
3580
|
+
"via"
|
|
3581
|
+
]);
|
|
3582
|
+
var FORBIDDEN_REQUEST_PREFIXES2 = Object.freeze(["proxy-", "sec-"]);
|
|
3583
|
+
function isForbiddenHeader(name, direction) {
|
|
3584
|
+
const normalized = name.toLowerCase();
|
|
3585
|
+
const forbidden = direction === "request" ? FORBIDDEN_REQUEST_HEADERS2 : FORBIDDEN_RESPONSE_HEADERS2;
|
|
3586
|
+
return forbidden.includes(normalized) || direction === "request" && FORBIDDEN_REQUEST_PREFIXES2.some(
|
|
3587
|
+
(prefix) => normalized.startsWith(prefix)
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
var HEADER_DIRECTIONS = [
|
|
3591
|
+
"request",
|
|
3592
|
+
"response"
|
|
3593
|
+
];
|
|
3594
|
+
var HEADER_OPERATIONS = [
|
|
3595
|
+
"set",
|
|
3596
|
+
"append",
|
|
3597
|
+
"remove"
|
|
3598
|
+
];
|
|
3599
|
+
function createSelect(document, options, value, onChange) {
|
|
3600
|
+
const select = document.createElement("select");
|
|
3601
|
+
for (const option of options) {
|
|
3602
|
+
const opt = document.createElement("option");
|
|
3603
|
+
opt.value = option;
|
|
3604
|
+
opt.textContent = option;
|
|
3605
|
+
if (option === value) opt.selected = true;
|
|
3606
|
+
select.append(opt);
|
|
3607
|
+
}
|
|
3608
|
+
select.addEventListener("change", () => onChange(select.value));
|
|
3609
|
+
return select;
|
|
3610
|
+
}
|
|
3611
|
+
function createInput(document, value, onChange, maxLength) {
|
|
3612
|
+
const input = document.createElement("input");
|
|
3613
|
+
input.type = "text";
|
|
3614
|
+
input.value = value;
|
|
3615
|
+
if (maxLength !== void 0) input.maxLength = maxLength;
|
|
3616
|
+
input.addEventListener("input", () => onChange(input.value));
|
|
3617
|
+
return input;
|
|
3618
|
+
}
|
|
3619
|
+
function createHeaderRuleType() {
|
|
3620
|
+
return {
|
|
3621
|
+
id: "header",
|
|
3622
|
+
label: "Header",
|
|
3623
|
+
matches(rule) {
|
|
3624
|
+
return rule.type === "header";
|
|
3625
|
+
},
|
|
3626
|
+
mount(context) {
|
|
3627
|
+
const document = context.document;
|
|
3628
|
+
const fieldset = document.createElement("fieldset");
|
|
3629
|
+
const legend = document.createElement("legend");
|
|
3630
|
+
legend.textContent = "Header rule";
|
|
3631
|
+
fieldset.append(legend);
|
|
3632
|
+
const directionLabel = document.createElement("label");
|
|
3633
|
+
directionLabel.textContent = "Direction";
|
|
3634
|
+
const directionValue = context.getField("headerDirection") ?? "request";
|
|
3635
|
+
const directionSelect = createSelect(
|
|
3636
|
+
document,
|
|
3637
|
+
HEADER_DIRECTIONS,
|
|
3638
|
+
directionValue,
|
|
3639
|
+
(value) => {
|
|
3640
|
+
context.setField("headerDirection", value);
|
|
3641
|
+
}
|
|
3642
|
+
);
|
|
3643
|
+
context.registerControl("/headerDirection", directionSelect);
|
|
3644
|
+
directionLabel.append(directionSelect);
|
|
3645
|
+
fieldset.append(directionLabel);
|
|
3646
|
+
const operationLabel = document.createElement("label");
|
|
3647
|
+
operationLabel.textContent = "Operation";
|
|
3648
|
+
const operationValue = context.getField("headerOperation") ?? "set";
|
|
3649
|
+
const operationSelect = createSelect(
|
|
3650
|
+
document,
|
|
3651
|
+
HEADER_OPERATIONS,
|
|
3652
|
+
operationValue,
|
|
3653
|
+
(value) => {
|
|
3654
|
+
context.setField("headerOperation", value);
|
|
3655
|
+
}
|
|
3656
|
+
);
|
|
3657
|
+
context.registerControl("/headerOperation", operationSelect);
|
|
3658
|
+
operationLabel.append(operationSelect);
|
|
3659
|
+
fieldset.append(operationLabel);
|
|
3660
|
+
const nameLabel = document.createElement("label");
|
|
3661
|
+
nameLabel.textContent = "Header name";
|
|
3662
|
+
const nameValue = context.getField("headerName") ?? "";
|
|
3663
|
+
const nameInput = createInput(
|
|
3664
|
+
document,
|
|
3665
|
+
nameValue,
|
|
3666
|
+
(value) => {
|
|
3667
|
+
context.setField("headerName", value);
|
|
3668
|
+
},
|
|
3669
|
+
256
|
|
3670
|
+
);
|
|
3671
|
+
context.registerControl("/headerName", nameInput);
|
|
3672
|
+
nameLabel.append(nameInput);
|
|
3673
|
+
fieldset.append(nameLabel);
|
|
3674
|
+
const valueLabel = document.createElement("label");
|
|
3675
|
+
valueLabel.textContent = "Header value";
|
|
3676
|
+
const valueValue = context.getField("headerValue") ?? "";
|
|
3677
|
+
const valueInput = createInput(
|
|
3678
|
+
document,
|
|
3679
|
+
valueValue,
|
|
3680
|
+
(value) => {
|
|
3681
|
+
context.setField("headerValue", value);
|
|
3682
|
+
},
|
|
3683
|
+
4096
|
|
3684
|
+
);
|
|
3685
|
+
context.registerControl("/headerValue", valueInput);
|
|
3686
|
+
valueLabel.append(valueInput);
|
|
3687
|
+
fieldset.append(valueLabel);
|
|
3688
|
+
context.container.append(fieldset);
|
|
3689
|
+
return {
|
|
3690
|
+
destroy() {
|
|
3691
|
+
directionSelect.removeEventListener("change", () => {
|
|
3692
|
+
});
|
|
3693
|
+
operationSelect.removeEventListener("change", () => {
|
|
3694
|
+
});
|
|
3695
|
+
nameInput.removeEventListener("input", () => {
|
|
3696
|
+
});
|
|
3697
|
+
valueInput.removeEventListener("input", () => {
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
};
|
|
3701
|
+
},
|
|
3702
|
+
validate(rule, rulePath) {
|
|
3703
|
+
const diagnostics = [];
|
|
3704
|
+
const direction = rule.headerDirection;
|
|
3705
|
+
const operation = rule.headerOperation;
|
|
3706
|
+
const headerName = rule.headerName;
|
|
3707
|
+
const headerValue = rule.headerValue;
|
|
3708
|
+
if (!direction || !HEADER_DIRECTIONS.includes(direction)) {
|
|
3709
|
+
diagnostics.push({
|
|
3710
|
+
code: "schema.invalid-value",
|
|
3711
|
+
severity: "error",
|
|
3712
|
+
path: `${rulePath}/headerDirection`,
|
|
3713
|
+
message: 'Direction must be "request" or "response".'
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
if (!operation || !HEADER_OPERATIONS.includes(operation)) {
|
|
3717
|
+
diagnostics.push({
|
|
3718
|
+
code: "schema.invalid-value",
|
|
3719
|
+
severity: "error",
|
|
3720
|
+
path: `${rulePath}/headerOperation`,
|
|
3721
|
+
message: 'Operation must be "set", "append", or "remove".'
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
if (typeof headerName !== "string" || headerName.length === 0 || headerName.length > 256) {
|
|
3725
|
+
diagnostics.push({
|
|
3726
|
+
code: "schema.out-of-range",
|
|
3727
|
+
severity: "error",
|
|
3728
|
+
path: `${rulePath}/headerName`,
|
|
3729
|
+
message: "Header name must be 1-256 characters."
|
|
3730
|
+
});
|
|
3731
|
+
} else if (direction && isForbiddenHeader(headerName, direction)) {
|
|
3732
|
+
diagnostics.push({
|
|
3733
|
+
code: "extension.forbidden-header",
|
|
3734
|
+
severity: "error",
|
|
3735
|
+
path: `${rulePath}/headerName`,
|
|
3736
|
+
message: `Header "${headerName}" is forbidden for ${direction} headers.`
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
if ((operation === "set" || operation === "append") && typeof headerValue !== "string") {
|
|
3740
|
+
diagnostics.push({
|
|
3741
|
+
code: "schema.required",
|
|
3742
|
+
severity: "error",
|
|
3743
|
+
path: `${rulePath}/headerValue`,
|
|
3744
|
+
message: "Header value is required for set and append operations."
|
|
3745
|
+
});
|
|
3746
|
+
} else if ((operation === "set" || operation === "append") && typeof headerValue === "string" && headerValue.length > 4096) {
|
|
3747
|
+
diagnostics.push({
|
|
3748
|
+
code: "schema.out-of-range",
|
|
3749
|
+
severity: "error",
|
|
3750
|
+
path: `${rulePath}/headerValue`,
|
|
3751
|
+
message: "Header value must be at most 4096 characters."
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
if (operation === "remove" && headerValue !== void 0) {
|
|
3755
|
+
diagnostics.push({
|
|
3756
|
+
code: "schema.unexpected",
|
|
3757
|
+
severity: "error",
|
|
3758
|
+
path: `${rulePath}/headerValue`,
|
|
3759
|
+
message: "Header value must not be provided for remove operation."
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
return diagnostics;
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
// packages/editor/src/rule-types/redirect.ts
|
|
3768
|
+
function createRedirectRuleType() {
|
|
3769
|
+
return {
|
|
3770
|
+
id: "redirect",
|
|
3771
|
+
label: "Redirect",
|
|
3772
|
+
matches(rule) {
|
|
3773
|
+
return rule.type === "redirect";
|
|
3774
|
+
},
|
|
3775
|
+
mount(context) {
|
|
3776
|
+
const document = context.document;
|
|
3777
|
+
const label = document.createElement("label");
|
|
3778
|
+
label.textContent = "Redirect destination URL";
|
|
3779
|
+
const input = document.createElement("input");
|
|
3780
|
+
input.type = "text";
|
|
3781
|
+
input.maxLength = 2048;
|
|
3782
|
+
const existing = context.getField("redirect.destination");
|
|
3783
|
+
input.value = typeof existing === "string" ? existing : "";
|
|
3784
|
+
input.addEventListener("input", () => {
|
|
3785
|
+
context.setField("redirect.destination", input.value);
|
|
3786
|
+
});
|
|
3787
|
+
context.registerControl("/redirect/destination", input);
|
|
3788
|
+
label.append(input);
|
|
3789
|
+
context.container.append(label);
|
|
3790
|
+
return {
|
|
3791
|
+
destroy() {
|
|
3792
|
+
input.removeEventListener("input", () => {
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3796
|
+
},
|
|
3797
|
+
validate(rule, rulePath) {
|
|
3798
|
+
const redirect = rule.redirect;
|
|
3799
|
+
const destination = redirect !== null && typeof redirect === "object" && typeof redirect.destination === "string" ? redirect.destination : "";
|
|
3800
|
+
const urlRegex = typeof rule.urlRegex === "string" ? rule.urlRegex : "";
|
|
3801
|
+
const issues = validateRedirectDestination(destination, urlRegex);
|
|
3802
|
+
return issues.map((issue) => ({
|
|
3803
|
+
code: `schema.${issue.code}`,
|
|
3804
|
+
severity: "error",
|
|
3805
|
+
path: `${rulePath}/redirect/destination`,
|
|
3806
|
+
message: issue.message
|
|
3807
|
+
}));
|
|
3808
|
+
}
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
export {
|
|
3812
|
+
EditorInitializationError,
|
|
3813
|
+
builtInRuleTypes,
|
|
3814
|
+
createEditor,
|
|
3815
|
+
createHeaderRuleType,
|
|
3816
|
+
createMockRuleType,
|
|
3817
|
+
createRedirectRuleType,
|
|
3818
|
+
createRequestBodyRuleType,
|
|
3819
|
+
createResponseBodyRuleType,
|
|
3820
|
+
queryRuleType,
|
|
3821
|
+
urlToExactRegex
|
|
3822
|
+
};
|