First commit
[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 // 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 {
18 // SQLite types without null (TODO: be more general)
19 return ["integer","real","text","blob"];
20 }
21
22 static get CARDINAL()
23 {
24 return {
25 "*": "0,n",
26 "+": "1,n",
27 "?": "0,1",
28 "1": "1,1"
29 };
30 }
31
32 //////////////////
33 // PARSING STAGE 1
34 //////////////////
35
36 // Parse a textual description into a json object
37 txt2json(text)
38 {
39 let lines = text.split("\n");
40 lines.push(""); //easier parsing: always empty line at the end
41 let start = -1;
42 for (let i=0; i < lines.length; i++)
43 {
44 lines[i] = lines[i].trim();
45 // Empty line ?
46 if (lines[i].length == 0)
47 {
48 if (start >= 0) //there is some group of lines to parse
49 {
50 this.parseThing(lines, start, i);
51 start = -1;
52 }
53 }
54 else //not empty line: just register starting point
55 {
56 if (start < 0)
57 start = i;
58 }
59 }
60 }
61
62 // Parse a group of lines into entity, association, ...
63 parseThing(lines, start, end) //start included, end excluded
64 {
65 switch (lines[start].charAt(0))
66 {
67 case '[':
68 // Entity = { name: { attributes, [weak] } }
69 let name = lines[start].match(/\w+/)[0];
70 let entity = { attributes: this.parseAttributes(lines, start+1, end) };
71 if (lines[start].charAt(1) == '[')
72 entity.weak = true;
73 this.entities[name] = entity;
74 break;
75 case 'i': //inheritance (arrows)
76 this.inheritances = this.inheritances.concat(this.parseInheritance(lines, start+1, end));
77 break;
78 case '{': //association
79 // Association = { [name], [attributes], [weak], entities: ArrayOf entity indices }
80 let relationship = { };
81 let nameRes = lines[start].match(/\w+/);
82 if (nameRes !== null)
83 relationship.name = nameRes[0];
84 if (lines[start].charAt(1) == '{')
85 relationship.weak = true;
86 this.associations.push(Object.assign({}, relationship, this.parseAssociation(lines, start+1, end)));
87 break;
88 }
89 }
90
91 // attributes: ArrayOf {name, [isKey], [type], [qualifiers]}
92 parseAttributes(lines, start, end)
93 {
94 let attributes = [];
95 for (let i=start; i<end; i++)
96 {
97 let field = { name: lines[i].match(/\w+/)[0] };
98 if (lines[i].charAt(0) == '#')
99 field.isKey = true;
100 let parenthesis = lines[i].match(/\((.+)\)/);
101 if (parenthesis !== null)
102 {
103 let sqlClues = parenthesis[1];
104 let qualifiers = sqlClues;
105 let firstWord = sqlClues.match(/\w+/)[0];
106 if (ErDiags.TYPES.includes(firstWord))
107 {
108 field.type = firstWord;
109 qualifiers = sqlClues.substring(firstWord.length).trim();
110 }
111 field.qualifiers = qualifiers;
112 }
113 attributes.push(field);
114 }
115 return attributes;
116 }
117
118 // GroupOf Inheritance: { parent, children: ArrayOf entity indices }
119 parseInheritance(lines, start, end)
120 {
121 let inheritance = [];
122 for (let i=start; i<end; i++)
123 {
124 let lineParts = lines[i].split(" ");
125 let children = [];
126 for (let j=1; j<lineParts.length; j++)
127 children.push(lineParts[j]);
128 inheritance.push({ parent:lineParts[0], children: children });
129 }
130 return inheritance;
131 }
132
133 // Association (parsed here): { entities: ArrayOf entity names + cardinality, [attributes: ArrayOf {name, [isKey], [type], [qualifiers]}] }
134 parseAssociation(lines, start, end)
135 {
136 let assoce = { };
137 let entities = [];
138 let i = start;
139 while (i < end)
140 {
141 if (lines[i].charAt(0) == '-')
142 {
143 assoce.attributes = this.parseAttributes(lines, i+1, end);
144 break;
145 }
146 else
147 {
148 // Read entity name + cardinality
149 let lineParts = lines[i].split(" ");
150 entities.push({ name:lineParts[0], card:lineParts[1] });
151 }
152 i++;
153 }
154 assoce.entities = entities;
155 return assoce;
156 }
157
158 //////////////////
159 // PARSING STAGE 2
160 //////////////////
161
162 static AjaxGet(dotInput, callback)
163 {
164 let xhr = new XMLHttpRequest();
165 xhr.onreadystatechange = function() {
166 if (this.readyState == 4 && this.status == 200)
167 callback(this.responseText);
168 };
169 xhr.open("GET", "scripts/getGraphSvg.php?dot=" + encodeURIComponent(dotInput), true);
170 xhr.send();
171 }
172
173 // "Modèle conceptuel des données". TODO: option for graph size
174 drawMcd(id, mcdStyle) //mcdStyle: bubble, or compact
175 {
176 let element = document.getElementById(id);
177 mcdStyle = mcdStyle || "compact";
178 if (this.mcdGraph.length > 0)
179 {
180 element.innerHTML = this.mcdGraph;
181 return;
182 }
183 // Build dot graph input
184 let mcdDot = 'graph {\n';
185 // Nodes:
186 Object.keys(this.entities).forEach( name => {
187 if (mcdStyle == "bubble")
188 {
189 mcdDot += name + '[shape=rectangle, label="' + name + '"';
190 if (this.entities[name].weak)
191 mcdDot += ', peripheries=2';
192 mcdDot += '];\n';
193 if (!!this.entities[name].attributes)
194 {
195 this.entities[name].attributes.forEach( a => {
196 let label = (a.isKey ? '#' : '') + a.name;
197 mcdDot += name + '_' + a.name + '[shape=ellipse, label="' + label + '"];\n';
198 mcdDot += name + '_' + a.name + ' -- ' + name + ';\n';
199 });
200 }
201 }
202 else
203 {
204 mcdDot += name + '[shape=plaintext, label=<';
205 if (this.entities[name].weak)
206 {
207 mcdDot += '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="0" CELLSPACING="3" CELLBORDER="0">' +
208 '<tr><td><table BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
209 }
210 else
211 mcdDot += '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
212 mcdDot += '<tr><td BGCOLOR="#ae7d4e" BORDER="0"><font COLOR="#FFFFFF">' + name + '</font></td></tr>\n';
213 if (!!this.entities[name].attributes)
214 {
215 this.entities[name].attributes.forEach( a => {
216 let label = (a.isKey ? '<u>' : '') + a.name + (a.isKey ? '</u>' : '');
217 mcdDot += '<tr><td BGCOLOR="#FFFFFF" BORDER="0" ALIGN="LEFT"><font COLOR="#000000" >' + label + '</font></td></tr>\n';
218 });
219 }
220 mcdDot += '</table>';
221 if (this.entities[name].weak)
222 mcdDot += '</td></tr></table>';
223 mcdDot += '>];\n';
224 }
225 });
226 // Inheritances:
227 this.inheritances.forEach( i => {
228 i.children.forEach( c => {
229 mcdDot += c + ':name -- ' + i.parent + ':name [len="1.00", dir="forward", arrowhead="vee", style="dashed"];\n';
230 });
231 });
232 // Relationships:
233 let assoceCounter = 0;
234 this.associations.forEach( a => {
235 let name = !!a.name && a.name.length > 0
236 ? a.name
237 : '_assoce' + assoceCounter++;
238 mcdDot += name + '[shape="diamond", style="filled", color="lightgrey", label="' + (!!a.name ? a.name : '') + '"';
239 if (a.weak)
240 mcdDot += ', peripheries=2';
241 mcdDot += '];\n';
242 a.entities.forEach( e => {
243 mcdDot += e.name + ':name -- ' + name + '[len="1.00", label="' + ErDiags.CARDINAL[e.card] + '"];\n';
244 });
245 if (!!a.attributes)
246 {
247 a.attributes.forEach( attr => {
248 let label = (attr.isKey ? '#' : '') + attr.name;
249 mcdDot += name + '_' + attr.name + '[len="1.00", shape=ellipse, label="' + label + '"];\n';
250 mcdDot += name + '_' + attr.name + ' -- ' + name + ';\n';
251 });
252 }
253 });
254 mcdDot += '}';
255 //console.log(mcdDot);
256 ErDiags.AjaxGet(mcdDot, graphSvg => {
257 this.mcdGraph = graphSvg;
258 element.innerHTML = graphSvg;
259 })
260 }
261
262 // "Modèle logique des données"
263 drawMld(id)
264 {
265 let element = document.getElementById(id);
266 if (this.mldGraph.length > 0)
267 {
268 element.innerHTML = this.mcdGraph;
269 return;
270 }
271 //UNIMPLEMENTED
272 // TODO: analyze cardinalities (eat attributes, create new tables...)
273 // mldDot = ...
274 // this.graphMld = ...
275 }
276
277 fillSql(id)
278 {
279 let element = document.getElementById(id);
280 if (this.sqlText.length > 0)
281 {
282 element.innerHTML = this.sqlText;
283 return;
284 }
285 //UNIMPLEMENTED (should be straightforward from MLD)
286 }
287 }