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