mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(flowchart): export to editable Visio .vsdx (C14)
New 'Export Visio' toolbar button sends the current graph to the
main process, which generates an OOXML .vsdx zip with JSZip and
writes it to a user-chosen path. The result is fully editable in
Microsoft Visio, draw.io, Lucidchart, and any other tool that
accepts the format — not an embedded image.
Architecture follows the existing pure-module pattern:
- src/flowchart/flowchart-vsdx-export.js: pure translator that
produces (a) page1.xml describing each node as a Visio Shape
with correct geometry (PinX/PinY/Width/Height in inches, with
the Y-axis flipped to match Visio's bottom-up coords), each
edge as a Connect entry, and (b) the seven static boilerplate
XML files that make a .vsdx zip valid ([Content_Types].xml,
_rels/.rels, visio/document.xml, visio/pages/pages.xml, etc.).
- src/main.js: new 'export-vsdx' IPC handler requires the pure
module + JSZip, builds the zip, writes to disk, returns
{ canceled, path } | { canceled: false, error }.
- src/preload.js: flowchart.exportVsdx(graph) bridge.
- src/renderer/flowchart-bundle.js: btnExportVsdx handler, status
feedback for cancel / save / error paths.
- src/flowchart-generator.html: button next to Export JPG.
Tests: 7 new (1362 total) cover graphBounds (empty / single /
multi-node), XML escaping of label text, Connect emission, the 5
node kinds mapping to 5 distinct master names, and that all
boilerplate files start with the XML declaration.
Amit Haridas
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*
|
||||
* Tests the pure Visio .vsdx exporter (src/flowchart/flowchart-vsdx-export.js).
|
||||
* The pure module returns string XML; the main-process IPC handler wraps
|
||||
* it in a zip and writes bytes. These tests verify the geometry, escaping,
|
||||
* and shape / connect emission.
|
||||
*/
|
||||
const {
|
||||
toVisioPageXml,
|
||||
visioBoilerplate,
|
||||
graphBounds,
|
||||
} = require('../src/flowchart/flowchart-vsdx-export');
|
||||
|
||||
describe('flowchart-vsdx-export: pure module', () => {
|
||||
test('graphBounds returns sensible defaults for empty graph', () => {
|
||||
expect(graphBounds({ nodes: [], edges: [] })).toEqual({ width: 200, height: 100 });
|
||||
expect(graphBounds(null)).toEqual({ width: 200, height: 100 });
|
||||
});
|
||||
|
||||
test('graphBounds spans the union of node rectangles', () => {
|
||||
const bounds = graphBounds({
|
||||
nodes: [
|
||||
{ id: 'a', x: 0, y: 0, width: 100, height: 60 },
|
||||
{ id: 'b', x: 200, y: 100, width: 100, height: 60 },
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
expect(bounds.width).toBe(300);
|
||||
expect(bounds.height).toBe(160);
|
||||
});
|
||||
|
||||
test('toVisioPageXml emits a Shape per node with correct geometry', () => {
|
||||
const xml = toVisioPageXml({
|
||||
nodes: [{ id: 'n1', kind: 'process', x: 0, y: 0, width: 120, height: 60, label: 'Hello' }],
|
||||
edges: [],
|
||||
});
|
||||
expect(xml).toContain('<?xml version="1.0"');
|
||||
expect(xml).toContain('<PageContents');
|
||||
expect(xml).toContain('ID="n1"');
|
||||
expect(xml).toContain('NameU="Process"');
|
||||
expect(xml).toContain('PinX');
|
||||
expect(xml).toContain('PinY');
|
||||
expect(xml).toContain('<Text>Hello</Text>');
|
||||
});
|
||||
|
||||
test('toVisioPageXml escapes XML-special characters in labels', () => {
|
||||
const xml = toVisioPageXml({
|
||||
nodes: [{ id: 'n1', kind: 'process', x: 0, y: 0, width: 120, height: 60, label: 'A & B <c>' }],
|
||||
edges: [],
|
||||
});
|
||||
expect(xml).toContain('A & B <c>');
|
||||
expect(xml).not.toContain('<c>');
|
||||
});
|
||||
|
||||
test('toVisioPageXml emits Connect entries for edges', () => {
|
||||
const xml = toVisioPageXml({
|
||||
nodes: [
|
||||
{ id: 'a', kind: 'process', x: 0, y: 0, width: 120, height: 60, label: 'A' },
|
||||
{ id: 'b', kind: 'process', x: 200, y: 0, width: 120, height: 60, label: 'B' },
|
||||
],
|
||||
edges: [{ id: 'e1', fromNodeId: 'a', toNodeId: 'b', kind: 'solid' }],
|
||||
});
|
||||
expect(xml).toContain('<Connects>');
|
||||
expect(xml).toContain('FromSheet="a"');
|
||||
expect(xml).toContain('ToSheet="b"');
|
||||
});
|
||||
|
||||
test('toVisioPageXml maps the 5 node kinds to distinct master names', () => {
|
||||
const kinds = ['process', 'decision', 'terminator', 'subroutine', 'document'];
|
||||
const names = new Set();
|
||||
for (const kind of kinds) {
|
||||
const xml = toVisioPageXml({
|
||||
nodes: [{ id: `n_${kind}`, kind, x: 0, y: 0, width: 120, height: 60, label: kind }],
|
||||
edges: [],
|
||||
});
|
||||
const match = xml.match(/NameU="([^"]+)"/);
|
||||
expect(match).not.toBeNull();
|
||||
names.add(match[1]);
|
||||
}
|
||||
expect(names.size).toBe(5);
|
||||
});
|
||||
|
||||
test('visioBoilerplate returns all required static XML files', () => {
|
||||
const files = visioBoilerplate();
|
||||
const keys = Object.keys(files);
|
||||
expect(keys).toContain('[Content_Types].xml');
|
||||
expect(keys).toContain('_rels/.rels');
|
||||
expect(keys).toContain('visio/document.xml');
|
||||
expect(keys).toContain('visio/pages/pages.xml');
|
||||
expect(keys).toContain('visio/pages/_rels/pages.xml.rels');
|
||||
expect(keys).toContain('docProps/core.xml');
|
||||
expect(keys).toContain('docProps/app.xml');
|
||||
for (const body of Object.values(files)) {
|
||||
expect(body.startsWith('<?xml')).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user