rork-xcode 0.11.0 → 0.12.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 +34 -1
- package/dist/index.d.ts +253 -29
- package/dist/index.js +626 -106
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -213,20 +213,25 @@ function createReferenceComments(root) {
|
|
|
213
213
|
*
|
|
214
214
|
* @module
|
|
215
215
|
*/
|
|
216
|
-
/** UTF-16 code unit of `\n`, used to count lines when reporting a failure. */
|
|
217
216
|
const LINE_FEED = 10;
|
|
217
|
+
const CARRIAGE_RETURN = 13;
|
|
218
218
|
/**
|
|
219
219
|
* Converts a source offset into a position.
|
|
220
220
|
*
|
|
221
221
|
* Runs only when an error is actually thrown, so parsing never pays for line
|
|
222
|
-
* tracking on the happy path.
|
|
222
|
+
* tracking on the happy path. All three line-ending conventions count as
|
|
223
|
+
* one break each, with the carriage return of a `\r\n` pair deferring to
|
|
224
|
+
* its line feed.
|
|
223
225
|
*/
|
|
224
226
|
function positionAt(source, offset) {
|
|
225
227
|
let line = 1;
|
|
226
228
|
let lineStart = 0;
|
|
227
|
-
for (let i = 0; i < offset && i < source.length; i++)
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
230
|
+
const code = source.charCodeAt(i);
|
|
231
|
+
if (code === LINE_FEED || code === CARRIAGE_RETURN && source.charCodeAt(i + 1) !== LINE_FEED) {
|
|
232
|
+
line++;
|
|
233
|
+
lineStart = i + 1;
|
|
234
|
+
}
|
|
230
235
|
}
|
|
231
236
|
return {
|
|
232
237
|
offset,
|
|
@@ -283,6 +288,30 @@ var XcschemeParseError = class extends Error {
|
|
|
283
288
|
}
|
|
284
289
|
};
|
|
285
290
|
/**
|
|
291
|
+
* Thrown when the source text is not a well-formed workspace data file
|
|
292
|
+
* (the XML dialect of `contents.xcworkspacedata` files).
|
|
293
|
+
*
|
|
294
|
+
* The message always embeds the line and column of the failure, and the
|
|
295
|
+
* same information is available in structured form on {@link position}
|
|
296
|
+
* for programmatic use.
|
|
297
|
+
*/
|
|
298
|
+
var XcworkspaceParseError = class extends Error {
|
|
299
|
+
/** Where in the source text parsing failed. */
|
|
300
|
+
position;
|
|
301
|
+
/**
|
|
302
|
+
* @param message Failure description without location. The location is
|
|
303
|
+
* appended automatically.
|
|
304
|
+
* @param source Full source text, used to compute the position.
|
|
305
|
+
* @param offset Character offset of the failure inside `source`.
|
|
306
|
+
*/
|
|
307
|
+
constructor(message, source, offset) {
|
|
308
|
+
const position = positionAt(source, offset);
|
|
309
|
+
super(`${message} (line ${position.line}, column ${position.column})`);
|
|
310
|
+
this.name = "XcworkspaceParseError";
|
|
311
|
+
this.position = position;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
286
315
|
* Thrown when the source text is not a well-formed build configuration
|
|
287
316
|
* file (the line-based format of `.xcconfig` files).
|
|
288
317
|
*
|
|
@@ -328,6 +357,27 @@ var XcschemeBuildError = class extends Error {
|
|
|
328
357
|
}
|
|
329
358
|
};
|
|
330
359
|
/**
|
|
360
|
+
* Thrown when a workspace element cannot be written as XML.
|
|
361
|
+
*
|
|
362
|
+
* Raised for element and attribute names that are not valid XML names and
|
|
363
|
+
* for attribute values carrying control characters XML 1.0 cannot encode.
|
|
364
|
+
* The {@link path} pinpoints the offending element inside the tree.
|
|
365
|
+
*/
|
|
366
|
+
var XcworkspaceBuildError = class extends Error {
|
|
367
|
+
/** Path to the offending element from the root, e.g. `Workspace.FileRef[0]`. */
|
|
368
|
+
path;
|
|
369
|
+
/**
|
|
370
|
+
* @param message Failure description without location. The element path
|
|
371
|
+
* is appended automatically.
|
|
372
|
+
* @param path Path to the offending element from the root.
|
|
373
|
+
*/
|
|
374
|
+
constructor(message, path) {
|
|
375
|
+
super(`${message} (at ${path})`);
|
|
376
|
+
this.name = "XcworkspaceBuildError";
|
|
377
|
+
this.path = path;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
331
381
|
* Thrown by the object model when an operation cannot proceed, because the
|
|
332
382
|
* document lacks the structure the operation needs (no objects dictionary,
|
|
333
383
|
* no root project object), a view's object was removed from the document,
|
|
@@ -524,17 +574,17 @@ function sanitizeComment(comment) {
|
|
|
524
574
|
* Sharing the strings avoids a `"\t".repeat(depth)` allocation on every
|
|
525
575
|
* line of output.
|
|
526
576
|
*/
|
|
527
|
-
const INDENTS = [""];
|
|
577
|
+
const INDENTS$1 = [""];
|
|
528
578
|
/**
|
|
529
579
|
* Returns the shared indentation string for a nesting depth.
|
|
530
580
|
*/
|
|
531
581
|
function indentString(depth) {
|
|
532
|
-
const cached = INDENTS[depth];
|
|
582
|
+
const cached = INDENTS$1[depth];
|
|
533
583
|
if (cached != null) return cached;
|
|
534
|
-
let known = INDENTS.at(-1);
|
|
535
|
-
while (INDENTS.length <= depth) {
|
|
584
|
+
let known = INDENTS$1.at(-1);
|
|
585
|
+
while (INDENTS$1.length <= depth) {
|
|
536
586
|
known += " ";
|
|
537
|
-
INDENTS.push(known);
|
|
587
|
+
INDENTS$1.push(known);
|
|
538
588
|
}
|
|
539
589
|
return known;
|
|
540
590
|
}
|
|
@@ -896,8 +946,7 @@ function expandReference(reference, inner, lookup, expandLookupValues, active) {
|
|
|
896
946
|
const name = colon === -1 ? inner : inner.slice(0, colon);
|
|
897
947
|
if (active.has(name)) return reference;
|
|
898
948
|
const raw = lookup(name);
|
|
899
|
-
const nested = new Set(active);
|
|
900
|
-
nested.add(name);
|
|
949
|
+
const nested = /* @__PURE__ */ new Set([...active, name]);
|
|
901
950
|
let value = raw;
|
|
902
951
|
if (raw != null && expandLookupValues) value = expand(raw, lookup, expandLookupValues, nested);
|
|
903
952
|
if (colon !== -1) {
|
|
@@ -2228,11 +2277,11 @@ const IS_LITERAL_CHAR = (() => {
|
|
|
2228
2277
|
for (const ch of "_$/:.-") table[ch.charCodeAt(0)] = 1;
|
|
2229
2278
|
return table;
|
|
2230
2279
|
})();
|
|
2231
|
-
const CODE_TAB$
|
|
2232
|
-
const CODE_LINE_FEED$
|
|
2233
|
-
const CODE_CARRIAGE_RETURN$
|
|
2280
|
+
const CODE_TAB$2 = 9;
|
|
2281
|
+
const CODE_LINE_FEED$2 = 10;
|
|
2282
|
+
const CODE_CARRIAGE_RETURN$2 = 13;
|
|
2234
2283
|
const CODE_SPACE$1 = 32;
|
|
2235
|
-
const CODE_QUOTE$
|
|
2284
|
+
const CODE_QUOTE$2 = 34;
|
|
2236
2285
|
const CODE_SINGLE_QUOTE = 39;
|
|
2237
2286
|
const CODE_OPEN_PAREN = 40;
|
|
2238
2287
|
const CODE_CLOSE_PAREN = 41;
|
|
@@ -2244,9 +2293,9 @@ const CODE_SLASH$1 = 47;
|
|
|
2244
2293
|
const CODE_ZERO = 48;
|
|
2245
2294
|
const CODE_NINE = 57;
|
|
2246
2295
|
const CODE_SEMICOLON = 59;
|
|
2247
|
-
const CODE_LESS_THAN$
|
|
2296
|
+
const CODE_LESS_THAN$2 = 60;
|
|
2248
2297
|
const CODE_EQUALS$1 = 61;
|
|
2249
|
-
const CODE_GREATER_THAN$
|
|
2298
|
+
const CODE_GREATER_THAN$2 = 62;
|
|
2250
2299
|
const CODE_BACKSLASH = 92;
|
|
2251
2300
|
const CODE_OPEN_BRACE = 123;
|
|
2252
2301
|
const CODE_CLOSE_BRACE = 125;
|
|
@@ -2259,9 +2308,9 @@ const CODE_CLOSE_BRACE = 125;
|
|
|
2259
2308
|
const IS_WHITESPACE = (() => {
|
|
2260
2309
|
const table = /* @__PURE__ */ new Uint8Array(256);
|
|
2261
2310
|
table[CODE_SPACE$1] = 1;
|
|
2262
|
-
table[CODE_TAB$
|
|
2263
|
-
table[CODE_CARRIAGE_RETURN$
|
|
2264
|
-
table[CODE_LINE_FEED$
|
|
2311
|
+
table[CODE_TAB$2] = 1;
|
|
2312
|
+
table[CODE_CARRIAGE_RETURN$2] = 1;
|
|
2313
|
+
table[CODE_LINE_FEED$2] = 1;
|
|
2265
2314
|
return table;
|
|
2266
2315
|
})();
|
|
2267
2316
|
/**
|
|
@@ -2439,7 +2488,7 @@ var Parser$1 = class {
|
|
|
2439
2488
|
const input = this.input;
|
|
2440
2489
|
const length = input.length;
|
|
2441
2490
|
const start = ++this.pos;
|
|
2442
|
-
while (this.pos < length && input.charCodeAt(this.pos) !== CODE_GREATER_THAN$
|
|
2491
|
+
while (this.pos < length && input.charCodeAt(this.pos) !== CODE_GREATER_THAN$2) this.pos++;
|
|
2443
2492
|
if (this.pos >= length) this.fail("Unterminated data run", start - 1);
|
|
2444
2493
|
let hex = "";
|
|
2445
2494
|
for (let i = start; i < this.pos; i++) {
|
|
@@ -2482,7 +2531,7 @@ var Parser$1 = class {
|
|
|
2482
2531
|
return result;
|
|
2483
2532
|
}
|
|
2484
2533
|
let key;
|
|
2485
|
-
if (code === CODE_QUOTE$
|
|
2534
|
+
if (code === CODE_QUOTE$2 || code === CODE_SINGLE_QUOTE) key = this.readQuotedString();
|
|
2486
2535
|
else if (IS_LITERAL_CHAR[code] === 1) key = this.readLiteral();
|
|
2487
2536
|
else this.fail(`Expected a key but found '${input[this.pos]}'`);
|
|
2488
2537
|
this.expect(CODE_EQUALS$1, "=");
|
|
@@ -2538,9 +2587,9 @@ var Parser$1 = class {
|
|
|
2538
2587
|
const code = this.input.charCodeAt(this.pos);
|
|
2539
2588
|
if (code === CODE_OPEN_BRACE) return this.parseObject();
|
|
2540
2589
|
if (code === CODE_OPEN_PAREN) return this.parseArray();
|
|
2541
|
-
if (code === CODE_QUOTE$
|
|
2590
|
+
if (code === CODE_QUOTE$2 || code === CODE_SINGLE_QUOTE) return this.readQuotedString();
|
|
2542
2591
|
if (IS_LITERAL_CHAR[code] === 1) return interpretLiteral(this.readLiteral());
|
|
2543
|
-
if (code === CODE_LESS_THAN$
|
|
2592
|
+
if (code === CODE_LESS_THAN$2) return this.readData();
|
|
2544
2593
|
this.fail(`Expected a value but found '${this.input[this.pos]}'`);
|
|
2545
2594
|
}
|
|
2546
2595
|
};
|
|
@@ -3208,8 +3257,7 @@ var Target = class extends XcodeObject {
|
|
|
3208
3257
|
const resolve = (name, fromLayer, active) => {
|
|
3209
3258
|
const guard = `${fromLayer}:${name}`;
|
|
3210
3259
|
if (active.has(guard)) return;
|
|
3211
|
-
const nested = new Set(active);
|
|
3212
|
-
nested.add(guard);
|
|
3260
|
+
const nested = /* @__PURE__ */ new Set([...active, guard]);
|
|
3213
3261
|
for (let layer = fromLayer; layer < layers.length; layer++) {
|
|
3214
3262
|
const settings = layers[layer];
|
|
3215
3263
|
if (settings == null || !(name in settings)) continue;
|
|
@@ -4286,57 +4334,159 @@ function stripReferences(properties, id) {
|
|
|
4286
4334
|
}
|
|
4287
4335
|
}
|
|
4288
4336
|
//#endregion
|
|
4289
|
-
//#region src/
|
|
4337
|
+
//#region src/xml/types.ts
|
|
4290
4338
|
/**
|
|
4291
4339
|
* Whether a node is an element rather than a comment.
|
|
4292
4340
|
*/
|
|
4293
|
-
function
|
|
4341
|
+
function isXmlElement(node) {
|
|
4294
4342
|
return "name" in node;
|
|
4295
4343
|
}
|
|
4344
|
+
/**
|
|
4345
|
+
* Collects elements of the given name anywhere under a root, in document
|
|
4346
|
+
* order, root included. Passing no name collects every element. Scheme
|
|
4347
|
+
* and workspace models both build their typed views over this query.
|
|
4348
|
+
*/
|
|
4349
|
+
function xmlElements(root, name) {
|
|
4350
|
+
const found = [];
|
|
4351
|
+
const visit = (element) => {
|
|
4352
|
+
if (name == null || element.name === name) found.push(element);
|
|
4353
|
+
for (const child of element.children) if (isXmlElement(child)) visit(child);
|
|
4354
|
+
};
|
|
4355
|
+
visit(root);
|
|
4356
|
+
return found;
|
|
4357
|
+
}
|
|
4296
4358
|
//#endregion
|
|
4297
|
-
//#region src/
|
|
4359
|
+
//#region src/xml/build.ts
|
|
4298
4360
|
/**
|
|
4299
|
-
* Serializer for
|
|
4361
|
+
* Serializer for Xcode's XML dialect, shared by scheme and workspace
|
|
4362
|
+
* files.
|
|
4300
4363
|
*
|
|
4301
4364
|
* The output reproduces Xcode's own layout byte for byte. Xcode writes
|
|
4302
4365
|
* the UTF-8 declaration, indents with three spaces, puts each attribute
|
|
4303
4366
|
* on its own line with spaces around the equals sign, glues the closing
|
|
4304
4367
|
* angle bracket to the last attribute, and closes every element with an
|
|
4305
|
-
* explicit close tag. Parsing an Xcode-written
|
|
4368
|
+
* explicit close tag. Parsing an Xcode-written file and building it
|
|
4306
4369
|
* back therefore yields the identical file, and any other input reaches
|
|
4307
4370
|
* that canonical form in one build.
|
|
4308
4371
|
*
|
|
4372
|
+
* Each file format wraps this core with its own error type, so a caller
|
|
4373
|
+
* always sees the error class of the format it asked for.
|
|
4374
|
+
*
|
|
4309
4375
|
* @module
|
|
4310
4376
|
*/
|
|
4311
|
-
/**
|
|
4377
|
+
/**
|
|
4378
|
+
* The internal failure the renderers throw. It carries the offending
|
|
4379
|
+
* node instead of a path, and {@link buildXmlDocument} resolves the path
|
|
4380
|
+
* by searching the tree only when a failure actually happens, so the
|
|
4381
|
+
* happy path never pays for path bookkeeping.
|
|
4382
|
+
*/
|
|
4383
|
+
var XmlBuildFailure = class extends Error {
|
|
4384
|
+
/** The element or comment node the failure points at. */
|
|
4385
|
+
node;
|
|
4386
|
+
constructor(message, node) {
|
|
4387
|
+
super(message);
|
|
4388
|
+
this.name = "XmlBuildFailure";
|
|
4389
|
+
this.node = node;
|
|
4390
|
+
}
|
|
4391
|
+
};
|
|
4392
|
+
/** The declaration line Xcode writes at the top of every file. */
|
|
4312
4393
|
const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
|
4313
|
-
/** One indentation step of Xcode's
|
|
4394
|
+
/** One indentation step of Xcode's writer. */
|
|
4314
4395
|
const INDENT = " ";
|
|
4315
4396
|
/**
|
|
4397
|
+
* Indentation strings by depth, extended on demand. Documents nest a
|
|
4398
|
+
* handful of levels, and reusing the strings keeps the writer from
|
|
4399
|
+
* re-allocating the same prefixes for every node.
|
|
4400
|
+
*/
|
|
4401
|
+
const INDENTS = [""];
|
|
4402
|
+
/**
|
|
4403
|
+
* The indentation prefix for a nesting depth.
|
|
4404
|
+
*/
|
|
4405
|
+
function indentAt(depth) {
|
|
4406
|
+
for (let known = INDENTS.length; known <= depth; known++) INDENTS.push(INDENTS[known - 1] + INDENT);
|
|
4407
|
+
return INDENTS[depth];
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4316
4410
|
* Matches valid XML names. A stray non-name, such as an empty string or
|
|
4317
4411
|
* one carrying spaces or XML syntax, must fail rather than produce a
|
|
4318
4412
|
* document no parser accepts.
|
|
4319
4413
|
*/
|
|
4320
4414
|
const NAME_PATTERN = /^[A-Za-z_:][A-Za-z0-9_:.-]*$/u;
|
|
4321
4415
|
/**
|
|
4322
|
-
*
|
|
4323
|
-
*
|
|
4324
|
-
*
|
|
4416
|
+
* Names that already passed {@link NAME_PATTERN}, capped so hostile
|
|
4417
|
+
* documents cannot grow the set without bound. Element and attribute
|
|
4418
|
+
* vocabularies repeat heavily, so nearly every check after the first is
|
|
4419
|
+
* a set lookup instead of a regex test.
|
|
4325
4420
|
*/
|
|
4326
|
-
const
|
|
4421
|
+
const VALID_NAMES = /* @__PURE__ */ new Set();
|
|
4327
4422
|
/**
|
|
4328
|
-
*
|
|
4329
|
-
*
|
|
4330
|
-
* @throws XcschemeBuildError for element or attribute names that are not
|
|
4331
|
-
* XML names and for attribute values carrying unencodable control
|
|
4332
|
-
* characters, with the path of the offending node.
|
|
4423
|
+
* Whether a name is a valid XML name, memoized across documents.
|
|
4333
4424
|
*/
|
|
4334
|
-
function
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4425
|
+
function isValidName(name) {
|
|
4426
|
+
if (VALID_NAMES.has(name)) return true;
|
|
4427
|
+
if (!NAME_PATTERN.test(name)) return false;
|
|
4428
|
+
if (VALID_NAMES.size < 512) VALID_NAMES.add(name);
|
|
4429
|
+
return true;
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* Serializes a document to text in Xcode's canonical layout, reporting
|
|
4433
|
+
* failures through the given error factory.
|
|
4434
|
+
*/
|
|
4435
|
+
function buildXmlDocument(document, makeError) {
|
|
4436
|
+
try {
|
|
4437
|
+
let output = XML_DECLARATION;
|
|
4438
|
+
for (const comment of document.leading) output += renderComment(comment, 0);
|
|
4439
|
+
output += renderElement(document.root, 0);
|
|
4440
|
+
for (const comment of document.trailing) output += renderComment(comment, 0);
|
|
4441
|
+
return output;
|
|
4442
|
+
} catch (error) {
|
|
4443
|
+
if (error instanceof XmlBuildFailure) throw makeError(error.message, pathOf(document, error.node));
|
|
4444
|
+
throw error;
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
/**
|
|
4448
|
+
* Resolves the path of a node for an error message, in the
|
|
4449
|
+
* `Scheme.BuildAction[0]` form, by walking the tree. This runs only when
|
|
4450
|
+
* a build actually fails.
|
|
4451
|
+
*/
|
|
4452
|
+
function pathOf(document, node) {
|
|
4453
|
+
if (node == null || document.leading.includes(node) || document.trailing.includes(node)) return "document";
|
|
4454
|
+
const search = (element, path) => {
|
|
4455
|
+
if (element === node) return path;
|
|
4456
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4457
|
+
for (const child of element.children) {
|
|
4458
|
+
if (!isXmlElement(child)) {
|
|
4459
|
+
if (child === node) return path;
|
|
4460
|
+
continue;
|
|
4461
|
+
}
|
|
4462
|
+
const index = seen.get(child.name) ?? 0;
|
|
4463
|
+
seen.set(child.name, index + 1);
|
|
4464
|
+
const found = search(child, `${path}.${child.name}[${index}]`);
|
|
4465
|
+
if (found != null) return found;
|
|
4466
|
+
}
|
|
4467
|
+
};
|
|
4468
|
+
return search(document.root, document.root.name) ?? document.root.name;
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
4471
|
+
* Matches the characters no XML document can carry, in any context. The
|
|
4472
|
+
* alternatives are the control characters XML cannot represent,
|
|
4473
|
+
* unpaired surrogate halves, and the two noncharacters at the end of
|
|
4474
|
+
* the basic plane. Comments validate against this directly, since
|
|
4475
|
+
* comment text has no escapes to fall back on.
|
|
4476
|
+
*/
|
|
4477
|
+
const UNCARRIABLE_PATTERN$1 = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uFFFE\uFFFF]/u;
|
|
4478
|
+
/**
|
|
4479
|
+
* Renders one comment, failing on text the comment grammar cannot hold.
|
|
4480
|
+
* A `--` inside the text or a `-` against the closing marker would
|
|
4481
|
+
* reparse at a different terminator, and characters outside XML's
|
|
4482
|
+
* character set have no escaped form inside a comment, so emitting
|
|
4483
|
+
* either would corrupt the document.
|
|
4484
|
+
*/
|
|
4485
|
+
function renderComment(node, depth) {
|
|
4486
|
+
const comment = node.comment;
|
|
4487
|
+
if (comment.includes("--") || comment.endsWith("-")) throw new XmlBuildFailure("Comments cannot contain -- or end with -", node);
|
|
4488
|
+
if (UNCARRIABLE_PATTERN$1.test(comment)) throw new XmlBuildFailure("Comments cannot contain characters XML cannot carry", node);
|
|
4489
|
+
return `${indentAt(depth)}<!--${comment}-->\n`;
|
|
4340
4490
|
}
|
|
4341
4491
|
/**
|
|
4342
4492
|
* Renders one element with Xcode's layout. Attributes go each on their
|
|
@@ -4344,67 +4494,109 @@ function buildXcscheme(document) {
|
|
|
4344
4494
|
* attribute, children indent one step, and the close tag is always
|
|
4345
4495
|
* explicit.
|
|
4346
4496
|
*/
|
|
4347
|
-
function renderElement(element, depth
|
|
4348
|
-
if (!
|
|
4349
|
-
const indent =
|
|
4497
|
+
function renderElement(element, depth) {
|
|
4498
|
+
if (!isValidName(element.name)) throw new XmlBuildFailure(`Element name ${JSON.stringify(element.name)} is not a valid XML name`, element);
|
|
4499
|
+
const indent = indentAt(depth);
|
|
4350
4500
|
const names = Object.keys(element.attributes);
|
|
4351
4501
|
let output;
|
|
4352
4502
|
if (names.length === 0) output = `${indent}<${element.name}>\n`;
|
|
4353
4503
|
else {
|
|
4354
4504
|
output = `${indent}<${element.name}\n`;
|
|
4355
|
-
const attributeIndent =
|
|
4505
|
+
const attributeIndent = indentAt(depth + 1);
|
|
4356
4506
|
for (let i = 0; i < names.length; i++) {
|
|
4357
4507
|
const name = names[i];
|
|
4358
|
-
if (!
|
|
4359
|
-
const value = escapeAttribute(element.attributes[name],
|
|
4508
|
+
if (!isValidName(name)) throw new XmlBuildFailure(`Attribute name ${JSON.stringify(name)} is not a valid XML name`, element);
|
|
4509
|
+
const value = escapeAttribute(element.attributes[name], element, name);
|
|
4360
4510
|
const terminator = i === names.length - 1 ? ">" : "";
|
|
4361
4511
|
output += `${attributeIndent}${name} = "${value}"${terminator}\n`;
|
|
4362
4512
|
}
|
|
4363
4513
|
}
|
|
4364
|
-
|
|
4514
|
+
const children = element.children;
|
|
4515
|
+
const childDepth = depth + 1;
|
|
4516
|
+
for (const child of children) output += isXmlElement(child) ? renderElement(child, childDepth) : renderComment(child, childDepth);
|
|
4365
4517
|
output += `${indent}</${element.name}>\n`;
|
|
4366
4518
|
return output;
|
|
4367
4519
|
}
|
|
4520
|
+
const CODE_TAB$1 = 9;
|
|
4521
|
+
const CODE_LINE_FEED$1 = 10;
|
|
4522
|
+
const CODE_CARRIAGE_RETURN$1 = 13;
|
|
4523
|
+
const CODE_QUOTE$1 = 34;
|
|
4524
|
+
const CODE_AMPERSAND$1 = 38;
|
|
4525
|
+
const CODE_APOSTROPHE$1 = 39;
|
|
4526
|
+
const CODE_LESS_THAN$1 = 60;
|
|
4527
|
+
const CODE_GREATER_THAN$1 = 62;
|
|
4368
4528
|
/**
|
|
4369
|
-
*
|
|
4370
|
-
*
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
for (const child of children) if (isXcschemeElement(child)) {
|
|
4376
|
-
const index = seen.get(child.name) ?? 0;
|
|
4377
|
-
seen.set(child.name, index + 1);
|
|
4378
|
-
output += renderElement(child, depth, `${path}.${child.name}[${index}]`);
|
|
4379
|
-
} else output += `${INDENT.repeat(depth)}<!--${child.comment}-->\n`;
|
|
4380
|
-
return output;
|
|
4381
|
-
}
|
|
4529
|
+
* Matches every code unit the writer must escape, validate, or reject.
|
|
4530
|
+
* Nearly all attribute values contain none of them, and the single
|
|
4531
|
+
* regex test is the cheapest full-string scan the engine offers, so a
|
|
4532
|
+
* miss returns the value with no further work.
|
|
4533
|
+
*/
|
|
4534
|
+
const NEEDS_WORK_PATTERN = /[\u0000-\u001F&<>"'\uD800-\uDFFF\uFFFE\uFFFF]/u;
|
|
4382
4535
|
/**
|
|
4383
4536
|
* Escapes an attribute value the way Xcode's writer does. XML syntax
|
|
4384
4537
|
* characters become the five named entities, and tab, line feed, and
|
|
4385
4538
|
* carriage return become character references so they survive
|
|
4386
4539
|
* attribute-value normalization on the next parse.
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4540
|
+
*
|
|
4541
|
+
* The common value with nothing to escape returns as-is after one regex
|
|
4542
|
+
* scan. Only values carrying an interesting code unit take the
|
|
4543
|
+
* classifying pass, which escapes what has an escape and rejects what
|
|
4544
|
+
* no XML document can carry, meaning the control characters XML cannot
|
|
4545
|
+
* represent, unpaired surrogate halves, and the two noncharacters at
|
|
4546
|
+
* the end of the basic plane.
|
|
4547
|
+
*/
|
|
4548
|
+
function escapeAttribute(value, element, attributeName) {
|
|
4549
|
+
if (!NEEDS_WORK_PATTERN.test(value)) return value;
|
|
4550
|
+
let needsEscaping = false;
|
|
4551
|
+
for (let i = 0; i < value.length; i++) {
|
|
4552
|
+
const code = value.charCodeAt(i);
|
|
4553
|
+
if (code < 32) {
|
|
4554
|
+
if (code === CODE_TAB$1 || code === CODE_LINE_FEED$1 || code === CODE_CARRIAGE_RETURN$1) {
|
|
4555
|
+
needsEscaping = true;
|
|
4556
|
+
continue;
|
|
4557
|
+
}
|
|
4558
|
+
throw new XmlBuildFailure(`Attribute ${attributeName} contains the control character U+${code.toString(16).padStart(4, "0").toUpperCase()}, which XML cannot encode`, element);
|
|
4559
|
+
}
|
|
4560
|
+
if (code === CODE_AMPERSAND$1 || code === CODE_LESS_THAN$1 || code === CODE_GREATER_THAN$1 || code === CODE_QUOTE$1 || code === CODE_APOSTROPHE$1) {
|
|
4561
|
+
needsEscaping = true;
|
|
4562
|
+
continue;
|
|
4563
|
+
}
|
|
4564
|
+
if (code >= 55296) {
|
|
4565
|
+
if (code <= 56319) {
|
|
4566
|
+
const next = value.charCodeAt(i + 1);
|
|
4567
|
+
if (next >= 56320 && next <= 57343) {
|
|
4568
|
+
i++;
|
|
4569
|
+
continue;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
if (code <= 57343 || code === 65534 || code === 65535) throw new XmlBuildFailure(`Attribute ${attributeName} contains a code point XML cannot carry (U+${code.toString(16).padStart(4, "0").toUpperCase()})`, element);
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
if (!needsEscaping) return value;
|
|
4391
4576
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'").replaceAll(" ", "	").replaceAll("\n", " ").replaceAll("\r", " ");
|
|
4392
4577
|
}
|
|
4393
4578
|
//#endregion
|
|
4394
|
-
//#region src/scheme/
|
|
4579
|
+
//#region src/scheme/build.ts
|
|
4395
4580
|
/**
|
|
4396
|
-
*
|
|
4397
|
-
*
|
|
4398
|
-
*
|
|
4399
|
-
* child elements, and there is no text content, no namespace, and no
|
|
4400
|
-
* DOCTYPE. The parser accepts that shape from any writer, since attribute
|
|
4401
|
-
* order, quoting style, and whitespace vary across tools. Character and
|
|
4402
|
-
* entity references in attribute values resolve to their characters, and
|
|
4403
|
-
* comments are preserved. Anything outside the shape fails loudly with a
|
|
4404
|
-
* position, in line with the pbxproj parser.
|
|
4581
|
+
* Serializer entry point for the `.xcscheme` dialect. The layout rules
|
|
4582
|
+
* live in the shared XML core, and this wrapper binds them to the scheme
|
|
4583
|
+
* error type.
|
|
4405
4584
|
*
|
|
4406
4585
|
* @module
|
|
4407
4586
|
*/
|
|
4587
|
+
/**
|
|
4588
|
+
* Serializes a scheme document to the text of a `.xcscheme` file in
|
|
4589
|
+
* Xcode's canonical layout.
|
|
4590
|
+
*
|
|
4591
|
+
* @throws XcschemeBuildError for element or attribute names that are not
|
|
4592
|
+
* XML names and for attribute values carrying unencodable control
|
|
4593
|
+
* characters, with the path of the offending node.
|
|
4594
|
+
*/
|
|
4595
|
+
function buildXcscheme(document) {
|
|
4596
|
+
return buildXmlDocument(document, (message, path) => new XcschemeBuildError(message, path));
|
|
4597
|
+
}
|
|
4598
|
+
//#endregion
|
|
4599
|
+
//#region src/xml/parse.ts
|
|
4408
4600
|
const CODE_TAB = 9;
|
|
4409
4601
|
const CODE_LINE_FEED = 10;
|
|
4410
4602
|
const CODE_CARRIAGE_RETURN = 13;
|
|
@@ -4429,7 +4621,7 @@ function isWhitespace(code) {
|
|
|
4429
4621
|
return code === CODE_SPACE || code === CODE_TAB || code === CODE_LINE_FEED || code === CODE_CARRIAGE_RETURN;
|
|
4430
4622
|
}
|
|
4431
4623
|
/**
|
|
4432
|
-
* Whether a code unit can start an XML name. The
|
|
4624
|
+
* Whether a code unit can start an XML name. The dialect's vocabulary is
|
|
4433
4625
|
* ASCII, so the accepted alphabet is letters, underscore, and colon.
|
|
4434
4626
|
*/
|
|
4435
4627
|
function isNameStart(code) {
|
|
@@ -4450,25 +4642,41 @@ const NAMED_ENTITIES = {
|
|
|
4450
4642
|
quot: "\""
|
|
4451
4643
|
};
|
|
4452
4644
|
/**
|
|
4453
|
-
*
|
|
4454
|
-
*
|
|
4455
|
-
*
|
|
4456
|
-
* document, with the line and column of the failure.
|
|
4645
|
+
* Matches the characters no XML document can carry, mirroring the
|
|
4646
|
+
* writer's validation so a comment the parser accepts is one the
|
|
4647
|
+
* serializer reproduces.
|
|
4457
4648
|
*/
|
|
4458
|
-
|
|
4459
|
-
|
|
4649
|
+
const UNCARRIABLE_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uFFFE\uFFFF]/u;
|
|
4650
|
+
/**
|
|
4651
|
+
* Whether a code point is a character XML 1.0 allows in a document.
|
|
4652
|
+
* The excluded ranges are the control characters other than tab, line
|
|
4653
|
+
* feed, and carriage return, the surrogate halves, and the two
|
|
4654
|
+
* noncharacters at the end of the basic plane.
|
|
4655
|
+
*/
|
|
4656
|
+
function isXmlChar(codePoint) {
|
|
4657
|
+
return codePoint === CODE_TAB || codePoint === CODE_LINE_FEED || codePoint === CODE_CARRIAGE_RETURN || codePoint >= 32 && codePoint <= 55295 || codePoint >= 57344 && codePoint <= 65533 || codePoint >= 65536 && codePoint <= 1114111;
|
|
4658
|
+
}
|
|
4659
|
+
/**
|
|
4660
|
+
* Parses the text of a document in the dialect into its node tree,
|
|
4661
|
+
* reporting failures through the given error factory.
|
|
4662
|
+
*/
|
|
4663
|
+
function parseXmlDocument(text, makeError) {
|
|
4664
|
+
return new Parser(text, makeError).parseDocument();
|
|
4460
4665
|
}
|
|
4461
4666
|
/**
|
|
4462
4667
|
* Single-pass recursive-descent parser over the source text. One instance
|
|
4463
4668
|
* parses one document and is discarded.
|
|
4464
4669
|
*/
|
|
4465
4670
|
var Parser = class {
|
|
4466
|
-
/** The full source text of the
|
|
4671
|
+
/** The full source text of the file. */
|
|
4467
4672
|
input;
|
|
4673
|
+
/** Constructs the format's parse error for a failure. */
|
|
4674
|
+
makeError;
|
|
4468
4675
|
/** Cursor into {@link input}, in UTF-16 code units. */
|
|
4469
4676
|
pos = 0;
|
|
4470
|
-
constructor(input) {
|
|
4677
|
+
constructor(input, makeError) {
|
|
4471
4678
|
this.input = input;
|
|
4679
|
+
this.makeError = makeError;
|
|
4472
4680
|
if (input.charCodeAt(0) === CODE_BOM) this.pos = 1;
|
|
4473
4681
|
}
|
|
4474
4682
|
/**
|
|
@@ -4502,10 +4710,14 @@ var Parser = class {
|
|
|
4502
4710
|
/**
|
|
4503
4711
|
* Skips the `<?xml ... ?>` declaration when present. Its attributes are
|
|
4504
4712
|
* not retained, since the writer always emits the canonical UTF-8
|
|
4505
|
-
* declaration.
|
|
4713
|
+
* declaration. Other processing instructions fail, because the dialect
|
|
4714
|
+
* has no place to keep them and dropping one silently would make the
|
|
4715
|
+
* round-trip lossy.
|
|
4506
4716
|
*/
|
|
4507
4717
|
skipDeclaration() {
|
|
4508
4718
|
if (this.input.charCodeAt(this.pos) !== CODE_LESS_THAN || this.input.charCodeAt(this.pos + 1) !== CODE_QUESTION) return;
|
|
4719
|
+
const after = this.input.charCodeAt(this.pos + 5);
|
|
4720
|
+
if (this.input.slice(this.pos + 2, this.pos + 5) !== "xml" || !isWhitespace(after) && after !== CODE_QUESTION) this.fail("Unsupported processing instruction");
|
|
4509
4721
|
const end = this.input.indexOf("?>", this.pos + 2);
|
|
4510
4722
|
if (end === -1) this.fail("Unterminated XML declaration");
|
|
4511
4723
|
this.pos = end + 2;
|
|
@@ -4577,12 +4789,17 @@ var Parser = class {
|
|
|
4577
4789
|
}
|
|
4578
4790
|
/**
|
|
4579
4791
|
* Parses one comment with the cursor on its `<!--` opener. The text
|
|
4580
|
-
* between the markers is kept verbatim.
|
|
4792
|
+
* between the markers is kept verbatim. A `--` inside the text or a
|
|
4793
|
+
* `-` against the closing marker fails the way XML requires, which
|
|
4794
|
+
* also keeps every accepted comment rebuildable, since such text would
|
|
4795
|
+
* reparse at a different terminator.
|
|
4581
4796
|
*/
|
|
4582
4797
|
parseComment() {
|
|
4583
4798
|
const end = this.input.indexOf("-->", this.pos + 4);
|
|
4584
4799
|
if (end === -1) this.fail("Unterminated comment");
|
|
4585
4800
|
const comment = this.input.slice(this.pos + 4, end);
|
|
4801
|
+
if (comment.includes("--") || comment.endsWith("-")) this.fail("Comments cannot contain -- or end with -");
|
|
4802
|
+
if (UNCARRIABLE_PATTERN.test(comment)) this.fail("Comments cannot contain characters XML cannot carry");
|
|
4586
4803
|
this.pos = end + 3;
|
|
4587
4804
|
return { comment };
|
|
4588
4805
|
}
|
|
@@ -4622,14 +4839,37 @@ var Parser = class {
|
|
|
4622
4839
|
value += this.input.slice(runStart, this.pos);
|
|
4623
4840
|
value += this.parseReference();
|
|
4624
4841
|
runStart = this.pos;
|
|
4625
|
-
|
|
4842
|
+
continue;
|
|
4843
|
+
}
|
|
4844
|
+
if (code < 32 && !isWhitespace(code)) this.fail("Attribute values cannot contain a raw control character");
|
|
4845
|
+
if (code >= 55296) {
|
|
4846
|
+
this.validateUpperCharacter(code);
|
|
4847
|
+
this.pos += code <= 56319 ? 2 : 1;
|
|
4848
|
+
continue;
|
|
4849
|
+
}
|
|
4850
|
+
this.pos++;
|
|
4626
4851
|
}
|
|
4627
4852
|
}
|
|
4628
4853
|
/**
|
|
4854
|
+
* Validates a code unit in the surrogate or upper basic-plane range at
|
|
4855
|
+
* the cursor. A high surrogate must pair with a low one, a bare low
|
|
4856
|
+
* surrogate is not a character, and the two noncharacters at the end
|
|
4857
|
+
* of the plane have no XML representation.
|
|
4858
|
+
*/
|
|
4859
|
+
validateUpperCharacter(code) {
|
|
4860
|
+
if (code <= 56319) {
|
|
4861
|
+
const next = this.input.charCodeAt(this.pos + 1);
|
|
4862
|
+
if (Number.isNaN(next) || next < 56320 || next > 57343) this.fail("Attribute values cannot contain an unpaired surrogate");
|
|
4863
|
+
return;
|
|
4864
|
+
}
|
|
4865
|
+
if (code <= 57343) this.fail("Attribute values cannot contain an unpaired surrogate");
|
|
4866
|
+
if (code === 65534 || code === 65535) this.fail("Attribute values cannot contain a noncharacter");
|
|
4867
|
+
}
|
|
4868
|
+
/**
|
|
4629
4869
|
* Resolves one `&...;` reference with the cursor on the ampersand. The
|
|
4630
4870
|
* accepted forms are the five XML named entities and decimal or hex
|
|
4631
4871
|
* character references, which covers everything observed in
|
|
4632
|
-
* Xcode-written
|
|
4872
|
+
* Xcode-written files (`"`, `&`, `'`, `<`, `>`,
|
|
4633
4873
|
* and ` `-style whitespace).
|
|
4634
4874
|
*/
|
|
4635
4875
|
parseReference() {
|
|
@@ -4641,9 +4881,10 @@ var Parser = class {
|
|
|
4641
4881
|
if (body.charCodeAt(0) === CODE_HASH) {
|
|
4642
4882
|
const isHex = body.charCodeAt(1) === 120 || body.charCodeAt(1) === 88;
|
|
4643
4883
|
const digits = body.slice(isHex ? 2 : 1);
|
|
4884
|
+
if (!(isHex ? /^[0-9A-Fa-f]+$/u : /^[0-9]+$/u).test(digits)) this.failAt(`Invalid character reference &${body};`, start);
|
|
4644
4885
|
const radix = isHex ? 16 : 10;
|
|
4645
4886
|
const codePoint = Number.parseInt(digits, radix);
|
|
4646
|
-
if (
|
|
4887
|
+
if (!isXmlChar(codePoint)) this.failAt(`Character reference &${body}; is not an XML character`, start);
|
|
4647
4888
|
return String.fromCodePoint(codePoint);
|
|
4648
4889
|
}
|
|
4649
4890
|
const named = Object.hasOwn(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : void 0;
|
|
@@ -4670,10 +4911,28 @@ var Parser = class {
|
|
|
4670
4911
|
* uses to point at the start of a bad reference rather than its end.
|
|
4671
4912
|
*/
|
|
4672
4913
|
failAt(message, offset) {
|
|
4673
|
-
throw
|
|
4914
|
+
throw this.makeError(message, this.input, offset);
|
|
4674
4915
|
}
|
|
4675
4916
|
};
|
|
4676
4917
|
//#endregion
|
|
4918
|
+
//#region src/scheme/parse.ts
|
|
4919
|
+
/**
|
|
4920
|
+
* Parser entry point for the `.xcscheme` dialect. The grammar lives in
|
|
4921
|
+
* the shared XML core, and this wrapper binds it to the scheme error
|
|
4922
|
+
* type.
|
|
4923
|
+
*
|
|
4924
|
+
* @module
|
|
4925
|
+
*/
|
|
4926
|
+
/**
|
|
4927
|
+
* Parses the text of a `.xcscheme` file into its node tree.
|
|
4928
|
+
*
|
|
4929
|
+
* @throws XcschemeParseError when the text is not a well-formed scheme
|
|
4930
|
+
* document, with the line and column of the failure.
|
|
4931
|
+
*/
|
|
4932
|
+
function parseXcscheme(text) {
|
|
4933
|
+
return parseXmlDocument(text, (message, source, offset) => new XcschemeParseError(message, source, offset));
|
|
4934
|
+
}
|
|
4935
|
+
//#endregion
|
|
4677
4936
|
//#region src/scheme/model.ts
|
|
4678
4937
|
/**
|
|
4679
4938
|
* The scheme object model and its helpers.
|
|
@@ -4694,13 +4953,7 @@ var Parser = class {
|
|
|
4694
4953
|
* element.
|
|
4695
4954
|
*/
|
|
4696
4955
|
function xcschemeElements(root, name) {
|
|
4697
|
-
|
|
4698
|
-
const visit = (element) => {
|
|
4699
|
-
if (name == null || element.name === name) found.push(element);
|
|
4700
|
-
for (const child of element.children) if (isXcschemeElement(child)) visit(child);
|
|
4701
|
-
};
|
|
4702
|
-
visit(root);
|
|
4703
|
-
return found;
|
|
4956
|
+
return xmlElements(root, name);
|
|
4704
4957
|
}
|
|
4705
4958
|
/**
|
|
4706
4959
|
* A `BuildableReference` element with property-style attribute access.
|
|
@@ -4970,6 +5223,273 @@ function createXcscheme(options) {
|
|
|
4970
5223
|
};
|
|
4971
5224
|
}
|
|
4972
5225
|
//#endregion
|
|
5226
|
+
//#region src/scheme/types.ts
|
|
5227
|
+
/**
|
|
5228
|
+
* The node model of a scheme document. Scheme files share Xcode's XML
|
|
5229
|
+
* dialect with workspace data files, so the shapes here are the shared
|
|
5230
|
+
* XML node types under their scheme names.
|
|
5231
|
+
*
|
|
5232
|
+
* A parsed `.xcscheme` is a tree of plain objects. Elements carry ordered
|
|
5233
|
+
* attributes and child nodes, and comments survive as their own nodes.
|
|
5234
|
+
* All state lives in this tree. There is no wrapper to keep in sync, so
|
|
5235
|
+
* callers mutate nodes directly and serialize whatever the tree currently
|
|
5236
|
+
* says.
|
|
5237
|
+
*
|
|
5238
|
+
* @module
|
|
5239
|
+
*/
|
|
5240
|
+
/**
|
|
5241
|
+
* Whether a node is an element rather than a comment.
|
|
5242
|
+
*/
|
|
5243
|
+
function isXcschemeElement(node) {
|
|
5244
|
+
return isXmlElement(node);
|
|
5245
|
+
}
|
|
5246
|
+
//#endregion
|
|
5247
|
+
//#region src/workspace/build.ts
|
|
5248
|
+
/**
|
|
5249
|
+
* Serializer entry point for the `contents.xcworkspacedata` dialect. The
|
|
5250
|
+
* layout rules live in the shared XML core, and this wrapper binds them
|
|
5251
|
+
* to the workspace error type.
|
|
5252
|
+
*
|
|
5253
|
+
* @module
|
|
5254
|
+
*/
|
|
5255
|
+
/**
|
|
5256
|
+
* Serializes a workspace document to the text of a
|
|
5257
|
+
* `contents.xcworkspacedata` file in Xcode's canonical layout.
|
|
5258
|
+
*
|
|
5259
|
+
* @throws XcworkspaceBuildError for element or attribute names that are
|
|
5260
|
+
* not XML names and for attribute values carrying unencodable control
|
|
5261
|
+
* characters, with the path of the offending node.
|
|
5262
|
+
*/
|
|
5263
|
+
function buildXcworkspace(document) {
|
|
5264
|
+
return buildXmlDocument(document, (message, path) => new XcworkspaceBuildError(message, path));
|
|
5265
|
+
}
|
|
5266
|
+
//#endregion
|
|
5267
|
+
//#region src/workspace/parse.ts
|
|
5268
|
+
/**
|
|
5269
|
+
* Parser entry point for the `contents.xcworkspacedata` dialect. The
|
|
5270
|
+
* grammar lives in the shared XML core, and this wrapper binds it to the
|
|
5271
|
+
* workspace error type.
|
|
5272
|
+
*
|
|
5273
|
+
* @module
|
|
5274
|
+
*/
|
|
5275
|
+
/**
|
|
5276
|
+
* Parses the text of a `contents.xcworkspacedata` file into its node
|
|
5277
|
+
* tree.
|
|
5278
|
+
*
|
|
5279
|
+
* @throws XcworkspaceParseError when the text is not a well-formed
|
|
5280
|
+
* workspace document, with the line and column of the failure.
|
|
5281
|
+
*/
|
|
5282
|
+
function parseXcworkspace(text) {
|
|
5283
|
+
return parseXmlDocument(text, (message, source, offset) => new XcworkspaceParseError(message, source, offset));
|
|
5284
|
+
}
|
|
5285
|
+
//#endregion
|
|
5286
|
+
//#region src/workspace/model.ts
|
|
5287
|
+
/**
|
|
5288
|
+
* The workspace object model.
|
|
5289
|
+
*
|
|
5290
|
+
* A `.xcworkspace` directory carries a `contents.xcworkspacedata` file
|
|
5291
|
+
* listing the projects and folders the workspace shows, in the same XML
|
|
5292
|
+
* dialect scheme files use. {@link Xcworkspace} wraps the parsed
|
|
5293
|
+
* document with typed access to the file references, so tooling resolves
|
|
5294
|
+
* which projects a workspace opens instead of globbing the directory
|
|
5295
|
+
* tree. The node tree stays the single source of truth. Views hold only
|
|
5296
|
+
* a reference into it, so model calls and direct tree edits compose
|
|
5297
|
+
* freely.
|
|
5298
|
+
*
|
|
5299
|
+
* @module
|
|
5300
|
+
*/
|
|
5301
|
+
/**
|
|
5302
|
+
* Splits a location attribute into its kind and path. Xcode writes the
|
|
5303
|
+
* kinds `group` (relative to the enclosing group), `container` (relative
|
|
5304
|
+
* to the directory containing the `.xcworkspace`), `absolute`, `self`
|
|
5305
|
+
* (the containing `.xcodeproj` itself), and `developer` (inside the
|
|
5306
|
+
* developer directory).
|
|
5307
|
+
*/
|
|
5308
|
+
function parseWorkspaceLocation(location) {
|
|
5309
|
+
const colon = location.indexOf(":");
|
|
5310
|
+
if (colon === -1) return {
|
|
5311
|
+
kind: "group",
|
|
5312
|
+
path: location
|
|
5313
|
+
};
|
|
5314
|
+
return {
|
|
5315
|
+
kind: location.slice(0, colon),
|
|
5316
|
+
path: location.slice(colon + 1)
|
|
5317
|
+
};
|
|
5318
|
+
}
|
|
5319
|
+
/**
|
|
5320
|
+
* A `FileRef` element with property-style attribute access. File
|
|
5321
|
+
* references are the elements workspace edits touch, since each one
|
|
5322
|
+
* names a project or folder the workspace shows. The view reads and
|
|
5323
|
+
* writes the element's attributes directly, so it never goes stale and
|
|
5324
|
+
* needs no separate save step.
|
|
5325
|
+
*/
|
|
5326
|
+
var WorkspaceFileRef = class {
|
|
5327
|
+
/** The underlying element inside the document tree. */
|
|
5328
|
+
element;
|
|
5329
|
+
constructor(element) {
|
|
5330
|
+
this.element = element;
|
|
5331
|
+
}
|
|
5332
|
+
/**
|
|
5333
|
+
* The reference's location attribute, when present, for example
|
|
5334
|
+
* `group:Pods/Pods.xcodeproj`.
|
|
5335
|
+
*/
|
|
5336
|
+
get location() {
|
|
5337
|
+
return this.element.attributes["location"];
|
|
5338
|
+
}
|
|
5339
|
+
set location(value) {
|
|
5340
|
+
this.element.attributes["location"] = value;
|
|
5341
|
+
}
|
|
5342
|
+
};
|
|
5343
|
+
/**
|
|
5344
|
+
* A workspace document with typed access to the elements editing flows
|
|
5345
|
+
* touch.
|
|
5346
|
+
*
|
|
5347
|
+
* The model is a thin layer over the node tree. All state lives in the
|
|
5348
|
+
* document itself, and {@link build} serializes whatever the tree
|
|
5349
|
+
* currently says, so typed edits and direct tree edits compose freely.
|
|
5350
|
+
*
|
|
5351
|
+
* ```ts
|
|
5352
|
+
* const workspace = Xcworkspace.parse(xcworkspacedataText);
|
|
5353
|
+
* workspace.projectFilePaths(); // ["DemoApp.xcodeproj", "Pods/Pods.xcodeproj"]
|
|
5354
|
+
* ```
|
|
5355
|
+
*/
|
|
5356
|
+
var Xcworkspace = class Xcworkspace {
|
|
5357
|
+
/** The underlying parsed document. */
|
|
5358
|
+
document;
|
|
5359
|
+
constructor(document) {
|
|
5360
|
+
this.document = document;
|
|
5361
|
+
}
|
|
5362
|
+
/**
|
|
5363
|
+
* Parses the text of a `contents.xcworkspacedata` file into a
|
|
5364
|
+
* workspace model.
|
|
5365
|
+
*
|
|
5366
|
+
* @throws XcworkspaceParseError when the text is not a well-formed
|
|
5367
|
+
* workspace document, with the line and column of the failure.
|
|
5368
|
+
*/
|
|
5369
|
+
static parse(text) {
|
|
5370
|
+
return new Xcworkspace(parseXcworkspace(text));
|
|
5371
|
+
}
|
|
5372
|
+
/**
|
|
5373
|
+
* Creates the workspace document Xcode writes, listing the given
|
|
5374
|
+
* locations as file references.
|
|
5375
|
+
*/
|
|
5376
|
+
static create(options) {
|
|
5377
|
+
const children = (options?.locations ?? []).map((location) => ({
|
|
5378
|
+
name: "FileRef",
|
|
5379
|
+
attributes: { location },
|
|
5380
|
+
children: []
|
|
5381
|
+
}));
|
|
5382
|
+
return new Xcworkspace({
|
|
5383
|
+
leading: [],
|
|
5384
|
+
root: {
|
|
5385
|
+
name: "Workspace",
|
|
5386
|
+
attributes: { version: "1.0" },
|
|
5387
|
+
children
|
|
5388
|
+
},
|
|
5389
|
+
trailing: []
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
/**
|
|
5393
|
+
* The document's `Workspace` element.
|
|
5394
|
+
*/
|
|
5395
|
+
get root() {
|
|
5396
|
+
return this.document.root;
|
|
5397
|
+
}
|
|
5398
|
+
/**
|
|
5399
|
+
* Serializes the workspace to the text of a `contents.xcworkspacedata`
|
|
5400
|
+
* file in Xcode's canonical layout.
|
|
5401
|
+
*/
|
|
5402
|
+
build() {
|
|
5403
|
+
return buildXcworkspace(this.document);
|
|
5404
|
+
}
|
|
5405
|
+
/**
|
|
5406
|
+
* Collects elements of the given name anywhere in the document, in
|
|
5407
|
+
* document order. Passing no name collects every element.
|
|
5408
|
+
*/
|
|
5409
|
+
elements(name) {
|
|
5410
|
+
return xmlElements(this.document.root, name);
|
|
5411
|
+
}
|
|
5412
|
+
/**
|
|
5413
|
+
* The views of every file reference in the document, in document
|
|
5414
|
+
* order, references nested inside groups included.
|
|
5415
|
+
*/
|
|
5416
|
+
fileRefs() {
|
|
5417
|
+
return this.elements("FileRef").map((element) => new WorkspaceFileRef(element));
|
|
5418
|
+
}
|
|
5419
|
+
/**
|
|
5420
|
+
* Appends a file reference to the workspace's top level and returns
|
|
5421
|
+
* its view.
|
|
5422
|
+
*/
|
|
5423
|
+
addFileRef(location) {
|
|
5424
|
+
const element = {
|
|
5425
|
+
name: "FileRef",
|
|
5426
|
+
attributes: { location },
|
|
5427
|
+
children: []
|
|
5428
|
+
};
|
|
5429
|
+
this.root.children.push(element);
|
|
5430
|
+
return new WorkspaceFileRef(element);
|
|
5431
|
+
}
|
|
5432
|
+
/**
|
|
5433
|
+
* Removes a file reference from whichever element holds it. Returns
|
|
5434
|
+
* whether the reference was found and removed.
|
|
5435
|
+
*/
|
|
5436
|
+
removeFileRef(reference) {
|
|
5437
|
+
for (const parent of this.elements()) {
|
|
5438
|
+
const index = parent.children.indexOf(reference.element);
|
|
5439
|
+
if (index !== -1) {
|
|
5440
|
+
parent.children.splice(index, 1);
|
|
5441
|
+
return true;
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
return false;
|
|
5445
|
+
}
|
|
5446
|
+
/**
|
|
5447
|
+
* The paths of every `.xcodeproj` the workspace references, in
|
|
5448
|
+
* document order, resolved relative to the directory containing the
|
|
5449
|
+
* `.xcworkspace`. Group locations compose through their enclosing
|
|
5450
|
+
* groups, container locations anchor at the workspace's directory, and
|
|
5451
|
+
* absolute locations pass through unchanged.
|
|
5452
|
+
*
|
|
5453
|
+
* The resolution is textual. The library never touches the
|
|
5454
|
+
* filesystem, so locations that only the running Xcode can resolve
|
|
5455
|
+
* (`self` and `developer`) are not listed.
|
|
5456
|
+
*/
|
|
5457
|
+
projectFilePaths() {
|
|
5458
|
+
const paths = [];
|
|
5459
|
+
const visit = (element, base) => {
|
|
5460
|
+
for (const child of element.children) {
|
|
5461
|
+
if (!isXmlElement(child)) continue;
|
|
5462
|
+
const resolved = resolveLocationPath(parseWorkspaceLocation(child.attributes["location"] ?? ""), base);
|
|
5463
|
+
if (child.name === "Group") visit(child, resolved ?? base);
|
|
5464
|
+
else if (child.name === "FileRef" && resolved != null && resolved.endsWith(".xcodeproj")) paths.push(resolved);
|
|
5465
|
+
}
|
|
5466
|
+
};
|
|
5467
|
+
visit(this.document.root, "");
|
|
5468
|
+
return paths;
|
|
5469
|
+
}
|
|
5470
|
+
};
|
|
5471
|
+
/**
|
|
5472
|
+
* Resolves a location against the enclosing group's base path, or
|
|
5473
|
+
* `undefined` for the kinds only a running Xcode can resolve. The empty
|
|
5474
|
+
* string names the workspace's own directory.
|
|
5475
|
+
*/
|
|
5476
|
+
function resolveLocationPath(location, base) {
|
|
5477
|
+
switch (location.kind) {
|
|
5478
|
+
case "group": return joinPaths(base, location.path);
|
|
5479
|
+
case "container": return location.path;
|
|
5480
|
+
case "absolute": return location.path;
|
|
5481
|
+
default: return;
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
/**
|
|
5485
|
+
* Joins two textual path segments, keeping the result free of empty
|
|
5486
|
+
* segments when either side is empty.
|
|
5487
|
+
*/
|
|
5488
|
+
function joinPaths(base, path) {
|
|
5489
|
+
if (base === "" || path === "") return base === "" ? path : base;
|
|
5490
|
+
return `${base}/${path}`;
|
|
5491
|
+
}
|
|
5492
|
+
//#endregion
|
|
4973
5493
|
//#region src/xcconfig/build.ts
|
|
4974
5494
|
/**
|
|
4975
5495
|
* Serializes a document back to `.xcconfig` text.
|
|
@@ -5294,4 +5814,4 @@ var Xcconfig = class Xcconfig {
|
|
|
5294
5814
|
}
|
|
5295
5815
|
};
|
|
5296
5816
|
//#endregion
|
|
5297
|
-
export { AggregateTarget, AppleScriptBuildPhase, BuildConfiguration, BuildFile, BuildFileExceptionSet, BuildPhase, BuildPhaseMembershipExceptionSet, BuildRule, BuildStyle, BuildableReference, ConfigurationList, ContainerItemProxy, CopyFilesBuildPhase, CopyFilesDestination, ExceptionSet, FileReference, FrameworksBuildPhase, Group, HeadersBuildPhase, Isa, LegacyTarget, LocalSwiftPackageReference, NativeTarget, PbxprojBuildError, PbxprojParseError, ProductType, ReferenceProxy, RemoteSwiftPackageReference, ResourcesBuildPhase, RezBuildPhase, RootProject, ShellScriptBuildPhase, SourcesBuildPhase, SwiftPackageProductDependency, SwiftPackageReference, SyncRootGroup, Target, TargetDependency, VariantGroup, VersionGroup, Xcconfig, XcconfigParseError, XcodeModelError, XcodeObject, XcodeProject, Xcscheme, XcschemeBuildError, XcschemeParseError, buildPbxproj, buildXcconfig, buildXcscheme, createXcscheme, expandBuildSettingReferences, generateObjectId, isXcschemeElement, parseApplePlatform, parsePbxproj, parseXcconfig, parseXcscheme, xcschemeElements };
|
|
5817
|
+
export { AggregateTarget, AppleScriptBuildPhase, BuildConfiguration, BuildFile, BuildFileExceptionSet, BuildPhase, BuildPhaseMembershipExceptionSet, BuildRule, BuildStyle, BuildableReference, ConfigurationList, ContainerItemProxy, CopyFilesBuildPhase, CopyFilesDestination, ExceptionSet, FileReference, FrameworksBuildPhase, Group, HeadersBuildPhase, Isa, LegacyTarget, LocalSwiftPackageReference, NativeTarget, PbxprojBuildError, PbxprojParseError, ProductType, ReferenceProxy, RemoteSwiftPackageReference, ResourcesBuildPhase, RezBuildPhase, RootProject, ShellScriptBuildPhase, SourcesBuildPhase, SwiftPackageProductDependency, SwiftPackageReference, SyncRootGroup, Target, TargetDependency, VariantGroup, VersionGroup, WorkspaceFileRef, Xcconfig, XcconfigParseError, XcodeModelError, XcodeObject, XcodeProject, Xcscheme, XcschemeBuildError, XcschemeParseError, Xcworkspace, XcworkspaceBuildError, XcworkspaceParseError, buildPbxproj, buildXcconfig, buildXcscheme, buildXcworkspace, createXcscheme, expandBuildSettingReferences, generateObjectId, isXcschemeElement, isXmlElement, parseApplePlatform, parsePbxproj, parseWorkspaceLocation, parseXcconfig, parseXcscheme, parseXcworkspace, xcschemeElements, xmlElements };
|