1 2 /** @fileOverview Helper functions for working with strings and easily formatting objects as HTML. 3 * 4 * @example 5 var person = { firstName: "Ben", lastName: "Bitfiddler" }; 6 person.toString = function() { 7 return this.firstName + " " + this.lastName; 8 } 9 print(person); // prints "Ben Bitfiddler" 10 */ 11 12 13 /* 14 * Helper function that converts a raw string to an HTML string, with 15 * character entities replaced by appropriate HTML codes, and newlines 16 * rentered as BRs. 17 * 18 * <p>A more general version of this function is toHTML(), which can operate 19 * on not just strings, but any object. 20 * 21 * @param {string} str the raw string 22 * @return {string} HTML-formatted string 23 */ 24 function _stringToHTML(str) { 25 // TODO: use an array and call join at the end instead of 26 // concatenation as a performance improvements? 27 var result = ""; 28 var lastCharBlank = false; 29 var len = str.length; 30 for(var i=0;i<len;i++) { 31 var c = str[i]; 32 if (c == ' ') { 33 // every second consecutive space becomes a 34 if (lastCharBlank) { 35 lastCharBlank = false; 36 result += ' '; 37 } 38 else { 39 lastCharBlank = true; 40 result += ' '; 41 } 42 } else { 43 lastCharBlank = false; 44 if (c == '&') result += '&'; 45 else if (c == '<') result += '<'; 46 else if (c == '>') result += '>'; 47 else if (c == '\n') result += '<br/>\n'; 48 else if (c == '\t') { 49 for(var j=1;j<=7;j++) { 50 result += ' '; 51 } 52 result += ' '; 53 } 54 else { 55 var code = c.charCodeAt(0); 56 if (code < 128) { 57 result += c; 58 } 59 else { 60 // use character code 61 result += (""+code+";"); 62 } 63 } 64 } 65 } 66 return result; 67 } 68 69 // used to convert an object to HTML when the object does not have a 70 // toHTML method. 71 // 72 function _coerceObjectToHTML(obj) { 73 var t = TABLE({border: 1, cellpadding: 2, cellspacing: 0}); 74 eachProperty(obj, function(name, value) { 75 t.push(TR(TH(String(name)), TD(String(value)))); 76 }); 77 return toHTML(t); 78 } 79 80 // Converts an array to an HTML list by listing its properties and 81 // recursively converting the values to HTML by calling toHTML() on 82 // each of them. 83 function _objectToOL(obj) { 84 var l = OL(); 85 eachProperty(obj, function(name, value) { 86 l.push(LI({value: name}, value)); 87 }); 88 return l; 89 } 90 91 function _sameProperties(obj1, obj2) { 92 if (typeof(obj1) != 'object' || typeof(obj2) != 'object') 93 return typeof(obj1) == typeof(obj2); 94 95 var mismatch = 0; 96 eachProperty(obj1, function(name) { 97 if (! obj2.hasOwnProperty(name)) { 98 mismatch++; 99 }}); 100 eachProperty(obj2, function(name) { 101 if (! obj1.hasOwnProperty(name)) { 102 mismatch++; 103 }}); 104 return mismatch < 2; 105 } 106 107 // 108 // for pretty-printing arrays. needs a lot of work. 109 // 110 function _arrayToHTML(a) { 111 if (a.length === 0) { 112 return ""; 113 } 114 if (typeof(a[0]) != 'object') { 115 return toHTML(_objectToOL(a)); 116 } else if (! _sameProperties(a[0], a[1])) { 117 return toHTML(_objectToOL(a)); 118 } else { 119 return appjet._internal.likeObjectsToHTML(function (f) { 120 a.forEach(function(value, i) { 121 f({index: i}, value, {}); 122 })}, null); 123 } 124 } 125 126 /** @ignore */ 127 128 // a foreaching function that takes three arguments: properties to put first, 129 // properties to put in the middle, and properties to put at the end. 130 // and a table header (with large colspan) 131 appjet._internal.likeObjectsToHTML = function(forEachFunction, tophead) { 132 objs = []; 133 prepnames = new StringSet(); 134 objpnames = new StringSet(); 135 postpnames = new StringSet(); 136 rows = [] 137 138 var t = TABLE({border: 1, cellpadding: 2, cellspacing: 0}); 139 var head = TR(); 140 if (tophead) 141 t.push(tophead); 142 t.push(head); 143 144 var butWaitTheresMore = false; 145 var howManyMore = 0; 146 147 forEachFunction(function(pre, o, post) { 148 if (objs.length >= 10) { 149 butWaitTheresMore = true; 150 howManyMore++; 151 return; 152 } 153 objs.push({pre: pre, o: o, post: post}); 154 var tr = TR() 155 rows.push(tr); 156 t.push(tr); 157 158 eachProperty(pre, function(name) { prepnames.add(name); }); 159 eachProperty(o, function(name) { objpnames.add(name); }); 160 eachProperty(post, function(name) { postpnames.add(name); }); 161 }); 162 var numpnames = 0; 163 var appendTDsForPropName = function (where) { 164 return function(name) { 165 numpnames++; 166 head.push(TH(name)); 167 for (var j = 0; j < objs.length; ++j) { 168 if (! (objs[j][where] === undefined) && ! (objs[j][where][name] === undefined)) 169 rows[j].push(TD(String(objs[j][where][name]))); 170 else 171 rows[j].push(TD()); 172 } 173 } 174 } 175 prepnames.forEach(appendTDsForPropName("pre")); 176 objpnames.forEach(appendTDsForPropName("o")); 177 postpnames.forEach(appendTDsForPropName("post")); 178 if (butWaitTheresMore) { 179 t.push(TR(TD({colspan: numpnames}, "..."+howManyMore+ 180 " additional element"+(howManyMore == 1 ? "" : "s")+" omitted..."))); 181 } 182 return toHTML(t); 183 } 184 185 186 //---------------------------------------------------------------- 187 // print calls 188 //---------------------------------------------------------------- 189 190 /** 191 * HTML-aware printing. This function prints its arguments to the 192 * body of the page. Printing a string will cause it to show up as-is 193 * on the screen (even if it contains HTML tags or angle-brackets). 194 * Printing an HTML tag object will cause it to be rendered on the 195 * screen. Printing a normal Javascript object or array will cause it to be rendered 196 * in an easily-readable HTML format. 197 * 198 * @param {*} thing1 any javascript type 199 * @param {*} thing2 any javascript type 200 * @param {*} etc ... 201 */ 202 function print(thing1, thing2, etc) { 203 var args = Array.prototype.slice.call(arguments); 204 205 args.forEach(function(x) { 206 _doWrite(toHTML(x)); 207 }); 208 } 209 210 function _doWrite(str) { 211 if (appjet.isShell) { 212 appjet._native.write(str); 213 } else { 214 page.body.write(str); 215 } 216 } 217 218 /** 219 * Like print(...), but prints its arguments inside an HTML p (paragraph) tag. 220 * 221 * @param {*} thing1 any javascript type 222 * @param {*} thing2 any javascript type 223 * @param {*} etc ... 224 */ 225 function printp(thing1, thing2, etc) { 226 var args = Array.prototype.slice.call(arguments); 227 args.unshift({}); // no HTML attributes, other args should not be taken as attributes 228 229 // newline before and after -- leaves empty line between printp's in the HTML source, 230 // but avoids a printp ever sharing a line with raw stuff, which could look ugly. 231 _doWrite("\n"); 232 print(P.apply(this, args)); 233 _doWrite("\n"); 234 } 235 236 /** 237 * Prints a string with any number of variables substituted in, as 238 * popularized by C's function of the same name. Some common substitutions: 239 * 240 * <ul><li>%d - an integer</li><li>%f - a floating-point number</li><li>%b - a boolean</li> 241 * <li>%s - a string</li></ul> 242 * 243 * <p>Each time one of these "slot" appears in your format string, the next argument is displayed 244 * according to the type of slot you specified. 245 * 246 * <p>AppJet supports <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/Formatter.html"> 247 * Java's specification of printf</a>, which has a ton of features, including selecting 248 * arguments out of order, formatting dates and times, and specifying how many characters 249 * wide each slot should be. 250 * 251 * @example 252 var x = 5; 253 printf("an integer: %d", x); 254 printf("Two strings: [%s] and [%s].", "string one", "string two"); 255 * 256 * @param {string} formatString 257 * @param {*} arg1 258 * @param {*} arg2 259 * @param {*} arg3 ... 260 */ 261 function printf(formatString, arg1, arg2, etc) { 262 print(sprintf.apply(this, arguments)); 263 } 264 265 /** 266 * Just like printf, but returns the string instead of printing it. 267 * @example 268 var result = sprintf("%f", Math.sqrt(2)); 269 print("The square root of two, as a string, is: ", result); 270 * 271 * @param {string} formatString 272 * @param {*} arg1 273 * @param {*} arg2 274 * @param {*} arg3 ... 275 */ 276 function sprintf(formatString, arg1, arg2, etc) { 277 if (typeof(formatString) != 'string') { 278 throw new Error('printf takes a string as the first argument.'); 279 } 280 var argList = []; 281 for (var i = 1; i < arguments.length; i++) { 282 if (arguments[i] instanceof Date) 283 argList.push(arguments[i].getTime()); 284 else 285 argList.push(arguments[i]) 286 } 287 return appjet._native.printf(formatString, argList); 288 }; 289 290 /** 291 * Replaces keys of data found in string with their corresponding values. 292 * 293 * <p>(Inspired by http://javascript.crockford.com/remedial.html) 294 * 295 * @example 296 var data = {name: "Aaron", age: 25, today: new Date()}; 297 print(supplant(data, """ 298 299 {name}'s age is {age} years, as of {today}. 300 301 """)); 302 303 * @param {object} data dictionary of values 304 * @param {string} str 305 * @return {string} str with keys of data replaced by their values 306 */ 307 function supplant(data, str) { 308 var s = str; 309 var o = data; 310 function rep(a, b) { 311 var r = o[b]; 312 if (typeof(r) != 'undefined') { 313 return r; 314 } else { 315 return a; 316 } 317 } 318 return s.replace(/{([^{}]*)}/g, rep); 319 }; 320 321 //---------------------------------------------------------------- 322 // raw printing 323 //---------------------------------------------------------------- 324 var _raw_prototype = object(Object.prototype); 325 _raw_prototype.toString = function() { return this._text; }; 326 _raw_prototype.toHTML = function() { return this._text; }; 327 328 /** 329 * Used for printing un-escaped HTML, such as your own HTML tags. 330 * 331 * <p>Normally, printing a string will cause it to be translated 332 * so that it appears the same on the screen as it did in your code. 333 * If you're writing your own HTML, you don't want it to be processed 334 * this way. Wrapping a string in html(...) by-passes normal printing behavior, 335 * so that print(html(" -- html goes here ---")) will write the HTML 336 * directly to the page. 337 * 338 * <p>If you want to mix your own HTML code with HTML code generated from a 339 * tag object, you can get the HTML for the tag by calling its toHTML(...) method. 340 * 341 * <p>Multiple arguments to html(...) will be concatenated into one string. 342 * 343 * @example 344 print(html(""" 345 <br /> 346 <br /> 347 <div><p>Here is some text inside a P inside a DIV.</p> 348 </div> 349 <br /> 350 """)); 351 * 352 * @param {string} text the raw text 353 * @return {object} an object which, when printed, prints the raw html text 354 */ 355 function html(text) { 356 var rawObj = object(_raw_prototype); 357 rawObj._text = Array.prototype.map.call(arguments, String).join(''); 358 return rawObj; 359 } 360 361 /** 362 * <p><b>The raw() function is deprecated. Now use <a href="#html">the 363 * html() function</a> instead. (It does the same thing).</b></p> 364 * 365 * @function 366 */ 367 function raw(text) { return html.apply(this, arguments); } 368 369 /** 370 * This function is used by print(...) to convert a string or object 371 * into nice-looking printable HTML. It may be useful in conjunction 372 * with raw(...) if you wish to work directly with HTML. 373 * 374 * <p>You can control how toHTML(...) (and therefore print(...)) behave on an object 375 * by giving that object a .toHTML() function. 376 * 377 * @param {*} x any javascript variable 378 * @return {string} html-formatted string 379 */ 380 function toHTML(x) { 381 if (typeof(x) == 'undefined') { 382 return 'undefined'; 383 } 384 if (x === null) { 385 return 'null'; 386 } 387 if (typeof x == "string") { 388 return _stringToHTML(x); 389 } 390 if (typeof(x.toHTML) == "function") { 391 return x.toHTML(); 392 } 393 if (typeof(x) == "xml") { 394 return _stringToHTML(x.toSource()); 395 } 396 if (x instanceof Array) { 397 return _arrayToHTML(x); 398 } 399 if (x instanceof Date) { 400 var pieces = x.toString().split(" "); 401 return pieces.slice(0, 5).join(' ') + ' ' + pieces[6]; 402 } 403 if (typeof(x) == "object") { 404 return _coerceObjectToHTML(x); 405 } 406 // TODO: add more types to auto-printing, such as functions, 407 // numbers, what else? 408 return _stringToHTML(""+x); 409 } 410 411 /** 412 * Helper function for printing a link (A HREF tag). 413 * 414 * @example 415 print(link("http://appjet.com")); 416 * 417 * @param {string} url an absolute or relative URL to link to 418 * @param {string} [optionalText] the text of the link, defaults to the url if absent 419 */ 420 421 function link(url, optionalText) { 422 if (optionalText === undefined) optionalText = url; 423 return A({href:url}, optionalText); 424 } 425 426 /** 427 * Helper fuction for printing a form. 428 * 429 * @example 430 if (request.path == "/bar") { 431 printp("Your name is: ", request.param("name")); 432 printp(link("/", "back")); 433 } else { 434 printp("Enter your name:"); 435 print(form("/bar", "name")); 436 } 437 * @param {string} url an absolute or relative URL to post the form data to 438 * @param {string} param1 the first form parameter name 439 * @param {string} param2 the second form parameter name 440 * @param {string} etc ... 441 */ 442 function form(path, param1, param2, etc) { 443 var f = FORM({method: "post", action: path}); 444 var args = Array.prototype.slice.call(arguments, 1) 445 args.forEach(function(param) { 446 if (args.length > 1) { 447 f.push(P(param, ": ", INPUT({name: param}))); 448 } else { 449 f.push(INPUT({type: "text", name: param})); 450 } 451 }); 452 f.push(INPUT({type: "submit", name: "Submit", value: "Submit"})); 453 return f; 454 } 455 456 /** 457 * Helper function for printing an image (an IMG tag). 458 * 459 * @example 460 print(image("http://i29.tinypic.com/vfwc60.gif")); 461 * 462 * @param {string} url an absolute or relative URL to an image. 463 */ 464 function image(url) { 465 return IMG({src:url}); 466 } 467