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