@lensmap/document 0.2.0 → 0.3.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/export-schema.js +6 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +93 -8
- package/dist/node-style.d.ts +17 -0
- package/dist/node-style.js +165 -0
- package/dist/project.d.ts +6 -0
- package/dist/project.js +91 -0
- package/dist/v1/schema.d.ts +8 -11
- package/dist/v1/schema.js +16 -10
- package/dist/vocabulary.d.ts +67 -0
- package/dist/vocabulary.js +530 -0
- package/package.json +7 -1
package/dist/export-schema.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { generateJSONSchema } from "./index.js";
|
|
3
|
+
import { generateJSONSchema, generateVocabularyJSONSchema } from "./index.js";
|
|
4
4
|
const target = process.argv[2];
|
|
5
5
|
if (!target)
|
|
6
6
|
throw new Error("Supply the generated schema's output filename.");
|
|
7
7
|
const path = resolve(target);
|
|
8
8
|
await mkdir(dirname(path), { recursive: true });
|
|
9
9
|
await writeFile(path, `${JSON.stringify({ ...generateJSONSchema(), title: "Lensmap experimental v1", $comment: "Generated from the owning Zod input schema. Not a public compatibility promise; document-wide IDs/references are checked by the engine." }, null, 2)}\n`);
|
|
10
|
+
if (process.argv[3]) {
|
|
11
|
+
const vocabulary = resolve(process.argv[3]);
|
|
12
|
+
await mkdir(dirname(vocabulary), { recursive: true });
|
|
13
|
+
await writeFile(vocabulary, `${JSON.stringify({ ...generateVocabularyJSONSchema(), title: "Lensmap experimental connection vocabulary", $comment: "Generated from the owning Zod input schema. Layered inheritance and CSS value semantics are checked by the engine." }, null, 2)}\n`);
|
|
14
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { type NodePresentation } from "./node-style.js";
|
|
2
|
+
export type { NodePresentation, NodeStyleDeclaration } from "./node-style.js";
|
|
3
|
+
import { type ConnectionPresentation, type ResolvedConnectionType, type VocabularyInput } from "./vocabulary.js";
|
|
4
|
+
export * from "./vocabulary.js";
|
|
1
5
|
import { z } from "zod";
|
|
2
6
|
import { type ConnectionType } from "./v1/schema.js";
|
|
3
7
|
export type { ConnectionType, LensmapDocument, Reference, RichNode, } from "./v1/schema.js";
|
|
@@ -5,10 +9,15 @@ export { ConnectionTypeSchema, DocumentSchema } from "./v1/schema.js";
|
|
|
5
9
|
export declare function generateJSONSchema(): z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
|
|
6
10
|
version: z.ZodLiteral<"v1">;
|
|
7
11
|
frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
12
|
+
connection_types: z.ZodOptional<z.ZodType<import("./vocabulary.js").ConnectionTypeDeclarations, unknown, z.core.$ZodTypeInternals<import("./vocabulary.js").ConnectionTypeDeclarations, unknown>>>;
|
|
13
|
+
node_styles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>]>>>;
|
|
8
14
|
nodes: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<import("./v1/schema.js").RichNode, unknown, z.core.$ZodTypeInternals<import("./v1/schema.js").RichNode, unknown>>]>>;
|
|
9
15
|
}, z.core.$strict>>;
|
|
16
|
+
export declare function generateVocabularyJSONSchema(): z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
|
|
17
|
+
connection_types: z.ZodType<import("./vocabulary.js").ConnectionTypeDeclarations, unknown, z.core.$ZodTypeInternals<import("./vocabulary.js").ConnectionTypeDeclarations, unknown>>;
|
|
18
|
+
}, z.core.$strict>>;
|
|
10
19
|
/** Read the edge from the inspected endpoint, independently of structural kind. */
|
|
11
|
-
export declare function connectionWording(type: ConnectionType | undefined, direction: "outgoing" | "incoming"): string;
|
|
20
|
+
export declare function connectionWording(type: ConnectionType | undefined, direction: "outgoing" | "incoming", presentation?: ConnectionPresentation): string;
|
|
12
21
|
type AnchorThought = {
|
|
13
22
|
readonly key: string;
|
|
14
23
|
readonly id?: string;
|
|
@@ -39,6 +48,7 @@ export type Thought = {
|
|
|
39
48
|
x: number;
|
|
40
49
|
y: number;
|
|
41
50
|
};
|
|
51
|
+
style?: NodePresentation;
|
|
42
52
|
path: string;
|
|
43
53
|
destination?: {
|
|
44
54
|
kind: "map_link" | "external_link";
|
|
@@ -51,6 +61,7 @@ export type Connection = {
|
|
|
51
61
|
to: string;
|
|
52
62
|
kind: "branch" | "cross";
|
|
53
63
|
type?: ConnectionType;
|
|
64
|
+
presentation?: ConnectionPresentation;
|
|
54
65
|
path: string;
|
|
55
66
|
};
|
|
56
67
|
export type SemanticGraph = {
|
|
@@ -66,8 +77,11 @@ export type Binding = {
|
|
|
66
77
|
export type Diagnostic = {
|
|
67
78
|
message: string;
|
|
68
79
|
range: SourceRange;
|
|
80
|
+
uri?: string;
|
|
69
81
|
};
|
|
70
82
|
export type DocumentSnapshot = {
|
|
83
|
+
vocabulary?: Record<string, ResolvedConnectionType>;
|
|
84
|
+
graphGeneration?: number;
|
|
71
85
|
attachmentId?: string;
|
|
72
86
|
uri: string;
|
|
73
87
|
revision: number;
|
|
@@ -98,6 +112,7 @@ export declare function positionToOffset(source: string, position: SourcePositio
|
|
|
98
112
|
export declare function offsetToPosition(source: string, offset: number): SourcePosition;
|
|
99
113
|
export declare class DocumentSession {
|
|
100
114
|
readonly uri: string;
|
|
115
|
+
private vocabularyInput;
|
|
101
116
|
private readonly attachmentId;
|
|
102
117
|
private text;
|
|
103
118
|
private version;
|
|
@@ -105,7 +120,8 @@ export declare class DocumentSession {
|
|
|
105
120
|
private records;
|
|
106
121
|
private validSource;
|
|
107
122
|
private serial;
|
|
108
|
-
constructor(uri: string, source?: string, revision?: number);
|
|
123
|
+
constructor(uri: string, source?: string, revision?: number, vocabularyInput?: VocabularyInput);
|
|
124
|
+
setVocabulary(input: VocabularyInput): DocumentSnapshot;
|
|
109
125
|
get source(): string;
|
|
110
126
|
get revision(): number;
|
|
111
127
|
receive(source: string, revision: number): DocumentSnapshot;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { parseNodeStyle, resolveNodeStyle, } from "./node-style.js";
|
|
2
|
+
import { resolveVocabulary, VocabularySchema, } from "./vocabulary.js";
|
|
3
|
+
export * from "./vocabulary.js";
|
|
1
4
|
import { isAlias, isMap, isScalar, parseDocument } from "yaml";
|
|
2
5
|
import { z } from "zod";
|
|
3
6
|
import { DocumentSchema, } from "./v1/schema.js";
|
|
@@ -5,8 +8,15 @@ export { ConnectionTypeSchema, DocumentSchema } from "./v1/schema.js";
|
|
|
5
8
|
export function generateJSONSchema() {
|
|
6
9
|
return z.toJSONSchema(DocumentSchema, { io: "input" });
|
|
7
10
|
}
|
|
11
|
+
export function generateVocabularyJSONSchema() {
|
|
12
|
+
return z.toJSONSchema(VocabularySchema, { io: "input" });
|
|
13
|
+
}
|
|
8
14
|
/** Read the edge from the inspected endpoint, independently of structural kind. */
|
|
9
|
-
export function connectionWording(type, direction) {
|
|
15
|
+
export function connectionWording(type, direction, presentation) {
|
|
16
|
+
if (presentation)
|
|
17
|
+
return direction === "outgoing"
|
|
18
|
+
? presentation.text
|
|
19
|
+
: presentation.reverseText;
|
|
10
20
|
switch (type) {
|
|
11
21
|
case "precedes":
|
|
12
22
|
return direction === "outgoing" ? "precedes" : "follows";
|
|
@@ -93,6 +103,7 @@ function rejectCycles(value, ancestors = new Set()) {
|
|
|
93
103
|
}
|
|
94
104
|
export class DocumentSession {
|
|
95
105
|
uri;
|
|
106
|
+
vocabularyInput;
|
|
96
107
|
attachmentId = `attachment:${++attachmentSerial}`;
|
|
97
108
|
text;
|
|
98
109
|
version;
|
|
@@ -100,8 +111,9 @@ export class DocumentSession {
|
|
|
100
111
|
records = [];
|
|
101
112
|
validSource = "";
|
|
102
113
|
serial = 0;
|
|
103
|
-
constructor(uri, source = "", revision = 0) {
|
|
114
|
+
constructor(uri, source = "", revision = 0, vocabularyInput = {}) {
|
|
104
115
|
this.uri = uri;
|
|
116
|
+
this.vocabularyInput = vocabularyInput;
|
|
105
117
|
this.text = source;
|
|
106
118
|
this.version = revision;
|
|
107
119
|
this.state = {
|
|
@@ -118,6 +130,10 @@ export class DocumentSession {
|
|
|
118
130
|
};
|
|
119
131
|
this.analyze();
|
|
120
132
|
}
|
|
133
|
+
setVocabulary(input) {
|
|
134
|
+
this.vocabularyInput = input;
|
|
135
|
+
return this.analyze();
|
|
136
|
+
}
|
|
121
137
|
get source() {
|
|
122
138
|
return this.text;
|
|
123
139
|
}
|
|
@@ -182,6 +198,36 @@ export class DocumentSession {
|
|
|
182
198
|
.sort((a, b) => b.path.length - a.path.length)[0];
|
|
183
199
|
throw Object.assign(new Error(located?.issue.message ?? z.prettifyError(result.error)), { offset: located?.offset ?? 0 });
|
|
184
200
|
}
|
|
201
|
+
const vocabulary = resolveVocabulary(this.vocabularyInput, result.data.connection_types, { source: this.text, uri: this.uri });
|
|
202
|
+
const definition = (type, offset = 0) => {
|
|
203
|
+
if (!Object.hasOwn(vocabulary, type))
|
|
204
|
+
throw Object.assign(new Error(`Unknown connection type: ${type}. Available types: ${Object.keys(vocabulary).join(", ")}`), { offset });
|
|
205
|
+
return vocabulary[type];
|
|
206
|
+
};
|
|
207
|
+
const styleRules = new Map();
|
|
208
|
+
const ruleSyntax = doc.get("node_styles", true);
|
|
209
|
+
const resolvedRules = isAlias(ruleSyntax)
|
|
210
|
+
? ruleSyntax.resolve(doc)
|
|
211
|
+
: ruleSyntax;
|
|
212
|
+
// JS objects reorder integer-looking keys. CSS rule order is the author's
|
|
213
|
+
// YAML order, not Object.entries order after structural validation.
|
|
214
|
+
const ruleNames = isMap(resolvedRules)
|
|
215
|
+
? resolvedRules.items.map((pair) => String(isScalar(pair.key) ? pair.key.value : ""))
|
|
216
|
+
: Object.keys(result.data.node_styles ?? {});
|
|
217
|
+
for (const name of ruleNames) {
|
|
218
|
+
const style = result.data.node_styles?.[name];
|
|
219
|
+
if (style === undefined)
|
|
220
|
+
continue;
|
|
221
|
+
try {
|
|
222
|
+
styleRules.set(name, parseNodeStyle(style));
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), {
|
|
226
|
+
offset: doc.getIn(["node_styles", name], true)
|
|
227
|
+
?.range?.[0] ?? 0,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
185
231
|
const records = [];
|
|
186
232
|
const edges = [];
|
|
187
233
|
const bindings = [];
|
|
@@ -237,9 +283,19 @@ export class DocumentSession {
|
|
|
237
283
|
: Array.isArray(entry)
|
|
238
284
|
? { full_text: null, connections: entry }
|
|
239
285
|
: entry;
|
|
240
|
-
const
|
|
286
|
+
const coreKind = content.type === undefined
|
|
287
|
+
? "ordinary"
|
|
288
|
+
: definition(content.type, span.start).coreKind;
|
|
289
|
+
if (!parent && content.type !== undefined && coreKind === "ordinary")
|
|
290
|
+
throw Object.assign(new Error("Ordinary relationship types are not starting thoughts"), { offset: span.start });
|
|
291
|
+
if ((coreKind === "ordinary" && content.url !== undefined) ||
|
|
292
|
+
(coreKind !== "ordinary" && content.url === undefined))
|
|
293
|
+
throw Object.assign(new Error("Invalid portal definition"), {
|
|
294
|
+
offset: span.start,
|
|
295
|
+
});
|
|
296
|
+
const destination = (coreKind === "map_link" || coreKind === "external_link") &&
|
|
241
297
|
content.url !== undefined
|
|
242
|
-
? { kind:
|
|
298
|
+
? { kind: coreKind, url: content.url }
|
|
243
299
|
: undefined;
|
|
244
300
|
const fallback = content.summary === undefined && content.full_text === undefined
|
|
245
301
|
? (destination?.url ?? "")
|
|
@@ -249,6 +305,20 @@ export class DocumentSession {
|
|
|
249
305
|
? (content.summary ?? "")
|
|
250
306
|
: (content.full_text ?? fallback).split(/\r?\n/, 1)[0];
|
|
251
307
|
const key = `thought:${++this.serial}`;
|
|
308
|
+
let style;
|
|
309
|
+
try {
|
|
310
|
+
style = resolveNodeStyle(content.classes, content.style, styleRules);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), {
|
|
314
|
+
offset: doc.getIn([
|
|
315
|
+
...path,
|
|
316
|
+
content.classes?.some((name) => !styleRules.has(name))
|
|
317
|
+
? "classes"
|
|
318
|
+
: "style",
|
|
319
|
+
], true)?.range?.[0] ?? span.start,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
252
322
|
const thought = {
|
|
253
323
|
key,
|
|
254
324
|
id: content.id,
|
|
@@ -258,6 +328,7 @@ export class DocumentSession {
|
|
|
258
328
|
position: content.position,
|
|
259
329
|
path: pathName,
|
|
260
330
|
...(destination ? { destination } : {}),
|
|
331
|
+
...(style ? { style } : {}),
|
|
261
332
|
};
|
|
262
333
|
records.push({ thought, span, ast });
|
|
263
334
|
bindings.push({ key, kind: "definition", span });
|
|
@@ -337,9 +408,10 @@ export class DocumentSession {
|
|
|
337
408
|
const to = ids.get(ref.id);
|
|
338
409
|
if (!to)
|
|
339
410
|
throw Object.assign(new Error(`${ref.path}.node_id: no node has id "${ref.id}".`), { offset: ref.span.start });
|
|
340
|
-
if (
|
|
411
|
+
if (ref.type !== undefined &&
|
|
412
|
+
definition(ref.type, ref.span.start).coreKind !== "ordinary" &&
|
|
341
413
|
records.find((r) => r.thought.key === to)?.thought.destination
|
|
342
|
-
?.kind !== ref.type)
|
|
414
|
+
?.kind !== definition(ref.type, ref.span.start).coreKind)
|
|
343
415
|
throw Object.assign(new Error(`${ref.path}.type: reference requires a ${ref.type} destination.`), { offset: ref.span.start });
|
|
344
416
|
const endpoints = `${ref.from}:${to}`;
|
|
345
417
|
const occurrence = parallelReferences.get(endpoints) ?? 0;
|
|
@@ -359,6 +431,11 @@ export class DocumentSession {
|
|
|
359
431
|
ownerKey: ref.from,
|
|
360
432
|
});
|
|
361
433
|
}
|
|
434
|
+
for (const edge of edges)
|
|
435
|
+
if (edge.type !== undefined) {
|
|
436
|
+
const { coreKind: _coreKind, ...presentation } = definition(edge.type);
|
|
437
|
+
edge.presentation = presentation;
|
|
438
|
+
}
|
|
362
439
|
this.records = records;
|
|
363
440
|
this.validSource = this.text;
|
|
364
441
|
this.state = {
|
|
@@ -367,6 +444,8 @@ export class DocumentSession {
|
|
|
367
444
|
revision: this.version,
|
|
368
445
|
analyzedRevision: this.version,
|
|
369
446
|
graphRevision: this.version,
|
|
447
|
+
graphGeneration: (this.state.graphGeneration ?? 0) + 1,
|
|
448
|
+
vocabulary,
|
|
370
449
|
valid: true,
|
|
371
450
|
pending: false,
|
|
372
451
|
graph: { nodes: records.map((r) => r.thought), edges },
|
|
@@ -378,6 +457,8 @@ export class DocumentSession {
|
|
|
378
457
|
const offset = typeof error === "object" && error !== null && "offset" in error
|
|
379
458
|
? Number(error.offset)
|
|
380
459
|
: 0;
|
|
460
|
+
const context = error;
|
|
461
|
+
const diagnosticSource = context?.source ?? this.text;
|
|
381
462
|
this.state = {
|
|
382
463
|
...this.state,
|
|
383
464
|
analyzedRevision: this.version,
|
|
@@ -387,7 +468,11 @@ export class DocumentSession {
|
|
|
387
468
|
diagnostics: [
|
|
388
469
|
{
|
|
389
470
|
message: error instanceof Error ? error.message : String(error),
|
|
390
|
-
range:
|
|
471
|
+
range: {
|
|
472
|
+
start: offsetToPosition(diagnosticSource, offset),
|
|
473
|
+
end: offsetToPosition(diagnosticSource, offset),
|
|
474
|
+
},
|
|
475
|
+
...(context?.uri ? { uri: context.uri } : {}),
|
|
391
476
|
},
|
|
392
477
|
],
|
|
393
478
|
};
|
|
@@ -490,7 +575,7 @@ export class DocumentSession {
|
|
|
490
575
|
source.slice(0, change.start) +
|
|
491
576
|
change.newText +
|
|
492
577
|
source.slice(change.end);
|
|
493
|
-
const candidate = new DocumentSession(this.uri, source, this.version);
|
|
578
|
+
const candidate = new DocumentSession(this.uri, source, this.version, this.vocabularyInput);
|
|
494
579
|
if (!candidate.snapshot().valid)
|
|
495
580
|
throw new ContentEditError("invalid-edit", "Proposed content does not form a valid document.");
|
|
496
581
|
return {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export type NodeStyleDeclaration = {
|
|
3
|
+
property: string;
|
|
4
|
+
value: string;
|
|
5
|
+
important: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type NodePresentation = {
|
|
8
|
+
declarations: NodeStyleDeclaration[];
|
|
9
|
+
scale?: number | "auto";
|
|
10
|
+
icon?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const NodeStyleSchema: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>]>;
|
|
13
|
+
export type NodeStyle = z.infer<typeof NodeStyleSchema>;
|
|
14
|
+
/** Parse inert declarations only; never return authored stylesheet text. */
|
|
15
|
+
export declare function parseNodeStyle(input: NodeStyle): NodeStyleDeclaration[];
|
|
16
|
+
/** Low-to-high cascade order preserves shorthand interactions for browser application. */
|
|
17
|
+
export declare function resolveNodeStyle(classes: string[] | undefined, inline: NodeStyle | undefined, rules: Map<string, NodeStyleDeclaration[]>): NodePresentation | undefined;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as css from "css-tree";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const NodeStyleSchema = z.union([
|
|
4
|
+
z.string(),
|
|
5
|
+
z.record(z.string(), z.union([z.string(), z.number().finite()])),
|
|
6
|
+
]);
|
|
7
|
+
const properties = new Set([
|
|
8
|
+
"color",
|
|
9
|
+
"background-color",
|
|
10
|
+
"font-family",
|
|
11
|
+
"font-size",
|
|
12
|
+
"font-weight",
|
|
13
|
+
"font-style",
|
|
14
|
+
"line-height",
|
|
15
|
+
"border",
|
|
16
|
+
"border-color",
|
|
17
|
+
"border-width",
|
|
18
|
+
"border-style",
|
|
19
|
+
"padding",
|
|
20
|
+
...["top", "right", "bottom", "left"].flatMap((side) => [
|
|
21
|
+
`padding-${side}`,
|
|
22
|
+
`border-${side}`,
|
|
23
|
+
`border-${side}-color`,
|
|
24
|
+
`border-${side}-width`,
|
|
25
|
+
`border-${side}-style`,
|
|
26
|
+
]),
|
|
27
|
+
]);
|
|
28
|
+
const functions = new Set([
|
|
29
|
+
"rgb",
|
|
30
|
+
"rgba",
|
|
31
|
+
"hsl",
|
|
32
|
+
"hsla",
|
|
33
|
+
"hwb",
|
|
34
|
+
"lab",
|
|
35
|
+
"lch",
|
|
36
|
+
"oklab",
|
|
37
|
+
"oklch",
|
|
38
|
+
"color",
|
|
39
|
+
"color-mix",
|
|
40
|
+
"light-dark",
|
|
41
|
+
]);
|
|
42
|
+
function fail(message) {
|
|
43
|
+
throw new Error(`Node style: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
const unsafeText = (text) => [...text].some((char) => char === "<" || char === ">" || char.charCodeAt(0) < 32);
|
|
46
|
+
/** Parse inert declarations only; never return authored stylesheet text. */
|
|
47
|
+
export function parseNodeStyle(input) {
|
|
48
|
+
const parse = (text) => {
|
|
49
|
+
const ast = css.parse(text, {
|
|
50
|
+
context: "declarationList",
|
|
51
|
+
onParseError: (error) => {
|
|
52
|
+
throw error;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const out = [];
|
|
56
|
+
if (ast.type !== "DeclarationList")
|
|
57
|
+
return fail("Expected declarations");
|
|
58
|
+
ast.children.forEach((node) => {
|
|
59
|
+
if (node.type !== "Declaration")
|
|
60
|
+
fail("Only declarations are supported");
|
|
61
|
+
const decodedProperty = css.ident.decode(node.property);
|
|
62
|
+
const property = decodedProperty.startsWith("--")
|
|
63
|
+
? decodedProperty
|
|
64
|
+
: decodedProperty.toLowerCase();
|
|
65
|
+
if (!properties.has(property) &&
|
|
66
|
+
property !== "--lensmap-scale" &&
|
|
67
|
+
property !== "--lensmap-icon")
|
|
68
|
+
fail(`Unsupported property ${property}`);
|
|
69
|
+
const valueText = css.generate(node.value).trim();
|
|
70
|
+
const value = css.parse(valueText, {
|
|
71
|
+
context: "value",
|
|
72
|
+
onParseError: (error) => {
|
|
73
|
+
throw error;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
css.walk(value, (token) => {
|
|
77
|
+
if (token.type === "Raw" ||
|
|
78
|
+
token.type === "Url" ||
|
|
79
|
+
(token.type === "Function" &&
|
|
80
|
+
!functions.has(css.ident.decode(token.name).toLowerCase())))
|
|
81
|
+
fail(`Unsafe or unsupported value for ${property}`);
|
|
82
|
+
if (token.type === "String" && unsafeText(token.value))
|
|
83
|
+
fail("Unsafe text");
|
|
84
|
+
if (token.type === "Identifier" &&
|
|
85
|
+
/^(inherit|initial|unset|revert|revert-layer|infinity|-infinity|nan)$/i.test(css.ident.decode(token.name)))
|
|
86
|
+
fail("CSS-wide keywords are unsupported");
|
|
87
|
+
if ((token.type === "Number" ||
|
|
88
|
+
token.type === "Dimension" ||
|
|
89
|
+
token.type === "Percentage") &&
|
|
90
|
+
!Number.isFinite(Number(token.value)))
|
|
91
|
+
fail("Lengths and numbers must be finite");
|
|
92
|
+
});
|
|
93
|
+
if (property === "--lensmap-scale") {
|
|
94
|
+
const scalar = value.type === "Value" ? value.children.first : undefined;
|
|
95
|
+
if (valueText !== "auto" &&
|
|
96
|
+
(value.type !== "Value" ||
|
|
97
|
+
value.children.size !== 1 ||
|
|
98
|
+
scalar?.type !== "Number" ||
|
|
99
|
+
!Number.isFinite(Number(scalar.value)) ||
|
|
100
|
+
Number(scalar.value) <= 0))
|
|
101
|
+
fail("Scale must be positive or auto");
|
|
102
|
+
}
|
|
103
|
+
else if (property === "--lensmap-icon") {
|
|
104
|
+
if (value.type !== "Value" ||
|
|
105
|
+
value.children.size !== 1 ||
|
|
106
|
+
value.children.first?.type !== "String")
|
|
107
|
+
fail("Icon must be a CSS string");
|
|
108
|
+
const first = value.type === "Value" ? value.children.first : undefined;
|
|
109
|
+
if (first?.type === "String" && unsafeText(first.value))
|
|
110
|
+
fail("Unsafe icon text");
|
|
111
|
+
}
|
|
112
|
+
else if (css.lexer.matchProperty(property, value).error)
|
|
113
|
+
fail(`Invalid value for ${property}`);
|
|
114
|
+
out.push({
|
|
115
|
+
property,
|
|
116
|
+
value: valueText,
|
|
117
|
+
important: Boolean(node.important),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return out;
|
|
121
|
+
};
|
|
122
|
+
if (typeof input === "string")
|
|
123
|
+
return parse(input);
|
|
124
|
+
return Object.entries(input).flatMap(([property, value]) => {
|
|
125
|
+
if (!/^(?:--lensmap-(?:scale|icon)|[a-z-]+)$/i.test(property))
|
|
126
|
+
return fail(`Invalid property ${property}`);
|
|
127
|
+
const text = property === "--lensmap-icon"
|
|
128
|
+
? css.generate({ type: "String", value: String(value) })
|
|
129
|
+
: String(value);
|
|
130
|
+
const declarations = parse(`${property}:${text}`);
|
|
131
|
+
if (declarations.length !== 1 ||
|
|
132
|
+
declarations[0].property !==
|
|
133
|
+
(property.startsWith("--") ? property : property.toLowerCase()))
|
|
134
|
+
fail("Mapping values must contain one declaration value");
|
|
135
|
+
return declarations;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Low-to-high cascade order preserves shorthand interactions for browser application. */
|
|
139
|
+
export function resolveNodeStyle(classes, inline, rules) {
|
|
140
|
+
for (const name of classes ?? [])
|
|
141
|
+
if (!rules.has(name))
|
|
142
|
+
fail(`Unknown class ${name}`);
|
|
143
|
+
const selected = new Set(classes);
|
|
144
|
+
const declarations = [...rules]
|
|
145
|
+
.filter(([name]) => selected.has(name))
|
|
146
|
+
.flatMap(([, values]) => values);
|
|
147
|
+
declarations.push(...(inline === undefined ? [] : parseNodeStyle(inline)));
|
|
148
|
+
declarations.sort((a, b) => Number(a.important) - Number(b.important));
|
|
149
|
+
if (!declarations.length)
|
|
150
|
+
return undefined;
|
|
151
|
+
const presentation = { declarations: [] };
|
|
152
|
+
for (const declaration of declarations) {
|
|
153
|
+
if (declaration.property === "--lensmap-scale")
|
|
154
|
+
presentation.scale =
|
|
155
|
+
declaration.value === "auto" ? "auto" : Number(declaration.value);
|
|
156
|
+
else if (declaration.property === "--lensmap-icon") {
|
|
157
|
+
const value = css.parse(declaration.value, { context: "value" });
|
|
158
|
+
if (value.type === "Value" && value.children.first?.type === "String")
|
|
159
|
+
presentation.icon = value.children.first.value;
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
presentation.declarations.push(declaration);
|
|
163
|
+
}
|
|
164
|
+
return presentation;
|
|
165
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type VocabularyInput } from "./vocabulary.js";
|
|
2
|
+
/** Root-bounded Node adapter. The browser entry deliberately never imports it. */
|
|
3
|
+
export declare function readProjectVocabulary(root: string, buffers?: ReadonlyMap<string, string>): Promise<{
|
|
4
|
+
input: VocabularyInput;
|
|
5
|
+
files: string[];
|
|
6
|
+
}>;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { isScalar, parseDocument } from "yaml";
|
|
5
|
+
import { parseVocabulary } from "./vocabulary.js";
|
|
6
|
+
/** Root-bounded Node adapter. The browser entry deliberately never imports it. */
|
|
7
|
+
export async function readProjectVocabulary(root, buffers = new Map()) {
|
|
8
|
+
const config = resolve(root, "lensmap.config.json");
|
|
9
|
+
const files = [config];
|
|
10
|
+
let source;
|
|
11
|
+
let offset = 0;
|
|
12
|
+
try {
|
|
13
|
+
const canonicalRoot = await realpath(root);
|
|
14
|
+
const inside = (path) => {
|
|
15
|
+
const rel = relative(canonicalRoot, path);
|
|
16
|
+
return (rel === "" ||
|
|
17
|
+
(!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)));
|
|
18
|
+
};
|
|
19
|
+
const authorize = async (path) => {
|
|
20
|
+
let ancestor = path;
|
|
21
|
+
for (;;) {
|
|
22
|
+
try {
|
|
23
|
+
await lstat(ancestor);
|
|
24
|
+
const canonical = await realpath(ancestor);
|
|
25
|
+
if (!inside(canonical))
|
|
26
|
+
throw new Error("Vocabulary path escapes project root");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== "ENOENT")
|
|
31
|
+
throw error;
|
|
32
|
+
const parent = dirname(ancestor);
|
|
33
|
+
if (parent === ancestor)
|
|
34
|
+
throw error;
|
|
35
|
+
ancestor = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
await authorize(config);
|
|
40
|
+
source = buffers.get(config);
|
|
41
|
+
if (source === undefined) {
|
|
42
|
+
try {
|
|
43
|
+
source = await readFile(config, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === "ENOENT")
|
|
47
|
+
return { input: {}, files };
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// JSON.parse enforces JSON syntax; YAML's JSON mode additionally detects duplicate keys.
|
|
52
|
+
const json = JSON.parse(source);
|
|
53
|
+
const parsed = parseDocument(source, { schema: "json", uniqueKeys: true });
|
|
54
|
+
if (parsed.errors.length)
|
|
55
|
+
throw new Error(parsed.errors[0].message);
|
|
56
|
+
if (!json ||
|
|
57
|
+
typeof json !== "object" ||
|
|
58
|
+
Array.isArray(json) ||
|
|
59
|
+
Object.keys(json).length !== 1 ||
|
|
60
|
+
!Object.hasOwn(json, "types") ||
|
|
61
|
+
typeof json.types !== "string" ||
|
|
62
|
+
!json.types.trim())
|
|
63
|
+
throw new Error("Config requires exactly one nonblank types path");
|
|
64
|
+
const field = parsed.get("types", true);
|
|
65
|
+
if (isScalar(field))
|
|
66
|
+
offset = field.range?.[0] ?? 0;
|
|
67
|
+
const target = resolve(dirname(config), json.types);
|
|
68
|
+
const lexical = relative(resolve(root), target);
|
|
69
|
+
if (lexical === ".." ||
|
|
70
|
+
lexical.startsWith(`..${sep}`) ||
|
|
71
|
+
isAbsolute(lexical))
|
|
72
|
+
throw new Error("Vocabulary path escapes project root");
|
|
73
|
+
await authorize(target);
|
|
74
|
+
if (!target.endsWith(".lenstypes"))
|
|
75
|
+
throw new Error("Vocabulary must be a .lenstypes file");
|
|
76
|
+
files.push(target);
|
|
77
|
+
const text = buffers.get(target) ?? (await readFile(target, "utf8"));
|
|
78
|
+
return { input: parseVocabulary(text, pathToFileURL(target).href), files };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
input: {
|
|
83
|
+
uri: pathToFileURL(config).href,
|
|
84
|
+
source,
|
|
85
|
+
error: error instanceof Error ? error.message : String(error),
|
|
86
|
+
offset,
|
|
87
|
+
},
|
|
88
|
+
files,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/v1/schema.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { ConnectionTypeDeclarations } from "../vocabulary.js";
|
|
2
|
+
export { ConnectionTypeDeclarationSchema, ConnectionTypeDeclarationsSchema, } from "../vocabulary.js";
|
|
1
3
|
import { z } from "zod";
|
|
2
4
|
/** Experimental v1 subset. Fields may be added or refined before v1 stabilizes. */
|
|
3
5
|
export declare const PositionSchema: z.ZodObject<{
|
|
@@ -13,18 +15,10 @@ export declare const ConnectionTypeSchema: z.ZodEnum<{
|
|
|
13
15
|
precedes: "precedes";
|
|
14
16
|
prevents: "prevents";
|
|
15
17
|
}>;
|
|
16
|
-
export type ConnectionType =
|
|
18
|
+
export type ConnectionType = string;
|
|
17
19
|
export declare const ReferenceSchema: z.ZodObject<{
|
|
18
20
|
node_id: z.ZodString;
|
|
19
|
-
type: z.ZodOptional<z.
|
|
20
|
-
connected: "connected";
|
|
21
|
-
external_link: "external_link";
|
|
22
|
-
follows: "follows";
|
|
23
|
-
map_link: "map_link";
|
|
24
|
-
optional: "optional";
|
|
25
|
-
precedes: "precedes";
|
|
26
|
-
prevents: "prevents";
|
|
27
|
-
}>>;
|
|
21
|
+
type: z.ZodOptional<z.ZodString>;
|
|
28
22
|
}, z.core.$strict>;
|
|
29
23
|
declare const NodeContentSchema: z.ZodObject<{
|
|
30
24
|
id: z.ZodOptional<z.ZodString>;
|
|
@@ -34,6 +28,8 @@ declare const NodeContentSchema: z.ZodObject<{
|
|
|
34
28
|
x: z.ZodNumber;
|
|
35
29
|
y: z.ZodNumber;
|
|
36
30
|
}, z.core.$strict>>;
|
|
31
|
+
classes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
32
|
+
style: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>]>>;
|
|
37
33
|
}, z.core.$strict>;
|
|
38
34
|
export type Reference = z.infer<typeof ReferenceSchema>;
|
|
39
35
|
export type RichNode = z.infer<typeof NodeContentSchema> & {
|
|
@@ -48,7 +44,8 @@ export declare const NodeSchema: z.ZodUnion<readonly [z.ZodString, z.ZodType<Ric
|
|
|
48
44
|
export declare const DocumentSchema: z.ZodObject<{
|
|
49
45
|
version: z.ZodLiteral<"v1">;
|
|
50
46
|
frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
47
|
+
connection_types: z.ZodOptional<z.ZodType<import("../vocabulary.js").ConnectionTypeDeclarations, unknown, z.core.$ZodTypeInternals<import("../vocabulary.js").ConnectionTypeDeclarations, unknown>>>;
|
|
48
|
+
node_styles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>]>>>;
|
|
51
49
|
nodes: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodType<RichNode, unknown, z.core.$ZodTypeInternals<RichNode, unknown>>]>>;
|
|
52
50
|
}, z.core.$strict>;
|
|
53
51
|
export type LensmapDocument = z.infer<typeof DocumentSchema>;
|
|
54
|
-
export {};
|
package/dist/v1/schema.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { NodeStyleSchema } from "../node-style.js";
|
|
2
|
+
import { ConnectionTypeDeclarationsSchema } from "../vocabulary.js";
|
|
3
|
+
export { ConnectionTypeDeclarationSchema, ConnectionTypeDeclarationsSchema, } from "../vocabulary.js";
|
|
1
4
|
import { z } from "zod";
|
|
2
5
|
/** Experimental v1 subset. Fields may be added or refined before v1 stabilizes. */
|
|
3
6
|
export const PositionSchema = z.strictObject({
|
|
@@ -14,26 +17,27 @@ export const ConnectionTypeSchema = z.enum([
|
|
|
14
17
|
"external_link",
|
|
15
18
|
"map_link",
|
|
16
19
|
]);
|
|
17
|
-
const
|
|
18
|
-
"connected",
|
|
19
|
-
"precedes",
|
|
20
|
-
"follows",
|
|
21
|
-
"prevents",
|
|
22
|
-
"optional",
|
|
23
|
-
]);
|
|
20
|
+
const AuthoredTypeSchema = z.string().regex(/\S/);
|
|
24
21
|
export const ReferenceSchema = z.strictObject({
|
|
25
22
|
node_id: IdSchema,
|
|
26
|
-
type:
|
|
23
|
+
type: AuthoredTypeSchema.optional(),
|
|
27
24
|
});
|
|
28
25
|
const NodeContentSchema = z.strictObject({
|
|
29
26
|
id: IdSchema.optional(),
|
|
30
27
|
summary: z.string().nullable().optional(),
|
|
31
28
|
full_text: z.string().nullable().optional(),
|
|
32
29
|
position: PositionSchema.optional(),
|
|
30
|
+
classes: z.array(z.string()).optional(),
|
|
31
|
+
style: NodeStyleSchema.optional(),
|
|
33
32
|
});
|
|
34
33
|
const ordinary = (typed) => {
|
|
35
34
|
const base = typed
|
|
36
|
-
? NodeContentSchema.extend({
|
|
35
|
+
? NodeContentSchema.extend({
|
|
36
|
+
type: z
|
|
37
|
+
.string()
|
|
38
|
+
.regex(/^(?!(?:map_link|external_link)$).*\S/s)
|
|
39
|
+
.optional(),
|
|
40
|
+
})
|
|
37
41
|
: NodeContentSchema;
|
|
38
42
|
return z.union([
|
|
39
43
|
base.extend({
|
|
@@ -47,7 +51,7 @@ const ordinary = (typed) => {
|
|
|
47
51
|
]);
|
|
48
52
|
};
|
|
49
53
|
const portal = () => NodeContentSchema.extend({
|
|
50
|
-
type:
|
|
54
|
+
type: AuthoredTypeSchema,
|
|
51
55
|
url: z.string().regex(/\S/, "A portal URL must not be blank."),
|
|
52
56
|
connections: z.array(ConnectionSchema).optional(),
|
|
53
57
|
});
|
|
@@ -63,5 +67,7 @@ export const NodeSchema = z.union([z.string(), RootNodeSchema]);
|
|
|
63
67
|
export const DocumentSchema = z.strictObject({
|
|
64
68
|
version: z.literal("v1"),
|
|
65
69
|
frontmatter: z.record(z.string(), z.unknown()).optional(),
|
|
70
|
+
connection_types: ConnectionTypeDeclarationsSchema.optional(),
|
|
71
|
+
node_styles: z.record(z.string(), NodeStyleSchema).optional(),
|
|
66
72
|
nodes: z.array(NodeSchema),
|
|
67
73
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export interface ConnectionPresentation {
|
|
3
|
+
strokeWidth: number | string;
|
|
4
|
+
lineStyle: "solid" | "dashed" | "dotted";
|
|
5
|
+
lineColor: string;
|
|
6
|
+
startMarker: "none" | "arrow" | "bar" | "circle" | "diamond";
|
|
7
|
+
endMarker: "none" | "arrow" | "bar" | "circle" | "diamond";
|
|
8
|
+
text: string;
|
|
9
|
+
reverseText: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ResolvedConnectionType extends ConnectionPresentation {
|
|
12
|
+
coreKind: "ordinary" | "map_link" | "external_link";
|
|
13
|
+
}
|
|
14
|
+
export declare const ConnectionTypeDeclarationSchema: z.ZodObject<{
|
|
15
|
+
extends: z.ZodOptional<z.ZodString>;
|
|
16
|
+
"stroke-width": z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
17
|
+
"line-style": z.ZodOptional<z.ZodEnum<{
|
|
18
|
+
dashed: "dashed";
|
|
19
|
+
dotted: "dotted";
|
|
20
|
+
solid: "solid";
|
|
21
|
+
}>>;
|
|
22
|
+
"line-color": z.ZodOptional<z.ZodString>;
|
|
23
|
+
"start-marker": z.ZodOptional<z.ZodEnum<{
|
|
24
|
+
arrow: "arrow";
|
|
25
|
+
bar: "bar";
|
|
26
|
+
circle: "circle";
|
|
27
|
+
diamond: "diamond";
|
|
28
|
+
none: "none";
|
|
29
|
+
}>>;
|
|
30
|
+
"end-marker": z.ZodOptional<z.ZodEnum<{
|
|
31
|
+
arrow: "arrow";
|
|
32
|
+
bar: "bar";
|
|
33
|
+
circle: "circle";
|
|
34
|
+
diamond: "diamond";
|
|
35
|
+
none: "none";
|
|
36
|
+
}>>;
|
|
37
|
+
text: z.ZodOptional<z.ZodString>;
|
|
38
|
+
reverse_text: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
export type ConnectionTypeDeclaration = z.infer<typeof ConnectionTypeDeclarationSchema>;
|
|
41
|
+
export type ConnectionTypeDeclarations = Record<string, z.infer<typeof ConnectionTypeDeclarationSchema>>;
|
|
42
|
+
export declare const ConnectionTypeDeclarationsSchema: z.ZodType<ConnectionTypeDeclarations>;
|
|
43
|
+
export interface VocabularyInput {
|
|
44
|
+
declarations?: ConnectionTypeDeclarations;
|
|
45
|
+
source?: string;
|
|
46
|
+
uri?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
offset?: number;
|
|
49
|
+
}
|
|
50
|
+
export declare const VocabularySchema: z.ZodObject<{
|
|
51
|
+
connection_types: z.ZodType<ConnectionTypeDeclarations, unknown, z.core.$ZodTypeInternals<ConnectionTypeDeclarations, unknown>>;
|
|
52
|
+
}, z.core.$strict>;
|
|
53
|
+
export declare function parseVocabulary(source: string, uri?: string): VocabularyInput;
|
|
54
|
+
export declare const builtinVocabulary: Record<string, ResolvedConnectionType>;
|
|
55
|
+
export declare function resolveVocabulary(shared?: VocabularyInput, local?: ConnectionTypeDeclarations, localContext?: VocabularyInput): Record<string, ResolvedConnectionType>;
|
|
56
|
+
/** Source-only provenance for host hover/diagnostics; never embedded in edges. */
|
|
57
|
+
export declare function vocabularyLocations(source: string, uri?: string): Record<string, {
|
|
58
|
+
uri?: string;
|
|
59
|
+
name: {
|
|
60
|
+
start: number;
|
|
61
|
+
end: number;
|
|
62
|
+
};
|
|
63
|
+
parent?: {
|
|
64
|
+
start: number;
|
|
65
|
+
end: number;
|
|
66
|
+
};
|
|
67
|
+
}>;
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { ident, lexer, parse, walk } from "css-tree";
|
|
2
|
+
import { parseDocument } from "yaml";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
function productDimension(a, b, divide) {
|
|
5
|
+
const powers = new Map();
|
|
6
|
+
for (const [dimension, sign] of [
|
|
7
|
+
[a, 1],
|
|
8
|
+
[b, divide ? -1 : 1],
|
|
9
|
+
]) {
|
|
10
|
+
if (dimension === "number")
|
|
11
|
+
continue;
|
|
12
|
+
for (const term of dimension.split("*")) {
|
|
13
|
+
const [unit, exponent] = term.split("^");
|
|
14
|
+
powers.set(unit, (powers.get(unit) ?? 0) + sign * Number(exponent ?? 1));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return ([...powers]
|
|
18
|
+
.filter(([, power]) => power !== 0)
|
|
19
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
20
|
+
.map(([unit, power]) => (power === 1 ? unit : `${unit}^${power}`))
|
|
21
|
+
.join("*") || "number");
|
|
22
|
+
}
|
|
23
|
+
function mathDimension(node, percentageHint = "length") {
|
|
24
|
+
const fail = () => {
|
|
25
|
+
throw new Error("Invalid CSS math dimensions");
|
|
26
|
+
};
|
|
27
|
+
const compatible = (a, b) => (a === b ? a : fail());
|
|
28
|
+
const expression = (nodes) => {
|
|
29
|
+
if (!nodes.length || nodes.length % 2 === 0)
|
|
30
|
+
return fail();
|
|
31
|
+
const values = [];
|
|
32
|
+
let current = mathDimension(nodes[0], percentageHint);
|
|
33
|
+
for (let i = 1; i < nodes.length; i += 2) {
|
|
34
|
+
const operator = nodes[i];
|
|
35
|
+
if (operator.type !== "Operator")
|
|
36
|
+
return fail();
|
|
37
|
+
const op = operator.value.trim();
|
|
38
|
+
const next = mathDimension(nodes[i + 1], percentageHint);
|
|
39
|
+
if (op === "*" || op === "/") {
|
|
40
|
+
current = productDimension(current, next, op === "/");
|
|
41
|
+
}
|
|
42
|
+
else if (op === "+" || op === "-") {
|
|
43
|
+
values.push(current);
|
|
44
|
+
current = next;
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
return fail();
|
|
48
|
+
}
|
|
49
|
+
values.push(current);
|
|
50
|
+
return values.reduce(compatible);
|
|
51
|
+
};
|
|
52
|
+
if (node.type === "Number") {
|
|
53
|
+
if (!Number.isFinite(Number(node.value)))
|
|
54
|
+
return fail();
|
|
55
|
+
return "number";
|
|
56
|
+
}
|
|
57
|
+
if (node.type === "Percentage") {
|
|
58
|
+
if (!Number.isFinite(Number(node.value)))
|
|
59
|
+
return fail();
|
|
60
|
+
return percentageHint;
|
|
61
|
+
}
|
|
62
|
+
if (node.type === "Dimension") {
|
|
63
|
+
if (!Number.isFinite(Number(node.value)))
|
|
64
|
+
return fail();
|
|
65
|
+
for (const unit of [
|
|
66
|
+
"length",
|
|
67
|
+
"angle",
|
|
68
|
+
"time",
|
|
69
|
+
"frequency",
|
|
70
|
+
"resolution",
|
|
71
|
+
"flex",
|
|
72
|
+
])
|
|
73
|
+
if (lexer.matchType(unit, node).matched)
|
|
74
|
+
return unit;
|
|
75
|
+
return fail();
|
|
76
|
+
}
|
|
77
|
+
if (node.type === "Identifier") {
|
|
78
|
+
if (["e", "pi"].includes(ident.decode(node.name).toLowerCase()))
|
|
79
|
+
return "number";
|
|
80
|
+
return fail();
|
|
81
|
+
}
|
|
82
|
+
if (node.type === "Value" || node.type === "Parentheses")
|
|
83
|
+
return expression(node.children.toArray());
|
|
84
|
+
if (node.type !== "Function")
|
|
85
|
+
return fail();
|
|
86
|
+
const groups = [[]];
|
|
87
|
+
for (const child of node.children) {
|
|
88
|
+
if (child.type === "Operator" && child.value.trim() === ",")
|
|
89
|
+
groups.push([]);
|
|
90
|
+
else
|
|
91
|
+
groups[groups.length - 1].push(child);
|
|
92
|
+
}
|
|
93
|
+
const fn = ident.decode(node.name).toLowerCase();
|
|
94
|
+
if (fn === "round" &&
|
|
95
|
+
groups[0].length === 1 &&
|
|
96
|
+
groups[0][0].type === "Identifier" &&
|
|
97
|
+
["nearest", "up", "down", "to-zero"].includes(groups[0][0].name.toLowerCase()))
|
|
98
|
+
groups.shift();
|
|
99
|
+
const args = groups.map(expression);
|
|
100
|
+
const arity = (min, max = min) => {
|
|
101
|
+
if (args.length < min || args.length > max)
|
|
102
|
+
fail();
|
|
103
|
+
};
|
|
104
|
+
if (fn === "calc") {
|
|
105
|
+
arity(1);
|
|
106
|
+
return args[0];
|
|
107
|
+
}
|
|
108
|
+
if (["min", "max", "hypot"].includes(fn)) {
|
|
109
|
+
arity(1, Infinity);
|
|
110
|
+
return args.reduce(compatible);
|
|
111
|
+
}
|
|
112
|
+
if (fn === "clamp") {
|
|
113
|
+
arity(3);
|
|
114
|
+
return args.reduce(compatible);
|
|
115
|
+
}
|
|
116
|
+
if (["mod", "rem"].includes(fn)) {
|
|
117
|
+
arity(2);
|
|
118
|
+
return args.reduce(compatible);
|
|
119
|
+
}
|
|
120
|
+
if (fn === "round") {
|
|
121
|
+
arity(1, 2);
|
|
122
|
+
if (args.length === 1 && args[0] !== "number")
|
|
123
|
+
return fail();
|
|
124
|
+
return args.reduce(compatible);
|
|
125
|
+
}
|
|
126
|
+
if (fn === "abs") {
|
|
127
|
+
arity(1);
|
|
128
|
+
return args[0];
|
|
129
|
+
}
|
|
130
|
+
if (fn === "sign") {
|
|
131
|
+
arity(1);
|
|
132
|
+
return "number";
|
|
133
|
+
}
|
|
134
|
+
if (["sin", "cos", "tan"].includes(fn)) {
|
|
135
|
+
arity(1);
|
|
136
|
+
if (!["number", "angle"].includes(args[0]))
|
|
137
|
+
return fail();
|
|
138
|
+
return "number";
|
|
139
|
+
}
|
|
140
|
+
if (["asin", "acos", "atan"].includes(fn)) {
|
|
141
|
+
arity(1);
|
|
142
|
+
if (args[0] !== "number")
|
|
143
|
+
return fail();
|
|
144
|
+
return "angle";
|
|
145
|
+
}
|
|
146
|
+
if (fn === "atan2") {
|
|
147
|
+
arity(2);
|
|
148
|
+
args.reduce(compatible);
|
|
149
|
+
return "angle";
|
|
150
|
+
}
|
|
151
|
+
if (["pow", "sqrt", "log", "exp"].includes(fn)) {
|
|
152
|
+
arity(fn === "pow" ? 2 : 1, fn === "log" ? 2 : fn === "pow" ? 2 : 1);
|
|
153
|
+
if (args.some((a) => a !== "number"))
|
|
154
|
+
return fail();
|
|
155
|
+
return "number";
|
|
156
|
+
}
|
|
157
|
+
return fail();
|
|
158
|
+
}
|
|
159
|
+
function css(value, property) {
|
|
160
|
+
try {
|
|
161
|
+
const ast = parse(value, { context: "value" });
|
|
162
|
+
let safe = true;
|
|
163
|
+
walk(ast, (node) => {
|
|
164
|
+
if (property === "color" && node.type === "Function") {
|
|
165
|
+
const fn = ident.decode(node.name).toLowerCase();
|
|
166
|
+
if ([
|
|
167
|
+
"rgb",
|
|
168
|
+
"rgba",
|
|
169
|
+
"hsl",
|
|
170
|
+
"hsla",
|
|
171
|
+
"hwb",
|
|
172
|
+
"lab",
|
|
173
|
+
"lch",
|
|
174
|
+
"oklab",
|
|
175
|
+
"oklch",
|
|
176
|
+
"color",
|
|
177
|
+
"device-cmyk",
|
|
178
|
+
].includes(fn)) {
|
|
179
|
+
const channels = node.children
|
|
180
|
+
.toArray()
|
|
181
|
+
.filter((child) => child.type !== "Operator");
|
|
182
|
+
const hue = ["hsl", "hsla", "hwb"].includes(fn)
|
|
183
|
+
? 0
|
|
184
|
+
: ["lch", "oklch"].includes(fn)
|
|
185
|
+
? 2
|
|
186
|
+
: -1;
|
|
187
|
+
channels.forEach((channel, index) => {
|
|
188
|
+
if (channel.type === "Function" &&
|
|
189
|
+
[
|
|
190
|
+
"calc",
|
|
191
|
+
"min",
|
|
192
|
+
"max",
|
|
193
|
+
"clamp",
|
|
194
|
+
"round",
|
|
195
|
+
"mod",
|
|
196
|
+
"rem",
|
|
197
|
+
"abs",
|
|
198
|
+
"sign",
|
|
199
|
+
"sin",
|
|
200
|
+
"cos",
|
|
201
|
+
"tan",
|
|
202
|
+
"asin",
|
|
203
|
+
"acos",
|
|
204
|
+
"atan",
|
|
205
|
+
"atan2",
|
|
206
|
+
"pow",
|
|
207
|
+
"sqrt",
|
|
208
|
+
"hypot",
|
|
209
|
+
"log",
|
|
210
|
+
"exp",
|
|
211
|
+
].includes(ident.decode(channel.name).toLowerCase())) {
|
|
212
|
+
const dimension = mathDimension(channel, index === hue ? "percentage" : "number");
|
|
213
|
+
if (index === hue
|
|
214
|
+
? !["number", "angle"].includes(dimension)
|
|
215
|
+
: dimension !== "number")
|
|
216
|
+
safe = false;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (property === "color" &&
|
|
222
|
+
node.type === "Function" &&
|
|
223
|
+
[
|
|
224
|
+
"calc",
|
|
225
|
+
"min",
|
|
226
|
+
"max",
|
|
227
|
+
"clamp",
|
|
228
|
+
"round",
|
|
229
|
+
"mod",
|
|
230
|
+
"rem",
|
|
231
|
+
"abs",
|
|
232
|
+
"sign",
|
|
233
|
+
"sin",
|
|
234
|
+
"cos",
|
|
235
|
+
"tan",
|
|
236
|
+
"asin",
|
|
237
|
+
"acos",
|
|
238
|
+
"atan",
|
|
239
|
+
"atan2",
|
|
240
|
+
"pow",
|
|
241
|
+
"sqrt",
|
|
242
|
+
"hypot",
|
|
243
|
+
"log",
|
|
244
|
+
"exp",
|
|
245
|
+
].includes(ident.decode(node.name).toLowerCase()))
|
|
246
|
+
mathDimension(node, "number");
|
|
247
|
+
if (node.type === "Raw" || node.type === "Url")
|
|
248
|
+
safe = false;
|
|
249
|
+
if (node.type === "Function" &&
|
|
250
|
+
["var", "env", "attr", "url", "paint"].includes(ident.decode(node.name).toLowerCase()))
|
|
251
|
+
safe = false;
|
|
252
|
+
if (node.type === "Identifier" &&
|
|
253
|
+
(/^(inherit|initial|unset|revert|revert-layer|infinity|-infinity|nan)$/i.test(node.name) ||
|
|
254
|
+
ident.decode(node.name).startsWith("--")))
|
|
255
|
+
safe = false;
|
|
256
|
+
});
|
|
257
|
+
if (!safe || !lexer.matchProperty(property, ast).matched)
|
|
258
|
+
return false;
|
|
259
|
+
if (property === "stroke-width" &&
|
|
260
|
+
!["number", "length"].includes(mathDimension(ast)))
|
|
261
|
+
return false;
|
|
262
|
+
if (property === "stroke-width" && ast.type === "Value") {
|
|
263
|
+
const literal = ast.children.first;
|
|
264
|
+
if (literal &&
|
|
265
|
+
["Number", "Dimension", "Percentage"].includes(literal.type) &&
|
|
266
|
+
"value" in literal &&
|
|
267
|
+
Number(literal.value) < 0)
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const name = z.string().regex(/\S/);
|
|
277
|
+
const marker = z.enum(["none", "arrow", "bar", "circle", "diamond"]);
|
|
278
|
+
export const ConnectionTypeDeclarationSchema = z
|
|
279
|
+
.strictObject({
|
|
280
|
+
extends: name.optional(),
|
|
281
|
+
"stroke-width": z
|
|
282
|
+
.union([
|
|
283
|
+
z.number().finite().nonnegative(),
|
|
284
|
+
z
|
|
285
|
+
.string()
|
|
286
|
+
.refine((v) => css(v, "stroke-width"), "Invalid scalar CSS width"),
|
|
287
|
+
])
|
|
288
|
+
.optional(),
|
|
289
|
+
"line-style": z.enum(["solid", "dashed", "dotted"]).optional(),
|
|
290
|
+
"line-color": z
|
|
291
|
+
.string()
|
|
292
|
+
.refine((v) => v === "auto" || css(v, "color"), "Invalid CSS colour")
|
|
293
|
+
.optional(),
|
|
294
|
+
"start-marker": marker.optional(),
|
|
295
|
+
"end-marker": marker.optional(),
|
|
296
|
+
text: name.optional(),
|
|
297
|
+
reverse_text: name.optional(),
|
|
298
|
+
})
|
|
299
|
+
.refine((v) => (v.text === undefined) === (v.reverse_text === undefined), "Supply both text and reverse_text")
|
|
300
|
+
.meta({
|
|
301
|
+
anyOf: [
|
|
302
|
+
{ required: ["text", "reverse_text"] },
|
|
303
|
+
{
|
|
304
|
+
not: {
|
|
305
|
+
anyOf: [{ required: ["text"] }, { required: ["reverse_text"] }],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
// Zod records intentionally discard __proto__; validate entries without copying
|
|
311
|
+
// through an ordinary object's property setter so every exact authored name survives.
|
|
312
|
+
export const ConnectionTypeDeclarationsSchema = z
|
|
313
|
+
.any()
|
|
314
|
+
.superRefine((value, ctx) => {
|
|
315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
316
|
+
ctx.addIssue({
|
|
317
|
+
code: "custom",
|
|
318
|
+
message: "Expected a declaration mapping",
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
for (const [key, declaration] of Object.entries(value)) {
|
|
323
|
+
if (!name.safeParse(key).success)
|
|
324
|
+
ctx.addIssue({
|
|
325
|
+
code: "custom",
|
|
326
|
+
path: [key],
|
|
327
|
+
message: "Type names must be nonblank",
|
|
328
|
+
});
|
|
329
|
+
const result = ConnectionTypeDeclarationSchema.safeParse(declaration);
|
|
330
|
+
if (!result.success)
|
|
331
|
+
for (const issue of result.error.issues)
|
|
332
|
+
ctx.addIssue({ ...issue, path: [key, ...issue.path] });
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
.meta({
|
|
336
|
+
type: "object",
|
|
337
|
+
propertyNames: { type: "string", pattern: "\\S" },
|
|
338
|
+
additionalProperties: z.toJSONSchema(ConnectionTypeDeclarationSchema, {
|
|
339
|
+
io: "input",
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
export const VocabularySchema = z.strictObject({
|
|
343
|
+
connection_types: ConnectionTypeDeclarationsSchema,
|
|
344
|
+
});
|
|
345
|
+
export function parseVocabulary(source, uri) {
|
|
346
|
+
try {
|
|
347
|
+
const doc = parseDocument(source, { uniqueKeys: true });
|
|
348
|
+
const problem = [...doc.errors, ...doc.warnings][0];
|
|
349
|
+
if (problem)
|
|
350
|
+
return { source, uri, error: problem.message, offset: problem.pos[0] };
|
|
351
|
+
const result = VocabularySchema.safeParse(doc.toJS({ maxAliasCount: 100 }));
|
|
352
|
+
if (!result.success) {
|
|
353
|
+
const issue = result.error.issues[0];
|
|
354
|
+
const ast = doc.getIn(issue.path, true);
|
|
355
|
+
return {
|
|
356
|
+
source,
|
|
357
|
+
uri,
|
|
358
|
+
error: issue.message,
|
|
359
|
+
offset: ast?.range?.[0] ?? 0,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const value = result.data;
|
|
363
|
+
return { source, uri, declarations: value.connection_types };
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
return {
|
|
367
|
+
source,
|
|
368
|
+
uri,
|
|
369
|
+
error: error instanceof Error ? error.message : String(error),
|
|
370
|
+
offset: 0,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const neutral = {
|
|
375
|
+
strokeWidth: 2.5,
|
|
376
|
+
lineStyle: "solid",
|
|
377
|
+
lineColor: "auto",
|
|
378
|
+
startMarker: "none",
|
|
379
|
+
endMarker: "none",
|
|
380
|
+
text: "connected to",
|
|
381
|
+
reverseText: "connected to",
|
|
382
|
+
coreKind: "ordinary",
|
|
383
|
+
};
|
|
384
|
+
export const builtinVocabulary = Object.fromEntries([
|
|
385
|
+
["connected", {}],
|
|
386
|
+
[
|
|
387
|
+
"precedes",
|
|
388
|
+
{ endMarker: "arrow", text: "precedes", reverseText: "follows" },
|
|
389
|
+
],
|
|
390
|
+
[
|
|
391
|
+
"follows",
|
|
392
|
+
{ startMarker: "arrow", text: "follows", reverseText: "precedes" },
|
|
393
|
+
],
|
|
394
|
+
[
|
|
395
|
+
"prevents",
|
|
396
|
+
{ endMarker: "bar", text: "prevents", reverseText: "is prevented by" },
|
|
397
|
+
],
|
|
398
|
+
[
|
|
399
|
+
"optional",
|
|
400
|
+
{
|
|
401
|
+
lineStyle: "dashed",
|
|
402
|
+
text: "optionally connected to",
|
|
403
|
+
reverseText: "optionally connected to",
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
[
|
|
407
|
+
"map_link",
|
|
408
|
+
{
|
|
409
|
+
coreKind: "map_link",
|
|
410
|
+
text: "links to map",
|
|
411
|
+
reverseText: "map linked from",
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
[
|
|
415
|
+
"external_link",
|
|
416
|
+
{
|
|
417
|
+
coreKind: "external_link",
|
|
418
|
+
text: "links to resource",
|
|
419
|
+
reverseText: "resource linked from",
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
].map(([key, value]) => [key, { ...neutral, ...value }]));
|
|
423
|
+
export function resolveVocabulary(shared = {}, local = {}, localContext = {}) {
|
|
424
|
+
if (shared.error)
|
|
425
|
+
throw Object.assign(new Error(shared.error), {
|
|
426
|
+
offset: shared.offset ?? 0,
|
|
427
|
+
uri: shared.uri,
|
|
428
|
+
source: shared.source,
|
|
429
|
+
});
|
|
430
|
+
const layers = [
|
|
431
|
+
ConnectionTypeDeclarationsSchema.parse(shared.declarations ?? {}),
|
|
432
|
+
ConnectionTypeDeclarationsSchema.parse(local),
|
|
433
|
+
];
|
|
434
|
+
const declarations = new Map();
|
|
435
|
+
for (const layer of layers)
|
|
436
|
+
for (const [key, value] of Object.entries(layer))
|
|
437
|
+
declarations.set(key, { ...declarations.get(key), ...value });
|
|
438
|
+
const fail = (key, message) => {
|
|
439
|
+
const parentIsLocal = Object.hasOwn(local, key) &&
|
|
440
|
+
(declarations.get(key)?.extends === undefined ||
|
|
441
|
+
Object.hasOwn(local[key], "extends"));
|
|
442
|
+
const context = parentIsLocal ? localContext : shared;
|
|
443
|
+
const location = context.source
|
|
444
|
+
? vocabularyLocations(context.source, context.uri)[key]
|
|
445
|
+
: undefined;
|
|
446
|
+
throw Object.assign(new Error(message), {
|
|
447
|
+
uri: context.uri,
|
|
448
|
+
source: context.source,
|
|
449
|
+
offset: location?.parent?.start ?? location?.name.start ?? 0,
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
const resolved = new Map();
|
|
453
|
+
const visiting = new Set();
|
|
454
|
+
const resolve = (key) => {
|
|
455
|
+
const existing = resolved.get(key);
|
|
456
|
+
if (existing)
|
|
457
|
+
return existing;
|
|
458
|
+
if (visiting.has(key))
|
|
459
|
+
fail(key, `Connection type inheritance cycle: ${key}`);
|
|
460
|
+
const seed = Object.hasOwn(builtinVocabulary, key)
|
|
461
|
+
? builtinVocabulary[key]
|
|
462
|
+
: undefined;
|
|
463
|
+
const authored = declarations.get(key);
|
|
464
|
+
if (!seed && !authored)
|
|
465
|
+
throw new Error(`Unknown connection type: ${key}`);
|
|
466
|
+
if (authored?.extends !== undefined &&
|
|
467
|
+
!declarations.has(authored.extends) &&
|
|
468
|
+
!Object.hasOwn(builtinVocabulary, authored.extends))
|
|
469
|
+
fail(key, `Unknown parent connection type: ${authored.extends}`);
|
|
470
|
+
visiting.add(key);
|
|
471
|
+
const base = authored?.extends !== undefined
|
|
472
|
+
? resolve(authored.extends)
|
|
473
|
+
: (seed ?? resolve("connected"));
|
|
474
|
+
if (seed && seed.coreKind !== base.coreKind)
|
|
475
|
+
fail(key, `Cannot change built-in core capability: ${key}`);
|
|
476
|
+
const value = { ...base };
|
|
477
|
+
const fields = {
|
|
478
|
+
"stroke-width": "strokeWidth",
|
|
479
|
+
"line-style": "lineStyle",
|
|
480
|
+
"line-color": "lineColor",
|
|
481
|
+
"start-marker": "startMarker",
|
|
482
|
+
"end-marker": "endMarker",
|
|
483
|
+
text: "text",
|
|
484
|
+
reverse_text: "reverseText",
|
|
485
|
+
};
|
|
486
|
+
for (const [field, target] of Object.entries(fields))
|
|
487
|
+
if (authored && Object.hasOwn(authored, field))
|
|
488
|
+
Object.assign(value, {
|
|
489
|
+
[target]: authored[field],
|
|
490
|
+
});
|
|
491
|
+
visiting.delete(key);
|
|
492
|
+
resolved.set(key, value);
|
|
493
|
+
return value;
|
|
494
|
+
};
|
|
495
|
+
for (const key of new Set([
|
|
496
|
+
...Object.keys(builtinVocabulary),
|
|
497
|
+
...declarations.keys(),
|
|
498
|
+
]))
|
|
499
|
+
resolve(key);
|
|
500
|
+
return Object.fromEntries(resolved);
|
|
501
|
+
}
|
|
502
|
+
/** Source-only provenance for host hover/diagnostics; never embedded in edges. */
|
|
503
|
+
export function vocabularyLocations(source, uri) {
|
|
504
|
+
const doc = parseDocument(source);
|
|
505
|
+
const mapping = doc.getIn(["connection_types"], true);
|
|
506
|
+
const entries = [];
|
|
507
|
+
if (mapping &&
|
|
508
|
+
typeof mapping === "object" &&
|
|
509
|
+
"items" in mapping &&
|
|
510
|
+
Array.isArray(mapping.items))
|
|
511
|
+
for (const pair of mapping.items) {
|
|
512
|
+
if (!pair || typeof pair !== "object" || !("key" in pair))
|
|
513
|
+
continue;
|
|
514
|
+
const key = pair.key;
|
|
515
|
+
if (!key || !("value" in key) || typeof key.value !== "string")
|
|
516
|
+
continue;
|
|
517
|
+
const parent = doc.getIn(["connection_types", key.value, "extends"], true);
|
|
518
|
+
entries.push([
|
|
519
|
+
key.value,
|
|
520
|
+
{
|
|
521
|
+
uri,
|
|
522
|
+
name: { start: key.range?.[0] ?? 0, end: key.range?.[1] ?? 0 },
|
|
523
|
+
...(parent?.range
|
|
524
|
+
? { parent: { start: parent.range[0], end: parent.range[1] } }
|
|
525
|
+
: {}),
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
}
|
|
529
|
+
return Object.fromEntries(entries);
|
|
530
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@lensmap/document",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"description": "Experimental Lensmap document engine: YAML analysis, semantic graphs and revision-bound source edits",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
8
8
|
],
|
|
@@ -28,13 +28,19 @@
|
|
|
28
28
|
"./v1": {
|
|
29
29
|
"types": "./dist/v1/schema.d.ts",
|
|
30
30
|
"default": "./dist/v1/schema.js"
|
|
31
|
+
},
|
|
32
|
+
"./project": {
|
|
33
|
+
"types": "./dist/project.d.ts",
|
|
34
|
+
"default": "./dist/project.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
38
|
+
"css-tree": "3.2.1",
|
|
34
39
|
"yaml": "2.9.1",
|
|
35
40
|
"zod": "4.6.7"
|
|
36
41
|
},
|
|
37
42
|
"devDependencies": {
|
|
43
|
+
"@types/css-tree": "3.2.0",
|
|
38
44
|
"@types/node": "24.19.2",
|
|
39
45
|
"ajv": "8.20.0",
|
|
40
46
|
"typescript": "7.0.2",
|