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">&#9992;</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