Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
TurtleScript/crender.js /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1823 lines (1715 sloc)
72.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // crender.js | |
| // Create widget tree for parsed Simplified JavaScript. | |
| // Written in Simplified JavaScript. | |
| // C. Scott Ananian | |
| // 2011-05-13 | |
| var make_crender = function() { | |
| // stub for i18n | |
| var _ = function(txt) { return txt; }; | |
| // basic graphics datatypes | |
| var Point = { | |
| create: function(x, y) { | |
| if (typeof(x)==="object") { y=x.y; x=x.x; } | |
| var p = Object.create(Point); | |
| p.x = x; | |
| p.y = y; | |
| return p; | |
| }, | |
| add: function(x, y) { | |
| if (typeof(x)==="object") { y=x.y; x=x.x; } | |
| return this.create((this.x||0) + (x||0), (this.y||0) + (y||0)); | |
| }, | |
| negate: function() { | |
| return this.create(-(this.x||0), -(this.y||0)); | |
| }, | |
| toString: function() { | |
| return "("+this.x+","+this.y+")"; | |
| } | |
| }; | |
| var pt = function(x, y) { return Point.create(x, y); }; | |
| // Bounding boxes are slightly fancy multiline rectangles. | |
| // They contain a starting indent and a trailing widow, like so: | |
| // | |
| // INDENTxxxx | |
| // xxxxxxxxxxxx | |
| // xxxxxxxxxxxx | |
| // xxWIDOW | |
| // | |
| // We also provide width() and height() properties that refer to the | |
| // overall dimensions, ignoring the indents. | |
| // | |
| // The multiline rect is specified as follows: | |
| // tl: top left coordinate of the overall bounding box | |
| // br: bottom right coordinate of the overall bounding box | |
| // indent: coordinate of the bottom left of the I in INDENT | |
| // widow: coordinate of the top right of the W in WIDOW | |
| // | |
| // By convention, we place the origin at the top-left of the I in INDENT, | |
| // so widgets typically return a bounding box with tl.y === 0, tl.x <=0, | |
| // and indent.x === 0. | |
| // Note that indent.x < tl.x is possible and valid. | |
| var MultiLineBBox = { | |
| // multiline should generally be equivalent to | |
| // this.indent.equals(this.bl) && this.widow.equals(this.tr); | |
| _multiline: false, | |
| multiline: function() { return this._multiline || false; }, | |
| tl: function() { return this._tl; }, | |
| tr: function() { return pt(this._br.x, this._tl.y); }, | |
| bl: function() { return pt(this._tl.x, this._br.y); }, | |
| br: function() { return this._br; }, | |
| indent: function() { return this._indent || this.bl(); }, | |
| widow: function() { return this._widow || this.tr(); }, | |
| width: function() { return this._br.x - this._tl.x; }, | |
| height: function() { return this._br.y - this._tl.y; }, | |
| top: function() { return this._tl.y; }, | |
| bottom: function() { return this._br.y; }, | |
| left: function() { return this._tl.x; }, | |
| right: function() { return this._br.x; }, | |
| widowHeight: function() { return this.bottom() - this.widow().y; }, | |
| create: function(tl, br, indent, widow, multiline) { | |
| var bb = Object.create(MultiLineBBox); | |
| bb._tl = tl; | |
| bb._br = br; | |
| if (indent) { bb._indent = indent; } | |
| if (widow) { bb._widow = widow; } | |
| if (typeof(multiline)==="boolean") { bb._multiline = multiline; } | |
| return bb; | |
| }, | |
| toString: function() { | |
| return "["+this.tl()+"-"+this.br()+" i:"+this.indent()+", "+ | |
| "w:"+this.widow()+", m:"+this.multiline()+"]"; | |
| }, | |
| translate: function(pt) { | |
| return this.create(this._tl.add(pt), | |
| this._br.add(pt), | |
| this._indent && this._indent.add(pt), | |
| this._widow && this._widow.add(pt), | |
| this._multiline); | |
| }, | |
| ensureHeight: function() { | |
| var nbb = this; | |
| var nHeight = Math.max.apply(Math, arguments); | |
| if (this.height() < nHeight) { | |
| nbb = Object.create(this); | |
| nbb._br = pt(this._br.x, this._tl.y + nHeight); | |
| } | |
| return nbb; | |
| }, | |
| contains: function(x, y) { | |
| // allow passing a pt object as first arg | |
| if (typeof(x)==="object") { y=x.y; x=x.x; } | |
| if (y < this._tl.y) { | |
| return false; | |
| } else if (y < this.indent.y()) { | |
| return (x >= this.indent().x) && (x < this._br.x); | |
| } else if (y < this.widow().y) { | |
| return (x >= this._tl.x) && (x < this._br.x); | |
| } else if (y < this._br.y) { | |
| return (x >= this._tl.x) && (x < this.widow().x); | |
| } else { | |
| return false; | |
| } | |
| }, | |
| // pad a box | |
| pad: function(padding, shift_origin) { | |
| var tl = pt(this.left() - (padding.left || 0), | |
| this.top() - (padding.top || 0)); | |
| var br = pt(this.right() + (padding.right || 0), | |
| this.bottom() + (padding.bottom || 0)); | |
| var indent = this._indent && | |
| (pt(this.indent().x - (padding.indentx || 0), | |
| this.indent().y - (padding.indenty || 0))); | |
| var widow = this._widow && | |
| (pt(this.widow().x + (padding.widowx || 0), | |
| this.widow().y + (padding.widowy || 0))); | |
| var result = this.create(tl, br, indent, widow, this._multiline); | |
| if (shift_origin) { | |
| result = result.translate(result.tl().negate()); | |
| } | |
| return result; | |
| }, | |
| // add a linebreak after a box and return the result | |
| linebreak: function(margin, lineHeight) { | |
| var height = Math.max(lineHeight||0, this.widowHeight()); | |
| var left = margin - this.widow().x; | |
| var lb; | |
| if (left < 0) { | |
| lb = this.create(pt(left, 0), pt(0, height), | |
| pt(0, height), pt(left, height), true); | |
| } else { | |
| lb = this.create(pt(0, 0), pt(left, height), | |
| pt(left, height), pt(0, height), true); | |
| } | |
| return this.chainHoriz(lb); | |
| }, | |
| // chain two bounding boxes together, top-aligning them. | |
| chainHoriz: function(bb) { | |
| var bb2 = bb.translate(this.widow()); | |
| var tl = pt(Math.min(this.left(), bb2.left()), | |
| Math.min(this.top(), bb2.top())); | |
| var br = pt(Math.max(this.right(), bb2.right()), | |
| Math.max(this.bottom(), bb2.bottom())); | |
| var ml = this.multiline() || bb2.multiline(); | |
| if (!ml) { | |
| return this.create(tl, br); | |
| } | |
| // handle multiline case | |
| var indent = this.indent(); | |
| if (!this.multiline()) { | |
| indent = pt(indent.x, Math.max(indent.y, bb2.indent().y)); | |
| // is this creating a box with a negative indent? | |
| if (this.left() < bb2.left()) { | |
| tl = bb2.tl(); | |
| } | |
| } | |
| var widow = bb2.widow(); // falls back to tr() | |
| return this.create(tl, br, indent, widow, ml); | |
| } | |
| }; | |
| var bbox = function (tl, br) { | |
| return MultiLineBBox.create(tl, br); | |
| }; | |
| var mlbbox = function(tl, br, indent, widow) { | |
| return MultiLineBBox.create(tl, br, indent, widow, true); | |
| }; | |
| var rect = function(w, h) { | |
| return bbox(pt(0,0), pt(w, h)); | |
| }; | |
| // FOR DEBUGGING | |
| MultiLineBBox.drawPath = function(canvas) { | |
| canvas.beginPath(); | |
| canvas.moveTo(this.indent()); | |
| canvas.lineTo(this.indent().x, this.top()); | |
| canvas.lineTo(this.tr()); | |
| canvas.lineTo(this.right(), this.widow().y); | |
| canvas.lineTo(this.widow()); | |
| canvas.lineTo(this.widow().x, this.bottom()); | |
| canvas.lineTo(this.bl()); | |
| canvas.lineTo(this.left(), this.indent().y); | |
| canvas.closePath(); | |
| }; | |
| // helper to save/restore contexts | |
| // also reset fill/stroke color and font height. | |
| var context_saved = function(f) { | |
| return function() { | |
| var nargs = Array.prototype.concat.apply([this], arguments); | |
| var g = f.bind.apply(f, nargs); | |
| return this.canvas.withContext(this, function() { | |
| // reset fill/stroke | |
| this.canvas.setFill(this.styles.textColor); | |
| this.canvas.setStroke(this.styles.tileOutlineColor); | |
| // reset font height | |
| this.canvas.setFontHeight(this.styles.textHeight); | |
| return g(); | |
| }); | |
| }; | |
| }; | |
| // first, let's make some widgets | |
| var DEFAULT_WIDGET_TEXT="...???..."; | |
| var Widget = { | |
| // layout the widget, compute sizes and bounding boxes. | |
| // cache the values, we use them a lot. | |
| // can be recalled to update canvas/styles or drawing properties. | |
| layout: function(canvas, styles, properties) { | |
| this.canvas = canvas; | |
| this.styles = styles; | |
| this.bbox = this.computeBBox(properties); | |
| }, | |
| // bounding box includes child widgets (which may extend) | |
| // but doesn't include puzzle sockets/plugs (which also hang over) | |
| // bbox may be a MultiLineBBox | |
| computeBBox: function(properties) { | |
| // in default implementation, the bounding box is the same | |
| // as the size (see below) | |
| this.size = this.computeSize(properties); | |
| return this.size; | |
| }, | |
| // allow child widgets to override default tile background color. | |
| bgColor: function() { | |
| return this.styles.tileColor; | |
| }, | |
| // helper to offset basic sizes | |
| pad: function(r, padding, shift_origin) { | |
| if (typeof(r.width) !== "function") { | |
| // handle output from measureText, which is not a real rect() | |
| r = rect(r.width, r.height); | |
| shift_origin = true; | |
| } | |
| if (typeof(padding) === "number") { | |
| padding = { left: padding, top: padding, | |
| right: padding, bottom: padding }; | |
| } | |
| if (typeof(padding) !== "object") { | |
| padding = this.styles.tilePadding; | |
| } | |
| return r.pad(padding, shift_origin); | |
| }, | |
| // by convention we compute a 'size' property which is the size of | |
| // the widget itself, ignoring children. This isn't a standard | |
| // method, though; | |
| // default widget rendering | |
| computeSize: context_saved(function(properties) { | |
| return this.pad(this.canvas.measureText(DEFAULT_WIDGET_TEXT)); | |
| }), | |
| // by convention, given a canvas translated so that our top-left | |
| // corner is 0, 0 | |
| draw: context_saved(function() { | |
| // very simple box. | |
| this.canvas.setFill(this.bgColor()); | |
| this.bbox.drawPath(this.canvas); | |
| this.canvas.fill(); | |
| this.canvas.stroke(); | |
| this.drawPaddedText(DEFAULT_WIDGET_TEXT, pt(0, 0), | |
| this.styles.textColor); | |
| }), | |
| // drawing aids | |
| drawPaddedText: function(text, pt, color) { | |
| if (color) { this.canvas.setFill(color); } | |
| this.canvas.drawText(text, | |
| pt.x + this.styles.tilePadding.left, | |
| pt.y + this.styles.tilePadding.top + | |
| this.styles.textHeight); | |
| }, | |
| // make a rounded corner. | |
| // from and to are [0-3] and represent angles in units of 90 degrees | |
| // "0" is in the positive x direction and angles increase CW | |
| drawRoundCorner: function(pt, from, isCW, radius) { | |
| var f = isCW ? from : (from===0) ? 3 : (from - 1); | |
| var rad = radius || this.styles.tileCornerRadius; | |
| var cx = pt.x + ((f===0 || f===3) ? -rad : rad); | |
| var cy = pt.y + ((f===0 || f===1) ? -rad : rad); | |
| var to = isCW ? (from+1) : (from - 1); | |
| this.canvas.arc(cx, cy, rad, from*Math.PI/2, to*Math.PI/2, !isCW); | |
| }, | |
| // make name and expression plugs/sockets | |
| drawCapUp: function(pt, isPlug, isRight, isName) { | |
| var ew = this.styles.expWidth; | |
| var eh = this.styles.expHalfHeight; | |
| if (isPlug) { isRight = !isRight; } | |
| if (isRight) { ew = -ew; } | |
| this.canvas.lineTo(pt.add(0, eh*2)); | |
| if (isName && !isRight) { | |
| this.canvas.lineTo(pt.add(0, eh)); | |
| } | |
| this.canvas.lineTo(pt.add(ew, eh)); | |
| if (isName && isRight) { | |
| this.canvas.lineTo(pt.add(0, eh)); | |
| } | |
| this.canvas.lineTo(pt); | |
| }, | |
| drawCapDown: function(pt, isPlug, isRight, isName) { | |
| var ew = this.styles.expWidth; | |
| var eh = this.styles.expHalfHeight; | |
| if (isPlug) { isRight = !isRight; } | |
| if (isRight) { ew = -ew; } | |
| this.canvas.lineTo(pt); | |
| if (isName && isRight) { | |
| this.canvas.lineTo(pt.add(0, eh)); | |
| } | |
| this.canvas.lineTo(pt.add(ew, eh)); | |
| if (isName && !isRight) { | |
| this.canvas.lineTo(pt.add(0, eh)); | |
| } | |
| this.canvas.lineTo(pt.add(0, eh*2)); | |
| }, | |
| // bounding box debugging | |
| debugBBox: context_saved(function(bbox) { | |
| this.canvas.setStroke(this.canvas.makeColor(255,0,0)); | |
| (bbox || this.bbox).drawPath(this.canvas); | |
| this.canvas.stroke(); | |
| }) | |
| }; | |
| // helpers | |
| var ContainerWidget = Object.create(Widget); | |
| ContainerWidget.length = 0; // an array-like object | |
| ContainerWidget.addChild = function(child) { | |
| Array.prototype.push.call(this, child); | |
| }; | |
| ContainerWidget.children = function() { | |
| return Array.prototype.slice.call(this); | |
| }; | |
| var HorizWidget = Object.create(Widget); | |
| HorizWidget.computeBBox = function(properties) { | |
| this.size = this.computeSize(properties); | |
| var r = this.size; | |
| // optionally leave space for connector on left | |
| var margin = (properties.margin || 0) + (this.extraMargin || 0); | |
| var lineHeight = (properties.lineHeight || 0) + | |
| (this.extraLineHeight || 0); | |
| this.children().forEach(function(c) { | |
| var child_properties = Object.create(properties); | |
| child_properties.margin = margin - r.widow().x; | |
| lineHeight = Math.max(lineHeight, r.widowHeight()); | |
| child_properties.lineHeight = lineHeight; | |
| c.layout(this.canvas, this.styles, child_properties); | |
| r = r.chainHoriz(c.bbox); | |
| }.bind(this)); | |
| return r; | |
| }; | |
| var VertWidget = Object.create(Widget); | |
| VertWidget.computeBBox = function(properties) { | |
| this.size = this.computeSize(properties); | |
| var r = this.size; | |
| this.childOrigin = []; | |
| // sum heights of children | |
| this.children().forEach(function(c) { | |
| var child_props = Object.create(properties); | |
| child_props.margin = 0; | |
| child_props.lineHeight = 0; | |
| c.layout(this.canvas, this.styles, properties); | |
| var p = pt(0, r.bottom()); | |
| this.childOrigin.push(p); | |
| var bb = c.bbox.translate(p); | |
| r = bbox(pt(Math.min(r.left(), bb.left()), | |
| Math.min(r.top(), bb.top())), | |
| pt(Math.max(bb.right(), r.right()), | |
| Math.max(bb.bottom(), r.bottom()))); | |
| }.bind(this)); | |
| return r; | |
| }; | |
| // simple c-shaped statement. | |
| var CeeWidget = Object.create(Widget); | |
| CeeWidget.ceeStartPt = function() { | |
| return pt(this.styles.puzzleIndent + 2*this.styles.puzzleRadius, 0); | |
| }; | |
| CeeWidget.ceeEndPt = function() { | |
| return pt(this.styles.puzzleIndent + 2*this.styles.puzzleRadius, | |
| this.size.bottom()); | |
| }; | |
| CeeWidget.draw = context_saved(function() { | |
| this.canvas.setFill(this.bgColor()); | |
| // start path at ceeStartPoint | |
| this.canvas.beginPath(); | |
| this.canvas.moveTo(this.ceeStartPt()); | |
| // make the puzzle piece socket arc | |
| this.canvas.arc(this.styles.puzzleIndent + this.styles.puzzleRadius, | |
| 0, this.styles.puzzleRadius, | |
| 0, Math.PI, false); | |
| // make the corner arcs | |
| this.drawRoundCorner(pt(0, 0), 3, false); | |
| this.drawRoundCorner(pt(0, this.size.bottom()), 2, false); | |
| // puzzle piece 'plug' arg | |
| this.canvas.arc(this.styles.puzzleIndent + this.styles.puzzleRadius, | |
| this.size.bottom(), this.styles.puzzleRadius, | |
| Math.PI, 0, true); | |
| this.canvas.lineTo(this.ceeEndPt()); | |
| // allow subclass to alter the right-hand side. | |
| this.rightHandPath(); | |
| // fill & stroke | |
| this.canvas.closePath(); | |
| this.canvas.fill(); | |
| this.canvas.stroke(); | |
| // allow subclass to actually draw the contents. | |
| this.canvas.setFill(this.styles.textColor); | |
| this.drawInterior(); | |
| }); | |
| CeeWidget.drawInterior = function() { /* no op */ }; | |
| CeeWidget.rightHandPath = function() { | |
| // basic rounded right-hand-side | |
| this.canvas.lineTo(this.size.right() - this.styles.tileCornerRadius, | |
| this.size.bottom()); | |
| this.drawRoundCorner(this.size.br(), 1, false); | |
| this.drawRoundCorner(this.size.tr(), 0, false); | |
| }; | |
| // Expression tiles | |
| var ExpWidget = Object.create(Widget); | |
| ExpWidget.outlineColor = function(){ return this.styles.tileOutlineColor; }; | |
| ExpWidget.draw = context_saved(function() { | |
| this.canvas.setFill(this.bgColor()); | |
| this.canvas.setStroke(this.outlineColor()); | |
| // start path at 0,0 | |
| this.canvas.beginPath(); | |
| this.canvas.moveTo(0,0); | |
| this.leftHandPath(); | |
| // draw line along bottom | |
| this.bottomPath(); | |
| // allow subclass to customize rhs | |
| this.rightHandPath(); | |
| // allow subclass to customize the top | |
| this.topSidePath(); | |
| // fill & stroke | |
| this.canvas.closePath(); | |
| this.canvas.fill(); | |
| this.canvas.stroke(); | |
| // allow subclass to actually draw the contents. | |
| this.canvas.setFill(this.styles.textColor); | |
| this.drawInterior(); | |
| }); | |
| ExpWidget.bottomPath = function() { | |
| this.canvas.lineTo(0, this.size.bottom()); | |
| this.canvas.lineTo(this.size.br()); | |
| }; | |
| ExpWidget.leftHandDir = -1; | |
| ExpWidget.rightHandDir = 1; | |
| ExpWidget.isName = false; | |
| ExpWidget.leftHandPath = function() { | |
| if (this.leftHandDir === 0) { return; } | |
| this.drawCapDown(pt(0,0), (this.leftHandDir < 0), false, this.isName); | |
| }; | |
| ExpWidget.rightHandPath = function() { | |
| this.canvas.lineTo(this.size.br()); | |
| if (this.rightHandDir === 0) { | |
| this.canvas.lineTo(this.size.right(), 0); | |
| return; | |
| } | |
| this.drawCapUp(pt(this.size.right(), 0), (this.rightHandDir > 0), true, | |
| this.isName); | |
| }; | |
| ExpWidget.topSidePath = function() { | |
| // straight line by default. | |
| }; | |
| // yada yada yada expression | |
| var YADA_TEXT = "..."; | |
| var YadaWidget = Object.create(ExpWidget); | |
| YadaWidget.bgColor = function() { return this.styles.yadaColor; }; | |
| YadaWidget.outlineColor = function() { return this.styles.yadaColor; }; | |
| YadaWidget.computeSize = context_saved(function(properties) { | |
| return this.pad(this.canvas.measureText(YADA_TEXT)); | |
| }); | |
| YadaWidget.drawInterior = function() { | |
| this.drawPaddedText(YADA_TEXT, pt(0, 0), this.styles.semiColor); | |
| }; | |
| // Horizonal combinations of widgets | |
| var HorizExpWidget = Object.create(ExpWidget); | |
| HorizExpWidget.computeBBox = function(properties) { | |
| return this.pad(HorizWidget.computeBBox.call(this, properties), | |
| { bottom: this.styles.expUnderHeight }); | |
| }; | |
| // lists of things, separated by symbols of some kind. | |
| // the things can be names or exps; the symbols are circled by | |
| // the widget's outline. The symbols/names/exps can be multiline. | |
| // XXX basically each symbol should have a 'line break after' property. | |
| var SeparatedListWidget = Object.create(Widget); | |
| // override this! | |
| SeparatedListWidget.computeItems = function(properties) { return []; }; | |
| // items don't have to be children, but they are by default. | |
| SeparatedListWidget.children = function() { | |
| var result = []; | |
| this.items.forEach(function(item) { | |
| if (item.widget && !item.hide) { | |
| result.push(item.widget); | |
| } | |
| }); | |
| return result; | |
| }; | |
| // meat & potatoes | |
| SeparatedListWidget.computeBBox = function(properties) { | |
| this.items = this.computeItems(properties); | |
| var bbox = rect(0, 0); | |
| var lineHeight = properties.lineHeight || 0; | |
| this.itemPos = []; | |
| this.itemBBox = []; | |
| this.items.forEach(function(item, index) { | |
| var child_props = Object.create(properties); | |
| // adjust margin for new start position as well as to allow for | |
| // a descender on the left. | |
| child_props.margin = (properties.margin||0) - bbox.widow().x; | |
| child_props.margin += this.styles.expUnderWidth; | |
| child_props.margin += (item.indent || 0); | |
| // lineheight has to account for underline | |
| child_props.lineHeight = this.styles.expUnderHeight + | |
| Math.max(lineHeight, bbox.widowHeight()); | |
| // add the child. | |
| var itemBB; | |
| if (item.widget) { | |
| item.widget.layout(this.canvas, this.styles, child_props); | |
| itemBB = item.widget.bbox; | |
| } else { | |
| if (typeof(item.bbox)==="function") { | |
| itemBB = item.bbox(child_props); | |
| } else { | |
| itemBB = item.bbox; | |
| } | |
| } | |
| this.itemPos.push(bbox.widow()); | |
| this.itemBBox.push(itemBB.translate(bbox.widow())); | |
| bbox = (index===0) ? itemBB : bbox.chainHoriz(itemBB); | |
| if (itemBB.multiline()) { | |
| // reset line height once we wrap | |
| lineHeight -= bbox.widow().y; | |
| } | |
| }.bind(this)); | |
| // misc. prettiness: don't underline if there's only one item | |
| // in the list | |
| if (this.items.length <= 1 && !this.underlineShortLists) { | |
| return bbox; | |
| } | |
| // and some height to account for the underline | |
| bbox = bbox.pad({bottom: this.styles.expUnderHeight}); | |
| // if we wrapped, we also need a leader on the left | |
| if (bbox.multiline()) { | |
| bbox = bbox.pad({left: this.styles.expUnderWidth}); | |
| var indent = bbox.indent().x - bbox.left(); | |
| var indentSign = (indent < 0) ? -1 : (indent > 0) ? 1 : 0; | |
| bbox = bbox.pad({indenty: indentSign*this.styles.expUnderHeight}); | |
| } | |
| return bbox; | |
| }; | |
| SeparatedListWidget.draw = context_saved(function() { | |
| this.drawOutline(); | |
| this.drawInterior(); | |
| this.drawChildren(); | |
| }); | |
| SeparatedListWidget.drawSymbol=function(item, index, props) { | |
| var bb = this.itemBBox[index]; | |
| // draw up left side | |
| if (!props.leftSuppress) { | |
| this.drawCapUp(this.itemPos[index], | |
| props.leftIsPlug || false, | |
| false/*left*/, props.leftIsName || false); | |
| } | |
| // trace top border of bbox | |
| if (bb.multiline()) { | |
| this.drawRoundCorner(bb.tr(), 3, true); | |
| this.drawRoundCorner(pt(bb.right(), bb.widow().y), 0, true); | |
| } | |
| // draw down right side | |
| if (!props.rightSuppress) { | |
| this.drawCapDown(bb.widow(), | |
| props.rightIsPlug || false, | |
| true/*right*/, props.rightIsName || false); | |
| } | |
| }; | |
| SeparatedListWidget.drawOutline = context_saved(function() { | |
| if (this.itemPos.length === 0) { return; } | |
| // along bottoms of each item and them up around the separator | |
| this.canvas.setFill(this.bgColor()); | |
| this.canvas.beginPath(); | |
| this.canvas.moveTo(this.bbox.indent()); | |
| this.items.forEach(function(item, index) { | |
| if (item.isSymbol) { | |
| // move up and outline the symbol (extension point) | |
| var props = { leftIsName: this.isName, leftIsPlug: true, | |
| rightIsName: this.isName, rightIsPlug: true }; | |
| if (index > 0) { | |
| props.leftIsName = this.items[index-1].isName; | |
| props.leftIsPlug = false; /* socket */ | |
| props.leftSuppress = this.items[index-1].isSymbol; | |
| } | |
| if ((index+1) < this.items.length) { | |
| props.rightIsName = this.items[index+1].isName; | |
| props.rightIsPlug = false; /* socket */ | |
| props.rightSuppress = this.items[index+1].isSymbol; | |
| } | |
| this.drawSymbol(item, index, props); | |
| } else if (this.itemBBox[index].width() > 0 || | |
| this.itemBBox[index].height() > 0) { | |
| // draw the bottom border of the child. | |
| // (skip this if this is a zero-size item) | |
| var bb = this.itemBBox[index]; | |
| this.canvas.lineTo(bb.indent()); | |
| this.canvas.lineTo(bb.left(), bb.indent().y); | |
| this.canvas.lineTo(bb.bl()); | |
| this.canvas.lineTo(bb.widow().x, bb.bottom()); | |
| } | |
| }.bind(this)); | |
| // now draw around my bounding box. | |
| this.canvas.lineTo(this.bbox.widow().x, this.bbox.bottom()); | |
| this.canvas.lineTo(this.bbox.bl()); | |
| this.canvas.lineTo(this.bbox.left(), this.bbox.indent().y); | |
| this.canvas.lineTo(this.bbox.indent()); | |
| this.canvas.closePath(); | |
| this.canvas.fill(); | |
| this.canvas.stroke(); | |
| }); | |
| SeparatedListWidget.drawInterior = function() {}; | |
| SeparatedListWidget.drawChildren = context_saved(function() { | |
| this.items.forEach(function(item, index) { | |
| if (item.widget) { | |
| this.canvas.withContext(this, function() { | |
| this.canvas.translate(this.itemPos[index]); | |
| item.widget.draw(); | |
| }); | |
| } | |
| }.bind(this)); | |
| }); | |
| // lists (of exprs/names). | |
| // XXX should eventually provide means for line wrapping. | |
| // XXX each comma should have a 'line break after' property, | |
| // but toggling between "each arg on its own line" and "all on one line" | |
| // is probably fine for now. | |
| var CommaListWidget = Object.create(SeparatedListWidget); | |
| CommaListWidget.length = 0; | |
| CommaListWidget.addChild = ContainerWidget.addChild; | |
| CommaListWidget.label = ","; | |
| CommaListWidget.children = function() { | |
| if (this.length === 0 && this.disallowEmptyList) { | |
| return [ YadaWidget ]; | |
| } | |
| return ContainerWidget.children.call(this); | |
| }; | |
| CommaListWidget.computeItems = function(properties) { | |
| if (this.length === 0 && this.disallowEmptyList) { | |
| return [ { widget: YadaWidget } ]; | |
| } | |
| this.size = this.computeSize(properties); | |
| var result = []; | |
| var comma = { bbox: this.size, isSymbol: true }; | |
| //line break! | |
| var commaNL = Object.create(comma); | |
| commaNL.bbox = function(props) { | |
| if (props.margin > -this.styles.commaBreakWidth) { | |
| return comma.bbox; | |
| } | |
| return comma.bbox.linebreak(props.margin, props.lineHeight). | |
| pad({widowx: this.styles.expWidth }); | |
| }.bind(this); | |
| Array.prototype.forEach.call(this, function(child, idx) { | |
| if (idx !== 0) { | |
| result.push( commaNL ); | |
| } | |
| result.push( { widget:child, isName: this.isName || false } ); | |
| }.bind(this)); | |
| return result; | |
| }; | |
| CommaListWidget.extraPadding = { left: -3, right: -3 }; // tighten up | |
| CommaListWidget.computeSize = context_saved(function(properties) { | |
| var r = this.pad(this.canvas.measureText(this.label)); | |
| // pad to account for expression sockets on both sides. | |
| r = this.pad(r, { left: this.styles.expWidth, | |
| right: this.styles.expWidth }, true); | |
| return this.pad(r, this.extraPadding, true); | |
| }); | |
| CommaListWidget.drawInterior = context_saved(function() { | |
| var offset = this.styles.expWidth + (this.extraPadding.left || 0); | |
| this.items.forEach(function(item, index) { | |
| if (!item.widget) { | |
| var pos = this.itemPos[index]; | |
| this.drawPaddedText(this.label, pos.add(offset,0), | |
| this.styles.semiColor); | |
| } | |
| }.bind(this)); | |
| }); | |
| // make a prefix operator widget | |
| var PrefixWidget = Object.create(SeparatedListWidget); | |
| PrefixWidget.operator = "?"; | |
| PrefixWidget.rightOperand = YadaWidget; | |
| PrefixWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| return [ addBBox({ isSymbol: true, operator: this.operator }), | |
| { widget: this.rightOperand } ]; | |
| }; | |
| PrefixWidget.computeSizeOf = context_saved(function(item, properties) { | |
| var txt = item.noPad ? item.operator : (" "+item.operator+" "); | |
| var r = this.pad(this.canvas.measureText(txt)); | |
| r = this.pad(r, { right: this.styles.expWidth /* for sockets */}); | |
| item.bbox = r; | |
| return item; | |
| }); | |
| PrefixWidget.drawInterior = context_saved(function() { | |
| var offset = Math.floor(this.styles.expWidth / 2) - 1; | |
| this.items.forEach(function(item, index) { | |
| if (item.isSymbol) { | |
| var txt = item.noPad ? item.operator : (" "+item.operator+" "); | |
| this.drawPaddedText(txt, | |
| this.itemPos[index].add(offset, 0)); | |
| } | |
| }.bind(this)); | |
| }); | |
| // Infix operator (from prefix widget) | |
| var InfixWidget = Object.create(PrefixWidget); | |
| InfixWidget.leftOperand = YadaWidget; | |
| InfixWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| return [ { widget: this.leftOperand }, | |
| addBBox({ isSymbol: true, operator: this.operator }), | |
| { widget: this.rightOperand } ]; | |
| }; | |
| // make ([ operators from the infix widget | |
| var WithSuffixWidget = Object.create(InfixWidget); | |
| WithSuffixWidget.closeOperator = '?'; | |
| WithSuffixWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| return [ { widget: this.leftOperand }, | |
| addBBox({ isSymbol: true, operator: this.operator, | |
| noPad: true }), | |
| { widget: this.rightOperand }, | |
| addBBox({ isSymbol: true, operator: this.closeOperator, | |
| noPad: true }) ]; | |
| }; | |
| var ParenWidget = Object.create(SeparatedListWidget); | |
| ParenWidget.operand = YadaWidget; | |
| ParenWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| return [ addBBox({ isSymbol: true, operator: '(', noPad:true }), | |
| { widget: this.operand }, | |
| addBBox({ isSymbol: true, operator: ')', noPad:true }) ]; | |
| }; | |
| ParenWidget.drawInterior= PrefixWidget.drawInterior; | |
| var NewArrayWidget = Object.create(SeparatedListWidget); | |
| NewArrayWidget._operand = CommaListWidget; | |
| NewArrayWidget.children = function() { | |
| return this._operand.children(); | |
| }; | |
| NewArrayWidget.addChild = function(child) { | |
| if (this._operand === CommaListWidget) { | |
| // don't mutate the prototype | |
| this._operand = Object.create(CommaListWidget); | |
| } | |
| this._operand.addChild(child); | |
| this.length = this._operand.length; | |
| }; | |
| NewArrayWidget.length = 0; | |
| NewArrayWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| return [ addBBox({ isSymbol: true, operator: '[' }), | |
| { widget: this._operand }, | |
| addBBox({ isSymbol: true, operator: ']' }) ]; | |
| }; | |
| NewArrayWidget.drawInterior= PrefixWidget.drawInterior; | |
| var ConditionalWidget = Object.create(SeparatedListWidget); | |
| ConditionalWidget.testOperand = YadaWidget; | |
| ConditionalWidget.trueOperand = YadaWidget; | |
| ConditionalWidget.falseOperand= YadaWidget; | |
| ConditionalWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| return [ { widget: this.testOperand }, | |
| addBBox({ isSymbol: true, operator: '?' }), | |
| { widget: this.trueOperand }, | |
| addBBox({ isSymbol: true, operator: ':' }), | |
| { widget: this.falseOperand } ]; | |
| }; | |
| ConditionalWidget.drawInterior= PrefixWidget.drawInterior; | |
| var DotNameWidget = Object.create(InfixWidget); | |
| DotNameWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| var dotItem = addBBox({ isSymbol: true, operator: ".", noPad: true }); | |
| var rightAdapter = rect(this.styles.expWidth, dotItem.bbox.height()); | |
| return [ { widget: this.leftOperand }, | |
| dotItem, | |
| { widget: this.rightOperand, isName: true }, | |
| { bbox: rightAdapter, isSymbol: true, operator: "" } ]; | |
| }; | |
| var DotNameInvokeWidget = Object.create(DotNameWidget); | |
| DotNameInvokeWidget.args = CommaListWidget; | |
| DotNameInvokeWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| var items = DotNameWidget.computeItems.call(this, properties); | |
| items[3] = addBBox({ isSymbol: true, operator: "(", noPad: true }); | |
| items[4] = { widget: this.args }; | |
| items[5] = addBBox({ isSymbol: true, operator: ")", noPad: true }); | |
| return items; | |
| }; | |
| // object creation, contains a funny sort of expression list (vertical?) | |
| var NewObjectWidget = Object.create(SeparatedListWidget); | |
| NewObjectWidget.length = 0; | |
| NewObjectWidget.addChild = function(name, value) { | |
| Array.prototype.push.call(this, {name:name, value:value}); | |
| }; | |
| NewObjectWidget.forEach = Array.prototype.forEach; | |
| NewObjectWidget.computeItems = function(properties) { | |
| var addBBox = PrefixWidget.computeSizeOf.bind(this); | |
| var r = []; | |
| r.push(addBBox({ isSymbol: true, operator: '{' }, properties)); | |
| // set up our colon and comma bboxes | |
| var colon = addBBox({ isSymbol: true, operator: ": ", noPad:true }); | |
| var comma = addBBox({ isSymbol: true, operator: ", ", noPad:true }); | |
| var lastComma = addBBox({ isSymbol: true, operator: " ", noPad:true }); | |
| // now add items. | |
| this.forEach(function(item, index) { | |
| r.push({ widget: item.name, isName: true }); | |
| r.push(colon); | |
| r.push({ widget: item.value, indent: this.styles.blockIndent }); | |
| if ((index+1) < this.length) { | |
| r.push(comma); | |
| } else { | |
| r.push(lastComma); | |
| } | |
| }.bind(this)); | |
| r.push(addBBox({ isSymbol: true, operator: '}' }, properties)); | |
| // break after each comma? | |
| if (this.length > 0) { // XXX USE BETTER MULTILINE CRITERION | |
| var indenter = function(old_bb, indent, props) { | |
| return old_bb.linebreak(props.margin, props.lineHeight). | |
| pad({ widowx: indent }); | |
| }; | |
| var objIndent = this.styles.objIndent; | |
| comma.bbox = indenter.bind(this, comma.bbox, objIndent); | |
| r[0].bbox = indenter.bind(this, r[0].bbox, objIndent); | |
| lastComma.bbox = indenter.bind(this, lastComma.bbox, 0); | |
| } | |
| return r; | |
| }; | |
| NewObjectWidget.drawInterior = PrefixWidget.drawInterior; | |
| var LabelledExpWidget = Object.create(ExpWidget); | |
| LabelledExpWidget.computeSize = context_saved(function(properties) { | |
| this.setFont(); | |
| return this.pad(this.canvas.measureText(this.getLabel())); | |
| }); | |
| LabelledExpWidget.drawInterior = function() { | |
| this.setFont(); | |
| this.drawPaddedText(this.getLabel(), pt(0, 0)); | |
| return; | |
| }; | |
| LabelledExpWidget.getLabel = function() { | |
| return this.label; | |
| }; | |
| LabelledExpWidget.setFont = function() { | |
| this.canvas.setFill(this.styles[this.fontStyle]); | |
| }; | |
| LabelledExpWidget.fontStyle = 'textColor'; | |
| // A name. Fits in an expression spot. | |
| var NameWidget = Object.create(LabelledExpWidget); | |
| NameWidget.name = '???'; // override | |
| NameWidget.leftHandDir = -1; | |
| NameWidget.rightHandDir = 1; | |
| NameWidget.isName = true; | |
| NameWidget.getLabel = function() { | |
| return this.name; | |
| }; | |
| NameWidget.setFont = function() { | |
| this.canvas.setFontBold(true); | |
| this.canvas.setFill(this.styles.nameColor); | |
| }; | |
| // Name literal -- kinda like a name, but different. | |
| // XXX figure out exactly how this is different | |
| // XXX one way is that it can have non-name characters, in which | |
| // case it should render in quotes. | |
| var NameLiteralWidget = Object.create(NameWidget); | |
| NameLiteralWidget.setFont = function() { | |
| this.canvas.setFill(this.styles.textColor); | |
| }; | |
| NameLiteralWidget.setName = function(name) { | |
| // XXX if name has non-name characters, put it in quotes here? | |
| // I guess the 'with quotes' rendering should really be dynamic? | |
| this.name = name; | |
| }; | |
| // Literals | |
| var THIS_TEXT = _("this"); | |
| var ThisWidget = Object.create(LabelledExpWidget); | |
| ThisWidget.label = THIS_TEXT; | |
| ThisWidget.fontStyle = 'constColor'; | |
| var UNDEFINED_TEXT = _("undefined"); | |
| var UndefinedWidget = Object.create(LabelledExpWidget); | |
| UndefinedWidget.label = UNDEFINED_TEXT; | |
| UndefinedWidget.fontStyle = 'constColor'; | |
| var NULL_TEXT = _("null"); | |
| var NullWidget = Object.create(LabelledExpWidget); | |
| NullWidget.label = NULL_TEXT; | |
| NullWidget.fontStyle = 'constColor'; | |
| var NumericWidget = Object.create(LabelledExpWidget); | |
| var StringWidget = Object.create(LabelledExpWidget); | |
| StringWidget.fontStyle = 'literalColor'; | |
| var BooleanWidget = Object.create(LabelledExpWidget); | |
| BooleanWidget.fontStyle = 'constColor'; | |
| // end caps for statements, while expressions, etc | |
| var EndCapWidget = Object.create(ExpWidget); | |
| EndCapWidget.computeSize = context_saved(function(properties) { | |
| var r = this.pad(this.canvas.measureText(this.label)); | |
| return this.pad(r, this.extraPadding, true/*shift origin*/); | |
| }); | |
| EndCapWidget.drawInterior = function() { | |
| this.drawPaddedText(this.label, pt(this.extraPadding.left||0, 0), | |
| this.styles.semiColor); | |
| }; | |
| EndCapWidget.extraPadding = { left: 0, right: 0 }; | |
| EndCapWidget.leftHandDir = 1; | |
| // round right-hand side | |
| EndCapWidget.bottomPath = function() { | |
| this.canvas.lineTo(0, this.size.bottom()); | |
| }; | |
| EndCapWidget.rightHandPath = CeeWidget.rightHandPath; | |
| // semicolon terminating an expression statement | |
| var SEMI_TEXT = ";"; | |
| var SemiWidget = Object.create(EndCapWidget); | |
| SemiWidget.label = SEMI_TEXT; | |
| SemiWidget.extraPadding = { left: 4 }; | |
| SemiWidget.bgColor = function() { return this.styles.stmtColor; }; | |
| // while/if end cap | |
| var ParenBraceWidget = Object.create(EndCapWidget); | |
| ParenBraceWidget.label = ") {"; | |
| ParenBraceWidget.extraPadding = { left: 5, right: 4 }; | |
| ParenBraceWidget.bgColor = function() { return this.styles.stmtColor; }; | |
| // expression statement tile; takes an expression on the right. | |
| var ExpStmtWidget = Object.create(CeeWidget); | |
| ExpStmtWidget.bgColor = function() { return this.styles.stmtColor; }; | |
| ExpStmtWidget.rightHandDir = -1; | |
| ExpStmtWidget.rightHandPath = ExpWidget.rightHandPath; | |
| ExpStmtWidget.expression = YadaWidget; // default | |
| ExpStmtWidget.semiProto = SemiWidget; // allow subclass to customize | |
| ExpStmtWidget.children = function() { | |
| // create this in the instance because we tweak its size directly | |
| if (!this.semi) { this.semi = Object.create(this.semiProto); } | |
| return [ this.expression, this.semi ]; | |
| }; | |
| ExpStmtWidget.computeSize = function(properties) { | |
| return this.pad(rect(this.styles.puzzleIndent + | |
| this.styles.puzzleRadius + | |
| this.styles.expWidth, | |
| this.styles.textHeight), this.styles.tilePadding, | |
| true/*shift origin*/); | |
| }; | |
| ExpStmtWidget.computeBBox = function(properties) { | |
| // adjust margin to move expression continuations past our | |
| // left-hand side. | |
| var indent = this.computeSize(properties); | |
| var nprop = Object.create(properties); | |
| nprop.margin += indent.width(); | |
| // compute 'natural' size | |
| var bb = HorizWidget.computeBBox.call(this, nprop); | |
| // now adjust so that our height and semicolon height match the | |
| // height of the RHS expression (including indent and widow) | |
| this.size = this.size.ensureHeight(this.expression.bbox.bottom()); | |
| var lastLineHeight = this.size.height()-this.expression.bbox.widow().y; | |
| this.semi.bbox = this.semi.size = | |
| this.semi.size.ensureHeight(lastLineHeight); | |
| return bb; | |
| }; | |
| ExpStmtWidget.draw = context_saved(function() { | |
| // draw me | |
| ExpStmtWidget.__proto__.draw.call(this); | |
| // draw my children | |
| var canvas = this.canvas; | |
| canvas.translate(this.size.widow()); | |
| this.children().forEach(function(c) { | |
| c.draw(); | |
| canvas.translate(c.bbox.widow()); | |
| }); | |
| }); | |
| var LabelledExpStmtWidget = Object.create(ExpStmtWidget); | |
| LabelledExpStmtWidget.label = "<override me>"; | |
| LabelledExpStmtWidget.computeSize = context_saved(function(properties) { | |
| var r = this.pad(this.canvas.measureText(this.label+" ")); | |
| // indent the text to match expression statements | |
| this.indent = ExpStmtWidget.computeSize.call(this, properties).right(); | |
| // make room for rhs socket | |
| return this.pad(r, { left: this.indent, | |
| right: this.styles.expWidth }, true/*shift*/); | |
| }); | |
| LabelledExpStmtWidget.drawInterior = function() { | |
| // indent the text to match expression statements | |
| this.drawPaddedText(this.label, pt(this.indent, 0), | |
| this.styles.keywordColor); | |
| }; | |
| // simple break statement tile. | |
| var BREAK_TEXT = _("break"); | |
| var BreakWidget = Object.create(CeeWidget); | |
| BreakWidget.bgColor = function() { return this.styles.stmtColor; }; | |
| BreakWidget.computeSize = context_saved(function(properties) { | |
| var r = this.pad(this.canvas.measureText(BREAK_TEXT+SEMI_TEXT)); | |
| // indent the text to match expression statements | |
| this.indent = ExpStmtWidget.computeSize.call(this, properties).right(); | |
| return this.pad(r, {left: this.indent }, true); | |
| }); | |
| BreakWidget.drawInterior = function() { | |
| // indent the text to match expression statements | |
| var x = this.indent; | |
| this.drawPaddedText(BREAK_TEXT, pt(x, 0), this.styles.keywordColor); | |
| x += this.canvas.measureText(BREAK_TEXT).width; | |
| this.drawPaddedText(SEMI_TEXT, pt(x, 0), this.styles.semiColor); | |
| }; | |
| // return statement tile; takes an expression on the right | |
| var RETURN_TEXT = _("return"); | |
| var ReturnWidget = Object.create(LabelledExpStmtWidget); | |
| ReturnWidget.label = RETURN_TEXT; | |
| // var statement, holds a name list. | |
| var VAR_TEXT = _("var"); | |
| var VarWidget = Object.create(LabelledExpStmtWidget); | |
| VarWidget.label = VAR_TEXT; | |
| VarWidget.isName = true; | |
| VarWidget.semiProto = Object.create(SemiWidget); | |
| VarWidget.semiProto.isName = true; | |
| VarWidget.expression = Object.create(YadaWidget); | |
| VarWidget.expression.isName = true; | |
| VarWidget.addName = function(nameWidget) { | |
| if (!this.hasOwnProperty("expression")) { | |
| this.expression = Object.create(CommaListWidget); | |
| this.expression.isName = true; | |
| } | |
| this.expression.addChild(nameWidget); | |
| }; | |
| // Invisible vertical stacking container | |
| var BlockWidget = Object.create(Widget); | |
| BlockWidget.length = 0; // this is an array-like object. | |
| BlockWidget.addChild = ContainerWidget.addChild; | |
| BlockWidget.children = function() { | |
| var r = []; | |
| if (this.vars) { r.push(this.vars); } | |
| return r.concat(ContainerWidget.children.call(this)); | |
| }; | |
| BlockWidget.addVar = function(nameWidget) { | |
| if (!this.vars) { this.vars = Object.create(VarWidget); } | |
| this.vars.addName(nameWidget); | |
| }; | |
| BlockWidget.computeSize = function(properties) { | |
| return rect(0, 0); // no size of our own | |
| }; | |
| BlockWidget.computeBBox = function(properties) { | |
| // add a little padding below last block | |
| return this.pad(VertWidget.computeBBox.call(this, properties), | |
| { bottom: this.styles.blockBottomPadding }); | |
| }; | |
| BlockWidget.draw = context_saved(function() { | |
| var children = this.children(); | |
| var drawChild = context_saved(function(c, idx) { | |
| this.canvas.translate(this.childOrigin[idx]); | |
| c.draw(); | |
| }).bind(this); | |
| children.forEach(drawChild); | |
| }); | |
| // function expression, contains a name list and a block | |
| // XXX render functions w/ no body inline? | |
| var FUNCTION_TEXT = _("function"); | |
| var FunctionWidget = Object.create(Widget); | |
| FunctionWidget.label = FUNCTION_TEXT; | |
| FunctionWidget.children = function() { | |
| var r = []; | |
| if (!this.args) { | |
| this.args = Object.create(CommaListWidget); | |
| this.args.isName = true; | |
| this.args.disallowEmptyList = true; | |
| } | |
| if (!this.block) { | |
| this.block = Object.create(BlockWidget); | |
| } | |
| if (this.name) { r.push(this.name); } | |
| return r.concat(this.args, this.block); | |
| }; | |
| FunctionWidget.computeBBox = context_saved(function(properties) { | |
| this.children(); // initialize fields as side effect | |
| this.functionBB = this.pad(this.canvas.measureText(FUNCTION_TEXT+" ")); | |
| if (this.name) { | |
| this.name.layout(this.canvas, this.styles, properties); | |
| this.nameBB = this.name.bbox; // simple bounding box | |
| } else { | |
| this.nameBB = rect(this.styles.functionNameSpace, | |
| this.functionBB.height()); | |
| } | |
| this.nameBB = this.nameBB.translate(this.functionBB.widow()); | |
| this.leftParenBB = this.pad(this.canvas.measureText(" (")); | |
| this.leftParenBB = this.leftParenBB.translate(this.nameBB.widow()); | |
| // args could be multiline, but aligns at the open paren | |
| var arg_props = Object.create(properties); | |
| arg_props.lineHeight = arg_props.margin = 0; | |
| this.args.layout(this.canvas, this.styles, arg_props); | |
| this.argsBB = this.args.bbox; | |
| // adjust args to have a minimum height/width (even w/ no args) | |
| this.argsBB = this.argsBB.ensureHeight(this.styles.expHalfHeight*2); | |
| if (this.argsBB.width() < this.styles.functionNameSpace) { | |
| this.argsBB=this.argsBB.pad({right:this.styles.functionNameSpace}); | |
| } | |
| this.argsBB = this.argsBB.translate(this.leftParenBB.widow()); | |
| this.rightParenBB = this.pad(this.canvas.measureText(") {")); | |
| this.rightParenBB = this.rightParenBB.translate(this.argsBB.widow()); | |
| // ensure this is tall enough to cover args | |
| this.rightParenBB = | |
| this.rightParenBB.ensureHeight(this.argsBB.widowHeight()); | |
| // ensure rightParenBB is tall enough for everything else on the | |
| // first line (if we haven't line-wrapped yet) | |
| if (!this.argsBB.multiline()) { | |
| this.rightParenBB = | |
| this.rightParenBB.ensureHeight(this.leftParenBB.height(), | |
| this.nameBB.height(), | |
| this.functionBB.height()); | |
| } | |
| // ensure that we're taller than the lineheight context, so we can | |
| // shoot a runner underneath the left hand side. | |
| if (this.rightParenBB.bottom() < (properties.lineHeight || 0)) { | |
| var extraPad = properties.lineHeight - this.rightParenBB.bottom(); | |
| this.rightParenBB = this.rightParenBB.pad({bottom: extraPad}); | |
| } | |
| // add enough for an underline. | |
| this.rightParenBB = | |
| this.rightParenBB.pad({bottom:this.styles.expUnderHeight}); | |
| // now we lay out the block | |
| var block_prop = Object.create(properties); | |
| block_prop.margin = 0; // already at the start of the line. | |
| block_prop.lineHeight = 0; | |
| this.block.layout(this.canvas, this.styles, block_prop); | |
| this.blockBB = this.block.bbox; | |
| var blkpt = pt(properties.margin + this.styles.functionIndent, | |
| this.rightParenBB.bottom()); | |
| this.blockBB = this.blockBB.translate(blkpt); | |
| // and the final close bracket | |
| this.rightBraceBB = this.pad(this.canvas.measureText("}")); | |
| this.rightBraceBB = this.rightBraceBB. | |
| translate(pt(properties.margin, this.blockBB.bottom())); | |
| // ok, add it all up! | |
| var firstLineWidth = this.rightParenBB.right(); // XXX multiline | |
| var blockWidth = this.blockBB.right(); | |
| var lastLineWidth = this.rightBraceBB.right(); | |
| var w = Math.max(firstLineWidth, blockWidth, lastLineWidth); | |
| var h = this.rightBraceBB.bottom(); | |
| var indent = | |
| pt(0, this.rightParenBB.bottom() - this.styles.expUnderHeight); | |
| var widow = this.rightBraceBB.tr(); | |
| return mlbbox(pt(properties.margin, 0), pt(w, h), indent, widow); | |
| }); | |
| FunctionWidget.draw = function() { | |
| this.drawOutline(); | |
| this.drawInterior(); | |
| this.drawChildren(); | |
| }; | |
| FunctionWidget.drawOutline = context_saved(function() { | |
| this.canvas.setFill(this.bgColor()); | |
| this.canvas.beginPath(); | |
| // expression plug on left | |
| this.drawCapDown(pt(0,0), true/*plug*/, false/*left*/, false/*exp*/); | |
| // first line indent | |
| this.canvas.lineTo(this.bbox.indent()); | |
| this.canvas.lineTo(this.bbox.left(), this.bbox.indent().y); | |
| // all the way down to the bottom | |
| this.canvas.lineTo(this.bbox.bl()); | |
| // to the end of rightBrace | |
| this.canvas.lineTo(this.rightBraceBB.br()); | |
| // expression plug on right | |
| this.drawCapUp(this.rightBraceBB.tr(), | |
| true/*plug*/, true/*right*/, false/*exp*/); | |
| // back up past block to bottom of right paren | |
| this.canvas.lineTo(this.bbox.left() + this.styles.functionIndent, | |
| this.rightBraceBB.top()); | |
| this.canvas.lineTo(this.bbox.left() + this.styles.functionIndent, | |
| this.rightParenBB.bottom()); | |
| // puzzle plug | |
| this.canvas.arc(this.bbox.left() + this.styles.functionIndent + | |
| this.styles.puzzleIndent + this.styles.puzzleRadius, | |
| this.rightParenBB.bottom(), this.styles.puzzleRadius, | |
| Math.PI, 0, true); | |
| // circle right paren | |
| this.drawRoundCorner(this.rightParenBB.br(), 1, false); | |
| this.drawRoundCorner(this.rightParenBB.tr(), 0, false); | |
| // arg list name socket (right side of arg list; left side socket) | |
| this.drawCapDown(this.rightParenBB.tl(), | |
| false/*socket*/, false/*left*/, true/*name*/); | |
| // underline the arg list | |
| this.canvas.lineTo(this.argsBB.widow().x, this.argsBB.bottom()); | |
| this.canvas.lineTo(this.argsBB.bl()); | |
| this.canvas.lineTo(this.argsBB.left(), this.argsBB.indent().y); | |
| this.canvas.lineTo(this.argsBB.indent()); | |
| // arg list name socket (left side of arg list; right side socket) | |
| this.drawCapUp(pt(this.argsBB.indent().x, this.argsBB.top()), | |
| false/*socket*/, true/*right*/, true/*name*/); | |
| // function name socket (right side of name; left side socket) | |
| this.drawCapDown(this.leftParenBB.tl(), | |
| false/*socket*/, false/*left*/, true/*name*/); | |
| // underline the function name | |
| this.canvas.lineTo(this.nameBB.br()); | |
| this.canvas.lineTo(this.nameBB.bl()); | |
| // function name socket (left side of name; right side socket) | |
| this.drawCapUp(this.nameBB.tl(), | |
| false/*socket*/, true/*right*/, true/*name*/); | |
| // we're done! | |
| this.canvas.closePath(); | |
| this.canvas.fill(); | |
| this.canvas.stroke(); | |
| }); | |
| FunctionWidget.drawInterior = context_saved(function() { | |
| // draw function label | |
| this.drawPaddedText(FUNCTION_TEXT, this.functionBB.tl(), | |
| this.styles.keywordColor); | |
| // draw open paren | |
| this.drawPaddedText(" (", this.leftParenBB.tl(), this.styles.semiColor); | |
| // draw close paren | |
| this.drawPaddedText(") {",this.rightParenBB.tl(),this.styles.semiColor); | |
| // draw close brace | |
| this.drawPaddedText("}", this.rightBraceBB.tl(), this.styles.semiColor); | |
| }); | |
| FunctionWidget.drawChildren = context_saved(function() { | |
| // draw function name | |
| this.canvas.withContext(this, function() { | |
| this.canvas.translate(this.nameBB.tl()); | |
| if (this.name) { | |
| this.name.draw(); | |
| } | |
| }); | |
| // draw args list | |
| this.canvas.withContext(this, function() { | |
| this.canvas.translate(this.argsBB.indent().x, this.argsBB.top()); | |
| this.args.draw(); | |
| }); | |
| // draw block | |
| this.canvas.withContext(this, function() { | |
| this.canvas.translate(this.blockBB.tl()); | |
| this.block.draw(); | |
| }); | |
| }); | |
| // while statement tile. c-shaped, also takes a right hand expression. | |
| var WHILE_TEXT = _("while"); | |
| var WhileWidget = Object.create(CeeWidget); | |
| WhileWidget.bgColor = function() { return this.styles.stmtColor; }; | |
| WhileWidget.testExpr = YadaWidget; | |
| WhileWidget.label = WHILE_TEXT; | |
| WhileWidget.children = function() { | |
| if (!this.parenBrace) { | |
| this.parenBrace = Object.create(ParenBraceWidget); | |
| } | |
| if (!this.block) { | |
| this.block = Object.create(BlockWidget); | |
| } | |
| // parenBrace is not a mutable child of this widget. | |
| return [ this.testExpr, this.block ]; | |
| }; | |
| WhileWidget.computeSize = context_saved(function(properties) { | |
| var w, h, indent; | |
| this.children(); // ensure children are defined/initialized | |
| this.topSize = this.pad(this.canvas.measureText(this.label+" (")); | |
| // make room for rhs socket | |
| this.topSize = this.pad(this.topSize, { right: this.styles.expWidth }); | |
| // grow vertically to match test testExpr | |
| var test_props = Object.create(properties); | |
| test_props.margin = test_props.lineHeight = 0; // align with open paren | |
| this.testExpr.layout(this.canvas, this.styles, test_props); | |
| this.topSize = this.topSize.ensureHeight(this.testExpr.bbox.bottom()); | |
| // increase the height to accomodate the block child. | |
| var block_props = Object.create(properties); | |
| block_props.margin = block_props.lineHeight = 0; | |
| this.block.layout(this.canvas, this.styles, block_props); | |
| h = this.topSize.height(); | |
| h += this.block.bbox.bottom(); | |
| this.blockBottom = h; | |
| // "other block stuff" (extension point used by IfWidget) | |
| this.indent = ExpStmtWidget.computeSize.call(this, properties).right(); | |
| h += this.computeExtraBlockSize(properties); | |
| // now accomodate the close brace below | |
| this.braceY = h; | |
| this.bottomSize = this.pad(this.canvas.measureText("} ")); | |
| h += this.bottomSize.height(); | |
| // indent the text to match expression statements | |
| this.topSize = this.pad(this.topSize, { left: this.indent }, true); | |
| this.bottomSize = this.pad(this.bottomSize, {left: this.indent}, true); | |
| w = Math.max(this.topSize.width(), this.bottomSize.width()); | |
| this.parenBrace.layout(this.canvas, this.styles, properties); | |
| return rect(w, h); | |
| }); | |
| WhileWidget.computeExtraBlockSize = function(properties) { return 0; }; | |
| WhileWidget.computeBBox = function(properties) { | |
| var w, h; | |
| this.size = this.computeSize(properties); | |
| var sz0 = this.testExpr.bbox; | |
| var sz1 = this.parenBrace.bbox; | |
| var sz2 = this.block.bbox; | |
| w = Math.max(this.size.width(), | |
| sz1.width() + sz0.width() + this.topSize.width(), | |
| sz2.width() + this.styles.blockIndent, | |
| this.bottomSize.width()); | |
| // force trailing brace to match expression height | |
| this.parenBrace.bbox = this.parenBrace.size = | |
| this.parenBrace.size.ensureHeight(this.testExpr.bbox.widowHeight()); | |
| return rect(w, this.size.height()); | |
| }; | |
| WhileWidget.rightHandPath = function() { | |
| this.canvas.lineTo(this.bottomSize.width() - this.styles.tileCornerRadius, | |
| this.size.height()); | |
| // bottom leg | |
| this.drawRoundCorner(pt(this.bottomSize.width(), this.size.height()), 1, false); | |
| this.drawRoundCorner(pt(this.bottomSize.width(), | |
| this.size.height() - this.bottomSize.height()), 0, false); | |
| // bottom puzzle piece socket | |
| if (this.styles.blockIndent + this.styles.puzzleIndent + | |
| 2 * this.styles.puzzleRadius <= | |
| this.bottomSize.width() - this.styles.tileCornerRadius) { | |
| this.canvas.arc(this.styles.blockIndent + this.styles.puzzleIndent + | |
| this.styles.puzzleRadius, | |
| this.size.height() - this.bottomSize.height(), | |
| this.styles.tileCornerRadius, | |
| 0, Math.PI, false); | |
| } | |
| // inside of the C | |
| this.canvas.lineTo(this.styles.blockIndent, | |
| this.size.height() - this.bottomSize.height()); | |
| this.extraRightHandPath(); // hook for subclass | |
| this.canvas.lineTo(this.styles.blockIndent, this.topSize.height()); | |
| // top puzzle piece plug | |
| this.canvas.arc(this.styles.blockIndent + this.styles.puzzleIndent + | |
| this.styles.puzzleRadius, this.topSize.height(), | |
| this.styles.puzzleRadius, | |
| Math.PI, 0, true); | |
| this.canvas.lineTo(this.topSize.width(), this.topSize.height()); | |
| // now the expression socket | |
| this.drawCapUp(pt(this.topSize.width(), 0), | |
| false/*socket*/, true/*right*/, false/*exp*/); | |
| }; | |
| WhileWidget.extraRightHandPath = function() { }; | |
| WhileWidget.drawInterior = function() { | |
| // indent the text to match expression statements | |
| var x = this.indent; | |
| this.drawPaddedText(this.label, pt(x, 0), this.styles.keywordColor); | |
| var wsz = this.pad(this.canvas.measureText(this.label), {}); | |
| this.drawPaddedText(" (", pt(x+wsz.width(), 0), this.styles.semiColor); | |
| var y = this.braceY - 2; // cheat the brace upwards a bit. | |
| this.drawPaddedText("}", pt(x, y), this.styles.semiColor); | |
| // now draw children | |
| this.canvas.withContext(this, function() { | |
| this.canvas.translate(this.topSize.widow()); | |
| this.testExpr.draw(); | |
| this.canvas.translate(this.testExpr.bbox.widow()); | |
| this.parenBrace.draw(); | |
| }); | |
| this.canvas.translate(this.styles.blockIndent, this.topSize.height()); | |
| this.block.draw(); | |
| }; | |
| // If statement widget, similar to the WhileWidget | |
| var IF_TEXT = _("if"); | |
| var IF_ELSE_TEXT = _("else"); | |
| var IfWidget = Object.create(WhileWidget); | |
| IfWidget.label = IF_TEXT; | |
| IfWidget.children = function() { | |
| var children = IfWidget.__proto__.children.call(this); | |
| if (this.elseBlock) { | |
| children.push(this.elseBlock); | |
| } | |
| return children; | |
| }; | |
| IfWidget.computeExtraBlockSize = function(properties) { | |
| if (!this.elseBlock) { return 0; } | |
| // height of 'else' clause and brace. | |
| this.elseBB = this.pad(this.canvas.measureText( | |
| "} "+IF_ELSE_TEXT+" {")).translate( | |
| pt(this.indent, this.blockBottom)); | |
| var block_props = Object.create(properties); | |
| block_props.margin = block_props.lineHeight = 0; | |
| this.elseBlock.layout(this.canvas, this.styles, block_props); | |
| this.elseBlockBB = this.elseBlock.bbox.translate( | |
| pt(this.styles.blockIndent, this.elseBB.bottom())); | |
| return this.elseBlockBB.bottom() - this.blockBottom; | |
| }; | |
| IfWidget.extraRightHandPath = function() { | |
| if (!this.elseBlock) { return; } | |
| this.canvas.lineTo(this.styles.blockIndent, this.elseBB.bottom()); | |
| // make a puzzle piece plug | |
| this.canvas.arc(this.styles.blockIndent + this.styles.puzzleIndent + | |
| this.styles.puzzleRadius, this.elseBB.bottom(), | |
| this.styles.puzzleRadius, | |
| Math.PI, 0, true); | |
| this.drawRoundCorner(this.elseBB.br(), 1, false); | |
| this.drawRoundCorner(this.elseBB.tr(), 0, false); | |
| this.canvas.lineTo(this.styles.blockIndent, this.elseBB.top()); | |
| }; | |
| IfWidget.drawInterior = function() { | |
| this.canvas.withContext(this, IfWidget.__proto__.drawInterior); | |
| if (!this.elseBlock) { return; } | |
| // draw the else keyword | |
| var pt = this.elseBB.tl(); | |
| pt = pt.add(0, -2); // cheat up a bit. | |
| this.drawPaddedText("} ", pt, this.styles.semiColor); | |
| pt = pt.add(this.canvas.measureText("} ").width, 0); | |
| this.drawPaddedText(IF_ELSE_TEXT, pt, this.styles.keywordColor); | |
| pt = pt.add(this.canvas.measureText(IF_ELSE_TEXT).width, 0); | |
| this.drawPaddedText(" {", pt, this.styles.semiColor); | |
| // draw the else block. | |
| this.canvas.translate(this.elseBlockBB.tl()); | |
| this.elseBlock.draw(); | |
| }; | |
| var crender, crender_stmt, crender_stmts; | |
| var indentation, prec_stack = [ 0 ]; | |
| var assert = function(b, obj) { | |
| if (!b) { | |
| console.log('ASSERTION FAILURE', obj); | |
| Object.error("Assertion failure", obj); | |
| } | |
| }; | |
| // set the precedence to 'prec' when evaluating f | |
| var with_prec = function(prec, f, obj) { | |
| return function() { | |
| var result; | |
| prec_stack.push(prec); | |
| result = f.apply(obj || this, arguments); | |
| prec_stack.pop(); | |
| return result; | |
| }; | |
| }; | |
| // set the precedence, and parenthesize the result if appropriate. | |
| var with_prec_paren = function(prec, f, obj) { | |
| return function() { | |
| var prev_prec = prec_stack[prec_stack.length - 1]; | |
| var result = with_prec(prec, f).apply(obj || this, arguments); | |
| // XXX this might be better done dynamically based on the | |
| // precedences of the widgets? that would handle the drag & drop | |
| // case better (where we need to dynamically add parens) | |
| if (prev_prec > prec) { | |
| var p = Object.create(ParenWidget); | |
| p.operand = result; | |
| result = p; | |
| } | |
| return result; | |
| }; | |
| }; | |
| var str_escape = function(s) { | |
| if (s.toSource) { | |
| // abuse toSource() to properly quote a string value. | |
| return s.toSource().slice(12,-2); | |
| } | |
| // Erg, use hand-coded version | |
| var quotes = '"'; | |
| if (s.indexOf('"') !== -1 && s.indexOf("'") === -1) { | |
| quotes = "'"; | |
| } | |
| var table = {}; | |
| table["\n"] = "n"; | |
| table["\r"] = "r"; | |
| table["\f"] = "f"; | |
| table["\b"] = "b"; | |
| table["\t"] = "t"; | |
| table["\\"] = "\\"; | |
| table[quotes] = quotes; | |
| var result = "", i=0; | |
| while (i < s.length) { | |
| var c = s.charAt(i); | |
| if (table.hasOwnProperty(c)) { | |
| result += "\\" + table[c]; | |
| } else if (c < ' ' || c > '~') { | |
| // XXX allow some accented UTF-8 characters (printable ones)? | |
| var cc = c.charCodeAt(0).toString(16); | |
| while (cc.length < 4) { | |
| cc = "0" + cc; | |
| } | |
| result += "\\u" + cc; | |
| } else { | |
| result += c; | |
| } | |
| i += 1; | |
| } | |
| return quotes + result + quotes; | |
| }; | |
| var dispatch = {}; | |
| dispatch.name = function() { | |
| var nw = Object.create(NameWidget); | |
| nw.name = this.value; | |
| return nw; | |
| }; | |
| dispatch.literal = function() { | |
| var w; | |
| if (this.value === null) { return Object.create(NullWidget); } | |
| if (typeof(this.value)==='object') { | |
| w = Object.create(LabelledExpWidget); | |
| if (this.value.length === 0) { w.label = "Array"; return w; } | |
| w.label = "Object"; | |
| return w; | |
| } | |
| if (typeof(this.value)==='string') { | |
| w = Object.create(StringWidget); | |
| w.label = str_escape(this.value); | |
| return w; | |
| } | |
| if (typeof(this.value)==='boolean') { | |
| w = Object.create(BooleanWidget); | |
| w.label = this.value.toString(); | |
| return w; | |
| } | |
| w = Object.create(NumericWidget); | |
| w.label = this.value.toString(); | |
| return w; | |
| }; | |
| // UNARY ASTs | |
| dispatch.unary = function() { | |
| assert(dispatch.unary[this.value], this); | |
| return dispatch.unary[this.value].apply(this); | |
| }; | |
| var unary = function(op, prec, f) { | |
| dispatch.unary[op] = f || with_prec_paren(prec, function() { | |
| var pw = Object.create(PrefixWidget); | |
| pw.operator = this.value; | |
| pw.rightOperand = crender(this.first); | |
| return pw; | |
| }); | |
| }; | |
| unary('!', 70); | |
| unary('-', 70); | |
| unary('typeof', 70); | |
| unary('[', 90, with_prec_paren(90, function() { | |
| var w = Object.create(NewArrayWidget); | |
| this.first.forEach(function(c) { | |
| w.addChild(with_prec(0, crender)(c)); | |
| }); | |
| return w; | |
| })); | |
| unary('{', 90, with_prec_paren(90, function() { | |
| // new object creation | |
| var w = Object.create(NewObjectWidget); | |
| this.first.forEach(function(item) { | |
| var name = Object.create(NameLiteralWidget); | |
| name.setName(item.key); | |
| w.addChild(name, with_prec(0, crender)(item)); | |
| }); | |
| return w; | |
| })); | |
| // Binary ASTs | |
| dispatch.binary = function() { | |
| assert(dispatch.binary[this.value], this); | |
| return dispatch.binary[this.value].apply(this); | |
| }; | |
| var binary = function(op, prec, f, is_right) { | |
| // with_prec_paren will add parentheses if necessary | |
| dispatch.binary[op] = f || with_prec_paren(prec, function() { | |
| var iw = Object.create(InfixWidget); | |
| iw.operator = this.value; | |
| iw.leftOperand = crender(this.first); | |
| iw.rightOperand = with_prec(is_right ? (prec-1) : (prec+1), | |
| crender)(this.second); | |
| return iw; | |
| }); | |
| }; | |
| var binaryr = function(op, prec) { binary(op, prec, null, 1/*is right*/); }; | |
| binaryr('=', 10); | |
| binaryr('+=', 10); | |
| binaryr('-=', 10); | |
| binaryr('||', 30); | |
| binaryr('&&', 35); | |
| binaryr('===',40); | |
| binaryr('!==',40); | |
| binaryr('<', 45); | |
| binaryr('<=',45); | |
| binaryr('>', 45); | |
| binaryr('>=',45); | |
| binary('+', 50); | |
| binary('-', 50); | |
| binary('*', 60); | |
| binary('/', 60); | |
| binary("[", 80, with_prec_paren(80, function() { | |
| var iw = Object.create(WithSuffixWidget); | |
| iw.operator = '['; | |
| iw.closeOperator = ']'; | |
| iw.leftOperand = crender(this.first); | |
| iw.rightOperand = with_prec(0, crender)(this.second); | |
| return iw; | |
| })); | |
| binary("(", 75, with_prec_paren(80, function() { | |
| // simple method invocation (doesn't set 'this') | |
| var iw = Object.create(WithSuffixWidget); | |
| iw.operator = '('; | |
| iw.closeOperator = ')'; | |
| iw.leftOperand = crender(this.first); | |
| iw.rightOperand = Object.create(CommaListWidget); | |
| this.second.forEach(function(c) { | |
| iw.rightOperand.addChild(with_prec(0, crender)(c)); | |
| }); | |
| return iw; | |
| })); | |
| binary(".", 80, with_prec_paren(80, function() { | |
| assert(this.second.arity==='literal', this.second); | |
| var w = Object.create(DotNameWidget); | |
| w.leftOperand = crender(this.first); | |
| w.rightOperand = Object.create(NameLiteralWidget); | |
| w.rightOperand.setName(this.second.value); | |
| return w; | |
| })); | |
| // Ternary ASTs | |
| dispatch.ternary = function() { | |
| assert(dispatch.ternary[this.value], this); | |
| return dispatch.ternary[this.value].apply(this); | |
| }; | |
| var ternary = function(op, prec, f) { | |
| dispatch.ternary[op] = with_prec_paren(prec, f); | |
| }; | |
| ternary("(", 80, function() { | |
| // precedence is 80, same as . and '(') | |
| assert(this.second.arity==='literal', this.second); | |
| var w = Object.create(DotNameInvokeWidget); | |
| w.leftOperand = crender(this.first); | |
| w.rightOperand = Object.create(NameLiteralWidget); | |
| w.rightOperand.setName(this.second.value); | |
| w.args = Object.create(CommaListWidget); | |
| this.third.forEach(function(c) { | |
| w.args.addChild(with_prec(0, crender)(c)); | |
| }); | |
| return w; | |
| }); | |
| ternary("?", 20, function() { | |
| var w = Object.create(ConditionalWidget); | |
| w.testOperand = crender(this.first); | |
| w.trueOperand = crender(this.second); | |
| w.falseOperand = crender(this.third); | |
| return w; | |
| }); | |
| // Statements | |
| dispatch.statement = function() { | |
| assert(dispatch.statement[this.value], this); | |
| return dispatch.statement[this.value].apply(this); | |
| }; | |
| var stmt = function(value, f) { | |
| dispatch.statement[value] = f; | |
| }; | |
| stmt("block", function() { | |
| return crender_stmts(this.first); | |
| }); | |
| stmt("var", function() { | |
| assert(false, "Should be handled by block context"); | |
| return Object.create(YadaWidget); | |
| }); | |
| stmt("if", function() { | |
| var iw = Object.create(IfWidget); | |
| iw.testExpr = crender(this.first); | |
| iw.block = crender(this.second); | |
| if (this.third) { | |
| iw.elseBlock = crender(this.third); | |
| } | |
| return iw; | |
| }); | |
| stmt("return", function() { | |
| var rw, w; | |
| if (this.first) { | |
| w = crender(this.first); | |
| } else { | |
| // XXX not quite right | |
| w = Object.create(UndefinedWidget); | |
| } | |
| rw = Object.create(ReturnWidget); | |
| rw.expression = w; | |
| return rw; | |
| }); | |
| stmt("break", function() { return Object.create(BreakWidget); }); | |
| stmt("while", function() { | |
| var ww = Object.create(WhileWidget); | |
| ww.testExpr = crender(this.first); | |
| ww.block = crender(this.second); | |
| return ww; | |
| }); | |
| // Odd cases | |
| dispatch['this'] = function() { | |
| return Object.create(ThisWidget); // literal | |
| }; | |
| dispatch['function'] = with_prec(0, function() { | |
| var fw = Object.create(FunctionWidget); | |
| if (this.name) { | |
| fw.name = Object.create(NameWidget); | |
| fw.name.name = this.name; | |
| } | |
| fw.args = Object.create(CommaListWidget); | |
| fw.args.isName = true; | |
| this.first.forEach(function(c) { | |
| fw.args.addChild(crender(c)); | |
| }); | |
| fw.block = crender_stmts(this.second); | |
| return fw; | |
| }); | |
| // Helpers | |
| crender = function(tree) { | |
| // make 'this' the parse tree in the dispatched function. | |
| assert(dispatch[tree.arity], tree); | |
| return dispatch[tree.arity].apply(tree); | |
| }; | |
| crender_stmt = function(tree) { | |
| var w = crender(tree); | |
| if (tree.arity !== "statement") { | |
| var esw = Object.create(ExpStmtWidget); | |
| esw.expression = w; | |
| return esw; | |
| } | |
| return w; | |
| }; | |
| crender_stmts = function(tree_list) { | |
| // collect leading 'var' statements | |
| var bw = Object.create(BlockWidget); | |
| var i = 0; | |
| // collect variables (if any) | |
| while (i < tree_list.length) { | |
| if (!(tree_list[i].arity === 'statement' && | |
| tree_list[i].value === 'var')) { | |
| break; | |
| } | |
| bw.addVar(crender(tree_list[i].first)); | |
| i += 1; | |
| } | |
| while (i < tree_list.length) { | |
| bw.addChild(crender_stmt(tree_list[i])); | |
| i += 1; | |
| } | |
| return bw; | |
| }; | |
| return function (parse_tree) { | |
| // parse_tree should be an array of statements. | |
| indentation = 0; | |
| prec_stack = [ 0 ]; | |
| return crender_stmts(parse_tree); | |
| }; | |
| }; |