diff --git a/README.md b/README.md index eab2209..3e0076c 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,9 @@ excalidraw-cli convert diagram.excalidraw --format svg --no-export-background | `->` | Arrow | Forward connection | | `<-` | Reverse Arrow | Reverse connection, logically parsed as right-to-left | | `<->` | Bidirectional Arrow | Connection with arrowheads on both ends | -| `-->` | Dashed Arrow | Dashed connection | +| `-->` | Dashed Arrow | Dashed forward connection | +| `<--` | Dashed Reverse Arrow | Dashed reversed connection | +| `<-->` | Dashed Bidirectional Arrow | Dashed connection with arrowheads on both ends | | `-> "text" ->` | Labeled Arrow | Connection with label | ### Example DSL @@ -104,6 +106,9 @@ excalidraw-cli convert diagram.excalidraw --format svg --no-export-background (Start) -> [Enter Credentials] -> {Valid?} {Valid?} -> "yes" -> [Dashboard] -> (End) {Valid?} -> "no" -> [Show Error] -> [Enter Credentials] +[Client] --> [Webhook] +[Worker] <-- [Queue] +[Service A] <--> [Service B] [Reviewer] <- [Approved] [Client] <-> [API] ``` diff --git a/package-lock.json b/package-lock.json index 814a277..54a6bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@swiftlysingh/excalidraw-cli", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@swiftlysingh/excalidraw-cli", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@excalidraw/utils": "0.1.3-test32", diff --git a/src/parser/dsl-parser.ts b/src/parser/dsl-parser.ts index 3d8b4ad..0bcb8fd 100644 --- a/src/parser/dsl-parser.ts +++ b/src/parser/dsl-parser.ts @@ -443,6 +443,25 @@ function tokenize(input: string): Token[] { continue; } + // Dashed bidirectional arrow <--> + if ( + input[i] === '<' && + input[i + 1] === '-' && + input[i + 2] === '-' && + input[i + 3] === '>' + ) { + tokens.push({ type: 'arrow', value: '<-->', dashed: true, bidirectional: true }); + i += 4; + continue; + } + + // Dashed reverse arrow <-- + if (input[i] === '<' && input[i + 1] === '-' && input[i + 2] === '-') { + tokens.push({ type: 'arrow', value: '<--', dashed: true, reversed: true }); + i += 3; + continue; + } + // Bidirectional arrow <-> if (input[i] === '<' && input[i + 1] === '-' && input[i + 2] === '>') { tokens.push({ type: 'arrow', value: '<->', bidirectional: true }); diff --git a/tests/unit/parser/dsl-parser.test.ts b/tests/unit/parser/dsl-parser.test.ts index 23c9448..e9022e3 100644 --- a/tests/unit/parser/dsl-parser.test.ts +++ b/tests/unit/parser/dsl-parser.test.ts @@ -95,7 +95,49 @@ describe('DSL Parser', () => { it('should parse dashed connections', () => { const result = parseDSL('[A] --> [B]'); - expect(result.edges[0].style?.strokeStyle).toBe('dashed'); + expect(result.edges[0].source).toBe(result.nodes[0].id); + expect(result.edges[0].target).toBe(result.nodes[1].id); + expect(result.edges[0].style).toEqual({ strokeStyle: 'dashed' }); + }); + + it('should parse dashed reverse connections', () => { + const result = parseDSL('[A] <-- [B]'); + expect(result.edges[0].source).toBe(result.nodes[1].id); + expect(result.edges[0].target).toBe(result.nodes[0].id); + expect(result.edges[0].style).toEqual({ + strokeStyle: 'dashed', + startArrowhead: 'arrow', + endArrowhead: null, + }); + }); + + it('should parse dashed bidirectional connections', () => { + const result = parseDSL('[A] <--> [B]'); + expect(result.edges[0].source).toBe(result.nodes[0].id); + expect(result.edges[0].target).toBe(result.nodes[1].id); + expect(result.edges[0].style).toEqual({ + strokeStyle: 'dashed', + startArrowhead: 'arrow', + endArrowhead: 'arrow', + }); + }); + + it('should preserve solid reverse and bidirectional semantics', () => { + const reverse = parseDSL('[A] <- [B]'); + expect(reverse.edges[0].source).toBe(reverse.nodes[1].id); + expect(reverse.edges[0].target).toBe(reverse.nodes[0].id); + expect(reverse.edges[0].style).toEqual({ + startArrowhead: 'arrow', + endArrowhead: null, + }); + + const bidirectional = parseDSL('[A] <-> [B]'); + expect(bidirectional.edges[0].source).toBe(bidirectional.nodes[0].id); + expect(bidirectional.edges[0].target).toBe(bidirectional.nodes[1].id); + expect(bidirectional.edges[0].style).toEqual({ + startArrowhead: 'arrow', + endArrowhead: 'arrow', + }); }); it('should parse reverse connections as logical right-to-left edges', () => { @@ -347,5 +389,35 @@ describe('DSL Parser', () => { backgroundColor: '#a5d8ff', }); }); + + it('should generate dashed reverse arrows with dashed styling and reversed arrowheads', async () => { + const graph = parseDSL('[A] <-- [B]'); + const layoutedGraph = await layoutGraph(graph); + const excalidrawFile = generateExcalidraw(layoutedGraph); + const arrow = excalidrawFile.elements.find( + (element) => element.type === 'arrow' && element.id === graph.edges[0].id + ); + + expect(arrow).toMatchObject({ + strokeStyle: 'dashed', + startArrowhead: 'arrow', + endArrowhead: null, + }); + }); + + it('should generate dashed bidirectional arrows with arrowheads on both ends', async () => { + const graph = parseDSL('[A] <--> [B]'); + const layoutedGraph = await layoutGraph(graph); + const excalidrawFile = generateExcalidraw(layoutedGraph); + const arrow = excalidrawFile.elements.find( + (element) => element.type === 'arrow' && element.id === graph.edges[0].id + ); + + expect(arrow).toMatchObject({ + strokeStyle: 'dashed', + startArrowhead: 'arrow', + endArrowhead: 'arrow', + }); + }); }); });