first draft of drawMld()
[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 types without null (TODO: be more general)
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(/\w+/)[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(/\w+/);
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 = { name: lines[i].match(/\w+/)[0] };
99 if (lines[i].charAt(0) == '#')
100 field.isKey = true;
101 let parenthesis = lines[i].match(/\((.+)\)/);
102 if (parenthesis !== null)
103 {
104 let sqlClues = parenthesis[1];
105 let qualifiers = sqlClues;
106 let firstWord = sqlClues.match(/\w+/)[0];
107 if (ErDiags.TYPES.includes(firstWord))
108 {
109 field.type = firstWord;
110 qualifiers = sqlClues.substring(firstWord.length).trim();
111 }
112 field.qualifiers = qualifiers;
113 }
114 attributes.push(field);
115 }
116 return attributes;
117 }
118
119 // GroupOf Inheritance: { parent, children: ArrayOf entity indices }
120 parseInheritance(lines, start, end)
121 {
122 let inheritance = [];
123 for (let i=start; i<end; i++)
124 {
125 let lineParts = lines[i].split(" ");
126 let children = [];
127 for (let j=1; j<lineParts.length; j++)
128 children.push(lineParts[j]);
129 inheritance.push({ parent:lineParts[0], children: children });
130 }
131 return inheritance;
132 }
133
134 // Association (parsed here): { entities: ArrayOf entity names + cardinality, [attributes: ArrayOf {name, [isKey], [type], [qualifiers]}] }
135 parseAssociation(lines, start, end)
136 {
137 let assoce = { };
138 let entities = [];
139 let i = start;
140 while (i < end)
141 {
142 if (lines[i].charAt(0) == '-')
143 {
144 assoce.attributes = this.parseAttributes(lines, i+1, end);
145 break;
146 }
147 else
148 {
149 // Read entity name + cardinality
150 let lineParts = lines[i].split(" ");
151 entities.push({ name:lineParts[0], card:lineParts[1] });
152 }
153 i++;
154 }
155 assoce.entities = entities;
156 return assoce;
157 }
158
159 //////////////////
160 // PARSING STAGE 2
161 //////////////////
162
163 static AjaxGet(dotInput, callback)
164 {
165 let xhr = new XMLHttpRequest();
166 xhr.onreadystatechange = function() {
167 if (this.readyState == 4 && this.status == 200)
168 callback(this.responseText);
169 };
170 xhr.open("GET", "scripts/getGraphSvg.php?dot=" + encodeURIComponent(dotInput), true);
171 xhr.send();
172 }
173
174 // "Modèle conceptuel des données". TODO: option for graph size
175 drawMcd(id, mcdStyle) //mcdStyle: bubble, or compact
176 {
177 let element = document.getElementById(id);
178 mcdStyle = mcdStyle || "compact";
179 if (this.mcdGraph.length > 0)
180 {
181 element.innerHTML = this.mcdGraph;
182 return;
183 }
184 // Build dot graph input
185 let mcdDot = 'graph {\n';
186 // Nodes:
187 Object.keys(this.entities).forEach( name => {
188 if (mcdStyle == "bubble")
189 {
190 mcdDot += name + '[shape=rectangle, label="' + name + '"';
191 if (this.entities[name].weak)
192 mcdDot += ', peripheries=2';
193 mcdDot += '];\n';
194 if (!!this.entities[name].attributes)
195 {
196 this.entities[name].attributes.forEach( a => {
197 let label = (a.isKey ? '#' : '') + a.name;
198 mcdDot += name + '_' + a.name + '[shape=ellipse, label="' + label + '"];\n';
199 mcdDot += name + '_' + a.name + ' -- ' + name + ';\n';
200 });
201 }
202 }
203 else
204 {
205 mcdDot += name + '[shape=plaintext, label=<';
206 if (this.entities[name].weak)
207 {
208 mcdDot += '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="0" CELLSPACING="3" CELLBORDER="0">' +
209 '<tr><td><table BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
210 }
211 else
212 mcdDot += '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
213 mcdDot += '<tr><td BGCOLOR="#ae7d4e" BORDER="0"><font COLOR="#FFFFFF">' + name + '</font></td></tr>\n';
214 if (!!this.entities[name].attributes)
215 {
216 this.entities[name].attributes.forEach( a => {
217 let label = (a.isKey ? '<u>' : '') + a.name + (a.isKey ? '</u>' : '');
218 mcdDot += '<tr><td BGCOLOR="#FFFFFF" BORDER="0" ALIGN="LEFT"><font COLOR="#000000" >' + label + '</font></td></tr>\n';
219 });
220 }
221 mcdDot += '</table>';
222 if (this.entities[name].weak)
223 mcdDot += '</td></tr></table>';
224 mcdDot += '>];\n';
225 }
226 });
227 // Inheritances:
228 this.inheritances.forEach( i => {
229 i.children.forEach( c => {
230 mcdDot += c + ':name -- ' + i.parent + ':name [len="1.00", dir="forward", arrowhead="vee", style="dashed"];\n';
231 });
232 });
233 // Relationships:
234 let assoceCounter = 0;
235 this.associations.forEach( a => {
236 let name = !!a.name && a.name.length > 0
237 ? a.name
238 : '_assoce' + assoceCounter++;
239 mcdDot += name + '[shape="diamond", style="filled", color="lightgrey", label="' + (!!a.name ? a.name : '') + '"';
240 if (a.weak)
241 mcdDot += ', peripheries=2';
242 mcdDot += '];\n';
243 a.entities.forEach( e => {
244 mcdDot += e.name + ':name -- ' + name + '[len="1.00", label="' + ErDiags.CARDINAL[e.card] + '"];\n';
245 });
246 if (!!a.attributes)
247 {
248 a.attributes.forEach( attr => {
249 let label = (attr.isKey ? '#' : '') + attr.name;
250 mcdDot += name + '_' + attr.name + '[len="1.00", shape=ellipse, label="' + label + '"];\n';
251 mcdDot += name + '_' + attr.name + ' -- ' + name + ';\n';
252 });
253 }
254 });
255 mcdDot += '}';
256 //console.log(mcdDot);
257 ErDiags.AjaxGet(mcdDot, graphSvg => {
258 this.mcdGraph = graphSvg;
259 element.innerHTML = graphSvg;
260 })
261 }
262
263 // "Modèle logique des données"
264 drawMld(id)
265 {
266 let element = document.getElementById(id);
267 if (this.mldGraph.length > 0)
268 {
269 element.innerHTML = this.mcdGraph;
270 return;
271 }
272 // Build dot graph input
273 let mldDot = 'graph {\n';
274 // Nodes:
275 Object.keys(this.entities).forEach( name => {
276 //mld. ... --> devient table
277 // mldDot = ...
278 });
279 // Relationships:
280 this.associations.forEach( a => {
281 a.entities.forEach( e => { // e.card e.name ...
282 // Pass 1 : entites deviennent tables
283 // Pass 2 : sur les assoces
284 // multi-arite : sub-loop si 0,1 ou 1,1 : aspiré comme attribut de l'association (phase 1)
285 // ensuite, que du 0,n ou 1,n : si == 1, OK une table
286 // si 2 ou + : n tables + 1 pour l'assoce, avec attrs clés étrangères
287 // clé étrangère NOT NULL si 1,1
288 });
289 });
290 // this.graphMld = ...
291 }
292
293 fillSql(id)
294 {
295 let element = document.getElementById(id);
296 if (this.sqlText.length > 0)
297 {
298 element.innerHTML = this.sqlText;
299 return;
300 }
301 //UNIMPLEMENTED (should be straightforward from MLD)
302 }
303 }