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