/
betterexamples.js
267 lines (253 loc) · 9.84 KB
/
betterexamples.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* @author Willem Mulder
* @license CC-BYSA 3.0 Unported
*/
$(function() {
$("body").append("<style>.betterExampleNoSetHeight { height: auto !important; }</style>");
});
// Name = sandbox.js?
BetterExamples = {
pointers : {},
documentLineNumberOfFirstInputLine : {},
instances : {},
getInstance : function(id) {
return this.instances[id];
}
};
BetterExample = function(inputelm, outputelm, options) {
var options = options || {};
var id = options.id || "instance_" + Math.floor(Math.random()*100000000000);
var inDebug = false;
var sleepingForDebug = false;
var inputelm = $(inputelm);
var outputelm = $(outputelm);
// use wrap="off" because white-space: nowrap does not function correctly in IE
var inputInnerElm = $("<textarea style='outline: none; z-index:10; position: relative; width: 100%; height: 95%; background: rgba(255,255,255,0); border: none; text-decoration: none; overflow-y: hidden; overflow-x: auto;' wrap='off'></textarea>");
inputInnerElm.html(inputelm.html());
inputelm.html(inputInnerElm);
inputelm.on("keyup", function(event) {
var firstHeight = inputelm.find("textarea").height();
inputelm.find("textarea").css("height", inputelm.find("textarea").get(0).scrollHeight + "px");
// If textarea was expanded, scroll to bottom of parent element
if (inputelm.find("textarea").height() > firstHeight) {
inputelm.parent().animate({
scrollTop: inputelm.find("textarea").height()
}, 0);
}
facade.clearOutput(true);
});
var functionBackups = [];
var inputText = inputelm.html();
inputelm.addClass("betterExampleNoSetHeight");
inputelm.html("one line");
var inputLineHeight = inputelm.innerHeight();
inputelm.html(inputText);
inputelm.removeClass("betterExampleNoSetHeight");
function visit(node, visitFunction, parentsList) {
var parents = (typeof parentsList === 'undefined') ? [] : parentsList;
visitFunction = visitFunction || false;
if (visitFunction && visitFunction(node,parentsList) == false) {
return;
}
for (key in node) {
if (node.hasOwnProperty(key)) {
child = node[key];
path = [ node ];
path.push(parents);
if (typeof child === 'object' && child !== null) {
visit(child, visitFunction, path);
}
}
}
};
function restoreFunctions() {
// Restore the altered functions
for(functionID in functionBackups) {
window[functionID] = functionBackups[functionID];
if (functionID == "console.log") {
// TODO make this generic
console.log = functionBackups[functionID];
}
}
}
function positionMessages() {
// Loop over the error messages and show as much of their info as possible (i.e. without overlapping other messages)
outputelm.find(".betterExamplesLine").each(function() {
var next = $(this).nextAll(".betterExamplesLine");
if (next.length > 0) {
var space = $(next).attr("line") - $(this).attr("line");
// See how much height the element would take
$(this).css("height", "auto");
// Now restrict the height if the height=auto would take up more height than there is space
if ($(this).height() > (space * inputLineHeight)) {
$(this).css("height", (space * inputLineHeight) + "px");
}
} else {
$(this).css("height", "auto"); // unlimited space
}
});
}
function drawDebugBar() {
ielm = inputelm.find(".betterExamplesDebugBar").first();
oelm = outputelm.find(".betterExamplesDebugBar").first();
var line = BetterExamples.pointers[id];
if (ielm.length == 0) {
var start = "<div class='betterExamplesDebugBar' line='" + line + "' style='background: #ffa; position:absolute; left: 0px; top:" + (inputLineHeight*(line-1)) + "px; height: " + inputLineHeight + "px; display: block; width: 100%; z-index: 2; overflow: hidden;'>";
var end = "</div>";
outputelm.append(start+" "+end);
inputelm.prepend(start+" "+end);
} else {
// existing bar
ielm.add(oelm).attr("line", line);
ielm.add(oelm).css("top", (inputLineHeight*(line-1)) + "px");
}
}
function removeDebugBar() {
outputelm.find(".betterExamplesDebugBar").remove();
inputelm.find(".betterExamplesDebugBar").remove();
}
var facade = {
"debug" : function() {
inDebug = true;
this.run();
inDebug = false;
removeDebugBar();
},
"run" : function() {
// Set linePointer to the first line
BetterExamples.pointers[id] = 1;
// Set alert or log functions to redirect to the output window
var instance = this;
functionBackups["alert"] = window.alert;
window.alert = function(output) {
instance.log(output);
}
if (typeof(console) != "undefined" && typeof(console.log) != "undefined") {
functionBackups["console.log"] = console.log;
console.log = alert;
}
// Set error function to redirect to 'alert'
functionBackups["onerror"] = window.onerror;
window.onerror = function (message, url, lineNo) {
var lineIndex = message.indexOf("Line");
if (lineIndex > -1) {
// Syntax error
var until = message.indexOf(":",lineIndex);
lineNo = message.slice(lineIndex+4, until);
message = message.slice(until+2);
} else {
// Runtime error
if (message.indexOf("Uncaught") === 0) {
message = message.slice(message.indexOf(":", 8)+2);
}
lineNo = BetterExamples.pointers[id];
}
// Firefox : if (message.indexOf("Line") === 0) {
// Webkit : if (message.indexOf("Uncaught Error: Line") === 0) {
// Opera : if (message.indexOf("Uncaught exception: Error: Line") === 0) {
BetterExamples.pointers[id] = lineNo;
if (lineIndex == -1) {
facade.log(message, "Runtime error");
// Restore functions and position messages. There will be no extra messages anyway, since a runtime error stops execution.
restoreFunctions();
positionMessages();
} else {
facade.log(message, "Syntax error");
}
return true; // Error was handled
}
// Remove output
this.clearOutput();
// Run the input and render the output
var input = inputelm.find("textarea").val();
// find which lines contain alert or log commands
var analysis = esprima.parse(input,{ range: true, loc: true });
var locations = [];
// Set pointers for every VariableDeclaration or ExpressionStatement
visit(analysis,function(node, parentsList) {
if (node.type === "VariableDeclaration" || node.type === "ExpressionStatement") {
locations.push({range: node.range, location: node.loc});
}
});
// Insert pointer function at the specified locations
var charactersInserted = 0;
for(locationId in locations) {
var range = locations[locationId].range;
var location = locations[locationId].location;
var func = "BetterExamples.pointers['" + id + "'] = "+location.start.line+"; BetterExamples.getInstance('" + id + "').enterStep();";
var length = func.length;
var firstPart = input.slice(0,range[0]+charactersInserted);
var secondPart = input.slice(range[0]+charactersInserted);
input = firstPart + func + secondPart;
charactersInserted += length;
}
eval(input);
// If a runtime-error has occurred in the evaluated code, we won't get here so the below will not be executed. The window.onerror will ensure that it will happen anyways.
restoreFunctions();
positionMessages();
},
"clear" : function() {
inputInnerElm.html("");
this.clearOutput();
},
"clearOutput" : function(withFade) {
if (withFade) {
inputelm.find(".betterExamplesLine").fadeOut(function() { inputelm.find(".betterExamplesLine").remove(); });
outputelm.find("*").fadeOut(function() { outputelm.html(""); });
if (outputelm.find("*").size() == 0) {
outputelm.html("");
}
} else {
inputelm.find(".betterExamplesLine").remove();
outputelm.html("");
}
},
"log" : function(obj,type) {
var type = type || typeof(obj);
type = type.charAt(0).toUpperCase() + type.slice(1); // Capitalize first character
var extraStyle = "";
if (type.indexOf("error")>-1) { extraStyle = "background: #CC1919; color: #fff;" }
var line = BetterExamples.pointers[id];
var start = function(zindex) {
return "<div class='betterExamplesLine' line='" + line + "' style='background: #eef; position:absolute; left: 0px; top:" + (inputLineHeight*(line-1)) + "px; height: " + inputLineHeight + "px; display: block; width: 100%; z-index: " + zindex + "; overflow: hidden;' onMouseOver='$(this).attr(\"backupZindex\",$(this).css(\"z-index\")); $(this).attr(\"backupHeight\",$(this).css(\"height\")); $(this).css(\"height\",\"auto\").css(\"z-index\",\"100\")' onMouseOut='$(this).css(\"height\",$(this).attr(\"backupHeight\")).css(\"z-index\",$(this).attr(\"backupZindex\"))'>";
}
var output = "<div style='width: 130px; background: #dde; "+ extraStyle + " display: inline-block; z-index: 3;'>" + type + "</div> ";
output += JSON.stringify(obj, null, 4);
var end = "</div>";
outputelm.append(start(3) + output + end);
inputelm.prepend(start(1) + " " + end);
},
"getId" : function() {
return id;
},
"setId" : function(newId) {
delete BetterExamples.instances[id];
id = newId;
BetterExamples.instances[id] = facade;
},
"enterStep" : function() {
// If we are in debug mode, then 'sleep' until we can get to the next step
if (inDebug) {
drawDebugBar();
//sleepingForDebug = true; // To be used with while loops if that turns out to be more useful
var returnValue = window.showModalDialog("js/debug.html",null,"dialogleft:0;dialogtop:0;dialogheight:20;dialogwidth:200"); // UI will sleep as long as the modal dialog is open
if (returnValue == "nextStep") {
// continue
} else if (returnValue == "exitDebug") {
inDebug = false;
removeDebugBar();
// continue
} else {
inDebug = false; // If anything goes wrong, just get out of debug and don't open the window again and again and again
removeDebugBar();
// continue
}
}
},
"goToNextStep" : function() {
//sleepingForDebug = false;
}
};
BetterExamples.instances[id] = facade;
return facade;
}