@gene-code/core 0.0.1

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/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # DSL
2
+
3
+ ```
4
+ geneDiagram
5
+ gene <NAME>
6
+ length <N> # total track length in coordinate units; optional (else inferred from max position)
7
+ exon <start> <end> [label]
8
+ variant <label> <pos> <class>
9
+ ```
10
+
11
+ Variant classes → colors** (standard palette):
12
+
13
+ class | color | meaning |
14
+ |---|---|---|
15
+ | missense | green | amino-acid substitution |
16
+ | nonsense | red | premature stop |
17
+ | frameshift | purple | indel shifting reading frame |
18
+ | splice | orange | splice-site |
19
+ | synonymous | gray | silent
20
+
21
+
22
+ - Coordinate units - protein/aa
23
+
24
+
25
+ ## Real worls examples
26
+
27
+ ```
28
+ geneDiagram
29
+ gene TP53
30
+ length 393
31
+ exon 94 312 DNA-binding
32
+ variant R175H 175 missense
33
+ variant R248Q 248 missense
34
+ variant R273H 273 missense
35
+ variant R213* 213 nonsense
36
+ ```
37
+ ## SVG layout
38
+
39
+ - width 800, height ~200, padding 40
40
+ - pos → padding + (pos/length) * (800 - 2*padding)
41
+ -
@@ -0,0 +1,14 @@
1
+ import { splitRows } from "./src/dsl";
2
+ import type { SVGString } from "./src/types";
3
+ export { splitRows };
4
+ /**
5
+ * Parses a DSL string into its diagram AST by dispatching on the first row's
6
+ * diagram keyword. Throws if the keyword is missing or unknown.
7
+ */
8
+ export declare function parse(dsl: string): unknown;
9
+ /**
10
+ * Renders a DSL string to an SVG. Returns null for empty, unknown, or
11
+ * malformed input (e.g. a partially-typed diagram) so callers can keep the
12
+ * previous output rather than handling thrown errors.
13
+ */
14
+ export declare function render(dsl: string): SVGString | null;
package/dist/index.js ADDED
@@ -0,0 +1,542 @@
1
+ // src/dsl.ts
2
+ function splitRows(text) {
3
+ return text.split(`
4
+ `).map((row) => row.trim()).filter((row) => row.length > 0);
5
+ }
6
+ function tokenize(text) {
7
+ return text.split(/\s+/);
8
+ }
9
+
10
+ // src/render.ts
11
+ var WIDTH_PX = 800;
12
+ var MARGIN_X = 10;
13
+ var MARGIN_Y = 14;
14
+ var INNER_WIDTH = WIDTH_PX - MARGIN_X * 2;
15
+ function renderNode(node) {
16
+ switch (node.type) {
17
+ case "line":
18
+ return `<line x1="${node.x1}" x2="${node.x2}" y1="${node.y1}" y2="${node.y2}" stroke="black" />`;
19
+ case "rect":
20
+ return `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" fill="${node.fill ?? "lightgray"}" stroke="${node.stroke ?? "black"}" />`;
21
+ case "text":
22
+ return `<text x="${node.x}" y="${node.y}" text-anchor="${node.anchor}" alignment-baseline="${node.baseline}" font-size="10">${node.text}</text>`;
23
+ case "circle":
24
+ return `<circle cx="${node.x}" cy="${node.y}" r="${node.radius}" fill="${node.color}" stroke="${node.stroke ?? "none"}" />`;
25
+ case "path":
26
+ return `<path d="${node.d}" fill="${node.fill ?? "none"}" stroke="${node.stroke ?? "none"}" />`;
27
+ case "group":
28
+ return `<g transform="translate(${node.x},${node.y})">
29
+ ${node.children.map(renderNode).join("")}
30
+ </g>`;
31
+ default:
32
+ return "";
33
+ }
34
+ }
35
+ function render(layout) {
36
+ const svg = `<svg viewBox="0 0 ${layout.width} ${layout.height}" width="100%" style="max-width:${layout.width}px" xmlns="http://www.w3.org/2000/svg">
37
+ ${layout.nodes.map(renderNode).join("")}
38
+ </svg>`;
39
+ return svg;
40
+ }
41
+
42
+ // src/modules/geneDiagram.ts
43
+ var Y_OFFSET_PX = 12;
44
+ var FIRST_VARIANT_GAP_PX = 15;
45
+ var BLOCK_HEIGHT_PX = 10;
46
+ var LOLLIPOP_RADIUS_PX = 3;
47
+ var ELBOW_RADIUS_PX = 3;
48
+ var FONT_SIZE_PX = 10;
49
+ var DOMAIN_LABEL_GAP_PX = BLOCK_HEIGHT_PX / 2 + MARGIN_Y - 4;
50
+ function stemPath(x, y0, headX, y1) {
51
+ const dx = headX - x;
52
+ if (Math.abs(dx) < 0.5) {
53
+ return `M ${x} ${y0} L ${x} ${y1}`;
54
+ }
55
+ const dir = Math.sign(dx);
56
+ const jogY = (y0 + y1) / 2;
57
+ const r = Math.min(ELBOW_RADIUS_PX, Math.abs(dx) / 2, Math.abs(y0 - y1) / 4);
58
+ return [
59
+ `M ${x} ${y0}`,
60
+ `L ${x} ${jogY + r}`,
61
+ `Q ${x} ${jogY} ${x + dir * r} ${jogY}`,
62
+ `L ${headX - dir * r} ${jogY}`,
63
+ `Q ${headX} ${jogY} ${headX} ${jogY - r}`,
64
+ `L ${headX} ${y1}`
65
+ ].join(" ");
66
+ }
67
+ var VARIANT_COLORS = {
68
+ missense: "#d1495b",
69
+ nonsense: "#5b7c99",
70
+ frameshift: "#e0955b",
71
+ splice: "#5a9367",
72
+ synonymous: "#9a9a9a"
73
+ };
74
+ var DOMAIN_FILL = "#e4e0d8";
75
+ var DOMAIN_STROKE = "#8c8577";
76
+ var STEM_COLOR = "#5c5c5c";
77
+ var HEAD_RING = "#fbfbfb";
78
+ function parseDomain(words) {
79
+ const [start, end, ...rest] = words;
80
+ return {
81
+ start: Number(start),
82
+ end: Number(end),
83
+ ...rest.length ? { label: rest.join(" ") } : {}
84
+ };
85
+ }
86
+ function parseVariant(words) {
87
+ const [label, pos, varClass] = words;
88
+ if (!label || !pos || !varClass) {
89
+ throw new Error(`Invalid variant row: ${words.join(" ")}`);
90
+ }
91
+ if (Number.isNaN(Number(pos))) {
92
+ throw new Error(`Invalid variant position: ${pos}`);
93
+ }
94
+ return {
95
+ label,
96
+ pos: Number(pos),
97
+ class: varClass
98
+ };
99
+ }
100
+ var CHAR_WIDTH_PX = 6;
101
+ var LABEL_GAP_PX = LOLLIPOP_RADIUS_PX + 2;
102
+ var HEAD_DODGE_PX = 2 * LOLLIPOP_RADIUS_PX + 1;
103
+ function labelWidth(v) {
104
+ return LABEL_GAP_PX + v.label.length * CHAR_WIDTH_PX;
105
+ }
106
+ function packVariants(variants, xScale) {
107
+ const sorted = [...variants].sort((a, b) => a.pos - b.pos);
108
+ const placed = [];
109
+ let prevPos = Number.NaN;
110
+ let prevHeadX = Number.NEGATIVE_INFINITY;
111
+ for (const variant of sorted) {
112
+ const x = xScale(variant.pos);
113
+ const headX = variant.pos === prevPos ? prevHeadX : Math.max(x, prevHeadX + HEAD_DODGE_PX);
114
+ prevPos = variant.pos;
115
+ prevHeadX = headX;
116
+ placed.push({ variant, x, headX, level: 0 });
117
+ }
118
+ for (let i = placed.length - 1;i >= 0; i--) {
119
+ const p = placed[i];
120
+ const labelEnd = p.headX + labelWidth(p.variant);
121
+ let level = 0;
122
+ for (let j = i + 1;j < placed.length; j++) {
123
+ const q = placed[j];
124
+ const samePos = q.variant.pos === p.variant.pos;
125
+ if (samePos || q.headX <= labelEnd) {
126
+ level = Math.max(level, q.level + 1);
127
+ }
128
+ }
129
+ p.level = level;
130
+ }
131
+ return placed;
132
+ }
133
+ function parse(rows) {
134
+ const diagram = { type: "geneDiagram", domains: [], variants: [] };
135
+ if (rows.length === 0) {
136
+ throw new Error("Empty DSL input");
137
+ }
138
+ for (const row of rows) {
139
+ const [keyword, ...rest] = tokenize(row);
140
+ switch (keyword) {
141
+ case "geneDiagram": {
142
+ diagram.type = "geneDiagram";
143
+ break;
144
+ }
145
+ case "length": {
146
+ diagram.length = Number(rest[0]);
147
+ break;
148
+ }
149
+ case "gene": {
150
+ diagram.gene = rest.join(" ");
151
+ break;
152
+ }
153
+ case "domain": {
154
+ diagram.domains.push(parseDomain(rest));
155
+ break;
156
+ }
157
+ case "variant": {
158
+ diagram.variants.push(parseVariant(rest));
159
+ break;
160
+ }
161
+ default: {
162
+ throw new Error(`Unknown keyword: ${keyword}`);
163
+ }
164
+ }
165
+ }
166
+ if (!diagram.length || Number.isNaN(diagram.length)) {
167
+ throw new Error("geneDiagram requires a `length` (protein length in amino acids)");
168
+ }
169
+ return diagram;
170
+ }
171
+ function layout(ast) {
172
+ const nodes = [];
173
+ const xDomain = [1, ast.length];
174
+ const xScale = (x) => MARGIN_X + INNER_WIDTH * (x - xDomain[0]) / (xDomain[1] - xDomain[0]);
175
+ const placedVariants = packVariants(ast.variants, xScale);
176
+ const maxVariantsStack = Math.max(1, ...placedVariants.map((p) => p.level + 1));
177
+ const stackHeight = FIRST_VARIANT_GAP_PX + maxVariantsStack * Y_OFFSET_PX;
178
+ const backboneY = MARGIN_Y + stackHeight;
179
+ nodes.push({
180
+ type: "line",
181
+ x1: xScale(xDomain[0]),
182
+ x2: xScale(xDomain[1]),
183
+ y1: backboneY,
184
+ y2: backboneY
185
+ });
186
+ nodes.push({
187
+ type: "text",
188
+ x: xScale(xDomain[0]),
189
+ y: backboneY + DOMAIN_LABEL_GAP_PX,
190
+ text: String(xDomain[0]),
191
+ anchor: "start",
192
+ baseline: "hanging"
193
+ });
194
+ nodes.push({
195
+ type: "text",
196
+ x: xScale(xDomain[1]),
197
+ y: backboneY + DOMAIN_LABEL_GAP_PX,
198
+ text: String(xDomain[1]),
199
+ anchor: "end",
200
+ baseline: "hanging"
201
+ });
202
+ for (const domain of ast.domains) {
203
+ const x = xScale(domain.start);
204
+ const width = xScale(domain.end) - xScale(domain.start);
205
+ nodes.push({
206
+ type: "group",
207
+ x,
208
+ y: backboneY,
209
+ children: [
210
+ {
211
+ type: "rect",
212
+ x: 0,
213
+ y: -BLOCK_HEIGHT_PX / 2,
214
+ width,
215
+ height: BLOCK_HEIGHT_PX,
216
+ fill: DOMAIN_FILL,
217
+ stroke: DOMAIN_STROKE
218
+ },
219
+ {
220
+ type: "text",
221
+ x: width / 2,
222
+ y: DOMAIN_LABEL_GAP_PX,
223
+ text: domain.label || "",
224
+ anchor: "middle",
225
+ baseline: "hanging"
226
+ }
227
+ ]
228
+ });
229
+ }
230
+ const levelY = (level) => backboneY - FIRST_VARIANT_GAP_PX - (level + 1) * Y_OFFSET_PX;
231
+ const columns = new Map;
232
+ for (const p of placedVariants) {
233
+ const col = columns.get(p.x);
234
+ if (col)
235
+ col.push(p);
236
+ else
237
+ columns.set(p.x, [p]);
238
+ }
239
+ const domainTopY = backboneY - BLOCK_HEIGHT_PX / 2;
240
+ const inDomain = (pos) => ast.domains.some((d) => pos >= d.start && pos <= d.end);
241
+ for (const [x, column] of columns) {
242
+ const headX = column[0].headX;
243
+ const topLevel = Math.max(...column.map((p) => p.level));
244
+ const anchorY = inDomain(column[0].variant.pos) ? domainTopY : backboneY;
245
+ nodes.push({
246
+ type: "path",
247
+ d: stemPath(x, anchorY, headX, levelY(topLevel)),
248
+ stroke: STEM_COLOR
249
+ });
250
+ for (const { variant, level } of column) {
251
+ const headY = levelY(level);
252
+ nodes.push({
253
+ type: "group",
254
+ x: 0,
255
+ y: 0,
256
+ children: [
257
+ {
258
+ type: "circle",
259
+ x: headX,
260
+ y: headY,
261
+ color: VARIANT_COLORS[variant.class],
262
+ radius: LOLLIPOP_RADIUS_PX,
263
+ stroke: HEAD_RING
264
+ },
265
+ {
266
+ type: "text",
267
+ x: headX + LABEL_GAP_PX,
268
+ y: headY + LOLLIPOP_RADIUS_PX,
269
+ text: variant.label
270
+ }
271
+ ]
272
+ });
273
+ }
274
+ }
275
+ const belowBackbone = DOMAIN_LABEL_GAP_PX + FONT_SIZE_PX;
276
+ return {
277
+ width: WIDTH_PX,
278
+ height: backboneY + belowBackbone + MARGIN_Y,
279
+ nodes
280
+ };
281
+ }
282
+ var geneDiagram = {
283
+ keyword: "geneDiagram",
284
+ parse,
285
+ layout
286
+ };
287
+
288
+ // src/modules/pedigreeDiagram.ts
289
+ function coupleId(id1, id2) {
290
+ return [id1, id2].sort().join("-");
291
+ }
292
+ function parse2(rows) {
293
+ const diagram = {};
294
+ if (rows.length === 0) {
295
+ throw new Error("Empty DSL input");
296
+ }
297
+ for (const row of rows) {
298
+ const [keyword, ...rest] = tokenize(row);
299
+ switch (keyword) {
300
+ case "pedigreeDiagram": {
301
+ diagram.type = "pedigreeDiagram";
302
+ break;
303
+ }
304
+ case "couple": {
305
+ const [id1, id2] = rest;
306
+ if (!id1 || !id2) {
307
+ throw new Error(`Invalid couple definition: ${row}`);
308
+ }
309
+ if (!diagram.couples) {
310
+ diagram.couples = [{ id1, id2 }];
311
+ } else {
312
+ diagram.couples.push({ id1, id2 });
313
+ }
314
+ break;
315
+ }
316
+ case "node": {
317
+ const [id, sex, phenotype, genotype, childOf] = rest;
318
+ if (!id || !isSex(sex) || !isPhenotype(phenotype) || !isGenotype(genotype)) {
319
+ throw new Error(`Invalid node definition: ${row}`);
320
+ }
321
+ if (!diagram.nodes) {
322
+ diagram.nodes = [{ id, sex, phenotype, genotype, childOf }];
323
+ } else {
324
+ diagram.nodes.push({ id, sex, phenotype, genotype, childOf });
325
+ }
326
+ break;
327
+ }
328
+ default:
329
+ throw new Error(`Unknown keyword: ${keyword}`);
330
+ }
331
+ }
332
+ return diagram;
333
+ }
334
+ function isSex(value) {
335
+ return value === "male" || value === "female";
336
+ }
337
+ function isPhenotype(value) {
338
+ return value === "affected" || value === "unaffected" || value === "unknown";
339
+ }
340
+ function isGenotype(value) {
341
+ return value === "carrier" || value === "noncarrier" || value === "unknown";
342
+ }
343
+ var ROW_HEIGHT_PX = 60;
344
+ var NODE_GAP_PX = 40;
345
+ var SYMBOL_PX = 24;
346
+ function layout2(diagram) {
347
+ const nodes = [];
348
+ const byId = new Map(diagram.nodes.map((n) => [n.id, n]));
349
+ const coupleById = new Map(diagram.couples.map((c) => [coupleId(c.id1, c.id2), c]));
350
+ const genMemo = new Map;
351
+ const gen = (id) => {
352
+ const cached = genMemo.get(id);
353
+ if (cached !== undefined)
354
+ return cached;
355
+ const node = byId.get(id);
356
+ if (!node?.childOf) {
357
+ genMemo.set(id, 0);
358
+ return 0;
359
+ }
360
+ const parents = coupleById.get(node.childOf);
361
+ if (!parents) {
362
+ throw new Error(`Node ${id} references unknown couple: ${node.childOf}`);
363
+ }
364
+ const g = 1 + Math.max(gen(parents.id1), gen(parents.id2));
365
+ genMemo.set(id, g);
366
+ return g;
367
+ };
368
+ const row = new Map;
369
+ for (const n of diagram.nodes)
370
+ row.set(n.id, gen(n.id));
371
+ for (const c of diagram.couples) {
372
+ const r = Math.max(row.get(c.id1) ?? 0, row.get(c.id2) ?? 0);
373
+ row.set(c.id1, r);
374
+ row.set(c.id2, r);
375
+ }
376
+ const childrenOf = new Map;
377
+ for (const n of diagram.nodes) {
378
+ if (!n.childOf)
379
+ continue;
380
+ (childrenOf.get(n.childOf) ?? childrenOf.set(n.childOf, []).get(n.childOf)).push(n.id);
381
+ }
382
+ const coupleByPartner = new Map;
383
+ for (const c of diagram.couples) {
384
+ coupleByPartner.set(c.id1, c);
385
+ coupleByPartner.set(c.id2, c);
386
+ }
387
+ const x = new Map;
388
+ let cursor = MARGIN_X;
389
+ const slot = () => {
390
+ const at = cursor;
391
+ cursor += SYMBOL_PX + NODE_GAP_PX;
392
+ return at;
393
+ };
394
+ const placed = new Set;
395
+ const placeNode = (id) => {
396
+ if (x.has(id))
397
+ return x.get(id);
398
+ placed.add(id);
399
+ const couple = coupleByPartner.get(id);
400
+ if (couple)
401
+ return placeCouple(couple);
402
+ const at = slot();
403
+ x.set(id, at);
404
+ return at;
405
+ };
406
+ const placeCouple = (c) => {
407
+ const key = coupleId(c.id1, c.id2);
408
+ if (x.has(key))
409
+ return x.get(key);
410
+ x.set(key, MARGIN_X);
411
+ const kids = childrenOf.get(key) ?? [];
412
+ const kidXs = kids.map(placeNode);
413
+ const half = (SYMBOL_PX + NODE_GAP_PX) / 2;
414
+ let mid;
415
+ if (kidXs.length > 0) {
416
+ mid = (Math.min(...kidXs) + Math.max(...kidXs)) / 2;
417
+ x.set(c.id1, mid - half);
418
+ x.set(c.id2, mid + half);
419
+ } else {
420
+ const p1x = slot();
421
+ const p2x = slot();
422
+ x.set(c.id1, p1x);
423
+ x.set(c.id2, p2x);
424
+ mid = (p1x + p2x) / 2;
425
+ }
426
+ x.set(key, mid);
427
+ return mid;
428
+ };
429
+ for (const c of diagram.couples) {
430
+ if ((row.get(c.id1) ?? 0) === Math.min(row.get(c.id1) ?? 0, row.get(c.id2) ?? 0)) {
431
+ placeCouple(c);
432
+ }
433
+ }
434
+ for (const n of diagram.nodes)
435
+ if (!placed.has(n.id))
436
+ placeNode(n.id);
437
+ const pos = new Map;
438
+ for (const n of diagram.nodes) {
439
+ pos.set(n.id, {
440
+ x: x.get(n.id) ?? MARGIN_X,
441
+ y: MARGIN_Y + (row.get(n.id) ?? 0) * ROW_HEIGHT_PX
442
+ });
443
+ }
444
+ for (const n of diagram.nodes) {
445
+ const p = pos.get(n.id);
446
+ nodes.push(symbol(n, p.x, p.y));
447
+ }
448
+ const SIB_BAR_DROP_PX = ROW_HEIGHT_PX / 2;
449
+ for (const c of diagram.couples) {
450
+ const a = pos.get(c.id1);
451
+ const b = pos.get(c.id2);
452
+ const [left, right] = a.x <= b.x ? [a, b] : [b, a];
453
+ const marriageY = left.y + SYMBOL_PX / 2;
454
+ nodes.push({
455
+ type: "line",
456
+ x1: left.x + SYMBOL_PX,
457
+ y1: marriageY,
458
+ x2: right.x,
459
+ y2: marriageY
460
+ });
461
+ const kids = childrenOf.get(coupleId(c.id1, c.id2)) ?? [];
462
+ if (kids.length === 0)
463
+ continue;
464
+ const midX = (left.x + SYMBOL_PX + right.x) / 2;
465
+ const barY = marriageY + SIB_BAR_DROP_PX;
466
+ const kidCenters = kids.map((kid) => pos.get(kid).x + SYMBOL_PX / 2);
467
+ nodes.push({ type: "line", x1: midX, y1: marriageY, x2: midX, y2: barY });
468
+ nodes.push({
469
+ type: "line",
470
+ x1: Math.min(midX, ...kidCenters),
471
+ y1: barY,
472
+ x2: Math.max(midX, ...kidCenters),
473
+ y2: barY
474
+ });
475
+ for (const kid of kids) {
476
+ const k = pos.get(kid);
477
+ nodes.push({ type: "line", x1: k.x + SYMBOL_PX / 2, y1: barY, x2: k.x + SYMBOL_PX / 2, y2: k.y });
478
+ }
479
+ }
480
+ const maxRow = Math.max(0, ...row.values());
481
+ return {
482
+ width: WIDTH_PX,
483
+ height: MARGIN_Y * 2 + maxRow * ROW_HEIGHT_PX + SYMBOL_PX,
484
+ nodes
485
+ };
486
+ }
487
+ function symbol(n, x, y) {
488
+ const affected = n.phenotype === "affected";
489
+ const carrier = n.genotype === "carrier";
490
+ const base = affected ? "black" : "white";
491
+ const r = SYMBOL_PX / 2;
492
+ const children = n.sex === "male" ? [{ type: "rect", x: 0, y: 0, width: SYMBOL_PX, height: SYMBOL_PX, fill: base }] : [{ type: "circle", x: r, y: r, radius: r, color: base, stroke: "black" }];
493
+ if (carrier && !affected) {
494
+ children.push(n.sex === "male" ? { type: "rect", x: 0, y: 0, width: r, height: SYMBOL_PX, fill: "black" } : { type: "path", d: `M ${r} 0 A ${r} ${r} 0 0 0 ${r} ${SYMBOL_PX} Z`, fill: "black", stroke: "black" });
495
+ }
496
+ return { type: "group", x, y, children };
497
+ }
498
+ var pedigreeDiagram = {
499
+ keyword: "pedigreeDiagram",
500
+ parse: parse2,
501
+ layout: layout2
502
+ };
503
+
504
+ // src/registry.ts
505
+ var registry = {
506
+ geneDiagram,
507
+ pedigreeDiagram
508
+ };
509
+
510
+ // index.ts
511
+ function parse3(dsl) {
512
+ const rows = splitRows(dsl);
513
+ const diagramKeyword = rows[0];
514
+ const module = diagramKeyword ? registry[diagramKeyword] : undefined;
515
+ if (!module) {
516
+ throw new Error(`Unknown diagram type: ${diagramKeyword ?? "(empty)"}`);
517
+ }
518
+ return module.parse(rows);
519
+ }
520
+ function render2(dsl) {
521
+ const rows = splitRows(dsl);
522
+ const diagramKeyword = rows[0];
523
+ if (!diagramKeyword) {
524
+ return null;
525
+ }
526
+ const module = registry[diagramKeyword];
527
+ if (!module) {
528
+ return null;
529
+ }
530
+ try {
531
+ const diagram = module.parse(rows);
532
+ const layout3 = module.layout(diagram);
533
+ return render(layout3);
534
+ } catch {
535
+ return null;
536
+ }
537
+ }
538
+ export {
539
+ splitRows,
540
+ render2 as render,
541
+ parse3 as parse
542
+ };
@@ -0,0 +1,2 @@
1
+ export declare function splitRows(text: string): string[];
2
+ export declare function tokenize(text: string): string[];
@@ -0,0 +1,26 @@
1
+ import type { DiagramModule, Layout } from "../types";
2
+ export type GeneDiagram = {
3
+ type: "geneDiagram";
4
+ gene: string;
5
+ /** Protein length in amino acids. Defines the right end of the backbone. */
6
+ length: number;
7
+ domains: Domain[];
8
+ variants: Variant[];
9
+ };
10
+ export interface Domain {
11
+ /** First residue of the domain (amino-acid position, 1-based). */
12
+ start: number;
13
+ /** Last residue of the domain (amino-acid position, 1-based). */
14
+ end: number;
15
+ label?: string;
16
+ }
17
+ export interface Variant {
18
+ label: string;
19
+ /** Residue number the variant affects (amino-acid position, 1-based). */
20
+ pos: number;
21
+ class: VariantClass;
22
+ }
23
+ export type VariantClass = "missense" | "nonsense" | "frameshift" | "splice" | "synonymous";
24
+ export declare function parse(rows: string[]): GeneDiagram;
25
+ export declare function layout(ast: GeneDiagram): Layout;
26
+ export declare const geneDiagram: DiagramModule<GeneDiagram>;
@@ -0,0 +1,21 @@
1
+ import type { DiagramModule, Layout } from "../types";
2
+ export interface PedigreeDiagram {
3
+ type: "pedigreeDiagram";
4
+ couples: Couple[];
5
+ nodes: PedigreeNode[];
6
+ }
7
+ interface Couple {
8
+ id1: string;
9
+ id2: string;
10
+ }
11
+ interface PedigreeNode {
12
+ id: string;
13
+ sex: "male" | "female";
14
+ phenotype: "affected" | "unaffected" | "unknown";
15
+ genotype: "carrier" | "noncarrier" | "unknown";
16
+ childOf?: string;
17
+ }
18
+ export declare function parse(rows: string[]): PedigreeDiagram;
19
+ export declare function layout(diagram: PedigreeDiagram): Layout;
20
+ export declare const pedigreeDiagram: DiagramModule<PedigreeDiagram>;
21
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { GeneDiagram } from "./modules/geneDiagram";
2
+ import type { PedigreeDiagram } from "./modules/pedigreeDiagram";
3
+ import type { DiagramModule } from "./types";
4
+ export type DiagramType = GeneDiagram["type"] | PedigreeDiagram["type"];
5
+ export declare const registry: Record<DiagramType, DiagramModule<unknown>>;
@@ -0,0 +1,7 @@
1
+ import type { Layout, LayoutNode, SVGString } from "./types";
2
+ export declare const WIDTH_PX = 800;
3
+ export declare const MARGIN_X = 10;
4
+ export declare const MARGIN_Y = 14;
5
+ export declare const INNER_WIDTH: number;
6
+ export declare function renderNode(node: LayoutNode): string;
7
+ export declare function render(layout: Layout): SVGString;
@@ -0,0 +1,59 @@
1
+ import type { GeneDiagram } from "./modules/geneDiagram";
2
+ export type Diagram = GeneDiagram;
3
+ export type SVGString = string;
4
+ export type Layout = {
5
+ width: number;
6
+ height: number;
7
+ nodes: LayoutNode[];
8
+ };
9
+ export type LayoutNode = Line | Rect | Text | Circle | Path | Group;
10
+ type Path = {
11
+ type: "path";
12
+ d: string;
13
+ fill?: string;
14
+ stroke?: string;
15
+ };
16
+ type Circle = {
17
+ type: "circle";
18
+ x: number;
19
+ y: number;
20
+ radius: number;
21
+ color: string;
22
+ stroke?: string;
23
+ };
24
+ type Text = {
25
+ type: "text";
26
+ x: number;
27
+ y: number;
28
+ text: string;
29
+ baseline?: "hanging" | "base";
30
+ anchor?: "start" | "middle" | "end";
31
+ };
32
+ type Rect = {
33
+ type: "rect";
34
+ x: number;
35
+ y: number;
36
+ width: number;
37
+ height: number;
38
+ fill?: string;
39
+ stroke?: string;
40
+ };
41
+ type Line = {
42
+ type: "line";
43
+ x1: number;
44
+ x2: number;
45
+ y1: number;
46
+ y2: number;
47
+ };
48
+ type Group = {
49
+ type: "group";
50
+ x: number;
51
+ y: number;
52
+ children: LayoutNode[];
53
+ };
54
+ export interface DiagramModule<AST> {
55
+ keyword: string;
56
+ parse(rows: string[]): AST;
57
+ layout(ast: AST): Layout;
58
+ }
59
+ export {};
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@gene-code/core",
3
+ "version": "0.0.1",
4
+ "description": "Core DSL parser and SVG renderer for gene-code diagrams.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/neznayer/gene-code.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "bun build ./index.ts --outdir dist --target node --format esm && tsc -p tsconfig.build.json",
27
+ "prepublishOnly": "bun run build"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5"
31
+ }
32
+ }