rork-xcode 0.10.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/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++) if (source.charCodeAt(i) === LINE_FEED) {
228
- line++;
229
- lineStart = i + 1;
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
  }
@@ -815,6 +865,153 @@ function buildPbxproj(root) {
815
865
  return new Writer(root).toString();
816
866
  }
817
867
  //#endregion
868
+ //#region src/expansion.ts
869
+ /**
870
+ * Expands every `$(NAME)` and `${NAME}` reference in a value through the
871
+ * lookup. Substituted text is expanded again, so references are followed
872
+ * through chains of values, and reference names may themselves contain
873
+ * references (`$(SETTING_$(VARIANT))`), which expand innermost first.
874
+ *
875
+ * A lookup answer of `undefined` leaves the reference in the output
876
+ * verbatim, so partial resolution loses no information. A reference
877
+ * whose expansion would recurse into itself also stays verbatim, which
878
+ * keeps cyclic definitions finite. Xcode's `:` operators are honored for
879
+ * the forms below, and a reference carrying any other operator stays
880
+ * verbatim rather than expanding to a wrongly transformed value.
881
+ *
882
+ * The supported operators are `lower` and `upper` for case mapping,
883
+ * `rfc1034identifier` and `c99extidentifier` for the identifier
884
+ * mappings bundle identifiers and product module names use, and
885
+ * `default=value` for substituting a fallback when the setting resolves
886
+ * empty or is not known to the lookup.
887
+ */
888
+ function expandBuildSettingReferences(value, lookup, options) {
889
+ return expand(value, lookup, options?.expandLookupValues !== false, /* @__PURE__ */ new Set());
890
+ }
891
+ /** The UTF-16 code unit of `$`, which starts every reference. */
892
+ const CODE_DOLLAR = 36;
893
+ /**
894
+ * One expansion pass over a value. Active names carry the references
895
+ * currently being expanded up the call stack, so a cycle is detected the
896
+ * moment a name would expand inside its own expansion.
897
+ */
898
+ function expand(value, lookup, expandLookupValues, active) {
899
+ if (!value.includes("$")) return value;
900
+ let out = "";
901
+ let index = 0;
902
+ while (index < value.length) {
903
+ const open = value.charCodeAt(index) === CODE_DOLLAR ? value[index + 1] : void 0;
904
+ if (open !== "(" && open !== "{") {
905
+ out += value[index];
906
+ index++;
907
+ continue;
908
+ }
909
+ const end = findClosingDelimiter(value, index + 2, open === "(" ? ")" : "}");
910
+ if (end === -1) {
911
+ out += value.slice(index);
912
+ break;
913
+ }
914
+ const reference = value.slice(index, end + 1);
915
+ const inner = expand(value.slice(index + 2, end), lookup, expandLookupValues, active);
916
+ out += expandReference(reference, inner, lookup, expandLookupValues, active);
917
+ index = end + 1;
918
+ }
919
+ return out;
920
+ }
921
+ /**
922
+ * Finds the index of the closing delimiter matching an opener at
923
+ * `start - 1`, honoring nested delimiters of the same kind, or -1 when
924
+ * the reference is unterminated.
925
+ */
926
+ function findClosingDelimiter(value, start, close) {
927
+ const open = close === ")" ? "(" : "{";
928
+ let depth = 0;
929
+ for (let i = start; i < value.length; i++) {
930
+ const ch = value[i];
931
+ if (ch === open) depth++;
932
+ else if (ch === close) {
933
+ if (depth === 0) return i;
934
+ depth--;
935
+ }
936
+ }
937
+ return -1;
938
+ }
939
+ /**
940
+ * Expands one reference whose name and operators have already had their
941
+ * own references resolved. Returns the original reference text whenever
942
+ * the expansion cannot honestly produce a value.
943
+ */
944
+ function expandReference(reference, inner, lookup, expandLookupValues, active) {
945
+ const colon = inner.indexOf(":");
946
+ const name = colon === -1 ? inner : inner.slice(0, colon);
947
+ if (active.has(name)) return reference;
948
+ const raw = lookup(name);
949
+ const nested = /* @__PURE__ */ new Set([...active, name]);
950
+ let value = raw;
951
+ if (raw != null && expandLookupValues) value = expand(raw, lookup, expandLookupValues, nested);
952
+ if (colon !== -1) {
953
+ for (const operator of splitOperators(inner.slice(colon + 1))) if (operator.startsWith("default=")) {
954
+ if (value == null || value === "") value = operator.slice(8);
955
+ } else if (value != null) {
956
+ const applied = applyOperator(operator, value);
957
+ if (applied == null) return reference;
958
+ value = applied;
959
+ } else if (applyOperator(operator, "") == null) return reference;
960
+ }
961
+ return value ?? reference;
962
+ }
963
+ /**
964
+ * Splits an operator chain at colons, keeping a `default=` value intact
965
+ * even when it contains colons of its own, the way Xcode consumes the
966
+ * rest of the reference as the default.
967
+ */
968
+ function splitOperators(operators) {
969
+ const parts = [];
970
+ let rest = operators;
971
+ while (rest.length > 0) {
972
+ if (rest.startsWith("default=")) {
973
+ parts.push(rest);
974
+ break;
975
+ }
976
+ const colon = rest.indexOf(":");
977
+ if (colon === -1) {
978
+ parts.push(rest);
979
+ break;
980
+ }
981
+ parts.push(rest.slice(0, colon));
982
+ rest = rest.slice(colon + 1);
983
+ }
984
+ return parts;
985
+ }
986
+ /**
987
+ * The transforming operators the expander supports, keyed by the name
988
+ * they are written with. `default=` is not listed because it carries an
989
+ * argument and substitutes rather than transforms, so the reference
990
+ * expansion handles it structurally.
991
+ */
992
+ const OPERATOR_TRANSFORMS = {
993
+ c99extidentifier: (value) => value.replaceAll(/[^A-Za-z0-9_]/gu, "_"),
994
+ lower: (value) => value.toLowerCase(),
995
+ rfc1034identifier: (value) => value.replaceAll(/[^A-Za-z0-9-]/gu, "-"),
996
+ upper: (value) => value.toUpperCase()
997
+ };
998
+ /**
999
+ * Whether a parsed operator names one of the supported transforms. The
1000
+ * check is an own-property test, so document text like `toString` cannot
1001
+ * reach the table's prototype.
1002
+ */
1003
+ function isBuildSettingOperator(operator) {
1004
+ return Object.hasOwn(OPERATOR_TRANSFORMS, operator);
1005
+ }
1006
+ /**
1007
+ * Applies one operator to a resolved value, or returns `undefined` for
1008
+ * operators outside {@link BuildSettingOperator}, which the caller turns
1009
+ * into a verbatim reference.
1010
+ */
1011
+ function applyOperator(operator, value) {
1012
+ return isBuildSettingOperator(operator) ? OPERATOR_TRANSFORMS[operator](value) : void 0;
1013
+ }
1014
+ //#endregion
818
1015
  //#region src/model/isa.ts
819
1016
  /**
820
1017
  * Names and well-known values of the pbxproj object vocabulary.
@@ -2080,11 +2277,11 @@ const IS_LITERAL_CHAR = (() => {
2080
2277
  for (const ch of "_$/:.-") table[ch.charCodeAt(0)] = 1;
2081
2278
  return table;
2082
2279
  })();
2083
- const CODE_TAB$1 = 9;
2084
- const CODE_LINE_FEED$1 = 10;
2085
- const CODE_CARRIAGE_RETURN$1 = 13;
2280
+ const CODE_TAB$2 = 9;
2281
+ const CODE_LINE_FEED$2 = 10;
2282
+ const CODE_CARRIAGE_RETURN$2 = 13;
2086
2283
  const CODE_SPACE$1 = 32;
2087
- const CODE_QUOTE$1 = 34;
2284
+ const CODE_QUOTE$2 = 34;
2088
2285
  const CODE_SINGLE_QUOTE = 39;
2089
2286
  const CODE_OPEN_PAREN = 40;
2090
2287
  const CODE_CLOSE_PAREN = 41;
@@ -2096,9 +2293,9 @@ const CODE_SLASH$1 = 47;
2096
2293
  const CODE_ZERO = 48;
2097
2294
  const CODE_NINE = 57;
2098
2295
  const CODE_SEMICOLON = 59;
2099
- const CODE_LESS_THAN$1 = 60;
2296
+ const CODE_LESS_THAN$2 = 60;
2100
2297
  const CODE_EQUALS$1 = 61;
2101
- const CODE_GREATER_THAN$1 = 62;
2298
+ const CODE_GREATER_THAN$2 = 62;
2102
2299
  const CODE_BACKSLASH = 92;
2103
2300
  const CODE_OPEN_BRACE = 123;
2104
2301
  const CODE_CLOSE_BRACE = 125;
@@ -2111,9 +2308,9 @@ const CODE_CLOSE_BRACE = 125;
2111
2308
  const IS_WHITESPACE = (() => {
2112
2309
  const table = /* @__PURE__ */ new Uint8Array(256);
2113
2310
  table[CODE_SPACE$1] = 1;
2114
- table[CODE_TAB$1] = 1;
2115
- table[CODE_CARRIAGE_RETURN$1] = 1;
2116
- table[CODE_LINE_FEED$1] = 1;
2311
+ table[CODE_TAB$2] = 1;
2312
+ table[CODE_CARRIAGE_RETURN$2] = 1;
2313
+ table[CODE_LINE_FEED$2] = 1;
2117
2314
  return table;
2118
2315
  })();
2119
2316
  /**
@@ -2291,7 +2488,7 @@ var Parser$1 = class {
2291
2488
  const input = this.input;
2292
2489
  const length = input.length;
2293
2490
  const start = ++this.pos;
2294
- while (this.pos < length && input.charCodeAt(this.pos) !== CODE_GREATER_THAN$1) this.pos++;
2491
+ while (this.pos < length && input.charCodeAt(this.pos) !== CODE_GREATER_THAN$2) this.pos++;
2295
2492
  if (this.pos >= length) this.fail("Unterminated data run", start - 1);
2296
2493
  let hex = "";
2297
2494
  for (let i = start; i < this.pos; i++) {
@@ -2334,7 +2531,7 @@ var Parser$1 = class {
2334
2531
  return result;
2335
2532
  }
2336
2533
  let key;
2337
- if (code === CODE_QUOTE$1 || code === CODE_SINGLE_QUOTE) key = this.readQuotedString();
2534
+ if (code === CODE_QUOTE$2 || code === CODE_SINGLE_QUOTE) key = this.readQuotedString();
2338
2535
  else if (IS_LITERAL_CHAR[code] === 1) key = this.readLiteral();
2339
2536
  else this.fail(`Expected a key but found '${input[this.pos]}'`);
2340
2537
  this.expect(CODE_EQUALS$1, "=");
@@ -2390,9 +2587,9 @@ var Parser$1 = class {
2390
2587
  const code = this.input.charCodeAt(this.pos);
2391
2588
  if (code === CODE_OPEN_BRACE) return this.parseObject();
2392
2589
  if (code === CODE_OPEN_PAREN) return this.parseArray();
2393
- if (code === CODE_QUOTE$1 || code === CODE_SINGLE_QUOTE) return this.readQuotedString();
2590
+ if (code === CODE_QUOTE$2 || code === CODE_SINGLE_QUOTE) return this.readQuotedString();
2394
2591
  if (IS_LITERAL_CHAR[code] === 1) return interpretLiteral(this.readLiteral());
2395
- if (code === CODE_LESS_THAN$1) return this.readData();
2592
+ if (code === CODE_LESS_THAN$2) return this.readData();
2396
2593
  this.fail(`Expected a value but found '${this.input[this.pos]}'`);
2397
2594
  }
2398
2595
  };
@@ -3034,6 +3231,47 @@ var Target = class extends XcodeObject {
3034
3231
  return (projectConfiguration == null ? void 0 : this.project.xcconfigSettingsOf(projectConfiguration))?.[key];
3035
3232
  }
3036
3233
  /**
3234
+ * Reads a build setting like {@link getBuildSetting} and expands the
3235
+ * `$(NAME)` and `${NAME}` references in it. Referenced settings resolve
3236
+ * through the same layering, `$(inherited)` and a setting referencing
3237
+ * itself continue from the next layer down the way Xcode composes
3238
+ * values, and `$(TARGET_NAME)` falls back to the target's name, so the
3239
+ * common `PRODUCT_NAME = "$(TARGET_NAME)"` resolves without any caller
3240
+ * setup.
3241
+ *
3242
+ * References the model cannot resolve (build-system paths like
3243
+ * `$(BUILT_PRODUCTS_DIR)`, environment values) stay in the result
3244
+ * verbatim unless the caller's lookup answers them, so no information
3245
+ * is invented. See {@link expandBuildSettingReferences} for the
3246
+ * operator forms honored during expansion.
3247
+ */
3248
+ resolveBuildSetting(key, options) {
3249
+ const targetConfiguration = defaultConfigurationOf(this.project, this.getString("buildConfigurationList"));
3250
+ const projectConfiguration = defaultConfigurationOf(this.project, this.project.rootProject.getString("buildConfigurationList"));
3251
+ const layers = [
3252
+ asDictionary(targetConfiguration?.properties["buildSettings"]),
3253
+ targetConfiguration == null ? void 0 : this.project.xcconfigSettingsOf(targetConfiguration),
3254
+ asDictionary(projectConfiguration?.properties["buildSettings"]),
3255
+ projectConfiguration == null ? void 0 : this.project.xcconfigSettingsOf(projectConfiguration)
3256
+ ];
3257
+ const resolve = (name, fromLayer, active) => {
3258
+ const guard = `${fromLayer}:${name}`;
3259
+ if (active.has(guard)) return;
3260
+ const nested = /* @__PURE__ */ new Set([...active, guard]);
3261
+ for (let layer = fromLayer; layer < layers.length; layer++) {
3262
+ const settings = layers[layer];
3263
+ if (settings == null || !(name in settings)) continue;
3264
+ const value = asString(settings[name]);
3265
+ if (value == null) return;
3266
+ return expandBuildSettingReferences(value, (reference) => {
3267
+ if (reference === "inherited" || reference === name) return resolve(name, layer + 1, nested) ?? "";
3268
+ return resolve(reference, 0, nested) ?? options?.lookup?.(reference) ?? builtinSetting(this, reference);
3269
+ }, { expandLookupValues: false });
3270
+ }
3271
+ };
3272
+ return resolve(key, 0, /* @__PURE__ */ new Set());
3273
+ }
3274
+ /**
3037
3275
  * Writes a build setting on every configuration of the target, so Debug
3038
3276
  * and Release stay consistent.
3039
3277
  */
@@ -3147,6 +3385,15 @@ var Target = class extends XcodeObject {
3147
3385
  }
3148
3386
  };
3149
3387
  /**
3388
+ * The settings the build system defines from the target itself rather
3389
+ * than from the document. `TARGET_NAME` is the one programmatic reads
3390
+ * meet constantly, through the template's `PRODUCT_NAME` of
3391
+ * `$(TARGET_NAME)`.
3392
+ */
3393
+ function builtinSetting(target, name) {
3394
+ return name === "TARGET_NAME" ? target.name : void 0;
3395
+ }
3396
+ /**
3150
3397
  * A `PBXNativeTarget` is a product the project compiles and packages
3151
3398
  * itself, such as an application or an app extension.
3152
3399
  */
@@ -4087,57 +4334,159 @@ function stripReferences(properties, id) {
4087
4334
  }
4088
4335
  }
4089
4336
  //#endregion
4090
- //#region src/scheme/types.ts
4337
+ //#region src/xml/types.ts
4091
4338
  /**
4092
4339
  * Whether a node is an element rather than a comment.
4093
4340
  */
4094
- function isXcschemeElement(node) {
4341
+ function isXmlElement(node) {
4095
4342
  return "name" in node;
4096
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
+ }
4097
4358
  //#endregion
4098
- //#region src/scheme/build.ts
4359
+ //#region src/xml/build.ts
4099
4360
  /**
4100
- * Serializer for the `.xcscheme` XML dialect.
4361
+ * Serializer for Xcode's XML dialect, shared by scheme and workspace
4362
+ * files.
4101
4363
  *
4102
4364
  * The output reproduces Xcode's own layout byte for byte. Xcode writes
4103
4365
  * the UTF-8 declaration, indents with three spaces, puts each attribute
4104
4366
  * on its own line with spaces around the equals sign, glues the closing
4105
4367
  * angle bracket to the last attribute, and closes every element with an
4106
- * explicit close tag. Parsing an Xcode-written scheme and building it
4368
+ * explicit close tag. Parsing an Xcode-written file and building it
4107
4369
  * back therefore yields the identical file, and any other input reaches
4108
4370
  * that canonical form in one build.
4109
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
+ *
4110
4375
  * @module
4111
4376
  */
4112
- /** The declaration line Xcode writes at the top of every scheme file. */
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. */
4113
4393
  const XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
4114
- /** One indentation step of Xcode's scheme writer. */
4394
+ /** One indentation step of Xcode's writer. */
4115
4395
  const INDENT = " ";
4116
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
+ /**
4117
4410
  * Matches valid XML names. A stray non-name, such as an empty string or
4118
4411
  * one carrying spaces or XML syntax, must fail rather than produce a
4119
4412
  * document no parser accepts.
4120
4413
  */
4121
4414
  const NAME_PATTERN = /^[A-Za-z_:][A-Za-z0-9_:.-]*$/u;
4122
4415
  /**
4123
- * Matches characters that cannot appear in an attribute value. XML 1.0
4124
- * has no representation for control characters other than tab, line
4125
- * feed, and carriage return.
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.
4126
4420
  */
4127
- const UNENCODABLE_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/u;
4421
+ const VALID_NAMES = /* @__PURE__ */ new Set();
4128
4422
  /**
4129
- * Serializes a scheme document to the text of a `.xcscheme` file.
4130
- *
4131
- * @throws XcschemeBuildError for element or attribute names that are not
4132
- * XML names and for attribute values carrying unencodable control
4133
- * characters, with the path of the offending node.
4423
+ * Whether a name is a valid XML name, memoized across documents.
4134
4424
  */
4135
- function buildXcscheme(document) {
4136
- let output = XML_DECLARATION;
4137
- for (const comment of document.leading) output += `<!--${comment.comment}-->\n`;
4138
- output += renderElement(document.root, 0, document.root.name);
4139
- for (const comment of document.trailing) output += `<!--${comment.comment}-->\n`;
4140
- return output;
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`;
4141
4490
  }
4142
4491
  /**
4143
4492
  * Renders one element with Xcode's layout. Attributes go each on their
@@ -4145,67 +4494,109 @@ function buildXcscheme(document) {
4145
4494
  * attribute, children indent one step, and the close tag is always
4146
4495
  * explicit.
4147
4496
  */
4148
- function renderElement(element, depth, path) {
4149
- if (!NAME_PATTERN.test(element.name)) throw new XcschemeBuildError(`Element name ${JSON.stringify(element.name)} is not a valid XML name`, path);
4150
- const indent = INDENT.repeat(depth);
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);
4151
4500
  const names = Object.keys(element.attributes);
4152
4501
  let output;
4153
4502
  if (names.length === 0) output = `${indent}<${element.name}>\n`;
4154
4503
  else {
4155
4504
  output = `${indent}<${element.name}\n`;
4156
- const attributeIndent = indent + INDENT;
4505
+ const attributeIndent = indentAt(depth + 1);
4157
4506
  for (let i = 0; i < names.length; i++) {
4158
4507
  const name = names[i];
4159
- if (!NAME_PATTERN.test(name)) throw new XcschemeBuildError(`Attribute name ${JSON.stringify(name)} is not a valid XML name`, path);
4160
- const value = escapeAttribute(element.attributes[name], path, 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);
4161
4510
  const terminator = i === names.length - 1 ? ">" : "";
4162
4511
  output += `${attributeIndent}${name} = "${value}"${terminator}\n`;
4163
4512
  }
4164
4513
  }
4165
- output += renderChildren(element.children, depth + 1, path);
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);
4166
4517
  output += `${indent}</${element.name}>\n`;
4167
4518
  return output;
4168
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;
4169
4528
  /**
4170
- * Renders an element's children in document order, numbering repeated
4171
- * element names in the error path so a failure points at one node.
4172
- */
4173
- function renderChildren(children, depth, path) {
4174
- let output = "";
4175
- const seen = /* @__PURE__ */ new Map();
4176
- for (const child of children) if (isXcschemeElement(child)) {
4177
- const index = seen.get(child.name) ?? 0;
4178
- seen.set(child.name, index + 1);
4179
- output += renderElement(child, depth, `${path}.${child.name}[${index}]`);
4180
- } else output += `${INDENT.repeat(depth)}<!--${child.comment}-->\n`;
4181
- return output;
4182
- }
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;
4183
4535
  /**
4184
4536
  * Escapes an attribute value the way Xcode's writer does. XML syntax
4185
4537
  * characters become the five named entities, and tab, line feed, and
4186
4538
  * carriage return become character references so they survive
4187
4539
  * attribute-value normalization on the next parse.
4188
- */
4189
- function escapeAttribute(value, path, attributeName) {
4190
- const unencodable = UNENCODABLE_PATTERN.exec(value);
4191
- if (unencodable != null) throw new XcschemeBuildError(`Attribute ${attributeName} contains the control character U+${unencodable[0].charCodeAt(0).toString(16).padStart(4, "0").toUpperCase()}, which XML cannot encode`, path);
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;
4192
4576
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;").replaceAll(" ", "&#9;").replaceAll("\n", "&#10;").replaceAll("\r", "&#13;");
4193
4577
  }
4194
4578
  //#endregion
4195
- //#region src/scheme/parse.ts
4579
+ //#region src/scheme/build.ts
4196
4580
  /**
4197
- * Parser for the `.xcscheme` XML dialect.
4198
- *
4199
- * Scheme files are XML with a narrow shape. Elements carry attributes and
4200
- * child elements, and there is no text content, no namespace, and no
4201
- * DOCTYPE. The parser accepts that shape from any writer, since attribute
4202
- * order, quoting style, and whitespace vary across tools. Character and
4203
- * entity references in attribute values resolve to their characters, and
4204
- * comments are preserved. Anything outside the shape fails loudly with a
4205
- * 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.
4206
4584
  *
4207
4585
  * @module
4208
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
4209
4600
  const CODE_TAB = 9;
4210
4601
  const CODE_LINE_FEED = 10;
4211
4602
  const CODE_CARRIAGE_RETURN = 13;
@@ -4230,7 +4621,7 @@ function isWhitespace(code) {
4230
4621
  return code === CODE_SPACE || code === CODE_TAB || code === CODE_LINE_FEED || code === CODE_CARRIAGE_RETURN;
4231
4622
  }
4232
4623
  /**
4233
- * Whether a code unit can start an XML name. The scheme vocabulary is
4624
+ * Whether a code unit can start an XML name. The dialect's vocabulary is
4234
4625
  * ASCII, so the accepted alphabet is letters, underscore, and colon.
4235
4626
  */
4236
4627
  function isNameStart(code) {
@@ -4251,25 +4642,41 @@ const NAMED_ENTITIES = {
4251
4642
  quot: "\""
4252
4643
  };
4253
4644
  /**
4254
- * Parses the text of a `.xcscheme` file into its node tree.
4255
- *
4256
- * @throws XcschemeParseError when the text is not a well-formed scheme
4257
- * 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.
4258
4648
  */
4259
- function parseXcscheme(text) {
4260
- return new Parser(text).parseDocument();
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();
4261
4665
  }
4262
4666
  /**
4263
4667
  * Single-pass recursive-descent parser over the source text. One instance
4264
4668
  * parses one document and is discarded.
4265
4669
  */
4266
4670
  var Parser = class {
4267
- /** The full source text of the scheme file. */
4671
+ /** The full source text of the file. */
4268
4672
  input;
4673
+ /** Constructs the format's parse error for a failure. */
4674
+ makeError;
4269
4675
  /** Cursor into {@link input}, in UTF-16 code units. */
4270
4676
  pos = 0;
4271
- constructor(input) {
4677
+ constructor(input, makeError) {
4272
4678
  this.input = input;
4679
+ this.makeError = makeError;
4273
4680
  if (input.charCodeAt(0) === CODE_BOM) this.pos = 1;
4274
4681
  }
4275
4682
  /**
@@ -4303,10 +4710,14 @@ var Parser = class {
4303
4710
  /**
4304
4711
  * Skips the `<?xml ... ?>` declaration when present. Its attributes are
4305
4712
  * not retained, since the writer always emits the canonical UTF-8
4306
- * 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.
4307
4716
  */
4308
4717
  skipDeclaration() {
4309
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");
4310
4721
  const end = this.input.indexOf("?>", this.pos + 2);
4311
4722
  if (end === -1) this.fail("Unterminated XML declaration");
4312
4723
  this.pos = end + 2;
@@ -4378,12 +4789,17 @@ var Parser = class {
4378
4789
  }
4379
4790
  /**
4380
4791
  * Parses one comment with the cursor on its `<!--` opener. The text
4381
- * 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.
4382
4796
  */
4383
4797
  parseComment() {
4384
4798
  const end = this.input.indexOf("-->", this.pos + 4);
4385
4799
  if (end === -1) this.fail("Unterminated comment");
4386
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");
4387
4803
  this.pos = end + 3;
4388
4804
  return { comment };
4389
4805
  }
@@ -4423,14 +4839,37 @@ var Parser = class {
4423
4839
  value += this.input.slice(runStart, this.pos);
4424
4840
  value += this.parseReference();
4425
4841
  runStart = this.pos;
4426
- } else this.pos++;
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++;
4851
+ }
4852
+ }
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;
4427
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");
4428
4867
  }
4429
4868
  /**
4430
4869
  * Resolves one `&...;` reference with the cursor on the ampersand. The
4431
4870
  * accepted forms are the five XML named entities and decimal or hex
4432
4871
  * character references, which covers everything observed in
4433
- * Xcode-written schemes (`&quot;`, `&amp;`, `&apos;`, `&lt;`, `&gt;`,
4872
+ * Xcode-written files (`&quot;`, `&amp;`, `&apos;`, `&lt;`, `&gt;`,
4434
4873
  * and `&#10;`-style whitespace).
4435
4874
  */
4436
4875
  parseReference() {
@@ -4442,9 +4881,10 @@ var Parser = class {
4442
4881
  if (body.charCodeAt(0) === CODE_HASH) {
4443
4882
  const isHex = body.charCodeAt(1) === 120 || body.charCodeAt(1) === 88;
4444
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);
4445
4885
  const radix = isHex ? 16 : 10;
4446
4886
  const codePoint = Number.parseInt(digits, radix);
4447
- if (digits.length === 0 || Number.isNaN(codePoint) || codePoint > 1114111) this.failAt(`Invalid character reference &${body};`, start);
4887
+ if (!isXmlChar(codePoint)) this.failAt(`Character reference &${body}; is not an XML character`, start);
4448
4888
  return String.fromCodePoint(codePoint);
4449
4889
  }
4450
4890
  const named = Object.hasOwn(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : void 0;
@@ -4471,10 +4911,28 @@ var Parser = class {
4471
4911
  * uses to point at the start of a bad reference rather than its end.
4472
4912
  */
4473
4913
  failAt(message, offset) {
4474
- throw new XcschemeParseError(message, this.input, offset);
4914
+ throw this.makeError(message, this.input, offset);
4475
4915
  }
4476
4916
  };
4477
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
4478
4936
  //#region src/scheme/model.ts
4479
4937
  /**
4480
4938
  * The scheme object model and its helpers.
@@ -4495,13 +4953,7 @@ var Parser = class {
4495
4953
  * element.
4496
4954
  */
4497
4955
  function xcschemeElements(root, name) {
4498
- const found = [];
4499
- const visit = (element) => {
4500
- if (name == null || element.name === name) found.push(element);
4501
- for (const child of element.children) if (isXcschemeElement(child)) visit(child);
4502
- };
4503
- visit(root);
4504
- return found;
4956
+ return xmlElements(root, name);
4505
4957
  }
4506
4958
  /**
4507
4959
  * A `BuildableReference` element with property-style attribute access.
@@ -4771,6 +5223,273 @@ function createXcscheme(options) {
4771
5223
  };
4772
5224
  }
4773
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
4774
5493
  //#region src/xcconfig/build.ts
4775
5494
  /**
4776
5495
  * Serializes a document back to `.xcconfig` text.
@@ -5095,4 +5814,4 @@ var Xcconfig = class Xcconfig {
5095
5814
  }
5096
5815
  };
5097
5816
  //#endregion
5098
- 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, 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 };