worldorbit 2.5.13 → 2.5.15

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.
Files changed (47) hide show
  1. package/README.md +37 -11
  2. package/dist/unpkg/worldorbit-core.min.js +12 -5
  3. package/dist/unpkg/worldorbit-markdown.min.js +32 -23
  4. package/dist/unpkg/worldorbit-viewer.min.js +55 -41
  5. package/dist/unpkg/worldorbit.js +1713 -231
  6. package/dist/unpkg/worldorbit.min.js +58 -44
  7. package/package.json +2 -2
  8. package/packages/core/README.md +5 -1
  9. package/packages/core/dist/atlas-edit.d.ts +2 -2
  10. package/packages/core/dist/atlas-edit.js +70 -7
  11. package/packages/core/dist/atlas-utils.d.ts +22 -0
  12. package/packages/core/dist/atlas-utils.js +189 -0
  13. package/packages/core/dist/atlas-validate.d.ts +2 -0
  14. package/packages/core/dist/atlas-validate.js +285 -0
  15. package/packages/core/dist/draft-parse.js +786 -153
  16. package/packages/core/dist/draft.d.ts +3 -0
  17. package/packages/core/dist/draft.js +47 -3
  18. package/packages/core/dist/format.js +165 -9
  19. package/packages/core/dist/load.js +58 -13
  20. package/packages/core/dist/normalize.js +7 -0
  21. package/packages/core/dist/scene.js +66 -13
  22. package/packages/core/dist/types.d.ts +97 -3
  23. package/packages/markdown/README.md +1 -1
  24. package/packages/viewer/README.md +2 -1
  25. package/packages/viewer/dist/atlas-state.js +7 -1
  26. package/packages/viewer/dist/atlas-viewer.js +35 -1
  27. package/packages/viewer/dist/render.js +16 -7
  28. package/packages/viewer/dist/theme.js +4 -0
  29. package/packages/viewer/dist/tooltip.js +35 -0
  30. package/packages/viewer/dist/types.d.ts +7 -0
  31. package/packages/viewer/dist/viewer.js +4 -0
  32. package/packages/editor/dist/editor.d.ts +0 -2
  33. package/packages/editor/dist/editor.js +0 -2998
  34. package/packages/editor/dist/index.d.ts +0 -2
  35. package/packages/editor/dist/index.js +0 -1
  36. package/packages/editor/dist/types.d.ts +0 -53
  37. package/packages/editor/dist/types.js +0 -1
  38. package/packages/markdown/dist/html.d.ts +0 -3
  39. package/packages/markdown/dist/html.js +0 -57
  40. package/packages/markdown/dist/index.d.ts +0 -4
  41. package/packages/markdown/dist/index.js +0 -3
  42. package/packages/markdown/dist/rehype.d.ts +0 -10
  43. package/packages/markdown/dist/rehype.js +0 -49
  44. package/packages/markdown/dist/remark.d.ts +0 -9
  45. package/packages/markdown/dist/remark.js +0 -28
  46. package/packages/markdown/dist/types.d.ts +0 -11
  47. package/packages/markdown/dist/types.js +0 -1
@@ -1,25 +1,102 @@
1
1
  import { WorldOrbitError } from "./errors.js";
2
- import { normalizeDocument } from "./normalize.js";
3
- import { getFieldSchema, isKnownFieldKey, WORLDORBIT_OBJECT_TYPES } from "./schema.js";
2
+ import { getFieldSchema, WORLDORBIT_OBJECT_TYPES } from "./schema.js";
4
3
  import { getIndent, tokenizeLineDetailed } from "./tokenize.js";
5
- import { validateDocument } from "./validate.js";
4
+ import { ensureAtlasFieldSupported, humanizeIdentifier, normalizeIdentifier, normalizeLegacyScalarValue, parseAtlasAtReference, parseAtlasBoolean, parseAtlasNumber, parseAtlasUnitValue, singleAtlasValue, tryParseAtlasUnitValue, } from "./atlas-utils.js";
5
+ import { collectAtlasDiagnostics } from "./atlas-validate.js";
6
+ const STRUCTURED_TYPED_BLOCKS = new Set([
7
+ "climate",
8
+ "habitability",
9
+ "settlement",
10
+ ]);
11
+ const DRAFT_OBJECT_FIELD_SPECS = new Map();
12
+ for (const key of [
13
+ "orbit",
14
+ "distance",
15
+ "semiMajor",
16
+ "eccentricity",
17
+ "period",
18
+ "angle",
19
+ "inclination",
20
+ "phase",
21
+ "at",
22
+ "surface",
23
+ "free",
24
+ "kind",
25
+ "class",
26
+ "culture",
27
+ "tags",
28
+ "color",
29
+ "image",
30
+ "hidden",
31
+ "radius",
32
+ "mass",
33
+ "density",
34
+ "gravity",
35
+ "temperature",
36
+ "albedo",
37
+ "atmosphere",
38
+ "inner",
39
+ "outer",
40
+ "on",
41
+ "source",
42
+ "cycle",
43
+ ]) {
44
+ const schema = getFieldSchema(key);
45
+ if (schema) {
46
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
47
+ key,
48
+ version: "2.0",
49
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
50
+ allowRepeat: false,
51
+ legacySchema: schema,
52
+ });
53
+ }
54
+ }
55
+ for (const spec of [
56
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
57
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
58
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
59
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
60
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
61
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
62
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
63
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
64
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
65
+ { key: "validate", inlineMode: "single", allowRepeat: true },
66
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
67
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true },
68
+ ]) {
69
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
70
+ key: spec.key,
71
+ version: "2.1",
72
+ inlineMode: spec.inlineMode,
73
+ allowRepeat: spec.allowRepeat,
74
+ });
75
+ }
76
+ const DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
6
77
  export function parseWorldOrbitAtlas(source) {
7
- return parseAtlasSource(source, "2.0");
78
+ return parseAtlasSource(source);
8
79
  }
9
80
  export function parseWorldOrbitDraft(source) {
10
81
  return parseAtlasSource(source, "2.0-draft");
11
82
  }
12
- function parseAtlasSource(source, outputVersion) {
13
- const lines = source.split(/\r?\n/);
83
+ function parseAtlasSource(source, forcedOutputVersion) {
84
+ const prepared = preprocessAtlasSource(source);
85
+ const lines = prepared.source.split(/\r?\n/);
86
+ const diagnostics = [];
14
87
  let sawSchemaHeader = false;
15
- let schemaVersion = "2.0";
88
+ let sourceSchemaVersion = "2.0";
16
89
  let system = null;
17
90
  let section = null;
18
91
  const objectNodes = [];
92
+ const groups = [];
93
+ const relations = [];
19
94
  let sawDefaults = false;
20
95
  let sawAtlas = false;
21
96
  const viewpointIds = new Set();
22
97
  const annotationIds = new Set();
98
+ const groupIds = new Set();
99
+ const relationIds = new Set();
23
100
  for (let index = 0; index < lines.length; index++) {
24
101
  const rawLine = lines[index];
25
102
  const lineNumber = index + 1;
@@ -35,15 +112,22 @@ function parseAtlasSource(source, outputVersion) {
35
112
  continue;
36
113
  }
37
114
  if (!sawSchemaHeader) {
38
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
115
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
39
116
  sawSchemaHeader = true;
117
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
118
+ diagnostics.push({
119
+ code: "parse.schema21.commentCompatibility",
120
+ severity: "warning",
121
+ source: "parse",
122
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
123
+ line: prepared.comments[0].line,
124
+ column: prepared.comments[0].column,
125
+ });
126
+ }
40
127
  continue;
41
128
  }
42
129
  if (indent === 0) {
43
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
44
- sawDefaults,
45
- sawAtlas,
46
- });
130
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
47
131
  if (section.kind === "system") {
48
132
  system = section.system;
49
133
  }
@@ -63,53 +147,64 @@ function parseAtlasSource(source, outputVersion) {
63
147
  if (!sawSchemaHeader) {
64
148
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
65
149
  }
66
- const ast = {
67
- type: "document",
68
- objects: objectNodes,
69
- };
70
- const normalizedObjects = normalizeDocument(ast).objects;
71
- validateDocument({
72
- format: "worldorbit",
73
- version: "1.0",
74
- system: null,
75
- objects: normalizedObjects,
76
- });
77
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0"
78
- ? [
79
- {
80
- code: "load.schema.deprecatedDraft",
81
- severity: "warning",
82
- source: "upgrade",
83
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".',
84
- },
85
- ]
86
- : [];
87
- return {
150
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
151
+ const outputVersion = forcedOutputVersion ??
152
+ (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
153
+ const baseDocument = {
88
154
  format: "worldorbit",
89
- version: outputVersion,
90
155
  sourceVersion: "1.0",
91
156
  system,
92
- objects: normalizedObjects,
157
+ groups,
158
+ relations,
159
+ objects,
93
160
  diagnostics,
94
161
  };
162
+ if (outputVersion === "2.0-draft") {
163
+ const document = {
164
+ ...baseDocument,
165
+ version: "2.0-draft",
166
+ schemaVersion: "2.0-draft",
167
+ };
168
+ document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
169
+ return document;
170
+ }
171
+ const document = {
172
+ ...baseDocument,
173
+ version: outputVersion,
174
+ schemaVersion: outputVersion,
175
+ };
176
+ if (sourceSchemaVersion === "2.0-draft") {
177
+ document.diagnostics.push({
178
+ code: "load.schema.deprecatedDraft",
179
+ severity: "warning",
180
+ source: "upgrade",
181
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".',
182
+ });
183
+ }
184
+ document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
185
+ return document;
95
186
  }
96
187
  function assertDraftSchemaHeader(tokens, line) {
97
188
  if (tokens.length !== 2 ||
98
189
  tokens[0].value.toLowerCase() !== "schema" ||
99
- (tokens[1].value.toLowerCase() !== "2.0-draft" &&
100
- tokens[1].value.toLowerCase() !== "2.0")) {
101
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
190
+ !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
191
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
102
192
  }
103
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
193
+ const version = tokens[1].value.toLowerCase();
194
+ return version === "2.1"
195
+ ? "2.1"
196
+ : version === "2.0-draft"
197
+ ? "2.0-draft"
198
+ : "2.0";
104
199
  }
105
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
200
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
106
201
  const keyword = tokens[0]?.value.toLowerCase();
107
202
  switch (keyword) {
108
203
  case "system":
109
204
  if (system) {
110
205
  throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
111
206
  }
112
- return startSystemSection(tokens, line);
207
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
113
208
  case "defaults":
114
209
  if (!system) {
115
210
  throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
@@ -145,13 +240,19 @@ function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, a
145
240
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
146
241
  }
147
242
  return startAnnotationSection(tokens, line, system, annotationIds);
243
+ case "group":
244
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
245
+ return startGroupSection(tokens, line, groups, groupIds);
246
+ case "relation":
247
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
248
+ return startRelationSection(tokens, line, relations, relationIds);
148
249
  case "object":
149
- return startObjectSection(tokens, line, objectNodes);
250
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
150
251
  default:
151
252
  throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
152
253
  }
153
254
  }
154
- function startSystemSection(tokens, line) {
255
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
155
256
  if (tokens.length !== 2) {
156
257
  throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
157
258
  }
@@ -159,6 +260,9 @@ function startSystemSection(tokens, line) {
159
260
  type: "system",
160
261
  id: tokens[1].value,
161
262
  title: null,
263
+ description: null,
264
+ epoch: null,
265
+ referencePlane: null,
162
266
  defaults: {
163
267
  view: "topdown",
164
268
  scale: null,
@@ -173,6 +277,8 @@ function startSystemSection(tokens, line) {
173
277
  return {
174
278
  kind: "system",
175
279
  system,
280
+ sourceSchemaVersion,
281
+ diagnostics,
176
282
  seenFields: new Set(),
177
283
  };
178
284
  }
@@ -238,24 +344,79 @@ function startAnnotationSection(tokens, line, system, annotationIds) {
238
344
  seenFields: new Set(),
239
345
  };
240
346
  }
241
- function startObjectSection(tokens, line, objectNodes) {
347
+ function startGroupSection(tokens, line, groups, groupIds) {
348
+ if (tokens.length !== 2) {
349
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
350
+ }
351
+ const id = normalizeIdentifier(tokens[1].value);
352
+ if (!id) {
353
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
354
+ }
355
+ if (groupIds.has(id)) {
356
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
357
+ }
358
+ const group = {
359
+ id,
360
+ label: humanizeIdentifier(id),
361
+ summary: "",
362
+ color: null,
363
+ tags: [],
364
+ hidden: false,
365
+ };
366
+ groups.push(group);
367
+ groupIds.add(id);
368
+ return {
369
+ kind: "group",
370
+ group,
371
+ seenFields: new Set(),
372
+ };
373
+ }
374
+ function startRelationSection(tokens, line, relations, relationIds) {
375
+ if (tokens.length !== 2) {
376
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
377
+ }
378
+ const id = normalizeIdentifier(tokens[1].value);
379
+ if (!id) {
380
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
381
+ }
382
+ if (relationIds.has(id)) {
383
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
384
+ }
385
+ const relation = {
386
+ id,
387
+ from: "",
388
+ to: "",
389
+ kind: "",
390
+ label: null,
391
+ summary: null,
392
+ tags: [],
393
+ color: null,
394
+ hidden: false,
395
+ };
396
+ relations.push(relation);
397
+ relationIds.add(id);
398
+ return {
399
+ kind: "relation",
400
+ relation,
401
+ seenFields: new Set(),
402
+ };
403
+ }
404
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
242
405
  if (tokens.length < 3) {
243
406
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
244
407
  }
245
408
  const objectTypeToken = tokens[1];
246
409
  const idToken = tokens[2];
247
410
  const objectType = objectTypeToken.value;
248
- if (!WORLDORBIT_OBJECT_TYPES.has(objectType) ||
249
- objectType === "system") {
411
+ if (!WORLDORBIT_OBJECT_TYPES.has(objectType) || objectType === "system") {
250
412
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
251
413
  }
252
414
  const objectNode = {
253
- type: "object",
254
- objectType,
255
- name: idToken.value,
256
- inlineFields: parseInlineFields(tokens.slice(3), line),
257
- blockFields: [],
415
+ objectType: objectType,
416
+ id: idToken.value,
417
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
258
418
  infoEntries: [],
419
+ typedBlockEntries: {},
259
420
  location: {
260
421
  line,
261
422
  column: objectTypeToken.column,
@@ -265,8 +426,12 @@ function startObjectSection(tokens, line, objectNodes) {
265
426
  return {
266
427
  kind: "object",
267
428
  objectNode,
268
- inInfoBlock: false,
269
- infoIndent: null,
429
+ sourceSchemaVersion,
430
+ diagnostics,
431
+ activeBlock: null,
432
+ blockIndent: null,
433
+ seenInfoKeys: new Set(),
434
+ seenTypedBlockKeys: {},
270
435
  };
271
436
  }
272
437
  function handleSectionLine(section, indent, tokens, line) {
@@ -286,6 +451,12 @@ function handleSectionLine(section, indent, tokens, line) {
286
451
  case "annotation":
287
452
  applyAnnotationField(section, tokens, line);
288
453
  return;
454
+ case "group":
455
+ applyGroupField(section, tokens, line);
456
+ return;
457
+ case "relation":
458
+ applyRelationField(section, tokens, line);
459
+ return;
289
460
  case "object":
290
461
  applyObjectField(section, indent, tokens, line);
291
462
  return;
@@ -293,10 +464,35 @@ function handleSectionLine(section, indent, tokens, line) {
293
464
  }
294
465
  function applySystemField(section, tokens, line) {
295
466
  const key = requireUniqueField(tokens, section.seenFields, line);
296
- if (key !== "title") {
297
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
467
+ const value = joinFieldValue(tokens, line);
468
+ switch (key) {
469
+ case "title":
470
+ section.system.title = value;
471
+ return;
472
+ case "description":
473
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
474
+ line,
475
+ column: tokens[0].column,
476
+ });
477
+ section.system.description = value;
478
+ return;
479
+ case "epoch":
480
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
481
+ line,
482
+ column: tokens[0].column,
483
+ });
484
+ section.system.epoch = value;
485
+ return;
486
+ case "referenceplane":
487
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
488
+ line,
489
+ column: tokens[0].column,
490
+ });
491
+ section.system.referencePlane = value;
492
+ return;
493
+ default:
494
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
298
495
  }
299
- section.system.title = joinFieldValue(tokens, line);
300
496
  }
301
497
  function applyDefaultsField(section, tokens, line) {
302
498
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -327,14 +523,11 @@ function applyAtlasField(section, indent, tokens, line) {
327
523
  section.metadataIndent = null;
328
524
  }
329
525
  if (section.inMetadata) {
330
- if (tokens.length < 2) {
331
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
332
- }
333
- const key = tokens[0].value;
334
- if (key in section.system.atlasMetadata) {
335
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
526
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
527
+ if (entry.key in section.system.atlasMetadata) {
528
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
336
529
  }
337
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
530
+ section.system.atlasMetadata[entry.key] = entry.value;
338
531
  return;
339
532
  }
340
533
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -436,21 +629,104 @@ function applyAnnotationField(section, tokens, line) {
436
629
  throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
437
630
  }
438
631
  }
632
+ function applyGroupField(section, tokens, line) {
633
+ const key = requireUniqueField(tokens, section.seenFields, line);
634
+ switch (key) {
635
+ case "label":
636
+ section.group.label = joinFieldValue(tokens, line);
637
+ return;
638
+ case "summary":
639
+ section.group.summary = joinFieldValue(tokens, line);
640
+ return;
641
+ case "color":
642
+ section.group.color = joinFieldValue(tokens, line);
643
+ return;
644
+ case "tags":
645
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
646
+ return;
647
+ case "hidden":
648
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
649
+ line,
650
+ column: tokens[0].column,
651
+ });
652
+ return;
653
+ default:
654
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
655
+ }
656
+ }
657
+ function applyRelationField(section, tokens, line) {
658
+ const key = requireUniqueField(tokens, section.seenFields, line);
659
+ switch (key) {
660
+ case "from":
661
+ section.relation.from = joinFieldValue(tokens, line);
662
+ return;
663
+ case "to":
664
+ section.relation.to = joinFieldValue(tokens, line);
665
+ return;
666
+ case "kind":
667
+ section.relation.kind = joinFieldValue(tokens, line);
668
+ return;
669
+ case "label":
670
+ section.relation.label = joinFieldValue(tokens, line);
671
+ return;
672
+ case "summary":
673
+ section.relation.summary = joinFieldValue(tokens, line);
674
+ return;
675
+ case "tags":
676
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
677
+ return;
678
+ case "color":
679
+ section.relation.color = joinFieldValue(tokens, line);
680
+ return;
681
+ case "hidden":
682
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
683
+ line,
684
+ column: tokens[0].column,
685
+ });
686
+ return;
687
+ default:
688
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
689
+ }
690
+ }
439
691
  function applyObjectField(section, indent, tokens, line) {
440
- if (tokens.length === 1 && tokens[0].value === "info") {
441
- section.inInfoBlock = true;
442
- section.infoIndent = indent;
443
- return;
692
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
693
+ section.activeBlock = null;
694
+ section.blockIndent = null;
444
695
  }
445
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
446
- section.inInfoBlock = false;
447
- section.infoIndent = null;
696
+ if (tokens.length === 1) {
697
+ const blockName = tokens[0].value.toLowerCase();
698
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
699
+ if (blockName !== "info") {
700
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
701
+ }
702
+ section.activeBlock = blockName;
703
+ section.blockIndent = indent;
704
+ return;
705
+ }
448
706
  }
449
- if (section.inInfoBlock) {
450
- section.objectNode.infoEntries.push(parseInfoEntry(tokens, line));
707
+ if (section.activeBlock) {
708
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
709
+ if (section.activeBlock === "info") {
710
+ if (section.seenInfoKeys.has(entry.key)) {
711
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
712
+ }
713
+ section.seenInfoKeys.add(entry.key);
714
+ section.objectNode.infoEntries.push(entry);
715
+ return;
716
+ }
717
+ const typedBlock = section.activeBlock;
718
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ??
719
+ (section.seenTypedBlockKeys[typedBlock] = new Set());
720
+ if (seenKeys.has(entry.key)) {
721
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
722
+ }
723
+ seenKeys.add(entry.key);
724
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ??
725
+ (section.objectNode.typedBlockEntries[typedBlock] = []);
726
+ entries.push(entry);
451
727
  return;
452
728
  }
453
- section.objectNode.blockFields.push(parseField(tokens, line));
729
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
454
730
  }
455
731
  function requireUniqueField(tokens, seenFields, line) {
456
732
  if (tokens.length < 2) {
@@ -474,64 +750,55 @@ function joinFieldValue(tokens, line) {
474
750
  .trim();
475
751
  }
476
752
  function parseObjectTypeTokens(tokens, line) {
477
- if (tokens.length === 0) {
478
- throw new WorldOrbitError("Missing value for atlas field", line);
479
- }
480
- return tokens.map((token) => {
481
- const value = token.value;
482
- if (value !== "star" &&
483
- value !== "planet" &&
484
- value !== "moon" &&
485
- value !== "belt" &&
486
- value !== "asteroid" &&
487
- value !== "comet" &&
488
- value !== "ring" &&
489
- value !== "structure" &&
490
- value !== "phenomenon") {
491
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
492
- }
493
- return value;
494
- });
495
- }
496
- function parseTokenList(tokens, line, field) {
497
- if (tokens.length === 0) {
498
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
499
- }
500
- return tokens.map((token) => token.value);
753
+ return parseTokenList(tokens, line, "objectTypes").filter((value) => value === "star" ||
754
+ value === "planet" ||
755
+ value === "moon" ||
756
+ value === "belt" ||
757
+ value === "asteroid" ||
758
+ value === "comet" ||
759
+ value === "ring" ||
760
+ value === "structure" ||
761
+ value === "phenomenon");
501
762
  }
502
763
  function parseLayerTokens(tokens, line) {
503
- if (tokens.length === 0) {
504
- throw new WorldOrbitError('Missing value for field "layers"', line);
505
- }
506
- const next = {};
507
- for (const token of tokens) {
508
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
509
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
510
- if (rawLayer === "orbits") {
511
- next["orbits-back"] = enabled;
512
- next["orbits-front"] = enabled;
764
+ const layers = {};
765
+ for (const token of parseTokenList(tokens, line, "layers")) {
766
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
767
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
768
+ if (raw === "orbits") {
769
+ layers["orbits-back"] = enabled;
770
+ layers["orbits-front"] = enabled;
513
771
  continue;
514
772
  }
515
- if (rawLayer === "background" ||
516
- rawLayer === "guides" ||
517
- rawLayer === "orbits-back" ||
518
- rawLayer === "orbits-front" ||
519
- rawLayer === "objects" ||
520
- rawLayer === "labels" ||
521
- rawLayer === "metadata") {
522
- next[rawLayer] = enabled;
523
- continue;
773
+ if (raw === "background" ||
774
+ raw === "guides" ||
775
+ raw === "orbits-back" ||
776
+ raw === "orbits-front" ||
777
+ raw === "relations" ||
778
+ raw === "objects" ||
779
+ raw === "labels" ||
780
+ raw === "metadata") {
781
+ layers[raw] = enabled;
524
782
  }
525
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
526
783
  }
527
- return next;
784
+ return layers;
785
+ }
786
+ function parseTokenList(tokens, line, fieldName) {
787
+ if (tokens.length === 0) {
788
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
789
+ }
790
+ const values = tokens.map((token) => token.value).filter(Boolean);
791
+ if (values.length === 0) {
792
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
793
+ }
794
+ return values;
528
795
  }
529
796
  function parseProjectionValue(value, line, column) {
530
797
  const normalized = value.toLowerCase();
531
- if (normalized === "topdown" || normalized === "isometric") {
532
- return normalized;
798
+ if (normalized !== "topdown" && normalized !== "isometric") {
799
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
533
800
  }
534
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
801
+ return normalized;
535
802
  }
536
803
  function parsePresetValue(value, line, column) {
537
804
  const normalized = value.toLowerCase();
@@ -544,16 +811,16 @@ function parsePresetValue(value, line, column) {
544
811
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
545
812
  }
546
813
  function parsePositiveNumber(value, line, column, field) {
547
- const parsed = Number(value);
548
- if (!Number.isFinite(parsed) || parsed <= 0) {
549
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
814
+ const parsed = parseFiniteNumber(value, line, column, field);
815
+ if (parsed <= 0) {
816
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
550
817
  }
551
818
  return parsed;
552
819
  }
553
820
  function parseFiniteNumber(value, line, column, field) {
554
821
  const parsed = Number(value);
555
822
  if (!Number.isFinite(parsed)) {
556
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
823
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
557
824
  }
558
825
  return parsed;
559
826
  }
@@ -565,30 +832,46 @@ function createEmptyViewpointFilter() {
565
832
  groupIds: [],
566
833
  };
567
834
  }
568
- function parseInlineFields(tokens, line) {
835
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
569
836
  const fields = [];
570
837
  let index = 0;
571
838
  while (index < tokens.length) {
572
839
  const keyToken = tokens[index];
573
- const schema = getFieldSchema(keyToken.value);
574
- if (!schema) {
840
+ const spec = getDraftObjectFieldSpec(keyToken.value);
841
+ if (!spec) {
575
842
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
576
843
  }
844
+ if (spec.version === "2.1") {
845
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
846
+ line,
847
+ column: keyToken.column,
848
+ });
849
+ }
577
850
  index++;
578
851
  const valueTokens = [];
579
- if (schema.arity === "multiple") {
580
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
581
- valueTokens.push(tokens[index]);
852
+ if (spec.inlineMode === "single") {
853
+ const nextToken = tokens[index];
854
+ if (nextToken) {
855
+ valueTokens.push(nextToken);
582
856
  index++;
583
857
  }
584
858
  }
585
- else {
586
- const nextToken = tokens[index];
587
- if (nextToken) {
859
+ else if (spec.inlineMode === "pair") {
860
+ for (let count = 0; count < 2; count++) {
861
+ const nextToken = tokens[index];
862
+ if (!nextToken) {
863
+ break;
864
+ }
588
865
  valueTokens.push(nextToken);
589
866
  index++;
590
867
  }
591
868
  }
869
+ else {
870
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
871
+ valueTokens.push(tokens[index]);
872
+ index++;
873
+ }
874
+ }
592
875
  if (valueTokens.length === 0) {
593
876
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
594
877
  }
@@ -599,25 +882,35 @@ function parseInlineFields(tokens, line) {
599
882
  location: { line, column: keyToken.column },
600
883
  });
601
884
  }
885
+ validateDraftObjectFieldCompatibility(fields, objectType);
602
886
  return fields;
603
887
  }
604
- function parseField(tokens, line) {
888
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
605
889
  if (tokens.length < 2) {
606
890
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
607
891
  }
608
- if (!getFieldSchema(tokens[0].value)) {
892
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
893
+ if (!spec) {
609
894
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
610
895
  }
611
- return {
896
+ if (spec.version === "2.1") {
897
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
898
+ line,
899
+ column: tokens[0].column,
900
+ });
901
+ }
902
+ const field = {
612
903
  type: "field",
613
904
  key: tokens[0].value,
614
905
  values: tokens.slice(1).map((token) => token.value),
615
906
  location: { line, column: tokens[0].column },
616
907
  };
908
+ validateDraftObjectFieldCompatibility([field], objectType);
909
+ return field;
617
910
  }
618
- function parseInfoEntry(tokens, line) {
911
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
619
912
  if (tokens.length < 2) {
620
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
913
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
621
914
  }
622
915
  return {
623
916
  type: "info-entry",
@@ -626,17 +919,357 @@ function parseInfoEntry(tokens, line) {
626
919
  location: { line, column: tokens[0].column },
627
920
  };
628
921
  }
629
- function normalizeIdentifier(value) {
630
- return value
631
- .trim()
632
- .toLowerCase()
633
- .replace(/[^a-z0-9_-]+/g, "-")
634
- .replace(/^-+|-+$/g, "");
635
- }
636
- function humanizeIdentifier(value) {
637
- return value
638
- .split(/[-_]+/)
639
- .filter(Boolean)
640
- .map((segment) => segment[0].toUpperCase() + segment.slice(1))
641
- .join(" ");
922
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
923
+ const fieldMap = collectDraftFields(node.fields);
924
+ const placement = extractDraftPlacement(node.objectType, fieldMap);
925
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
926
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
927
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
928
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
929
+ const tidalLock = fieldMap.has("tidalLock")
930
+ ? parseAtlasBoolean(singleFieldValue(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location)
931
+ : undefined;
932
+ const resonance = fieldMap.has("resonance")
933
+ ? parseResonanceField(fieldMap.get("resonance")[0])
934
+ : undefined;
935
+ const renderHints = extractRenderHints(fieldMap);
936
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
937
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
938
+ rule: singleFieldValue(field),
939
+ }));
940
+ const lockedFields = fieldMap.has("locked")
941
+ ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))]
942
+ : undefined;
943
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
944
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
945
+ const info = normalizeInfoEntries(node.infoEntries, "info");
946
+ const object = {
947
+ type: node.objectType,
948
+ id: node.id,
949
+ properties,
950
+ placement,
951
+ info,
952
+ };
953
+ if (groups.length > 0)
954
+ object.groups = groups;
955
+ if (epoch)
956
+ object.epoch = epoch;
957
+ if (referencePlane)
958
+ object.referencePlane = referencePlane;
959
+ if (tidalLock !== undefined)
960
+ object.tidalLock = tidalLock;
961
+ if (resonance)
962
+ object.resonance = resonance;
963
+ if (renderHints)
964
+ object.renderHints = renderHints;
965
+ if (deriveRules?.length)
966
+ object.deriveRules = deriveRules;
967
+ if (validationRules?.length)
968
+ object.validationRules = validationRules;
969
+ if (lockedFields?.length)
970
+ object.lockedFields = lockedFields;
971
+ if (tolerances?.length)
972
+ object.tolerances = tolerances;
973
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
974
+ object.typedBlocks = typedBlocks;
975
+ if (sourceSchemaVersion !== "2.1") {
976
+ if (object.groups ||
977
+ object.epoch ||
978
+ object.referencePlane ||
979
+ object.tidalLock !== undefined ||
980
+ object.resonance ||
981
+ object.renderHints ||
982
+ object.deriveRules?.length ||
983
+ object.validationRules?.length ||
984
+ object.lockedFields?.length ||
985
+ object.tolerances?.length ||
986
+ object.typedBlocks) {
987
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
988
+ }
989
+ }
990
+ return object;
991
+ }
992
+ function collectDraftFields(fields) {
993
+ const grouped = new Map();
994
+ for (const field of fields) {
995
+ const spec = getDraftObjectFieldSpec(field.key);
996
+ if (!spec) {
997
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
998
+ }
999
+ if (!spec.allowRepeat && grouped.has(field.key)) {
1000
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
1001
+ }
1002
+ const existing = grouped.get(field.key) ?? [];
1003
+ existing.push(field);
1004
+ grouped.set(field.key, existing);
1005
+ }
1006
+ return grouped;
1007
+ }
1008
+ function extractDraftPlacement(objectType, fieldMap) {
1009
+ const orbitField = fieldMap.get("orbit")?.[0];
1010
+ const atField = fieldMap.get("at")?.[0];
1011
+ const surfaceField = fieldMap.get("surface")?.[0];
1012
+ const freeField = fieldMap.get("free")?.[0];
1013
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
1014
+ if (count > 1) {
1015
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
1016
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
1017
+ }
1018
+ if (orbitField) {
1019
+ return {
1020
+ mode: "orbit",
1021
+ target: singleFieldValue(orbitField),
1022
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
1023
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
1024
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
1025
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
1026
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
1027
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
1028
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase"),
1029
+ };
1030
+ }
1031
+ if (atField) {
1032
+ const target = singleFieldValue(atField);
1033
+ return {
1034
+ mode: "at",
1035
+ target,
1036
+ reference: parseAtlasAtReference(target, atField.location),
1037
+ };
1038
+ }
1039
+ if (surfaceField) {
1040
+ return {
1041
+ mode: "surface",
1042
+ target: singleFieldValue(surfaceField),
1043
+ };
1044
+ }
1045
+ if (freeField) {
1046
+ const raw = singleFieldValue(freeField);
1047
+ const distance = tryParseAtlasUnitValue(raw);
1048
+ return {
1049
+ mode: "free",
1050
+ distance: distance ?? undefined,
1051
+ descriptor: distance ? undefined : raw,
1052
+ };
1053
+ }
1054
+ return null;
1055
+ }
1056
+ function normalizeDraftProperties(objectType, fieldMap) {
1057
+ const properties = {};
1058
+ for (const [key, fields] of fieldMap.entries()) {
1059
+ const field = fields[0];
1060
+ const spec = getDraftObjectFieldSpec(key);
1061
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
1062
+ continue;
1063
+ }
1064
+ ensureAtlasFieldSupported(key, objectType, field.location);
1065
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
1066
+ }
1067
+ return properties;
1068
+ }
1069
+ function normalizeInfoEntries(entries, label) {
1070
+ const normalized = {};
1071
+ for (const entry of entries) {
1072
+ if (entry.key in normalized) {
1073
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
1074
+ }
1075
+ normalized[entry.key] = entry.value;
1076
+ }
1077
+ return normalized;
1078
+ }
1079
+ function normalizeTypedBlocks(typedBlockEntries) {
1080
+ const typedBlocks = {};
1081
+ for (const blockName of Object.keys(typedBlockEntries)) {
1082
+ const entries = typedBlockEntries[blockName];
1083
+ if (entries?.length) {
1084
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
1085
+ }
1086
+ }
1087
+ return typedBlocks;
1088
+ }
1089
+ function extractRenderHints(fieldMap) {
1090
+ const renderHints = {};
1091
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
1092
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
1093
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
1094
+ if (renderLabelField) {
1095
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue(renderLabelField), "renderLabel", renderLabelField.location);
1096
+ }
1097
+ if (renderOrbitField) {
1098
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue(renderOrbitField), "renderOrbit", renderOrbitField.location);
1099
+ }
1100
+ if (renderPriorityField) {
1101
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue(renderPriorityField), "renderPriority", renderPriorityField.location);
1102
+ }
1103
+ return Object.keys(renderHints).length > 0 ? renderHints : undefined;
1104
+ }
1105
+ function parseResonanceField(field) {
1106
+ if (field.values.length !== 2) {
1107
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
1108
+ }
1109
+ const ratio = field.values[1];
1110
+ if (!/^\d+:\d+$/.test(ratio)) {
1111
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
1112
+ }
1113
+ return {
1114
+ targetObjectId: field.values[0],
1115
+ ratio,
1116
+ };
1117
+ }
1118
+ function parseDeriveField(field) {
1119
+ if (field.values.length !== 2) {
1120
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
1121
+ }
1122
+ return {
1123
+ field: field.values[0],
1124
+ strategy: field.values[1],
1125
+ };
1126
+ }
1127
+ function parseToleranceField(field) {
1128
+ if (field.values.length !== 2) {
1129
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
1130
+ }
1131
+ const rawValue = field.values[1];
1132
+ const unitValue = tryParseAtlasUnitValue(rawValue);
1133
+ const numericValue = Number(rawValue);
1134
+ return {
1135
+ field: field.values[0],
1136
+ value: unitValue ?? (Number.isFinite(numericValue) ? numericValue : rawValue),
1137
+ };
1138
+ }
1139
+ function parseOptionalTokenList(field) {
1140
+ return field ? [...new Set(field.values)] : [];
1141
+ }
1142
+ function parseOptionalJoinedValue(field) {
1143
+ if (!field) {
1144
+ return null;
1145
+ }
1146
+ return field.values.join(" ").trim() || null;
1147
+ }
1148
+ function parseOptionalUnitField(field, key) {
1149
+ return field ? parseAtlasUnitValue(singleFieldValue(field), field.location, key) : undefined;
1150
+ }
1151
+ function parseOptionalNumberField(field, key) {
1152
+ return field ? parseAtlasNumber(singleFieldValue(field), key, field.location) : undefined;
1153
+ }
1154
+ function singleFieldValue(field) {
1155
+ return singleAtlasValue(field.values, field.key, field.location);
1156
+ }
1157
+ function getDraftObjectFieldSpec(key) {
1158
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
1159
+ }
1160
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
1161
+ for (const field of fields) {
1162
+ const spec = getDraftObjectFieldSpec(field.key);
1163
+ if (!spec) {
1164
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
1165
+ }
1166
+ if (spec.legacySchema) {
1167
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
1168
+ continue;
1169
+ }
1170
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") &&
1171
+ field.values.length !== 1) {
1172
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
1173
+ }
1174
+ }
1175
+ }
1176
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
1177
+ if (sourceSchemaVersion === "2.1") {
1178
+ return;
1179
+ }
1180
+ diagnostics.push({
1181
+ code: "parse.schema21.featureCompatibility",
1182
+ severity: "warning",
1183
+ source: "parse",
1184
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
1185
+ line: location.line,
1186
+ column: location.column,
1187
+ });
1188
+ }
1189
+ function preprocessAtlasSource(source) {
1190
+ const chars = [...source];
1191
+ const comments = [];
1192
+ let inString = false;
1193
+ let inBlockComment = false;
1194
+ let blockCommentStart = null;
1195
+ let line = 1;
1196
+ let column = 1;
1197
+ for (let index = 0; index < chars.length; index++) {
1198
+ const ch = chars[index];
1199
+ const next = chars[index + 1];
1200
+ if (inBlockComment) {
1201
+ if (ch === "*" && next === "/") {
1202
+ chars[index] = " ";
1203
+ chars[index + 1] = " ";
1204
+ inBlockComment = false;
1205
+ blockCommentStart = null;
1206
+ index++;
1207
+ column += 2;
1208
+ continue;
1209
+ }
1210
+ if (ch !== "\n" && ch !== "\r") {
1211
+ chars[index] = " ";
1212
+ }
1213
+ if (ch === "\n") {
1214
+ line++;
1215
+ column = 1;
1216
+ }
1217
+ else {
1218
+ column++;
1219
+ }
1220
+ continue;
1221
+ }
1222
+ if (!inString && ch === "/" && next === "*") {
1223
+ comments.push({ kind: "block", line, column });
1224
+ chars[index] = " ";
1225
+ chars[index + 1] = " ";
1226
+ inBlockComment = true;
1227
+ blockCommentStart = { line, column };
1228
+ index++;
1229
+ column += 2;
1230
+ continue;
1231
+ }
1232
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
1233
+ comments.push({ kind: "line", line, column });
1234
+ chars[index] = " ";
1235
+ let inner = index + 1;
1236
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
1237
+ chars[inner] = " ";
1238
+ inner++;
1239
+ }
1240
+ column += inner - index;
1241
+ index = inner - 1;
1242
+ continue;
1243
+ }
1244
+ if (ch === '"' && chars[index - 1] !== "\\") {
1245
+ inString = !inString;
1246
+ }
1247
+ if (ch === "\n") {
1248
+ line++;
1249
+ column = 1;
1250
+ }
1251
+ else {
1252
+ column++;
1253
+ }
1254
+ }
1255
+ if (inBlockComment) {
1256
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? undefined);
1257
+ }
1258
+ return {
1259
+ source: chars.join(""),
1260
+ comments,
1261
+ };
1262
+ }
1263
+ function isHexColorLiteral(chars, start) {
1264
+ let index = start + 1;
1265
+ let length = 0;
1266
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
1267
+ index++;
1268
+ length++;
1269
+ }
1270
+ if (![3, 4, 6, 8].includes(length)) {
1271
+ return false;
1272
+ }
1273
+ const next = chars[index];
1274
+ return next === undefined || next === " " || next === "\t" || next === "\r" || next === "\n";
642
1275
  }