(xml: string)
| 539 | * @returns null if valid, error message string if invalid |
| 540 | */ |
| 541 | export function validateMxCellStructure(xml: string): string | null { |
| 542 | const parser = new DOMParser() |
| 543 | const doc = parser.parseFromString(xml, "text/xml") |
| 544 | |
| 545 | // Check for XML parsing errors (includes unescaped special characters) |
| 546 | const parseError = doc.querySelector("parsererror") |
| 547 | if (parseError) { |
| 548 | return `Invalid XML: The XML contains syntax errors (likely unescaped special characters like <, >, & in attribute values). Please escape special characters: use < for <, > for >, & for &, " for ". Regenerate the diagram with properly escaped values.` |
| 549 | } |
| 550 | |
| 551 | // Get all mxCell elements once for all validations |
| 552 | const allCells = doc.querySelectorAll("mxCell") |
| 553 | |
| 554 | // Single pass: collect IDs, check for duplicates, nesting, orphans, and invalid parents |
| 555 | const cellIds = new Set<string>() |
| 556 | const duplicateIds: string[] = [] |
| 557 | const nestedCells: string[] = [] |
| 558 | const orphanCells: string[] = [] |
| 559 | const invalidParents: { id: string; parent: string }[] = [] |
| 560 | const edgesToValidate: { |
| 561 | id: string |
| 562 | source: string | null |
| 563 | target: string | null |
| 564 | }[] = [] |
| 565 | |
| 566 | allCells.forEach((cell) => { |
| 567 | const id = cell.getAttribute("id") |
| 568 | const parent = cell.getAttribute("parent") |
| 569 | const isEdge = cell.getAttribute("edge") === "1" |
| 570 | |
| 571 | // Check for duplicate IDs |
| 572 | if (id) { |
| 573 | if (cellIds.has(id)) { |
| 574 | duplicateIds.push(id) |
| 575 | } else { |
| 576 | cellIds.add(id) |
| 577 | } |
| 578 | } |
| 579 | |
| 580 | // Check for nested mxCell (parent element is also mxCell) |
| 581 | if (cell.parentElement?.tagName === "mxCell") { |
| 582 | nestedCells.push(id || "unknown") |
| 583 | } |
| 584 | |
| 585 | // Check parent attribute (skip root cell id="0") |
| 586 | if (id !== "0") { |
| 587 | if (!parent) { |
| 588 | if (id) orphanCells.push(id) |
| 589 | } else { |
| 590 | // Store for later validation (after all IDs collected) |
| 591 | invalidParents.push({ id: id || "unknown", parent }) |
| 592 | } |
| 593 | } |
| 594 | |
| 595 | // Collect edges for connection validation |
| 596 | if (isEdge) { |
| 597 | edgesToValidate.push({ |
| 598 | id: id || "unknown", |
no outgoing calls
no test coverage detected