├── .babelrc ├── .gitignore ├── .vscode └── launch.json ├── LICENSE.txt ├── README.md ├── package.json ├── src ├── bnfdisplay │ └── bnfdisplay.js ├── main.js ├── model │ ├── bnftogrammar.js │ ├── choice.js │ ├── chunk.js │ ├── expression.js │ ├── grammar.js │ ├── grammartobnf.js │ ├── grammartorrdiagram.js │ ├── literal.js │ ├── repetition.js │ ├── rule.js │ ├── rulereference.js │ ├── sequence.js │ └── specialsequence.js ├── ui │ ├── layoutinfo.js │ ├── rrbreak.js │ ├── rrchoice.js │ ├── rrdiagram.js │ ├── rrdiagramtosvg.js │ ├── rrelement.js │ ├── rrline.js │ ├── rrloop.js │ ├── rrsequence.js │ ├── rrtext.js │ └── svg │ │ ├── svgcontent.js │ │ ├── svgline.js │ │ └── svgpath.js └── utils │ └── utils.js ├── webpack.config.js └── www └── index1.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | www/rrdiagram.js 3 | www/rrdiagram.js.map -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome against localhost", 8 | "url": "http://localhost:8080", 9 | "webRoot": "${workspaceRoot}" 10 | }, 11 | { 12 | "type": "chrome", 13 | "request": "attach", 14 | "name": "Attach to Chrome", 15 | "port": 9222, 16 | "webRoot": "${workspaceRoot}" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RRDiagram-JS 2 | --- 3 | 4 | Generate railroad diagrams from code or BNF. Generate BNF from code. 5 | 6 | RR Diagram is a Javascript library that generates railroad diagrams (also called syntax diagrams) from code or from BNF notation. The output format is a very compact SVG image where rules can contain links. 7 | 8 | RR Diagram can also be used to generate BNF notation from a model. 9 | 10 | This is a Javascript port of the [Java-based version](https://github.com/Chrriis/RRDiagram). This version adds the capability of converting BNF present in an HTML page as well as relying on CSS styles from the page to style the SVG content. 11 | 12 | Example 13 | ======= 14 | 15 | This is the kind of diagrams that can get generated: 16 | ![H2 Select](http://rrdiagram.sourceforge.net/H2Select.svg) 17 | 18 | The above is generated using the right conversion options on this BNF: 19 |
 20 | H2_SELECT = 
 21 | 'SELECT' [ 'TOP' term ] [ 'DISTINCT' | 'ALL' ] selectExpression {',' selectExpression} \
 22 | 'FROM' tableExpression {',' tableExpression} [ 'WHERE' expression ] \
 23 | [ 'GROUP BY' expression {',' expression} ] [ 'HAVING' expression ] \
 24 | [ ( 'UNION' [ 'ALL' ] | 'MINUS' | 'EXCEPT' | 'INTERSECT' ) select ] [ 'ORDER BY' order {',' order} ] \
 25 | [ 'LIMIT' expression [ 'OFFSET' expression ] [ 'SAMPLE_SIZE' rowCountInt ] ] \
 26 | [ 'FOR UPDATE' ];
 27 | 
28 | 29 | Usage 30 | ===== 31 | 32 | To convert BNF text to a nice diagram, place the text in a `pre` tag and give it a class like `BNF`. Then include rrdiagram.js in your webpage. At the end of your page, add the following script to replace all those `pre` tags using the `BNF` class with a div that uses the `BNFSVG` class: 33 | ```Javascript 34 | var bnfDisplay = new rrdiagram.bnfdisplay.BNFDisplay(); 35 | bnfDisplay.replaceBNF('BNF', 'BNFSVG'); 36 | ``` 37 | 38 | Styles used by the produced diagrams must be defined in the page. Here is an example of those definitions: 39 | ```CSS 40 | .rrConnector {fill:none;stroke:#222222;} 41 | .rrRule {fill:#d3f0ff;stroke:#222222;} 42 | .rrRuleText {fill:#000000;font-family:Verdana,Sans-serif;font-size:12px;} 43 | .rrLiteral {fill:#90d9ff;stroke:#222222;} 44 | .rrLiteralText {fill:#000000;font-family:Verdana,Sans-serif;font-size:12px;} 45 | .rrSpecialSequence {fill:#e4f4ff;stroke:#222222;} 46 | .rrSpecialSequenceText {fill:#000000;font-family:Verdana,Sans-serif;font-size:12px;} 47 | .rrLoopCardinalities {fill:#000000;font-family:Verdana,Sans-serif;font-size:10px;} 48 | ``` 49 | 50 | The whole API is available too. 51 | 52 | The diagram model represents the actual constructs visible on the diagram. 53 | To convert a diagram model to SVG: 54 | ```Javascript 55 | var rrDiagram = new rrdiagram.ui.RRDiagram(rrElement); 56 | var rrDiagramToSVG = new rrdiagram.ui.RRDiagramToSVG(); 57 | var svg = rrDiagramToSVG.convert(rrDiagram); 58 | ``` 59 | 60 | The grammar model represents a BNF-like grammar. 61 | It can be converted to a diagram model: 62 | ```Javascript 63 | var grammar = new rrdiagram.model.Grammar(rules); 64 | var grammarToRRDiagram = new rrdiagram.model.GrammarToRRDiagram(); 65 | var rules = grammar.getRules(); 66 | for(var i=0; i 91 | - definition 92 | = 93 | := 94 | ::= 95 | - concatenation 96 | , 97 | <whitespace> 98 | - termination 99 | ; 100 | - alternation 101 | | 102 | - option 103 | [ ... ] 104 | ? 105 | - repetition 106 | { ... } => 0..N 107 | expression* => 0..N 108 | expression+ => 1..N 109 | <digits> * expression => <digits>...<digits> 110 | <digits> * [expression] => <0>...<digits> 111 | <digits> * expression? => <0>...<digits> 112 | - grouping 113 | ( ... ) 114 | - literal 115 | " ... " or ' ... ' 116 | - special characters 117 | (? ... ?) 118 | - comments 119 | (* ... *) 120 | 121 | 122 | When getting the BNF syntax from the grammar model, it is possible to tweak the kind of BNF to get by changing some options on the converter. 123 | 124 | License 125 | ======= 126 | This library is provided under the ASL, version 2.0 or later. 127 | 128 | 129 | 130 | Setup 131 | --- 132 | 133 | ``` 134 | npm install 135 | ``` 136 | 137 | 138 | 139 | Compile 140 | --- 141 | 142 | ``` 143 | npm run compile 144 | ``` 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rrdiagram-js", 3 | "version": "1.0.7", 4 | "description": "Generate railroad diagrams from code or BNF, generate BNF from code", 5 | "main": "main.js", 6 | "scripts": { 7 | "compile": "webpack", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type" : "git", 12 | "url" : "https://github.com/Chrriis/rrdiagram-js.git" 13 | }, 14 | "keywords": ["railroad", "syntax", "diagram", "bnf", "ebnf", "svg"], 15 | "author": "Christopher Deckers ", 16 | "license": "ASL 2.0", 17 | "dependencies": { 18 | "babel-core": "^6.25.0", 19 | "babel-loader": "^7.0.0", 20 | "babel-polyfill": "^6.23.0", 21 | "babel-preset-latest": "^6.24.1", 22 | "webpack": "^2.6.1" 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/bnfdisplay/bnfdisplay.js: -------------------------------------------------------------------------------- 1 | import BNFToGrammar from '../model/bnftogrammar'; 2 | import GrammarToRRDiagram from '../model/grammartorrdiagram'; 3 | import RRDiagramToSVG from '../ui/rrdiagramtosvg'; 4 | 5 | export default class BNFDisplay { 6 | 7 | constructor() { 8 | this.bnfToGrammar = new BNFToGrammar(); 9 | this.grammarToRRDiagram = new GrammarToRRDiagram(); 10 | this.grammarToRRDiagram.ruleConsideredAsLineBreak = "\\"; 11 | this.rrDiagramToSVG = new RRDiagramToSVG(); 12 | } 13 | 14 | /** 15 | * @return {BNFToGrammar} 16 | */ 17 | getBNFToGrammar() { 18 | return this.bnfToGrammar; 19 | } 20 | 21 | /** 22 | * @return {GrammarToRRDiagram} 23 | */ 24 | getGrammarToRRDiagram() { 25 | return this.grammarToRRDiagram; 26 | } 27 | 28 | /** 29 | * @return {RRDiagramToSVG} 30 | */ 31 | getRRDiagramToSVG() { 32 | return this.rrDiagramToSVG; 33 | } 34 | 35 | /** 36 | * @param {string} className 37 | * @param {string} newClassName 38 | */ 39 | replaceBNF(className, newClassName) { 40 | const elements = Array.from(document.getElementsByClassName(className)); 41 | for (const element of elements) { 42 | if(element.tagName.toLowerCase() === 'pre') { 43 | const newElement = document.createElement('div'); 44 | // Give a dummy rule definition to satisfy parser. 45 | const bnf = element.innerHTML; 46 | const grammar = this.bnfToGrammar.convert('a = ' + bnf); 47 | const rules = grammar.getRules(); 48 | if(rules.length == 1) { 49 | const rule = rules[0]; 50 | const rrDiagram = this.grammarToRRDiagram.convert(rule); 51 | const svg = this.rrDiagramToSVG.convert(rrDiagram); 52 | const svgContainer = document.createElement('div'); 53 | svgContainer.className = newClassName; 54 | svgContainer.innerHTML = svg; 55 | newElement.appendChild(svgContainer); 56 | } else { 57 | newElement.appendChild(document.createTextNode('Error while loading BNF: ' + bnf)); 58 | } 59 | element.parentElement.replaceChild(newElement, element); 60 | } 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import BNFDisplay from './bnfdisplay/BNFDisplay'; 2 | 3 | import BNFToGrammar from './model/bnftogrammar'; 4 | import Choice from './model/choice'; 5 | import Grammar from './model/grammar'; 6 | import GrammarToBNF from './model/grammartobnf'; 7 | import GrammarToRRDiagram from './model/grammartorrdiagram'; 8 | import Literal from './model/literal'; 9 | import Repetition from './model/repetition'; 10 | import Rule from './model/rule'; 11 | import RuleReference from './model/rulereference'; 12 | import Sequence from './model/sequence'; 13 | import SpecialSequence from './model/specialsequence'; 14 | 15 | import RRBreak from './ui/rrbreak'; 16 | import RRChoice from './ui/rrchoice'; 17 | import RRDiagram from './ui/rrdiagram'; 18 | import RRDiagramToSVG from './ui/rrdiagramtosvg'; 19 | import RRLine from './ui/rrline'; 20 | import RRLoop from './ui/rrloop'; 21 | import RRSequence from './ui/rrsequence'; 22 | import RRText from './ui/rrtext'; 23 | 24 | 25 | export const bnfdisplay = { 26 | BNFDisplay, 27 | } 28 | 29 | export const model = { 30 | BNFToGrammar, 31 | Choice, 32 | Grammar, 33 | GrammarToBNF, 34 | GrammarToRRDiagram, 35 | Literal, 36 | Repetition, 37 | Rule, 38 | RuleReference, 39 | Sequence, 40 | SpecialSequence, 41 | } 42 | 43 | export const ui = { 44 | RRBreak, 45 | RRChoice, 46 | RRDiagram, 47 | RRDiagramToSVG, 48 | RRLine, 49 | RRLoop, 50 | RRSequence, 51 | RRText, 52 | } 53 | -------------------------------------------------------------------------------- /src/model/bnftogrammar.js: -------------------------------------------------------------------------------- 1 | import Chunk from './chunk'; 2 | import Rule from './rule'; 3 | import Grammar from './grammar'; 4 | 5 | /** 6 | * @param {string} name 7 | * @param {Chunk} chunk 8 | * @param {string} originalExpressionText 9 | */ 10 | function createRule(name, chunk, originalExpressionText) { 11 | chunk.prune(); 12 | const expression = chunk.getExpression(); 13 | const rule = new Rule(name, expression, originalExpressionText); 14 | return rule; 15 | } 16 | 17 | /** 18 | * @param {Chunk} parentChunk 19 | * @param {function(): string} readNext 20 | * @param {string} stopChar 21 | * @return {string} 22 | */ 23 | function loadExpression(parentChunk, readNext, stopChar) { 24 | const expressionTextSB = []; 25 | let lastChar = 0; 26 | const sb = []; 27 | let isFirst = true; 28 | let isInSpecialGroup = false; 29 | let specialGroupChar = 0; 30 | const isLiteral = parentChunk.getType() == Chunk.ChunkType.LITERAL; 31 | for (let c; (c = readNext()) != -1;) { 32 | expressionTextSB.push(c); 33 | if (isLiteral) { 34 | if (c == stopChar) { 35 | const s = sb.join(""); 36 | parentChunk.setText(s); 37 | return expressionTextSB.join(""); 38 | } 39 | sb.push(c); 40 | } else { 41 | if (isFirst && parentChunk.getType() == Chunk.ChunkType.GROUP) { 42 | switch (c) { 43 | case '*': 44 | isInSpecialGroup = true; 45 | specialGroupChar = c; 46 | break; 47 | case '?': 48 | isInSpecialGroup = true; 49 | specialGroupChar = c; 50 | break; 51 | } 52 | } 53 | isFirst = false; 54 | if (isInSpecialGroup) { 55 | if (c == ')' && lastChar == specialGroupChar) { 56 | // Mutate parent group 57 | switch (specialGroupChar) { 58 | case '*': parentChunk.setType(Chunk.ChunkType.COMMENT); break; 59 | case '?': parentChunk.setType(Chunk.ChunkType.SPECIAL_SEQUENCE); break; 60 | } 61 | let comment = sb.join(""); 62 | comment = comment.slice(1, comment.length - 1).trim(); 63 | parentChunk.setText(comment); 64 | return expressionTextSB.join(""); 65 | } 66 | if (sb.length > 0 || !/\s/.test(c)) { 67 | sb.push(c); 68 | } 69 | } else { 70 | if (c == stopChar) { 71 | const content = sb.join("").trim(); 72 | if (content.length > 0) { 73 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 74 | } 75 | return expressionTextSB.join(""); 76 | } 77 | switch (c) { 78 | case ',': 79 | case ' ': 80 | case '\n': 81 | case '\r': 82 | case '\t': { 83 | const content = sb.join("").trim(); 84 | if (content.length > 0) { 85 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 86 | } 87 | sb.length = 0; 88 | // parentChunk.addChunk(new Chunk(Chunk.ChunkType.CONCATENATION)); 89 | break; 90 | } 91 | case '|': { 92 | const content = sb.join("").trim(); 93 | if (content.length > 0) { 94 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 95 | } 96 | sb.length = 0; 97 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.ALTERNATION)); 98 | break; 99 | } 100 | case '*': 101 | case '+': 102 | case '?': { 103 | const content = sb.join("").trim(); 104 | if (content.length > 0) { 105 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 106 | } 107 | sb.length = 0; 108 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.REPETITION_TOKEN, c)); 109 | break; 110 | } 111 | case '\"': { 112 | const content = sb.join("").trim(); 113 | if (content.length > 0) { 114 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 115 | } 116 | sb.length = 0; 117 | const literalChunk = new Chunk(Chunk.ChunkType.LITERAL); 118 | const subExpressionText = loadExpression(literalChunk, readNext, '\"'); 119 | expressionTextSB.push(subExpressionText); 120 | parentChunk.addChunk(literalChunk); 121 | break; 122 | } 123 | case '\'': { 124 | const content = sb.join("").trim(); 125 | if (content.length > 0) { 126 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 127 | } 128 | sb.length = 0; 129 | const literalChunk = new Chunk(Chunk.ChunkType.LITERAL); 130 | const subExpressionText = loadExpression(literalChunk, readNext, '\''); 131 | expressionTextSB.push(subExpressionText); 132 | parentChunk.addChunk(literalChunk); 133 | break; 134 | } 135 | case '(': { 136 | const content = sb.join("").trim(); 137 | if (content.length > 0) { 138 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 139 | } 140 | sb.length = 0; 141 | const groupChunk = new Chunk(Chunk.ChunkType.GROUP); 142 | const subExpressionText = loadExpression(groupChunk, readNext, ')'); 143 | expressionTextSB.push(subExpressionText); 144 | parentChunk.addChunk(groupChunk); 145 | break; 146 | } 147 | case '[': { 148 | const content = sb.join("").trim(); 149 | if (content.length > 0) { 150 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 151 | } 152 | sb.length = 0; 153 | const optionChunk = new Chunk(Chunk.ChunkType.OPTION); 154 | const subExpressionText = loadExpression(optionChunk, readNext, ']'); 155 | expressionTextSB.push(subExpressionText); 156 | parentChunk.addChunk(optionChunk); 157 | break; 158 | } 159 | case '{': { 160 | const content = sb.join("").trim(); 161 | if (content.length > 0) { 162 | parentChunk.addChunk(new Chunk(Chunk.ChunkType.RULE, content)); 163 | } 164 | sb.length = 0; 165 | const repetitionChunk = new Chunk(Chunk.ChunkType.REPETITION); 166 | repetitionChunk.setMinCount(0); 167 | const subExpressionText = loadExpression(repetitionChunk, readNext, '}'); 168 | expressionTextSB.push(subExpressionText); 169 | parentChunk.addChunk(repetitionChunk); 170 | break; 171 | } 172 | default: { 173 | if (sb.length > 0 || !/\s/.test(c)) { 174 | sb.push(c); 175 | } 176 | break; 177 | } 178 | } 179 | } 180 | lastChar = c; 181 | } 182 | } 183 | return expressionTextSB.join(""); 184 | } 185 | 186 | 187 | export default class BNFToGrammar { 188 | 189 | /** 190 | * @param {string} text 191 | * @return {Grammar} 192 | */ 193 | convert(text) { 194 | const readNext = (function () { 195 | // all your code here 196 | let index = 0; 197 | return function () { 198 | if (index < text.length) { 199 | const char = text[index]; 200 | index++; 201 | return char; 202 | } 203 | return -1; 204 | }; 205 | })(); 206 | const sb = []; 207 | const ruleList = []; 208 | for (let c; (c = readNext()) != -1;) { 209 | switch (c) { 210 | case '=': { 211 | const chunk = new Chunk(Chunk.ChunkType.GROUP); 212 | let expressionText = loadExpression(chunk, readNext, ';'); 213 | if(expressionText.endsWith(";")) { 214 | expressionText = expressionText.slice(0, expressionText.length - 1); 215 | } 216 | let ruleName = sb.join(""); 217 | sb.length = 0; 218 | if (ruleName.endsWith(":")) { 219 | ruleName = ruleName.slice(0, ruleName.length - 1); 220 | if (ruleName.endsWith(":")) { 221 | ruleName = ruleName.slice(0, ruleName.length - 1); 222 | } 223 | } 224 | ruleName = ruleName.trim(); 225 | const rule = createRule(ruleName, chunk, expressionText); 226 | ruleList.push(rule); 227 | break; 228 | } 229 | // Consider that '(' in rule name is start of a comment. 230 | case '(': { 231 | if (readNext() != '*') { 232 | throw "Expecting start of a comment after '(' but could not find '*'!"; 233 | } 234 | let lastChar = 0; 235 | for (let c2; (c2 = readNext()) != -1;) { 236 | if (c2 == ')' && lastChar == '*') { 237 | break; 238 | } 239 | lastChar = c2; 240 | } 241 | break; 242 | } 243 | default: { 244 | if (!/\s/.test(c) || sb.length > 0) { 245 | sb.push(c); 246 | } 247 | break; 248 | } 249 | } 250 | } 251 | return new Grammar(ruleList); 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /src/model/choice.js: -------------------------------------------------------------------------------- 1 | import Expression from './expression'; 2 | import Sequence from './sequence'; 3 | import GrammarToRRDiagram from './grammartorrdiagram'; 4 | import RRElement from '../ui/rrelement'; 5 | import GrammarToBNF from './grammartobnf'; 6 | import RRChoice from '../ui/rrchoice'; 7 | 8 | export default class Choice extends Expression { 9 | 10 | /** 11 | * @param {Expression | Expression[]} expressions 12 | */ 13 | constructor(expressions) { 14 | super(); 15 | if (arguments.length == 0) { 16 | expressions = []; 17 | } else if (expressions.constructor !== Array) { 18 | expressions = arguments; 19 | } 20 | this.expressions = expressions; 21 | } 22 | 23 | /** 24 | * @return {Expression[]} 25 | */ 26 | getExpressions() { 27 | return this.expressions; 28 | } 29 | 30 | /** 31 | * @param {GrammarToRRDiagram} grammarToRRDiagram 32 | * @return {RRElement} 33 | */ 34 | toRRElement(grammarToRRDiagram) { 35 | const rrElements = []; 36 | for (let expression of this.expressions) { 37 | rrElements.push(expression.toRRElement(grammarToRRDiagram)); 38 | } 39 | return new RRChoice(rrElements); 40 | } 41 | 42 | /** 43 | * @param {GrammarToBNF} grammarToBNF 44 | * @param {string[]} sb 45 | * @param {boolean} isNested 46 | */ 47 | toBNF(grammarToBNF, sb, isNested) { 48 | const expressionList = []; 49 | let hasNoop = false; 50 | for (const expression of this.expressions) { 51 | if (expression instanceof Sequence && expression.getExpressions().length == 0) { 52 | hasNoop = true; 53 | } else { 54 | expressionList.push(expression); 55 | } 56 | } 57 | if (expressionList.length == 0) { 58 | sb.push("( )"); 59 | } else if (hasNoop && expressionList.length == 1) { 60 | const isUsingMultiplicationTokens = grammarToBNF.isUsingMultiplicationTokens; 61 | if (!isUsingMultiplicationTokens) { 62 | sb.push("[ "); 63 | } 64 | expressionList[0].toBNF(grammarToBNF, sb, isUsingMultiplicationTokens); 65 | if (!isUsingMultiplicationTokens) { 66 | sb.push(" ]"); 67 | } 68 | } else { 69 | const isUsingMultiplicationTokens = grammarToBNF.isUsingMultiplicationTokens; 70 | if (hasNoop && !isUsingMultiplicationTokens) { 71 | sb.push("[ "); 72 | } else if (hasNoop || isNested && expressionList.length > 1) { 73 | sb.push("( "); 74 | } 75 | const count = expressionList.length; 76 | for (let i = 0; i < count; i++) { 77 | if (i > 0) { 78 | sb.push(" | "); 79 | } 80 | expressionList[i].toBNF(grammarToBNF, sb, false); 81 | } 82 | if (hasNoop && !isUsingMultiplicationTokens) { 83 | sb.push(" ]"); 84 | } else if (hasNoop || isNested && expressionList.length > 1) { 85 | sb.push(" )"); 86 | if (hasNoop) { 87 | sb.push("?"); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * @param {*} o 95 | * @return {boolean} 96 | */ 97 | equals(o) { 98 | if(!(o instanceof Choice)) { 99 | return false; 100 | } 101 | if(this.expressions.length != o.expressions.length) { 102 | return false; 103 | } 104 | for (let i = 0; i < this.expressions.length; i++) { 105 | if(!this.expressions[i].equals(o.expressions[i])) { 106 | return false; 107 | } 108 | } 109 | return true; 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /src/model/chunk.js: -------------------------------------------------------------------------------- 1 | import Sequence from './sequence'; 2 | import SpecialSequence from './specialsequence'; 3 | import Repetition from './repetition'; 4 | import RuleReference from './rulereference'; 5 | import Choice from './choice'; 6 | import Literal from './literal'; 7 | import Expression from './expression'; 8 | 9 | 10 | const ChunkType = { 11 | RULE: 'RULE', 12 | REPETITION_TOKEN: 'REPETITION_TOKEN', 13 | // CONCATENATION: 'CONCATENATION', 14 | ALTERNATION: 'ALTERNATION', 15 | GROUP: 'GROUP', 16 | COMMENT: 'COMMENT', 17 | SPECIAL_SEQUENCE: 'SPECIAL_SEQUENCE', 18 | LITERAL: 'LITERAL', 19 | OPTION: 'OPTION', 20 | REPETITION: 'REPETITION', 21 | CHOICE: 'CHOICE', 22 | }; 23 | 24 | /** 25 | * @param {Expression} expression 26 | * @return {boolean} 27 | */ 28 | function isNoop(expression) { 29 | return expression instanceof Sequence && expression.getExpressions().length == 0; 30 | } 31 | 32 | export default class Chunk { 33 | 34 | static get ChunkType() { 35 | return ChunkType; 36 | } 37 | 38 | constructor(type, text) { 39 | this.type = type; 40 | this.text = text; 41 | this.minCount = 0; 42 | this.maxCount = null; 43 | this.chunkList = null; 44 | } 45 | 46 | getType() { 47 | return this.type; 48 | } 49 | 50 | setType(type) { 51 | this.type = type; 52 | } 53 | 54 | setText(text) { 55 | this.text = text; 56 | } 57 | 58 | setMinCount(minCount) { 59 | this.minCount = minCount; 60 | } 61 | 62 | setMaxCount(maxCount) { 63 | this.maxCount = maxCount; 64 | } 65 | 66 | addChunk(chunk) { 67 | if (this.chunkList == null) { 68 | this.chunkList = []; 69 | } 70 | this.chunkList.push(chunk); 71 | } 72 | 73 | prune() { 74 | let hasAlternation = false; 75 | for (let i = this.chunkList.length - 1; i >= 0; i--) { 76 | const chunk = this.chunkList[i]; 77 | switch (chunk.getType()) { 78 | case ChunkType.REPETITION_TOKEN: { 79 | if ("*" === chunk.text) { 80 | this.chunkList.splice(i, 1); 81 | const previousChunk = this.chunkList[i - 1]; 82 | let multiplier = null; 83 | // Case of: 3 * expression 84 | if (previousChunk.getType() == ChunkType.RULE) { 85 | multiplier = +previousChunk.text; 86 | if(isNaN(multiplier)) { 87 | multiplier = null; 88 | } 89 | } 90 | if (multiplier != null) { 91 | // The current one is removed, so next one is at index i. 92 | const nextChunk = this.chunkList[i]; 93 | if (nextChunk.getType() == ChunkType.OPTION) { 94 | const newChunk = new Chunk(ChunkType.REPETITION); 95 | newChunk.setMinCount(0); 96 | newChunk.setMaxCount(multiplier); 97 | for (const c of nextChunk.chunkList) { 98 | newChunk.addChunk(c); 99 | } 100 | this.chunkList.splice(i, 1); 101 | this.chunkList[i - 1] = newChunk; 102 | } else { 103 | const newChunk = new Chunk(ChunkType.REPETITION); 104 | newChunk.setMinCount(multiplier); 105 | newChunk.setMaxCount(multiplier); 106 | newChunk.addChunk(nextChunk); 107 | this.chunkList.splice(i, 1); 108 | this.chunkList[i - 1] = newChunk; 109 | } 110 | } else { 111 | const newChunk = new Chunk(ChunkType.REPETITION); 112 | newChunk.setMinCount(0); 113 | newChunk.addChunk(previousChunk); 114 | this.chunkList[i - 1] = newChunk; 115 | } 116 | } else if ("+" === chunk.text) { 117 | this.chunkList.splice(i, 1); 118 | const newChunk = new Chunk(ChunkType.REPETITION); 119 | newChunk.setMinCount(1); 120 | const previousChunk = this.chunkList[i - 1]; 121 | newChunk.addChunk(previousChunk); 122 | this.chunkList[i - 1] = newChunk; 123 | } else if ("?" === chunk.text) { 124 | this.chunkList.splice(i, 1); 125 | const newChunk = new Chunk(ChunkType.OPTION); 126 | const previousChunk = this.chunkList[i - 1]; 127 | newChunk.addChunk(previousChunk); 128 | this.chunkList[i - 1] = newChunk; 129 | } 130 | break; 131 | } 132 | case ChunkType.COMMENT: { 133 | // For now, nothing to do 134 | this.chunkList.splice(i, 1); 135 | } 136 | case ChunkType.ALTERNATION: { 137 | hasAlternation = true; 138 | break; 139 | } 140 | case ChunkType.GROUP: { 141 | // Group could be empty 142 | if (chunk.chunkList != null) { 143 | chunk.prune(); 144 | if (chunk.chunkList.length == 1) { 145 | this.chunkList[i] = chunk.chunkList[0]; 146 | } 147 | } 148 | break; 149 | } 150 | case ChunkType.OPTION: 151 | case ChunkType.REPETITION: { 152 | chunk.prune(); 153 | break; 154 | } 155 | } 156 | } 157 | if (hasAlternation) { 158 | const alternationSequenceList = []; 159 | alternationSequenceList.push([]); 160 | for (const chunk of this.chunkList) { 161 | if (chunk.getType() == ChunkType.ALTERNATION) { 162 | alternationSequenceList.push([]); 163 | } else { 164 | const list = alternationSequenceList[alternationSequenceList.length - 1]; 165 | list.push(chunk); 166 | } 167 | } 168 | const choiceChunk = new Chunk(ChunkType.CHOICE); 169 | for (const subList of alternationSequenceList) { 170 | if (subList.length == 1) { 171 | choiceChunk.addChunk(subList[0]); 172 | } else { 173 | const groupChunk = new Chunk(ChunkType.GROUP); 174 | for (const c of subList) { 175 | groupChunk.addChunk(c); 176 | } 177 | choiceChunk.addChunk(groupChunk); 178 | } 179 | } 180 | this.chunkList.length = 0; 181 | this.chunkList.push(choiceChunk); 182 | } 183 | } 184 | 185 | getExpression() { 186 | switch (this.type) { 187 | case ChunkType.GROUP: { 188 | if (this.chunkList == null) { 189 | // Group is empty. 190 | return new Sequence(); 191 | } 192 | if (this.chunkList.length == 1) { 193 | return this.chunkList[0].getExpression(); 194 | } 195 | const expressionList = []; 196 | for (const chunk of this.chunkList) { 197 | expressionList.push(chunk.getExpression()); 198 | } 199 | return new Sequence(expressionList); 200 | } 201 | case ChunkType.CHOICE: { 202 | if (this.chunkList.length == 1) { 203 | return this.chunkList[0].getExpression(); 204 | } 205 | const expressionList = []; 206 | let hasLine = false; 207 | for (const chunk of this.chunkList) { 208 | let expression = chunk.getExpression(); 209 | if (expression instanceof Repetition) { 210 | const repetition = expression; 211 | if (repetition.getMinRepetitionCount() == 0) { 212 | if (repetition.getMaxRepetitionCount() == null || repetition.getMaxRepetitionCount() != 1) { 213 | expression = new Repetition(repetition.getExpression(), 1, repetition.getMaxRepetitionCount()); 214 | } else { 215 | expression = repetition.getExpression(); 216 | } 217 | hasLine = true; 218 | } 219 | } 220 | if (expression instanceof Choice) { 221 | for (const exp of expression.getExpressions()) { 222 | expressionList.push(exp); 223 | } 224 | } else { 225 | expressionList.push(expression); 226 | } 227 | } 228 | if (hasLine && (expressionList.length == 0 || !isNoop(expressionList[expressionList.length - 1]))) { 229 | expressionList.push(new Sequence()); 230 | } 231 | return new Choice(expressionList); 232 | } 233 | case ChunkType.RULE: { 234 | return new RuleReference(this.text); 235 | } 236 | case ChunkType.LITERAL: { 237 | return new Literal(this.text); 238 | } 239 | case ChunkType.SPECIAL_SEQUENCE: { 240 | return new SpecialSequence(this.text); 241 | } 242 | case ChunkType.OPTION: { 243 | if (this.chunkList.length == 1) { 244 | const subChunk = this.chunkList[0]; 245 | if (subChunk.getType() == ChunkType.CHOICE) { 246 | const newChunk = new Chunk(ChunkType.CHOICE); 247 | for (const cChunk of subChunk.chunkList) { 248 | newChunk.addChunk(cChunk); 249 | } 250 | newChunk.addChunk(new Chunk(ChunkType.GROUP)); 251 | return newChunk.getExpression(); 252 | } 253 | return new Repetition(subChunk.getExpression(), 0, 1); 254 | } 255 | const expressionList = []; 256 | for (const chunk of this.chunkList) { 257 | expressionList.push(chunk.getExpression()); 258 | } 259 | return new Repetition(new Sequence(expressionList), 0, 1); 260 | } 261 | case ChunkType.REPETITION: { 262 | if (this.chunkList.length == 1) { 263 | return new Repetition(this.chunkList[0].getExpression(), this.minCount, this.maxCount); 264 | } 265 | const expressionList = []; 266 | for (const chunk of this.chunkList) { 267 | expressionList.push(chunk.getExpression()); 268 | } 269 | return new Repetition(new Sequence(expressionList), this.minCount, this.maxCount); 270 | } 271 | } 272 | throw "Type should not be reachable: " + this.type; 273 | } 274 | 275 | toString() { 276 | let s = "" + this.type; 277 | if (this.text != null) { 278 | s += " (" + this.text + ")"; 279 | } 280 | return s; 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /src/model/expression.js: -------------------------------------------------------------------------------- 1 | import GrammarToRRDiagram from './grammartorrdiagram'; 2 | import GrammarToBNF from './grammartobnf'; 3 | import RRElement from '../ui/rrelement'; 4 | 5 | export default class Expression { 6 | 7 | /** 8 | * @param {GrammarToRRDiagram} grammarToRRDiagram 9 | * @return {RRElement} 10 | */ 11 | toRRElement(grammarToRRDiagram) { 12 | // Not reachable, we don't instanciate this class. 13 | return new RRElement(); 14 | } 15 | 16 | /** 17 | * @param {GrammarToBNF} grammarToBNF 18 | * @param {string[]} sb 19 | * @param {boolean} isNested 20 | */ 21 | toBNF(grammarToBNF, sb, isNested) { 22 | } 23 | 24 | /** 25 | * @param {*} o 26 | * @return {boolean} 27 | */ 28 | equals(o) { 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/model/grammar.js: -------------------------------------------------------------------------------- 1 | import Rule from './rule'; 2 | import GrammarToBNF from './grammartobnf'; 3 | 4 | export default class Grammar { 5 | 6 | constructor(rules) { 7 | if(arguments.length == 0) { 8 | rules = []; 9 | } else if (rules.constructor !== Array) { 10 | rules = arguments; 11 | } 12 | this.rules = rules; 13 | } 14 | 15 | /** 16 | * @return {Rule[]} 17 | */ 18 | getRules() { 19 | return this.rules; 20 | } 21 | 22 | /** 23 | * @param {GrammarToBNF} grammarToBNF 24 | * @return {string} 25 | */ 26 | toBNF(grammarToBNF) { 27 | const sb = []; 28 | for (let i = 0; i < this.rules.length; i++) { 29 | if (i > 0) { 30 | sb.push("\n"); 31 | } 32 | sb.push(this.rules[i].toBNF(grammarToBNF)); 33 | } 34 | return sb.join(""); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/model/grammartobnf.js: -------------------------------------------------------------------------------- 1 | import Grammar from './grammar'; 2 | 3 | const RuleDefinitionSign = { 4 | EQUAL: 1, 5 | COLON_EQUAL: 2, 6 | COLON_COLON_EQUAL: 3, 7 | }; 8 | 9 | const LiteralDefinitionSign = { 10 | QUOTE: 1, 11 | DOUBLE_QUOTE: 2, 12 | }; 13 | 14 | export default class GrammarToBNF { 15 | 16 | static get RuleDefinitionSign() { 17 | return RuleDefinitionSign; 18 | } 19 | 20 | static get LiteralDefinitionSign() { 21 | return LiteralDefinitionSign; 22 | } 23 | 24 | constructor() { 25 | this.ruleDefinitionSign = RuleDefinitionSign.EQUAL; 26 | this.literalDefinitionSign = LiteralDefinitionSign.QUOTE; 27 | this.isCommaSeparator = false; 28 | this.isUsingMultiplicationTokens = false; 29 | this.ruleConsideredAsLineBreak = null; 30 | } 31 | 32 | /** 33 | * @param {Grammar} grammar 34 | * @return {string} 35 | */ 36 | convert(grammar) { 37 | return grammar.toBNF(this); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/model/grammartorrdiagram.js: -------------------------------------------------------------------------------- 1 | import Rule from './rule'; 2 | import RRDiagram from '../ui/rrdiagram'; 3 | 4 | export default class GrammarToRRDiagram { 5 | 6 | constructor() { 7 | this.ruleLinkProvider = (ruleName) => '#' + ruleName; 8 | this.ruleConsideredAsLineBreak = null; 9 | } 10 | 11 | /** 12 | * @param {Rule} rule 13 | * @return {RRDiagram} 14 | */ 15 | convert(rule) { 16 | return rule.toRRDiagram(this); 17 | } 18 | } -------------------------------------------------------------------------------- /src/model/literal.js: -------------------------------------------------------------------------------- 1 | import Expression from './expression'; 2 | import RRText from '../ui/rrtext'; 3 | import GrammarToRRDiagram from './grammartorrdiagram'; 4 | import RRElement from '../ui/rrelement'; 5 | import GrammarToBNF from './grammartobnf'; 6 | 7 | export default class Literal extends Expression { 8 | 9 | /** 10 | * @param {string} text 11 | */ 12 | constructor(text) { 13 | super(); 14 | this.text = text; 15 | } 16 | 17 | /** 18 | * @param {GrammarToRRDiagram} grammarToRRDiagram 19 | * @return {RRElement} 20 | */ 21 | toRRElement(grammarToRRDiagram) { 22 | return new RRText(RRText.Type.LITERAL, this.text, null); 23 | } 24 | 25 | /** 26 | * @param {GrammarToBNF} grammarToBNF 27 | * @param {string[]} sb 28 | * @param {boolean} isNested 29 | */ 30 | toBNF(grammarToBNF, sb, isNested) { 31 | const c = grammarToBNF.literalDefinitionSign == GrammarToBNF.LiteralDefinitionSign.DOUBLE_QUOTE ? '"' : '\''; 32 | sb.push(c); 33 | sb.push(this.text); 34 | sb.push(c); 35 | } 36 | 37 | /** 38 | * @param {*} o 39 | * @return {boolean} 40 | */ 41 | equals(o) { 42 | if(!(o instanceof Literal)) { 43 | return false; 44 | } 45 | return this.text == o.text; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/model/repetition.js: -------------------------------------------------------------------------------- 1 | import Expression from './expression'; 2 | import RRChoice from '../ui/rrchoice'; 3 | import RRLine from '../ui/rrline'; 4 | import RRLoop from '../ui/rrloop'; 5 | import GrammarToRRDiagram from './grammartorrdiagram'; 6 | import RRElement from '../ui/rrelement'; 7 | import GrammarToBNF from './grammartobnf'; 8 | 9 | export default class Repetition extends Expression { 10 | 11 | /** 12 | * @param {Expression} expression 13 | * @param {number} minRepetitionCount 14 | * @param {?number} maxRepetitionCount 15 | */ 16 | constructor(expression, minRepetitionCount, maxRepetitionCount) { 17 | super(); 18 | this.expression = expression; 19 | this.minRepetitionCount = minRepetitionCount | 0; 20 | this.maxRepetitionCount = maxRepetitionCount; 21 | } 22 | 23 | /** 24 | * @return {Expression} 25 | */ 26 | getExpression() { 27 | return this.expression; 28 | } 29 | 30 | /** 31 | * @return {number} 32 | */ 33 | getMinRepetitionCount() { 34 | return this.minRepetitionCount; 35 | } 36 | 37 | /** 38 | * @return {?number} 39 | */ 40 | getMaxRepetitionCount() { 41 | return this.maxRepetitionCount; 42 | } 43 | 44 | /** 45 | * @param {GrammarToRRDiagram} grammarToRRDiagram 46 | * @return {RRElement} 47 | */ 48 | toRRElement(grammarToRRDiagram) { 49 | const rrElement = this.expression.toRRElement(grammarToRRDiagram); 50 | if (this.minRepetitionCount == 0) { 51 | if (this.maxRepetitionCount == null || this.maxRepetitionCount > 1) { 52 | return new RRChoice(new RRLoop(rrElement, null, 0, (this.maxRepetitionCount == null ? null : this.maxRepetitionCount - 1)), new RRLine()); 53 | } 54 | return new RRChoice(rrElement, new RRLine()); 55 | } 56 | return new RRLoop(rrElement, null, this.minRepetitionCount - 1, (this.maxRepetitionCount == null ? null : this.maxRepetitionCount - 1)); 57 | } 58 | 59 | /** 60 | * @param {GrammarToBNF} grammarToBNF 61 | * @param {string[]} sb 62 | * @param {boolean} isNested 63 | */ 64 | toBNF(grammarToBNF, sb, isNested) { 65 | const isUsingMultiplicationTokens = grammarToBNF.isUsingMultiplicationTokens; 66 | if (this.maxRepetitionCount == null) { 67 | if (this.minRepetitionCount > 0) { 68 | if (this.minRepetitionCount == 1 && isUsingMultiplicationTokens) { 69 | this.expression.toBNF(grammarToBNF, sb, true); 70 | sb.push("+"); 71 | } else { 72 | if (isNested) { 73 | sb.push("( "); 74 | } 75 | if (this.minRepetitionCount > 1) { 76 | sb.push(this.minRepetitionCount, " * "); 77 | } 78 | this.expression.toBNF(grammarToBNF, sb, false); 79 | if (grammarToBNF.isCommaSeparator) { 80 | sb.push(" ,"); 81 | } 82 | sb.push(" ", "{ "); 83 | this.expression.toBNF(grammarToBNF, sb, false); 84 | sb.push(" }"); 85 | if (isNested) { 86 | sb.push(" )"); 87 | } 88 | } 89 | } else { 90 | if (isUsingMultiplicationTokens) { 91 | this.expression.toBNF(grammarToBNF, sb, true); 92 | sb.push("*"); 93 | } else { 94 | sb.push("{ "); 95 | this.expression.toBNF(grammarToBNF, sb, false); 96 | sb.push(" }"); 97 | } 98 | } 99 | } else { 100 | if (this.minRepetitionCount == 0) { 101 | if (this.maxRepetitionCount == 1 && isUsingMultiplicationTokens) { 102 | this.expression.toBNF(grammarToBNF, sb, true); 103 | sb.push("?"); 104 | } else { 105 | if (this.maxRepetitionCount > 1) { 106 | sb.push(this.maxRepetitionCount, " * "); 107 | } 108 | sb.push("[ "); 109 | this.expression.toBNF(grammarToBNF, sb, false); 110 | sb.push(" ]"); 111 | } 112 | } else { 113 | if (this.minRepetitionCount == this.maxRepetitionCount) { 114 | sb.push(this.minRepetitionCount, " * "); 115 | this.expression.toBNF(grammarToBNF, sb, isNested); 116 | } else { 117 | if (isNested) { 118 | sb.push("( "); 119 | } 120 | sb.push(this.minRepetitionCount, " * "); 121 | this.expression.toBNF(grammarToBNF, sb, false); 122 | if (grammarToBNF.isCommaSeparator) { 123 | sb.push(" ,"); 124 | } 125 | sb.push(" ", this.maxRepetitionCount - this.minRepetitionCount, " * ", "[ "); 126 | this.expression.toBNF(grammarToBNF, sb, false); 127 | sb.push(" ]"); 128 | if (isNested) { 129 | sb.push(" )"); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * @param {*} o 138 | * @return {boolean} 139 | */ 140 | equals(o) { 141 | if(!(o instanceof Repetition)) { 142 | return false; 143 | } 144 | return this.expression.equals(o.expression) && this.minRepetitionCount == o.minRepetitionCount && this.maxRepetitionCount == null? o.maxRepetitionCount == null: this.maxRepetitionCount.equals(o.maxRepetitionCount); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/model/rule.js: -------------------------------------------------------------------------------- 1 | import RRDiagram from '../ui/rrdiagram'; 2 | import GrammarToBNF from './grammartobnf'; 3 | import Expression from './expression'; 4 | import GrammarToRRDiagram from './grammartorrdiagram'; 5 | 6 | export default class Rule { 7 | 8 | /** 9 | * @param {string} name 10 | * @param {Expression} expression 11 | * @param {?string} originalExpressionText 12 | */ 13 | constructor(name, expression, originalExpressionText) { 14 | this.name = name; 15 | this.expression = expression; 16 | this.originalExpressionText = originalExpressionText; 17 | } 18 | 19 | /** 20 | * @return {string} 21 | */ 22 | getName() { 23 | return this.name; 24 | } 25 | 26 | /** 27 | * @return {?string} 28 | */ 29 | getOriginalExpressionText() { 30 | return this.originalExpressionText; 31 | } 32 | 33 | /** 34 | * @param {GrammarToRRDiagram} grammarToRRDiagram 35 | * @return {RRDiagram} 36 | */ 37 | toRRDiagram(grammarToRRDiagram) { 38 | return new RRDiagram(this.expression.toRRElement(grammarToRRDiagram)); 39 | } 40 | 41 | /** 42 | * @param {GrammarToBNF} grammarToBNF 43 | * @return {string} 44 | */ 45 | toBNF(grammarToBNF) { 46 | const sb = []; 47 | sb.push(this.name, " "); 48 | switch (grammarToBNF.ruleDefinitionSign) { 49 | case GrammarToBNF.RuleDefinitionSign.EQUAL: sb.push("="); break; 50 | case GrammarToBNF.RuleDefinitionSign.COLON_EQUAL: sb.push(":="); break; 51 | case GrammarToBNF.RuleDefinitionSign.COLON_COLON_EQUAL: sb.push("::="); break; 52 | } 53 | sb.push(" "); 54 | this.expression.toBNF(grammarToBNF, sb, false); 55 | sb.push(";"); 56 | return sb.join(""); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/model/rulereference.js: -------------------------------------------------------------------------------- 1 | import Expression from './expression'; 2 | import RRBreak from '../ui/rrbreak'; 3 | import RRText from '../ui/rrtext'; 4 | import GrammarToRRDiagram from './grammartorrdiagram'; 5 | import RRElement from '../ui/rrelement'; 6 | import GrammarToBNF from './grammartobnf'; 7 | 8 | export default class RuleReference extends Expression { 9 | 10 | /** 11 | * @param {string} ruleName 12 | */ 13 | constructor(ruleName) { 14 | super(); 15 | this.ruleName = ruleName; 16 | } 17 | 18 | getRuleName() { 19 | return this.ruleName; 20 | } 21 | 22 | /** 23 | * @param {GrammarToRRDiagram} grammarToRRDiagram 24 | * @return {RRElement} 25 | */ 26 | toRRElement(grammarToRRDiagram) { 27 | const ruleConsideredAsLineBreak = grammarToRRDiagram.ruleConsideredAsLineBreak; 28 | if (ruleConsideredAsLineBreak != null && ruleConsideredAsLineBreak === this.ruleName) { 29 | return new RRBreak(); 30 | } 31 | const ruleLinkProvider = grammarToRRDiagram.ruleLinkProvider; 32 | return new RRText(RRText.Type.RULE, this.ruleName, ruleLinkProvider == null ? null : ruleLinkProvider(this.ruleName)); 33 | } 34 | 35 | /** 36 | * @param {GrammarToBNF} grammarToBNF 37 | * @param {string[]} sb 38 | * @param {boolean} isNested 39 | */ 40 | toBNF(grammarToBNF, sb, isNested) { 41 | sb.push(this.ruleName); 42 | const ruleConsideredAsLineBreak = grammarToBNF.ruleConsideredAsLineBreak; 43 | if (ruleConsideredAsLineBreak != null && ruleConsideredAsLineBreak === this.ruleName) { 44 | sb.push("\n"); 45 | } 46 | } 47 | 48 | /** 49 | * @param {*} o 50 | * @return {boolean} 51 | */ 52 | equals(o) { 53 | if(!(o instanceof RuleReference)) { 54 | return false; 55 | } 56 | return this.ruleName == o.ruleName; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/model/sequence.js: -------------------------------------------------------------------------------- 1 | import Expression from './expression'; 2 | import RRLoop from '../ui/rrloop'; 3 | import RRSequence from '../ui/rrsequence'; 4 | import RuleReference from './rulereference'; 5 | import Repetition from './repetition'; 6 | import Literal from './literal'; 7 | import GrammarToRRDiagram from './grammartorrdiagram'; 8 | import RRElement from '../ui/rrelement'; 9 | import GrammarToBNF from './grammartobnf'; 10 | 11 | export default class Sequence extends Expression { 12 | 13 | /** 14 | * @param {(Expression | Expression[])} expressions 15 | */ 16 | constructor(expressions) { 17 | super(); 18 | if (arguments.length == 0) { 19 | expressions = []; 20 | } else if (expressions.constructor !== Array) { 21 | expressions = arguments; 22 | } 23 | this.expressions = expressions; 24 | } 25 | 26 | /** 27 | * @param {Expression[]} 28 | */ 29 | getExpressions() { 30 | return this.expressions; 31 | } 32 | 33 | /** 34 | * @param {GrammarToRRDiagram} grammarToRRDiagram 35 | * @return {RRElement} 36 | */ 37 | toRRElement(grammarToRRDiagram) { 38 | const rrElementList = []; 39 | for (let i = 0; i < this.expressions.length; i++) { 40 | const expression = this.expressions[i]; 41 | let rrElement = expression.toRRElement(grammarToRRDiagram); 42 | // Treat special case of: "a (',' a)*" and "a (a)*" 43 | if (i < this.expressions.length - 1 && this.expressions[i + 1] instanceof Repetition) { 44 | const repetition = this.expressions[i + 1]; 45 | const repetitionExpression = repetition.getExpression(); 46 | if (repetitionExpression instanceof Sequence) { 47 | // Treat special case of: "expr (',' expr)*" 48 | const subExpressions = repetitionExpression.getExpressions(); 49 | if (subExpressions.length == 2 && subExpressions[0] instanceof Literal) { 50 | if(expression.equals(subExpressions[1])) { 51 | const maxRepetitionCount = repetition.getMaxRepetitionCount(); 52 | if (maxRepetitionCount == null || maxRepetitionCount > 1) { 53 | rrElement = new RRLoop(expression.toRRElement(grammarToRRDiagram), subExpressions[0].toRRElement(grammarToRRDiagram), repetition.getMinRepetitionCount(), (maxRepetitionCount == null ? null : maxRepetitionCount)); 54 | i++; 55 | } 56 | } 57 | } 58 | } else if(expression instanceof RuleReference) { 59 | const ruleLink = expression; 60 | // Treat special case of: a (a)* 61 | if (repetitionExpression instanceof RuleReference && repetitionExpression.getRuleName().equals(ruleLink.getRuleName())) { 62 | const maxRepetitionCount = repetition.getMaxRepetitionCount(); 63 | if (maxRepetitionCount == null || maxRepetitionCount > 1) { 64 | rrElement = new RRLoop(ruleLink.toRRElement(grammarToRRDiagram), null, repetition.getMinRepetitionCount(), (maxRepetitionCount == null ? null : maxRepetitionCount)); 65 | i++; 66 | } 67 | } 68 | } 69 | } 70 | rrElementList.push(rrElement); 71 | } 72 | return new RRSequence(rrElementList); 73 | } 74 | 75 | /** 76 | * @param {GrammarToBNF} grammarToBNF 77 | * @param {string[]} sb 78 | * @param {boolean} isNested 79 | */ 80 | toBNF(grammarToBNF, sb, isNested) { 81 | if (this.expressions.length == 0) { 82 | sb.push("( )"); 83 | return; 84 | } 85 | if (isNested && this.expressions.length > 1) { 86 | sb.push("( "); 87 | } 88 | const isCommaSeparator = grammarToBNF.isCommaSeparator; 89 | for (let i = 0; i < this.expressions.length; i++) { 90 | if (i > 0) { 91 | if (isCommaSeparator) { 92 | sb.push(" ,"); 93 | } 94 | sb.push(" "); 95 | } 96 | this.expressions[i].toBNF(grammarToBNF, sb, this.expressions.length == 1 && isNested || !isCommaSeparator); 97 | } 98 | if (isNested && this.expressions.length > 1) { 99 | sb.push(" )"); 100 | } 101 | } 102 | 103 | /** 104 | * @param {*} o 105 | * @return {boolean} 106 | */ 107 | equals(o) { 108 | if(!(o instanceof Sequence)) { 109 | return false; 110 | } 111 | if(this.expressions.length != o.expressions.length) { 112 | return false; 113 | } 114 | for (let i = 0; i < this.expressions.length; i++) { 115 | if(!this.expressions[i].equals(o.expressions[i])) { 116 | return false; 117 | } 118 | } 119 | return true; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/model/specialsequence.js: -------------------------------------------------------------------------------- 1 | import Expression from './expression'; 2 | import RRText from '../ui/rrtext'; 3 | import GrammarToRRDiagram from './grammartorrdiagram'; 4 | import RRElement from '../ui/rrelement'; 5 | import GrammarToBNF from './grammartobnf'; 6 | 7 | export default class SpecialSequence extends Expression { 8 | 9 | /** 10 | * @param {string} text 11 | */ 12 | constructor(text) { 13 | super(); 14 | this.text = text; 15 | } 16 | 17 | /** 18 | * @param {GrammarToRRDiagram} grammarToRRDiagram 19 | * @return {RRElement} 20 | */ 21 | toRRElement(grammarToRRDiagram) { 22 | return new RRText(RRText.Type.SPECIAL_SEQUENCE, this.text, null); 23 | } 24 | 25 | /** 26 | * @param {GrammarToBNF} grammarToBNF 27 | * @param {string[]} sb 28 | * @param {boolean} isNested 29 | */ 30 | toBNF(grammarToBNF, sb, isNested) { 31 | sb.push("(? "); 32 | sb.push(this.text); 33 | sb.push(" ?)"); 34 | } 35 | 36 | /** 37 | * @param {*} o 38 | * @return {boolean} 39 | */ 40 | equals(o) { 41 | if(!(o instanceof SpecialSequence)) { 42 | return false; 43 | } 44 | return this.text == o.text; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/ui/layoutinfo.js: -------------------------------------------------------------------------------- 1 | export default class LayoutInfo { 2 | 3 | /** 4 | * @param {number} width 5 | * @param {number} height 6 | * @param {number} connectorOffset 7 | */ 8 | constructor(width, height, connectorOffset) { 9 | this.width = width; 10 | this.height = height; 11 | this.connectorOffset = connectorOffset; 12 | } 13 | 14 | /** 15 | * @return {number} 16 | */ 17 | getWidth() { 18 | return this.width; 19 | } 20 | 21 | /** 22 | * @return {number} 23 | */ 24 | getHeight() { 25 | return this.height; 26 | } 27 | 28 | /** 29 | * @return {number} 30 | */ 31 | getConnectorOffset() { 32 | return this.connectorOffset; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/rrbreak.js: -------------------------------------------------------------------------------- 1 | import RRElement from './rrelement'; 2 | export default class RRBreak extends RRElement { 3 | 4 | constructor() { 5 | super(); 6 | } 7 | 8 | computeLayoutInfo(rrDiagramToSVG) { 9 | throw "This element must not be nested and should have been processed before entering generation."; 10 | } 11 | 12 | toSVG(rrDiagramToSVG, xOffset, yOffset, svgContent) { 13 | throw "This element must not be nested and should have been processed before entering generation."; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/rrchoice.js: -------------------------------------------------------------------------------- 1 | import RRElement from './rrelement'; 2 | import LayoutInfo from './layoutinfo'; 3 | 4 | export default class RRChoice extends RRElement { 5 | 6 | /** 7 | * @param {(RRElement[] | RRElement)} rrElements 8 | */ 9 | constructor(rrElements) { 10 | super(); 11 | if(arguments.length == 0) { 12 | rrElements = []; 13 | } else if(rrElements.constructor !== Array) { 14 | rrElements = arguments; 15 | } 16 | this.rrElements = rrElements; 17 | } 18 | 19 | computeLayoutInfo(rrDiagramToSVG) { 20 | let width = 0; 21 | let height = 0; 22 | let connectorOffset = 0; 23 | for (let i = 0; i < this.rrElements.length; i++) { 24 | const rrElement = this.rrElements[i]; 25 | rrElement.computeLayoutInfo(rrDiagramToSVG); 26 | const layoutInfo = rrElement.getLayoutInfo(); 27 | if (i == 0) { 28 | connectorOffset = layoutInfo.getConnectorOffset(); 29 | } else { 30 | height += 5; 31 | } 32 | height += layoutInfo.getHeight(); 33 | width = Math.max(width, layoutInfo.getWidth()); 34 | } 35 | width += 20 + 20; 36 | this.setLayoutInfo(new LayoutInfo(width, height, connectorOffset)); 37 | } 38 | 39 | toSVG(rrDiagramToSVG, xOffset, yOffset, svgContent) { 40 | const layoutInfo = this.getLayoutInfo(); 41 | const y1 = yOffset + layoutInfo.getConnectorOffset(); 42 | const x1 = xOffset + 10; 43 | const x2 = xOffset + layoutInfo.getWidth() - 10; 44 | const xOffset2 = xOffset + 20; 45 | let y2 = 0; 46 | let yOffset2 = yOffset; 47 | for (let i = 0; i < this.rrElements.length; i++) { 48 | const rrElement = this.rrElements[i]; 49 | const layoutInfo2 = rrElement.getLayoutInfo(); 50 | const width = layoutInfo2.getWidth(); 51 | const height = layoutInfo2.getHeight(); 52 | y2 = yOffset2 + layoutInfo2.getConnectorOffset(); 53 | if (i == 0) { 54 | // Line to first element 55 | svgContent.addLineConnector(x1 - 10, y1, x1 + 10, y1); 56 | } else { 57 | if (i == this.rrElements.length - 1) { 58 | // Curve and vertical down 59 | svgContent.addPathConnector(x1 - 5, y1, "q5 0 5 5", x1, y1 + 5); 60 | svgContent.addLineConnector(x1, y1 + 5, x1, y2 - 5); 61 | } 62 | // Curve and horizontal line to element 63 | svgContent.addPathConnector(x1, y2 - 5, "q0 5 5 5", x1 + 5, y2); 64 | svgContent.addLineConnector(x1 + 5, y2, xOffset2, y2); 65 | } 66 | rrElement.toSVG(rrDiagramToSVG, xOffset2, yOffset2, svgContent); 67 | if (i == 0) { 68 | // Line to first element 69 | svgContent.addLineConnector(xOffset2 + width, y2, x2 + 10, y2); 70 | } else { 71 | // Horizontal line to element and curve 72 | svgContent.addLineConnector(x2 - 5, y2, xOffset2 + width, y2); 73 | svgContent.addPathConnector(x2 - 5, y2, "q5 0 5-5", x2, y2 - 5); 74 | if (i == this.rrElements.length - 1) { 75 | // Vertical up and curve 76 | svgContent.addLineConnector(x2, y2 - 5, x2, y1 + 5); 77 | svgContent.addPathConnector(x2, y1 + 5, "q0-5 5-5", x2 + 5, y1); 78 | } 79 | } 80 | yOffset2 += height + 5; 81 | } 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /src/ui/rrdiagram.js: -------------------------------------------------------------------------------- 1 | import RRElement from './rrelement'; 2 | import RRSequence from './rrsequence'; 3 | import SvgContent from './svg/svgcontent'; 4 | import RRBreak from './rrbreak'; 5 | 6 | export default class RRDiagram { 7 | 8 | /** 9 | * @param {RRElement} rrElement 10 | */ 11 | constructor(rrElement) { 12 | this.rrElement = rrElement; 13 | } 14 | 15 | toSVG(rrDiagramToSVG) { 16 | const rrElementList = []; 17 | if (this.rrElement instanceof RRSequence) { 18 | const cursorElementList = []; 19 | for (let element of this.rrElement.getRRElements()) { 20 | if (element instanceof RRBreak) { 21 | if (cursorElementList.length != 0) { 22 | rrElementList.push(cursorElementList.length == 1 ? cursorElementList[0] : new RRSequence(cursorElementList.slice())); 23 | cursorElementList.length = 0; 24 | } 25 | } else { 26 | cursorElementList.push(element); 27 | } 28 | } 29 | if (cursorElementList.length != 0) { 30 | rrElementList.push(cursorElementList.length == 1 ? cursorElementList[0] : new RRSequence(cursorElementList.slice())); 31 | } 32 | } else { 33 | rrElementList.push(this.rrElement); 34 | } 35 | let width = 5; 36 | let height = 5; 37 | for (let i = 0; i < rrElementList.length; i++) { 38 | if (i > 0) { 39 | height += 5; 40 | } 41 | const rrElement_ = rrElementList[i]; 42 | rrElement_.computeLayoutInfo(rrDiagramToSVG); 43 | const layoutInfo = rrElement_.getLayoutInfo(); 44 | width = Math.max(width, 5 + layoutInfo.getWidth() + 5); 45 | height += layoutInfo.getHeight() + 5; 46 | } 47 | const svgContent = new SvgContent(); 48 | // First, generate the XML for the elements, to know the usage. 49 | const xOffset = 0; 50 | let yOffset = 5; 51 | for (let rrElement_ of rrElementList) { 52 | const layoutInfo2 = rrElement_.getLayoutInfo(); 53 | const connectorOffset2 = layoutInfo2.getConnectorOffset(); 54 | const width2 = layoutInfo2.getWidth(); 55 | const height2 = layoutInfo2.getHeight(); 56 | const y1 = yOffset + connectorOffset2; 57 | svgContent.addLineConnector(xOffset, y1, xOffset + 5, y1); 58 | // TODO: add decorations (like arrows)? 59 | rrElement_.toSVG(rrDiagramToSVG, xOffset + 5, yOffset, svgContent); 60 | svgContent.addLineConnector(xOffset + 5 + width2, y1, xOffset + 5 + width2 + 5, y1); 61 | yOffset += height2 + 10; 62 | } 63 | const connectorElement = svgContent.getConnectorElement(rrDiagramToSVG); 64 | const elements = svgContent.getElements(); 65 | // Then generate the rest (CSS and SVG container tags) based on that usage. 66 | const sb = []; 67 | sb.push(""); 68 | /* String styles = svgContent.getCSSStyles(); 69 | if(styles.length() > 0) { 70 | sb.push(""); 73 | }*/ 74 | sb.push(connectorElement); 75 | sb.push(elements); 76 | sb.push(""); 77 | return sb.join(""); 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /src/ui/rrdiagramtosvg.js: -------------------------------------------------------------------------------- 1 | import RRText from './rrtext'; 2 | import RRDiagram from './rrdiagram' 3 | 4 | const BoxShape = { 5 | RECTANGLE: 1, 6 | ROUNDED_RECTANGLE: 2, 7 | HEXAGON: 3, 8 | }; 9 | 10 | export default class RRDiagramToSVG { 11 | 12 | static get BoxShape() { 13 | return BoxShape; 14 | } 15 | 16 | constructor() { 17 | this.cssConnectorClass = "rrConnector";//{fill:none;stroke:#222222;} 18 | this.cssRuleClass = "rrRule";//{fill:#d3f0ff;stroke:#222222;} 19 | this.cssRuleTextClass = "rrRuleText";//{fill:#000000;font-family:Verdana,Sans-serif;font-size:12px;} 20 | this.cssLiteralClass = "rrLiteral";//{fill:#90d9ff;stroke:#222222;} 21 | this.cssLiteralTextClass = "rrLiteralText";//{fill:#000000;font-family:Verdana,Sans-serif;font-size:12px;} 22 | this.cssSpecialSequenceClass = "rrSpecialSequence";//{fill:#e4f4ff;stroke:#222222;} 23 | this.cssSpecialSequenceTextClass = "rrSpecialSequenceText";//{fill:#000000;font-family:Verdana,Sans-serif;font-size:12px;} 24 | this.cssLoopCardinalitiesTextClass = "rrLoopCardinalities";//{fill:#000000;font-family:Verdana,Sans-serif;font-size:10px;} 25 | this.ruleShape = BoxShape.RECTANGLE; 26 | this.literalShape = BoxShape.ROUNDED_RECTANGLE; 27 | this.specialSequenceShape = BoxShape.HEXAGON; 28 | } 29 | 30 | /** 31 | * @param {RRDiagram} rrDiagram 32 | * @return {string} 33 | */ 34 | convert(rrDiagram) { 35 | return rrDiagram.toSVG(this); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/ui/rrelement.js: -------------------------------------------------------------------------------- 1 | export default class RRElement { 2 | 3 | constructor() { 4 | this.layoutInfo = null; 5 | } 6 | 7 | setLayoutInfo(layoutInfo) { 8 | this.layoutInfo = layoutInfo; 9 | } 10 | 11 | getLayoutInfo() { 12 | return this.layoutInfo; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/rrline.js: -------------------------------------------------------------------------------- 1 | import RRElement from './rrelement'; 2 | import LayoutInfo from './layoutinfo'; 3 | 4 | export default class RRLine extends RRElement { 5 | 6 | constructor() { 7 | super(); 8 | } 9 | 10 | computeLayoutInfo(rrDiagramToSVG) { 11 | this.setLayoutInfo(new LayoutInfo(0, 10, 5)); 12 | } 13 | 14 | toSVG(rrDiagramToSVG, xOffset, yOffset, svgContent) { 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/ui/rrloop.js: -------------------------------------------------------------------------------- 1 | import RRElement from './rrelement'; 2 | import RRDiagramToSVG from './rrdiagramtosvg'; 3 | import LayoutInfo from './layoutinfo'; 4 | import { escapeXml, getFontInfo } from '../utils/utils'; 5 | 6 | export default class RRLoop extends RRElement { 7 | 8 | /** 9 | * @param {RRElement} rrElement 10 | * @param {RRElement} loopElement 11 | * @param {?number} minRepetitionCount 12 | * @param {?number} maxRepetitionCount 13 | */ 14 | constructor(rrElement, loopElement, minRepetitionCount, maxRepetitionCount) { 15 | super(); 16 | this.rrElement = rrElement; 17 | this.loopElement = loopElement; 18 | if (minRepetitionCount < 0) { 19 | throw new IllegalArgumentException("Minimum repetition must be positive!"); 20 | } 21 | if (maxRepetitionCount != null && maxRepetitionCount < minRepetitionCount) { 22 | throw new IllegalArgumentException("Maximum repetition must not be smaller than minimum!"); 23 | } 24 | this.minRepetitionCount = minRepetitionCount; 25 | this.maxRepetitionCount = maxRepetitionCount; 26 | this.cardinalitiesText = null; 27 | this.cardinalitiesWidth = null; 28 | this.fontYOffset = null; 29 | } 30 | 31 | computeLayoutInfo(rrDiagramToSVG) { 32 | this.cardinalitiesText = null; 33 | this.cardinalitiesWidth = 0; 34 | this.fontYOffset = 0; 35 | if (this.minRepetitionCount > 0 || this.maxRepetitionCount != null) { 36 | this.cardinalitiesText = this.minRepetitionCount + ".." + (this.maxRepetitionCount == null ? "N" : this.maxRepetitionCount); 37 | // TODO: get font from CSS tag. 38 | const fontInfo = getFontInfo(this.cardinalitiesText, rrDiagramToSVG.cssLoopCardinalitiesTextClass); 39 | this.fontYOffset = fontInfo.descent; 40 | this.cardinalitiesWidth = fontInfo.textWidth + 2; 41 | } 42 | this.rrElement.computeLayoutInfo(rrDiagramToSVG); 43 | const layoutInfo1 = this.rrElement.getLayoutInfo(); 44 | let width = layoutInfo1.getWidth(); 45 | let height = layoutInfo1.getHeight(); 46 | let connectorOffset = layoutInfo1.getConnectorOffset(); 47 | if (this.loopElement != null) { 48 | this.loopElement.computeLayoutInfo(rrDiagramToSVG); 49 | const layoutInfo2 = this.loopElement.getLayoutInfo(); 50 | width = Math.max(width, layoutInfo2.getWidth()); 51 | const height2 = layoutInfo2.getHeight(); 52 | height += 5 + height2; 53 | connectorOffset += 5 + height2; 54 | } else { 55 | height += 15; 56 | connectorOffset += 15; 57 | } 58 | width += 20 + 20 + this.cardinalitiesWidth; 59 | this.setLayoutInfo(new LayoutInfo(width, height, connectorOffset)); 60 | } 61 | 62 | toSVG(rrDiagramToSVG, xOffset, yOffset, svgContent) { 63 | const layoutInfo1 = this.rrElement.getLayoutInfo(); 64 | const width1 = layoutInfo1.getWidth(); 65 | let maxWidth = width1; 66 | let yOffset2 = yOffset; 67 | const layoutInfo = this.getLayoutInfo(); 68 | const connectorOffset = layoutInfo.getConnectorOffset(); 69 | let y1 = yOffset; 70 | let loopOffset = 0; 71 | let loopWidth = 0; 72 | if (this.loopElement != null) { 73 | const layoutInfo2 = this.loopElement.getLayoutInfo(); 74 | loopWidth = layoutInfo2.getWidth(); 75 | maxWidth = Math.max(maxWidth, loopWidth); 76 | loopOffset = xOffset + 20 + Math.floor((maxWidth - loopWidth) / 2); 77 | yOffset2 += 5 + layoutInfo2.getHeight(); 78 | y1 += layoutInfo2.getConnectorOffset(); 79 | } else { 80 | yOffset2 += 15; 81 | y1 += 5; 82 | } 83 | const x1 = xOffset + 10; 84 | const x2 = xOffset + 20 + maxWidth + 10 + this.cardinalitiesWidth; 85 | const y2 = yOffset + connectorOffset; 86 | svgContent.addLineConnector(x1 - 10, y2, x1 + 10 + Math.floor((maxWidth - width1) / 2), y2); 87 | let loopPathStartX = x1 + 5; 88 | svgContent.addPathConnector(x1 + 5, y2, "q-5 0-5-5", x1, y2 - 5); 89 | svgContent.addLineConnector(x1, y2 - 5, x1, y1 + 5); 90 | svgContent.addPathConnector(x1, y1 + 5, "q0-5 5-5", x1 + 5, y1); 91 | if (this.loopElement != null) { 92 | svgContent.addLineConnector(x1 + 5, y1, loopOffset, y1); 93 | this.loopElement.toSVG(rrDiagramToSVG, loopOffset, yOffset, svgContent); 94 | loopPathStartX = loopOffset + loopWidth; 95 | } 96 | svgContent.addLineConnector(loopPathStartX, y1, x2 - 5, y1); 97 | svgContent.addPathConnector(x2 - 5, y1, "q5 0 5 5", x2, y1 + 5); 98 | svgContent.addLineConnector(x2, y1 + 5, x2, y2 - 5); 99 | svgContent.addPathConnector(x2, y2 - 5, "q0 5-5 5", x2 - 5, y2); 100 | if (this.cardinalitiesText != null) { 101 | svgContent.addElement("" + escapeXml(this.cardinalitiesText) + ""); 102 | } 103 | this.rrElement.toSVG(rrDiagramToSVG, xOffset + 20 + Math.floor((maxWidth - width1) / 2), yOffset2, svgContent); 104 | svgContent.addLineConnector(x2 - this.cardinalitiesWidth - 10 - Math.floor((maxWidth - width1) / 2), y2, xOffset + layoutInfo.getWidth(), y2); 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /src/ui/rrsequence.js: -------------------------------------------------------------------------------- 1 | import RRElement from './rrelement'; 2 | import LayoutInfo from './layoutinfo'; 3 | 4 | export default class RRSequence extends RRElement { 5 | 6 | 7 | /** 8 | * @param {(RRElement | RRElement[])} rrElements 9 | */ 10 | constructor(rrElements) { 11 | super(); 12 | if(arguments.length == 0) { 13 | rrElements = []; 14 | } else if(rrElements.constructor !== Array) { 15 | rrElements = arguments; 16 | } 17 | this.rrElements = rrElements; 18 | } 19 | 20 | getRRElements() { 21 | return this.rrElements; 22 | } 23 | 24 | computeLayoutInfo(rrDiagramToSVG) { 25 | let width = 0; 26 | let aboveConnector = 0; 27 | let belowConnector = 0; 28 | for (let i = 0; i < this.rrElements.length; i++) { 29 | const rrElement = this.rrElements[i]; 30 | rrElement.computeLayoutInfo(rrDiagramToSVG); 31 | if (i > 0) { 32 | width += 10; 33 | } 34 | const layoutInfo = rrElement.getLayoutInfo(); 35 | width += layoutInfo.getWidth(); 36 | const height = layoutInfo.getHeight(); 37 | const connectorOffset = layoutInfo.getConnectorOffset(); 38 | aboveConnector = Math.max(aboveConnector, connectorOffset); 39 | belowConnector = Math.max(belowConnector, height - connectorOffset); 40 | } 41 | this.setLayoutInfo(new LayoutInfo(width, aboveConnector + belowConnector, aboveConnector)); 42 | } 43 | 44 | toSVG(rrDiagramToSVG, xOffset, yOffset, svgContent) { 45 | const layoutInfo = this.getLayoutInfo(); 46 | const connectorOffset = layoutInfo.getConnectorOffset(); 47 | let widthOffset = 0; 48 | for (let i = 0; i < this.rrElements.length; i++) { 49 | const rrElement = this.rrElements[i]; 50 | const layoutInfo2 = rrElement.getLayoutInfo(); 51 | const width2 = layoutInfo2.getWidth(); 52 | const connectorOffset2 = layoutInfo2.getConnectorOffset(); 53 | const xOffset2 = widthOffset + xOffset; 54 | const yOffset2 = yOffset + connectorOffset - connectorOffset2; 55 | if (i > 0) { 56 | svgContent.addLineConnector(xOffset2 - 10, yOffset + connectorOffset, xOffset2, yOffset + connectorOffset); 57 | } 58 | rrElement.toSVG(rrDiagramToSVG, xOffset2, yOffset2, svgContent); 59 | widthOffset += 10; 60 | widthOffset += width2; 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/ui/rrtext.js: -------------------------------------------------------------------------------- 1 | import RRElement from './rrelement'; 2 | import RRDiagramToSVG from './rrdiagramtosvg'; 3 | import LayoutInfo from './layoutinfo'; 4 | import { escapeXml, getFontInfo } from '../utils/utils'; 5 | 6 | const Type = { 7 | LITERAL: 1, 8 | RULE: 2, 9 | SPECIAL_SEQUENCE: 3, 10 | }; 11 | 12 | export default class RRText extends RRElement { 13 | 14 | static get Type() { 15 | return Type; 16 | } 17 | 18 | /** 19 | * 20 | * @param {Type} type 21 | * @param {string} text 22 | * @param {?string} link 23 | */ 24 | constructor(type, text, link) { 25 | super(); 26 | this.type = type; 27 | this.text = text; 28 | this.link = link; 29 | this.fontInfo = null; 30 | } 31 | 32 | getType() { 33 | return this.type; 34 | } 35 | 36 | getText() { 37 | return this.text; 38 | } 39 | 40 | getLink() { 41 | return this.link; 42 | } 43 | 44 | computeLayoutInfo(rrDiagramToSVG) { 45 | const insets = { 46 | top: 5, 47 | left: 10, 48 | bottom: 5, 49 | right: 10, 50 | }; 51 | let cssTextClass; 52 | if (this.type == Type.RULE) { 53 | cssTextClass = rrDiagramToSVG.cssRuleTextClass; 54 | } else if (this.type == Type.LITERAL) { 55 | cssTextClass = rrDiagramToSVG.cssLiteralTextClass; 56 | } else if (this.type == Type.SPECIAL_SEQUENCE) { 57 | cssTextClass = rrDiagramToSVG.cssSpecialSequenceTextClass; 58 | } else { 59 | throw 'Unknown type: type'; 60 | } 61 | this.fontInfo = getFontInfo(this.text, cssTextClass); 62 | let width = this.fontInfo.textWidth; 63 | let height = this.fontInfo.height; 64 | const fontYOffset = this.fontInfo.descent; 65 | const connectorOffset = insets.top + height - fontYOffset; 66 | width += insets.left + insets.right; 67 | height += insets.top + insets.bottom; 68 | this.setLayoutInfo(new LayoutInfo(width, height, connectorOffset)); 69 | } 70 | 71 | toSVG(rrDiagramToSVG, xOffset, yOffset, svgContent) { 72 | const insets = { 73 | top: 5, 74 | left: 10, 75 | bottom: 5, 76 | right: 10, 77 | }; 78 | const layoutInfo = this.getLayoutInfo(); 79 | const width = layoutInfo.getWidth(); 80 | const height = layoutInfo.getHeight(); 81 | if (this.link != null) { 82 | svgContent.addElement(""); 83 | } 84 | let cssClass; 85 | let cssTextClass; 86 | let shape; 87 | if (this.type == Type.RULE) { 88 | cssClass = rrDiagramToSVG.cssRuleClass; 89 | cssTextClass = rrDiagramToSVG.cssRuleTextClass; 90 | shape = rrDiagramToSVG.ruleShape; 91 | } else if (this.type == Type.LITERAL) { 92 | cssClass = rrDiagramToSVG.cssLiteralClass; 93 | cssTextClass = rrDiagramToSVG.cssLiteralTextClass; 94 | shape = rrDiagramToSVG.literalShape; 95 | } else if (this.type == Type.SPECIAL_SEQUENCE) { 96 | cssClass = rrDiagramToSVG.cssSpecialSequenceClass; 97 | cssTextClass = rrDiagramToSVG.cssSpecialSequenceTextClass; 98 | shape = rrDiagramToSVG.specialSequenceShape; 99 | } else { 100 | throw 'Unknown type: type'; 101 | } 102 | if (shape == RRDiagramToSVG.BoxShape.RECTANGLE) { 103 | svgContent.addElement(""); 104 | } else if (shape == RRDiagramToSVG.BoxShape.ROUNDED_RECTANGLE) { 105 | const rx = Math.floor((insets.left + insets.right + insets.top + insets.bottom) / 4); 106 | svgContent.addElement(""); 107 | } else if (shape == RRDiagramToSVG.BoxShape.HEXAGON) { 108 | // We don't calculate the exact length of the connector: it goes behind the shape. 109 | // We should calculate if we want to support transparent shapes. 110 | const connectorOffset = layoutInfo.getConnectorOffset(); 111 | svgContent.addLineConnector(xOffset, yOffset + connectorOffset, xOffset + insets.left, yOffset + connectorOffset); 112 | svgContent.addElement(""); 113 | svgContent.addLineConnector(xOffset + width, yOffset + connectorOffset, xOffset + width - insets.right, yOffset + connectorOffset); 114 | } 115 | const fontYOffset = this.fontInfo.descent; 116 | const textHeight = this.fontInfo.textHeight; 117 | const textXOffset = xOffset + insets.left; 118 | const textYOffset = yOffset + insets.top + textHeight - fontYOffset; 119 | svgContent.addElement("" + escapeXml(this.text) + ""); 120 | if (this.link != null) { 121 | svgContent.addElement(""); 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/ui/svg/svgcontent.js: -------------------------------------------------------------------------------- 1 | import {escapeXml} from '../../utils/utils'; 2 | import SvgLine from './svgline'; 3 | import SvgPath from './svgpath'; 4 | 5 | export default class SvgContent { 6 | 7 | constructor() { 8 | this.connectorList = []; 9 | this.elements = []; 10 | } 11 | 12 | addPathConnector(x1, y1, path, x2, y2) { 13 | const c = this.connectorList.length == 0 ? null : this.connectorList[this.connectorList.length - 1]; 14 | if (c != null) { 15 | if (c instanceof SvgPath) { 16 | c.addPath(new SvgPath(x1, y1, path, x2, y2)); 17 | } else { 18 | const svgLine = c; 19 | const x1_ = svgLine.getX1(); 20 | const y1_ = svgLine.getY1(); 21 | const x2_ = svgLine.getX2(); 22 | const y2_ = svgLine.getY2(); 23 | if (x1_ == x2_ && x1 == x1_) { 24 | if (y2_ == y1 - 1) { 25 | svgLine.mergeLine(x1_, y1_, x2_, y2_ + 1); 26 | } else if (y1_ == y1 + 1) { 27 | svgLine.mergeLine(x1_, y1_ - 1, x2_, y2_); 28 | } 29 | } else if (y1_ == y2_ && y1 == y1_) { 30 | if (x2_ == x1 - 1) { 31 | svgLine.mergeLine(x1_, y1_, x2_ + 1, y2_); 32 | } else if (x1_ == x1 + 1) { 33 | svgLine.mergeLine(x1_ - 1, y1_, x2_, y2_); 34 | } 35 | } 36 | this.connectorList.push(new SvgPath(x1, y1, path, x2, y2)); 37 | } 38 | } else { 39 | this.connectorList.push(new SvgPath(x1, y1, path, x2, y2)); 40 | } 41 | } 42 | 43 | addLineConnector(x1, y1, x2, y2) { 44 | const x1_ = Math.min(x1, x2); 45 | const y1_ = Math.min(y1, y2); 46 | const x2_ = Math.max(x1, x2); 47 | const y2_ = Math.max(y1, y2); 48 | const c = this.connectorList.length == 0 ? null : this.connectorList[this.connectorList.length - 1]; 49 | if (c == null || !(c instanceof SvgLine) || !c.mergeLine(x1_, y1_, x2_, y2_)) { 50 | this.connectorList.push(new SvgLine(x1_, y1_, x2_, y2_)); 51 | } 52 | } 53 | 54 | getConnectorElement(rrDiagramToSVG) { 55 | if (this.connectorList.length == 0) { 56 | return ""; 57 | } 58 | let path0 = null; 59 | for (let connector of this.connectorList) { 60 | if (path0 == null) { 61 | if (connector instanceof SvgPath) { 62 | path0 = connector; 63 | } else { 64 | const svgLine = connector; 65 | const x1 = svgLine.getX1(); 66 | const y1 = svgLine.getY1(); 67 | path0 = new SvgPath(x1, y1, "M" + x1 + (y1 < 0 ? y1 : " " + y1), x1, y1); 68 | path0.addLine(svgLine); 69 | } 70 | } else { 71 | if(connector instanceof SvgPath) { 72 | path0.addPath(connector); 73 | } else { 74 | path0.addLine(connector); 75 | } 76 | } 77 | } 78 | return ""; 79 | } 80 | 81 | addElement(element) { 82 | this.elements.push(element); 83 | } 84 | 85 | getElements() { 86 | return this.elements; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/ui/svg/svgline.js: -------------------------------------------------------------------------------- 1 | export default class svgline { 2 | 3 | constructor(x1, y1, x2, y2) { 4 | this.x1 = x1; 5 | this.y1 = y1; 6 | this.x2 = x2; 7 | this.y2 = y2; 8 | } 9 | 10 | getX1() { 11 | return this.x1; 12 | } 13 | 14 | getY1() { 15 | return this.y1; 16 | } 17 | 18 | getX2() { 19 | return this.x2; 20 | } 21 | 22 | getY2() { 23 | return this.y2; 24 | } 25 | 26 | mergeLine(x1, y1, x2, y2) { 27 | if (x1 == x2 && this.x1 == this.x2 && x1 == this.x1) { 28 | if (y2 >= this.y1 - 1 && y1 <= this.y2 + 1) { 29 | this.y1 = Math.min(this.y1, y1); 30 | this.y2 = Math.max(this.y2, y2); 31 | return true; 32 | } 33 | } else if (y1 == y2 && this.y1 == this.y2 && y1 == this.y1) { 34 | if (x2 >= this.x1 - 1 && x1 <= this.x2 + 1) { 35 | this.x1 = Math.min(this.x1, x1); 36 | this.x2 = Math.max(this.x2, x2); 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/ui/svg/svgpath.js: -------------------------------------------------------------------------------- 1 | export default class SvgPath { 2 | 3 | constructor(startX, startY, path, endX, endY) { 4 | this.pathSB = []; 5 | this.startX = startX; 6 | this.startY = startY; 7 | this.pathSB.push(path); 8 | this.endX = endX; 9 | this.endY = endY; 10 | } 11 | 12 | addPath(svgPath) { 13 | const x1 = svgPath.startX; 14 | const y1 = svgPath.startY; 15 | const path = svgPath.getPath(); 16 | const x2 = svgPath.endX; 17 | const y2 = svgPath.endY; 18 | if (x1 != this.endX || y1 != this.endY) { 19 | if (x1 == this.endX && y1 == this.endY + 1) { 20 | this.pathSB.push("v", y1 - y2); 21 | } else if (y1 == this.endY && x1 == this.endX + 1) { 22 | this.pathSB.push("h", x1 - x2); 23 | } else { 24 | this.pathSB.push("m", x1 - this.endX); 25 | if (y1 - this.endY >= 0) { 26 | this.pathSB.push(" "); 27 | } 28 | this.pathSB.push(y1 - this.endY); 29 | } 30 | } 31 | this.pathSB.push(path); 32 | this.endX = x2; 33 | this.endY = y2; 34 | } 35 | 36 | addLine(svgLine) { 37 | const x1 = svgLine.getX1(); 38 | const y1 = svgLine.getY1(); 39 | const x2 = svgLine.getX2(); 40 | const y2 = svgLine.getY2(); 41 | if (x1 == x2 && this.endX == x1) { 42 | if (this.endY == y1 || this.endY == y1 - 1) { 43 | this.pathSB.push("v", y2 - this.endY); 44 | this.endY = y2; 45 | return; 46 | } 47 | if (this.endY == y2 || this.endY == y2 + 1) { 48 | this.pathSB.push("v", y1 - this.endY); 49 | this.endY = y1; 50 | return; 51 | } 52 | } else if (y1 == y2 && this.endY == y1) { 53 | if (this.endX == x1 || this.endX == x1 - 1) { 54 | this.pathSB.push("h", x2 - this.endX); 55 | this.endX = x2; 56 | return; 57 | } 58 | if (this.endX == x2 || this.endX == x2 + 1) { 59 | this.pathSB.push("h", x1 - this.endX); 60 | this.endX = x1; 61 | return; 62 | } 63 | } 64 | this.pathSB.push("m", x1 - this.endX); 65 | if (y1 - this.endY >= 0) { 66 | this.pathSB.push(" "); 67 | } 68 | this.pathSB.push(y1 - this.endY); 69 | if (x1 == x2) { 70 | this.pathSB.push("v", y2 - y1); 71 | } else if (y1 == y2) { 72 | this.pathSB.push("h", x2 - x1); 73 | } else { 74 | this.pathSB.push("l", x2 - x1); 75 | if (y2 - y1 >= 0) { 76 | this.pathSB.push(" "); 77 | } 78 | this.pathSB.push(y2 - y1); 79 | } 80 | this.endX = x2; 81 | this.endY = y2; 82 | } 83 | 84 | getPath() { 85 | return this.pathSB.join(""); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} unsafe 3 | * @return {string} 4 | */ 5 | export function escapeXml(unsafe) { 6 | return unsafe.replace(/[<>&'"]/g, function (c) { 7 | switch (c) { 8 | case '<': return '<'; 9 | case '>': return '>'; 10 | case '&': return '&'; 11 | case '\'': return '''; 12 | case '"': return '"'; 13 | } 14 | }); 15 | } 16 | 17 | /** 18 | * @param {string} text 19 | * @param {string} fontCssClass 20 | * @return {{textWidth: number, textHeight: number, descent: number, height: number}} 21 | */ 22 | export function getFontInfo(text, fontCssClass) { 23 | // TODO: add caching of fontInfo per CssClass 24 | // Code inspired from: https://galactic.ink/journal/2011/01/html5-typographic-metrics/ 25 | const container = document.body; 26 | const testDiv = document.createElement("div"); 27 | testDiv.className = fontCssClass; 28 | container.appendChild(testDiv); 29 | const computedStyle = window.getComputedStyle(testDiv, null); 30 | const fontSize = computedStyle.getPropertyValue('font-size'); 31 | const fontFamily = computedStyle.getPropertyValue('font-family'); 32 | container.removeChild(testDiv); 33 | const parent = document.createElement("div"); 34 | parent.style.fontFamily = fontFamily; 35 | parent.style.fontSize = fontSize; 36 | const image = document.createElement("img"); 37 | image.width = 1; 38 | image.height = 1; 39 | //image.src = "./media/1x1.png"; 40 | image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGP6DwABBQECz6AuzQAAAABJRU5ErkJggg=='; 41 | const sampleHeight = 500; 42 | const textNode = document.createTextNode(text); 43 | parent.appendChild(textNode); 44 | parent.appendChild(image); 45 | container.appendChild(parent); 46 | // getting css equivalent of ctx.measureText() 47 | image.style.display = "none"; 48 | parent.style.display = "inline"; 49 | const textHeight = parent.offsetHeight; 50 | const textWidth = parent.offsetWidth; 51 | // making sure super-wide text stays in-bounds 52 | image.style.display = "inline"; 53 | const forceWidth = textWidth + image.offsetWidth; 54 | // capturing the "top" and "bottom" baseline 55 | parent.style.cssText = "margin: " + sampleHeight + "px 0; display: block; width: " + forceWidth + "px; white-space: nowrap; overflow: hidden; position: absolute; top: 0;"; 56 | parent.style.fontFamily = fontFamily; 57 | parent.style.fontSize = fontSize; 58 | const descent = textHeight - image.offsetTop; 59 | const height = parent.offsetHeight; 60 | const fontInfo = { 61 | textWidth, 62 | textHeight, 63 | descent, 64 | height, 65 | }; 66 | container.removeChild(parent); 67 | return fontInfo; 68 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | context: path.join(__dirname, 'src'), 6 | entry: [ 7 | 'babel-polyfill', 8 | './main.js', 9 | ], 10 | devtool: 'source-map', 11 | output: { 12 | path: path.join(__dirname, 'www'), 13 | filename: 'rrdiagram.js', 14 | library: 'rrdiagram', 15 | libraryTarget: 'umd', 16 | umdNamedDefine: true, 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | 'babel-loader', 25 | ], 26 | }, 27 | ], 28 | }, 29 | resolve: { 30 | modules: [ 31 | path.join(__dirname, 'node_modules'), 32 | ], 33 | }, 34 | plugins: [ 35 | new webpack.optimize.UglifyJsPlugin({ 36 | sourceMap: true, 37 | compress: { 38 | warnings: false, 39 | screw_ie8: true, 40 | conditionals: true, 41 | unused: true, 42 | comparisons: true, 43 | sequences: true, 44 | dead_code: true, 45 | evaluate: true, 46 | join_vars: true, 47 | if_return: true 48 | }, 49 | output: { 50 | comments: false 51 | } 52 | }), 53 | ], 54 | }; 55 | -------------------------------------------------------------------------------- /www/index1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RRDiagam-JS Test 1 7 | 17 | 18 | 19 | 20 | Hello World 21 | 22 | x=
"DSL.least(" ( FieldExpression (", " FieldExpression)* | ObjectExpression (", " ObjectExpression)* ) ")" Field?;
23 |
24 | done. 25 | 43 | 76 | 84 | 94 | 123 | 124 | 125 | 126 | --------------------------------------------------------------------------------