Better association display in compact mode
[erdiag.git] / parser.js
index 57cf81c..8fc65ed 100644 (file)
--- a/parser.js
+++ b/parser.js
@@ -16,7 +16,7 @@ class ErDiags
 
        static get TYPES()
        {
-               // SQLite types without null (TODO: be more general)
+               // SQLite storage classes without null
                return ["integer","real","text","blob"];
        }
 
@@ -26,7 +26,9 @@ class ErDiags
                        "*": "0,n",
                        "+": "1,n",
                        "?": "0,1",
-                       "1": "1,1"
+                       "1": "1,1",
+                       "?R": "(0,1)",
+                       "1R": "(1,1)",
                };
        }
 
@@ -67,7 +69,7 @@ class ErDiags
                {
                        case '[':
                                // Entity = { name: { attributes, [weak] } }
-                               let name = lines[start].match(/\w+/)[0];
+                               let name = lines[start].match(/[^\[\]"\s]+/)[0];
                                let entity = { attributes: this.parseAttributes(lines, start+1, end) };
                                if (lines[start].charAt(1) == '[')
                                        entity.weak = true;
@@ -79,7 +81,7 @@ class ErDiags
                        case '{': //association
                                // Association = { [name], [attributes], [weak], entities: ArrayOf entity indices }
                                let relationship = { };
-                               let nameRes = lines[start].match(/\w+/);
+                               let nameRes = lines[start].match(/[^{}"\s]+/);
                                if (nameRes !== null)
                                        relationship.name = nameRes[0];
                                if (lines[start].charAt(1) == '{')
@@ -95,15 +97,20 @@ class ErDiags
                let attributes = [];
                for (let i=start; i<end; i++)
                {
-                       let field = { name: lines[i].match(/\w+/)[0] };
-                       if (lines[i].charAt(0) == '#')
+                       let field = { };
+                       let line = lines[i];
+                       if (line.charAt(0) == '#')
+                       {
                                field.isKey = true;
-                       let parenthesis = lines[i].match(/\((.+)\)/);
+                               line = line.slice(1);
+                       }
+                       field.name = line.match(/[^()"\s]+/)[0];
+                       let parenthesis = line.match(/\((.+)\)/);
                        if (parenthesis !== null)
                        {
                                let sqlClues = parenthesis[1];
                                let qualifiers = sqlClues;
-                               let firstWord = sqlClues.match(/\w+/)[0];
+                               let firstWord = sqlClues.match(/[^\s]+/)[0];
                                if (ErDiags.TYPES.includes(firstWord))
                                {
                                        field.type = firstWord;
@@ -131,7 +138,10 @@ class ErDiags
                return inheritance;
        }
 
-       // Association (parsed here): { entities: ArrayOf entity names + cardinality, [attributes: ArrayOf {name, [isKey], [type], [qualifiers]}] }
+       // Association (parsed here): {
+       //   entities: ArrayOf entity names + cardinality,
+       //   [attributes: ArrayOf {name, [isKey], [type], [qualifiers]}]
+       // }
        parseAssociation(lines, start, end)
        {
                let assoce = { };
@@ -172,6 +182,7 @@ class ErDiags
        }
 
        // "Modèle conceptuel des données". TODO: option for graph size
+       // NOTE: randomizing helps to obtain better graphs (sometimes)
        drawMcd(id, mcdStyle) //mcdStyle: bubble, or compact
        {
                let element = document.getElementById(id);
@@ -183,11 +194,14 @@ class ErDiags
                }
                // Build dot graph input
                let mcdDot = 'graph {\n';
+               mcdDot += 'rankdir="LR";\n';
                // Nodes:
-               Object.keys(this.entities).forEach( name => {
+               if (mcdStyle == "compact")
+                       mcdDot += 'node [shape=plaintext];\n';
+               _.shuffle(Object.keys(this.entities)).forEach( name => {
                        if (mcdStyle == "bubble")
                        {
-                               mcdDot += name + '[shape=rectangle, label="' + name + '"';
+                               mcdDot += '"' + name + '" [shape=rectangle, label="' + name + '"';
                                if (this.entities[name].weak)
                                        mcdDot += ', peripheries=2';
                                mcdDot += '];\n';
@@ -195,14 +209,18 @@ class ErDiags
                                {
                                        this.entities[name].attributes.forEach( a => {
                                                let label = (a.isKey ? '#' : '') + a.name;
-                                               mcdDot += name + '_' + a.name + '[shape=ellipse, label="' + label + '"];\n';
-                                               mcdDot += name + '_' + a.name + ' -- ' + name + ';\n';
+                                               let attrName = name + '_' + a.name;
+                                               mcdDot += '"' + attrName + '" [shape=ellipse, label="' + label + '"];\n';
+                                               if (Math.random() < 0.5)
+                                                       mcdDot += '"' + attrName + '" -- "' + name + '";\n';
+                                               else
+                                                       mcdDot += '"' + name + '" -- "' + attrName + '";\n';
                                        });
                                }
                        }
                        else
                        {
-                               mcdDot += name + '[shape=plaintext, label=<';
+                               mcdDot += '"' + name + '" [label=<';
                                if (this.entities[name].weak)
                                {
                                        mcdDot += '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="0" CELLSPACING="3" CELLBORDER="0">' +
@@ -225,42 +243,79 @@ class ErDiags
                        }
                });
                // Inheritances:
-               this.inheritances.forEach( i => {
-                       i.children.forEach( c => {
-                               mcdDot += c + ':name -- ' + i.parent + ':name [len="1.00", dir="forward", arrowhead="vee", style="dashed"];\n';
+               _.shuffle(this.inheritances).forEach( i => {
+                       // TODO: node shape = triangle fill yellow. See
+                       // https://merise.developpez.com/faq/?page=MCD#CIF-ou-dependance-fonctionnelle-de-A-a-Z
+                       // https://merise.developpez.com/faq/?page=MLD#Comment-transformer-un-MCD-en-MLD
+                       // https://www.developpez.net/forums/d1088964/general-developpement/alm/modelisation/structure-agregation-l-association-d-association/
+                       _.shuffle(i.children).forEach( c => {
+                               if (Math.random() < 0.5)
+                                       mcdDot += '"' + c + '":name -- "' + i.parent;
+                               else
+                                       mcdDot += '"' + i.parent + '":name -- "' + c;
+                               mcdDot += '":name [dir="forward", arrowhead="vee", style="dashed"];\n';
                        });
                });
                // Relationships:
+               if (mcdStyle == "compact")
+                       mcdDot += 'node [shape=rectangle, style=rounded];\n';
                let assoceCounter = 0;
-               this.associations.forEach( a => {
+               _.shuffle(this.associations).forEach( a => {
                        let name = !!a.name && a.name.length > 0
                                ? a.name
                                : '_assoce' + assoceCounter++;
-                       mcdDot += name + '[shape="diamond", style="filled", color="lightgrey", label="' + (!!a.name ? a.name : '') + '"';
-                       if (a.weak)
-                               mcdDot += ', peripheries=2';
-                       mcdDot += '];\n';
-                       a.entities.forEach( e => {
-                               mcdDot += e.name + ':name -- ' + name + '[len="1.00", label="' + ErDiags.CARDINAL[e.card] + '"];\n';
-                       });
-                       if (!!a.attributes)
+                       if (mcdStyle == "bubble")
                        {
-                               a.attributes.forEach( attr => {
-                                       let label = (attr.isKey ? '#' : '') + attr.name;
-                                       mcdDot += name + '_' + attr.name + '[len="1.00", shape=ellipse, label="' + label + '"];\n';
-                                       mcdDot += name + '_' + attr.name + ' -- ' + name + ';\n';
-                               });
+                               mcdDot += '"' + name + '" [shape="diamond", style="filled", color="lightgrey", label="' + name + '"';
+                               if (a.weak)
+                                       mcdDot += ', peripheries=2';
+                               mcdDot += '];\n';
+                               if (!!a.attributes)
+                               {
+                                       a.attributes.forEach( attr => {
+                                               let label = (attr.isKey ? '#' : '') + attr.name;
+                                               mcdDot += '"' + name + '_' + attr.name + '" [shape=ellipse, label="' + label + '"];\n';
+                                               let attrName = name + '_' + attr.name;
+                                               if (Math.random() < 0.5)
+                                                       mcdDot += '"' + attrName + '" -- "' + name + '";\n';
+                                               else
+                                                       mcdDot += '"' + name + '" -- "' + attrName + '";\n';
+                                       });
+                               }
                        }
+                       else
+                       {
+                               let label = name;
+                               if (!!a.attributes)
+                               {
+                                       a.attributes.forEach( attr => {
+                                               let attrLabel = (attr.isKey ? '#' : '') + attr.name;
+                                               label += '\\n<' + attrLabel + '>';
+                                       });
+                               }
+                               mcdDot += '"' + name + '" [color="lightgrey", label="' + label + '"';
+                               if (a.weak)
+                                       mcdDot += ', peripheries=2';
+                               mcdDot += '];\n';
+                       }
+                       _.shuffle(a.entities).forEach( e => {
+                               if (Math.random() < 0.5)
+                                       mcdDot += '"' + e.name + '":name -- "' + name + '"';
+                               else
+                                       mcdDot += '"' + name + '" -- "' + e.name + '":name';
+                               mcdDot += '[label="' + ErDiags.CARDINAL[e.card] + '"];\n';
+                       });
                });
                mcdDot += '}';
-               //console.log(mcdDot);
+               console.log(mcdDot);
                ErDiags.AjaxGet(mcdDot, graphSvg => {
                        this.mcdGraph = graphSvg;
                        element.innerHTML = graphSvg;
-               })
+               });
        }
 
        // "Modèle logique des données"
+       // TODO: this one should draw links from foreign keys to keys (port=... in <TD>)
        drawMld(id)
        {
                let element = document.getElementById(id);
@@ -288,6 +343,11 @@ class ErDiags
                        });
                });
                // this.graphMld = ...
+               //console.log(mldDot);
+               ErDiags.AjaxGet(mldDot, graphSvg => {
+                       this.mldGraph = graphSvg;
+                       element.innerHTML = graphSvg;
+               });
        }
 
        fillSql(id)