"&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/
+
+
+
+
+
+
+
+
+
+
+
+
+
+*/
+
+
+/* Coding gotchas:
+
+- NEVER use naked $(__) or d3.select(__) statements to select DOM elements.
+
+ ALWAYS use myViz.domRoot or myViz.domRootD3 for jQuery and D3, respectively.
+
+ Otherwise things will break in weird ways when you have more than one visualization
+ embedded within a webpage, due to multiple matches in the global namespace.
+
+
+- always use generateID to generate unique CSS IDs, or else things will break
+ when multiple ExecutionVisualizer instances are displayed on a webpage
+
+*/
+
+
+var SVG_ARROW_POLYGON = '0,3 12,3 12,0 18,5 12,10 12,7 0,7';
+var SVG_ARROW_HEIGHT = 10; // must match height of SVG_ARROW_POLYGON
+
+var curVisualizerID = 1; // global to uniquely identify each ExecutionVisualizer instance
+
+// domRootID is the string ID of the root element where to render this instance
+// dat is data returned by the Python Tutor backend consisting of two fields:
+// code - string of executed code
+// trace - a full execution trace
+//
+// params is an object containing optional parameters, such as:
+// jumpToEnd - if non-null, jump to the very end of execution
+// startingInstruction - the (zero-indexed) execution point to display upon rendering
+// hideOutput - hide "Program output" display
+// codeDivHeight - maximum height of #pyCodeOutputDiv (in integer pixels)
+// codeDivWidth - maximum width of #pyCodeOutputDiv (in integer pixels)
+// editCodeBaseURL - the base URL to visit when the user clicks 'Edit code' (if null, then 'Edit code' link hidden)
+// allowEditAnnotations - allow user to edit per-step annotations (default: false)
+// embeddedMode - shortcut for hideOutput=true, allowEditAnnotations=false,
+// codeDivWidth=this.DEFAULT_EMBEDDED_CODE_DIV_WIDTH,
+// codeDivHeight=this.DEFAULT_EMBEDDED_CODE_DIV_HEIGHT
+// disableHeapNesting - if true, then render all heap objects at the top level (i.e., no nested objects)
+// drawParentPointers - if true, then draw environment diagram parent pointers for all frames
+// WARNING: there are hard-to-debug MEMORY LEAKS associated with activating this option
+// textualMemoryLabels - render references using textual memory labels rather than as jsPlumb arrows.
+// this is good for slow browsers or when used with disableHeapNesting
+// to prevent "arrow overload"
+// showOnlyOutputs - show only program outputs and NOT internal data structures
+// updateOutputCallback - function to call (with 'this' as parameter)
+// whenever this.updateOutput() is called
+// (BEFORE rendering the output display)
+// heightChangeCallback - function to call (with 'this' as parameter)
+// whenever the HEIGHT of #dataViz changes
+// verticalStack - if true, then stack code display ON TOP of visualization
+// (else place side-by-side)
+// executeCodeWithRawInputFunc - function to call when you want to re-execute the given program
+// with some new user input (somewhat hacky!)
+// highlightLines - highlight current and previously executed lines (default: false)
+// arrowLines - draw arrows pointing to current and previously executed lines (default: true)
+// compactFuncLabels - render functions with a 'func' prefix and no type label
+// pyCrazyMode - run with Py2crazy, which provides expression-level
+// granularity instead of line-level granularity (HIGHLY EXPERIMENTAL!)
+function ExecutionVisualizer(domRootID, dat, params) {
+ serverReply = dat;
+ this.curInputCode = dat.code.rtrim(); // kill trailing spaces
+ this.curTrace = dat.trace;
+
+ this.DEFAULT_EMBEDDED_CODE_DIV_WIDTH = 350;
+ this.DEFAULT_EMBEDDED_CODE_DIV_HEIGHT = 400;
+
+ // optional filtering to remove redundancy ...
+ // ok, we're gonna filter out all trace entries of 'call' events,
+ // because each one contains IDENTICAL state information as the
+ // 'step_line' entry immediately following it. this filtering allows the
+ // visualization to not show as much redundancy.
+
+ //this.curTrace = this.curTrace.filter(function(e) {return e.event != 'call';});
+
+ // if the final entry is raw_input or mouse_input, then trim it from the trace and
+ // set a flag to prompt for user input when execution advances to the
+ // end of the trace
+ if (this.curTrace.length > 0) {
+ var lastEntry = this.curTrace[this.curTrace.length - 1];
+ if (lastEntry.event == 'raw_input') {
+ this.promptForUserInput = true;
+ this.userInputPromptStr = lastEntry.prompt;
+ this.curTrace.pop() // kill last entry so that it doesn't get displayed
+ }
+ else if (lastEntry.event == 'mouse_input') {
+ this.promptForMouseInput = true;
+ this.userInputPromptStr = lastEntry.prompt;
+ this.curTrace.pop() // kill last entry so that it doesn't get displayed
+ }
+ }
+
+ this.hasStdout = ((this.curTrace.length > 0)
+ && this.curTrace[this.curTrace.length-1]
+ && this.curTrace[this.curTrace.length-1].stdout);
+
+ if (this.hasStdout) {
+ this.stdoutLines = this.curTrace[this.curTrace.length-1].stdout.split("\n").length;
+ }
+ else
+ this.stdoutLines = -1;
+
+ this.curInstr = 0;
+
+ this.params = params;
+ if (!this.params) {
+ this.params = {}; // make it an empty object by default
+ }
+
+ var arrowLinesDef = (this.params.arrowLines !== undefined);
+ var highlightLinesDef = (this.params.highlightLines !== undefined);
+
+ if (!arrowLinesDef && !highlightLinesDef) {
+ // neither is set
+ this.params.highlightLines = false;
+ this.params.arrowLines = true;
+ }
+ else if (arrowLinesDef && highlightLinesDef) {
+ // both are set, so just use their set values
+ }
+ else if (arrowLinesDef) {
+ // only arrowLines set
+ this.params.highlightLines = !(this.params.arrowLines);
+ }
+ else {
+ // only highlightLines set
+ this.params.arrowLines = !(this.params.highlightLines);
+ }
+
+ if (this.params.lang == undefined)
+ this.params.lang = "python";
+
+ this.compactFuncLabels = this.params.compactFuncLabels;
+
+ // audible!
+ if (this.params.pyCrazyMode) {
+ this.params.arrowLines = this.params.highlightLines = false;
+ }
+
+ // needs to be unique!
+ this.visualizerID = curVisualizerID;
+ curVisualizerID++;
+
+ this.leftGutterSvgInitialized = false;
+ this.arrowOffsetY = undefined;
+ this.codeRowHeight = undefined;
+
+ // avoid 'undefined' state
+ this.disableHeapNesting = (this.params.disableHeapNesting == true);
+ this.drawParentPointers = (this.params.drawParentPointers == true);
+ this.textualMemoryLabels = (this.params.textualMemoryLabels == true);
+ this.showOnlyOutputs = (this.params.showOnlyOutputs == true);
+
+ this.executeCodeWithRawInputFunc = this.params.executeCodeWithRawInputFunc;
+
+ // cool, we can create a separate jsPlumb instance for each visualization:
+ this.jsPlumbInstance = jsPlumb.getInstance({
+ Endpoint: ["Dot", {radius:3}],
+ EndpointStyles: [{fillStyle: connectorBaseColor}, {fillstyle: null} /* make right endpoint invisible */],
+ Anchors: ["RightMiddle", "LeftMiddle"],
+ PaintStyle: {lineWidth:1, strokeStyle: connectorBaseColor},
+
+ // bezier curve style:
+ //Connector: [ "Bezier", { curviness:15 }], /* too much 'curviness' causes lines to run together */
+ //Overlays: [[ "Arrow", { length: 14, width:10, foldback:0.55, location:0.35 }]],
+
+ // state machine curve style:
+ Connector: [ "StateMachine" ],
+ Overlays: [[ "Arrow", { length: 10, width:7, foldback:0.55, location:1 }]],
+ EndpointHoverStyles: [{fillStyle: connectorHighlightColor}, {fillstyle: null} /* make right endpoint invisible */],
+ HoverPaintStyle: {lineWidth: 1, strokeStyle: connectorHighlightColor},
+ });
+
+
+ // true iff trace ended prematurely since maximum instruction limit has
+ // been reached
+ var instrLimitReached = false;
+
+
+ // the root elements for jQuery and D3 selections, respectively.
+ // ALWAYS use these and never use naked $(__) or d3.select(__)
+ this.domRoot = $('#' + domRootID);
+ this.domRoot.data("vis",this); // bnm store a reference to this as div data for use later.
+ this.domRootD3 = d3.select('#' + domRootID);
+
+ // stick a new div.ExecutionVisualizer within domRoot and make that
+ // the new domRoot:
+ this.domRoot.html('');
+
+ this.domRoot = this.domRoot.find('div.ExecutionVisualizer');
+ this.domRootD3 = this.domRootD3.select('div.ExecutionVisualizer');
+
+
+ // initialize in renderPyCodeOutput()
+ this.codeOutputLines = null;
+ this.breakpoints = null; // set of execution points to set as breakpoints
+ this.sortedBreakpointsList = null; // sorted and synced with breakpointLines
+ this.hoverBreakpoints = null; // set of breakpoints because we're HOVERING over a given line
+
+ this.enableTransitions = false; // EXPERIMENTAL - enable transition effects
+
+
+ this.hasRendered = false;
+
+ this.render(); // go for it!
+
+}
+
+
+// create a unique ID, which is often necessary so that jsPlumb doesn't get confused
+// due to multiple ExecutionVisualizer instances being displayed simultaneously
+ExecutionVisualizer.prototype.generateID = function(original_id) {
+ // (it's safer to start names with a letter rather than a number)
+ return 'v' + this.visualizerID + '__' + original_id;
+}
+
+
+ExecutionVisualizer.prototype.render = function() {
+ if (this.hasRendered) {
+ alert('ERROR: You should only call render() ONCE on an ExecutionVisualizer object.');
+ return;
+ }
+
+
+ var myViz = this; // to prevent confusion of 'this' inside of nested functions
+
+ var codeDisplayHTML =
+ '
');
+ }
+
+ if (this.showOnlyOutputs) {
+ myViz.domRoot.find('#dataViz').hide();
+ this.domRoot.find('#vizLayoutTdSecond').append(outputsHTML);
+
+ if (this.params.verticalStack) {
+ this.domRoot.find('#vizLayoutTdSecond').css('padding-top', '25px');
+ }
+ else {
+ this.domRoot.find('#vizLayoutTdSecond').css('padding-left', '25px');
+ }
+ }
+ else {
+ this.domRoot.find('#vizLayoutTdFirst').append(outputsHTML);
+ }
+
+ if (this.params.arrowLines) {
+ this.domRoot.find('#legendDiv')
+ .append(' line that has just executed')
+ .append('
next line to execute
');
+
+ myViz.domRootD3.select('svg#prevLegendArrowSVG')
+ .append('polygon')
+ .attr('points', SVG_ARROW_POLYGON)
+ .attr('fill', lightArrowColor);
+
+ myViz.domRootD3.select('svg#curLegendArrowSVG')
+ .append('polygon')
+ .attr('points', SVG_ARROW_POLYGON)
+ .attr('fill', darkArrowColor);
+ }
+ else if (this.params.highlightLines) {
+ myViz.domRoot.find('#legendDiv')
+ .append('line that has just executed ')
+ .append('next line to execute')
+ }
+ else if (this.params.pyCrazyMode) {
+ myViz.domRoot.find('#legendDiv')
+ .append('Py2crazy mode!')
+ .append(' Stepping through (roughly) each executed expression. Color codes:')
+ .append('expression that just executed ')
+ .append('next expression to execute');
+ }
+
+
+ if (this.params.editCodeBaseURL) {
+ var urlStr = $.param.fragment(this.params.editCodeBaseURL,
+ {code: this.curInputCode},
+ 2);
+ this.domRoot.find('#editBtn').attr('href', urlStr);
+ }
+ else {
+ this.domRoot.find('#editCodeLinkDiv').hide(); // just hide for simplicity!
+ this.domRoot.find('#editBtn').attr('href', "#");
+ this.domRoot.find('#editBtn').click(function(){return false;}); // DISABLE the link!
+ }
+
+ if (this.params.allowEditAnnotations !== undefined) {
+ this.allowEditAnnotations = this.params.allowEditAnnotations;
+ }
+ else {
+ this.allowEditAnnotations = false;
+ }
+
+ if (this.params.pyCrazyMode !== undefined) {
+ this.pyCrazyMode = this.params.pyCrazyMode;
+ }
+ else {
+ this.pyCrazyMode = false;
+ }
+
+ this.domRoot.find('#stepAnnotationEditor').hide();
+
+ if (this.params.embeddedMode) {
+ this.params.hideOutput = true; // put this before hideOutput handler
+
+ // don't override if they've already been set!
+ if (this.params.codeDivWidth === undefined) {
+ this.params.codeDivWidth = this.DEFAULT_EMBEDDED_CODE_DIV_WIDTH;
+ }
+
+ if (this.params.codeDivHeight === undefined) {
+ this.params.codeDivHeight = this.DEFAULT_EMBEDDED_CODE_DIV_HEIGHT;
+ }
+
+ this.allowEditAnnotations = false;
+ }
+
+ myViz.editAnnotationMode = false;
+
+ if (this.allowEditAnnotations) {
+ var ab = this.domRoot.find('#annotateBtn');
+
+ ab.click(function() {
+ if (myViz.editAnnotationMode) {
+ myViz.enterViewAnnotationsMode();
+
+ myViz.domRoot.find("#jmpFirstInstr,#jmpLastInstr,#jmpStepBack,#jmpStepFwd,#executionSlider,#editCodeLinkDiv,#stepAnnotationViewer").show();
+ myViz.domRoot.find('#stepAnnotationEditor').hide();
+ ab.html('Annotate this step');
+ }
+ else {
+ myViz.enterEditAnnotationsMode();
+
+ myViz.domRoot.find("#jmpFirstInstr,#jmpLastInstr,#jmpStepBack,#jmpStepFwd,#executionSlider,#editCodeLinkDiv,#stepAnnotationViewer").hide();
+ myViz.domRoot.find('#stepAnnotationEditor').show();
+ ab.html('Done annotating');
+ }
+ });
+ }
+ else {
+ this.domRoot.find('#annotateBtn').hide();
+ }
+
+
+ // not enough room for these extra buttons ...
+ if (this.params.codeDivWidth &&
+ this.params.codeDivWidth < 470) {
+ this.domRoot.find('#jmpFirstInstr').hide();
+ this.domRoot.find('#jmpLastInstr').hide();
+ }
+
+ // enable left-right draggable pane resizer (originally from David Pritchard)
+ this.domRoot.find('#codeDisplayDiv').resizable({
+ handles: "e",
+ minWidth: 100, //otherwise looks really goofy
+ resize: function(event, ui){ // old name: syncStdoutWidth, now not appropriate
+ $("#codeDisplayDiv").css("height", "auto"); // redetermine height if necessary
+ if (myViz.params.updateOutputCallback) // report size change
+ myViz.params.updateOutputCallback(this);
+ }});
+
+ if (this.params.codeDivWidth) {
+ // set width once
+ this.domRoot.find('#codeDisplayDiv').width(
+ this.params.codeDivWidth);
+ // it will propagate to the slider
+ }
+
+ if (this.params.codeDivHeight) {
+ this.domRoot.find('#pyCodeOutputDiv')
+ .css('max-height', this.params.codeDivHeight + 'px');
+ }
+
+ var globalsLabel = "Global frame";
+ if (myViz.params.lang == 'java') globalsLabel = "Static fields";
+
+ // create a persistent globals frame
+ // (note that we need to keep #globals_area separate from #stack for d3 to work its magic)
+ this.domRoot.find("#globals_area").append('
'+globalsLabel+'
');
+
+
+ if (this.params.hideOutput) {
+ this.domRoot.find('#progOutputs').hide();
+ }
+
+ this.domRoot.find("#jmpFirstInstr").click(function() {
+ myViz.curInstr = 0;
+ myViz.updateOutput();
+ });
+
+ this.domRoot.find("#jmpLastInstr").click(function() {
+ myViz.curInstr = myViz.curTrace.length - 1;
+ myViz.updateOutput();
+ });
+
+ this.domRoot.find("#jmpStepBack").click(function() {
+ myViz.stepBack();
+ });
+
+ this.domRoot.find("#jmpStepFwd").click(function() {
+ myViz.stepForward();
+ });
+
+ // disable controls initially ...
+ this.domRoot.find("#vcrControls #jmpFirstInstr").attr("disabled", true);
+ this.domRoot.find("#vcrControls #jmpStepBack").attr("disabled", true);
+ this.domRoot.find("#vcrControls #jmpStepFwd").attr("disabled", true);
+ this.domRoot.find("#vcrControls #jmpLastInstr").attr("disabled", true);
+
+
+
+ // must postprocess curTrace prior to running precomputeCurTraceLayouts() ...
+ var lastEntry = this.curTrace[this.curTrace.length - 1];
+
+ this.instrLimitReached = (lastEntry.event == 'instruction_limit_reached');
+
+ if (this.instrLimitReached) {
+ this.curTrace.pop() // kill last entry
+ var warningMsg = lastEntry.exception_msg;
+ myViz.domRoot.find("#errorOutput").html(htmlspecialchars(warningMsg));
+ myViz.domRoot.find("#errorOutput").show();
+ }
+
+ // set up slider after postprocessing curTrace
+
+ var sliderDiv = this.domRoot.find('#executionSlider');
+ sliderDiv.slider({min: 0, max: this.curTrace.length - 1, step: 1});
+ //disable keyboard actions on the slider itself (to prevent double-firing of events)
+ sliderDiv.find(".ui-slider-handle").unbind('keydown');
+ // make skinnier and taller
+ sliderDiv.find(".ui-slider-handle").css('width', '0.8em');
+ sliderDiv.find(".ui-slider-handle").css('height', '1.4em');
+ this.domRoot.find(".ui-widget-content").css('font-size', '0.9em');
+
+ this.domRoot.find('#executionSlider').bind('slide', function(evt, ui) {
+ // this is SUPER subtle. if this value was changed programmatically,
+ // then evt.originalEvent will be undefined. however, if this value
+ // was changed by a user-initiated event, then this code should be
+ // executed ...
+ if (evt.originalEvent) {
+ myViz.curInstr = ui.value;
+ myViz.updateOutput();
+ }
+ });
+
+
+ if (this.params.startingInstruction) {
+ assert(0 <= this.params.startingInstruction &&
+ this.params.startingInstruction < this.curTrace.length);
+ this.curInstr = this.params.startingInstruction;
+ }
+
+ if (this.params.jumpToEnd) {
+ this.curInstr = this.curTrace.length - 1;
+ }
+
+
+ this.precomputeCurTraceLayouts();
+
+ this.renderPyCodeOutput();
+
+ this.updateOutput();
+
+ this.hasRendered = true;
+}
+
+
+ExecutionVisualizer.prototype.showVizHeaderViewMode = function() {
+ var titleVal = this.domRoot.find('#vizTitleEditor').val().trim();
+ var descVal = this.domRoot.find('#vizDescriptionEditor').val().trim();
+
+ this.domRoot.find('#vizTitleEditor,#vizDescriptionEditor').hide();
+
+ if (!titleVal && !descVal) {
+ this.domRoot.find('#vizHeader').hide();
+ }
+ else {
+ this.domRoot.find('#vizHeader,#vizTitleViewer,#vizDescriptionViewer').show();
+ if (titleVal) {
+ this.domRoot.find('#vizTitleViewer').html(htmlsanitize(titleVal)); // help prevent HTML/JS injection attacks
+ }
+
+ if (descVal) {
+ this.domRoot.find('#vizDescriptionViewer').html(htmlsanitize(descVal)); // help prevent HTML/JS injection attacks
+ }
+ }
+}
+
+ExecutionVisualizer.prototype.showVizHeaderEditMode = function() {
+ this.domRoot.find('#vizHeader').show();
+
+ this.domRoot.find('#vizTitleViewer,#vizDescriptionViewer').hide();
+ this.domRoot.find('#vizTitleEditor,#vizDescriptionEditor').show();
+}
+
+
+ExecutionVisualizer.prototype.destroyAllAnnotationBubbles = function() {
+ var myViz = this;
+
+ // hopefully destroys all old bubbles and reclaims their memory
+ if (myViz.allAnnotationBubbles) {
+ $.each(myViz.allAnnotationBubbles, function(i, e) {
+ e.destroyQTip();
+ });
+ }
+
+ // remove this handler as well!
+ this.domRoot.find('#pyCodeOutputDiv').unbind('scroll');
+
+ myViz.allAnnotationBubbles = null;
+}
+
+ExecutionVisualizer.prototype.initStepAnnotation = function() {
+ var curEntry = this.curTrace[this.curInstr];
+ if (curEntry.stepAnnotation) {
+ this.domRoot.find("#stepAnnotationViewer").html(htmlsanitize(curEntry.stepAnnotation)); // help prevent HTML/JS injection attacks
+ this.domRoot.find("#stepAnnotationEditor").val(curEntry.stepAnnotation);
+ }
+ else {
+ this.domRoot.find("#stepAnnotationViewer").html('');
+ this.domRoot.find("#stepAnnotationEditor").val('');
+ }
+}
+
+ExecutionVisualizer.prototype.initAllAnnotationBubbles = function() {
+ var myViz = this;
+
+ // TODO: check for memory leaks
+ //console.log('initAllAnnotationBubbles');
+
+ myViz.destroyAllAnnotationBubbles();
+
+ var codelineIDs = [];
+ $.each(this.domRoot.find('#pyCodeOutput .cod'), function(i, e) {
+ codelineIDs.push($(e).attr('id'));
+ });
+
+ var heapObjectIDs = [];
+ $.each(this.domRoot.find('.heapObject'), function(i, e) {
+ heapObjectIDs.push($(e).attr('id'));
+ });
+
+ var variableIDs = [];
+ $.each(this.domRoot.find('.variableTr'), function(i, e) {
+ variableIDs.push($(e).attr('id'));
+ });
+
+ var frameIDs = [];
+ $.each(this.domRoot.find('.stackFrame'), function(i, e) {
+ frameIDs.push($(e).attr('id'));
+ });
+
+ myViz.allAnnotationBubbles = [];
+
+ $.each(codelineIDs, function(i,e) {myViz.allAnnotationBubbles.push(new AnnotationBubble(myViz, 'codeline', e));});
+ $.each(heapObjectIDs, function(i,e) {myViz.allAnnotationBubbles.push(new AnnotationBubble(myViz, 'object', e));});
+ $.each(variableIDs, function(i,e) {myViz.allAnnotationBubbles.push(new AnnotationBubble(myViz, 'variable', e));});
+ $.each(frameIDs, function(i,e) {myViz.allAnnotationBubbles.push(new AnnotationBubble(myViz, 'frame', e));});
+
+
+ this.domRoot.find('#pyCodeOutputDiv').scroll(function() {
+ $.each(myViz.allAnnotationBubbles, function(i, e) {
+ if (e.type == 'codeline') {
+ e.redrawCodelineBubble();
+ }
+ });
+ });
+
+ //console.log('initAllAnnotationBubbles', myViz.allAnnotationBubbles.length);
+}
+
+
+ExecutionVisualizer.prototype.enterViewAnnotationsMode = function() {
+ this.editAnnotationMode = false;
+ var curEntry = this.curTrace[this.curInstr];
+
+ // TODO: check for memory leaks!!!
+ var myViz = this;
+
+ if (!myViz.allAnnotationBubbles) {
+ if (curEntry.bubbleAnnotations) {
+ // If there is an existing annotations object, then initiate all annotations bubbles
+ // and display them in 'View' mode
+ myViz.initAllAnnotationBubbles();
+
+ $.each(myViz.allAnnotationBubbles, function(i, e) {
+ var txt = curEntry.bubbleAnnotations[e.domID];
+ if (txt) {
+ e.preseedText(txt);
+ }
+ });
+ }
+ }
+
+
+ if (myViz.allAnnotationBubbles) {
+ var curAnnotations = {};
+
+ $.each(myViz.allAnnotationBubbles, function(i, e) {
+ e.enterViewMode();
+
+ if (e.text) {
+ curAnnotations[e.domID] = e.text;
+ }
+ });
+
+ // Save annotations directly into the current trace entry as an 'annotations' object
+ // directly mapping domID -> text.
+ //
+ // NB: This scheme can break if the functions for generating domIDs are altered.
+ curEntry.bubbleAnnotations = curAnnotations;
+ }
+
+ var stepAnnotationEditorVal = myViz.domRoot.find("#stepAnnotationEditor").val().trim();
+ if (stepAnnotationEditorVal) {
+ curEntry.stepAnnotation = stepAnnotationEditorVal;
+ }
+ else {
+ delete curEntry.stepAnnotation; // go as far as to DELETE this field entirely
+ }
+
+ myViz.initStepAnnotation();
+
+ myViz.showVizHeaderViewMode();
+
+ // redraw all connectors and bubbles in new vertical position ..
+ myViz.redrawConnectors();
+ myViz.redrawAllAnnotationBubbles();
+}
+
+ExecutionVisualizer.prototype.enterEditAnnotationsMode = function() {
+ this.editAnnotationMode = true;
+
+ // TODO: check for memory leaks!!!
+ var myViz = this;
+
+ var curEntry = this.curTrace[this.curInstr];
+
+ if (!myViz.allAnnotationBubbles) {
+ myViz.initAllAnnotationBubbles();
+ }
+
+ $.each(myViz.allAnnotationBubbles, function(i, e) {
+ e.enterEditMode();
+ });
+
+
+ if (curEntry.stepAnnotation) {
+ myViz.domRoot.find("#stepAnnotationEditor").val(curEntry.stepAnnotation);
+ }
+ else {
+ myViz.domRoot.find("#stepAnnotationEditor").val('');
+ }
+
+
+ myViz.showVizHeaderEditMode();
+
+ // redraw all connectors and bubbles in new vertical position ..
+ myViz.redrawConnectors();
+ myViz.redrawAllAnnotationBubbles();
+}
+
+
+ExecutionVisualizer.prototype.redrawAllAnnotationBubbles = function() {
+ if (this.allAnnotationBubbles) {
+ $.each(this.allAnnotationBubbles, function(i, e) {
+ e.redrawBubble();
+ });
+ }
+}
+
+
+// find the previous/next breakpoint to c or return -1 if it doesn't exist
+ExecutionVisualizer.prototype.findPrevBreakpoint = function() {
+ var myViz = this;
+ var c = myViz.curInstr;
+
+ if (myViz.sortedBreakpointsList.length == 0) {
+ return -1;
+ }
+ else {
+ for (var i = 1; i < myViz.sortedBreakpointsList.length; i++) {
+ var prev = myViz.sortedBreakpointsList[i-1];
+ var cur = myViz.sortedBreakpointsList[i];
+ if (c <= prev)
+ return -1;
+ if (cur >= c)
+ return prev;
+ }
+
+ // final edge case:
+ var lastElt = myViz.sortedBreakpointsList[myViz.sortedBreakpointsList.length - 1];
+ return (lastElt < c) ? lastElt : -1;
+ }
+}
+
+ExecutionVisualizer.prototype.findNextBreakpoint = function() {
+ var myViz = this;
+ var c = myViz.curInstr;
+
+ if (myViz.sortedBreakpointsList.length == 0) {
+ return -1;
+ }
+ // usability hack: if you're currently on a breakpoint, then
+ // single-step forward to the next execution point, NOT the next
+ // breakpoint. it's often useful to see what happens when the line
+ // at a breakpoint executes.
+ else if ($.inArray(c, myViz.sortedBreakpointsList) >= 0) {
+ return c + 1;
+ }
+ else {
+ for (var i = 0; i < myViz.sortedBreakpointsList.length - 1; i++) {
+ var cur = myViz.sortedBreakpointsList[i];
+ var next = myViz.sortedBreakpointsList[i+1];
+ if (c < cur)
+ return cur;
+ if (cur <= c && c < next) // subtle
+ return next;
+ }
+
+ // final edge case:
+ var lastElt = myViz.sortedBreakpointsList[myViz.sortedBreakpointsList.length - 1];
+ return (lastElt > c) ? lastElt : -1;
+ }
+}
+
+
+// returns true if action successfully taken
+ExecutionVisualizer.prototype.stepForward = function() {
+ var myViz = this;
+
+ if (myViz.editAnnotationMode) {
+ return;
+ }
+
+ if (myViz.curInstr < myViz.curTrace.length - 1) {
+ // if there is a next breakpoint, then jump to it ...
+ if (myViz.sortedBreakpointsList.length > 0) {
+ var nextBreakpoint = myViz.findNextBreakpoint();
+ if (nextBreakpoint != -1)
+ myViz.curInstr = nextBreakpoint;
+ else
+ myViz.curInstr += 1; // prevent "getting stuck" on a solitary breakpoint
+ }
+ else {
+ myViz.curInstr += 1;
+ }
+ myViz.updateOutput(true);
+ return true;
+ }
+
+ return false;
+}
+
+// returns true if action successfully taken
+ExecutionVisualizer.prototype.stepBack = function() {
+ var myViz = this;
+
+ if (myViz.editAnnotationMode) {
+ return;
+ }
+
+ if (myViz.curInstr > 0) {
+ // if there is a prev breakpoint, then jump to it ...
+ if (myViz.sortedBreakpointsList.length > 0) {
+ var prevBreakpoint = myViz.findPrevBreakpoint();
+ if (prevBreakpoint != -1)
+ myViz.curInstr = prevBreakpoint;
+ else
+ myViz.curInstr -= 1; // prevent "getting stuck" on a solitary breakpoint
+ }
+ else {
+ myViz.curInstr -= 1;
+ }
+ myViz.updateOutput();
+ return true;
+ }
+
+ return false;
+}
+
+
+ExecutionVisualizer.prototype.renderPyCodeOutput = function() {
+ var myViz = this; // to prevent confusion of 'this' inside of nested functions
+
+
+ // initialize!
+ this.breakpoints = d3.map();
+ this.sortedBreakpointsList = [];
+ this.hoverBreakpoints = d3.map();
+
+ // an array of objects with the following fields:
+ // 'text' - the text of the line of code
+ // 'lineNumber' - one-indexed (always the array index + 1)
+ // 'executionPoints' - an ordered array of zero-indexed execution points where this line was executed
+ // 'breakpointHere' - has a breakpoint been set here?
+ this.codeOutputLines = [];
+
+
+ function renderSliderBreakpoints() {
+ myViz.domRoot.find("#executionSliderFooter").empty();
+
+ // I originally didn't want to delete and re-create this overlay every time,
+ // but if I don't do so, there are weird flickering artifacts with clearing
+ // the SVG container; so it's best to just delete and re-create the container each time
+ var sliderOverlay = myViz.domRootD3.select('#executionSliderFooter')
+ .append('svg')
+ .attr('id', 'sliderOverlay')
+ .attr('width', myViz.domRoot.find('#executionSlider').width())
+ .attr('height', 12);
+
+ var xrange = d3.scale.linear()
+ .domain([0, myViz.curTrace.length - 1])
+ .range([0, myViz.domRoot.find('#executionSlider').width()]);
+
+ sliderOverlay.selectAll('rect')
+ .data(myViz.sortedBreakpointsList)
+ .enter().append('rect')
+ .attr('x', function(d, i) {
+ // make the edge cases look decent
+ if (d == 0) {
+ return 0;
+ }
+ else {
+ return xrange(d) - 3;
+ }
+ })
+ .attr('y', 0)
+ .attr('width', 2)
+ .attr('height', 12)
+ .style('fill', function(d) {
+ if (myViz.hoverBreakpoints.has(d)) {
+ return hoverBreakpointColor;
+ }
+ else {
+ return breakpointColor;
+ }
+ });
+ }
+
+ function _getSortedBreakpointsList() {
+ var ret = [];
+ myViz.breakpoints.forEach(function(k, v) {
+ ret.push(Number(k)); // these should be NUMBERS, not strings
+ });
+ ret.sort(function(x,y){return x-y}); // WTF, javascript sort is lexicographic by default!
+ return ret;
+ }
+
+ function addToBreakpoints(executionPoints) {
+ $.each(executionPoints, function(i, ep) {
+ myViz.breakpoints.set(ep, 1);
+ });
+ myViz.sortedBreakpointsList = _getSortedBreakpointsList();
+ }
+
+ function removeFromBreakpoints(executionPoints) {
+ $.each(executionPoints, function(i, ep) {
+ myViz.breakpoints.remove(ep);
+ });
+ myViz.sortedBreakpointsList = _getSortedBreakpointsList();
+ }
+
+
+ function setHoverBreakpoint(t) {
+ var exePts = d3.select(t).datum().executionPoints;
+
+ // don't do anything if exePts is empty
+ // (i.e., this line was never executed)
+ if (!exePts || exePts.length == 0) {
+ return;
+ }
+
+ myViz.hoverBreakpoints = d3.map();
+ $.each(exePts, function(i, ep) {
+ // don't add redundant entries
+ if (!myViz.breakpoints.has(ep)) {
+ myViz.hoverBreakpoints.set(ep, 1);
+ }
+ });
+
+ addToBreakpoints(exePts);
+ renderSliderBreakpoints();
+ }
+
+
+ function setBreakpoint(t) {
+ var exePts = d3.select(t).datum().executionPoints;
+
+ // don't do anything if exePts is empty
+ // (i.e., this line was never executed)
+ if (!exePts || exePts.length == 0) {
+ return;
+ }
+
+ addToBreakpoints(exePts);
+
+ // remove from hoverBreakpoints so that slider display immediately changes color
+ $.each(exePts, function(i, ep) {
+ myViz.hoverBreakpoints.remove(ep);
+ });
+
+ d3.select(t.parentNode).select('td.lineNo').style('color', breakpointColor);
+ d3.select(t.parentNode).select('td.lineNo').style('font-weight', 'bold');
+
+ renderSliderBreakpoints();
+ }
+
+ function unsetBreakpoint(t) {
+ var exePts = d3.select(t).datum().executionPoints;
+
+ // don't do anything if exePts is empty
+ // (i.e., this line was never executed)
+ if (!exePts || exePts.length == 0) {
+ return;
+ }
+
+ removeFromBreakpoints(exePts);
+
+ var lineNo = d3.select(t).datum().lineNumber;
+
+ renderSliderBreakpoints();
+ }
+
+ var lines = this.curInputCode.split('\n');
+
+ for (var i = 0; i < lines.length; i++) {
+ var cod = lines[i];
+
+ var n = {};
+ n.text = cod;
+ n.lineNumber = i + 1;
+ n.executionPoints = [];
+ n.breakpointHere = false;
+
+ $.each(this.curTrace, function(j, elt) {
+ if (elt.line == n.lineNumber) {
+ n.executionPoints.push(j);
+ }
+ });
+
+
+ // if there is a comment containing 'breakpoint' and this line was actually executed,
+ // then set a breakpoint on this line
+ var breakpointInComment = false;
+ var toks = cod.split('#');
+ for (var j = 1 /* start at index 1, not 0 */; j < toks.length; j++) {
+ if (toks[j].indexOf('breakpoint') != -1) {
+ breakpointInComment = true;
+ }
+ }
+
+ if (breakpointInComment && n.executionPoints.length > 0) {
+ n.breakpointHere = true;
+ addToBreakpoints(n.executionPoints);
+ }
+
+ this.codeOutputLines.push(n);
+ }
+
+
+ myViz.domRoot.find('#pyCodeOutputDiv').empty();
+
+ // maps this.codeOutputLines to both table columns
+ var codeOutputD3 = this.domRootD3.select('#pyCodeOutputDiv')
+ .append('table')
+ .attr('id', 'pyCodeOutput')
+ .selectAll('tr')
+ .data(this.codeOutputLines)
+ .enter().append('tr')
+ .selectAll('td')
+ .data(function(d, i){return [d, d] /* map full data item down both columns */;})
+ .enter().append('td')
+ .attr('class', function(d, i) {
+ if (i == 0) {
+ return 'lineNo';
+ }
+ else {
+ return 'cod';
+ }
+ })
+ .attr('id', function(d, i) {
+ if (i == 0) {
+ return 'lineNo' + d.lineNumber;
+ }
+ else {
+ return myViz.generateID('cod' + d.lineNumber); // make globally unique (within the page)
+ }
+ })
+ .html(function(d, i) {
+ if (i == 0) {
+ return d.lineNumber;
+ }
+ else {
+ return htmlspecialchars(d.text);
+ }
+ });
+
+ // create a left-most gutter td that spans ALL rows ...
+ // (NB: valign="top" is CRUCIAL for this to work in IE)
+ if (myViz.params.arrowLines) {
+ myViz.domRoot.find('#pyCodeOutput tr:first')
+ .prepend('
');
+
+ // create prevLineArrow and curLineArrow
+ myViz.domRootD3.select('svg#leftCodeGutterSVG')
+ .append('polygon')
+ .attr('id', 'prevLineArrow')
+ .attr('points', SVG_ARROW_POLYGON)
+ .attr('fill', lightArrowColor);
+
+ myViz.domRootD3.select('svg#leftCodeGutterSVG')
+ .append('polygon')
+ .attr('id', 'curLineArrow')
+ .attr('points', SVG_ARROW_POLYGON)
+ .attr('fill', darkArrowColor);
+ }
+
+ // 2012-09-05: Disable breakpoints for now to simplify UX
+ /*
+ if (!this.params.embeddedMode) {
+ codeOutputD3.style('cursor', function(d, i) {return 'pointer'})
+ .on('mouseover', function() {
+ setHoverBreakpoint(this);
+ })
+ .on('mouseout', function() {
+ myViz.hoverBreakpoints = d3.map();
+
+ var breakpointHere = d3.select(this).datum().breakpointHere;
+
+ if (!breakpointHere) {
+ unsetBreakpoint(this);
+ }
+
+ renderSliderBreakpoints(); // get rid of hover breakpoint colors
+ })
+ .on('mousedown', function() {
+ // don't do anything if exePts is empty
+ // (i.e., this line was never executed)
+ var exePts = d3.select(this).datum().executionPoints;
+ if (!exePts || exePts.length == 0) {
+ return;
+ }
+
+ // toggle breakpoint
+ d3.select(this).datum().breakpointHere = !d3.select(this).datum().breakpointHere;
+
+ var breakpointHere = d3.select(this).datum().breakpointHere;
+ if (breakpointHere) {
+ setBreakpoint(this);
+ }
+ else {
+ unsetBreakpoint(this);
+ }
+ });
+
+ renderSliderBreakpoints(); // renders breakpoints written in as code comments
+ }
+ */
+
+}
+
+
+// takes a string inputStr and returns an HTML version with
+// the characters from [highlightIndex, highlightIndex+extent) highlighted with
+// a span of class highlightCssClass
+function htmlWithHighlight(inputStr, highlightInd, extent, highlightCssClass) {
+ var prefix = '';
+ if (highlightInd > 0) {
+ prefix = inputStr.slice(0, highlightInd);
+ }
+
+ var highlightedChars = inputStr.slice(highlightInd, highlightInd + extent);
+
+ var suffix = '';
+ if (highlightInd + extent < inputStr.length) {
+ suffix = inputStr.slice(highlightInd + extent, inputStr.length);
+ }
+
+ // ... then set the current line to lineHTML
+ var lineHTML = htmlspecialchars(prefix) +
+ '' +
+ htmlspecialchars(highlightedChars) +
+ '' +
+ htmlspecialchars(suffix);
+ return lineHTML;
+}
+
+
+// This function is called every time the display needs to be updated
+// smoothTransition is OPTIONAL!
+ExecutionVisualizer.prototype.updateOutput = function(smoothTransition) {
+ assert(this.curTrace);
+
+ var myViz = this; // to prevent confusion of 'this' inside of nested functions
+
+ // there's no point in re-rendering if this pane isn't even visible in the first place!
+ if (!myViz.domRoot.is(':visible')) {
+ return;
+ }
+
+
+ // really nitpicky!!! gets the difference in width between the code display
+ // and the maximum width of its enclosing div
+ myViz.codeHorizontalOverflow = myViz.domRoot.find('#pyCodeOutput').width() - myViz.domRoot.find('#pyCodeOutputDiv').width();
+ // should always be positive
+ if (myViz.codeHorizontalOverflow < 0) {
+ myViz.codeHorizontalOverflow = 0;
+ }
+
+
+ // crucial resets for annotations (TODO: kludgy)
+ myViz.destroyAllAnnotationBubbles();
+ myViz.initStepAnnotation();
+
+
+ var prevDataVizHeight = myViz.domRoot.find('#dataViz').height();
+
+
+ var gutterSVG = myViz.domRoot.find('svg#leftCodeGutterSVG');
+
+ // one-time initialization of the left gutter
+ // (we often can't do this earlier since the entire pane
+ // might be invisible and hence returns a height of zero or NaN
+ // -- the exact format depends on browser)
+ if (!myViz.leftGutterSvgInitialized && myViz.params.arrowLines) {
+ // set the gutter's height to match that of its parent
+ gutterSVG.height(gutterSVG.parent().height());
+
+ var firstRowOffsetY = myViz.domRoot.find('table#pyCodeOutput tr:first').offset().top;
+
+ // first take care of edge case when there's only one line ...
+ myViz.codeRowHeight = myViz.domRoot.find('table#pyCodeOutput td.cod:first').height();
+
+ // ... then handle the (much more common) multi-line case ...
+ // this weird contortion is necessary to get the accurate row height on Internet Explorer
+ // (simpler methods work on all other major browsers, erghhhhhh!!!)
+ if (this.codeOutputLines && this.codeOutputLines.length > 1) {
+ var secondRowOffsetY = myViz.domRoot.find('table#pyCodeOutput tr:nth-child(2)').offset().top;
+ myViz.codeRowHeight = secondRowOffsetY - firstRowOffsetY;
+ }
+
+ assert(myViz.codeRowHeight > 0);
+
+ var gutterOffsetY = gutterSVG.offset().top;
+ var teenyAdjustment = gutterOffsetY - firstRowOffsetY;
+
+ // super-picky detail to adjust the vertical alignment of arrows so that they line up
+ // well with the pointed-to code text ...
+ // (if you want to manually adjust tableTop, then ~5 is a reasonable number)
+ myViz.arrowOffsetY = Math.floor((myViz.codeRowHeight / 2) - (SVG_ARROW_HEIGHT / 2)) - teenyAdjustment;
+
+ myViz.leftGutterSvgInitialized = true;
+ }
+
+ if (myViz.params.arrowLines) {
+ assert(myViz.arrowOffsetY !== undefined);
+ assert(myViz.codeRowHeight !== undefined);
+ assert(0 <= myViz.arrowOffsetY && myViz.arrowOffsetY <= myViz.codeRowHeight);
+ }
+
+ // call the callback if necessary (BEFORE rendering)
+ if (this.params.updateOutputCallback) {
+ this.params.updateOutputCallback(this);
+ }
+
+
+ var curEntry = this.curTrace[this.curInstr];
+ var hasError = false;
+ // bnm Render a question
+ if (curEntry.question) {
+ //alert(curEntry.question.text);
+
+ $('#'+curEntry.question.div).modal({position:["25%","50%"]});
+ }
+
+ // render VCR controls:
+ var totalInstrs = this.curTrace.length;
+
+ var isLastInstr = (this.curInstr == (totalInstrs-1));
+
+ var vcrControls = myViz.domRoot.find("#vcrControls");
+
+ if (isLastInstr) {
+ if (this.promptForUserInput || this.promptForMouseInput) {
+ vcrControls.find("#curInstr").html('' + this.userInputPromptStr + '');
+
+ // don't do smooth transition since prompt() is modal so it doesn't
+ // give the animation background thread time to run
+ smoothTransition = false;
+ }
+ else if (this.instrLimitReached) {
+ vcrControls.find("#curInstr").html("Instruction limit reached");
+ }
+ else {
+ vcrControls.find("#curInstr").html("Program terminated");
+ }
+ }
+ else {
+ vcrControls.find("#curInstr").html("Step " +
+ String(this.curInstr + 1) +
+ " of " + String(totalInstrs-1));
+ }
+
+
+ vcrControls.find("#jmpFirstInstr").attr("disabled", false);
+ vcrControls.find("#jmpStepBack").attr("disabled", false);
+ vcrControls.find("#jmpStepFwd").attr("disabled", false);
+ vcrControls.find("#jmpLastInstr").attr("disabled", false);
+
+ if (this.curInstr == 0) {
+ vcrControls.find("#jmpFirstInstr").attr("disabled", true);
+ vcrControls.find("#jmpStepBack").attr("disabled", true);
+ }
+ if (isLastInstr) {
+ vcrControls.find("#jmpLastInstr").attr("disabled", true);
+ vcrControls.find("#jmpStepFwd").attr("disabled", true);
+ }
+
+
+ // PROGRAMMATICALLY change the value, so evt.originalEvent should be undefined
+ myViz.domRoot.find('#executionSlider').slider('value', this.curInstr);
+
+
+ // render error (if applicable):
+ if (curEntry.event == 'exception' ||
+ curEntry.event == 'uncaught_exception') {
+ assert(curEntry.exception_msg);
+
+ if (curEntry.exception_msg == "Unknown error") {
+ myViz.domRoot.find("#errorOutput").html('Unknown error: Please email a bug report to philip@pgbovine.net');
+ }
+ else {
+ myViz.domRoot.find("#errorOutput").html(htmlspecialchars(curEntry.exception_msg));
+ }
+
+ myViz.domRoot.find("#errorOutput").show();
+
+ hasError = true;
+ }
+ else {
+ if (!this.instrLimitReached) { // ugly, I know :/
+ myViz.domRoot.find("#errorOutput").hide();
+ }
+ }
+
+
+ function highlightCodeLine() {
+ /* if instrLimitReached, then treat like a normal non-terminating line */
+ var isTerminated = (!myViz.instrLimitReached && isLastInstr);
+
+ var pcod = myViz.domRoot.find('#pyCodeOutputDiv');
+
+ var curLineNumber = null;
+ var prevLineNumber = null;
+
+ // only relevant if in myViz.pyCrazyMode
+ var prevColumn = undefined;
+ var prevExprStartCol = undefined;
+ var prevExprWidth = undefined;
+
+ var curIsReturn = (curEntry.event == 'return');
+ var prevIsReturn = false;
+
+
+ if (myViz.curInstr > 0) {
+ prevLineNumber = myViz.curTrace[myViz.curInstr - 1].line;
+ prevIsReturn = (myViz.curTrace[myViz.curInstr - 1].event == 'return');
+
+ if (myViz.pyCrazyMode) {
+ var p = myViz.curTrace[myViz.curInstr - 1];
+ prevColumn = p.column;
+ // if these don't exist, set reasonable defaults
+ prevExprStartCol = (p.expr_start_col !== undefined) ? p.expr_start_col : p.column;
+ prevExprWidth = (p.expr_width !== undefined) ? p.expr_width : 1;
+ }
+ }
+
+ curLineNumber = curEntry.line;
+
+ if (myViz.pyCrazyMode) {
+ var curColumn = curEntry.column;
+
+ // if these don't exist, set reasonable defaults
+ var curExprStartCol = (curEntry.expr_start_col !== undefined) ? curEntry.expr_start_col : curColumn;
+ var curExprWidth = (curEntry.expr_width !== undefined) ? curEntry.expr_width : 1;
+
+ var curLineInfo = myViz.codeOutputLines[curLineNumber - 1];
+ assert(curLineInfo.lineNumber == curLineNumber);
+ var codeAtLine = curLineInfo.text;
+
+ // shotgun approach: reset ALL lines to their natural (unbolded) state
+ $.each(myViz.codeOutputLines, function(i, e) {
+ var d = myViz.generateID('cod' + e.lineNumber);
+ myViz.domRoot.find('#' + d).html(htmlspecialchars(e.text));
+ });
+
+
+ // Three possible cases:
+ // 1. previous and current trace entries are on the SAME LINE
+ // 2. previous and current trace entries are on different lines
+ // 3. there is no previous trace entry
+
+ if (prevLineNumber == curLineNumber) {
+ var curLineHTML = '';
+
+ // tricky tricky!
+ // generate a combined line with both previous and current
+ // columns highlighted
+
+ for (var i = 0; i < codeAtLine.length; i++) {
+ var isCur = (curExprStartCol <= i) && (i < curExprStartCol + curExprWidth);
+ var isPrev = (prevExprStartCol <= i) && (i < prevExprStartCol + prevExprWidth);
+
+ var htmlEscapedChar = htmlspecialchars(codeAtLine[i]);
+
+ if (isCur && isPrev) {
+ curLineHTML += '' + htmlEscapedChar + '';
+ }
+ else if (isPrev) {
+ curLineHTML += '' + htmlEscapedChar + '';
+ }
+ else if (isCur) {
+ curLineHTML += '' + htmlEscapedChar + '';
+ }
+ else {
+ curLineHTML += htmlEscapedChar;
+ }
+ }
+
+ assert(curLineHTML);
+ myViz.domRoot.find('#' + myViz.generateID('cod' + curLineNumber)).html(curLineHTML);
+ }
+ else {
+ if (prevLineNumber) {
+ var prevLineInfo = myViz.codeOutputLines[prevLineNumber - 1];
+ var prevLineHTML = htmlWithHighlight(prevLineInfo.text, prevExprStartCol, prevExprWidth, 'pycrazy-highlight-prev');
+ myViz.domRoot.find('#' + myViz.generateID('cod' + prevLineNumber)).html(prevLineHTML);
+ }
+ var curLineHTML = htmlWithHighlight(codeAtLine, curExprStartCol, curExprWidth, 'pycrazy-highlight-cur');
+ myViz.domRoot.find('#' + myViz.generateID('cod' + curLineNumber)).html(curLineHTML);
+ }
+ }
+
+ // on 'return' events, give a bit more of a vertical nudge to show that
+ // the arrow is aligned with the 'bottom' of the line ...
+ var prevVerticalNudge = prevIsReturn ? Math.floor(myViz.codeRowHeight / 2) : 0;
+ var curVerticalNudge = curIsReturn ? Math.floor(myViz.codeRowHeight / 2) : 0;
+
+
+ // edge case for the final instruction :0
+ if (isTerminated && !hasError) {
+ // don't show redundant arrows on the same line when terminated ...
+ if (prevLineNumber == curLineNumber) {
+ curLineNumber = null;
+ }
+ // otherwise have a smaller vertical nudge (to fit at bottom of display table)
+ else {
+ curVerticalNudge = curVerticalNudge - 2;
+ }
+ }
+
+ if (myViz.params.arrowLines) {
+ if (prevLineNumber) {
+ var pla = myViz.domRootD3.select('#prevLineArrow');
+ var translatePrevCmd = 'translate(0, ' + (((prevLineNumber - 1) * myViz.codeRowHeight) + myViz.arrowOffsetY + prevVerticalNudge) + ')';
+
+ if (smoothTransition) {
+ pla
+ .transition()
+ .duration(200)
+ .attr('fill', 'white')
+ .each('end', function() {
+ pla
+ .attr('transform', translatePrevCmd)
+ .attr('fill', lightArrowColor);
+
+ gutterSVG.find('#prevLineArrow').show(); // show at the end to avoid flickering
+ });
+ }
+ else {
+ pla.attr('transform', translatePrevCmd)
+ gutterSVG.find('#prevLineArrow').show();
+ }
+
+ }
+ else {
+ gutterSVG.find('#prevLineArrow').hide();
+ }
+
+ if (curLineNumber) {
+ var cla = myViz.domRootD3.select('#curLineArrow');
+ var translateCurCmd = 'translate(0, ' + (((curLineNumber - 1) * myViz.codeRowHeight) + myViz.arrowOffsetY + curVerticalNudge) + ')';
+
+ if (smoothTransition) {
+ cla
+ .transition()
+ .delay(200)
+ .duration(250)
+ .attr('transform', translateCurCmd);
+ }
+ else {
+ cla.attr('transform', translateCurCmd);
+ }
+
+ gutterSVG.find('#curLineArrow').show();
+ }
+ else {
+ gutterSVG.find('#curLineArrow').hide();
+ }
+ }
+
+ myViz.domRootD3.selectAll('#pyCodeOutputDiv td.cod')
+ .style('border-top', function(d) {
+ if (hasError && (d.lineNumber == curEntry.line)) {
+ return '1px solid ' + errorColor;
+ }
+ else {
+ return '';
+ }
+ })
+ .style('border-bottom', function(d) {
+ // COPY AND PASTE ALERT!
+ if (hasError && (d.lineNumber == curEntry.line)) {
+ return '1px solid ' + errorColor;
+ }
+ else {
+ return '';
+ }
+ });
+
+ // returns True iff lineNo is visible in pyCodeOutputDiv
+ function isOutputLineVisible(lineNo) {
+ var lineNoTd = myViz.domRoot.find('#lineNo' + lineNo);
+ var LO = lineNoTd.offset().top;
+
+ var PO = pcod.offset().top;
+ var ST = pcod.scrollTop();
+ var H = pcod.height();
+
+ // add a few pixels of fudge factor on the bottom end due to bottom scrollbar
+ return (PO <= LO) && (LO < (PO + H - 30));
+ }
+
+
+ // smoothly scroll pyCodeOutputDiv so that the given line is at the center
+ function scrollCodeOutputToLine(lineNo) {
+ var lineNoTd = myViz.domRoot.find('#lineNo' + lineNo);
+ var LO = lineNoTd.offset().top;
+
+ var PO = pcod.offset().top;
+ var ST = pcod.scrollTop();
+ var H = pcod.height();
+
+ pcod.stop(); // first stop all previously-queued animations
+ pcod.animate({scrollTop: (ST + (LO - PO - (Math.round(H / 2))))}, 300);
+ }
+
+ if (myViz.params.highlightLines) {
+ myViz.domRoot.find('#pyCodeOutputDiv td.cod').removeClass('highlight-prev');
+ myViz.domRoot.find('#pyCodeOutputDiv td.cod').removeClass('highlight-cur');
+ if (curLineNumber)
+ myViz.domRoot.find('#'+myViz.generateID('cod'+curLineNumber)).addClass('highlight-cur');
+ if (prevLineNumber)
+ myViz.domRoot.find('#'+myViz.generateID('cod'+prevLineNumber)).addClass('highlight-prev');
+ }
+
+
+ // smoothly scroll code display
+ if (!isOutputLineVisible(curEntry.line)) {
+ scrollCodeOutputToLine(curEntry.line);
+ }
+
+ } // end of highlightCodeLine
+
+
+ // render code output:
+ if (curEntry.line) {
+ highlightCodeLine();
+ }
+
+ // render stdout:
+
+ // if there isn't anything in curEntry.stdout, don't even bother
+ // displaying the pane
+ if (myViz.hasStdout) {
+ this.domRoot.find('#progOutputs').show();
+
+ // keep original horizontal scroll level:
+ var oldLeft = myViz.domRoot.find("#pyStdout").scrollLeft();
+ myViz.domRoot.find("#pyStdout").val(curEntry.stdout);
+
+ myViz.domRoot.find("#pyStdout").scrollLeft(oldLeft);
+ // scroll to bottom, though:
+ myViz.domRoot.find("#pyStdout").scrollTop(myViz.domRoot.find("#pyStdout")[0].scrollHeight);
+ }
+ else {
+ this.domRoot.find('#progOutputs').hide();
+ }
+
+
+ // inject user-specified HTML/CSS/JS output:
+ // YIKES -- HUGE CODE INJECTION VULNERABILITIES :O
+ myViz.domRoot.find("#htmlOutputDiv").empty();
+ if (curEntry.html_output) {
+ if (curEntry.css_output) {
+ myViz.domRoot.find("#htmlOutputDiv").append('');
+ }
+ myViz.domRoot.find("#htmlOutputDiv").append(curEntry.html_output);
+
+ // inject and run JS *after* injecting HTML and CSS
+ if (curEntry.js_output) {
+ // NB: when jQuery injects JS, it executes the code immediately
+ // and then removes the entire ');
+ }
+ }
+
+
+ // finally, render all of the data structures
+ this.renderDataStructures();
+
+ this.enterViewAnnotationsMode(); // ... and render optional annotations (if any exist)
+
+
+ // call the callback if necessary (BEFORE rendering)
+ if (myViz.domRoot.find('#dataViz').height() != prevDataVizHeight) {
+ if (this.params.heightChangeCallback) {
+ this.params.heightChangeCallback(this);
+ }
+ }
+
+ if (isLastInstr && this.executeCodeWithRawInputFunc) {
+ if (this.promptForUserInput) {
+ // blocking prompt dialog!
+ // put a default string of '' or else it looks ugly in IE
+ var userInput = prompt(this.userInputPromptStr, '');
+
+ // if you hit 'Cancel' in prompt(), it returns null
+ if (userInput !== null) {
+ // after executing, jump back to this.curInstr to give the
+ // illusion of continuity
+ this.executeCodeWithRawInputFunc(userInput, this.curInstr);
+ }
+ }
+ }
+
+} // end of updateOutput
+
+
+// Pre-compute the layout of top-level heap objects for ALL execution
+// points as soon as a trace is first loaded. The reason why we want to
+// do this is so that when the user steps through execution points, the
+// heap objects don't "jiggle around" (i.e., preserving positional
+// invariance). Also, if we set up the layout objects properly, then we
+// can take full advantage of d3 to perform rendering and transitions.
+
+ExecutionVisualizer.prototype.precomputeCurTraceLayouts = function() {
+
+ // curTraceLayouts is a list of top-level heap layout "objects" with the
+ // same length as curTrace after it's been fully initialized. Each
+ // element of curTraceLayouts is computed from the contents of its
+ // immediate predecessor, thus ensuring that objects don't "jiggle
+ // around" between consecutive execution points.
+ //
+ // Each top-level heap layout "object" is itself a LIST of LISTS of
+ // object IDs, where each element of the outer list represents a row,
+ // and each element of the inner list represents columns within a
+ // particular row. Each row can have a different number of columns. Most
+ // rows have exactly ONE column (representing ONE object ID), but rows
+ // containing 1-D linked data structures have multiple columns. Each
+ // inner list element looks something like ['row1', 3, 2, 1] where the
+ // first element is a unique row ID tag, which is used as a key for d3 to
+ // preserve "object constancy" for updates, transitions, etc. The row ID
+ // is derived from the FIRST object ID inserted into the row. Since all
+ // object IDs are unique, all row IDs will also be unique.
+
+ /* This is a good, simple example to test whether objects "jiggle"
+
+ x = [1, [2, [3, None]]]
+ y = [4, [5, [6, None]]]
+
+ x[1][1] = y[1]
+
+ */
+ this.curTraceLayouts = [];
+ this.curTraceLayouts.push([]); // pre-seed with an empty sentinel to simplify the code
+
+ assert(this.curTrace.length > 0);
+
+ var myViz = this; // to prevent confusion of 'this' inside of nested functions
+
+
+ $.each(this.curTrace, function(i, curEntry) {
+ var prevLayout = myViz.curTraceLayouts[myViz.curTraceLayouts.length - 1];
+
+ // make a DEEP COPY of prevLayout to use as the basis for curLine
+ var curLayout = $.extend(true /* deep copy */ , [], prevLayout);
+
+ // initialize with all IDs from curLayout
+ var idsToRemove = d3.map();
+ $.each(curLayout, function(i, row) {
+ for (var j = 1 /* ignore row ID tag */; j < row.length; j++) {
+ idsToRemove.set(row[j], 1);
+ }
+ });
+
+ var idsAlreadyLaidOut = d3.map(); // to prevent infinite recursion
+
+
+ function curLayoutIndexOf(id) {
+ for (var i = 0; i < curLayout.length; i++) {
+ var row = curLayout[i];
+ var index = row.indexOf(id);
+ if (index > 0) { // index of 0 is impossible since it's the row ID tag
+ return {row: row, index: index}
+ }
+ }
+ return null;
+ }
+
+
+ function recurseIntoObject(id, curRow, newRow) {
+ //console.log('recurseIntoObject', id,
+ // $.extend(true /* make a deep copy */ , [], curRow),
+ // $.extend(true /* make a deep copy */ , [], newRow));
+
+ // heuristic for laying out 1-D linked data structures: check for enclosing elements that are
+ // structurally identical and then lay them out as siblings in the same "row"
+ var heapObj = curEntry.heap[id];
+ assert(heapObj);
+
+ if (heapObj[0] == 'LIST' || heapObj[0] == 'TUPLE' || heapObj[0] == 'SET') {
+ $.each(heapObj, function(ind, child) {
+ if (ind < 1) return; // skip type tag
+
+ if (!isPrimitiveType(child)) {
+ var childID = getRefID(child);
+
+ // comment this out to make "linked lists" that aren't
+ // structurally equivalent look good, e.g.,:
+ // x = (1, 2, (3, 4, 5, 6, (7, 8, 9, None)))
+ //if (structurallyEquivalent(heapObj, curEntry.heap[childID])) {
+ // updateCurLayout(childID, curRow, newRow);
+ //}
+ if (myViz.disableHeapNesting) {
+ updateCurLayout(childID, [], []);
+ }
+ else {
+ updateCurLayout(childID, curRow, newRow);
+ }
+ }
+ });
+ }
+ else if (heapObj[0] == 'DICT') {
+ $.each(heapObj, function(ind, child) {
+ if (ind < 1) return; // skip type tag
+
+ if (myViz.disableHeapNesting) {
+ var dictKey = child[0];
+ if (!isPrimitiveType(dictKey)) {
+ var keyChildID = getRefID(dictKey);
+ updateCurLayout(keyChildID, [], []);
+ }
+ }
+
+ var dictVal = child[1];
+ if (!isPrimitiveType(dictVal)) {
+ var childID = getRefID(dictVal);
+ if (structurallyEquivalent(heapObj, curEntry.heap[childID])) {
+ updateCurLayout(childID, curRow, newRow);
+ }
+ else if (myViz.disableHeapNesting) {
+ updateCurLayout(childID, [], []);
+ }
+ }
+ });
+ }
+ else if (heapObj[0] == 'INSTANCE' || heapObj[0] == 'CLASS') {
+ jQuery.each(heapObj, function(ind, child) {
+ var headerLength = (heapObj[0] == 'INSTANCE') ? 2 : 3;
+ if (ind < headerLength) return;
+
+ if (myViz.disableHeapNesting) {
+ var instKey = child[0];
+ if (!isPrimitiveType(instKey)) {
+ var keyChildID = getRefID(instKey);
+ updateCurLayout(keyChildID, [], []);
+ }
+ }
+
+ var instVal = child[1];
+ if (!isPrimitiveType(instVal)) {
+ var childID = getRefID(instVal);
+ if (structurallyEquivalent(heapObj, curEntry.heap[childID])) {
+ updateCurLayout(childID, curRow, newRow);
+ }
+ else if (myViz.disableHeapNesting) {
+ updateCurLayout(childID, [], []);
+ }
+ }
+ });
+ }
+ }
+
+
+ // a krazy function!
+ // id - the new object ID to be inserted somewhere in curLayout
+ // (if it's not already in there)
+ // curRow - a row within curLayout where new linked list
+ // elements can be appended onto (might be null)
+ // newRow - a new row that might be spliced into curRow or appended
+ // as a new row in curLayout
+ function updateCurLayout(id, curRow, newRow) {
+ if (idsAlreadyLaidOut.has(id)) {
+ return; // PUNT!
+ }
+
+ var curLayoutLoc = curLayoutIndexOf(id);
+
+ var alreadyLaidOut = idsAlreadyLaidOut.has(id);
+ idsAlreadyLaidOut.set(id, 1); // unconditionally set now
+
+ // if id is already in curLayout ...
+ if (curLayoutLoc) {
+ var foundRow = curLayoutLoc.row;
+ var foundIndex = curLayoutLoc.index;
+
+ idsToRemove.remove(id); // this id is already accounted for!
+
+ // very subtle ... if id hasn't already been handled in
+ // this iteration, then splice newRow into foundRow. otherwise
+ // (later) append newRow onto curLayout as a truly new row
+ if (!alreadyLaidOut) {
+ // splice the contents of newRow right BEFORE foundIndex.
+ // (Think about when you're trying to insert in id=3 into ['row1', 2, 1]
+ // to represent a linked list 3->2->1. You want to splice the 3
+ // entry right before the 2 to form ['row1', 3, 2, 1])
+ if (newRow.length > 1) {
+ var args = [foundIndex, 0];
+ for (var i = 1; i < newRow.length; i++) { // ignore row ID tag
+ args.push(newRow[i]);
+ idsToRemove.remove(newRow[i]);
+ }
+ foundRow.splice.apply(foundRow, args);
+
+ // remove ALL elements from newRow since they've all been accounted for
+ // (but don't reassign it away to an empty list, since the
+ // CALLER checks its value. TODO: how to get rid of this gross hack?!?)
+ newRow.splice(0, newRow.length);
+ }
+ }
+
+ // recurse to find more top-level linked entries to append onto foundRow
+ recurseIntoObject(id, foundRow, []);
+ }
+ else {
+ // push id into newRow ...
+ if (newRow.length == 0) {
+ newRow.push('row' + id); // unique row ID (since IDs are unique)
+ }
+ newRow.push(id);
+
+ // recurse to find more top-level linked entries ...
+ recurseIntoObject(id, curRow, newRow);
+
+
+ // if newRow hasn't been spliced into an existing row yet during
+ // a child recursive call ...
+ if (newRow.length > 0) {
+ if (curRow && curRow.length > 0) {
+ // append onto the END of curRow if it exists
+ for (var i = 1; i < newRow.length; i++) { // ignore row ID tag
+ curRow.push(newRow[i]);
+ }
+ }
+ else {
+ // otherwise push to curLayout as a new row
+ //
+ // TODO: this might not always look the best, since we might
+ // sometimes want to splice newRow in the MIDDLE of
+ // curLayout. Consider this example:
+ //
+ // x = [1,2,3]
+ // y = [4,5,6]
+ // x = [7,8,9]
+ //
+ // when the third line is executed, the arrows for x and y
+ // will be crossed (ugly!) since the new row for the [7,8,9]
+ // object is pushed to the end (bottom) of curLayout. The
+ // proper behavior is to push it to the beginning of
+ // curLayout where the old row for 'x' used to be.
+ curLayout.push($.extend(true /* make a deep copy */ , [], newRow));
+ }
+
+ // regardless, newRow is now accounted for, so clear it
+ for (var i = 1; i < newRow.length; i++) { // ignore row ID tag
+ idsToRemove.remove(newRow[i]);
+ }
+ newRow.splice(0, newRow.length); // kill it!
+ }
+
+ }
+ }
+
+
+ // iterate through all globals and ordered stack frames and call updateCurLayout
+ $.each(curEntry.ordered_globals, function(i, varname) {
+ var val = curEntry.globals[varname];
+ if (val !== undefined) { // might not be defined at this line, which is OKAY!
+ if (!isPrimitiveType(val)) {
+ var id = getRefID(val);
+ updateCurLayout(id, null, []);
+ }
+ }
+ });
+
+ $.each(curEntry.stack_to_render, function(i, frame) {
+ $.each(frame.ordered_varnames, function(xxx, varname) {
+ var val = frame.encoded_locals[varname];
+
+ if (!isPrimitiveType(val)) {
+ var id = getRefID(val);
+ updateCurLayout(id, null, []);
+ }
+ });
+ });
+
+
+ // iterate through remaining elements of idsToRemove and REMOVE them from curLayout
+ idsToRemove.forEach(function(id, xxx) {
+ id = Number(id); // keys are stored as strings, so convert!!!
+ $.each(curLayout, function(rownum, row) {
+ var ind = row.indexOf(id);
+ if (ind > 0) { // remember that index 0 of the row is the row ID tag
+ row.splice(ind, 1);
+ }
+ });
+ });
+
+ // now remove empty rows (i.e., those with only a row ID tag) from curLayout
+ curLayout = curLayout.filter(function(row) {return row.length > 1});
+
+ myViz.curTraceLayouts.push(curLayout);
+ });
+
+ this.curTraceLayouts.splice(0, 1); // remove seeded empty sentinel element
+ assert (this.curTrace.length == this.curTraceLayouts.length);
+}
+
+
+var heapPtrSrcRE = /__heap_pointer_src_/;
+
+// The "3.0" version of renderDataStructures renders variables in
+// a stack, values in a separate heap, and draws line connectors
+// to represent both stack->heap object references and, more importantly,
+// heap->heap references. This version was created in August 2012.
+//
+// The "2.0" version of renderDataStructures renders variables in
+// a stack and values in a separate heap, with data structure aliasing
+// explicitly represented via line connectors (thanks to jsPlumb lib).
+// This version was created in September 2011.
+//
+// The ORIGINAL "1.0" version of renderDataStructures
+// was created in January 2010 and rendered variables and values
+// INLINE within each stack frame without any explicit representation
+// of data structure aliasing. That is, aliased objects were rendered
+// multiple times, and a unique ID label was used to identify aliases.
+ExecutionVisualizer.prototype.renderDataStructures = function() {
+
+ var myViz = this; // to prevent confusion of 'this' inside of nested functions
+
+ var curEntry = this.curTrace[this.curInstr];
+ var curToplevelLayout = this.curTraceLayouts[this.curInstr];
+
+ // for simplicity (but sacrificing some performance), delete all
+ // connectors and redraw them from scratch. doing so avoids mysterious
+ // jsPlumb connector alignment issues when the visualizer's enclosing
+ // div contains, say, a "position: relative;" CSS tag
+ // (which happens in the IPython Notebook)
+ var existingConnectionEndpointIDs = d3.map();
+ myViz.jsPlumbInstance.select({scope: 'varValuePointer'}).each(function(c) {
+ // This is VERY crude, but to prevent multiple redundant HEAP->HEAP
+ // connectors from being drawn with the same source and origin, we need to first
+ // DELETE ALL existing HEAP->HEAP connections, and then re-render all of
+ // them in each call to this function. The reason why we can't safely
+ // hold onto them is because there's no way to guarantee that the
+ // *__heap_pointer_src_ IDs are consistent across execution points.
+ //
+ // thus, only add to existingConnectionEndpointIDs if this is NOT heap->heap
+ if (!c.sourceId.match(heapPtrSrcRE)) {
+ existingConnectionEndpointIDs.set(c.sourceId, c.targetId);
+ }
+ });
+
+ var existingParentPointerConnectionEndpointIDs = d3.map();
+ myViz.jsPlumbInstance.select({scope: 'frameParentPointer'}).each(function(c) {
+ existingParentPointerConnectionEndpointIDs.set(c.sourceId, c.targetId);
+ });
+
+
+ // Heap object rendering phase:
+
+
+ // Key: CSS ID of the div element representing the stack frame variable
+ // (for stack->heap connections) or heap object (for heap->heap connections)
+ // the format is: '__heap_pointer_src_'
+ // Value: CSS ID of the div element representing the value rendered in the heap
+ // (the format is: '__heap_object_')
+ //
+ // The reason we need to prepend this.visualizerID is because jsPlumb needs
+ // GLOBALLY UNIQUE IDs for use as connector endpoints.
+
+ // the only elements in these sets are NEW elements to be rendered in this
+ // particular call to renderDataStructures.
+ var connectionEndpointIDs = d3.map();
+ var heapConnectionEndpointIDs = d3.map(); // subset of connectionEndpointIDs for heap->heap connections
+
+ // analogous to connectionEndpointIDs, except for environment parent pointers
+ var parentPointerConnectionEndpointIDs = d3.map();
+
+ var heap_pointer_src_id = 1; // increment this to be unique for each heap_pointer_src_*
+
+
+ var renderedObjectIDs = d3.map();
+
+ // count everything in curToplevelLayout as already rendered since we will render them
+ // in d3 .each() statements
+ $.each(curToplevelLayout, function(xxx, row) {
+ for (var i = 0; i < row.length; i++) {
+ renderedObjectIDs.set(row[i], 1);
+ }
+ });
+
+
+
+ // use d3 to render the heap by mapping curToplevelLayout into
+ // and
elements
+
+ var heapRows = myViz.domRootD3.select('#heap')
+ .selectAll('table.heapRow')
+ .data(curToplevelLayout, function(objLst) {
+ return objLst[0]; // return first element, which is the row ID tag
+ });
+
+
+ // insert new heap rows
+ heapRows.enter().append('table')
+ //.each(function(objLst, i) {console.log('NEW ROW:', objLst, i);})
+ .attr('class', 'heapRow');
+
+ // delete a heap row
+ var hrExit = heapRows.exit();
+
+ if (myViz.enableTransitions) {
+ hrExit
+ .style('opacity', '1')
+ .transition()
+ .style('opacity', '0')
+ .duration(500)
+ .each('end', function() {
+ hrExit
+ .each(function(d, idx) {
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+ myViz.redrawConnectors();
+ });
+ }
+ else {
+ hrExit
+ .each(function(d, idx) {
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+ }
+
+
+ // update an existing heap row
+ var toplevelHeapObjects = heapRows
+ //.each(function(objLst, i) { console.log('UPDATE ROW:', objLst, i); })
+ .selectAll('td.toplevelHeapObject')
+ .data(function(d, i) {return d.slice(1, d.length);}, /* map over each row, skipping row ID tag */
+ function(objID) {return objID;} /* each object ID is unique for constancy */);
+
+ // insert a new toplevelHeapObject
+ var tlhEnter = toplevelHeapObjects.enter().append('td')
+ .attr('class', 'toplevelHeapObject')
+ .attr('id', function(d, i) {return 'toplevel_heap_object_' + d;});
+
+ if (myViz.enableTransitions) {
+ tlhEnter
+ .style('opacity', '0')
+ .style('border-color', 'red')
+ .transition()
+ .style('opacity', '1') /* fade in */
+ .duration(700)
+ .each('end', function() {
+ tlhEnter.transition()
+ .style('border-color', 'white') /* kill border */
+ .duration(300)
+ });
+ }
+
+ // remember that the enter selection is added to the update
+ // selection so that we can process it later ...
+
+ // update a toplevelHeapObject
+ toplevelHeapObjects
+ .order() // VERY IMPORTANT to put in the order corresponding to data elements
+ .each(function(objID, i) {
+ //console.log('NEW/UPDATE ELT', objID);
+
+ // TODO: add a smoother transition in the future
+ // Right now, just delete the old element and render a new one in its place
+ $(this).empty();
+ renderCompoundObject(objID, $(this), true);
+ });
+
+ // delete a toplevelHeapObject
+ var tlhExit = toplevelHeapObjects.exit();
+
+ if (myViz.enableTransitions) {
+ tlhExit.transition()
+ .style('opacity', '0') /* fade out */
+ .duration(500)
+ .each('end', function() {
+ tlhExit
+ .each(function(d, idx) {
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+ myViz.redrawConnectors();
+ });
+ }
+ else {
+ tlhExit
+ .each(function(d, idx) {
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+ }
+
+
+ function renderNestedObject(obj, d3DomElement) {
+ if (isPrimitiveType(obj)) {
+ renderPrimitiveObject(obj, d3DomElement);
+ }
+ else {
+ renderCompoundObject(getRefID(obj), d3DomElement, false);
+ }
+ }
+
+
+ function renderPrimitiveObject(obj, d3DomElement) {
+ var typ = typeof obj;
+
+ if (obj == null) {
+ if (myViz.params.lang == 'java')
+ d3DomElement.append('null');
+ else
+ d3DomElement.append('None');
+ }
+ else if (typ == "number") {
+ d3DomElement.append('' + obj + '');
+ }
+ else if (typ == "boolean") {
+ if (obj) {
+ if (myViz.params.lang == 'java')
+ d3DomElement.append('true');
+ else
+ d3DomElement.append('True');
+ }
+ else {
+ if (myViz.params.lang == 'java')
+ d3DomElement.append('false');
+ else
+ d3DomElement.append('False');
+ }
+ }
+ else if (typ == "string") {
+ // escape using htmlspecialchars to prevent HTML/script injection
+ var literalStr = htmlspecialchars(obj);
+
+ // print as a double-quoted string literal
+ literalStr = literalStr.replace(new RegExp('\"', 'g'), '\\"'); // replace ALL
+ literalStr = '"' + literalStr + '"';
+
+ d3DomElement.append('' + literalStr + '');
+ }
+ else if (typ == "number") {
+ d3DomElement.append('' + obj + '');
+ }
+ else if (obj instanceof Array && obj[0] == "VOID") {
+ d3DomElement.append('void');
+ }
+ else if (obj instanceof Array && obj[0] == "NUMBER-LITERAL") {
+ // actually transmitted as a string
+ d3DomElement.append('' + obj[1] + '');
+ }
+ else {
+ assert(false);
+ }
+ }
+
+
+ function renderCompoundObject(objID, d3DomElement, isTopLevel) {
+ if (!isTopLevel && renderedObjectIDs.has(objID)) {
+ var srcDivID = myViz.generateID('heap_pointer_src_' + heap_pointer_src_id);
+ heap_pointer_src_id++; // just make sure each source has a UNIQUE ID
+
+ var dstDivID = myViz.generateID('heap_object_' + objID);
+
+ if (myViz.textualMemoryLabels) {
+ var labelID = srcDivID + '_text_label';
+ d3DomElement.append('
id' + objID + '
');
+
+ myViz.domRoot.find('div#' + labelID).hover(
+ function() {
+ myViz.jsPlumbInstance.connect({source: labelID, target: dstDivID,
+ scope: 'varValuePointer'});
+ },
+ function() {
+ myViz.jsPlumbInstance.select({source: labelID}).detach();
+ });
+ }
+ else {
+ // render jsPlumb arrow source since this heap object has already been rendered
+ // (or will be rendered soon)
+
+ // add a stub so that we can connect it with a connector later.
+ // IE needs this div to be NON-EMPTY in order to properly
+ // render jsPlumb endpoints, so that's why we add an " "!
+ d3DomElement.append('
');
+
+ assert(!connectionEndpointIDs.has(srcDivID));
+ connectionEndpointIDs.set(srcDivID, dstDivID);
+ //console.log('HEAP->HEAP', srcDivID, dstDivID);
+
+ assert(!heapConnectionEndpointIDs.has(srcDivID));
+ heapConnectionEndpointIDs.set(srcDivID, dstDivID);
+ }
+
+ return; // early return!
+ }
+
+
+ var heapObjID = myViz.generateID('heap_object_' + objID);
+
+
+ // wrap ALL compound objects in a heapObject div so that jsPlumb
+ // connectors can point to it:
+ d3DomElement.append('');
+ d3DomElement = myViz.domRoot.find('#' + heapObjID);
+
+ renderedObjectIDs.set(objID, 1);
+
+ var obj = curEntry.heap[objID];
+ assert($.isArray(obj));
+
+ // prepend the type label with a memory address label
+ var typeLabelPrefix = '';
+ if (myViz.textualMemoryLabels) {
+ typeLabelPrefix = 'id' + objID + ':';
+ }
+
+ if (obj[0] == 'LIST' || obj[0] == 'TUPLE' || obj[0] == 'SET' || obj[0] == 'DICT') {
+ var label = obj[0].toLowerCase();
+ if (myViz.params.lang == 'java' && label == 'list')
+ visibleLabel = 'array';
+ else
+ visibleLabel = label;
+
+ assert(obj.length >= 1);
+ if (obj.length == 1) {
+ d3DomElement.append('
' + typeLabelPrefix + 'empty ' + visibleLabel + '
');
+ }
+ else {
+ d3DomElement.append('
' + typeLabelPrefix + visibleLabel + '
');
+ d3DomElement.append('
');
+ var tbl = d3DomElement.children('table');
+
+ if (obj[0] == 'LIST' || obj[0] == 'TUPLE') {
+ tbl.append('
');
+ var headerTr = tbl.find('tr:first');
+ var contentTr = tbl.find('tr:last');
+ $.each(obj, function(ind, val) {
+ if (ind < 1) return; // skip type tag and ID entry
+
+ // add a new column and then pass in that newly-added column
+ // as d3DomElement to the recursive call to child:
+ headerTr.append('
');
+ }
+
+ // right now, let's NOT display class members, since that clutters
+ // up the display too much. in the future, consider displaying
+ // class members in a pop-up pane on mouseover or mouseclick
+ // actually nix what i just said above ...
+ //if (!isInstance) return;
+
+ if (obj.length > headerLength) {
+ var lab = isInstance ? 'inst' : 'class';
+ d3DomElement.append('
');
+
+ var newRow = tbl.find('tr:last');
+ var keyTd = newRow.find('td:first');
+ var valTd = newRow.find('td:last');
+
+ // the keys should always be strings, so render them directly (and without quotes):
+ // (actually this isn't the case when strings are rendered on the heap)
+ if (kvPair[0] instanceof Array
+ && kvPair[0][0] == "NO-LABEL") {
+ $(keyTd).hide();
+ }
+ else if (typeof kvPair[0] == "string") {
+ // common case ...
+ var attrnameStr = htmlspecialchars(kvPair[0]);
+ keyTd.append('' + attrnameStr + '');
+ }
+ else {
+ // when strings are rendered as heap objects ...
+ renderNestedObject(kvPair[0], keyTd);
+ }
+
+ // values can be arbitrary objects, so recurse:
+ renderNestedObject(kvPair[1], valTd);
+ });
+ }
+ }
+ else if (obj[0] == 'INSTANCE_PPRINT') {
+ d3DomElement.append('
');
+ }
+ }
+
+
+ // Render globals and then stack frames using d3:
+
+
+ // TODO: this sometimes seems buggy on Safari, so nix it for now:
+ function highlightAliasedConnectors(d, i) {
+ // if this row contains a stack pointer, then highlight its arrow and
+ // ALL aliases that also point to the same heap object
+ var stackPtrId = $(this).find('div.stack_pointer').attr('id');
+ if (stackPtrId) {
+ var foundTargetId = null;
+ myViz.jsPlumbInstance.select({source: stackPtrId}).each(function(c) {foundTargetId = c.targetId;});
+
+ // use foundTargetId to highlight ALL ALIASES
+ myViz.jsPlumbInstance.select().each(function(c) {
+ if (c.targetId == foundTargetId) {
+ c.setHover(true);
+ $(c.canvas).css("z-index", 2000); // ... and move it to the VERY FRONT
+ }
+ else {
+ c.setHover(false);
+ }
+ });
+ }
+ }
+
+ function unhighlightAllConnectors(d, i) {
+ myViz.jsPlumbInstance.select().each(function(c) {
+ c.setHover(false);
+ });
+ }
+
+
+
+ // TODO: coalesce code for rendering globals and stack frames,
+ // since there's so much copy-and-paste grossness right now
+
+ // render all global variables IN THE ORDER they were created by the program,
+ // in order to ensure continuity:
+
+ // Derive a list where each element contains varname
+ // as long as value is NOT undefined.
+ // (Sometimes entries in curEntry.ordered_globals are undefined,
+ // so filter those out.)
+ var realGlobalsLst = [];
+ $.each(curEntry.ordered_globals, function(i, varname) {
+ var val = curEntry.globals[varname];
+
+ // (use '!==' to do an EXACT match against undefined)
+ if (val !== undefined) { // might not be defined at this line, which is OKAY!
+ realGlobalsLst.push(varname);
+ }
+ });
+
+ var globalsID = myViz.generateID('globals');
+ var globalTblID = myViz.generateID('global_table');
+
+ var globalVarTable = myViz.domRootD3.select('#' + globalTblID)
+ .selectAll('tr')
+ .data(realGlobalsLst,
+ function(d) {return d;} // use variable name as key
+ );
+
+ globalVarTable
+ .enter()
+ .append('tr')
+ .attr('class', 'variableTr')
+ .attr('id', function(d, i) {
+ return myViz.generateID(varnameToCssID('global__' + d + '_tr')); // make globally unique (within the page)
+ });
+
+
+ var globalVarTableCells = globalVarTable
+ .selectAll('td.stackFrameVar,td.stackFrameValue')
+ .data(function(d, i){return [d, d];}) /* map varname down both columns */
+
+ globalVarTableCells.enter()
+ .append('td')
+ .attr('class', function(d, i) {return (i == 0) ? 'stackFrameVar' : 'stackFrameValue';});
+
+ // remember that the enter selection is added to the update
+ // selection so that we can process it later ...
+
+ // UPDATE
+ globalVarTableCells
+ .order() // VERY IMPORTANT to put in the order corresponding to data elements
+ .each(function(varname, i) {
+ if (i == 0) {
+ $(this).html(varname);
+ }
+ else {
+ // always delete and re-render the global var ...
+ // NB: trying to cache and compare the old value using,
+ // say -- $(this).attr('data-curvalue', valStringRepr) -- leads to
+ // a mysterious and killer memory leak that I can't figure out yet
+ $(this).empty();
+
+ // make sure varname doesn't contain any weird
+ // characters that are illegal for CSS ID's ...
+ var varDivID = myViz.generateID('global__' + varnameToCssID(varname));
+
+ // need to get rid of the old connector in preparation for rendering a new one:
+ existingConnectionEndpointIDs.remove(varDivID);
+
+ var val = curEntry.globals[varname];
+ if (isPrimitiveType(val)) {
+ renderPrimitiveObject(val, $(this));
+ }
+ else {
+ var heapObjID = myViz.generateID('heap_object_' + getRefID(val));
+
+ if (myViz.textualMemoryLabels) {
+ var labelID = varDivID + '_text_label';
+ $(this).append('
id' + getRefID(val) + '
');
+ $(this).find('div#' + labelID).hover(
+ function() {
+ myViz.jsPlumbInstance.connect({source: labelID, target: heapObjID,
+ scope: 'varValuePointer'});
+ },
+ function() {
+ myViz.jsPlumbInstance.select({source: labelID}).detach();
+ });
+ }
+ else {
+ // add a stub so that we can connect it with a connector later.
+ // IE needs this div to be NON-EMPTY in order to properly
+ // render jsPlumb endpoints, so that's why we add an " "!
+ $(this).append('
');
+
+ assert(!connectionEndpointIDs.has(varDivID));
+ connectionEndpointIDs.set(varDivID, heapObjID);
+ //console.log('STACK->HEAP', varDivID, heapObjID);
+ }
+ }
+ }
+ });
+
+
+
+ globalVarTableCells.exit()
+ .each(function(d, idx) {
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+
+ globalVarTable.exit()
+ .each(function(d, i) {
+ // detach all stack_pointer connectors for divs that are being removed
+ $(this).find('.stack_pointer').each(function(i, sp) {
+ existingConnectionEndpointIDs.remove($(sp).attr('id'));
+ });
+
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+
+
+ // for aesthetics, hide globals if there aren't any globals to display
+ if (curEntry.ordered_globals.length == 0) {
+ this.domRoot.find('#' + globalsID).hide();
+ }
+ else {
+ this.domRoot.find('#' + globalsID).show();
+ }
+
+
+ // holy cow, the d3 code for stack rendering is ABSOLUTELY NUTS!
+
+ var stackDiv = myViz.domRootD3.select('#stack');
+
+ // VERY IMPORTANT for selectAll selector to be SUPER specific here!
+ var stackFrameDiv = stackDiv.selectAll('div.stackFrame,div.zombieStackFrame')
+ .data(curEntry.stack_to_render, function(frame) {
+ // VERY VERY VERY IMPORTANT for properly handling closures and nested functions
+ // (see the backend code for more details)
+ return frame.unique_hash;
+ });
+
+ var sfdEnter = stackFrameDiv.enter()
+ .append('div')
+ .attr('class', function(d, i) {return d.is_zombie ? 'zombieStackFrame' : 'stackFrame';})
+ .attr('id', function(d, i) {return d.is_zombie ? myViz.generateID("zombie_stack" + i)
+ : myViz.generateID("stack" + i);
+ })
+ // HTML5 custom data attributes
+ .attr('data-frame_id', function(frame, i) {return frame.frame_id;})
+ .attr('data-parent_frame_id', function(frame, i) {
+ return (frame.parent_frame_id_list.length > 0) ? frame.parent_frame_id_list[0] : null;
+ })
+ .each(function(frame, i) {
+ if (!myViz.drawParentPointers) {
+ return;
+ }
+ // only run if myViz.drawParentPointers is true ...
+
+ var my_CSS_id = $(this).attr('id');
+
+ //console.log(my_CSS_id, 'ENTER');
+
+ // render a parent pointer whose SOURCE node is this frame
+ // i.e., connect this frame to p, where this.parent_frame_id == p.frame_id
+ // (if this.parent_frame_id is null, then p is the global frame)
+ if (frame.parent_frame_id_list.length > 0) {
+ var parent_frame_id = frame.parent_frame_id_list[0];
+ // tricky turkey!
+ // ok this hack just HAPPENS to work by luck ... usually there will only be ONE frame
+ // that matches this selector, but sometimes multiple frames match, in which case the
+ // FINAL frame wins out (since parentPointerConnectionEndpointIDs is a map where each
+ // key can be mapped to only ONE value). it so happens that the final frame winning
+ // out looks "desirable" for some of the closure test cases that I've tried. but
+ // this code is quite brittle :(
+ myViz.domRoot.find('div#stack [data-frame_id=' + parent_frame_id + ']').each(function(i, e) {
+ var parent_CSS_id = $(this).attr('id');
+ //console.log('connect', my_CSS_id, parent_CSS_id);
+ parentPointerConnectionEndpointIDs.set(my_CSS_id, parent_CSS_id);
+ });
+ }
+ else {
+ // render a parent pointer to the global frame
+ //console.log('connect', my_CSS_id, globalsID);
+ // only do this if there are actually some global variables to display ...
+ if (curEntry.ordered_globals.length > 0) {
+ parentPointerConnectionEndpointIDs.set(my_CSS_id, globalsID);
+ }
+ }
+
+ // tricky turkey: render parent pointers whose TARGET node is this frame.
+ // i.e., for all frames f such that f.parent_frame_id == my_frame_id,
+ // connect f to this frame.
+ // (make sure not to confuse frame IDs with CSS IDs!!!)
+ var my_frame_id = frame.frame_id;
+ myViz.domRoot.find('div#stack [data-parent_frame_id=' + my_frame_id + ']').each(function(i, e) {
+ var child_CSS_id = $(this).attr('id');
+ //console.log('connect', child_CSS_id, my_CSS_id);
+ parentPointerConnectionEndpointIDs.set(child_CSS_id, my_CSS_id);
+ });
+ });
+
+ sfdEnter
+ .append('div')
+ .attr('class', 'stackFrameHeader')
+ .html(function(frame, i) {
+
+ // pretty-print lambdas and display other weird characters
+ // (might contain '<' or '>' for weird names like )
+ var funcName = htmlspecialchars(frame.func_name).replace('<lambda>', '\u03bb')
+ .replace('\n', ' ');
+
+ var headerLabel = funcName;
+
+ // only display if you're someone's parent
+ if (frame.is_parent) {
+ headerLabel = 'f' + frame.frame_id + ': ' + headerLabel;
+ }
+
+ // optional (btw, this isn't a CSS id)
+ if (frame.parent_frame_id_list.length > 0) {
+ var parentFrameID = frame.parent_frame_id_list[0];
+ headerLabel = headerLabel + ' [parent=f' + parentFrameID + ']';
+ }
+
+ return headerLabel;
+ });
+
+ sfdEnter
+ .append('table')
+ .attr('class', 'stackFrameVarTable');
+
+
+ var stackVarTable = stackFrameDiv
+ .order() // VERY IMPORTANT to put in the order corresponding to data elements
+ .select('table').selectAll('tr')
+ .data(function(frame) {
+ // each list element contains a reference to the entire frame
+ // object as well as the variable name
+ // TODO: look into whether we can use d3 parent nodes to avoid
+ // this hack ... http://bost.ocks.org/mike/nest/
+ return frame.ordered_varnames.map(function(varname) {return {varname:varname, frame:frame};});
+ },
+ function(d) {return d.varname;} // use variable name as key
+ );
+
+ stackVarTable
+ .enter()
+ .append('tr')
+ .attr('class', 'variableTr')
+ .attr('id', function(d, i) {
+ return myViz.generateID(varnameToCssID(d.frame.unique_hash + '__' + d.varname + '_tr')); // make globally unique (within the page)
+ });
+
+
+ var stackVarTableCells = stackVarTable
+ .selectAll('td.stackFrameVar,td.stackFrameValue')
+ .data(function(d, i) {return [d, d] /* map identical data down both columns */;});
+
+ stackVarTableCells.enter()
+ .append('td')
+ .attr('class', function(d, i) {return (i == 0) ? 'stackFrameVar' : 'stackFrameValue';});
+
+ stackVarTableCells
+ .order() // VERY IMPORTANT to put in the order corresponding to data elements
+ .each(function(d, i) {
+ var varname = d.varname;
+ var frame = d.frame;
+
+ if (i == 0) {
+ if (varname == '__return__')
+ $(this).html('Return value');
+ else
+ $(this).html(varname);
+ }
+ else {
+ // always delete and re-render the stack var ...
+ // NB: trying to cache and compare the old value using,
+ // say -- $(this).attr('data-curvalue', valStringRepr) -- leads to
+ // a mysterious and killer memory leak that I can't figure out yet
+ $(this).empty();
+
+ // make sure varname and frame.unique_hash don't contain any weird
+ // characters that are illegal for CSS ID's ...
+ var varDivID = myViz.generateID(varnameToCssID(frame.unique_hash + '__' + varname));
+
+ // need to get rid of the old connector in preparation for rendering a new one:
+ existingConnectionEndpointIDs.remove(varDivID);
+
+ var val = frame.encoded_locals[varname];
+ if (isPrimitiveType(val)) {
+ renderPrimitiveObject(val, $(this));
+ }
+ else {
+ var heapObjID = myViz.generateID('heap_object_' + getRefID(val));
+ if (myViz.textualMemoryLabels) {
+ var labelID = varDivID + '_text_label';
+ $(this).append('
id' + getRefID(val) + '
');
+ $(this).find('div#' + labelID).hover(
+ function() {
+ myViz.jsPlumbInstance.connect({source: labelID, target: heapObjID,
+ scope: 'varValuePointer'});
+ },
+ function() {
+ myViz.jsPlumbInstance.select({source: labelID}).detach();
+ });
+ }
+ else {
+ // add a stub so that we can connect it with a connector later.
+ // IE needs this div to be NON-EMPTY in order to properly
+ // render jsPlumb endpoints, so that's why we add an " "!
+ $(this).append('
');
+
+ assert(!connectionEndpointIDs.has(varDivID));
+ connectionEndpointIDs.set(varDivID, heapObjID);
+ //console.log('STACK->HEAP', varDivID, heapObjID);
+ }
+ }
+ }
+ });
+
+
+ stackVarTableCells.exit()
+ .each(function(d, idx) {
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+
+ stackVarTable.exit()
+ .each(function(d, i) {
+ $(this).find('.stack_pointer').each(function(i, sp) {
+ // detach all stack_pointer connectors for divs that are being removed
+ existingConnectionEndpointIDs.remove($(sp).attr('id'));
+ });
+
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+
+ stackFrameDiv.exit()
+ .each(function(frame, i) {
+ $(this).find('.stack_pointer').each(function(i, sp) {
+ // detach all stack_pointer connectors for divs that are being removed
+ existingConnectionEndpointIDs.remove($(sp).attr('id'));
+ });
+
+ var my_CSS_id = $(this).attr('id');
+
+ //console.log(my_CSS_id, 'EXIT');
+
+ // Remove all pointers where either the source or destination end is my_CSS_id
+ existingParentPointerConnectionEndpointIDs.forEach(function(k, v) {
+ if (k == my_CSS_id || v == my_CSS_id) {
+ //console.log('remove EPP', k, v);
+ existingParentPointerConnectionEndpointIDs.remove(k);
+ }
+ });
+
+ $(this).empty(); // crucial for garbage collecting jsPlumb connectors!
+ })
+ .remove();
+
+
+ // NB: ugh, I'm not very happy about this hack, but it seems necessary
+ // for embedding within sophisticated webpages such as IPython Notebook
+
+ // delete all connectors. do this AS LATE AS POSSIBLE so that
+ // (presumably) the calls to $(this).empty() earlier in this function
+ // will properly garbage collect the connectors
+ //
+ // WARNING: for environment parent pointers, garbage collection doesn't seem to
+ // be working as intended :(
+ //
+ // I suspect that this is due to the fact that parent pointers are SIBLINGS
+ // of stackFrame divs and not children, so when stackFrame divs get destroyed,
+ // their associated parent pointers do NOT.)
+ myViz.jsPlumbInstance.reset();
+
+
+ // use jsPlumb scopes to keep the different kinds of pointers separated
+ function renderVarValueConnector(varID, valueID) {
+ myViz.jsPlumbInstance.connect({source: varID, target: valueID, scope: 'varValuePointer'});
+ }
+
+
+ var totalParentPointersRendered = 0;
+
+ function renderParentPointerConnector(srcID, dstID) {
+ // SUPER-DUPER-ugly hack since I can't figure out a cleaner solution for now:
+ // if either srcID or dstID no longer exists, then SKIP rendering ...
+ if ((myViz.domRoot.find('#' + srcID).length == 0) ||
+ (myViz.domRoot.find('#' + dstID).length == 0)) {
+ return;
+ }
+
+ //console.log('renderParentPointerConnector:', srcID, dstID);
+
+ myViz.jsPlumbInstance.connect({source: srcID, target: dstID,
+ anchors: ["LeftMiddle", "LeftMiddle"],
+
+ // 'horizontally offset' the parent pointers up so that they don't look as ugly ...
+ //connector: ["Flowchart", { stub: 9 + (6 * (totalParentPointersRendered + 1)) }],
+
+ // actually let's try a bezier curve ...
+ connector: [ "Bezier", { curviness: 45 }],
+
+ endpoint: ["Dot", {radius: 4}],
+ //hoverPaintStyle: {lineWidth: 1, strokeStyle: connectorInactiveColor}, // no hover colors
+ scope: 'frameParentPointer'});
+ totalParentPointersRendered++;
+ }
+
+ if (!myViz.textualMemoryLabels) {
+ // re-render existing connectors and then ...
+ existingConnectionEndpointIDs.forEach(renderVarValueConnector);
+ // add all the NEW connectors that have arisen in this call to renderDataStructures
+ connectionEndpointIDs.forEach(renderVarValueConnector);
+ }
+ // do the same for environment parent pointers
+ if (myViz.drawParentPointers) {
+ existingParentPointerConnectionEndpointIDs.forEach(renderParentPointerConnector);
+ parentPointerConnectionEndpointIDs.forEach(renderParentPointerConnector);
+ }
+
+ /*
+ myViz.jsPlumbInstance.select().each(function(c) {
+ console.log('CONN:', c.sourceId, c.targetId);
+ });
+ */
+ //console.log('---', myViz.jsPlumbInstance.select().length, '---');
+
+
+ function highlight_frame(frameID) {
+ myViz.jsPlumbInstance.select().each(function(c) {
+ // this is VERY VERY fragile code, since it assumes that going up
+ // FOUR layers of parent() calls will get you from the source end
+ // of the connector to the enclosing stack frame
+ var stackFrameDiv = c.source.parent().parent().parent().parent();
+
+ // if this connector starts in the selected stack frame ...
+ if (stackFrameDiv.attr('id') == frameID) {
+ // then HIGHLIGHT IT!
+ c.setPaintStyle({lineWidth:1, strokeStyle: connectorBaseColor});
+ c.endpoints[0].setPaintStyle({fillStyle: connectorBaseColor});
+ //c.endpoints[1].setVisible(false, true, true); // JUST set right endpoint to be invisible
+
+ $(c.canvas).css("z-index", 1000); // ... and move it to the VERY FRONT
+ }
+ // for heap->heap connectors
+ else if (heapConnectionEndpointIDs.has(c.endpoints[0].elementId)) {
+ // NOP since it's already the color and style we set by default
+ }
+ else {
+ // else unhighlight it
+ c.setPaintStyle({lineWidth:1, strokeStyle: connectorInactiveColor});
+ c.endpoints[0].setPaintStyle({fillStyle: connectorInactiveColor});
+ //c.endpoints[1].setVisible(false, true, true); // JUST set right endpoint to be invisible
+
+ $(c.canvas).css("z-index", 0);
+ }
+ });
+
+
+ // clear everything, then just activate this one ...
+ myViz.domRoot.find(".stackFrame").removeClass("highlightedStackFrame");
+ myViz.domRoot.find('#' + frameID).addClass("highlightedStackFrame");
+ }
+
+
+ // highlight the top-most non-zombie stack frame or, if not available, globals
+ var frame_already_highlighted = false;
+ $.each(curEntry.stack_to_render, function(i, e) {
+ if (e.is_highlighted) {
+ highlight_frame(myViz.generateID('stack' + i));
+ frame_already_highlighted = true;
+ }
+ });
+
+ if (!frame_already_highlighted) {
+ highlight_frame(myViz.generateID('globals'));
+ }
+
+}
+
+
+
+ExecutionVisualizer.prototype.redrawConnectors = function() {
+ this.jsPlumbInstance.repaintEverything();
+}
+
+
+// Utilities
+
+
+/* colors - see pytutor.css for more colors */
+
+var highlightedLineColor = '#e4faeb';
+var highlightedLineBorderColor = '#005583';
+
+var highlightedLineLighterColor = '#e8fff0';
+
+var funcCallLineColor = '#a2eebd';
+
+var brightRed = '#e93f34';
+
+var connectorBaseColor = '#005583';
+var connectorHighlightColor = brightRed;
+var connectorInactiveColor = '#cccccc';
+
+var errorColor = brightRed;
+
+var breakpointColor = brightRed;
+var hoverBreakpointColor = connectorBaseColor;
+
+
+// Unicode arrow types: '\u21d2', '\u21f0', '\u2907'
+var darkArrowColor = brightRed;
+var lightArrowColor = '#c9e6ca';
+
+
+function assert(cond) {
+ if (!cond) {
+ alert("Assertion Failure (see console log for backtrace)");
+ throw 'Assertion Failure';
+ }
+}
+
+// taken from http://www.toao.net/32-my-htmlspecialchars-function-for-javascript
+function htmlspecialchars(str) {
+ if (typeof(str) == "string") {
+ str = str.replace(/&/g, "&"); /* must do & first */
+
+ // ignore these for now ...
+ //str = str.replace(/"/g, """);
+ //str = str.replace(/'/g, "'");
+
+ str = str.replace(//g, ">");
+
+ // replace spaces:
+ str = str.replace(/ /g, " ");
+
+ // replace tab as four spaces:
+ str = str.replace(/\t/g, " ");
+ }
+ return str;
+}
+
+
+// same as htmlspecialchars except don't worry about expanding spaces or
+// tabs since we want proper word wrapping in divs.
+function htmlsanitize(str) {
+ if (typeof(str) == "string") {
+ str = str.replace(/&/g, "&"); /* must do & first */
+
+ str = str.replace(//g, ">");
+ }
+ return str;
+}
+
+
+String.prototype.rtrim = function() {
+ return this.replace(/\s*$/g, "");
+}
+
+
+// make sure varname doesn't contain any weird
+// characters that are illegal for CSS ID's ...
+//
+// I know for a fact that iterator tmp variables named '_[1]'
+// are NOT legal names for CSS ID's.
+// I also threw in '{', '}', '(', ')', '<', '>' as illegal characters.
+//
+// also some variable names are like '.0' (for generator expressions),
+// and '.' seems to be illegal.
+// TODO: what other characters are illegal???
+var lbRE = new RegExp('\\[|{|\\(|<', 'g');
+var rbRE = new RegExp('\\]|}|\\)|>', 'g');
+function varnameToCssID(varname) {
+ return varname.replace(lbRE, 'LeftB_').replace(rbRE, '_RightB').replace('.', '_DOT_');
+}
+
+
+// compare two JSON-encoded compound objects for structural equivalence:
+function structurallyEquivalent(obj1, obj2) {
+ // punt if either isn't a compound type
+ if (isPrimitiveType(obj1) || isPrimitiveType(obj2)) {
+ return false;
+ }
+
+ // must be the same compound type
+ if (obj1[0] != obj2[0]) {
+ return false;
+ }
+
+ // must have the same number of elements or fields
+ if (obj1.length != obj2.length) {
+ return false;
+ }
+
+ // for a list or tuple, same size (e.g., a cons cell is a list/tuple of size 2)
+ if (obj1[0] == 'LIST' || obj1[0] == 'TUPLE') {
+ return true;
+ }
+ else {
+ var startingInd = -1;
+
+ if (obj1[0] == 'DICT') {
+ startingInd = 2;
+ }
+ else if (obj1[0] == 'INSTANCE') {
+ startingInd = 3;
+ }
+ else {
+ return false; // punt on all other types
+ }
+
+ var obj1fields = d3.map();
+
+ // for a dict or object instance, same names of fields (ordering doesn't matter)
+ for (var i = startingInd; i < obj1.length; i++) {
+ obj1fields.set(obj1[i][0], 1); // use as a set
+ }
+
+ for (var i = startingInd; i < obj2.length; i++) {
+ if (!obj1fields.has(obj2[i][0])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
+
+
+function isPrimitiveType(obj) {
+ var typ = typeof obj;
+ return ((obj == null)
+ || (typ != "object")
+ || (obj instanceof Array && obj[0] == "VOID")
+ || (obj instanceof Array && obj[0] == "NUMBER-LITERAL")
+ );
+}
+
+function getRefID(obj) {
+ assert(obj[0] == 'REF');
+ return obj[1];
+}
+
+
+// Annotation bubbles
+
+var qtipShared = {
+ show: {
+ ready: true, // show on document.ready instead of on mouseenter
+ delay: 0,
+ event: null,
+ effect: function() {$(this).show();}, // don't do any fancy fading because it screws up with scrolling
+ },
+ hide: {
+ fixed: true,
+ event: null,
+ effect: function() {$(this).hide();}, // don't do any fancy fading because it screws up with scrolling
+ },
+ style: {
+ classes: 'ui-tooltip-pgbootstrap', // my own customized version of the bootstrap style
+ },
+};
+
+
+// a speech bubble annotation to attach to:
+// 'codeline' - a line of code
+// 'frame' - a stack frame
+// 'variable' - a variable within a stack frame
+// 'object' - an object on the heap
+// (as determined by the 'type' param)
+//
+// domID is the ID of the element to attach to (without the leading '#' sign)
+function AnnotationBubble(parentViz, type, domID) {
+ this.parentViz = parentViz;
+
+ this.domID = domID;
+ this.hashID = '#' + domID;
+
+ this.type = type;
+
+ if (type == 'codeline') {
+ this.my = 'left center';
+ this.at = 'right center';
+ }
+ else if (type == 'frame') {
+ this.my = 'right center';
+ this.at = 'left center';
+ }
+ else if (type == 'variable') {
+ this.my = 'right center';
+ this.at = 'left center';
+ }
+ else if (type == 'object') {
+ this.my = 'bottom left';
+ this.at = 'top center';
+ }
+ else {
+ assert(false);
+ }
+
+ // possible states:
+ // 'invisible'
+ // 'edit'
+ // 'view'
+ // 'minimized'
+ // 'stub'
+ this.state = 'invisible';
+
+ this.text = ''; // the actual contents of the annotation bubble
+
+ this.qtipHidden = false; // is there a qtip object present but hidden? (TODO: kinda confusing)
+}
+
+AnnotationBubble.prototype.showStub = function() {
+ assert(this.state == 'invisible' || this.state == 'edit');
+ assert(this.text == '');
+
+ var myBubble = this; // to avoid name clashes with 'this' in inner scopes
+
+ // destroy then create a new tip:
+ this.destroyQTip();
+ $(this.hashID).qtip($.extend({}, qtipShared, {
+ content: ' ',
+ id: this.domID,
+ position: {
+ my: this.my,
+ at: this.at,
+ adjust: {
+ x: (myBubble.type == 'codeline' ? -6 : 0), // shift codeline tips over a bit for aesthetics
+ },
+ effect: null, // disable all cutesy animations
+ },
+ style: {
+ classes: 'ui-tooltip-pgbootstrap ui-tooltip-pgbootstrap-stub'
+ }
+ }));
+
+
+ $(this.qTipID())
+ .unbind('click') // unbind all old handlers
+ .click(function() {
+ myBubble.showEditor();
+ });
+
+ this.state = 'stub';
+}
+
+AnnotationBubble.prototype.showEditor = function() {
+ assert(this.state == 'stub' || this.state == 'view' || this.state == 'minimized');
+
+ var myBubble = this; // to avoid name clashes with 'this' in inner scopes
+
+ var ta = '';
+
+ // destroy then create a new tip:
+ this.destroyQTip();
+ $(this.hashID).qtip($.extend({}, qtipShared, {
+ content: ta,
+ id: this.domID,
+ position: {
+ my: this.my,
+ at: this.at,
+ adjust: {
+ x: (myBubble.type == 'codeline' ? -6 : 0), // shift codeline tips over a bit for aesthetics
+ },
+ effect: null, // disable all cutesy animations
+ }
+ }));
+
+
+ $(this.qTipContentID()).find('textarea.bubbleInputText')
+ // set handler when the textarea loses focus
+ .blur(function() {
+ myBubble.text = $(this).val().trim(); // strip all leading and trailing spaces
+
+ if (myBubble.text) {
+ myBubble.showViewer();
+ }
+ else {
+ myBubble.showStub();
+ }
+ })
+ .focus(); // grab focus so that the user can start typing right away!
+
+ this.state = 'edit';
+}
+
+
+AnnotationBubble.prototype.bindViewerClickHandler = function() {
+ var myBubble = this;
+
+ $(this.qTipID())
+ .unbind('click') // unbind all old handlers
+ .click(function() {
+ if (myBubble.parentViz.editAnnotationMode) {
+ myBubble.showEditor();
+ }
+ else {
+ myBubble.minimizeViewer();
+ }
+ });
+}
+
+AnnotationBubble.prototype.showViewer = function() {
+ assert(this.state == 'edit' || this.state == 'invisible');
+ assert(this.text); // must be non-empty!
+
+ var myBubble = this;
+ // destroy then create a new tip:
+ this.destroyQTip();
+ $(this.hashID).qtip($.extend({}, qtipShared, {
+ content: htmlsanitize(this.text), // help prevent HTML/JS injection attacks
+ id: this.domID,
+ position: {
+ my: this.my,
+ at: this.at,
+ adjust: {
+ x: (myBubble.type == 'codeline' ? -6 : 0), // shift codeline tips over a bit for aesthetics
+ },
+ effect: null, // disable all cutesy animations
+ }
+ }));
+
+ this.bindViewerClickHandler();
+ this.state = 'view';
+}
+
+
+AnnotationBubble.prototype.minimizeViewer = function() {
+ assert(this.state == 'view');
+
+ var myBubble = this;
+
+ $(this.hashID).qtip('option', 'content.text', ' '); //hack to "minimize" its size
+
+ $(this.qTipID())
+ .unbind('click') // unbind all old handlers
+ .click(function() {
+ if (myBubble.parentViz.editAnnotationMode) {
+ myBubble.showEditor();
+ }
+ else {
+ myBubble.restoreViewer();
+ }
+ });
+
+ this.state = 'minimized';
+}
+
+AnnotationBubble.prototype.restoreViewer = function() {
+ assert(this.state == 'minimized');
+ $(this.hashID).qtip('option', 'content.text', htmlsanitize(this.text)); // help prevent HTML/JS injection attacks
+ this.bindViewerClickHandler();
+ this.state = 'view';
+}
+
+// NB: actually DESTROYS the QTip object
+AnnotationBubble.prototype.makeInvisible = function() {
+ assert(this.state == 'stub' || this.state == 'edit');
+ this.destroyQTip();
+ this.state = 'invisible';
+}
+
+
+AnnotationBubble.prototype.destroyQTip = function() {
+ $(this.hashID).qtip('destroy');
+}
+
+AnnotationBubble.prototype.qTipContentID = function() {
+ return '#ui-tooltip-' + this.domID + '-content';
+}
+
+AnnotationBubble.prototype.qTipID = function() {
+ return '#ui-tooltip-' + this.domID;
+}
+
+
+AnnotationBubble.prototype.enterEditMode = function() {
+ assert(this.parentViz.editAnnotationMode);
+ if (this.state == 'invisible') {
+ this.showStub();
+
+ if (this.type == 'codeline') {
+ this.redrawCodelineBubble();
+ }
+ }
+}
+
+AnnotationBubble.prototype.enterViewMode = function() {
+ assert(!this.parentViz.editAnnotationMode);
+ if (this.state == 'stub') {
+ this.makeInvisible();
+ }
+ else if (this.state == 'edit') {
+ this.text = $(this.qTipContentID()).find('textarea.bubbleInputText').val().trim(); // strip all leading and trailing spaces
+
+ if (this.text) {
+ this.showViewer();
+
+ if (this.type == 'codeline') {
+ this.redrawCodelineBubble();
+ }
+ }
+ else {
+ this.makeInvisible();
+ }
+ }
+ else if (this.state == 'invisible') {
+ // this happens when, say, you first enter View Mode
+ if (this.text) {
+ this.showViewer();
+
+ if (this.type == 'codeline') {
+ this.redrawCodelineBubble();
+ }
+ }
+ }
+}
+
+AnnotationBubble.prototype.preseedText = function(txt) {
+ assert(this.state == 'invisible');
+ this.text = txt;
+}
+
+AnnotationBubble.prototype.redrawCodelineBubble = function() {
+ assert(this.type == 'codeline');
+
+ if (isOutputLineVisibleForBubbles(this.domID)) {
+ if (this.qtipHidden) {
+ $(this.hashID).qtip('show');
+ }
+ else {
+ $(this.hashID).qtip('reposition');
+ }
+
+ this.qtipHidden = false;
+ }
+ else {
+ $(this.hashID).qtip('hide');
+ this.qtipHidden = true;
+ }
+}
+
+AnnotationBubble.prototype.redrawBubble = function() {
+ $(this.hashID).qtip('reposition');
+}
+
+
+// NB: copy-and-paste from isOutputLineVisible with some minor tweaks
+function isOutputLineVisibleForBubbles(lineDivID) {
+ var pcod = $('#pyCodeOutputDiv');
+
+ var lineNoTd = $('#' + lineDivID);
+ var LO = lineNoTd.offset().top;
+
+ var PO = pcod.offset().top;
+ var ST = pcod.scrollTop();
+ var H = pcod.height();
+
+ // add a few pixels of fudge factor on the bottom end due to bottom scrollbar
+ return (PO <= LO) && (LO < (PO + H - 25));
+}
+
+
+// popup question dialog code from Brad Miller
+
+// inputId is the ID of the input element
+// divId is the div that containsthe visualizer
+// answer is a dotted form of an attribute that lives in the curEntry of the trace
+// So if we want to ask for the value of a global variable we would say 'globals.a'
+// this allows us do do curTrace[i].globals.a But we do it in the loop below using the
+// [] operator.
+function traceQCheckMe(inputId, divId, answer) {
+ var vis = $("#"+divId).data("vis")
+ var i = vis.curInstr
+ var curEntry = vis.curTrace[i+1];
+ var ans = $('#'+inputId).val()
+ var attrs = answer.split(".")
+ var correctAns = curEntry;
+ for (j in attrs) {
+ correctAns = correctAns[attrs[j]]
+ }
+ feedbackElement = $("#" + divId + "_feedbacktext")
+ if (ans.length > 0 && ans == correctAns) {
+ feedbackElement.html('Correct')
+ } else {
+ feedbackElement.html(vis.curTrace[i].question.feedback)
+ }
+
+}
+
+function closeModal(divId) {
+ $.modal.close()
+ $("#"+divId).data("vis").stepForward();
+}
diff --git a/example-code/.gitignore b/example-code/.gitignore
new file mode 100644
index 0000000..6ed1f42
--- /dev/null
+++ b/example-code/.gitignore
@@ -0,0 +1 @@
+Fakey.java
\ No newline at end of file
diff --git a/iframe-embed.html b/iframe-embed.html
new file mode 100644
index 0000000..62eb27f
--- /dev/null
+++ b/iframe-embed.html
@@ -0,0 +1,44 @@
+
+
+
+
+ Online Python Tutor - iframe embed page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.html b/index.html
index ded0c54..94cf717 100644
--- a/index.html
+++ b/index.html
@@ -74,13 +74,9 @@
-
+
-
-
@@ -115,7 +111,12 @@
+
+
+
@@ -342,8 +343,6 @@