Skip to content

feat: support escaped quoted edge labels in DSL#28

Open
swiftlysingh wants to merge 1 commit intomainfrom
feat/issue-20-quote-escaping
Open

feat: support escaped quoted edge labels in DSL#28
swiftlysingh wants to merge 1 commit intomainfrom
feat/issue-20-quote-escaping

Conversation

@swiftlysingh
Copy link
Copy Markdown
Owner

@swiftlysingh swiftlysingh commented Apr 10, 2026

Summary

  • add proper single-quoted and double-quoted edge label parsing
  • preserve escaped quotes and backslashes in edge labels
  • reject ambiguous labeled-edge forms like --> "x" -> instead of silently guessing
  • add parser, integration, and README coverage for real CLI usage

Testing

  • npm run build
  • npm run lint
  • npm run test:run
  • node dist/cli.js parse tests/tmp/edge-label-parse.dsl
  • node dist/cli.js create --inline "[API] -> \"GET /users?name=\\\"pp\\\" \\\\ cache\" -> [Client]" -o tests/tmp/edge-label-demo.excalidraw

Notes

  • npm run test:run still has pre-existing failures in tests/integration/exporter/cli-export.test.ts because that suite assumes a built dist/cli.js in a way this repo currently doesn't guarantee during Vitest runs. The new parser/unit/integration coverage for this feature passes.

Summary by CodeRabbit

  • New Features

    • Edge labels now support both single and double-quote syntax with improved escape character handling.
  • Documentation

    • Added edge label escaping guidance with shell-escaping examples and special character support details.
  • Tests

    • Added comprehensive edge label tests covering quote styles, escaping, rendering, and error scenarios.

Copilot AI review requested due to automatic review settings April 10, 2026 21:28
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

The DSL parser now supports edge labels with both single and double quote syntax. A new parseQuotedLabel() helper handles quoted label parsing with backslash escaping and preserves the original raw text. Labeled-edge validation enforces strict sequence matching (-> + label + ->), rejecting malformed combinations. Documentation and comprehensive tests have been added to cover the new functionality.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added "Edge label escaping" section with examples of single-quoted and double-quoted edge labels, including shell-escaping guidance and escaped content examples.
Parser Implementation
src/parser/dsl-parser.ts
Extended Token type with optional raw field; introduced parseQuotedLabel() helper supporting both single and double quotes with backslash escaping; enhanced labeled-edge validation to strictly match -> + label + -> sequence with explicit error reporting using raw label text.
Parser Unit Tests
tests/unit/parser/dsl-parser.test.ts
Added tests covering single-quoted and double-quoted edge labels, escaped characters preservation, and error cases for malformed arrow combinations and unterminated quoted strings.
Integration Tests
tests/integration/exporter/edge-labels.test.ts
Added end-to-end tests validating edge label handling in generated Excalidraw JSON, including escaped characters and apostrophes, with assertions on arrow-text element linkage and label content accuracy.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Through single quotes and doubles too,
Your edge labels now shine bright and new—
Escaped characters dance in their place,
With arrows and text in perfect embrace! ✨
The parser's robust, the tests all pass,
A cleaner DSL for the whole mass!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support escaped quoted edge labels in DSL' clearly and specifically describes the main change: adding support for escaped quoted edge labels in the DSL parser.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-20-quote-escaping

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds robust parsing for quoted (single/double) edge labels in the DSL, preserving escapes and rejecting ambiguous labeled-edge arrow forms, with accompanying tests and documentation updates.

Changes:

  • Extend DSL tokenizer/parser to support both single- and double-quoted edge labels (with escape preservation) and enforce the strict -> "label" -> / -> 'label' -> form.
  • Add unit and integration coverage for escaped quotes/backslashes and for invalid/ambiguous labeled-edge syntax.
  • Update README with examples and CLI-focused guidance for label escaping.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/parser/dsl-parser.ts Adds quoted-label parsing (single/double) and stricter labeled-edge syntax validation with clearer error reporting.
tests/unit/parser/dsl-parser.test.ts Adds unit tests covering new quoting/escaping behavior and malformed labeled-edge rejections.
tests/integration/exporter/edge-labels.test.ts New end-to-end tests verifying labels bind to arrows and single-quoted labels work through generation.
README.md Documents single-quoted labels and shell escaping guidance for CLI usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/parser/dsl-parser.ts
Comment on lines +302 to +307
const escaped = input[i + 1];
if (escaped === quote || escaped === '\\') {
value += escaped;
} else {
value += escaped;
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseQuotedLabel has a redundant conditional: both branches append escaped, so the if (escaped === quote || escaped === "\\") block is dead code. Simplify to a single append (or adjust logic if you intended different behavior for non-quote/non-backslash escapes).

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +17
const arrow = file.elements.find((element: any) => element.type === 'arrow');
const text = file.elements.find(
(element: any) => element.type === 'text' && element.containerId === arrow?.id
);

expect(arrow).toBeTruthy();
expect(arrow.boundElements).toEqual([{ id: text.id, type: 'text' }]);
expect(text).toBeTruthy();
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses text.id before asserting text is defined. If the label binding breaks, this will throw a TypeError and hide the actual failure. Assert text exists before dereferencing (and consider finding text only after verifying arrow).

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
describe('edge label integration', () => {
it('binds escaped edge labels to arrows in generated Excalidraw output', async () => {
const dsl = String.raw`[API] -> "GET /users?name=\"pp\" \\ path" -> [Client]`;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These integration tests call createFlowchartFromDSL (which runs ELK layout) but don't set an explicit timeout, while other exporter integration tests use 30s. Consider adding a timeout to avoid CI flakiness on slower runners.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/integration/exporter/edge-labels.test.ts (1)

10-19: Assert the lookup results before dereferencing them.

text.id, text.text, and arrow.id are read before the test has conclusively proven those elements exist, so a regression here can surface as a TypeError instead of a clear assertion failure.

✅ Suggested cleanup
     const arrow = file.elements.find((element: any) => element.type === 'arrow');
-    const text = file.elements.find(
-      (element: any) => element.type === 'text' && element.containerId === arrow?.id
-    );
-
-    expect(arrow).toBeTruthy();
-    expect(arrow.boundElements).toEqual([{ id: text.id, type: 'text' }]);
-    expect(text).toBeTruthy();
-    expect(text.text).toBe('GET /users?name="pp" \\ path');
-    expect(text.containerId).toBe(arrow.id);
+    expect(arrow).toBeTruthy();
+
+    const text = file.elements.find(
+      (element: any) => element.type === 'text' && element.containerId === arrow!.id
+    );
+    expect(text).toBeTruthy();
+
+    expect(arrow!.boundElements).toEqual([{ id: text!.id, type: 'text' }]);
+    expect(text!.text).toBe('GET /users?name="pp" \\ path');
+    expect(text!.containerId).toBe(arrow!.id);
   });
 
   it('supports single-quoted edge labels end to end', async () => {
     const raw = await createFlowchartFromDSL(String.raw`[Decision] -> 'team\'s choice' -> [Next]`);
     const file = JSON.parse(raw);
 
     const text = file.elements.find((element: any) => element.type === 'text' && element.containerId);
-    expect(text.text).toBe("team's choice");
+    expect(text).toBeTruthy();
+    expect(text!.text).toBe("team's choice");
   });

Also applies to: 26-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/exporter/edge-labels.test.ts` around lines 10 - 19, The
test currently dereferences text.id, text.text and arrow.id before ensuring
those objects exist; update the assertions in
tests/integration/exporter/edge-labels.test.ts to first assert arrow and text
are truthy (e.g., expect(arrow).toBeTruthy(); expect(text).toBeTruthy();) before
accessing arrow.id or text.id/text.text, and then perform the deeper checks
(boundElements, containerId, and text content) so any missing element yields a
clear assertion failure instead of a TypeError; apply the same change to the
other occurrences around lines 26-27.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 102-107: The fenced DSL block containing "(Start) -> [Enter
Credentials] -> {Valid?} ..." is missing a language tag which trips markdownlint
MD040; update that fenced code block in README.md by adding the "excalidraw"
language tag (i.e., change the opening ``` to ```excalidraw) so the snippet
(including lines with {Valid?} -> "yes" -> [Dashboard] and [API] -> "GET
/users?name=\"pp\" \\ cache" -> [Client]) is syntax-highlighted and consistent
with the other DSL examples.

In `@src/parser/dsl-parser.ts`:
- Around line 750-770: The parser currently accepts labeled-edge syntax even
when there is no source node (e.g., `-> "x" -> [B]`) because the branch that
handles arrow + label sets pendingLabel/pendingDashed and advances i without
verifying a source; update the branch in the function handling tokens (the code
referencing token, nextToken, trailingArrow, targetNodeToken, pendingLabel,
pendingDashed, i) to verify a valid source (e.g., that lastNode is not null or
the previous token was a node/image) before setting pendingLabel; if no source
exists, throw a descriptive Error (similar to the other labeled-edge errors)
instead of silently accepting, and add a regression test for the `-> "x" -> [B]`
case.

---

Nitpick comments:
In `@tests/integration/exporter/edge-labels.test.ts`:
- Around line 10-19: The test currently dereferences text.id, text.text and
arrow.id before ensuring those objects exist; update the assertions in
tests/integration/exporter/edge-labels.test.ts to first assert arrow and text
are truthy (e.g., expect(arrow).toBeTruthy(); expect(text).toBeTruthy();) before
accessing arrow.id or text.id/text.text, and then perform the deeper checks
(boundElements, containerId, and text content) so any missing element yields a
clear assertion failure instead of a TypeError; apply the same change to the
other occurrences around lines 26-27.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8bb0437-5d2f-41a6-a709-9e0e8d5d6fc6

📥 Commits

Reviewing files that changed from the base of the PR and between a26f7dd and 65261e4.

📒 Files selected for processing (4)
  • README.md
  • src/parser/dsl-parser.ts
  • tests/integration/exporter/edge-labels.test.ts
  • tests/unit/parser/dsl-parser.test.ts

Comment thread README.md
Comment on lines 102 to +107
```
(Start) -> [Enter Credentials] -> {Valid?}
{Valid?} -> "yes" -> [Dashboard] -> (End)
{Valid?} -> "no" -> [Show Error] -> [Enter Credentials]
{Valid?} -> 'no' -> [Show Error] -> [Enter Credentials]
[API] -> "GET /users?name=\"pp\" \\ cache" -> [Client]
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language tag to this DSL example fence.

This block trips markdownlint MD040 and loses syntax highlighting; excalidraw would match the other DSL snippets in this README.

📝 Suggested change
-```
+```excalidraw
 (Start) -> [Enter Credentials] -> {Valid?}
 {Valid?} -> "yes" -> [Dashboard] -> (End)
 {Valid?} -> 'no' -> [Show Error] -> [Enter Credentials]
 [API] -> "GET /users?name=\"pp\" \\ cache" -> [Client]
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 102-102: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 102 - 107, The fenced DSL block containing "(Start)
-> [Enter Credentials] -> {Valid?} ..." is missing a language tag which trips
markdownlint MD040; update that fenced code block in README.md by adding the
"excalidraw" language tag (i.e., change the opening ``` to ```excalidraw) so the
snippet (including lines with {Valid?} -> "yes" -> [Dashboard] and [API] -> "GET
/users?name=\"pp\" \\ cache" -> [Client]) is syntax-highlighted and consistent
with the other DSL examples.

Comment thread src/parser/dsl-parser.ts
Comment on lines 750 to +770
if (token.type === 'arrow') {
const nextToken = tokens[i + 1];
if (nextToken?.type === 'label') {
const trailingArrow = tokens[i + 2];
const targetNodeToken = tokens[i + 3];

if (token.value !== '->' || trailingArrow?.type !== 'arrow' || trailingArrow.value !== '->') {
const rawLabel = nextToken.raw ?? JSON.stringify(nextToken.value);
throw new Error(
`Invalid labeled edge syntax around ${rawLabel}: expected [A] -> ${rawLabel} -> [B], got ${formatArrowToken(token)} ${rawLabel} ${formatArrowToken(trailingArrow)}`
);
}

if (!targetNodeToken || (targetNodeToken.type !== 'node' && targetNodeToken.type !== 'image')) {
const rawLabel = nextToken.raw ?? JSON.stringify(nextToken.value);
throw new Error(`Invalid labeled edge syntax around ${rawLabel}: missing target node after label`);
}

pendingDashed = false;
pendingLabel = nextToken.value;
i += 3;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject labeled edges that have no source node.

-> "x" -> [B] currently passes this branch, sets pendingLabel, and then gets silently discarded because lastNode is still null. The parser should fail fast here instead of accepting malformed DSL. A regression test for that exact input would be worth adding with the fix.

🐛 Suggested fix
     if (token.type === 'arrow') {
       const nextToken = tokens[i + 1];
       if (nextToken?.type === 'label') {
+        if (!lastNode) {
+          const rawLabel = nextToken.raw ?? JSON.stringify(nextToken.value);
+          throw new Error(`Invalid labeled edge syntax around ${rawLabel}: missing source node before label`);
+        }
+
         const trailingArrow = tokens[i + 2];
         const targetNodeToken = tokens[i + 3];
 
         if (token.value !== '->' || trailingArrow?.type !== 'arrow' || trailingArrow.value !== '->') {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parser/dsl-parser.ts` around lines 750 - 770, The parser currently
accepts labeled-edge syntax even when there is no source node (e.g., `-> "x" ->
[B]`) because the branch that handles arrow + label sets
pendingLabel/pendingDashed and advances i without verifying a source; update the
branch in the function handling tokens (the code referencing token, nextToken,
trailingArrow, targetNodeToken, pendingLabel, pendingDashed, i) to verify a
valid source (e.g., that lastNode is not null or the previous token was a
node/image) before setting pendingLabel; if no source exists, throw a
descriptive Error (similar to the other labeled-edge errors) instead of silently
accepting, and add a regression test for the `-> "x" -> [B]` case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants