2015-04-12 20:56:49 +00:00
|
|
|
// The Regexper class manages the top-level behavior for the entire
|
|
|
|
// application. This includes event handlers for all user interactions.
|
|
|
|
|
2014-12-20 15:25:32 +00:00
|
|
|
import util from './util.js';
|
2014-12-18 16:13:15 +00:00
|
|
|
import Parser from './parser/javascript.js';
|
2014-12-22 21:57:30 +00:00
|
|
|
import _ from 'lodash';
|
2014-12-04 23:37:35 +00:00
|
|
|
|
|
|
|
export default class Regexper {
|
|
|
|
constructor(root) {
|
|
|
|
this.root = root;
|
2015-06-17 00:19:04 +00:00
|
|
|
this.buggyHash = false;
|
2014-12-04 23:37:35 +00:00
|
|
|
this.form = root.querySelector('#regexp-form');
|
|
|
|
this.field = root.querySelector('#regexp-input');
|
|
|
|
this.error = root.querySelector('#error');
|
2014-12-22 21:57:30 +00:00
|
|
|
this.warnings = root.querySelector('#warnings');
|
2014-12-30 21:20:37 +00:00
|
|
|
|
|
|
|
this.links = this.form.querySelector('ul');
|
2016-03-09 02:44:49 +00:00
|
|
|
this.permalink = this.links.querySelector('a[data-action="permalink"]');
|
2018-02-10 18:05:42 +00:00
|
|
|
this.downloadSvg = this.links.querySelector('a[data-action="download-svg"]');
|
|
|
|
this.downloadPng = this.links.querySelector('a[data-action="download-png"]');
|
2014-12-30 21:20:37 +00:00
|
|
|
|
2014-12-25 04:01:32 +00:00
|
|
|
this.svgContainer = root.querySelector('#regexp-render');
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Event handler for key presses in the regular expression form field.
|
2014-12-04 23:37:35 +00:00
|
|
|
keypressListener(event) {
|
2015-04-12 20:56:49 +00:00
|
|
|
// Pressing Shift-Enter displays the expression.
|
2014-12-04 23:37:35 +00:00
|
|
|
if (event.shiftKey && event.keyCode === 13) {
|
2014-12-15 23:06:16 +00:00
|
|
|
event.returnValue = false;
|
|
|
|
if (event.preventDefault) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
|
2014-12-20 15:25:32 +00:00
|
|
|
this.form.dispatchEvent(util.customEvent('submit'));
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Event handler for key presses while focused anywhere in the application.
|
2014-12-18 00:24:33 +00:00
|
|
|
documentKeypressListener(event) {
|
2015-04-12 20:56:49 +00:00
|
|
|
// Pressing escape will cancel a currently running render.
|
2014-12-30 02:31:36 +00:00
|
|
|
if (event.keyCode === 27 && this.running) {
|
|
|
|
this.running.cancel();
|
2014-12-18 00:24:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Event handler for submission of the regular expression. Changes the URL
|
|
|
|
// hash which leads to the expression being rendered.
|
2014-12-04 23:37:35 +00:00
|
|
|
submitListener(event) {
|
2014-12-16 21:08:36 +00:00
|
|
|
event.returnValue = false;
|
|
|
|
if (event.preventDefault) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
2014-12-04 23:37:35 +00:00
|
|
|
|
2014-12-15 23:06:16 +00:00
|
|
|
try {
|
2014-12-16 21:08:36 +00:00
|
|
|
this._setHash(this.field.value);
|
2014-12-15 23:06:16 +00:00
|
|
|
}
|
|
|
|
catch(e) {
|
2015-04-12 20:56:49 +00:00
|
|
|
// Failed to set the URL hash (probably because the expression is too
|
|
|
|
// long). Turn off display of the permalink and just show the expression.
|
2014-12-16 21:08:36 +00:00
|
|
|
this.permalinkEnabled = false;
|
2014-12-15 23:06:16 +00:00
|
|
|
this.showExpression(this.field.value);
|
|
|
|
}
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Event handler for URL hash changes. Starts rendering of the expression.
|
2014-12-04 23:37:35 +00:00
|
|
|
hashchangeListener() {
|
2016-07-27 15:00:17 +00:00
|
|
|
let expr = this._getHash();
|
2015-01-19 15:15:34 +00:00
|
|
|
|
|
|
|
if (expr instanceof Error) {
|
|
|
|
this.state = 'has-error';
|
|
|
|
this.error.innerHTML = 'Malformed expression in URL';
|
2016-05-24 01:10:50 +00:00
|
|
|
util.track('send', 'event', 'visualization', 'malformed URL');
|
2015-01-19 15:15:34 +00:00
|
|
|
} else {
|
|
|
|
this.permalinkEnabled = true;
|
|
|
|
this.showExpression(expr);
|
|
|
|
}
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Binds all event listeners.
|
2014-12-04 23:37:35 +00:00
|
|
|
bindListeners() {
|
|
|
|
this.field.addEventListener('keypress', this.keypressListener.bind(this));
|
|
|
|
this.form.addEventListener('submit', this.submitListener.bind(this));
|
2014-12-18 00:24:33 +00:00
|
|
|
this.root.addEventListener('keyup', this.documentKeypressListener.bind(this));
|
2014-12-16 18:34:02 +00:00
|
|
|
window.addEventListener('hashchange', this.hashchangeListener.bind(this));
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
|
2015-06-17 00:19:04 +00:00
|
|
|
// Detect if https://bugzilla.mozilla.org/show_bug.cgi?id=483304 is in effect
|
|
|
|
detectBuggyHash() {
|
2015-06-23 02:05:03 +00:00
|
|
|
if (typeof window.URL === 'function') {
|
|
|
|
try {
|
2016-07-27 15:00:17 +00:00
|
|
|
let url = new URL('http://regexper.com/#%25');
|
2015-06-23 02:05:03 +00:00
|
|
|
this.buggyHash = (url.hash === '#%');
|
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
this.buggyHash = false;
|
|
|
|
}
|
2015-06-17 00:19:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Set the URL hash. This method exists to facilitate automated testing
|
|
|
|
// (since changing the URL can throw off most JavaScript testing tools).
|
2014-12-16 21:08:36 +00:00
|
|
|
_setHash(hash) {
|
2018-02-10 16:37:00 +00:00
|
|
|
location.hash = encodeURIComponent(hash)
|
|
|
|
.replace(/\(/g, '%28')
|
|
|
|
.replace(/\)/g, '%29');
|
2014-12-16 21:08:36 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Retrieve the current URL hash. This method is also mostly for supporting
|
|
|
|
// automated testing, but also does some basic error handling for malformed
|
|
|
|
// URLs.
|
2014-12-16 21:08:36 +00:00
|
|
|
_getHash() {
|
2015-01-19 15:15:34 +00:00
|
|
|
try {
|
2016-07-27 15:00:17 +00:00
|
|
|
let hash = location.hash.slice(1)
|
2015-06-17 00:19:04 +00:00
|
|
|
return this.buggyHash ? hash : decodeURIComponent(hash);
|
2015-01-19 15:15:34 +00:00
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
return e;
|
|
|
|
}
|
2014-12-16 21:08:36 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Currently state of the application. Useful values are:
|
|
|
|
// - `''` - State of the application when the page initially loads
|
|
|
|
// - `'is-loading'` - Displays the loading indicator
|
|
|
|
// - `'has-error'` - Displays the error message
|
|
|
|
// - `'has-results'` - Displays rendered results
|
2014-12-16 18:34:02 +00:00
|
|
|
set state(state) {
|
2014-12-15 23:06:16 +00:00
|
|
|
this.root.className = state;
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
|
2014-12-16 21:08:36 +00:00
|
|
|
get state() {
|
|
|
|
return this.root.className;
|
|
|
|
}
|
2014-12-08 02:40:05 +00:00
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Start the rendering of a regular expression.
|
|
|
|
//
|
|
|
|
// - __expression__ - Regular expression to display.
|
2014-12-16 21:08:36 +00:00
|
|
|
showExpression(expression) {
|
|
|
|
this.field.value = expression;
|
|
|
|
this.state = '';
|
|
|
|
|
|
|
|
if (expression !== '') {
|
2015-03-14 21:27:59 +00:00
|
|
|
this.renderRegexp(expression).catch(util.exposeError);
|
2014-12-16 21:08:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Creates a blob URL for linking to a rendered regular expression image.
|
|
|
|
//
|
|
|
|
// - __content__ - SVG image markup.
|
2014-12-16 21:08:36 +00:00
|
|
|
buildBlobURL(content) {
|
2015-04-12 20:56:49 +00:00
|
|
|
// Blob object has to stick around for IE, so the instance is stored on the
|
|
|
|
// `window` object.
|
|
|
|
window.blob = new Blob([content], { type: 'image/svg+xml' });
|
|
|
|
return URL.createObjectURL(window.blob);
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Update the URLs of the 'download' and 'permalink' links.
|
2014-12-04 23:37:35 +00:00
|
|
|
updateLinks() {
|
2018-02-10 18:05:42 +00:00
|
|
|
let classes = _.without(this.links.className.split(' '), ['hide-download-svg', 'hide-permalink']);
|
|
|
|
let svg = this.svgContainer.querySelector('.svg');
|
2014-12-30 21:20:37 +00:00
|
|
|
|
2018-02-10 18:05:42 +00:00
|
|
|
// Create the SVG 'download' image URL.
|
2014-12-15 23:06:16 +00:00
|
|
|
try {
|
2018-02-10 18:05:42 +00:00
|
|
|
this.downloadSvg.parentNode.style.display = null;
|
|
|
|
this.downloadSvg.href = this.buildBlobURL(svg.innerHTML);
|
2014-12-15 23:06:16 +00:00
|
|
|
}
|
|
|
|
catch(e) {
|
2015-04-12 20:56:49 +00:00
|
|
|
// Blobs or URLs created from a blob URL don't work in the current
|
|
|
|
// browser. Giving up on the download link.
|
2018-02-10 18:05:42 +00:00
|
|
|
classes.push('hide-download-svg');
|
2014-12-15 23:06:16 +00:00
|
|
|
}
|
|
|
|
|
2018-02-10 18:05:42 +00:00
|
|
|
//Create the PNG 'download' image URL.
|
|
|
|
try {
|
|
|
|
let canvas = document.createElement('canvas');
|
|
|
|
let context = canvas.getContext('2d');
|
|
|
|
let loader = new Image;
|
|
|
|
|
|
|
|
loader.width = canvas.width = Number(svg.querySelector('svg').getAttribute('width'));
|
|
|
|
loader.height = canvas.height = Number(svg.querySelector('svg').getAttribute('height'));
|
|
|
|
loader.onload = () => {
|
|
|
|
try {
|
|
|
|
context.drawImage(loader, 0, 0, loader.width, loader.height);
|
|
|
|
canvas.toBlob(blob => {
|
|
|
|
window.pngBlob = blob;
|
|
|
|
this.downloadPng.href = URL.createObjectURL(window.pngBlob);
|
|
|
|
this.links.className = this.links.className.replace(/\bhide-download-png\b/, '');
|
|
|
|
}, 'image/png');
|
|
|
|
}
|
|
|
|
catch(e) {}
|
|
|
|
};
|
|
|
|
loader.src = 'data:image/svg+xml,' + encodeURIComponent(svg.innerHTML);
|
|
|
|
classes.push('hide-download-png');
|
|
|
|
}
|
|
|
|
catch(e) {}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Create the 'permalink' URL.
|
2014-12-16 21:08:36 +00:00
|
|
|
if (this.permalinkEnabled) {
|
2014-12-15 23:06:16 +00:00
|
|
|
this.permalink.parentNode.style.display = null;
|
2014-12-16 21:08:36 +00:00
|
|
|
this.permalink.href = location.toString();
|
|
|
|
} else {
|
2014-12-30 21:20:37 +00:00
|
|
|
classes.push('hide-permalink');
|
2014-12-15 23:06:16 +00:00
|
|
|
}
|
2014-12-30 21:20:37 +00:00
|
|
|
|
|
|
|
this.links.className = classes.join(' ');
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Display any warnings that were generated while rendering a regular expression.
|
|
|
|
//
|
|
|
|
// - __warnings__ - Array of warning messages to display.
|
2014-12-22 21:57:30 +00:00
|
|
|
displayWarnings(warnings) {
|
2016-07-27 02:41:34 +00:00
|
|
|
this.warnings.innerHTML = _.map(warnings, warning => (
|
|
|
|
`<li class="inline-icon">${util.icon("#warning")}${warning}</li>`
|
|
|
|
)).join('');
|
2014-12-22 21:57:30 +00:00
|
|
|
}
|
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// Render regular expression
|
|
|
|
//
|
|
|
|
// - __expression__ - Regular expression to render
|
2014-12-08 02:40:05 +00:00
|
|
|
renderRegexp(expression) {
|
2016-07-27 15:00:17 +00:00
|
|
|
let parseError = false,
|
2015-01-01 16:57:11 +00:00
|
|
|
startTime, endTime;
|
2014-12-29 23:37:21 +00:00
|
|
|
|
2015-04-12 20:56:49 +00:00
|
|
|
// When a render is already in progress, cancel it and try rendering again
|
|
|
|
// after a short delay (canceling a render is not instantaneous).
|
2014-12-30 02:31:36 +00:00
|
|
|
if (this.running) {
|
|
|
|
this.running.cancel();
|
2014-12-18 16:13:15 +00:00
|
|
|
|
2016-07-27 02:41:34 +00:00
|
|
|
return util.wait(10).then(() => this.renderRegexp(expression));
|
2014-12-18 16:13:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.state = 'is-loading';
|
2016-05-24 01:10:50 +00:00
|
|
|
util.track('send', 'event', 'visualization', 'start');
|
2015-01-01 16:57:11 +00:00
|
|
|
startTime = new Date().getTime();
|
2014-12-18 16:13:15 +00:00
|
|
|
|
2014-12-30 02:31:36 +00:00
|
|
|
this.running = new Parser(this.svgContainer);
|
2014-12-18 16:13:15 +00:00
|
|
|
|
2014-12-30 02:31:36 +00:00
|
|
|
return this.running
|
2015-04-12 20:56:49 +00:00
|
|
|
// Parse the expression.
|
2014-12-30 02:31:36 +00:00
|
|
|
.parse(expression)
|
2015-04-12 20:56:49 +00:00
|
|
|
// Display any error messages from the parser and abort the render.
|
2015-03-14 21:11:14 +00:00
|
|
|
.catch(message => {
|
2014-12-16 21:08:36 +00:00
|
|
|
this.state = 'has-error';
|
|
|
|
this.error.innerHTML = '';
|
|
|
|
this.error.appendChild(document.createTextNode(message));
|
|
|
|
|
2014-12-30 02:31:36 +00:00
|
|
|
parseError = true;
|
2014-12-18 00:54:58 +00:00
|
|
|
|
2014-12-16 21:08:36 +00:00
|
|
|
throw message;
|
|
|
|
})
|
2015-04-12 20:56:49 +00:00
|
|
|
// When parsing is successful, render the parsed expression.
|
2016-07-27 02:41:34 +00:00
|
|
|
.then(parser => parser.render())
|
2015-04-12 20:56:49 +00:00
|
|
|
// Once rendering is complete:
|
|
|
|
// - Update links
|
|
|
|
// - Display any warnings
|
|
|
|
// - Track the completion of the render and how long it took
|
2014-12-30 02:31:36 +00:00
|
|
|
.then(() => {
|
2015-01-01 16:56:36 +00:00
|
|
|
this.state = 'has-results';
|
|
|
|
this.updateLinks();
|
|
|
|
this.displayWarnings(this.running.warnings);
|
2016-05-24 01:10:50 +00:00
|
|
|
util.track('send', 'event', 'visualization', 'complete');
|
2015-01-01 16:57:11 +00:00
|
|
|
|
|
|
|
endTime = new Date().getTime();
|
2016-05-24 01:10:50 +00:00
|
|
|
util.track('send', 'timing', 'visualization', 'total time', endTime - startTime);
|
2014-12-30 02:31:36 +00:00
|
|
|
})
|
2015-04-12 20:56:49 +00:00
|
|
|
// Handle any errors that happened during the rendering pipeline.
|
|
|
|
// Swallows parse errors and render cancellations. Any other exceptions
|
|
|
|
// are allowed to continue on to be tracked by the global error handler.
|
2015-03-14 21:11:14 +00:00
|
|
|
.catch(message => {
|
2014-12-18 16:13:15 +00:00
|
|
|
if (message === 'Render cancelled') {
|
2016-05-24 01:10:50 +00:00
|
|
|
util.track('send', 'event', 'visualization', 'cancelled');
|
2014-12-18 16:13:15 +00:00
|
|
|
this.state = '';
|
2014-12-30 02:31:36 +00:00
|
|
|
} else if (parseError) {
|
2016-05-24 01:10:50 +00:00
|
|
|
util.track('send', 'event', 'visualization', 'parse error');
|
2014-12-18 16:13:15 +00:00
|
|
|
} else {
|
|
|
|
throw message;
|
|
|
|
}
|
|
|
|
})
|
2015-04-12 20:56:49 +00:00
|
|
|
// Finally, mark rendering as complete (and pass along any exceptions
|
|
|
|
// that were thrown).
|
2015-03-14 21:11:14 +00:00
|
|
|
.then(
|
|
|
|
() => {
|
|
|
|
this.running = false;
|
|
|
|
},
|
|
|
|
message => {
|
|
|
|
this.running = false;
|
|
|
|
throw message;
|
|
|
|
}
|
|
|
|
);
|
2014-12-04 23:37:35 +00:00
|
|
|
}
|
|
|
|
}
|