├── .editorconfig ├── .gitattributes ├── .gitignore ├── .htaccess ├── .nvmrc ├── LICENSE ├── README.md ├── assets ├── card.png ├── icons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── og.png ├── privacy.html ├── themes │ ├── dark.css │ └── light.css └── workers │ └── RegExWorker.js ├── dev ├── icons │ ├── RegExr.svg │ ├── add.svg │ ├── alert.svg │ ├── arrowleft.svg │ ├── arrowright.svg │ ├── cheatsheet.svg │ ├── check.svg │ ├── close.svg │ ├── code.svg │ ├── community.svg │ ├── copy.svg │ ├── delete.svg │ ├── distractor.svg │ ├── dropdown.svg │ ├── favorites.svg │ ├── flags.svg │ ├── github.svg │ ├── google.svg │ ├── help.svg │ ├── info.svg │ ├── link.svg │ ├── load.svg │ ├── menu.svg │ ├── moon.svg │ ├── private.svg │ ├── reference.svg │ ├── search.svg │ ├── share.svg │ ├── thumb.svg │ └── user.svg ├── inject │ └── icons.svg ├── lib │ ├── clipboard.js │ ├── codemirror.css │ ├── codemirror.js │ └── native.js ├── sass │ ├── codemirror.scss │ ├── colors.scss │ ├── colors_dark.scss │ ├── colors_light.scss │ ├── controls.scss │ ├── export.scss │ ├── fonts.scss │ ├── mobile.scss │ ├── regexr.scss │ ├── styles.scss │ ├── variables.scss │ └── views │ │ ├── ad.scss │ │ ├── community.scss │ │ ├── doc.scss │ │ ├── expression.scss │ │ ├── header.scss │ │ ├── share.scss │ │ ├── sidebar.scss │ │ ├── text.scss │ │ ├── tools.scss │ │ └── tools │ │ ├── details.scss │ │ └── explain.scss └── src │ ├── ExpressionLexer.js │ ├── Flavor.js │ ├── RefCoverage.js │ ├── RegExr.js │ ├── SubstLexer.js │ ├── app.js │ ├── controls │ ├── LinkRow.js │ ├── List.js │ ├── Status.js │ └── Tooltip.js │ ├── docs │ ├── Reference.js │ ├── reference_content.js │ └── sidebar_content.js │ ├── events │ ├── Event.js │ └── EventDispatcher.js │ ├── helpers │ ├── BrowserSolver.js │ ├── Prefs.js │ └── ServerSolver.js │ ├── net │ └── Server.js │ ├── profiles │ ├── core.js │ ├── javascript.js │ ├── pcre.js │ └── profiles.js │ ├── utils │ ├── CMUtils.js │ ├── DOMUtils.js │ ├── Track.js │ ├── UID.js │ └── Utils.js │ └── views │ ├── Account.js │ ├── Community.js │ ├── Example.js │ ├── Expression.js │ ├── ExpressionHighlighter.js │ ├── ExpressionHover.js │ ├── Share.js │ ├── Sidebar.js │ ├── Text.js │ ├── TextHighlighter.js │ ├── TextHover.js │ ├── Theme.js │ ├── Tools.js │ └── tools │ ├── Details.js │ ├── Explain.js │ └── Replace.js ├── gulpfile.babel.js ├── index.html ├── index.php ├── package-lock.json ├── package.json └── server ├── .editorconfig ├── Config.sample.php ├── Maintenance.php ├── actions ├── account │ ├── login.php │ ├── logout.php │ ├── patterns.php │ └── verify.php ├── oauthCallback.php ├── patterns │ ├── delete.php │ ├── favorite.php │ ├── load.php │ ├── multiFavorite.php │ ├── rate.php │ ├── save.php │ ├── search.php │ └── setAccess.php └── regex │ └── solve.php ├── api.php ├── apiDocs.php ├── bootstrap.php ├── composer.json ├── composer.lock ├── core ├── API.php ├── APIError.php ├── AbstractAction.php ├── Authentication.php ├── Cache.php ├── DB.php ├── ErrorCodes.php ├── NotImplemented.php ├── PatternVisibility.php ├── Result.php └── Session.php ├── schema.sql └── utils.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force all line endings to be \n 2 | * text eol=lf 3 | 4 | ############################################################ 5 | # git can corrupt binary files if they're not set to binary. 6 | ############################################################ 7 | 8 | # Apple office documents are actually folders, so treat them as binary. 9 | *.numbers binary 10 | *.pages binary 11 | *.keynote binary 12 | 13 | # Image files 14 | *.png binary 15 | *.jpg binary 16 | *.jpeg binary 17 | *.gif binary 18 | *.webp binary 19 | *.ico binary 20 | 21 | # Movie and audio files 22 | *.mov binary 23 | *.mp4 binary 24 | *.mp3 binary 25 | *.flv binary 26 | *.ogg binary 27 | 28 | # Compression formats 29 | *.gz binary 30 | *.bz2 binary 31 | *.7z binary 32 | *.zip binary 33 | 34 | # Web fonts 35 | *.ttf binary 36 | *.eot binary 37 | *.woff binary 38 | *.otf binary 39 | 40 | # Other 41 | *.fla binary 42 | *.swf binary 43 | *.pdf binary 44 | 45 | ############################################################ 46 | # End binary settings 47 | ############################################################ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #----------------------------- 2 | # SYSTEM 3 | #----------------------------- 4 | .DS_Store 5 | 6 | #----------------------------- 7 | # INVALID FILES 8 | # (for cross OS compatibility) 9 | #----------------------------- 10 | *[\<\>\:\"\/\\\|\?\*]* 11 | 12 | #----------------------------- 13 | # WORKSPACE 14 | #----------------------------- 15 | .idea/ 16 | *.sublime-project 17 | *.sublime-workspace 18 | *.vscode 19 | 20 | #----------------------------- 21 | # BUILD/DEBUGGING 22 | #----------------------------- 23 | node_modules/ 24 | .sass-cache/ 25 | build/ 26 | 27 | #----------------------------- 28 | # PROJECT SPECIFIC 29 | #----------------------------- 30 | regexr.css 31 | *.map 32 | scripts.min.js 33 | yarn.lock 34 | TMP_* 35 | /deploy 36 | server/cache/ 37 | server/Config.php 38 | server/**/.php_cs.cache 39 | server/vendor/ 40 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # www to non-www 2 | RewriteEngine On 3 | RewriteBase / 4 | RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] 5 | RewriteRule ^(.*)$ http://%1/$1 [R=301,L] 6 | 7 | # Handle deep links 8 | RewriteEngine on 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteCond %{REQUEST_FILENAME} !-f 11 | RewriteRule . / [L] 12 | RewriteRule (.+)\.md / 13 | 14 | # Font handling 15 | AddType 'application/x-font-woff' .woff 16 | 17 | # HTTPS to HTTP 18 | RewriteCond %{HTTPS} on 19 | RewriteRule (.*) http://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 20 | 21 | # BEGIN GZIP 22 | 23 | AddOutputFilterByType DEFLATE text/text text/html text/plain text/xml text/css application/x-javascript application/json application/javascript application/x-font-woff application/octet-stream 24 | 25 | # END GZIP 26 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.21.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | RegExr 4 | ====== 5 | 6 | # About 7 | [RegExr.com](http://regexr.com/) is an online tool to learn, build, and test Regular Expressions. It was created by [Grant Skinner](http://twitter.com/gskinner) and the nice people at [gskinner.com](http://gskinner.com/). 8 | 9 | # Features 10 | * Results update in real-time as you type. 11 | * Supports JavaScript & PHP/PCRE RegEx. 12 | * Roll over a match or expression for details. 13 | * Save & share expressions with others. 14 | * Use Tools to explore your results. 15 | * Browse the Reference for help & examples. 16 | * Undo & Redo with cmd-Z / Y in editors. 17 | * Search for & rate Community patterns. 18 | 19 | # Issues & Feature Requests 20 | Please report issues & feature requests on [GitHub](https://github.com/gskinner/regexr/issues). Please do not post questions about regular expressions (use Stack Overflow instead). 21 | 22 | # Contributing 23 | If you would like to contribute back to RegExr.com please send us pull requests. 24 | 25 | Please make sure they are well formatted and follow the style specified out in the existing files. 26 | 27 | # License 28 | This version of RegExr is licensed under GPL v3. If you're interested in using the source under other terms, feel free to [get in touch](https://gskinner.com). 29 | 30 | # Build 31 | RegExr uses Gulp to manage the build process. You will need to install Node and Gulp, and install other dependencies via `npm install`. Running `gulp` (default) will run dev builds and set up a test server. -------------------------------------------------------------------------------- /assets/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/card.png -------------------------------------------------------------------------------- /assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #70b0e0 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/icons/favicon.ico -------------------------------------------------------------------------------- /assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /assets/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RegExr", 3 | "short_name": "RegExr", 4 | "icons": [ 5 | { 6 | "src": "/assets/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /assets/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gskinner/regexr/1e382719041f8b1e5290472e14e2c98a6c05b61a/assets/og.png -------------------------------------------------------------------------------- /assets/privacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RegExr: Privacy Policy 6 | 23 | 24 | 25 | 26 |

RegExr Privacy Policy

27 |

We'll keep this simple: We take your privacy seriously. When you sign in to RegExr, we capture only the following information:

31 |

Your information is not shared with third parties, or used for any other purposes.

32 | 33 | -------------------------------------------------------------------------------- /assets/workers/RegExWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is now added to the index.html file 3 | * in .regexWorker 4 | * 5 | * Its currently manually minified. But that should move to the build process. 6 | */ 7 | 8 | // in plain JS for now: 9 | onmessage = function (evt) { 10 | postMessage("onload"); 11 | var data = evt.data, text = data.text, tests = data.tests, mode = data.mode; 12 | var regex = new RegExp(data.pattern, data.flags); 13 | 14 | // shared between BrowserSolver & RegExWorker 15 | var matches = [], match, index, error; 16 | if (mode === "tests") { 17 | for (var i=0, l=tests.length; i 2 | 3 | 4 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /dev/icons/add.svg: -------------------------------------------------------------------------------- 1 | Add -------------------------------------------------------------------------------- /dev/icons/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/arrowleft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dev/icons/arrowright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dev/icons/cheatsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /dev/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /dev/icons/code.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dev/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/distractor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | Artboard 1 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /dev/icons/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /dev/icons/favorites.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/flags.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /dev/icons/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /dev/icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dev/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/load.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /dev/icons/private.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/reference.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/icons/thumb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /dev/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /dev/lib/native.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window._native = function () { 4 | var _options = {}; 5 | var _construct = function _construct(e) { 6 | var defaultOptions = { 7 | carbonZoneKey: '', 8 | fallback: '', 9 | ignore: 'false', 10 | placement: '', 11 | prefix: 'native', 12 | targetClass: 'native-ad' 13 | }; 14 | 15 | if (typeof e === 'undefined') return defaultOptions; 16 | Object.keys(defaultOptions).forEach(function (key, index) { 17 | if (typeof e[key] === 'undefined') { 18 | e[key] = defaultOptions[key]; 19 | } 20 | }); 21 | return e; 22 | }; 23 | 24 | var init = function init(zone, options) { 25 | _options = _construct(options); 26 | 27 | var jsonUrl = 'https://srv.buysellads.com/ads/' + zone + '.json?callback=_native_go'; 28 | if (_options['placement'] !== '') { 29 | jsonUrl += '&segment=placement:' + _options['placement']; 30 | } 31 | if (_options['ignore'] === 'true') { 32 | jsonUrl += '&ignore=yes'; 33 | } 34 | 35 | var srv = document.createElement('script'); 36 | srv.src = jsonUrl; 37 | document.getElementsByTagName('head')[0].appendChild(srv); 38 | }; 39 | 40 | var carbon = function carbon(e) { 41 | var srv = document.createElement('script'); 42 | srv.src = '//cdn.carbonads.com/carbon.js?serve=' + e['carbonZoneKey'] + '&placement=' + e['placement']; 43 | srv.id = '_carbonads_js'; 44 | 45 | return srv; 46 | }; 47 | 48 | var sanitize = function sanitize(ads) { 49 | return ads.filter(function (ad) { 50 | return Object.keys(ad).length > 0; 51 | }).filter(function (ad) { 52 | return ad.hasOwnProperty('statlink'); 53 | }); 54 | }; 55 | 56 | var pixel = function pixel(p, timestamp) { 57 | var c = ''; 58 | if (p) { 59 | p.split('||').forEach(function (pixel, index) { 60 | c += ''; 61 | }); 62 | } 63 | return c; 64 | }; 65 | 66 | var options = function options() { 67 | return _options; 68 | }; 69 | 70 | return { 71 | carbon: carbon, 72 | init: init, 73 | options: options, 74 | pixel: pixel, 75 | sanitize: sanitize 76 | }; 77 | }({}); 78 | 79 | window._native_go = function (json) { 80 | var options = _native.options(); 81 | var ads = _native.sanitize(json['ads']); 82 | var selectedClass = document.querySelectorAll('.' + options['targetClass']); 83 | 84 | if (ads.length < 1) { 85 | selectedClass.forEach(function (className, index) { 86 | var selectedTarget = document.getElementsByClassName(options['targetClass'])[index]; 87 | 88 | if (options['fallback'] !== '' || options['carbonZoneKey'] !== '') selectedTarget.setAttribute('data-state', 'visible'); 89 | selectedTarget.innerHTML = options['fallback']; 90 | if (options['carbonZoneKey'] !== '') selectedTarget.appendChild(_native.carbon(options)); 91 | }); 92 | 93 | // End at this line if no ads are found, avoiding unnecessary steps 94 | return; 95 | } 96 | 97 | selectedClass.forEach(function (className, index) { 98 | var selectedTarget = document.getElementsByClassName(options['targetClass'])[index]; 99 | var adElement = selectedTarget.innerHTML||""; 100 | var prefix = options['prefix']; 101 | var ad = ads[index]; 102 | 103 | if (ad && className) { 104 | var adInnerHtml = adElement.replace(new RegExp('#' + prefix + '_bg_color#', 'g'), ad['backgroundColor']).replace(new RegExp('#' + prefix + '_bg_color_hover#', 'g'), ad['backgroundHoverColor']).replace(new RegExp('#' + prefix + '_company#', 'g'), ad['company']).replace(new RegExp('#' + prefix + '_cta#', 'g'), ad['callToAction']).replace(new RegExp('#' + prefix + '_cta_bg_color#', 'g'), ad['ctaBackgroundColor']).replace(new RegExp('#' + prefix + '_cta_bg_color_hover#', 'g'), ad['ctaBackgroundHoverColor']).replace(new RegExp('#' + prefix + '_cta_color#', 'g'), ad['ctaTextColor']).replace(new RegExp('#' + prefix + '_cta_color_hover#', 'g'), ad['ctaTextColorHover']).replace(new RegExp('#' + prefix + '_desc#', 'g'), ad['description']).replace(new RegExp('#' + prefix + '_index#', 'g'), prefix + '-' + ad['i']).replace(new RegExp('#' + prefix + '_img#', 'g'), ad['image']).replace(new RegExp('#' + prefix + '_small_img#', 'g'), ad['smallImage']).replace(new RegExp('#' + prefix + '_link#', 'g'), ad['statlink']).replace(new RegExp('#' + prefix + '_logo#', 'g'), ad['logo']).replace(new RegExp('#' + prefix + '_color#', 'g'), ad['textColor']).replace(new RegExp('#' + prefix + '_color_hover#', 'g'), ad['textColorHover']).replace(new RegExp('#' + prefix + '_title#', 'g'), ad['title']); 105 | selectedTarget.innerHTML = adInnerHtml + _native.pixel(ad['pixel'], ad['timestamp']); 106 | selectedTarget.setAttribute('data-state', 'visible'); 107 | } else { 108 | selectedTarget.innerHTML = ""; 109 | } 110 | }); 111 | }; -------------------------------------------------------------------------------- /dev/sass/codemirror.scss: -------------------------------------------------------------------------------- 1 | /* CodeMirror */ 2 | 3 | 4 | .CodeMirror { 5 | font-family: $monospace; 6 | background: none; 7 | position: absolute; // need this, and a wrapper with `position:relative` to work in a flexbox 8 | box-sizing: border-box; 9 | 10 | div.CodeMirror-cursor { 11 | pointer-events: none; /* this doesn't work in IE<11 */ 12 | border-left: 1px solid $doc-black; 13 | } 14 | } 15 | 16 | .CodeMirror-selected { 17 | background: rgba(170, 170, 170, 0.55); 18 | } 19 | 20 | .CodeMirror-focused .CodeMirror-selected { 21 | background: rgba(140, 150, 255, 0.8); 22 | } 23 | 24 | // this works, but of course the character isn't selectable, or highlightable: 25 | .editor.multiline { 26 | .CodeMirror-line:not(:last-child)>span:after { 27 | pointer-events: none; 28 | color: $invischar-color; 29 | content: "\AC"; // ¬ alternately: \B6 ¶ 30 | } 31 | } 32 | 33 | .CodeMirror-line { 34 | .cm-space::before, .cm-special::before { 35 | color: $invischar-color; 36 | content: "•"; // alternately: \B7 · 37 | position: absolute; 38 | } 39 | 40 | .cm-special::before { 41 | color: $error-color; 42 | } 43 | 44 | .cm-tab { 45 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTM4IDc5LjE1OTgyNCwgMjAxNi8wOS8xNC0wMTowOTowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTcgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REZENEEyN0Q3NTc0MTFFNzlFMTZGQ0Q1MEREODEyREEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REZENEEyN0U3NTc0MTFFNzlFMTZGQ0Q1MEREODEyREEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpERkQ0QTI3Qjc1NzQxMUU3OUUxNkZDRDUwREQ4MTJEQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpERkQ0QTI3Qzc1NzQxMUU3OUUxNkZDRDUwREQ4MTJEQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgWEz28AAABVSURBVHja7JVBCgAgCAQt+rf6cstjYN4iFxoQxJMjos3MCJlO4HyB26iqZLXyAswskQTUCmUSI7OruE4usxX9jCKELKK8w04e6Qqdmnfa/8SPmQIMANcZrZCVJGBIAAAAAElFTkSuQmCC); 46 | background-position: 100%; 47 | background-repeat: no-repeat; 48 | /* 49 | // ::before implementation. Doesn't stretch. 50 | pointer-events: none; 51 | content: "⟶"; 52 | position: absolute; 53 | color: $invischar-color; 54 | */ 55 | } 56 | } -------------------------------------------------------------------------------- /dev/sass/colors.scss: -------------------------------------------------------------------------------- 1 | // base colors: 2 | $theme-color: #70B0E0; // #66CCFF 3 | $base-color: mix(#808080, $theme-color, 85); 4 | 5 | // core colors: 6 | $black: darken($base-color, 46%); 7 | $darkest: mix($black, $base-color, 78%); 8 | $darker: mix($black, $base-color, 62%); 9 | $dark: mix($black, $base-color, 34%); 10 | 11 | $mid: $base-color; 12 | 13 | $white: lighten($base-color, 42%); 14 | $lightest: mix($white, $base-color, 92%); 15 | $lighter: mix($white, $base-color, 78%); 16 | $light: mix($white, $base-color, 50%); 17 | 18 | // doc colors: 19 | $doc-black: $black; 20 | $doc-darkest: mix($doc-black, $base-color, 80%); 21 | $doc-darker: mix($doc-black, $base-color, 64%); 22 | $doc-dark: mix($doc-black, $base-color, 35%); 23 | 24 | $doc-mid: $base-color; 25 | 26 | $doc-white: $white; 27 | $doc-lightest: mix($doc-white, $base-color, 92%); 28 | $doc-lighter: mix($doc-white, $base-color, 78%); 29 | $doc-light: mix($doc-white, $base-color, 50%); 30 | 31 | // control colors: 32 | $title-bg: $doc-light; 33 | $tooltip-bg: rgba($doc-black,0.85); 34 | $selected-stroke-color: rgba($doc-black, 0.3); 35 | $match-color: rgba($theme-color, 0.5); 36 | 37 | $dark-shadow: rgba(0,0,0, 0.25); 38 | $light-shadow: rgba(0,0,0, 0.1); 39 | $strong-shadow: rgba(0,0,0, 0.45); 40 | 41 | $details-group-alpha: 0.5; 42 | 43 | // notice colors: 44 | $error-color: #D22; 45 | $warning-color: $error-color; 46 | $fail-color: $error-color; 47 | $pass-color: #0A0; 48 | 49 | // syntax colors: 50 | $group-color: #0A0; 51 | $groupbg-color: #0E0; 52 | $alt-color: $group-color; 53 | 54 | $set-color: #D70; 55 | $setbg-color: #FE0; 56 | 57 | $anchor-color: #840; 58 | $quant-color: #58F; 59 | $esc-color: #C0C; 60 | $special-color: $esc-color; 61 | 62 | // invisible char color: 63 | // this isn't bound to the theme color, because it's used in the tab bitmap: 64 | $invischar-color: rgba(127,127,127,0.33); 65 | -------------------------------------------------------------------------------- /dev/sass/colors_dark.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | // base colors: 4 | $theme-color: desaturate(darken($theme-color, 18), 20); 5 | $base-color: mix(#808080, darken($theme-color, 20), 90); 6 | 7 | // core colors: 8 | // inherited 9 | 10 | // doc colors: 11 | $doc-black: $lighter; 12 | $doc-darkest: mix($doc-black, $base-color, 90%); 13 | $doc-darker: mix($doc-black, $base-color, 72%); 14 | $doc-dark: mix($doc-black, $base-color, 50%); 15 | 16 | $doc-mid: $mid; 17 | 18 | $doc-white: mix($black, $darkest, 30%); 19 | $doc-lightest: mix($doc-white, $base-color, 92%); 20 | $doc-lighter: mix($doc-white, $base-color, 80%); 21 | $doc-light: mix($doc-white, $base-color, 60%); 22 | 23 | // control colors: 24 | $title-bg: $doc-light; 25 | $tooltip-bg: rgba($doc-black, 0.85); 26 | $selected-stroke-color: rgba($doc-black, 0.4); 27 | $match-color: rgba($theme-color, 0.6); 28 | 29 | $details-group-alpha: 0.33; 30 | 31 | // shadows inherited 32 | 33 | // notice colors: 34 | 35 | // syntax coloring: 36 | // inherited 37 | 38 | // invisible char color: 39 | // inherited 40 | -------------------------------------------------------------------------------- /dev/sass/colors_light.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | // base colors: 4 | // inherit theme-color 5 | $theme-color: darken($theme-color, 6); 6 | $base-color: mix(#808080, invert($theme-color), 95); 7 | 8 | // core colors: 9 | $black: lighten($base-color, 44%); 10 | $darkest: mix($black, $base-color, 80%); 11 | $darker: mix($black, $base-color, 64%); 12 | $dark: mix($black, $base-color, 35%); 13 | 14 | $mid: $base-color; 15 | 16 | $white: darken($base-color, 45%); 17 | $lightest: mix($white, $base-color, 92%); 18 | $lighter: mix($white, $base-color, 78%); 19 | $light: mix($white, $base-color, 50%); 20 | 21 | // doc colors: 22 | $doc-black: $white; 23 | $doc-darkest: mix($doc-black, $base-color, 80%); 24 | $doc-darker: mix($doc-black, $base-color, 64%); 25 | $doc-dark: mix($doc-black, $base-color, 35%); 26 | 27 | $doc-mid: $base-color; 28 | 29 | $doc-white: mix($black, $base-color, 90%); 30 | $doc-lightest: mix($doc-white, $base-color, 92%); 31 | $doc-lighter: mix($doc-white, $base-color, 78%); 32 | $doc-light: mix($doc-white, $base-color, 50%); 33 | 34 | // control colors: 35 | $title-bg: $doc-light; 36 | $tooltip-bg: rgba($doc-black,0.9); 37 | $selected-stroke-color: rgba($doc-black, 0.4); 38 | $match-color: rgba($theme-color, 0.45); 39 | $dark-shadow: rgba(0,0,0, 0.25); 40 | $light-shadow: rgba(0,0,0, 0.1); 41 | $strong-shadow: rgba(0,0,0, 0.35); 42 | 43 | // syntax coloring: 44 | // inherited 45 | 46 | // invisible char color: 47 | // inherited 48 | -------------------------------------------------------------------------------- /dev/sass/export.scss: -------------------------------------------------------------------------------- 1 | // this shares values with js: 2 | #export { 3 | &.theme { color: $theme-color; } 4 | &.match { color: $match-color; } 5 | &.selected-stroke { color: $selected-stroke-color; } 6 | } -------------------------------------------------------------------------------- /dev/sass/fonts.scss: -------------------------------------------------------------------------------- 1 | $font-size: 16px; 2 | $font-family: 'Roboto Condensed', sans-serif; 3 | $monospace: 'Source Code Pro', monospace; 4 | -------------------------------------------------------------------------------- /dev/sass/mobile.scss: -------------------------------------------------------------------------------- 1 | // keep synced with RegExr._initUI: 2 | @media (max-width: 900px) { 3 | body { 4 | //font-size: 14px; 5 | 6 | >.container > .header { 7 | > .etc > .github { 8 | display: none; 9 | } 10 | 11 | > .file .savekey { 12 | display: none; 13 | } 14 | } 15 | 16 | > .container > .app { 17 | display: block; 18 | position:relative; 19 | background: $doc-lightest; 20 | 21 | 22 | > .doc { 23 | position: absolute; 24 | top: 0; 25 | bottom: 0; 26 | left: $closed-sidebar-width; 27 | right: 0; 28 | 29 | > .blocker { 30 | display: none; 31 | width: 100%; 32 | height: 100%; 33 | position: fixed; 34 | background: rgba($doc-lighter, 0.5); 35 | z-index: 10; 36 | } 37 | 38 | &.fadeback { 39 | filter: blur(2px); 40 | 41 | > .blocker { 42 | display: block; 43 | } 44 | } 45 | } 46 | 47 | > .sidebar { 48 | position:absolute; 49 | top: 0; 50 | bottom: 0; 51 | left: 0; 52 | z-index: 10000; 53 | transition: width $transition-t ease-out; 54 | width: calc(10% + 350px); 55 | box-shadow: 10px 0px 12px $strong-shadow; 56 | background: $darkest; 57 | opacity: 0.94; 58 | 59 | /* 60 | &:before { 61 | content: ""; 62 | position: fixed; 63 | width: 100%; 64 | height: 100%; 65 | background: rgba($black, 0.7); 66 | z-index: -1; 67 | } 68 | */ 69 | &.closed { 70 | width: $closed-sidebar-width; 71 | min-width: $closed-sidebar-width; 72 | box-shadow: 1px 2px 8px $strong-shadow; 73 | opacity: 1; 74 | &:before { content: none; } 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /dev/sass/regexr.scss: -------------------------------------------------------------------------------- 1 | // default and theme colors imported via build process 2 | @import "variables"; 3 | @import "fonts"; 4 | 5 | @import "styles"; 6 | @import "controls"; 7 | @import "views/header"; 8 | @import "views/doc"; 9 | @import "views/expression"; 10 | @import "views/text"; 11 | @import "views/tools"; 12 | @import "views/tools/explain"; 13 | @import "views/tools/details"; 14 | @import "views/sidebar"; 15 | @import "views/community"; 16 | @import "views/share"; 17 | 18 | @import "views/ad"; 19 | 20 | @import "mobile"; 21 | @import "export"; 22 | 23 | @import "codemirror"; 24 | -------------------------------------------------------------------------------- /dev/sass/styles.scss: -------------------------------------------------------------------------------- 1 | %title { 2 | display: flex; 3 | align-items: center; 4 | padding: 0 $pad 0 $pad; 5 | flex: 0 $title-height; 6 | 7 | h1 { 8 | flex: 1; 9 | } 10 | } 11 | 12 | %link { 13 | transition: color $transition-t; 14 | color: $lighter; 15 | 16 | &:not(.inactive) { 17 | cursor: pointer; 18 | 19 | &:hover { 20 | color: $lightest; 21 | 22 | svg.icon { 23 | color: currentColor; 24 | } 25 | } 26 | } 27 | 28 | svg.icon { 29 | transition: fill $transition-t; 30 | } 31 | } 32 | 33 | %ellipsis { 34 | background: $dark; 35 | color: $white; 36 | font-weight: normal; 37 | padding: 0 0.25em; 38 | margin-left: 0.25em; 39 | } 40 | 41 | %selected-token { 42 | outline: $selected-stroke; 43 | z-index: 10; 44 | } 45 | 46 | %related-token { 47 | outline: $related-stroke; 48 | z-index: 9; 49 | } 50 | 51 | html, body { 52 | margin: 0; 53 | width: 100%; 54 | height: 100%; 55 | min-height: 540px; 56 | min-width: 500px; 57 | font-family: $font-family; 58 | font-size: $font-size; 59 | color: $white; 60 | user-select: none; 61 | cursor: default; 62 | 63 | // This shouldn't be necessary if everything is working correctly 64 | // overflow: hidden; 65 | 66 | > .container { 67 | /* wraps all content, since flex layouts don't work reliably on body */ 68 | height: 100%; 69 | display: flex; 70 | flex-flow: column; 71 | 72 | > .app { 73 | /* wraps the sidebar and doc */ 74 | flex: 1; 75 | min-height: 0; // fix for Chrome 72+ 76 | display: flex; 77 | align-items: stretch; 78 | } 79 | } 80 | } 81 | 82 | // TODO: revisit for support in browsers other than Chrome & Safari: 83 | *::-webkit-scrollbar { 84 | width: 0.5em; 85 | height: 0.5em; 86 | } 87 | *::-webkit-scrollbar-track { 88 | background: rgba($mid, 0.25); 89 | } 90 | 91 | *::-webkit-scrollbar-thumb { 92 | background-color: $mid; 93 | border-radius: 0.5em; 94 | } 95 | 96 | .app .sidebar, .container .header, .tooltip { 97 | -moz-osx-font-smoothing: grayscale; 98 | -webkit-font-smoothing: antialiased; 99 | } 100 | 101 | code, pre { 102 | font-family: $monospace; 103 | } 104 | 105 | pre { 106 | margin: 0; 107 | } 108 | 109 | a { 110 | @extend %link; 111 | color: $theme-color; 112 | text-decoration: none; 113 | } 114 | 115 | h1, h2 { 116 | font-size: 1rem; 117 | font-weight: bold; 118 | margin: 0; 119 | } 120 | 121 | .list { 122 | display: flex; 123 | flex-direction: column; 124 | margin: 0; 125 | padding: 0; 126 | 127 | li { 128 | display: block; 129 | } 130 | } 131 | 132 | hr { 133 | border: 0; 134 | border-top: 1px solid rgba($mid, 0.25); 135 | margin: 0.75em 0 1em 0; 136 | } 137 | 138 | 139 | span.match { 140 | background: $match-color; 141 | color: $doc-black; 142 | } 143 | 144 | span.error { 145 | color: $error-color; 146 | font-weight: bold; 147 | 148 | &.warning { 149 | color: $warning-color; 150 | } 151 | } 152 | 153 | .anim-spin { 154 | animation: 1s linear infinite spin; 155 | } 156 | 157 | @keyframes spin { 158 | from { 159 | transform: rotate(0deg); 160 | } 161 | 162 | to { 163 | transform: rotate(360deg); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /dev/sass/variables.scss: -------------------------------------------------------------------------------- 1 | $pad: 1rem; 2 | $control-radius: 0.125rem; 3 | $input-height: 3.75rem; 4 | $title-height: 2.5rem; 5 | $transition-t: 0.2s; 6 | 7 | $control-opacity: 0.2; 8 | $button-opacity: 0.35; 9 | $input-opacity: 0.05; 10 | $button-hover-opacity: 0.75; 11 | $disabled-opacity: 0.35; 12 | 13 | $related-stroke-width: 1px; 14 | $related-stroke: solid $related-stroke-width $selected-stroke-color; 15 | $selected-stroke-width: 2px; 16 | $selected-stroke: solid $selected-stroke-width $selected-stroke-color; 17 | 18 | $closed-sidebar-width: 50px; -------------------------------------------------------------------------------- /dev/sass/views/ad.scss: -------------------------------------------------------------------------------- 1 | .native-js { 2 | transition: all 0.25s ease-in-out; 3 | opacity: 0; 4 | position: absolute; 5 | top: 0; 6 | } 7 | 8 | .native-js[data-state=visible] { 9 | position: static; 10 | opacity: 1; 11 | + .noad { 12 | display: none; 13 | } 14 | } 15 | 16 | .native-img { 17 | margin-right: $pad*0.75; 18 | max-height: 40px; 19 | max-width: 27%; 20 | border-radius: 3px; 21 | } 22 | 23 | .native-flex { 24 | display: flex; 25 | padding: $pad*0.75; 26 | text-decoration: none; 27 | 28 | flex-flow: row nowrap; 29 | justify-content: space-between; 30 | align-items: center; 31 | } 32 | 33 | .native-main { 34 | display: flex; 35 | 36 | flex-flow: row nowrap; 37 | align-items: center; 38 | } 39 | 40 | .native-details { 41 | display: flex; 42 | font-size: 12px; 43 | position: relative; 44 | 45 | max-height: 5em; 46 | 47 | flex-flow: column; 48 | } 49 | 50 | .native-company { 51 | margin-bottom: 4px; 52 | text-transform: uppercase; 53 | letter-spacing: 1px; 54 | font-weight: bold; 55 | font-size: 11px; 56 | } 57 | 58 | .native-desc { 59 | letter-spacing: 1px; 60 | font-weight: 300; 61 | line-height: 1.3em; 62 | text-overflow: ellipsis; 63 | max-height: 5em; 64 | overflow: hidden; 65 | position: relative; 66 | } 67 | 68 | 69 | 70 | /* Carbon Ads */ 71 | 72 | #carbonads { 73 | display: block; 74 | overflow: hidden; 75 | background-color: $black; 76 | font-size: 13px; 77 | line-height: 1.5; 78 | } 79 | 80 | #carbonads a { 81 | color: inherit; 82 | text-decoration: none; 83 | } 84 | 85 | #carbonads a:hover { 86 | color: inherit; 87 | } 88 | 89 | #carbonads span { 90 | position: relative; 91 | display: block; 92 | overflow: hidden; 93 | } 94 | 95 | .carbon-img { 96 | display: block; 97 | float: left; 98 | margin: 0; 99 | line-height: 1; 100 | } 101 | 102 | .carbon-img img { 103 | display: block; 104 | } 105 | 106 | .carbon-text { 107 | display: block; 108 | float: left; 109 | padding: 8px 1em; 110 | max-width: calc(100% - 130px - 2em); 111 | text-align: left; 112 | letter-spacing: .5px; 113 | } 114 | 115 | .carbon-poweredby { 116 | position: absolute; 117 | right: 0; 118 | bottom: 0; 119 | left: 130px; 120 | display: block; 121 | padding: 8px 13px; 122 | border-top: solid 1px $darkest; 123 | text-transform: uppercase; 124 | letter-spacing: 1px; 125 | font-weight: 400; 126 | font-size: 9px; 127 | line-height: 1; 128 | } 129 | 130 | -------------------------------------------------------------------------------- /dev/sass/views/community.scss: -------------------------------------------------------------------------------- 1 | .community { 2 | 3 | > header { 4 | .icon { 5 | @extend %link; 6 | color: $dark; 7 | 8 | &.selected:not(:hover) { 9 | color: $theme-color; 10 | } 11 | } 12 | 13 | background: $black; 14 | padding: $pad; 15 | margin: -$pad; 16 | margin-bottom: $pad*0.5; 17 | display: flex; 18 | justify-content: space-between; 19 | 20 | > .name { 21 | color: $white; 22 | font-weight: bold; 23 | overflow: hidden; 24 | display: block; 25 | white-space: nowrap; 26 | text-overflow: ellipsis; 27 | } 28 | 29 | > div { 30 | display: flex; 31 | 32 | .favorites { 33 | margin: 0 $pad; 34 | } 35 | } 36 | } 37 | 38 | .row.rate { 39 | .icon.thumbdown, .icon.thumbup { 40 | cursor: pointer; 41 | 42 | &:hover, &.selected:hover { 43 | color: $white; 44 | } 45 | 46 | &.selected { 47 | color: $theme-color; 48 | } 49 | } 50 | 51 | .icon.thumbdown { 52 | transform: translate(0,0.125em) scale(1,-1); 53 | } 54 | } 55 | 56 | >.author { 57 | margin-top: 0.125em; 58 | color: $mid; 59 | } 60 | 61 | .actions.list { 62 | background: 0; 63 | 64 | margin: -0.5*$pad -1*$pad 0 -1*$pad; 65 | 66 | } 67 | 68 | >.desc { 69 | margin-top: $pad; 70 | } 71 | } -------------------------------------------------------------------------------- /dev/sass/views/doc.scss: -------------------------------------------------------------------------------- 1 | .doc { 2 | flex: 8 800px; 3 | 4 | display: flex; 5 | flex-flow: column; 6 | 7 | > section { 8 | background: $doc-white; 9 | color: $doc-dark; 10 | flex: 1 1 0%; // fix for FireFox (vs just `1`) 11 | min-height: 0; // fix for Chrome 72+ 12 | transition: flex $transition-t ease-out; 13 | 14 | display: flex; 15 | flex-direction: column; 16 | 17 | > header { 18 | @extend %title; 19 | color: $doc-darkest; 20 | background: $title-bg; 21 | 22 | > .max { 23 | transition: transform $transition-t; 24 | color: $doc-mid; 25 | margin-left: 1rem; 26 | cursor: pointer; 27 | 28 | &:hover { 29 | color: $doc-white; 30 | } 31 | } 32 | } 33 | 34 | > article { 35 | flex: 1; 36 | min-height: 0; // fix for Chrome 72+ 37 | } 38 | 39 | &.closed { 40 | flex: 0 $title-height; 41 | 42 | > header { 43 | cursor: pointer; 44 | 45 | > *:not(h1) { 46 | opacity: 0.4; 47 | } 48 | 49 | > .max { 50 | transform: rotate(45deg); 51 | } 52 | } 53 | } 54 | } 55 | 56 | &.tests-mode { 57 | section.tools { 58 | display: none; 59 | } 60 | } 61 | 62 | // overwrite colors: 63 | input, textarea { 64 | background: rgba($doc-white, $input-opacity); 65 | border-color: rgba($doc-white, $input-opacity); 66 | } 67 | 68 | .control { 69 | background: rgba($doc-white, $control-opacity); 70 | } 71 | 72 | .button, .buttonbar > *, .segcontrol > * { 73 | background: rgba($doc-white, $button-opacity); 74 | 75 | &:hover, &.selected { 76 | background: rgba($doc-white, $button-hover-opacity); 77 | } 78 | 79 | &.default:hover { 80 | background: $doc-white; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /dev/sass/views/expression.scss: -------------------------------------------------------------------------------- 1 | .doc > section.expression { 2 | flex: 0 $title-height+$input-height; 3 | 4 | > header { 5 | background: $theme-color; 6 | } 7 | 8 | > .editor { 9 | position: relative; 10 | 11 | .icon.alert { 12 | display: none; 13 | position: relative; 14 | float: right; 15 | margin: $pad * 1.25; 16 | color: $error-color; 17 | z-index: 10; 18 | user-select: none; 19 | } 20 | 21 | > .CodeMirror { 22 | padding: $pad 0 0 $pad; 23 | font-weight: bold; 24 | } 25 | } 26 | 27 | > .editor.error > .icon.alert { 28 | display: block; 29 | } 30 | } 31 | 32 | // tooltips: 33 | #tooltip-flavor, #tooltip-flags { 34 | > .list { 35 | li { 36 | cursor: pointer; 37 | color: $doc-light; 38 | 39 | &:hover { 40 | color: $doc-white; 41 | } 42 | 43 | + li { 44 | margin-top: 0.5em; 45 | } 46 | 47 | .check.icon { 48 | color: $doc-dark; 49 | margin-right: 0.25em; 50 | } 51 | 52 | &.selected .check.icon { 53 | color: $theme-color; 54 | } 55 | 56 | em { 57 | font-style: normal; 58 | font-weight: bold; 59 | color: $doc-white; 60 | padding: 0 0.15em; 61 | background: rgba($doc-mid, 0.3); 62 | border-radius: $control-radius; 63 | } 64 | } 65 | } 66 | } 67 | 68 | 69 | 70 | /* Syntax highlighting */ 71 | 72 | .exp-related { 73 | border-bottom: $related-stroke; 74 | border-top: $related-stroke; 75 | margin-bottom: -$related-stroke-width; 76 | margin-top: -$related-stroke-width; 77 | } 78 | 79 | .exp-related-left { 80 | border-left: $related-stroke; 81 | margin-left: -$related-stroke-width; 82 | } 83 | 84 | .exp-related-right { 85 | border-right: $related-stroke; 86 | margin-right: -$related-stroke-width; 87 | } 88 | 89 | .exp-selected { 90 | border-top: $selected-stroke; 91 | border-bottom: $selected-stroke; 92 | } 93 | 94 | .exp-selected-left { 95 | border-left: $selected-stroke; 96 | margin-left: -$selected-stroke-width; 97 | } 98 | 99 | .exp-selected-right { 100 | border-right: $selected-stroke; 101 | margin-right: -$selected-stroke-width; 102 | } 103 | 104 | .exp-error { 105 | border-bottom: solid 2px $error-color; 106 | } 107 | 108 | .exp-warning { 109 | border-bottom: dotted 2px $warning-color; 110 | } 111 | 112 | .exp-char { 113 | color: $doc-black; 114 | } 115 | 116 | .exp-decorator { 117 | color: $doc-light; 118 | } 119 | 120 | .exp-esc { 121 | color: $esc-color; 122 | } 123 | 124 | .exp-quant, .exp-lazy, .exp-possessive { 125 | color: $quant-color; 126 | } 127 | 128 | .exp-alt { 129 | color: $alt-color; 130 | } 131 | 132 | .exp-anchor { 133 | color: $anchor-color; 134 | } 135 | 136 | .exp-group, .exp-ref, .exp-lookaround { 137 | color: $group-color; 138 | } 139 | 140 | .exp-charclass, .exp-set, .exp-subst { 141 | color: $set-color; 142 | } 143 | 144 | .exp-group-0 { background: rgba($groupbg-color, 0.11); } 145 | 146 | .exp-group-1 { background: rgba($groupbg-color, 0.22); } 147 | 148 | .exp-group-2 { background: rgba($groupbg-color, 0.33); } 149 | 150 | .exp-group-3 { background: rgba($groupbg-color, 0.44); } 151 | 152 | .exp-group-set { background: rgba($setbg-color, 0.3); } 153 | 154 | .exp-comment { 155 | color: $doc-light; 156 | background: rgba($doc-black, 0.05); 157 | font-style: italic; 158 | border-bottom: solid 3px $doc-lighter; 159 | } 160 | 161 | .exp-special { color: $special-color; } 162 | -------------------------------------------------------------------------------- /dev/sass/views/header.scss: -------------------------------------------------------------------------------- 1 | .container .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | 6 | background: $black; 7 | padding: 0 $pad; 8 | height: 54px; 9 | z-index: 1000; 10 | 11 | .logo { 12 | flex: none; 13 | font-size: 1.125rem; 14 | margin-top: $pad/16; 15 | color: $theme-color; 16 | width: 1.5em; 17 | height: 1.5em; 18 | } 19 | 20 | .settings { 21 | @extend %link; 22 | color: $theme-color; 23 | display: flex; 24 | min-width: 0; 25 | max-width: 20em; 26 | 27 | .name { 28 | margin: 0 $pad*0.25 0 $pad*0.5; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | overflow: hidden; 32 | } 33 | 34 | .icon.share { 35 | flex: none; 36 | color: $dark; 37 | width: 1em; 38 | margin-right: $pad*0.5; 39 | } 40 | } 41 | 42 | .signin.selected a { 43 | color: $white !important; 44 | } 45 | 46 | ul { 47 | padding: 0; 48 | white-space: nowrap; 49 | 50 | li { 51 | display: inline-block; 52 | 53 | &:first-child { 54 | margin-left: 0; 55 | } 56 | } 57 | } 58 | 59 | ul.file { 60 | text-align: right; 61 | 62 | li { 63 | @extend .button; 64 | 65 | color: $light; 66 | background: $darkest; 67 | 68 | &:hover { 69 | background: $darker; 70 | } 71 | 72 | &.save { 73 | color: $theme-color; 74 | font-weight: bold; 75 | 76 | .savekey { 77 | font-size: 0.875em; 78 | font-weight: normal; 79 | color: $mid; 80 | } 81 | } 82 | } 83 | } 84 | 85 | .button.theme { 86 | padding: $pad * 0.3755;; 87 | background: $darkest; 88 | 89 | &.selected .icon { 90 | color: $theme-color; 91 | } 92 | 93 | &:hover { 94 | background: $darker; 95 | } 96 | } 97 | 98 | ul.etc { 99 | flex: 1; 100 | text-align: right; 101 | font-size: 0.875rem; 102 | color: $dark; 103 | 104 | li { 105 | margin-left: $pad; 106 | } 107 | } 108 | } 109 | 110 | #tooltip-signin { 111 | ul.list li { 112 | @extend .button; 113 | margin: $pad*0.25 0; 114 | padding: $pad*0.625; 115 | color: $lightest; 116 | 117 | opacity: 0.85; 118 | &:hover { 119 | opacity: 1; 120 | } 121 | 122 | &[data-id='GitHub'] { 123 | background: #777777 !important; 124 | } 125 | 126 | &[data-id='Google'] { 127 | background: #C94130 !important; 128 | } 129 | 130 | >svg.icon { 131 | margin-right: $pad*0.625; 132 | } 133 | } 134 | 135 | .signout { 136 | display: none; 137 | 138 | .signoutbtn { 139 | cursor: pointer; 140 | color: $theme-color; 141 | font-weight: bold; 142 | 143 | &:hover { 144 | color: $doc-white; 145 | } 146 | } 147 | } 148 | 149 | &.authenticated { 150 | .signin { 151 | display: none; 152 | } 153 | .signout { 154 | display: block; 155 | } 156 | } 157 | 158 | .distract { 159 | display: none; 160 | color: $mid; 161 | 162 | .distractor { 163 | margin-right: $pad/2; 164 | } 165 | } 166 | 167 | &.wait { 168 | .distract { 169 | display: block; 170 | } 171 | 172 | .signout .signoutbtn, .signin .list { 173 | display: none; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /dev/sass/views/share.scss: -------------------------------------------------------------------------------- 1 | #share_main, #share_community { 2 | margin: -$pad; 3 | 4 | .list { 5 | background: transparent; 6 | border: none; 7 | } 8 | 9 | .inputs { 10 | input, textarea { 11 | color: $white; 12 | margin: 2px 0; 13 | } 14 | } 15 | 16 | .button { 17 | padding-left: $pad*2; 18 | padding-right: $pad*2; 19 | } 20 | 21 | .buttons.wait { 22 | .button { 23 | opacity: $disabled-opacity; 24 | pointer-events: none; 25 | } 26 | } 27 | 28 | .status { 29 | margin-left: $pad/2; 30 | } 31 | } 32 | 33 | #share_main { 34 | .signin.row { 35 | .signout { 36 | display: none; 37 | } 38 | 39 | &.authenticated { 40 | display: none; 41 | } 42 | } 43 | 44 | .row:not(.active) .icon.check { 45 | display: none; 46 | } 47 | 48 | .delete.row:hover { 49 | background: rgba($error-color, 0.6); 50 | } 51 | 52 | > .info { 53 | padding: $pad; 54 | margin-bottom: $pad * 0.5; 55 | border-bottom: solid 1px $darker; 56 | 57 | > .row { 58 | padding: 0 0 $pad/2 0; 59 | border: 0; 60 | color: $light; 61 | align-items: flex-start; 62 | 63 | svg.icon { 64 | color: currentColor; 65 | margin: 0; 66 | } 67 | } 68 | } 69 | 70 | > .save { 71 | flex-direction: column; 72 | align-items: flex-start; 73 | padding: $pad*1.25 $pad*0.5; 74 | margin-bottom: $pad*0.5; 75 | background: $darker; 76 | border: 0; 77 | box-shadow: -3px 3px 6px $light-shadow; 78 | 79 | .message { 80 | color: $white; 81 | } 82 | 83 | .buttons { 84 | padding: $pad*0.5 0 0 0; 85 | border: 0; 86 | color: $white; 87 | 88 | .button { 89 | margin: 0 $pad*0.5 0 0; 90 | } 91 | } 92 | } 93 | } 94 | 95 | 96 | 97 | #share_community { 98 | margin: 0 $pad/2; 99 | 100 | > .buttons { 101 | justify-content: flex-end; 102 | color: $white; 103 | border: none; 104 | 105 | } 106 | 107 | .inputs, .buttons { 108 | margin: $pad 0; 109 | } 110 | } -------------------------------------------------------------------------------- /dev/sass/views/tools.scss: -------------------------------------------------------------------------------- 1 | .doc > section.tools > article { 2 | display: flex; 3 | flex-direction: column; 4 | background: $doc-lightest; 5 | 6 | .icon.help { 7 | @extend %link; 8 | // this is a bit hacky, but lets us reuse the help icon everywhere. 9 | position: absolute; 10 | right: $pad*0.25; 11 | transform: translate(0, -50%) translate(0, $input-height/2); 12 | padding: $pad; 13 | color: rgba($doc-black, 0.25); 14 | z-index: 10; 15 | &:hover { 16 | color: $doc-darkest !important; 17 | } 18 | } 19 | 20 | .inputtool { 21 | display: none; 22 | flex-direction: column; 23 | flex: 1; 24 | 25 | .editor { 26 | position: relative; 27 | flex: 0 $input-height; 28 | background: $doc-white; 29 | border-bottom: solid 1px $doc-lighter; 30 | 31 | > .CodeMirror { 32 | padding: $pad 0 0 $pad; 33 | height: 100%; 34 | font-weight: bold; 35 | } 36 | } 37 | 38 | 39 | .result { 40 | flex: 1; 41 | 42 | textarea { 43 | box-sizing: border-box; 44 | padding: $pad 0 $pad $pad; 45 | resize: none; 46 | display: block; 47 | border: 0; 48 | background: none; 49 | width: 100%; 50 | height: 100%; 51 | margin: 0; 52 | font-family: $monospace; 53 | font-size: $font-size; 54 | color: $doc-black; 55 | &:focus { 56 | outline: none; 57 | } 58 | } 59 | } 60 | } 61 | 62 | .content { 63 | flex: 1 1 0%; // fix for FireFox (vs just `1`) 64 | min-height: 0; // fix for Chrome 72+ 65 | overflow-y: auto; 66 | display: block; 67 | padding: $pad; 68 | } 69 | 70 | &.showinput { 71 | .inputtool { 72 | display: flex; 73 | } 74 | .content { 75 | display: none; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /dev/sass/views/tools/details.scss: -------------------------------------------------------------------------------- 1 | .doc > section.tools > article .details { 2 | 3 | > div.desc { 4 | padding: $pad * 0.5; 5 | color: $doc-mid; 6 | margin-bottom: 0.5em; 7 | 8 | code { 9 | color: $doc-dark; 10 | font-weight: bold; 11 | } 12 | } 13 | 14 | table { 15 | font-family: $monospace; 16 | width: 100%; 17 | border-spacing: 0; 18 | border: 0; 19 | 20 | tr:nth-child(odd) td { 21 | background: mix($doc-lighter, $doc-lightest, 40); 22 | } 23 | 24 | tr.match td { 25 | background: $doc-lighter; 26 | border-bottom: 1px dotted $doc-light; 27 | } 28 | 29 | tr.group:hover { 30 | @extend %selected-token; 31 | } 32 | 33 | td { 34 | padding: $pad * 0.5 $pad; 35 | vertical-align: top; 36 | 37 | &:first-child { 38 | font-weight: bold; 39 | white-space: nowrap; 40 | padding-right: 0; 41 | } 42 | 43 | &:nth-child(2) { 44 | color: $doc-mid; 45 | white-space: nowrap; 46 | border-right: 1px dotted $doc-light; 47 | } 48 | 49 | &:nth-child(3) { 50 | white-space: pre-wrap; 51 | width: 100%; 52 | color: $doc-black; 53 | user-select: text; 54 | user-select: contain; 55 | 56 | em { 57 | color: $doc-mid; 58 | } 59 | } 60 | } 61 | } 62 | 63 | span.hover { 64 | @extend %selected-token; 65 | } 66 | 67 | .group-1 { background: hsla(60, 100, 50, $details-group-alpha); } 68 | .group-2 { background: hsla(120, 100, 50, $details-group-alpha); } 69 | .group-3 { background: hsla(230, 100, 70, $details-group-alpha); } 70 | .group-4 { background: hsla(280, 100, 60, $details-group-alpha); } 71 | .group-5 { background: hsla(-10, 100, 60, $details-group-alpha); } 72 | .group-0 { background: hsla(30, 100, 50, $details-group-alpha); } 73 | .group-0, .group-1, .group-2, .group-3, .group-4, .group-5 { outline: 0.5px solid rgba($doc-black, 0.25); } 74 | 75 | } -------------------------------------------------------------------------------- /dev/sass/views/tools/explain.scss: -------------------------------------------------------------------------------- 1 | .doc > section.tools > article .explain { 2 | 3 | > .desc { 4 | color: $doc-mid; 5 | display: block; 6 | padding: $pad*0.5; 7 | } 8 | 9 | div { 10 | padding: 0.5em 1em 0.5em 2.5em; 11 | margin-top: 0.75em; 12 | border: solid 1px rgba($doc-light, 0.5); 13 | border-left: solid 0.25em rgba($doc-light, 0.5); 14 | background: $doc-white; 15 | cursor: pointer; 16 | } 17 | 18 | div.selected { 19 | @extend %selected-token; 20 | } 21 | 22 | div.related { 23 | @extend %related-token; 24 | } 25 | 26 | div.applied { 27 | margin-left: 1em; 28 | margin-top: 0; 29 | border: none; 30 | background: rgba($doc-light, 0.3); 31 | 32 | + div.applied { 33 | margin-top: 1px; 34 | margin-left: 2em; 35 | } 36 | } 37 | 38 | div.error { 39 | background: mix($doc-white, $error-color, 85); 40 | border-color: $error-color; 41 | 42 | &.warning { 43 | /* nothing yet */ 44 | } 45 | 46 | .error-title { 47 | font-weight: bold; 48 | } 49 | 50 | .warningtext { 51 | color: $doc-mid; 52 | margin-left: 0.5rem; 53 | } 54 | } 55 | 56 | code { 57 | &.token { 58 | display: inline-block; 59 | min-width: 1.25em; 60 | margin: -1px 0.5em 0 -1.75em; 61 | float: left; 62 | } 63 | font-weight: bold; 64 | } 65 | 66 | div.close { 67 | margin: 0; 68 | padding: 0; 69 | border: none; 70 | background: none; 71 | } 72 | 73 | /* 74 | Group backgrounds need to be transparent so the selection shows through, but it looks wrong for explain. 75 | */ 76 | .exp-group-0, .exp-group-1, .exp-group-2, .exp-group-3 { 77 | border-color: rgba($group-color, 0.25); 78 | } 79 | 80 | .exp-group-0 { background: mix($doc-white, $groupbg-color, 92); } 81 | 82 | .exp-group-1 { background: mix($doc-white, $groupbg-color, 87); } 83 | 84 | .exp-group-2 { background: mix($doc-white, $groupbg-color, 82); } 85 | 86 | .exp-group-3 { background: mix($doc-white, $groupbg-color, 77); } 87 | 88 | // silly: 89 | .exp-group-4 { background: $doc-darker; color: $doc-white; padding: $pad; } 90 | 91 | .exp-group-set { 92 | background: mix($doc-white, $setbg-color, 75); 93 | border-color: rgba($set-color, 0.25); 94 | } 95 | } -------------------------------------------------------------------------------- /dev/src/Flavor.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import Track from "./utils/Track"; 20 | 21 | import EventDispatcher from "./events/EventDispatcher.js"; 22 | import BrowserSolver from "./helpers/BrowserSolver.js"; 23 | import ServerSolver from "./helpers/ServerSolver.js"; 24 | import profiles from "./profiles/profiles.js"; 25 | 26 | import app from "./app"; 27 | 28 | export default class Flavor extends EventDispatcher { 29 | 30 | constructor(flavor) { 31 | super(); 32 | this.value = app.prefs.read("flavor"); 33 | this._browserSolver = new BrowserSolver(); 34 | this._serverSolver = new ServerSolver(); 35 | } 36 | 37 | set value(id) { 38 | let profile = profiles[(id && id.toLowerCase()) || "js"]; 39 | if (!profile || profile === this._profile) { return; } 40 | 41 | this._profile = profile; 42 | this._buildSupportMap(profile); 43 | app.prefs.write("flavor", id); 44 | this.dispatchEvent("change"); 45 | } 46 | 47 | get value() { 48 | return this._profile.id; 49 | } 50 | 51 | get profile() { 52 | return this._profile; 53 | } 54 | 55 | get profiles() { 56 | return [profiles.js, profiles.pcre]; 57 | } 58 | 59 | get solver() { 60 | return this._profile.browser ? this._browserSolver : this._serverSolver; 61 | } 62 | 63 | isTokenSupported(id) { 64 | return !!this._profile._supportMap[id]; 65 | } 66 | 67 | getDocs(id) { 68 | return this._profile.docs[id]; 69 | } 70 | 71 | validateFlags(list) { 72 | let flags = this._profile.flags, dupes = {}; 73 | return list.filter((id)=>(!!flags[id] && !dupes[id] && (dupes[id] = true))); 74 | } 75 | 76 | validateFlagsStr(str) { 77 | return this.validateFlags(str.split("")).join(""); 78 | } 79 | 80 | isFlagSupported(id) { 81 | return !!this._profile.flags[id]; 82 | } 83 | 84 | _buildSupportMap(profile) { 85 | if (profile._supportMap) { return; } 86 | let map = profile._supportMap = {}, props = Flavor.SUPPORT_MAP_PROPS, n; 87 | for (n in props) { this._addToSupportMap(map, profile[n], !!props[n]); } 88 | let o = profile.escCharCodes, esc = profile.escChars; 89 | for (n in o) { map["esc_"+o[n]] = true; } 90 | for (n in esc) { map["esc_"+esc[n]] = true; } 91 | } 92 | 93 | _addToSupportMap(map, o, rev) { 94 | if (rev) { for (let n in o) { map[o[n]] = true; } } 95 | else { for (let n in o) { map[n] = o[n]; } } 96 | } 97 | } 98 | 99 | Flavor.SUPPORT_MAP_PROPS = { 100 | // 1 = reverse, 0 - normal 101 | flags: 1, 102 | // escape is handled separately 103 | // escCharCodes is handled separately 104 | escCharTypes: 1, 105 | charTypes: 1, 106 | // unquantifiables not included 107 | // unicodeScripts not included 108 | // unicodeCategories not included 109 | // posixCharClasses not included 110 | // modes not included 111 | tokens: 0, 112 | substTokens: 0 113 | // config not included 114 | // docs not included 115 | }; -------------------------------------------------------------------------------- /dev/src/RefCoverage.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import core from "./profiles/core.js"; 20 | import app from "./app"; 21 | 22 | export default class RefCoverage { 23 | constructor() { 24 | app.flavor._buildSupportMap(core); 25 | let ref = app.reference._idMap, undoc=[], unused=[], all=core._supportMap; 26 | let ignore = { 27 | "escchar": true, // literal char 28 | "groupclose": true, 29 | "setclose": true, 30 | "condition": true, // proxies to conditional 31 | "conditionalelse": true, // proxies to conditional 32 | subst_$group: true, // resolved to subst_group 33 | subst_$bgroup: true, // resolved to subst_group 34 | subst_bsgroup: true, // resolved to subst_group 35 | escoctalo: true // resolved to escoctal 36 | } 37 | 38 | for (let n in all) { if (!ref[n] && !ignore[n]) { undoc.push(n); } } 39 | for (let n in ref) { if (!all[n] && !ref[n].kids) { unused.push(n); } } 40 | 41 | console.log("--- UNDOCUMENTED IDS ---\n"+undoc.join("\n")+"\n\n--- UNUSED DOCS? ---\n"+unused.join("\n")); 42 | } 43 | } -------------------------------------------------------------------------------- /dev/src/SubstLexer.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import Utils from "./utils/Utils"; 20 | 21 | import app from "./app"; 22 | 23 | export default class SubstLexer { 24 | constructor() { 25 | this.profile = null; 26 | } 27 | 28 | set profile(profile) { 29 | this._profile = profile; 30 | this.string = this.token = this.errors = null; 31 | } 32 | 33 | parse(str) { 34 | if (!this._profile) { return null; } 35 | 36 | this.token = null; 37 | this.string = str; 38 | this.errors = []; 39 | 40 | // TODO: should this be passed in from Tools? 41 | let capGroups = app.expression.lexer.captureGroups; 42 | 43 | let prev=null, token, c; 44 | for (let i=0, l=str.length; i= 10 && capGroups[(num = (num/10|0))-1]) { l = numStr.length-1; } 116 | if (l) { 117 | token.l += l; 118 | // we don't assign the original type, because the docs combine them all into one id: 119 | token.type = num > 0 ? "subst_group" : "subst_0match"; 120 | token.clss = "subst"; 121 | if (num > 0) { token.group = capGroups[num-1]; } 122 | } 123 | } 124 | } 125 | 126 | SubstLexer.$_TYPES = { 127 | "$": "subst_$esc", 128 | "&": "subst_$&match", 129 | "`": "subst_$before", 130 | "'": "subst_$after", 131 | "0": "subst_0match" 132 | }; 133 | 134 | SubstLexer.SUBST_ESC_RE = new RegExp("^"+Utils.SUBST_ESC_RE.source,"i"); 135 | -------------------------------------------------------------------------------- /dev/src/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import "../lib/codemirror.js"; 20 | import "../lib/clipboard.js"; 21 | import "../lib/native.js"; 22 | import RegExr from "./RegExr"; 23 | 24 | let app = new RegExr(); 25 | export default app; -------------------------------------------------------------------------------- /dev/src/controls/LinkRow.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import $ from "../utils/DOMUtils"; 20 | import Utils from "../utils/Utils"; 21 | import app from "../app"; 22 | 23 | export default class LinkRow { 24 | constructor(el) { 25 | this.el = el; 26 | this._initUI(); 27 | this.url = null; 28 | } 29 | 30 | set pattern(val) { 31 | let url = Utils.getPatternURLStr(val) 32 | this._pattern = val; 33 | $.query(".url", this.el).innerText = url || ""; 34 | $.toggleClass(this.el, "disabled", !url); 35 | $.toggleClass(this.el, "active", !!url); 36 | } 37 | 38 | showMessage(message) { 39 | // for some reason this displays one line too low if it's synchronous: 40 | setTimeout(()=>app.tooltip.toggle.showOn("linkrow", message, $.query(".copy.icon", this.el), true, 0), 1); 41 | } 42 | 43 | _initUI() { 44 | this.el.onclick = (evt) => this._onClick(evt); 45 | 46 | let fld=$.query(".url", this.el), copyBtn = $.query(".copy", this.el); 47 | let clipboard = new Clipboard(copyBtn, { target: () => fld }); 48 | clipboard.on("success", () => app.tooltip.toggle.toggleOn("copy", "Copied to clipboard.", copyBtn, true, 3)); 49 | clipboard.on("error", (e) => app.tooltip.toggle.toggleOn("copy", Utils.getCtrlKey()+"-C to copy.", copyBtn, true, 3)); // TODO: cmd/ctrl 50 | } 51 | 52 | _onClick(evt) { 53 | if ($.query(".copy", this.el).contains(evt.target)) { return; } 54 | if (evt.which === 2 || evt.metaKey || evt.ctrlKey) { 55 | window.open(Utils.getPatternURL(this._pattern)); 56 | } else { 57 | app.load(this._pattern); 58 | } 59 | } 60 | }; -------------------------------------------------------------------------------- /dev/src/controls/List.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import $ from "../utils/DOMUtils"; 20 | import EventDispatcher from "../events/EventDispatcher"; 21 | 22 | export default class List extends EventDispatcher { 23 | constructor(el, opts) { 24 | super(); 25 | this.el = el; 26 | this.multi = opts.multi; 27 | this.template = opts.template; 28 | this.data = opts.data; 29 | if (opts.selected) { this.selected = opts.selected; } 30 | } 31 | 32 | set data(data) { 33 | $.empty(this.el); 34 | this._data = data; 35 | if (!data || !data.length) { return; } 36 | for (let i=0, l=data.length; i$.addClass($.query("[data-id='"+id+"']",this.el), "selected")); 49 | 50 | if (!this.multi) { this.scrollTo(ids[0]); } 51 | } 52 | 53 | get selected() { 54 | let els = $.queryAll("li.selected", this.el); 55 | if (!els[0]) { return null; } 56 | if (!this.multi) { return els[0].dataset.id; } 57 | let ids = []; 58 | for (let i=0, l=els.length; i o.id === id); 70 | } 71 | 72 | get selectedItem() { 73 | let el = this.selectedEl; 74 | return el && el.item; 75 | } 76 | 77 | get selectedEl() { 78 | return $.query("li.selected", this.el); 79 | } 80 | 81 | refresh() { 82 | let sel = this.selected; 83 | this.data = this._data; 84 | this.selected = sel; 85 | } 86 | 87 | addItem(o, selected=null) { 88 | let label, id, sel; 89 | let f=(evt) => this.handleClick(evt), template=this.template; 90 | if (typeof o === "string") { 91 | id = o; 92 | label = template ? template(o) : o; 93 | } else { 94 | if (o.hide) { return; } 95 | id = o.id || o.label; 96 | label = template ? template(o) : o.label; 97 | if (selected === null) { sel = o.selected; } 98 | } 99 | let item = $.create("li", sel ? "selected" : null, label, this.el); 100 | item.dataset.id = id; 101 | item.item = o; 102 | item.addEventListener("click", f); 103 | item.addEventListener("dblclick", f); 104 | 105 | if (selected) { 106 | this.selected = o.id; 107 | } 108 | } 109 | 110 | removeItem(id) { 111 | let el = $.query("[data-id='"+id+"']",this.el); 112 | el && el.remove(); 113 | } 114 | 115 | handleClick(evt) { 116 | let id = evt.currentTarget.dataset.id, old = this.selected; 117 | if (!this.getEl(id)) { return; } 118 | if (evt.type === "dblclick") { 119 | if (id != null) { this.dispatchEvent("dblclick"); } 120 | return; 121 | } else if (this.multi) { 122 | $.toggleClass(evt.currentTarget, "selected"); 123 | } else if (old === id) { 124 | if (id != null) { this.dispatchEvent("selclick"); } 125 | return; 126 | } else { 127 | this.selected = id; 128 | } 129 | if (!this.dispatchEvent("change", false, true)) { this.selected = old; } 130 | } 131 | 132 | scrollTo(id=this.selected) { 133 | let el = this.getEl(id); 134 | if (!el) { return; } 135 | //el.scrollIntoView(); // this is too jumpy, but would handle horizontal. 136 | 137 | let scrollEl = this.scrollEl || this.el; 138 | let top = el.offsetTop - scrollEl.offsetTop; 139 | if (top + el.offsetHeight > scrollEl.scrollTop+scrollEl.offsetHeight) { 140 | scrollEl.scrollTop = top+el.offsetHeight-scrollEl.offsetHeight+10; 141 | } else if (top < scrollEl.scrollTop) { 142 | scrollEl.scrollTop = top-10; 143 | } 144 | } 145 | 146 | getEl(id) { 147 | return $.query("[data-id='"+id+"']", this.el); 148 | } 149 | }; -------------------------------------------------------------------------------- /dev/src/controls/Status.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import $ from "../utils/DOMUtils"; 20 | import app from "../app"; 21 | 22 | export default class Status { 23 | constructor(el) { 24 | if (!el) { el = document.createElement("div"); } 25 | this.el = el; 26 | $.addClass(el, "status"); 27 | 28 | el.addEventListener("mouseover", () => this._showTooltip()); 29 | el.addEventListener("mouseout", () => this._hideTooltip()); 30 | } 31 | 32 | distract() { 33 | this.el.innerHTML = ''; 34 | this._show(); 35 | return this; 36 | } 37 | 38 | hide(t=0) { 39 | this._clearTimeout(); 40 | if (t) { 41 | this._timeoutId = setTimeout(()=>this._hide(), t*1000); 42 | } else { this._hide(); } 43 | return this; 44 | } 45 | 46 | success() { 47 | this.el.innerHTML = ''; 48 | this._show(); 49 | return this; 50 | } 51 | 52 | error(msg) { 53 | let el = this.el; 54 | el.innerHTML = ''; 55 | this._show(); 56 | this._ttMsg = msg; 57 | return this; 58 | } 59 | 60 | _showTooltip() { 61 | if (!this._ttMsg) { return; } 62 | app.tooltip.hover.showOn("status", this._ttMsg, this.el, true, 0); 63 | } 64 | 65 | _hideTooltip() { 66 | app.tooltip.hover.hide("status"); 67 | } 68 | 69 | _show() { 70 | this.el.style.display = null; 71 | this._ttMsg = null; 72 | this._hideTooltip(); 73 | this._clearTimeout(); 74 | } 75 | 76 | _hide() { 77 | this.el.style.display = "none"; 78 | this._hideTooltip(); 79 | this._clearTimeout(); 80 | } 81 | 82 | _clearTimeout() { 83 | if (this._timeoutId == null) { return; } 84 | clearTimeout(this._timeoutId); 85 | this._timeoutId = null; 86 | } 87 | } -------------------------------------------------------------------------------- /dev/src/controls/Tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import $ from "../utils/DOMUtils"; 20 | 21 | export default class Tooltip { 22 | 23 | constructor(el, transition=false) { 24 | this.el = $.remove(el); 25 | this.transition = transition; 26 | this.contentEl = $.query(".content", el); 27 | this.tipEl = $.query(".tip", el); 28 | this.hideF = (evt)=> Date.now()>this._showT && this.handleBodyClick(evt); 29 | this.curId = null; 30 | } 31 | 32 | toggle(id, content, x, y, autohide, th) { 33 | if (id === this.curId) { return this.hide(id); } 34 | this.show(id, content, x, y, autohide, th); 35 | } 36 | 37 | toggleOn(id, content, el, autohide, th) { 38 | if (id === this.curId) { return this.hide(id); } 39 | this.showOn(id, content, el, autohide, th); 40 | this.toggleEl = el; 41 | $.addClass(el, "selected"); 42 | } 43 | 44 | hide(id) { 45 | if (id && this.curId !== id) { return; } 46 | let el = this.el, elStyle = el.style; 47 | $.empty($.query(".content", $.remove(el))); 48 | $.removeClass(el, "flipped"); 49 | document.body.removeEventListener("mousedown", this.hideF); 50 | 51 | if (this.toggleEl) { 52 | $.removeClass(this.toggleEl, "selected"); 53 | this.toggleEl = null; 54 | } 55 | 56 | // reset position and width so that content wrapping resolves properly: 57 | elStyle.left = elStyle.top = "0"; 58 | elStyle.width = ""; 59 | if (this.transition) { 60 | elStyle.opacity = 0; 61 | elStyle.marginTop = "-0.25em"; 62 | } 63 | this.curId = null; 64 | } 65 | 66 | show(id, content, x, y, autohide = false, th = 0) { 67 | this.hide(); 68 | if (!content) { return; } 69 | 70 | let el = this.el, elStyle = el.style, contentEl = this.contentEl, body = document.body, pad = 8; 71 | if (content instanceof HTMLElement) { contentEl.appendChild(content); } 72 | else { contentEl.innerHTML = content; } 73 | 74 | if (autohide) { 75 | this._showT = Date.now()+30; // ignore double clicks and events in the current stack. 76 | body.addEventListener("mousedown", this.hideF); 77 | } 78 | 79 | body.appendChild(el); 80 | 81 | let wh = window.innerHeight, ww = window.innerWidth; 82 | let rect = el.getBoundingClientRect(), w = rect.right - rect.left, h = rect.bottom - rect.top, off = 0; 83 | if (y + h > wh - pad) { 84 | $.addClass(el, "flipped"); 85 | y -= th; 86 | } 87 | if (x - w / 2 < pad) { off = pad - x + w / 2; } 88 | else if (x + w / 2 > ww - pad) { off = ww - pad - x - w / 2; } 89 | this.tipEl.style.marginRight = Math.max(-w / 2 + 10, Math.min(w / 2 - 10, off)) * 2 + "px"; 90 | elStyle.width = Math.ceil(w/2)*2 + "px"; 91 | elStyle.top = Math.round(y) + "px"; 92 | elStyle.left = Math.round(x + off) + "px"; 93 | if (this.transition) { 94 | elStyle.opacity = 1; 95 | elStyle.marginTop = 0; 96 | } 97 | 98 | this.curId = id; 99 | } 100 | 101 | showOn(id, content, el, autohide, th=0) { 102 | let rect = el.getBoundingClientRect(); 103 | let x = Math.round((rect.left+rect.right)/2); 104 | let y = rect.bottom+th; 105 | let h = rect.bottom-rect.top; 106 | this.show(id, content, x, y, autohide, h); 107 | } 108 | 109 | handleBodyClick(evt) { 110 | let id = this.curId; 111 | if (this.el.contains(evt.target) || (this.toggleEl && this.toggleEl.contains(evt.target))) { return; } 112 | this.hide(id); 113 | } 114 | } -------------------------------------------------------------------------------- /dev/src/helpers/Prefs.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | 20 | export default class Prefs { 21 | constructor (el) { 22 | this._load(); 23 | } 24 | 25 | read(key) { 26 | return this._data[key]; 27 | } 28 | 29 | write(key, value) { 30 | if (this._data[key] === value) { return; } 31 | this._data[key] = value; 32 | this._save(); 33 | } 34 | 35 | clear(key) { 36 | delete(this._data[key]); 37 | this._save(); 38 | } 39 | 40 | _load() { 41 | let match = /(?:^|;\s*)prefs=\s*([^;]*)/.exec(document.cookie); 42 | if (match && match[1]) { 43 | try { 44 | this._data = JSON.parse(unescape(match[1])); 45 | return; 46 | } catch (e) {} 47 | } 48 | this._data = {}; 49 | } 50 | 51 | _save() { 52 | let str = escape(JSON.stringify(this._data)); 53 | document.cookie = "prefs="+str+"; expires=Fri, 31 Dec 9999 23:59:59 GMT;"; 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /dev/src/helpers/ServerSolver.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import Utils from "../utils/Utils"; 20 | import Server from "../net/Server"; 21 | 22 | export default class ServerSolver { 23 | 24 | solve(o, callback) { 25 | // unescape tool input: 26 | if (o.tool && o.tool.input != null) { o.tool.input = Utils.unescSubstStr(o.tool.input); } 27 | if (this._serverPromise) { this._serverPromise.abort(); } 28 | Utils.defer(()=>this._solve(o, callback), "ServerSolver._solve", 250); 29 | } 30 | 31 | _solve(o, callback) { 32 | this._callback = callback; 33 | this._serverPromise = Server.solve(o).then((o) => this._onLoad(o)).catch((o) => this._onError(o)); 34 | } 35 | 36 | _onLoad(data) { 37 | this._callback(data); 38 | } 39 | 40 | _onError(msg) { 41 | this._callback({error:{id:msg}}); 42 | } 43 | } -------------------------------------------------------------------------------- /dev/src/profiles/pcre.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | /* 20 | The PCRE profile is almost a straight copy of the core profile. 21 | */ 22 | let y=true, n=false; 23 | 24 | let pcre = { 25 | id: "pcre", 26 | label: "PCRE", 27 | browser: false, 28 | 29 | flags: { 30 | "u": n, 31 | "y": n 32 | }, 33 | 34 | badEscChars: "uUlLN".split("").reduce((o, c) => { o[c] = y; return o}, {}), 35 | 36 | escCharCodes: { 37 | "v": n // vertical tab // PCRE support \v as vertical whitespace 38 | }, 39 | 40 | tokens: { 41 | "escunicodeu": n, // \uFFFF 42 | "escunicodeub": n, // \u{00A9} 43 | // octalo PCRE 8.34+ 44 | }, 45 | 46 | substTokens: { 47 | "subst_$esc": n, // $$ 48 | "subst_$&match": n, // $& 49 | "subst_$before": n, // $` 50 | "subst_$after": n // $' 51 | }, 52 | 53 | config: { 54 | "reftooctalalways": n, // does a single digit reference \1 become an octal? (vs remain an unmatched ref) 55 | "substdecomposeref": n, // will a subst reference decompose? (ex. \3 becomes "\" & "3" if < 3 groups) 56 | "looseesc": n // should unrecognized escape sequences match the character (ex. \u could match "u") // disabled when `u` flag is set 57 | }, 58 | 59 | docs: { 60 | "escoctal":{ext:"+

The syntax \\o{FFF} is also supported.

"}, 61 | "numref":{ 62 | ext:"

There are multiple syntaxes for this feature: \\1 \\g1 \\g{1}.

"+ 63 | "

The latter syntaxes support relative values preceded by + or -. For example \\g-1 would match the group preceding the reference.

" 64 | }, 65 | "lazy": { ext:"+

This behaviour is reversed by the ungreedy (U) flag/modifier.

" } 66 | } 67 | }; 68 | 69 | export default pcre; -------------------------------------------------------------------------------- /dev/src/profiles/profiles.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import core from "./core"; 20 | import pcre from "./pcre"; 21 | import js from "./javascript"; 22 | 23 | let profiles = {core}; 24 | export default profiles; 25 | 26 | profiles.pcre = merge(core, pcre); 27 | profiles.js = merge(core, js); 28 | 29 | function merge(p1, p2) { 30 | // merges p1 into p2, essentially just a simple deep copy without array support. 31 | for (let n in p1) { 32 | if (p2[n] === false) { continue; } 33 | else if (typeof p1[n] === "object") { p2[n] = merge(p1[n], p2[n] || {}); } 34 | else if (p2[n] === undefined) { p2[n] = p1[n]; } 35 | } 36 | return p2; 37 | }; -------------------------------------------------------------------------------- /dev/src/utils/CMUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | /* 20 | Utilities for working with CodeMirror. 21 | */ 22 | 23 | import Utils from "../utils/Utils"; 24 | import $ from "../utils/DOMUtils"; 25 | 26 | let CMUtils = {}; 27 | export default CMUtils; 28 | 29 | CMUtils.create = function (target, opts={}, width="100%", height="100%") { 30 | let keys = {}, ctrlKey = Utils.getCtrlKey(); 31 | //keys[ctrlKey + "-Z"] = keys[ctrlKey + "-Y"] = keys["Shift-" + ctrlKey + "-Z"] = () => false; // block CM handling 32 | 33 | let o = Utils.copy({ 34 | lineNumbers: false, 35 | tabSize: 3, 36 | indentWithTabs: true, 37 | extraKeys: keys, 38 | specialChars: /[ \u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/, 39 | specialCharPlaceholder: (ch) => $.create("span", ch === " " ? "cm-space" : "cm-special", " ") // needs to be a space so wrapping works 40 | }, opts); 41 | 42 | let cm = CodeMirror(target, o); 43 | cm.setSize(width, height); 44 | 45 | 46 | if (cm.getOption("maxLength")) { 47 | cm.on("beforeChange", CMUtils.enforceMaxLength); 48 | } 49 | if (cm.getOption("singleLine")) { 50 | cm.on("beforeChange", CMUtils.enforceSingleLine); 51 | } 52 | 53 | return cm; 54 | }; 55 | 56 | CMUtils.getCharIndexAt = function (cm, winX, winY) { 57 | let pos = cm.coordsChar({left: winX, top: winY}, "page"); 58 | // test current and prev character, since CM seems to use the center of each character for coordsChar: 59 | for (let i = 0; i <= 1; i++) { 60 | let rect = cm.charCoords(pos, "page"); 61 | if (winX >= rect.left && winX <= rect.right && winY >= rect.top && winY <= rect.bottom) { 62 | return cm.indexFromPos(pos); 63 | } 64 | if (pos.ch-- <= 0) { 65 | break; 66 | } 67 | } 68 | return null; 69 | }; 70 | /* 71 | // unused? 72 | CMUtils.getEOLPos = function (cm, pos) { 73 | if (!isNaN(pos)) { 74 | pos = cm.posFromIndex(pos); 75 | } 76 | let rect = cm.charCoords(pos, "local"), w = cm.getScrollInfo().width; 77 | return cm.coordsChar({left: w - 1, top: rect.top}, "local"); 78 | }; 79 | */ 80 | CMUtils.getCharRect = function (cm, index) { 81 | if (index == null) { return null; } 82 | let pos = cm.posFromIndex(index), rect = cm.charCoords(pos); 83 | rect.x = rect.left; 84 | rect.y = rect.top; 85 | rect.width = rect.right - rect.left; 86 | rect.height = rect.bottom - rect.top; 87 | return rect; 88 | }; 89 | 90 | 91 | CMUtils.enforceMaxLength = function (cm, change) { 92 | let maxLength = cm.getOption("maxLength"); 93 | if (maxLength && change.update) { 94 | let str = change.text.join("\n"); 95 | let delta = str.length - (cm.indexFromPos(change.to) - cm.indexFromPos(change.from)); 96 | if (delta <= 0) { return true; 97 | } 98 | delta = cm.getValue().length + delta - maxLength; 99 | if (delta > 0) { 100 | str = str.substr(0, str.length - delta); 101 | change.update(change.from, change.to, str.split("\n")); 102 | } 103 | } 104 | return true; 105 | }; 106 | 107 | CMUtils.enforceSingleLine = function (cm, change) { 108 | if (change.update) { 109 | let str = change.text.join("").replace(/(\n|\r)/g, ""); 110 | change.update(change.from, change.to, [str]); 111 | } 112 | return true; 113 | }; 114 | 115 | CMUtils.selectAll = function(cm) { 116 | cm.focus(); 117 | cm.setSelection({ch:0,line:0},{ch:0, line:cm.lineCount()}); 118 | } 119 | 120 | CMUtils.calcRangePos = function(cm, i, l=0, o={}) { 121 | let doc = cm.getDoc(); 122 | o.startPos = doc.posFromIndex(i); 123 | o.endPos = doc.posFromIndex(i+l); 124 | return o; 125 | } -------------------------------------------------------------------------------- /dev/src/utils/Track.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | let Track = {}; 20 | export default Track; 21 | 22 | Track.GA_ID = "UA-3579542-6"; 23 | 24 | Track.page = function(path) { 25 | gtag("config", Track.GA_ID, {"page_path": "/"+path}); 26 | }; 27 | 28 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 29 | Track.event = function(name, category, label) { 30 | let o = {}; 31 | if (category) { o.event_category = category; } 32 | if (label) { o.event_label = label; } 33 | gtag("event", name, o); 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /dev/src/utils/UID.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | let UID = { 20 | _next: 0, 21 | get id() { return Date.now() + "_" + this._next++; }, 22 | assign(list, force=false) { 23 | list.forEach((o) => o.id = o.id == null || force ? this.id : o.id ); 24 | } 25 | }; 26 | export default UID; 27 | 28 | -------------------------------------------------------------------------------- /dev/src/views/Account.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import EventDispatcher from "../events/EventDispatcher"; 20 | 21 | import $ from "../utils/DOMUtils" 22 | import Track from "../utils/Track" 23 | 24 | import List from "../controls/List"; 25 | import Server from "../net/Server"; 26 | 27 | import app from "../app"; 28 | 29 | export default class Account extends EventDispatcher { 30 | constructor () { 31 | super(); 32 | this._value = {}; 33 | this._initUI(); 34 | } 35 | 36 | get value() { 37 | return this._value; 38 | } 39 | 40 | set value(val={}) { 41 | this._value = val; 42 | this._updateUI(); 43 | this.dispatchEvent("change"); 44 | } 45 | 46 | get userId() { return this._value.userId; } 47 | get author() { return this._value.author || this._value.username || ""; } 48 | get username() { return this._value.username || ""; } 49 | get authenticated() { return !!this._value.username; } // this._value.authenticated; 50 | get type() { return this._value.type; } 51 | 52 | showTooltip() { 53 | app.tooltip.toggle.toggleOn("signin", this.tooltipEl, this.signinBtn, true, 20); 54 | } 55 | 56 | // private methods: 57 | _initUI() { 58 | let template = (o) => ''+o; 59 | this.signinBtn = $.query(".header .signin"); 60 | this.tooltipEl = $.query("#library > #tooltip-signin"); 61 | this.signinEl = $.query(".signin", this.tooltipEl); 62 | this.signoutEl = $.query(".signout", this.tooltipEl); 63 | $.query(".signoutbtn", this.signoutEl).addEventListener("click", (evt) => this._doSignout()); 64 | this.signinBtn.addEventListener("click", (evt) => this.showTooltip()); 65 | $.query(".icon.help", this.signinEl).addEventListener("click", ()=> app.sidebar.goto("signin")); 66 | this.signinList = new List($.query("ul.list", this.signinEl), {data:["GitHub", "Google"], template}); 67 | this.signinList.on("change", ()=>this._signinListChange()); 68 | } 69 | 70 | _updateUI() { 71 | let auth = this.authenticated; 72 | $.toggleClass(this.tooltipEl, "authenticated", auth); 73 | $.query(".label", this.signinBtn).innerText = auth ? "Sign Out" : "Sign In"; 74 | if (auth) { 75 | $.query(".username", this.signoutEl).innerText = this.username; 76 | $.query(".type", this.signoutEl).innerText = this.type; 77 | } 78 | } 79 | 80 | _doSignout() { 81 | $.addClass(this.tooltipEl, "wait"); 82 | Server.logout().then((data) => { this._handleSignout(data); }).finally(()=>this._cleanSignout()); 83 | } 84 | 85 | _handleSignout(data) { 86 | this.value = data; 87 | } 88 | 89 | _cleanSignout(err) { 90 | $.removeClass(this.tooltipEl, "wait"); 91 | } 92 | 93 | _signinListChange() { 94 | let service = this.signinList.selected.toLowerCase(); 95 | $.addClass(this.tooltipEl, "wait"); 96 | Track.event("login", "access", service); 97 | setTimeout(() => Server.login(service), 100); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /dev/src/views/Community.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import LinkRow from "../controls/LinkRow"; 20 | import $ from "../utils/DOMUtils"; 21 | import Utils from "../utils/Utils"; 22 | import Example from "./Example"; 23 | import Server from "../net/Server"; 24 | 25 | import app from "../app"; 26 | 27 | // also used for My Patterns. 28 | export default class CommunityContent { 29 | constructor (el) { 30 | this.el = el; 31 | this.example = new Example(); 32 | el.appendChild(this.example.el); 33 | $.query(".icon.thumbup", el).addEventListener("click", ()=>this._rate(1)); 34 | $.query(".icon.thumbdown", el).addEventListener("click", ()=>this._rate(-1)); 35 | $.query(".icon.favorites", el).addEventListener("click", ()=>this._favorite()); 36 | this.linkRow = new LinkRow($.query(".row.link", el)) 37 | $.query(".icon.share", el).addEventListener("click", ()=>this._share()); 38 | } 39 | 40 | set item(o) { 41 | let el = this.el; 42 | this._pattern = o; 43 | $.query(".author", el).innerText = o.author ? "by "+o.author : ""; 44 | $.query(".name.label", el).innerText = o.name; 45 | $.query(".desc", el).innerText = o.description || "No description available."; 46 | this._updateRating(); 47 | this._updateFavorite(); 48 | this.example.example = [o.expression, o.text]; 49 | 50 | this.linkRow.pattern = o; 51 | } 52 | 53 | // private methods: 54 | _updateRating() { 55 | let o = this._pattern, el = this.el; 56 | $.query(".rating", el).innerText = o.rating.toFixed(1); 57 | $.removeClass($.query(".icon.rate.selected", el), "selected"); 58 | if (o.userRating === 1) { $.addClass($.query(".icon.thumbup", el), "selected"); } 59 | else if (o.userRating === -1) { $.addClass($.query(".icon.thumbdown", el), "selected"); } 60 | } 61 | 62 | _updateFavorite() { 63 | let o = this._pattern, el = this.el; 64 | $.toggleClass($.query(".icon.favorites", el), "selected", !!o.favorite); 65 | } 66 | 67 | _rate(val) { 68 | let o = this._pattern; 69 | o.userRating = (val === o.userRating) ? 0 : val; 70 | this._updateRating(); 71 | 72 | Server.rate(o.id, o.userRating).then((data) => this._handleRate(data)); 73 | } 74 | 75 | _share() { 76 | app.load(this._pattern); 77 | app.share.show(); 78 | } 79 | 80 | _handleRate(data) { 81 | if (data.id === this._pattern.id) { 82 | this._pattern.rating = data.rating; 83 | this._updateRating(); 84 | } 85 | } 86 | 87 | _favorite() { 88 | let o = this._pattern; 89 | Server.favorite(o.id, !o.favorite).then((data) => this._handleFavorite(data)); 90 | } 91 | 92 | _handleFavorite(data) { 93 | if (data.id === this._pattern.id) { 94 | this._pattern.favorite = data.favorite; 95 | this._updateFavorite(); 96 | } 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /dev/src/views/Example.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import $ from "../utils/DOMUtils"; 20 | import Utils from "../utils/Utils.js"; 21 | 22 | import app from "../app"; 23 | 24 | export default class Example { 25 | constructor (title, ex) { 26 | this.el = $.create("div", "example"); 27 | this.title = title; 28 | this.example = ex; 29 | } 30 | 31 | set example(ex) { 32 | if (ex === this._example) { return; } 33 | this._example = ex; 34 | 35 | let str = "", txt, exp, regex; 36 | if (ex) { 37 | exp = ex[0]; 38 | txt = ex[1]; 39 | regex = Utils.getRegExp(exp, "g"); 40 | if (this.title) { str += "

" + this.title + "


"; } 41 | str += "Load expression" + Utils.htmlSafe(exp) + ""; 42 | if (txt && regex) { 43 | let over=Math.max(0, txt.length-160), s=txt; 44 | if (over) { s = Utils.htmlSafe(s.substr(0,159)); } 45 | if (regex) { s = s.replace(regex, "$&"); } 46 | // TODO: this won't match on html elements: 47 | str += "
Load text" + s + (over?"\u2026" : "") + ""; 48 | } 49 | } 50 | this.el.innerHTML = str; 51 | if (exp) { 52 | $.query("code.expression > .load", this.el).addEventListener("click", ()=> { 53 | // TODO: this will need to be updated when we support other delimiters: 54 | app.expression.value = exp[0] === "/" ? exp : "/"+exp+"/g"; 55 | }); 56 | 57 | } 58 | if (txt) { $.query("code.text > .load", this.el).addEventListener("click", ()=> app.text.value = txt); } 59 | } 60 | 61 | 62 | } -------------------------------------------------------------------------------- /dev/src/views/ExpressionHighlighter.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | 20 | import EventDispatcher from "../events/EventDispatcher"; 21 | import CMUtils from "../utils/CMUtils"; 22 | 23 | export default class ExpressionHighlighter extends EventDispatcher { 24 | constructor(cm) { 25 | super(); 26 | this.cm = cm; 27 | this._activeMarks = []; 28 | this._hoverMarks = []; 29 | this._hoverToken = null; 30 | } 31 | 32 | clear() { 33 | this.cm.operation(() => { 34 | let marks = this._activeMarks; 35 | for (var i = 0, l = marks.length; i < l; i++) { 36 | marks[i].clear(); 37 | } 38 | marks.length = 0; 39 | }); 40 | } 41 | 42 | draw(token) { 43 | let cm = this.cm, pre = ExpressionHighlighter.CSS_PREFIX; 44 | 45 | this.clear(); 46 | cm.operation(() => { 47 | 48 | let groupClasses = ExpressionHighlighter.GROUP_CLASS_BY_TYPE; 49 | let doc = cm.getDoc(), endToken, marks = this._activeMarks; 50 | 51 | while (token) { 52 | if (token.clear) { 53 | token = token.next; 54 | continue; 55 | } 56 | token = this._calcTokenPos(token); 57 | 58 | var className = pre + (token.clss || token.type); 59 | if (token.error) { 60 | className += " " + pre + (token.error.warning ? "warning" : "error"); 61 | } 62 | 63 | if (className) { 64 | marks.push(doc.markText(token.startPos, token.endPos, {className: className})); 65 | } 66 | 67 | if (token.close) { 68 | endToken = this._calcTokenPos(token.close); 69 | className = groupClasses[token.clss || token.type]; 70 | if (className) { 71 | className = className.replace("%depth%", token.depth); 72 | marks.push(doc.markText(token.startPos, endToken.endPos, {className: className})); 73 | } 74 | } 75 | token = token.next; 76 | } 77 | }); 78 | } 79 | 80 | set hoverToken(token) { 81 | if (token === this._hoverToken) { return; } 82 | if (token && token.set && token.set.indexOf(this._hoverToken) !== -1) { return; } 83 | while (this._hoverMarks.length) { this._hoverMarks.pop().clear(); } 84 | 85 | this._hoverToken = token; 86 | if (token) { 87 | if (token.open) { 88 | this._drawSelect(token.open); 89 | } else { 90 | this._drawSelect(token); 91 | } 92 | if (token.related) { 93 | for (let i = 0, l=token.related.length; i < l; i++) { 94 | this._drawSelect(token.related[i], ExpressionHighlighter.CSS_PREFIX + "related"); 95 | } 96 | } 97 | } 98 | 99 | this.dispatchEvent("hover"); 100 | }; 101 | 102 | get hoverToken() { 103 | return this._hoverToken; 104 | } 105 | 106 | 107 | // private methods: 108 | _drawSelect(token, style = ExpressionHighlighter.CSS_PREFIX+"selected") { 109 | let doc = this.cm.getDoc(), endToken = token.close || token; 110 | if (token.set) { 111 | endToken = token.set[token.set.length - 1]; 112 | token = token.set[0]; 113 | } 114 | 115 | this._calcTokenPos(endToken); 116 | this._calcTokenPos(token); 117 | this._hoverMarks.push(doc.markText(token.startPos, endToken.endPos, { 118 | className: style, 119 | startStyle: style + "-left", 120 | endStyle: style + "-right" 121 | })); 122 | }; 123 | 124 | _calcTokenPos(token) { 125 | if (token.startPos || token == null) { 126 | return token; 127 | } 128 | CMUtils.calcRangePos(this.cm, token.i, token.l, token); 129 | return token; 130 | }; 131 | 132 | }; 133 | 134 | ExpressionHighlighter.CSS_PREFIX = "exp-"; 135 | 136 | ExpressionHighlighter.GROUP_CLASS_BY_TYPE = { 137 | set: ExpressionHighlighter.CSS_PREFIX+"group-set", 138 | setnot: ExpressionHighlighter.CSS_PREFIX+"group-set", 139 | group: ExpressionHighlighter.CSS_PREFIX+"group-%depth%", 140 | lookaround: ExpressionHighlighter.CSS_PREFIX+"group-%depth%" 141 | }; -------------------------------------------------------------------------------- /dev/src/views/ExpressionHover.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import CMUtils from "../utils/CMUtils"; 20 | 21 | import app from "../app"; 22 | 23 | export default class ExpressionHover { 24 | constructor (editor, highlighter) { 25 | this.editor = editor; 26 | this.highlighter = highlighter; 27 | this.isMouseDown = false; 28 | this.token = null; 29 | 30 | let o = editor.display.lineDiv; 31 | o.addEventListener("mousemove", (evt)=> this._handleMouseMove(evt)); 32 | o.addEventListener("mouseout", (evt)=> this._handleMouseOut(evt)); 33 | o.addEventListener("mousedown", (evt)=> this._handleMouseDown(evt)); 34 | 35 | } 36 | 37 | 38 | // private methods: 39 | _handleMouseMove(evt) { 40 | if (this.isMouseDown) { return; } 41 | 42 | let index, editor = this.editor, token = this.token, target = null; 43 | 44 | if (evt && token && (index = CMUtils.getCharIndexAt(editor, evt.clientX, evt.clientY + window.pageYOffset)) != null) { 45 | while (token) { 46 | if (index >= token.i && index < token.i+token.l) { 47 | target = token; 48 | break; 49 | } 50 | token = token.next; 51 | } 52 | } 53 | 54 | while (target) { 55 | if (target.open) { target = target.open; } 56 | else if (target.proxy) { target = target.proxy; } 57 | else { break; } 58 | } 59 | 60 | this.highlighter.hoverToken = target; 61 | let rect = (index != null) && CMUtils.getCharRect(editor, index); 62 | if (rect) { rect.right = rect.left = evt.clientX; } 63 | app.tooltip.hover.show("ExpressionHover", app.reference.tipForToken(target), evt.clientX, rect.bottom, true, 0); 64 | } 65 | 66 | _handleMouseOut(evt) { 67 | this.highlighter.hoverToken = null; 68 | app.tooltip.hover.hide("ExpressionHover"); 69 | } 70 | 71 | _handleMouseDown(evt) { 72 | // TODO: Should this also be in TextHover? 73 | if (evt.which !== 1 && evt.button !== 1) { return; } 74 | 75 | this.isMouseDown = true; 76 | let f, t = window.addEventListener ? window : document; 77 | t.addEventListener("mouseup", f = () => { 78 | t.removeEventListener("mouseup", f); 79 | this.isMouseDown = false; 80 | }); 81 | } 82 | } -------------------------------------------------------------------------------- /dev/src/views/TextHover.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import CMUtils from "../utils/CMUtils"; 20 | import $ from "../utils/DOMUtils"; 21 | 22 | import app from "../app"; 23 | 24 | export default class TextHover { 25 | constructor (editor, highlighter) { 26 | this.editor = editor; 27 | this.highlighter = highlighter; 28 | this._matches = this._x = null; 29 | 30 | let o = editor.display.lineDiv; 31 | o.addEventListener("mousemove", (evt)=> this._handleMouseMove(evt)); 32 | o.addEventListener("mouseout", (evt)=> this._handleMouseOut(evt)); 33 | } 34 | 35 | set matches(val) { 36 | this._matches = val; 37 | this._update(); 38 | } 39 | 40 | // private methods: 41 | _handleMouseMove(evt) { 42 | this._x = evt.clientX; 43 | this._y = evt.clientY + window.pageYOffset; 44 | this._update(); 45 | } 46 | 47 | _handleMouseOut(evt) { 48 | this._x = null; 49 | this._update(); 50 | } 51 | 52 | _update() { 53 | if (this._x === null) { 54 | this.highlighter.hoverMatch = null; 55 | app.tooltip.hover.hide("TextHover"); 56 | return; 57 | } 58 | let index, cm = this.editor, match, matches = this._matches, x = this._x, y = this._y; 59 | 60 | if (matches && matches.length && (index = CMUtils.getCharIndexAt(cm, x, y)) != null) { 61 | match = this.highlighter.hoverMatch = app.text.getMatchAt(index); 62 | } 63 | let rect = (index != null) && CMUtils.getCharRect(cm, index); 64 | if (rect) { rect.right = rect.left = x; } 65 | let tip = app.reference.tipForMatch(match, cm.getValue()); 66 | if (tip) { 67 | let div = $.create("div", "texthover", tip); 68 | app.tooltip.hover.show("TextHover", div, x, rect.bottom, true, 0); 69 | } 70 | 71 | } 72 | } -------------------------------------------------------------------------------- /dev/src/views/Theme.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import EventDispatcher from "../events/EventDispatcher"; 20 | 21 | import $ from "../utils/DOMUtils" 22 | import app from "../app"; 23 | 24 | export default class Theme extends EventDispatcher { 25 | constructor (el) { 26 | super(); 27 | this.el = el; 28 | this.urlTemplate = "./assets/themes/%name%.css"; 29 | this.targetNode = this._node = null; 30 | this._dark = false; 31 | this._initUI(); 32 | this.dark = !!app.prefs.read("dark"); 33 | } 34 | 35 | set dark(val) { 36 | val = !!val; 37 | if (this._dark === val) { return; } 38 | this._dark = val; 39 | this._load(val ? "dark" : null); 40 | $.toggleClass(this.themeBtn, "selected", val); 41 | app.prefs.write("dark", val); 42 | } 43 | 44 | get dark() { 45 | return this._dark; 46 | } 47 | 48 | _initUI() { 49 | this.themeBtn = $.query(".header .button.theme", this.el); 50 | this.themeBtn.addEventListener("click", (evt) => this._toggleTheme()); 51 | } 52 | 53 | _load(id) { 54 | if (id === this._id) { return; } 55 | this._id = id; 56 | if (this._node) { this._node.remove(); } 57 | if (!id) { this._change(); return; } 58 | let tmpl = this.urlTemplate, n = $.create("link"); 59 | n.addEventListener("load", () => this._change()); 60 | n.rel = "stylesheet"; 61 | n.type = "text/css"; 62 | n.href = tmpl ? tmpl.replace(/%name%/g, id) : id; 63 | this._node = (this.targetNode || document.head).appendChild(n); 64 | } 65 | 66 | _change() { 67 | this.dispatchEvent("change"); 68 | } 69 | 70 | _toggleTheme() { 71 | this.dark = !this.dark; 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /dev/src/views/tools/Details.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import $ from "../../utils/DOMUtils"; 20 | import Utils from "../../utils/Utils"; 21 | 22 | import app from "../../app"; 23 | 24 | export default class Details { 25 | 26 | constructor(el) { 27 | this.el = el; 28 | $.addClass(el, "details"); 29 | this._update(); 30 | 31 | this._bound_handleEvent = (evt) => this._handleEvent(evt); 32 | app.addEventListener("result", this._bound_handleEvent); 33 | app.text.addEventListener("select", this._bound_handleEvent); 34 | } 35 | 36 | 37 | cleanup() { 38 | $.empty(this.el); 39 | $.removeClass(this.el, "details"); 40 | app.removeEventListener("result", this._bound_handleEvent); 41 | app.text.removeEventListener("select", this._bound_handleEvent); 42 | Utils.defer(null, "Details._update"); 43 | } 44 | 45 | // private methods: 46 | _update() { 47 | $.empty(this.el); 48 | $.create("div", "desc", "Click a match above to display match & group details. Mouse over a Group row to highlight it in the Expression.", this.el); 49 | this._addMatch(app.text.selectedMatch, app.text.value); 50 | } 51 | 52 | _addMatch(match, textVal) { 53 | if (!match) { return; } 54 | let groups = match.groups, l=groups&&groups.length, ext=l&&(groups[0].i != null), matchVal=this._getMatchVal(match, textVal), extStr="", me = match.i+match.l; 55 | let groupTokens = app.expression.lexer.captureGroups; 56 | 57 | let tableEl = $.create("table", null, null, this.el); 58 | let matchEl = $.create("tr", "match", "Match "+match.num+""+this._getRangeStr(match)+"", tableEl); 59 | 60 | if (l) { 61 | let inGroups = [], lastIndex = match.i; 62 | for (let i = 0; i <= l; i++) { 63 | let group = groups[i], index = group ? group.i : me, num = i + 1, token = groupTokens[i]; 64 | if (ext) { 65 | for (let j = inGroups.length - 1; j >= 0; j--) { 66 | let inGroup = inGroups[j], ge = inGroup.i + inGroup.l; 67 | if (ge > index) { break; } 68 | inGroups.pop(); 69 | extStr += Utils.htmlSafe(textVal.substring(lastIndex, ge)) + ""; 70 | lastIndex = ge; 71 | } 72 | } 73 | if (!group) { break; } 74 | if (group.l) { 75 | extStr += Utils.htmlSafe(textVal.substring(lastIndex, index)) + ""; 76 | inGroups.push(group); 77 | lastIndex = index; 78 | } 79 | let val = "" + this._getMatchVal(group, textVal) + ""; 80 | let label = token.name ? "'"+token.name+"'" : ("Group " + num); 81 | let tr = $.create("tr", "group", "" + label + "" + this._getRangeStr(group) + "" + val + "", tableEl); 82 | 83 | tr.token = token; 84 | tr.addEventListener("mouseover", this._handleMouseEvent); 85 | tr.addEventListener("mouseout", this._handleMouseEvent); 86 | } 87 | if (ext) { extStr += Utils.htmlSafe(textVal.substring(lastIndex, me)); } 88 | } else { 89 | $.create("tr", "nogroup", "No groups.", tableEl); 90 | } 91 | 92 | $.query("td:last-child", matchEl).innerHTML = extStr || matchVal; 93 | } 94 | 95 | _getMatchVal(match, str) { 96 | let val = match.s || (match.i === undefined ? "" : str.substr(match.i, match.l)); 97 | return val ? Utils.htmlSafe(val) : "<empty>"; 98 | } 99 | 100 | _getRangeStr(match) { 101 | // we could check for match.l>0 to catch empty matches, but having a weird range might be more accurate. 102 | return match.i != null ? match.i + "-" + (match.i+match.l-1) : "n/a"; 103 | } 104 | 105 | _handleEvent(evt) { 106 | Utils.defer(()=>this._update(), "Details._update"); 107 | } 108 | 109 | _handleMouseEvent(evt) { 110 | let type = evt.type, token = evt.currentTarget.token; 111 | app.expression.highlighter.hoverToken = type === "mouseout" ? null : token; 112 | if (type === "mouseover") { $.addClass($.query("span.num-"+token.num, this.el), "hover"); } 113 | else { $.removeClass($.query("span.hover", this.el), "hover"); } 114 | evt.stopPropagation(); 115 | } 116 | } -------------------------------------------------------------------------------- /dev/src/views/tools/Replace.js: -------------------------------------------------------------------------------- 1 | /* 2 | RegExr: Learn, Build, & Test RegEx 3 | Copyright (C) 2017 gskinner.com, inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import $ from "../../utils/DOMUtils"; 20 | import Utils from "../../utils/Utils"; 21 | import app from "../../app"; 22 | 23 | export default class Replace { 24 | constructor(el, cm) { 25 | this.el = el; 26 | this.editor = cm; 27 | 28 | this._bound_handleEvent = (evt) => this._handleEvent(evt); 29 | app.addEventListener("result", this._bound_handleEvent); 30 | 31 | this._initUI(); 32 | this._update(); 33 | } 34 | 35 | cleanup() { 36 | $.empty(this.el); 37 | this.output.value = ""; 38 | $.removeClass(this.el, "details"); 39 | app.removeEventListener("result", this._bound_handleEvent); 40 | Utils.defer(null, "Replace._update"); 41 | } 42 | 43 | // private methods: 44 | _initUI() { 45 | this.output = $.create("textarea", null, null, this.el); 46 | this.output.readOnly = true; 47 | } 48 | 49 | _update() { 50 | let o = app.result && app.result.tool, result = o&&o.result; 51 | this.output.value = result || "no result"; 52 | } 53 | 54 | _handleEvent(evt) { 55 | Utils.defer(()=>this._update(), "Replace._update"); 56 | } 57 | } -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | try { 21 | require_once("./server/bootstrap.php"); 22 | $api = new \core\API(); 23 | $api->connect(); 24 | $db = $api->getDB(); 25 | 26 | $userProfile = (object)(new \account\verify())->run($db); 27 | } catch (\Exception $ex) { 28 | $userProfile = []; 29 | } 30 | 31 | $pattern = 'null'; 32 | $result = []; 33 | $success = preg_match("/([a-z0-9]+)\$/", $_SERVER['REQUEST_URI'], $result); 34 | if ($success == true) { 35 | $stringId = $result[0]; 36 | $id = convertFromURL($stringId); 37 | if (!is_null($id) && $id > 0) { 38 | try { 39 | $pattern = json_encode((new \patterns\load(['patternId'=>$stringId]))->run($db)); 40 | } catch (\Exception $ex) { 41 | $pattern = null; 42 | } 43 | } 44 | } 45 | 46 | $defaults = json_encode([ 47 | "userId" => idx($userProfile, 'userId'), 48 | "authenticate" => idx($userProfile, 'authenticated'), 49 | "username" => idx($userProfile, 'username'), 50 | "author" => idx($userProfile, 'author'), 51 | "type" => idx($userProfile, 'type') 52 | ]); 53 | 54 | $versions = json_encode([ 55 | "PCREVersion" => PCRE_VERSION, 56 | "PHPVersion" => PHP_VERSION 57 | ]); 58 | 59 | $pattern = is_null($pattern)?'null':$pattern; 60 | 61 | $initTemplate = "regexr.init($pattern,$defaults,$versions);"; 62 | 63 | $indexFile = file_get_contents('./index.html'); 64 | 65 | $openScriptTag = ''; 67 | $scriptIdx = strrpos($indexFile, $openScriptTag) + strlen($openScriptTag); 68 | $endIndexFile = strrpos($indexFile, $closeScriptTag, $scriptIdx); 69 | 70 | ob_start('ob_gzhandler'); 71 | echo(substr($indexFile, 0, $scriptIdx) . $initTemplate . substr($indexFile, $endIndexFile)); 72 | ob_flush(); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regexr", 3 | "version": "3.8.0", 4 | "description": "Regular expression tester with syntax highlighting, PHP / PCRE & JS Support, contextual help, cheat sheet, reference, and searchable community patterns.", 5 | "author": "Grant Skinner", 6 | "license": "GPL-3.0", 7 | "private": true, 8 | "devDependencies": { 9 | "@babel/core": "^7.9.6", 10 | "@babel/preset-env": "^7.9.6", 11 | "@babel/register": "^7.9.0", 12 | "browser-sync": "^2.26.7", 13 | "del": "^4.1.1", 14 | "gulp": "^4.0.2", 15 | "gulp-autoprefixer": "^6.1.0", 16 | "gulp-clean-css": "^4.3.0", 17 | "gulp-htmlmin": "^5.0.1", 18 | "gulp-inject": "^5.0.5", 19 | "gulp-rename": "^1.4.0", 20 | "gulp-sass": "^4.0.2", 21 | "gulp-svgmin": "^2.2.0", 22 | "gulp-svgstore": "^7.0.1", 23 | "gulp-template": "^5.0.0", 24 | "rollup": "^1.32.1", 25 | "rollup-plugin-babel": "4.3.2", 26 | "rollup-plugin-replace": "^2.2.0", 27 | "rollup-plugin-terser": "^5.3.0", 28 | "sass": "^1.26.5", 29 | "vinyl": "^2.2.0" 30 | }, 31 | "babel": { 32 | "presets": [ 33 | [ 34 | "@babel/env", 35 | { 36 | "targets": { 37 | "node": "current" 38 | } 39 | } 40 | ] 41 | ] 42 | }, 43 | "browserslist": [ 44 | "> 2%", 45 | "not ie < 10" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /server/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | [*] 3 | indent_style = space 4 | indent_size = 4 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /server/Config.sample.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | // Database defaults. 21 | define('DB_HOST', 'YOUR_DB_HOST'); 22 | define('DB_USER_NAME', 'YOUR_DB_USER_NAME'); 23 | define('DB_PASSWORD', 'YOUR_DB_PASSWORD'); 24 | define('DB_NAME', 'YOUR_DB_NAME'); 25 | 26 | define('CACHE_PATH', './cache/'); 27 | define('DEBUG', true); 28 | 29 | // OAUTH 30 | define('SESSION_NAME', 'session'); 31 | 32 | if (DEBUG) { 33 | // We can pass a special session header for debugging, this key must match to use it. 34 | define('DEBUG_SESSION', '--some random string--'); 35 | } 36 | 37 | // Create one at: https://console.developers.google.com/apis/credentials 38 | // You also need to enable Google+ support https://console.developers.google.com/apis/api/plus.googleapis.com 39 | define('GOOGLE_ID', '--'); 40 | define('GOOGLE_SECRET', '--'); 41 | 42 | // Create one at: https://github.com/settings/applications/ 43 | define('GITHUB_ID', '--'); 44 | define('GITHUB_SECRET', '--'); 45 | 46 | // https://developers.facebook.com/apps 47 | define('FACEBOOK_ID', '--'); 48 | define('FACEBOOK_SECRET', '--'); 49 | -------------------------------------------------------------------------------- /server/Maintenance.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | require_once(__DIR__ . "/bootstrap.php"); 21 | 22 | if (!isCLI()) { 23 | exit("cli only, bye."); 24 | } 25 | 26 | /** 27 | * Maintenance class that should run on a cron. 28 | * Runs cleanup functions on the database / filesystem. 29 | */ 30 | 31 | $api = new \core\API(); 32 | 33 | $api->connect(); 34 | $db = $api->getDB(); 35 | 36 | $runCount = 0; 37 | $deletedCount = 0; 38 | while (true) { 39 | // Delete temporary users, and their private patterns, when they have no session. 40 | $users = $db->execute("SELECT u.id as userId 41 | FROM users as u 42 | LEFT JOIN sessions as s ON s.userId=u.id 43 | WHERE u.type='temporary' AND s.id IS NULL LIMIT 10000"); 44 | 45 | $tmpUserCount = count($users); 46 | 47 | if ($tmpUserCount == 0) { 48 | break; 49 | } 50 | 51 | $deletedCount += $tmpUserCount; 52 | 53 | $runCount++; 54 | 55 | $usersToDelete = quoteStringArray(array_column($users, "userId")); 56 | 57 | $db->begin(); 58 | $db->execute("DELETE FROM patterns WHERE visibility='private' AND owner IN ($usersToDelete)"); 59 | $db->execute("DELETE FROM favorites WHERE userId IN ($usersToDelete)"); 60 | $db->execute("DELETE FROM users WHERE id IN ($usersToDelete)"); 61 | $db->commit(); 62 | 63 | sleep(3); 64 | } 65 | 66 | if (DEBUG) { 67 | echo("Completed! Deleted $deletedCount users.\n"); 68 | } 69 | -------------------------------------------------------------------------------- /server/actions/account/logout.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace account; 21 | 22 | class logout extends \core\AbstractAction { 23 | 24 | public $description = "Deletes the current users session."; 25 | 26 | public function execute() { 27 | $session = new \core\Session($this->db, SESSION_NAME); 28 | 29 | session_destroy(); 30 | 31 | if (isset($_COOKIE[SESSION_NAME])) { 32 | unset($_COOKIE[SESSION_NAME]); 33 | setcookie(SESSION_NAME, '', time() - 3600, '/'); 34 | } 35 | 36 | @session_write_close(); 37 | 38 | session_start(); 39 | // Return the new temporary user. 40 | $userProfile = (object)(new \account\verify())->run($this->db); 41 | 42 | return new \core\Result($userProfile); 43 | } 44 | 45 | public function getSchema() { 46 | return array(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/actions/account/patterns.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace account; 21 | 22 | class patterns extends \core\AbstractAction { 23 | public $description = 'Returns all the patterns either created or favorited by the current user. Favorite patterns are flagged with isFavorite.'; 24 | 25 | public function execute() { 26 | $userProfile = $this->getUserProfile(); 27 | 28 | // When using a single query performance is awful, so we run favorites and created separate. 29 | $createdResult = $this->db->execute("SELECT patterns.*, ur.rating as userRating 30 | FROM patterns 31 | LEFT JOIN userRatings as ur ON ur.userId=? AND ur.patternId=patterns.id 32 | WHERE patterns.owner = ?", [ 33 | ["s", $userProfile->userId], 34 | ["s", $userProfile->userId], 35 | ]); 36 | 37 | $favoriteResult = $this->db->execute("SELECT patterns.*, favorites.patternId = patterns.id as favorite, ur.rating as userRating 38 | FROM patterns 39 | LEFT JOIN userRatings as ur ON ur.userId=? AND ur.patternId=patterns.id 40 | LEFT JOIN favorites ON favorites.userId = ? 41 | WHERE favorites.patternId = patterns.id", [ 42 | ["s", $userProfile->userId], 43 | ["s", $userProfile->userId], 44 | ]); 45 | 46 | // Merge everything and filter out duplicate results. 47 | $result = \array_merge(\is_array($createdResult)?$createdResult:[], \is_array($favoriteResult)?$favoriteResult:[]); 48 | 49 | $cleanResult = []; 50 | if (!is_null($result)) { 51 | $keys = []; 52 | foreach ($result as $value) { 53 | $id = $value->id; 54 | if (!array_key_exists($id, $keys)) { 55 | $cleanResult[] = $value; 56 | $value->favorite = property_exists($value, "favorite")?true:null; 57 | $keys[$id] = true; 58 | } 59 | } 60 | 61 | usort($cleanResult, function ($a, $b) { 62 | return $a->rating > $b->rating?1:-1; 63 | }); 64 | } 65 | 66 | return new \core\Result(createPatternSet($cleanResult)); 67 | } 68 | 69 | public function getSchema() { 70 | return array(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/actions/account/verify.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace account; 21 | 22 | class verify extends \core\AbstractAction { 23 | 24 | public $description = "Verifies the current users session is valid, and returns their username and userId."; 25 | 26 | public function execute() { 27 | $username = null; 28 | $authorName = null; 29 | $userId = null; 30 | $type = null; 31 | $isAuthenticated = false; 32 | 33 | $session = new \core\Session($this->db, SESSION_NAME); 34 | 35 | $sessionId = session_id(); 36 | $sessionData = idx($_SESSION, 'data'); 37 | 38 | if (DEBUG === true && idx($_REQUEST, 'userId') !== null) { 39 | $tempUserId = intval(idx($_REQUEST, 'userId')); 40 | $sessionData = [ 41 | 'userId' => $tempUserId, 42 | 'type' => 'temporary' 43 | ]; 44 | $isAuthenticated = true; 45 | } 46 | 47 | if (!is_null($sessionData)) { 48 | $sessionData = (object)$sessionData; 49 | } 50 | 51 | if (!is_null($sessionData) && $sessionData->type != 'temporary') { 52 | $auth = new \core\Authentication(); 53 | $adapter = $auth->connect($sessionData->type); 54 | 55 | if (!is_null($adapter)) { 56 | try { 57 | $userProfile = $adapter->getUserProfile(); 58 | $username = idx($userProfile, 'displayName'); 59 | $authorName = idx($userProfile, 'authorName'); 60 | $userId = $sessionData->userId; 61 | $isAuthenticated = true; 62 | } catch (\Exception $ex) { 63 | $user = $this->db->execute("SELECT * FROM users WHERE id=?", ["s", $sessionData->userId], true); 64 | 65 | if (is_null($user)) { 66 | $isAuthenticated = false; 67 | } else { 68 | $username = $user->username; 69 | $authorName = $user->authorName; 70 | $isAuthenticated = true; 71 | } 72 | } 73 | } 74 | } else if (is_null($sessionData)) { 75 | // Create a new user, with a temporary flag. 76 | $tempUserEmail = uniqid(); 77 | $tempOauthId = uniqid(); 78 | 79 | $sql = "INSERT INTO users 80 | (email, username, authorName, type, oauthUserId, lastLogin) 81 | VALUES (?, '', '', 'temporary', ?, NOW())"; 82 | 83 | $this->db->execute($sql, [ 84 | ["s", $tempUserEmail], 85 | ["s", $tempOauthId] 86 | ]); 87 | 88 | $userId = $this->db->getLastId(); 89 | $sessionData = (object)[ 90 | 'userId' => $userId, 91 | 'userEmail' => $tempUserEmail, 92 | 'type' => 'temporary' 93 | ]; 94 | 95 | $isAuthenticated = false; 96 | 97 | $_SESSION['data'] = $sessionData; 98 | } 99 | 100 | $userDetails = $this->db->execute("SELECT * FROM users WHERE id=?",["s", $sessionData->userId], true); 101 | 102 | $result = array( 103 | 'authenticated' => $isAuthenticated, 104 | 'username'=> $username, 105 | 'author' => $authorName, 106 | 'userId' => intval($sessionData->userId), 107 | 'type' => $sessionData->type 108 | ); 109 | 110 | if (idx($userDetails, 'admin') == 1) { 111 | $result['admin'] = true; 112 | } 113 | 114 | @session_write_close(); 115 | 116 | if (!empty($userId)) { 117 | $this->db->execute("UPDATE sessions SET userId=? WHERE id=?", [ 118 | ["s", $userId], 119 | ["s", $sessionId], 120 | ]); 121 | } 122 | 123 | return new \core\Result($result); 124 | } 125 | 126 | public function getSchema() { 127 | return array(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /server/actions/oauthCallback.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | // This cannot be in a namespace (like users/login), or facebook login will fail. 21 | // Details: https://stackoverflow.com/a/5389447/4150286 22 | // *** If this name is ever changed also update Authentication->createCallbackURL() *** 23 | class oauthCallback extends \account\login { 24 | 25 | public $description = 'After account/login is called the OAUTH provider redirects back here.'; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /server/actions/patterns/delete.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace patterns; 21 | 22 | class delete extends \core\AbstractAction { 23 | 24 | public $description = 'If a user has access, deletes a pattern.'; 25 | 26 | public function execute() { 27 | $urlId = $this->getValue("patternId"); 28 | $patternId = convertFromURL($urlId); 29 | 30 | $exists = $this->db->exists("patterns", ["id"=>$patternId]); 31 | if (!$exists) { 32 | throw new \core\APIError(\core\ErrorCodes::API_PATTERN_NOT_FOUND); 33 | } 34 | 35 | $userProfile = $this->getUserProfile(); 36 | 37 | if (idx($userProfile, 'admin') != true) { 38 | $privateConst = \core\PatternVisibility::PRIVATE; 39 | $protectedConst = \core\PatternVisibility::PROTECTED; 40 | 41 | $sql = "SELECT id FROM patterns 42 | WHERE id=? 43 | AND owner=? 44 | AND visibility IN ('$privateConst', '$protectedConst')"; 45 | 46 | $exists = $this->db->execute($sql, [ 47 | ["s", $patternId], 48 | ["s", $userProfile->userId] 49 | ], true); 50 | 51 | if (!is_null($exists)) { 52 | $this->db->execute("DELETE IGNORE FROM patterns WHERE id=?", ["s", $patternId]); 53 | $this->clean($patternId); 54 | } else { 55 | throw new \core\APIError(\core\ErrorCodes::API_NOT_ALLOWED); 56 | } 57 | } else { 58 | // Admins can delete anything. 59 | $this->db->execute("DELETE IGNORE FROM patterns WHERE id=?", ["s", $patternId]); 60 | $this->clean($patternId); 61 | } 62 | 63 | return new \core\Result(['id' => $urlId]); 64 | } 65 | 66 | /* 67 | After a delete, also remove any linked items from the DB. 68 | */ 69 | private function clean($patternId) { 70 | $this->db->begin(); 71 | $this->db->execute("DELETE IGNORE FROM favorites WHERE patternId=?", ["s", $patternId]); 72 | $this->db->execute("DELETE IGNORE FROM userRatings WHERE patternId=?", ["s", $patternId]); 73 | $this->db->commit(); 74 | // We also have `patternLink` .. but not sure its required to delete this, since if we do use it we can check for a `null` pattern and assume it was deleted. 75 | } 76 | 77 | public function getSchema() { 78 | return array( 79 | "patternId" => array("type"=>self::STRING, "required"=>true) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server/actions/patterns/favorite.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | namespace patterns; 20 | 21 | class favorite extends \core\AbstractAction { 22 | public $description = 'Favorite or unfavorite any pattern.'; 23 | 24 | public function execute() { 25 | $urlId = $this->getValue("patternId"); 26 | $patternId = convertFromURL($urlId); 27 | $favorite = $this->getValue("favorite"); 28 | 29 | if (!$this->db->exists("patterns", ["id"=>$patternId])) { 30 | throw new \core\APIError(\core\ErrorCodes::API_PATTERN_NOT_FOUND); 31 | } 32 | 33 | $userProfile = $this->getUserProfile(); 34 | 35 | if ($favorite) { 36 | $sql = "INSERT IGNORE INTO favorites (userId, patternId) VALUES (?, ?)"; 37 | $this->db->execute($sql, [ 38 | ["s", $userProfile->userId], 39 | ["s", $patternId], 40 | ]); 41 | } else { 42 | $sql = "DELETE IGNORE FROM favorites WHERE patternId=? AND userId=?"; 43 | $this->db->execute($sql, [ 44 | ["s", $patternId], 45 | ["s", $userProfile->userId], 46 | ]); 47 | } 48 | 49 | return new \core\Result(['id' => $urlId, "favorite" => $favorite]); 50 | } 51 | 52 | public function getSchema() { 53 | return array( 54 | "patternId" => array("type"=>self::STRING, "required"=>true), 55 | "favorite" => array("type"=>self::BOOL, "required"=>true) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/actions/patterns/load.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace patterns; 21 | 22 | class load extends \core\AbstractAction { 23 | 24 | public $description = 'Return a pattern by its id. You can pass either the URL id or numeric id.'; 25 | 26 | public function execute() { 27 | $urlId = $this->getValue("patternId"); 28 | $patternId = convertFromURL($urlId); 29 | 30 | $userProfile = $this->getUserProfile(); 31 | 32 | $patternCacheKey = \core\Cache::PatternKey($patternId); 33 | $item = null;//\core\Cache::LoadItem($patternCacheKey); 34 | if (!is_null($item)) { 35 | $this->trackVisit($patternId); 36 | return new \core\Result($item); 37 | exit; 38 | } 39 | 40 | $sql = "SELECT p.*, ur.rating as userRating, fJoin.patternId as favorite 41 | FROM patterns p 42 | LEFT JOIN userRatings as ur ON ur.userId=? AND ur.patternId=p.id 43 | LEFT JOIN favorites as fJoin ON fJoin.userId=? AND fJoin.patternId=p.id 44 | WHERE p.id=? GROUP BY p.id 45 | "; 46 | $result = $this->db->execute($sql, [ 47 | ["s", $userProfile->userId], 48 | ["s", $userProfile->userId], 49 | ["s", $patternId] 50 | ], true); 51 | 52 | if (!is_null($result)) { 53 | // Check that the current user has access. 54 | if ($result->visibility == \core\PatternVisibility::PRIVATE) { 55 | $userProfile = $this->getUserProfile(); 56 | if ($userProfile->userId != $result->owner) { 57 | throw new \core\APIError(\core\ErrorCodes::API_NOT_ALLOWED); 58 | } 59 | } 60 | 61 | $json = createPatternNode($result); 62 | $this->trackVisit($patternId); 63 | 64 | if (intval($result->visits) > 10) { 65 | // \core\Cache::SaveItem($patternCacheKey, $json, true); 66 | } 67 | } else { 68 | throw new \core\APIError(\core\ErrorCodes::API_PATTERN_NOT_FOUND); 69 | } 70 | 71 | return new \core\Result($json); 72 | } 73 | 74 | function trackVisit($id) { 75 | $sql = "UPDATE patterns SET visits=visits+1, lastAccessed=NOW() WHERE id=?"; 76 | $this->db->execute($sql, [ 77 | ["s", $id] 78 | ]); 79 | } 80 | 81 | public function getSchema() { 82 | return array( 83 | "patternId" => array("type"=>self::STRING, "required"=>true) 84 | ); 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /server/actions/patterns/multiFavorite.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace patterns; 21 | 22 | class multiFavorite extends \core\AbstractAction { 23 | 24 | public $description = 'Favorite an array of patterns.'; 25 | 26 | public function execute() { 27 | $urlIds = $this->getValue("patternIds"); 28 | 29 | // Each id needs to be a number. 30 | $idList = quoteStringArray(array_map(function($id) { 31 | return \convertFromURL($id); 32 | }, $urlIds)); 33 | 34 | $existingIds = $this->db->execute("SELECT id FROM patterns WHERE id IN ($idList)"); 35 | $cleanIds = []; 36 | $vars = []; 37 | 38 | if (!is_null($existingIds)) { 39 | $idList = []; 40 | $userProfile = $this->getUserProfile(); 41 | 42 | for ($i=0; $i < count($existingIds); $i++) { 43 | $id = $existingIds[$i]->id; 44 | $cleanIds[] = convertToURL($id); 45 | 46 | $vars[] = ["s", $userProfile->userId]; 47 | $vars[] = ["s", $id]; 48 | $idList[] = "(?, ?)"; 49 | } 50 | 51 | $this->db->execute("INSERT IGNORE INTO favorites (userId, patternId) VALUES ". implode(",", $idList), $vars); 52 | } 53 | 54 | return new \core\Result($cleanIds); 55 | } 56 | 57 | public function getSchema() { 58 | return array( 59 | "patternIds" => array("type"=>self::JSON, "required"=>true) 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/actions/patterns/search.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace patterns; 21 | 22 | class search extends \core\AbstractAction { 23 | 24 | public $description = 'Search all the public patterns names and descriptions.'; 25 | 26 | public function execute() { 27 | $query = $this->getValue("query"); 28 | $startIndex = $this->getValue("startIndex"); 29 | $limit = $this->getValue("limit"); 30 | $type = $this->getValue("flavor"); 31 | 32 | $this->userProfile = $this->getUserProfile(); 33 | 34 | $result = null; 35 | 36 | $result = $this->searchCommunity($query, $startIndex, $limit, $type); 37 | 38 | return new \core\Result($result); 39 | } 40 | 41 | public function searchCommunity($query, $startIndex, $limit, $type) { 42 | // Build the search query. 43 | $whereStatements = []; 44 | $searchSqlParams = []; 45 | 46 | // Search everything using the query. 47 | if (!empty($query)) { 48 | $whereStatements[] = "MATCH(`name`, `description`, `author`) AGAINST(? IN NATURAL LANGUAGE MODE)"; 49 | $searchSqlParams[] = ["s", $query]; 50 | } 51 | 52 | // Do the actual search. 53 | $q = "SELECT p.* FROM patterns p WHERE p.visibility='public'"; 54 | 55 | if (!is_null($type)) { 56 | $typeArray = quoteStringArray($type); 57 | $q .= " AND p.flavor IN ($typeArray)"; 58 | } 59 | 60 | if (count($whereStatements) > 0) { 61 | $q .= " AND (" . implode(" OR ", $whereStatements) . ")"; 62 | } 63 | 64 | $q .= " ORDER BY p.ratingSort DESC LIMIT ?, ?"; 65 | $searchSqlParams[] = ["s", $startIndex]; 66 | $searchSqlParams[] = ["s", $limit]; 67 | 68 | $result = $this->db->execute($q, $searchSqlParams); 69 | 70 | // Inject userRating and favorite 71 | $patternIds = quoteStringArray(array_map(function ($pattern) { 72 | return idx($pattern, 'id'); 73 | }, $result)); 74 | 75 | $userId = $this->userProfile->userId; 76 | 77 | $userRatings = $this->db->execute("SELECT rating, patternId FROM userRatings WHERE patternId IN ($patternIds) AND userId=?", [ 78 | ['s', $userId] 79 | ]); 80 | 81 | $userFavorites = $this->db->execute("SELECT patternId FROM favorites WHERE patternId IN ($patternIds) AND userId=?", [ 82 | ['s', $userId] 83 | ]); 84 | 85 | function injectIntoResults($result, $sourceList, $sourceKey, $destKey) 86 | { 87 | for ($i = 0; $i < count($sourceList); $i++) { 88 | $sourceValue = $sourceList[$i]; 89 | for ($j = 0; $j < count($result); $j++) { 90 | if (idx($result[$j], 'id') === $sourceValue->patternId) { 91 | $result[$i]->{$destKey} = idx($result[$j], $sourceKey, true); 92 | break; 93 | } 94 | } 95 | } 96 | } 97 | 98 | injectIntoResults($result, $userRatings, 'rating', 'userRating'); 99 | injectIntoResults($result, $userFavorites, null, 'favorite'); 100 | 101 | $json = createPatternSet($result); 102 | 103 | return $json; 104 | } 105 | 106 | function getFlavorValues() { 107 | return $this->db->getEnumValues('patterns', 'flavor'); 108 | } 109 | 110 | public function getSchema() { 111 | $flavorValues = $this->getFlavorValues(); 112 | return array( 113 | "query" => array("type" => self::STRING, "required" => true), 114 | "startIndex" => array("type" => self::NUMBER, "required" => false, "default" => 0), 115 | "limit" => array("type" => self::NUMBER, "required" => false, "default" => 100), 116 | "flavor" => array("type" => self::ENUM_ARRAY, "values" => $flavorValues, "default" => $flavorValues, "required" => false) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /server/actions/patterns/setAccess.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace patterns; 21 | 22 | class setAccess extends \core\AbstractAction { 23 | 24 | public $description = 'Change the access of a pattern.'; 25 | 26 | public function execute() { 27 | $urlId = $this->getValue("patternId"); 28 | $patternId = convertFromURL($urlId); 29 | $visibility = $this->getValue("access"); 30 | 31 | $userProfile = $this->getUserProfile(); 32 | 33 | $exists = $this->db->exists("patterns", ['id' => $patternId, 'owner' => $userProfile->userId]); 34 | if ($exists == true) { 35 | $result = $this->db->execute("UPDATE patterns SET visibility=? WHERE id=?", [ 36 | ["s", $visibility], 37 | ["s", $patternId] 38 | ], true); 39 | } else { 40 | throw new \core\APIError(\core\ErrorCodes::API_PATTERN_NOT_FOUND); 41 | } 42 | 43 | // Clear the cache for this pattern. 44 | $patternCacheKey = \core\Cache::PatternKey($patternId); 45 | \core\Cache::DeleteItem($patternCacheKey); 46 | 47 | return new \core\Result(['id'=>$urlId, 'access'=>$visibility]); 48 | } 49 | 50 | public function getSchema() { 51 | return array( 52 | "patternId" => array("type"=>self::STRING, "required"=>true), 53 | "access" => array("type"=>self::ENUM, "values"=>['private', 'protected'], "required"=>true) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/api.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | require_once("bootstrap.php"); 21 | 22 | // Everything comes back as JSON 23 | header('Content-Type: application/json'); 24 | header('Access-Control-Allow-Methods: ' . (DEBUG?'POST, GET':'POST')); 25 | 26 | if (DEBUG) { 27 | header("Access-Control-Allow-Origin: *"); 28 | } 29 | 30 | // Grab the correct values. 31 | // OAUTH will use $_GET, the internal RegExr apis will use $_POST; 32 | $values = $_REQUEST; 33 | 34 | // Run the application! 35 | (new \core\API())->execute($values); 36 | -------------------------------------------------------------------------------- /server/apiDocs.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | require_once('bootstrap.php'); 21 | 22 | class apiDocs { 23 | 24 | public $description = 'Returns all the available APIs.'; 25 | 26 | function __construct() { 27 | $actions = $this->loadActionList("actions/"); 28 | 29 | header('Content-Type: application/json'); 30 | echo(json_encode($actions, JSON_PRETTY_PRINT)); 31 | } 32 | 33 | function loadActionList($base) { 34 | $actions = []; 35 | 36 | $db = new \core\DB(); 37 | $db->connect(DB_HOST, DB_USER_NAME, DB_PASSWORD, DB_NAME); 38 | 39 | $Directory = new RecursiveDirectoryIterator($base); 40 | $Iterator = new RecursiveIteratorIterator($Directory); 41 | $Regex = new RegexIterator($Iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); 42 | 43 | foreach ($Regex as $result) { 44 | $file = __DIR__ . "/actions/" . substr($result[0], strlen('actions/')); 45 | 46 | require_once($file); 47 | 48 | $file = str_replace(__DIR__.'/actions/', "", $file); 49 | 50 | $info = (object)pathinfo($file); 51 | $name = $info->filename; 52 | $dir = preg_replace('/[0-9\.]/i', '', $info->dirname); 53 | $className = "$dir\\$name"; 54 | $class = new ReflectionClass($className); 55 | $constants = $class->getConstants(); 56 | if (!$class->isInstantiable()) { 57 | continue; 58 | } 59 | 60 | $isAbstractAction = false; 61 | while ($parent = $class->getParentClass()) { 62 | if ($parent && $parent->getName() == 'core\AbstractAction') { 63 | $isAbstractAction = true; 64 | break; 65 | } 66 | $class = $parent; 67 | } 68 | 69 | if ($isAbstractAction) { 70 | $c = new $className(); 71 | $c->setDB($db); 72 | 73 | $schema = $c->getSchema(); 74 | $formattedSchema = []; 75 | 76 | foreach ($schema as $name => $value) { 77 | $value['type'] = $this->getConstantName($constants, $value['type']); 78 | $formattedSchema[$name] = $value; 79 | } 80 | 81 | $actions[] = [ 82 | 'name' => ltrim(str_replace("\\", "/", $className), '/'), 83 | 'description' => $c->description, 84 | 'parameters' => $formattedSchema 85 | ]; 86 | } 87 | } 88 | 89 | usort($actions, function ($a, $b) { 90 | return strcmp($a['name'], $b['name']); 91 | }); 92 | 93 | return $actions; 94 | } 95 | 96 | function getConstantName($constants, $type) { 97 | foreach ($constants as $name => $value) { 98 | if ($value == $type) { 99 | return $name; 100 | } 101 | } 102 | } 103 | } 104 | 105 | new apiDocs(); 106 | -------------------------------------------------------------------------------- /server/bootstrap.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * Setup the bare minimum required to run the app. 22 | */ 23 | 24 | $cwd = realpath(dirname(__FILE__)); 25 | 26 | if (!file_exists("$cwd/Config.php")) { 27 | echo('You need to create a Config.php file. Copy Config.sample.php, and edit.'); 28 | die; 29 | } 30 | 31 | require_once("$cwd/Config.php"); 32 | require_once("$cwd/utils.php"); 33 | 34 | // For composer 35 | if (isCLI()) { 36 | $cwd = dirname(realpath($argv[0])); 37 | } 38 | 39 | ini_set('session.gc_maxlifetime', 3600*24); 40 | session_set_cookie_params(3600*24*60); 41 | 42 | define('__DIR__', $cwd . '/vendor'); 43 | require "$cwd/vendor/autoload.php"; 44 | 45 | register_shutdown_function("shutdown_handler"); 46 | 47 | function shutdown_handler() { 48 | $error = error_get_last(); 49 | 50 | if (!is_null($error)) { 51 | $result = ob_get_contents(); 52 | 53 | if (!is_null($result)) { 54 | // See if the result is JSON, if not just display the text (probably was a nested echo, print_r or var_dump) 55 | $json = json_decode($result); 56 | if (is_null(json_last_error())) { 57 | $json['unhandled_error'] = $error; 58 | } else { 59 | ob_flush(); 60 | die; 61 | } 62 | } 63 | 64 | ob_clean(); 65 | echo(json_encode($json)); 66 | } 67 | 68 | if(ob_get_level() > 0) { 69 | ob_flush(); 70 | } 71 | } 72 | 73 | spl_autoload_extensions('.php'); 74 | spl_autoload_register(function ($class) { 75 | $className = str_replace('\\', '/', $class) . '.php'; 76 | $fileName = __DIR__ . '/' . $className; 77 | $actionsFilename = __DIR__ . '/actions/' . $className; 78 | 79 | if (file_exists($actionsFilename)) { 80 | include $actionsFilename; 81 | } else if (file_exists($fileName)) { 82 | include $fileName; 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /server/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "respect/validation": "1.1.12", 4 | "hybridauth/hybridauth": "v3.2.0" 5 | }, 6 | "require-dev": { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/core/API.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | use interfaces\IAuthenticated; 23 | 24 | class API { 25 | 26 | protected $db; 27 | protected $resultShouldReturn; 28 | 29 | public function execute($values, $returnResult = false) { 30 | date_default_timezone_set('UTC'); 31 | 32 | ob_start('ob_gzhandler'); 33 | $this->resultShouldReturn = $returnResult; 34 | $result = null; 35 | 36 | $action = idx($values, 'action'); 37 | if (empty($action)) { 38 | $this->result(new \core\APIError(\core\ErrorCodes::NO_ACTION)); 39 | } else { 40 | safeRequireOnce("actions/$action.php", false); 41 | 42 | $action = formatActionNameForExecution($action); 43 | 44 | if (!is_null($action)) { 45 | // Any call here could throw. 46 | try { 47 | $action = new $action($values); 48 | if ($action->requiresDatabase()) { 49 | $this->connect(); 50 | $action->setDB($this->db); 51 | } 52 | $result = $action->validate()->execute(); 53 | } catch (\core\APIError $err) { // Custom error 54 | $result = $err; 55 | } catch (\Exception $err) { // A unknown PHP error happened 56 | $message = array('error' => $err); 57 | 58 | if (DEBUG) { 59 | $message['stack'] = debug_backtrace(true); 60 | } 61 | 62 | if ($this->db->inTransaction()) { 63 | $this->db->rollback(); 64 | } 65 | 66 | $result = new \core\APIError(\core\ErrorCodes::UNKNOWN, $message); 67 | } 68 | 69 | // Assume success if the action returns no result. (Ex; a DB write); 70 | if (is_null($result)) { 71 | $result = new \core\Result(); 72 | } 73 | 74 | $totalTime = number_format((\microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"])*1000, 2, '.', ''); 75 | 76 | return $this->result($result, $totalTime.'ms'); 77 | } else { 78 | return $this->result(new \core\APIError(\core\ErrorCodes::NO_ACTION, "Action $action does not exist.")); 79 | } 80 | } 81 | } 82 | 83 | function getDB() { 84 | return $this->db; 85 | } 86 | 87 | function connect() { 88 | $this->db = new \core\DB(); 89 | $this->db->connect(DB_HOST, DB_USER_NAME, DB_PASSWORD, DB_NAME, DB_PORT, DB_SOCK); 90 | } 91 | 92 | function result($data, $time=null) { 93 | $success = $data instanceof \core\Result; 94 | $resultData = array('success' => $success); 95 | $returnData = $data->getData(); 96 | $metadata = $data->getMetadata(); 97 | 98 | if ($success == false) { 99 | // http_response_code(500); 100 | } 101 | 102 | if (!is_null($returnData)) { 103 | $resultData['data'] = $returnData; 104 | } 105 | 106 | if (!is_null($time)) { 107 | if (is_null($metadata)) { 108 | $metadata = array(); 109 | } 110 | $metadata['script-time'] = $time; 111 | } 112 | 113 | if (!is_null($metadata)) { 114 | $resultData['metadata'] = $metadata; 115 | } 116 | 117 | echo(json_encode($resultData)); 118 | 119 | if ($this->resultShouldReturn) { 120 | return ob_get_contents(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /server/core/APIError.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | class APIError extends \Exception { 23 | 24 | public $error; 25 | public $code; 26 | public $message; 27 | 28 | public function __construct($code, $error = null, $message = null) { 29 | $this->code = $code; 30 | $this->error = $error; 31 | $this->message = $message; 32 | } 33 | 34 | public function getData() { 35 | return array('code' => $this->code, 'data' => $this->error, 'message' => ErrorCodes::getMessage($this->code).(is_null($this->message)?"":" ".$this->message)); 36 | } 37 | 38 | public function getMetadata() { 39 | return array(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/core/Authentication.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | class Authentication { 23 | 24 | public function connect($type) { 25 | $config = [ 26 | 'callback' => $this->createCallbackURL($type), 27 | 'providers' => [ 28 | 'GitHub' => [ 29 | 'enabled' => true, 30 | 'keys' => [ 31 | 'id' => GITHUB_ID, 32 | 'secret' => GITHUB_SECRET 33 | ] 34 | ], 35 | 'Google' => [ 36 | 'enabled' => true, 37 | 'keys' => [ 38 | 'id' => GOOGLE_ID, 39 | 'secret' => GOOGLE_SECRET 40 | ] 41 | ], 42 | 'Facebook' => [ 43 | 'enabled' => true, 44 | 'keys' => [ 45 | 'id' => FACEBOOK_ID, 46 | 'secret' => FACEBOOK_SECRET 47 | ], 48 | 'scope' => 'email' 49 | ] 50 | ] 51 | ]; 52 | 53 | // Normalize names, to match with Hybridauth's setup. 54 | switch ($type) { 55 | case "github": 56 | $type = "GitHub"; 57 | break; 58 | case "google": 59 | $type = "Google"; 60 | break; 61 | case "facebook": 62 | $type = "Facebook"; 63 | break; 64 | } 65 | 66 | if (function_exists("curl_init")) { 67 | $auth = new \Hybridauth\Hybridauth($config); 68 | return $auth->authenticate($type); 69 | } else { 70 | throw new \core\APIError(\core\ErrorCodes::API_MISSING_REQUIREMENTS, "curl is required for login."); 71 | } 72 | 73 | return null; 74 | } 75 | 76 | function getBaseDomain() { 77 | return 'https://'.$_SERVER['SERVER_NAME']; 78 | } 79 | 80 | function createCallbackURL($type) { 81 | $domain = $this->getBaseDomain(); 82 | $baseUrl = "$domain/server/api.php?action=oauthCallback"; 83 | return $baseUrl . "&type=" . strtolower($type); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/core/Cache.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | class Cache { 23 | 24 | public static function CommunityKey($query, $startIndex, $limit) { 25 | return sprintf("Community_%s_%s_%s", $query, $startIndex, $limit); 26 | } 27 | 28 | public static function PatternKey($key) { 29 | return sprintf("Pattern_%s", $key); 30 | } 31 | 32 | public static function SaveItem($key, $data, $overwrite = false) { 33 | $path = self::Path($key); 34 | $exists = file_exists($path); 35 | 36 | if ($exists == false || $overwrite == true || $overwrite == true && $exists == true) { 37 | file_put_contents($path, gzencode(json_encode($data))); 38 | } 39 | } 40 | 41 | public static function LoadItem($key) { 42 | $file = self::Path($key); 43 | 44 | if (!file_exists($file)) { 45 | return null; 46 | } 47 | 48 | touch($file); 49 | return json_decode(gzdecode(file_get_contents($file))); 50 | } 51 | 52 | public static function DeleteItem($key) { 53 | $file = self::Path($key); 54 | 55 | if (file_exists($file)) { 56 | unlink($file); 57 | } 58 | } 59 | 60 | public static function Clean() { 61 | $files = scandir(CACHE_PATH); 62 | $actions = array(); 63 | 64 | foreach ($files as $file) { 65 | $path = CACHE_PATH . $file; 66 | 67 | if (is_dir($path)) { 68 | continue; 69 | } 70 | 71 | $now = time(); 72 | 73 | // Delete the file after 7 days of no activity. 74 | $dirtyTime = 60*60*24*7; 75 | if (file_exists($path) && filemtime($path) + $dirtyTime < $now) { 76 | unlink($path); 77 | } 78 | } 79 | } 80 | 81 | private static function Path($key) { 82 | return CACHE_PATH . md5($key); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /server/core/ErrorCodes.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | class ErrorCodes { 23 | 24 | // Generic (1000 - 1099) 25 | const UNKNOWN = 1000; 26 | const NO_ACTION = 1001; 27 | 28 | // Mysql (1100 - 1199) 29 | const MYSQL_CONNECTION_ERR = 1100; 30 | const MYSQL_QUERY_ERR = 1101; 31 | 32 | // Generic API Errors (1200 - 1299) 33 | const API_ERROR = 1200; 34 | const API_DOESNT_EXIST = 1201; 35 | const API_MISSING_VALUES = 1202; 36 | const API_NO_DATA = 1203; 37 | const API_INVALID_JSON = 1204; 38 | const API_INVALID_NUMBER = 1205; 39 | const API_INVALID_EMAIL = 1206; 40 | const API_NO_SESSION = 1207; 41 | const API_NOT_ALLOWED = 1208; 42 | const API_INVALID_GUID = 1209; 43 | const API_INVALID_DATE = 1210; 44 | const API_INVALID_COLOR = 1211; 45 | const API_INVALID_BOOL = 1212; 46 | const API_VALUE_DOES_NOT_EXIST = 1213; 47 | const API_TIME_OVERLAPS = 1214; 48 | const API_INCORRECT_VALUE = 1215; 49 | const API_INVALID_ENUM = 1216; 50 | const API_INVALID_STRING = 1217; 51 | const API_PCRE_ERROR = 1218; 52 | const API_LOGIN_ERROR = 1219; 53 | const API_PATTERN_NOT_FOUND = 1221; 54 | const API_MISSING_REQUIREMENTS = 1222; 55 | 56 | public static function getMessage($code) { 57 | switch ($code) { 58 | case self::NO_ACTION: 59 | return 'No action was specified'; 60 | case self::MYSQL_CONNECTION_ERR: 61 | return 'Error connecting to MySQL'; 62 | case self::MYSQL_QUERY_ERR: 63 | return 'MySQL query error'; 64 | case self::API_ERROR: 65 | return 'An action reported an error.'; 66 | case self::API_DOESNT_EXIST: 67 | return "Value doesn't exist"; 68 | case self::API_MISSING_VALUES: 69 | return 'Required values are missing.'; 70 | case self::API_NO_DATA: 71 | return 'No data was found.'; 72 | case self::API_INVALID_JSON: 73 | return 'JSON was not valid'; 74 | case self::API_INVALID_NUMBER: 75 | return 'Type is not a valid number.'; 76 | case self::API_INVALID_EMAIL: 77 | return 'Not a valid Email.'; 78 | case self::API_NO_SESSION: 79 | return 'User is not logged in.'; 80 | case self::API_NOT_ALLOWED: 81 | return 'Access to this action was denied.'; 82 | case self::API_INVALID_GUID: 83 | return 'Invalid GUID'; 84 | case self::API_INVALID_DATE: 85 | return 'Invalid date'; 86 | case self::API_INVALID_COLOR: 87 | return 'A valid hex color is required.'; 88 | case self::API_INVALID_BOOL: 89 | return 'A valid bool is required (0, "false", 1 or "true" are valid)'; 90 | case self::API_INCORRECT_VALUE: 91 | return 'A value passed was not correct.'; 92 | case self::API_INVALID_ENUM: 93 | return 'A enum value was incorrect.'; 94 | case self::API_PCRE_ERROR: 95 | return "pcre execution error."; 96 | case self::API_LOGIN_ERROR: 97 | return "Login failed"; 98 | case self::API_PATTERN_NOT_FOUND: 99 | return "No pattern found."; 100 | case self::API_MISSING_REQUIREMENTS: 101 | return "Server doesn't fullfil all the requirements."; 102 | case self::UNKNOWN: 103 | default: 104 | return 'An unknown error occurred'; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /server/core/NotImplemented.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | class NotImplemented extends ErrorException { 23 | 24 | public function getData() { 25 | return 'action NotImplemented'; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /server/core/PatternVisibility.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * List of available visibility states. 22 | * This file needs to match the enum value on patterns tables visibility column. 23 | */ 24 | 25 | namespace core; 26 | 27 | class PatternVisibility { 28 | 29 | const PUBLIC = "public"; 30 | const PRIVATE = "private"; 31 | const PROTECTED = "protected"; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /server/core/Result.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | class Result { 23 | 24 | private $data; 25 | private $metadata; 26 | 27 | function __construct($data = null, $metadata = null) { 28 | $this->data = $data; 29 | $this->metadata = $metadata; 30 | } 31 | 32 | public function getData() { 33 | return $this->data; 34 | } 35 | 36 | public function getMetadata() { 37 | return $this->metadata; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/core/Session.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace core; 21 | 22 | class Session { 23 | 24 | protected $db; 25 | 26 | public function __construct($db, $name) { 27 | $this->db = $db; 28 | 29 | session_set_save_handler( 30 | array($this, "open"), 31 | array($this, "close"), 32 | array($this, "read"), 33 | array($this, "write"), 34 | array($this, "destroy"), 35 | array($this, "gc") 36 | ); 37 | 38 | session_name($name); 39 | 40 | if(!isset($_SESSION)) { 41 | session_start(); 42 | } 43 | } 44 | 45 | public function open() { 46 | return $this->db->isConnected(); 47 | } 48 | 49 | public function close() { 50 | return true; 51 | } 52 | 53 | public function read($id) { 54 | try { 55 | $result = $this->db->execute("SELECT data FROM sessions WHERE id = ?", ["s", $id], true); 56 | } catch (\Exception $ex) { 57 | return ''; 58 | } 59 | return !is_null($result)?$result->data:''; 60 | } 61 | 62 | public function write($id, $data) { 63 | $lastAccess = time(); 64 | 65 | try { 66 | $sql = "INSERT INTO sessions (id, access, data) 67 | VALUES (?, ?, ?) 68 | ON DUPLICATE KEY UPDATE `access`=?, `data`=? 69 | "; 70 | 71 | $result = $this->db->execute($sql, [ 72 | ["s", $id], 73 | ["s", $lastAccess], 74 | ["s", $data], 75 | ["s", $lastAccess], 76 | ["s", $data], 77 | ]); 78 | } catch(\Exception $ex) { 79 | return false; 80 | } 81 | return !is_null($result); 82 | } 83 | 84 | public function destroy($id) { 85 | try { 86 | $result = $this->db->execute("DELETE FROM sessions WHERE id = ?", ["s", $id]); 87 | } catch (\Exception $ex) { 88 | return false; 89 | } 90 | return !is_null($result); 91 | } 92 | 93 | public function gc($max) { 94 | $old = time() - $max; 95 | 96 | try { 97 | $result = $this->db->execute("DELETE FROM sessions WHERE access < ?", ["s", $old]); 98 | } catch (\Exception $ex) { 99 | return false; 100 | } 101 | return !is_null($result); 102 | } 103 | } 104 | --------------------------------------------------------------------------------