@slxu/graphsx 0.1.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.
@@ -0,0 +1,436 @@
1
+ import { GraphDslError } from "./errors.js";
2
+
3
+ export const REF_LITERAL = "__graphDslRef";
4
+ export const ADDRESS_LITERAL = "__graphDslAddress";
5
+ export const POINT_LITERAL = "__graphDslPoint";
6
+ export const TEMPLATE_LITERAL = "__graphDslTemplate";
7
+ export const EXPRESSION_LITERAL = "__graphDslExpression";
8
+
9
+ export function parseBraceLiteral(source) {
10
+ const value = source.trim();
11
+ if (/^`[\s\S]*`$/.test(value)) {
12
+ return templateLiteral(value.slice(1, -1));
13
+ }
14
+ if (/^\{.*\}$/.test(value)) {
15
+ return parseObjectLiteral(value);
16
+ }
17
+ if (/^\[.*\]$/.test(value)) {
18
+ return parseArrayLiteral(value);
19
+ }
20
+ if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
21
+ if (value === "true") return true;
22
+ if (value === "false") return false;
23
+ if (value === "null") return null;
24
+
25
+ const quoted = value.match(/^(['"])(.*)\1$/);
26
+ if (quoted) return unescapeQuotedString(quoted[2]);
27
+
28
+ const pointExpression = parsePointExpression(value);
29
+ if (pointExpression) {
30
+ return pointLiteral(pointExpression);
31
+ }
32
+
33
+ if (isAddress(value)) {
34
+ return addressLiteral(value);
35
+ }
36
+
37
+ if (looksLikeExpression(value)) {
38
+ return expressionLiteral(value);
39
+ }
40
+
41
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)) {
42
+ return refLiteral(value);
43
+ }
44
+
45
+ throw new GraphDslError(`Unsupported braced literal "{${source}}"`);
46
+ }
47
+
48
+ export function parseArrayLiteral(source) {
49
+ const inner = source.slice(1, -1).trim();
50
+ if (!inner) return [];
51
+
52
+ return splitTopLevel(inner, ",").map((part) => parseObjectValue(part.trim()));
53
+ }
54
+
55
+ function parseObjectLiteral(source) {
56
+ const inner = source.slice(1, -1).trim();
57
+ if (!inner) return {};
58
+
59
+ return Object.fromEntries(splitTopLevel(inner, ",").map((entry) => {
60
+ const [rawKey, ...rawValueParts] = splitTopLevel(entry, ":");
61
+ if (rawValueParts.length === 0) {
62
+ throw new GraphDslError(`Invalid object entry "${entry}"`);
63
+ }
64
+ const key = parseObjectKey(rawKey.trim());
65
+ const value = parseObjectValue(rawValueParts.join(":").trim());
66
+ return [key, value];
67
+ }));
68
+ }
69
+
70
+ function parseObjectKey(source) {
71
+ const quoted = source.match(/^(['"])(.*)\1$/);
72
+ if (quoted) return unescapeQuotedString(quoted[2]);
73
+ if (/^[A-Za-z_$][A-Za-z0-9_$-]*$/.test(source)) return source;
74
+ throw new GraphDslError(`Invalid object key "${source}"`);
75
+ }
76
+
77
+ function parseObjectValue(source) {
78
+ if (/^`[\s\S]*`$/.test(source)) return templateLiteral(source.slice(1, -1));
79
+ if (/^-?\d+(\.\d+)?$/.test(source)) return Number(source);
80
+ if (source === "true") return true;
81
+ if (source === "false") return false;
82
+ if (source === "null") return null;
83
+ if (/^\[.*\]$/.test(source)) return parseArrayLiteral(source);
84
+ if (/^\{.*\}$/.test(source)) return parseObjectLiteral(source);
85
+
86
+ const quoted = source.match(/^(['"])(.*)\1$/);
87
+ if (quoted) return unescapeQuotedString(quoted[2]);
88
+
89
+ const pointExpression = parsePointExpression(source);
90
+ if (pointExpression) {
91
+ return pointLiteral(pointExpression);
92
+ }
93
+
94
+ if (isAddress(source)) {
95
+ return addressLiteral(source);
96
+ }
97
+
98
+ if (looksLikeExpression(source)) {
99
+ return expressionLiteral(source);
100
+ }
101
+
102
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(source)) {
103
+ return refLiteral(source);
104
+ }
105
+
106
+ throw new GraphDslError(`Unsupported object value "${source}"`);
107
+ }
108
+
109
+ export function evaluateExpression(source, scope, options = {}) {
110
+ const parser = new ExpressionParser(source, scope, options);
111
+ return parser.parse();
112
+ }
113
+
114
+ class ExpressionParser {
115
+ constructor(source, scope, options) {
116
+ this.source = source;
117
+ this.scope = scope;
118
+ this.options = options;
119
+ this.index = 0;
120
+ this.complete = true;
121
+ }
122
+
123
+ parse() {
124
+ const value = this.parseExpression();
125
+ this.skipWhitespace();
126
+ if (!this.isDone()) {
127
+ throw new GraphDslError(`Unsupported expression "${this.source}"`);
128
+ }
129
+ if (!this.complete) {
130
+ return { resolved: false };
131
+ }
132
+ if (!Number.isFinite(value)) {
133
+ throw new GraphDslError(`Expression "${this.source}" did not evaluate to a finite number`);
134
+ }
135
+ return { resolved: true, value };
136
+ }
137
+
138
+ parseExpression() {
139
+ let value = this.parseTerm();
140
+ while (true) {
141
+ this.skipWhitespace();
142
+ if (this.consume("+")) {
143
+ value += this.parseTerm();
144
+ } else if (this.consume("-")) {
145
+ value -= this.parseTerm();
146
+ } else {
147
+ return value;
148
+ }
149
+ }
150
+ }
151
+
152
+ parseTerm() {
153
+ let value = this.parseFactor();
154
+ while (true) {
155
+ this.skipWhitespace();
156
+ if (this.consume("*")) {
157
+ value *= this.parseFactor();
158
+ } else if (this.consume("/")) {
159
+ value /= this.parseFactor();
160
+ } else {
161
+ return value;
162
+ }
163
+ }
164
+ }
165
+
166
+ parseFactor() {
167
+ this.skipWhitespace();
168
+ if (this.consume("+")) return this.parseFactor();
169
+ if (this.consume("-")) return -this.parseFactor();
170
+ if (this.consume("(")) {
171
+ const value = this.parseExpression();
172
+ this.skipWhitespace();
173
+ if (!this.consume(")")) {
174
+ throw new GraphDslError(`Unclosed expression "${this.source}"`);
175
+ }
176
+ return value;
177
+ }
178
+ if (isDigit(this.peek()) || this.peek() === ".") {
179
+ return this.parseNumber();
180
+ }
181
+ if (isIdentifierStart(this.peek())) {
182
+ return this.parseIdentifier();
183
+ }
184
+ throw new GraphDslError(`Unsupported expression "${this.source}"`);
185
+ }
186
+
187
+ parseNumber() {
188
+ const start = this.index;
189
+ while (isDigit(this.peek())) this.index += 1;
190
+ if (this.peek() === ".") {
191
+ this.index += 1;
192
+ while (isDigit(this.peek())) this.index += 1;
193
+ }
194
+ const raw = this.source.slice(start, this.index);
195
+ if (raw === ".") {
196
+ throw new GraphDslError(`Unsupported expression "${this.source}"`);
197
+ }
198
+ return Number(raw);
199
+ }
200
+
201
+ parseIdentifier() {
202
+ const start = this.index;
203
+ this.index += 1;
204
+ while (isIdentifierPart(this.peek())) this.index += 1;
205
+ const name = this.source.slice(start, this.index);
206
+ if (!this.scope.has(name)) {
207
+ if (this.options.strict) {
208
+ throw new GraphDslError(`Unknown template variable "${name}"`);
209
+ }
210
+ this.complete = false;
211
+ return 0;
212
+ }
213
+ const value = Number(this.scope.get(name));
214
+ if (!Number.isFinite(value)) {
215
+ if (this.options.strict) {
216
+ throw new GraphDslError(`Expression variable "${name}" must be a number`);
217
+ }
218
+ this.complete = false;
219
+ return 0;
220
+ }
221
+ return value;
222
+ }
223
+
224
+ skipWhitespace() {
225
+ while (/\s/.test(this.peek() ?? "")) this.index += 1;
226
+ }
227
+
228
+ consume(value) {
229
+ if (!this.source.startsWith(value, this.index)) return false;
230
+ this.index += value.length;
231
+ return true;
232
+ }
233
+
234
+ peek() {
235
+ return this.source[this.index];
236
+ }
237
+
238
+ isDone() {
239
+ return this.index >= this.source.length;
240
+ }
241
+ }
242
+
243
+ function isDigit(char) {
244
+ return char != null && /[0-9]/.test(char);
245
+ }
246
+
247
+ function isIdentifierStart(char) {
248
+ return char != null && /[A-Za-z_]/.test(char);
249
+ }
250
+
251
+ function isIdentifierPart(char) {
252
+ return char != null && /[A-Za-z0-9_]/.test(char);
253
+ }
254
+
255
+ function refLiteral(name) {
256
+ return { [REF_LITERAL]: name };
257
+ }
258
+
259
+ function addressLiteral(name) {
260
+ return { [ADDRESS_LITERAL]: name };
261
+ }
262
+
263
+ export function pointLiteral(expression) {
264
+ return { [POINT_LITERAL]: expression };
265
+ }
266
+
267
+ export function substitutePointExpression(expression, substitute) {
268
+ return {
269
+ address: expression.address,
270
+ offsets: expression.offsets.map((offset) => ({
271
+ x: substitute(offset.x),
272
+ y: substitute(offset.y)
273
+ }))
274
+ };
275
+ }
276
+
277
+ export function templateLiteral(source) {
278
+ return { [TEMPLATE_LITERAL]: source };
279
+ }
280
+
281
+ function expressionLiteral(source) {
282
+ return { [EXPRESSION_LITERAL]: source };
283
+ }
284
+
285
+ function unescapeQuotedString(source) {
286
+ return source
287
+ .replace(/\\n/g, "\n")
288
+ .replace(/\\r/g, "\r")
289
+ .replace(/\\t/g, "\t")
290
+ .replace(/\\(["'\\])/g, "$1");
291
+ }
292
+
293
+ export function isRefLiteral(value) {
294
+ return value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, REF_LITERAL);
295
+ }
296
+
297
+ export function isAddressLiteral(value) {
298
+ return value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, ADDRESS_LITERAL);
299
+ }
300
+
301
+ export function isPointLiteral(value) {
302
+ return value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, POINT_LITERAL);
303
+ }
304
+
305
+ export function isTemplateLiteral(value) {
306
+ return value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, TEMPLATE_LITERAL);
307
+ }
308
+
309
+ export function isExpressionLiteral(value) {
310
+ return value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, EXPRESSION_LITERAL);
311
+ }
312
+
313
+ function looksLikeExpression(source) {
314
+ return /[+\-*/()]/.test(source);
315
+ }
316
+
317
+ export function isAddress(source) {
318
+ return /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)+$/.test(source);
319
+ }
320
+
321
+ function parsePointExpression(source) {
322
+ const parts = splitPointExpression(source.trim());
323
+ if (parts.length < 3) return null;
324
+ const first = parts.shift();
325
+ if (!first || first.type !== "term" || !isAddress(first.value)) return null;
326
+
327
+ const offsets = [];
328
+ while (parts.length > 0) {
329
+ const operator = parts.shift();
330
+ const term = parts.shift();
331
+ if (!operator || operator.type !== "operator" || !term || term.type !== "term") {
332
+ throw new GraphDslError(`Invalid point expression "${source}"`);
333
+ }
334
+ const vector = parseVectorLiteral(term.value, source);
335
+ offsets.push({
336
+ x: operator.value === "-" ? negateVectorValue(vector.x) : vector.x,
337
+ y: operator.value === "-" ? negateVectorValue(vector.y) : vector.y
338
+ });
339
+ }
340
+
341
+ return { address: first.value, offsets };
342
+ }
343
+
344
+ function negateVectorValue(value) {
345
+ if (typeof value === "number") return -value;
346
+ if (isExpressionLiteral(value)) return expressionLiteral(`-(${value[EXPRESSION_LITERAL]})`);
347
+ return expressionLiteral(`-(${String(value)})`);
348
+ }
349
+
350
+ function splitPointExpression(source) {
351
+ const parts = [];
352
+ let start = 0;
353
+ let depth = 0;
354
+ let quote = null;
355
+
356
+ for (let index = 0; index < source.length; index += 1) {
357
+ const char = source[index];
358
+ if (quote) {
359
+ if (char === quote && source[index - 1] !== "\\") {
360
+ quote = null;
361
+ }
362
+ continue;
363
+ }
364
+ if (char === '"' || char === "'") {
365
+ quote = char;
366
+ continue;
367
+ }
368
+ if (char === "[") {
369
+ depth += 1;
370
+ continue;
371
+ }
372
+ if (char === "]") {
373
+ depth -= 1;
374
+ continue;
375
+ }
376
+ if ((char === "+" || char === "-") && depth === 0) {
377
+ const term = source.slice(start, index).trim();
378
+ if (term) parts.push({ type: "term", value: term });
379
+ parts.push({ type: "operator", value: char });
380
+ start = index + 1;
381
+ }
382
+ }
383
+
384
+ const tail = source.slice(start).trim();
385
+ if (tail) parts.push({ type: "term", value: tail });
386
+ return parts;
387
+ }
388
+
389
+ function parseVectorLiteral(source, expression) {
390
+ if (!/^\[.*\]$/.test(source)) {
391
+ throw new GraphDslError(`Point expression "${expression}" only supports vector offsets like [x, y]`);
392
+ }
393
+ const vector = parseArrayLiteral(source);
394
+ if (vector.length < 2) {
395
+ throw new GraphDslError(`Point expression "${expression}" requires [x, y] vectors`);
396
+ }
397
+ return { x: vector[0], y: vector[1] };
398
+ }
399
+
400
+ export function pointExpressionNumber(value, expression) {
401
+ const number = Number(value);
402
+ if (!Number.isFinite(number)) {
403
+ throw new GraphDslError(`Point expression for "${expression.address}" vector values must be numbers`);
404
+ }
405
+ return number;
406
+ }
407
+
408
+ export function splitTopLevel(source, delimiter) {
409
+ const parts = [];
410
+ let start = 0;
411
+ let depth = 0;
412
+ let quote = null;
413
+
414
+ for (let index = 0; index < source.length; index += 1) {
415
+ const char = source[index];
416
+ if (quote) {
417
+ if (char === quote && source[index - 1] !== "\\") {
418
+ quote = null;
419
+ }
420
+ continue;
421
+ }
422
+ if (char === '"' || char === "'") {
423
+ quote = char;
424
+ continue;
425
+ }
426
+ if (char === "{" || char === "[") depth += 1;
427
+ if (char === "}" || char === "]") depth -= 1;
428
+ if (char === delimiter && depth === 0) {
429
+ parts.push(source.slice(start, index).trim());
430
+ start = index + 1;
431
+ }
432
+ }
433
+
434
+ parts.push(source.slice(start).trim());
435
+ return parts.filter(Boolean);
436
+ }
@@ -0,0 +1,24 @@
1
+ .graphsx-defs {
2
+ display: none !important;
3
+ }
4
+
5
+ .graphsx-rendered,
6
+ .graphsx-block[data-graphsx] {
7
+ display: block;
8
+ width: fit-content;
9
+ max-width: 100%;
10
+ margin: 1rem auto;
11
+ overflow: auto;
12
+ }
13
+
14
+ .graphsx-rendered > svg {
15
+ display: block;
16
+ position: static;
17
+ max-width: 100%;
18
+ height: auto;
19
+ }
20
+
21
+ .graphsx-error {
22
+ color: #b42318;
23
+ white-space: pre-wrap;
24
+ }
@@ -0,0 +1,111 @@
1
+ import { parseMarkup } from "./parser.js";
2
+ import { parseGraphSXDocument, renderGraphSXDocument } from "./document.js";
3
+
4
+ export const GRAPHSX_FENCE = "graphsx";
5
+ export const GRAPHSX_DEFS_FENCE = "graphsx-defs";
6
+
7
+ export function graphsxMarkdownIt(md, options = {}) {
8
+ const fenceName = options.fenceName ?? GRAPHSX_FENCE;
9
+ const defsFenceName = options.defsFenceName ?? GRAPHSX_DEFS_FENCE;
10
+ const markerClass = options.markerClass ?? "graphsx-block";
11
+ const defsClass = options.defsClass ?? "graphsx-defs";
12
+ const previousFence = md.renderer.rules.fence;
13
+
14
+ md.renderer.rules.fence = (tokens, index, renderOptions, env, self) => {
15
+ const token = tokens[index];
16
+ const info = parseFenceInfo(token.info);
17
+
18
+ if (info.name === defsFenceName) {
19
+ const name = info.attrs.name ?? info.attrs.id ?? info.args[0] ?? "default";
20
+ const source = md.utils.escapeHtml(token.content);
21
+ return `<div class="${defsClass}" data-graphsx-defs="${md.utils.escapeHtml(name)}" hidden><template class="graphsx-source">${source}</template></div>\n`;
22
+ }
23
+
24
+ if (info.name !== fenceName) {
25
+ if (previousFence) {
26
+ return previousFence(tokens, index, renderOptions, env, self);
27
+ }
28
+ return self.renderToken(tokens, index, renderOptions);
29
+ }
30
+
31
+ const source = md.utils.escapeHtml(token.content);
32
+ const use = info.attrs.use ? ` data-graphsx-use="${md.utils.escapeHtml(info.attrs.use)}"` : "";
33
+ return `<div class="${markerClass}" data-graphsx="true"${use}><template class="graphsx-source">${source}</template></div>\n`;
34
+ };
35
+ }
36
+
37
+ export function renderGraphSXBlocks(root, options = {}) {
38
+ const libraries = collectGraphSXLibraries(root);
39
+ const blocks = [
40
+ ...root.querySelectorAll(".graphsx-block[data-graphsx]"),
41
+ ...root.querySelectorAll("pre > code.language-graphsx")
42
+ ];
43
+
44
+ for (const block of blocks) {
45
+ const host = block.matches("code") ? block.closest("pre") : block;
46
+ const source = block.matches("code") ? block.textContent : block.querySelector("template.graphsx-source")?.content.textContent;
47
+ const use = block.matches("code") ? "" : block.getAttribute("data-graphsx-use") ?? "";
48
+ if (!host || source == null) continue;
49
+
50
+ host.replaceChildren();
51
+ host.classList.add("graphsx-rendered");
52
+
53
+ try {
54
+ const graph = parseGraphWithLibraries(source, libraries, use);
55
+ const documentRef = options.document ?? host.ownerDocument ?? document;
56
+ const svg = documentRef.createElementNS("http://www.w3.org/2000/svg", "svg");
57
+ const renderOptions = {
58
+ minWidth: 0,
59
+ minHeight: 0,
60
+ viewportPadding: 24,
61
+ ...options
62
+ };
63
+ const size = renderGraphSXDocument(svg, graph, renderOptions);
64
+ svg.setAttribute("width", size.width);
65
+ svg.setAttribute("height", size.height);
66
+ host.append(svg);
67
+ } catch (error) {
68
+ host.classList.add("graphsx-error");
69
+ const pre = (options.document ?? host.ownerDocument ?? document).createElement("pre");
70
+ pre.textContent = error.message;
71
+ host.append(pre);
72
+ }
73
+ }
74
+ }
75
+
76
+ export function parseGraphWithLibraries(source, libraries, use) {
77
+ return parseGraphSXDocument(source, { libraries, use });
78
+ }
79
+
80
+ export function parseFenceInfo(rawInfo = "") {
81
+ const source = rawInfo.trim();
82
+ const nameMatch = source.match(/^(\S+)/);
83
+ const name = nameMatch?.[1] ?? "";
84
+ const rest = name ? source.slice(name.length).trim() : "";
85
+ const args = [];
86
+ const attrs = {};
87
+
88
+ const pattern = /([A-Za-z_:][A-Za-z0-9_.:-]*)=("([^"]*)"|'([^']*)'|[^\s]+)|"([^"]*)"|'([^']*)'|(\S+)/g;
89
+ let match = pattern.exec(rest);
90
+ while (match) {
91
+ if (match[1]) {
92
+ attrs[match[1]] = match[3] ?? match[4] ?? match[2];
93
+ } else {
94
+ args.push(match[5] ?? match[6] ?? match[7]);
95
+ }
96
+ match = pattern.exec(rest);
97
+ }
98
+
99
+ return { name, args, attrs };
100
+ }
101
+
102
+ function collectGraphSXLibraries(root) {
103
+ const libraries = new Map();
104
+ for (const block of root.querySelectorAll(".graphsx-defs[data-graphsx-defs]")) {
105
+ const name = block.getAttribute("data-graphsx-defs");
106
+ const source = block.querySelector("template.graphsx-source")?.content.textContent;
107
+ if (!name || source == null) continue;
108
+ libraries.set(name, { name, source });
109
+ }
110
+ return libraries;
111
+ }