Merging the rendering code from main.js and regexper.js

This commit is contained in:
Jeff Avallone 2014-12-29 21:31:36 -05:00
parent d6e81a2932
commit e271115d24
7 changed files with 182 additions and 130 deletions

View File

@ -6,7 +6,30 @@ import Q from 'q';
describe('parser/javascript.js', function() { describe('parser/javascript.js', function() {
beforeEach(function() { beforeEach(function() {
this.parser = new Parser(); this.container = document.createElement('div');
this.parser = new Parser(this.container);
});
describe('container property', function() {
it('sets the content of the element', function() {
var element = document.createElement('div');
this.parser.container = element;
expect(element.innerHTML).not.toEqual('');
});
it('keeps the original content if the keepContent option is set', function() {
var element = document.createElement('div');
element.innerHTML = 'example content';
this.parser.options.keepContent = true;
this.parser.container = element;
expect(element.innerHTML).toContain('example content');
expect(element.innerHTML).not.toEqual('example content');
});
}); });
describe('#parse', function() { describe('#parse', function() {
@ -15,6 +38,12 @@ describe('parser/javascript.js', function() {
spyOn(regexpParser, 'parse'); spyOn(regexpParser, 'parse');
}); });
it('adds the "loading" class', function() {
spyOn(this.parser, '_addClass');
this.parser.parse('example expression');
expect(this.parser._addClass).toHaveBeenCalledWith('loading');
});
it('parses the expression', function(done) { it('parses the expression', function(done) {
this.parser.parse('example expression') this.parser.parse('example expression')
.then(() => { .then(() => {
@ -59,23 +88,10 @@ describe('parser/javascript.js', function() {
this.renderPromise = Q.defer(); this.renderPromise = Q.defer();
this.parser.parsed = jasmine.createSpyObj('parsed', ['render']); this.parser.parsed = jasmine.createSpyObj('parsed', ['render']);
this.parser.parsed.render.and.returnValue(this.renderPromise.promise); this.parser.parsed.render.and.returnValue(this.renderPromise.promise);
this.svgBase = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1"></svt>';
this.svgContainer = document.createElement('div');
});
it('creates the SVG element', function() {
var svg;
this.parser.render(this.svgContainer, this.svgBase);
svg = this.svgContainer.querySelector('svg');
expect(svg.getAttribute('xmlns')).toEqual('http://www.w3.org/2000/svg');
expect(svg.getAttribute('version')).toEqual('1.1');
}); });
it('render the parsed expression', function() { it('render the parsed expression', function() {
this.parser.render(this.svgContainer, this.svgBase); this.parser.render();
expect(this.parser.parsed.render).toHaveBeenCalled(); expect(this.parser.parsed.render).toHaveBeenCalled();
}); });
@ -90,12 +106,11 @@ describe('parser/javascript.js', function() {
height: 24 height: 24
}); });
this.parser.render(this.svgContainer, this.svgBase);
this.renderPromise.resolve(this.result); this.renderPromise.resolve(this.result);
}); });
it('positions the renderd expression', function(done) { it('positions the renderd expression', function(done) {
this.parser.render(this.svgContainer, this.svgBase) this.parser.render()
.then(() => { .then(() => {
expect(this.result.transform).toHaveBeenCalledWith(Snap.matrix() expect(this.result.transform).toHaveBeenCalledWith(Snap.matrix()
.translate(6, 8)); .translate(6, 8));
@ -105,9 +120,9 @@ describe('parser/javascript.js', function() {
}); });
it('sets the dimensions of the image', function(done) { it('sets the dimensions of the image', function(done) {
this.parser.render(this.svgContainer, this.svgBase) this.parser.render()
.then(() => { .then(() => {
var svg = this.svgContainer.querySelector('svg'); var svg = this.container.querySelector('svg');
expect(svg.getAttribute('width')).toEqual('62'); expect(svg.getAttribute('width')).toEqual('62');
expect(svg.getAttribute('height')).toEqual('44'); expect(svg.getAttribute('height')).toEqual('44');
@ -116,6 +131,25 @@ describe('parser/javascript.js', function() {
.done(); .done();
}); });
it('removes the "loading" class', function(done) {
spyOn(this.parser, '_removeClass');
this.parser.render()
.then(() => {
expect(this.parser._removeClass).toHaveBeenCalledWith('loading');
})
.finally(done)
.done();
});
it('removes the progress element', function(done) {
this.parser.render()
.then(() => {
expect(this.container.querySelector('.loading')).toBeNull();
})
.finally(done)
.done();
});
}); });
}); });

View File

@ -15,7 +15,6 @@ describe('regexper.js', function() {
'<div><a href="#" data-glyph="link-intact"></a></div>', '<div><a href="#" data-glyph="link-intact"></a></div>',
'<div><a href="#" data-glyph="data-transfer-download"></a></div>', '<div><a href="#" data-glyph="data-transfer-download"></a></div>',
'<div id="regexp-render"></div>', '<div id="regexp-render"></div>',
'<script type="text/html" id="svg-base"><svg></svg></script>'
].join(''); ].join('');
this.regexper = new Regexper(this.root); this.regexper = new Regexper(this.root);
@ -102,7 +101,7 @@ describe('regexper.js', function() {
beforeEach(function() { beforeEach(function() {
this.event = util.customEvent('keyup'); this.event = util.customEvent('keyup');
this.regexper.runningParser = jasmine.createSpyObj('parser', ['cancel']); this.regexper.running = jasmine.createSpyObj('parser', ['cancel']);
}); });
describe('when the keyCode is not 27 (Escape)', function() { describe('when the keyCode is not 27 (Escape)', function() {
@ -113,7 +112,7 @@ describe('regexper.js', function() {
it('does not cancel the parser', function() { it('does not cancel the parser', function() {
this.regexper.documentKeypressListener(this.event); this.regexper.documentKeypressListener(this.event);
expect(this.regexper.runningParser.cancel).not.toHaveBeenCalled(); expect(this.regexper.running.cancel).not.toHaveBeenCalled();
}); });
}); });
@ -126,7 +125,7 @@ describe('regexper.js', function() {
it('cancels the parser', function() { it('cancels the parser', function() {
this.regexper.documentKeypressListener(this.event); this.regexper.documentKeypressListener(this.event);
expect(this.regexper.runningParser.cancel).toHaveBeenCalled(); expect(this.regexper.running.cancel).toHaveBeenCalled();
}); });
}); });
@ -375,14 +374,14 @@ describe('regexper.js', function() {
expect(this.regexper._trackEvent).toHaveBeenCalledWith('visualization', 'start'); expect(this.regexper._trackEvent).toHaveBeenCalledWith('visualization', 'start');
}); });
it('keeps a copy of the running parser', function() { it('keeps a copy of the running property parser', function() {
this.regexper.renderRegexp('example expression'); this.regexper.renderRegexp('example expression');
expect(this.regexper.runningParser).toBeTruthy(); expect(this.regexper.running).toBeTruthy();
}); });
it('parses the expression', function() { it('parses the expression', function() {
this.regexper.renderRegexp('example expression'); this.regexper.renderRegexp('example expression');
expect(this.regexper.runningParser.parse).toHaveBeenCalledWith('example expression'); expect(this.regexper.running.parse).toHaveBeenCalledWith('example expression');
}); });
describe('when parsing fails', function() { describe('when parsing fails', function() {
@ -423,7 +422,7 @@ describe('regexper.js', function() {
describe('when parsing succeeds', function() { describe('when parsing succeeds', function() {
beforeEach(function() { beforeEach(function() {
this.parser = new Parser(); this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser); this.parsePromise.resolve(this.parser);
this.renderPromise.resolve(); this.renderPromise.resolve();
}); });
@ -431,7 +430,7 @@ describe('regexper.js', function() {
it('renders the expression', function(done) { it('renders the expression', function(done) {
this.regexper.renderRegexp('example expression') this.regexper.renderRegexp('example expression')
.then(() => { .then(() => {
expect(this.parser.render).toHaveBeenCalledWith(this.regexper.svgContainer.querySelector('.svg'), this.regexper.svgBase); expect(this.parser.render).toHaveBeenCalled();
}, fail) }, fail)
.finally(done) .finally(done)
.done(); .done();
@ -442,7 +441,7 @@ describe('regexper.js', function() {
describe('when rendering is complete', function() { describe('when rendering is complete', function() {
beforeEach(function() { beforeEach(function() {
this.parser = new Parser(); this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser); this.parsePromise.resolve(this.parser);
this.renderPromise.resolve(); this.renderPromise.resolve();
}); });
@ -483,10 +482,10 @@ describe('regexper.js', function() {
.done(); .done();
}); });
it('sets the runningParser to false', function(done) { it('sets the running property to false', function(done) {
this.regexper.renderRegexp('example expression') this.regexper.renderRegexp('example expression')
.then(() => { .then(() => {
expect(this.regexper.runningParser).toBeFalsy(); expect(this.regexper.running).toBeFalsy();
}, fail) }, fail)
.finally(done) .finally(done)
.done(); .done();
@ -497,7 +496,7 @@ describe('regexper.js', function() {
describe('when the rendering is cancelled', function() { describe('when the rendering is cancelled', function() {
beforeEach(function() { beforeEach(function() {
this.parser = new Parser(); this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser); this.parsePromise.resolve(this.parser);
this.renderPromise.reject('Render cancelled'); this.renderPromise.reject('Render cancelled');
}); });
@ -520,10 +519,10 @@ describe('regexper.js', function() {
.done(); .done();
}); });
it('sets the runningParser to false', function(done) { it('sets the running property to false', function(done) {
this.regexper.renderRegexp('example expression') this.regexper.renderRegexp('example expression')
.then(() => { .then(() => {
expect(this.regexper.runningParser).toBeFalsy(); expect(this.regexper.running).toBeFalsy();
}, fail) }, fail)
.finally(done) .finally(done)
.done(); .done();
@ -534,15 +533,15 @@ describe('regexper.js', function() {
describe('when the rendering fails', function() { describe('when the rendering fails', function() {
beforeEach(function() { beforeEach(function() {
this.parser = new Parser(); this.parser = new Parser(this.regexper.svgContainer);
this.parsePromise.resolve(this.parser); this.parsePromise.resolve(this.parser);
this.renderPromise.reject('example render failure'); this.renderPromise.reject('example render failure');
}); });
it('sets the runningParser to false', function(done) { it('sets the running property to false', function(done) {
this.regexper.renderRegexp('example expression') this.regexper.renderRegexp('example expression')
.then(fail, () => { .then(fail, () => {
expect(this.regexper.runningParser).toBeFalsy(); expect(this.regexper.running).toBeFalsy();
}) })
.finally(done) .finally(done)
.done(); .done();

14
spec/setup_spec.js Normal file
View File

@ -0,0 +1,14 @@
beforeEach(function() {
var template = document.createElement('script');
template.setAttribute('type', 'text/html');
template.setAttribute('id', 'svg-container-base');
template.innerHTML = [
'<div class="svg"><svg></svg></div>',
'<div class="progress"><div></div></div>'
].join('');
document.body.appendChild(template);
});
afterEach(function() {
document.body.removeChild(document.body.querySelector('#svg-container-base'));
});

View File

@ -27,31 +27,10 @@ window._gaq = (typeof _gaq !== 'undefined') ? _gaq : {
}); });
} }
_.each(document.querySelectorAll('figure[data-expr]'), element => { _.each(document.querySelectorAll('[data-expr]'), element => {
var parser = new Parser(), new Parser(element, { keepContent: true })
svg, percentage; .parse(element.getAttribute('data-expr'))
.invoke('render')
element.className = _.compact([element.className, 'loading']).join(' '); .done();
element.innerHTML = [
'<div class="svg"></div>',
'<div class="progress"><div style="width: 0;"></div></div>',
element.innerHTML
].join('');
svg = element.querySelector('.svg');
percentage = element.querySelector('.progress div');
setTimeout(() => {
parser.parse(element.getAttribute('data-expr'))
.invoke('render', svg, document.querySelector('#svg-base').innerHTML)
.then(null, null, progress => {
percentage.style.width = progress * 100 + '%';
})
.finally(() => {
element.className = _.without(element.className.split(' '), 'loading').join(' ');
element.removeChild(element.querySelector('.progress'));
})
.done();
}, 1);
}); });
}()); }());

View File

@ -1,10 +1,11 @@
import Q from 'q'; import Q from 'q';
import Snap from 'snapsvg'; import Snap from 'snapsvg';
import _ from 'lodash';
import javascript from './javascript/parser.js'; import javascript from './javascript/parser.js';
export default class Parser { export default class Parser {
constructor() { constructor(container, options) {
this.state = { this.state = {
groupCounter: 1, groupCounter: 1,
renderCounter: 0, renderCounter: 0,
@ -12,11 +13,40 @@ export default class Parser {
cancelRender: false, cancelRender: false,
warnings: [] warnings: []
}; };
this.options = options || {};
_.defaults(this.options, {
keepContent: false
});
this.container = container;
}
set container(cont) {
this._container = cont;
this._container.innerHTML = [
document.querySelector('#svg-container-base').innerHTML,
this.options.keepContent ? this.container.innerHTML : ''
].join('');
}
get container() {
return this._container;
}
_addClass(className) {
this.container.className = _.compact([this.container.className, className]).join(' ');
}
_removeClass(className) {
this.container.className = _.without(this.container.className.split(' '), className).join(' ');
} }
parse(expression) { parse(expression) {
var deferred = Q.defer(); var deferred = Q.defer();
this._addClass('loading');
setTimeout(() => { setTimeout(() => {
try { try {
javascript.Parser.SyntaxNode.state = this.state; javascript.Parser.SyntaxNode.state = this.state;
@ -32,23 +62,30 @@ export default class Parser {
return deferred.promise; return deferred.promise;
} }
render(containerElement, svgBase) { render() {
var svg; var svg = Snap(this.container.querySelector('svg')),
progress = this.container.querySelector('.progress div');
containerElement.innerHTML = svgBase;
svg = Snap(containerElement.querySelector('svg'));
return this.parsed.render(svg.group()) return this.parsed.render(svg.group())
.then(result => { .then(
var box = result.getBBox(); result => {
var box = result.getBBox();
result.transform(Snap.matrix() result.transform(Snap.matrix()
.translate(10 - box.x, 10 - box.y)); .translate(10 - box.x, 10 - box.y));
svg.attr({ svg.attr({
width: box.width + 20, width: box.width + 20,
height: box.height + 20 height: box.height + 20
}); });
},
null,
percent => {
progress.style.width = percent * 100 + '%';
}
)
.finally(() => {
this._removeClass('loading');
this.container.removeChild(this.container.querySelector('.progress'));
}); });
} }

View File

@ -13,7 +13,6 @@ export default class Regexper {
this.permalink = root.querySelector('a[data-glyph="link-intact"]'); this.permalink = root.querySelector('a[data-glyph="link-intact"]');
this.download = root.querySelector('a[data-glyph="data-transfer-download"]'); this.download = root.querySelector('a[data-glyph="data-transfer-download"]');
this.svgContainer = root.querySelector('#regexp-render'); this.svgContainer = root.querySelector('#regexp-render');
this.svgBase = this.root.querySelector('#svg-base').innerHTML;
} }
keypressListener(event) { keypressListener(event) {
@ -28,8 +27,8 @@ export default class Regexper {
} }
documentKeypressListener(event) { documentKeypressListener(event) {
if (event.keyCode === 27 && this.runningParser) { if (event.keyCode === 27 && this.running) {
this.runningParser.cancel(); this.running.cancel();
} }
} }
@ -123,12 +122,12 @@ export default class Regexper {
} }
renderRegexp(expression) { renderRegexp(expression) {
var svg, percentage; var parseError = false;
if (this.runningParser) { if (this.running) {
let deferred = Q.defer(); let deferred = Q.defer();
this.runningParser.cancel(); this.running.cancel();
setTimeout(() => { setTimeout(() => {
deferred.resolve(this.renderRegexp(expression)); deferred.resolve(this.renderRegexp(expression));
@ -140,53 +139,38 @@ export default class Regexper {
this.state = 'is-loading'; this.state = 'is-loading';
this._trackEvent('visualization', 'start'); this._trackEvent('visualization', 'start');
this.runningParser = new Parser(); this.running = new Parser(this.svgContainer);
this.svgContainer.innerHTML = [ return this.running
'<div class="svg"></div>', .parse(expression)
'<div class="progress"><div style="width: 0;"></div></div>',
].join('');
svg = this.svgContainer.querySelector('.svg');
percentage = this.svgContainer.querySelector('.progress div');
return this.runningParser.parse(expression)
.then(null, message => { .then(null, message => {
this.state = 'has-error'; this.state = 'has-error';
this.error.innerHTML = ''; this.error.innerHTML = '';
this.error.appendChild(document.createTextNode(message)); this.error.appendChild(document.createTextNode(message));
this.parseError = true; parseError = true;
throw message; throw message;
}) })
.invoke('render', svg, this.svgBase) .invoke('render')
.then( .then(() => {
() => {
this.state = 'has-results'; this.state = 'has-results';
this.updateLinks(); this.updateLinks();
this.displayWarnings(this.runningParser.warnings); this.displayWarnings(this.running.warnings);
this._trackEvent('visualization', 'complete'); this._trackEvent('visualization', 'complete');
}, })
null,
progress => {
percentage.style.width = progress * 100 + '%';
}
)
.then(null, message => { .then(null, message => {
if (message === 'Render cancelled') { if (message === 'Render cancelled') {
this._trackEvent('visualization', 'cancelled'); this._trackEvent('visualization', 'cancelled');
this.state = ''; this.state = '';
} else if (this.parseError) { } else if (parseError) {
this._trackEvent('visualization', 'parse error'); this._trackEvent('visualization', 'parse error');
} else { } else {
throw message; throw message;
} }
}) })
.finally(() => { .finally(() => {
this.runningParser = false; this.running = false;
this.parseError = false;
this.svgContainer.removeChild(this.svgContainer.querySelector('.progress'));
}); });
} }
} }

View File

@ -24,27 +24,32 @@
</div> </div>
<![endif]--> <![endif]-->
<!--[if gt IE 8]> --> <!--[if gt IE 8]> -->
<script type="text/html" id="svg-base"> <script type="text/html" id="svg-container-base">
<svg <div class="svg">
xmlns="http://www.w3.org/2000/svg" <svg
xmlns:cc="http://creativecommons.org/ns#" xmlns="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#"
version="1.1"> xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
<defs> version="1.1">
<style type="text/css">${svgStyles}</style> <defs>
</defs> <style type="text/css">${svgStyles}</style>
<metadata> </defs>
<rdf:RDF> <metadata>
<cc:License rdf:about="http://creativecommons.org/licenses/by/3.0/"> <rdf:RDF>
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction" /> <cc:License rdf:about="http://creativecommons.org/licenses/by/3.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution" /> <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice" /> <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution" /> <cc:requires rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution" />
</cc:License> <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</rdf:RDF> </cc:License>
</metadata> </rdf:RDF>
</svg> </metadata>
</svg>
</div>
<div class="progress">
<div style="width:0;"></div>
</div>
</script> </script>
<header> <header>