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