diff --git a/spec/parser/javascript_spec.js b/spec/parser/javascript_spec.js
deleted file mode 100644
index b0620f4..0000000
--- a/spec/parser/javascript_spec.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import parser from 'src/js/parser/javascript.js';
-
-describe('parser/javascript.peg', function() {
-
- describe('regular expression literals', function() {
-
- describe('flags support', function() {
-
- it('handles the ignore case flag', function() {
- expect(parser.parse('/test/i').flags().ignore_case).toEqual(true);
- expect(parser.parse('/test/').flags().ignore_case).toEqual(false);
- });
-
- it('handles the global flag', function() {
- expect(parser.parse('/test/g').flags().global).toEqual(true);
- expect(parser.parse('/test/').flags().global).toEqual(false);
- });
-
- it('handles the multiline flag', function() {
- expect(parser.parse('/test/m').flags().multiline).toEqual(true);
- expect(parser.parse('/test/').flags().multiline).toEqual(false);
- });
-
- });
-
- });
-
-});
diff --git a/spec/regexper_spec.js b/spec/regexper_spec.js
new file mode 100644
index 0000000..f6a2cb5
--- /dev/null
+++ b/spec/regexper_spec.js
@@ -0,0 +1,333 @@
+import Regexper from 'src/js/regexper.js';
+import Q from 'q';
+
+describe('regexper.js', function() {
+
+ beforeEach(function() {
+ this.root = document.createElement('div');
+ this.root.innerHTML = [
+ '
',
+ '',
+ '',
+ '',
+ '',
+ ''
+ ].join('');
+
+ this.regexper = new Regexper(this.root);
+ spyOn(this.regexper, '_setHash');
+ spyOn(this.regexper, '_getHash').and.returnValue('example hash value');
+ });
+
+ describe('#keypressListener', function() {
+
+ beforeEach(function() {
+ this.event = document.createEvent('Event');
+ spyOn(this.event, 'preventDefault');
+ spyOn(this.regexper.form, 'dispatchEvent');
+ });
+
+ describe('when the shift key is not depressed', function() {
+
+ beforeEach(function() {
+ this.event.shiftKey = false;
+ this.event.keyCode = 13;
+ });
+
+ it('does not prevent the default action', function() {
+ this.regexper.keypressListener(this.event);
+ expect(this.event.returnValue).not.toEqual(false);
+ expect(this.event.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger a submit event', function() {
+ this.regexper.keypressListener(this.event);
+ expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled();
+ });
+
+ });
+
+ describe('when the keyCode is not 13 (Enter)', function() {
+
+ beforeEach(function() {
+ this.event.shiftKey = true;
+ this.event.keyCode = 42;
+ });
+
+ it('does not prevent the default action', function() {
+ this.regexper.keypressListener(this.event);
+ expect(this.event.returnValue).not.toEqual(false);
+ expect(this.event.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger a submit event', function() {
+ this.regexper.keypressListener(this.event);
+ expect(this.regexper.form.dispatchEvent).not.toHaveBeenCalled();
+ });
+
+ });
+
+ describe('when the shift key is depressed and the keyCode is 13 (Enter)', function() {
+
+ beforeEach(function() {
+ this.event.shiftKey = true;
+ this.event.keyCode = 13;
+ });
+
+ it('prevents the default action', function() {
+ this.regexper.keypressListener(this.event);
+ expect(this.event.returnValue).not.toEqual(true);
+ expect(this.event.preventDefault).toHaveBeenCalled();
+ });
+
+ it('triggers a submit event', function() {
+ var event;
+
+ this.regexper.keypressListener(this.event);
+ expect(this.regexper.form.dispatchEvent).toHaveBeenCalled();
+
+ event = this.regexper.form.dispatchEvent.calls.mostRecent().args[0];
+ expect(event.type).toEqual('submit');
+ });
+
+ });
+
+ });
+
+ describe('#submitListener', function() {
+
+ beforeEach(function() {
+ this.event = document.createEvent('Event');
+ spyOn(this.event, 'preventDefault');
+
+ this.regexper.field.value = 'example value';
+ });
+
+ it('prevents the default action', function() {
+ this.regexper.submitListener(this.event);
+ expect(this.event.returnValue).not.toEqual(true);
+ expect(this.event.preventDefault).toHaveBeenCalled();
+ });
+
+ it('sets the location.hash', function() {
+ this.regexper.submitListener(this.event);
+ expect(this.regexper._setHash).toHaveBeenCalledWith('example value');
+ });
+
+ describe('when setting location.hash fails', function() {
+
+ beforeEach(function() {
+ this.regexper._setHash.and.throwError('hash failure');
+ });
+
+ it('disables the permalink', function() {
+ this.regexper.submitListener(this.event);
+ expect(this.regexper.permalinkEnabled).toEqual(false);
+ });
+
+ it('shows the expression directly', function() {
+ spyOn(this.regexper, 'showExpression');
+ this.regexper.submitListener(this.event);
+ expect(this.regexper.showExpression).toHaveBeenCalledWith('example value');
+ });
+
+ });
+
+ });
+
+ describe('#hashchangeListener', function() {
+
+ it('enables the permalink', function() {
+ this.regexper.hashchangeListener();
+ expect(this.regexper.permalinkEnabled).toEqual(true);
+ });
+
+ it('shows the expression from the hash', function() {
+ spyOn(this.regexper, 'showExpression');
+ this.regexper.hashchangeListener();
+ expect(this.regexper.showExpression).toHaveBeenCalledWith('example hash value');
+ });
+
+ });
+
+ describe('#updatePercentage', function() {
+
+ beforeEach(function() {
+ this.event = document.createEvent('Event');
+ this.event.detail = { percentage: 0.42 };
+ });
+
+ it('sets the width of the progress bar', function() {
+ this.regexper.updatePercentage(this.event);
+ expect(this.regexper.percentage.style.width).toEqual('42%');
+ });
+
+ });
+
+ describe('#bindListeners', function() {
+
+ it('binds #keypressListener to keypress on the text field', function() {
+ spyOn(this.regexper.field, 'addEventListener');
+ spyOn(this.regexper, 'keypressListener');
+ this.regexper.bindListeners();
+ expect(this.regexper.field.addEventListener).toHaveBeenCalledWith('keypress', jasmine.any(Function));
+
+ this.regexper.field.addEventListener.calls.mostRecent().args[1]();
+ expect(this.regexper.keypressListener).toHaveBeenCalled();
+ });
+
+ it('binds #submitListener to submit on the form', function() {
+ spyOn(this.regexper.form, 'addEventListener');
+ spyOn(this.regexper, 'submitListener');
+ this.regexper.bindListeners();
+ expect(this.regexper.form.addEventListener).toHaveBeenCalledWith('submit', jasmine.any(Function));
+
+ this.regexper.form.addEventListener.calls.mostRecent().args[1]();
+ expect(this.regexper.submitListener).toHaveBeenCalled();
+ });
+
+ it('binds #updatePercentage to updateStatus on the root', function() {
+ spyOn(this.regexper.root, 'addEventListener');
+ spyOn(this.regexper, 'updatePercentage');
+ this.regexper.bindListeners();
+ expect(this.regexper.root.addEventListener).toHaveBeenCalledWith('updateStatus', jasmine.any(Function));
+
+ this.regexper.root.addEventListener.calls.mostRecent().args[1]();
+ expect(this.regexper.updatePercentage).toHaveBeenCalled();
+ });
+
+ it('binds #hashchangeListener to hashchange on the window', function() {
+ spyOn(window, 'addEventListener');
+ spyOn(this.regexper, 'hashchangeListener');
+ this.regexper.bindListeners();
+ expect(window.addEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function));
+
+ window.addEventListener.calls.mostRecent().args[1]();
+ expect(this.regexper.hashchangeListener).toHaveBeenCalled();
+ });
+
+ });
+
+ describe('#showExpression', function() {
+
+ beforeEach(function() {
+ this.renderPromise = Q.defer();
+ spyOn(this.regexper, 'renderRegexp').and.returnValue(this.renderPromise.promise);
+ });
+
+ it('sets the text field value', function() {
+ this.regexper.showExpression('example expression');
+ expect(this.regexper.field.value).toEqual('example expression');
+ });
+
+ describe('when the expression is blank', function() {
+
+ it('clears the state', function() {
+ this.regexper.showExpression('');
+ expect(this.regexper.state).toEqual('');
+ });
+
+ });
+
+ describe('when the expression is not blank', function() {
+
+ it('sets the state to "is-loading"', function() {
+ this.regexper.showExpression('example expression');
+ expect(this.regexper.state).toEqual('is-loading');
+ });
+
+ it('renders the expression', function() {
+ this.regexper.showExpression('example expression');
+ expect(this.regexper.renderRegexp).toHaveBeenCalledWith('example expression');
+ });
+
+ describe('when the expression finishes rendering', function() {
+
+ beforeEach(function(done) {
+ spyOn(this.regexper, 'updateLinks');
+ this.regexper.showExpression('example expression');
+ this.renderPromise.resolve();
+ setTimeout(done, 100);
+ });
+
+ it('sets the state to "has-results"', function() {
+ expect(this.regexper.state).toEqual('has-results');
+ });
+
+ it('updates the links', function() {
+ expect(this.regexper.updateLinks).toHaveBeenCalled();
+ });
+
+ });
+
+ });
+
+ });
+
+ describe('#updateLinks', function() {
+
+ beforeEach(function() {
+ spyOn(this.regexper, 'buildBlobURL');
+ });
+
+ describe('when blob URLs are supported', function() {
+
+ beforeEach(function() {
+ this.regexper.buildBlobURL.and.returnValue('http://example.com/blob');
+ });
+
+ it('sets the download link href', function() {
+ this.regexper.updateLinks();
+ expect(this.regexper.download.href).toEqual('http://example.com/blob');
+ });
+
+ });
+
+ describe('when blob URLs are not supported', function() {
+
+ beforeEach(function() {
+ this.regexper.buildBlobURL.and.throwError('blob failure');
+ });
+
+ it('hides the download link', function() {
+ this.regexper.updateLinks();
+ expect(this.regexper.download.parentNode.style.display).toEqual('none');
+ });
+
+ });
+
+ describe('when the permalink is enabled', function() {
+
+ beforeEach(function() {
+ this.regexper.permalinkEnabled = true;
+ });
+
+ it('sets the permalink href', function() {
+ this.regexper.updateLinks();
+ expect(this.regexper.permalink.href).toEqual(location.toString());
+ });
+
+ });
+
+ describe('when the permalink is disabled', function() {
+
+ beforeEach(function() {
+ this.regexper.permalinkEnabled = false;
+ });
+
+ it('hides the permalink', function() {
+ this.regexper.updateLinks();
+ expect(this.regexper.permalink.parentNode.style.display).toEqual('none');
+ });
+
+ });
+
+ });
+
+ describe('#renderRegexp', function() {
+
+
+
+ });
+
+});
diff --git a/src/js/regexper.js b/src/js/regexper.js
index 04db34a..9d96629 100644
--- a/src/js/regexper.js
+++ b/src/js/regexper.js
@@ -33,40 +33,25 @@ export default class Regexper {
}
submitListener(event) {
- event.preventDefault();
+ event.returnValue = false;
+ if (event.preventDefault) {
+ event.preventDefault();
+ }
try {
- this.disablePermalink = false;
- location.hash = this.field.value;
+ this._setHash(this.field.value);
}
catch(e) {
// Most likely failed to set the URL has (probably because the expression
// is too long). Turn off the permalink and just show the expression
- this.disablePermalink = true;
+ this.permalinkEnabled = false;
this.showExpression(this.field.value);
}
}
hashchangeListener() {
- var expression = decodeURIComponent(location.hash.slice(1));
-
- this.showExpression(expression);
- }
-
- showExpression(expression) {
- this.field.value = expression;
- this.state = '';
-
- if (expression !== '') {
- this.state = 'is-loading';
-
- this.renderRegexp(expression.replace(/\n/g, '\\n'))
- .then(() => {
- this.state = 'has-results';
- this.updateLinks();
- })
- .done();
- }
+ this.permalinkEnabled = true;
+ this.showExpression(this._getHash());
}
updatePercentage(event) {
@@ -80,27 +65,50 @@ export default class Regexper {
window.addEventListener('hashchange', this.hashchangeListener.bind(this));
}
+ _setHash(hash) {
+ location.hash = encodeURIComponent(hash);
+ }
+
+ _getHash() {
+ return decodeURIComponent(location.hash.slice(1));
+ }
+
set state(state) {
this.root.className = state;
}
- showError(message) {
- this.state = 'has-error';
- this.error.innerHTML = '';
- this.error.appendChild(document.createTextNode(message));
+ get state() {
+ return this.root.className;
+ }
- throw message;
+ showExpression(expression) {
+ this.field.value = expression;
+ this.state = '';
+
+ if (expression !== '') {
+ this.state = 'is-loading';
+
+ this.renderRegexp(expression)
+ .then(() => {
+ this.state = 'has-results';
+ this.updateLinks();
+ })
+ .done();
+ }
+ }
+
+ buildBlobURL(content) {
+ blob = new Blob([content], { type: 'image/svg+xml' });
+ window.blob = blob; // Blob object has to stick around for IE
+ return URL.createObjectURL(blob);
}
updateLinks() {
var blob, url;
try {
- blob = new Blob([this.svg.parentNode.innerHTML], { type: 'image/svg+xml' });
- url = URL.createObjectURL(blob);
- window.blob = blob; // Blob object has to stick around for IE
-
- this.download.setAttribute('href', url);
+ this.download.parentNode.style.display = null;
+ this.download.href = this.buildBlobURL(this.svg.parentNode.innerHTML);
}
catch(e) {
// Blobs or URLs created from them don't work here.
@@ -108,11 +116,11 @@ export default class Regexper {
this.download.parentNode.style.display = 'none';
}
- if (this.disablePermalink) {
- this.permalink.parentNode.style.display = 'none';
- } else {
+ if (this.permalinkEnabled) {
this.permalink.parentNode.style.display = null;
- this.permalink.setAttribute('href', location.toString());
+ this.permalink.href = location.toString();
+ } else {
+ this.permalink.parentNode.style.display = 'none';
}
}
@@ -123,8 +131,14 @@ export default class Regexper {
parser.resetGroupCounter();
- return Q.fcall(parser.parse.bind(parser), expression)
- .then(null, this.showError.bind(this))
+ return Q.fcall(parser.parse.bind(parser), expression.replace(/\n/g, '\\n'))
+ .then(null, message => {
+ this.state = 'has-error';
+ this.error.innerHTML = '';
+ this.error.appendChild(document.createTextNode(message));
+
+ throw message;
+ })
.invoke('render', snap.group())
.then(result => {
var box;