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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worldorbit",
3
- "version": "2.5.13",
3
+ "version": "2.5.15",
4
4
  "description": "A text-based DSL and parser pipeline for orbital worldbuilding",
5
5
  "type": "module",
6
6
  "main": "./dist/unpkg/worldorbit.esm.js",
@@ -64,4 +64,4 @@
64
64
  "typescript": "^5.6.0",
65
65
  "unified": "^11.0.5"
66
66
  }
67
- }
67
+ }
@@ -1,13 +1,17 @@
1
1
  # @worldorbit/core
2
2
 
3
- WorldOrbit core contains the parser, schema, normalization, validation, formatting, and scene generation APIs.
3
+ WorldOrbit core contains the parser, schema, normalization, validation, formatting, and scene generation APIs for Schema 1.0, canonical Schema 2.0, and Schema 2.1 atlas source.
4
4
 
5
5
  Main exports:
6
6
 
7
7
  - `parse(source)`
8
+ - `parseWorldOrbitAtlas(source)`
9
+ - `parseWorldOrbitDraft(source)`
8
10
  - `parseWorldOrbit(source)`
9
11
  - `normalizeDocument(ast)`
10
12
  - `validateDocument(document)`
13
+ - `validateAtlasDocumentWithDiagnostics(document)`
11
14
  - `renderDocumentToScene(document, options?)`
12
15
  - `formatDocument(document)`
16
+ - `loadWorldOrbitSource(source)`
13
17
  - `extractWorldOrbitBlocks(markdown)`
@@ -1,5 +1,5 @@
1
- import type { AtlasDocumentPath, AtlasResolvedDiagnostic, WorldOrbitAtlasDocument, WorldOrbitDiagnostic } from "./types.js";
2
- export declare function createEmptyAtlasDocument(systemId?: string): WorldOrbitAtlasDocument;
1
+ import type { AtlasDocumentPath, AtlasResolvedDiagnostic, WorldOrbitAtlasDocument, WorldOrbitAtlasDocumentVersion, WorldOrbitDiagnostic } from "./types.js";
2
+ export declare function createEmptyAtlasDocument(systemId?: string, version?: WorldOrbitAtlasDocumentVersion): WorldOrbitAtlasDocument;
3
3
  export declare function cloneAtlasDocument(document: WorldOrbitAtlasDocument): WorldOrbitAtlasDocument;
4
4
  export declare function listAtlasDocumentPaths(document: WorldOrbitAtlasDocument): AtlasDocumentPath[];
5
5
  export declare function getAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath): unknown;
@@ -1,14 +1,17 @@
1
- import { materializeAtlasDocument } from "./draft.js";
2
- import { validateDocumentWithDiagnostics } from "./diagnostics.js";
3
- export function createEmptyAtlasDocument(systemId = "WorldOrbit") {
1
+ import { collectAtlasDiagnostics } from "./atlas-validate.js";
2
+ export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
4
3
  return {
5
4
  format: "worldorbit",
6
- version: "2.0",
5
+ version,
6
+ schemaVersion: version,
7
7
  sourceVersion: "1.0",
8
8
  system: {
9
9
  type: "system",
10
10
  id: systemId,
11
11
  title: systemId,
12
+ description: null,
13
+ epoch: null,
14
+ referencePlane: null,
12
15
  defaults: {
13
16
  view: "topdown",
14
17
  scale: null,
@@ -20,6 +23,8 @@ export function createEmptyAtlasDocument(systemId = "WorldOrbit") {
20
23
  viewpoints: [],
21
24
  annotations: [],
22
25
  },
26
+ groups: [],
27
+ relations: [],
23
28
  objects: [],
24
29
  diagnostics: [],
25
30
  };
@@ -40,6 +45,12 @@ export function listAtlasDocumentPaths(document) {
40
45
  paths.push({ kind: "annotation", id: annotation.id });
41
46
  }
42
47
  }
48
+ for (const group of [...document.groups].sort(compareIdLike)) {
49
+ paths.push({ kind: "group", id: group.id });
50
+ }
51
+ for (const relation of [...document.relations].sort(compareIdLike)) {
52
+ paths.push({ kind: "relation", id: relation.id });
53
+ }
43
54
  for (const object of [...document.objects].sort(compareIdLike)) {
44
55
  paths.push({ kind: "object", id: object.id });
45
56
  }
@@ -53,12 +64,16 @@ export function getAtlasDocumentNode(document, path) {
53
64
  return document.system?.defaults ?? null;
54
65
  case "metadata":
55
66
  return path.key ? (document.system?.atlasMetadata[path.key] ?? null) : null;
67
+ case "group":
68
+ return path.id ? findGroup(document, path.id) : null;
56
69
  case "object":
57
70
  return path.id ? findObject(document, path.id) : null;
58
71
  case "viewpoint":
59
72
  return path.id ? findViewpoint(document.system, path.id) : null;
60
73
  case "annotation":
61
74
  return path.id ? findAnnotation(document.system, path.id) : null;
75
+ case "relation":
76
+ return path.id ? findRelation(document, path.id) : null;
62
77
  }
63
78
  }
64
79
  export function upsertAtlasDocumentNode(document, path, value) {
@@ -85,6 +100,12 @@ export function upsertAtlasDocumentNode(document, path, value) {
85
100
  system.atlasMetadata[path.key] = String(value);
86
101
  }
87
102
  return next;
103
+ case "group":
104
+ if (!path.id) {
105
+ throw new Error('Group updates require an "id" value.');
106
+ }
107
+ upsertById(next.groups, value);
108
+ return next;
88
109
  case "object":
89
110
  if (!path.id) {
90
111
  throw new Error('Object updates require an "id" value.');
@@ -103,6 +124,12 @@ export function upsertAtlasDocumentNode(document, path, value) {
103
124
  }
104
125
  upsertById(system.annotations, value);
105
126
  return next;
127
+ case "relation":
128
+ if (!path.id) {
129
+ throw new Error('Relation updates require an "id" value.');
130
+ }
131
+ upsertById(next.relations, value);
132
+ return next;
106
133
  }
107
134
  }
108
135
  export function updateAtlasDocumentNode(document, path, updater) {
@@ -122,6 +149,11 @@ export function removeAtlasDocumentNode(document, path) {
122
149
  next.objects = next.objects.filter((object) => object.id !== path.id);
123
150
  }
124
151
  return next;
152
+ case "group":
153
+ if (path.id) {
154
+ next.groups = next.groups.filter((group) => group.id !== path.id);
155
+ }
156
+ return next;
125
157
  case "viewpoint":
126
158
  if (path.id) {
127
159
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -132,6 +164,11 @@ export function removeAtlasDocumentNode(document, path) {
132
164
  system.annotations = system.annotations.filter((annotation) => annotation.id !== path.id);
133
165
  }
134
166
  return next;
167
+ case "relation":
168
+ if (path.id) {
169
+ next.relations = next.relations.filter((relation) => relation.id !== path.id);
170
+ }
171
+ return next;
135
172
  default:
136
173
  return next;
137
174
  }
@@ -149,6 +186,15 @@ export function resolveAtlasDiagnosticPath(document, diagnostic) {
149
186
  id: diagnostic.objectId,
150
187
  };
151
188
  }
189
+ if (diagnostic.field?.startsWith("group.")) {
190
+ const parts = diagnostic.field.split(".");
191
+ if (parts[1] && findGroup(document, parts[1])) {
192
+ return {
193
+ kind: "group",
194
+ id: parts[1],
195
+ };
196
+ }
197
+ }
152
198
  if (diagnostic.field?.startsWith("viewpoint.")) {
153
199
  const parts = diagnostic.field.split(".");
154
200
  if (parts[1] && findViewpoint(document.system, parts[1])) {
@@ -167,6 +213,15 @@ export function resolveAtlasDiagnosticPath(document, diagnostic) {
167
213
  };
168
214
  }
169
215
  }
216
+ if (diagnostic.field?.startsWith("relation.")) {
217
+ const parts = diagnostic.field.split(".");
218
+ if (parts[1] && findRelation(document, parts[1])) {
219
+ return {
220
+ kind: "relation",
221
+ id: parts[1],
222
+ };
223
+ }
224
+ }
170
225
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
171
226
  return {
172
227
  kind: "metadata",
@@ -176,9 +231,11 @@ export function resolveAtlasDiagnosticPath(document, diagnostic) {
176
231
  return null;
177
232
  }
178
233
  export function validateAtlasDocumentWithDiagnostics(document) {
179
- const materialized = materializeAtlasDocument(document);
180
- const result = validateDocumentWithDiagnostics(materialized);
181
- return resolveAtlasDiagnostics(document, result.diagnostics);
234
+ const diagnostics = [
235
+ ...document.diagnostics,
236
+ ...collectAtlasDiagnostics(document, document.version),
237
+ ];
238
+ return resolveAtlasDiagnostics(document, diagnostics);
182
239
  }
183
240
  function ensureSystem(document) {
184
241
  if (document.system) {
@@ -190,6 +247,12 @@ function ensureSystem(document) {
190
247
  function findObject(document, objectId) {
191
248
  return document.objects.find((object) => object.id === objectId) ?? null;
192
249
  }
250
+ function findGroup(document, groupId) {
251
+ return document.groups.find((group) => group.id === groupId) ?? null;
252
+ }
253
+ function findRelation(document, relationId) {
254
+ return document.relations.find((relation) => relation.id === relationId) ?? null;
255
+ }
193
256
  function findViewpoint(system, viewpointId) {
194
257
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
195
258
  }
@@ -0,0 +1,22 @@
1
+ import type { AstFieldNode, AstSourceLocation, AtReference, NormalizedValue, UnitValue, WorldOrbitObjectType } from "./types.js";
2
+ export interface AtlasFieldLike {
3
+ key: string;
4
+ values: string[];
5
+ location: AstSourceLocation;
6
+ }
7
+ export declare function normalizeIdentifier(value: string): string;
8
+ export declare function humanizeIdentifier(value: string): string;
9
+ export declare function parseAtlasUnitValue(input: string, location?: AstSourceLocation, fieldKey?: string): UnitValue;
10
+ export declare function tryParseAtlasUnitValue(input: string): UnitValue | null;
11
+ export declare function parseAtlasNumber(input: string, key: string, location?: AstSourceLocation): number;
12
+ export declare function parseAtlasBoolean(input: string, key: string, location?: AstSourceLocation): boolean;
13
+ export declare function parseAtlasFieldBoolean(field: AtlasFieldLike): boolean;
14
+ export declare function parseAtlasAtReference(target: string, location?: AstSourceLocation): AtReference;
15
+ export declare function validateAtlasImageSource(value: string, location?: AstSourceLocation): void;
16
+ export declare function normalizeLegacyScalarValue(key: string, values: string[], location: AstSourceLocation): NormalizedValue;
17
+ export declare function ensureAtlasFieldSupported(key: string, objectType: WorldOrbitObjectType, location: AstSourceLocation): void;
18
+ export declare function singleAtlasFieldValue(field: Pick<AtlasFieldLike, "key" | "values" | "location">): string;
19
+ export declare function singleAtlasValue(values: string[], key: string, location?: AstSourceLocation): string;
20
+ export declare function isStructureLikeObjectType(objectType: WorldOrbitObjectType): boolean;
21
+ export declare function cloneNormalizedValue(value: NormalizedValue): NormalizedValue;
22
+ export declare function cloneFieldNode(field: AstFieldNode): AstFieldNode;
@@ -0,0 +1,189 @@
1
+ import { WorldOrbitError } from "./errors.js";
2
+ import { getFieldSchema, unitFamilyAllowsUnit } from "./schema.js";
3
+ const UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
4
+ const BOOLEAN_VALUES = new Map([
5
+ ["true", true],
6
+ ["false", false],
7
+ ["yes", true],
8
+ ["no", false],
9
+ ]);
10
+ const URL_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;
11
+ export function normalizeIdentifier(value) {
12
+ return value
13
+ .trim()
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9_-]+/g, "-")
16
+ .replace(/^-+|-+$/g, "");
17
+ }
18
+ export function humanizeIdentifier(value) {
19
+ return value
20
+ .split(/[-_]+/)
21
+ .filter(Boolean)
22
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
23
+ .join(" ");
24
+ }
25
+ export function parseAtlasUnitValue(input, location, fieldKey) {
26
+ const match = input.match(UNIT_PATTERN);
27
+ if (!match) {
28
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
29
+ }
30
+ const unitValue = {
31
+ value: Number(match[1]),
32
+ unit: match[2] ?? null,
33
+ };
34
+ if (fieldKey) {
35
+ const schema = getFieldSchema(fieldKey);
36
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
37
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
38
+ }
39
+ }
40
+ return unitValue;
41
+ }
42
+ export function tryParseAtlasUnitValue(input) {
43
+ const match = input.match(UNIT_PATTERN);
44
+ if (!match) {
45
+ return null;
46
+ }
47
+ return {
48
+ value: Number(match[1]),
49
+ unit: match[2] ?? null,
50
+ };
51
+ }
52
+ export function parseAtlasNumber(input, key, location) {
53
+ const value = Number(input);
54
+ if (!Number.isFinite(value)) {
55
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
56
+ }
57
+ return value;
58
+ }
59
+ export function parseAtlasBoolean(input, key, location) {
60
+ const parsed = BOOLEAN_VALUES.get(input.toLowerCase());
61
+ if (parsed === undefined) {
62
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
63
+ }
64
+ return parsed;
65
+ }
66
+ export function parseAtlasFieldBoolean(field) {
67
+ return parseAtlasBoolean(singleAtlasFieldValue(field), field.key, field.location);
68
+ }
69
+ export function parseAtlasAtReference(target, location) {
70
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
71
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
72
+ }
73
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
74
+ if (pairedMatch) {
75
+ return {
76
+ kind: "lagrange",
77
+ primary: pairedMatch[1],
78
+ secondary: pairedMatch[2],
79
+ point: pairedMatch[3],
80
+ };
81
+ }
82
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
83
+ if (simpleMatch) {
84
+ return {
85
+ kind: "lagrange",
86
+ primary: simpleMatch[1],
87
+ secondary: null,
88
+ point: simpleMatch[2],
89
+ };
90
+ }
91
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
92
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
93
+ }
94
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
95
+ if (anchorMatch) {
96
+ return {
97
+ kind: "anchor",
98
+ objectId: anchorMatch[1],
99
+ anchor: anchorMatch[2],
100
+ };
101
+ }
102
+ return {
103
+ kind: "named",
104
+ name: target,
105
+ };
106
+ }
107
+ export function validateAtlasImageSource(value, location) {
108
+ if (!value) {
109
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
110
+ }
111
+ if (value.startsWith("//")) {
112
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
113
+ }
114
+ const schemeMatch = value.match(URL_SCHEME_PATTERN);
115
+ if (!schemeMatch) {
116
+ return;
117
+ }
118
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
119
+ if (scheme !== "http" && scheme !== "https") {
120
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
121
+ }
122
+ }
123
+ export function normalizeLegacyScalarValue(key, values, location) {
124
+ const schema = getFieldSchema(key);
125
+ if (!schema) {
126
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
127
+ }
128
+ if (schema.arity === "single" && values.length !== 1) {
129
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
130
+ }
131
+ switch (schema.kind) {
132
+ case "list":
133
+ return values;
134
+ case "boolean":
135
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
136
+ case "number":
137
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
138
+ case "unit":
139
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
140
+ case "string": {
141
+ const value = values.join(" ").trim();
142
+ if (key === "image") {
143
+ validateAtlasImageSource(value, location);
144
+ }
145
+ return value;
146
+ }
147
+ }
148
+ }
149
+ export function ensureAtlasFieldSupported(key, objectType, location) {
150
+ const schema = getFieldSchema(key);
151
+ if (!schema) {
152
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
153
+ }
154
+ if (!schema.objectTypes.includes(objectType)) {
155
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
156
+ }
157
+ }
158
+ export function singleAtlasFieldValue(field) {
159
+ return singleAtlasValue(field.values, field.key, field.location);
160
+ }
161
+ export function singleAtlasValue(values, key, location) {
162
+ if (values.length !== 1) {
163
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
164
+ }
165
+ return values[0];
166
+ }
167
+ export function isStructureLikeObjectType(objectType) {
168
+ return objectType === "structure" || objectType === "phenomenon";
169
+ }
170
+ export function cloneNormalizedValue(value) {
171
+ if (Array.isArray(value)) {
172
+ return [...value];
173
+ }
174
+ if (value && typeof value === "object" && "value" in value) {
175
+ return {
176
+ value: value.value,
177
+ unit: value.unit,
178
+ };
179
+ }
180
+ return value;
181
+ }
182
+ export function cloneFieldNode(field) {
183
+ return {
184
+ type: "field",
185
+ key: field.key,
186
+ values: [...field.values],
187
+ location: { ...field.location },
188
+ };
189
+ }
@@ -0,0 +1,2 @@
1
+ import type { WorldOrbitAnyDocumentVersion, WorldOrbitAtlasDocument, WorldOrbitDiagnostic, WorldOrbitDraftDocument } from "./types.js";
2
+ export declare function collectAtlasDiagnostics(document: WorldOrbitAtlasDocument | WorldOrbitDraftDocument, sourceSchemaVersion?: WorldOrbitAnyDocumentVersion): WorldOrbitDiagnostic[];