Better association display in compact mode
[erdiag.git] / parser.js
CommitLineData
525c4d2a
BA
1// ER diagram description parser
2class ErDiags
3{
4 constructor(description)
5 {
6 this.entities = {};
7 this.inheritances = [];
8 this.associations = [];
9 this.txt2json(description);
e2610c05 10 this.tables = [];
525c4d2a
BA
11 // Cache SVG graphs returned by server (in addition to server cache = good perfs)
12 this.mcdGraph = "";
13 this.mldGraph = "";
14 this.sqlText = "";
15 }
16
17 static get TYPES()
18 {
d6c9499e 19 // SQLite storage classes without null
525c4d2a
BA
20 return ["integer","real","text","blob"];
21 }
22
23 static get CARDINAL()
24 {
25 return {
26 "*": "0,n",
27 "+": "1,n",
28 "?": "0,1",
c728aeca
BA
29 "1": "1,1",
30 "?R": "(0,1)",
31 "1R": "(1,1)",
525c4d2a
BA
32 };
33 }
34
35 //////////////////
36 // PARSING STAGE 1
37 //////////////////
38
39 // Parse a textual description into a json object
40 txt2json(text)
41 {
42 let lines = text.split("\n");
43 lines.push(""); //easier parsing: always empty line at the end
44 let start = -1;
45 for (let i=0; i < lines.length; i++)
46 {
47 lines[i] = lines[i].trim();
48 // Empty line ?
49 if (lines[i].length == 0)
50 {
51 if (start >= 0) //there is some group of lines to parse
52 {
53 this.parseThing(lines, start, i);
54 start = -1;
55 }
56 }
57 else //not empty line: just register starting point
58 {
59 if (start < 0)
60 start = i;
61 }
62 }
63 }
64
65 // Parse a group of lines into entity, association, ...
66 parseThing(lines, start, end) //start included, end excluded
67 {
68 switch (lines[start].charAt(0))
69 {
70 case '[':
71 // Entity = { name: { attributes, [weak] } }
006d95a3 72 let name = lines[start].match(/[^\[\]"\s]+/)[0];
525c4d2a
BA
73 let entity = { attributes: this.parseAttributes(lines, start+1, end) };
74 if (lines[start].charAt(1) == '[')
75 entity.weak = true;
76 this.entities[name] = entity;
77 break;
78 case 'i': //inheritance (arrows)
79 this.inheritances = this.inheritances.concat(this.parseInheritance(lines, start+1, end));
80 break;
81 case '{': //association
82 // Association = { [name], [attributes], [weak], entities: ArrayOf entity indices }
83 let relationship = { };
006d95a3 84 let nameRes = lines[start].match(/[^{}"\s]+/);
525c4d2a
BA
85 if (nameRes !== null)
86 relationship.name = nameRes[0];
87 if (lines[start].charAt(1) == '{')
88 relationship.weak = true;
89 this.associations.push(Object.assign({}, relationship, this.parseAssociation(lines, start+1, end)));
90 break;
91 }
92 }
93
94 // attributes: ArrayOf {name, [isKey], [type], [qualifiers]}
95 parseAttributes(lines, start, end)
96 {
97 let attributes = [];
98 for (let i=start; i<end; i++)
99 {
d6c9499e
BA
100 let field = { };
101 let line = lines[i];
102 if (line.charAt(0) == '#')
103 {
525c4d2a 104 field.isKey = true;
d6c9499e
BA
105 line = line.slice(1);
106 }
006d95a3 107 field.name = line.match(/[^()"\s]+/)[0];
d6c9499e 108 let parenthesis = line.match(/\((.+)\)/);
525c4d2a
BA
109 if (parenthesis !== null)
110 {
111 let sqlClues = parenthesis[1];
112 let qualifiers = sqlClues;
d6c9499e 113 let firstWord = sqlClues.match(/[^\s]+/)[0];
525c4d2a
BA
114 if (ErDiags.TYPES.includes(firstWord))
115 {
116 field.type = firstWord;
117 qualifiers = sqlClues.substring(firstWord.length).trim();
118 }
119 field.qualifiers = qualifiers;
120 }
121 attributes.push(field);
122 }
123 return attributes;
124 }
125
126 // GroupOf Inheritance: { parent, children: ArrayOf entity indices }
127 parseInheritance(lines, start, end)
128 {
129 let inheritance = [];
130 for (let i=start; i<end; i++)
131 {
132 let lineParts = lines[i].split(" ");
133 let children = [];
134 for (let j=1; j<lineParts.length; j++)
135 children.push(lineParts[j]);
136 inheritance.push({ parent:lineParts[0], children: children });
137 }
138 return inheritance;
139 }
140
c728aeca
BA
141 // Association (parsed here): {
142 // entities: ArrayOf entity names + cardinality,
143 // [attributes: ArrayOf {name, [isKey], [type], [qualifiers]}]
144 // }
525c4d2a
BA
145 parseAssociation(lines, start, end)
146 {
147 let assoce = { };
148 let entities = [];
149 let i = start;
150 while (i < end)
151 {
152 if (lines[i].charAt(0) == '-')
153 {
154 assoce.attributes = this.parseAttributes(lines, i+1, end);
155 break;
156 }
157 else
158 {
159 // Read entity name + cardinality
160 let lineParts = lines[i].split(" ");
161 entities.push({ name:lineParts[0], card:lineParts[1] });
162 }
163 i++;
164 }
165 assoce.entities = entities;
166 return assoce;
167 }
168
169 //////////////////
170 // PARSING STAGE 2
171 //////////////////
172
173 static AjaxGet(dotInput, callback)
174 {
175 let xhr = new XMLHttpRequest();
176 xhr.onreadystatechange = function() {
177 if (this.readyState == 4 && this.status == 200)
178 callback(this.responseText);
179 };
180 xhr.open("GET", "scripts/getGraphSvg.php?dot=" + encodeURIComponent(dotInput), true);
181 xhr.send();
182 }
183
184 // "Modèle conceptuel des données". TODO: option for graph size
48a55161 185 // NOTE: randomizing helps to obtain better graphs (sometimes)
525c4d2a
BA
186 drawMcd(id, mcdStyle) //mcdStyle: bubble, or compact
187 {
188 let element = document.getElementById(id);
189 mcdStyle = mcdStyle || "compact";
190 if (this.mcdGraph.length > 0)
191 {
192 element.innerHTML = this.mcdGraph;
193 return;
194 }
195 // Build dot graph input
196 let mcdDot = 'graph {\n';
48a55161 197 mcdDot += 'rankdir="LR";\n';
525c4d2a 198 // Nodes:
48a55161 199 if (mcdStyle == "compact")
c728aeca 200 mcdDot += 'node [shape=plaintext];\n';
48a55161 201 _.shuffle(Object.keys(this.entities)).forEach( name => {
525c4d2a
BA
202 if (mcdStyle == "bubble")
203 {
006d95a3 204 mcdDot += '"' + name + '" [shape=rectangle, label="' + name + '"';
525c4d2a
BA
205 if (this.entities[name].weak)
206 mcdDot += ', peripheries=2';
207 mcdDot += '];\n';
208 if (!!this.entities[name].attributes)
209 {
b74cfe41 210 this.entities[name].attributes.forEach( a => {
525c4d2a 211 let label = (a.isKey ? '#' : '') + a.name;
48a55161 212 let attrName = name + '_' + a.name;
006d95a3 213 mcdDot += '"' + attrName + '" [shape=ellipse, label="' + label + '"];\n';
48a55161 214 if (Math.random() < 0.5)
006d95a3 215 mcdDot += '"' + attrName + '" -- "' + name + '";\n';
48a55161 216 else
006d95a3 217 mcdDot += '"' + name + '" -- "' + attrName + '";\n';
525c4d2a
BA
218 });
219 }
220 }
221 else
222 {
006d95a3 223 mcdDot += '"' + name + '" [label=<';
525c4d2a
BA
224 if (this.entities[name].weak)
225 {
226 mcdDot += '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="0" CELLSPACING="3" CELLBORDER="0">' +
227 '<tr><td><table BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
228 }
229 else
230 mcdDot += '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
231 mcdDot += '<tr><td BGCOLOR="#ae7d4e" BORDER="0"><font COLOR="#FFFFFF">' + name + '</font></td></tr>\n';
232 if (!!this.entities[name].attributes)
233 {
b74cfe41 234 this.entities[name].attributes.forEach( a => {
525c4d2a
BA
235 let label = (a.isKey ? '<u>' : '') + a.name + (a.isKey ? '</u>' : '');
236 mcdDot += '<tr><td BGCOLOR="#FFFFFF" BORDER="0" ALIGN="LEFT"><font COLOR="#000000" >' + label + '</font></td></tr>\n';
237 });
238 }
239 mcdDot += '</table>';
240 if (this.entities[name].weak)
241 mcdDot += '</td></tr></table>';
242 mcdDot += '>];\n';
243 }
244 });
245 // Inheritances:
006d95a3 246 _.shuffle(this.inheritances).forEach( i => {
a2d8ba72
BA
247 // TODO: node shape = triangle fill yellow. See
248 // https://merise.developpez.com/faq/?page=MCD#CIF-ou-dependance-fonctionnelle-de-A-a-Z
249 // https://merise.developpez.com/faq/?page=MLD#Comment-transformer-un-MCD-en-MLD
250 // https://www.developpez.net/forums/d1088964/general-developpement/alm/modelisation/structure-agregation-l-association-d-association/
48a55161
BA
251 _.shuffle(i.children).forEach( c => {
252 if (Math.random() < 0.5)
006d95a3 253 mcdDot += '"' + c + '":name -- "' + i.parent;
48a55161 254 else
006d95a3
BA
255 mcdDot += '"' + i.parent + '":name -- "' + c;
256 mcdDot += '":name [dir="forward", arrowhead="vee", style="dashed"];\n';
525c4d2a
BA
257 });
258 });
259 // Relationships:
c728aeca
BA
260 if (mcdStyle == "compact")
261 mcdDot += 'node [shape=rectangle, style=rounded];\n';
525c4d2a 262 let assoceCounter = 0;
48a55161 263 _.shuffle(this.associations).forEach( a => {
525c4d2a
BA
264 let name = !!a.name && a.name.length > 0
265 ? a.name
266 : '_assoce' + assoceCounter++;
c728aeca
BA
267 if (mcdStyle == "bubble")
268 {
269 mcdDot += '"' + name + '" [shape="diamond", style="filled", color="lightgrey", label="' + name + '"';
270 if (a.weak)
271 mcdDot += ', peripheries=2';
272 mcdDot += '];\n';
273 if (!!a.attributes)
274 {
275 a.attributes.forEach( attr => {
276 let label = (attr.isKey ? '#' : '') + attr.name;
277 mcdDot += '"' + name + '_' + attr.name + '" [shape=ellipse, label="' + label + '"];\n';
278 let attrName = name + '_' + attr.name;
279 if (Math.random() < 0.5)
280 mcdDot += '"' + attrName + '" -- "' + name + '";\n';
281 else
282 mcdDot += '"' + name + '" -- "' + attrName + '";\n';
283 });
284 }
285 }
286 else
287 {
288 let label = name;
289 if (!!a.attributes)
290 {
291 a.attributes.forEach( attr => {
292 let attrLabel = (attr.isKey ? '#' : '') + attr.name;
293 label += '\\n<' + attrLabel + '>';
294 });
295 }
296 mcdDot += '"' + name + '" [color="lightgrey", label="' + label + '"';
297 if (a.weak)
298 mcdDot += ', peripheries=2';
299 mcdDot += '];\n';
300 }
48a55161
BA
301 _.shuffle(a.entities).forEach( e => {
302 if (Math.random() < 0.5)
006d95a3 303 mcdDot += '"' + e.name + '":name -- "' + name + '"';
48a55161 304 else
006d95a3 305 mcdDot += '"' + name + '" -- "' + e.name + '":name';
48a55161 306 mcdDot += '[label="' + ErDiags.CARDINAL[e.card] + '"];\n';
525c4d2a 307 });
525c4d2a
BA
308 });
309 mcdDot += '}';
48a55161 310 console.log(mcdDot);
525c4d2a
BA
311 ErDiags.AjaxGet(mcdDot, graphSvg => {
312 this.mcdGraph = graphSvg;
313 element.innerHTML = graphSvg;
d6c9499e 314 });
525c4d2a
BA
315 }
316
317 // "Modèle logique des données"
48a55161 318 // TODO: this one should draw links from foreign keys to keys (port=... in <TD>)
525c4d2a
BA
319 drawMld(id)
320 {
321 let element = document.getElementById(id);
322 if (this.mldGraph.length > 0)
323 {
324 element.innerHTML = this.mcdGraph;
325 return;
326 }
e2610c05
BA
327 // Build dot graph input
328 let mldDot = 'graph {\n';
329 // Nodes:
330 Object.keys(this.entities).forEach( name => {
331 //mld. ... --> devient table
332 // mldDot = ...
333 });
334 // Relationships:
335 this.associations.forEach( a => {
336 a.entities.forEach( e => { // e.card e.name ...
337 // Pass 1 : entites deviennent tables
338 // Pass 2 : sur les assoces
339 // multi-arite : sub-loop si 0,1 ou 1,1 : aspiré comme attribut de l'association (phase 1)
340 // ensuite, que du 0,n ou 1,n : si == 1, OK une table
341 // si 2 ou + : n tables + 1 pour l'assoce, avec attrs clés étrangères
342 // clé étrangère NOT NULL si 1,1
343 });
344 });
525c4d2a 345 // this.graphMld = ...
d6c9499e
BA
346 //console.log(mldDot);
347 ErDiags.AjaxGet(mldDot, graphSvg => {
348 this.mldGraph = graphSvg;
349 element.innerHTML = graphSvg;
350 });
525c4d2a
BA
351 }
352
353 fillSql(id)
354 {
355 let element = document.getElementById(id);
356 if (this.sqlText.length > 0)
357 {
358 element.innerHTML = this.sqlText;
359 return;
360 }
361 //UNIMPLEMENTED (should be straightforward from MLD)
362 }
363}