├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── regex-railroad-diagram.cson ├── lib ├── railroad-diagram-element.coffee ├── railroad-diagrams.js ├── regex-railroad-diagram.coffee └── regex-to-railroad.coffee ├── package.json ├── regex-railroad-diagrams.png ├── spec ├── fixtures │ ├── coffeescript.coffee │ ├── javascript.js │ ├── perl.pl │ ├── php.php │ ├── python.py │ ├── ruby.rb │ └── test.js ├── regex-railroad-diagram-spec.coffee └── regex-railroad-diagram-view-spec.coffee ├── src └── regex-parser.coffee └── styles └── regex-railroad-diagram.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | scratchpad.rst 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.16 2 | - make texts lighter for dark theme to improve readablility 3 | 4 | ## 0.15 5 | - add inline-flag interpretation "(?i)foo" 6 | 7 | ## 0.14 8 | - add unicode categories as requested by issue #67 9 | - add enable/disable commands for controlling diagram display (partly #61) 10 | 11 | ## 0.13 12 | - fix issue #64, nested capture indexing 13 | 14 | ## 0.12 15 | - capture text in dark themes a little lighter for better reading 16 | - fix enhancement #68, add named capture support 17 | - fix issue #65, and set max-height to half of view height 18 | 19 | ## 0.11 20 | - support julia (thanks to jkroso) 21 | 22 | ## 0.10 23 | - colorize graph 24 | 25 | ## 0.9 26 | - complete redesign to use new style HTMLElement and addBottomPanel 27 | - improve handling of regexes (support perl's substitutions 28 | - more compact display with tooltips for more info on NonTerminals 29 | 30 | ## 0.8.4 31 | - fix issue #52, enable installation for atom behind proxy 32 | 33 | ## 0.8.3 34 | - fix issue #41, add characterclass parsing 35 | 36 | ## 0.8.2 37 | - fix issue #48, thanks to lordjavac for hints on fixing 38 | - fix issue #47, using debounce now (thanks to aki77) 39 | 40 | 41 | ## 0.8.0 42 | - make rrd work with curren atom (0.209.0) 43 | - better extracting of regexes (make use of punctuation scope names) 44 | 45 | ## 0.7.4 46 | - require $$ and View from space-pen-views fixing #36 and #35, #33 and #32 47 | - use getActiveTextEditor fixing #34 48 | 49 | ## 0.7.3 50 | - fix issue #31 (broken installation) 51 | 52 | ## 0.7.2 53 | - fix issue #25 54 | - fix issue #20 55 | 56 | ## 0.7.1 57 | - fix CHANGELOG, because mistakenly made a minor version step instead of patch 58 | 59 | ## 0.7.0 60 | - Merge pull request #29 from imperez/atom-regex-railroad-diagrams to fix issue #27 61 | 62 | ## 0.6.3 63 | - fix issue #16 64 | 65 | ## 0.6.2 66 | - fix issue #13 and #14 67 | 68 | ## 0.6.1 69 | - Merge pull request #12 from maschs/ms-fixMinQuantifier 70 | - Fix Error on single (min) quantifier 71 | 72 | ## 0.6.0 73 | - nothing changed, the previous apm publish minor failed 74 | 75 | ## 0.5.0 76 | - Merge branch 'master' of github.com:klorenz/atom-regex-railroad-diagrams 77 | 78 | ## 0.4.0 79 | - improve visualization of spaces 80 | - fix issue #8, workaround with special case 81 | - Merge pull request #9 from Zod-/master 82 | - fix issue #7, add python support 83 | - fix issue #10, assert min < max quantifier 84 | - Merge pull request #11 from lucas-clemente/patch-1 85 | - Scroll long regexes 86 | - Changed comments on quantifiers with fixed length "0 to 0 times" -> "0 times" "1 to 1 times" -> "once" "x to x times" -> "x times" 87 | 88 | ## 0.3.0 89 | - fix issue #4 90 | 91 | ## 0.2.7-0.2.6 92 | - issues with apm publish 93 | 94 | ## 0.2.5 95 | - add support for charsets and special characters 96 | 97 | ## 0.2.4 98 | - fix issue #1 and #2 99 | - add description to package.json 100 | 101 | ## 0.2.2 102 | - Prepare 0.2.2 release 103 | - fix link to screenshot 104 | 105 | ## 0.2.1 106 | - improve screenshot 107 | 108 | ## 0.2.0 109 | - add little readme 110 | 111 | ## 0.1.0 112 | - fix repository name and version 113 | - add dependencies 114 | - initial 115 | - Initial commit 116 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Kay-Uwe (Kiwi) Lorenz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # regex-railroad-diagram package 2 | 3 | A regular expression railroad diagram view for regular expression 4 | under cursor. 5 | 6 | An (old) Screenshot: 7 | 8 | ![regex-railraod-diagram in action](https://raw.githubusercontent.com/klorenz/atom-regex-railroad-diagrams/master/regex-railroad-diagrams.png) 9 | 10 | It also shows you a parsing error message, if your regex is not syntactically 11 | correct. 12 | 13 | Regexes parsed are not language specific, so some language specific features may 14 | not parsed or displayed correctly. 15 | 16 | ## Usage 17 | 18 | - if the cursor is on some text, which is marked by language as a regex, the 19 | railroad diagram automatically opens. It changes, while you change the text. 20 | 21 | - if you have some text selected or your cursor is somewhere else (where no 22 | regex is recognized), you can hit **ctrl-r ctrl-r** to open the railroad 23 | diagram view. You then can edit the regex and hit **enter** to insert it at your cursor position or replace current selection. Hit **esc** to cancel the view 24 | 25 | ## Contributors 26 | 27 | Many thanks to @mikesprague, who maintains this package, and to other contributers: 28 | 29 | - @hayes 30 | - @imperez 31 | - @ypresto 32 | - @goddamnhippie 33 | - @jkroso 34 | - @lucas-clemente 35 | -------------------------------------------------------------------------------- /keymaps/regex-railroad-diagram.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | '.platform-darwin': 11 | 'cmd-r cmd-r': 'regex-railroad-diagram:show' 12 | 13 | '.platform-linux, .platform-win32': 14 | 'ctrl-r ctrl-r': 'regex-railroad-diagram:show' 15 | -------------------------------------------------------------------------------- /lib/railroad-diagram-element.coffee: -------------------------------------------------------------------------------- 1 | {$} = require 'atom-space-pen-views' 2 | {Regex2RailRoadDiagram} = require './regex-to-railroad.coffee' 3 | {CompositeDisposable, TextEditor} = require 'atom' 4 | 5 | 6 | class RailroadDiagramElement extends HTMLElement 7 | createdCallback: -> 8 | 9 | initialize: (@model) -> 10 | @panel = atom.workspace.addBottomPanel item: this, visible: false 11 | @classList.add "regex-railroad-diagram" 12 | @currentRegex = null 13 | @subscriptions = null 14 | @createView() 15 | this 16 | 17 | createView: -> 18 | @textEditor = new TextEditor 19 | mini: true 20 | tabLength: 2 21 | softTabs: true 22 | softWrapped: false 23 | placeholderText: 'Type in your regex' 24 | 25 | @textEditorSubscriptions = new CompositeDisposable 26 | 27 | @is_visible = false 28 | 29 | changeDelay = null 30 | @textEditorSubscriptions.add @textEditor.onDidChange => 31 | # TODO: if inserted a (, add the ) (and so on.) 32 | 33 | @showRailRoadDiagram @textEditor.getText(), @options 34 | 35 | 36 | # # with a little delay, we do not get flickering if person types fast 37 | # if changeDelay 38 | # clearTimeout(changeDelay) 39 | # changeDelay = null 40 | # 41 | # changeDelay = setTimeout( 42 | # (=> @showRailRoadDiagram @textEditor.getText(), @options), 43 | # 300) 44 | 45 | @regexGrammars = {} 46 | for grammar in atom.grammars.getGrammars() 47 | console.log "grammar", grammar.name 48 | if grammar.name?.match /.*reg.*ex/i 49 | displayName = grammar.name 50 | @textEditor.setGrammar(grammar) 51 | #if m = grammar.name.match /\((.*)\)/ 52 | # displayName = m[1] 53 | @regexGrammars[grammar.name] = grammar 54 | 55 | possibleGrammars = [ 56 | 'Regular Expression Replacement (Javascript)' 57 | 'Regular Expressions (Javascript)' 58 | 'Regular Expressions (Python)' 59 | ] 60 | 61 | for name in possibleGrammars 62 | if name in @regexGrammars 63 | @textEditor.setGrammar(@regexGrammars[name]) 64 | break 65 | 66 | @innerHTML = """ 67 |
68 |
69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 |
77 | """ 78 | 79 | @viewContainer = @querySelector('.regex-railroad-view-container') 80 | @options = null 81 | 82 | @multilineButton = @querySelector('.btn-multiline') 83 | @dotallButton = @querySelector('.btn-dotall') 84 | 85 | btnClick = (btnSelector, opt) => 86 | btn = @querySelector(btnSelector) 87 | if 'selected' in btn.classList 88 | btn.classList.remove 'selected' 89 | @options.options = @options.options.replace opt, '' 90 | else 91 | btn.classList.add 'selected' 92 | @options.options = @options.options + opt 93 | 94 | @showRailRoadDiagram @textEditor.getText(), @options 95 | 96 | @multilineButton.onclick = => 97 | btnClick '.btn-multiline','m' 98 | 99 | @dotallButton.onclick = => 100 | btnClick '.btn-dotall','s' 101 | 102 | @textEditorView = atom.views.getView(@textEditor) 103 | 104 | @querySelector('.texteditor-container').appendChild @textEditorView 105 | 106 | @textEditorSubscriptions.add atom.commands.add @textEditor.element, 107 | 'core:confirm': => @confirm() 108 | 'core:cancel': => @cancel() 109 | 110 | focusTextEditor: -> 111 | @textEditorView.focus() 112 | 113 | confirm: -> 114 | editor = atom.workspace.getActiveTextEditor() 115 | selections = editor.getSelections() 116 | for selection in selections 117 | editor.setTextInBufferRange selection.getBufferRange(), @textEditor.getText() 118 | atom.views.getView(atom.workspace).focus() 119 | 120 | cancel: -> 121 | @assertHidden() 122 | atom.views.getView(atom.workspace).focus() 123 | 124 | isVisible: -> 125 | @is_visible 126 | 127 | setModel: (@model) -> 128 | 129 | removeDiagram: -> 130 | for child in @viewContainer.childNodes 131 | child.remove() 132 | @subscriptions?.dispose() 133 | 134 | destroy: -> 135 | @is_visible = false 136 | @removeDiagram() 137 | @panel.remove() 138 | @remove() 139 | @textEditorSubscriptions?.dispose() 140 | 141 | showDiagram: (regex, options) -> 142 | return if @currentRegex is regex and not @hidden and options.options is @options?.options 143 | @is_visible = true 144 | @activeEditor = atom.workspace.getActiveTextEditor() 145 | @options = options 146 | @textEditor.setText(regex) 147 | @panel.show() 148 | 149 | showRailRoadDiagram: (regex, options) -> 150 | @removeDiagram() 151 | 152 | @subscriptions = new CompositeDisposable 153 | try 154 | Regex2RailRoadDiagram regex, @viewContainer, options 155 | 156 | for e in $(@viewContainer).find('g[title]') 157 | @subscriptions.add atom.tooltips.add e, title: $(e).attr('title') 158 | 159 | @currentRegex = regex 160 | catch e 161 | @showError regex, e 162 | 163 | setTimeout (=> @activeEditor.scrollToCursorPosition()), 200 164 | 165 | showError: (regex, e) -> 166 | #console.log "caught error when trying to display regex #{regex}", e.stack 167 | if e.offset 168 | sp = " ".repeat e.offset 169 | @viewContainer.innerHTML = """
#{regex}\n#{sp}^ #{e.message}
""" 170 | else 171 | @viewContainer.innerHTML = """
#{regex}

#{e.message}

""" 172 | 173 | assertHidden: -> 174 | @panel.hide() unless @hidden 175 | @currentRegex = null 176 | @subscriptions?.dispose() 177 | @is_visible = false 178 | 179 | module.exports = RailroadDiagramElement = document.registerElement 'regex-railroad-diagram', prototype: RailroadDiagramElement.prototype 180 | -------------------------------------------------------------------------------- /lib/railroad-diagrams.js: -------------------------------------------------------------------------------- 1 | /* 2 | Railroad Diagrams 3 | by Tab Atkins Jr. (and others) 4 | http://xanthir.com 5 | http://twitter.com/tabatkins 6 | http://github.com/tabatkins/railroad-diagrams 7 | 8 | This document and all associated files in the github project are licensed under CC0: http://creativecommons.org/publicdomain/zero/1.0/ 9 | This means you can reuse, remix, or otherwise appropriate this project for your own use WITHOUT RESTRICTION. 10 | (The actual legal meaning can be found at the above link.) 11 | Don't ask me for permission to use any part of this project, JUST USE IT. 12 | I would appreciate attribution, but that is not required by the license. 13 | */ 14 | 15 | /* 16 | This file uses a module pattern to avoid leaking names into the global scope. 17 | The only accidental leakage is the name "temp". 18 | The exported names can be found at the bottom of this file; 19 | simply change the names in the array of strings to change what they are called in your application. 20 | 21 | As well, several configuration constants are passed into the module function at the bottom of this file. 22 | At runtime, these constants can be found on the Diagram class. 23 | */ 24 | 25 | var options = { 26 | VERTICAL_SEPARATION: 8, 27 | ARC_RADIUS: 10, 28 | DIAGRAM_CLASS: 'railroad-diagram', 29 | STROKE_ODD_PIXEL_LENGTH: true, 30 | INTERNAL_ALIGNMENT: 'center', 31 | } 32 | 33 | function textWidth(text) { 34 | var m; 35 | var narrow = (m = text.match(/[jiIl]/g)) ? m.length : 0; 36 | var small = (m = text.match(/(?![jil])[a-z]/g)) ? m.length : 0; 37 | var big = (m = text.match(/(?![MW])[A-Z]/g)) ? m.length : 0; 38 | var large = text.length - narrow - small - big; 39 | 40 | return narrow * 4 + small * 6 + big * 8 + large * 9; 41 | } 42 | 43 | function subclassOf(baseClass, superClass) { 44 | baseClass.prototype = Object.create(superClass.prototype); 45 | baseClass.prototype.$super = superClass.prototype; 46 | } 47 | 48 | function unnull(/* children */) { 49 | return [].slice.call(arguments).reduce(function(sofar, x) { return sofar !== undefined ? sofar : x; }); 50 | } 51 | 52 | function determineGaps(outer, inner) { 53 | var diff = outer - inner; 54 | switch(Diagram.INTERNAL_ALIGNMENT) { 55 | case 'left': return [0, diff]; break; 56 | case 'right': return [diff, 0]; break; 57 | case 'center': 58 | default: return [diff/2, diff/2]; break; 59 | } 60 | } 61 | 62 | function wrapString(value, attrs) { 63 | return ((typeof value) == 'string') ? new Terminal(value, attrs) : value; 64 | } 65 | 66 | 67 | function SVG(name, attrs, text) { 68 | attrs = attrs || {}; 69 | text = text || ''; 70 | var el = document.createElementNS("http://www.w3.org/2000/svg",name); 71 | for(var attr in attrs) { 72 | el.setAttribute(attr, attrs[attr]); 73 | } 74 | el.textContent = text; 75 | return el; 76 | } 77 | 78 | function FakeSVG(tagName, attrs, text){ 79 | if(!(this instanceof FakeSVG)) return new FakeSVG(tagName, attrs, text); 80 | if(text) this.children = text; 81 | else this.children = []; 82 | this.tagName = tagName; 83 | this.attrs = unnull(attrs, {}); 84 | 85 | return this; 86 | }; 87 | FakeSVG.prototype.format = function(x, y, width) { 88 | // Virtual 89 | }; 90 | FakeSVG.prototype.addTo = function(parent) { 91 | if(parent instanceof FakeSVG) { 92 | parent.children.push(this); 93 | return this; 94 | } else { 95 | var svg = this.toSVG(); 96 | parent.appendChild(svg); 97 | return svg; 98 | } 99 | }; 100 | FakeSVG.prototype.toSVG = function() { 101 | var el = SVG(this.tagName, this.attrs); 102 | if(typeof this.children == 'string') { 103 | el.textContent = this.children; 104 | } else { 105 | this.children.forEach(function(e) { 106 | el.appendChild(e.toSVG()); 107 | }); 108 | } 109 | return el; 110 | }; 111 | FakeSVG.prototype.toString = function() { 112 | var str = '<' + this.tagName; 113 | var group = this.tagName == "g" || this.tagName == "svg"; 114 | for(var attr in this.attrs) { 115 | str += ' ' + attr + '="' + (this.attrs[attr]+'').replace(/&/g, '&').replace(/"/g, '"') + '"'; 116 | } 117 | str += '>'; 118 | if(group) str += "\n"; 119 | if(typeof this.children == 'string') { 120 | str += this.children.replace(/&/g, '&').replace(/\n'; 127 | return str; 128 | } 129 | 130 | function Path(x,y) { 131 | if(!(this instanceof Path)) return new Path(x,y); 132 | FakeSVG.call(this, 'path'); 133 | this.attrs.d = "M"+x+' '+y; 134 | } 135 | subclassOf(Path, FakeSVG); 136 | Path.prototype.m = function(x,y) { 137 | this.attrs.d += 'm'+x+' '+y; 138 | return this; 139 | } 140 | Path.prototype.h = function(val) { 141 | this.attrs.d += 'h'+val; 142 | return this; 143 | } 144 | Path.prototype.right = Path.prototype.h; 145 | Path.prototype.left = function(val) { return this.h(-val); } 146 | Path.prototype.v = function(val) { 147 | this.attrs.d += 'v'+val; 148 | return this; 149 | } 150 | Path.prototype.down = Path.prototype.v; 151 | Path.prototype.up = function(val) { return this.v(-val); } 152 | Path.prototype.arc = function(sweep){ 153 | var x = Diagram.ARC_RADIUS; 154 | var y = Diagram.ARC_RADIUS; 155 | if(sweep[0] == 'e' || sweep[1] == 'w') { 156 | x *= -1; 157 | } 158 | if(sweep[0] == 's' || sweep[1] == 'n') { 159 | y *= -1; 160 | } 161 | if(sweep == 'ne' || sweep == 'es' || sweep == 'sw' || sweep == 'wn') { 162 | var cw = 1; 163 | } else { 164 | var cw = 0; 165 | } 166 | this.attrs.d += "a"+Diagram.ARC_RADIUS+" "+Diagram.ARC_RADIUS+" 0 0 "+cw+' '+x+' '+y; 167 | return this; 168 | } 169 | Path.prototype.format = function() { 170 | // All paths in this library start/end horizontally. 171 | // The extra .5 ensures a minor overlap, so there's no seams in bad rasterizers. 172 | this.attrs.d += 'h.5'; 173 | return this; 174 | } 175 | 176 | function Diagram(items) { 177 | if(!(this instanceof Diagram)) return new Diagram([].slice.call(arguments)); 178 | FakeSVG.call(this, 'svg', {class: Diagram.DIAGRAM_CLASS}); 179 | this.items = items.map(wrapString); 180 | this.items.unshift(new Start); 181 | this.items.push(new End); 182 | this.width = this.items.reduce(function(sofar, el) { return sofar + el.width + (el.needsSpace?20:0)}, 0)+1; 183 | this.up = Math.max.apply(null, this.items.map(function (x) { return x.up; })); 184 | this.down = Math.max.apply(null, this.items.map(function (x) { return x.down; })); 185 | this.formatted = false; 186 | } 187 | subclassOf(Diagram, FakeSVG); 188 | for(var option in options) { 189 | Diagram[option] = options[option]; 190 | } 191 | Diagram.prototype.format = function(paddingt, paddingr, paddingb, paddingl) { 192 | paddingt = unnull(paddingt, 20); 193 | paddingr = unnull(paddingr, paddingt, 20); 194 | paddingb = unnull(paddingb, paddingt, 20); 195 | paddingl = unnull(paddingl, paddingr, 20); 196 | var x = paddingl; 197 | var y = paddingt; 198 | y += this.up; 199 | var g = FakeSVG('g', Diagram.STROKE_ODD_PIXEL_LENGTH ? {transform:'translate(.5 .5)'} : {}); 200 | for(var i = 0; i < this.items.length; i++) { 201 | var item = this.items[i]; 202 | if(item.needsSpace) { 203 | Path(x,y).h(10).addTo(g); 204 | x += 10; 205 | } 206 | item.format(x, y, item.width).addTo(g); 207 | x += item.width; 208 | if(item.needsSpace) { 209 | Path(x,y).h(10).addTo(g); 210 | x += 10; 211 | } 212 | } 213 | this.attrs.width = this.width + paddingl + paddingr; 214 | this.attrs.height = this.up + this.down + paddingt + paddingb; 215 | this.attrs.viewBox = "0 0 " + this.attrs.width + " " + this.attrs.height; 216 | g.addTo(this); 217 | this.formatted = true; 218 | return this; 219 | } 220 | Diagram.prototype.addTo = function(parent) { 221 | var scriptTag = document.getElementsByTagName('script'); 222 | scriptTag = scriptTag[scriptTag.length - 1]; 223 | var parentTag = scriptTag.parentNode; 224 | parent = parent || parentTag; 225 | return this.$super.addTo.call(this, parent); 226 | } 227 | Diagram.prototype.toSVG = function() { 228 | if (!this.formatted) { 229 | this.format(); 230 | } 231 | return this.$super.toSVG.call(this); 232 | } 233 | Diagram.prototype.toString = function() { 234 | if (!this.formatted) { 235 | this.format(); 236 | } 237 | return this.$super.toString.call(this); 238 | } 239 | 240 | function Sequence(items) { 241 | if(!(this instanceof Sequence)) return new Sequence([].slice.call(arguments)); 242 | FakeSVG.call(this, 'g'); 243 | this.items = items.map(wrapString); 244 | this.width = this.items.reduce(function(sofar, el) { return sofar + el.width + (el.needsSpace?20:0)}, 0); 245 | this.up = this.items.reduce(function(sofar,el) { return Math.max(sofar, el.up)}, 0); 246 | this.down = this.items.reduce(function(sofar,el) { return Math.max(sofar, el.down)}, 0); 247 | } 248 | subclassOf(Sequence, FakeSVG); 249 | Sequence.prototype.format = function(x,y,width) { 250 | // Hook up the two sides if this is narrower than its stated width. 251 | var gaps = determineGaps(width, this.width); 252 | Path(x,y).h(gaps[0]).addTo(this); 253 | Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); 254 | x += gaps[0]; 255 | 256 | for(var i = 0; i < this.items.length; i++) { 257 | var item = this.items[i]; 258 | if(item.needsSpace) { 259 | Path(x,y).h(10).addTo(this); 260 | x += 10; 261 | } 262 | item.format(x, y, item.width).addTo(this); 263 | x += item.width; 264 | if(item.needsSpace) { 265 | Path(x,y).h(10).addTo(this); 266 | x += 10; 267 | } 268 | } 269 | return this; 270 | } 271 | 272 | function Choice(normal, items) { 273 | if(!(this instanceof Choice)) return new Choice(normal, [].slice.call(arguments,1)); 274 | FakeSVG.call(this, 'g'); 275 | if( typeof normal !== "number" || normal !== Math.floor(normal) ) { 276 | throw new TypeError("The first argument of Choice() must be an integer."); 277 | } else if(normal < 0 || normal >= items.length) { 278 | throw new RangeError("The first argument of Choice() must be an index for one of the items."); 279 | } else { 280 | this.normal = normal; 281 | } 282 | this.items = items.map(wrapString); 283 | this.width = this.items.reduce(function(sofar, el){return Math.max(sofar, el.width)},0) + Diagram.ARC_RADIUS*4; 284 | this.up = this.down = 0; 285 | for(var i = 0; i < this.items.length; i++) { 286 | var item = this.items[i]; 287 | if(i < normal) { this.up += Math.max(Diagram.ARC_RADIUS,item.up + item.down + Diagram.VERTICAL_SEPARATION); } 288 | if(i == normal) { this.up += Math.max(Diagram.ARC_RADIUS, item.up); this.down += Math.max(Diagram.ARC_RADIUS, item.down); } 289 | if(i > normal) { this.down += Math.max(Diagram.ARC_RADIUS,Diagram.VERTICAL_SEPARATION + item.up + item.down); } 290 | } 291 | } 292 | subclassOf(Choice, FakeSVG); 293 | Choice.prototype.format = function(x,y,width) { 294 | // Hook up the two sides if this is narrower than its stated width. 295 | var gaps = determineGaps(width, this.width); 296 | Path(x,y).h(gaps[0]).addTo(this); 297 | Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); 298 | x += gaps[0]; 299 | 300 | var last = this.items.length -1; 301 | var innerWidth = this.width - Diagram.ARC_RADIUS*4; 302 | 303 | // Do the elements that curve above 304 | for(var i = this.normal - 1; i >= 0; i--) { 305 | var item = this.items[i]; 306 | if( i == this.normal - 1 ) { 307 | var distanceFromY = Math.max(Diagram.ARC_RADIUS*2, this.items[i+1].up + Diagram.VERTICAL_SEPARATION + item.down); 308 | } 309 | Path(x,y).arc('se').up(distanceFromY - Diagram.ARC_RADIUS*2).arc('wn').addTo(this); 310 | item.format(x+Diagram.ARC_RADIUS*2,y - distanceFromY,innerWidth).addTo(this); 311 | Path(x+Diagram.ARC_RADIUS*2+innerWidth, y-distanceFromY).arc('ne').down(distanceFromY - Diagram.ARC_RADIUS*2).arc('ws').addTo(this); 312 | distanceFromY += Math.max(Diagram.ARC_RADIUS, item.up + Diagram.VERTICAL_SEPARATION + (i == 0 ? 0 : this.items[i-1].down)); 313 | } 314 | 315 | // Do the straight-line path. 316 | Path(x,y).right(Diagram.ARC_RADIUS*2).addTo(this); 317 | this.items[this.normal].format(x+Diagram.ARC_RADIUS*2, y, innerWidth).addTo(this); 318 | Path(x+Diagram.ARC_RADIUS*2+innerWidth, y).right(Diagram.ARC_RADIUS*2).addTo(this); 319 | 320 | // Do the elements that curve below 321 | for(var i = this.normal+1; i <= last; i++) { 322 | var item = this.items[i]; 323 | if( i == this.normal + 1 ) { 324 | var distanceFromY = Math.max(Diagram.ARC_RADIUS*2, this.items[i-1].down + Diagram.VERTICAL_SEPARATION + item.up); 325 | } 326 | Path(x,y).arc('ne').down(distanceFromY - Diagram.ARC_RADIUS*2).arc('ws').addTo(this); 327 | item.format(x+Diagram.ARC_RADIUS*2, y+distanceFromY, innerWidth).addTo(this); 328 | Path(x+Diagram.ARC_RADIUS*2+innerWidth, y+distanceFromY).arc('se').up(distanceFromY - Diagram.ARC_RADIUS*2).arc('wn').addTo(this); 329 | distanceFromY += Math.max(Diagram.ARC_RADIUS, item.down + Diagram.VERTICAL_SEPARATION + (i == last ? 0 : this.items[i+1].up)); 330 | } 331 | 332 | return this; 333 | } 334 | 335 | function Optional(item, skip) { 336 | if( skip === undefined ) 337 | return Choice(1, Skip(), item); 338 | else if ( skip === "skip" ) 339 | return Choice(0, Skip(), item); 340 | else 341 | throw "Unknown value for Optional()'s 'skip' argument."; 342 | } 343 | 344 | function Group(item, caption, options) { 345 | if(!(this instanceof Group)) return new Group(item, caption, options); 346 | options = options || {}; 347 | 348 | FakeSVG.call(this, 'g', options.attrs || {}); 349 | caption = caption || (new Skip); 350 | this.item = wrapString(item); 351 | this.caption = caption; 352 | 353 | this.padding = 10; 354 | 355 | this.width = this.item.width + 2*this.padding; 356 | 357 | if (options.minWidth) { 358 | if (this.width < options.minWidth) 359 | this.width = options.minWidth; 360 | } 361 | 362 | var height = this.item.up + this.item.down; 363 | if (caption) 364 | { 365 | height += Diagram.VERTICAL_SEPARATION + this.caption.up + this.caption.down; 366 | } 367 | 368 | this.up = this.item.up + this.padding; 369 | this.down = height-this.up + this.padding; 370 | } 371 | subclassOf(Group, FakeSVG); 372 | Group.prototype.needsSpace = true; 373 | Group.prototype.format = function(x, y, width) { 374 | // Hook up the two sides if this is narrower than its stated width. 375 | var gaps = determineGaps(width, this.width); 376 | Path(x,y).h(gaps[0]).addTo(this); 377 | Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); 378 | x += gaps[0]; 379 | 380 | FakeSVG('rect', { 381 | x:x, y:y-this.up, 382 | width:this.width, height:this.up+this.down, 383 | rx: 5, ry: 5 384 | }).addTo(this); 385 | 386 | this.item.format(x, y, this.width).addTo(this); 387 | 388 | if (this.caption) { 389 | var caption_y = y+this.item.down+Diagram.VERTICAL_SEPARATION+this.caption.up; 390 | var caption_x = x + (this.width - this.caption.width)/2; 391 | this.caption.format(caption_x, caption_y, this.caption.width).addTo(this); 392 | } 393 | 394 | // FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text).addTo(this); 395 | return this; 396 | } 397 | 398 | function OneOrMore(item, rep) { 399 | if(!(this instanceof OneOrMore)) return new OneOrMore(item, rep); 400 | FakeSVG.call(this, 'g'); 401 | rep = rep || (new Skip); 402 | this.item = wrapString(item); 403 | this.rep = wrapString(rep); 404 | this.width = Math.max(this.item.width, this.rep.width) + Diagram.ARC_RADIUS*2; 405 | this.up = this.item.up; 406 | this.down = Math.max(Diagram.ARC_RADIUS*2, this.item.down + Diagram.VERTICAL_SEPARATION + this.rep.up + this.rep.down); 407 | } 408 | subclassOf(OneOrMore, FakeSVG); 409 | OneOrMore.prototype.needsSpace = true; 410 | OneOrMore.prototype.format = function(x,y,width) { 411 | // Hook up the two sides if this is narrower than its stated width. 412 | var gaps = determineGaps(width, this.width); 413 | Path(x,y).h(gaps[0]).addTo(this); 414 | Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); 415 | x += gaps[0]; 416 | 417 | // Draw item 418 | Path(x,y).right(Diagram.ARC_RADIUS).addTo(this); 419 | this.item.format(x+Diagram.ARC_RADIUS,y,this.width-Diagram.ARC_RADIUS*2).addTo(this); 420 | Path(x+this.width-Diagram.ARC_RADIUS,y).right(Diagram.ARC_RADIUS).addTo(this); 421 | 422 | // Draw repeat arc 423 | var distanceFromY = Math.max(Diagram.ARC_RADIUS*2, this.item.down+Diagram.VERTICAL_SEPARATION+this.rep.up); 424 | Path(x+Diagram.ARC_RADIUS,y).arc('nw').down(distanceFromY-Diagram.ARC_RADIUS*2).arc('ws').addTo(this); 425 | this.rep.format(x+Diagram.ARC_RADIUS, y+distanceFromY, this.width - Diagram.ARC_RADIUS*2).addTo(this); 426 | Path(x+this.width-Diagram.ARC_RADIUS, y+distanceFromY).arc('se').up(distanceFromY-Diagram.ARC_RADIUS*2).arc('en').addTo(this); 427 | 428 | return this; 429 | } 430 | 431 | function ZeroOrMore(item, rep, skip) { 432 | return Optional(OneOrMore(item, rep), skip); 433 | } 434 | 435 | function Start() { 436 | if(!(this instanceof Start)) return new Start(); 437 | FakeSVG.call(this, 'path'); 438 | this.width = 20; 439 | this.up = 10; 440 | this.down = 10; 441 | } 442 | subclassOf(Start, FakeSVG); 443 | Start.prototype.format = function(x,y) { 444 | this.attrs.d = 'M '+x+' '+(y-10)+' v 20 m 10 -20 v 20 m -10 -10 h 20.5'; 445 | return this; 446 | } 447 | 448 | function End() { 449 | if(!(this instanceof End)) return new End(); 450 | FakeSVG.call(this, 'path'); 451 | this.width = 20; 452 | this.up = 10; 453 | this.down = 10; 454 | } 455 | subclassOf(End, FakeSVG); 456 | End.prototype.format = function(x,y) { 457 | this.attrs.d = 'M '+x+' '+y+' h 20 m -10 -10 v 20 m 10 -20 v 20'; 458 | return this; 459 | } 460 | 461 | function Terminal(text, attrs) { 462 | if(!(this instanceof Terminal)) return new Terminal(text, attrs); 463 | FakeSVG.call(this, 'g', attrs); 464 | this.text = text; 465 | this.width = textWidth(text) + 20; /* Assume that each char is .5em, and that the em is 16px */ 466 | this.up = 11; 467 | this.down = 11; 468 | } 469 | subclassOf(Terminal, FakeSVG); 470 | Terminal.prototype.needsSpace = true; 471 | Terminal.prototype.format = function(x, y, width) { 472 | // Hook up the two sides if this is narrower than its stated width. 473 | var gaps = determineGaps(width, this.width); 474 | Path(x,y).h(gaps[0]).addTo(this); 475 | 476 | Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); 477 | x += gaps[0]; 478 | 479 | FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down, rx:10, ry:10}).addTo(this); 480 | FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text).addTo(this); 481 | return this; 482 | } 483 | 484 | function NonTerminal(text, attrs) { 485 | if(!(this instanceof NonTerminal)) return new NonTerminal(text, attrs); 486 | FakeSVG.call(this, 'g', attrs); 487 | this.text = text; 488 | this.width = textWidth(text) + 12; 489 | this.up = 11; 490 | this.down = 11; 491 | } 492 | subclassOf(NonTerminal, FakeSVG); 493 | NonTerminal.prototype.needsSpace = true; 494 | NonTerminal.prototype.format = function(x, y, width) { 495 | // Hook up the two sides if this is narrower than its stated width. 496 | var gaps = determineGaps(width, this.width); 497 | Path(x,y).h(gaps[0]).addTo(this); 498 | Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); 499 | x += gaps[0]; 500 | 501 | FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down}).addTo(this); 502 | attrs = {x:x+this.width/2, y:y+4} 503 | // if (this.title) { 504 | // debugger 505 | // attrs['title'] = this.title; 506 | // } 507 | 508 | FakeSVG('text', attrs, this.text).addTo(this); 509 | return this; 510 | } 511 | 512 | function Comment(text, attrs) { 513 | if(!(this instanceof Comment)) return new Comment(text, attrs); 514 | FakeSVG.call(this, 'g', attrs); 515 | this.text = text; 516 | this.paddingH = 2 517 | this.paddingV = 5 518 | this.width = textWidth(text) + this.paddingH*2; 519 | //this.attrs = attrs || {}; 520 | this.up = 11; 521 | this.down = 11; 522 | } 523 | subclassOf(Comment, FakeSVG); 524 | Comment.prototype.needsSpace = true; 525 | Comment.prototype.format = function(x, y, width) { 526 | // Hook up the two sides if this is narrower than its stated width. 527 | var gaps = determineGaps(width, this.width); 528 | Path(x,y).h(gaps[0]).addTo(this); 529 | Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); 530 | x += gaps[0]; 531 | FakeSVG('text', {x: x+this.width/2, y: y+this.paddingV}, this.text).addTo(this); 532 | return this; 533 | } 534 | 535 | function Skip() { 536 | if(!(this instanceof Skip)) return new Skip(); 537 | FakeSVG.call(this, 'g'); 538 | this.width = 0; 539 | this.up = 0; 540 | this.down = 0; 541 | } 542 | subclassOf(Skip, FakeSVG); 543 | Skip.prototype.format = function(x, y, width) { 544 | Path(x,y).right(width).addTo(this); 545 | return this; 546 | } 547 | 548 | module.exports = { 549 | "Diagram": Diagram, 550 | "Sequence": Sequence, 551 | "Choice": Choice, 552 | "Optional": Optional, 553 | "OneOrMore": OneOrMore, 554 | "ZeroOrMore": ZeroOrMore, 555 | "Terminal": Terminal, 556 | "NonTerminal": NonTerminal, 557 | "Comment": Comment, 558 | "Group": Group, 559 | "Skip": Skip 560 | }; 561 | 562 | /* 563 | These are the names that the internal classes are exported as. 564 | If you would like different names, adjust them here. 565 | */ 566 | //['Diagram', 'Sequence', 'Choice', 'Optional', 'OneOrMore', 'ZeroOrMore', 'Terminal', 'NonTerminal', 'Comment', 'Skip'] 567 | // .forEach(function(e,i) { window[e] = temp[i]; }); 568 | -------------------------------------------------------------------------------- /lib/regex-railroad-diagram.coffee: -------------------------------------------------------------------------------- 1 | #RegexRailroadDiagramView = require './regex-railroad-diagram-view' 2 | {CompositeDisposable, Emitter, Range} = require 'atom' 3 | {debounce} = require "underscore-plus" 4 | RailroadDiagramElement = require "./railroad-diagram-element.coffee" 5 | 6 | MATCH_PAIRS = '(': ')', '[': ']', '{': '}', '<': '>' 7 | 8 | # I do not know, when this has been fixed, but with current 1.18.x it is gone 9 | issue58 = require('semver').lt(atom.appVersion, "1.18.0") 10 | 11 | log_debug = console.log.bind console, "rx2rr" 12 | log_debug = -> 13 | 14 | module.exports = 15 | regexRailroadDiagramView: null 16 | 17 | config: 18 | enabled: 19 | type: "boolean" 20 | default: true 21 | 22 | activate: (state) -> 23 | @subscriptions = new CompositeDisposable 24 | @emitter = new Emitter 25 | 26 | @element = (new RailroadDiagramElement).initialize this 27 | 28 | @subscriptions.add atom.workspace.observeTextEditors (editor) => 29 | @subscriptions.add editor.onDidChangeCursorPosition debounce (=> @checkForRegExp()), 100 30 | 31 | @subscriptions.add atom.commands.add 'atom-text-editor', 32 | 'regex-railroad-diagram:show': => 33 | if not @element.isVisible() 34 | console.log "is not visible" 35 | flavour = 'perl' 36 | options = '' 37 | editor = atom.workspace.getActiveTextEditor() 38 | @element.showDiagram editor.getSelectedText(), {flavour, options} 39 | #editor.scrollToCursorPosition() 40 | 41 | @element.focusTextEditor() 42 | else 43 | console.log "is visible" 44 | 45 | # else TODO 46 | # Problem: on confirm the regex markers are also overridden 47 | # editor = atom.workspace.getActiveTextEditor() 48 | # # select current regex first 49 | # [range, flavour] = @getRegexpBufferRange editor 50 | # editor.setSelectedBufferRange range 51 | # @element.focusTextEditor() 52 | 53 | 54 | 55 | if atom.config.get('regex-railroad-diagram.enabled') 56 | @addDisableCommand() 57 | else 58 | @addEnableCommand() 59 | 60 | 61 | addDisableCommand: -> 62 | @cur_cmd = atom.commands.add "atom-workspace", "regex-railroad-diagram:disable", => 63 | @cur_cmd.dispose() 64 | atom.config.set('regex-railroad-diagram.enabled', false) 65 | @addEnableCommand() 66 | @checkForRegExp() 67 | 68 | addEnableCommand: -> 69 | @cur_cmd = atom.commands.add "atom-workspace", "regex-railroad-diagram:enable", => 70 | @cur_cmd.dispose() 71 | atom.config.set('regex-railroad-diagram.enabled', true) 72 | @addDisableCommand() 73 | @checkForRegExp() 74 | 75 | deactivate: -> 76 | #@regexRailroadDiagramView.destroy() 77 | @subscriptions.dispose() 78 | 79 | serialize: -> 80 | #regexRailroadDiagramViewState: @regexRailroadDiagramView.serialize() 81 | 82 | bufferRangeForScope: (editor, scope, position=null) -> 83 | unless issue58 84 | if position? 85 | result = editor.bufferRangeForScopeAtPosition(scope, position) 86 | else 87 | result = editor.bufferRangeForScopeAtCursor(scope) 88 | return result 89 | 90 | # here follows a workaround for fixing #58, till bufferRangeForScopeAtCursor 91 | # delivers correct address 92 | # 93 | 94 | tabLength = editor.getTabLength() 95 | 96 | unless position? 97 | position = editor.getCursorBufferPosition().copy() 98 | 99 | lineStart = [[position.row, 0], [position.row, position.column]] 100 | 101 | if m = editor.getTextInBufferRange(lineStart).match(/\t/g) 102 | startTabs = m.length 103 | else 104 | startTabs = 0 105 | 106 | # shift the position a little, such that als in case of tabs in beginning 107 | # start of regex is recognized as regex 108 | if startTabs 109 | position.column = position.column - startTabs + startTabs*tabLength 110 | 111 | result = editor.bufferRangeForScopeAtPosition(scope, position) 112 | return result unless result 113 | 114 | # this is usually only one row, but if at some point the range would span 115 | # multiple rows, this still works 116 | 117 | {start, end} = result 118 | 119 | lineStart = [[end.row, 0], [end.row, end.column]] 120 | if m = editor.getTextInBufferRange(lineStart).match(/\t/g) 121 | endTabs = m.length 122 | else 123 | endTabs = 0 124 | 125 | return new Range( 126 | [start.row, start.column - startTabs*tabLength + startTabs], 127 | [end.row, end.column - endTabs*tabLength + endTabs] 128 | ) 129 | 130 | getRegexpBufferRange: (editor) -> 131 | position = editor.getCursorBufferPosition() 132 | flavour = editor.scopeDescriptorForBufferPosition(position).scopes[0] 133 | range = @bufferRangeForScope(editor, '.raw-regex') 134 | 135 | unless range 136 | range = @bufferRangeForScope(editor, '.unicode-raw-regex') 137 | 138 | unless range 139 | range = @bufferRangeForScope(editor, '.regexp') 140 | 141 | unless range 142 | return [null, null] 143 | 144 | return [range, flavour] 145 | 146 | cleanRegex: (regex, flavour) -> 147 | opts = "" 148 | 149 | log_debug "cleanRegex", regex, flavour 150 | 151 | #console.log "regex", regex, "flavour", flavour 152 | 153 | if m = (flavour.match(/php/) and regex.match(/^(["'])\/(.*)\/(\w*)\1$/)) 154 | [regex, opts] = m[2..] 155 | else if m = (flavour.match(/python|julia/) and regex.match(/^u?r('''|"""|"|')(.*)\1$/)) 156 | regex = m[2] 157 | else if m = (flavour.match(/coffee/) and regex.match(/^\/\/\/(.*)\/\/\/(\w*)/)) 158 | [regex, opts] = m[1..] 159 | else if m = (flavour.match(/ruby/) and regex.match(/^%r(.)(.*)(\W)(\w*)$/)) 160 | [open, text, close, opts] = m[1..] 161 | expectedClose = MATCH_PAIRS[open] or open 162 | if close != expectedClose 163 | text = text + close + m[4] 164 | close = expectedClose 165 | regexForEscaped = new RegExp("\\\\(#{open}|#{close})", 'g') 166 | regex = text.replace(new RegExp("\\/", '\\/').replace(regexForEscaped, '$1')) 167 | else if m = (flavour.match(/perl/) and ( 168 | regex.match(/^(?:m|qr)(.)(.*)(\1|\W)(\w*)$/) or 169 | regex.match(/^s(.)(.*)(\1|\W)(?:\1.*\W|.*\1)(\w*)$/) 170 | )) 171 | [open, text, close, opts] = m[1..] 172 | expectedClose = MATCH_PAIRS[open] or open 173 | if close != expectedClose 174 | text = text + close + m[4] 175 | close = expectedClose 176 | regexForEscaped = new RegExp("\\\\(#{open}|#{close})", 'g') 177 | regex = text.replace(/\//, '\\/').replace(regexForEscaped, '$1') 178 | else if m = regex.match(/^\/(.*)\/(\w*)$/) 179 | [regex, opts] = m[1..] 180 | 181 | #console.log "regex", regex, "flavour", flavour, "opts", opts 182 | 183 | log_debug "cleanRegex done:", regex, opts 184 | return [regex, opts] 185 | 186 | checkForRegExp: -> 187 | if not atom.config.get('regex-railroad-diagram.enabled') 188 | return @element.assertHidden() 189 | 190 | editor = atom.workspace.getActiveTextEditor() 191 | return unless editor? 192 | 193 | [range, flavour] = @getRegexpBufferRange editor 194 | log_debug "range", range, "flavour", flavour 195 | 196 | if not range 197 | @element.assertHidden() 198 | else 199 | regex = editor.getTextInBufferRange(range).trim() 200 | 201 | # special case, maybe we get a comment, but it might be already 202 | # marked as regex by language grammar, although it might result in 203 | # a comment 204 | return @element.assertHidden() if regex is '/' 205 | 206 | [regex, options] = @cleanRegex regex, flavour 207 | @element.showDiagram regex, {flavour, options} 208 | #editor.scrollToCursorPosition() 209 | 210 | # if not range 211 | # @emitter.emit 'did-not-find-regexp' 212 | # else 213 | # @emitter.emit 'did-find-regexp', editor.getTextInBufferRange range 214 | # 215 | # onDidNotFindRegexp: (callback) -> 216 | # @emitter.on 'did-not-find-regexp', callback 217 | # 218 | # onDidFindRegexp: (callback) -> 219 | # @emitter.on 'did-find-regexp', callback 220 | -------------------------------------------------------------------------------- /lib/regex-to-railroad.coffee: -------------------------------------------------------------------------------- 1 | parse = require "regexp" 2 | 3 | {Diagram, Sequence, Choice, Optional, OneOrMore, ZeroOrMore, Terminal, 4 | NonTerminal, Comment, Skip, Group } = require './railroad-diagrams' 5 | 6 | doSpace = -> NonTerminal("SP", title: "Space character", class: "literal whitespace") 7 | 8 | 9 | makeLiteral = (text) -> 10 | #debugger 11 | if text == " " 12 | doSpace() 13 | else 14 | parts = text.split /(^ +| {2,}| +$)/ 15 | sequence = [] 16 | for part in parts 17 | continue unless part.length 18 | if /^ +$/.test(part) 19 | if part.length == 1 20 | sequence.push doSpace() 21 | else 22 | sequence.push OneOrMore(doSpace(), Comment("#{part.length}x", title: "repeat #{part.length} times")) 23 | else 24 | sequence.push Terminal(part, class: "literal") 25 | 26 | if sequence.length == 1 27 | sequence[0] 28 | else 29 | new Sequence sequence 30 | 31 | get_flag_name = (flag) -> 32 | flag_names = { 33 | A: 'pcre:anchored' 34 | D: 'pcre:dollar-endonly' 35 | S: 'pcre:study' 36 | U: 'pcre:ungreedy' 37 | X: 'pcre:extra' 38 | J: 'pcre:extra' 39 | i: 'case-insensitive' 40 | m: 'multi-line' 41 | s: 'dotall' 42 | e: 'evaluate' 43 | o: 'compile-once' 44 | x: 'extended-legilibility' 45 | g: 'global' 46 | c: 'current-position' 47 | p: 'preserve' 48 | d: 'no-unicode-rules' 49 | u: 'unicode-rules' 50 | a: 'ascii-rules' 51 | l: 'current-locale' 52 | } 53 | 54 | if flag of flag_names 55 | flag_names[flag] 56 | else 57 | "unknown:#{flag}" 58 | 59 | rx2rr = (node, options) -> 60 | opts = options.options 61 | 62 | isSingleString = -> opts.match /s/ 63 | 64 | doStartOfString = -> 65 | if opts.match /m/ 66 | title = "Beginning of line" 67 | else 68 | title = "Beginning of string" 69 | NonTerminal("START", title: title, class: 'zero-width-assertion') 70 | 71 | doEndOfString = -> 72 | if opts.match /m/ 73 | title = "End of line" 74 | else 75 | title = "End of string" 76 | 77 | NonTerminal("END", title: title, class: 'zero-width-assertion') 78 | 79 | # debugger 80 | switch node.type 81 | when "match" 82 | literal = '' 83 | sequence = [] 84 | 85 | for n in node.body 86 | if n.type is "literal" and n.escaped 87 | if n.body is "A" 88 | sequence.push doStartOfString() 89 | else if n.body is "Z" 90 | sequence.push doEndOfString() 91 | else 92 | literal += n.body 93 | 94 | else if n.type is "literal" # and not n.escaped 95 | literal += n.body 96 | else 97 | if literal 98 | sequence.push makeLiteral(literal) 99 | literal = '' 100 | 101 | sequence.push rx2rr n, options 102 | 103 | if literal 104 | sequence.push makeLiteral(literal) 105 | 106 | if sequence.length == 1 107 | sequence[0] 108 | else 109 | new Sequence sequence 110 | 111 | when "alternate" 112 | alternatives = [] 113 | while node.type is "alternate" 114 | alternatives.push rx2rr node.left, options 115 | node = node.right 116 | 117 | alternatives.push rx2rr node, options 118 | 119 | new Choice Math.floor(alternatives.length/2)-1, alternatives 120 | 121 | when "quantified" 122 | {min, max, greedy} = node.quantifier 123 | 124 | body = rx2rr node.body, options 125 | 126 | throw new Error("Minimum quantifier (#{min}) must be lower than " 127 | + "maximum quantifier (#{max})") unless min <= max 128 | 129 | plural = (x) -> if x != 1 then "s" else "" 130 | 131 | switch min 132 | when 0 133 | if max is 1 134 | Optional(body) 135 | else 136 | if max == 0 137 | ZeroOrMore(body, quantifiedComment("0x", greedy, title: "exact 0 times repitition does not make sense")) 138 | else if max != Infinity 139 | ZeroOrMore(body, quantifiedComment("0-#{max}x", greedy, title: "repeat 0 to #{max} time" + plural(max))) 140 | else 141 | ZeroOrMore(body, quantifiedComment("*", greedy, title: "repeat zero or more times")) 142 | when 1 143 | if max == 1 144 | OneOrMore(body, Comment("1", title: "once")) 145 | else if max != Infinity 146 | OneOrMore(body, quantifiedComment("1-#{max}x", greedy, title: "repeat 1 to #{max} times")) 147 | else 148 | OneOrMore(body, quantifiedComment("+", greedy, title: "repeat at least one time")) 149 | else 150 | if max == min 151 | OneOrMore(body, Comment("#{max}x", title: "repeat #{max} times")) 152 | else if max != Infinity 153 | OneOrMore(body, quantifiedComment("#{min}-#{max}x", greedy, title: "repeat #{min} to #{max} times")) 154 | else 155 | OneOrMore(body, quantifiedComment(">= #{min}x", greedy, title: "repeat at least #{min} time" + plural(min))) 156 | 157 | when "capture-group" 158 | text = "capture #{node.index}" 159 | min_width = 55 160 | if node.name 161 | text += " (#{node.name})" 162 | min_width = 55 + (node.name.split('').length+3)*7 163 | Group rx2rr(node.body, options), Comment(text, class: "caption"), minWidth: min_width, attrs: {class: 'capture-group group'} 164 | 165 | when "flags" 166 | turn_on_long = [] 167 | turn_off_long = [] 168 | console.log node 169 | flags = node.body.join('') 170 | [turn_on, turn_off] = flags.split('-') 171 | turn_on ?= '' 172 | turn_off ?= '' 173 | for f in turn_on.split('') 174 | turn_on_long.push get_flag_name(f) 175 | 176 | for f in turn_off.split('') 177 | if f == 'i' 178 | turn_on_long.push('case-sensitive') 179 | else 180 | turn_off_long.push get_flag_name(f) 181 | 182 | _title = [] 183 | if turn_on 184 | _title.push "Turn on: "+turn_on_long.join(', ') 185 | if turn_off 186 | _title.push "Turn off: "+turn_off_long.join(', ') 187 | 188 | NonTerminal("SET: "+node.body.join(''), title: _title.join("\n"), class: 'zero-width-assertion') 189 | #NonTerminal("WORD", title: "Word character A-Z, 0-9, _", class: 'character-class') 190 | 191 | when "non-capture-group" 192 | # Group rx2rr(node.body, options), null, attrs: {class: 'group'} 193 | rx2rr(node.body, options) 194 | 195 | when "positive-lookahead" 196 | Group rx2rr(node.body, options), Comment("=> ?", title: "Positive lookahead", class: "caption"), attrs: {class: "lookahead positive zero-width-assertion group"} 197 | 198 | when "negative-lookahead" 199 | Group rx2rr(node.body, options), Comment("!> ?", title: "Negative lookahead", class: "caption"), attrs: {class: "lookahead negative zero-width-assertion group"} 200 | 201 | when "positive-lookbehind" 202 | Group rx2rr(node.body, options), Comment("<= ?", title: "Positive lookbehind", class: "caption"), attrs: {class: "lookbehind positive zero-width-assertion group"} 203 | 204 | when "negative-lookbehind" 205 | Group rx2rr(node.body, options), Comment(" 343 | if comment and greedy 344 | attrs.title += ', longest possible match' 345 | attrs.class = 'quantified greedy' 346 | Comment(comment + ' (greedy)', attrs) 347 | else if greedy 348 | attrs.title = 'longest possible match' 349 | attrs.class = 'quantified greedy' 350 | Comment('greedy', attrs) 351 | else if comment 352 | attrs.title += ', shortest possible match' 353 | attrs.class = 'quantified lazy' 354 | Comment(comment + ' (lazy)', attrs) 355 | else 356 | attrs.title = 'shortest possible match' 357 | attrs.class = 'quantified lazy' 358 | Comment('lazy', attrs) 359 | 360 | parseRegex = (regex) -> 361 | if regex instanceof RegExp 362 | regex = regex.source 363 | 364 | parse regex 365 | 366 | module.exports = 367 | Regex2RailRoadDiagram: (regex, parent, opts) -> 368 | Diagram(rx2rr(parseRegex(regex), opts)).addTo(parent) 369 | 370 | ParseRegex: parseRegex 371 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regex-railroad-diagram", 3 | "main": "./lib/regex-railroad-diagram", 4 | "version": "0.19.4", 5 | "description": "Display railroad diagram of regex under cursor.", 6 | "repository": "https://github.com/klorenz/atom-regex-railroad-diagrams", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">1.13.0" 10 | }, 11 | "dependencies": { 12 | "atom-space-pen-views": "^2.0.3", 13 | "railroad-diagrams": "git+https://github.com/klorenz/railroad-diagrams.git", 14 | "regexp": "git+https://github.com/klorenz/regexp.git#v1.4.0", 15 | "semver": "^5.3.0", 16 | "underscore-plus": "^1.6.6" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /regex-railroad-diagrams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klorenz/atom-regex-railroad-diagrams/b6f4e420ffb4677e9b0d62c1782ee71c331ae646/regex-railroad-diagrams.png -------------------------------------------------------------------------------- /spec/fixtures/coffeescript.coffee: -------------------------------------------------------------------------------- 1 | /(foo|bar)/ 2 | -------------------------------------------------------------------------------- /spec/fixtures/javascript.js: -------------------------------------------------------------------------------- 1 | /(foo|bar)/ 2 | const DEFAULT_PATTERN = /([a-zA-Z]{2,5})-?(\d{3,5})/g; 3 | 4 | var a = /^foo/ 5 | 6 | .replace(/(\+\d+)? ?(\d{2}) ?(\d{2}) ?(\d{2}) ?(\d{2})/g, '$1 $2 $3 $4 $5') 7 | .replace(/(\+\d+)? ?(\d{2}) ?(\d{2}) ?(\d{2}) ?(\d{2})/g, '$1 $2 $3 $4 $5') 8 | /./ 9 | 10 | foo|bar 11 | /dbo\.Test\(\)/ 12 | 13 | /^\/((deu|fra|nel)\/(de|en|fr|nl))\//i 14 | -------------------------------------------------------------------------------- /spec/fixtures/perl.pl: -------------------------------------------------------------------------------- 1 | while (<>) { 2 | /foo/ 3 | 4 | m{foobar|x/f} 5 | m{(foo|bar)} 6 | 7 | m/.*/s 8 | 9 | 10 | $variablename =~ /REGEX[:alnum:]/ 11 | $variablename =~ /REGEX[[:alnum:]]/ 12 | $variablename =~ /$var:REGEX[[:alnum:]]/ 13 | $variablename =~ m{\d+|\d+} 14 | $variablename =~ s/REGEX[[:alnum:]]/foo/g 15 | } 16 | -------------------------------------------------------------------------------- /spec/fixtures/php.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /spec/fixtures/python.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | 5 | re1 = re.compile(r'foo') 6 | re1 = re.compile(r'(foo|bar)') 7 | re1 = re.compile(r'''foo''') 8 | re1 = re.compile(r'(.*)(?=foo)(?!bar)$') 9 | 10 | regexShort = r'^(https|ftp|http)://\S*$' 11 | -------------------------------------------------------------------------------- /spec/fixtures/ruby.rb: -------------------------------------------------------------------------------- 1 | /foo/ 2 | /(foo|bar)$/ 3 | /(foo|bar)/ 4 | /(?bar|bar|a|a|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t)/ 5 | /(?\p{Latin} \P{Latin})/ 6 | -------------------------------------------------------------------------------- /spec/fixtures/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klorenz/atom-regex-railroad-diagrams/b6f4e420ffb4677e9b0d62c1782ee71c331ae646/spec/fixtures/test.js -------------------------------------------------------------------------------- /spec/regex-railroad-diagram-spec.coffee: -------------------------------------------------------------------------------- 1 | {WorkspaceView} = require 'atom' 2 | RegexRailroadDiagram = require '../lib/regex-railroad-diagram' 3 | 4 | {ParseRegex, Regex2RailRoadDiagram} = require '../lib/regex-to-railroad' 5 | 6 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 7 | # 8 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 9 | # or `fdescribe`). Remove the `f` to unfocus the block. 10 | 11 | 12 | describe "RegexRailroadDiagram", -> 13 | activationPromise = null 14 | 15 | beforeEach -> 16 | workspaceElement = atom.views.getView(atom.workspace) 17 | activationPromise = atom.packages.activatePackage('regex-railroad-diagram') 18 | 19 | waitsForPromise -> 20 | activationPromise 21 | runs -> 22 | debugger 23 | 24 | describe "regex-to-railroad diagram converter", -> 25 | 26 | it "parses a regex with alternatives", -> 27 | r = ParseRegex /a|b|c/ 28 | expect(r.toString()).toEqual { 29 | type: 'alternate', offset: 0, text : 'a|b|c', left : { 30 | type : 'match', offset : 0, text : 'a', body : [ 31 | { 32 | type : 'literal', offset : 0, 33 | text : 'a', body : 'a', escaped : false 34 | } 35 | ] 36 | }, right : { 37 | type : 'alternate', offset : 2, text : 'b|c', left : { 38 | type : 'match', offset : 2, text : 'b', body : [ 39 | { 40 | type : 'literal', offset : 2, 41 | text : 'b', body : 'b', escaped : false 42 | } 43 | ] 44 | }, right : { 45 | type : 'match', offset : 4, text : 'c', body : [ 46 | { 47 | type : 'literal', offset : 4, text : 'c', 48 | body : 'c', escaped : false 49 | } 50 | ] 51 | } 52 | } 53 | }.toString() 54 | 55 | it "parses a regex", -> 56 | r = Regex2RailRoadDiagram /foo*/, null 57 | expect(r).toBe "foo" 58 | 59 | describe "when the regex-railroad-diagram:toggle event is triggered", -> 60 | it "attaches and then detaches the view", -> 61 | expect(atom.workspaceView.find('.regex-railroad-diagram')).not.toExist() 62 | 63 | # This is an activation event, triggering it will cause the package to be 64 | # activated. 65 | atom.workspaceView.trigger 'regex-railroad-diagram:toggle' 66 | 67 | waitsForPromise -> 68 | activationPromise 69 | 70 | runs -> 71 | expect(atom.workspaceView.find('.regex-railroad-diagram')).toExist() 72 | atom.workspaceView.trigger 'regex-railroad-diagram:toggle' 73 | expect(atom.workspaceView.find('.regex-railroad-diagram')).not.toExist() 74 | -------------------------------------------------------------------------------- /spec/regex-railroad-diagram-view-spec.coffee: -------------------------------------------------------------------------------- 1 | RegexRailroadDiagramView = require '../lib/regex-railroad-diagram-view' 2 | 3 | describe "RegexRailroadDiagramView", -> 4 | it "has one valid test", -> 5 | expect("life").toBe "easy" 6 | -------------------------------------------------------------------------------- /src/regex-parser.coffee: -------------------------------------------------------------------------------- 1 | grammar = 2 | macros: 3 | {} 4 | 5 | init: (s) -> 6 | class Parsing 7 | constructur: (@string) -> 8 | @capture = 0 9 | 10 | rules: 11 | RegEx: 12 | o "RegEx *", (regex) -> OneOrMore(0, regex) 13 | o "RegEx ?", (regex) -> OneOrMore(0, regex, Comment("0 or 1 time")) 14 | o "RegEx +", (regex) -> ZeroOrMore() 15 | o "RegEx Quantifier", (regex, quantifier) -> 16 | {min, max} = quantifier 17 | if min == 0 18 | ZeroOrMore(0, regex, Comment("#{max} times")) 19 | else if min == max 20 | if min == 1 21 | OneOrMore(0, regex, Comment("once")) 22 | else 23 | OneOrMore(0, regex, Comment("#{max} times")) 24 | else 25 | OneOrMore(0, regex, Comment("#{min} to #{max} times")) 26 | o "(?= Regex )", (regex) -> Sequence(0, regex, Comment("Lookahead")) # maybe we pass some extra classed for getting this boxed 27 | o "(?! Regex )", (regex) -> Sequence(0, regex, Comment("Negative Lookahead")) # maybe we pass some extra classed for getting this boxed 28 | o "(?<= Regex )", (regex) -> Sequence(0, regex, Comment("Lookbehind")) # maybe we pass some extra classed for getting this boxed 29 | o "(? Sequence(0, regex, Comment("Negative Lookbehind")) # maybe we pass some extra classed for getting this boxed 30 | o "(?P< IDENT > Regex )", (name, regex) -> 31 | Sequence(0, regex, Comment("Capture #{capture} (#{name})")) # maybe we pass some extra classed for getting this boxed 32 | @capture += 1 33 | o "( RegEx )", (regex) -> 34 | Sequence(0, "capture #{capture}") 35 | @capture += 1 36 | 37 | o "Regex Regex" 38 | 39 | Quantifier: 40 | -> "{ , }", () -> min: 0, max: Infinity 41 | -> "{ INT , }", (start) -> min: start, max: Infinity 42 | -> "{ INT , INT }", (start, end) -> min: start, max: max 43 | -> "{ , INT }", (end) -> min: 0, max: max 44 | -------------------------------------------------------------------------------- /styles/regex-railroad-diagram.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .regex-railroad-diagram { 8 | background: @tab-bar-background-color; 9 | overflow-x: scroll; 10 | 11 | display: block; 12 | 13 | @terminal-text-color: @text-color-highlight; 14 | @non-terminal-text-color: @text-color; 15 | @line-color: @text-color-highlight; 16 | @box-color: @background-color-highlight; 17 | //@box-outline-color: @tab-border-color; 18 | @box-outline-color: @text-color-highlight; 19 | 20 | .settings-view { 21 | display: flex; 22 | 23 | padding: 5px; 24 | 25 | > div { 26 | padding: 5px 5px; 27 | } 28 | 29 | .texteditor-container { 30 | flex: 100 1; 31 | } 32 | 33 | .option-buttons { 34 | flex: 1; 35 | flex-wrap: nowrap; 36 | display: flex; 37 | font-weight: bold; 38 | } 39 | 40 | } 41 | 42 | .set-color-basis() when (lightness(@text-color) >= 50%) { 43 | // for stroke 44 | @L: 240; 45 | @D: 200; 46 | @A: 0.3; 47 | 48 | // for fill 49 | @l: 30; 50 | @d: 0; 51 | @a: 0.6; 52 | 53 | // for text 54 | @tl: 240; 55 | @td: 200; 56 | @ta: 1; 57 | 58 | @B: 0; 59 | 60 | @tL: 200; 61 | @tD: 150; 62 | } 63 | .set-color-basis() when (lightness(@text-color) < 50%) { 64 | // for stroke 65 | @L: 210; 66 | @D: 120; 67 | @A: 0.7; 68 | 69 | // for fill 70 | @l: 120; 71 | @d: 80; 72 | @a: 0.5; 73 | 74 | // for text 75 | @tl: 220; 76 | @td: 180; 77 | @ta: 1; 78 | 79 | @B: 0; 80 | 81 | @tL: 200; 82 | @tD: 150; 83 | } 84 | .set-color-basis(); 85 | 86 | @literal-stroke-color: rgba(@D, @L, @D, @A); 87 | @assertion-stroke-color: rgba(@L, @D, @L, @A); 88 | @capture-stroke-color: rgba(@D, @D, @L, @A); 89 | @back-reference-stroke-color: rgba(@D, @D, @L, @A); 90 | @character-class-stroke-color: rgba(@L, @L, @D, @A); 91 | @character-class-invert-stroke-color: rgba(@L, @D, @D, @A); 92 | 93 | @zero-width-positive-stroke-color: rgba(@D, @L, @D, @A); 94 | @zero-width-negative-stroke-color: rgba(@L, @D, @D, @A); 95 | 96 | @literal-fill-color: rgba(@d, @l, @d, @a); 97 | @assertion-fill-color: rgba(@l, @d, @l, @a); 98 | @capture-fill-color: rgba(@d, @d, @l, 0.1); 99 | @back-reference-fill-color: rgba(@d, @d, @l, 0.1); 100 | @zero-width-positive-fill-color: rgba(@l, @d, @l, 0.1); 101 | @zero-width-negative-fill-color: rgba(@l, @d, @l, 0.1); 102 | @character-class-fill-color: rgba(@l, @l, @d, @a); 103 | @character-class-invert-fill-color: rgba(@l, @d, @d, @a); 104 | 105 | @literal-text-color: rgba(@td, @tl, @td, @ta); 106 | @assertion-text-color: rgba(@tl, @td, @tl, @ta); 107 | @capture-text-color: rgba(@td, @td, @tl, @ta); 108 | @back-reference-text-color: rgba(@td, @td, @tl, @ta); 109 | @zero-width-positive-text-color: rgba(@td, @tl, @td, @ta); 110 | @zero-width-negative-text-color: rgba(@tl, @td, @td, @ta); 111 | @character-class-text-color: rgba(@tl, @tl, @td, @ta); 112 | @character-class-invert-text-color: rgba(@tl, @td, @td, @ta); 113 | 114 | @quantified-lazy-text-color: rgba(@tD, @tL, @tL, @ta); 115 | @quantified-greedy-text-color: rgba(@tL, @tD, @tL, @ta); 116 | 117 | .error-message { 118 | color: @text-color-error; 119 | // background-color: @background-color-error; 120 | } 121 | 122 | svg.railroad-diagram { 123 | stroke: @line-color; 124 | fill: @non-terminal-text-color; 125 | max-width: 100%; 126 | max-height: 50vh; 127 | 128 | path { 129 | stroke-width: 2; 130 | fill: rgba(0,0,0,0); 131 | } 132 | 133 | text { 134 | font-size: @font-size; 135 | font-family: @font-family; 136 | stroke-width: 0; 137 | fill: @text-color-highlight; 138 | 139 | // font: bold 14px monospace; 140 | text-anchor: middle; 141 | } 142 | 143 | // text.label { 144 | // text-anchor: start; 145 | // } 146 | // 147 | // text.comment { 148 | // font-size: @font-size; 149 | // font-family: @font-family; 150 | // fill: @text-color; 151 | // } 152 | 153 | rect { 154 | stroke-width: 3; 155 | stroke: @box-outline-color; 156 | fill: @box-color; 157 | } 158 | 159 | g.literal { 160 | > rect { 161 | stroke: @literal-stroke-color; 162 | fill: @literal-fill-color; 163 | } 164 | > text { 165 | fill: @literal-text-color; 166 | } 167 | } 168 | 169 | g.character-class { 170 | > rect { 171 | stroke: @character-class-stroke-color; 172 | fill: @character-class-fill-color; 173 | } 174 | > text { 175 | fill: @character-class-text-color; 176 | } 177 | } 178 | 179 | g.character-class.invert { 180 | > rect { 181 | stroke: @character-class-invert-stroke-color; 182 | fill: @character-class-invert-fill-color; 183 | } 184 | > text { 185 | fill: @character-class-invert-text-color; 186 | } 187 | } 188 | 189 | g.zero-width-assertion { 190 | > rect { 191 | stroke: @assertion-stroke-color; 192 | fill: @assertion-fill-color; 193 | } 194 | > text { 195 | fill: @assertion-text-color; 196 | } 197 | } 198 | 199 | g.group { 200 | > rect { 201 | stroke-width: 1; 202 | fill: rgba(0, 0, 0, 0); 203 | } 204 | } 205 | 206 | g.group.capture-group { 207 | > rect { 208 | stroke: @capture-stroke-color; 209 | fill: @capture-fill-color; 210 | } 211 | g.caption text { 212 | fill: @capture-text-color; 213 | } 214 | } 215 | 216 | g.zero-width-assertion.group { 217 | > rect { 218 | stroke-width: 2; 219 | } 220 | } 221 | 222 | g.zero-width-assertion.positive { 223 | > rect { 224 | stroke: @zero-width-positive-stroke-color; 225 | fill: @zero-width-positive-fill-color; 226 | } 227 | g.caption text { 228 | fill: @zero-width-positive-text-color; 229 | } 230 | } 231 | 232 | g.zero-width-assertion.negative { 233 | > rect { 234 | stroke: @zero-width-negative-stroke-color; 235 | fill: @zero-width-negative-fill-color; 236 | } 237 | g.caption text { 238 | fill: @zero-width-negative-text-color; 239 | } 240 | } 241 | 242 | g.back-reference { 243 | > rect { 244 | stroke: @back-reference-stroke-color; 245 | fill: @back-reference-fill-color; 246 | } 247 | > text { 248 | fill: @back-reference-text-color; 249 | } 250 | } 251 | 252 | g.quantified.lazy { 253 | text { 254 | fill: @quantified-lazy-text-color; 255 | } 256 | } 257 | 258 | g.quantified.greedy { 259 | text { 260 | fill: @quantified-greedy-text-color; 261 | } 262 | } 263 | } 264 | } 265 | --------------------------------------------------------------------------------