From a9a73a7bebdfe5e44ddac6ff9ae4edafdd4bc000 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 10:54:56 -0800 Subject: [PATCH 01/61] Update bower.json to reflect v0.1.1 --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 41ce273290..fef49c3b9f 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "plottable.js", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "d3": "3.4.1" }, From 9f9988b8043e821af777d5f08aacb52077388f4c Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 11:09:27 -0800 Subject: [PATCH 02/61] Minor cleanup to label.ts, better defaulting, space alignment, use utils --- src/label.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/label.ts b/src/label.ts index f3b375f9c9..8914646332 100644 --- a/src/label.ts +++ b/src/label.ts @@ -7,16 +7,16 @@ class Label extends Component { public yAlignment = "CENTER"; private textElement: D3.Selection; - private text:string; - private orientation = "horizontal"; + private text: string; + private orientation: string; - constructor(text: string, orientation?: string) { + constructor(text: string, orientation = "horizontal") { super(); this.classed(Label.CSS_CLASS, true); this.text = text; if (orientation === "horizontal" || orientation === "vertical-left" || orientation === "vertical-right") { this.orientation = orientation; - } else if (orientation != null) { + } else { throw new Error(orientation + " is not a valid orientation for LabelComponent"); } } @@ -25,10 +25,10 @@ class Label extends Component { super.anchor(element); this.textElement = this.element.append("text").text(this.text); - var bbox = ( this.textElement.node()).getBBox(); + var bbox = Utils.getBBox(this.element); this.textElement.attr("dy", -bbox.y); var clientHeight = bbox.height; - var clientWidth = bbox.width; + var clientWidth = bbox.width; if (this.orientation === "horizontal") { this.rowMinimum(clientHeight); From bcf557934575496ec24d08971d469d4a7f2bfcd1 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Mon, 10 Feb 2014 11:59:47 -0800 Subject: [PATCH 03/61] Cleaned up error style. Close #83. --- src/component.ts | 8 ++++---- src/label.ts | 2 +- src/table.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/component.ts b/src/component.ts index 098d252e8c..cfecebac43 100644 --- a/src/component.ts +++ b/src/component.ts @@ -40,7 +40,7 @@ class Component { public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { if (xOffset == null || yOffset == null || availableWidth == null || availableHeight == null) { if (this.element == null) { - throw new Error("It's impossible to computeLayout before anchoring"); + throw "anchor() must be called before computeLayout()"; } else if (this.element.node().nodeName === "svg") { // we are the root node, let's guess width and height for convenience xOffset = 0; @@ -48,7 +48,7 @@ class Component { availableWidth = parseFloat(this.element.attr("width" )); availableHeight = parseFloat(this.element.attr("height")); } else { - throw new Error("You need to pass non-null arguments when calling computeLayout on a non-root node"); + throw "null arguments cannot be passed to computeLayout() on a non-root (non-) node"; } } if (this.rowWeight() === 0 && this.rowMinimum() !== 0) { @@ -62,7 +62,7 @@ class Component { yOffset += availableHeight - this.rowMinimum(); break; default: - throw new Error("unsupported alignment"); + throw this.yAlignment + " is not a supported alignment"; } availableHeight = this.rowMinimum(); } @@ -77,7 +77,7 @@ class Component { xOffset += availableWidth - this.colMinimum(); break; default: - throw new Error("unsupported alignment"); + throw this.xAlignment + " is not a supported alignment"; } availableWidth = this.colMinimum(); } diff --git a/src/label.ts b/src/label.ts index 8914646332..609d0984fd 100644 --- a/src/label.ts +++ b/src/label.ts @@ -17,7 +17,7 @@ class Label extends Component { if (orientation === "horizontal" || orientation === "vertical-left" || orientation === "vertical-right") { this.orientation = orientation; } else { - throw new Error(orientation + " is not a valid orientation for LabelComponent"); + throw orientation + " is not a valid orientation for LabelComponent"; } } diff --git a/src/table.ts b/src/table.ts index 5f5b67ee53..7f934e2b37 100644 --- a/src/table.ts +++ b/src/table.ts @@ -48,7 +48,7 @@ class Table extends Component { var freeWidth = this.availableWidth - this.colMinimum(); var freeHeight = this.availableHeight - this.rowMinimum(); if (freeWidth < 0 || freeHeight < 0) { - throw "InsufficientSpaceError"; + throw "InsufficientSpaceException"; } // distribute remaining height to rows @@ -106,7 +106,7 @@ class Table extends Component { public rowMinimum(newVal: number): Component; public rowMinimum(newVal?: number): any { if (newVal != null) { - throw new Error("Row minimum cannot be directly set on Table."); + throw "Row minimum cannot be directly set on Table"; } else { this.rowMinimums = this.rows.map((row: Component[]) => d3.max(row, (r: Component) => r.rowMinimum())); return d3.sum(this.rowMinimums) + this.rowPadding * (this.rows.length - 1); @@ -117,7 +117,7 @@ class Table extends Component { public colMinimum(newVal: number): Component; public colMinimum(newVal?: number): any { if (newVal != null) { - throw new Error("Col minimum cannot be directly set on Table."); + throw "Col minimum cannot be directly set on Table"; } else { this.colMinimums = this.cols.map((col: Component[]) => d3.max(col, (r: Component) => r.colMinimum())); return d3.sum(this.colMinimums) + this.colPadding * (this.cols.length - 1); From 9cb4e546e83e5451002fd5305e5447e67c9a2281 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Mon, 10 Feb 2014 14:37:21 -0800 Subject: [PATCH 04/61] Convert errors to "new Error([...])" style Close #83. --- src/component.ts | 8 ++++---- src/label.ts | 2 +- src/table.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/component.ts b/src/component.ts index cfecebac43..cef5a57af4 100644 --- a/src/component.ts +++ b/src/component.ts @@ -40,7 +40,7 @@ class Component { public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { if (xOffset == null || yOffset == null || availableWidth == null || availableHeight == null) { if (this.element == null) { - throw "anchor() must be called before computeLayout()"; + throw new Error("anchor() must be called before computeLayout()"); } else if (this.element.node().nodeName === "svg") { // we are the root node, let's guess width and height for convenience xOffset = 0; @@ -48,7 +48,7 @@ class Component { availableWidth = parseFloat(this.element.attr("width" )); availableHeight = parseFloat(this.element.attr("height")); } else { - throw "null arguments cannot be passed to computeLayout() on a non-root (non-) node"; + throw new Error("null arguments cannot be passed to computeLayout() on a non-root (non-) node"); } } if (this.rowWeight() === 0 && this.rowMinimum() !== 0) { @@ -62,7 +62,7 @@ class Component { yOffset += availableHeight - this.rowMinimum(); break; default: - throw this.yAlignment + " is not a supported alignment"; + throw new Error(this.yAlignment + " is not a supported alignment"); } availableHeight = this.rowMinimum(); } @@ -77,7 +77,7 @@ class Component { xOffset += availableWidth - this.colMinimum(); break; default: - throw this.xAlignment + " is not a supported alignment"; + throw new Error(this.xAlignment + " is not a supported alignment"); } availableWidth = this.colMinimum(); } diff --git a/src/label.ts b/src/label.ts index 609d0984fd..8914646332 100644 --- a/src/label.ts +++ b/src/label.ts @@ -17,7 +17,7 @@ class Label extends Component { if (orientation === "horizontal" || orientation === "vertical-left" || orientation === "vertical-right") { this.orientation = orientation; } else { - throw orientation + " is not a valid orientation for LabelComponent"; + throw new Error(orientation + " is not a valid orientation for LabelComponent"); } } diff --git a/src/table.ts b/src/table.ts index 7f934e2b37..f2f3c0b03b 100644 --- a/src/table.ts +++ b/src/table.ts @@ -48,7 +48,7 @@ class Table extends Component { var freeWidth = this.availableWidth - this.colMinimum(); var freeHeight = this.availableHeight - this.rowMinimum(); if (freeWidth < 0 || freeHeight < 0) { - throw "InsufficientSpaceException"; + throw new Error("InsufficientSpaceError"); } // distribute remaining height to rows @@ -106,7 +106,7 @@ class Table extends Component { public rowMinimum(newVal: number): Component; public rowMinimum(newVal?: number): any { if (newVal != null) { - throw "Row minimum cannot be directly set on Table"; + throw new Error("Row minimum cannot be directly set on Table"); } else { this.rowMinimums = this.rows.map((row: Component[]) => d3.max(row, (r: Component) => r.rowMinimum())); return d3.sum(this.rowMinimums) + this.rowPadding * (this.rows.length - 1); @@ -117,7 +117,7 @@ class Table extends Component { public colMinimum(newVal: number): Component; public colMinimum(newVal?: number): any { if (newVal != null) { - throw "Col minimum cannot be directly set on Table"; + throw new Error("Col minimum cannot be directly set on Table"); } else { this.colMinimums = this.cols.map((col: Component[]) => d3.max(col, (r: Component) => r.colMinimum())); return d3.sum(this.colMinimums) + this.colPadding * (this.cols.length - 1); From ae5e73d54adb66401a1bdc9650eb7ba437c27304 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 15:12:33 -0800 Subject: [PATCH 05/61] Improvements to label.ts: - Add a setText method that changes the text. Since this is now a possibility, can call constructor with no arguments and default text to empty string. - Break out setMinimumsByCalculatingTextSize from the anchor call. This way, if we change the text or style, we can recompute the size without re-anchoring (which is disallowed). - Handle default orientation in a more transparent fashion. - Reset textElement.attr("dy", 0) so that new setMinimumsByCalculatingTextSize is idemptotent. --- src/label.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/label.ts b/src/label.ts index 609d0984fd..8623854397 100644 --- a/src/label.ts +++ b/src/label.ts @@ -10,7 +10,7 @@ class Label extends Component { private text: string; private orientation: string; - constructor(text: string, orientation = "horizontal") { + constructor(text = "", orientation = "horizontal") { super(); this.classed(Label.CSS_CLASS, true); this.text = text; @@ -24,7 +24,17 @@ class Label extends Component { public anchor(element: D3.Selection) { super.anchor(element); this.textElement = this.element.append("text").text(this.text); + this.setMinimumsByCalculatingTextSize(); + } + + public setText(text: string) { + this.text = text; + this.textElement.text(text); + this.setMinimumsByCalculatingTextSize(); + } + private setMinimumsByCalculatingTextSize() { + this.textElement.attr("dy", 0); // Reset this so we maintain idempotence var bbox = Utils.getBBox(this.element); this.textElement.attr("dy", -bbox.y); var clientHeight = bbox.height; @@ -47,7 +57,7 @@ class Label extends Component { class TitleLabel extends Label { private static CSS_CLASS = "title-label"; - constructor(text: string, orientation?: string) { + constructor(text?: string, orientation?: string) { super(text, orientation); this.classed(TitleLabel.CSS_CLASS, true); } @@ -55,7 +65,7 @@ class TitleLabel extends Label { class AxisLabel extends Label { private static CSS_CLASS = "axis-label"; - constructor(text: string, orientation?: string) { + constructor(text?: string, orientation?: string) { super(text, orientation); this.classed(AxisLabel.CSS_CLASS, true); } From 4ac710ebee235d15e8394384662c794e20bd2204 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 15:14:25 -0800 Subject: [PATCH 06/61] Add unit tests for label.ts. Two of them are currently failing, tracked by #88, #84. Close #80. --- test/labelTests.ts | 106 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 test/labelTests.ts diff --git a/test/labelTests.ts b/test/labelTests.ts new file mode 100644 index 0000000000..eb3ebae354 --- /dev/null +++ b/test/labelTests.ts @@ -0,0 +1,106 @@ +/// + +var assert = chai.assert; + +describe("Labels", () => { + + it("Standard text title label generates properly", () => { + var svg = d3.select("body").append("svg:svg"); + var label = new TitleLabel("A CHART TITLE"); + label.anchor(svg); + label.computeLayout(0, 0, 400, 400); + + var element = label.element; + assert.isTrue(label.element.classed("label"), "title element has label css class"); + assert.isTrue(label.element.classed("title-label"), "title element has title-label css class"); + var textChildren = element.selectAll("text"); + assert.lengthOf(textChildren, 1, "There is one text node in the parent element"); + + var text = element.select("text"); + var bbox = Utils.getBBox(text); + assert.equal(bbox.width, label.colMinimum(), "text width === label.colMinimum()"); + assert.equal(bbox.height, label.rowMinimum(), "text height === label.rowMinimum()"); + assert.equal(0, label.colWeight(), "label.colWeight is 0"); + assert.equal(0, label.rowWeight(), "label.rowWeight is 0"); + assert.equal(text.node().textContent, "A CHART TITLE", "node's text content is as expected"); + svg.remove(); + }); + + it("Italicized text is handled properly", () => { + var svg = d3.select("body").append("svg:svg"); + var label = new TitleLabel("A CHART TITLE"); + label.anchor(svg); + var element = label.element; + var text = element.select("text"); + text.style("font-style", "italic"); + ( label).setMinimumsByCalculatingTextSize(); // to access private method + label.computeLayout(0, 0, 400, 400); + label.render(); + var bbox = Utils.getBBox(text); + assert.operator(bbox.width, "<", label.colMinimum(), "text width is less than the col minimum (to account for italicized overhang)"); + assert.equal(bbox.height, label.rowMinimum(), "text height === label.rowMinimum()"); + svg.remove(); + }); + + it("Rotated text is handled properly", () => { + var svg = d3.select("body").append("svg:svg"); + var label = new AxisLabel("LEFT-ROTATED LABEL", "vertical-left"); + label.anchor(svg); + var element = label.element; + var text = element.select("text"); + label.computeLayout(0, 0, 400, 400); + label.render(); + var bbox = Utils.getBBox(text); + assert.equal(bbox.width, label.rowMinimum(), "text width === label.rowMinimum() (its rotated)"); + assert.equal(bbox.height, label.colMinimum(), "text height === label.colMinimum() (its rotated)"); + assert.equal(text.attr("transform"), "rotate(-90)", "the text element is rotated -90 degrees"); + svg.remove(); + }); + + it("Superlong text is handled in a sane fashion", () => { + var svgWidth = 400; + var svg = d3.select("body").append("svg:svg"); + var label = new TitleLabel("THIS LABEL IS SO LONG WHOEVER WROTE IT WAS PROBABLY DERANGED"); + label.anchor(svg); + var element = label.element; + var text = element.select("text"); + label.computeLayout(0, 0, svgWidth, 400); + label.render(); + var bbox = Utils.getBBox(text); + assert.equal(bbox.width, label.colMinimum(), "text width === label.colMinimum()"); + assert.equal(bbox.height, label.rowMinimum(), "text height === label.rowMinimum()"); + assert.operator(bbox.width, "<=", svgWidth, "the text is not wider than the SVG width"); + assert.equal(text.attr("transform"), "rotate(-90)", "the text element is rotated -90 degrees"); + svg.remove(); + }); + + it("Labels with different font sizes have different space requirements", () => { + var svg = d3.select("body").append("svg:svg"); + var label = new TitleLabel("A CHART TITLE"); + label.anchor(svg); + label.element.select("text").style("font-size", "18pt"); + ( label).setMinimumsByCalculatingTextSize(); + var originalWidth = label.colMinimum(); + label.element.select("text").style("font-size", "6pt"); + ( label).setMinimumsByCalculatingTextSize(); + var newWidth = label.colMinimum(); + assert.operator(newWidth, "<", originalWidth, "Smaller font size implies smaller label width"); + + svg.remove(); + }); + + it("Label text can be changed after label is created", () => { + var svg = d3.select("body").append("svg:svg"); + var label = new TitleLabel(); + label.anchor(svg); + var textEl = label.element.select("text"); + assert.equal(textEl.text(), "", "the text defaulted to empty string when constructor was called w/o arguments"); + assert.equal(label.rowMinimum(), 0, "rowMin is 0 for empty string"); + assert.equal(label.colMinimum(), 0, "colMin is 0 for empty string"); + label.setText("hello world"); + assert.equal(textEl.text(), "hello world", "the label text updated properly"); + assert.operator(label.rowMinimum(), ">", 0, "rowMin is > 0 for non-empty string"); + assert.operator(label.colMinimum(), ">", 0, "colMin is > 0 for non-empty string"); + svg.remove(); + }); +}); From 4f295755fb81be1f7c876bb6a8e6286ba4aebdf7 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 15:25:23 -0800 Subject: [PATCH 07/61] Component.anchor now throws an error message if its element has any child nodes. Close #89. --- src/component.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/component.ts b/src/component.ts index cef5a57af4..14e925daed 100644 --- a/src/component.ts +++ b/src/component.ts @@ -24,6 +24,9 @@ class Component { public yAlignment = "TOP"; // TOP, CENTER, BOTTOM public anchor(element: D3.Selection) { + if (element.node().childNodes.length > 0) { + throw new Error("Anchoring to a non-empty component is disallowed"); + } this.element = element; if (this.clipPathEnabled) {this.generateClipPath();}; this.cssClasses.forEach((cssClass: string) => { @@ -40,7 +43,7 @@ class Component { public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { if (xOffset == null || yOffset == null || availableWidth == null || availableHeight == null) { if (this.element == null) { - throw new Error("anchor() must be called before computeLayout()"); + throw "anchor() must be called before computeLayout()"; } else if (this.element.node().nodeName === "svg") { // we are the root node, let's guess width and height for convenience xOffset = 0; @@ -48,7 +51,7 @@ class Component { availableWidth = parseFloat(this.element.attr("width" )); availableHeight = parseFloat(this.element.attr("height")); } else { - throw new Error("null arguments cannot be passed to computeLayout() on a non-root (non-) node"); + throw "null arguments cannot be passed to computeLayout() on a non-root (non-) node"; } } if (this.rowWeight() === 0 && this.rowMinimum() !== 0) { @@ -62,7 +65,7 @@ class Component { yOffset += availableHeight - this.rowMinimum(); break; default: - throw new Error(this.yAlignment + " is not a supported alignment"); + throw this.yAlignment + " is not a supported alignment"; } availableHeight = this.rowMinimum(); } @@ -77,7 +80,7 @@ class Component { xOffset += availableWidth - this.colMinimum(); break; default: - throw new Error(this.xAlignment + " is not a supported alignment"); + throw this.xAlignment + " is not a supported alignment"; } availableWidth = this.colMinimum(); } From 07d75edafeff9b8149de0001ee38ea1cda70b97c Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 15:30:50 -0800 Subject: [PATCH 08/61] Change Component API so that anchor, computeLayout, render are chainable. Also update the Table unit tests to use the new API. Close #87. --- src/axis.ts | 2 ++ src/component.ts | 4 +++- src/label.ts | 1 + src/renderer.ts | 6 ++++++ src/table.ts | 9 ++++++--- test/tableTests.ts | 12 +++--------- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/axis.ts b/src/axis.ts index 68d8283938..776ba42686 100644 --- a/src/axis.ts +++ b/src/axis.ts @@ -46,6 +46,7 @@ class Axis extends Component { public anchor(element: D3.Selection) { super.anchor(element); this.axisElement = this.element.append("g").classed("axis", true); // TODO: remove extraneous sub-element + return this; } private transformString(translate: number, scale: number) { @@ -87,6 +88,7 @@ class Axis extends Component { } // chai.assert.operator(this.element.node().getBBox().height, '<=', height, "axis height is appropriate"); // chai.assert.operator(this.element.node().getBBox().width, '<=', width, "axis width is appropriate"); + return this; } public rescale() { diff --git a/src/component.ts b/src/component.ts index 14e925daed..341f4dfae9 100644 --- a/src/component.ts +++ b/src/component.ts @@ -38,6 +38,7 @@ class Component { this.addBox("bounding-box"); this.registeredInteractions.forEach((r) => r.anchor(this.hitBox)); + return this; } public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { @@ -90,10 +91,11 @@ class Component { this.availableHeight = availableHeight; this.element.attr("transform", "translate(" + this.xOffset + "," + this.yOffset + ")"); this.boxes.forEach((b: D3.Selection) => b.attr("width", this.availableWidth).attr("height", this.availableHeight)); + return this; } public render() { - //no-op + return this; } private addBox(className?: string, parentElement?: D3.Selection) { diff --git a/src/label.ts b/src/label.ts index b8dbd46a1c..36483fea00 100644 --- a/src/label.ts +++ b/src/label.ts @@ -25,6 +25,7 @@ class Label extends Component { super.anchor(element); this.textElement = this.element.append("text").text(this.text); this.setMinimumsByCalculatingTextSize(); + return this; } public setText(text: string) { diff --git a/src/renderer.ts b/src/renderer.ts index 7312fb06a0..0f4d997581 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -32,6 +32,7 @@ class Renderer extends Component { public anchor(element: D3.Selection) { super.anchor(element); this.renderArea = element.append("g").classed("render-area", true).classed(this.dataset.seriesName, true); + return this; } } @@ -73,6 +74,7 @@ class XYRenderer extends Renderer { super.computeLayout(xOffset, yOffset, availableWidth, availableHeight); this.xScale.range([0, this.availableWidth]); this.yScale.range([this.availableHeight, 0]); + return this; } public invertXYSelectionArea(area: SelectionArea) { @@ -129,6 +131,7 @@ class LineRenderer extends XYRenderer { public anchor(element: D3.Selection) { super.anchor(element); this.renderArea = this.renderArea.append("path"); + return this; } public render() { @@ -140,6 +143,7 @@ class LineRenderer extends XYRenderer { .classed(this.dataset.seriesName, true) .datum(this.dataset.data); this.renderArea.attr("d", this.line); + return this; } } @@ -160,6 +164,7 @@ class CircleRenderer extends XYRenderer { .attr("cy", (datum: any) => this.yScale.scale(this.yAccessor(datum))) .attr("r", this.size); this.dataSelection.exit().remove(); + return this; } } @@ -207,5 +212,6 @@ class BarRenderer extends XYRenderer { - this.BAR_START_PADDING_PX - this.BAR_END_PADDING_PX)) .attr("height", (d: any) => maxScaledY - this.yScale.scale(this.yAccessor(d)) ); this.dataSelection.exit().remove(); + return this; } } diff --git a/src/table.ts b/src/table.ts index f2f3c0b03b..78c5cc19fa 100644 --- a/src/table.ts +++ b/src/table.ts @@ -39,6 +39,7 @@ class Table extends Component { component.anchor(this.element.append("g")); }); }); + return this; } public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { @@ -48,7 +49,7 @@ class Table extends Component { var freeWidth = this.availableWidth - this.colMinimum(); var freeHeight = this.availableHeight - this.rowMinimum(); if (freeWidth < 0 || freeHeight < 0) { - throw new Error("InsufficientSpaceError"); + throw "InsufficientSpaceException"; } // distribute remaining height to rows @@ -73,6 +74,7 @@ class Table extends Component { childYOffset += rowHeights[rowIndex] + this.rowPadding; }); chai.assert.operator(childYOffset - this.rowPadding, "<=", this.availableHeight + 0.1, "final yOffset was <= availableHeight"); + return this; } private static rowProportionalSpace(rows: Component[][], freeHeight: number) { @@ -99,6 +101,7 @@ class Table extends Component { component.render(); }); }); + return this; } /* Getters */ @@ -106,7 +109,7 @@ class Table extends Component { public rowMinimum(newVal: number): Component; public rowMinimum(newVal?: number): any { if (newVal != null) { - throw new Error("Row minimum cannot be directly set on Table"); + throw "Row minimum cannot be directly set on Table"; } else { this.rowMinimums = this.rows.map((row: Component[]) => d3.max(row, (r: Component) => r.rowMinimum())); return d3.sum(this.rowMinimums) + this.rowPadding * (this.rows.length - 1); @@ -117,7 +120,7 @@ class Table extends Component { public colMinimum(newVal: number): Component; public colMinimum(newVal?: number): any { if (newVal != null) { - throw new Error("Col minimum cannot be directly set on Table"); + throw "Col minimum cannot be directly set on Table"; } else { this.colMinimums = this.cols.map((col: Component[]) => d3.max(col, (r: Component) => r.colMinimum())); return d3.sum(this.colMinimums) + this.colPadding * (this.cols.length - 1); diff --git a/test/tableTests.ts b/test/tableTests.ts index c1099c828d..b45976ce1b 100644 --- a/test/tableTests.ts +++ b/test/tableTests.ts @@ -36,9 +36,7 @@ describe("Table layout", () => { var renderers = tableAndRenderers.renderers; var svg = d3.select("body").append("svg:svg"); - table.anchor(svg); - table.computeLayout(0, 0, 400, 400); - table.render(); + table.anchor(svg).computeLayout(0, 0, 400, 400).render(); var elements = renderers.map((r) => r.element); var translates = elements.map((e) => Utils.getTranslate(e)); @@ -62,9 +60,7 @@ describe("Table layout", () => { table.padding(5,5); var svg = d3.select("body").append("svg:svg"); - table.anchor(svg); - table.computeLayout(0, 0, 415, 415); - table.render(); + table.anchor(svg).computeLayout(0, 0, 415, 415).render(); var elements = renderers.map((r) => r.element); var translates = elements.map((e) => Utils.getTranslate(e)); @@ -98,9 +94,7 @@ describe("Table layout", () => { // finally the center 'plot' object has a weight renderers[4].rowWeight(1).colWeight(1); - table.anchor(svg); - table.computeLayout(0, 0, 400, 400); - table.render(); + table.anchor(svg).computeLayout(0, 0, 400, 400).render(); var elements = renderers.map((r) => r.element); var translates = elements.map((e) => Utils.getTranslate(e)); From 2b847d0bdff161e2b5284f4e9079985cb5418aa1 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 15:46:20 -0800 Subject: [PATCH 09/61] Add grunt-mocha-phantomjs for commandline testing. Add grunt commands "test" and "watch-test". Modify tests.html to load properly. Close #70. --- Gruntfile.js | 11 +++++++++++ package.json | 3 ++- tests.html | 7 ++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 183e6ede72..e9a499a603 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -74,7 +74,14 @@ module.exports = function(grunt) { "examples": { "tasks": ["ts:examples", "tslint"], "files": ["examples/**.ts"] + }, + "test": { + "tasks": ["test"], + "files": ["src/*.ts", "test/**.ts"] } + }, + mocha_phantomjs: { + all: ['tests.html'] } }); @@ -92,4 +99,8 @@ module.exports = function(grunt) { grunt.registerTask("compile", ["ts:dev", "ts:test", "ts:examples", "tslint", "concat:license"] ); + + grunt.registerTask("test", ["mocha_phantomjs"]); + + grunt.registerTask("watch-test", ["mocha_phantomjs", "watch:test"]); }; diff --git a/package.json b/package.json index 6a1010a20d..65a29baf0f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "grunt-tsd": "0.0.1", "tslint": "~0.4.2", "grunt-tslint": "~0.4.0", - "grunt-contrib-concat": "~0.3.0" + "grunt-contrib-concat": "~0.3.0", + "grunt-mocha-phantomjs": "~0.4.0" } } diff --git a/tests.html b/tests.html index 195a2c5234..5c09d38dc7 100644 --- a/tests.html +++ b/tests.html @@ -20,7 +20,12 @@
From 0783b1e9fb03a734f09a7d706e4b7d190ce7f006 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 15:51:56 -0800 Subject: [PATCH 10/61] Install grunt-contrib-connect, autoserve html on port 7007. Close #91. --- Gruntfile.js | 8 ++++++++ package.json | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index e9a499a603..4e6545067a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -82,6 +82,13 @@ module.exports = function(grunt) { }, mocha_phantomjs: { all: ['tests.html'] + }, + connect: { + server: { + options: { + port: 7007 + } + } } }); @@ -92,6 +99,7 @@ module.exports = function(grunt) { grunt.registerTask("build", [ + "connect", "compile", "watch" ] diff --git a/package.json b/package.json index 65a29baf0f..54e1b864be 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "tslint": "~0.4.2", "grunt-tslint": "~0.4.0", "grunt-contrib-concat": "~0.3.0", - "grunt-mocha-phantomjs": "~0.4.0" + "grunt-mocha-phantomjs": "~0.4.0", + "grunt-contrib-connect": "~0.6.0" } } From 95ce50e53deb68a7f0192a4ad80ea41d3554e14a Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Mon, 10 Feb 2014 16:03:33 -0800 Subject: [PATCH 11/61] Added Scale tests. Moved Coordinator to separate file. Close #81. --- src/coordinator.ts | 27 +++++++++++++++++++++++++++ src/reference.ts | 1 + src/scale.ts | 41 +++++++---------------------------------- test/scaleTests.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 34 deletions(-) create mode 100644 src/coordinator.ts create mode 100644 test/scaleTests.ts diff --git a/src/coordinator.ts b/src/coordinator.ts new file mode 100644 index 0000000000..e00da3ab6f --- /dev/null +++ b/src/coordinator.ts @@ -0,0 +1,27 @@ +/// + +class ScaleDomainCoordinator { + /* This class is responsible for maintaining coordination between linked scales. + It registers event listeners for when one of its scales changes its domain. When the scale + does change its domain, it re-propogates the change to every linked scale. + */ + private currentDomain: any[] = []; + constructor(private scales: Scale[]) { + this.scales.forEach((s) => s.registerListener((sx: Scale) => this.rescale(sx))); + } + + public rescale(scale: Scale) { + var newDomain = scale.domain(); + if (newDomain === this.currentDomain) { + // Avoid forming a really funky call stack with depth proportional to number of scales + // pointer equality check is sufficient in this case + return; + } + this.currentDomain = newDomain; + // This will repropogate the change to every scale, including the scale that + // originated it. This is fine because the scale will check if the new domain is + // different from its current one and will disregard the change if they are equal. + // It would be easy to stop repropogating to the original scale if it mattered. + this.scales.forEach((s) => s.domain(newDomain)); + } +} diff --git a/src/reference.ts b/src/reference.ts index d04664e255..5163dad2eb 100644 --- a/src/reference.ts +++ b/src/reference.ts @@ -2,6 +2,7 @@ /// /// /// +/// /// //grunt-start /// diff --git a/src/scale.ts b/src/scale.ts index 134d28ceba..6e68c89354 100644 --- a/src/scale.ts +++ b/src/scale.ts @@ -44,14 +44,6 @@ class Scale implements IBroadcaster { return new Scale(this.scale.copy()); } - - public widenDomain(newDomain: number[]) { - var currentDomain = this.domain(); - var wideDomain = [Math.min(newDomain[0], currentDomain[0]), Math.max(newDomain[1], currentDomain[1])]; - this.domain(wideDomain); - return this; - } - public registerListener(callback: IBroadcasterCallback) { this.broadcasterCallbacks.push(callback); return this; @@ -75,6 +67,13 @@ class QuantitiveScale extends Scale { public copy(): QuantitiveScale { return new QuantitiveScale(this.scale.copy()); } + + public widenDomain(newDomain: number[]) { + var currentDomain = this.domain(); + var wideDomain = [Math.min(newDomain[0], currentDomain[0]), Math.max(newDomain[1], currentDomain[1])]; + this.domain(wideDomain); + return this; + } } class LinearScale extends QuantitiveScale { @@ -89,29 +88,3 @@ class LinearScale extends QuantitiveScale { return new LinearScale(this.scale.copy()); } } - -class ScaleDomainCoordinator { - /* This class is responsible for maintaining coordination between linked scales. - It registers event listeners for when one of its scales changes its domain. When the scale - does change its domain, it re-propogates the change to every linked scale. - */ - private currentDomain: any[] = []; - constructor(private scales: Scale[]) { - this.scales.forEach((s) => s.registerListener((sx: Scale) => this.rescale(sx))); - } - - public rescale(scale: Scale) { - var newDomain = scale.domain(); - if (newDomain === this.currentDomain) { - // Avoid forming a really funky call stack with depth proportional to number of scales - // pointer equality check is sufficient in this case - return; - } - this.currentDomain = newDomain; - // This will repropogate the change to every scale, including the scale that - // originated it. This is fine because the scale will check if the new domain is - // different from its current one and will disregard the change if they are equal. - // It would be easy to stop repropogating to the original scale if it mattered. - this.scales.forEach((s) => s.domain(newDomain)); - } -} diff --git a/test/scaleTests.ts b/test/scaleTests.ts new file mode 100644 index 0000000000..87c6c794d2 --- /dev/null +++ b/test/scaleTests.ts @@ -0,0 +1,46 @@ +/// + +var assert = chai.assert; + +describe("Scales", () => { + it("Scale's copy() works correctly", () => { + var testCallback: IBroadcasterCallback = (broadcaster: IBroadcaster) => { + return true; // doesn't do anything + }; + var scale = new Scale(d3.scale.linear()); + scale.registerListener(testCallback); + var scaleCopy = scale.copy(); + assert.deepEqual(scale.domain(), scaleCopy.domain(), "Copied scale has the same domain as the original."); + assert.deepEqual(scale.range(), scaleCopy.range(), "Copied scale has the same range as the original."); + assert.notDeepEqual(( scale).broadcasterCallbacks, ( scaleCopy).broadcasterCallbacks, + "Registered callbacks are not copied over"); + }); + + it("Scale alerts listeners when its domain is updated", () => { + var scale = new Scale(d3.scale.linear()); + var callbackWasCalled = false; + var testCallback: IBroadcasterCallback = (broadcaster: IBroadcaster) => { + callbackWasCalled = true; + }; + scale.registerListener(testCallback); + scale.domain([0, 10]); + assert.isTrue(callbackWasCalled, "The registered callback was called"); + }); + + it("QuantitiveScale.widenDomain() functions correctly", () => { + var scale = new QuantitiveScale(d3.scale.linear()); + assert.deepEqual(scale.domain(), [0, 1], "Initial domain is [0, 1]"); + scale.widenDomain([1, 2]); + assert.deepEqual(scale.domain(), [0, 2], "Domain was wided to [0, 2]"); + scale.widenDomain([-1, 1]); + assert.deepEqual(scale.domain(), [-1, 2], "Domain was wided to [-1, 2]"); + scale.widenDomain([0, 1]); + assert.deepEqual(scale.domain(), [-1, 2], "Domain does not get shrink if \"widened\" to a smaller value"); + }); + + it("Linear Scales default to a domain of [Infinity, -Infinity]", () => { + var scale = new LinearScale(); + var domain = scale.domain(); + assert.deepEqual(domain, [Infinity, -Infinity]); + }); +}); From 883e9305c6da4b35a84f5d9a5295055a0c0b7ce7 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Mon, 10 Feb 2014 16:08:30 -0800 Subject: [PATCH 12/61] Put Coordinator farther down in reference.ts. --- src/reference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference.ts b/src/reference.ts index 5163dad2eb..3748ea00c9 100644 --- a/src/reference.ts +++ b/src/reference.ts @@ -2,7 +2,6 @@ /// /// /// -/// /// //grunt-start /// @@ -12,4 +11,5 @@ /// /// /// +/// //grunt-end From 426989161f05ae35855742f0bab5b019856e33be Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Mon, 10 Feb 2014 16:14:46 -0800 Subject: [PATCH 13/61] Modified callback in test to check that it received the scale. --- test/scaleTests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/scaleTests.ts b/test/scaleTests.ts index 87c6c794d2..7fd7762642 100644 --- a/test/scaleTests.ts +++ b/test/scaleTests.ts @@ -20,6 +20,7 @@ describe("Scales", () => { var scale = new Scale(d3.scale.linear()); var callbackWasCalled = false; var testCallback: IBroadcasterCallback = (broadcaster: IBroadcaster) => { + assert.equal(broadcaster, scale, "Callback received the calling scale as the first argument"); callbackWasCalled = true; }; scale.registerListener(testCallback); From cc0c4d5e15e34337efcf8409da304c43ffa48af9 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 17:10:51 -0800 Subject: [PATCH 14/61] Add livereload for super duper awesome --- Gruntfile.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 4e6545067a..3f51a29af3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -60,6 +60,9 @@ module.exports = function(grunt) { files: ["src/*.ts", "test/*.ts"] }, watch: { + "options": { + livereload: true + }, "rebuild": { "tasks": ["build:rebuild"], "files": [ @@ -86,7 +89,8 @@ module.exports = function(grunt) { connect: { server: { options: { - port: 7007 + port: 7007, + livereload: true } } } @@ -99,8 +103,8 @@ module.exports = function(grunt) { grunt.registerTask("build", [ - "connect", "compile", + "connect", "watch" ] ); From 43a197722bf3e486c4395ad20c0f1533b13ed0a9 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Mon, 10 Feb 2014 17:15:49 -0800 Subject: [PATCH 15/61] Oops, unbreak the tests.html browser functionality --- tests.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests.html b/tests.html index 5c09d38dc7..84cafabfce 100644 --- a/tests.html +++ b/tests.html @@ -19,8 +19,9 @@
+ From c7f297556532434ebf5448e9a5336913ba61e7c2 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 13:11:00 -0800 Subject: [PATCH 22/61] Some improvements to component.ts: - addBox throws an error if element not yet anchored (possibly improve in future); - hitBox opacity/fill is set programatically and not in css (since it is structural not stylistic) - addBox sizes the new box if computeLayout already called --- src/component.ts | 15 +++++++++------ style.css | 4 ---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/component.ts b/src/component.ts index 341f4dfae9..972a1e1551 100644 --- a/src/component.ts +++ b/src/component.ts @@ -25,7 +25,7 @@ class Component { public anchor(element: D3.Selection) { if (element.node().childNodes.length > 0) { - throw new Error("Anchoring to a non-empty component is disallowed"); + throw new Error("Anchoring to a non-empty element is disallowed"); } this.element = element; if (this.clipPathEnabled) {this.generateClipPath();}; @@ -37,6 +37,7 @@ class Component { this.hitBox = this.addBox("hit-box"); this.addBox("bounding-box"); + this.hitBox.style("fill", "#ffffff").style("opacity", 0); // We need to set these so Chrome will register events this.registeredInteractions.forEach((r) => r.anchor(this.hitBox)); return this; } @@ -44,7 +45,7 @@ class Component { public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { if (xOffset == null || yOffset == null || availableWidth == null || availableHeight == null) { if (this.element == null) { - throw "anchor() must be called before computeLayout()"; + throw new Error("anchor() must be called before computeLayout()"); } else if (this.element.node().nodeName === "svg") { // we are the root node, let's guess width and height for convenience xOffset = 0; @@ -99,10 +100,16 @@ class Component { } private addBox(className?: string, parentElement?: D3.Selection) { + if (this.element == null) { + throw new Error("Adding boxes before anchoring is currently disallowed"); + } var parentElement = parentElement == null ? this.element : parentElement; var box = parentElement.append("rect"); if (className != null) {box.classed(className, true);}; this.boxes.push(box); + if (this.availableWidth != null && this.availableHeight != null) { + box.attr("width", this.availableWidth).attr("height", this.availableHeight); + } return box; } @@ -125,10 +132,6 @@ class Component { } } - public zoom(translate, scale) { - this.render(); // if not overwritten, a zoom event just causes the component to rerender - } - public classed(cssClass: string): boolean; public classed(cssClass: string, addClass: boolean): Component; public classed(cssClass: string, addClass?:boolean): any { diff --git a/style.css b/style.css index fd3f481b4f..4b0e3b1ec9 100644 --- a/style.css +++ b/style.css @@ -66,10 +66,6 @@ rect { stroke: green; } -.hit-box { - fill: red; - opacity: 0; -} /* .error { From d82e8b1ab41424cf8b00065a0d2e8aa94e9c0f8b Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Tue, 11 Feb 2014 13:12:19 -0800 Subject: [PATCH 23/61] Apply consistent Error([...]) style again Git shenanegans caused error styles fix to be un-applied. Close #83. --- src/component.ts | 8 ++++---- src/table.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/component.ts b/src/component.ts index 972a1e1551..331f08a651 100644 --- a/src/component.ts +++ b/src/component.ts @@ -25,7 +25,7 @@ class Component { public anchor(element: D3.Selection) { if (element.node().childNodes.length > 0) { - throw new Error("Anchoring to a non-empty element is disallowed"); + throw new Error("Can't anchor to a non-empty element"); } this.element = element; if (this.clipPathEnabled) {this.generateClipPath();}; @@ -53,7 +53,7 @@ class Component { availableWidth = parseFloat(this.element.attr("width" )); availableHeight = parseFloat(this.element.attr("height")); } else { - throw "null arguments cannot be passed to computeLayout() on a non-root (non-) node"; + throw new Error("null arguments cannot be passed to computeLayout() on a non-root (non-) node"); } } if (this.rowWeight() === 0 && this.rowMinimum() !== 0) { @@ -67,7 +67,7 @@ class Component { yOffset += availableHeight - this.rowMinimum(); break; default: - throw this.yAlignment + " is not a supported alignment"; + throw new Error(this.yAlignment + " is not a supported alignment"); } availableHeight = this.rowMinimum(); } @@ -82,7 +82,7 @@ class Component { xOffset += availableWidth - this.colMinimum(); break; default: - throw this.xAlignment + " is not a supported alignment"; + throw new Error(this.xAlignment + " is not a supported alignment"); } availableWidth = this.colMinimum(); } diff --git a/src/table.ts b/src/table.ts index 78c5cc19fa..d8d0af2a4e 100644 --- a/src/table.ts +++ b/src/table.ts @@ -49,7 +49,7 @@ class Table extends Component { var freeWidth = this.availableWidth - this.colMinimum(); var freeHeight = this.availableHeight - this.rowMinimum(); if (freeWidth < 0 || freeHeight < 0) { - throw "InsufficientSpaceException"; + throw new Error("InsufficientSpaceError"); } // distribute remaining height to rows @@ -109,7 +109,7 @@ class Table extends Component { public rowMinimum(newVal: number): Component; public rowMinimum(newVal?: number): any { if (newVal != null) { - throw "Row minimum cannot be directly set on Table"; + throw new Error("Row minimum cannot be directly set on Table"); } else { this.rowMinimums = this.rows.map((row: Component[]) => d3.max(row, (r: Component) => r.rowMinimum())); return d3.sum(this.rowMinimums) + this.rowPadding * (this.rows.length - 1); @@ -120,7 +120,7 @@ class Table extends Component { public colMinimum(newVal: number): Component; public colMinimum(newVal?: number): any { if (newVal != null) { - throw "Col minimum cannot be directly set on Table"; + throw new Error("Col minimum cannot be directly set on Table"); } else { this.colMinimums = this.cols.map((col: Component[]) => d3.max(col, (r: Component) => r.colMinimum())); return d3.sum(this.colMinimums) + this.colPadding * (this.cols.length - 1); From 4ebff0b99c78e25ccca137a3d44b32836a3406f4 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 13:54:44 -0800 Subject: [PATCH 24/61] Add unit testing for component.ts. Close #79 --- test/componentTests.ts | 124 +++++++++++++++++++++++++++++++++-------- 1 file changed, 102 insertions(+), 22 deletions(-) diff --git a/test/componentTests.ts b/test/componentTests.ts index 6083d7c7b3..3dc2ba41b8 100644 --- a/test/componentTests.ts +++ b/test/componentTests.ts @@ -3,42 +3,122 @@ var assert = chai.assert; function assertComponentXY(component: Component, x: number, y: number, message: string) { - // use to examine the private variables - var xOffset = ( component).xOffset; - var yOffset = ( component).yOffset; - assert.equal(xOffset, x, message); - assert.equal(yOffset, y, message); + // use to examine the private variables + var xOffset = ( component).xOffset; + var yOffset = ( component).yOffset; + assert.equal(xOffset, x, "X: " + message); + assert.equal(yOffset, y, "Y: " + message); } describe("Component behavior", () => { + var svg: D3.Selection; + var c: Component; + var SVG_WIDTH = 400; + var SVG_HEIGHT = 300; + beforeEach(() => { + svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); + c = new Component(); + }); + + it("components are sized properly when computeLayout is called with explicit arguments", () => { + svg.remove(); + }); + it("fixed-width component will align to the right spot", () => { - var svg = generateSVG(300, 300); - var component = new Component(); - component.rowMinimum(100).colMinimum(100); - component.anchor(svg); - component.computeLayout(); - assertComponentXY(component, 0, 0, "top-left component aligns correctly"); + c.rowMinimum(100).colMinimum(100); + c.anchor(svg); + c.computeLayout(); + assertComponentXY(c, 0, 0, "top-left component aligns correctly"); - component.xAlignment = "CENTER"; - component.yAlignment = "CENTER"; - component.computeLayout(); - assertComponentXY(component, 100, 100, "center component aligns correctly"); + c.xAlignment = "CENTER"; + c.yAlignment = "CENTER"; + c.computeLayout(); + assertComponentXY(c, 150, 100, "center component aligns correctly"); - component.xAlignment = "RIGHT"; - component.yAlignment = "BOTTOM"; - component.computeLayout(); - assertComponentXY(component, 200, 200, "bottom-right component aligns correctly"); + c.xAlignment = "RIGHT"; + c.yAlignment = "BOTTOM"; + c.computeLayout(); + assertComponentXY(c, 300, 200, "bottom-right component aligns correctly"); svg.remove(); }); it("component defaults are as expected", () => { - var c = new Component(); assert.equal(c.rowMinimum(), 0, "rowMinimum defaults to 0"); - assert.equal(c.rowWeight() , 0, "rowWeight defaults to 0"); assert.equal(c.colMinimum(), 0, "colMinimum defaults to 0"); - assert.equal(c.colWeight() , 0, "colWeight defaults to 0"); + assert.equal(c.rowWeight() , 0, "rowWeight defaults to 0"); + assert.equal(c.colWeight() , 0, "colWeight defaults to 0"); assert.equal(c.xAlignment, "LEFT", "xAlignment defaults to LEFT"); assert.equal(c.yAlignment, "TOP" , "yAlignment defaults to TOP"); + svg.remove(); }); + it("you cannot anchor to non-empty elements", () => { + svg.append("rect"); + assert.throws(() => c.anchor(svg), Error); + svg.remove(); + }); + + it("you cannot computeLayout before anchoring", () => { + assert.throws(() => c.computeLayout(), Error); + svg.remove(); + }); + + it("getters and setters work as expected", () => { + c.rowMinimum(12); + assert.equal(c.rowMinimum(), 12, "rowMinimum setter works"); + c.colMinimum(14); + assert.equal(c.colMinimum(), 14, "colMinimum setter works"); + c.rowWeight(16); + assert.equal(c.rowWeight(), 16, "rowWeight setter works"); + c.colWeight(18); + assert.equal(c.colWeight(), 18, "colWeight setter works"); + svg.remove(); + }); + + it("clipPath works as expected", () => { + assert.isFalse(c.clipPathEnabled, "clipPathEnabled defaults to false"); + c.clipPathEnabled = true; + c.anchor(svg).computeLayout(0, 0, 100, 100).render(); + assert.equal(( Component).clipPathId, 1, "clipPathId incremented"); + assert.equal(c.element.attr("clip-path"), "url(#clipPath0)", "the element has clip-path url attached"); + var clipRect = c.element.select(".clip-rect"); + assert.equal(clipRect.attr("width"), 100, "the clipRect has an appropriate width"); + assert.equal(clipRect.attr("height"), 100, "the clipRect has an appropriate height"); + svg.remove(); + }); + + it("boxes work as expected", () => { + assert.throws(() => ( c).addBox("pre-anchor"), Error, "Adding boxes before anchoring is currently disallowed"); + c.anchor(svg).computeLayout().render(); + ( c).addBox("post-anchor"); + var e = c.element; + var boxStrings = [".hit-box", ".bounding-box", ".post-anchor"]; + + boxStrings.forEach((s) => { + var box = e.select(s); + assert.isNotNull(box.node(), s + " box was created"); + var bb = Utils.getBBox(box); + assert.equal(bb.width, SVG_WIDTH, s + " width as expected"); + assert.equal(bb.height, SVG_HEIGHT, s + " height as expected"); + }); + + var hitBox = ( c).hitBox; + assert.equal(hitBox.style("fill"), "#ffffff", "the hitBox has a fill, ensuring that it will detect events"); + assert.equal(hitBox.style("opacity"), "0", "the hitBox is transparent, otherwise it would look weird"); + svg.remove(); + }); + + it("interaction registration works properly", () => { + var hitBox1: Element = null; + var hitBox2: Element = null; + var interaction1: any = {anchor: (hb) => hitBox1 = hb.node()}; + var interaction2: any = {anchor: (hb) => hitBox2 = hb.node()}; + c.registerInteraction(interaction1); + c.anchor(svg).computeLayout().render(); + c.registerInteraction(interaction2); + var hitNode = c.hitBox.node(); + assert.equal(hitBox1, hitNode, "hitBox1 was registerd"); + assert.equal(hitBox2, hitNode, "hitBox2 was registerd"); + svg.remove(); + }); }); From 48402ecebf41051d1d17fbf871992afb0e07cb41 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 14:04:29 -0800 Subject: [PATCH 25/61] Minor change to an error message --- src/component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component.ts b/src/component.ts index 972a1e1551..0eeb54fdaf 100644 --- a/src/component.ts +++ b/src/component.ts @@ -45,7 +45,7 @@ class Component { public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { if (xOffset == null || yOffset == null || availableWidth == null || availableHeight == null) { if (this.element == null) { - throw new Error("anchor() must be called before computeLayout()"); + throw new Error("anchor must be called before computeLayout"); } else if (this.element.node().nodeName === "svg") { // we are the root node, let's guess width and height for convenience xOffset = 0; From 4fcc5c6197cd4f404918acd7c75c4decc44cf846 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 14:04:45 -0800 Subject: [PATCH 26/61] Move some utils from tableTests into testUtils --- test/tableTests.ts | 11 ----------- test/testUtils.ts | 11 +++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/tableTests.ts b/test/tableTests.ts index b222649b7a..60191f1ad4 100644 --- a/test/tableTests.ts +++ b/test/tableTests.ts @@ -21,17 +21,6 @@ function generateBasicTable(nRows, nCols) { return {"table": table, "renderers": renderers}; } -function getTranslate(element: D3.Selection) { - return d3.transform(element.attr("transform")).translate; -} - -function assertBBoxEquivalence(bbox, widthAndHeightPair, message) { - var width = widthAndHeightPair[0]; - var height = widthAndHeightPair[1]; - assert.equal(bbox.width, width, "width: " + message); - assert.equal(bbox.height, height, "height: " + message); -} - describe("Table layout", () => { it("basic table with 2 rows 2 cols lays out properly", () => { diff --git a/test/testUtils.ts b/test/testUtils.ts index 9e3bd00c2d..3ee3886870 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -13,3 +13,14 @@ function generateSVG(width=400, height=400) { var svg = parent.append("svg").attr("width", width).attr("height", height); return svg; } + +function getTranslate(element: D3.Selection) { + return d3.transform(element.attr("transform")).translate; +} + +function assertBBoxEquivalence(bbox, widthAndHeightPair, message) { + var width = widthAndHeightPair[0]; + var height = widthAndHeightPair[1]; + assert.equal(bbox.width, width, "width: " + message); + assert.equal(bbox.height, height, "height: " + message); +} From b3c3a31794ca21f1f8c5c90b8daab0ca689b99f9 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 14:11:35 -0800 Subject: [PATCH 27/61] Add more tests for component --- test/componentTests.ts | 62 +++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/test/componentTests.ts b/test/componentTests.ts index 3dc2ba41b8..20021066f4 100644 --- a/test/componentTests.ts +++ b/test/componentTests.ts @@ -20,8 +20,55 @@ describe("Component behavior", () => { c = new Component(); }); - it("components are sized properly when computeLayout is called with explicit arguments", () => { - svg.remove(); + describe("anchor", () => { + it("anchoring works as expected", () => { + c.anchor(svg); + assert.equal(c.element, svg, "the component anchored to the svg"); + svg.remove(); + }); + + it("you cannot anchor to non-empty elements", () => { + svg.append("rect"); + assert.throws(() => c.anchor(svg), Error); + svg.remove(); + }); + }); + + describe("computeLayout", () => { + it("computeLayout defaults intelligently", () => { + c.anchor(svg).computeLayout(); + assert.equal(c.availableWidth, SVG_WIDTH, "computeLayout defaulted width to svg width"); + assert.equal(c.availableHeight, SVG_HEIGHT, "computeLayout defaulted height to svg height"); + assert.equal(( c).xOffset, 0 ,"xOffset defaulted to 0"); + assert.equal(( c).yOffset, 0 ,"yOffset defaulted to 0"); + svg.remove(); + }); + + it("computeLayout will not default when attached to non-root node", () => { + var g = svg.append("g"); + c.anchor(g); + assert.throws(() => c.computeLayout(), "null arguments"); + svg.remove(); + }); + + it("computeLayout throws an error when called on un-anchored component", () => { + assert.throws(() => c.computeLayout(), Error, "anchor must be called before computeLayout"); + svg.remove(); + }); + + it("computeLayout uses its arguments apropriately", () => { + var g = svg.append("g"); + var xOff = 10; + var yOff = 20; + var width = 100; + var height = 200; + c.anchor(g).computeLayout(xOff, yOff, width, height); + var translate = getTranslate(c.element); + assert.deepEqual(translate, [xOff, yOff], "the element translated appropriately"); + assert.equal(c.availableWidth, width, "the width set properly"); + assert.equal(c.availableHeight, height, "the height set propery"); + svg.remove(); + }); }); it("fixed-width component will align to the right spot", () => { @@ -52,17 +99,6 @@ describe("Component behavior", () => { svg.remove(); }); - it("you cannot anchor to non-empty elements", () => { - svg.append("rect"); - assert.throws(() => c.anchor(svg), Error); - svg.remove(); - }); - - it("you cannot computeLayout before anchoring", () => { - assert.throws(() => c.computeLayout(), Error); - svg.remove(); - }); - it("getters and setters work as expected", () => { c.rowMinimum(12); assert.equal(c.rowMinimum(), 12, "rowMinimum setter works"); From 519351e1671bd432905f763e3bca0ab6ccd5f79c Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 15:22:34 -0800 Subject: [PATCH 28/61] Add more unit tests for table.ts. #24 --- test/tableTests.ts | 71 ++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/test/tableTests.ts b/test/tableTests.ts index 60191f1ad4..a6bf08abbb 100644 --- a/test/tableTests.ts +++ b/test/tableTests.ts @@ -4,34 +4,55 @@ var assert = chai.assert; function generateBasicTable(nRows, nCols) { // makes a table with exactly nRows * nCols children in a regular grid, with each - // child being a basic Renderer (todo: maybe change to basic component) + // child being a basic component var emptyDataset: IDataset = {data: [], seriesName: "blah"}; - var rows: Renderer[][] = []; - var renderers: Renderer[] = []; + var rows: Component[][] = []; + var components: Component[] = []; for(var i=0; i { +describe("Tables", () => { + it("tables are classed properly", () => { + var table = new Table([[]]); + assert.isTrue(table.classed("table")); + }); + + it("tables transform null instances into base components", () => { + var table = new Table([[null]]); // table with a single null component + var component = table.rows[0][0]; + assert.isNotNull(component, "the component is not null"); + assert.equal(component.constructor.name, "Component", "the component is a base Component"); + }); + + it("tables with insufficient space throw InsufficientSpaceError", () => { + var svg = generateSVG(200, 200); + var c = new Component(); + c.rowMinimum(300).colMinimum(300); + var t = new Table([[c]]); + t.anchor(svg); + assert.throws(() => t.computeLayout(), Error, "InsufficientSpaceError"); + svg.remove(); + }); it("basic table with 2 rows 2 cols lays out properly", () => { - var tableAndRenderers = generateBasicTable(2,2); - var table = tableAndRenderers.table; - var renderers = tableAndRenderers.renderers; + var tableAndcomponents = generateBasicTable(2,2); + var table = tableAndcomponents.table; + var components = tableAndcomponents.components; var svg = generateSVG(); table.anchor(svg).computeLayout().render(); - var elements = renderers.map((r) => r.element); + var elements = components.map((r) => r.element); var translates = elements.map((e) => getTranslate(e)); assert.deepEqual(translates[0], [0, 0], "first element is centered at origin"); assert.deepEqual(translates[1], [200, 0], "second element is located properly"); @@ -46,16 +67,16 @@ describe("Table layout", () => { }); it("table with 2 rows 2 cols and margin/padding lays out properly", () => { - var tableAndRenderers = generateBasicTable(2,2); - var table = tableAndRenderers.table; - var renderers = tableAndRenderers.renderers; + var tableAndcomponents = generateBasicTable(2,2); + var table = tableAndcomponents.table; + var components = tableAndcomponents.components; table.padding(5,5); var svg = generateSVG(415, 415); table.anchor(svg).computeLayout().render(); - var elements = renderers.map((r) => r.element); + var elements = components.map((r) => r.element); var translates = elements.map((e) => getTranslate(e)); var bboxes = elements.map((e) => Utils.getBBox(e)); assert.deepEqual(translates[0], [0, 0], "first element is centered properly"); @@ -71,25 +92,25 @@ describe("Table layout", () => { it("table with fixed-size objects on every side lays out properly", () => { var svg = generateSVG(); - var tableAndRenderers = generateBasicTable(3,3); - var table = tableAndRenderers.table; - var renderers = tableAndRenderers.renderers; + var tableAndcomponents = generateBasicTable(3,3); + var table = tableAndcomponents.table; + var components = tableAndcomponents.components; // [0 1 2] \\ // [3 4 5] \\ // [6 7 8] \\ // First, set everything to have no weight - renderers.forEach((r) => r.colWeight(0).rowWeight(0).colMinimum(0).rowMinimum(0)); + components.forEach((r) => r.colWeight(0).rowWeight(0).colMinimum(0).rowMinimum(0)); // give the axis-like objects a minimum - renderers[1].rowMinimum(30); - renderers[7].rowMinimum(30); - renderers[3].colMinimum(50); - renderers[5].colMinimum(50); + components[1].rowMinimum(30); + components[7].rowMinimum(30); + components[3].colMinimum(50); + components[5].colMinimum(50); // finally the center 'plot' object has a weight - renderers[4].rowWeight(1).colWeight(1); + components[4].rowWeight(1).colWeight(1); table.anchor(svg).computeLayout().render(); - var elements = renderers.map((r) => r.element); + var elements = components.map((r) => r.element); var translates = elements.map((e) => getTranslate(e)); var bboxes = elements.map((e) => Utils.getBBox(e)); // test the translates From dd5364778a9e62112876f2fa1603fbebcf75bcf5 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 15:41:02 -0800 Subject: [PATCH 29/61] Fix boxes test so it passes in FF --- test/componentTests.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/componentTests.ts b/test/componentTests.ts index 20021066f4..bf3201dfa1 100644 --- a/test/componentTests.ts +++ b/test/componentTests.ts @@ -139,7 +139,9 @@ describe("Component behavior", () => { }); var hitBox = ( c).hitBox; - assert.equal(hitBox.style("fill"), "#ffffff", "the hitBox has a fill, ensuring that it will detect events"); + var hitBoxFill = hitBox.style("fill"); + var hitBoxFilled = hitBoxFill === "#ffffff" || hitBoxFill === "rgb(255, 255, 255)"; + assert.isTrue(hitBoxFilled, hitBoxFill + "<- this should be filled, so the hitbox will detect events"); assert.equal(hitBox.style("opacity"), "0", "the hitBox is transparent, otherwise it would look weird"); svg.remove(); }); From 31763753f407414c24dd7a8524d8becb5a2363a2 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 15:41:12 -0800 Subject: [PATCH 30/61] Add cast to access private vars --- test/tableTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tableTests.ts b/test/tableTests.ts index a6bf08abbb..1fc8d7df02 100644 --- a/test/tableTests.ts +++ b/test/tableTests.ts @@ -29,7 +29,7 @@ describe("Tables", () => { it("tables transform null instances into base components", () => { var table = new Table([[null]]); // table with a single null component - var component = table.rows[0][0]; + var component = ( table).rows[0][0]; assert.isNotNull(component, "the component is not null"); assert.equal(component.constructor.name, "Component", "the component is a base Component"); }); From fbbde93b434beffd4826846205d1c7e765c43ca9 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 16:28:05 -0800 Subject: [PATCH 31/61] Add unit testing for base renderer class, and slightly modify renderer functionality --- src/renderer.ts | 12 ++++-------- test/rendererTests.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 test/rendererTests.ts diff --git a/src/renderer.ts b/src/renderer.ts index 0f4d997581..5175a42b6c 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -8,9 +8,7 @@ class Renderer extends Component { public element: D3.Selection; public scales: Scale[]; - constructor( - dataset: IDataset - ) { + constructor(dataset: IDataset = {seriesName: "", data: []}) { super(); super.rowWeight(1); super.colWeight(1); @@ -21,14 +19,12 @@ class Renderer extends Component { } public data(dataset: IDataset): Renderer { + this.renderArea.classed(this.dataset.seriesName, false); this.dataset = dataset; + this.renderArea.classed(dataset.seriesName, true); return this; } - public zoom(translate, scale) { - this.renderArea.attr("transform", "translate("+translate+") scale("+scale+")"); - } - public anchor(element: D3.Selection) { super.anchor(element); this.renderArea = element.append("g").classed("render-area", true).classed(this.dataset.seriesName, true); @@ -41,7 +37,7 @@ interface IAccessor { }; class XYRenderer extends Renderer { - private static CSS_CLASS = "x-y-renderer"; + private static CSS_CLASS = "xy-renderer"; public dataSelection: D3.UpdateSelection; private static defaultXAccessor = (d: any) => d.x; private static defaultYAccessor = (d: any) => d.y; diff --git a/test/rendererTests.ts b/test/rendererTests.ts new file mode 100644 index 0000000000..e00fee2a97 --- /dev/null +++ b/test/rendererTests.ts @@ -0,0 +1,36 @@ +/// + +var assert = chai.assert; + +describe("Renderers", () => { + var svg: D3.Selection; + var SVG_WIDTH = 400; + var SVG_HEIGHT = 300; + beforeEach(() => { + svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); + }); + + it("Renderers default correctly", () => { + var r = new Renderer(); + assert.equal(r.colWeight(), 1, "colWeight defaults to 1"); + assert.equal(r.rowWeight(), 1, "rowWeight defaults to 1"); + assert.isTrue(r.clipPathEnabled, "clipPathEnabled defaults to true"); + svg.remove(); + }); + + it("Base renderer functionality works", () => { + var d1 = {data: ["foo"], seriesName: "bar"}; + var r = new Renderer(d1); + r.anchor(svg).computeLayout(); + var renderArea = r.element.select(".render-area"); + assert.isNotNull(renderArea.node(), "there is a render-area"); + assert.isTrue(renderArea.classed("bar"), "the render area is classed w/ dataset.seriesName"); + assert.deepEqual(r.dataset, d1, "the dataset is set properly"); + var d2 = {data: ["bar"], seriesName: "boo"}; + r.data(d2); + assert.isFalse(renderArea.classed("bar"), "the renderArea is no longer classed bar"); + assert.isTrue(renderArea.classed("boo"), "the renderArea is now classed boo"); + assert.deepEqual(r.dataset, d2, "the dataset was replaced properly"); + svg.remove(); + }); +}); From 72d51ff092da1e19638a9d10bb647a35b62da186 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 17:42:33 -0800 Subject: [PATCH 32/61] Small refactor, now invertXYSelectionArea returns a fullSelectionArea rather than just a DataArea. --- src/interaction.ts | 3 +-- src/renderer.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/interaction.ts b/src/interaction.ts index 114164115f..80e2809066 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -136,8 +136,7 @@ class AreaInteraction extends Interaction { var yMin = Math.min(this.origin[1], this.location[1]); var yMax = Math.max(this.origin[1], this.location[1]); var pixelArea = {xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax}; - var dataArea = this.rendererComponent.invertXYSelectionArea(pixelArea); - var fullArea = {pixel: pixelArea, data: dataArea}; + var fullArea = this.rendererComponent.invertXYSelectionArea(pixelArea); if (this.areaCallback != null) { this.areaCallback(fullArea); } diff --git a/src/renderer.ts b/src/renderer.ts index 5175a42b6c..950dba83e3 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -73,12 +73,13 @@ class XYRenderer extends Renderer { return this; } - public invertXYSelectionArea(area: SelectionArea) { - var xMin = this.xScale.invert(area.xMin); - var xMax = this.xScale.invert(area.xMax); - var yMin = this.yScale.invert(area.yMin); - var yMax = this.yScale.invert(area.yMax); - return {xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax}; + public invertXYSelectionArea(pixelArea: SelectionArea): FullSelectionArea { + var xMin = this.xScale.invert(pixelArea.xMin); + var xMax = this.xScale.invert(pixelArea.xMax); + var yMin = this.yScale.invert(pixelArea.yMin); + var yMax = this.yScale.invert(pixelArea.yMax); + var dataArea = {xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax}; + return {pixel: pixelArea, data: dataArea}; } public getSelectionFromArea(area: FullSelectionArea) { From c997cc858b8b25c26ca4d2bb872d9c7668805d42 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 17:43:10 -0800 Subject: [PATCH 33/61] Add some unit testing for the Renderer classes per #26. More to come. --- test/rendererTests.ts | 127 +++++++++++++++++++++++++++++++++--------- 1 file changed, 101 insertions(+), 26 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index e00fee2a97..773d80dd21 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -2,35 +2,110 @@ var assert = chai.assert; +function makeQuadraticSeries(n: number): IDataset { + function makePoint(x: number) { + return {x: x, y: x*x}; + } + var data = d3.range(n).map(makePoint); + return {data: data, seriesName: "quadratic-series"}; +} + +var dataset1 = makeQuadraticSeries(10); + describe("Renderers", () => { - var svg: D3.Selection; - var SVG_WIDTH = 400; - var SVG_HEIGHT = 300; - beforeEach(() => { - svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); - }); - it("Renderers default correctly", () => { - var r = new Renderer(); - assert.equal(r.colWeight(), 1, "colWeight defaults to 1"); - assert.equal(r.rowWeight(), 1, "rowWeight defaults to 1"); - assert.isTrue(r.clipPathEnabled, "clipPathEnabled defaults to true"); - svg.remove(); + describe("base Renderer", () => { + it("Renderers default correctly", () => { + var r = new Renderer(); + assert.equal(r.colWeight(), 1, "colWeight defaults to 1"); + assert.equal(r.rowWeight(), 1, "rowWeight defaults to 1"); + assert.isTrue(r.clipPathEnabled, "clipPathEnabled defaults to true"); + console.log("wee"); + }); + + it("Base renderer functionality works", () => { + var svg = generateSVG(400, 300); + var d1 = {data: ["foo"], seriesName: "bar"}; + var r = new Renderer(d1); + r.anchor(svg).computeLayout(); + var renderArea = r.element.select(".render-area"); + assert.isNotNull(renderArea.node(), "there is a render-area"); + assert.isTrue(renderArea.classed("bar"), "the render area is classed w/ dataset.seriesName"); + assert.deepEqual(r.dataset, d1, "the dataset is set properly"); + var d2 = {data: ["bar"], seriesName: "boo"}; + r.data(d2); + assert.isFalse(renderArea.classed("bar"), "the renderArea is no longer classed bar"); + assert.isTrue(renderArea.classed("boo"), "the renderArea is now classed boo"); + assert.deepEqual(r.dataset, d2, "the dataset was replaced properly"); + svg.remove(); + }); }); - it("Base renderer functionality works", () => { - var d1 = {data: ["foo"], seriesName: "bar"}; - var r = new Renderer(d1); - r.anchor(svg).computeLayout(); - var renderArea = r.element.select(".render-area"); - assert.isNotNull(renderArea.node(), "there is a render-area"); - assert.isTrue(renderArea.classed("bar"), "the render area is classed w/ dataset.seriesName"); - assert.deepEqual(r.dataset, d1, "the dataset is set properly"); - var d2 = {data: ["bar"], seriesName: "boo"}; - r.data(d2); - assert.isFalse(renderArea.classed("bar"), "the renderArea is no longer classed bar"); - assert.isTrue(renderArea.classed("boo"), "the renderArea is now classed boo"); - assert.deepEqual(r.dataset, d2, "the dataset was replaced properly"); - svg.remove(); + describe("CircleRenderer", () => { + describe("Example CircleRenderer with quadratic series", () => { + var svg: D3.Selection; + var xScale: LinearScale; + var yScale: LinearScale; + var circleRenderer: CircleRenderer; + var pixelAreaFull: SelectionArea; + var pixelAreaPartial: SelectionArea; + + before(() => { + svg = generateSVG(600, 300); + xScale = new LinearScale(); + yScale = new LinearScale(); + circleRenderer = new CircleRenderer(dataset1, xScale, yScale); + circleRenderer.anchor(svg).computeLayout().render(); + pixelAreaFull = {xMin: 0, xMax: 600, yMin: 0, yMax: 300}; + pixelAreaPartial = {xMin: 200, xMax: 600, yMin: 100, yMax: 200}; + }); + + it("setup is handled properly", () => { + assert.deepEqual(xScale.domain(), [0, 9], "xScale domain was set by the renderer"); + assert.deepEqual(yScale.domain(), [0, 81], "yScale domain was set by the renderer"); + assert.deepEqual(xScale.range(), [0, 600], "xScale range was set by the renderer"); + assert.deepEqual(yScale.range(), [300, 0], "yScale range was set by the renderer"); + assert.lengthOf(circleRenderer.renderArea.selectAll("circle")[0], 10, "10 circles were drawn"); + }); + + it("invertXYSelectionArea works", () => { + var expectedDataAreaFull = {xMin: 0, xMax: 9, yMin: 81, yMax: 0}; + var actualDataAreaFull = circleRenderer.invertXYSelectionArea(pixelAreaFull).data; + assert.deepEqual(actualDataAreaFull, expectedDataAreaFull, "the full data area is as expected"); + + var expectedDataAreaPartial = {xMin: 3, xMax: 9, yMin: 54, yMax: 27}; + var actualDataAreaPartial = circleRenderer.invertXYSelectionArea(pixelAreaPartial).data; + + assert.closeTo(actualDataAreaPartial.xMin, expectedDataAreaPartial.xMin, 1, "partial xMin is close"); + assert.closeTo(actualDataAreaPartial.xMax, expectedDataAreaPartial.xMax, 1, "partial xMax is close"); + assert.closeTo(actualDataAreaPartial.yMin, expectedDataAreaPartial.yMin, 1, "partial yMin is close"); + assert.closeTo(actualDataAreaPartial.yMax, expectedDataAreaPartial.yMax, 1, "partial yMax is close"); + }); + + it("getSelectionFromArea works", () => { + var fullSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaFull); + var selectionFull = circleRenderer.getSelectionFromArea(fullSelectionArea); + assert.lengthOf(selectionFull[0], 10, "all 10 circles were selected by the full region"); + + var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); + var selectionPartial = circleRenderer.getSelectionFromArea(partialSelectionArea); + assert.lengthOf(selectionPartial[0], 2, "2 circles were selected by the partial region"); + }); + + it("getDataIndicesFromArea works", () => { + var fullSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaFull); + var indicesFull = circleRenderer.getDataIndicesFromArea(fullSelectionArea); + assert.deepEqual(indicesFull, d3.range(10), "all 10 circles were selected by the full region"); + + var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); + var indicesPartial = circleRenderer.getDataIndicesFromArea(partialSelectionArea); + assert.deepEqual(indicesPartial, [6, 7], "2 circles were selected by the partial region"); + }); + + after(() => { + svg.remove(); + }); + + }); }); }); From 0d3f3bc5fa01fd25d61408e23e9f94dfbd1ca433 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Tue, 11 Feb 2014 17:47:06 -0800 Subject: [PATCH 34/61] No wee :/ --- test/rendererTests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index 773d80dd21..e47fc50eef 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -20,7 +20,6 @@ describe("Renderers", () => { assert.equal(r.colWeight(), 1, "colWeight defaults to 1"); assert.equal(r.rowWeight(), 1, "rowWeight defaults to 1"); assert.isTrue(r.clipPathEnabled, "clipPathEnabled defaults to true"); - console.log("wee"); }); it("Base renderer functionality works", () => { From cd6ea6b0248046a637f68edf419ca3a046016ff1 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Tue, 11 Feb 2014 14:19:11 -0800 Subject: [PATCH 35/61] Label now truncates; fixed label tests. Col/Row minimum is no longer set for horizontal/veritical labels. If the text is too long, the label is truncated intelligently and ellipses are appended. Italic text is padded a bit to compensate for the tilted letters. Close #84. Close #89. --- src/label.ts | 97 ++++++++++++++++++++++++++++++++++++++++------ style.css | 2 +- test/labelTests.ts | 85 ++++++++++++++++++++-------------------- 3 files changed, 130 insertions(+), 54 deletions(-) diff --git a/src/label.ts b/src/label.ts index 36483fea00..795e6dae77 100644 --- a/src/label.ts +++ b/src/label.ts @@ -7,8 +7,11 @@ class Label extends Component { public yAlignment = "CENTER"; private textElement: D3.Selection; - private text: string; + private text: string; // text assigned to the element; may not be the actual text displaed due to truncation + private displayText: string; private orientation: string; + private textLength: number; + private textHeight: number; constructor(text = "", orientation = "horizontal") { super(); @@ -36,23 +39,95 @@ class Label extends Component { private setMinimumsByCalculatingTextSize() { this.textElement.attr("dy", 0); // Reset this so we maintain idempotence - var bbox = Utils.getBBox(this.element); + var bbox = Utils.getBBox(this.textElement); this.textElement.attr("dy", -bbox.y); - var clientHeight = bbox.height; - var clientWidth = bbox.width; + this.textHeight = bbox.height; + this.textLength = bbox.width; + + // italic text needs a slightly larger bounding box + if (this.textElement.style("font-style") === "italic") { + var textNode = this.textElement.node(); + // pad by half the width of the last character + this.textLength += 0.5 * textNode.getExtentOfChar(textNode.textContent.length-1).width; + } + + if (this.orientation === "horizontal") { + this.rowMinimum(this.textHeight); + } else { + this.colMinimum(this.textHeight); + } + } + + private truncateTextToLength(availableLength: number) { + this.textElement.text(this.text + "..."); + var textNode = this.textElement.node(); + var numChars = textNode.textContent.length; + var dotLength = textNode.getSubStringLength(numChars-3, 3); + for (var i=0; i availableLength) { + if (i > 0) { + this.textElement.text(this.text.substr(0, i-1).trim() + "..."); + } else { + this.textElement.text(""); // no room even for ellipsis + } + this.setMinimumsByCalculatingTextSize(); + return; + } + } + } + + public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { + super.computeLayout(xOffset, yOffset, availableWidth, availableHeight); + + var xShift = 0; + var yShift = 0; if (this.orientation === "horizontal") { - this.rowMinimum(clientHeight); - this.colMinimum(clientWidth); + if (this.availableWidth < this.textLength) { + this.truncateTextToLength(this.availableWidth); + } + switch (this.xAlignment) { + case "LEFT": + break; + case "CENTER": + xShift = (this.availableWidth - this.textLength) / 2; + break; + case "RIGHT": + xShift = this.availableWidth - this.textLength; + break; + default: + throw this.xAlignment + " is not a supported alignment"; + } } else { - this.colMinimum(clientHeight); - this.rowMinimum(clientWidth); + if (this.availableHeight < this.textLength) { + this.truncateTextToLength(this.availableHeight); + } + switch (this.yAlignment) { + case "TOP": + break; + case "CENTER": + xShift = (this.availableHeight - this.textLength) / 2; + break; + case "BOTTOM": + xShift = this.availableHeight - this.textLength; + break; + default: + throw this.yAlignment + " is not a supported alignment"; + } + if (this.orientation === "vertical-right") { - this.textElement.attr("transform", "rotate(90)").attr("y", -clientHeight); - } else if (this.orientation === "vertical-left") { - this.textElement.attr("transform", "rotate(-90)").attr("x", -clientWidth); + this.textElement.attr("transform", "rotate(90)"); + yShift = -this.textHeight; + } else { // vertical-left + this.textElement.attr("transform", "rotate(-90)"); + xShift = -xShift - this.textLength; // flip xShift } } + + this.textElement.attr("x", xShift); + this.textElement.attr("y", yShift); + return this; } } diff --git a/style.css b/style.css index 4b0e3b1ec9..fb1501ac49 100644 --- a/style.css +++ b/style.css @@ -7,7 +7,7 @@ div { display: inline; } -.text-label text { +.label text { font-family: Helvetica; } diff --git a/test/labelTests.ts b/test/labelTests.ts index 14e11e2577..e68869a610 100644 --- a/test/labelTests.ts +++ b/test/labelTests.ts @@ -18,7 +18,6 @@ describe("Labels", () => { var text = element.select("text"); var bbox = Utils.getBBox(text); - assert.equal(bbox.width, label.colMinimum(), "text width === label.colMinimum()"); assert.equal(bbox.height, label.rowMinimum(), "text height === label.rowMinimum()"); assert.equal(0, label.colWeight(), "label.colWeight is 0"); assert.equal(0, label.rowWeight(), "label.rowWeight is 0"); @@ -26,36 +25,46 @@ describe("Labels", () => { svg.remove(); }); - it("Italicized text is handled properly", () => { - var svg = generateSVG(400, 80); - var label = new TitleLabel("A CHART TITLE"); + it("Left-rotated text is handled properly", () => { + var svg = generateSVG(100, 400); + var label = new AxisLabel("LEFT-ROTATED LABEL", "vertical-left"); label.anchor(svg); var element = label.element; var text = element.select("text"); - text.style("font-style", "italic"); - ( label).setMinimumsByCalculatingTextSize(); // to access private method label.computeLayout(); label.render(); var bbox = Utils.getBBox(text); - assert.operator(bbox.width, "<", label.colMinimum(), "text width is less than the col minimum (to account for italicized overhang)"); - assert.equal(bbox.height, label.rowMinimum(), "text height === label.rowMinimum()"); + assert.equal(bbox.height, label.colMinimum(), "text height === label.colMinimum() (it's rotated)"); + assert.equal(text.attr("transform"), "rotate(-90)", "the text element is rotated -90 degrees"); svg.remove(); - }); + }); - it("Rotated text is handled properly", () => { + it("Right-rotated text is handled properly", () => { var svg = generateSVG(100, 400); - var label = new AxisLabel("LEFT-ROTATED LABEL", "vertical-left"); + var label = new AxisLabel("RIGHT-ROTATED LABEL", "vertical-right"); label.anchor(svg); var element = label.element; var text = element.select("text"); label.computeLayout(); label.render(); var bbox = Utils.getBBox(text); - assert.equal(bbox.width, label.rowMinimum(), "text width === label.rowMinimum() (its rotated)"); - assert.equal(bbox.height, label.colMinimum(), "text height === label.colMinimum() (its rotated)"); - assert.equal(text.attr("transform"), "rotate(-90)", "the text element is rotated -90 degrees"); + assert.equal(bbox.height, label.colMinimum(), "text height === label.colMinimum() (it's rotated)"); + assert.equal(text.attr("transform"), "rotate(90)", "the text element is rotated 90 degrees"); + svg.remove(); + }); + + it("Label text can be changed after label is created", () => { + var svg = generateSVG(400, 80); + var label = new TitleLabel(); + label.anchor(svg); + var textEl = label.element.select("text"); + assert.equal(textEl.text(), "", "the text defaulted to empty string when constructor was called w/o arguments"); + assert.equal(label.rowMinimum(), 0, "rowMin is 0 for empty string"); + label.setText("hello world"); + assert.equal(textEl.text(), "hello world", "the label text updated properly"); + assert.operator(label.rowMinimum(), ">", 0, "rowMin is > 0 for non-empty string"); svg.remove(); - }); + }); it("Superlong text is handled in a sane fashion", () => { var svgWidth = 400; @@ -67,40 +76,32 @@ describe("Labels", () => { label.computeLayout(); label.render(); var bbox = Utils.getBBox(text); - assert.equal(bbox.width, label.colMinimum(), "text width === label.colMinimum()"); assert.equal(bbox.height, label.rowMinimum(), "text height === label.rowMinimum()"); assert.operator(bbox.width, "<=", svgWidth, "the text is not wider than the SVG width"); - assert.equal(text.attr("transform"), "rotate(-90)", "the text element is rotated -90 degrees"); svg.remove(); - }); + }); - it("Labels with different font sizes have different space requirements", () => { + it("Italicized text is handled properly", () => { var svg = generateSVG(400, 80); var label = new TitleLabel("A CHART TITLE"); label.anchor(svg); - label.element.select("text").style("font-size", "18pt"); - ( label).setMinimumsByCalculatingTextSize(); - var originalWidth = label.colMinimum(); - label.element.select("text").style("font-size", "6pt"); - ( label).setMinimumsByCalculatingTextSize(); - var newWidth = label.colMinimum(); - assert.operator(newWidth, "<", originalWidth, "Smaller font size implies smaller label width"); - - svg.remove(); - }); + var text = label.element.select("text"); + ( label).setMinimumsByCalculatingTextSize(); // to access private method + svg.attr("width", ( label).textLength); // to access private field + label.computeLayout(); + label.render(); + var svgWidth = Number(svg.attr("width")); + assert.equal(( label).textHeight, label.rowMinimum(), "text height === label.rowMinimum()"); + assert.operator(( label).textLength, "<=", svgWidth, "the non-italic text is not wider than the SVG width"); - it("Label text can be changed after label is created", () => { - var svg = generateSVG(400, 80); - var label = new TitleLabel(); - label.anchor(svg); - var textEl = label.element.select("text"); - assert.equal(textEl.text(), "", "the text defaulted to empty string when constructor was called w/o arguments"); - assert.equal(label.rowMinimum(), 0, "rowMin is 0 for empty string"); - assert.equal(label.colMinimum(), 0, "colMin is 0 for empty string"); - label.setText("hello world"); - assert.equal(textEl.text(), "hello world", "the label text updated properly"); - assert.operator(label.rowMinimum(), ">", 0, "rowMin is > 0 for non-empty string"); - assert.operator(label.colMinimum(), ">", 0, "colMin is > 0 for non-empty string"); + text.style("font-style", "italic"); // the text should overflow now + ( label).setMinimumsByCalculatingTextSize(); // manually call this since Label can't detect a style change + label.computeLayout(); + label.render(); + assert.equal(( label).textHeight, label.rowMinimum(), "text height === label.rowMinimum()"); + assert.operator(( label).textLength, "<=", svgWidth, "the italic text is not wider than the SVG width"); + var textContent = text.node().textContent; + assert.equal(textContent.substr(textContent.length-3), "...", "Italicized text overflowed and was truncated"); svg.remove(); - }); + }); }); From 78b66e501a211f8ec6ff53378219cec2d908586d Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswamy Date: Tue, 11 Feb 2014 20:42:02 -0500 Subject: [PATCH 36/61] get blanket working in the browser --- blanket_mocha.js | 5437 ++++++++++++++++++++++++++++++++++++++++++++++ tests.html | 27 +- 2 files changed, 5453 insertions(+), 11 deletions(-) create mode 100644 blanket_mocha.js diff --git a/blanket_mocha.js b/blanket_mocha.js new file mode 100644 index 0000000000..d8a785e220 --- /dev/null +++ b/blanket_mocha.js @@ -0,0 +1,5437 @@ +/*! blanket - v1.1.5 */ + +(function(define){ +/* + Copyright (C) 2012 Ariya Hidayat + Copyright (C) 2012 Mathias Bynens + Copyright (C) 2012 Joost-Wim Boekesteijn + Copyright (C) 2012 Kris Kowal + Copyright (C) 2012 Yusuke Suzuki + Copyright (C) 2012 Arpad Borsos + Copyright (C) 2011 Ariya Hidayat + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/*jslint bitwise:true plusplus:true */ +/*global esprima:true, define:true, exports:true, window: true, +throwError: true, createLiteral: true, generateStatement: true, +parseAssignmentExpression: true, parseBlock: true, parseExpression: true, +parseFunctionDeclaration: true, parseFunctionExpression: true, +parseFunctionSourceElements: true, parseVariableIdentifier: true, +parseLeftHandSideExpression: true, +parseStatement: true, parseSourceElement: true */ + +(function (root, factory) { + 'use strict'; + + // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, + // Rhino, and plain browser loading. + if (typeof define === 'function' && define.amd) { + define(['exports'], factory); + } else if (typeof exports !== 'undefined') { + factory(exports); + } else { + factory((root.esprima = {})); + } +}(this, function (exports) { + 'use strict'; + + var Token, + TokenName, + Syntax, + PropertyKind, + Messages, + Regex, + source, + strict, + index, + lineNumber, + lineStart, + length, + buffer, + state, + extra; + + Token = { + BooleanLiteral: 1, + EOF: 2, + Identifier: 3, + Keyword: 4, + NullLiteral: 5, + NumericLiteral: 6, + Punctuator: 7, + StringLiteral: 8 + }; + + TokenName = {}; + TokenName[Token.BooleanLiteral] = 'Boolean'; + TokenName[Token.EOF] = ''; + TokenName[Token.Identifier] = 'Identifier'; + TokenName[Token.Keyword] = 'Keyword'; + TokenName[Token.NullLiteral] = 'Null'; + TokenName[Token.NumericLiteral] = 'Numeric'; + TokenName[Token.Punctuator] = 'Punctuator'; + TokenName[Token.StringLiteral] = 'String'; + + Syntax = { + AssignmentExpression: 'AssignmentExpression', + ArrayExpression: 'ArrayExpression', + BlockStatement: 'BlockStatement', + BinaryExpression: 'BinaryExpression', + BreakStatement: 'BreakStatement', + CallExpression: 'CallExpression', + CatchClause: 'CatchClause', + ConditionalExpression: 'ConditionalExpression', + ContinueStatement: 'ContinueStatement', + DoWhileStatement: 'DoWhileStatement', + DebuggerStatement: 'DebuggerStatement', + EmptyStatement: 'EmptyStatement', + ExpressionStatement: 'ExpressionStatement', + ForStatement: 'ForStatement', + ForInStatement: 'ForInStatement', + FunctionDeclaration: 'FunctionDeclaration', + FunctionExpression: 'FunctionExpression', + Identifier: 'Identifier', + IfStatement: 'IfStatement', + Literal: 'Literal', + LabeledStatement: 'LabeledStatement', + LogicalExpression: 'LogicalExpression', + MemberExpression: 'MemberExpression', + NewExpression: 'NewExpression', + ObjectExpression: 'ObjectExpression', + Program: 'Program', + Property: 'Property', + ReturnStatement: 'ReturnStatement', + SequenceExpression: 'SequenceExpression', + SwitchStatement: 'SwitchStatement', + SwitchCase: 'SwitchCase', + ThisExpression: 'ThisExpression', + ThrowStatement: 'ThrowStatement', + TryStatement: 'TryStatement', + UnaryExpression: 'UnaryExpression', + UpdateExpression: 'UpdateExpression', + VariableDeclaration: 'VariableDeclaration', + VariableDeclarator: 'VariableDeclarator', + WhileStatement: 'WhileStatement', + WithStatement: 'WithStatement' + }; + + PropertyKind = { + Data: 1, + Get: 2, + Set: 4 + }; + + // Error messages should be identical to V8. + Messages = { + UnexpectedToken: 'Unexpected token %0', + UnexpectedNumber: 'Unexpected number', + UnexpectedString: 'Unexpected string', + UnexpectedIdentifier: 'Unexpected identifier', + UnexpectedReserved: 'Unexpected reserved word', + UnexpectedEOS: 'Unexpected end of input', + NewlineAfterThrow: 'Illegal newline after throw', + InvalidRegExp: 'Invalid regular expression', + UnterminatedRegExp: 'Invalid regular expression: missing /', + InvalidLHSInAssignment: 'Invalid left-hand side in assignment', + InvalidLHSInForIn: 'Invalid left-hand side in for-in', + MultipleDefaultsInSwitch: 'More than one default clause in switch statement', + NoCatchOrFinally: 'Missing catch or finally after try', + UnknownLabel: 'Undefined label \'%0\'', + Redeclaration: '%0 \'%1\' has already been declared', + IllegalContinue: 'Illegal continue statement', + IllegalBreak: 'Illegal break statement', + IllegalReturn: 'Illegal return statement', + StrictModeWith: 'Strict mode code may not include a with statement', + StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', + StrictVarName: 'Variable name may not be eval or arguments in strict mode', + StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', + StrictParamDupe: 'Strict mode function may not have duplicate parameter names', + StrictFunctionName: 'Function name may not be eval or arguments in strict mode', + StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', + StrictDelete: 'Delete of an unqualified identifier in strict mode.', + StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode', + AccessorDataProperty: 'Object literal may not have data and accessor property with the same name', + AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name', + StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', + StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', + StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', + StrictReservedWord: 'Use of future reserved word in strict mode' + }; + + // See also tools/generate-unicode-regex.py. + Regex = { + NonAsciiIdentifierStart: new RegExp('[\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]'), + NonAsciiIdentifierPart: new RegExp('[\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0300-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u0483-\u0487\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u05d0-\u05ea\u05f0-\u05f2\u0610-\u061a\u0620-\u0669\u066e-\u06d3\u06d5-\u06dc\u06df-\u06e8\u06ea-\u06fc\u06ff\u0710-\u074a\u074d-\u07b1\u07c0-\u07f5\u07fa\u0800-\u082d\u0840-\u085b\u08a0\u08a2-\u08ac\u08e4-\u08fe\u0900-\u0963\u0966-\u096f\u0971-\u0977\u0979-\u097f\u0981-\u0983\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bc-\u09c4\u09c7\u09c8\u09cb-\u09ce\u09d7\u09dc\u09dd\u09df-\u09e3\u09e6-\u09f1\u0a01-\u0a03\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a59-\u0a5c\u0a5e\u0a66-\u0a75\u0a81-\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abc-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ad0\u0ae0-\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3c-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5c\u0b5d\u0b5f-\u0b63\u0b66-\u0b6f\u0b71\u0b82\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd0\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c58\u0c59\u0c60-\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbc-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0cde\u0ce0-\u0ce3\u0ce6-\u0cef\u0cf1\u0cf2\u0d02\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d-\u0d44\u0d46-\u0d48\u0d4a-\u0d4e\u0d57\u0d60-\u0d63\u0d66-\u0d6f\u0d7a-\u0d7f\u0d82\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e01-\u0e3a\u0e40-\u0e4e\u0e50-\u0e59\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb9\u0ebb-\u0ebd\u0ec0-\u0ec4\u0ec6\u0ec8-\u0ecd\u0ed0-\u0ed9\u0edc-\u0edf\u0f00\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e-\u0f47\u0f49-\u0f6c\u0f71-\u0f84\u0f86-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1049\u1050-\u109d\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u135d-\u135f\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176c\u176e-\u1770\u1772\u1773\u1780-\u17d3\u17d7\u17dc\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1820-\u1877\u1880-\u18aa\u18b0-\u18f5\u1900-\u191c\u1920-\u192b\u1930-\u193b\u1946-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u19d0-\u19d9\u1a00-\u1a1b\u1a20-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1aa7\u1b00-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1bf3\u1c00-\u1c37\u1c40-\u1c49\u1c4d-\u1c7d\u1cd0-\u1cd2\u1cd4-\u1cf6\u1d00-\u1de6\u1dfc-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u200c\u200d\u203f\u2040\u2054\u2071\u207f\u2090-\u209c\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d7f-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2de0-\u2dff\u2e2f\u3005-\u3007\u3021-\u302f\u3031-\u3035\u3038-\u303c\u3041-\u3096\u3099\u309a\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua62b\ua640-\ua66f\ua674-\ua67d\ua67f-\ua697\ua69f-\ua6f1\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua827\ua840-\ua873\ua880-\ua8c4\ua8d0-\ua8d9\ua8e0-\ua8f7\ua8fb\ua900-\ua92d\ua930-\ua953\ua960-\ua97c\ua980-\ua9c0\ua9cf-\ua9d9\uaa00-\uaa36\uaa40-\uaa4d\uaa50-\uaa59\uaa60-\uaa76\uaa7a\uaa7b\uaa80-\uaac2\uaadb-\uaadd\uaae0-\uaaef\uaaf2-\uaaf6\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabea\uabec\uabed\uabf0-\uabf9\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]') + }; + + // Ensure the condition is true, otherwise throw an error. + // This is only to have a better contract semantic, i.e. another safety net + // to catch a logic error. The condition shall be fulfilled in normal case. + // Do NOT use this to enforce a certain condition on any user input. + + function assert(condition, message) { + if (!condition) { + throw new Error('ASSERT: ' + message); + } + } + + function sliceSource(from, to) { + return source.slice(from, to); + } + + if (typeof 'esprima'[0] === 'undefined') { + sliceSource = function sliceArraySource(from, to) { + return source.slice(from, to).join(''); + }; + } + + function isDecimalDigit(ch) { + return '0123456789'.indexOf(ch) >= 0; + } + + function isHexDigit(ch) { + return '0123456789abcdefABCDEF'.indexOf(ch) >= 0; + } + + function isOctalDigit(ch) { + return '01234567'.indexOf(ch) >= 0; + } + + + // 7.2 White Space + + function isWhiteSpace(ch) { + return (ch === ' ') || (ch === '\u0009') || (ch === '\u000B') || + (ch === '\u000C') || (ch === '\u00A0') || + (ch.charCodeAt(0) >= 0x1680 && + '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(ch) >= 0); + } + + // 7.3 Line Terminators + + function isLineTerminator(ch) { + return (ch === '\n' || ch === '\r' || ch === '\u2028' || ch === '\u2029'); + } + + // 7.6 Identifier Names and Identifiers + + function isIdentifierStart(ch) { + return (ch === '$') || (ch === '_') || (ch === '\\') || + (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + ((ch.charCodeAt(0) >= 0x80) && Regex.NonAsciiIdentifierStart.test(ch)); + } + + function isIdentifierPart(ch) { + return (ch === '$') || (ch === '_') || (ch === '\\') || + (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + ((ch >= '0') && (ch <= '9')) || + ((ch.charCodeAt(0) >= 0x80) && Regex.NonAsciiIdentifierPart.test(ch)); + } + + // 7.6.1.2 Future Reserved Words + + function isFutureReservedWord(id) { + switch (id) { + + // Future reserved words. + case 'class': + case 'enum': + case 'export': + case 'extends': + case 'import': + case 'super': + return true; + } + + return false; + } + + function isStrictModeReservedWord(id) { + switch (id) { + + // Strict Mode reserved words. + case 'implements': + case 'interface': + case 'package': + case 'private': + case 'protected': + case 'public': + case 'static': + case 'yield': + case 'let': + return true; + } + + return false; + } + + function isRestrictedWord(id) { + return id === 'eval' || id === 'arguments'; + } + + // 7.6.1.1 Keywords + + function isKeyword(id) { + var keyword = false; + switch (id.length) { + case 2: + keyword = (id === 'if') || (id === 'in') || (id === 'do'); + break; + case 3: + keyword = (id === 'var') || (id === 'for') || (id === 'new') || (id === 'try'); + break; + case 4: + keyword = (id === 'this') || (id === 'else') || (id === 'case') || (id === 'void') || (id === 'with'); + break; + case 5: + keyword = (id === 'while') || (id === 'break') || (id === 'catch') || (id === 'throw'); + break; + case 6: + keyword = (id === 'return') || (id === 'typeof') || (id === 'delete') || (id === 'switch'); + break; + case 7: + keyword = (id === 'default') || (id === 'finally'); + break; + case 8: + keyword = (id === 'function') || (id === 'continue') || (id === 'debugger'); + break; + case 10: + keyword = (id === 'instanceof'); + break; + } + + if (keyword) { + return true; + } + + switch (id) { + // Future reserved words. + // 'const' is specialized as Keyword in V8. + case 'const': + return true; + + // For compatiblity to SpiderMonkey and ES.next + case 'yield': + case 'let': + return true; + } + + if (strict && isStrictModeReservedWord(id)) { + return true; + } + + return isFutureReservedWord(id); + } + + // 7.4 Comments + + function skipComment() { + var ch, blockComment, lineComment; + + blockComment = false; + lineComment = false; + + while (index < length) { + ch = source[index]; + + if (lineComment) { + ch = source[index++]; + if (isLineTerminator(ch)) { + lineComment = false; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + ++lineNumber; + lineStart = index; + } + } else if (blockComment) { + if (isLineTerminator(ch)) { + if (ch === '\r' && source[index + 1] === '\n') { + ++index; + } + ++lineNumber; + ++index; + lineStart = index; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + ch = source[index++]; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + if (ch === '*') { + ch = source[index]; + if (ch === '/') { + ++index; + blockComment = false; + } + } + } + } else if (ch === '/') { + ch = source[index + 1]; + if (ch === '/') { + index += 2; + lineComment = true; + } else if (ch === '*') { + index += 2; + blockComment = true; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + break; + } + } else if (isWhiteSpace(ch)) { + ++index; + } else if (isLineTerminator(ch)) { + ++index; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + ++lineNumber; + lineStart = index; + } else { + break; + } + } + } + + function scanHexEscape(prefix) { + var i, len, ch, code = 0; + + len = (prefix === 'u') ? 4 : 2; + for (i = 0; i < len; ++i) { + if (index < length && isHexDigit(source[index])) { + ch = source[index++]; + code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); + } else { + return ''; + } + } + return String.fromCharCode(code); + } + + function scanIdentifier() { + var ch, start, id, restore; + + ch = source[index]; + if (!isIdentifierStart(ch)) { + return; + } + + start = index; + if (ch === '\\') { + ++index; + if (source[index] !== 'u') { + return; + } + ++index; + restore = index; + ch = scanHexEscape('u'); + if (ch) { + if (ch === '\\' || !isIdentifierStart(ch)) { + return; + } + id = ch; + } else { + index = restore; + id = 'u'; + } + } else { + id = source[index++]; + } + + while (index < length) { + ch = source[index]; + if (!isIdentifierPart(ch)) { + break; + } + if (ch === '\\') { + ++index; + if (source[index] !== 'u') { + return; + } + ++index; + restore = index; + ch = scanHexEscape('u'); + if (ch) { + if (ch === '\\' || !isIdentifierPart(ch)) { + return; + } + id += ch; + } else { + index = restore; + id += 'u'; + } + } else { + id += source[index++]; + } + } + + // There is no keyword or literal with only one character. + // Thus, it must be an identifier. + if (id.length === 1) { + return { + type: Token.Identifier, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (isKeyword(id)) { + return { + type: Token.Keyword, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // 7.8.1 Null Literals + + if (id === 'null') { + return { + type: Token.NullLiteral, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // 7.8.2 Boolean Literals + + if (id === 'true' || id === 'false') { + return { + type: Token.BooleanLiteral, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + return { + type: Token.Identifier, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // 7.7 Punctuators + + function scanPunctuator() { + var start = index, + ch1 = source[index], + ch2, + ch3, + ch4; + + // Check for most common single-character punctuators. + + if (ch1 === ';' || ch1 === '{' || ch1 === '}') { + ++index; + return { + type: Token.Punctuator, + value: ch1, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === ',' || ch1 === '(' || ch1 === ')') { + ++index; + return { + type: Token.Punctuator, + value: ch1, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // Dot (.) can also start a floating-point number, hence the need + // to check the next character. + + ch2 = source[index + 1]; + if (ch1 === '.' && !isDecimalDigit(ch2)) { + return { + type: Token.Punctuator, + value: source[index++], + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // Peek more characters. + + ch3 = source[index + 2]; + ch4 = source[index + 3]; + + // 4-character punctuator: >>>= + + if (ch1 === '>' && ch2 === '>' && ch3 === '>') { + if (ch4 === '=') { + index += 4; + return { + type: Token.Punctuator, + value: '>>>=', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + } + + // 3-character punctuators: === !== >>> <<= >>= + + if (ch1 === '=' && ch2 === '=' && ch3 === '=') { + index += 3; + return { + type: Token.Punctuator, + value: '===', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '!' && ch2 === '=' && ch3 === '=') { + index += 3; + return { + type: Token.Punctuator, + value: '!==', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '>' && ch2 === '>' && ch3 === '>') { + index += 3; + return { + type: Token.Punctuator, + value: '>>>', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '<' && ch2 === '<' && ch3 === '=') { + index += 3; + return { + type: Token.Punctuator, + value: '<<=', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '>' && ch2 === '>' && ch3 === '=') { + index += 3; + return { + type: Token.Punctuator, + value: '>>=', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // 2-character punctuators: <= >= == != ++ -- << >> && || + // += -= *= %= &= |= ^= /= + + if (ch2 === '=') { + if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { + index += 2; + return { + type: Token.Punctuator, + value: ch1 + ch2, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + } + + if (ch1 === ch2 && ('+-<>&|'.indexOf(ch1) >= 0)) { + if ('+-<>&|'.indexOf(ch2) >= 0) { + index += 2; + return { + type: Token.Punctuator, + value: ch1 + ch2, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + } + + // The remaining 1-character punctuators. + + if ('[]<>+-*%&|^!~?:=/'.indexOf(ch1) >= 0) { + return { + type: Token.Punctuator, + value: source[index++], + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + } + + // 7.8.3 Numeric Literals + + function scanNumericLiteral() { + var number, start, ch; + + ch = source[index]; + assert(isDecimalDigit(ch) || (ch === '.'), + 'Numeric literal must start with a decimal digit or a decimal point'); + + start = index; + number = ''; + if (ch !== '.') { + number = source[index++]; + ch = source[index]; + + // Hex number starts with '0x'. + // Octal number starts with '0'. + if (number === '0') { + if (ch === 'x' || ch === 'X') { + number += source[index++]; + while (index < length) { + ch = source[index]; + if (!isHexDigit(ch)) { + break; + } + number += source[index++]; + } + + if (number.length <= 2) { + // only 0x + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + if (index < length) { + ch = source[index]; + if (isIdentifierStart(ch)) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + return { + type: Token.NumericLiteral, + value: parseInt(number, 16), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } else if (isOctalDigit(ch)) { + number += source[index++]; + while (index < length) { + ch = source[index]; + if (!isOctalDigit(ch)) { + break; + } + number += source[index++]; + } + + if (index < length) { + ch = source[index]; + if (isIdentifierStart(ch) || isDecimalDigit(ch)) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + return { + type: Token.NumericLiteral, + value: parseInt(number, 8), + octal: true, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // decimal number starts with '0' such as '09' is illegal. + if (isDecimalDigit(ch)) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + while (index < length) { + ch = source[index]; + if (!isDecimalDigit(ch)) { + break; + } + number += source[index++]; + } + } + + if (ch === '.') { + number += source[index++]; + while (index < length) { + ch = source[index]; + if (!isDecimalDigit(ch)) { + break; + } + number += source[index++]; + } + } + + if (ch === 'e' || ch === 'E') { + number += source[index++]; + + ch = source[index]; + if (ch === '+' || ch === '-') { + number += source[index++]; + } + + ch = source[index]; + if (isDecimalDigit(ch)) { + number += source[index++]; + while (index < length) { + ch = source[index]; + if (!isDecimalDigit(ch)) { + break; + } + number += source[index++]; + } + } else { + ch = 'character ' + ch; + if (index >= length) { + ch = ''; + } + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + if (index < length) { + ch = source[index]; + if (isIdentifierStart(ch)) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + return { + type: Token.NumericLiteral, + value: parseFloat(number), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // 7.8.4 String Literals + + function scanStringLiteral() { + var str = '', quote, start, ch, code, unescaped, restore, octal = false; + + quote = source[index]; + assert((quote === '\'' || quote === '"'), + 'String literal must starts with a quote'); + + start = index; + ++index; + + while (index < length) { + ch = source[index++]; + + if (ch === quote) { + quote = ''; + break; + } else if (ch === '\\') { + ch = source[index++]; + if (!isLineTerminator(ch)) { + switch (ch) { + case 'n': + str += '\n'; + break; + case 'r': + str += '\r'; + break; + case 't': + str += '\t'; + break; + case 'u': + case 'x': + restore = index; + unescaped = scanHexEscape(ch); + if (unescaped) { + str += unescaped; + } else { + index = restore; + str += ch; + } + break; + case 'b': + str += '\b'; + break; + case 'f': + str += '\f'; + break; + case 'v': + str += '\x0B'; + break; + + default: + if (isOctalDigit(ch)) { + code = '01234567'.indexOf(ch); + + // \0 is not octal escape sequence + if (code !== 0) { + octal = true; + } + + if (index < length && isOctalDigit(source[index])) { + octal = true; + code = code * 8 + '01234567'.indexOf(source[index++]); + + // 3 digits are only allowed when string starts + // with 0, 1, 2, 3 + if ('0123'.indexOf(ch) >= 0 && + index < length && + isOctalDigit(source[index])) { + code = code * 8 + '01234567'.indexOf(source[index++]); + } + } + str += String.fromCharCode(code); + } else { + str += ch; + } + break; + } + } else { + ++lineNumber; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + } + } else if (isLineTerminator(ch)) { + break; + } else { + str += ch; + } + } + + if (quote !== '') { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.StringLiteral, + value: str, + octal: octal, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + function scanRegExp() { + var str, ch, start, pattern, flags, value, classMarker = false, restore, terminated = false; + + buffer = null; + skipComment(); + + start = index; + ch = source[index]; + assert(ch === '/', 'Regular expression literal must start with a slash'); + str = source[index++]; + + while (index < length) { + ch = source[index++]; + str += ch; + if (ch === '\\') { + ch = source[index++]; + // ECMA-262 7.8.5 + if (isLineTerminator(ch)) { + throwError({}, Messages.UnterminatedRegExp); + } + str += ch; + } else if (classMarker) { + if (ch === ']') { + classMarker = false; + } + } else { + if (ch === '/') { + terminated = true; + break; + } else if (ch === '[') { + classMarker = true; + } else if (isLineTerminator(ch)) { + throwError({}, Messages.UnterminatedRegExp); + } + } + } + + if (!terminated) { + throwError({}, Messages.UnterminatedRegExp); + } + + // Exclude leading and trailing slash. + pattern = str.substr(1, str.length - 2); + + flags = ''; + while (index < length) { + ch = source[index]; + if (!isIdentifierPart(ch)) { + break; + } + + ++index; + if (ch === '\\' && index < length) { + ch = source[index]; + if (ch === 'u') { + ++index; + restore = index; + ch = scanHexEscape('u'); + if (ch) { + flags += ch; + str += '\\u'; + for (; restore < index; ++restore) { + str += source[restore]; + } + } else { + index = restore; + flags += 'u'; + str += '\\u'; + } + } else { + str += '\\'; + } + } else { + flags += ch; + str += ch; + } + } + + try { + value = new RegExp(pattern, flags); + } catch (e) { + throwError({}, Messages.InvalidRegExp); + } + + return { + literal: str, + value: value, + range: [start, index] + }; + } + + function isIdentifierName(token) { + return token.type === Token.Identifier || + token.type === Token.Keyword || + token.type === Token.BooleanLiteral || + token.type === Token.NullLiteral; + } + + function advance() { + var ch, token; + + skipComment(); + + if (index >= length) { + return { + type: Token.EOF, + lineNumber: lineNumber, + lineStart: lineStart, + range: [index, index] + }; + } + + token = scanPunctuator(); + if (typeof token !== 'undefined') { + return token; + } + + ch = source[index]; + + if (ch === '\'' || ch === '"') { + return scanStringLiteral(); + } + + if (ch === '.' || isDecimalDigit(ch)) { + return scanNumericLiteral(); + } + + token = scanIdentifier(); + if (typeof token !== 'undefined') { + return token; + } + + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + function lex() { + var token; + + if (buffer) { + index = buffer.range[1]; + lineNumber = buffer.lineNumber; + lineStart = buffer.lineStart; + token = buffer; + buffer = null; + return token; + } + + buffer = null; + return advance(); + } + + function lookahead() { + var pos, line, start; + + if (buffer !== null) { + return buffer; + } + + pos = index; + line = lineNumber; + start = lineStart; + buffer = advance(); + index = pos; + lineNumber = line; + lineStart = start; + + return buffer; + } + + // Return true if there is a line terminator before the next token. + + function peekLineTerminator() { + var pos, line, start, found; + + pos = index; + line = lineNumber; + start = lineStart; + skipComment(); + found = lineNumber !== line; + index = pos; + lineNumber = line; + lineStart = start; + + return found; + } + + // Throw an exception + + function throwError(token, messageFormat) { + var error, + args = Array.prototype.slice.call(arguments, 2), + msg = messageFormat.replace( + /%(\d)/g, + function (whole, index) { + return args[index] || ''; + } + ); + + if (typeof token.lineNumber === 'number') { + error = new Error('Line ' + token.lineNumber + ': ' + msg); + error.index = token.range[0]; + error.lineNumber = token.lineNumber; + error.column = token.range[0] - lineStart + 1; + } else { + error = new Error('Line ' + lineNumber + ': ' + msg); + error.index = index; + error.lineNumber = lineNumber; + error.column = index - lineStart + 1; + } + + throw error; + } + + function throwErrorTolerant() { + try { + throwError.apply(null, arguments); + } catch (e) { + if (extra.errors) { + extra.errors.push(e); + } else { + throw e; + } + } + } + + + // Throw an exception because of the token. + + function throwUnexpected(token) { + if (token.type === Token.EOF) { + throwError(token, Messages.UnexpectedEOS); + } + + if (token.type === Token.NumericLiteral) { + throwError(token, Messages.UnexpectedNumber); + } + + if (token.type === Token.StringLiteral) { + throwError(token, Messages.UnexpectedString); + } + + if (token.type === Token.Identifier) { + throwError(token, Messages.UnexpectedIdentifier); + } + + if (token.type === Token.Keyword) { + if (isFutureReservedWord(token.value)) { + throwError(token, Messages.UnexpectedReserved); + } else if (strict && isStrictModeReservedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictReservedWord); + return; + } + throwError(token, Messages.UnexpectedToken, token.value); + } + + // BooleanLiteral, NullLiteral, or Punctuator. + throwError(token, Messages.UnexpectedToken, token.value); + } + + // Expect the next token to match the specified punctuator. + // If not, an exception will be thrown. + + function expect(value) { + var token = lex(); + if (token.type !== Token.Punctuator || token.value !== value) { + throwUnexpected(token); + } + } + + // Expect the next token to match the specified keyword. + // If not, an exception will be thrown. + + function expectKeyword(keyword) { + var token = lex(); + if (token.type !== Token.Keyword || token.value !== keyword) { + throwUnexpected(token); + } + } + + // Return true if the next token matches the specified punctuator. + + function match(value) { + var token = lookahead(); + return token.type === Token.Punctuator && token.value === value; + } + + // Return true if the next token matches the specified keyword + + function matchKeyword(keyword) { + var token = lookahead(); + return token.type === Token.Keyword && token.value === keyword; + } + + // Return true if the next token is an assignment operator + + function matchAssign() { + var token = lookahead(), + op = token.value; + + if (token.type !== Token.Punctuator) { + return false; + } + return op === '=' || + op === '*=' || + op === '/=' || + op === '%=' || + op === '+=' || + op === '-=' || + op === '<<=' || + op === '>>=' || + op === '>>>=' || + op === '&=' || + op === '^=' || + op === '|='; + } + + function consumeSemicolon() { + var token, line; + + // Catch the very common case first. + if (source[index] === ';') { + lex(); + return; + } + + line = lineNumber; + skipComment(); + if (lineNumber !== line) { + return; + } + + if (match(';')) { + lex(); + return; + } + + token = lookahead(); + if (token.type !== Token.EOF && !match('}')) { + throwUnexpected(token); + } + } + + // Return true if provided expression is LeftHandSideExpression + + function isLeftHandSide(expr) { + return expr.type === Syntax.Identifier || expr.type === Syntax.MemberExpression; + } + + // 11.1.4 Array Initialiser + + function parseArrayInitialiser() { + var elements = []; + + expect('['); + + while (!match(']')) { + if (match(',')) { + lex(); + elements.push(null); + } else { + elements.push(parseAssignmentExpression()); + + if (!match(']')) { + expect(','); + } + } + } + + expect(']'); + + return { + type: Syntax.ArrayExpression, + elements: elements + }; + } + + // 11.1.5 Object Initialiser + + function parsePropertyFunction(param, first) { + var previousStrict, body; + + previousStrict = strict; + body = parseFunctionSourceElements(); + if (first && strict && isRestrictedWord(param[0].name)) { + throwErrorTolerant(first, Messages.StrictParamName); + } + strict = previousStrict; + + return { + type: Syntax.FunctionExpression, + id: null, + params: param, + defaults: [], + body: body, + rest: null, + generator: false, + expression: false + }; + } + + function parseObjectPropertyKey() { + var token = lex(); + + // Note: This function is called only from parseObjectProperty(), where + // EOF and Punctuator tokens are already filtered out. + + if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) { + if (strict && token.octal) { + throwErrorTolerant(token, Messages.StrictOctalLiteral); + } + return createLiteral(token); + } + + return { + type: Syntax.Identifier, + name: token.value + }; + } + + function parseObjectProperty() { + var token, key, id, param; + + token = lookahead(); + + if (token.type === Token.Identifier) { + + id = parseObjectPropertyKey(); + + // Property Assignment: Getter and Setter. + + if (token.value === 'get' && !match(':')) { + key = parseObjectPropertyKey(); + expect('('); + expect(')'); + return { + type: Syntax.Property, + key: key, + value: parsePropertyFunction([]), + kind: 'get' + }; + } else if (token.value === 'set' && !match(':')) { + key = parseObjectPropertyKey(); + expect('('); + token = lookahead(); + if (token.type !== Token.Identifier) { + expect(')'); + throwErrorTolerant(token, Messages.UnexpectedToken, token.value); + return { + type: Syntax.Property, + key: key, + value: parsePropertyFunction([]), + kind: 'set' + }; + } else { + param = [ parseVariableIdentifier() ]; + expect(')'); + return { + type: Syntax.Property, + key: key, + value: parsePropertyFunction(param, token), + kind: 'set' + }; + } + } else { + expect(':'); + return { + type: Syntax.Property, + key: id, + value: parseAssignmentExpression(), + kind: 'init' + }; + } + } else if (token.type === Token.EOF || token.type === Token.Punctuator) { + throwUnexpected(token); + } else { + key = parseObjectPropertyKey(); + expect(':'); + return { + type: Syntax.Property, + key: key, + value: parseAssignmentExpression(), + kind: 'init' + }; + } + } + + function parseObjectInitialiser() { + var properties = [], property, name, kind, map = {}, toString = String; + + expect('{'); + + while (!match('}')) { + property = parseObjectProperty(); + + if (property.key.type === Syntax.Identifier) { + name = property.key.name; + } else { + name = toString(property.key.value); + } + kind = (property.kind === 'init') ? PropertyKind.Data : (property.kind === 'get') ? PropertyKind.Get : PropertyKind.Set; + if (Object.prototype.hasOwnProperty.call(map, name)) { + if (map[name] === PropertyKind.Data) { + if (strict && kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.StrictDuplicateProperty); + } else if (kind !== PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); + } + } else { + if (kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); + } else if (map[name] & kind) { + throwErrorTolerant({}, Messages.AccessorGetSet); + } + } + map[name] |= kind; + } else { + map[name] = kind; + } + + properties.push(property); + + if (!match('}')) { + expect(','); + } + } + + expect('}'); + + return { + type: Syntax.ObjectExpression, + properties: properties + }; + } + + // 11.1.6 The Grouping Operator + + function parseGroupExpression() { + var expr; + + expect('('); + + expr = parseExpression(); + + expect(')'); + + return expr; + } + + + // 11.1 Primary Expressions + + function parsePrimaryExpression() { + var token = lookahead(), + type = token.type; + + if (type === Token.Identifier) { + return { + type: Syntax.Identifier, + name: lex().value + }; + } + + if (type === Token.StringLiteral || type === Token.NumericLiteral) { + if (strict && token.octal) { + throwErrorTolerant(token, Messages.StrictOctalLiteral); + } + return createLiteral(lex()); + } + + if (type === Token.Keyword) { + if (matchKeyword('this')) { + lex(); + return { + type: Syntax.ThisExpression + }; + } + + if (matchKeyword('function')) { + return parseFunctionExpression(); + } + } + + if (type === Token.BooleanLiteral) { + lex(); + token.value = (token.value === 'true'); + return createLiteral(token); + } + + if (type === Token.NullLiteral) { + lex(); + token.value = null; + return createLiteral(token); + } + + if (match('[')) { + return parseArrayInitialiser(); + } + + if (match('{')) { + return parseObjectInitialiser(); + } + + if (match('(')) { + return parseGroupExpression(); + } + + if (match('/') || match('/=')) { + return createLiteral(scanRegExp()); + } + + return throwUnexpected(lex()); + } + + // 11.2 Left-Hand-Side Expressions + + function parseArguments() { + var args = []; + + expect('('); + + if (!match(')')) { + while (index < length) { + args.push(parseAssignmentExpression()); + if (match(')')) { + break; + } + expect(','); + } + } + + expect(')'); + + return args; + } + + function parseNonComputedProperty() { + var token = lex(); + + if (!isIdentifierName(token)) { + throwUnexpected(token); + } + + return { + type: Syntax.Identifier, + name: token.value + }; + } + + function parseNonComputedMember() { + expect('.'); + + return parseNonComputedProperty(); + } + + function parseComputedMember() { + var expr; + + expect('['); + + expr = parseExpression(); + + expect(']'); + + return expr; + } + + function parseNewExpression() { + var expr; + + expectKeyword('new'); + + expr = { + type: Syntax.NewExpression, + callee: parseLeftHandSideExpression(), + 'arguments': [] + }; + + if (match('(')) { + expr['arguments'] = parseArguments(); + } + + return expr; + } + + function parseLeftHandSideExpressionAllowCall() { + var expr; + + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + + while (match('.') || match('[') || match('(')) { + if (match('(')) { + expr = { + type: Syntax.CallExpression, + callee: expr, + 'arguments': parseArguments() + }; + } else if (match('[')) { + expr = { + type: Syntax.MemberExpression, + computed: true, + object: expr, + property: parseComputedMember() + }; + } else { + expr = { + type: Syntax.MemberExpression, + computed: false, + object: expr, + property: parseNonComputedMember() + }; + } + } + + return expr; + } + + + function parseLeftHandSideExpression() { + var expr; + + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + + while (match('.') || match('[')) { + if (match('[')) { + expr = { + type: Syntax.MemberExpression, + computed: true, + object: expr, + property: parseComputedMember() + }; + } else { + expr = { + type: Syntax.MemberExpression, + computed: false, + object: expr, + property: parseNonComputedMember() + }; + } + } + + return expr; + } + + // 11.3 Postfix Expressions + + function parsePostfixExpression() { + var expr = parseLeftHandSideExpressionAllowCall(), token; + + token = lookahead(); + if (token.type !== Token.Punctuator) { + return expr; + } + + if ((match('++') || match('--')) && !peekLineTerminator()) { + // 11.3.1, 11.3.2 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant({}, Messages.StrictLHSPostfix); + } + if (!isLeftHandSide(expr)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } + + expr = { + type: Syntax.UpdateExpression, + operator: lex().value, + argument: expr, + prefix: false + }; + } + + return expr; + } + + // 11.4 Unary Operators + + function parseUnaryExpression() { + var token, expr; + + token = lookahead(); + if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { + return parsePostfixExpression(); + } + + if (match('++') || match('--')) { + token = lex(); + expr = parseUnaryExpression(); + // 11.4.4, 11.4.5 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant({}, Messages.StrictLHSPrefix); + } + + if (!isLeftHandSide(expr)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } + + expr = { + type: Syntax.UpdateExpression, + operator: token.value, + argument: expr, + prefix: true + }; + return expr; + } + + if (match('+') || match('-') || match('~') || match('!')) { + expr = { + type: Syntax.UnaryExpression, + operator: lex().value, + argument: parseUnaryExpression(), + prefix: true + }; + return expr; + } + + if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) { + expr = { + type: Syntax.UnaryExpression, + operator: lex().value, + argument: parseUnaryExpression(), + prefix: true + }; + if (strict && expr.operator === 'delete' && expr.argument.type === Syntax.Identifier) { + throwErrorTolerant({}, Messages.StrictDelete); + } + return expr; + } + + return parsePostfixExpression(); + } + + // 11.5 Multiplicative Operators + + function parseMultiplicativeExpression() { + var expr = parseUnaryExpression(); + + while (match('*') || match('/') || match('%')) { + expr = { + type: Syntax.BinaryExpression, + operator: lex().value, + left: expr, + right: parseUnaryExpression() + }; + } + + return expr; + } + + // 11.6 Additive Operators + + function parseAdditiveExpression() { + var expr = parseMultiplicativeExpression(); + + while (match('+') || match('-')) { + expr = { + type: Syntax.BinaryExpression, + operator: lex().value, + left: expr, + right: parseMultiplicativeExpression() + }; + } + + return expr; + } + + // 11.7 Bitwise Shift Operators + + function parseShiftExpression() { + var expr = parseAdditiveExpression(); + + while (match('<<') || match('>>') || match('>>>')) { + expr = { + type: Syntax.BinaryExpression, + operator: lex().value, + left: expr, + right: parseAdditiveExpression() + }; + } + + return expr; + } + // 11.8 Relational Operators + + function parseRelationalExpression() { + var expr, previousAllowIn; + + previousAllowIn = state.allowIn; + state.allowIn = true; + + expr = parseShiftExpression(); + + while (match('<') || match('>') || match('<=') || match('>=') || (previousAllowIn && matchKeyword('in')) || matchKeyword('instanceof')) { + expr = { + type: Syntax.BinaryExpression, + operator: lex().value, + left: expr, + right: parseShiftExpression() + }; + } + + state.allowIn = previousAllowIn; + return expr; + } + + // 11.9 Equality Operators + + function parseEqualityExpression() { + var expr = parseRelationalExpression(); + + while (match('==') || match('!=') || match('===') || match('!==')) { + expr = { + type: Syntax.BinaryExpression, + operator: lex().value, + left: expr, + right: parseRelationalExpression() + }; + } + + return expr; + } + + // 11.10 Binary Bitwise Operators + + function parseBitwiseANDExpression() { + var expr = parseEqualityExpression(); + + while (match('&')) { + lex(); + expr = { + type: Syntax.BinaryExpression, + operator: '&', + left: expr, + right: parseEqualityExpression() + }; + } + + return expr; + } + + function parseBitwiseXORExpression() { + var expr = parseBitwiseANDExpression(); + + while (match('^')) { + lex(); + expr = { + type: Syntax.BinaryExpression, + operator: '^', + left: expr, + right: parseBitwiseANDExpression() + }; + } + + return expr; + } + + function parseBitwiseORExpression() { + var expr = parseBitwiseXORExpression(); + + while (match('|')) { + lex(); + expr = { + type: Syntax.BinaryExpression, + operator: '|', + left: expr, + right: parseBitwiseXORExpression() + }; + } + + return expr; + } + + // 11.11 Binary Logical Operators + + function parseLogicalANDExpression() { + var expr = parseBitwiseORExpression(); + + while (match('&&')) { + lex(); + expr = { + type: Syntax.LogicalExpression, + operator: '&&', + left: expr, + right: parseBitwiseORExpression() + }; + } + + return expr; + } + + function parseLogicalORExpression() { + var expr = parseLogicalANDExpression(); + + while (match('||')) { + lex(); + expr = { + type: Syntax.LogicalExpression, + operator: '||', + left: expr, + right: parseLogicalANDExpression() + }; + } + + return expr; + } + + // 11.12 Conditional Operator + + function parseConditionalExpression() { + var expr, previousAllowIn, consequent; + + expr = parseLogicalORExpression(); + + if (match('?')) { + lex(); + previousAllowIn = state.allowIn; + state.allowIn = true; + consequent = parseAssignmentExpression(); + state.allowIn = previousAllowIn; + expect(':'); + + expr = { + type: Syntax.ConditionalExpression, + test: expr, + consequent: consequent, + alternate: parseAssignmentExpression() + }; + } + + return expr; + } + + // 11.13 Assignment Operators + + function parseAssignmentExpression() { + var token, expr; + + token = lookahead(); + expr = parseConditionalExpression(); + + if (matchAssign()) { + // LeftHandSideExpression + if (!isLeftHandSide(expr)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } + + // 11.13.1 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant(token, Messages.StrictLHSAssignment); + } + + expr = { + type: Syntax.AssignmentExpression, + operator: lex().value, + left: expr, + right: parseAssignmentExpression() + }; + } + + return expr; + } + + // 11.14 Comma Operator + + function parseExpression() { + var expr = parseAssignmentExpression(); + + if (match(',')) { + expr = { + type: Syntax.SequenceExpression, + expressions: [ expr ] + }; + + while (index < length) { + if (!match(',')) { + break; + } + lex(); + expr.expressions.push(parseAssignmentExpression()); + } + + } + return expr; + } + + // 12.1 Block + + function parseStatementList() { + var list = [], + statement; + + while (index < length) { + if (match('}')) { + break; + } + statement = parseSourceElement(); + if (typeof statement === 'undefined') { + break; + } + list.push(statement); + } + + return list; + } + + function parseBlock() { + var block; + + expect('{'); + + block = parseStatementList(); + + expect('}'); + + return { + type: Syntax.BlockStatement, + body: block + }; + } + + // 12.2 Variable Statement + + function parseVariableIdentifier() { + var token = lex(); + + if (token.type !== Token.Identifier) { + throwUnexpected(token); + } + + return { + type: Syntax.Identifier, + name: token.value + }; + } + + function parseVariableDeclaration(kind) { + var id = parseVariableIdentifier(), + init = null; + + // 12.2.1 + if (strict && isRestrictedWord(id.name)) { + throwErrorTolerant({}, Messages.StrictVarName); + } + + if (kind === 'const') { + expect('='); + init = parseAssignmentExpression(); + } else if (match('=')) { + lex(); + init = parseAssignmentExpression(); + } + + return { + type: Syntax.VariableDeclarator, + id: id, + init: init + }; + } + + function parseVariableDeclarationList(kind) { + var list = []; + + do { + list.push(parseVariableDeclaration(kind)); + if (!match(',')) { + break; + } + lex(); + } while (index < length); + + return list; + } + + function parseVariableStatement() { + var declarations; + + expectKeyword('var'); + + declarations = parseVariableDeclarationList(); + + consumeSemicolon(); + + return { + type: Syntax.VariableDeclaration, + declarations: declarations, + kind: 'var' + }; + } + + // kind may be `const` or `let` + // Both are experimental and not in the specification yet. + // see http://wiki.ecmascript.org/doku.php?id=harmony:const + // and http://wiki.ecmascript.org/doku.php?id=harmony:let + function parseConstLetDeclaration(kind) { + var declarations; + + expectKeyword(kind); + + declarations = parseVariableDeclarationList(kind); + + consumeSemicolon(); + + return { + type: Syntax.VariableDeclaration, + declarations: declarations, + kind: kind + }; + } + + // 12.3 Empty Statement + + function parseEmptyStatement() { + expect(';'); + + return { + type: Syntax.EmptyStatement + }; + } + + // 12.4 Expression Statement + + function parseExpressionStatement() { + var expr = parseExpression(); + + consumeSemicolon(); + + return { + type: Syntax.ExpressionStatement, + expression: expr + }; + } + + // 12.5 If statement + + function parseIfStatement() { + var test, consequent, alternate; + + expectKeyword('if'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + consequent = parseStatement(); + + if (matchKeyword('else')) { + lex(); + alternate = parseStatement(); + } else { + alternate = null; + } + + return { + type: Syntax.IfStatement, + test: test, + consequent: consequent, + alternate: alternate + }; + } + + // 12.6 Iteration Statements + + function parseDoWhileStatement() { + var body, test, oldInIteration; + + expectKeyword('do'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + expectKeyword('while'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + if (match(';')) { + lex(); + } + + return { + type: Syntax.DoWhileStatement, + body: body, + test: test + }; + } + + function parseWhileStatement() { + var test, body, oldInIteration; + + expectKeyword('while'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + return { + type: Syntax.WhileStatement, + test: test, + body: body + }; + } + + function parseForVariableDeclaration() { + var token = lex(); + + return { + type: Syntax.VariableDeclaration, + declarations: parseVariableDeclarationList(), + kind: token.value + }; + } + + function parseForStatement() { + var init, test, update, left, right, body, oldInIteration; + + init = test = update = null; + + expectKeyword('for'); + + expect('('); + + if (match(';')) { + lex(); + } else { + if (matchKeyword('var') || matchKeyword('let')) { + state.allowIn = false; + init = parseForVariableDeclaration(); + state.allowIn = true; + + if (init.declarations.length === 1 && matchKeyword('in')) { + lex(); + left = init; + right = parseExpression(); + init = null; + } + } else { + state.allowIn = false; + init = parseExpression(); + state.allowIn = true; + + if (matchKeyword('in')) { + // LeftHandSideExpression + if (!isLeftHandSide(init)) { + throwErrorTolerant({}, Messages.InvalidLHSInForIn); + } + + lex(); + left = init; + right = parseExpression(); + init = null; + } + } + + if (typeof left === 'undefined') { + expect(';'); + } + } + + if (typeof left === 'undefined') { + + if (!match(';')) { + test = parseExpression(); + } + expect(';'); + + if (!match(')')) { + update = parseExpression(); + } + } + + expect(')'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + if (typeof left === 'undefined') { + return { + type: Syntax.ForStatement, + init: init, + test: test, + update: update, + body: body + }; + } + + return { + type: Syntax.ForInStatement, + left: left, + right: right, + body: body, + each: false + }; + } + + // 12.7 The continue statement + + function parseContinueStatement() { + var token, label = null; + + expectKeyword('continue'); + + // Optimize the most common form: 'continue;'. + if (source[index] === ';') { + lex(); + + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return { + type: Syntax.ContinueStatement, + label: null + }; + } + + if (peekLineTerminator()) { + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return { + type: Syntax.ContinueStatement, + label: null + }; + } + + token = lookahead(); + if (token.type === Token.Identifier) { + label = parseVariableIdentifier(); + + if (!Object.prototype.hasOwnProperty.call(state.labelSet, label.name)) { + throwError({}, Messages.UnknownLabel, label.name); + } + } + + consumeSemicolon(); + + if (label === null && !state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return { + type: Syntax.ContinueStatement, + label: label + }; + } + + // 12.8 The break statement + + function parseBreakStatement() { + var token, label = null; + + expectKeyword('break'); + + // Optimize the most common form: 'break;'. + if (source[index] === ';') { + lex(); + + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return { + type: Syntax.BreakStatement, + label: null + }; + } + + if (peekLineTerminator()) { + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return { + type: Syntax.BreakStatement, + label: null + }; + } + + token = lookahead(); + if (token.type === Token.Identifier) { + label = parseVariableIdentifier(); + + if (!Object.prototype.hasOwnProperty.call(state.labelSet, label.name)) { + throwError({}, Messages.UnknownLabel, label.name); + } + } + + consumeSemicolon(); + + if (label === null && !(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return { + type: Syntax.BreakStatement, + label: label + }; + } + + // 12.9 The return statement + + function parseReturnStatement() { + var token, argument = null; + + expectKeyword('return'); + + if (!state.inFunctionBody) { + throwErrorTolerant({}, Messages.IllegalReturn); + } + + // 'return' followed by a space and an identifier is very common. + if (source[index] === ' ') { + if (isIdentifierStart(source[index + 1])) { + argument = parseExpression(); + consumeSemicolon(); + return { + type: Syntax.ReturnStatement, + argument: argument + }; + } + } + + if (peekLineTerminator()) { + return { + type: Syntax.ReturnStatement, + argument: null + }; + } + + if (!match(';')) { + token = lookahead(); + if (!match('}') && token.type !== Token.EOF) { + argument = parseExpression(); + } + } + + consumeSemicolon(); + + return { + type: Syntax.ReturnStatement, + argument: argument + }; + } + + // 12.10 The with statement + + function parseWithStatement() { + var object, body; + + if (strict) { + throwErrorTolerant({}, Messages.StrictModeWith); + } + + expectKeyword('with'); + + expect('('); + + object = parseExpression(); + + expect(')'); + + body = parseStatement(); + + return { + type: Syntax.WithStatement, + object: object, + body: body + }; + } + + // 12.10 The swith statement + + function parseSwitchCase() { + var test, + consequent = [], + statement; + + if (matchKeyword('default')) { + lex(); + test = null; + } else { + expectKeyword('case'); + test = parseExpression(); + } + expect(':'); + + while (index < length) { + if (match('}') || matchKeyword('default') || matchKeyword('case')) { + break; + } + statement = parseStatement(); + if (typeof statement === 'undefined') { + break; + } + consequent.push(statement); + } + + return { + type: Syntax.SwitchCase, + test: test, + consequent: consequent + }; + } + + function parseSwitchStatement() { + var discriminant, cases, clause, oldInSwitch, defaultFound; + + expectKeyword('switch'); + + expect('('); + + discriminant = parseExpression(); + + expect(')'); + + expect('{'); + + cases = []; + + if (match('}')) { + lex(); + return { + type: Syntax.SwitchStatement, + discriminant: discriminant, + cases: cases + }; + } + + oldInSwitch = state.inSwitch; + state.inSwitch = true; + defaultFound = false; + + while (index < length) { + if (match('}')) { + break; + } + clause = parseSwitchCase(); + if (clause.test === null) { + if (defaultFound) { + throwError({}, Messages.MultipleDefaultsInSwitch); + } + defaultFound = true; + } + cases.push(clause); + } + + state.inSwitch = oldInSwitch; + + expect('}'); + + return { + type: Syntax.SwitchStatement, + discriminant: discriminant, + cases: cases + }; + } + + // 12.13 The throw statement + + function parseThrowStatement() { + var argument; + + expectKeyword('throw'); + + if (peekLineTerminator()) { + throwError({}, Messages.NewlineAfterThrow); + } + + argument = parseExpression(); + + consumeSemicolon(); + + return { + type: Syntax.ThrowStatement, + argument: argument + }; + } + + // 12.14 The try statement + + function parseCatchClause() { + var param; + + expectKeyword('catch'); + + expect('('); + if (match(')')) { + throwUnexpected(lookahead()); + } + + param = parseVariableIdentifier(); + // 12.14.1 + if (strict && isRestrictedWord(param.name)) { + throwErrorTolerant({}, Messages.StrictCatchVariable); + } + + expect(')'); + + return { + type: Syntax.CatchClause, + param: param, + body: parseBlock() + }; + } + + function parseTryStatement() { + var block, handlers = [], finalizer = null; + + expectKeyword('try'); + + block = parseBlock(); + + if (matchKeyword('catch')) { + handlers.push(parseCatchClause()); + } + + if (matchKeyword('finally')) { + lex(); + finalizer = parseBlock(); + } + + if (handlers.length === 0 && !finalizer) { + throwError({}, Messages.NoCatchOrFinally); + } + + return { + type: Syntax.TryStatement, + block: block, + guardedHandlers: [], + handlers: handlers, + finalizer: finalizer + }; + } + + // 12.15 The debugger statement + + function parseDebuggerStatement() { + expectKeyword('debugger'); + + consumeSemicolon(); + + return { + type: Syntax.DebuggerStatement + }; + } + + // 12 Statements + + function parseStatement() { + var token = lookahead(), + expr, + labeledBody; + + if (token.type === Token.EOF) { + throwUnexpected(token); + } + + if (token.type === Token.Punctuator) { + switch (token.value) { + case ';': + return parseEmptyStatement(); + case '{': + return parseBlock(); + case '(': + return parseExpressionStatement(); + default: + break; + } + } + + if (token.type === Token.Keyword) { + switch (token.value) { + case 'break': + return parseBreakStatement(); + case 'continue': + return parseContinueStatement(); + case 'debugger': + return parseDebuggerStatement(); + case 'do': + return parseDoWhileStatement(); + case 'for': + return parseForStatement(); + case 'function': + return parseFunctionDeclaration(); + case 'if': + return parseIfStatement(); + case 'return': + return parseReturnStatement(); + case 'switch': + return parseSwitchStatement(); + case 'throw': + return parseThrowStatement(); + case 'try': + return parseTryStatement(); + case 'var': + return parseVariableStatement(); + case 'while': + return parseWhileStatement(); + case 'with': + return parseWithStatement(); + default: + break; + } + } + + expr = parseExpression(); + + // 12.12 Labelled Statements + if ((expr.type === Syntax.Identifier) && match(':')) { + lex(); + + if (Object.prototype.hasOwnProperty.call(state.labelSet, expr.name)) { + throwError({}, Messages.Redeclaration, 'Label', expr.name); + } + + state.labelSet[expr.name] = true; + labeledBody = parseStatement(); + delete state.labelSet[expr.name]; + + return { + type: Syntax.LabeledStatement, + label: expr, + body: labeledBody + }; + } + + consumeSemicolon(); + + return { + type: Syntax.ExpressionStatement, + expression: expr + }; + } + + // 13 Function Definition + + function parseFunctionSourceElements() { + var sourceElement, sourceElements = [], token, directive, firstRestricted, + oldLabelSet, oldInIteration, oldInSwitch, oldInFunctionBody; + + expect('{'); + + while (index < length) { + token = lookahead(); + if (token.type !== Token.StringLiteral) { + break; + } + + sourceElement = parseSourceElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; + } + directive = sliceSource(token.range[0] + 1, token.range[1] - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + + oldLabelSet = state.labelSet; + oldInIteration = state.inIteration; + oldInSwitch = state.inSwitch; + oldInFunctionBody = state.inFunctionBody; + + state.labelSet = {}; + state.inIteration = false; + state.inSwitch = false; + state.inFunctionBody = true; + + while (index < length) { + if (match('}')) { + break; + } + sourceElement = parseSourceElement(); + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + + expect('}'); + + state.labelSet = oldLabelSet; + state.inIteration = oldInIteration; + state.inSwitch = oldInSwitch; + state.inFunctionBody = oldInFunctionBody; + + return { + type: Syntax.BlockStatement, + body: sourceElements + }; + } + + function parseFunctionDeclaration() { + var id, param, params = [], body, token, stricted, firstRestricted, message, previousStrict, paramSet; + + expectKeyword('function'); + token = lookahead(); + id = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); + } + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } + } + + expect('('); + + if (!match(')')) { + paramSet = {}; + while (index < length) { + token = lookahead(); + param = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + stricted = token; + message = Messages.StrictParamName; + } + if (Object.prototype.hasOwnProperty.call(paramSet, token.value)) { + stricted = token; + message = Messages.StrictParamDupe; + } + } else if (!firstRestricted) { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictParamName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } else if (Object.prototype.hasOwnProperty.call(paramSet, token.value)) { + firstRestricted = token; + message = Messages.StrictParamDupe; + } + } + params.push(param); + paramSet[param.name] = true; + if (match(')')) { + break; + } + expect(','); + } + } + + expect(')'); + + previousStrict = strict; + body = parseFunctionSourceElements(); + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && stricted) { + throwErrorTolerant(stricted, message); + } + strict = previousStrict; + + return { + type: Syntax.FunctionDeclaration, + id: id, + params: params, + defaults: [], + body: body, + rest: null, + generator: false, + expression: false + }; + } + + function parseFunctionExpression() { + var token, id = null, stricted, firstRestricted, message, param, params = [], body, previousStrict, paramSet; + + expectKeyword('function'); + + if (!match('(')) { + token = lookahead(); + id = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); + } + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } + } + } + + expect('('); + + if (!match(')')) { + paramSet = {}; + while (index < length) { + token = lookahead(); + param = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + stricted = token; + message = Messages.StrictParamName; + } + if (Object.prototype.hasOwnProperty.call(paramSet, token.value)) { + stricted = token; + message = Messages.StrictParamDupe; + } + } else if (!firstRestricted) { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictParamName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } else if (Object.prototype.hasOwnProperty.call(paramSet, token.value)) { + firstRestricted = token; + message = Messages.StrictParamDupe; + } + } + params.push(param); + paramSet[param.name] = true; + if (match(')')) { + break; + } + expect(','); + } + } + + expect(')'); + + previousStrict = strict; + body = parseFunctionSourceElements(); + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && stricted) { + throwErrorTolerant(stricted, message); + } + strict = previousStrict; + + return { + type: Syntax.FunctionExpression, + id: id, + params: params, + defaults: [], + body: body, + rest: null, + generator: false, + expression: false + }; + } + + // 14 Program + + function parseSourceElement() { + var token = lookahead(); + + if (token.type === Token.Keyword) { + switch (token.value) { + case 'const': + case 'let': + return parseConstLetDeclaration(token.value); + case 'function': + return parseFunctionDeclaration(); + default: + return parseStatement(); + } + } + + if (token.type !== Token.EOF) { + return parseStatement(); + } + } + + function parseSourceElements() { + var sourceElement, sourceElements = [], token, directive, firstRestricted; + + while (index < length) { + token = lookahead(); + if (token.type !== Token.StringLiteral) { + break; + } + + sourceElement = parseSourceElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; + } + directive = sliceSource(token.range[0] + 1, token.range[1] - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + + while (index < length) { + sourceElement = parseSourceElement(); + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + return sourceElements; + } + + function parseProgram() { + var program; + strict = false; + program = { + type: Syntax.Program, + body: parseSourceElements() + }; + return program; + } + + // The following functions are needed only when the option to preserve + // the comments is active. + + function addComment(type, value, start, end, loc) { + assert(typeof start === 'number', 'Comment must have valid position'); + + // Because the way the actual token is scanned, often the comments + // (if any) are skipped twice during the lexical analysis. + // Thus, we need to skip adding a comment if the comment array already + // handled it. + if (extra.comments.length > 0) { + if (extra.comments[extra.comments.length - 1].range[1] > start) { + return; + } + } + + extra.comments.push({ + type: type, + value: value, + range: [start, end], + loc: loc + }); + } + + function scanComment() { + var comment, ch, loc, start, blockComment, lineComment; + + comment = ''; + blockComment = false; + lineComment = false; + + while (index < length) { + ch = source[index]; + + if (lineComment) { + ch = source[index++]; + if (isLineTerminator(ch)) { + loc.end = { + line: lineNumber, + column: index - lineStart - 1 + }; + lineComment = false; + addComment('Line', comment, start, index - 1, loc); + if (ch === '\r' && source[index] === '\n') { + ++index; + } + ++lineNumber; + lineStart = index; + comment = ''; + } else if (index >= length) { + lineComment = false; + comment += ch; + loc.end = { + line: lineNumber, + column: length - lineStart + }; + addComment('Line', comment, start, length, loc); + } else { + comment += ch; + } + } else if (blockComment) { + if (isLineTerminator(ch)) { + if (ch === '\r' && source[index + 1] === '\n') { + ++index; + comment += '\r\n'; + } else { + comment += ch; + } + ++lineNumber; + ++index; + lineStart = index; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + ch = source[index++]; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + comment += ch; + if (ch === '*') { + ch = source[index]; + if (ch === '/') { + comment = comment.substr(0, comment.length - 1); + blockComment = false; + ++index; + loc.end = { + line: lineNumber, + column: index - lineStart + }; + addComment('Block', comment, start, index, loc); + comment = ''; + } + } + } + } else if (ch === '/') { + ch = source[index + 1]; + if (ch === '/') { + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + start = index; + index += 2; + lineComment = true; + if (index >= length) { + loc.end = { + line: lineNumber, + column: index - lineStart + }; + lineComment = false; + addComment('Line', comment, start, index, loc); + } + } else if (ch === '*') { + start = index; + index += 2; + blockComment = true; + loc = { + start: { + line: lineNumber, + column: index - lineStart - 2 + } + }; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + break; + } + } else if (isWhiteSpace(ch)) { + ++index; + } else if (isLineTerminator(ch)) { + ++index; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + ++lineNumber; + lineStart = index; + } else { + break; + } + } + } + + function filterCommentLocation() { + var i, entry, comment, comments = []; + + for (i = 0; i < extra.comments.length; ++i) { + entry = extra.comments[i]; + comment = { + type: entry.type, + value: entry.value + }; + if (extra.range) { + comment.range = entry.range; + } + if (extra.loc) { + comment.loc = entry.loc; + } + comments.push(comment); + } + + extra.comments = comments; + } + + function collectToken() { + var start, loc, token, range, value; + + skipComment(); + start = index; + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + + token = extra.advance(); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + + if (token.type !== Token.EOF) { + range = [token.range[0], token.range[1]]; + value = sliceSource(token.range[0], token.range[1]); + extra.tokens.push({ + type: TokenName[token.type], + value: value, + range: range, + loc: loc + }); + } + + return token; + } + + function collectRegex() { + var pos, loc, regex, token; + + skipComment(); + + pos = index; + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + + regex = extra.scanRegExp(); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + + // Pop the previous token, which is likely '/' or '/=' + if (extra.tokens.length > 0) { + token = extra.tokens[extra.tokens.length - 1]; + if (token.range[0] === pos && token.type === 'Punctuator') { + if (token.value === '/' || token.value === '/=') { + extra.tokens.pop(); + } + } + } + + extra.tokens.push({ + type: 'RegularExpression', + value: regex.literal, + range: [pos, index], + loc: loc + }); + + return regex; + } + + function filterTokenLocation() { + var i, entry, token, tokens = []; + + for (i = 0; i < extra.tokens.length; ++i) { + entry = extra.tokens[i]; + token = { + type: entry.type, + value: entry.value + }; + if (extra.range) { + token.range = entry.range; + } + if (extra.loc) { + token.loc = entry.loc; + } + tokens.push(token); + } + + extra.tokens = tokens; + } + + function createLiteral(token) { + return { + type: Syntax.Literal, + value: token.value + }; + } + + function createRawLiteral(token) { + return { + type: Syntax.Literal, + value: token.value, + raw: sliceSource(token.range[0], token.range[1]) + }; + } + + function createLocationMarker() { + var marker = {}; + + marker.range = [index, index]; + marker.loc = { + start: { + line: lineNumber, + column: index - lineStart + }, + end: { + line: lineNumber, + column: index - lineStart + } + }; + + marker.end = function () { + this.range[1] = index; + this.loc.end.line = lineNumber; + this.loc.end.column = index - lineStart; + }; + + marker.applyGroup = function (node) { + if (extra.range) { + node.groupRange = [this.range[0], this.range[1]]; + } + if (extra.loc) { + node.groupLoc = { + start: { + line: this.loc.start.line, + column: this.loc.start.column + }, + end: { + line: this.loc.end.line, + column: this.loc.end.column + } + }; + } + }; + + marker.apply = function (node) { + if (extra.range) { + node.range = [this.range[0], this.range[1]]; + } + if (extra.loc) { + node.loc = { + start: { + line: this.loc.start.line, + column: this.loc.start.column + }, + end: { + line: this.loc.end.line, + column: this.loc.end.column + } + }; + } + }; + + return marker; + } + + function trackGroupExpression() { + var marker, expr; + + skipComment(); + marker = createLocationMarker(); + expect('('); + + expr = parseExpression(); + + expect(')'); + + marker.end(); + marker.applyGroup(expr); + + return expr; + } + + function trackLeftHandSideExpression() { + var marker, expr; + + skipComment(); + marker = createLocationMarker(); + + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + + while (match('.') || match('[')) { + if (match('[')) { + expr = { + type: Syntax.MemberExpression, + computed: true, + object: expr, + property: parseComputedMember() + }; + marker.end(); + marker.apply(expr); + } else { + expr = { + type: Syntax.MemberExpression, + computed: false, + object: expr, + property: parseNonComputedMember() + }; + marker.end(); + marker.apply(expr); + } + } + + return expr; + } + + function trackLeftHandSideExpressionAllowCall() { + var marker, expr; + + skipComment(); + marker = createLocationMarker(); + + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + + while (match('.') || match('[') || match('(')) { + if (match('(')) { + expr = { + type: Syntax.CallExpression, + callee: expr, + 'arguments': parseArguments() + }; + marker.end(); + marker.apply(expr); + } else if (match('[')) { + expr = { + type: Syntax.MemberExpression, + computed: true, + object: expr, + property: parseComputedMember() + }; + marker.end(); + marker.apply(expr); + } else { + expr = { + type: Syntax.MemberExpression, + computed: false, + object: expr, + property: parseNonComputedMember() + }; + marker.end(); + marker.apply(expr); + } + } + + return expr; + } + + function filterGroup(node) { + var n, i, entry; + + n = (Object.prototype.toString.apply(node) === '[object Array]') ? [] : {}; + for (i in node) { + if (node.hasOwnProperty(i) && i !== 'groupRange' && i !== 'groupLoc') { + entry = node[i]; + if (entry === null || typeof entry !== 'object' || entry instanceof RegExp) { + n[i] = entry; + } else { + n[i] = filterGroup(entry); + } + } + } + return n; + } + + function wrapTrackingFunction(range, loc) { + + return function (parseFunction) { + + function isBinary(node) { + return node.type === Syntax.LogicalExpression || + node.type === Syntax.BinaryExpression; + } + + function visit(node) { + var start, end; + + if (isBinary(node.left)) { + visit(node.left); + } + if (isBinary(node.right)) { + visit(node.right); + } + + if (range) { + if (node.left.groupRange || node.right.groupRange) { + start = node.left.groupRange ? node.left.groupRange[0] : node.left.range[0]; + end = node.right.groupRange ? node.right.groupRange[1] : node.right.range[1]; + node.range = [start, end]; + } else if (typeof node.range === 'undefined') { + start = node.left.range[0]; + end = node.right.range[1]; + node.range = [start, end]; + } + } + if (loc) { + if (node.left.groupLoc || node.right.groupLoc) { + start = node.left.groupLoc ? node.left.groupLoc.start : node.left.loc.start; + end = node.right.groupLoc ? node.right.groupLoc.end : node.right.loc.end; + node.loc = { + start: start, + end: end + }; + } else if (typeof node.loc === 'undefined') { + node.loc = { + start: node.left.loc.start, + end: node.right.loc.end + }; + } + } + } + + return function () { + var marker, node; + + skipComment(); + + marker = createLocationMarker(); + node = parseFunction.apply(null, arguments); + marker.end(); + + if (range && typeof node.range === 'undefined') { + marker.apply(node); + } + + if (loc && typeof node.loc === 'undefined') { + marker.apply(node); + } + + if (isBinary(node)) { + visit(node); + } + + return node; + }; + }; + } + + function patch() { + + var wrapTracking; + + if (extra.comments) { + extra.skipComment = skipComment; + skipComment = scanComment; + } + + if (extra.raw) { + extra.createLiteral = createLiteral; + createLiteral = createRawLiteral; + } + + if (extra.range || extra.loc) { + + extra.parseGroupExpression = parseGroupExpression; + extra.parseLeftHandSideExpression = parseLeftHandSideExpression; + extra.parseLeftHandSideExpressionAllowCall = parseLeftHandSideExpressionAllowCall; + parseGroupExpression = trackGroupExpression; + parseLeftHandSideExpression = trackLeftHandSideExpression; + parseLeftHandSideExpressionAllowCall = trackLeftHandSideExpressionAllowCall; + + wrapTracking = wrapTrackingFunction(extra.range, extra.loc); + + extra.parseAdditiveExpression = parseAdditiveExpression; + extra.parseAssignmentExpression = parseAssignmentExpression; + extra.parseBitwiseANDExpression = parseBitwiseANDExpression; + extra.parseBitwiseORExpression = parseBitwiseORExpression; + extra.parseBitwiseXORExpression = parseBitwiseXORExpression; + extra.parseBlock = parseBlock; + extra.parseFunctionSourceElements = parseFunctionSourceElements; + extra.parseCatchClause = parseCatchClause; + extra.parseComputedMember = parseComputedMember; + extra.parseConditionalExpression = parseConditionalExpression; + extra.parseConstLetDeclaration = parseConstLetDeclaration; + extra.parseEqualityExpression = parseEqualityExpression; + extra.parseExpression = parseExpression; + extra.parseForVariableDeclaration = parseForVariableDeclaration; + extra.parseFunctionDeclaration = parseFunctionDeclaration; + extra.parseFunctionExpression = parseFunctionExpression; + extra.parseLogicalANDExpression = parseLogicalANDExpression; + extra.parseLogicalORExpression = parseLogicalORExpression; + extra.parseMultiplicativeExpression = parseMultiplicativeExpression; + extra.parseNewExpression = parseNewExpression; + extra.parseNonComputedProperty = parseNonComputedProperty; + extra.parseObjectProperty = parseObjectProperty; + extra.parseObjectPropertyKey = parseObjectPropertyKey; + extra.parsePostfixExpression = parsePostfixExpression; + extra.parsePrimaryExpression = parsePrimaryExpression; + extra.parseProgram = parseProgram; + extra.parsePropertyFunction = parsePropertyFunction; + extra.parseRelationalExpression = parseRelationalExpression; + extra.parseStatement = parseStatement; + extra.parseShiftExpression = parseShiftExpression; + extra.parseSwitchCase = parseSwitchCase; + extra.parseUnaryExpression = parseUnaryExpression; + extra.parseVariableDeclaration = parseVariableDeclaration; + extra.parseVariableIdentifier = parseVariableIdentifier; + + parseAdditiveExpression = wrapTracking(extra.parseAdditiveExpression); + parseAssignmentExpression = wrapTracking(extra.parseAssignmentExpression); + parseBitwiseANDExpression = wrapTracking(extra.parseBitwiseANDExpression); + parseBitwiseORExpression = wrapTracking(extra.parseBitwiseORExpression); + parseBitwiseXORExpression = wrapTracking(extra.parseBitwiseXORExpression); + parseBlock = wrapTracking(extra.parseBlock); + parseFunctionSourceElements = wrapTracking(extra.parseFunctionSourceElements); + parseCatchClause = wrapTracking(extra.parseCatchClause); + parseComputedMember = wrapTracking(extra.parseComputedMember); + parseConditionalExpression = wrapTracking(extra.parseConditionalExpression); + parseConstLetDeclaration = wrapTracking(extra.parseConstLetDeclaration); + parseEqualityExpression = wrapTracking(extra.parseEqualityExpression); + parseExpression = wrapTracking(extra.parseExpression); + parseForVariableDeclaration = wrapTracking(extra.parseForVariableDeclaration); + parseFunctionDeclaration = wrapTracking(extra.parseFunctionDeclaration); + parseFunctionExpression = wrapTracking(extra.parseFunctionExpression); + parseLeftHandSideExpression = wrapTracking(parseLeftHandSideExpression); + parseLogicalANDExpression = wrapTracking(extra.parseLogicalANDExpression); + parseLogicalORExpression = wrapTracking(extra.parseLogicalORExpression); + parseMultiplicativeExpression = wrapTracking(extra.parseMultiplicativeExpression); + parseNewExpression = wrapTracking(extra.parseNewExpression); + parseNonComputedProperty = wrapTracking(extra.parseNonComputedProperty); + parseObjectProperty = wrapTracking(extra.parseObjectProperty); + parseObjectPropertyKey = wrapTracking(extra.parseObjectPropertyKey); + parsePostfixExpression = wrapTracking(extra.parsePostfixExpression); + parsePrimaryExpression = wrapTracking(extra.parsePrimaryExpression); + parseProgram = wrapTracking(extra.parseProgram); + parsePropertyFunction = wrapTracking(extra.parsePropertyFunction); + parseRelationalExpression = wrapTracking(extra.parseRelationalExpression); + parseStatement = wrapTracking(extra.parseStatement); + parseShiftExpression = wrapTracking(extra.parseShiftExpression); + parseSwitchCase = wrapTracking(extra.parseSwitchCase); + parseUnaryExpression = wrapTracking(extra.parseUnaryExpression); + parseVariableDeclaration = wrapTracking(extra.parseVariableDeclaration); + parseVariableIdentifier = wrapTracking(extra.parseVariableIdentifier); + } + + if (typeof extra.tokens !== 'undefined') { + extra.advance = advance; + extra.scanRegExp = scanRegExp; + + advance = collectToken; + scanRegExp = collectRegex; + } + } + + function unpatch() { + if (typeof extra.skipComment === 'function') { + skipComment = extra.skipComment; + } + + if (extra.raw) { + createLiteral = extra.createLiteral; + } + + if (extra.range || extra.loc) { + parseAdditiveExpression = extra.parseAdditiveExpression; + parseAssignmentExpression = extra.parseAssignmentExpression; + parseBitwiseANDExpression = extra.parseBitwiseANDExpression; + parseBitwiseORExpression = extra.parseBitwiseORExpression; + parseBitwiseXORExpression = extra.parseBitwiseXORExpression; + parseBlock = extra.parseBlock; + parseFunctionSourceElements = extra.parseFunctionSourceElements; + parseCatchClause = extra.parseCatchClause; + parseComputedMember = extra.parseComputedMember; + parseConditionalExpression = extra.parseConditionalExpression; + parseConstLetDeclaration = extra.parseConstLetDeclaration; + parseEqualityExpression = extra.parseEqualityExpression; + parseExpression = extra.parseExpression; + parseForVariableDeclaration = extra.parseForVariableDeclaration; + parseFunctionDeclaration = extra.parseFunctionDeclaration; + parseFunctionExpression = extra.parseFunctionExpression; + parseGroupExpression = extra.parseGroupExpression; + parseLeftHandSideExpression = extra.parseLeftHandSideExpression; + parseLeftHandSideExpressionAllowCall = extra.parseLeftHandSideExpressionAllowCall; + parseLogicalANDExpression = extra.parseLogicalANDExpression; + parseLogicalORExpression = extra.parseLogicalORExpression; + parseMultiplicativeExpression = extra.parseMultiplicativeExpression; + parseNewExpression = extra.parseNewExpression; + parseNonComputedProperty = extra.parseNonComputedProperty; + parseObjectProperty = extra.parseObjectProperty; + parseObjectPropertyKey = extra.parseObjectPropertyKey; + parsePrimaryExpression = extra.parsePrimaryExpression; + parsePostfixExpression = extra.parsePostfixExpression; + parseProgram = extra.parseProgram; + parsePropertyFunction = extra.parsePropertyFunction; + parseRelationalExpression = extra.parseRelationalExpression; + parseStatement = extra.parseStatement; + parseShiftExpression = extra.parseShiftExpression; + parseSwitchCase = extra.parseSwitchCase; + parseUnaryExpression = extra.parseUnaryExpression; + parseVariableDeclaration = extra.parseVariableDeclaration; + parseVariableIdentifier = extra.parseVariableIdentifier; + } + + if (typeof extra.scanRegExp === 'function') { + advance = extra.advance; + scanRegExp = extra.scanRegExp; + } + } + + function stringToArray(str) { + var length = str.length, + result = [], + i; + for (i = 0; i < length; ++i) { + result[i] = str.charAt(i); + } + return result; + } + + function parse(code, options) { + var program, toString; + + toString = String; + if (typeof code !== 'string' && !(code instanceof String)) { + code = toString(code); + } + + source = code; + index = 0; + lineNumber = (source.length > 0) ? 1 : 0; + lineStart = 0; + length = source.length; + buffer = null; + state = { + allowIn: true, + labelSet: {}, + inFunctionBody: false, + inIteration: false, + inSwitch: false + }; + + extra = {}; + if (typeof options !== 'undefined') { + extra.range = (typeof options.range === 'boolean') && options.range; + extra.loc = (typeof options.loc === 'boolean') && options.loc; + extra.raw = (typeof options.raw === 'boolean') && options.raw; + if (typeof options.tokens === 'boolean' && options.tokens) { + extra.tokens = []; + } + if (typeof options.comment === 'boolean' && options.comment) { + extra.comments = []; + } + if (typeof options.tolerant === 'boolean' && options.tolerant) { + extra.errors = []; + } + } + + if (length > 0) { + if (typeof source[0] === 'undefined') { + // Try first to convert to a string. This is good as fast path + // for old IE which understands string indexing for string + // literals only and not for string object. + if (code instanceof String) { + source = code.valueOf(); + } + + // Force accessing the characters via an array. + if (typeof source[0] === 'undefined') { + source = stringToArray(code); + } + } + } + + patch(); + try { + program = parseProgram(); + if (typeof extra.comments !== 'undefined') { + filterCommentLocation(); + program.comments = extra.comments; + } + if (typeof extra.tokens !== 'undefined') { + filterTokenLocation(); + program.tokens = extra.tokens; + } + if (typeof extra.errors !== 'undefined') { + program.errors = extra.errors; + } + if (extra.range || extra.loc) { + program.body = filterGroup(program.body); + } + } catch (e) { + throw e; + } finally { + unpatch(); + extra = {}; + } + + return program; + } + + // Sync with package.json. + exports.version = '1.0.4'; + + exports.parse = parse; + + // Deep copy. + exports.Syntax = (function () { + var name, types = {}; + + if (typeof Object.create === 'function') { + types = Object.create(null); + } + + for (name in Syntax) { + if (Syntax.hasOwnProperty(name)) { + types[name] = Syntax[name]; + } + } + + if (typeof Object.freeze === 'function') { + Object.freeze(types); + } + + return types; + }()); + +})); +/* vim: set sw=4 ts=4 et tw=80 : */ + +})(null); +/*! + * falafel (c) James Halliday / MIT License + * https://github.com/substack/node-falafel + */ + +(function(require,module){ +var parse = require('esprima').parse; +var objectKeys = Object.keys || function (obj) { + var keys = []; + for (var key in obj) keys.push(key); + return keys; +}; +var forEach = function (xs, fn) { + if (xs.forEach) return xs.forEach(fn); + for (var i = 0; i < xs.length; i++) { + fn.call(xs, xs[i], i, xs); + } +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +module.exports = function (src, opts, fn) { + if (typeof opts === 'function') { + fn = opts; + opts = {}; + } + if (typeof src === 'object') { + opts = src; + src = opts.source; + delete opts.source; + } + src = src === undefined ? opts.source : src; + opts.range = true; + if (typeof src !== 'string') src = String(src); + + var ast = parse(src, opts); + + var result = { + chunks : src.split(''), + toString : function () { return result.chunks.join('') }, + inspect : function () { return result.toString() } + }; + var index = 0; + + (function walk (node, parent) { + insertHelpers(node, parent, result.chunks); + + forEach(objectKeys(node), function (key) { + if (key === 'parent') return; + + var child = node[key]; + if (isArray(child)) { + forEach(child, function (c) { + if (c && typeof c.type === 'string') { + walk(c, node); + } + }); + } + else if (child && typeof child.type === 'string') { + insertHelpers(child, node, result.chunks); + walk(child, node); + } + }); + fn(node); + })(ast, undefined); + + return result; +}; + +function insertHelpers (node, parent, chunks) { + if (!node.range) return; + + node.parent = parent; + + node.source = function () { + return chunks.slice( + node.range[0], node.range[1] + ).join(''); + }; + + if (node.update && typeof node.update === 'object') { + var prev = node.update; + forEach(objectKeys(prev), function (key) { + update[key] = prev[key]; + }); + node.update = update; + } + else { + node.update = update; + } + + function update (s) { + chunks[node.range[0]] = s; + for (var i = node.range[0] + 1; i < node.range[1]; i++) { + chunks[i] = ''; + } + }; +} + +window.falafel = module.exports;})(function(){return {parse: esprima.parse};},{exports: {}}); +var inBrowser = typeof window !== 'undefined' && this === window; +var parseAndModify = (inBrowser ? window.falafel : require("falafel")); + +(inBrowser ? window : exports).blanket = (function(){ + var linesToAddTracking = [ + "ExpressionStatement", + "BreakStatement" , + "ContinueStatement" , + "VariableDeclaration", + "ReturnStatement" , + "ThrowStatement" , + "TryStatement" , + "FunctionDeclaration" , + "IfStatement" , + "WhileStatement" , + "DoWhileStatement" , + "ForStatement" , + "ForInStatement" , + "SwitchStatement" , + "WithStatement" + ], + linesToAddBrackets = [ + "IfStatement" , + "WhileStatement" , + "DoWhileStatement" , + "ForStatement" , + "ForInStatement" , + "WithStatement" + ], + __blanket, + copynumber = Math.floor(Math.random()*1000), + coverageInfo = {},options = { + reporter: null, + adapter:null, + filter: null, + customVariable: null, + loader: null, + ignoreScriptError: false, + existingRequireJS:false, + autoStart: false, + timeout: 180, + ignoreCors: false, + branchTracking: false, + sourceURL: false, + debug:false, + engineOnly:false, + testReadyCallback:null, + commonJS:false, + instrumentCache:false, + modulePattern: null + }; + + if (inBrowser && typeof window.blanket !== 'undefined'){ + __blanket = window.blanket.noConflict(); + } + + _blanket = { + noConflict: function(){ + if (__blanket){ + return __blanket; + } + return _blanket; + }, + _getCopyNumber: function(){ + //internal method + //for differentiating between instances + return copynumber; + }, + extend: function(obj) { + //borrowed from underscore + _blanket._extend(_blanket,obj); + }, + _extend: function(dest,source){ + if (source) { + for (var prop in source) { + if ( dest[prop] instanceof Object && typeof dest[prop] !== "function"){ + _blanket._extend(dest[prop],source[prop]); + }else{ + dest[prop] = source[prop]; + } + } + } + }, + getCovVar: function(){ + var opt = _blanket.options("customVariable"); + if (opt){ + if (_blanket.options("debug")) {console.log("BLANKET-Using custom tracking variable:",opt);} + return inBrowser ? "window."+opt : opt; + } + return inBrowser ? "window._$blanket" : "_$jscoverage"; + }, + options: function(key,value){ + if (typeof key !== "string"){ + _blanket._extend(options,key); + }else if (typeof value === 'undefined'){ + return options[key]; + }else{ + options[key]=value; + } + }, + instrument: function(config, next){ + //check instrumented hash table, + //return instrumented code if available. + var inFile = config.inputFile, + inFileName = config.inputFileName; + //check instrument cache + if (_blanket.options("instrumentCache") && sessionStorage && sessionStorage.getItem("blanket_instrument_store-"+inFileName)){ + if (_blanket.options("debug")) {console.log("BLANKET-Reading instrumentation from cache: ",inFileName);} + next(sessionStorage.getItem("blanket_instrument_store-"+inFileName)); + }else{ + var sourceArray = _blanket._prepareSource(inFile); + _blanket._trackingArraySetup=[]; + //remove shebang + inFile = inFile.replace(/^\#\!.*/, ""); + var instrumented = parseAndModify(inFile,{loc:true,comment:true}, _blanket._addTracking(inFileName)); + instrumented = _blanket._trackingSetup(inFileName,sourceArray)+instrumented; + if (_blanket.options("sourceURL")){ + instrumented += "\n//@ sourceURL="+inFileName.replace("http://",""); + } + if (_blanket.options("debug")) {console.log("BLANKET-Instrumented file: ",inFileName);} + if (_blanket.options("instrumentCache") && sessionStorage){ + if (_blanket.options("debug")) {console.log("BLANKET-Saving instrumentation to cache: ",inFileName);} + sessionStorage.setItem("blanket_instrument_store-"+inFileName,instrumented); + } + next(instrumented); + } + }, + _trackingArraySetup: [], + _branchingArraySetup: [], + _prepareSource: function(source){ + return source.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/(\r\n|\n|\r)/gm,"\n").split('\n'); + }, + _trackingSetup: function(filename,sourceArray){ + var branches = _blanket.options("branchTracking"); + var sourceString = sourceArray.join("',\n'"); + var intro = ""; + var covVar = _blanket.getCovVar(); + + intro += "if (typeof "+covVar+" === 'undefined') "+covVar+" = {};\n"; + if (branches){ + intro += "var _$branchFcn=function(f,l,c,r){ "; + intro += "if (!!r) { "; + intro += covVar+"[f].branchData[l][c][0] = "+covVar+"[f].branchData[l][c][0] || [];"; + intro += covVar+"[f].branchData[l][c][0].push(r); }"; + intro += "else { "; + intro += covVar+"[f].branchData[l][c][1] = "+covVar+"[f].branchData[l][c][1] || [];"; + intro += covVar+"[f].branchData[l][c][1].push(r); }"; + intro += "return r;};\n"; + } + intro += "if (typeof "+covVar+"['"+filename+"'] === 'undefined'){"; + + intro += covVar+"['"+filename+"']=[];\n"; + if (branches){ + intro += covVar+"['"+filename+"'].branchData=[];\n"; + } + intro += covVar+"['"+filename+"'].source=['"+sourceString+"'];\n"; + //initialize array values + _blanket._trackingArraySetup.sort(function(a,b){ + return parseInt(a,10) > parseInt(b,10); + }).forEach(function(item){ + intro += covVar+"['"+filename+"']["+item+"]=0;\n"; + }); + if (branches){ + _blanket._branchingArraySetup.sort(function(a,b){ + return a.line > b.line; + }).sort(function(a,b){ + return a.column > b.column; + }).forEach(function(item){ + if (item.file === filename){ + intro += "if (typeof "+ covVar+"['"+filename+"'].branchData["+item.line+"] === 'undefined'){\n"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]=[];\n"; + intro += "}"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"] = [];\n"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].consequent = "+JSON.stringify(item.consequent)+";\n"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].alternate = "+JSON.stringify(item.alternate)+";\n"; + } + }); + } + intro += "}"; + + return intro; + }, + _blockifyIf: function(node){ + if (linesToAddBrackets.indexOf(node.type) > -1){ + var bracketsExistObject = node.consequent || node.body; + var bracketsExistAlt = node.alternate; + if( bracketsExistAlt && bracketsExistAlt.type !== "BlockStatement") { + bracketsExistAlt.update("{\n"+bracketsExistAlt.source()+"}\n"); + } + if( bracketsExistObject && bracketsExistObject.type !== "BlockStatement") { + bracketsExistObject.update("{\n"+bracketsExistObject.source()+"}\n"); + } + } + }, + _trackBranch: function(node,filename){ + //recursive on consequent and alternative + var line = node.loc.start.line; + var col = node.loc.start.column; + + _blanket._branchingArraySetup.push({ + line: line, + column: col, + file:filename, + consequent: node.consequent.loc, + alternate: node.alternate.loc + }); + + var updated = "_$branchFcn"+ + "('"+filename+"',"+line+","+col+","+node.test.source()+ + ")?"+node.consequent.source()+":"+node.alternate.source(); + node.update(updated); + }, + _addTracking: function (filename) { + //falafel doesn't take a file name + //so we include the filename in a closure + //and return the function to falafel + var covVar = _blanket.getCovVar(); + + return function(node){ + _blanket._blockifyIf(node); + + if (linesToAddTracking.indexOf(node.type) > -1 && node.parent.type !== "LabeledStatement") { + _blanket._checkDefs(node,filename); + if (node.type === "VariableDeclaration" && + (node.parent.type === "ForStatement" || node.parent.type === "ForInStatement")){ + return; + } + if (node.loc && node.loc.start){ + node.update(covVar+"['"+filename+"']["+node.loc.start.line+"]++;\n"+node.source()); + _blanket._trackingArraySetup.push(node.loc.start.line); + }else{ + //I don't think we can handle a node with no location + throw new Error("The instrumenter encountered a node with no location: "+Object.keys(node)); + } + }else if (_blanket.options("branchTracking") && node.type === "ConditionalExpression"){ + _blanket._trackBranch(node,filename); + } + }; + }, + _checkDefs: function(node,filename){ + // Make sure developers don't redefine window. if they do, inform them it is wrong. + if (inBrowser){ + if (node.type === "VariableDeclaration" && node.declarations) { + node.declarations.forEach(function(declaration) { + if (declaration.id.name === "window") { + throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line); + } + }); + } + if (node.type === "FunctionDeclaration" && node.params) { + node.params.forEach(function(param) { + if (param.name === "window") { + throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line); + } + }); + } + //Make sure developers don't redefine the coverage variable + if (node.type === "ExpressionStatement" && + node.expression && node.expression.left && + node.expression.left.object && node.expression.left.property && + node.expression.left.object.name + + "." + node.expression.left.property.name === _blanket.getCovVar()) { + throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line); + } + }else{ + //Make sure developers don't redefine the coverage variable in node + if (node.type === "ExpressionStatement" && + node.expression && node.expression.left && + !node.expression.left.object && !node.expression.left.property && + node.expression.left.name === _blanket.getCovVar()) { + throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line); + } + } + }, + setupCoverage: function(){ + coverageInfo.instrumentation = "blanket"; + coverageInfo.stats = { + "suites": 0, + "tests": 0, + "passes": 0, + "pending": 0, + "failures": 0, + "start": new Date() + }; + }, + _checkIfSetup: function(){ + if (!coverageInfo.stats){ + throw new Error("You must call blanket.setupCoverage() first."); + } + }, + onTestStart: function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Test event started");} + this._checkIfSetup(); + coverageInfo.stats.tests++; + coverageInfo.stats.pending++; + }, + onTestDone: function(total,passed){ + this._checkIfSetup(); + if(passed === total){ + coverageInfo.stats.passes++; + }else{ + coverageInfo.stats.failures++; + } + coverageInfo.stats.pending--; + }, + onModuleStart: function(){ + this._checkIfSetup(); + coverageInfo.stats.suites++; + }, + onTestsDone: function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Test event done");} + this._checkIfSetup(); + coverageInfo.stats.end = new Date(); + + if (inBrowser){ + this.report(coverageInfo); + }else{ + if (!_blanket.options("branchTracking")){ + delete (inBrowser ? window : global)[_blanket.getCovVar()].branchFcn; + } + this.options("reporter").call(this,coverageInfo); + } + } + }; + return _blanket; +})(); + +(function(_blanket){ + var oldOptions = _blanket.options; +_blanket.extend({ + outstandingRequireFiles:[], + options: function(key,value){ + var newVal={}; + + if (typeof key !== "string"){ + //key is key/value map + oldOptions(key); + newVal = key; + }else if (typeof value === 'undefined'){ + //accessor + return oldOptions(key); + }else{ + //setter + oldOptions(key,value); + newVal[key] = value; + } + + if (newVal.adapter){ + _blanket._loadFile(newVal.adapter); + } + if (newVal.loader){ + _blanket._loadFile(newVal.loader); + } + }, + requiringFile: function(filename,done){ + if (typeof filename === "undefined"){ + _blanket.outstandingRequireFiles=[]; + }else if (typeof done === "undefined"){ + _blanket.outstandingRequireFiles.push(filename); + }else{ + _blanket.outstandingRequireFiles.splice(_blanket.outstandingRequireFiles.indexOf(filename),1); + } + }, + requireFilesLoaded: function(){ + return _blanket.outstandingRequireFiles.length === 0; + }, + showManualLoader: function(){ + if (document.getElementById("blanketLoaderDialog")){ + return; + } + //copied from http://blog.avtex.com/2012/01/26/cross-browser-css-only-modal-box/ + var loader = "
"; + loader += " 
"; + loader += "
"; + loader += "
"; + loader += "Error: Blanket.js encountered a cross origin request error while instrumenting the source files. "; + loader += "

This is likely caused by the source files being referenced locally (using the file:// protocol). "; + loader += "

Some solutions include starting Chrome with special flags, running a server locally, or using a browser without these CORS restrictions (Safari)."; + loader += "
"; + if (typeof FileReader !== "undefined"){ + loader += "
Or, try the experimental loader. When prompted, simply click on the directory containing all the source files you want covered."; + loader += "Start Loader"; + loader += ""; + } + loader += "
Close"; + loader += "
"; + loader += "
"; + + var css = ".blanketDialogWrapper {"; + css += "display:block;"; + css += "position:fixed;"; + css += "z-index:40001; }"; + + css += ".blanketDialogOverlay {"; + css += "position:fixed;"; + css += "width:100%;"; + css += "height:100%;"; + css += "background-color:black;"; + css += "opacity:.5; "; + css += "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; "; + css += "filter:alpha(opacity=50); "; + css += "z-index:40001; }"; + + css += ".blanketDialogVerticalOffset { "; + css += "position:fixed;"; + css += "top:30%;"; + css += "width:100%;"; + css += "z-index:40002; }"; + + css += ".blanketDialogBox { "; + css += "width:405px; "; + css += "position:relative;"; + css += "margin:0 auto;"; + css += "background-color:white;"; + css += "padding:10px;"; + css += "border:1px solid black; }"; + + var dom = document.createElement("style"); + dom.innerHTML = css; + document.head.appendChild(dom); + + var div = document.createElement("div"); + div.id = "blanketLoaderDialog"; + div.className = "blanketDialogWrapper"; + div.innerHTML = loader; + document.body.insertBefore(div,document.body.firstChild); + + }, + manualFileLoader: function(files){ + var toArray =Array.prototype.slice; + files = toArray.call(files).filter(function(item){ + return item.type !== ""; + }); + var sessionLength = files.length-1; + var sessionIndx=0; + var sessionArray = {}; + if (sessionStorage["blanketSessionLoader"]){ + sessionArray = JSON.parse(sessionStorage["blanketSessionLoader"]); + } + + + var fileLoader = function(event){ + var fileContent = event.currentTarget.result; + var file = files[sessionIndx]; + var filename = file.webkitRelativePath && file.webkitRelativePath !== '' ? file.webkitRelativePath : file.name; + sessionArray[filename] = fileContent; + sessionIndx++; + if (sessionIndx === sessionLength){ + sessionStorage.setItem("blanketSessionLoader", JSON.stringify(sessionArray)); + document.location.reload(); + }else{ + readFile(files[sessionIndx]); + } + }; + function readFile(file){ + var reader = new FileReader(); + reader.onload = fileLoader; + reader.readAsText(file); + } + readFile(files[sessionIndx]); + }, + _loadFile: function(path){ + if (typeof path !== "undefined"){ + var request = new XMLHttpRequest(); + request.open('GET', path, false); + request.send(); + _blanket._addScript(request.responseText); + } + }, + _addScript: function(data){ + var script = document.createElement("script"); + script.type = "text/javascript"; + script.text = data; + (document.body || document.getElementsByTagName('head')[0]).appendChild(script); + }, + hasAdapter: function(callback){ + return _blanket.options("adapter") !== null; + }, + report: function(coverage_data){ + if (!document.getElementById("blanketLoaderDialog")){ + //all found, clear it + _blanket.blanketSession = null; + } + coverage_data.files = window._$blanket; + var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; + + // Check if we have any covered files that requires reporting + // otherwise just exit gracefully. + if (!coverage_data.files || !Object.keys(coverage_data.files).length) { + if (_blanket.options("debug")) {console.log("BLANKET-Reporting No files were instrumented.");} + return; + } + + if (typeof coverage_data.files.branchFcn !== "undefined"){ + delete coverage_data.files.branchFcn; + } + if (typeof _blanket.options("reporter") === "string"){ + _blanket._loadFile(_blanket.options("reporter")); + _blanket.customReporter(coverage_data,_blanket.options("reporter_options")); + }else if (typeof _blanket.options("reporter") === "function"){ + _blanket.options("reporter")(coverage_data,_blanket.options("reporter_options")); + }else if (typeof _blanket.defaultReporter === 'function'){ + _blanket.defaultReporter(coverage_data,_blanket.options("reporter_options")); + }else{ + throw new Error("no reporter defined."); + } + }, + _bindStartTestRunner: function(bindEvent,startEvent){ + if (bindEvent){ + bindEvent(startEvent); + }else{ + window.addEventListener("load",startEvent,false); + } + }, + _loadSourceFiles: function(callback){ + var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; + function copy(o){ + var _copy = Object.create( Object.getPrototypeOf(o) ); + var propNames = Object.getOwnPropertyNames(o); + + propNames.forEach(function(name){ + var desc = Object.getOwnPropertyDescriptor(o, name); + Object.defineProperty(_copy, name, desc); + }); + + return _copy; + } + if (_blanket.options("debug")) {console.log("BLANKET-Collecting page scripts");} + var scripts = _blanket.utils.collectPageScripts(); + //_blanket.options("filter",scripts); + if (scripts.length === 0){ + callback(); + }else{ + + //check session state + if (sessionStorage["blanketSessionLoader"]){ + _blanket.blanketSession = JSON.parse(sessionStorage["blanketSessionLoader"]); + } + + scripts.forEach(function(file,indx){ + _blanket.utils.cache[file]={ + loaded:false + }; + }); + + var currScript=-1; + _blanket.utils.loadAll(function(test){ + if (test){ + return typeof scripts[currScript+1] !== 'undefined'; + } + currScript++; + if (currScript >= scripts.length){ + return null; + } + return scripts[currScript]; + },callback); + } + }, + beforeStartTestRunner: function(opts){ + opts = opts || {}; + opts.checkRequirejs = typeof opts.checkRequirejs === "undefined" ? true : opts.checkRequirejs; + opts.callback = opts.callback || function() { }; + opts.coverage = typeof opts.coverage === "undefined" ? true : opts.coverage; + if (opts.coverage) { + _blanket._bindStartTestRunner(opts.bindEvent, + function(){ + _blanket._loadSourceFiles(function() { + + var allLoaded = function(){ + return opts.condition ? opts.condition() : _blanket.requireFilesLoaded(); + }; + var check = function() { + if (allLoaded()) { + if (_blanket.options("debug")) {console.log("BLANKET-All files loaded, init start test runner callback.");} + var cb = _blanket.options("testReadyCallback"); + + if (cb){ + if (typeof cb === "function"){ + cb(opts.callback); + }else if (typeof cb === "string"){ + _blanket._addScript(cb); + opts.callback(); + } + }else{ + opts.callback(); + } + } else { + setTimeout(check, 13); + } + }; + check(); + }); + }); + }else{ + opts.callback(); + } + }, + utils: { + qualifyURL: function (url) { + //http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue + var a = document.createElement('a'); + a.href = url; + return a.href; + } + } +}); + +})(blanket); + +blanket.defaultReporter = function(coverage){ + var cssSytle = "#blanket-main {margin:2px;background:#EEE;color:#333;clear:both;font-family:'Helvetica Neue Light', 'HelveticaNeue-Light', 'Helvetica Neue', Calibri, Helvetica, Arial, sans-serif; font-size:17px;} #blanket-main a {color:#333;text-decoration:none;} #blanket-main a:hover {text-decoration:underline;} .blanket {margin:0;padding:5px;clear:both;border-bottom: 1px solid #FFFFFF;} .bl-error {color:red;}.bl-success {color:#5E7D00;} .bl-file{width:auto;} .bl-cl{float:left;} .blanket div.rs {margin-left:50px; width:150px; float:right} .bl-nb {padding-right:10px;} #blanket-main a.bl-logo {color: #EB1764;cursor: pointer;font-weight: bold;text-decoration: none} .bl-source{ overflow-x:scroll; background-color: #FFFFFF; border: 1px solid #CBCBCB; color: #363636; margin: 25px 20px; width: 80%;} .bl-source div{white-space: pre;font-family: monospace;} .bl-source > div > span:first-child{background-color: #EAEAEA;color: #949494;display: inline-block;padding: 0 10px;text-align: center;width: 30px;} .bl-source .miss{background-color:#e6c3c7} .bl-source span.branchWarning{color:#000;background-color:yellow;} .bl-source span.branchOkay{color:#000;background-color:transparent;}", + successRate = 60, + head = document.head, + fileNumber = 0, + body = document.body, + headerContent, + hasBranchTracking = Object.keys(coverage.files).some(function(elem){ + return typeof coverage.files[elem].branchData !== 'undefined'; + }), + bodyContent = "
results
Coverage (%)
Covered/Total Smts.
"+(hasBranchTracking ? "
Covered/Total Branches
":"")+"
", + fileTemplate = "
{{fileNumber}}.{{file}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
"+( hasBranchTracking ? "
{{passedBranches}}/{{totalBranches}}
" : "" )+"
"; + grandTotalTemplate = "
{{rowTitle}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
"+( hasBranchTracking ? "
{{passedBranches}}/{{totalBranches}}
" : "" ) + "
"; + + function blanket_toggleSource(id) { + var element = document.getElementById(id); + if(element.style.display === 'block') { + element.style.display = 'none'; + } else { + element.style.display = 'block'; + } + } + + + var script = document.createElement("script"); + script.type = "text/javascript"; + script.text = blanket_toggleSource.toString().replace('function ' + blanket_toggleSource.name, 'function blanket_toggleSource'); + body.appendChild(script); + + var percentage = function(number, total) { + return (Math.round(((number/total) * 100)*100)/100); + }; + + var appendTag = function (type, el, str) { + var dom = document.createElement(type); + dom.innerHTML = str; + el.appendChild(dom); + }; + + function escapeInvalidXmlChars(str) { + return str.replace(/\&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/\'/g, "'"); + } + + function isBranchFollowed(data,bool){ + var mode = bool ? 0 : 1; + if (typeof data === 'undefined' || + typeof data === null || + typeof data[mode] === 'undefined'){ + return false; + } + return data[mode].length > 0; + } + + var branchStack = []; + + function branchReport(colsIndex,src,cols,offset,lineNum){ + var newsrc=""; + var postfix=""; + if (branchStack.length > 0){ + newsrc += ""; + if (branchStack[0][0].end.line === lineNum){ + newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + ""; + src = src.slice(branchStack[0][0].end.column); + branchStack.shift(); + if (branchStack.length > 0){ + newsrc += ""; + if (branchStack[0][0].end.line === lineNum){ + newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + ""; + src = src.slice(branchStack[0][0].end.column); + branchStack.shift(); + if (!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols}; + } + } + else if (!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) + "",cols:cols}; + } + else{ + postfix = ""; + } + }else if (!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols}; + } + }else if(!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) + "",cols:cols}; + }else{ + postfix = ""; + } + } + var thisline = cols[colsIndex]; + //consequent + + var cons = thisline.consequent; + if (cons.start.line > lineNum){ + branchStack.unshift([thisline.alternate,thisline]); + branchStack.unshift([cons,thisline]); + src = escapeInvalidXmlChars(src); + }else{ + var style = ""; + newsrc += escapeInvalidXmlChars(src.slice(0,cons.start.column-offset)) + style; + + if (cols.length > colsIndex+1 && + cols[colsIndex+1].consequent.start.line === lineNum && + cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].consequent.end.column-offset) + { + var res = branchReport(colsIndex+1,src.slice(cons.start.column-offset,cons.end.column-offset),cols,cons.start.column-offset,lineNum); + newsrc += res.src; + cols = res.cols; + cols[colsIndex+1] = cols[colsIndex+2]; + cols.length--; + }else{ + newsrc += escapeInvalidXmlChars(src.slice(cons.start.column-offset,cons.end.column-offset)); + } + newsrc += ""; + + var alt = thisline.alternate; + if (alt.start.line > lineNum){ + newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset)); + branchStack.unshift([alt,thisline]); + }else{ + newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset,alt.start.column-offset)); + style = ""; + newsrc += style; + if (cols.length > colsIndex+1 && + cols[colsIndex+1].consequent.start.line === lineNum && + cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].alternate.end.column-offset) + { + var res2 = branchReport(colsIndex+1,src.slice(alt.start.column-offset,alt.end.column-offset),cols,alt.start.column-offset,lineNum); + newsrc += res2.src; + cols = res2.cols; + cols[colsIndex+1] = cols[colsIndex+2]; + cols.length--; + }else{ + newsrc += escapeInvalidXmlChars(src.slice(alt.start.column-offset,alt.end.column-offset)); + } + newsrc += ""; + newsrc += escapeInvalidXmlChars(src.slice(alt.end.column-offset)); + src = newsrc; + } + } + return {src:src+postfix, cols:cols}; + } + + var isUndefined = function(item){ + return typeof item !== 'undefined'; + }; + + var files = coverage.files; + var totals = { + totalSmts: 0, + numberOfFilesCovered: 0, + passedBranches: 0, + totalBranches: 0, + moduleTotalStatements : {}, + moduleTotalCoveredStatements : {}, + moduleTotalBranches : {}, + moduleTotalCoveredBranches : {} + }; + + // check if a data-cover-modulepattern was provided for per-module coverage reporting + var modulePattern = _blanket.options("modulePattern"); + var modulePatternRegex = ( modulePattern ? new RegExp(modulePattern) : null ); + + for(var file in files) + { + fileNumber++; + + var statsForFile = files[file], + totalSmts = 0, + numberOfFilesCovered = 0, + code = [], + i; + + + var end = []; + for(i = 0; i < statsForFile.source.length; i +=1){ + var src = statsForFile.source[i]; + + if (branchStack.length > 0 || + typeof statsForFile.branchData !== 'undefined') + { + if (typeof statsForFile.branchData[i+1] !== 'undefined') + { + var cols = statsForFile.branchData[i+1].filter(isUndefined); + var colsIndex=0; + + + src = branchReport(colsIndex,src,cols,0,i+1).src; + + }else if (branchStack.length){ + src = branchReport(0,src,null,0,i+1).src; + }else{ + src = escapeInvalidXmlChars(src); + } + }else{ + src = escapeInvalidXmlChars(src); + } + var lineClass=""; + if(statsForFile[i+1]) { + numberOfFilesCovered += 1; + totalSmts += 1; + lineClass = 'hit'; + }else{ + if(statsForFile[i+1] === 0){ + totalSmts++; + lineClass = 'miss'; + } + } + code[i + 1] = "
"+(i + 1)+""+src+"
"; + } + totals.totalSmts += totalSmts; + totals.numberOfFilesCovered += numberOfFilesCovered; + var totalBranches=0; + var passedBranches=0; + if (typeof statsForFile.branchData !== 'undefined'){ + for(var j=0;j 0 && + typeof statsForFile.branchData[j][k][1] !== 'undefined' && + statsForFile.branchData[j][k][1].length > 0){ + passedBranches++; + } + } + } + } + } + } + totals.passedBranches += passedBranches; + totals.totalBranches += totalBranches; + + // if "data-cover-modulepattern" was provided, + // track totals per module name as well as globally + if (modulePatternRegex) { + var moduleName = file.match(modulePatternRegex)[1]; + + if(!totals.moduleTotalStatements.hasOwnProperty(moduleName)) { + totals.moduleTotalStatements[moduleName] = 0; + totals.moduleTotalCoveredStatements[moduleName] = 0; + } + + totals.moduleTotalStatements[moduleName] += totalSmts; + totals.moduleTotalCoveredStatements[moduleName] += numberOfFilesCovered; + + if(!totals.moduleTotalBranches.hasOwnProperty(moduleName)) { + totals.moduleTotalBranches[moduleName] = 0; + totals.moduleTotalCoveredBranches[moduleName] = 0; + } + + totals.moduleTotalBranches[moduleName] += totalBranches; + totals.moduleTotalCoveredBranches[moduleName] += passedBranches; + } + + var result = percentage(numberOfFilesCovered, totalSmts); + + var output = fileTemplate.replace("{{file}}", file) + .replace("{{percentage}}",result) + .replace("{{numberCovered}}", numberOfFilesCovered) + .replace(/\{\{fileNumber\}\}/g, fileNumber) + .replace("{{totalSmts}}", totalSmts) + .replace("{{totalBranches}}", totalBranches) + .replace("{{passedBranches}}", passedBranches) + .replace("{{source}}", code.join(" ")); + if(result < successRate) + { + output = output.replace("{{statusclass}}", "bl-error"); + } else { + output = output.replace("{{statusclass}}", "bl-success"); + } + bodyContent += output; + } + + // create temporary function for use by the global totals reporter, + // as well as the per-module totals reporter + var createAggregateTotal = function(numSt, numCov, numBranch, numCovBr, moduleName) { + + var totalPercent = percentage(numCov, numSt); + var statusClass = totalPercent < successRate ? "bl-error" : "bl-success"; + var rowTitle = ( moduleName ? "Total for module: " + moduleName : "Global total" ); + var totalsOutput = grandTotalTemplate.replace("{{rowTitle}}", rowTitle) + .replace("{{percentage}}", totalPercent) + .replace("{{numberCovered}}", numCov) + .replace("{{totalSmts}}", numSt) + .replace("{{passedBranches}}", numCovBr) + .replace("{{totalBranches}}", numBranch) + .replace("{{statusclass}}", statusClass); + + bodyContent += totalsOutput; + }; + + // if "data-cover-modulepattern" was provided, + // output the per-module totals alongside the global totals + if (modulePatternRegex) { + for (var thisModuleName in totals.moduleTotalStatements) { + if (totals.moduleTotalStatements.hasOwnProperty(thisModuleName)) { + + var moduleTotalSt = totals.moduleTotalStatements[thisModuleName]; + var moduleTotalCovSt = totals.moduleTotalCoveredStatements[thisModuleName]; + + var moduleTotalBr = totals.moduleTotalBranches[thisModuleName]; + var moduleTotalCovBr = totals.moduleTotalCoveredBranches[thisModuleName]; + + createAggregateTotal(moduleTotalSt, moduleTotalCovSt, moduleTotalBr, moduleTotalCovBr, thisModuleName); + } + } + } + + createAggregateTotal(totals.totalSmts, totals.numberOfFilesCovered, totals.totalBranches, totals.passedBranches, null); + bodyContent += "
"; //closing main + + + appendTag('style', head, cssSytle); + //appendStyle(body, headerContent); + if (document.getElementById("blanket-main")){ + document.getElementById("blanket-main").innerHTML= + bodyContent.slice(23,-6); + }else{ + appendTag('div', body, bodyContent); + } + //appendHtml(body, ''); +}; + +(function(){ + var newOptions={}; + //http://stackoverflow.com/a/2954896 + var toArray =Array.prototype.slice; + var scripts = toArray.call(document.scripts); + toArray.call(scripts[scripts.length - 1].attributes) + .forEach(function(es){ + if(es.nodeName === "data-cover-only"){ + newOptions.filter = es.nodeValue; + } + if(es.nodeName === "data-cover-never"){ + newOptions.antifilter = es.nodeValue; + } + if(es.nodeName === "data-cover-reporter"){ + newOptions.reporter = es.nodeValue; + } + if (es.nodeName === "data-cover-adapter"){ + newOptions.adapter = es.nodeValue; + } + if (es.nodeName === "data-cover-loader"){ + newOptions.loader = es.nodeValue; + } + if (es.nodeName === "data-cover-timeout"){ + newOptions.timeout = es.nodeValue; + } + if (es.nodeName === "data-cover-modulepattern") { + newOptions.modulePattern = es.nodeValue; + } + if (es.nodeName === "data-cover-reporter-options"){ + try{ + newOptions.reporter_options = JSON.parse(es.nodeValue); + }catch(e){ + if (blanket.options("debug")){ + throw new Error("Invalid reporter options. Must be a valid stringified JSON object."); + } + } + } + if (es.nodeName === "data-cover-testReadyCallback"){ + newOptions.testReadyCallback = es.nodeValue; + } + if (es.nodeName === "data-cover-customVariable"){ + newOptions.customVariable = es.nodeValue; + } + if (es.nodeName === "data-cover-flags"){ + var flags = " "+es.nodeValue+" "; + if (flags.indexOf(" ignoreError ") > -1){ + newOptions.ignoreScriptError = true; + } + if (flags.indexOf(" autoStart ") > -1){ + newOptions.autoStart = true; + } + if (flags.indexOf(" ignoreCors ") > -1){ + newOptions.ignoreCors = true; + } + if (flags.indexOf(" branchTracking ") > -1){ + newOptions.branchTracking = true; + } + if (flags.indexOf(" sourceURL ") > -1){ + newOptions.sourceURL = true; + } + if (flags.indexOf(" debug ") > -1){ + newOptions.debug = true; + } + if (flags.indexOf(" engineOnly ") > -1){ + newOptions.engineOnly = true; + } + if (flags.indexOf(" commonJS ") > -1){ + newOptions.commonJS = true; + } + if (flags.indexOf(" instrumentCache ") > -1){ + newOptions.instrumentCache = true; + } + } + }); + blanket.options(newOptions); + + if (typeof requirejs !== 'undefined'){ + blanket.options("existingRequireJS",true); + } + /* setup requirejs loader, if needed */ + + if (blanket.options("commonJS")){ + blanket._commonjs = {}; + } +})(); +(function(_blanket){ +_blanket.extend({ + utils: { + normalizeBackslashes: function(str) { + return str.replace(/\\/g, '/'); + }, + matchPatternAttribute: function(filename,pattern){ + if (typeof pattern === 'string'){ + if (pattern.indexOf("[") === 0){ + //treat as array + var pattenArr = pattern.slice(1,pattern.length-1).split(","); + return pattenArr.some(function(elem){ + return _blanket.utils.matchPatternAttribute(filename,_blanket.utils.normalizeBackslashes(elem.slice(1,-1))); + //return filename.indexOf(_blanket.utils.normalizeBackslashes(elem.slice(1,-1))) > -1; + }); + }else if ( pattern.indexOf("//") === 0){ + var ex = pattern.slice(2,pattern.lastIndexOf('/')); + var mods = pattern.slice(pattern.lastIndexOf('/')+1); + var regex = new RegExp(ex,mods); + return regex.test(filename); + }else if (pattern.indexOf("#") === 0){ + return window[pattern.slice(1)].call(window,filename); + }else{ + return filename.indexOf(_blanket.utils.normalizeBackslashes(pattern)) > -1; + } + }else if ( pattern instanceof Array ){ + return pattern.some(function(elem){ + return _blanket.utils.matchPatternAttribute(filename,elem); + }); + }else if (pattern instanceof RegExp){ + return pattern.test(filename); + }else if (typeof pattern === "function"){ + return pattern.call(window,filename); + } + }, + blanketEval: function(data){ + _blanket._addScript(data); + }, + collectPageScripts: function(){ + var toArray = Array.prototype.slice; + var scripts = toArray.call(document.scripts); + var selectedScripts=[],scriptNames=[]; + var filter = _blanket.options("filter"); + if(filter != null){ + //global filter in place, data-cover-only + var antimatch = _blanket.options("antifilter"); + selectedScripts = toArray.call(document.scripts) + .filter(function(s){ + return toArray.call(s.attributes).filter(function(sn){ + return sn.nodeName === "src" && _blanket.utils.matchPatternAttribute(sn.nodeValue,filter) && + (typeof antimatch === "undefined" || !_blanket.utils.matchPatternAttribute(sn.nodeValue,antimatch)); + }).length === 1; + }); + }else{ + selectedScripts = toArray.call(document.querySelectorAll("script[data-cover]")); + } + scriptNames = selectedScripts.map(function(s){ + return _blanket.utils.qualifyURL( + toArray.call(s.attributes).filter( + function(sn){ + return sn.nodeName === "src"; + })[0].nodeValue); + }); + if (!filter){ + _blanket.options("filter","['"+scriptNames.join("','")+"']"); + } + return scriptNames; + }, + loadAll: function(nextScript,cb,preprocessor){ + /** + * load dependencies + * @param {nextScript} factory for priority level + * @param {cb} the done callback + */ + var currScript=nextScript(); + var isLoaded = _blanket.utils.scriptIsLoaded( + currScript, + _blanket.utils.ifOrdered, + nextScript, + cb + ); + + if (!(_blanket.utils.cache[currScript] && _blanket.utils.cache[currScript].loaded)){ + var attach = function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Mark script:"+currScript+", as loaded and move to next script.");} + isLoaded(); + }; + var whenDone = function(result){ + if (_blanket.options("debug")) {console.log("BLANKET-File loading finished");} + if (typeof result !== 'undefined'){ + if (_blanket.options("debug")) {console.log("BLANKET-Add file to DOM.");} + _blanket._addScript(result); + } + attach(); + }; + + _blanket.utils.attachScript( + { + url: currScript + }, + function (content){ + _blanket.utils.processFile( + content, + currScript, + whenDone, + whenDone + ); + } + ); + }else{ + isLoaded(); + } + }, + attachScript: function(options,cb){ + var timeout = _blanket.options("timeout") || 3000; + setTimeout(function(){ + if (!_blanket.utils.cache[options.url].loaded){ + throw new Error("error loading source script"); + } + },timeout); + _blanket.utils.getFile( + options.url, + cb, + function(){ throw new Error("error loading source script");} + ); + }, + ifOrdered: function(nextScript,cb){ + /** + * ordered loading callback + * @param {nextScript} factory for priority level + * @param {cb} the done callback + */ + var currScript = nextScript(true); + if (currScript){ + _blanket.utils.loadAll(nextScript,cb); + }else{ + cb(new Error("Error in loading chain.")); + } + }, + scriptIsLoaded: function(url,orderedCb,nextScript,cb){ + /** + * returns a callback that checks a loading list to see if a script is loaded. + * @param {orderedCb} callback if ordered loading is being done + * @param {nextScript} factory for next priority level + * @param {cb} the done callback + */ + if (_blanket.options("debug")) {console.log("BLANKET-Returning function");} + return function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Marking file as loaded: "+url);} + + _blanket.utils.cache[url].loaded=true; + + if (_blanket.utils.allLoaded()){ + if (_blanket.options("debug")) {console.log("BLANKET-All files loaded");} + cb(); + }else if (orderedCb){ + //if it's ordered we need to + //traverse down to the next + //priority level + if (_blanket.options("debug")) {console.log("BLANKET-Load next file.");} + orderedCb(nextScript,cb); + } + }; + }, + cache: {}, + allLoaded: function (){ + /** + * check if depdencies are loaded in cache + */ + var cached = Object.keys(_blanket.utils.cache); + for (var i=0;i -1){ + callback(_blanket.blanketSession[key]); + foundInSession=true; + return; + } + } + } + if (!foundInSession){ + var xhr = _blanket.utils.createXhr(); + xhr.open('GET', url, true); + + //Allow overrides specified in config + if (onXhr) { + onXhr(xhr, url); + } + + xhr.onreadystatechange = function (evt) { + var status, err; + + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + status = xhr.status; + if ((status > 399 && status < 600) /*|| + (status === 0 && + navigator.userAgent.toLowerCase().indexOf('firefox') > -1) + */ ) { + //An http 4xx or 5xx error. Signal an error. + err = new Error(url + ' HTTP status: ' + status); + err.xhr = xhr; + errback(err); + } else { + callback(xhr.responseText); + } + } + }; + try{ + xhr.send(null); + }catch(e){ + if (e.code && (e.code === 101 || e.code === 1012) && _blanket.options("ignoreCors") === false){ + //running locally and getting error from browser + _blanket.showManualLoader(); + } else { + throw e; + } + } + } + } + } +}); + +(function(){ + var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; + var requirejs = blanket.options("commonJS") ? blanket._commonjs.requirejs : window.requirejs; + if (!_blanket.options("engineOnly") && _blanket.options("existingRequireJS")){ + + _blanket.utils.oldloader = requirejs.load; + + requirejs.load = function (context, moduleName, url) { + _blanket.requiringFile(url); + _blanket.utils.getFile(url, + function(content){ + _blanket.utils.processFile( + content, + url, + function newLoader(){ + context.completeLoad(moduleName); + }, + function oldLoader(){ + _blanket.utils.oldloader(context, moduleName, url); + } + ); + }, function (err) { + _blanket.requiringFile(); + throw err; + }); + }; + } + // Save the XHR constructor, just in case frameworks like Sinon would sandbox it. + _blanket.utils.cacheXhrConstructor(); +})(); + +})(blanket); + +(function() { + + if(!mocha) { + throw new Exception("mocha library does not exist in global namespace!"); + } + + + /* + * Mocha Events: + * + * - `start` execution started + * - `end` execution complete + * - `suite` (suite) test suite execution started + * - `suite end` (suite) all tests (and sub-suites) have finished + * - `test` (test) test execution started + * - `test end` (test) test completed + * - `hook` (hook) hook execution started + * - `hook end` (hook) hook complete + * - `pass` (test) test passed + * - `fail` (test, err) test failed + * + */ + + var OriginalReporter = mocha._reporter; + + var BlanketReporter = function(runner) { + runner.on('start', function() { + blanket.setupCoverage(); + }); + + runner.on('end', function() { + blanket.onTestsDone(); + }); + + runner.on('suite', function() { + blanket.onModuleStart(); + }); + + runner.on('test', function() { + blanket.onTestStart(); + }); + + runner.on('test end', function(test) { + blanket.onTestDone(test.parent.tests.length, test.state === 'passed'); + }); + + // NOTE: this is an instance of BlanketReporter + new OriginalReporter(runner); + }; + + mocha.reporter(BlanketReporter); + + var oldRun = mocha.run, + oldCallback = null; + + mocha.run = function (finishCallback) { + oldCallback = finishCallback; + console.log("waiting for blanket..."); + }; + blanket.beforeStartTestRunner({ + callback: function(){ + if (!blanket.options("existingRequireJS")){ + oldRun(oldCallback); + } + mocha.run = oldRun; + } + }); +})(); diff --git a/tests.html b/tests.html index 1c1897d76a..8d2b032856 100644 --- a/tests.html +++ b/tests.html @@ -2,25 +2,28 @@ - plottable Tests - - + Plottable.js Tests + + + - - - - - - + + -
+ + From 0e45cf176dac8e3120a1005785a6974133de0a6c Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Wed, 12 Feb 2014 10:43:23 -0800 Subject: [PATCH 37/61] Organized internal size calculation calls. Added private method calculateTextSize(), which measures the size of the text using getBBox(). setText() uses that sizing information to set the row/col minimum when the text is changed. anchor() now calls setText() so the correct measurements are set when the Label is first anchored. --- src/label.ts | 69 +++++++++++++++++++++++----------------------- test/labelTests.ts | 2 -- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/label.ts b/src/label.ts index 795e6dae77..f4978ba9c6 100644 --- a/src/label.ts +++ b/src/label.ts @@ -7,8 +7,7 @@ class Label extends Component { public yAlignment = "CENTER"; private textElement: D3.Selection; - private text: string; // text assigned to the element; may not be the actual text displaed due to truncation - private displayText: string; + private text: string; // text assigned to the Label; may not be the actual text displayed due to truncation private orientation: string; private textLength: number; private textHeight: number; @@ -26,67 +25,69 @@ class Label extends Component { public anchor(element: D3.Selection) { super.anchor(element); - this.textElement = this.element.append("text").text(this.text); - this.setMinimumsByCalculatingTextSize(); + this.textElement = this.element.append("text"); + this.setText(this.text); return this; } public setText(text: string) { this.text = text; this.textElement.text(text); - this.setMinimumsByCalculatingTextSize(); + this.calculateTextSize(); + if (this.orientation === "horizontal") { + this.rowMinimum(this.textHeight); + } else { + this.colMinimum(this.textHeight); + } } - private setMinimumsByCalculatingTextSize() { - this.textElement.attr("dy", 0); // Reset this so we maintain idempotence + private calculateTextSize() { var bbox = Utils.getBBox(this.textElement); - this.textElement.attr("dy", -bbox.y); this.textHeight = bbox.height; this.textLength = bbox.width; - // italic text needs a slightly larger bounding box if (this.textElement.style("font-style") === "italic") { var textNode = this.textElement.node(); - // pad by half the width of the last character - this.textLength += 0.5 * textNode.getExtentOfChar(textNode.textContent.length-1).width; - } - - if (this.orientation === "horizontal") { - this.rowMinimum(this.textHeight); - } else { - this.colMinimum(this.textHeight); + // pad by half the height of the last character (equivalent to 30-degree tilt) + this.textLength += 0.5 * textNode.getExtentOfChar(textNode.textContent.length-1).height; } } private truncateTextToLength(availableLength: number) { + if (this.textLength < availableLength) { + return; + } + this.textElement.text(this.text + "..."); var textNode = this.textElement.node(); - var numChars = textNode.textContent.length; - var dotLength = textNode.getSubStringLength(numChars-3, 3); - for (var i=0; i availableLength) { + this.textElement.text(""); // no room even for ellipsis + } + + var numChars = this.text.length; + for (var i=1; i availableLength) { - if (i > 0) { - this.textElement.text(this.text.substr(0, i-1).trim() + "..."); - } else { - this.textElement.text(""); // no room even for ellipsis - } - this.setMinimumsByCalculatingTextSize(); - return; + this.textElement.text(this.text.substr(0, i-1).trim() + "..."); + break; } } + this.calculateTextSize(); } public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { super.computeLayout(xOffset, yOffset, availableWidth, availableHeight); + this.textElement.attr("dy", 0); // Reset this so we maintain idempotence + var bbox = Utils.getBBox(this.textElement); + this.textElement.attr("dy", -bbox.y); + var xShift = 0; var yShift = 0; if (this.orientation === "horizontal") { - if (this.availableWidth < this.textLength) { - this.truncateTextToLength(this.availableWidth); - } + this.truncateTextToLength(this.availableWidth); switch (this.xAlignment) { case "LEFT": break; @@ -97,12 +98,10 @@ class Label extends Component { xShift = this.availableWidth - this.textLength; break; default: - throw this.xAlignment + " is not a supported alignment"; + throw new Error(this.xAlignment + " is not a supported alignment"); } } else { - if (this.availableHeight < this.textLength) { - this.truncateTextToLength(this.availableHeight); - } + this.truncateTextToLength(this.availableHeight); switch (this.yAlignment) { case "TOP": break; @@ -113,7 +112,7 @@ class Label extends Component { xShift = this.availableHeight - this.textLength; break; default: - throw this.yAlignment + " is not a supported alignment"; + throw new Error(this.yAlignment + " is not a supported alignment"); } if (this.orientation === "vertical-right") { diff --git a/test/labelTests.ts b/test/labelTests.ts index e68869a610..fb115f365c 100644 --- a/test/labelTests.ts +++ b/test/labelTests.ts @@ -86,7 +86,6 @@ describe("Labels", () => { var label = new TitleLabel("A CHART TITLE"); label.anchor(svg); var text = label.element.select("text"); - ( label).setMinimumsByCalculatingTextSize(); // to access private method svg.attr("width", ( label).textLength); // to access private field label.computeLayout(); label.render(); @@ -95,7 +94,6 @@ describe("Labels", () => { assert.operator(( label).textLength, "<=", svgWidth, "the non-italic text is not wider than the SVG width"); text.style("font-style", "italic"); // the text should overflow now - ( label).setMinimumsByCalculatingTextSize(); // manually call this since Label can't detect a style change label.computeLayout(); label.render(); assert.equal(( label).textHeight, label.rowMinimum(), "text height === label.rowMinimum()"); From b345f8ffa766c8ae8e77328254c7770e6ff729a9 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 11:03:27 -0800 Subject: [PATCH 38/61] Some minor refactoring we discussed in person --- src/label.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/label.ts b/src/label.ts index f4978ba9c6..618c5acf8f 100644 --- a/src/label.ts +++ b/src/label.ts @@ -33,7 +33,7 @@ class Label extends Component { public setText(text: string) { this.text = text; this.textElement.text(text); - this.calculateTextSize(); + this.measureAndSetTextSize(); if (this.orientation === "horizontal") { this.rowMinimum(this.textHeight); } else { @@ -41,7 +41,7 @@ class Label extends Component { } } - private calculateTextSize() { + private measureAndSetTextSize() { var bbox = Utils.getBBox(this.textElement); this.textHeight = bbox.height; this.textLength = bbox.width; @@ -63,6 +63,8 @@ class Label extends Component { var dotLength = textNode.getSubStringLength(textNode.textContent.length-3, 3); if (dotLength > availableLength) { this.textElement.text(""); // no room even for ellipsis + this.measureAndSetTextSize(); + return; } var numChars = this.text.length; @@ -70,10 +72,10 @@ class Label extends Component { var testLength = textNode.getSubStringLength(0, i); if ((testLength + dotLength) > availableLength) { this.textElement.text(this.text.substr(0, i-1).trim() + "..."); - break; + this.measureAndSetTextSize(); + return; } } - this.calculateTextSize(); } public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight?: number) { From e2f635d863ccf3f17b62a3da06f6cf08416005b2 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Wed, 12 Feb 2014 11:15:05 -0800 Subject: [PATCH 39/61] Removed italics-padding logic. With the new truncation logic, italic overflow will only happen in the event of a label that's just barely big enough for non- italic text to fit without overflow. --- src/label.ts | 8 +------- test/labelTests.ts | 22 ---------------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/label.ts b/src/label.ts index 618c5acf8f..81fda9ca87 100644 --- a/src/label.ts +++ b/src/label.ts @@ -45,16 +45,10 @@ class Label extends Component { var bbox = Utils.getBBox(this.textElement); this.textHeight = bbox.height; this.textLength = bbox.width; - // italic text needs a slightly larger bounding box - if (this.textElement.style("font-style") === "italic") { - var textNode = this.textElement.node(); - // pad by half the height of the last character (equivalent to 30-degree tilt) - this.textLength += 0.5 * textNode.getExtentOfChar(textNode.textContent.length-1).height; - } } private truncateTextToLength(availableLength: number) { - if (this.textLength < availableLength) { + if (this.textLength <= availableLength) { return; } diff --git a/test/labelTests.ts b/test/labelTests.ts index fb115f365c..d9eb85bc3e 100644 --- a/test/labelTests.ts +++ b/test/labelTests.ts @@ -80,26 +80,4 @@ describe("Labels", () => { assert.operator(bbox.width, "<=", svgWidth, "the text is not wider than the SVG width"); svg.remove(); }); - - it("Italicized text is handled properly", () => { - var svg = generateSVG(400, 80); - var label = new TitleLabel("A CHART TITLE"); - label.anchor(svg); - var text = label.element.select("text"); - svg.attr("width", ( label).textLength); // to access private field - label.computeLayout(); - label.render(); - var svgWidth = Number(svg.attr("width")); - assert.equal(( label).textHeight, label.rowMinimum(), "text height === label.rowMinimum()"); - assert.operator(( label).textLength, "<=", svgWidth, "the non-italic text is not wider than the SVG width"); - - text.style("font-style", "italic"); // the text should overflow now - label.computeLayout(); - label.render(); - assert.equal(( label).textHeight, label.rowMinimum(), "text height === label.rowMinimum()"); - assert.operator(( label).textLength, "<=", svgWidth, "the italic text is not wider than the SVG width"); - var textContent = text.node().textContent; - assert.equal(textContent.substr(textContent.length-3), "...", "Italicized text overflowed and was truncated"); - svg.remove(); - }); }); From a18e23b86c760c681abcfd110d0c463e24b4277f Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 13:20:51 -0800 Subject: [PATCH 40/61] Move perfdiagnostics out of src/ and into test/ --- src/reference.ts | 1 - {src => test}/perfdiagnostics.ts | 1 - 2 files changed, 2 deletions(-) rename {src => test}/perfdiagnostics.ts (98%) diff --git a/src/reference.ts b/src/reference.ts index 3748ea00c9..20e1459021 100644 --- a/src/reference.ts +++ b/src/reference.ts @@ -8,7 +8,6 @@ /// /// /// -/// /// /// /// diff --git a/src/perfdiagnostics.ts b/test/perfdiagnostics.ts similarity index 98% rename from src/perfdiagnostics.ts rename to test/perfdiagnostics.ts index 5066ba6044..36094e74a4 100644 --- a/src/perfdiagnostics.ts +++ b/test/perfdiagnostics.ts @@ -1,4 +1,3 @@ -/// module PerfDiagnostics { class PerfDiagnostics { From 8c42f2c71c94e2d3fbf82ced0f8b24c5cc22003f Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 13:41:03 -0800 Subject: [PATCH 41/61] Get blanket-mocha running and working. Remove unused grunt modules from package.json --- Gruntfile.js | 11 +++++++---- package.json | 5 ++--- tests.html | 5 +++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 70c169d698..9b9fba88db 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -76,8 +76,11 @@ module.exports = function(grunt) { "files": ["examples/**.ts"] } }, - mocha_phantomjs: { - all: ['tests.html'] + blanket_mocha: { + all: ['tests.html'], + options: { + threshold: 60 + } }, connect: { server: { @@ -100,7 +103,7 @@ module.exports = function(grunt) { ["ts:dev", "ts:test", "ts:examples", "tslint", "concat:license"] ); - grunt.registerTask("test", ["mocha_phantomjs"]); + grunt.registerTask("test", ["blanket_mocha"]); - grunt.registerTask("watch-test", ["mocha_phantomjs", "watch:test"]); + grunt.registerTask("watch-test", ["blanket_mocha", "watch:test"]); }; diff --git a/package.json b/package.json index 54e1b864be..a1c27bf4c9 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,11 @@ "grunt-ts": "~1.6.4", "tsd": "~0.5.5", "bower": "~1.2.8", - "grunt-bower-task": "~0.3.4", - "grunt-tsd": "0.0.1", "tslint": "~0.4.2", "grunt-tslint": "~0.4.0", "grunt-contrib-concat": "~0.3.0", "grunt-mocha-phantomjs": "~0.4.0", - "grunt-contrib-connect": "~0.6.0" + "grunt-contrib-connect": "~0.6.0", + "grunt-blanket-mocha": "~0.4.0" } } diff --git a/tests.html b/tests.html index 8d2b032856..bcb8146578 100644 --- a/tests.html +++ b/tests.html @@ -34,5 +34,10 @@ + From b048eba65616bd29cddd602d882bfc7ab3686b5c Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 13:58:07 -0800 Subject: [PATCH 42/61] Remove Function.bind, since it is not supported by phantomjs --- src/axis.ts | 2 +- src/interaction.ts | 8 ++++---- src/renderer.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/axis.ts b/src/axis.ts index 776ba42686..7967b4c47a 100644 --- a/src/axis.ts +++ b/src/axis.ts @@ -39,7 +39,7 @@ class Axis extends Component { this.cachedScale = 1; this.cachedTranslate = 0; - this.scale.registerListener(this.rescale.bind(this)); + this.scale.registerListener(() => this.rescale()); } diff --git a/src/interaction.ts b/src/interaction.ts index 80e2809066..c6c4180cc4 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -43,7 +43,7 @@ class PanZoomInteraction extends Interaction { this.zoom = d3.behavior.zoom(); this.zoom.x(this.xScale.scale); this.zoom.y(this.yScale.scale); - this.zoom.on("zoom", this.rerenderZoomed.bind(this)); + this.zoom.on("zoom", () => this.rerenderZoomed()); this.registerWithComponent(); } @@ -94,9 +94,9 @@ class AreaInteraction extends Interaction { ) { super(rendererComponent); this.dragBehavior = d3.behavior.drag(); - this.dragBehavior.on("dragstart", this.dragstart.bind(this)); - this.dragBehavior.on("drag", this.drag .bind(this)); - this.dragBehavior.on("dragend", this.dragend .bind(this)); + this.dragBehavior.on("dragstart", () => this.dragstart()); + this.dragBehavior.on("drag", () => this.drag ()); + this.dragBehavior.on("dragend", () => this.dragend ()); this.registerWithComponent(); } diff --git a/src/renderer.ts b/src/renderer.ts index 950dba83e3..3c5c4ba33e 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -62,8 +62,8 @@ class XYRenderer extends Renderer { var yDomain = d3.extent(data, this.yAccessor); this.yScale.widenDomain(yDomain); - this.xScale.registerListener(this.rescale.bind(this)); - this.yScale.registerListener(this.rescale.bind(this)); + this.xScale.registerListener(() => this.rescale()); + this.yScale.registerListener(() => this.rescale()); } public computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight? :number) { From 0eff6383f13fa0362a702e0dcbe0980a83f0722e Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 14:37:06 -0800 Subject: [PATCH 43/61] Add more component unit testing: alignment error messages and class management --- test/componentTests.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/componentTests.ts b/test/componentTests.ts index bf3201dfa1..41632ec0fa 100644 --- a/test/componentTests.ts +++ b/test/componentTests.ts @@ -159,4 +159,38 @@ describe("Component behavior", () => { assert.equal(hitBox2, hitNode, "hitBox2 was registerd"); svg.remove(); }); + + it("errors are thrown on bad alignments", () => { + c.rowWeight(0); + c.colWeight(0); + c.rowMinimum(10); + c.colMinimum(10); + c.xAlignment = "foo"; + c.anchor(svg); + assert.throws(() => c.computeLayout(), Error, "not a supported alignment"); + c.xAlignment = "CENTER"; + c.yAlignment = "bar"; + assert.throws(() => c.computeLayout(), Error, "not a supported alignment"); + svg.remove(); + }); + + it("css classing works as expected", () => { + assert.isFalse(c.classed("CSS-PREANCHOR-KEEP")); + c.classed("CSS-PREANCHOR-KEEP", true); + assert.isTrue(c.classed("CSS-PREANCHOR-KEEP")); + c.classed("CSS-PREANCHOR-REMOVE", true); + assert.isTrue(c.classed("CSS-PREANCHOR-REMOVE")); + c.classed("CSS-PREANCHOR-REMOVE", false); + assert.isFalse(c.classed("CSS-PREANCHOR-REMOVE")); + + c.anchor(svg); + assert.isTrue(c.classed("CSS-PREANCHOR-KEEP")); + assert.isFalse(c.classed("CSS-PREANCHOR-REMOVE")); + assert.isFalse(c.classed("CSS-POSTANCHOR")); + c.classed("CSS-POSTANCHOR", true); + assert.isTrue(c.classed("CSS-POSTANCHOR")); + c.classed("CSS-POSTANCHOR", false); + assert.isFalse(c.classed("CSS-POSTANCHOR")); + svg.remove(); + }); }); From 20d3e7269aa95a705479a126a0fcf759620a75bc Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 16:16:44 -0800 Subject: [PATCH 44/61] Add more renderer tests: Confirm that if scale changes, the renderer updates properly --- test/rendererTests.ts | 80 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index e47fc50eef..95764e2e74 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -40,7 +40,7 @@ describe("Renderers", () => { }); }); - describe("CircleRenderer", () => { + describe("XYRenderer functionality", () => { describe("Example CircleRenderer with quadratic series", () => { var svg: D3.Selection; var xScale: LinearScale; @@ -48,22 +48,23 @@ describe("Renderers", () => { var circleRenderer: CircleRenderer; var pixelAreaFull: SelectionArea; var pixelAreaPartial: SelectionArea; - + var SVG_WIDTH = 600; + var SVG_HEIGHT = 300; before(() => { - svg = generateSVG(600, 300); + svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new LinearScale(); yScale = new LinearScale(); circleRenderer = new CircleRenderer(dataset1, xScale, yScale); circleRenderer.anchor(svg).computeLayout().render(); - pixelAreaFull = {xMin: 0, xMax: 600, yMin: 0, yMax: 300}; + pixelAreaFull = {xMin: 0, xMax: SVG_WIDTH, yMin: 0, yMax: SVG_HEIGHT}; pixelAreaPartial = {xMin: 200, xMax: 600, yMin: 100, yMax: 200}; }); it("setup is handled properly", () => { assert.deepEqual(xScale.domain(), [0, 9], "xScale domain was set by the renderer"); assert.deepEqual(yScale.domain(), [0, 81], "yScale domain was set by the renderer"); - assert.deepEqual(xScale.range(), [0, 600], "xScale range was set by the renderer"); - assert.deepEqual(yScale.range(), [300, 0], "yScale range was set by the renderer"); + assert.deepEqual(xScale.range(), [0, SVG_WIDTH], "xScale range was set by the renderer"); + assert.deepEqual(yScale.range(), [SVG_HEIGHT, 0], "yScale range was set by the renderer"); assert.lengthOf(circleRenderer.renderArea.selectAll("circle")[0], 10, "10 circles were drawn"); }); @@ -101,6 +102,73 @@ describe("Renderers", () => { assert.deepEqual(indicesPartial, [6, 7], "2 circles were selected by the partial region"); }); + describe("after the scale has changed", () => { + before(() => { + xScale.domain([0, 3]); + }); + + it("invertXYSelectionArea works", () => { + var expectedDataAreaFull = {xMin: 0, xMax: 3, yMin: 81, yMax: 0}; + var actualDataAreaFull = circleRenderer.invertXYSelectionArea(pixelAreaFull).data; + assert.deepEqual(actualDataAreaFull, expectedDataAreaFull, "the full data area is as expected"); + + var expectedDataAreaPartial = {xMin: 1, xMax: 3, yMin: 54, yMax: 27}; + var actualDataAreaPartial = circleRenderer.invertXYSelectionArea(pixelAreaPartial).data; + + assert.closeTo(actualDataAreaPartial.xMin, expectedDataAreaPartial.xMin, 1, "partial xMin is close"); + assert.closeTo(actualDataAreaPartial.xMax, expectedDataAreaPartial.xMax, 1, "partial xMax is close"); + assert.closeTo(actualDataAreaPartial.yMin, expectedDataAreaPartial.yMin, 1, "partial yMin is close"); + assert.closeTo(actualDataAreaPartial.yMax, expectedDataAreaPartial.yMax, 1, "partial yMax is close"); + }); + + it("getSelectionFromArea works", () => { + var fullSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaFull); + var selectionFull = circleRenderer.getSelectionFromArea(fullSelectionArea); + assert.lengthOf(selectionFull[0], 4, "four circles were selected by the full region"); + + var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); + var selectionPartial = circleRenderer.getSelectionFromArea(partialSelectionArea); + assert.lengthOf(selectionPartial[0], 0, "no circles were selected by the partial region"); + }); + + it("getDataIndicesFromArea works", () => { + var fullSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaFull); + var indicesFull = circleRenderer.getDataIndicesFromArea(fullSelectionArea); + assert.deepEqual(indicesFull, [0,1,2,3], "four circles were selected by the full region"); + + var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); + var indicesPartial = circleRenderer.getDataIndicesFromArea(partialSelectionArea); + assert.deepEqual(indicesPartial, [], "no circles were selected by the partial region"); + }); + + it("the circles re-rendered properly", () => { + var renderArea = circleRenderer.renderArea; + var circles = renderArea.selectAll("circle"); + var circlesInArea = 0; + var renderAreaTransform = d3.transform(renderArea.attr("transform")); + var translate = renderAreaTransform.translate; + var scale = renderAreaTransform.scale; + + function elementFunction(datum, index) { + // This function takes special care to compute the position of circles after taking svg transformation + // into account. + var selection = d3.select(this); + var elementTransform = d3.transform(selection.attr("transform")); + var elementTranslate = elementTransform.translate; + var x = +selection.attr("cx") * scale[0] + translate[0] + elementTranslate[0]; + var y = +selection.attr("cy") * scale[1] + translate[1] + elementTranslate[1]; + console.log(x, +selection.attr("cx"), scale.x, translate.x, elementTranslate.x); + if (0 <= x && x <= SVG_WIDTH && 0 <= y && y <= SVG_HEIGHT) { + circlesInArea++; + assert.equal(x, xScale.scale(datum.x), "the scaled/translated x is correct"); + assert.equal(y, yScale.scale(datum.y), "the scaled/translated y is correct"); + }; + }; + circles.each(elementFunction); + assert.equal(circlesInArea, 4, "four circles were found in the render area"); + }); + }); + after(() => { svg.remove(); }); From 8fcd959f83ec0b8d0c39bd5cc3074ddcf1ed73d1 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 16:23:46 -0800 Subject: [PATCH 45/61] Additional test for Table: confirm you cannot set rowMin or colMin --- test/tableTests.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/tableTests.ts b/test/tableTests.ts index 1fc8d7df02..57bd2b13db 100644 --- a/test/tableTests.ts +++ b/test/tableTests.ts @@ -127,4 +127,10 @@ describe("Tables", () => { assertBBoxEquivalence(bboxes[4], [300, 340], "plot bbox"); svg.remove(); }); + + it("you can't set colMinimum or rowMinimum on tables directly", () => { + var table = new Table([[]]); + assert.throws(() => table.rowMinimum(3), Error, "cannot be directly set"); + assert.throws(() => table.colMinimum(3), Error, "cannot be directly set"); + }); }); From 6fa624df15ce5bbea5aaa22ad5c2d8918ceeb632 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 16:35:20 -0800 Subject: [PATCH 46/61] Now if any CircleRenderer tests fail, then the svg hangs around --- test/rendererTests.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index 95764e2e74..c666b22abe 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -50,6 +50,17 @@ describe("Renderers", () => { var pixelAreaPartial: SelectionArea; var SVG_WIDTH = 600; var SVG_HEIGHT = 300; + var allTestsPassed = true; + var tempTestsPassed: boolean; + beforeEach(() => { + // We want to persist the effect where the svg hangs around if any of the tests passed. + // Set allTestsPassed to false at start of each test, and cache old allTestsPassed in temp var + // When test completes, swap the temp var back into allTestsPassed. Failure propogates thru to end. + tempTestsPassed = allTestsPassed; + allTestsPassed = false; + }); + + before(() => { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new LinearScale(); @@ -66,6 +77,7 @@ describe("Renderers", () => { assert.deepEqual(xScale.range(), [0, SVG_WIDTH], "xScale range was set by the renderer"); assert.deepEqual(yScale.range(), [SVG_HEIGHT, 0], "yScale range was set by the renderer"); assert.lengthOf(circleRenderer.renderArea.selectAll("circle")[0], 10, "10 circles were drawn"); + allTestsPassed = tempTestsPassed; }); it("invertXYSelectionArea works", () => { @@ -80,6 +92,7 @@ describe("Renderers", () => { assert.closeTo(actualDataAreaPartial.xMax, expectedDataAreaPartial.xMax, 1, "partial xMax is close"); assert.closeTo(actualDataAreaPartial.yMin, expectedDataAreaPartial.yMin, 1, "partial yMin is close"); assert.closeTo(actualDataAreaPartial.yMax, expectedDataAreaPartial.yMax, 1, "partial yMax is close"); + allTestsPassed = tempTestsPassed; }); it("getSelectionFromArea works", () => { @@ -90,6 +103,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var selectionPartial = circleRenderer.getSelectionFromArea(partialSelectionArea); assert.lengthOf(selectionPartial[0], 2, "2 circles were selected by the partial region"); + allTestsPassed = tempTestsPassed; }); it("getDataIndicesFromArea works", () => { @@ -100,6 +114,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var indicesPartial = circleRenderer.getDataIndicesFromArea(partialSelectionArea); assert.deepEqual(indicesPartial, [6, 7], "2 circles were selected by the partial region"); + allTestsPassed = tempTestsPassed; }); describe("after the scale has changed", () => { @@ -119,6 +134,7 @@ describe("Renderers", () => { assert.closeTo(actualDataAreaPartial.xMax, expectedDataAreaPartial.xMax, 1, "partial xMax is close"); assert.closeTo(actualDataAreaPartial.yMin, expectedDataAreaPartial.yMin, 1, "partial yMin is close"); assert.closeTo(actualDataAreaPartial.yMax, expectedDataAreaPartial.yMax, 1, "partial yMax is close"); + allTestsPassed = tempTestsPassed; }); it("getSelectionFromArea works", () => { @@ -129,6 +145,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var selectionPartial = circleRenderer.getSelectionFromArea(partialSelectionArea); assert.lengthOf(selectionPartial[0], 0, "no circles were selected by the partial region"); + allTestsPassed = tempTestsPassed; }); it("getDataIndicesFromArea works", () => { @@ -139,6 +156,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var indicesPartial = circleRenderer.getDataIndicesFromArea(partialSelectionArea); assert.deepEqual(indicesPartial, [], "no circles were selected by the partial region"); + allTestsPassed = tempTestsPassed; }); it("the circles re-rendered properly", () => { @@ -148,7 +166,6 @@ describe("Renderers", () => { var renderAreaTransform = d3.transform(renderArea.attr("transform")); var translate = renderAreaTransform.translate; var scale = renderAreaTransform.scale; - function elementFunction(datum, index) { // This function takes special care to compute the position of circles after taking svg transformation // into account. @@ -166,11 +183,12 @@ describe("Renderers", () => { }; circles.each(elementFunction); assert.equal(circlesInArea, 4, "four circles were found in the render area"); + allTestsPassed = tempTestsPassed; }); }); after(() => { - svg.remove(); + if (allTestsPassed) {svg.remove();}; }); }); From b97a2678e3f8f7674649a699031cbf80770a89ec Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 17:51:09 -0800 Subject: [PATCH 47/61] Improve testing of initial render for CircleRenderer, verify idempotence, refactor to reuse a complicated fn --- test/rendererTests.ts | 57 ++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index c666b22abe..404c0b2175 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -52,6 +52,31 @@ describe("Renderers", () => { var SVG_HEIGHT = 300; var allTestsPassed = true; var tempTestsPassed: boolean; + var circlesInArea; + function getCircleRendererVerifier() { + // creates a function that verifies that circles are drawn properly after accounting for svg transform + // and then modifies circlesInArea to contain the number of circles that were discovered in the plot area + circlesInArea = 0; + var renderArea = circleRenderer.renderArea; + var renderAreaTransform = d3.transform(renderArea.attr("transform")); + var translate = renderAreaTransform.translate; + var scale = renderAreaTransform.scale; + return function (datum, index) { + // This function takes special care to compute the position of circles after taking svg transformation + // into account. + var selection = d3.select(this); + var elementTransform = d3.transform(selection.attr("transform")); + var elementTranslate = elementTransform.translate; + var x = +selection.attr("cx") * scale[0] + translate[0] + elementTranslate[0]; + var y = +selection.attr("cy") * scale[1] + translate[1] + elementTranslate[1]; + if (0 <= x && x <= SVG_WIDTH && 0 <= y && y <= SVG_HEIGHT) { + circlesInArea++; + assert.equal(x, xScale.scale(datum.x), "the scaled/translated x is correct"); + assert.equal(y, yScale.scale(datum.y), "the scaled/translated y is correct"); + }; + }; + }; + beforeEach(() => { // We want to persist the effect where the svg hangs around if any of the tests passed. // Set allTestsPassed to false at start of each test, and cache old allTestsPassed in temp var @@ -60,7 +85,6 @@ describe("Renderers", () => { allTestsPassed = false; }); - before(() => { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new LinearScale(); @@ -76,7 +100,15 @@ describe("Renderers", () => { assert.deepEqual(yScale.domain(), [0, 81], "yScale domain was set by the renderer"); assert.deepEqual(xScale.range(), [0, SVG_WIDTH], "xScale range was set by the renderer"); assert.deepEqual(yScale.range(), [SVG_HEIGHT, 0], "yScale range was set by the renderer"); - assert.lengthOf(circleRenderer.renderArea.selectAll("circle")[0], 10, "10 circles were drawn"); + circleRenderer.renderArea.selectAll("circle").each(getCircleRendererVerifier()); + assert.equal(circlesInArea, 10, "10 circles were drawn"); + allTestsPassed = tempTestsPassed; + }); + + it("rendering is idempotent", () => { + circleRenderer.render().render(); + circleRenderer.renderArea.selectAll("circle").each(getCircleRendererVerifier()); + assert.equal(circlesInArea, 10, "10 circles were drawn"); allTestsPassed = tempTestsPassed; }); @@ -162,26 +194,7 @@ describe("Renderers", () => { it("the circles re-rendered properly", () => { var renderArea = circleRenderer.renderArea; var circles = renderArea.selectAll("circle"); - var circlesInArea = 0; - var renderAreaTransform = d3.transform(renderArea.attr("transform")); - var translate = renderAreaTransform.translate; - var scale = renderAreaTransform.scale; - function elementFunction(datum, index) { - // This function takes special care to compute the position of circles after taking svg transformation - // into account. - var selection = d3.select(this); - var elementTransform = d3.transform(selection.attr("transform")); - var elementTranslate = elementTransform.translate; - var x = +selection.attr("cx") * scale[0] + translate[0] + elementTranslate[0]; - var y = +selection.attr("cy") * scale[1] + translate[1] + elementTranslate[1]; - console.log(x, +selection.attr("cx"), scale.x, translate.x, elementTranslate.x); - if (0 <= x && x <= SVG_WIDTH && 0 <= y && y <= SVG_HEIGHT) { - circlesInArea++; - assert.equal(x, xScale.scale(datum.x), "the scaled/translated x is correct"); - assert.equal(y, yScale.scale(datum.y), "the scaled/translated y is correct"); - }; - }; - circles.each(elementFunction); + circles.each(getCircleRendererVerifier()); assert.equal(circlesInArea, 4, "four circles were found in the render area"); allTestsPassed = tempTestsPassed; }); From 604277b2566ddb6fc3526139f06dacca9dcf9b17 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 17:54:11 -0800 Subject: [PATCH 48/61] Update renderer test to verify yScale domain propogation as well --- test/rendererTests.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index 404c0b2175..ecfb43b4a5 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -152,14 +152,15 @@ describe("Renderers", () => { describe("after the scale has changed", () => { before(() => { xScale.domain([0, 3]); + yScale.domain([0, 9]); }); it("invertXYSelectionArea works", () => { - var expectedDataAreaFull = {xMin: 0, xMax: 3, yMin: 81, yMax: 0}; + var expectedDataAreaFull = {xMin: 0, xMax: 3, yMin: 9, yMax: 0}; var actualDataAreaFull = circleRenderer.invertXYSelectionArea(pixelAreaFull).data; assert.deepEqual(actualDataAreaFull, expectedDataAreaFull, "the full data area is as expected"); - var expectedDataAreaPartial = {xMin: 1, xMax: 3, yMin: 54, yMax: 27}; + var expectedDataAreaPartial = {xMin: 1, xMax: 3, yMin: 6, yMax: 3}; var actualDataAreaPartial = circleRenderer.invertXYSelectionArea(pixelAreaPartial).data; assert.closeTo(actualDataAreaPartial.xMin, expectedDataAreaPartial.xMin, 1, "partial xMin is close"); @@ -176,7 +177,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var selectionPartial = circleRenderer.getSelectionFromArea(partialSelectionArea); - assert.lengthOf(selectionPartial[0], 0, "no circles were selected by the partial region"); + assert.lengthOf(selectionPartial[0], 1, "one circle was selected by the partial region"); allTestsPassed = tempTestsPassed; }); @@ -187,7 +188,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var indicesPartial = circleRenderer.getDataIndicesFromArea(partialSelectionArea); - assert.deepEqual(indicesPartial, [], "no circles were selected by the partial region"); + assert.deepEqual(indicesPartial, [2], "circle 2 was selected by the partial region"); allTestsPassed = tempTestsPassed; }); From a34b36478bceade11b250353e26a64d37e930bd9 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 18:39:43 -0800 Subject: [PATCH 49/61] Modify tests.html so that lines will have a stroke --- tests.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests.html b/tests.html index bcb8146578..2b63482e95 100644 --- a/tests.html +++ b/tests.html @@ -9,6 +9,9 @@ svg { border: 2px solid blue; } + .line { + stroke: black; + } From 0ef35b7ec67b215d1df22af4379a7faad368e8f3 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 18:40:08 -0800 Subject: [PATCH 50/61] Add basic line renderer tests --- test/rendererTests.ts | 45 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index ecfb43b4a5..522d95ec17 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -10,7 +10,8 @@ function makeQuadraticSeries(n: number): IDataset { return {data: data, seriesName: "quadratic-series"}; } -var dataset1 = makeQuadraticSeries(10); +var quadraticDataset = makeQuadraticSeries(10); + describe("Renderers", () => { @@ -41,6 +42,46 @@ describe("Renderers", () => { }); describe("XYRenderer functionality", () => { + + describe("Basic LineRenderer functionality", () => { + // We test all the underlying XYRenderer logic with our CircleRenderer, let's just verify that the line + // draws properly for the LineRenderer + var svg: D3.Selection; + var xScale; + var yScale; + var lineRenderer; + var simpleDataset = {seriesName: "simpleDataset", data: [{x: 0, y:0}, {x:1, y:1}]}; + var renderArea; + + before(() => { + svg = generateSVG(500, 500); + xScale = new LinearScale(); + yScale = new LinearScale(); + lineRenderer = new LineRenderer(simpleDataset, xScale, yScale); + lineRenderer.anchor(svg).computeLayout().render(); + renderArea = lineRenderer.renderArea; + }); + + it("the line renderer drew an appropriate line", () => { + assert.equal(renderArea.attr("d"), "M0,500L500,0"); + }); + + it("rendering is idempotent", () => { + lineRenderer.render(); + assert.equal(renderArea.attr("d"), "M0,500L500,0"); + }); + + it("rescaled rerender works properly", () => { + xScale.domain([0, 5]); + yScale.domain([0, 10]); + assert.equal(renderArea.attr("d"), "M0,500L100,450"); + }); + + after(() => { + svg.remove(); + }); + }); + describe("Example CircleRenderer with quadratic series", () => { var svg: D3.Selection; var xScale: LinearScale; @@ -89,7 +130,7 @@ describe("Renderers", () => { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new LinearScale(); yScale = new LinearScale(); - circleRenderer = new CircleRenderer(dataset1, xScale, yScale); + circleRenderer = new CircleRenderer(quadraticDataset, xScale, yScale); circleRenderer.anchor(svg).computeLayout().render(); pixelAreaFull = {xMin: 0, xMax: SVG_WIDTH, yMin: 0, yMax: SVG_HEIGHT}; pixelAreaPartial = {xMin: 200, xMax: 600, yMin: 100, yMax: 200}; From c5052f5920daaa6c99a798e9dd3affa85b1f5359 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 18:42:07 -0800 Subject: [PATCH 51/61] Modify LineRenderer so it creates this.path rather than overwriting this.renderArea. Update tests. --- src/renderer.ts | 7 ++++--- test/rendererTests.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/renderer.ts b/src/renderer.ts index 3c5c4ba33e..4b227efe43 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -118,6 +118,7 @@ class XYRenderer extends Renderer { class LineRenderer extends XYRenderer { private static CSS_CLASS = "line-renderer"; + private path: D3.Selection; private line: D3.Svg.Line; constructor(dataset: IDataset, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor) { @@ -127,7 +128,7 @@ class LineRenderer extends XYRenderer { public anchor(element: D3.Selection) { super.anchor(element); - this.renderArea = this.renderArea.append("path"); + this.path = this.renderArea.append("path"); return this; } @@ -136,10 +137,10 @@ class LineRenderer extends XYRenderer { this.line = d3.svg.line() .x((datum: any) => this.xScale.scale(this.xAccessor(datum))) .y((datum: any) => this.yScale.scale(this.yAccessor(datum))); - this.dataSelection = this.renderArea.classed("line", true) + this.dataSelection = this.path.classed("line", true) .classed(this.dataset.seriesName, true) .datum(this.dataset.data); - this.renderArea.attr("d", this.line); + this.path.attr("d", this.line); return this; } } diff --git a/test/rendererTests.ts b/test/rendererTests.ts index 522d95ec17..8cea5dd95b 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -63,18 +63,21 @@ describe("Renderers", () => { }); it("the line renderer drew an appropriate line", () => { - assert.equal(renderArea.attr("d"), "M0,500L500,0"); + var path = renderArea.select("path"); + assert.equal(path.attr("d"), "M0,500L500,0"); }); it("rendering is idempotent", () => { lineRenderer.render(); - assert.equal(renderArea.attr("d"), "M0,500L500,0"); + var path = renderArea.select("path"); + assert.equal(path.attr("d"), "M0,500L500,0"); }); it("rescaled rerender works properly", () => { xScale.domain([0, 5]); yScale.domain([0, 10]); - assert.equal(renderArea.attr("d"), "M0,500L100,450"); + var path = renderArea.select("path"); + assert.equal(path.attr("d"), "M0,500L100,450"); }); after(() => { From d6ae69e4b26dd83e93d256a48e326ec98d609dd5 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Wed, 12 Feb 2014 18:48:23 -0800 Subject: [PATCH 52/61] Factor logic for making svg.remove() get called iff all tests pass into a microclass called MultiTestVerifier --- test/rendererTests.ts | 40 ++++++++++++++++++++++------------------ test/testUtils.ts | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index 8cea5dd95b..f175e6fcfc 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -52,6 +52,7 @@ describe("Renderers", () => { var lineRenderer; var simpleDataset = {seriesName: "simpleDataset", data: [{x: 0, y:0}, {x:1, y:1}]}; var renderArea; + var verifier = new MultiTestVerifier(); before(() => { svg = generateSVG(500, 500); @@ -62,15 +63,21 @@ describe("Renderers", () => { renderArea = lineRenderer.renderArea; }); + beforeEach(() => { + verifier.start(); + }); + it("the line renderer drew an appropriate line", () => { var path = renderArea.select("path"); assert.equal(path.attr("d"), "M0,500L500,0"); + verifier.end(); }); it("rendering is idempotent", () => { lineRenderer.render(); var path = renderArea.select("path"); assert.equal(path.attr("d"), "M0,500L500,0"); + verifier.end(); }); it("rescaled rerender works properly", () => { @@ -78,10 +85,11 @@ describe("Renderers", () => { yScale.domain([0, 10]); var path = renderArea.select("path"); assert.equal(path.attr("d"), "M0,500L100,450"); + verifier.end(); }); after(() => { - svg.remove(); + if (verifier.passed) {svg.remove();}; }); }); @@ -94,8 +102,8 @@ describe("Renderers", () => { var pixelAreaPartial: SelectionArea; var SVG_WIDTH = 600; var SVG_HEIGHT = 300; - var allTestsPassed = true; - var tempTestsPassed: boolean; + var verifier = new MultiTestVerifier(); + var circlesInArea; function getCircleRendererVerifier() { // creates a function that verifies that circles are drawn properly after accounting for svg transform @@ -122,11 +130,7 @@ describe("Renderers", () => { }; beforeEach(() => { - // We want to persist the effect where the svg hangs around if any of the tests passed. - // Set allTestsPassed to false at start of each test, and cache old allTestsPassed in temp var - // When test completes, swap the temp var back into allTestsPassed. Failure propogates thru to end. - tempTestsPassed = allTestsPassed; - allTestsPassed = false; + verifier.start(); }); before(() => { @@ -146,14 +150,14 @@ describe("Renderers", () => { assert.deepEqual(yScale.range(), [SVG_HEIGHT, 0], "yScale range was set by the renderer"); circleRenderer.renderArea.selectAll("circle").each(getCircleRendererVerifier()); assert.equal(circlesInArea, 10, "10 circles were drawn"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); it("rendering is idempotent", () => { circleRenderer.render().render(); circleRenderer.renderArea.selectAll("circle").each(getCircleRendererVerifier()); assert.equal(circlesInArea, 10, "10 circles were drawn"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); it("invertXYSelectionArea works", () => { @@ -168,7 +172,7 @@ describe("Renderers", () => { assert.closeTo(actualDataAreaPartial.xMax, expectedDataAreaPartial.xMax, 1, "partial xMax is close"); assert.closeTo(actualDataAreaPartial.yMin, expectedDataAreaPartial.yMin, 1, "partial yMin is close"); assert.closeTo(actualDataAreaPartial.yMax, expectedDataAreaPartial.yMax, 1, "partial yMax is close"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); it("getSelectionFromArea works", () => { @@ -179,7 +183,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var selectionPartial = circleRenderer.getSelectionFromArea(partialSelectionArea); assert.lengthOf(selectionPartial[0], 2, "2 circles were selected by the partial region"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); it("getDataIndicesFromArea works", () => { @@ -190,7 +194,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var indicesPartial = circleRenderer.getDataIndicesFromArea(partialSelectionArea); assert.deepEqual(indicesPartial, [6, 7], "2 circles were selected by the partial region"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); describe("after the scale has changed", () => { @@ -211,7 +215,7 @@ describe("Renderers", () => { assert.closeTo(actualDataAreaPartial.xMax, expectedDataAreaPartial.xMax, 1, "partial xMax is close"); assert.closeTo(actualDataAreaPartial.yMin, expectedDataAreaPartial.yMin, 1, "partial yMin is close"); assert.closeTo(actualDataAreaPartial.yMax, expectedDataAreaPartial.yMax, 1, "partial yMax is close"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); it("getSelectionFromArea works", () => { @@ -222,7 +226,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var selectionPartial = circleRenderer.getSelectionFromArea(partialSelectionArea); assert.lengthOf(selectionPartial[0], 1, "one circle was selected by the partial region"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); it("getDataIndicesFromArea works", () => { @@ -233,7 +237,7 @@ describe("Renderers", () => { var partialSelectionArea = circleRenderer.invertXYSelectionArea(pixelAreaPartial); var indicesPartial = circleRenderer.getDataIndicesFromArea(partialSelectionArea); assert.deepEqual(indicesPartial, [2], "circle 2 was selected by the partial region"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); it("the circles re-rendered properly", () => { @@ -241,12 +245,12 @@ describe("Renderers", () => { var circles = renderArea.selectAll("circle"); circles.each(getCircleRendererVerifier()); assert.equal(circlesInArea, 4, "four circles were found in the render area"); - allTestsPassed = tempTestsPassed; + verifier.end(); }); }); after(() => { - if (allTestsPassed) {svg.remove();}; + if (verifier.passed) {svg.remove();}; }); }); diff --git a/test/testUtils.ts b/test/testUtils.ts index 3ee3886870..f4a8b155de 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -24,3 +24,17 @@ function assertBBoxEquivalence(bbox, widthAndHeightPair, message) { assert.equal(bbox.width, width, "width: " + message); assert.equal(bbox.height, height, "height: " + message); } + +class MultiTestVerifier { + public passed = true; + private temp: boolean; + + public start() { + this.temp = this.passed; + this.passed = false; + } + + public end() { + this.passed = this.temp; + } +} From 92236162805f09e9245d5385af908d7cdbe2f8a5 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Thu, 13 Feb 2014 11:50:17 -0800 Subject: [PATCH 53/61] Minor changes to Renderer.ts: - Merge BAR_START_PADDING, BAR_END_PADDING into barPaddingPx and make it public not private - Stop adding scale autorange padding in the renderer (add it back in scale later) - Turn off transition for easier unit testing (add back in in more general way later) --- src/renderer.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/renderer.ts b/src/renderer.ts index 4b227efe43..49e926009e 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -169,8 +169,7 @@ class CircleRenderer extends XYRenderer { class BarRenderer extends XYRenderer { private static CSS_CLASS = "bar-renderer"; private static defaultX2Accessor = (d: any) => d.x2; - private BAR_START_PADDING_PX = 1; - private BAR_END_PADDING_PX = 1; + public barPaddingPx = 1; public x2Accessor: IAccessor; @@ -186,7 +185,7 @@ class BarRenderer extends XYRenderer { var yDomain = this.yScale.domain(); if (!Utils.inRange(0, yDomain[0], yDomain[1])) { var newMin = 0; - var newMax = 1.1 * yDomain[1]; + var newMax = yDomain[1]; this.yScale.widenDomain([newMin, newMax]); // TODO: make this handle reversed scales } @@ -203,11 +202,11 @@ class BarRenderer extends XYRenderer { this.dataSelection = this.renderArea.selectAll("rect").data(this.dataset.data); this.dataSelection.enter().append("rect"); - this.dataSelection.transition() - .attr("x", (d: any) => this.xScale.scale(this.xAccessor(d)) + this.BAR_START_PADDING_PX) + this.dataSelection + .attr("x", (d: any) => this.xScale.scale(this.xAccessor(d)) + this.barPaddingPx) .attr("y", (d: any) => this.yScale.scale(this.yAccessor(d))) .attr("width", (d: any) => (this.xScale.scale(this.x2Accessor(d)) - this.xScale.scale(this.xAccessor(d)) - - this.BAR_START_PADDING_PX - this.BAR_END_PADDING_PX)) + - 2 * this.barPaddingPx)) .attr("height", (d: any) => maxScaledY - this.yScale.scale(this.yAccessor(d)) ); this.dataSelection.exit().remove(); return this; From 29c9489d2414db0a9604cb3bd776a0cad3082160 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Thu, 13 Feb 2014 11:52:07 -0800 Subject: [PATCH 54/61] Add unit testing for BarRenderer. Close #26. --- test/rendererTests.ts | 68 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/test/rendererTests.ts b/test/rendererTests.ts index f175e6fcfc..826b042058 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -43,14 +43,14 @@ describe("Renderers", () => { describe("XYRenderer functionality", () => { - describe("Basic LineRenderer functionality", () => { + describe("Basic LineRenderer functionality, with custom accessors", () => { // We test all the underlying XYRenderer logic with our CircleRenderer, let's just verify that the line // draws properly for the LineRenderer var svg: D3.Selection; var xScale; var yScale; var lineRenderer; - var simpleDataset = {seriesName: "simpleDataset", data: [{x: 0, y:0}, {x:1, y:1}]}; + var simpleDataset = {seriesName: "simpleDataset", data: [{foo: 0, bar:0}, {foo:1, bar:1}]}; var renderArea; var verifier = new MultiTestVerifier(); @@ -58,7 +58,9 @@ describe("Renderers", () => { svg = generateSVG(500, 500); xScale = new LinearScale(); yScale = new LinearScale(); - lineRenderer = new LineRenderer(simpleDataset, xScale, yScale); + var xAccessor = (d) => d.foo; + var yAccessor = (d) => d.bar; + lineRenderer = new LineRenderer(simpleDataset, xScale, yScale, xAccessor, yAccessor); lineRenderer.anchor(svg).computeLayout().render(); renderArea = lineRenderer.renderArea; }); @@ -252,7 +254,67 @@ describe("Renderers", () => { after(() => { if (verifier.passed) {svg.remove();}; }); + }); + + describe("Bar Renderer", () => { + var svg: D3.Selection; + var xScale: LinearScale; + var yScale: LinearScale; + var barRenderer: BarRenderer; + var SVG_WIDTH = 600; + var SVG_HEIGHT = 400; + var verifier = new MultiTestVerifier(); + var d0 = {x: 0, x2: 1, y: 1}; + var d1 = {x: 2, x2: 6, y: 4}; + var dataset = {seriesName: "sampleBarData", data: [d0, d1]}; + + before(() => { + svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); + xScale = new LinearScale(); + yScale = new LinearScale(); + barRenderer = new BarRenderer(dataset, xScale, yScale); + barRenderer.anchor(svg).computeLayout(); + }); + beforeEach(() => { + verifier.start(); + }); + + it("bars were rendered correctly with padding disabled", () => { + barRenderer.barPaddingPx = 0; + barRenderer.render(); + var renderArea = barRenderer.renderArea; + var bars = renderArea.selectAll("rect"); + var bar0 = d3.select(bars[0][0]); + var bar1 = d3.select(bars[0][1]); + assert.equal(bar0.attr("width"), "100", "bar0 width is correct"); + assert.equal(bar1.attr("width"), "400", "bar1 width is correct"); + assert.equal(bar0.attr("height"), "100", "bar0 height is correct"); + assert.equal(bar1.attr("height"), "400", "bar1 height is correct"); + assert.equal(bar0.attr("x"), "0", "bar0 x is correct"); + assert.equal(bar1.attr("x"), "200", "bar1 x is correct"); + verifier.end(); + }); + + it("bars were rendered correctly with padding enabled", () => { + barRenderer.barPaddingPx = 1; + barRenderer.render(); + var renderArea = barRenderer.renderArea; + var bars = renderArea.selectAll("rect"); + var bar0 = d3.select(bars[0][0]); + var bar1 = d3.select(bars[0][1]); + assert.equal(bar0.attr("width"), "98", "bar0 width is correct"); + assert.equal(bar1.attr("width"), "398", "bar1 width is correct"); + assert.equal(bar0.attr("height"), "100", "bar0 height is correct"); + assert.equal(bar1.attr("height"), "400", "bar1 height is correct"); + assert.equal(bar0.attr("x"), "1", "bar0 x is correct"); + assert.equal(bar1.attr("x"), "201", "bar1 x is correct"); + verifier.end(); + }); + + after(() => { + if (verifier.passed) {svg.remove();}; + }); }); }); }); From e4a9061f3c2957fd0169b60b8b22089476c10952 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Thu, 13 Feb 2014 12:01:04 -0800 Subject: [PATCH 55/61] Add coordinatorTests.ts. The unit test is failing, revealing that the ScaleDomainCoordinator has not survived the removal of lodash unharmed. Opened issue #97. --- test/coordinatorTests.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/coordinatorTests.ts diff --git a/test/coordinatorTests.ts b/test/coordinatorTests.ts new file mode 100644 index 0000000000..71557d2d7f --- /dev/null +++ b/test/coordinatorTests.ts @@ -0,0 +1,23 @@ +/// + +var assert = chai.assert; + +describe("Coordinators", () => { + describe("ScaleDomainCoordinator", () => { + it("domains are coordinated", () => { + var s1 = new LinearScale(); + var s2 = new LinearScale(); + var s3 = new LinearScale(); + var dc = new ScaleDomainCoordinator([s1, s2, s3]); + s1.domain([0, 100]); + assert.deepEqual(s1.domain(), [0, 100]); + assert.deepEqual(s1.domain(), s2.domain()); + assert.deepEqual(s1.domain(), s3.domain()); + + s1.domain([-100, 5000]); + assert.deepEqual(s1.domain(), [-100, 5000]); + assert.deepEqual(s1.domain(), s2.domain()); + assert.deepEqual(s1.domain(), s3.domain()); + }); + }); +}); From 15735f41745d8a0d98a3abd592255198c73c7cb0 Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Thu, 13 Feb 2014 12:43:04 -0800 Subject: [PATCH 56/61] Fix the problem of circular ScaleDomainCoordinator rescaling in a simple and elegant way. Close #97. --- src/coordinator.ts | 15 +++++---------- src/scale.ts | 7 +------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/coordinator.ts b/src/coordinator.ts index e00da3ab6f..36375e83d8 100644 --- a/src/coordinator.ts +++ b/src/coordinator.ts @@ -5,23 +5,18 @@ class ScaleDomainCoordinator { It registers event listeners for when one of its scales changes its domain. When the scale does change its domain, it re-propogates the change to every linked scale. */ - private currentDomain: any[] = []; + private rescaleInProgress = false; constructor(private scales: Scale[]) { this.scales.forEach((s) => s.registerListener((sx: Scale) => this.rescale(sx))); } public rescale(scale: Scale) { - var newDomain = scale.domain(); - if (newDomain === this.currentDomain) { - // Avoid forming a really funky call stack with depth proportional to number of scales - // pointer equality check is sufficient in this case + if (this.rescaleInProgress) { return; } - this.currentDomain = newDomain; - // This will repropogate the change to every scale, including the scale that - // originated it. This is fine because the scale will check if the new domain is - // different from its current one and will disregard the change if they are equal. - // It would be easy to stop repropogating to the original scale if it mattered. + this.rescaleInProgress = true; + var newDomain = scale.domain(); this.scales.forEach((s) => s.domain(newDomain)); + this.rescaleInProgress = false; } } diff --git a/src/scale.ts b/src/scale.ts index 6e68c89354..611cba3577 100644 --- a/src/scale.ts +++ b/src/scale.ts @@ -3,7 +3,6 @@ class Scale implements IBroadcaster { public scale: D3.Scale.Scale; private broadcasterCallbacks: IBroadcasterCallback[] = []; - private cachedDomain: any[]; constructor(scale: D3.Scale.Scale) { this.scale = scale; @@ -16,11 +15,7 @@ class Scale implements IBroadcaster { public domain(): any[]; public domain(values: any[]): Scale; public domain(values?: any[]): any { - if (values != null && values !== this.cachedDomain) { - // It is important that the scale does not update if the new domain is the same as - // the current domain, to prevent circular propogation of events. We can do a - // pointer check against this.cachedDomain, but not against this.scale.domain(), since - // d3 modifies the domain as it comes in + if (values != null) { this.scale.domain(values); this.broadcasterCallbacks.forEach((b) => b(this)); return this; From b47cbc763e517e2508afde1c40fd55827eee4e6b Mon Sep 17 00:00:00 2001 From: Daniel Mane Date: Thu, 13 Feb 2014 12:48:32 -0800 Subject: [PATCH 57/61] Add some more label tests --- test/labelTests.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/labelTests.ts b/test/labelTests.ts index d9eb85bc3e..b5a58a8397 100644 --- a/test/labelTests.ts +++ b/test/labelTests.ts @@ -80,4 +80,26 @@ describe("Labels", () => { assert.operator(bbox.width, "<=", svgWidth, "the text is not wider than the SVG width"); svg.remove(); }); + + it("text in a tiny box is truncated to empty string", () => { + var svg = generateSVG(10, 10); + var label = new TitleLabel("Yeah, not gonna fit..."); + label.anchor(svg).computeLayout().render(); + var text = label.element.select("text"); + assert.equal(text.text(), "", "text was truncated to empty string"); + svg.remove(); + }); + + it("unsupported alignments and orientations are unsupported", () => { + assert.throws(() => new Label("foo", "bar"), Error, "not a valid orientation"); + var l1 = new Label("foo", "horizontal"); + var svg = generateSVG(10, 10); + l1.anchor(svg); + l1.xAlignment = "bar"; + l1.yAlignment = "bar"; + assert.throws(() => l1.computeLayout(), Error, "supported alignment"); + ( l1).orientation = "vertical-left"; + assert.throws(() => l1.computeLayout(), Error, "supported alignment"); + svg.remove(); + }); }); From 30306735faececbb6a11f2913838b1d8977dca78 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Wed, 12 Feb 2014 18:50:01 -0800 Subject: [PATCH 58/61] Added AreaInteraction tests. --- examples/demo.ts | 2 +- src/interaction.ts | 6 +- test/interactionTests.ts | 118 +++++++++++++++++++++++++++++++++++++++ test/rendererTests.ts | 9 --- test/testUtils.ts | 16 ++++++ 5 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 test/interactionTests.ts diff --git a/examples/demo.ts b/examples/demo.ts index 2596353f60..927143be2f 100644 --- a/examples/demo.ts +++ b/examples/demo.ts @@ -16,7 +16,7 @@ module Demo { basicTable.anchor(svg1); basicTable.computeLayout(); basicTable.render(); - new PanZoomInteraction(renderAreaD1, [xAxis, yAxis, renderAreaD1], xScale, yScale); + //new PanZoomInteraction(renderAreaD1, [xAxis, yAxis, renderAreaD1], xScale, yScale); diff --git a/src/interaction.ts b/src/interaction.ts index c6c4180cc4..7e9f52a4bc 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -32,7 +32,7 @@ interface ZoomInfo { } class PanZoomInteraction extends Interaction { - private zoom; + private zoom: D3.Behavior.Zoom; public renderers: Component[]; public xScale: QuantitiveScale; public yScale: QuantitiveScale; @@ -55,7 +55,7 @@ class PanZoomInteraction extends Interaction { private rerenderZoomed() { // HACKHACK since the d3.zoom.x modifies d3 scales and not our TS scales, and the TS scales have the - // event listener machinery, let's grab the domain out of hte d3 scale and pipe it back into the TS scale + // event listener machinery, let's grab the domain out of the d3 scale and pipe it back into the TS scale var xDomain = this.xScale.scale.domain(); var yDomain = this.yScale.scale.domain(); this.xScale.domain(xDomain); @@ -101,7 +101,7 @@ class AreaInteraction extends Interaction { } private dragstart(){ - this.dragBox.attr("height", 0).attr("width", 0); + this.clearBox(); var availableWidth = parseFloat(this.hitBox.attr("width")); var availableHeight = parseFloat(this.hitBox.attr("height")); // the constraint functions ensure that the selection rectangle will not exceed the hit box diff --git a/test/interactionTests.ts b/test/interactionTests.ts new file mode 100644 index 0000000000..d0d0b69b51 --- /dev/null +++ b/test/interactionTests.ts @@ -0,0 +1,118 @@ +/// + +var assert = chai.assert; + +function makeFakeEvent(x: number, y: number): D3.Event { + return { + dx: 0, + dy: 0, + clientX: x, + clientY: y, + translate: [x, y], + scale: 1, + sourceEvent: null, + x: x, + y: y, + keyCode: 0, + altKey: false + }; +} + +function fakeDragSequence(anyedInteraction: any, startX: number, startY: number, endX: number, endY: number) { + anyedInteraction.dragstart(); + d3.event = makeFakeEvent(startX, startY); + anyedInteraction.drag(); + d3.event = makeFakeEvent(endX, endY); + anyedInteraction.drag(); + anyedInteraction.dragend(); + d3.event = null; +} + +describe("Interactions", () => { + describe("AreaInteraction", () => { + var svgWidth = 400; + var svgHeight = 400; + var svg: D3.Selection; + var dataset: IDataset; + var xScale: QuantitiveScale; + var yScale: QuantitiveScale; + var renderer: XYRenderer; + var interaction: AreaInteraction; + + var dragstartX = 20; + var dragstartY = svgHeight-100; + var dragendX = 100; + var dragendY = svgHeight-20; + + before(() => { + svg = generateSVG(svgWidth, svgHeight); + dataset = makeLinearSeries(10); + xScale = new LinearScale(); + yScale = new LinearScale(); + renderer = new CircleRenderer(dataset, xScale, yScale); + renderer.anchor(svg).computeLayout().render(); + interaction = new AreaInteraction(renderer); + }); + + afterEach(() => { + interaction.areaCallback = null; + interaction.selectionCallback = null; + interaction.indicesCallback = null; + interaction.clearBox(); + }); + + it("All callbacks are notified with appropriate data when a drag finishes", () => { + var areaCallbackCalled = false; + var areaCallback = (a: FullSelectionArea) => { + areaCallbackCalled = true; + var expectedPixelArea = { + xMin: dragstartX, + xMax: dragendX, + yMin: dragstartY, + yMax: dragendY + }; + assert.deepEqual(a.pixel, expectedPixelArea, "areaCallback was passed the correct pixel area"); + }; + var selectionCallbackCalled = false; + var selectionCallback = (a: D3.Selection) => { + selectionCallbackCalled = true; + }; + var indicesCallbackCalled = false; + var indicesCallback = (a: number[]) => { + indicesCallbackCalled = true; + }; + + interaction.areaCallback = areaCallback; + interaction.selectionCallback = selectionCallback; + interaction.indicesCallback = indicesCallback; + + // fake a drag event + fakeDragSequence(( interaction), dragstartX, dragstartY, dragendX, dragendY); + + assert.isTrue(areaCallbackCalled, "areaCallback was called"); + assert.isTrue(selectionCallbackCalled, "selectionCallback was called"); + assert.isTrue(indicesCallbackCalled, "indicesCallback was called"); + }); + + it("Highlights and un-highlights areas appropriately", () => { + fakeDragSequence(( interaction), dragstartX, dragstartY, dragendX, dragendY); + var dragBoxClass = "." + ( AreaInteraction).CLASS_DRAG_BOX; + var dragBox = renderer.element.select(dragBoxClass); + var actualStartPosition = {x: parseFloat(dragBox.attr("x")), y: parseFloat(dragBox.attr("y"))}; + var expectedStartPosition = {x: Math.min(dragstartX, dragendX), y: Math.min(dragstartY, dragendY)}; + assert.deepEqual(actualStartPosition, expectedStartPosition, "highlighted box is positioned correctly"); + assert.equal(parseFloat(dragBox.attr("width")), Math.abs(dragstartX-dragendX), "highlighted box has correct width"); + assert.equal(parseFloat(dragBox.attr("height")), Math.abs(dragstartY-dragendY), "highlighted box has correct height"); + + ( interaction).dragstart(); + dragBox = renderer.element.select(dragBoxClass); + assert.equal(dragBox.attr("width"), "0", "highlighted box disappears when a new drag begins"); + assert.equal(dragBox.attr("height"), "0", "highlighted box disappears when a new drag begins"); + ( interaction).dragend(); + }); + + after(() => { + svg.remove(); + }); + }); +}); diff --git a/test/rendererTests.ts b/test/rendererTests.ts index 826b042058..3bda6ef152 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -2,17 +2,8 @@ var assert = chai.assert; -function makeQuadraticSeries(n: number): IDataset { - function makePoint(x: number) { - return {x: x, y: x*x}; - } - var data = d3.range(n).map(makePoint); - return {data: data, seriesName: "quadratic-series"}; -} - var quadraticDataset = makeQuadraticSeries(10); - describe("Renderers", () => { describe("base Renderer", () => { diff --git a/test/testUtils.ts b/test/testUtils.ts index f4a8b155de..f9f39fe046 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -25,6 +25,22 @@ function assertBBoxEquivalence(bbox, widthAndHeightPair, message) { assert.equal(bbox.height, height, "height: " + message); } +function makeLinearSeries(n: number): IDataset { + function makePoint(x: number) { + return {x: x, y: x}; + } + var data = d3.range(n).map(makePoint); + return {data: data, seriesName: "linear-series"}; +} + +function makeQuadraticSeries(n: number): IDataset { + function makeQuadraticPoint(x: number) { + return {x: x, y: x*x}; + } + var data = d3.range(n).map(makeQuadraticPoint); + return {data: data, seriesName: "quadratic-series"}; +} + class MultiTestVerifier { public passed = true; private temp: boolean; From 55ea4eb68570e9bfd973cf6269b0b4c625bc7e59 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Thu, 13 Feb 2014 12:23:57 -0800 Subject: [PATCH 59/61] Trying to add jQuery and jQuery.simulate to simulate UI events. --- bower.json | 4 +++- examples/demo.ts | 2 +- test/interactionTests.ts | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/bower.json b/bower.json index fef49c3b9f..580d84d5f1 100644 --- a/bower.json +++ b/bower.json @@ -6,6 +6,8 @@ }, "devDependencies": { "chai": "1.9.0", - "mocha": "1.17.1" + "mocha": "1.17.1", + "jQuery": "2.1.0", + "jquery.simulate": "1.2.0" } } diff --git a/examples/demo.ts b/examples/demo.ts index 927143be2f..2596353f60 100644 --- a/examples/demo.ts +++ b/examples/demo.ts @@ -16,7 +16,7 @@ module Demo { basicTable.anchor(svg1); basicTable.computeLayout(); basicTable.render(); - //new PanZoomInteraction(renderAreaD1, [xAxis, yAxis, renderAreaD1], xScale, yScale); + new PanZoomInteraction(renderAreaD1, [xAxis, yAxis, renderAreaD1], xScale, yScale); diff --git a/test/interactionTests.ts b/test/interactionTests.ts index d0d0b69b51..47810ed5c3 100644 --- a/test/interactionTests.ts +++ b/test/interactionTests.ts @@ -29,6 +29,29 @@ function fakeDragSequence(anyedInteraction: any, startX: number, startY: number, } describe("Interactions", () => { + describe("PanZoomInteraction", () => { + it.skip("Properly updates scales on zoom", () => { + // + var xScale = new LinearScale(); + var yScale = new LinearScale(); + var xDomainBefore = xScale.domain(); + var yDomainBefore = yScale.domain(); + + var svg = generateSVG(); + var component = new Component(); + component.anchor(svg).computeLayout().render(); + var interaction = new PanZoomInteraction(component, [], xScale, yScale); + + // var zoomEvent = document.createEvent("WheelEvent"); + // zoomEvent.initEvent("mousewheel", true, true); + // var hb = component.element.select(".hit-box").node(); + + // hb.dispatchEvent(zoomEvent); + + svg.remove(); + }); + }); + describe("AreaInteraction", () => { var svgWidth = 400; var svgHeight = 400; From 8d6547c1d15264e2edfc2f0d0e8911c21dcbede0 Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Fri, 14 Feb 2014 14:01:49 -0800 Subject: [PATCH 60/61] Finish adding in interaction tests. Close #27. --- test/interactionTests.ts | 92 +++++++++++++++++++++++++++++++++------- test/testReference.ts | 2 + tests.html | 5 ++- tsd.json | 6 +++ 4 files changed, 89 insertions(+), 16 deletions(-) diff --git a/test/interactionTests.ts b/test/interactionTests.ts index 47810ed5c3..b214849035 100644 --- a/test/interactionTests.ts +++ b/test/interactionTests.ts @@ -30,23 +30,47 @@ function fakeDragSequence(anyedInteraction: any, startX: number, startY: number, describe("Interactions", () => { describe("PanZoomInteraction", () => { - it.skip("Properly updates scales on zoom", () => { - // + it("Pans properly", () => { + // The only difference between pan and zoom is internal to d3 + // Simulating zoom events is painful, so panning will suffice here var xScale = new LinearScale(); var yScale = new LinearScale(); + + var svg = generateSVG(); + var dataset = makeLinearSeries(11); + var renderer = new CircleRenderer(dataset, xScale, yScale); + renderer.anchor(svg).computeLayout().render(); + var xDomainBefore = xScale.domain(); var yDomainBefore = yScale.domain(); - var svg = generateSVG(); - var component = new Component(); - component.anchor(svg).computeLayout().render(); - var interaction = new PanZoomInteraction(component, [], xScale, yScale); + var interaction = new PanZoomInteraction(renderer, [], xScale, yScale); - // var zoomEvent = document.createEvent("WheelEvent"); - // zoomEvent.initEvent("mousewheel", true, true); - // var hb = component.element.select(".hit-box").node(); + var hb = renderer.element.select(".hit-box").node(); + var dragDistancePixelX = 10; + var dragDistancePixelY = 20; + $(hb).simulate("drag", { + dx: dragDistancePixelX, + dy: dragDistancePixelY + }); + + var xDomainAfter = xScale.domain(); + var yDomainAfter = yScale.domain(); + + assert.notDeepEqual(xDomainAfter, xDomainBefore, "x domain was changed by panning"); + assert.notDeepEqual(yDomainAfter, yDomainBefore, "y domain was changed by panning"); + + function getSlope(scale: LinearScale) { + var range = scale.range(); + var domain = scale.domain(); + return (domain[1]-domain[0])/(range[1]-range[0]); + }; - // hb.dispatchEvent(zoomEvent); + var expectedXDragChange = -dragDistancePixelX * getSlope(xScale); + var expectedYDragChange = -dragDistancePixelY * getSlope(yScale); + + assert.equal(xDomainAfter[0]-xDomainBefore[0], expectedXDragChange, "x domain changed by the correct amount"); + assert.equal(yDomainAfter[0]-yDomainBefore[0], expectedYDragChange, "y domain changed by the correct amount"); svg.remove(); }); @@ -127,15 +151,53 @@ describe("Interactions", () => { assert.equal(parseFloat(dragBox.attr("width")), Math.abs(dragstartX-dragendX), "highlighted box has correct width"); assert.equal(parseFloat(dragBox.attr("height")), Math.abs(dragstartY-dragendY), "highlighted box has correct height"); - ( interaction).dragstart(); - dragBox = renderer.element.select(dragBoxClass); - assert.equal(dragBox.attr("width"), "0", "highlighted box disappears when a new drag begins"); - assert.equal(dragBox.attr("height"), "0", "highlighted box disappears when a new drag begins"); - ( interaction).dragend(); + interaction.clearBox(); + var boxGone = dragBox.attr("width") === "0" && dragBox.attr("height") === "0"; + assert.isTrue(boxGone, "highlighted box disappears when clearBox is called"); }); after(() => { svg.remove(); }); }); + + describe("BrushZoomInteraction", () => { + it("Zooms in correctly on drag", () =>{ + var xScale = new LinearScale(); + var yScale = new LinearScale(); + + var svgWidth = 400; + var svgHeight = 400; + var svg = generateSVG(svgWidth, svgHeight); + var dataset = makeLinearSeries(11); + var renderer = new CircleRenderer(dataset, xScale, yScale); + renderer.anchor(svg).computeLayout().render(); + + var xDomainBefore = xScale.domain(); + var yDomainBefore = yScale.domain(); + + var dragstartX = 10; + var dragstartY = 210; + var dragendX = 190; + var dragendY = 390; + + var expectedXDomain = [xScale.invert(dragstartX), xScale.invert(dragendX)]; + var expectedYDomain = [yScale.invert(dragendY), yScale.invert(dragstartY)]; // reversed because Y scale is + + var indicesCallbackCalled = false; + var indicesCallback = (a: number[]) => { + indicesCallbackCalled = true; + interaction.clearBox(); + assert.deepEqual(a, [1, 2, 3, 4], "the correct points were selected"); + assert.deepEqual(xScale.domain(), expectedXDomain, "X scale domain was updated correctly"); + assert.deepEqual(yScale.domain(), expectedYDomain, "Y scale domain was updated correclty"); + }; + var interaction = new BrushZoomInteraction(renderer, xScale, yScale, indicesCallback); + + fakeDragSequence(( interaction), dragstartX, dragstartY, dragendX, dragendY); + assert.isTrue(indicesCallbackCalled, "indicesCallback was called"); + + svg.remove(); + }); + }); }); diff --git a/test/testReference.ts b/test/testReference.ts index c94ab10905..12dd300f6a 100644 --- a/test/testReference.ts +++ b/test/testReference.ts @@ -1,5 +1,7 @@ /// /// /// +/// +/// /// /// diff --git a/tests.html b/tests.html index 2b63482e95..6452bd7bb3 100644 --- a/tests.html +++ b/tests.html @@ -14,9 +14,12 @@ } - + + + + diff --git a/tsd.json b/tsd.json index 48c9c8a766..1a7a705f82 100644 --- a/tsd.json +++ b/tsd.json @@ -12,6 +12,12 @@ }, "mocha/mocha.d.ts": { "commit": "16dd1ab76fb4c65e532ca820dd45c875636521b6" + }, + "jquery/jquery.d.ts": { + "commit": "dfa8d86385232fabd220c43a81a0bf4ca418f761" + }, + "jquery.simulate/jquery.simulate.d.ts": { + "commit": "dfa8d86385232fabd220c43a81a0bf4ca418f761" } } } From 555ac519eb1b86379fc6787e185934c7855b795d Mon Sep 17 00:00:00 2001 From: Justin Lan Date: Mon, 17 Feb 2014 11:01:52 -0800 Subject: [PATCH 61/61] Added a rudimentary axis test. We'll need to rewrite these when the Axis class is improved. --- src/axis.ts | 2 +- test/axisTests.ts | 15 +++++++++++++++ test/componentTests.ts | 6 ++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/axis.ts b/src/axis.ts index 7967b4c47a..b4ed3c34ea 100644 --- a/src/axis.ts +++ b/src/axis.ts @@ -55,7 +55,7 @@ class Axis extends Component { } public render() { - if (this.orientation === "left") {this.axisElement.attr("transform", "translate(" + Axis.yWidth + ")");}; + if (this.orientation === "left") {this.axisElement.attr("transform", "translate(" + Axis.yWidth + ", 0)");}; if (this.orientation === "top") {this.axisElement.attr("transform", "translate(0," + Axis.xHeight + ")");}; var domain = this.scale.domain(); var extent = Math.abs(domain[1] - domain[0]); diff --git a/test/axisTests.ts b/test/axisTests.ts index 2176f0c8f4..aadb1e9089 100644 --- a/test/axisTests.ts +++ b/test/axisTests.ts @@ -1,3 +1,18 @@ /// var assert = chai.assert; + +describe("Axes", () => { + // TODO: Rewrite these tests when the Axis class gets refactored + it("Renders ticks", () => { + var svg = generateSVG(500, 100); + var xScale = new LinearScale(); + xScale.domain([0, 10]); + xScale.range([0, 500]); + var axis = new XAxis(xScale, "bottom"); + axis.anchor(svg).computeLayout().render(); + var ticks = svg.selectAll(".tick"); + assert.operator(ticks[0].length, ">=", 2, "There are at least two ticks."); + svg.remove(); + }); +}); diff --git a/test/componentTests.ts b/test/componentTests.ts index 41632ec0fa..5b2c142efc 100644 --- a/test/componentTests.ts +++ b/test/componentTests.ts @@ -114,9 +114,11 @@ describe("Component behavior", () => { it("clipPath works as expected", () => { assert.isFalse(c.clipPathEnabled, "clipPathEnabled defaults to false"); c.clipPathEnabled = true; + var expectedClipPathID: number = ( Component).clipPathId; c.anchor(svg).computeLayout(0, 0, 100, 100).render(); - assert.equal(( Component).clipPathId, 1, "clipPathId incremented"); - assert.equal(c.element.attr("clip-path"), "url(#clipPath0)", "the element has clip-path url attached"); + assert.equal(( Component).clipPathId, expectedClipPathID+1, "clipPathId incremented"); + var expectedClipPathURL = "url(#clipPath" + expectedClipPathID+ ")"; + assert.equal(c.element.attr("clip-path"), expectedClipPathURL, "the element has clip-path url attached"); var clipRect = c.element.select(".clip-rect"); assert.equal(clipRect.attr("width"), 100, "the clipRect has an appropriate width"); assert.equal(clipRect.attr("height"), 100, "the clipRect has an appropriate height");