1 /**
  2  * @fileOverview The page object is what gets rendered by default at the end
  3  * of a request.
  4  */
  5 
  6 if (!appjet) {
  7   throw new Error('appjet library is required for page library.');
  8 }
  9 
 10 /**
 11  * The page object is what gets rendered by default at the end of a request. 
 12  * @type object
 13  * @constructor
 14  * @class The page object is what gets rendered by default at the end of a request.  
 15  */
 16 page = {};
 17 appjet._internal.page = page;
 18 
 19 // some defaults
 20 
 21 // TODO: turn _modes into a set.
 22 var _MODES = new StringSet('html', 'plain', 'facebook');
 23 
 24 var _mode = 'html';
 25 var _title = appjet.appName;
 26 var _icon = "http://apps.jgate.de/static/favicon.ico";
 27 var _showRenderTime = true;
 28 var _headbuffer = [];
 29 var _bodybuffer = [];
 30 
 31 /**
 32  * Changes the mode of response output to something other than
 33  * HTML.  For example, setMode("plain") will not print the default
 34  * AppJet HTML document structure (in fact it will not print
 35  * anything but your calls to print()).
 36  * If you setMode("plain"), you are on your own.
 37  *
 38  * The default output mode is "html", which creates a simple xhtml
 39  * document structure.
 40  *
 41  * @param {string} newMode one of ['html', 'plain']
 42  */
 43 page.setMode = function(newMode) {
 44   if (_MODES.contains(newMode)) {
 45     _mode = newMode;
 46   } else {
 47     throw new Error('Unknown page mode: '+newMode);
 48   }
 49 };
 50 
 51 /**
 52  * Sets the HTML title tag of the page.
 53  * @param {string} newTitle
 54  */
 55 page.setTitle = function(newTitle) {
 56   _title = newTitle;
 57 };
 58 
 59 /**
 60  * Sets the favicon of the page.
 61  * @param {string} url A URL pointing to the image to use as a favicon.
 62  */
 63 page.setFavicon = function(url) {
 64   _icon = url;
 65 }
 66 
 67 /**
 68  * Sets whether or not to print the number of milliseconds taken to render
 69  * the page in the page's footer.  Default is true.  Applies only to HTML mode.
 70  * @param {boolean} show
 71  */
 72 page.showRenderTime = function(show) {
 73   _showRenderTime = show;
 74 };
 75 
 76 /**
 77  * The body object handles the HTML body section. 
 78  * @type object
 79  * @constructor
 80  * @class The body object handles the HTML body section. 
 81  */
 82 page.body = {};
 83 
 84 /**
 85  * Append a raw string to the page's BODY section.
 86  * @param {string} rawText
 87  */
 88 page.body.write = function(rawText) {
 89   _bodybuffer.push(rawText);
 90 };
 91 
 92 /**
 93  * Gets the page's BODY section as written so far.
 94  */
 95 page.body.get = function() {
 96   return _bodybuffer.join('');
 97 }
 98 
 99 
100 /**
101  * The header object handles the HTML head section. 
102  * @type object
103  * @constructor
104  * @class The header object handles the HTML head section. 
105  */
106 page.head = {};
107 
108 /**
109  * Append raw text to the page's HEAD section.
110  * @example
111 page.head.write("""
112 <style>
113   p { font-size: 200%; }
114 </style>
115 """);
116  *
117  * @param {string} rawText
118  */
119 page.head.write = function(rawText) {
120   _headbuffer.push(rawText);
121 };
122 
123 /**
124  * Gets the page's HEAD section as written so far. Useful especially
125  * for page mode "plain", when a library might write to the HEAD
126  * section.
127  */
128 page.head.get = function() {
129   return _headbuffer.join('');
130 }
131 
132 //----------------------------------------------------------------
133 // rendering the standard page / templates
134 //----------------------------------------------------------------
135 
136 var _HTML_HEAD_TEMPLATE = """
137 <!DOCTYPE html PUBLIC
138           "-//W3C//DTD XHTML 1.0 Transitional//EN"
139           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
140 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
141 <head>
142 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
143 <meta http-equiv="Content-Language" content="en-us" />
144 <link rel="icon" href="{icon}" />
145 <title>{title}</title>
146 <style type="text/css">
147 /* begin appjet default css */
148 html { font-family: sans-serif; }
149 form.appjet_form { border: 1px solid #ccc; background-color: #eee; margin: 1.0em 0; }
150 form.appjet_form input { margin: 0.2em; }
151 /* end appjet default css */
152 
153 /* begin appjet:css section */
154 {customCss}
155 /* end appjet:css section */
156 </style>
157 
158 <!-- begin app-written head.  (use page.head.write() to put stuff here) -->
159 {customHead}
160 <!-- end app-written head -->
161 
162 <script type="text/javascript">
163 // begin appjet:client section
164 // <![CDATA[
165 {customJs}
166 // ]]>
167 // end appjet:client section
168 </script>
169 
170 </head>
171 """; // " // <-- unconfuse emacs
172 
173 var _HTML_FOOTER_TEMPLATE = """
174 <div style="clear: both;"><!-- --></div>
175 <div id="appjetfooter"
176      style="border-top: 1px solid #ccc; margin-top: 1.2em; font-family: verdana, helvetica, sans-serif; font-size: 0.8em;">
177 <div style="float: left;">
178   <span style="vertical-align: top;">
179     Powered by <a target="_blank" href="http://{mainDomain}/">JGate on AppJet</a>
180   </span>
181 </div>
182 <div style="float: right;">
183   <a target="_blank" href="{sourceLink}">source</a>
184 </div>
185 {statsDiv}
186 </div>
187 """; // "; // <-- unconfuse emacs
188 
189 var _HTML_FOOTER_SOURCE_PREVIEW = "http://{mainDomain}/platform/source?{appName}";
190 //var _HTML_FOOTER_SOURCE_PUBLISH = "http://source.{appName}.{mainDomain}/";
191 var _HTML_FOOTER_SOURCE_PUBLISH = "http://{mainDomain}/platform/source?{appName}";
192 
193 function _setAggressiveNoCacheHeaders() {
194   // be aggressive about not letting the response be cached.
195   function sh(k,v) { response.setHeader(k,v); }
196   sh('Expires', 'Sat, 18 Jun 1983 07:07:07 GMT');
197   sh('Last-Modified', (new Date()).toGMTString());
198   sh('Cache-Control', ('no-store, no-cache, must-revalidate, '+
199 		       'post-check=0, pre-check=0'));
200   sh('Pragma', 'no-cache');
201 }
202 
203 function _setAggressiveYesCacheHeaders() {
204   function sh(k,v) { response.setHeader(k,v); }
205   var futureDate = new Date();
206   futureDate.setTime(Date.now() + 315360000000);
207   sh('Expires', futureDate.toGMTString());
208   sh('Cache-Control', 'max-age=315360000');
209 }
210 
211 
212 /**
213  * Renders the entire current page to a string.  By default, this is called at the
214  * end of every request and printed to the response buffer, so usually there is
215  * no reason for you to call this.
216  *
217  * @return {string} rendered HTML string of the current page
218  */
219 page.render = function() {
220   // set proper cache headers
221   if (appjet._internal.cacheable === true) {
222     _setAggressiveYesCacheHeaders();
223   } else if (appjet._internal.cacheable === false) {
224     _setAggressiveNoCacheHeaders();
225   }
226   // determine output mode
227   if (_mode == "plain") { return _renderPlain(); }
228   if (_mode == "facebook") { return _renderFacebook(); }
229   if (_mode == "html") { return _renderHtml(); }
230   response.setHeader('Content-Type', 'text/plain');
231   return ["Unknown output mode: "+mode];
232 };
233 
234 function _renderPlain() {
235   return _bodybuffer;
236 }
237 
238 function _renderFacebook() {
239   return _bodybuffer.concat(_renderFooter());
240 }
241 
242 function _renderHtml() {
243   var result = [];
244   function getcustom(secnames) {
245     var custom = '';
246     var secList = [];
247     secnames.forEach(function(secname) {
248       secList = secList.concat(appjet._native.codeSection(secname));
249     });
250     secList.sort(function (a, b) { return a.startLine - b.startLine });
251     secList.forEach(function(sec) {
252       custom += sec.code;
253     });
254     return custom;
255   }
256   // header
257   var data = {
258     title: _title,
259     icon: _icon,
260     customCss: getcustom(['css']),
261     customJs: getcustom(['client', 'common']),
262     customHead: _headbuffer.join('')
263   };
264   result.push(supplant(data, _HTML_HEAD_TEMPLATE));
265   // body
266   result.push('<body>\n<!-- ********** begin body ********** -->\n\n');
267   _bodybuffer.forEach(function(x) {
268     result.push(x);
269   });
270   result.push('\n\n<!-- ********** end body ********** -->\n');
271   // footer
272   result = result.concat(_renderFooter());
273   result.push('</body>\n</html>');
274   return result;
275 }
276 
277 function _renderFooter() {
278   if (appjet.isTransient) return;
279   var statsDiv = '';
280   if (_showRenderTime) {
281     var seconds = ((new Date()).valueOf() - _tstart) / 1000.0;
282     var tstr = sprintf("%8s", sprintf("%.3f", seconds));
283 
284     var kbytes, memstr = "";
285     if (appjet.isPreview) {
286       kbytes = appjet._native.storage_usage()/1024;
287       if (kbytes > 0.01) {
288 	if (kbytes < 1024) {
289 	  memstr += sprintf("%5sKB", sprintf("%.0f", kbytes));
290 	} else if (kbytes < 10*1024) {
291 	  memstr += sprintf("%5sMB", sprintf("%.2f", kbytes/1024));
292 	} else {
293 	  memstr += sprintf("<span style='color: red'>%5sM</span>", sprintf("%.2f", kbytes/1024));
294 	}
295 	memstr = ' – using '+memstr+' of 10MB'
296       }
297     }
298     statsDiv =('<div style="text-align: center; color: #666;">'+
299 	       'rendered in '+ tstr + 's'+memstr+
300 	       '</div>');
301   }
302 
303   var data = {
304     mainDomain: appjet.mainDomain,
305     appName: appjet.appName,
306     statsDiv: statsDiv,
307     encodedAppKey: appjet.encodedAppKey
308   };
309 
310   var sourceLinkTemplate;
311   if (appjet.isPreview) sourceLinkTemplate = _HTML_FOOTER_SOURCE_PREVIEW;
312   else sourceLinkTemplate = _HTML_FOOTER_SOURCE_PUBLISH;
313   data.sourceLink = supplant(data, sourceLinkTemplate);
314   
315   return [supplant(data, _HTML_FOOTER_TEMPLATE)];
316 }
317