From b51f700cfb8a4fc4d6538b2861dd3e48675aa19c Mon Sep 17 00:00:00 2001 From: asivery Date: Fri, 24 Oct 2025 11:52:49 +0200 Subject: [PATCH 1/3] feat: Add initial support for named edges --- src/core/adot/ast.ts | 6 ++++ src/core/adot/evaluator.ts | 3 ++ src/core/adot/lexer.ts | 30 ++++++++++++++++++++ src/core/adot/parser.ts | 6 ++++ src/core/adot/token.ts | 1 + src/core/simulator/graph.ts | 1 + src/features/graph-view/components/Edge.tsx | 10 +++---- src/features/graph-view/components/Edges.tsx | 2 +- src/pages/editor/store/editor.ts | 4 +++ 9 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/core/adot/ast.ts b/src/core/adot/ast.ts index 7d639b6..3cb9229 100644 --- a/src/core/adot/ast.ts +++ b/src/core/adot/ast.ts @@ -54,6 +54,12 @@ export class NumberLiteral extends Expression { } } +export class StringLiteral extends Expression { + constructor(public readonly token: Token, public value: string) { + super(); + } +} + export class Identifier extends Expression { constructor(public readonly token: Token, public value: string) { super(); diff --git a/src/core/adot/evaluator.ts b/src/core/adot/evaluator.ts index a07a86a..9301612 100644 --- a/src/core/adot/evaluator.ts +++ b/src/core/adot/evaluator.ts @@ -66,6 +66,9 @@ export class Evaluator { attributesList["cost"] && typeof attributesList["cost"] === "number" ? attributesList["cost"] : null, + attributesList["name"] && typeof attributesList["name"] === "string" + ? attributesList["name"] + : null, isDirected ); diff --git a/src/core/adot/lexer.ts b/src/core/adot/lexer.ts index e86881d..a3ab52f 100644 --- a/src/core/adot/lexer.ts +++ b/src/core/adot/lexer.ts @@ -29,6 +29,14 @@ export class Lexer { token = this.newToken(TOKEN_TYPE.Illegal, this.char + this.peekChar()); } break; + case '"': + const string = this.readString(); + if (string === null) { + token = this.newToken(TOKEN_TYPE.Illegal, this.char); + } else { + token = this.newToken(TOKEN_TYPE.String, string); + } + break; case "=": token = this.newToken(TOKEN_TYPE.Eq, this.char); break; @@ -67,6 +75,28 @@ export class Lexer { return token; } + private readString(): string | null { + let output = ""; + this.readChar(); // Skip the initial quote + let escape = false; + while (this.char != '"' || escape) { + if (this.char == "\n") return null; // Early EoL + if (escape) { + output += this.char; + escape = false; + } else { + if (this.char == "\\") { + escape = true; + } else { + output += this.char; + } + } + this.readChar(); + } + + return output; + } + private readNumber(): string { const position = this.pos; diff --git a/src/core/adot/parser.ts b/src/core/adot/parser.ts index ffde2c0..243ce5a 100644 --- a/src/core/adot/parser.ts +++ b/src/core/adot/parser.ts @@ -205,6 +205,8 @@ export class Parser { return this.parseIdentifier(); case TOKEN_TYPE.Number: return this.parseNumberLiteral(); + case TOKEN_TYPE.String: + return this.parseStringLiteral(); default: throw this.error(errorFmtAt(this.currToken, "Expected an expression.")); } @@ -214,6 +216,10 @@ export class Parser { return new ast.NumberLiteral(this.currToken, parseFloat(this.currToken.literal)); } + private parseStringLiteral(): ast.StringLiteral { + return new ast.StringLiteral(this.currToken, this.currToken.literal); + } + private parseIdentifier(): ast.Identifier { return new ast.Identifier(this.currToken, this.currToken.literal); } diff --git a/src/core/adot/token.ts b/src/core/adot/token.ts index d9b1bff..ea3c47e 100644 --- a/src/core/adot/token.ts +++ b/src/core/adot/token.ts @@ -14,6 +14,7 @@ export const TOKEN_TYPE = { Eq: "EQ", Illegal: "ILLEGAL", EOF: "EOF", + String: "STRING", } as const; export type TokenType = (typeof TOKEN_TYPE)[keyof typeof TOKEN_TYPE]; diff --git a/src/core/simulator/graph.ts b/src/core/simulator/graph.ts index a45696b..dc85e1f 100644 --- a/src/core/simulator/graph.ts +++ b/src/core/simulator/graph.ts @@ -13,6 +13,7 @@ export class Edge { public readonly from: string, public readonly to: string, public weight: number | null, + public name: string | null, public readonly directed: boolean ) {} diff --git a/src/features/graph-view/components/Edge.tsx b/src/features/graph-view/components/Edge.tsx index db632e7..fe086c3 100644 --- a/src/features/graph-view/components/Edge.tsx +++ b/src/features/graph-view/components/Edge.tsx @@ -17,7 +17,7 @@ export type EdgeProps = { position?: number; circular?: boolean; thicken?: boolean; - weight?: number; + name?: string; }; export function Edge(props: EdgeProps) { @@ -34,7 +34,7 @@ function StraightEdge({ color = "slate", thicken = false, position = 0, - weight, + name, }: EdgeProps) { const edgePathRef = useRef(null); @@ -69,7 +69,7 @@ function StraightEdge({ d={path} stroke={stroke} /> - {weight && } + {name && } {directed && } ); @@ -81,7 +81,7 @@ const getPathCenter = (path: SVGPathElement) => { return new Vec2(centerPoint.x, centerPoint.y); }; -const CircularEdge = ({ x, y, directed, position = 0, color = "slate", weight }: EdgeProps) => { +const CircularEdge = ({ x, y, directed, position = 0, color = "slate", name }: EdgeProps) => { const radius = 6 * position + 16; const cx = radius + vertexRadius / 1.61; const transform = `translate(${x} ${y}) rotate(65)`; @@ -103,7 +103,7 @@ const CircularEdge = ({ x, y, directed, position = 0, color = "slate", weight }: r={radius} stroke={stroke} /> - {weight && } + {name && } {directed && } ); diff --git a/src/features/graph-view/components/Edges.tsx b/src/features/graph-view/components/Edges.tsx index 6009cc9..0fd4dc7 100644 --- a/src/features/graph-view/components/Edges.tsx +++ b/src/features/graph-view/components/Edges.tsx @@ -34,7 +34,7 @@ export function Edges({ positionedEdges, arrangement, highlights }: EdgesProps) circular={isCircular} color={color} thicken={!!color} - weight={edge.weight ?? undefined} + name={edge.weight?.toString() ?? edge.name ?? undefined} /> ); }); diff --git a/src/pages/editor/store/editor.ts b/src/pages/editor/store/editor.ts index dada06f..80523ff 100644 --- a/src/pages/editor/store/editor.ts +++ b/src/pages/editor/store/editor.ts @@ -58,6 +58,10 @@ const initialCode = `graph { c -- c0 [cost=0.3]; d -- d0 [cost=0.3]; e -- e0 [cost=0.8]; + + # You can also give edges explicit names + + x -- z [name="Connection"] } `; From 85eaf634235dadf160241f6047e26585be3128c8 Mon Sep 17 00:00:00 2001 From: asivery Date: Fri, 24 Oct 2025 11:53:55 +0200 Subject: [PATCH 2/3] feat: Add lexer tests for string literal --- src/core/adot/lexer.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/core/adot/lexer.test.ts b/src/core/adot/lexer.test.ts index caace65..dd19c98 100644 --- a/src/core/adot/lexer.test.ts +++ b/src/core/adot/lexer.test.ts @@ -11,6 +11,8 @@ describe("Lexer next token", () => { a -- b; c [cost=10.23] c -- d0 [cost=10] + + x -- y [name="test"] } test {} @@ -54,6 +56,16 @@ subgraph [TOKEN_TYPE.Number, "10"], [TOKEN_TYPE.RBracket, "]"], + // x -- y [name="test"] + [TOKEN_TYPE.Id, "x"], + [TOKEN_TYPE.Edge, "--"], + [TOKEN_TYPE.Id, "y"], + [TOKEN_TYPE.LBracket, "["], + [TOKEN_TYPE.Id, "name"], + [TOKEN_TYPE.Eq, "="], + [TOKEN_TYPE.String, "test"], + [TOKEN_TYPE.RBracket, "]"], + // } [TOKEN_TYPE.RBrace, "}"], From 24edc150e1aa99a83b68ff96bd621789f844e4b8 Mon Sep 17 00:00:00 2001 From: asivery Date: Fri, 24 Oct 2025 20:25:15 +0200 Subject: [PATCH 3/3] fix: Allow edges to have both names and weights --- src/features/graph-view/components/Edges.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/features/graph-view/components/Edges.tsx b/src/features/graph-view/components/Edges.tsx index 0fd4dc7..1a0fa41 100644 --- a/src/features/graph-view/components/Edges.tsx +++ b/src/features/graph-view/components/Edges.tsx @@ -22,6 +22,15 @@ export function Edges({ positionedEdges, arrangement, highlights }: EdgesProps) const { x: toX, y: toY } = arrangement[edge.to] ?? centerPosition; const isCircular = edge.from === edge.to; + let name = edge.name ?? undefined; + if (edge.weight !== null) { + if (name) { + name += `: ${edge.weight}`; + } else { + name = edge.weight.toString(); + } + } + return ( ); });