whalesong/js-assembler/runtime-src/baselib-format.js
2011-09-27 17:24:05 -04:00

484 lines
16 KiB
JavaScript

/*jslint browser: true, undef: false, unparam: true, sub: true, vars: true, white: true, plusplus: true, maxerr: 50, indent: 4 */
// Formatting library.
// Produces string and DOM representations of values.
//
/*global $*/
(function(baselib, $) {
'use strict';
var exports = {};
baselib.format = exports;
var replaceUnprintableStringChars = function(s) {
var ret = [], i;
for (i = 0; i < s.length; i++) {
var val = s.charCodeAt(i);
switch(val) {
case 7: ret.push('\\a'); break;
case 8: ret.push('\\b'); break;
case 9: ret.push('\\t'); break;
case 10: ret.push('\\n'); break;
case 11: ret.push('\\v'); break;
case 12: ret.push('\\f'); break;
case 13: ret.push('\\r'); break;
case 34: ret.push('\\"'); break;
case 92: ret.push('\\\\'); break;
default: if (val >= 32 && val <= 126) {
ret.push( s.charAt(i) );
}
else {
var numStr = val.toString(16).toUpperCase();
while (numStr.length < 4) {
numStr = '0' + numStr;
}
ret.push('\\u' + numStr);
}
break;
}
}
return ret.join('');
};
var escapeString = function(s) {
return '"' + replaceUnprintableStringChars(s) + '"';
};
// toWrittenString: Any Hashtable -> String
var toWrittenString = function(x, cache) {
if (! cache) {
cache = baselib.hashes.makeLowLevelEqHash();
}
if (x === null) {
return "null";
}
if (x === true) { return "true"; }
if (x === false) { return "false"; }
if (typeof(x) === 'object') {
if (cache.containsKey(x)) {
return "...";
}
}
if (x === undefined) {
return "#<undefined>";
}
if (typeof(x) === 'string') {
return escapeString(x.toString());
}
if (baselib.functions.isProcedure(x)) {
return '#<procedure:' + x.displayName + '>';
}
if (typeof(x) !== 'object' && typeof(x) !== 'function') {
return x.toString();
}
var returnVal;
if (x.toWrittenString) {
returnVal = x.toWrittenString(cache);
} else if (x.toDisplayedString) {
returnVal = x.toDisplayedString(cache);
} else {
returnVal = x.toString();
}
return returnVal;
};
// toDisplayedString: Any Hashtable -> String
var toDisplayedString = function(x, cache) {
if (! cache) {
cache = baselib.hashes.makeLowLevelEqHash();
}
if (x === null) {
return "null";
}
if (x === true) { return "true"; }
if (x === false) { return "false"; }
if (typeof(x) === 'object') {
if (cache.containsKey(x)) {
return "...";
}
}
if (x === undefined || x === null) {
return "#<undefined>";
}
if (typeof(x) === 'string') {
return x;
}
if (baselib.functions.isProcedure(x)) {
return '#<procedure:' + x.displayName + '>';
}
if (typeof(x) !== 'object' && typeof(x) !== 'function') {
return x.toString();
}
var returnVal;
if (x.toDisplayedString) {
returnVal = x.toDisplayedString(cache);
} else if (x.toWrittenString) {
returnVal = x.toWrittenString(cache);
} else {
returnVal = x.toString();
}
return returnVal;
};
var formatRegexp1 = new RegExp('~[sSaA]', 'g');
var formatRegexp2 = new RegExp("~[sSaAnevE%~]", "g");
// format: string [X ...] string -> string
// String formatting. If an exception occurs, throws
// a plain Error whose message describes the formatting error.
var format = function(formatStr, args, functionName) {
var throwFormatError = function() {
functionName = functionName || 'format';
var matches = formatStr.match(formatRegexp1);
var expectedNumberOfArgs = (matches === null ? 0 : matches.length);
var errorStrBuffer = [functionName + ': format string requires ' + expectedNumberOfArgs
+ ' arguments, given ' + args.length + '; arguments were:',
toWrittenString(formatStr)];
var i;
for (i = 0; i < args.length; i++) {
errorStrBuffer.push( toWrittenString(args[i]) );
}
throw new Error(errorStrBuffer.join(' '));
};
var buffer = args.slice(0);
var onTemplate = function(s) {
if (s === "~~") {
return "~";
} else if (s === '~n' || s === '~%') {
return "\n";
} else if (s === '~s' || s === "~S") {
if (buffer.length === 0) {
throwFormatError();
}
return toWrittenString(buffer.shift());
} else if (s === '~e' || s === "~E") {
// FIXME: we don't yet have support for the error-print
// handler, and currently treat ~e just like ~s.
if (buffer.length === 0) {
throwFormatError();
}
return toWrittenString(buffer.shift());
}
else if (s === '~v') {
if (buffer.length === 0) {
throwFormatError();
}
// fprintf must do something more interesting here by
// printing the dom representation directly...
return toWrittenString(buffer.shift());
} else if (s === '~a' || s === "~A") {
if (buffer.length === 0) {
throwFormatError();
}
return toDisplayedString(buffer.shift());
} else {
throw new Error(functionName +
': string.replace matched invalid regexp');
}
};
var result = formatStr.replace(formatRegexp2, onTemplate);
if (buffer.length > 0) {
throwFormatError();
}
return result;
};
var ToDomNodeParameters = function(params) {
if (! params) { params = {}; }
var k;
for (k in params) {
if (params.hasOwnProperty(k)) {
this[k] = params[k];
}
}
if (this.cache === undefined) {
this.cache = baselib.hashes.makeLowLevelEqHash();
}
if (this.cycles === undefined) {
this.cycles = baselib.hashes.makeLowLevelEqHash();
}
if (this.depth === undefined) {
this.depth = 0;
}
if (this.objectCounter === undefined) {
this.objectCounter = 0;
}
};
ToDomNodeParameters.prototype.incrementDepth = function() {
return new ToDomNodeParameters({ mode : this.mode,
depth: this.depth + 1,
cache: this.cache,
cycles: this.cycles,
objectCounter: this.objectCounter });
};
// getMode: -> (U "print" "display" "write" "constructor")
ToDomNodeParameters.prototype.getMode = function() {
if (this.mode) {
return this.mode;
}
return 'print';
};
ToDomNodeParameters.prototype.getDepth = function(x) {
return this.depth;
};
ToDomNodeParameters.prototype.containsKey = function(x) {
return this.cache.containsKey(x);
};
ToDomNodeParameters.prototype.seesOldCycle = function(x) {
return this.cycles.containsKey(x);
};
ToDomNodeParameters.prototype.get = function(x) {
return this.cache.get(x);
};
ToDomNodeParameters.prototype.remove = function(x) {
return this.cache.remove(x);
};
ToDomNodeParameters.prototype.put = function(x, v) {
return this.cache.put(x, v);
};
ToDomNodeParameters.prototype.recur = function(x) {
return toDomNode(x, this.incrementDepth());
};
// rationalToDomNode: rational -> dom-node
var rationalToDomNode = function(n) {
var repeatingDecimalNode = document.createElement("span");
var chunks = baselib.numbers.toRepeatingDecimal(baselib.numbers.numerator(n),
baselib.numbers.denominator(n),
{limit: 25});
repeatingDecimalNode.appendChild(document.createTextNode(chunks[0] + '.'));
repeatingDecimalNode.appendChild(document.createTextNode(chunks[1]));
if (chunks[2] === '...') {
repeatingDecimalNode.appendChild(
document.createTextNode(chunks[2]));
} else if (chunks[2] !== '0') {
var overlineSpan = document.createElement("span");
overlineSpan.style.textDecoration = 'overline';
overlineSpan.appendChild(document.createTextNode(chunks[2]));
repeatingDecimalNode.appendChild(overlineSpan);
}
var fractionalNode = document.createElement("span");
var numeratorNode = document.createElement("sup");
numeratorNode.appendChild(document.createTextNode(String(baselib.numbers.numerator(n))));
var denominatorNode = document.createElement("sub");
denominatorNode.appendChild(document.createTextNode(String(baselib.numbers.denominator(n))));
fractionalNode.appendChild(numeratorNode);
fractionalNode.appendChild(document.createTextNode("/"));
fractionalNode.appendChild(denominatorNode);
var numberNode = document.createElement("span");
numberNode.appendChild(repeatingDecimalNode);
numberNode.appendChild(fractionalNode);
fractionalNode.style['display'] = 'none';
var showingRepeating = true;
numberNode.onclick = function(e) {
showingRepeating = !showingRepeating;
repeatingDecimalNode.style['display'] =
(showingRepeating ? 'inline' : 'none');
fractionalNode.style['display'] =
(!showingRepeating ? 'inline' : 'none');
};
numberNode.style['cursor'] = 'pointer';
return numberNode;
};
// numberToDomNode: jsnum -> dom
// Given a jsnum, produces a dom-node representation.
var numberToDomNode = function(n, params) {
var node;
if (baselib.numbers.isExact(n)) {
if (baselib.numbers.isInteger(n)) {
node = document.createElement("span");
node.appendChild(document.createTextNode(n.toString()));
return node;
} else if (baselib.numbers.isRational(n)) {
return rationalToDomNode(n);
} else if (baselib.numbers.isComplex(n)) {
node = document.createElement("span");
node.appendChild(document.createTextNode(n.toString()));
return node;
} else {
node = document.createElement("span");
node.appendChild(document.createTextNode(n.toString()));
return node;
}
} else {
node = document.createElement("span");
node.appendChild(document.createTextNode(n.toString()));
return node;
}
};
var coerseToParams = function(params) {
if (params === 'write') {
params = new ToDomNodeParameters({'mode' : 'write'});
} else if (params === 'print') {
params = new ToDomNodeParameters({'mode' : 'print'});
} else if (params === 'display') {
params = new ToDomNodeParameters({'mode' : 'display'});
} else if (params === 'constructor') {
params = new ToDomNodeParameters({'mode' : 'constructor'});
} else {
params = params || new ToDomNodeParameters({'mode' : 'display'});
}
return params;
};
// toDomNode: scheme-value -> dom-node
var toDomNode = function(x, params) {
var node, retval;
params = coerseToParams(params);
if (x === null) {
node = document.createElement("span");
node.appendChild(document.createTextNode("#<null>"));
$(node).addClass("null");
return node;
}
if (x === undefined) {
node = document.createElement("span");
node.appendChild(document.createTextNode("#<undefined>"));
$(node).addClass("undefined");
return node;
}
if (baselib.numbers.isSchemeNumber(x)) {
node = numberToDomNode(x, params);
$(node).addClass("number");
return node;
}
if (typeof(x) === 'string') {
var wrapper = document.createElement("span");
wrapper.style["white-space"] = "pre";
if (params.getMode() === 'write' || params.getMode() === 'print' || params.getMode() === 'constructor') {
node = document.createTextNode(toWrittenString(x));
} else {
node = document.createTextNode(toDisplayedString(x));
}
wrapper.appendChild(node);
$(wrapper).addClass("string");
return wrapper;
}
if (x === true || x === false) {
node = document.createElement("span");
node.appendChild(document.createTextNode(x ? "true" : "false"));
$(node).addClass("boolean");
return node;
}
if (baselib.functions.isProcedure(x)) {
node = document.createElement("span");
node.appendChild(document.createTextNode('#<procedure:' + x.displayName + '>'));
$(node).addClass("procedure");
return node;
}
if (typeof(x) !== 'object') {
node = document.createElement("span");
node.appendChild(document.createTextNode(x.toString()));
return node;
}
if (x.nodeType) {
return x;
}
// Otherwise, we know the value is an object.
// If we're along a print path with a loop, we need to stop
// and return the key.
if (params.seesOldCycle(x)) {
node = document.createElement("span");
node.appendChild(document.createTextNode("#" + params.cycles.get(x) + "#"));
$(node).addClass("cycle");
return node;
}
// If we see a fresh cycle, register it.
if (params.containsKey(x)) {
$('<span/>').text('#' + params.objectCounter +'=')
.prependTo(params.get(x));
params.cycles.put(x, params.objectCounter);
params.objectCounter++;
node = document.createElement("span");
node.appendChild(document.createTextNode("#" + params.cycles.get(x) + "#"));
$(node).addClass("cycle");
return node;
}
node = document.createElement("span");
params.put(x, node);
if (x.toDomNode) {
node.appendChild(x.toDomNode(params));
} else if (params.getMode() === 'write' && x.toWrittenString) {
node.appendChild(document.createTextNode(
x.toWrittenString(params)));
} else if (params.getMode() === 'display' && x.toDisplayedString) {
node.appendChild(document.createTextNode(
x.toDisplayedString(params)));
} else {
node.appendChild(document.createTextNode(x.toString()));
}
params.remove(x);
return node;
};
//////////////////////////////////////////////////////////////////////
exports.ToDomNodeParameters = ToDomNodeParameters;
exports.format = format;
exports.toWrittenString = toWrittenString;
exports.toDisplayedString = toDisplayedString;
exports.toDomNode = toDomNode;
exports.escapeString = escapeString;
}(this.plt.baselib, $));