• Jump To … +
    asm-llvm.js bcompile.js binterp.js browsercanvas.js bytecode-table.js canvastest.js ccanvas.js crender-styles.js crender.js ctiles.js events.js eventtests.js extensions.js global-es5.js global.js html-escape.js jcompile.js json2.js nodemain.js parse.js render.js render2.js require.js stdlib.js str-escape.js tdop.js tests.js text.js tiles.js tokenize.js top-level.js ts.js write-lua-bytecode.js write-lua-ops.js write-php-bytecode.js write-php-ops.js write-rust-bytecode.js write-rust-ops.js
  • ¶

    crender.js

    Create widget tree for parsed Simplified JavaScript. Written in Simplified JavaScript.

    C. Scott Ananian 2011-05-13

    define(["text!crender.js", "str-escape", "gfx/Point", "gfx/Color"], function make_crender(crender_source, str_escape, Point, Color) {
  • ¶

    stub for i18n

        var _ = function(txt) { return txt; };
  • ¶

    basic graphics datatypes

        var pt = function(x, y) { return Point.New(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(Color.red);
                (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);
            }, 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())));
            }, 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;
                }
            }, 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());
                }
            }, 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();
                    });
                }
            }, 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 } );
            }, 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);
                }
            }, 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));
                }
            }, 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);
                }
            }, 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();
            });
    
            children.forEach(drawChild, this);
        });
  • ¶

    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);
                console.assert(false);
            }
        };
  • ¶

    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 dispatch = {};
        dispatch.name = function() {
            var nw = Object.create(NameWidget);
            nw.name = this.value;
            return nw;
        };
        dispatch.literal = function() {
            var w;
            if (this.value === undefined) { return Object.create(UndefinedWidget); }
            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('*=', 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;
        };
    
        var c = function (parse_tree) {
  • ¶

    parse_tree should be an array of statements.

            indentation = 0;
            prec_stack = [ 0 ];
            return crender_stmts(parse_tree);
        };
        c.__module_name__ = "crender";
        c.__module_init__ = make_crender;
        c.__module_deps__ = ['str-escape'];
        c.__module_source__ = crender_source;
        return c;
    });