|
| 1 | +/* |
| 2 | +Copyright 2023-present The maxGraph project Contributors |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +import { describe, expect, test } from '@jest/globals'; |
| 18 | +import { Cell, Codec, Geometry, Graph, GraphDataModel, Point } from '../../src'; |
| 19 | +import { getPrettyXml, parseXml } from '../../src/util/xmlUtils'; |
| 20 | + |
| 21 | +type ModelExportOptions = { |
| 22 | + /** |
| 23 | + * @default true |
| 24 | + */ |
| 25 | + pretty?: boolean; |
| 26 | +}; |
| 27 | + |
| 28 | +/** |
| 29 | + * Convenient utility class using {@link Codec} to manage maxGraph model import and export. |
| 30 | + * |
| 31 | + * @internal |
| 32 | + * @alpha subject to change (class and method names) |
| 33 | + */ |
| 34 | +class ModelXmlSerializer { |
| 35 | + // Include 'XML' in the class name as there were past discussions about supporting other format (JSON for example {@link https://github.com/maxGraph/maxGraph/discussions/60}). |
| 36 | + constructor(private dataModel: GraphDataModel) {} |
| 37 | + |
| 38 | + import(xml: string): void { |
| 39 | + const doc = parseXml(xml); |
| 40 | + new Codec(doc).decode(doc.documentElement, this.dataModel); |
| 41 | + } |
| 42 | + |
| 43 | + export(options?: ModelExportOptions): string { |
| 44 | + const encodedNode = new Codec().encode(this.dataModel); |
| 45 | + return options?.pretty ?? true |
| 46 | + ? getPrettyXml(encodedNode) |
| 47 | + : getPrettyXml(encodedNode, '', '', ''); |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +// inspired by VertexMixin.createVertex |
| 52 | +const newVertex = (id: string, value: string) => { |
| 53 | + const vertex = new Cell(value); |
| 54 | + vertex.setId(id); |
| 55 | + vertex.setVertex(true); |
| 56 | + return vertex; |
| 57 | +}; |
| 58 | + |
| 59 | +// inspired by EdgeMixin.createEdge |
| 60 | +const newEdge = (id: string, value: string) => { |
| 61 | + const edge = new Cell(value, new Geometry()); |
| 62 | + edge.setId(id); |
| 63 | + edge.setEdge(true); |
| 64 | + return edge; |
| 65 | +}; |
| 66 | + |
| 67 | +const getParent = (model: GraphDataModel) => { |
| 68 | + // As done in the Graph object |
| 69 | + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- here we know that model is not null |
| 70 | + return model.getRoot()!.getChildAt(0); |
| 71 | +}; |
| 72 | + |
| 73 | +// Adapted from https://github.com/maxGraph/maxGraph/issues/178 |
| 74 | +const xmlFromIssue178 = `<GraphDataModel> |
| 75 | + <root> |
| 76 | + <Cell id="0"> |
| 77 | + <Object as="style"/> |
| 78 | + </Cell> |
| 79 | + <Cell id="1" parent="0"> |
| 80 | + <Object as="style"/> |
| 81 | + </Cell> |
| 82 | + <Cell id="B_#0" value="rootNode" vertex="1" parent="1"> |
| 83 | + <Geometry _x="100" _y="100" _width="100" _height="80" as="geometry"/> |
| 84 | + <!-- not in the xml of issue 178, same issue as with Geometry --> |
| 85 | + <Object fillColor="green" strokeWidth="4" shape="triangle" as="style" /> |
| 86 | + </Cell> |
| 87 | + </root> |
| 88 | +</GraphDataModel>`; |
| 89 | + |
| 90 | +describe('import before the export (reproduce https://github.com/maxGraph/maxGraph/issues/178)', () => { |
| 91 | + test('only use GraphDataModel', () => { |
| 92 | + const model = new GraphDataModel(); |
| 93 | + new ModelXmlSerializer(model).import(xmlFromIssue178); |
| 94 | + |
| 95 | + const cell = model.getCell('B_#0'); |
| 96 | + expect(cell).not.toBeNull(); |
| 97 | + expect(cell?.value).toEqual('rootNode'); |
| 98 | + expect(cell?.vertex).toEqual(1); // FIX should be set to true |
| 99 | + expect(cell?.isVertex()).toBeTruthy(); |
| 100 | + expect(cell?.getParent()?.id).toEqual('1'); |
| 101 | + const geometry = <Element>(<unknown>cell?.geometry); // FIX should be new Geometry(100, 100, 100, 80) |
| 102 | + expect(geometry.getAttribute('_x')).toEqual('100'); |
| 103 | + expect(geometry.getAttribute('_y')).toEqual('100'); |
| 104 | + expect(geometry.getAttribute('_height')).toEqual('80'); |
| 105 | + expect(geometry.getAttribute('_width')).toEqual('100'); |
| 106 | + |
| 107 | + const style = <Element>(<unknown>cell?.style); // FIX should be { fillColor: 'green', shape: 'triangle', strokeWidth: 4, } |
| 108 | + expect(style.getAttribute('fillColor')).toEqual('green'); |
| 109 | + expect(style.getAttribute('shape')).toEqual('triangle'); |
| 110 | + expect(style.getAttribute('strokeWidth')).toEqual('4'); |
| 111 | + }); |
| 112 | + |
| 113 | + test('use Graph - reproduced what is described in issue 178', () => { |
| 114 | + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
| 115 | + const graph = new Graph(null!); |
| 116 | + expect(() => |
| 117 | + new ModelXmlSerializer(graph.getDataModel()).import(xmlFromIssue178) |
| 118 | + ).toThrow(new Error('Invalid x supplied.')); |
| 119 | + }); |
| 120 | +}); |
| 121 | + |
| 122 | +describe('export', () => { |
| 123 | + test('empty model exported as pretty XML', () => { |
| 124 | + expect(new ModelXmlSerializer(new GraphDataModel()).export()).toEqual( |
| 125 | + `<GraphDataModel> |
| 126 | + <root> |
| 127 | + <Cell id="0"> |
| 128 | + <Object as="style" /> |
| 129 | + </Cell> |
| 130 | + <Cell id="1" parent="0"> |
| 131 | + <Object as="style" /> |
| 132 | + </Cell> |
| 133 | + </root> |
| 134 | +</GraphDataModel> |
| 135 | +` |
| 136 | + ); |
| 137 | + }); |
| 138 | + |
| 139 | + test('empty model exported as non pretty XML', () => { |
| 140 | + expect( |
| 141 | + new ModelXmlSerializer(new GraphDataModel()).export({ pretty: false }) |
| 142 | + ).toEqual( |
| 143 | + `<GraphDataModel><root><Cell id="0"><Object as="style" /></Cell><Cell id="1" parent="0"><Object as="style" /></Cell></root></GraphDataModel>` |
| 144 | + ); |
| 145 | + }); |
| 146 | + |
| 147 | + test('model with 2 vertices linked with an edge', () => { |
| 148 | + const model = new GraphDataModel(); |
| 149 | + const parent = getParent(model); |
| 150 | + |
| 151 | + const v1 = newVertex('v1', 'vertex 1'); |
| 152 | + model.add(parent, v1); |
| 153 | + v1.setStyle({ fillColor: 'green', strokeWidth: 4 }); |
| 154 | + v1.geometry = new Geometry(100, 100, 100, 80); |
| 155 | + const v2 = newVertex('v2', 'vertex 2'); |
| 156 | + v2.style = { bendable: false, rounded: true, fontColor: 'yellow' }; |
| 157 | + model.add(parent, v2); |
| 158 | + |
| 159 | + const edge = newEdge('e1', 'edge'); |
| 160 | + model.add(parent, edge); |
| 161 | + model.setTerminal(edge, v1, true); |
| 162 | + model.setTerminal(edge, v2, false); |
| 163 | + (<Geometry>edge.geometry).points = [ |
| 164 | + new Point(0, 10), |
| 165 | + new Point(0, 40), |
| 166 | + new Point(40, 40), |
| 167 | + ]; |
| 168 | + |
| 169 | + // FIX boolean values should be set to true/false instead of 1/0 |
| 170 | + expect(new ModelXmlSerializer(model).export()).toEqual( |
| 171 | + `<GraphDataModel> |
| 172 | + <root> |
| 173 | + <Cell id="0"> |
| 174 | + <Object as="style" /> |
| 175 | + </Cell> |
| 176 | + <Cell id="1" parent="0"> |
| 177 | + <Object as="style" /> |
| 178 | + </Cell> |
| 179 | + <Cell id="v1" value="vertex 1" vertex="1" parent="1"> |
| 180 | + <Geometry _x="100" _y="100" _width="100" _height="80" as="geometry" /> |
| 181 | + <Object fillColor="green" strokeWidth="4" as="style" /> |
| 182 | + </Cell> |
| 183 | + <Cell id="v2" value="vertex 2" vertex="1" parent="1"> |
| 184 | + <Object bendable="0" rounded="1" fontColor="yellow" as="style" /> |
| 185 | + </Cell> |
| 186 | + <Cell id="e1" value="edge" edge="1" parent="1" source="v1" target="v2"> |
| 187 | + <Geometry as="geometry"> |
| 188 | + <Array as="points"> |
| 189 | + <Point _y="10" /> |
| 190 | + <Point _y="40" /> |
| 191 | + <Point _x="40" _y="40" /> |
| 192 | + </Array> |
| 193 | + </Geometry> |
| 194 | + <Object as="style" /> |
| 195 | + </Cell> |
| 196 | + </root> |
| 197 | +</GraphDataModel> |
| 198 | +` |
| 199 | + ); |
| 200 | + }); |
| 201 | +}); |
| 202 | + |
| 203 | +describe('import', () => { |
| 204 | + test('XML from issue 178', () => { |
| 205 | + const model = new GraphDataModel(); |
| 206 | + new ModelXmlSerializer(model).import(xmlFromIssue178); |
| 207 | + |
| 208 | + const cell = model.getCell('B_#0'); |
| 209 | + expect(cell).toBeDefined(); |
| 210 | + expect(cell?.value).toEqual('rootNode'); |
| 211 | + expect(cell?.vertex).toEqual(1); // FIX should be set to true |
| 212 | + expect(cell?.isVertex()).toBeTruthy(); |
| 213 | + expect(cell?.getParent()?.id).toEqual('1'); |
| 214 | + expect(cell?.geometry).toEqual(new Geometry(100, 100, 100, 80)); |
| 215 | + expect(cell?.style).toEqual({ |
| 216 | + fillColor: 'green', |
| 217 | + shape: 'triangle', |
| 218 | + strokeWidth: 4, |
| 219 | + }); |
| 220 | + }); |
| 221 | +}); |
0 commit comments